class OWFeatureConstructor(OWWidget): name = "Feature Constructor" description = "Construct new features (data columns) from a set of " \ "existing features in the input data set." icon = "icons/FeatureConstructor.svg" inputs = [("Data", Orange.data.Table, "setData")] outputs = [("Data", Orange.data.Table)] want_main_area = False settingsHandler = FeatureConstructorSettingsHandler() descriptors = ContextSetting([]) currentIndex = ContextSetting(-1) EDITORS = [ (ContinuousDescriptor, ContinuousFeatureEditor), (DiscreteDescriptor, DiscreteFeatureEditor), (StringDescriptor, StringFeatureEditor) ] class Error(OWWidget.Error): more_values_needed = Msg("Discrete feature {} needs more values.") invalid_expressions = Msg("Invalid expressions: {}.") def __init__(self): super().__init__() self.data = None self.editors = {} box = gui.vBox(self.controlArea, "Variable Definitions") toplayout = QHBoxLayout() toplayout.setContentsMargins(0, 0, 0, 0) box.layout().addLayout(toplayout) self.editorstack = QStackedWidget( sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) ) for descclass, editorclass in self.EDITORS: editor = editorclass() editor.featureChanged.connect(self._on_modified) self.editors[descclass] = editor self.editorstack.addWidget(editor) self.editorstack.setEnabled(False) buttonlayout = QVBoxLayout(spacing=10) buttonlayout.setContentsMargins(0, 0, 0, 0) self.addbutton = QPushButton( "New", toolTip="Create a new variable", minimumWidth=120, shortcut=QKeySequence.New ) def unique_name(fmt, reserved): candidates = (fmt.format(i) for i in count(1)) return next(c for c in candidates if c not in reserved) def reserved_names(): varnames = [] if self.data is not None: varnames = [var.name for var in self.data.domain.variables + self.data.domain.metas] varnames += [desc.name for desc in self.featuremodel] return set(varnames) def generate_newname(fmt): return unique_name(fmt, reserved_names()) menu = QMenu(self.addbutton) cont = menu.addAction("Continuous") cont.triggered.connect( lambda: self.addFeature( ContinuousDescriptor(generate_newname("X{}"), "", 3)) ) disc = menu.addAction("Discrete") disc.triggered.connect( lambda: self.addFeature( DiscreteDescriptor(generate_newname("D{}"), "", ("A", "B"), -1, False)) ) string = menu.addAction("String") string.triggered.connect( lambda: self.addFeature( StringDescriptor(generate_newname("S{}"), "")) ) menu.addSeparator() self.duplicateaction = menu.addAction("Duplicate Selected Variable") self.duplicateaction.triggered.connect(self.duplicateFeature) self.duplicateaction.setEnabled(False) self.addbutton.setMenu(menu) self.removebutton = QPushButton( "Remove", toolTip="Remove selected variable", minimumWidth=120, shortcut=QKeySequence.Delete ) self.removebutton.clicked.connect(self.removeSelectedFeature) buttonlayout.addWidget(self.addbutton) buttonlayout.addWidget(self.removebutton) buttonlayout.addStretch(10) toplayout.addLayout(buttonlayout, 0) toplayout.addWidget(self.editorstack, 10) # Layout for the list view layout = QVBoxLayout(spacing=1, margin=0) self.featuremodel = DescriptorModel(parent=self) self.featureview = QListView( minimumWidth=200, sizePolicy=QSizePolicy(QSizePolicy.Minimum, QSizePolicy.MinimumExpanding) ) self.featureview.setItemDelegate(FeatureItemDelegate(self)) self.featureview.setModel(self.featuremodel) self.featureview.selectionModel().selectionChanged.connect( self._on_selectedVariableChanged ) layout.addWidget(self.featureview) box.layout().addLayout(layout, 1) box = gui.hBox(self.controlArea) box.layout().addWidget(self.report_button) self.report_button.setMinimumWidth(180) gui.rubber(box) commit = gui.button(box, self, "Send", callback=self.apply, default=True) commit.setMinimumWidth(180) def setCurrentIndex(self, index): index = min(index, len(self.featuremodel) - 1) self.currentIndex = index if index >= 0: itemmodels.select_row(self.featureview, index) desc = self.featuremodel[min(index, len(self.featuremodel) - 1)] editor = self.editors[type(desc)] self.editorstack.setCurrentWidget(editor) editor.setEditorData(desc, self.data.domain if self.data else None) self.editorstack.setEnabled(index >= 0) self.duplicateaction.setEnabled(index >= 0) self.removebutton.setEnabled(index >= 0) def _on_selectedVariableChanged(self, selected, *_): index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) else: self.setCurrentIndex(-1) def _on_modified(self): if self.currentIndex >= 0: editor = self.editorstack.currentWidget() self.featuremodel[self.currentIndex] = editor.editorData() self.descriptors = list(self.featuremodel) def setDescriptors(self, descriptors): """ Set a list of variable descriptors to edit. """ self.descriptors = descriptors self.featuremodel[:] = list(self.descriptors) @check_sql_input def setData(self, data=None): """Set the input dataset.""" self.closeContext() self.data = data if self.data is not None: descriptors = list(self.descriptors) currindex = self.currentIndex self.descriptors = [] self.currentIndex = -1 self.openContext(data) if descriptors != self.descriptors or \ self.currentIndex != currindex: # disconnect from the selection model while reseting the model selmodel = self.featureview.selectionModel() selmodel.selectionChanged.disconnect( self._on_selectedVariableChanged) self.featuremodel[:] = list(self.descriptors) self.setCurrentIndex(self.currentIndex) selmodel.selectionChanged.connect( self._on_selectedVariableChanged) self.editorstack.setEnabled(self.currentIndex >= 0) def handleNewSignals(self): if self.data is not None: self.apply() else: self.send("Data", None) def addFeature(self, descriptor): self.featuremodel.append(descriptor) self.setCurrentIndex(len(self.featuremodel) - 1) editor = self.editorstack.currentWidget() editor.nameedit.setFocus() editor.nameedit.selectAll() def removeFeature(self, index): del self.featuremodel[index] index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) elif index is None and len(self.featuremodel) > 0: # Deleting the last item clears selection self.setCurrentIndex(len(self.featuremodel) - 1) def removeSelectedFeature(self): if self.currentIndex >= 0: self.removeFeature(self.currentIndex) def duplicateFeature(self): desc = self.featuremodel[self.currentIndex] self.addFeature(copy.deepcopy(desc)) def check_attrs_values(self, attr, data): for i in range(len(data)): for var in attr: if not math.isnan(data[i, var]) \ and int(data[i, var]) >= len(var.values): return var.name return None def _validate_descriptors(self, desc): def validate(source): try: return validate_exp(ast.parse(source, mode="eval")) except Exception: return False final = [] invalid = [] for d in desc: if validate(d.expression): final.append(d) else: final.append(d._replace(expression="")) invalid.append(d) if invalid: self.Error.invalid_expressions(", ".join(s.name for s in invalid)) return final def apply(self): self.Error.clear() if self.data is None: return desc = list(self.featuremodel) desc = self._validate_descriptors(desc) source_vars = tuple(self.data.domain) + self.data.domain.metas new_variables = construct_variables(desc, source_vars) attrs = [var for var in new_variables if var.is_primitive()] metas = [var for var in new_variables if not var.is_primitive()] new_domain = Orange.data.Domain( self.data.domain.attributes + tuple(attrs), self.data.domain.class_vars, metas=self.data.domain.metas + tuple(metas) ) try: data = Orange.data.Table(new_domain, self.data) except Exception as err: log = logging.getLogger(__name__) log.error("", exc_info=True) self.error("".join(format_exception_only(type(err), err)).rstrip()) return disc_attrs_not_ok = self.check_attrs_values( [var for var in attrs if var.is_discrete], data) if disc_attrs_not_ok: self.Error.more_values_needed(disc_attrs_not_ok) return self.send("Data", data) def send_report(self): items = OrderedDict() for feature in self.featuremodel: if isinstance(feature, DiscreteDescriptor): items[feature.name] = "{} (discrete with values {}{})".format( feature.expression, feature.values, "; ordered" * feature.ordered) elif isinstance(feature, ContinuousDescriptor): items[feature.name] = "{} (numeric)".format(feature.expression) else: items[feature.name] = "{} (text)".format(feature.expression) self.report_items( report.plural("Constructed feature{s}", len(items)), items)
class OWFeatureConstructor(OWWidget): name = "Feature Constructor" description = "Construct new features (data columns) from a set of " \ "existing features in the input data set." icon = "icons/FeatureConstructor.svg" class Inputs: data = Input("Data", Orange.data.Table) class Outputs: data = Output("Data", Orange.data.Table) want_main_area = False settingsHandler = FeatureConstructorHandler() descriptors = ContextSetting([]) currentIndex = ContextSetting(-1) EDITORS = [ (ContinuousDescriptor, ContinuousFeatureEditor), (DiscreteDescriptor, DiscreteFeatureEditor), (StringDescriptor, StringFeatureEditor) ] class Error(OWWidget.Error): more_values_needed = Msg("Categorical feature {} needs more values.") invalid_expressions = Msg("Invalid expressions: {}.") def __init__(self): super().__init__() self.data = None self.editors = {} box = gui.vBox(self.controlArea, "Variable Definitions") toplayout = QHBoxLayout() toplayout.setContentsMargins(0, 0, 0, 0) box.layout().addLayout(toplayout) self.editorstack = QStackedWidget( sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding) ) for descclass, editorclass in self.EDITORS: editor = editorclass() editor.featureChanged.connect(self._on_modified) self.editors[descclass] = editor self.editorstack.addWidget(editor) self.editorstack.setEnabled(False) buttonlayout = QVBoxLayout(spacing=10) buttonlayout.setContentsMargins(0, 0, 0, 0) self.addbutton = QPushButton( "New", toolTip="Create a new variable", minimumWidth=120, shortcut=QKeySequence.New ) def unique_name(fmt, reserved): candidates = (fmt.format(i) for i in count(1)) return next(c for c in candidates if c not in reserved) def reserved_names(): varnames = [] if self.data is not None: varnames = [var.name for var in self.data.domain.variables + self.data.domain.metas] varnames += [desc.name for desc in self.featuremodel] return set(varnames) def generate_newname(fmt): return unique_name(fmt, reserved_names()) menu = QMenu(self.addbutton) cont = menu.addAction("Numeric") cont.triggered.connect( lambda: self.addFeature( ContinuousDescriptor(generate_newname("X{}"), "", 3)) ) disc = menu.addAction("Categorical") disc.triggered.connect( lambda: self.addFeature( DiscreteDescriptor(generate_newname("D{}"), "", ("A", "B"), -1, False)) ) string = menu.addAction("Text") string.triggered.connect( lambda: self.addFeature( StringDescriptor(generate_newname("S{}"), "")) ) menu.addSeparator() self.duplicateaction = menu.addAction("Duplicate Selected Variable") self.duplicateaction.triggered.connect(self.duplicateFeature) self.duplicateaction.setEnabled(False) self.addbutton.setMenu(menu) self.removebutton = QPushButton( "Remove", toolTip="Remove selected variable", minimumWidth=120, shortcut=QKeySequence.Delete ) self.removebutton.clicked.connect(self.removeSelectedFeature) buttonlayout.addWidget(self.addbutton) buttonlayout.addWidget(self.removebutton) buttonlayout.addStretch(10) toplayout.addLayout(buttonlayout, 0) toplayout.addWidget(self.editorstack, 10) # Layout for the list view layout = QVBoxLayout(spacing=1, margin=0) self.featuremodel = DescriptorModel(parent=self) self.featureview = QListView( minimumWidth=200, sizePolicy=QSizePolicy(QSizePolicy.Minimum, QSizePolicy.MinimumExpanding) ) self.featureview.setItemDelegate(FeatureItemDelegate(self)) self.featureview.setModel(self.featuremodel) self.featureview.selectionModel().selectionChanged.connect( self._on_selectedVariableChanged ) layout.addWidget(self.featureview) box.layout().addLayout(layout, 1) box = gui.hBox(self.controlArea) gui.rubber(box) commit = gui.button(box, self, "Send", callback=self.apply, default=True) commit.setMinimumWidth(180) def setCurrentIndex(self, index): index = min(index, len(self.featuremodel) - 1) self.currentIndex = index if index >= 0: itemmodels.select_row(self.featureview, index) desc = self.featuremodel[min(index, len(self.featuremodel) - 1)] editor = self.editors[type(desc)] self.editorstack.setCurrentWidget(editor) editor.setEditorData(desc, self.data.domain if self.data else None) self.editorstack.setEnabled(index >= 0) self.duplicateaction.setEnabled(index >= 0) self.removebutton.setEnabled(index >= 0) def _on_selectedVariableChanged(self, selected, *_): index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) else: self.setCurrentIndex(-1) def _on_modified(self): if self.currentIndex >= 0: editor = self.editorstack.currentWidget() self.featuremodel[self.currentIndex] = editor.editorData() self.descriptors = list(self.featuremodel) def setDescriptors(self, descriptors): """ Set a list of variable descriptors to edit. """ self.descriptors = descriptors self.featuremodel[:] = list(self.descriptors) @Inputs.data @check_sql_input def setData(self, data=None): """Set the input dataset.""" self.closeContext() self.data = data if self.data is not None: descriptors = list(self.descriptors) currindex = self.currentIndex self.descriptors = [] self.currentIndex = -1 self.openContext(data) if descriptors != self.descriptors or \ self.currentIndex != currindex: # disconnect from the selection model while reseting the model selmodel = self.featureview.selectionModel() selmodel.selectionChanged.disconnect( self._on_selectedVariableChanged) self.featuremodel[:] = list(self.descriptors) self.setCurrentIndex(self.currentIndex) selmodel.selectionChanged.connect( self._on_selectedVariableChanged) self.editorstack.setEnabled(self.currentIndex >= 0) def handleNewSignals(self): if self.data is not None: self.apply() else: self.Outputs.data.send(None) def addFeature(self, descriptor): self.featuremodel.append(descriptor) self.setCurrentIndex(len(self.featuremodel) - 1) editor = self.editorstack.currentWidget() editor.nameedit.setFocus() editor.nameedit.selectAll() def removeFeature(self, index): del self.featuremodel[index] index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) elif index is None and len(self.featuremodel) > 0: # Deleting the last item clears selection self.setCurrentIndex(len(self.featuremodel) - 1) def removeSelectedFeature(self): if self.currentIndex >= 0: self.removeFeature(self.currentIndex) def duplicateFeature(self): desc = self.featuremodel[self.currentIndex] self.addFeature(copy.deepcopy(desc)) def check_attrs_values(self, attr, data): for i in range(len(data)): for var in attr: if not math.isnan(data[i, var]) \ and int(data[i, var]) >= len(var.values): return var.name return None def _validate_descriptors(self, desc): def validate(source): try: return validate_exp(ast.parse(source, mode="eval")) except Exception: return False final = [] invalid = [] for d in desc: if validate(d.expression): final.append(d) else: final.append(d._replace(expression="")) invalid.append(d) if invalid: self.Error.invalid_expressions(", ".join(s.name for s in invalid)) return final def apply(self): self.Error.clear() if self.data is None: return desc = list(self.featuremodel) desc = self._validate_descriptors(desc) source_vars = self.data.domain.variables + self.data.domain.metas new_variables = construct_variables(desc, source_vars) attrs = [var for var in new_variables if var.is_primitive()] metas = [var for var in new_variables if not var.is_primitive()] new_domain = Orange.data.Domain( self.data.domain.attributes + tuple(attrs), self.data.domain.class_vars, metas=self.data.domain.metas + tuple(metas) ) try: data = self.data.transform(new_domain) except Exception as err: log = logging.getLogger(__name__) log.error("", exc_info=True) self.error("".join(format_exception_only(type(err), err)).rstrip()) return disc_attrs_not_ok = self.check_attrs_values( [var for var in attrs if var.is_discrete], data) if disc_attrs_not_ok: self.Error.more_values_needed(disc_attrs_not_ok) return self.Outputs.data.send(data) def send_report(self): items = OrderedDict() for feature in self.featuremodel: if isinstance(feature, DiscreteDescriptor): items[feature.name] = "{} (categorical with values {}{})".format( feature.expression, feature.values, "; ordered" * feature.ordered) elif isinstance(feature, ContinuousDescriptor): items[feature.name] = "{} (numeric)".format(feature.expression) else: items[feature.name] = "{} (text)".format(feature.expression) self.report_items( report.plural("Constructed feature{s}", len(items)), items)
class ImagePlot(QWidget, OWComponent): attr_x = ContextSetting(None) attr_y = ContextSetting(None) gamma = Setting(0) threshold_low = Setting(0.0) threshold_high = Setting(1.0) palette_index = Setting(0) def __init__(self, parent, select_fn=None): QWidget.__init__(self) OWComponent.__init__(self, parent) self.parent = parent self.select_fn = select_fn self.selection_type = SELECTMANY self.saving_enabled = hasattr(self.parent, "save_graph") self.selection_enabled = True self.viewtype = INDIVIDUAL # required bt InteractiveViewBox self.highlighted = None self.data_points = None self.data_values = None self.data_imagepixels = None self.selection = None self.plotview = pg.PlotWidget(background="w", viewBox=InteractiveViewBox(self)) self.plot = self.plotview.getPlotItem() self.plot.scene().installEventFilter( HelpEventDelegate(self.help_event, self)) layout = QVBoxLayout() self.setLayout(layout) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self.plotview) self.img = ImageItemNan() self.img.setOpts(axisOrder='row-major') self.plot.addItem(self.img) self.plot.vb.setAspectLocked() self.plot.scene().sigMouseMoved.connect(self.plot.vb.mouseMovedEvent) layout = QGridLayout() self.plotview.setLayout(layout) self.button = QPushButton("View", self.plotview) self.button.setAutoDefault(False) layout.setRowStretch(1, 1) layout.setColumnStretch(1, 1) layout.addWidget(self.button, 0, 0) view_menu = MenuFocus(self) self.button.setMenu(view_menu) actions = [] zoom_in = QAction( "Zoom in", self, triggered=self.plot.vb.set_mode_zooming ) zoom_in.setShortcuts([Qt.Key_Z, QKeySequence(QKeySequence.ZoomIn)]) zoom_in.setShortcutContext(Qt.WidgetWithChildrenShortcut) actions.append(zoom_in) zoom_fit = QAction( "Zoom to fit", self, triggered=lambda x: (self.plot.vb.autoRange(), self.plot.vb.set_mode_panning()) ) zoom_fit.setShortcuts([Qt.Key_Backspace, QKeySequence(Qt.ControlModifier | Qt.Key_0)]) zoom_fit.setShortcutContext(Qt.WidgetWithChildrenShortcut) actions.append(zoom_fit) select_square = QAction( "Select (square)", self, triggered=self.plot.vb.set_mode_select_square, ) select_square.setShortcuts([Qt.Key_S]) select_square.setShortcutContext(Qt.WidgetWithChildrenShortcut) actions.append(select_square) select_polygon = QAction( "Select (polygon)", self, triggered=self.plot.vb.set_mode_select_polygon, ) select_polygon.setShortcuts([Qt.Key_P]) select_polygon.setShortcutContext(Qt.WidgetWithChildrenShortcut) actions.append(select_polygon) if self.saving_enabled: save_graph = QAction( "Save graph", self, triggered=self.save_graph, ) save_graph.setShortcuts([QKeySequence(Qt.ControlModifier | Qt.Key_I)]) actions.append(save_graph) view_menu.addActions(actions) self.addActions(actions) common_options = dict( labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str) choose_xy = QWidgetAction(self) box = gui.vBox(self) box.setFocusPolicy(Qt.TabFocus) self.xy_model = DomainModel(DomainModel.METAS | DomainModel.CLASSES, valid_types=DomainModel.PRIMITIVE) self.cb_attr_x = gui.comboBox( box, self, "attr_x", label="Axis x:", callback=self.update_attr, model=self.xy_model, **common_options) self.cb_attr_y = gui.comboBox( box, self, "attr_y", label="Axis y:", callback=self.update_attr, model=self.xy_model, **common_options) box.setFocusProxy(self.cb_attr_x) self.color_cb = gui.comboBox(box, self, "palette_index", label="Color:", labelWidth=50, orientation=Qt.Horizontal) self.color_cb.setIconSize(QSize(64, 16)) palettes = _color_palettes self.palette_index = min(self.palette_index, len(palettes) - 1) model = color_palette_model(palettes, self.color_cb.iconSize()) model.setParent(self) self.color_cb.setModel(model) self.color_cb.activated.connect(self.update_color_schema) self.color_cb.setCurrentIndex(self.palette_index) form = QFormLayout( formAlignment=Qt.AlignLeft, labelAlignment=Qt.AlignLeft, fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow ) lowslider = gui.hSlider( box, self, "threshold_low", minValue=0.0, maxValue=1.0, step=0.05, ticks=True, intOnly=False, createLabel=False, callback=self.update_color_schema) highslider = gui.hSlider( box, self, "threshold_high", minValue=0.0, maxValue=1.0, step=0.05, ticks=True, intOnly=False, createLabel=False, callback=self.update_color_schema) form.addRow("Low:", lowslider) form.addRow("High:", highslider) box.layout().addLayout(form) choose_xy.setDefaultWidget(box) view_menu.addAction(choose_xy) self.markings_integral = [] self.lsx = None # info about the X axis self.lsy = None # info about the Y axis self.data = None self.data_ids = {} def help_event(self, ev): pos = self.plot.vb.mapSceneToView(ev.scenePos()) sel = self._points_at_pos(pos) prepared = [] if sel is not None: data, vals, points = self.data[sel], self.data_values[sel], self.data_points[sel] for d, v, p in zip(data, vals, points): basic = "({}, {}): {}".format(p[0], p[1], v) variables = [ v for v in self.data.domain.metas + self.data.domain.class_vars if v not in [self.attr_x, self.attr_y]] features = ['{} = {}'.format(attr.name, d[attr]) for attr in variables] prepared.append("\n".join([basic] + features)) text = "\n\n".join(prepared) if text: text = ('<span style="white-space:pre">{}</span>' .format(escape(text))) QToolTip.showText(ev.screenPos(), text, widget=self.plotview) return True else: return False def update_color_schema(self): if not self.threshold_low < self.threshold_high: # TODO this belongs here, not in the parent self.parent.Warning.threshold_error() return else: self.parent.Warning.threshold_error.clear() data = self.color_cb.itemData(self.palette_index, role=Qt.UserRole) _, colors = max(data.items()) cols = color_palette_table( colors, threshold_low=self.threshold_low, threshold_high=self.threshold_high) self.img.setLookupTable(cols) # use defined discrete palette if self.parent.value_type == 1: dat = self.data.domain[self.parent.attr_value] if isinstance(dat, DiscreteVariable): self.img.setLookupTable(dat.colors) def update_attr(self): self.update_view() def init_attr_values(self): domain = self.data.domain if self.data is not None else None self.xy_model.set_domain(domain) self.attr_x = self.xy_model[0] if self.xy_model else None self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \ else self.attr_x def save_graph(self): self.parent.save_graph() def set_data(self, data): self.img.clear() if data is not None: same_domain = (self.data and data.domain.checksum() == self.data.domain.checksum()) self.data = data self.data_ids = {e: i for i, e in enumerate(data.ids)} if not same_domain: self.init_attr_values() else: self.data = None self.data_ids = {} def set_integral_limits(self): self.update_view() def refresh_markings(self, di): refresh_integral_markings(di, self.markings_integral, self.parent.curveplot) def update_view(self): self.img.clear() self.img.setSelection(None) self.lsx = None self.lsy = None self.data_points = None self.data_values = None self.data_imagepixels = None if self.data and self.attr_x and self.attr_y: xat = self.data.domain[self.attr_x] yat = self.data.domain[self.attr_y] ndom = Orange.data.Domain([xat, yat]) datam = Orange.data.Table(ndom, self.data) coorx = datam.X[:, 0] coory = datam.X[:, 1] self.data_points = datam.X self.lsx = lsx = values_to_linspace(coorx) self.lsy = lsy = values_to_linspace(coory) if lsx[-1] * lsy[-1] > IMAGE_TOO_BIG: self.parent.Error.image_too_big(lsx[-1], lsy[-1]) return else: self.parent.Error.image_too_big.clear() di = {} if self.parent.value_type == 0: # integrals imethod = self.parent.integration_methods[self.parent.integration_method] l1, l2, l3 = self.parent.lowlim, self.parent.highlim, self.parent.choose gx = getx(self.data) if l1 is None: l1 = min(gx) - 1 if l2 is None: l2 = max(gx) + 1 l1, l2 = min(l1, l2), max(l1, l2) if l3 is None: l3 = (l1 + l2)/2 if imethod != Integrate.PeakAt: datai = Integrate(methods=imethod, limits=[[l1, l2]])(self.data) else: datai = Integrate(methods=imethod, limits=[[l3, l3]])(self.data) if self.parent.curveplot.selected_indices: # curveplot can have a subset of curves on the input> match IDs ind = list(self.parent.curveplot.selected_indices)[0] dind = self.data_ids[self.parent.curveplot.data[ind].id] di = datai.domain.attributes[0].compute_value.draw_info(self.data[dind:dind+1]) d = datai.X[:, 0] else: dat = self.data.domain[self.parent.attr_value] ndom = Orange.data.Domain([dat]) d = Orange.data.Table(ndom, self.data).X[:, 0] self.refresh_markings(di) # set data imdata = np.ones((lsy[2], lsx[2])) * float("nan") # if previous or saved selection is valid for this data set keep it if self.selection is None or len(self.selection) != len(self.data): self.selection = np.zeros(len(self.data), dtype="bool") xindex = index_values(coorx, lsx) yindex = index_values(coory, lsy) imdata[yindex, xindex] = d self.data_values = d self.data_imagepixels = np.vstack((yindex, xindex)).T levels = get_levels(imdata) self.update_color_schema() self.img.setImage(imdata, levels=levels) # shift centres of the pixels so that the axes are useful shiftx = _shift(lsx) shifty = _shift(lsy) left = lsx[0] - shiftx bottom = lsy[0] - shifty width = (lsx[1]-lsx[0]) + 2*shiftx height = (lsy[1]-lsy[0]) + 2*shifty self.img.setRect(QRectF(left, bottom, width, height)) self.send_selection() self.refresh_img_selection() def refresh_img_selection(self): selected_px = np.zeros((self.lsy[2], self.lsx[2]), dtype=bool) selected_px_ind = self.data_imagepixels[self.selection] selected_px[selected_px_ind[:, 0], selected_px_ind[:, 1]] = 1 self.img.setSelection(selected_px) def make_selection(self, selected, add): """Add selected indices to the selection.""" if self.data and self.lsx and self.lsy: if selected is None and not add: self.selection *= False # set all to False elif selected is not None: if add: self.selection = np.logical_or(self.selection, selected) else: self.selection = selected self.refresh_img_selection() self.send_selection() def send_selection(self): if self.data and self.selection is not None: selected = np.where(self.selection)[0] else: selected = [] if self.select_fn: self.select_fn(selected) def select_square(self, p1, p2, add): """ Select elements within a square drawn by the user. A selection needs to contain whole pixels """ x1, y1 = p1.x(), p1.y() x2, y2 = p2.x(), p2.y() polygon = [QPointF(x1, y1), QPointF(x2, y1), QPointF(x2, y2), QPointF(x1, y2), QPointF(x1, y1)] self.select_polygon(polygon, add) def select_polygon(self, polygon, add): """ Select by a polygon which has to contain whole pixels. """ if self.data and self.lsx and self.lsy: polygon = [(p.x(), p.y()) for p in polygon] # a polygon should contain all pixel shiftx = _shift(self.lsx) shifty = _shift(self.lsy) points_edges = [self.data_points + [[shiftx, shifty]], self.data_points + [[-shiftx, shifty]], self.data_points + [[shiftx, -shifty]], self.data_points + [[-shiftx, -shifty]]] inp = in_polygon(points_edges[0], polygon) for p in points_edges[1:]: inp *= in_polygon(p, polygon) self.make_selection(inp, add) def _points_at_pos(self, pos): if self.data and self.lsx and self.lsy: x, y = pos.x(), pos.y() distance = np.abs(self.data_points - [[x, y]]) sel = (distance[:, 0] < _shift(self.lsx)) * (distance[:, 1] < _shift(self.lsy)) return sel def select_by_click(self, pos, add): sel = self._points_at_pos(pos) self.make_selection(sel, add)
class CurvePlot(QWidget, OWComponent): label_title = Setting("") label_xaxis = Setting("") label_yaxis = Setting("") range_x1 = Setting(None) range_x2 = Setting(None) range_y1 = Setting(None) range_y2 = Setting(None) color_attr = ContextSetting(0) invertX = Setting(False) selected_indices = Setting(set()) data_size = Setting(None) # to invalidate selected_indices def __init__(self, parent=None, select=SELECTNONE): QWidget.__init__(self) OWComponent.__init__(self, parent) self.parent = parent self.selection_type = select self.saving_enabled = hasattr(self.parent, "save_graph") self.clear_data(init=True) self.plotview = pg.PlotWidget(background="w", viewBox=InteractiveViewBox(self)) self.plot = self.plotview.getPlotItem() self.plot.setDownsampling(auto=True, mode="peak") self.markings = [] self.vLine = pg.InfiniteLine(angle=90, movable=False) self.hLine = pg.InfiniteLine(angle=0, movable=False) self.proxy = pg.SignalProxy(self.plot.scene().sigMouseMoved, rateLimit=20, slot=self.mouseMoved, delay=0.1) self.plot.scene().sigMouseMoved.connect(self.plot.vb.mouseMovedEvent) self.plot.vb.sigRangeChanged.connect(self.resized) self.pen_mouse = pg.mkPen(color=(0, 0, 255), width=2) self.pen_normal = defaultdict(lambda: pg.mkPen(color=(200, 200, 200, 127), width=1)) self.pen_subset = defaultdict(lambda: pg.mkPen(color=(0, 0, 0, 127), width=1)) self.pen_selected = defaultdict(lambda: pg.mkPen(color=(0, 0, 0, 127), width=2, style=Qt.DotLine)) self.label = pg.TextItem("", anchor=(1, 0)) self.label.setText("", color=(0, 0, 0)) self.discrete_palette = None QPixmapCache.setCacheLimit(max(QPixmapCache.cacheLimit(), 100 * 1024)) self.curves_cont = PlotCurvesItem() self.important_decimals = 4, 4 self.MOUSE_RADIUS = 20 self.clear_graph() #interface settings self.location = True #show current position self.markclosest = True #mark self.crosshair = True self.crosshair_hidden = True self.viewtype = INDIVIDUAL layout = QVBoxLayout() self.setLayout(layout) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self.plotview) actions = [] zoom_in = QAction( "Zoom in", self, triggered=self.plot.vb.set_mode_zooming ) zoom_in.setShortcuts([Qt.Key_Z, QKeySequence(QKeySequence.ZoomIn)]) actions.append(zoom_in) zoom_fit = QAction( "Zoom to fit", self, triggered=lambda x: (self.plot.vb.autoRange(), self.plot.vb.set_mode_panning()) ) zoom_fit.setShortcuts([Qt.Key_Backspace, QKeySequence(Qt.ControlModifier | Qt.Key_0)]) actions.append(zoom_fit) rescale_y = QAction( "Rescale Y to fit", self, shortcut=Qt.Key_D, triggered=self.rescale_current_view_y ) actions.append(rescale_y) view_individual = QAction( "Show individual", self, shortcut=Qt.Key_I, triggered=lambda x: self.show_individual() ) actions.append(view_individual) view_average = QAction( "Show averages", self, shortcut=Qt.Key_A, triggered=lambda x: self.show_average() ) actions.append(view_average) self.show_grid = False self.show_grid_a = QAction( "Show grid", self, shortcut=Qt.Key_G, checkable=True, triggered=self.grid_changed ) actions.append(self.show_grid_a) self.invertX_menu = QAction( "Invert X", self, shortcut=Qt.Key_X, checkable=True, triggered=self.invertX_changed ) actions.append(self.invertX_menu) if self.selection_type == SELECTMANY: select_curves = QAction( "Select (line)", self, triggered=self.plot.vb.set_mode_select, ) select_curves.setShortcuts([Qt.Key_S]) actions.append(select_curves) if self.saving_enabled: save_graph = QAction( "Save graph", self, triggered=self.save_graph, ) save_graph.setShortcuts([QKeySequence(Qt.ControlModifier | Qt.Key_S)]) actions.append(save_graph) range_menu = MenuFocus("Define view range", self) range_action = QWidgetAction(self) layout = QGridLayout() range_box = gui.widgetBox(self, margin=5, orientation=layout) range_box.setFocusPolicy(Qt.TabFocus) self.range_e_x1 = lineEditFloatOrNone(None, self, "range_x1", label="e") range_box.setFocusProxy(self.range_e_x1) self.range_e_x2 = lineEditFloatOrNone(None, self, "range_x2", label="e") layout.addWidget(QLabel("X"), 0, 0, Qt.AlignRight) layout.addWidget(self.range_e_x1, 0, 1) layout.addWidget(QLabel("-"), 0, 2) layout.addWidget(self.range_e_x2, 0, 3) self.range_e_y1 = lineEditFloatOrNone(None, self, "range_y1", label="e") self.range_e_y2 = lineEditFloatOrNone(None, self, "range_y2", label="e") layout.addWidget(QLabel("Y"), 1, 0, Qt.AlignRight) layout.addWidget(self.range_e_y1, 1, 1) layout.addWidget(QLabel("-"), 1, 2) layout.addWidget(self.range_e_y2, 1, 3) b = gui.button(None, self, "Apply", callback=self.set_limits) layout.addWidget(b, 2, 3, Qt.AlignRight) range_action.setDefaultWidget(range_box) range_menu.addAction(range_action) layout = QGridLayout() self.plotview.setLayout(layout) self.button = QPushButton("View", self.plotview) self.button.setAutoDefault(False) layout.setRowStretch(1, 1) layout.setColumnStretch(1, 1) layout.addWidget(self.button, 0, 0) view_menu = MenuFocus(self) self.button.setMenu(view_menu) view_menu.addActions(actions) view_menu.addMenu(range_menu) self.addActions(actions) choose_color_action = QWidgetAction(self) choose_color_box = gui.hBox(self) choose_color_box.setFocusPolicy(Qt.TabFocus) model = VariableListModel() self.attrs = [] model.wrap(self.attrs) label = gui.label(choose_color_box, self, "Color by") label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) self.attrCombo = gui.comboBox( choose_color_box, self, value="color_attr", contentsLength=12, callback=self.change_color_attr) self.attrCombo.setModel(model) choose_color_box.setFocusProxy(self.attrCombo) choose_color_action.setDefaultWidget(choose_color_box) view_menu.addAction(choose_color_action) labels_action = QWidgetAction(self) layout = QGridLayout() labels_box = gui.widgetBox(self, margin=0, orientation=layout) t = gui.lineEdit(None, self, "label_title", label="Title:", callback=self.labels_changed, callbackOnType=self.labels_changed) layout.addWidget(QLabel("Title:"), 0, 0, Qt.AlignRight) layout.addWidget(t, 0, 1) t = gui.lineEdit(None, self, "label_xaxis", label="X-axis:", callback=self.labels_changed, callbackOnType=self.labels_changed) layout.addWidget(QLabel("X-axis:"), 1, 0, Qt.AlignRight) layout.addWidget(t, 1, 1) t = gui.lineEdit(None, self, "label_yaxis", label="Y-axis:", callback=self.labels_changed, callbackOnType=self.labels_changed) layout.addWidget(QLabel("Y-axis:"), 2, 0, Qt.AlignRight) layout.addWidget(t, 2, 1) labels_action.setDefaultWidget(labels_box) view_menu.addAction(labels_action) self.labels_changed() # apply saved labels self.invertX_apply() self.plot.vb.set_mode_panning() self.reports = {} # current reports self.viewhelpers_show() def report(self, reporter, contents): self.reports[id(reporter)] = contents def report_finished(self, reporter): try: self.reports.pop(id(reporter)) except KeyError: pass # ok if it was already removed if not self.reports: pass def set_limits(self): vr = self.plot.vb.viewRect() x1 = self.range_x1 if self.range_x1 is not None else vr.left() x2 = self.range_x2 if self.range_x2 is not None else vr.right() y1 = self.range_y1 if self.range_y1 is not None else vr.top() y2 = self.range_y2 if self.range_y2 is not None else vr.bottom() self.plot.vb.setXRange(x1, x2) self.plot.vb.setYRange(y1, y2) def labels_changed(self): self.plot.setTitle(self.label_title) if not self.label_title: self.plot.setTitle(None) self.plot.setLabels(bottom=self.label_xaxis) self.plot.showLabel("bottom", bool(self.label_xaxis)) self.plot.getAxis("bottom").resizeEvent() # align text self.plot.setLabels(left=self.label_yaxis) self.plot.showLabel("left", bool(self.label_yaxis)) self.plot.getAxis("left").resizeEvent() # align text def grid_changed(self): self.show_grid = not self.show_grid self.grid_apply() def grid_apply(self): self.plot.showGrid(self.show_grid, self.show_grid, alpha=0.3) self.show_grid_a.setChecked(self.show_grid) def invertX_changed(self): self.invertX = not self.invertX self.invertX_apply() def invertX_apply(self): self.plot.vb.invertX(self.invertX) self.resized() # force redraw of axes (to avoid a pyqtgraph bug) vr = self.plot.vb.viewRect() self.plot.vb.setRange(xRange=(0,1), yRange=(0,1)) self.plot.vb.setRange(rect=vr) self.invertX_menu.setChecked(self.invertX) def save_graph(self): self.viewhelpers_hide() self.plot.showAxis("top", True) self.plot.showAxis("right", True) self.parent.save_graph() self.plot.showAxis("top", False) self.plot.showAxis("right", False) self.viewhelpers_show() def clear_data(self, init=True): self.subset_ids = set() self.data = None self.data_x = None self.data_ys = None self.sampled_indices = [] self.sampled_indices_inverse = {} self.sampling = None if not init: self.selection_changed() self.discrete_palette = None def clear_graph(self): # reset caching. if not, it is not cleared when view changing when zoomed self.highlighted = None self.curves_cont.setCacheMode(QGraphicsItem.NoCache) self.curves_cont.setCacheMode(QGraphicsItem.DeviceCoordinateCache) self.plot.vb.disableAutoRange() self.curves_cont.clear() self.curves_cont.update() self.plotview.clear() self.curves_plotted = [] # currently plotted elements (for rescale) self.curves = [] # for finding closest curve self.plotview.addItem(self.label, ignoreBounds=True) self.highlighted_curve = pg.PlotCurveItem(pen=self.pen_mouse) self.highlighted_curve.setZValue(10) self.highlighted_curve.hide() self.selection_line = pg.PlotCurveItem() self.selection_line.setPen(pg.mkPen(color=QColor(Qt.black), width=2, style=Qt.DotLine)) self.selection_line.setZValue(1e9) self.selection_line.hide() self.plot.addItem(self.highlighted_curve) self.plot.addItem(self.vLine, ignoreBounds=True) self.plot.addItem(self.hLine, ignoreBounds=True) self.viewhelpers = True self.plot.addItem(self.selection_line, ignoreBounds=True) self.plot.addItem(self.curves_cont) for m in self.markings: self.plot.addItem(m, ignoreBounds=True) def resized(self): vr = self.plot.vb.viewRect() xpixel, ypixel = self.plot.vb.viewPixelSize() def important_decimals(n): return max(-int(math.floor(math.log10(n))) + 1, 0) self.important_decimals = important_decimals(xpixel), important_decimals(ypixel) if self.invertX: self.label.setPos(vr.bottomLeft()) else: self.label.setPos(vr.bottomRight()) xd, yd = self.important_decimals self.range_e_x1.setPlaceholderText(("%0." + str(xd) + "f") % vr.left()) self.range_e_x2.setPlaceholderText(("%0." + str(xd) + "f") % vr.right()) self.range_e_y1.setPlaceholderText(("%0." + str(yd) + "f") % vr.top()) self.range_e_y2.setPlaceholderText(("%0." + str(yd) + "f") % vr.bottom()) def make_selection(self, data_indices, add=False): selected_indices = self.selected_indices oldids = selected_indices.copy() invd = self.sampled_indices_inverse if data_indices is None: if not add: selected_indices.clear() self.set_curve_pens([invd[a] for a in oldids if a in invd]) else: if add: selected_indices.update(data_indices) self.set_curve_pens([invd[a] for a in data_indices if a in invd]) else: selected_indices.clear() selected_indices.update(data_indices) self.set_curve_pens([invd[a] for a in (oldids | selected_indices) if a in invd]) self.selection_changed() def selection_changed(self): if self.selection_type: self.parent.selection_changed() def viewhelpers_hide(self): self.label.hide() self.vLine.hide() self.hLine.hide() def viewhelpers_show(self): self.label.show() if self.crosshair and not self.crosshair_hidden: self.vLine.show() self.hLine.show() else: self.vLine.hide() self.hLine.hide() def mouseMoved(self, evt): pos = evt[0] if self.plot.sceneBoundingRect().contains(pos): mousePoint = self.plot.vb.mapSceneToView(pos) posx, posy = mousePoint.x(), mousePoint.y() labels = [] for a, vs in sorted(self.reports.items()): for v in vs: if isinstance(v, tuple) and len(v) == 2: if v[0] == "x": labels.append(("%0." + str(self.important_decimals[0]) + "f") % v[1]) continue labels.append(str(v)) labels = " ".join(labels) self.crosshair_hidden = bool(labels) if self.location and not labels: fs = "%0." + str(self.important_decimals[0]) + "f %0." + str(self.important_decimals[1]) + "f" labels = fs % (posx, posy) self.label.setText(labels, color=(0, 0, 0)) if self.curves and len(self.curves[0][0]): #need non-zero x axis! cache = {} bd = None if self.markclosest and self.plot.vb.action != ZOOMING: xpixel, ypixel = self.plot.vb.viewPixelSize() distances = distancetocurves(self.curves[0], posx, posy, xpixel, ypixel, r=self.MOUSE_RADIUS, cache=cache) try: bd = np.nanargmin(distances) bd = (bd, distances[bd]) except ValueError: # if all distances are NaN pass if self.highlighted is not None: self.highlighted = None self.highlighted_curve.hide() if bd and bd[1] < self.MOUSE_RADIUS: self.highlighted = bd[0] x = self.curves[0][0] y = self.curves[0][1][self.highlighted] self.highlighted_curve.setData(x=x,y=y) self.highlighted_curve.show() self.vLine.setPos(posx) self.hLine.setPos(posy) self.viewhelpers_show() else: self.viewhelpers_hide() def set_curve_pen(self, idc): idcdata = self.sampled_indices[idc] insubset = not self.subset_ids or self.data[idcdata].id in self.subset_ids inselected = self.selection_type and idcdata in self.selected_indices thispen = self.pen_subset if insubset else self.pen_normal if inselected: thispen = self.pen_selected color_var = self._current_color_var() value = None if isinstance(color_var, str) else str(self.data[idcdata][color_var]) self.curves_cont.objs[idc].setPen(thispen[value]) self.curves_cont.objs[idc].setZValue(int(insubset) + int(inselected)) def set_curve_pens(self, curves=None): if self.viewtype == INDIVIDUAL and self.curves: curves = range(len(self.curves[0][1])) if curves is None else curves for i in curves: self.set_curve_pen(i) self.curves_cont.update() def add_marking(self, item): self.markings.append(item) self.plot.addItem(item, ignoreBounds=True) def remove_marking(self, item): self.plot.removeItem(item) self.markings.remove(item) def clear_markings(self): for m in self.markings: self.plot.removeItem(m) self.markings = [] def add_curves(self, x, ys, addc=True): """ Add multiple curves with the same x domain. """ if len(ys) > MAX_INSTANCES_DRAWN: self.sampled_indices = sorted(random.Random(0).sample(range(len(ys)), MAX_INSTANCES_DRAWN)) self.sampling = True else: self.sampled_indices = list(range(len(ys))) random.Random(0).shuffle(self.sampled_indices) #for sequential classes# self.sampled_indices_inverse = {s: i for i, s in enumerate(self.sampled_indices)} ys = ys[self.sampled_indices] self.curves.append((x, ys)) for y in ys: c = pg.PlotCurveItem(x=x, y=y, pen=self.pen_normal[None]) self.curves_cont.add_curve(c) self.curves_plotted = self.curves def add_curve(self, x, y, pen=None): c = pg.PlotCurveItem(x=x, y=y, pen=pen if pen else self.pen_normal[None]) self.curves_cont.add_curve(c) # for rescale to work correctly self.curves_plotted.append((x, np.array([y]))) def add_fill_curve(self, x, ylow, yhigh, pen): phigh = pg.PlotCurveItem(x, yhigh, pen=pen) plow = pg.PlotCurveItem(x, ylow, pen=pen) color = pen.color() color.setAlphaF(0.2) cc = pg.mkBrush(color) pfill = pg.FillBetweenItem(plow, phigh, brush=cc) pfill.setZValue(10) self.curves_cont.add_curve(pfill) # for zoom to work correctly self.curves_plotted.append((x, np.array([ylow, yhigh]))) def _current_color_var(self): color_var = "(Same color)" try: color_var = self.attrs[self.color_attr] except IndexError: pass return color_var def change_color_attr(self): self.set_pen_colors() self.update_view() def set_pen_colors(self): self.pen_normal.clear() self.pen_subset.clear() self.pen_selected.clear() color_var = self._current_color_var() if color_var != "(Same color)": colors = color_var.colors discrete_palette = ColorPaletteGenerator( number_of_colors=len(colors), rgb_colors=colors) for v in color_var.values: basecolor = discrete_palette[color_var.to_val(v)] basecolor = QColor(basecolor) basecolor.setAlphaF(0.9) self.pen_subset[v] = pg.mkPen(color=basecolor, width=1) self.pen_selected[v] = pg.mkPen(color=basecolor, width=2, style=Qt.DotLine) notselcolor = basecolor.lighter(150) notselcolor.setAlphaF(0.5) self.pen_normal[v] = pg.mkPen(color=notselcolor, width=1) def show_individual(self): if not self.data: return self.viewtype = INDIVIDUAL self.clear_graph() self.add_curves(self.data_x, self.data_ys) self.set_curve_pens() self.curves_cont.update() def rescale_current_view_y(self): if self.curves_plotted: cache = {} qrect = self.plot.vb.targetRect() bleft = qrect.left() bright = qrect.right() ymax = max(np.max(ys[:, searchsorted_cached(cache, x, bleft): searchsorted_cached(cache, x, bright, side="right")]) for x, ys in self.curves_plotted) ymin = min(np.min(ys[:, searchsorted_cached(cache, x, bleft): searchsorted_cached(cache, x, bright, side="right")]) for x, ys in self.curves_plotted) self.plot.vb.setYRange(ymin, ymax, padding=0.0) self.plot.vb.pad_current_view_y() def _split_by_color_value(self, data): color_var = self._current_color_var() rd = defaultdict(list) for i, inst in enumerate(data): value = None if isinstance(color_var, str) else str(inst[color_var]) rd[value].append(i) return rd def show_average(self): if not self.data: return self.viewtype = AVERAGE self.clear_graph() x = self.data_x if self.data: ysall = [] subset_indices = [i for i, id in enumerate(self.data.ids) if id in self.subset_ids] dsplit = self._split_by_color_value(self.data) for colorv, indices in dsplit.items(): for part in ["everything", "subset", "selection"]: if part == "everything": ys = self.data_ys[indices] pen = self.pen_normal if subset_indices else self.pen_subset elif part == "selection" and self.selection_type: current_selected = sorted(set(self.selected_indices) & set(indices)) if not current_selected: continue ys = self.data_ys[current_selected] pen = self.pen_selected elif part == "subset": current_subset = sorted(set(subset_indices) & set(indices)) if not current_subset: continue ys = self.data_ys[current_subset] pen = self.pen_subset std = np.std(ys, axis=0) mean = np.mean(ys, axis=0) ysall.append(mean) penc = QPen(pen[colorv]) penc.setWidth(3) self.add_curve(x, mean, pen=penc) self.add_fill_curve(x, mean + std, mean - std, pen=penc) self.curves.append((x, np.array(ysall))) self.curves_cont.update() def update_view(self): if self.viewtype == INDIVIDUAL: self.show_individual() elif self.viewtype == AVERAGE: self.show_average() def set_data(self, data, rescale="auto"): self.clear_graph() self.clear_data() self.attrs[:] = [] if data is not None: self.attrs[:] = ["(Same color)"] + [ var for var in chain(data.domain, data.domain.metas) if isinstance(var, str) or var.is_discrete] self.color_attr = 0 self.set_pen_colors() if data is not None: if rescale == "auto": if self.data: rescale = not data.domain == self.data.domain else: rescale = True self.data = data # reset selection if dataset sizes do not match if self.selected_indices and \ (max(self.selected_indices) >= len(self.data) or self.data_size != len(self.data)): self.selected_indices.clear() self.data_size = len(self.data) # get and sort input data x = getx(self.data) xsind = np.argsort(x) self.data_x = x[xsind] self.data_ys = data.X[:, xsind] self.update_view() if rescale == True: self.plot.vb.autoRange() def update_display(self): self.curves_cont.update() def set_data_subset(self, ids): self.subset_ids = set(ids) if ids is not None else set() self.set_curve_pens() self.update_view()
class LineScanPlot(QWidget, OWComponent, SelectionGroupMixin, ImageColorSettingMixin, ImageZoomMixin): attr_x = ContextSetting(None) gamma = Setting(0) selection_changed = Signal() def __init__(self, parent): QWidget.__init__(self) OWComponent.__init__(self, parent) SelectionGroupMixin.__init__(self) ImageColorSettingMixin.__init__(self) self.parent = parent self.selection_type = SELECTMANY self.saving_enabled = True self.selection_enabled = True self.viewtype = INDIVIDUAL # required bt InteractiveViewBox self.highlighted = None self.data_points = None self.data_imagepixels = None self.plotview = pg.PlotWidget(background="w", viewBox=InteractiveViewBox(self)) self.plot = self.plotview.getPlotItem() self.plot.scene().installEventFilter( HelpEventDelegate(self.help_event, self)) layout = QVBoxLayout() self.setLayout(layout) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self.plotview) self.img = ImageItemNan() self.img.setOpts(axisOrder='row-major') self.plot.addItem(self.img) self.plot.scene().sigMouseMoved.connect(self.plot.vb.mouseMovedEvent) layout = QGridLayout() self.plotview.setLayout(layout) self.button = QPushButton("Menu", self.plotview) self.button.setAutoDefault(False) layout.setRowStretch(1, 1) layout.setColumnStretch(1, 1) layout.addWidget(self.button, 0, 0) view_menu = MenuFocus(self) self.button.setMenu(view_menu) # prepare interface according to the new context self.parent.contextAboutToBeOpened.connect(lambda x: self.init_interface_data(x[0])) self.add_zoom_actions(view_menu) common_options = dict( labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str) choose_xy = QWidgetAction(self) box = gui.vBox(self) box.setFocusPolicy(Qt.TabFocus) self.xy_model = DomainModel(DomainModel.METAS | DomainModel.CLASSES, valid_types=DomainModel.PRIMITIVE, placeholder="Position (index)") self.cb_attr_x = gui.comboBox( box, self, "attr_x", label="Axis x:", callback=self.update_attr, model=self.xy_model, **common_options) box.setFocusProxy(self.cb_attr_x) box.layout().addWidget(self.color_settings_box()) choose_xy.setDefaultWidget(box) view_menu.addAction(choose_xy) self.lsx = None # info about the X axis self.lsy = None # info about the Y axis self.data = None self.data_ids = {} def init_interface_data(self, data): same_domain = (self.data and data and data.domain == self.data.domain) if not same_domain: self.init_attr_values(data) def help_event(self, ev): pos = self.plot.vb.mapSceneToView(ev.scenePos()) sel, wavenumber_ind = self._points_at_pos(pos) prepared = [] if sel is not None: prepared.append(str(self.wavenumbers[wavenumber_ind])) for d in self.data[sel]: variables = [v for v in self.data.domain.metas + self.data.domain.class_vars if v not in [self.attr_x]] features = ['{} = {}'.format(attr.name, d[attr]) for attr in variables] features.append('value = {}'.format(d[wavenumber_ind])) prepared.append("\n".join(features)) text = "\n\n".join(prepared) if text: text = ('<span style="white-space:pre">{}</span>' .format(escape(text))) QToolTip.showText(ev.screenPos(), text, widget=self.plotview) return True else: return False def update_attr(self): self.update_view() def init_attr_values(self, data): domain = data.domain if data is not None else None self.xy_model.set_domain(domain) self.attr_x = self.xy_model[0] if self.xy_model else None def set_data(self, data): if data: self.data = data self.data_ids = {e: i for i, e in enumerate(data.ids)} self.restore_selection_settings() else: self.data = None self.data_ids = {} def update_view(self): self.img.clear() self.img.setSelection(None) self.lsx = None self.lsy = None self.wavenumbers = None self.data_xs = None self.data_imagepixels = None if self.data and len(self.data.domain.attributes): if self.attr_x is not None: xat = self.data.domain[self.attr_x] ndom = Domain([xat]) datam = Table(ndom, self.data) coorx = datam.X[:, 0] else: coorx = np.arange(len(self.data)) self.lsx = lsx = values_to_linspace(coorx) self.data_xs = coorx self.wavenumbers = wavenumbers = getx(self.data) self.lsy = lsy = values_to_linspace(wavenumbers) # set data imdata = np.ones((lsy[2], lsx[2])) * float("nan") xindex = index_values(coorx, lsx) yindex = index_values(wavenumbers, lsy) for xind, d in zip(xindex, self.data.X): imdata[yindex, xind] = d self.data_imagepixels = xindex self.img.setImage(imdata, autoLevels=False) self.img.setLevels([0, 1]) self.update_levels() self.update_color_schema() # shift centres of the pixels so that the axes are useful shiftx = _shift(lsx) shifty = _shift(lsy) left = lsx[0] - shiftx bottom = lsy[0] - shifty width = (lsx[1]-lsx[0]) + 2*shiftx height = (lsy[1]-lsy[0]) + 2*shifty self.img.setRect(QRectF(left, bottom, width, height)) self.refresh_img_selection() def refresh_img_selection(self): selected_px = np.zeros((self.lsy[2], self.lsx[2]), dtype=np.uint8) selected_px[:, self.data_imagepixels] = self.selection_group self.img.setSelection(selected_px) def make_selection(self, selected, add): """Add selected indices to the selection.""" add_to_group, add_group, remove = selection_modifiers() if self.data and self.lsx and self.lsy: if add_to_group: # both keys - need to test it before add_group selnum = np.max(self.selection_group) elif add_group: selnum = np.max(self.selection_group) + 1 elif remove: selnum = 0 else: self.selection_group *= 0 selnum = 1 if selected is not None: self.selection_group[selected] = selnum self.refresh_img_selection() self.prepare_settings_for_saving() self.selection_changed.emit() def _points_at_pos(self, pos): if self.data and self.lsx and self.lsy: x, y = pos.x(), pos.y() x_distance = np.abs(self.data_xs - x) sel = (x_distance < _shift(self.lsx)) wavenumber_distance = np.abs(self.wavenumbers - y) wavenumber_ind = np.argmin(wavenumber_distance) return sel, wavenumber_ind return None, None def select_by_click(self, pos, add): sel, _ = self._points_at_pos(pos) self.make_selection(sel, add)
class LineScanPlot(QWidget, OWComponent, SelectionGroupMixin, ImageColorSettingMixin, ImageZoomMixin): attr_x = ContextSetting(None) gamma = Setting(0) selection_changed = Signal() def __init__(self, parent): QWidget.__init__(self) OWComponent.__init__(self, parent) SelectionGroupMixin.__init__(self) ImageColorSettingMixin.__init__(self) self.parent = parent self.selection_type = SELECTMANY self.saving_enabled = True self.selection_enabled = True self.viewtype = INDIVIDUAL # required bt InteractiveViewBox self.highlighted = None self.data_points = None self.data_imagepixels = None self.plotview = pg.GraphicsLayoutWidget() self.plot = pg.PlotItem(background="w", viewBox=InteractiveViewBox(self)) self.plotview.addItem(self.plot) self.legend = ImageColorLegend() self.plotview.addItem(self.legend) self.plot.scene().installEventFilter( HelpEventDelegate(self.help_event, self)) layout = QVBoxLayout() self.setLayout(layout) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self.plotview) self.img = ImageItemNan() self.img.setOpts(axisOrder='row-major') self.plot.addItem(self.img) self.plot.scene().sigMouseMoved.connect(self.plot.vb.mouseMovedEvent) layout = QGridLayout() self.plotview.setLayout(layout) self.button = QPushButton("Menu", self.plotview) self.button.setAutoDefault(False) layout.setRowStretch(1, 1) layout.setColumnStretch(1, 1) layout.addWidget(self.button, 0, 0) view_menu = MenuFocus(self) self.button.setMenu(view_menu) # prepare interface according to the new context self.parent.contextAboutToBeOpened.connect( lambda x: self.init_interface_data(x[0])) self.add_zoom_actions(view_menu) common_options = dict(labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str) choose_xy = QWidgetAction(self) box = gui.vBox(self) box.setFocusPolicy(Qt.TabFocus) self.xy_model = DomainModel(DomainModel.METAS | DomainModel.CLASSES, valid_types=DomainModel.PRIMITIVE, placeholder="Position (index)") self.cb_attr_x = gui.comboBox(box, self, "attr_x", label="Axis x:", callback=self.update_attr, model=self.xy_model, **common_options) box.setFocusProxy(self.cb_attr_x) box.layout().addWidget(self.setup_color_settings_box()) choose_xy.setDefaultWidget(box) view_menu.addAction(choose_xy) self.lsx = None # info about the X axis self.lsy = None # info about the Y axis self.data = None self.data_ids = {} def init_interface_data(self, data): same_domain = (self.data and data and data.domain == self.data.domain) if not same_domain: self.init_attr_values(data) def help_event(self, ev): pos = self.plot.vb.mapSceneToView(ev.scenePos()) sel, wavenumber_ind = self._points_at_pos(pos) prepared = [] if sel is not None: prepared.append(str(self.wavenumbers[wavenumber_ind])) for d in self.data[sel]: variables = [ v for v in self.data.domain.metas + self.data.domain.class_vars if v not in [self.attr_x] ] features = [ '{} = {}'.format(attr.name, d[attr]) for attr in variables ] features.append('value = {}'.format(d[wavenumber_ind])) prepared.append("\n".join(features)) text = "\n\n".join(prepared) if text: text = ('<span style="white-space:pre">{}</span>'.format( escape(text))) QToolTip.showText(ev.screenPos(), text, widget=self.plotview) return True else: return False def update_attr(self): self.update_view() def init_attr_values(self, data): domain = data.domain if data is not None else None self.xy_model.set_domain(domain) self.attr_x = self.xy_model[0] if self.xy_model else None def set_data(self, data): if data: self.data = data self.data_ids = {e: i for i, e in enumerate(data.ids)} self.restore_selection_settings() else: self.data = None self.data_ids = {} def update_view(self): self.img.clear() self.img.setSelection(None) self.legend.set_colors(None) self.lsx = None self.lsy = None self.wavenumbers = None self.data_xs = None self.data_imagepixels = None if self.data and len(self.data.domain.attributes): if self.attr_x is not None: xat = self.data.domain[self.attr_x] ndom = Domain([xat]) datam = self.data.transform(ndom) coorx = datam.X[:, 0] else: coorx = np.arange(len(self.data)) self.lsx = lsx = values_to_linspace(coorx) self.data_xs = coorx self.wavenumbers = wavenumbers = getx(self.data) self.lsy = lsy = values_to_linspace(wavenumbers) # set data imdata = np.ones((lsy[2], lsx[2])) * float("nan") xindex = index_values(coorx, lsx) yindex = index_values(wavenumbers, lsy) for xind, d in zip(xindex, self.data.X): imdata[yindex, xind] = d self.data_imagepixels = xindex self.img.setImage(imdata, autoLevels=False) self.update_levels() self.update_color_schema() # shift centres of the pixels so that the axes are useful shiftx = _shift(lsx) shifty = _shift(lsy) left = lsx[0] - shiftx bottom = lsy[0] - shifty width = (lsx[1] - lsx[0]) + 2 * shiftx height = (lsy[1] - lsy[0]) + 2 * shifty self.img.setRect(QRectF(left, bottom, width, height)) self.refresh_img_selection() def refresh_img_selection(self): selected_px = np.zeros((self.lsy[2], self.lsx[2]), dtype=np.uint8) selected_px[:, self.data_imagepixels] = self.selection_group self.img.setSelection(selected_px) def make_selection(self, selected): """Add selected indices to the selection.""" add_to_group, add_group, remove = selection_modifiers() if self.data and self.lsx and self.lsy: if add_to_group: # both keys - need to test it before add_group selnum = np.max(self.selection_group) elif add_group: selnum = np.max(self.selection_group) + 1 elif remove: selnum = 0 else: self.selection_group *= 0 selnum = 1 if selected is not None: self.selection_group[selected] = selnum self.refresh_img_selection() self.prepare_settings_for_saving() self.selection_changed.emit() def _points_at_pos(self, pos): if self.data and self.lsx and self.lsy: x, y = pos.x(), pos.y() x_distance = np.abs(self.data_xs - x) sel = (x_distance < _shift(self.lsx)) wavenumber_distance = np.abs(self.wavenumbers - y) wavenumber_ind = np.argmin(wavenumber_distance) return sel, wavenumber_ind return None, None def select_by_click(self, pos): sel, _ = self._points_at_pos(pos) self.make_selection(sel)
class ImagePlot(QWidget, OWComponent, SelectionGroupMixin, ImageColorSettingMixin, ImageZoomMixin, ConcurrentMixin): attr_x = ContextSetting(None) attr_y = ContextSetting(None) gamma = Setting(0) selection_changed = Signal() image_updated = Signal() def __init__(self, parent): QWidget.__init__(self) OWComponent.__init__(self, parent) SelectionGroupMixin.__init__(self) ImageColorSettingMixin.__init__(self) ImageZoomMixin.__init__(self) ConcurrentMixin.__init__(self) self.parent = parent self.selection_type = SELECTMANY self.saving_enabled = True self.selection_enabled = True self.viewtype = INDIVIDUAL # required bt InteractiveViewBox self.highlighted = None self.data_points = None self.data_values = None self.data_imagepixels = None self.data_valid_positions = None self.plotview = pg.GraphicsLayoutWidget() self.plot = pg.PlotItem(background="w", viewBox=InteractiveViewBox(self)) self.plotview.addItem(self.plot) self.legend = ImageColorLegend() self.plotview.addItem(self.legend) self.plot.scene().installEventFilter( HelpEventDelegate(self.help_event, self)) layout = QVBoxLayout() self.setLayout(layout) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self.plotview) self.img = ImageItemNan() self.img.setOpts(axisOrder='row-major') self.plot.addItem(self.img) self.vis_img = pg.ImageItem() self.vis_img.setOpts(axisOrder='row-major') self.plot.vb.setAspectLocked() self.plot.scene().sigMouseMoved.connect(self.plot.vb.mouseMovedEvent) layout = QGridLayout() self.plotview.setLayout(layout) self.button = QPushButton("Menu", self.plotview) self.button.setAutoDefault(False) layout.setRowStretch(1, 1) layout.setColumnStretch(1, 1) layout.addWidget(self.button, 0, 0) view_menu = MenuFocus(self) self.button.setMenu(view_menu) # prepare interface according to the new context self.parent.contextAboutToBeOpened.connect( lambda x: self.init_interface_data(x[0])) actions = [] self.add_zoom_actions(view_menu) select_square = QAction( "Select (square)", self, triggered=self.plot.vb.set_mode_select_square, ) select_square.setShortcuts([Qt.Key_S]) select_square.setShortcutContext(Qt.WidgetWithChildrenShortcut) actions.append(select_square) select_polygon = QAction( "Select (polygon)", self, triggered=self.plot.vb.set_mode_select_polygon, ) select_polygon.setShortcuts([Qt.Key_P]) select_polygon.setShortcutContext(Qt.WidgetWithChildrenShortcut) actions.append(select_polygon) if self.saving_enabled: save_graph = QAction( "Save graph", self, triggered=self.save_graph, ) save_graph.setShortcuts( [QKeySequence(Qt.ControlModifier | Qt.Key_I)]) actions.append(save_graph) view_menu.addActions(actions) self.addActions(actions) common_options = dict(labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True) choose_xy = QWidgetAction(self) box = gui.vBox(self) box.setFocusPolicy(Qt.TabFocus) self.xy_model = DomainModel(DomainModel.METAS | DomainModel.CLASSES, valid_types=DomainModel.PRIMITIVE) self.cb_attr_x = gui.comboBox(box, self, "attr_x", label="Axis x:", callback=self.update_attr, model=self.xy_model, **common_options) self.cb_attr_y = gui.comboBox(box, self, "attr_y", label="Axis y:", callback=self.update_attr, model=self.xy_model, **common_options) box.setFocusProxy(self.cb_attr_x) box.layout().addWidget(self.color_settings_box()) choose_xy.setDefaultWidget(box) view_menu.addAction(choose_xy) self.lsx = None # info about the X axis self.lsy = None # info about the Y axis self.data = None self.data_ids = {} def init_interface_data(self, data): same_domain = (self.data and data and data.domain == self.data.domain) if not same_domain: self.init_attr_values(data) def help_event(self, ev): pos = self.plot.vb.mapSceneToView(ev.scenePos()) sel = self._points_at_pos(pos) prepared = [] if sel is not None: data, vals, points = self.data[sel], self.data_values[ sel], self.data_points[sel] for d, v, p in zip(data, vals, points): basic = "({}, {}): {}".format(p[0], p[1], v) variables = [ v for v in self.data.domain.metas + self.data.domain.class_vars if v not in [self.attr_x, self.attr_y] ] features = [ '{} = {}'.format(attr.name, d[attr]) for attr in variables ] prepared.append("\n".join([basic] + features)) text = "\n\n".join(prepared) if text: text = ('<span style="white-space:pre">{}</span>'.format( escape(text))) QToolTip.showText(ev.screenPos(), text, widget=self.plotview) return True else: return False def update_attr(self): self.update_view() def init_attr_values(self, data): domain = data.domain if data is not None else None self.xy_model.set_domain(domain) self.attr_x = self.xy_model[0] if self.xy_model else None self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \ else self.attr_x def save_graph(self): saveplot.save_plot(self.plotview, self.parent.graph_writers) def set_data(self, data): if data: self.data = data self.data_ids = {e: i for i, e in enumerate(data.ids)} self.restore_selection_settings() else: self.data = None self.data_ids = {} def refresh_img_selection(self): selected_px = np.zeros((self.lsy[2], self.lsx[2]), dtype=np.uint8) selected_px[self.data_imagepixels[self.data_valid_positions, 0], self.data_imagepixels[self.data_valid_positions, 1]] = \ self.selection_group[self.data_valid_positions] self.img.setSelection(selected_px) def make_selection(self, selected): """Add selected indices to the selection.""" add_to_group, add_group, remove = selection_modifiers() if self.data and self.lsx and self.lsy: if add_to_group: # both keys - need to test it before add_group selnum = np.max(self.selection_group) elif add_group: selnum = np.max(self.selection_group) + 1 elif remove: selnum = 0 else: self.selection_group *= 0 selnum = 1 if selected is not None: self.selection_group[selected] = selnum self.refresh_img_selection() self.prepare_settings_for_saving() self.selection_changed.emit() def select_square(self, p1, p2): """ Select elements within a square drawn by the user. A selection needs to contain whole pixels """ x1, y1 = p1.x(), p1.y() x2, y2 = p2.x(), p2.y() polygon = [ QPointF(x1, y1), QPointF(x2, y1), QPointF(x2, y2), QPointF(x1, y2), QPointF(x1, y1) ] self.select_polygon(polygon) def select_polygon(self, polygon): """ Select by a polygon which has to contain whole pixels. """ if self.data and self.lsx and self.lsy: polygon = [(p.x(), p.y()) for p in polygon] # a polygon should contain all pixel shiftx = _shift(self.lsx) shifty = _shift(self.lsy) points_edges = [ self.data_points + [[shiftx, shifty]], self.data_points + [[-shiftx, shifty]], self.data_points + [[shiftx, -shifty]], self.data_points + [[-shiftx, -shifty]] ] inp = in_polygon(points_edges[0], polygon) for p in points_edges[1:]: inp *= in_polygon(p, polygon) self.make_selection(inp) def _points_at_pos(self, pos): if self.data and self.lsx and self.lsy: x, y = pos.x(), pos.y() distance = np.abs(self.data_points - [[x, y]]) sel = (distance[:, 0] < _shift(self.lsx)) * (distance[:, 1] < _shift(self.lsy)) return sel def select_by_click(self, pos): sel = self._points_at_pos(pos) self.make_selection(sel) def update_view(self): self.cancel() self.parent.Error.image_too_big.clear() self.parent.Information.not_shown.clear() self.img.clear() self.img.setSelection(None) self.legend.set_colors(None) self.lsx = None self.lsy = None self.data_points = None self.data_values = None self.data_imagepixels = None self.data_valid_positions = None if self.data and self.attr_x and self.attr_y: self.start(self.compute_image, self.data, self.attr_x, self.attr_y, self.parent.image_values(), self.parent.image_values_fixed_levels()) else: self.image_updated.emit() def set_visible_image(self, img: np.ndarray, rect: QRectF): self.vis_img.setImage(img) self.vis_img.setRect(rect) def show_visible_image(self): if self.vis_img not in self.plot.items: self.plot.addItem(self.vis_img) def hide_visible_image(self): self.plot.removeItem(self.vis_img) def set_visible_image_opacity(self, opacity: int): """Opacity is an alpha channel intensity integer from 0 to 255""" self.vis_img.setOpacity(opacity / 255) def set_visible_image_comp_mode(self, comp_mode: QPainter.CompositionMode): self.vis_img.setCompositionMode(comp_mode) @staticmethod def compute_image(data: Orange.data.Table, attr_x, attr_y, image_values, image_values_fixed_levels, state: TaskState): def progress_interrupt(i: float): if state.is_interruption_requested(): raise InterruptException class Result(): pass res = Result() xat = data.domain[attr_x] yat = data.domain[attr_y] def extract_col(data, var): nd = Domain([var]) d = data.transform(nd) return d.X[:, 0] progress_interrupt(0) res.coorx = extract_col(data, xat) res.coory = extract_col(data, yat) res.data_points = np.hstack( [res.coorx.reshape(-1, 1), res.coory.reshape(-1, 1)]) res.lsx = lsx = values_to_linspace(res.coorx) res.lsy = lsy = values_to_linspace(res.coory) res.image_values_fixed_levels = image_values_fixed_levels progress_interrupt(0) if lsx[-1] * lsy[-1] > IMAGE_TOO_BIG: raise ImageTooBigException((lsx[-1], lsy[-1])) # the code below does this, but part-wise: # d = image_values(data).X[:, 0] parts = [] for slice in split_to_size(len(data), 10000): part = image_values(data[slice]).X[:, 0] parts.append(part) progress_interrupt(0) d = np.concatenate(parts) res.d = d progress_interrupt(0) return res def on_done(self, res): self.lsx, self.lsy = res.lsx, res.lsy lsx, lsy = self.lsx, self.lsy d = res.d self.fixed_levels = res.image_values_fixed_levels self.data_points = res.data_points xindex, xnan = index_values_nan(res.coorx, self.lsx) yindex, ynan = index_values_nan(res.coory, self.lsy) self.data_valid_positions = valid = np.logical_not( np.logical_or(xnan, ynan)) invalid_positions = len(d) - np.sum(valid) if invalid_positions: self.parent.Information.not_shown(invalid_positions) imdata = np.ones((lsy[2], lsx[2])) * float("nan") imdata[yindex[valid], xindex[valid]] = d[valid] self.data_values = d self.data_imagepixels = np.vstack((yindex, xindex)).T self.img.setImage(imdata, autoLevels=False) self.update_levels() self.update_color_schema() self.update_legend_visible() # shift centres of the pixels so that the axes are useful shiftx = _shift(lsx) shifty = _shift(lsy) left = lsx[0] - shiftx bottom = lsy[0] - shifty width = (lsx[1] - lsx[0]) + 2 * shiftx height = (lsy[1] - lsy[0]) + 2 * shifty self.img.setRect(QRectF(left, bottom, width, height)) self.refresh_img_selection() self.image_updated.emit() def on_partial_result(self, result): pass def on_exception(self, ex: Exception): if isinstance(ex, InterruptException): return if isinstance(ex, ImageTooBigException): self.parent.Error.image_too_big(ex.args[0][0], ex.args[0][1]) self.image_updated.emit() else: raise ex
class ImagePlot(QWidget, OWComponent): attr_x = ContextSetting(None) attr_y = ContextSetting(None) gamma = Setting(0) threshold_low = Setting(0.0) threshold_high = Setting(1.0) def __init__(self, parent): QWidget.__init__(self) OWComponent.__init__(self, parent) self.parent = parent self.selection_enabled = False self.plotview = pg.PlotWidget(background="w", viewBox=InteractiveViewBox(self)) self.plot = self.plotview.getPlotItem() layout = QVBoxLayout() self.setLayout(layout) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self.plotview) self.img = ImageItemNan() self.img.setOpts(axisOrder='row-major') self.plot.addItem(self.img) self.plot.vb.setAspectLocked() layout = QGridLayout() self.plotview.setLayout(layout) self.button = QPushButton("View", self.plotview) self.button.setAutoDefault(False) layout.setRowStretch(1, 1) layout.setColumnStretch(1, 1) layout.addWidget(self.button, 0, 0) view_menu = MenuFocus(self) self.button.setMenu(view_menu) common_options = dict(labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str) choose_xy = QWidgetAction(self) box = gui.vBox(self) box.setFocusPolicy(Qt.TabFocus) self.xy_model = DomainModel(DomainModel.METAS | DomainModel.CLASSES, valid_types=DomainModel.PRIMITIVE) self.models = [self.xy_model] self.cb_attr_x = gui.comboBox(box, self, "attr_x", label="Axis x:", callback=self.update_attr, model=self.xy_model, **common_options) self.cb_attr_y = gui.comboBox(box, self, "attr_y", label="Axis y:", callback=self.update_attr, model=self.xy_model, **common_options) box.setFocusProxy(self.cb_attr_x) form = QFormLayout(formAlignment=Qt.AlignLeft, labelAlignment=Qt.AlignLeft, fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow) lowslider = gui.hSlider(box, self, "threshold_low", minValue=0.0, maxValue=1.0, step=0.05, ticks=True, intOnly=False, createLabel=False, callback=self.update_color_schema) highslider = gui.hSlider(box, self, "threshold_high", minValue=0.0, maxValue=1.0, step=0.05, ticks=True, intOnly=False, createLabel=False, callback=self.update_color_schema) gammaslider = gui.hSlider(box, self, "gamma", minValue=0.0, maxValue=20.0, step=1.0, ticks=True, intOnly=False, createLabel=False, callback=self.update_color_schema) form.addRow("Low:", lowslider) form.addRow("High:", highslider) form.addRow("Gamma:", gammaslider) box.layout().addLayout(form) choose_xy.setDefaultWidget(box) view_menu.addAction(choose_xy) self.markings_integral = [] self.data = None def update_color_schema(self): if not self.threshold_low < self.threshold_high: # TODO this belongs here, not in the parent self.parent.Warning.threshold_error() return else: self.parent.Warning.threshold_error.clear() # TODO add color chooser colors = [(0, 0, 255), (255, 255, 0)] cols = color_palette_table(colors, threshold_low=self.threshold_low, threshold_high=self.threshold_high, gamma=self.gamma) self.img.setLookupTable(cols) def update_attr(self): self.show_data() def init_attr_values(self): domain = self.data and self.data.domain for model in self.models: model.set_domain(domain) self.attr_x = self.xy_model[0] if self.xy_model else None self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \ else self.attr_x def set_data(self, data): self.img.clear() if data is not None: same_domain = (self.data and data.domain.checksum() == self.data.domain.checksum()) self.data = data if not same_domain: self.init_attr_values() self.show_data() def set_integral_limits(self): self.show_data() def refresh_markings(self, di): for m in self.markings_integral: self.parent.curveplot.remove_marking(m) self.markings_integral = [] color = Qt.red def add_marking(a): self.markings_integral.append(a) self.parent.curveplot.add_marking(a) if "baseline" in di: bs_x, bs_ys = di["baseline"] baseline = pg.PlotCurveItem() baseline.setPen( pg.mkPen(color=QColor(color), width=2, style=Qt.DotLine)) baseline.setZValue(10) baseline.setData(x=bs_x, y=bs_ys[0]) add_marking(baseline) if "curve" in di: bs_x, bs_ys = di["curve"] curve = pg.PlotCurveItem() curve.setPen(pg.mkPen(color=QColor(color), width=2)) curve.setZValue(10) curve.setData(x=bs_x, y=bs_ys[0]) add_marking(curve) if "fill" in di: (x1, ys1), (x2, ys2) = di["fill"] phigh = pg.PlotCurveItem(x1, ys1[0], pen=None) plow = pg.PlotCurveItem(x2, ys2[0], pen=None) color = QColor(color) color.setAlphaF(0.5) cc = pg.mkBrush(color) pfill = pg.FillBetweenItem(plow, phigh, brush=cc) pfill.setZValue(9) add_marking(pfill) if "line" in di: (x1, y1), (x2, y2) = di["line"] line = pg.PlotCurveItem() line.setPen(pg.mkPen(color=QColor(color), width=4)) line.setZValue(10) line.setData(x=[x1[0], x2[0]], y=[y1[0], y2[0]]) add_marking(line) def show_data(self): self.img.clear() if self.data: xat = self.data.domain[self.attr_x] yat = self.data.domain[self.attr_y] ndom = Orange.data.Domain([xat, yat]) datam = Orange.data.Table(ndom, self.data) coorx = datam.X[:, 0] coory = datam.X[:, 1] lsx = values_to_linspace(coorx) lsy = values_to_linspace(coory) l1, l2 = self.parent.lowlim, self.parent.highlim gx = getx(self.data) if l1 is None: l1 = min(gx) - 1 if l2 is None: l2 = max(gx) + 1 l1, l2 = min(l1, l2), max(l1, l2) imethod = self.parent.integration_methods[ self.parent.integration_method] datai = Integrate(method=imethod, limits=[[l1, l2]])(self.data) di = {} if self.parent.curveplot.selected_indices: ind = list(self.parent.curveplot.selected_indices)[0] di = datai.domain.attributes[0].compute_value.draw_info( self.data[ind:ind + 1]) self.refresh_markings(di) d = datai.X[:, 0] # set data imdata = np.ones((lsy[2], lsx[2])) * float("nan") xindex = index_values(coorx, lsx) yindex = index_values(coory, lsy) imdata[yindex, xindex] = d levels = get_levels(imdata) self.update_color_schema() self.img.setImage(imdata, levels=levels) # shift centres of the pixels so that the axes are useful shiftx = (lsx[1] - lsx[0]) / (2 * (lsx[2] - 1)) shifty = (lsy[1] - lsy[0]) / (2 * (lsy[2] - 1)) left = lsx[0] - shiftx bottom = lsy[0] - shifty width = (lsx[1] - lsx[0]) + 2 * shiftx height = (lsy[1] - lsy[0]) + 2 * shifty self.img.setRect(QRectF(left, bottom, width, height))
class ImagePlot(QWidget, OWComponent, SelectionGroupMixin, ImageColorSettingMixin, ImageZoomMixin): attr_x = ContextSetting(None) attr_y = ContextSetting(None) gamma = Setting(0) selection_changed = Signal() def __init__(self, parent): QWidget.__init__(self) OWComponent.__init__(self, parent) SelectionGroupMixin.__init__(self) ImageColorSettingMixin.__init__(self) ImageZoomMixin.__init__(self) self.parent = parent self.selection_type = SELECTMANY self.saving_enabled = True self.selection_enabled = True self.viewtype = INDIVIDUAL # required bt InteractiveViewBox self.highlighted = None self.data_points = None self.data_values = None self.data_imagepixels = None self.data_valid_positions = None self.plotview = pg.GraphicsLayoutWidget() self.plot = pg.PlotItem(background="w", viewBox=InteractiveViewBox(self)) self.plotview.addItem(self.plot) self.legend = ImageColorLegend() self.plotview.addItem(self.legend) self.plot.scene().installEventFilter( HelpEventDelegate(self.help_event, self)) layout = QVBoxLayout() self.setLayout(layout) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self.plotview) self.img = ImageItemNan() self.img.setOpts(axisOrder='row-major') self.plot.addItem(self.img) self.plot.vb.setAspectLocked() self.plot.scene().sigMouseMoved.connect(self.plot.vb.mouseMovedEvent) layout = QGridLayout() self.plotview.setLayout(layout) self.button = QPushButton("Menu", self.plotview) self.button.setAutoDefault(False) layout.setRowStretch(1, 1) layout.setColumnStretch(1, 1) layout.addWidget(self.button, 0, 0) view_menu = MenuFocus(self) self.button.setMenu(view_menu) # prepare interface according to the new context self.parent.contextAboutToBeOpened.connect( lambda x: self.init_interface_data(x[0])) actions = [] self.add_zoom_actions(view_menu) select_square = QAction( "Select (square)", self, triggered=self.plot.vb.set_mode_select_square, ) select_square.setShortcuts([Qt.Key_S]) select_square.setShortcutContext(Qt.WidgetWithChildrenShortcut) actions.append(select_square) select_polygon = QAction( "Select (polygon)", self, triggered=self.plot.vb.set_mode_select_polygon, ) select_polygon.setShortcuts([Qt.Key_P]) select_polygon.setShortcutContext(Qt.WidgetWithChildrenShortcut) actions.append(select_polygon) if self.saving_enabled: save_graph = QAction( "Save graph", self, triggered=self.save_graph, ) save_graph.setShortcuts( [QKeySequence(Qt.ControlModifier | Qt.Key_I)]) actions.append(save_graph) view_menu.addActions(actions) self.addActions(actions) common_options = dict(labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str) choose_xy = QWidgetAction(self) box = gui.vBox(self) box.setFocusPolicy(Qt.TabFocus) self.xy_model = DomainModel(DomainModel.METAS | DomainModel.CLASSES, valid_types=DomainModel.PRIMITIVE) self.cb_attr_x = gui.comboBox(box, self, "attr_x", label="Axis x:", callback=self.update_attr, model=self.xy_model, **common_options) self.cb_attr_y = gui.comboBox(box, self, "attr_y", label="Axis y:", callback=self.update_attr, model=self.xy_model, **common_options) box.setFocusProxy(self.cb_attr_x) box.layout().addWidget(self.color_settings_box()) choose_xy.setDefaultWidget(box) view_menu.addAction(choose_xy) self.markings_integral = [] self.lsx = None # info about the X axis self.lsy = None # info about the Y axis self.data = None self.data_ids = {} def init_interface_data(self, data): same_domain = (self.data and data and data.domain == self.data.domain) if not same_domain: self.init_attr_values(data) def help_event(self, ev): pos = self.plot.vb.mapSceneToView(ev.scenePos()) sel = self._points_at_pos(pos) prepared = [] if sel is not None: data, vals, points = self.data[sel], self.data_values[ sel], self.data_points[sel] for d, v, p in zip(data, vals, points): basic = "({}, {}): {}".format(p[0], p[1], v) variables = [ v for v in self.data.domain.metas + self.data.domain.class_vars if v not in [self.attr_x, self.attr_y] ] features = [ '{} = {}'.format(attr.name, d[attr]) for attr in variables ] prepared.append("\n".join([basic] + features)) text = "\n\n".join(prepared) if text: text = ('<span style="white-space:pre">{}</span>'.format( escape(text))) QToolTip.showText(ev.screenPos(), text, widget=self.plotview) return True else: return False def update_attr(self): self.update_view() def init_attr_values(self, data): domain = data.domain if data is not None else None self.xy_model.set_domain(domain) self.attr_x = self.xy_model[0] if self.xy_model else None self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \ else self.attr_x def save_graph(self): saveplot.save_plot(self.plotview, self.parent.graph_writers) def set_data(self, data): if data: self.data = data self.data_ids = {e: i for i, e in enumerate(data.ids)} self.restore_selection_settings() else: self.data = None self.data_ids = {} def refresh_markings(self, di): refresh_integral_markings([{ "draw": di }], self.markings_integral, self.parent.curveplot) def update_view(self): self.parent.Error.image_too_big.clear() self.parent.Information.not_shown.clear() self.img.clear() self.img.setSelection(None) self.legend.set_colors(None) self.lsx = None self.lsy = None self.data_points = None self.data_values = None self.data_imagepixels = None self.data_valid_positions = None if self.data and self.attr_x and self.attr_y: xat = self.data.domain[self.attr_x] yat = self.data.domain[self.attr_y] ndom = Orange.data.Domain([xat, yat]) datam = Orange.data.Table(ndom, self.data) coorx = datam.X[:, 0] coory = datam.X[:, 1] self.data_points = datam.X self.lsx = lsx = values_to_linspace(coorx) self.lsy = lsy = values_to_linspace(coory) if lsx[-1] * lsy[-1] > IMAGE_TOO_BIG: self.parent.Error.image_too_big(lsx[-1], lsy[-1]) return di = {} if self.parent.value_type == 0: # integrals imethod = self.parent.integration_methods[ self.parent.integration_method] if imethod != Integrate.PeakAt: datai = Integrate( methods=imethod, limits=[[self.parent.lowlim, self.parent.highlim]])(self.data) else: datai = Integrate( methods=imethod, limits=[[self.parent.choose, self.parent.choose]])(self.data) if np.any(self.parent.curveplot.selection_group): # curveplot can have a subset of curves on the input> match IDs ind = np.flatnonzero( self.parent.curveplot.selection_group)[0] dind = self.data_ids[self.parent.curveplot.data[ind].id] di = datai.domain.attributes[0].compute_value.draw_info( self.data[dind:dind + 1]) d = datai.X[:, 0] else: dat = self.data.domain[self.parent.attr_value] ndom = Orange.data.Domain([dat]) d = Orange.data.Table(ndom, self.data).X[:, 0] self.refresh_markings(di) xindex, xnan = index_values_nan(coorx, lsx) yindex, ynan = index_values_nan(coory, lsy) self.data_valid_positions = valid = np.logical_not( np.logical_or(xnan, ynan)) invalid_positions = len(d) - np.sum(valid) if invalid_positions: self.parent.Information.not_shown(invalid_positions) imdata = np.ones((lsy[2], lsx[2])) * float("nan") imdata[yindex[valid], xindex[valid]] = d[valid] self.data_values = d self.data_imagepixels = np.vstack((yindex, xindex)).T self.img.setImage(imdata, autoLevels=False) self.update_levels() self.update_color_schema() # shift centres of the pixels so that the axes are useful shiftx = _shift(lsx) shifty = _shift(lsy) left = lsx[0] - shiftx bottom = lsy[0] - shifty width = (lsx[1] - lsx[0]) + 2 * shiftx height = (lsy[1] - lsy[0]) + 2 * shifty self.img.setRect(QRectF(left, bottom, width, height)) self.refresh_img_selection() def refresh_img_selection(self): selected_px = np.zeros((self.lsy[2], self.lsx[2]), dtype=np.uint8) selected_px[self.data_imagepixels[self.data_valid_positions, 0], self.data_imagepixels[self.data_valid_positions, 1]] = \ self.selection_group[self.data_valid_positions] self.img.setSelection(selected_px) def make_selection(self, selected): """Add selected indices to the selection.""" add_to_group, add_group, remove = selection_modifiers() if self.data and self.lsx and self.lsy: if add_to_group: # both keys - need to test it before add_group selnum = np.max(self.selection_group) elif add_group: selnum = np.max(self.selection_group) + 1 elif remove: selnum = 0 else: self.selection_group *= 0 selnum = 1 if selected is not None: self.selection_group[selected] = selnum self.refresh_img_selection() self.prepare_settings_for_saving() self.selection_changed.emit() def select_square(self, p1, p2): """ Select elements within a square drawn by the user. A selection needs to contain whole pixels """ x1, y1 = p1.x(), p1.y() x2, y2 = p2.x(), p2.y() polygon = [ QPointF(x1, y1), QPointF(x2, y1), QPointF(x2, y2), QPointF(x1, y2), QPointF(x1, y1) ] self.select_polygon(polygon) def select_polygon(self, polygon): """ Select by a polygon which has to contain whole pixels. """ if self.data and self.lsx and self.lsy: polygon = [(p.x(), p.y()) for p in polygon] # a polygon should contain all pixel shiftx = _shift(self.lsx) shifty = _shift(self.lsy) points_edges = [ self.data_points + [[shiftx, shifty]], self.data_points + [[-shiftx, shifty]], self.data_points + [[shiftx, -shifty]], self.data_points + [[-shiftx, -shifty]] ] inp = in_polygon(points_edges[0], polygon) for p in points_edges[1:]: inp *= in_polygon(p, polygon) self.make_selection(inp) def _points_at_pos(self, pos): if self.data and self.lsx and self.lsy: x, y = pos.x(), pos.y() distance = np.abs(self.data_points - [[x, y]]) sel = (distance[:, 0] < _shift(self.lsx)) * (distance[:, 1] < _shift(self.lsy)) return sel def select_by_click(self, pos): sel = self._points_at_pos(pos) self.make_selection(sel)
class CurvePlot(QWidget, OWComponent): sample_seed = Setting(0, schema_only=True) label_title = Setting("") label_xaxis = Setting("") label_yaxis = Setting("") range_x1 = Setting(None) range_x2 = Setting(None) range_y1 = Setting(None) range_y2 = Setting(None) feature_color = ContextSetting(None) invertX = Setting(False) selected_indices = Setting(set()) data_size = Setting(None) # to invalidate selected_indices viewtype = Setting(INDIVIDUAL) def __init__(self, parent=None, select=SELECTNONE): QWidget.__init__(self) OWComponent.__init__(self, parent) self.parent = parent self.selection_type = select self.saving_enabled = hasattr(self.parent, "save_graph") self.clear_data(init=True) self.subset = None # current subset input self.subset_indices = None # boolean index array with indices in self.data self.plotview = pg.PlotWidget(background="w", viewBox=InteractiveViewBoxC(self)) self.plot = self.plotview.getPlotItem() self.plot.setDownsampling(auto=True, mode="peak") self.markings = [] self.vLine = pg.InfiniteLine(angle=90, movable=False) self.hLine = pg.InfiniteLine(angle=0, movable=False) self.proxy = pg.SignalProxy(self.plot.scene().sigMouseMoved, rateLimit=20, slot=self.mouseMoved, delay=0.1) self.plot.scene().sigMouseMoved.connect(self.plot.vb.mouseMovedEvent) self.plot.vb.sigRangeChanged.connect(self.resized) self.pen_mouse = pg.mkPen(color=(0, 0, 255), width=2) self.pen_normal = defaultdict( lambda: pg.mkPen(color=(200, 200, 200, 127), width=1)) self.pen_subset = defaultdict( lambda: pg.mkPen(color=(0, 0, 0, 127), width=1)) self.pen_selected = defaultdict( lambda: pg.mkPen(color=(0, 0, 0, 127), width=2, style=Qt.DotLine)) self.label = pg.TextItem("", anchor=(1, 0)) self.label.setText("", color=(0, 0, 0)) self.discrete_palette = None QPixmapCache.setCacheLimit(max(QPixmapCache.cacheLimit(), 100 * 1024)) self.curves_cont = PlotCurvesItem() self.important_decimals = 4, 4 self.plot.scene().installEventFilter( HelpEventDelegate(self.help_event, self)) # whether to rescale at next update self.rescale_next = True self.MOUSE_RADIUS = 20 self.clear_graph() # interface settings self.location = True # show current position self.markclosest = True # mark self.crosshair = True self.crosshair_hidden = True layout = QVBoxLayout() self.setLayout(layout) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self.plotview) actions = [] resample_curves = QAction( "Resample curves", self, shortcut=Qt.Key_R, triggered=lambda x: self.resample_curves(self.sample_seed + 1)) actions.append(resample_curves) reset_curves = QAction("Resampling reset", self, shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_R), triggered=lambda x: self.resample_curves(0)) actions.append(reset_curves) zoom_in = QAction("Zoom in", self, triggered=self.plot.vb.set_mode_zooming) zoom_in.setShortcuts([Qt.Key_Z, QKeySequence(QKeySequence.ZoomIn)]) zoom_in.setShortcutContext(Qt.WidgetWithChildrenShortcut) actions.append(zoom_in) zoom_fit = QAction( "Zoom to fit", self, triggered=lambda x: (self.plot.vb.autoRange(), self.plot.vb.set_mode_panning())) zoom_fit.setShortcuts( [Qt.Key_Backspace, QKeySequence(Qt.ControlModifier | Qt.Key_0)]) zoom_fit.setShortcutContext(Qt.WidgetWithChildrenShortcut) actions.append(zoom_fit) rescale_y = QAction("Rescale Y to fit", self, shortcut=Qt.Key_D, triggered=self.rescale_current_view_y) actions.append(rescale_y) self.view_average_menu = QAction( "Show averages", self, shortcut=Qt.Key_A, checkable=True, triggered=lambda x: self.viewtype_changed()) actions.append(self.view_average_menu) self.show_grid = False self.show_grid_a = QAction("Show grid", self, shortcut=Qt.Key_G, checkable=True, triggered=self.grid_changed) actions.append(self.show_grid_a) self.invertX_menu = QAction("Invert X", self, shortcut=Qt.Key_X, checkable=True, triggered=self.invertX_changed) actions.append(self.invertX_menu) if self.selection_type == SELECTMANY: select_curves = QAction( "Select (line)", self, triggered=self.line_select_start, ) select_curves.setShortcuts([Qt.Key_S]) select_curves.setShortcutContext(Qt.WidgetWithChildrenShortcut) actions.append(select_curves) if self.saving_enabled: save_graph = QAction( "Save graph", self, triggered=self.save_graph, ) save_graph.setShortcuts( [QKeySequence(Qt.ControlModifier | Qt.Key_S)]) actions.append(save_graph) range_menu = MenuFocus("Define view range", self) range_action = QWidgetAction(self) layout = QGridLayout() range_box = gui.widgetBox(self, margin=5, orientation=layout) range_box.setFocusPolicy(Qt.TabFocus) self.range_e_x1 = lineEditFloatOrNone(None, self, "range_x1", label="e") range_box.setFocusProxy(self.range_e_x1) self.range_e_x2 = lineEditFloatOrNone(None, self, "range_x2", label="e") layout.addWidget(QLabel("X"), 0, 0, Qt.AlignRight) layout.addWidget(self.range_e_x1, 0, 1) layout.addWidget(QLabel("-"), 0, 2) layout.addWidget(self.range_e_x2, 0, 3) self.range_e_y1 = lineEditFloatOrNone(None, self, "range_y1", label="e") self.range_e_y2 = lineEditFloatOrNone(None, self, "range_y2", label="e") layout.addWidget(QLabel("Y"), 1, 0, Qt.AlignRight) layout.addWidget(self.range_e_y1, 1, 1) layout.addWidget(QLabel("-"), 1, 2) layout.addWidget(self.range_e_y2, 1, 3) b = gui.button(None, self, "Apply", callback=self.set_limits) layout.addWidget(b, 2, 3, Qt.AlignRight) range_action.setDefaultWidget(range_box) range_menu.addAction(range_action) layout = QGridLayout() self.plotview.setLayout(layout) self.button = QPushButton("View", self.plotview) self.button.setAutoDefault(False) layout.setRowStretch(1, 1) layout.setColumnStretch(1, 1) layout.addWidget(self.button, 0, 0) view_menu = MenuFocus(self) self.button.setMenu(view_menu) view_menu.addActions(actions) view_menu.addMenu(range_menu) self.addActions(actions) choose_color_action = QWidgetAction(self) choose_color_box = gui.hBox(self) choose_color_box.setFocusPolicy(Qt.TabFocus) label = gui.label(choose_color_box, self, "Color by") label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) self.feature_color_model = DomainModel( DomainModel.METAS | DomainModel.CLASSES, valid_types=(DiscreteVariable, ), placeholder="None") self.feature_color_combo = gui.comboBox(choose_color_box, self, "feature_color", callback=self.update_view, model=self.feature_color_model, valueType=str) choose_color_box.setFocusProxy(self.feature_color_combo) choose_color_action.setDefaultWidget(choose_color_box) view_menu.addAction(choose_color_action) cycle_colors = QShortcut(Qt.Key_C, self, self.cycle_color_attr, context=Qt.WidgetWithChildrenShortcut) labels_action = QWidgetAction(self) layout = QGridLayout() labels_box = gui.widgetBox(self, margin=0, orientation=layout) t = gui.lineEdit(None, self, "label_title", label="Title:", callback=self.labels_changed, callbackOnType=self.labels_changed) layout.addWidget(QLabel("Title:"), 0, 0, Qt.AlignRight) layout.addWidget(t, 0, 1) t = gui.lineEdit(None, self, "label_xaxis", label="X-axis:", callback=self.labels_changed, callbackOnType=self.labels_changed) layout.addWidget(QLabel("X-axis:"), 1, 0, Qt.AlignRight) layout.addWidget(t, 1, 1) t = gui.lineEdit(None, self, "label_yaxis", label="Y-axis:", callback=self.labels_changed, callbackOnType=self.labels_changed) layout.addWidget(QLabel("Y-axis:"), 2, 0, Qt.AlignRight) layout.addWidget(t, 2, 1) labels_action.setDefaultWidget(labels_box) view_menu.addAction(labels_action) self.labels_changed() # apply saved labels self.invertX_apply() self.plot.vb.set_mode_panning() self.reports = {} # current reports self.viewhelpers_show() def line_select_start(self): if self.viewtype == INDIVIDUAL: self.plot.vb.set_mode_select() def help_event(self, ev): text = "" if self.highlighted is not None: if self.viewtype == INDIVIDUAL: index = self.sampled_indices[self.highlighted] variables = self.data.domain.metas + self.data.domain.class_vars text += "".join( '{} = {}\n'.format(attr.name, self.data[index][attr]) for attr in variables) elif self.viewtype == AVERAGE: c = self.multiple_curves_info[self.highlighted] nc = sum(c[2]) if c[0] is not None: text += str(c[0]) + " " if c[1]: text += "({})".format(c[1]) if text: text += "\n" text += "{} curves".format(nc) if text: text = text.rstrip() text = ('<span style="white-space:pre">{}</span>'.format( escape(text))) QToolTip.showText(ev.screenPos(), text, widget=self.plotview) return True else: return False def report(self, reporter, contents): self.reports[id(reporter)] = contents def report_finished(self, reporter): try: self.reports.pop(id(reporter)) except KeyError: pass # ok if it was already removed if not self.reports: pass def cycle_color_attr(self): elements = [(a.name if isinstance(a, Variable) else a) for a in self.feature_color_model] currentind = 0 try: currentind = elements.index(self.feature_color) except ValueError: pass next = (currentind + 1) % len(self.feature_color_model) self.feature_color = elements[next] self.update_view() def set_limits(self): vr = self.plot.vb.viewRect() x1 = self.range_x1 if self.range_x1 is not None else vr.left() x2 = self.range_x2 if self.range_x2 is not None else vr.right() y1 = self.range_y1 if self.range_y1 is not None else vr.top() y2 = self.range_y2 if self.range_y2 is not None else vr.bottom() self.plot.vb.setXRange(x1, x2) self.plot.vb.setYRange(y1, y2) def labels_changed(self): self.plot.setTitle(self.label_title) if not self.label_title: self.plot.setTitle(None) self.plot.setLabels(bottom=self.label_xaxis) self.plot.showLabel("bottom", bool(self.label_xaxis)) self.plot.getAxis("bottom").resizeEvent() # align text self.plot.setLabels(left=self.label_yaxis) self.plot.showLabel("left", bool(self.label_yaxis)) self.plot.getAxis("left").resizeEvent() # align text def grid_changed(self): self.show_grid = not self.show_grid self.grid_apply() def grid_apply(self): self.plot.showGrid(self.show_grid, self.show_grid, alpha=0.3) self.show_grid_a.setChecked(self.show_grid) def invertX_changed(self): self.invertX = not self.invertX self.invertX_apply() def invertX_apply(self): self.plot.vb.invertX(self.invertX) self.resized() # force redraw of axes (to avoid a pyqtgraph bug) vr = self.plot.vb.viewRect() self.plot.vb.setRange(xRange=(0, 1), yRange=(0, 1)) self.plot.vb.setRange(rect=vr) self.invertX_menu.setChecked(self.invertX) def save_graph(self): self.viewhelpers_hide() self.plot.showAxis("top", True) self.plot.showAxis("right", True) self.parent.save_graph() self.plot.showAxis("top", False) self.plot.showAxis("right", False) self.viewhelpers_show() def clear_data(self, init=True): self.data = None self.data_x = None # already sorted x-axis self.data_xsind = None # sorting indices for x-axis self.sampled_indices = [] self.sampled_indices_inverse = {} self.sampling = None if not init: self.selection_changed() self.discrete_palette = None def clear_graph(self): # reset caching. if not, it is not cleared when view changing when zoomed self.highlighted = None self.curves_cont.setCacheMode(QGraphicsItem.NoCache) self.curves_cont.setCacheMode(QGraphicsItem.DeviceCoordinateCache) self.plot.vb.disableAutoRange() self.curves_cont.clear() self.curves_cont.update() self.plotview.clear() self.multiple_curves_info = [] self.curves_plotted = [] # currently plotted elements (for rescale) self.curves = [] # for finding closest curve self.plotview.addItem(self.label, ignoreBounds=True) self.highlighted_curve = pg.PlotCurveItem(pen=self.pen_mouse) self.highlighted_curve.setZValue(10) self.highlighted_curve.hide() self.plot.addItem(self.highlighted_curve) self.plot.addItem(self.vLine, ignoreBounds=True) self.plot.addItem(self.hLine, ignoreBounds=True) self.viewhelpers = True self.plot.addItem(self.curves_cont) for m in self.markings: self.plot.addItem(m, ignoreBounds=True) def resized(self): vr = self.plot.vb.viewRect() xpixel, ypixel = self.plot.vb.viewPixelSize() def important_decimals(n): return max(-int(math.floor(math.log10(n))) + 1, 0) self.important_decimals = important_decimals( xpixel), important_decimals(ypixel) if self.invertX: self.label.setPos(vr.bottomLeft()) else: self.label.setPos(vr.bottomRight()) xd, yd = self.important_decimals self.range_e_x1.setPlaceholderText(("%0." + str(xd) + "f") % vr.left()) self.range_e_x2.setPlaceholderText( ("%0." + str(xd) + "f") % vr.right()) self.range_e_y1.setPlaceholderText(("%0." + str(yd) + "f") % vr.top()) self.range_e_y2.setPlaceholderText( ("%0." + str(yd) + "f") % vr.bottom()) def make_selection(self, data_indices, add=False): selected_indices = self.selected_indices oldids = selected_indices.copy() invd = self.sampled_indices_inverse if data_indices is None: if not add: selected_indices.clear() self.set_curve_pens([invd[a] for a in oldids if a in invd]) else: if add: selected_indices.update(data_indices) self.set_curve_pens( [invd[a] for a in data_indices if a in invd]) else: selected_indices.clear() selected_indices.update(data_indices) self.set_curve_pens([ invd[a] for a in (oldids | selected_indices) if a in invd ]) self.selection_changed() def selection_changed(self): if self.selection_type: self.parent.selection_changed() def viewhelpers_hide(self): self.label.hide() self.vLine.hide() self.hLine.hide() def viewhelpers_show(self): self.label.show() if self.crosshair and not self.crosshair_hidden: self.vLine.show() self.hLine.show() else: self.vLine.hide() self.hLine.hide() def mouseMoved(self, evt): pos = evt[0] if self.plot.sceneBoundingRect().contains(pos): mousePoint = self.plot.vb.mapSceneToView(pos) posx, posy = mousePoint.x(), mousePoint.y() labels = [] for a, vs in sorted(self.reports.items()): for v in vs: if isinstance(v, tuple) and len(v) == 2: if v[0] == "x": labels.append( ("%0." + str(self.important_decimals[0]) + "f") % v[1]) continue labels.append(str(v)) labels = " ".join(labels) self.crosshair_hidden = bool(labels) if self.location and not labels: fs = "%0." + str(self.important_decimals[0]) + "f %0." + str( self.important_decimals[1]) + "f" labels = fs % (posx, posy) self.label.setText(labels, color=(0, 0, 0)) if self.curves and len(self.curves[0][0]): # need non-zero x axis! cache = {} bd = None if self.markclosest and self.plot.vb.action != ZOOMING: xpixel, ypixel = self.plot.vb.viewPixelSize() distances = distancetocurves(self.curves[0], posx, posy, xpixel, ypixel, r=self.MOUSE_RADIUS, cache=cache) try: mindi = np.nanargmin(distances) if distances[mindi] < self.MOUSE_RADIUS: bd = mindi except ValueError: # if all distances are NaN pass if self.highlighted != bd: QToolTip.hideText() if self.highlighted is not None and bd is None: self.highlighted = None self.highlighted_curve.hide() if bd is not None: self.highlighted = bd x = self.curves[0][0] y = self.curves[0][1][self.highlighted] self.highlighted_curve.setData(x=x, y=y) self.highlighted_curve.show() self.vLine.setPos(posx) self.hLine.setPos(posy) self.viewhelpers_show() else: self.viewhelpers_hide() def set_curve_pen(self, idc): idcdata = self.sampled_indices[idc] insubset = self.subset_indices[idcdata] inselected = self.selection_type and idcdata in self.selected_indices have_subset = np.any(self.subset_indices) thispen = self.pen_subset if insubset or not have_subset else self.pen_normal if inselected: thispen = self.pen_selected color_var = self._current_color_var() value = None if color_var is None else str( self.data[idcdata][color_var]) self.curves_cont.objs[idc].setPen(thispen[value]) self.curves_cont.objs[idc].setZValue(int(insubset) + int(inselected)) def set_curve_pens(self, curves=None): if self.viewtype == INDIVIDUAL and self.curves: curves = range(len( self.curves[0][1])) if curves is None else curves for i in curves: self.set_curve_pen(i) self.curves_cont.update() def add_marking(self, item): self.markings.append(item) self.plot.addItem(item, ignoreBounds=True) def remove_marking(self, item): self.plot.removeItem(item) self.markings.remove(item) def clear_markings(self): for m in self.markings: self.plot.removeItem(m) self.markings = [] def add_curves(self, x, ys, addc=True): """ Add multiple curves with the same x domain. """ if len(ys) > MAX_INSTANCES_DRAWN: sample_selection = random.Random(self.sample_seed).sample( range(len(ys)), MAX_INSTANCES_DRAWN) # with random selection also show at most MAX_INSTANCES_DRAW elements from the subset subset = set(np.where(self.subset_indices)[0]) subset_to_show = subset - set(sample_selection) subset_additional = MAX_INSTANCES_DRAWN - (len(subset) - len(subset_to_show)) if len(subset_to_show) > subset_additional: subset_to_show = random.Random(self.sample_seed).sample( subset_to_show, subset_additional) self.sampled_indices = sorted(sample_selection + list(subset_to_show)) self.sampling = True else: self.sampled_indices = list(range(len(ys))) random.Random(self.sample_seed).shuffle( self.sampled_indices) # for sequential classes# self.sampled_indices_inverse = { s: i for i, s in enumerate(self.sampled_indices) } ys = self.data.X[self.sampled_indices][:, self.data_xsind] self.curves.append((x, ys)) for y in ys: c = pg.PlotCurveItem(x=x, y=y, pen=self.pen_normal[None]) self.curves_cont.add_curve(c) self.curves_plotted = self.curves def add_curve(self, x, y, pen=None): c = pg.PlotCurveItem(x=x, y=y, pen=pen if pen else self.pen_normal[None]) self.curves_cont.add_curve(c) # for rescale to work correctly self.curves_plotted.append((x, np.array([y]))) def add_fill_curve(self, x, ylow, yhigh, pen): phigh = pg.PlotCurveItem(x, yhigh, pen=pen) plow = pg.PlotCurveItem(x, ylow, pen=pen) color = pen.color() color.setAlphaF(0.2) cc = pg.mkBrush(color) pfill = pg.FillBetweenItem(plow, phigh, brush=cc) pfill.setZValue(10) self.curves_cont.add_bounds(phigh) self.curves_cont.add_bounds(plow) self.curves_cont.add_curve(pfill, ignore_bounds=True) # for zoom to work correctly self.curves_plotted.append((x, np.array([ylow, yhigh]))) def _current_color_var(self): color_var = None if self.feature_color and self.data: color_var = self.data.domain[self.feature_color] return color_var def set_pen_colors(self): self.pen_normal.clear() self.pen_subset.clear() self.pen_selected.clear() color_var = self._current_color_var() if color_var is not None: colors = color_var.colors discrete_palette = ColorPaletteGenerator( number_of_colors=len(colors), rgb_colors=colors) for v in color_var.values: basecolor = discrete_palette[color_var.to_val(v)] basecolor = QColor(basecolor) basecolor.setAlphaF(0.9) self.pen_subset[v] = pg.mkPen(color=basecolor, width=1) self.pen_selected[v] = pg.mkPen(color=basecolor, width=2, style=Qt.DotLine) notselcolor = basecolor.lighter(150) notselcolor.setAlphaF(0.5) self.pen_normal[v] = pg.mkPen(color=notselcolor, width=1) def show_individual(self): self.view_average_menu.setChecked(False) self.set_pen_colors() self.clear_graph() self.viewtype = INDIVIDUAL if not self.data: return self.add_curves(self.data_x, self.data.X) self.set_curve_pens() self.curves_cont.update() self.plot.vb.set_mode_panning() def resample_curves(self, seed): self.sample_seed = seed self.update_view() def rescale_current_view_y(self): if self.curves_plotted: cache = {} qrect = self.plot.vb.targetRect() bleft = qrect.left() bright = qrect.right() ymax = max( np.max(ys[:, searchsorted_cached(cache, x, bleft): searchsorted_cached(cache, x, bright, side="right")]) for x, ys in self.curves_plotted) ymin = min( np.min(ys[:, searchsorted_cached(cache, x, bleft): searchsorted_cached(cache, x, bright, side="right")]) for x, ys in self.curves_plotted) self.plot.vb.setYRange(ymin, ymax, padding=0.0) self.plot.vb.pad_current_view_y() def _split_by_color_value(self, data): color_var = self._current_color_var() rd = {} if color_var is None: rd[None] = np.full((len(data.X), ), True, dtype=bool) else: cvd = Orange.data.Table(Orange.data.Domain([color_var]), data) for v in range(len(color_var.values)): v1 = np.in1d(cvd.X, v) if np.any(v1): rd[color_var.values[v]] = v1 nanind = np.isnan(cvd.X) if np.any(nanind): rd[None] = nanind return rd def viewtype_changed(self): if self.viewtype == AVERAGE: self.viewtype = INDIVIDUAL else: self.viewtype = AVERAGE self.update_view() def show_average(self): self.view_average_menu.setChecked(True) self.set_pen_colors() self.clear_graph() self.viewtype = AVERAGE if not self.data: return x = self.data_x if self.data: ysall = [] cinfo = [] selected_indices = np.full(self.data_size, False, dtype=bool) selected_indices[list(self.selected_indices)] = True dsplit = self._split_by_color_value(self.data) for colorv, indices in dsplit.items(): for part in [None, "subset", "selection"]: if part is None: part_selection = indices pen = self.pen_normal if np.any( self.subset_indices) else self.pen_subset elif part == "selection" and self.selection_type: part_selection = indices & selected_indices pen = self.pen_selected elif part == "subset": part_selection = indices & self.subset_indices pen = self.pen_subset if np.any(part_selection): ys = self.data.X[part_selection] std = np.nanstd(ys, axis=0) mean = np.nanmean(ys, axis=0) std = std[self.data_xsind] mean = mean[self.data_xsind] ysall.append(mean) penc = QPen(pen[colorv]) penc.setWidth(3) self.add_curve(x, mean, pen=penc) self.add_fill_curve(x, mean + std, mean - std, pen=penc) cinfo.append((colorv, part, part_selection)) self.curves.append((x, np.array(ysall))) self.multiple_curves_info = cinfo self.curves_cont.update() self.plot.vb.set_mode_panning() def update_view(self): if self.viewtype == INDIVIDUAL: self.show_individual() elif self.viewtype == AVERAGE: self.show_average() if self.rescale_next: self.plot.vb.autoRange() def set_data(self, data): old_domain = self.data.domain if self.data else None self.clear_data() domain = data.domain if data is not None else None self.feature_color_model.set_domain(domain) if old_domain and domain != old_domain: # do not reset feature_color self.feature_color = self.feature_color_model[ 0] if self.feature_color_model else None if data is not None: if self.data: self.rescale_next = not data.domain == self.data.domain else: self.rescale_next = True self.data = data # reset selection if dataset sizes do not match if self.selected_indices and \ (max(self.selected_indices) >= len(self.data) or self.data_size != len(self.data)): self.selected_indices.clear() self.data_size = len(self.data) # get and sort input data x = getx(self.data) xsind = np.argsort(x) self.data_x = x[xsind] self.data_xsind = xsind self._set_subset_indices( ) # refresh subset indices according to the current subset def _set_subset_indices(self): ids = self.subset if ids is None: ids = [] if self.data: self.subset_indices = np.in1d(self.data.ids, ids) def set_data_subset(self, ids): self.subset = ids self._set_subset_indices() self.update_view() def select_by_click(self, pos, add): clicked_curve = self.highlighted if clicked_curve is not None: if self.viewtype == INDIVIDUAL: self.make_selection([self.sampled_indices[clicked_curve]], add) elif self.viewtype == AVERAGE: sel = np.where(self.multiple_curves_info[clicked_curve][2])[0] self.make_selection(sel, add) else: self.make_selection(None, add) if self.viewtype == AVERAGE: # reset average view self.show_average() def select_line(self, startp, endp, add): intersected = self.intersect_curves((startp.x(), startp.y()), (endp.x(), endp.y())) self.make_selection(intersected if len(intersected) else None, add) def intersect_curves(self, q1, q2): x, ys = self.data_x, self.data.X if len(x) < 2: return [] x1, x2 = min(q1[0], q2[0]), max(q1[0], q2[0]) xmin = closestindex(x, x1) xmax = closestindex(x, x2, side="right") xmin = max(0, xmin - 1) xmax = xmax + 2 sel = np.flatnonzero( intersect_curves_chunked(x, ys, self.data_xsind, q1, q2, xmin, xmax)) return sel
class ImagePlot(QWidget, OWComponent): attr_x = ContextSetting(None) attr_y = ContextSetting(None) gamma = Setting(0) threshold_low = Setting(0.0) threshold_high = Setting(1.0) def __init__(self, parent, select_fn=None): QWidget.__init__(self) OWComponent.__init__(self, parent) self.parent = parent self.select_fn = select_fn self.selection_type = SELECTMANY self.selection_enabled = True self.viewtype = INDIVIDUAL # required bt InteractiveViewBox self.highlighted = None self.selection_matrix = None self.selection_indices = None self.plotview = pg.PlotWidget(background="w", viewBox=InteractiveViewBox(self)) self.plot = self.plotview.getPlotItem() layout = QVBoxLayout() self.setLayout(layout) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self.plotview) self.img = ImageItemNan() self.img.setOpts(axisOrder='row-major') self.plot.addItem(self.img) self.plot.vb.setAspectLocked() self.plot.scene().sigMouseMoved.connect(self.plot.vb.mouseMovedEvent) layout = QGridLayout() self.plotview.setLayout(layout) self.button = QPushButton("View", self.plotview) self.button.setAutoDefault(False) layout.setRowStretch(1, 1) layout.setColumnStretch(1, 1) layout.addWidget(self.button, 0, 0) view_menu = MenuFocus(self) self.button.setMenu(view_menu) actions = [] zoom_in = QAction("Zoom in", self, triggered=self.plot.vb.set_mode_zooming) zoom_in.setShortcuts([Qt.Key_Z, QKeySequence(QKeySequence.ZoomIn)]) zoom_in.setShortcutContext(Qt.WidgetWithChildrenShortcut) actions.append(zoom_in) zoom_fit = QAction( "Zoom to fit", self, triggered=lambda x: (self.plot.vb.autoRange(), self.plot.vb.set_mode_panning())) zoom_fit.setShortcuts( [Qt.Key_Backspace, QKeySequence(Qt.ControlModifier | Qt.Key_0)]) zoom_fit.setShortcutContext(Qt.WidgetWithChildrenShortcut) actions.append(zoom_fit) select_square = QAction( "Select (square)", self, triggered=self.plot.vb.set_mode_select_square, ) select_square.setShortcuts([Qt.Key_S]) select_square.setShortcutContext(Qt.WidgetWithChildrenShortcut) actions.append(select_square) view_menu.addActions(actions) self.addActions(actions) common_options = dict(labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str) choose_xy = QWidgetAction(self) box = gui.vBox(self) box.setFocusPolicy(Qt.TabFocus) self.xy_model = DomainModel(DomainModel.METAS | DomainModel.CLASSES, valid_types=DomainModel.PRIMITIVE) self.models = [self.xy_model] self.cb_attr_x = gui.comboBox(box, self, "attr_x", label="Axis x:", callback=self.update_attr, model=self.xy_model, **common_options) self.cb_attr_y = gui.comboBox(box, self, "attr_y", label="Axis y:", callback=self.update_attr, model=self.xy_model, **common_options) box.setFocusProxy(self.cb_attr_x) form = QFormLayout(formAlignment=Qt.AlignLeft, labelAlignment=Qt.AlignLeft, fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow) lowslider = gui.hSlider(box, self, "threshold_low", minValue=0.0, maxValue=1.0, step=0.05, ticks=True, intOnly=False, createLabel=False, callback=self.update_color_schema) highslider = gui.hSlider(box, self, "threshold_high", minValue=0.0, maxValue=1.0, step=0.05, ticks=True, intOnly=False, createLabel=False, callback=self.update_color_schema) form.addRow("Low:", lowslider) form.addRow("High:", highslider) box.layout().addLayout(form) choose_xy.setDefaultWidget(box) view_menu.addAction(choose_xy) self.markings_integral = [] self.lsx = None # info about the X axis self.lsy = None # info about the Y axis self.data = None self.data_ids = {} def update_color_schema(self): if not self.threshold_low < self.threshold_high: # TODO this belongs here, not in the parent self.parent.Warning.threshold_error() return else: self.parent.Warning.threshold_error.clear() # TODO add color chooser # bgy color scheme colors = np.array(colorcet.linear_bgy_10_95_c74) * 255 cols = color_palette_table(colors, threshold_low=self.threshold_low, threshold_high=self.threshold_high) self.img.setLookupTable(cols) # use defined discrete palette if self.parent.value_type == 1: dat = self.data.domain[self.parent.attr_value] if isinstance(dat, DiscreteVariable): self.img.setLookupTable(dat.colors) def update_attr(self): self.update_view() def init_attr_values(self): domain = self.data.domain if self.data is not None else None for model in self.models: model.set_domain(domain) self.attr_x = self.xy_model[0] if self.xy_model else None self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \ else self.attr_x def set_data(self, data): self.img.clear() if data is not None: same_domain = (self.data and data.domain.checksum() == self.data.domain.checksum()) self.data = data self.data_ids = {e: i for i, e in enumerate(data.ids)} if not same_domain: self.init_attr_values() else: self.data = None self.data_ids = {} def set_integral_limits(self): self.update_view() def refresh_markings(self, di): for m in self.markings_integral: self.parent.curveplot.remove_marking(m) self.markings_integral = [] if di is None: return # nothing to draw color = Qt.red def add_marking(a): self.markings_integral.append(a) self.parent.curveplot.add_marking(a) if "baseline" in di: bs_x, bs_ys = di["baseline"] baseline = pg.PlotCurveItem() baseline.setPen( pg.mkPen(color=QColor(color), width=2, style=Qt.DotLine)) baseline.setZValue(10) baseline.setData(x=bs_x, y=bs_ys[0]) add_marking(baseline) if "curve" in di: bs_x, bs_ys = di["curve"] curve = pg.PlotCurveItem() curve.setPen(pg.mkPen(color=QColor(color), width=2)) curve.setZValue(10) curve.setData(x=bs_x, y=bs_ys[0]) add_marking(curve) if "fill" in di: (x1, ys1), (x2, ys2) = di["fill"] phigh = pg.PlotCurveItem(x1, ys1[0], pen=None) plow = pg.PlotCurveItem(x2, ys2[0], pen=None) color = QColor(color) color.setAlphaF(0.5) cc = pg.mkBrush(color) pfill = pg.FillBetweenItem(plow, phigh, brush=cc) pfill.setZValue(9) add_marking(pfill) if "line" in di: (x1, y1), (x2, y2) = di["line"] line = pg.PlotCurveItem() line.setPen(pg.mkPen(color=QColor(color), width=4)) line.setZValue(10) line.setData(x=[x1[0], x2[0]], y=[y1[0], y2[0]]) add_marking(line) def update_view(self): self.img.clear() self.img.setSelection(None) self.lsx = None self.lsy = None self.selection_matrix = None self.selection_indices = None if self.data and self.attr_x and self.attr_y: xat = self.data.domain[self.attr_x] yat = self.data.domain[self.attr_y] ndom = Orange.data.Domain([xat, yat]) datam = Orange.data.Table(ndom, self.data) coorx = datam.X[:, 0] coory = datam.X[:, 1] self.lsx = lsx = values_to_linspace(coorx) self.lsy = lsy = values_to_linspace(coory) if lsx[-1] * lsy[-1] > IMAGE_TOO_BIG: self.parent.Error.image_too_big(lsx[-1], lsy[-1]) return else: self.parent.Error.image_too_big.clear() di = {} if self.parent.value_type == 0: # integrals imethod = self.parent.integration_methods[ self.parent.integration_method] l1, l2, l3 = self.parent.lowlim, self.parent.highlim, self.parent.choose gx = getx(self.data) if l1 is None: l1 = min(gx) - 1 if l2 is None: l2 = max(gx) + 1 l1, l2 = min(l1, l2), max(l1, l2) if l3 is None: l3 = (l1 + l2) / 2 if imethod != Integrate.PeakAt: datai = Integrate(method=imethod, limits=[[l1, l2]])(self.data) else: datai = Integrate(method=imethod, limits=[[l3, l3]])(self.data) if self.parent.curveplot.selected_indices: # curveplot can have a subset of curves on the input> match IDs ind = list(self.parent.curveplot.selected_indices)[0] dind = self.data_ids[self.parent.curveplot.data[ind].id] di = datai.domain.attributes[0].compute_value.draw_info( self.data[dind:dind + 1]) d = datai.X[:, 0] else: dat = self.data.domain[self.parent.attr_value] ndom = Orange.data.Domain([dat]) d = Orange.data.Table(ndom, self.data).X[:, 0] self.refresh_markings(di) # set data imdata = np.ones((lsy[2], lsx[2])) * float("nan") self.selection_indices = np.ones((lsy[2], lsx[2]), dtype=int) * -1 self.selection_matrix = np.zeros((lsy[2], lsx[2]), dtype=bool) xindex = index_values(coorx, lsx) yindex = index_values(coory, lsy) imdata[yindex, xindex] = d self.selection_indices[yindex, xindex] = np.arange(0, len(d), dtype=int) levels = get_levels(imdata) self.update_color_schema() self.img.setImage(imdata, levels=levels) # shift centres of the pixels so that the axes are useful shiftx = _shift(lsx) shifty = _shift(lsy) left = lsx[0] - shiftx bottom = lsy[0] - shifty width = (lsx[1] - lsx[0]) + 2 * shiftx height = (lsy[1] - lsy[0]) + 2 * shifty self.img.setRect(QRectF(left, bottom, width, height)) def make_selection(self, selected, add): """Add selected indices to the selection.""" if self.data and self.selection_matrix is not None: if selected is None and not add: self.selection_matrix[:, :] = 0 elif selected is not None: if add: self.selection_matrix = np.logical_or( self.selection_matrix, selected) else: self.selection_matrix = selected self.img.setSelection(self.selection_matrix) self.send_selection() def send_selection(self): if self.data and self.selection_matrix is not None: selected = self.selection_indices[np.where(self.selection_matrix)] selected = selected[selected >= 0] # filter undefined values selected.sort() else: selected = [] if self.select_fn: self.select_fn(selected) def select_square(self, p1, p2, add): """ Select elements within a square drawn by the user. A selection square needs to contain whole pixels """ if self.data and self.lsx and self.lsy: # get edges x1, x2 = min(p1.x(), p2.x()), max(p1.x(), p2.x()) y1, y2 = min(p1.y(), p2.y()), max(p1.y(), p2.y()) # here we change edges of the square so that next # pixel centers need to be in the square x1, x2, y1, y2 shiftx = _shift(self.lsx) shifty = _shift(self.lsy) x1 += shiftx x2 -= shiftx y1 += shifty y2 -= shifty # get locations in image pixels x1 = location_values(x1, self.lsx) x2 = location_values(x2, self.lsx) y1 = location_values(y1, self.lsy) y2 = location_values(y2, self.lsy) # pixel centre need to within the square to be selected x1, x2 = np.ceil(x1).astype(int), np.floor(x2).astype(int) y1, y2 = np.ceil(y1).astype(int), np.floor(y2).astype(int) # select a range x1 = max(x1, 0) y1 = max(y1, 0) x2 = max(x2 + 1, 0) y2 = max(y2 + 1, 0) select_data = np.zeros((self.lsy[2], self.lsx[2]), dtype=bool) select_data[y1:y2, x1:x2] = 1 self.make_selection(select_data, add) def select_by_click(self, pos, add): if self.data: x, y = pos.x(), pos.y() x = location_values(x, self.lsy) y = location_values(y, self.lsy) x = np.round(x).astype(int) y = np.round(y).astype(int) if 0 <= x < self.lsx[2] and 0 <= y < self.lsy[2]: select_data = np.zeros((self.lsy[2], self.lsx[2]), dtype=bool) select_data[y, x] = 1 self.make_selection(select_data, add) else: self.make_selection(None, add)
class ImagePlot(QWidget, OWComponent, SelectionGroupMixin, ImageColorSettingMixin, ImageZoomMixin): attr_x = ContextSetting(None) attr_y = ContextSetting(None) gamma = Setting(0) selection_changed = Signal() def __init__(self, parent): QWidget.__init__(self) OWComponent.__init__(self, parent) SelectionGroupMixin.__init__(self) ImageColorSettingMixin.__init__(self) ImageZoomMixin.__init__(self) self.parent = parent self.selection_type = SELECTMANY self.saving_enabled = True self.selection_enabled = True self.viewtype = INDIVIDUAL # required bt InteractiveViewBox self.highlighted = None self.data_points = None self.data_values = None self.data_imagepixels = None self.plotview = pg.PlotWidget(background="w", viewBox=InteractiveViewBox(self)) self.plot = self.plotview.getPlotItem() self.plot.scene().installEventFilter( HelpEventDelegate(self.help_event, self)) layout = QVBoxLayout() self.setLayout(layout) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self.plotview) self.img = ImageItemNan() self.img.setOpts(axisOrder='row-major') self.plot.addItem(self.img) self.plot.vb.setAspectLocked() self.plot.scene().sigMouseMoved.connect(self.plot.vb.mouseMovedEvent) layout = QGridLayout() self.plotview.setLayout(layout) self.button = QPushButton("Menu", self.plotview) self.button.setAutoDefault(False) layout.setRowStretch(1, 1) layout.setColumnStretch(1, 1) layout.addWidget(self.button, 0, 0) view_menu = MenuFocus(self) self.button.setMenu(view_menu) # prepare interface according to the new context self.parent.contextAboutToBeOpened.connect(lambda x: self.init_interface_data(x[0])) actions = [] self.add_zoom_actions(view_menu) select_square = QAction( "Select (square)", self, triggered=self.plot.vb.set_mode_select_square, ) select_square.setShortcuts([Qt.Key_S]) select_square.setShortcutContext(Qt.WidgetWithChildrenShortcut) actions.append(select_square) select_polygon = QAction( "Select (polygon)", self, triggered=self.plot.vb.set_mode_select_polygon, ) select_polygon.setShortcuts([Qt.Key_P]) select_polygon.setShortcutContext(Qt.WidgetWithChildrenShortcut) actions.append(select_polygon) if self.saving_enabled: save_graph = QAction( "Save graph", self, triggered=self.save_graph, ) save_graph.setShortcuts([QKeySequence(Qt.ControlModifier | Qt.Key_I)]) actions.append(save_graph) view_menu.addActions(actions) self.addActions(actions) common_options = dict( labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str) choose_xy = QWidgetAction(self) box = gui.vBox(self) box.setFocusPolicy(Qt.TabFocus) self.xy_model = DomainModel(DomainModel.METAS | DomainModel.CLASSES, valid_types=DomainModel.PRIMITIVE) self.cb_attr_x = gui.comboBox( box, self, "attr_x", label="Axis x:", callback=self.update_attr, model=self.xy_model, **common_options) self.cb_attr_y = gui.comboBox( box, self, "attr_y", label="Axis y:", callback=self.update_attr, model=self.xy_model, **common_options) box.setFocusProxy(self.cb_attr_x) box.layout().addWidget(self.color_settings_box()) choose_xy.setDefaultWidget(box) view_menu.addAction(choose_xy) self.markings_integral = [] self.lsx = None # info about the X axis self.lsy = None # info about the Y axis self.data = None self.data_ids = {} def init_interface_data(self, data): same_domain = (self.data and data and data.domain == self.data.domain) if not same_domain: self.init_attr_values(data) def help_event(self, ev): pos = self.plot.vb.mapSceneToView(ev.scenePos()) sel = self._points_at_pos(pos) prepared = [] if sel is not None: data, vals, points = self.data[sel], self.data_values[sel], self.data_points[sel] for d, v, p in zip(data, vals, points): basic = "({}, {}): {}".format(p[0], p[1], v) variables = [v for v in self.data.domain.metas + self.data.domain.class_vars if v not in [self.attr_x, self.attr_y]] features = ['{} = {}'.format(attr.name, d[attr]) for attr in variables] prepared.append("\n".join([basic] + features)) text = "\n\n".join(prepared) if text: text = ('<span style="white-space:pre">{}</span>' .format(escape(text))) QToolTip.showText(ev.screenPos(), text, widget=self.plotview) return True else: return False def update_attr(self): self.update_view() def init_attr_values(self, data): domain = data.domain if data is not None else None self.xy_model.set_domain(domain) self.attr_x = self.xy_model[0] if self.xy_model else None self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \ else self.attr_x def save_graph(self): saveplot.save_plot(self.plotview, self.parent.graph_writers) def set_data(self, data): if data: self.data = data self.data_ids = {e: i for i, e in enumerate(data.ids)} self.restore_selection_settings() else: self.data = None self.data_ids = {} def refresh_markings(self, di): refresh_integral_markings([{"draw": di}], self.markings_integral, self.parent.curveplot) def update_view(self): self.img.clear() self.img.setSelection(None) self.lsx = None self.lsy = None self.data_points = None self.data_values = None self.data_imagepixels = None if self.data and self.attr_x and self.attr_y: xat = self.data.domain[self.attr_x] yat = self.data.domain[self.attr_y] ndom = Orange.data.Domain([xat, yat]) datam = Orange.data.Table(ndom, self.data) coorx = datam.X[:, 0] coory = datam.X[:, 1] self.data_points = datam.X self.lsx = lsx = values_to_linspace(coorx) self.lsy = lsy = values_to_linspace(coory) if lsx[-1] * lsy[-1] > IMAGE_TOO_BIG: self.parent.Error.image_too_big(lsx[-1], lsy[-1]) return else: self.parent.Error.image_too_big.clear() di = {} if self.parent.value_type == 0: # integrals imethod = self.parent.integration_methods[self.parent.integration_method] if imethod != Integrate.PeakAt: datai = Integrate(methods=imethod, limits=[[self.parent.lowlim, self.parent.highlim]])(self.data) else: datai = Integrate(methods=imethod, limits=[[self.parent.choose, self.parent.choose]])(self.data) if np.any(self.parent.curveplot.selection_group): # curveplot can have a subset of curves on the input> match IDs ind = np.flatnonzero(self.parent.curveplot.selection_group)[0] dind = self.data_ids[self.parent.curveplot.data[ind].id] di = datai.domain.attributes[0].compute_value.draw_info(self.data[dind:dind+1]) d = datai.X[:, 0] else: dat = self.data.domain[self.parent.attr_value] ndom = Orange.data.Domain([dat]) d = Orange.data.Table(ndom, self.data).X[:, 0] self.refresh_markings(di) # set data imdata = np.ones((lsy[2], lsx[2])) * float("nan") xindex = index_values(coorx, lsx) yindex = index_values(coory, lsy) imdata[yindex, xindex] = d self.data_values = d self.data_imagepixels = np.vstack((yindex, xindex)).T self.img.setImage(imdata, autoLevels=False) self.img.setLevels([0, 1]) self.update_levels() self.update_color_schema() # shift centres of the pixels so that the axes are useful shiftx = _shift(lsx) shifty = _shift(lsy) left = lsx[0] - shiftx bottom = lsy[0] - shifty width = (lsx[1]-lsx[0]) + 2*shiftx height = (lsy[1]-lsy[0]) + 2*shifty self.img.setRect(QRectF(left, bottom, width, height)) self.refresh_img_selection() def refresh_img_selection(self): selected_px = np.zeros((self.lsy[2], self.lsx[2]), dtype=np.uint8) selected_px[self.data_imagepixels[:, 0], self.data_imagepixels[:, 1]] = self.selection_group self.img.setSelection(selected_px) def make_selection(self, selected, add): """Add selected indices to the selection.""" add_to_group, add_group, remove = selection_modifiers() if self.data and self.lsx and self.lsy: if add_to_group: # both keys - need to test it before add_group selnum = np.max(self.selection_group) elif add_group: selnum = np.max(self.selection_group) + 1 elif remove: selnum = 0 else: self.selection_group *= 0 selnum = 1 if selected is not None: self.selection_group[selected] = selnum self.refresh_img_selection() self.prepare_settings_for_saving() self.selection_changed.emit() def select_square(self, p1, p2, add): """ Select elements within a square drawn by the user. A selection needs to contain whole pixels """ x1, y1 = p1.x(), p1.y() x2, y2 = p2.x(), p2.y() polygon = [QPointF(x1, y1), QPointF(x2, y1), QPointF(x2, y2), QPointF(x1, y2), QPointF(x1, y1)] self.select_polygon(polygon, add) def select_polygon(self, polygon, add): """ Select by a polygon which has to contain whole pixels. """ if self.data and self.lsx and self.lsy: polygon = [(p.x(), p.y()) for p in polygon] # a polygon should contain all pixel shiftx = _shift(self.lsx) shifty = _shift(self.lsy) points_edges = [self.data_points + [[shiftx, shifty]], self.data_points + [[-shiftx, shifty]], self.data_points + [[shiftx, -shifty]], self.data_points + [[-shiftx, -shifty]]] inp = in_polygon(points_edges[0], polygon) for p in points_edges[1:]: inp *= in_polygon(p, polygon) self.make_selection(inp, add) def _points_at_pos(self, pos): if self.data and self.lsx and self.lsy: x, y = pos.x(), pos.y() distance = np.abs(self.data_points - [[x, y]]) sel = (distance[:, 0] < _shift(self.lsx)) * (distance[:, 1] < _shift(self.lsy)) return sel def select_by_click(self, pos, add): sel = self._points_at_pos(pos) self.make_selection(sel, add)
class OWFeatureConstructor(OWWidget): name = "特征构造器(Feature Constructor)" description = "用输入数据集中的现有特征构造新特征。" icon = "icons/FeatureConstructor.svg" keywords = ['tezheng', 'gouzao', 'tezhenggouzao'] category = "数据(Data)" icon = "icons/FeatureConstructor.svg" class Inputs: data = Input("数据(Data)", Orange.data.Table, replaces=['Data']) class Outputs: data = Output("数据(Data)", Orange.data.Table, replaces=['Data']) want_main_area = False settingsHandler = FeatureConstructorHandler() descriptors = ContextSetting([]) currentIndex = ContextSetting(-1) expressions_with_values = ContextSetting(False) settings_version = 2 EDITORS = [(ContinuousDescriptor, ContinuousFeatureEditor), (DateTimeDescriptor, DateTimeFeatureEditor), (DiscreteDescriptor, DiscreteFeatureEditor), (StringDescriptor, StringFeatureEditor)] class Error(OWWidget.Error): more_values_needed = Msg("Categorical feature {} needs more values.") invalid_expressions = Msg("Invalid expressions: {}.") class Warning(OWWidget.Warning): renamed_var = Msg("Recently added variable has been renamed, " "to avoid duplicates.\n") def __init__(self): super().__init__() self.data = None self.editors = {} box = gui.vBox(self.controlArea, "变量定义") toplayout = QHBoxLayout() toplayout.setContentsMargins(0, 0, 0, 0) box.layout().addLayout(toplayout) self.editorstack = QStackedWidget(sizePolicy=QSizePolicy( QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) for descclass, editorclass in self.EDITORS: editor = editorclass() editor.featureChanged.connect(self._on_modified) self.editors[descclass] = editor self.editorstack.addWidget(editor) self.editorstack.setEnabled(False) buttonlayout = QVBoxLayout(spacing=10) buttonlayout.setContentsMargins(0, 0, 0, 0) self.addbutton = QPushButton("新建", toolTip="Create a new variable", minimumWidth=120, shortcut=QKeySequence.New) def unique_name(fmt, reserved): candidates = (fmt.format(i) for i in count(1)) return next(c for c in candidates if c not in reserved) def generate_newname(fmt): return unique_name(fmt, self.reserved_names()) menu = QMenu(self.addbutton) cont = menu.addAction("数值数据") cont.triggered.connect(lambda: self.addFeature( ContinuousDescriptor(generate_newname("X{}"), "", 3))) disc = menu.addAction("分类数据") disc.triggered.connect(lambda: self.addFeature( DiscreteDescriptor(generate_newname("D{}"), "", (), False))) string = menu.addAction("文本") string.triggered.connect(lambda: self.addFeature( StringDescriptor(generate_newname("S{}"), ""))) datetime = menu.addAction("日期/时间") datetime.triggered.connect(lambda: self.addFeature( DateTimeDescriptor(generate_newname("T{}"), ""))) menu.addSeparator() self.duplicateaction = menu.addAction("复制选中变量") self.duplicateaction.triggered.connect(self.duplicateFeature) self.duplicateaction.setEnabled(False) self.addbutton.setMenu(menu) self.removebutton = QPushButton("删除", toolTip="删除选中变量", minimumWidth=120, shortcut=QKeySequence.Delete) self.removebutton.clicked.connect(self.removeSelectedFeature) buttonlayout.addWidget(self.addbutton) buttonlayout.addWidget(self.removebutton) buttonlayout.addStretch(10) toplayout.addLayout(buttonlayout, 0) toplayout.addWidget(self.editorstack, 10) # Layout for the list view layout = QVBoxLayout(spacing=1, margin=0) self.featuremodel = DescriptorModel(parent=self) self.featureview = QListView(minimumWidth=200, minimumHeight=50, sizePolicy=QSizePolicy( QSizePolicy.Minimum, QSizePolicy.MinimumExpanding)) self.featureview.setItemDelegate(FeatureItemDelegate(self)) self.featureview.setModel(self.featuremodel) self.featureview.selectionModel().selectionChanged.connect( self._on_selectedVariableChanged) layout.addWidget(self.featureview) box.layout().addLayout(layout, 1) self.fix_button = gui.button(self.buttonsArea, self, "Upgrade Expressions", callback=self.fix_expressions) self.fix_button.setHidden(True) gui.button(self.buttonsArea, self, "Send", callback=self.apply, default=True) def setCurrentIndex(self, index): index = min(index, len(self.featuremodel) - 1) self.currentIndex = index if index >= 0: itemmodels.select_row(self.featureview, index) desc = self.featuremodel[min(index, len(self.featuremodel) - 1)] editor = self.editors[type(desc)] self.editorstack.setCurrentWidget(editor) editor.setEditorData(desc, self.data.domain if self.data else None) self.editorstack.setEnabled(index >= 0) self.duplicateaction.setEnabled(index >= 0) self.removebutton.setEnabled(index >= 0) def _on_selectedVariableChanged(self, selected, *_): index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) else: self.setCurrentIndex(-1) def _on_modified(self): if self.currentIndex >= 0: self.Warning.clear() editor = self.editorstack.currentWidget() proposed = editor.editorData().name uniq = get_unique_names(self.reserved_names(self.currentIndex), proposed) feature = editor.editorData() if editor.editorData().name != uniq: self.Warning.renamed_var() feature = feature.__class__(uniq, *feature[1:]) self.featuremodel[self.currentIndex] = feature self.descriptors = list(self.featuremodel) def setDescriptors(self, descriptors): """ Set a list of variable descriptors to edit. """ self.descriptors = descriptors self.featuremodel[:] = list(self.descriptors) def reserved_names(self, idx_=None): varnames = [] if self.data is not None: varnames = [ var.name for var in self.data.domain.variables + self.data.domain.metas ] varnames += [ desc.name for idx, desc in enumerate(self.featuremodel) if idx != idx_ ] return set(varnames) @Inputs.data @check_sql_input def setData(self, data=None): """Set the input dataset.""" self.closeContext() self.data = data self.expressions_with_values = False self.descriptors = [] self.currentIndex = -1 if self.data is not None: self.openContext(data) # disconnect from the selection model while reseting the model selmodel = self.featureview.selectionModel() selmodel.selectionChanged.disconnect(self._on_selectedVariableChanged) self.featuremodel[:] = list(self.descriptors) self.setCurrentIndex(self.currentIndex) selmodel.selectionChanged.connect(self._on_selectedVariableChanged) self.fix_button.setHidden(not self.expressions_with_values) self.editorstack.setEnabled(self.currentIndex >= 0) def handleNewSignals(self): if self.data is not None: self.apply() else: self.Outputs.data.send(None) self.fix_button.setHidden(True) def addFeature(self, descriptor): self.featuremodel.append(descriptor) self.setCurrentIndex(len(self.featuremodel) - 1) editor = self.editorstack.currentWidget() editor.nameedit.setFocus() editor.nameedit.selectAll() def removeFeature(self, index): del self.featuremodel[index] index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) elif index is None and self.featuremodel.rowCount(): # Deleting the last item clears selection self.setCurrentIndex(self.featuremodel.rowCount() - 1) def removeSelectedFeature(self): if self.currentIndex >= 0: self.removeFeature(self.currentIndex) def duplicateFeature(self): desc = self.featuremodel[self.currentIndex] self.addFeature(copy.deepcopy(desc)) @staticmethod def check_attrs_values(attr, data): for i in range(len(data)): for var in attr: if not math.isnan(data[i, var]) \ and int(data[i, var]) >= len(var.values): return var.name return None def _validate_descriptors(self, desc): def validate(source): try: return validate_exp(ast.parse(source, mode="eval")) # ast.parse can return arbitrary errors, not only SyntaxError # pylint: disable=broad-except except Exception: return False final = [] invalid = [] for d in desc: if validate(d.expression): final.append(d) else: final.append(d._replace(expression="")) invalid.append(d) if invalid: self.Error.invalid_expressions(", ".join(s.name for s in invalid)) return final def apply(self): def report_error(err): log = logging.getLogger(__name__) log.error("", exc_info=True) self.error("".join(format_exception_only(type(err), err)).rstrip()) self.Error.clear() if self.data is None: return desc = list(self.featuremodel) desc = self._validate_descriptors(desc) try: new_variables = construct_variables(desc, self.data, self.expressions_with_values) # user's expression can contain arbitrary errors except Exception as err: # pylint: disable=broad-except report_error(err) return attrs = [var for var in new_variables if var.is_primitive()] metas = [var for var in new_variables if not var.is_primitive()] new_domain = Orange.data.Domain( self.data.domain.attributes + tuple(attrs), self.data.domain.class_vars, metas=self.data.domain.metas + tuple(metas)) try: for variable in new_variables: variable.compute_value.mask_exceptions = False data = self.data.transform(new_domain) # user's expression can contain arbitrary errors # pylint: disable=broad-except except Exception as err: report_error(err) return finally: for variable in new_variables: variable.compute_value.mask_exceptions = True disc_attrs_not_ok = self.check_attrs_values( [var for var in attrs if var.is_discrete], data) if disc_attrs_not_ok: self.Error.more_values_needed(disc_attrs_not_ok) return self.Outputs.data.send(data) def send_report(self): items = OrderedDict() for feature in self.featuremodel: if isinstance(feature, DiscreteDescriptor): desc = "categorical" if feature.values: desc += " with values " \ + ", ".join(f"'{val}'" for val in feature.values) if feature.ordered: desc += "; ordered" elif isinstance(feature, ContinuousDescriptor): desc = "numeric" elif isinstance(feature, DateTimeDescriptor): desc = "date/time" else: desc = "text" items[feature.name] = f"{feature.expression} ({desc})" self.report_items(report.plural("Constructed feature{s}", len(items)), items) def fix_expressions(self): dlg = QMessageBox( QMessageBox.Question, "Fix Expressions", "This widget's behaviour has changed. Values of categorical " "variables are now inserted as their textual representations " "(strings); previously they appeared as integer numbers, with an " "attribute '.value' that contained the text.\n\n" "The widget currently runs in compatibility mode. After " "expressions are updated, manually check for their correctness.") dlg.addButton("Update", QMessageBox.ApplyRole) dlg.addButton("Cancel", QMessageBox.RejectRole) if dlg.exec() == QMessageBox.RejectRole: return def fixer(mo): var = domain[mo.group(2)] if mo.group(3) == ".value": # uses string; remove `.value` return "".join(mo.group(1, 2, 4)) # Uses ints: get them by indexing return mo.group(1) + "{" + \ ", ".join(f"'{val}': {i}" for i, val in enumerate(var.values)) + \ f"}}[{var.name}]" + mo.group(4) domain = self.data.domain disc_vars = "|".join(f"{var.name}" for var in chain(domain.variables, domain.metas) if var.is_discrete) expr = re.compile(r"(^|\W)(" + disc_vars + r")(\.value)?(\W|$)") self.descriptors[:] = [ descriptor._replace( expression=expr.sub(fixer, descriptor.expression)) for descriptor in self.descriptors ] self.expressions_with_values = False self.fix_button.hide() index = self.currentIndex self.featuremodel[:] = list(self.descriptors) self.setCurrentIndex(index) self.apply() @classmethod def migrate_context(cls, context, version): if version is None or version < 2: used_vars = set( chain(*( freevars(ast.parse(descriptor.expression, mode="eval"), []) for descriptor in context.values["descriptors"] if descriptor.expression))) disc_vars = { name for (name, vtype) in chain(context.attributes.items(), context.metas.items()) if vtype == 1 } if used_vars & disc_vars: context.values["expressions_with_values"] = True
class OWFeatureConstructor(OWWidget): name = "Feature Constructor" description = "Construct new features (data columns) from a set of " \ "existing features in the input dataset." icon = "icons/FeatureConstructor.svg" keywords = ['function', 'lambda'] class Inputs: data = Input("Data", Orange.data.Table) class Outputs: data = Output("Data", Orange.data.Table) want_main_area = False settingsHandler = FeatureConstructorHandler() descriptors = ContextSetting([]) currentIndex = ContextSetting(-1) EDITORS = [(ContinuousDescriptor, ContinuousFeatureEditor), (DateTimeDescriptor, DateTimeFeatureEditor), (DiscreteDescriptor, DiscreteFeatureEditor), (StringDescriptor, StringFeatureEditor)] class Error(OWWidget.Error): more_values_needed = Msg("Categorical feature {} needs more values.") invalid_expressions = Msg("Invalid expressions: {}.") class Warning(OWWidget.Warning): renamed_var = Msg("Recently added variable has been renamed, " "to avoid duplicates.\n") def __init__(self): super().__init__() self.data = None self.editors = {} box = gui.vBox(self.controlArea, "Variable Definitions") toplayout = QHBoxLayout() toplayout.setContentsMargins(0, 0, 0, 0) box.layout().addLayout(toplayout) self.editorstack = QStackedWidget(sizePolicy=QSizePolicy( QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)) for descclass, editorclass in self.EDITORS: editor = editorclass() editor.featureChanged.connect(self._on_modified) self.editors[descclass] = editor self.editorstack.addWidget(editor) self.editorstack.setEnabled(False) buttonlayout = QVBoxLayout(spacing=10) buttonlayout.setContentsMargins(0, 0, 0, 0) self.addbutton = QPushButton("New", toolTip="Create a new variable", minimumWidth=120, shortcut=QKeySequence.New) def unique_name(fmt, reserved): candidates = (fmt.format(i) for i in count(1)) return next(c for c in candidates if c not in reserved) def generate_newname(fmt): return unique_name(fmt, self.reserved_names()) menu = QMenu(self.addbutton) cont = menu.addAction("Numeric") cont.triggered.connect(lambda: self.addFeature( ContinuousDescriptor(generate_newname("X{}"), "", 3))) disc = menu.addAction("Categorical") disc.triggered.connect(lambda: self.addFeature( DiscreteDescriptor(generate_newname("D{}"), "", (), False))) string = menu.addAction("Text") string.triggered.connect(lambda: self.addFeature( StringDescriptor(generate_newname("S{}"), ""))) datetime = menu.addAction("Date/Time") datetime.triggered.connect(lambda: self.addFeature( DateTimeDescriptor(generate_newname("T{}"), ""))) menu.addSeparator() self.duplicateaction = menu.addAction("Duplicate Selected Variable") self.duplicateaction.triggered.connect(self.duplicateFeature) self.duplicateaction.setEnabled(False) self.addbutton.setMenu(menu) self.removebutton = QPushButton("Remove", toolTip="Remove selected variable", minimumWidth=120, shortcut=QKeySequence.Delete) self.removebutton.clicked.connect(self.removeSelectedFeature) buttonlayout.addWidget(self.addbutton) buttonlayout.addWidget(self.removebutton) buttonlayout.addStretch(10) toplayout.addLayout(buttonlayout, 0) toplayout.addWidget(self.editorstack, 10) # Layout for the list view layout = QVBoxLayout(spacing=1, margin=0) self.featuremodel = DescriptorModel(parent=self) self.featureview = QListView(minimumWidth=200, minimumHeight=50, sizePolicy=QSizePolicy( QSizePolicy.Minimum, QSizePolicy.MinimumExpanding)) self.featureview.setItemDelegate(FeatureItemDelegate(self)) self.featureview.setModel(self.featuremodel) self.featureview.selectionModel().selectionChanged.connect( self._on_selectedVariableChanged) self.info.set_input_summary(self.info.NoInput) self.info.set_output_summary(self.info.NoOutput) layout.addWidget(self.featureview) box.layout().addLayout(layout, 1) gui.button(self.buttonsArea, self, "Send", callback=self.apply, default=True) def setCurrentIndex(self, index): index = min(index, len(self.featuremodel) - 1) self.currentIndex = index if index >= 0: itemmodels.select_row(self.featureview, index) desc = self.featuremodel[min(index, len(self.featuremodel) - 1)] editor = self.editors[type(desc)] self.editorstack.setCurrentWidget(editor) editor.setEditorData(desc, self.data.domain if self.data else None) self.editorstack.setEnabled(index >= 0) self.duplicateaction.setEnabled(index >= 0) self.removebutton.setEnabled(index >= 0) def _on_selectedVariableChanged(self, selected, *_): index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) else: self.setCurrentIndex(-1) def _on_modified(self): if self.currentIndex >= 0: self.Warning.clear() editor = self.editorstack.currentWidget() proposed = editor.editorData().name unique = get_unique_names(self.reserved_names(self.currentIndex), proposed) feature = editor.editorData() if editor.editorData().name != unique: self.Warning.renamed_var() feature = feature.__class__(unique, *feature[1:]) self.featuremodel[self.currentIndex] = feature self.descriptors = list(self.featuremodel) def setDescriptors(self, descriptors): """ Set a list of variable descriptors to edit. """ self.descriptors = descriptors self.featuremodel[:] = list(self.descriptors) def reserved_names(self, idx_=None): varnames = [] if self.data is not None: varnames = [ var.name for var in self.data.domain.variables + self.data.domain.metas ] varnames += [ desc.name for idx, desc in enumerate(self.featuremodel) if idx != idx_ ] return set(varnames) @Inputs.data @check_sql_input def setData(self, data=None): """Set the input dataset.""" self.closeContext() self.data = data self.info.set_input_summary(self.info.NoInput) if self.data is not None: descriptors = list(self.descriptors) currindex = self.currentIndex self.descriptors = [] self.currentIndex = -1 self.openContext(data) self.info.set_input_summary(len(data), format_summary_details(data)) if descriptors != self.descriptors or \ self.currentIndex != currindex: # disconnect from the selection model while reseting the model selmodel = self.featureview.selectionModel() selmodel.selectionChanged.disconnect( self._on_selectedVariableChanged) self.featuremodel[:] = list(self.descriptors) self.setCurrentIndex(self.currentIndex) selmodel.selectionChanged.connect( self._on_selectedVariableChanged) self.editorstack.setEnabled(self.currentIndex >= 0) def handleNewSignals(self): if self.data is not None: self.apply() else: self.info.set_output_summary(self.info.NoOutput) self.Outputs.data.send(None) def addFeature(self, descriptor): self.featuremodel.append(descriptor) self.setCurrentIndex(len(self.featuremodel) - 1) editor = self.editorstack.currentWidget() editor.nameedit.setFocus() editor.nameedit.selectAll() def removeFeature(self, index): del self.featuremodel[index] index = selected_row(self.featureview) if index is not None: self.setCurrentIndex(index) elif index is None and self.featuremodel.rowCount(): # Deleting the last item clears selection self.setCurrentIndex(self.featuremodel.rowCount() - 1) def removeSelectedFeature(self): if self.currentIndex >= 0: self.removeFeature(self.currentIndex) def duplicateFeature(self): desc = self.featuremodel[self.currentIndex] self.addFeature(copy.deepcopy(desc)) @staticmethod def check_attrs_values(attr, data): for i in range(len(data)): for var in attr: if not math.isnan(data[i, var]) \ and int(data[i, var]) >= len(var.values): return var.name return None def _validate_descriptors(self, desc): def validate(source): try: return validate_exp(ast.parse(source, mode="eval")) # ast.parse can return arbitrary errors, not only SyntaxError # pylint: disable=broad-except except Exception: return False final = [] invalid = [] for d in desc: if validate(d.expression): final.append(d) else: final.append(d._replace(expression="")) invalid.append(d) if invalid: self.Error.invalid_expressions(", ".join(s.name for s in invalid)) return final def apply(self): def report_error(err): log = logging.getLogger(__name__) log.error("", exc_info=True) self.error("".join(format_exception_only(type(err), err)).rstrip()) self.Error.clear() if self.data is None: return desc = list(self.featuremodel) desc = self._validate_descriptors(desc) try: new_variables = construct_variables(desc, self.data) # user's expression can contain arbitrary errors except Exception as err: # pylint: disable=broad-except report_error(err) return attrs = [var for var in new_variables if var.is_primitive()] metas = [var for var in new_variables if not var.is_primitive()] new_domain = Orange.data.Domain( self.data.domain.attributes + tuple(attrs), self.data.domain.class_vars, metas=self.data.domain.metas + tuple(metas)) try: for variable in new_variables: variable.compute_value.mask_exceptions = False data = self.data.transform(new_domain) # user's expression can contain arbitrary errors # pylint: disable=broad-except except Exception as err: report_error(err) return finally: for variable in new_variables: variable.compute_value.mask_exceptions = True disc_attrs_not_ok = self.check_attrs_values( [var for var in attrs if var.is_discrete], data) if disc_attrs_not_ok: self.Error.more_values_needed(disc_attrs_not_ok) return self.info.set_output_summary(len(data), format_summary_details(data)) self.Outputs.data.send(data) def send_report(self): items = OrderedDict() for feature in self.featuremodel: if isinstance(feature, DiscreteDescriptor): items[ feature.name] = "{} (categorical with values {}{})".format( feature.expression, feature.values, "; ordered" * feature.ordered) elif isinstance(feature, ContinuousDescriptor): items[feature.name] = "{} (numeric)".format(feature.expression) elif isinstance(feature, DateTimeDescriptor): items[feature.name] = "{} (date/time)".format( feature.expression) else: items[feature.name] = "{} (text)".format(feature.expression) self.report_items(report.plural("Constructed feature{s}", len(items)), items)