def __init__(self): super().__init__() self.data = None self.component_x = 0 self.component_y = 1 box = gui.vBox(self.controlArea, "变量") self.varlist = itemmodels.VariableListModel() self.varview = view = ListViewSearch( selectionMode=QListView.MultiSelection, uniformItemSizes=True) view.setModel(self.varlist) view.selectionModel().selectionChanged.connect(self._var_changed) box.layout().addWidget(view) axes_box = gui.vBox(self.controlArea, "轴") self.axis_x_cb = gui.comboBox(axes_box, self, "component_x", label="X:", callback=self._component_changed, orientation=Qt.Horizontal, sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)) self.axis_y_cb = gui.comboBox(axes_box, self, "component_y", label="Y:", callback=self._component_changed, orientation=Qt.Horizontal, sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)) self.infotext = gui.widgetLabel( gui.vBox(self.controlArea, "对惯性的贡献(Contribution to Inertia)"), "\n") gui.auto_send(self.buttonsArea, self, "auto_commit") self.plot = PlotWidget() self.plot.setMenuEnabled(False) self.mainArea.layout().addWidget(self.plot)
def _init_ui(self): namesBox = gui.vBox(self.controlArea, "Names") hbox = gui.hBox(namesBox, margin=0, spacing=0) gui.lineEdit(hbox, self, "attr1", "Variable X: ", controlWidth=80, orientation=Qt.Horizontal, callback=self._attr_name_changed) gui.separator(hbox, 21) hbox = gui.hBox(namesBox, margin=0, spacing=0) attr2 = gui.lineEdit(hbox, self, "attr2", "Variable Y: ", controlWidth=80, orientation=Qt.Horizontal, callback=self._attr_name_changed) gui.separator(hbox) gui.checkBox(hbox, self, "hasAttr2", '', disables=attr2, labelWidth=0, callback=self.set_dimensions) gui.widgetLabel(namesBox, "Labels") self.classValuesView = listView = gui.ListViewWithSizeHint( preferred_size=(-1, 30)) listView.setModel(self.class_model) itemmodels.select_row(listView, 0) namesBox.layout().addWidget(listView) self.addClassLabel = QAction( "+", self, toolTip="Add new class label", triggered=self.add_new_class_label ) self.removeClassLabel = QAction( unicodedata.lookup("MINUS SIGN"), self, toolTip="Remove selected class label", triggered=self.remove_selected_class_label ) actionsWidget = itemmodels.ModelActionsWidget( [self.addClassLabel, self.removeClassLabel], self ) actionsWidget.layout().addStretch(10) actionsWidget.layout().setSpacing(1) namesBox.layout().addWidget(actionsWidget) tBox = gui.vBox(self.buttonsArea, "Tools") toolsBox = gui.widgetBox(tBox, orientation=QGridLayout()) self.toolActions = QActionGroup(self) self.toolActions.setExclusive(True) self.toolButtons = [] for i, (name, tooltip, tool, icon) in enumerate(self.TOOLS): action = QAction( name, self, toolTip=tooltip, checkable=tool.checkable, icon=QIcon(icon), ) action.triggered.connect(partial(self.set_current_tool, tool)) button = QToolButton( iconSize=QSize(24, 24), toolButtonStyle=Qt.ToolButtonTextUnderIcon, sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed) ) button.setDefaultAction(action) self.toolButtons.append((button, tool)) toolsBox.layout().addWidget(button, i // 3, i % 3) self.toolActions.addAction(action) for column in range(3): toolsBox.layout().setColumnMinimumWidth(column, 10) toolsBox.layout().setColumnStretch(column, 1) undo = self.undo_stack.createUndoAction(self) redo = self.undo_stack.createRedoAction(self) undo.setShortcut(QKeySequence.Undo) redo.setShortcut(QKeySequence.Redo) self.addActions([undo, redo]) self.undo_stack.indexChanged.connect(self.invalidate) indBox = gui.indentedBox(tBox, sep=8) form = QFormLayout( formAlignment=Qt.AlignLeft, labelAlignment=Qt.AlignLeft, fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow ) indBox.layout().addLayout(form) slider = gui.hSlider( indBox, self, "brushRadius", minValue=1, maxValue=100, createLabel=False, addToLayout=False ) form.addRow("Radius:", slider) slider = gui.hSlider( indBox, self, "density", None, minValue=1, maxValue=100, createLabel=False, addToLayout=False ) form.addRow("Intensity:", slider) slider = gui.hSlider( indBox, self, "symbol_size", None, minValue=1, maxValue=100, createLabel=False, callback=self.set_symbol_size, addToLayout=False ) form.addRow("Symbol:", slider) self.btResetToInput = gui.button( tBox, self, "Reset to Input Data", self.reset_to_input) self.btResetToInput.setDisabled(True) gui.auto_send(self.buttonsArea, self, "autocommit") # main area GUI viewbox = PaintViewBox(enableMouse=False) self.plotview = PlotWidget(viewBox=viewbox) self.plot = self.plotview.getPlotItem() axis_color = self.palette().color(QPalette.Text) axis_pen = QPen(axis_color) tickfont = QFont(self.font()) tickfont.setPixelSize(max(int(tickfont.pixelSize() * 2 // 3), 11)) axis = self.plot.getAxis("bottom") axis.setLabel(self.attr1) axis.setPen(axis_pen) axis.setTickFont(tickfont) axis = self.plot.getAxis("left") axis.setLabel(self.attr2) axis.setPen(axis_pen) axis.setTickFont(tickfont) if not self.hasAttr2: self.plot.hideAxis('left') self.plot.hideButtons() self.plot.setXRange(0, 1, padding=0.01) self.mainArea.layout().addWidget(self.plotview) # enable brush tool self.toolActions.actions()[0].setChecked(True) self.set_current_tool(self.TOOLS[0][2]) self.set_dimensions()
class OWCorrespondenceAnalysis(widget.OWWidget): name = "对应分析(Correspondence Analysis)" description = "分类多元数据的对应分析。" icon = "icons/CorrespondenceAnalysis.svg" keywords = ['duiyingfenxi'] category = '非监督(Unsupervised)' class Inputs: data = Input("数据(Data)", Table, replaces=['Data']) class Outputs: coordinates = Output("坐标(Coordinates)", Table, replaces=['Coordinates']) Invalidate = QEvent.registerEventType() settingsHandler = settings.DomainContextHandler() selected_var_indices = settings.ContextSetting([]) auto_commit = Setting(True) graph_name = "plot.plotItem" class Error(widget.OWWidget.Error): empty_data = widget.Msg("Empty dataset") no_disc_vars = widget.Msg("No categorical data") def __init__(self): super().__init__() self.data = None self.component_x = 0 self.component_y = 1 box = gui.vBox(self.controlArea, "变量") self.varlist = itemmodels.VariableListModel() self.varview = view = ListViewSearch( selectionMode=QListView.MultiSelection, uniformItemSizes=True) view.setModel(self.varlist) view.selectionModel().selectionChanged.connect(self._var_changed) box.layout().addWidget(view) axes_box = gui.vBox(self.controlArea, "轴") self.axis_x_cb = gui.comboBox(axes_box, self, "component_x", label="X:", callback=self._component_changed, orientation=Qt.Horizontal, sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)) self.axis_y_cb = gui.comboBox(axes_box, self, "component_y", label="Y:", callback=self._component_changed, orientation=Qt.Horizontal, sizePolicy=(QSizePolicy.MinimumExpanding, QSizePolicy.Preferred)) self.infotext = gui.widgetLabel( gui.vBox(self.controlArea, "对惯性的贡献(Contribution to Inertia)"), "\n") gui.auto_send(self.buttonsArea, self, "auto_commit") self.plot = PlotWidget() self.plot.setMenuEnabled(False) self.mainArea.layout().addWidget(self.plot) @Inputs.data def set_data(self, data): self.closeContext() self.clear() self.Error.clear() if data is not None and not len(data): self.Error.empty_data() data = None self.data = data if data is not None: self.varlist[:] = [ var for var in data.domain.variables if var.is_discrete ] if not len(self.varlist[:]): self.Error.no_disc_vars() self.data = None else: self.selected_var_indices = [0, 1][:len(self.varlist)] # This widget's update flow is broken in many ways, starting # from using context domain handler without having any valid # context settings. Getting rid of these warnings would require # rewriting large portins; @ales-erjavec is doing it and will # finish it eventually, so let us these warnings are # uninformative and would better be silenced. with warnings.catch_warnings(): warnings.filterwarnings("ignore", "combo box 'component_[xy]' .*", UserWarning) self.component_x = 0 self.component_y = int( len(self.varlist[self.selected_var_indices[-1]].values) > 1) self.openContext(data) self._restore_selection() self._update_CA() self.commit.now() @gui.deferred def commit(self): output_table = None if self.ca is not None: sel_vars = self.selected_vars() if len(sel_vars) == 2: rf = np.vstack((self.ca.row_factors, self.ca.col_factors)) else: rf = self.ca.row_factors vars_data = [(val.name, var) for val in sel_vars for var in val.values] output_table = Table(Domain( [ ContinuousVariable(f"Component {i + 1}") for i in range(rf.shape[1]) ], metas=[StringVariable("Variable"), StringVariable("Value")]), rf, metas=vars_data) self.Outputs.coordinates.send(output_table) def clear(self): self.data = None self.ca = None self.plot.clear() self.varlist[:] = [] def selected_vars(self): rows = sorted(ind.row() for ind in self.varview.selectionModel().selectedRows()) return [self.varlist[i] for i in rows] def _restore_selection(self): def restore(view, indices): with itemmodels.signal_blocking(view.selectionModel()): select_rows(view, indices) restore(self.varview, self.selected_var_indices) def _p_axes(self): return self.component_x, self.component_y def _var_changed(self): self.selected_var_indices = sorted( ind.row() for ind in self.varview.selectionModel().selectedRows()) rfs = self.update_XY() if rfs is not None: if self.component_x >= rfs: self.component_x = rfs - 1 if self.component_y >= rfs: self.component_y = rfs - 1 self._invalidate() def _component_changed(self): if self.ca is not None: self._setup_plot() self._update_info() def _invalidate(self): self.__invalidated = True QApplication.postEvent(self, QEvent(self.Invalidate)) def customEvent(self, event): if event.type() == self.Invalidate: self.ca = None self.plot.clear() self._update_CA() self.commit.deferred() return return super().customEvent(event) def _update_CA(self): self.update_XY() # See the comment about catch_warnings above. with warnings.catch_warnings(): warnings.filterwarnings("ignore", "combo box 'component_[xy]' .*", UserWarning) self.component_x, self.component_y = \ self.component_x, self.component_y self._setup_plot() self._update_info() def update_XY(self): self.axis_x_cb.clear() self.axis_y_cb.clear() ca_vars = self.selected_vars() if len(ca_vars) == 0: return multi = len(ca_vars) != 2 if multi: _, ctable = burt_table(self.data, ca_vars) else: ctable = contingency.get_contingency(self.data, *ca_vars[::-1]) self.ca = correspondence(ctable, ) rfs = self.ca.row_factors.shape[1] axes = ["{}".format(i + 1) for i in range(rfs)] self.axis_x_cb.addItems(axes) self.axis_y_cb.addItems(axes) return rfs def _setup_plot(self): def get_minmax(points): minmax = [float('inf'), float('-inf'), float('inf'), float('-inf')] for pp in points: for p in pp: minmax[0] = min(p[0], minmax[0]) minmax[1] = max(p[0], minmax[1]) minmax[2] = min(p[1], minmax[2]) minmax[3] = max(p[1], minmax[3]) return minmax self.plot.clear() points = self.ca variables = self.selected_vars() colors = colorpalettes.LimitedDiscretePalette(len(variables)) p_axes = self._p_axes() if points is None: return if len(variables) == 2: row_points = self.ca.row_factors[:, p_axes] col_points = self.ca.col_factors[:, p_axes] points = [row_points, col_points] else: points = self.ca.row_factors[:, p_axes] counts = [len(var.values) for var in variables] range_indices = np.cumsum([0] + counts) ranges = zip(range_indices, range_indices[1:]) points = [points[s:e] for s, e in ranges] minmax = get_minmax(points) margin = abs(minmax[0] - minmax[1]) margin = margin * 0.05 if margin > 1e-10 else 1 self.plot.setXRange(minmax[0] - margin, minmax[1] + margin) margin = abs(minmax[2] - minmax[3]) margin = margin * 0.05 if margin > 1e-10 else 1 self.plot.setYRange(minmax[2] - margin, minmax[3] + margin) foreground = self.palette().color(QPalette.Text) for i, (v, points) in enumerate(zip(variables, points)): color_outline = colors[i] color_outline.setAlpha(200) color = QColor(color_outline) color.setAlpha(120) item = ScatterPlotItem( x=points[:, 0], y=points[:, 1], brush=QBrush(color), pen=pg.mkPen(color_outline.darker(120), width=1.5), size=np.full((points.shape[0], ), 10.1), ) self.plot.addItem(item) for name, point in zip(v.values, points): item = pg.TextItem(name, anchor=(0.5, 0), color=foreground) self.plot.addItem(item) item.setPos(point[0], point[1]) inertia = self.ca.inertia_of_axis() if np.sum(inertia) == 0: inertia = 100 * inertia else: inertia = 100 * inertia / np.sum(inertia) ax = self.plot.getAxis("bottom") ax.setLabel("组分 {} ({:.1f}%)".format(p_axes[0] + 1, inertia[p_axes[0]])) ax = self.plot.getAxis("left") ax.setLabel("组分 {} ({:.1f}%)".format(p_axes[1] + 1, inertia[p_axes[1]])) def _update_info(self): if self.ca is None: self.infotext.setText("\n\n") else: fmt = ("轴线 1: {:.2f}\n" "轴线 2: {:.2f}") inertia = self.ca.inertia_of_axis() if np.sum(inertia) == 0: inertia = 100 * inertia else: inertia = 100 * inertia / np.sum(inertia) ax1, ax2 = self._p_axes() self.infotext.setText(fmt.format(inertia[ax1], inertia[ax2])) def send_report(self): if self.data is None: return vars = self.selected_vars() if not vars: return items = OrderedDict() items["Data instances"] = len(self.data) if len(vars) == 1: items["Selected variable"] = vars[0] else: items["Selected variables"] = "{} and {}".format( ", ".join(var.name for var in vars[:-1]), vars[-1].name) self.report_items(items) self.report_plot()
class OWPaintData(OWWidget): TOOLS = [ ("Brush", "Create multiple instances", AirBrushTool, _icon("brush.svg")), ("Put", "Put individual instances", PutInstanceTool, _icon("put.svg")), ("Select", "Select and move instances", SelectTool, _icon("select-transparent_42px.png")), ("Jitter", "Jitter instances", JitterTool, _icon("jitter.svg")), ("Magnet", "Attract multiple instances", MagnetTool, _icon("magnet.svg")), ("Clear", "Clear the plot", ClearTool, _icon("../../../icons/Dlg_clear.png")) ] name = "Paint Data" description = "Create data by painting data points on a plane." icon = "icons/PaintData.svg" priority = 60 keywords = ["create", "draw"] class Inputs: data = Input("Data", Table) class Outputs: data = Output("Data", Table) autocommit = Setting(True) table_name = Setting("Painted data") attr1 = Setting("x") attr2 = Setting("y") hasAttr2 = Setting(True) brushRadius = Setting(75) density = Setting(7) symbol_size = Setting(10) #: current data array (shape=(N, 3)) as presented on the output data = Setting(None, schema_only=True) labels = Setting(["C1", "C2"], schema_only=True) buttons_area_orientation = Qt.Vertical graph_name = "plot" class Warning(OWWidget.Warning): no_input_variables = Msg("Input data has no variables") continuous_target = Msg("Numeric target value can not be used.") sparse_not_supported = Msg("Sparse data is ignored.") renamed_vars = Msg("Some variables have been renamed " "to avoid duplicates.\n{}") class Information(OWWidget.Information): use_first_two = \ Msg("Paint Data uses data from the first two attributes.") def __init__(self): super().__init__() self.input_data = None self.input_classes = [] self.input_colors = None self.input_has_attr2 = True self.current_tool = None self._selected_indices = None self._scatter_item = None #: A private data buffer (can be modified in place). `self.data` is #: a copy of this array (as seen when the `invalidate` method is #: called self.__buffer = None self.undo_stack = QUndoStack(self) self.class_model = ColoredListModel( self.labels, self, flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled | Qt.ItemIsEditable) self.class_model.dataChanged.connect(self._class_value_changed) self.class_model.rowsInserted.connect(self._class_count_changed) self.class_model.rowsRemoved.connect(self._class_count_changed) # if self.data: raises Deprecation warning in older workflows, where # data could be a np.array. This would raise an error in the future. if self.data is None or len(self.data) == 0: self.data = [] self.__buffer = np.zeros((0, 3)) elif isinstance(self.data, np.ndarray): self.__buffer = self.data.copy() self.data = self.data.tolist() else: self.__buffer = np.array(self.data) self.colors = colorpalettes.DefaultRGBColors self.tools_cache = {} self._init_ui() self.commit.now() def _init_ui(self): namesBox = gui.vBox(self.controlArea, "Names") hbox = gui.hBox(namesBox, margin=0, spacing=0) gui.lineEdit(hbox, self, "attr1", "Variable X: ", controlWidth=80, orientation=Qt.Horizontal, callback=self._attr_name_changed) gui.separator(hbox, 21) hbox = gui.hBox(namesBox, margin=0, spacing=0) attr2 = gui.lineEdit(hbox, self, "attr2", "Variable Y: ", controlWidth=80, orientation=Qt.Horizontal, callback=self._attr_name_changed) gui.separator(hbox) gui.checkBox(hbox, self, "hasAttr2", '', disables=attr2, labelWidth=0, callback=self.set_dimensions) gui.widgetLabel(namesBox, "Labels") self.classValuesView = listView = gui.ListViewWithSizeHint( preferred_size=(-1, 30)) listView.setModel(self.class_model) itemmodels.select_row(listView, 0) namesBox.layout().addWidget(listView) self.addClassLabel = QAction( "+", self, toolTip="Add new class label", triggered=self.add_new_class_label ) self.removeClassLabel = QAction( unicodedata.lookup("MINUS SIGN"), self, toolTip="Remove selected class label", triggered=self.remove_selected_class_label ) actionsWidget = itemmodels.ModelActionsWidget( [self.addClassLabel, self.removeClassLabel], self ) actionsWidget.layout().addStretch(10) actionsWidget.layout().setSpacing(1) namesBox.layout().addWidget(actionsWidget) tBox = gui.vBox(self.buttonsArea, "Tools") toolsBox = gui.widgetBox(tBox, orientation=QGridLayout()) self.toolActions = QActionGroup(self) self.toolActions.setExclusive(True) self.toolButtons = [] for i, (name, tooltip, tool, icon) in enumerate(self.TOOLS): action = QAction( name, self, toolTip=tooltip, checkable=tool.checkable, icon=QIcon(icon), ) action.triggered.connect(partial(self.set_current_tool, tool)) button = QToolButton( iconSize=QSize(24, 24), toolButtonStyle=Qt.ToolButtonTextUnderIcon, sizePolicy=QSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.Fixed) ) button.setDefaultAction(action) self.toolButtons.append((button, tool)) toolsBox.layout().addWidget(button, i // 3, i % 3) self.toolActions.addAction(action) for column in range(3): toolsBox.layout().setColumnMinimumWidth(column, 10) toolsBox.layout().setColumnStretch(column, 1) undo = self.undo_stack.createUndoAction(self) redo = self.undo_stack.createRedoAction(self) undo.setShortcut(QKeySequence.Undo) redo.setShortcut(QKeySequence.Redo) self.addActions([undo, redo]) self.undo_stack.indexChanged.connect(self.invalidate) indBox = gui.indentedBox(tBox, sep=8) form = QFormLayout( formAlignment=Qt.AlignLeft, labelAlignment=Qt.AlignLeft, fieldGrowthPolicy=QFormLayout.AllNonFixedFieldsGrow ) indBox.layout().addLayout(form) slider = gui.hSlider( indBox, self, "brushRadius", minValue=1, maxValue=100, createLabel=False, addToLayout=False ) form.addRow("Radius:", slider) slider = gui.hSlider( indBox, self, "density", None, minValue=1, maxValue=100, createLabel=False, addToLayout=False ) form.addRow("Intensity:", slider) slider = gui.hSlider( indBox, self, "symbol_size", None, minValue=1, maxValue=100, createLabel=False, callback=self.set_symbol_size, addToLayout=False ) form.addRow("Symbol:", slider) self.btResetToInput = gui.button( tBox, self, "Reset to Input Data", self.reset_to_input) self.btResetToInput.setDisabled(True) gui.auto_send(self.buttonsArea, self, "autocommit") # main area GUI viewbox = PaintViewBox(enableMouse=False) self.plotview = PlotWidget(viewBox=viewbox) self.plot = self.plotview.getPlotItem() axis_color = self.palette().color(QPalette.Text) axis_pen = QPen(axis_color) tickfont = QFont(self.font()) tickfont.setPixelSize(max(int(tickfont.pixelSize() * 2 // 3), 11)) axis = self.plot.getAxis("bottom") axis.setLabel(self.attr1) axis.setPen(axis_pen) axis.setTickFont(tickfont) axis = self.plot.getAxis("left") axis.setLabel(self.attr2) axis.setPen(axis_pen) axis.setTickFont(tickfont) if not self.hasAttr2: self.plot.hideAxis('left') self.plot.hideButtons() self.plot.setXRange(0, 1, padding=0.01) self.mainArea.layout().addWidget(self.plotview) # enable brush tool self.toolActions.actions()[0].setChecked(True) self.set_current_tool(self.TOOLS[0][2]) self.set_dimensions() def set_symbol_size(self): if self._scatter_item: self._scatter_item.setSize(self.symbol_size) def set_dimensions(self): if self.hasAttr2: self.plot.setYRange(0, 1, padding=0.01) self.plot.showAxis('left') self.plotview.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Minimum) else: self.plot.setYRange(-.5, .5, padding=0.01) self.plot.hideAxis('left') self.plotview.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Maximum) self._replot() for button, tool in self.toolButtons: if tool.only2d: button.setDisabled(not self.hasAttr2) @Inputs.data def set_data(self, data): """Set the input_data and call reset_to_input""" def _check_and_set_data(data): self.clear_messages() if data and data.is_sparse(): self.Warning.sparse_not_supported() return False if data: if not data.domain.attributes: self.Warning.no_input_variables() data = None elif len(data.domain.attributes) > 2: self.Information.use_first_two() self.input_data = data self.btResetToInput.setDisabled(data is None) return bool(data) if not _check_and_set_data(data): return X = np.array([scale(vals) for vals in data.X[:, :2].T]).T try: y = next(cls for cls in data.domain.class_vars if cls.is_discrete) except StopIteration: if data.domain.class_vars: self.Warning.continuous_target() self.input_classes = ["C1"] self.input_colors = None y = np.zeros(len(data)) else: self.input_classes = y.values self.input_colors = y.palette y = data[:, y].Y self.input_has_attr2 = len(data.domain.attributes) >= 2 if not self.input_has_attr2: self.input_data = np.column_stack((X, np.zeros(len(data)), y)) else: self.input_data = np.column_stack((X, y)) self.reset_to_input() self.commit.now() def reset_to_input(self): """Reset the painting to input data if present.""" if self.input_data is None: return self.undo_stack.clear() index = self.selected_class_label() if self.input_colors is not None: palette = self.input_colors else: palette = colorpalettes.DefaultRGBColors self.colors = palette self.class_model.colors = palette self.class_model[:] = self.input_classes newindex = min(max(index, 0), len(self.class_model) - 1) itemmodels.select_row(self.classValuesView, newindex) self.data = self.input_data.tolist() self.__buffer = self.input_data.copy() prev_attr2 = self.hasAttr2 self.hasAttr2 = self.input_has_attr2 if prev_attr2 != self.hasAttr2: self.set_dimensions() else: # set_dimensions already calls _replot, no need to call it again self._replot() self.commit.deferred() def add_new_class_label(self, undoable=True): newlabel = next(label for label in namegen('C', 1) if label not in self.class_model) command = SimpleUndoCommand( lambda: self.class_model.append(newlabel), lambda: self.class_model.__delitem__(-1) ) if undoable: self.undo_stack.push(command) else: command.redo() def remove_selected_class_label(self): index = self.selected_class_label() if index is None: return label = self.class_model[index] mask = self.__buffer[:, 2] == index move_mask = self.__buffer[~mask][:, 2] > index self.undo_stack.beginMacro("Delete class label") self.undo_stack.push(UndoCommand(DeleteIndices(mask), self)) self.undo_stack.push(UndoCommand(Move((move_mask, 2), -1), self)) self.undo_stack.push( SimpleUndoCommand(lambda: self.class_model.__delitem__(index), lambda: self.class_model.insert(index, label))) self.undo_stack.endMacro() newindex = min(max(index - 1, 0), len(self.class_model) - 1) itemmodels.select_row(self.classValuesView, newindex) def _class_count_changed(self): self.labels = list(self.class_model) self.removeClassLabel.setEnabled(len(self.class_model) > 1) self.addClassLabel.setEnabled( len(self.class_model) < len(self.colors)) if self.selected_class_label() is None: itemmodels.select_row(self.classValuesView, 0) def _class_value_changed(self, index, _): index = index.row() newvalue = self.class_model[index] oldvalue = self.labels[index] if newvalue != oldvalue: self.labels[index] = newvalue # command = Command( # lambda: self.class_model.__setitem__(index, newvalue), # lambda: self.class_model.__setitem__(index, oldvalue), # ) # self.undo_stack.push(command) def selected_class_label(self): rows = self.classValuesView.selectedIndexes() if rows: return rows[0].row() return None def set_current_tool(self, tool): prev_tool = self.current_tool.__class__ if self.current_tool is not None: self.current_tool.deactivate() self.current_tool.editingStarted.disconnect( self._on_editing_started) self.current_tool.editingFinished.disconnect( self._on_editing_finished) self.current_tool = None self.plot.getViewBox().tool = None if tool not in self.tools_cache: newtool = tool(self, self.plot) self.tools_cache[tool] = newtool newtool.issueCommand.connect(self._add_command) self.current_tool = tool = self.tools_cache[tool] self.plot.getViewBox().tool = tool tool.editingStarted.connect(self._on_editing_started) tool.editingFinished.connect(self._on_editing_finished) tool.activate() if not tool.checkable: self.set_current_tool(prev_tool) def _on_editing_started(self): self.undo_stack.beginMacro("macro") def _on_editing_finished(self): self.undo_stack.endMacro() def execute(self, command): assert isinstance(command, (Append, DeleteIndices, Insert, Move)), \ "Non normalized command" if isinstance(command, (DeleteIndices, Insert)): self._selected_indices = None if isinstance(self.current_tool, SelectTool): self.current_tool.reset() self.__buffer, undo = transform(command, self.__buffer) self._replot() return undo def _add_command(self, cmd): # pylint: disable=too-many-branches name = "Name" if (not self.hasAttr2 and isinstance(cmd, (Move, MoveSelection, Jitter, Magnet))): # tool only supported if both x and y are enabled return if isinstance(cmd, Append): cls = self.selected_class_label() points = np.array([(p.x(), p.y() if self.hasAttr2 else 0, cls) for p in cmd.points]) self.undo_stack.push(UndoCommand(Append(points), self, text=name)) elif isinstance(cmd, Move): self.undo_stack.push(UndoCommand(cmd, self, text=name)) elif isinstance(cmd, SelectRegion): indices = [i for i, (x, y) in enumerate(self.__buffer[:, :2]) if cmd.region.contains(QPointF(x, y))] indices = np.array(indices, dtype=int) self._selected_indices = indices elif isinstance(cmd, DeleteSelection): indices = self._selected_indices if indices is not None and indices.size: self.undo_stack.push( UndoCommand(DeleteIndices(indices), self, text="Delete") ) elif isinstance(cmd, MoveSelection): indices = self._selected_indices if indices is not None and indices.size: self.undo_stack.push( UndoCommand( Move((self._selected_indices, slice(0, 2)), np.array([cmd.delta.x(), cmd.delta.y()])), self, text="Move") ) elif isinstance(cmd, DeleteIndices): self.undo_stack.push(UndoCommand(cmd, self, text="Delete")) elif isinstance(cmd, Insert): self.undo_stack.push(UndoCommand(cmd, self)) elif isinstance(cmd, AirBrush): data = create_data(cmd.pos.x(), cmd.pos.y(), self.brushRadius / 1000, int(1 + self.density / 20), cmd.rstate) data = data[(np.min(data, axis=1) >= 0) & (np.max(data, axis=1) <= 1), :] if data.size: self._add_command(Append([QPointF(*p) for p in zip(*data.T)])) elif isinstance(cmd, Jitter): point = np.array([cmd.pos.x(), cmd.pos.y()]) delta = - apply_jitter(self.__buffer[:, :2], point, self.density / 100.0, 0, cmd.rstate) self._add_command(Move((..., slice(0, 2)), delta)) elif isinstance(cmd, Magnet): point = np.array([cmd.pos.x(), cmd.pos.y()]) delta = - apply_attractor(self.__buffer[:, :2], point, self.density / 100.0, 0) self._add_command(Move((..., slice(0, 2)), delta)) else: assert False, "unreachable" def _replot(self): def pen(color): pen = QPen(color, 1) pen.setCosmetic(True) return pen if self._scatter_item is not None: self.plot.removeItem(self._scatter_item) self._scatter_item = None x = self.__buffer[:, 0].copy() if self.hasAttr2: y = self.__buffer[:, 1].copy() else: y = np.zeros(self.__buffer.shape[0]) colors = self.colors[self.__buffer[:, 2]] pens = [pen(c) for c in colors] brushes = [QBrush(c) for c in colors] self._scatter_item = pg.ScatterPlotItem( x, y, symbol="+", brush=brushes, pen=pens ) self.plot.addItem(self._scatter_item) self.set_symbol_size() def _attr_name_changed(self): self.plot.getAxis("bottom").setLabel(self.attr1) self.plot.getAxis("left").setLabel(self.attr2) self.invalidate() def invalidate(self): self.data = self.__buffer.tolist() self.commit.deferred() @gui.deferred def commit(self): self.Warning.renamed_vars.clear() if not self.data: self.Outputs.data.send(None) return data = np.array(self.data) if self.hasAttr2: X, Y = data[:, :2], data[:, 2] proposed = [self.attr1.strip(), self.attr2.strip()] else: X, Y = data[:, np.newaxis, 0], data[:, 2] proposed = [self.attr1.strip()] if len(np.unique(Y)) >= 2: proposed.append("Class") unique_names, renamed = get_unique_names_duplicates(proposed, True) domain = Domain( (map(ContinuousVariable, unique_names[:-1])), DiscreteVariable( unique_names[-1], values=tuple(self.class_model)) ) data = Table.from_numpy(domain, X, Y) else: unique_names, renamed = get_unique_names_duplicates(proposed, True) domain = Domain(map(ContinuousVariable, unique_names)) data = Table.from_numpy(domain, X) if renamed: self.Warning.renamed_vars(", ".join(renamed)) self.plot.getAxis("bottom").setLabel(unique_names[0]) self.plot.getAxis("left").setLabel(unique_names[1]) data.name = self.table_name self.Outputs.data.send(data) def sizeHint(self): sh = super().sizeHint() return sh.expandedTo(QSize(570, 690)) def onDeleteWidget(self): self.undo_stack.indexChanged.disconnect(self.invalidate) self.plot.clear() def send_report(self): if self.data is None: return settings = [] if self.attr1 != "x" or self.attr2 != "y": settings += [("Axis x", self.attr1), ("Axis y", self.attr2)] settings += [("Number of points", len(self.data))] self.report_items("Painted data", settings) self.report_plot()