def __init__(self): super().__init__() self.input_data = None self.input_classes = [] self.input_has_attr2 = True self.current_tool = None self._selected_indices = None self._scatter_item = None self.labels = ["C1", "C2"] 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 is None: self.data = np.zeros((0, 3)) self.colors = colorpalette.ColorPaletteGenerator( len(colorpalette.DefaultRGBColors)) self.tools_cache = {} self._init_ui() self.commit()
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()
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 not self.data: 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 = colorpalette.ColorPaletteGenerator( len(colorpalette.DefaultRGBColors)) self.tools_cache = {} self._init_ui() self.commit()
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 not self.data: 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 = colorpalette.ColorPaletteGenerator( len(colorpalette.DefaultRGBColors)) self.tools_cache = {} self._init_ui() self.commit()
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() 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 = pg.PlotWidget(background="w", viewBox=viewbox) self.plotview.sizeHint = lambda: QSize( 200, 100) # Minimum size for 1-d painting 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.unconditional_commit() 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() 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) 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() 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()
class OWPaintData(OWWidget): TOOLS = [ ("Brush", "Create multiple instances", AirBrushTool, _i("brush.svg")), ("Put", "Put individual instances", PutInstanceTool, _i("put.svg")), ("Select", "Select and move instances", SelectTool, _i("select-transparent_42px.png")), ("Jitter", "Jitter instances", JitterTool, _i("jitter.svg")), ("Magnet", "Attract multiple instances", MagnetTool, _i("magnet.svg")), ("Clear", "Clear the plot", ClearTool, _i("../../../icons/Dlg_clear.png")) ] name = "Paint Data" description = "Create data by painting data points on a plane." icon = "icons/PaintData.svg" priority = 15 keywords = ["data", "paint", "create"] outputs = [("Data", Orange.data.Table)] inputs = [("Data", Orange.data.Table, "set_data")] autocommit = Setting(False) table_name = Setting("Painted data") attr1 = Setting("x") attr2 = Setting("y") hasAttr2 = Setting(True) brushRadius = Setting(75) density = Setting(7) data = Setting(None, schema_only=True) graph_name = "plot" class Warning(OWWidget.Warning): no_input_variables = Msg("Input data has no variables") continuous_target = Msg("Continuous target value can not be used.") 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_has_attr2 = True self.current_tool = None self._selected_indices = None self._scatter_item = None self.labels = ["C1", "C2"] 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 is None: self.data = np.zeros((0, 3)) self.colors = colorpalette.ColorPaletteGenerator( len(colorpalette.DefaultRGBColors)) self.tools_cache = {} self._init_ui() self.commit() 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.separator(namesBox) gui.widgetLabel(namesBox, "Labels") self.classValuesView = listView = QListView( selectionMode=QListView.SingleSelection, sizePolicy=QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Maximum) ) 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.controlArea, "Tools", addSpace=True) buttonBox = gui.hBox(tBox) toolsBox = gui.widgetBox(buttonBox, 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(lambda _: self.invalidate()) gui.separator(tBox) 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 ) form.addRow("Radius:", slider) slider = gui.hSlider( indBox, self, "density", None, minValue=1, maxValue=100, createLabel=False ) form.addRow("Intensity:", slider) self.btResetToInput = gui.button( tBox, self, "Reset to Input Data", self.reset_to_input) self.btResetToInput.setDisabled(True) gui.rubber(self.controlArea) gui.auto_commit(self.left_side, self, "autocommit", "Send") # main area GUI viewbox = PaintViewBox(enableMouse=False) self.plotview = pg.PlotWidget(background="w", viewBox=viewbox) self.plotview.sizeHint = lambda: QSize(200, 100) # Minimum size for 1-d painting 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_dimensions(self): if self.hasAttr2: self.plot.setYRange(0, 1, padding=0.01) self.plot.showAxis('left') self.plotview.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Minimum) else: self.plot.setYRange(-.5, .5, padding=0.01) self.plot.hideAxis('left') self.plotview.setSizePolicy(QSizePolicy.Ignored, QSizePolicy.Maximum) self._replot() for button, tool in self.toolButtons: if tool.only2d: button.setDisabled(not self.hasAttr2) 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 is not None: 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 data is not None 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"] y = np.zeros(len(data)) else: self.input_classes = y.values 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() 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() 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 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() 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.data[:, 2] == index move_mask = self.data[~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) < self.colors.number_of_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() else: 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._selected_region = QRectF() 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): if isinstance(command, (Append, DeleteIndices, Insert, Move)): if isinstance(command, (DeleteIndices, Insert)): self._selected_indices = None if isinstance(self.current_tool, SelectTool): self.current_tool._reset() self.data, undo = transform(command, self.data) self._replot() return undo else: assert False, "Non normalized command" def _add_command(self, cmd): 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.data[:, :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) 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.data[:, :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.data[:, :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 nclasses = len(self.class_model) pens = [pen(self.colors[i]) for i in range(nclasses)] self._scatter_item = pg.ScatterPlotItem( self.data[:, 0], self.data[:, 1] if self.hasAttr2 else np.zeros(self.data.shape[0]), symbol="+", pen=[pens[int(ci)] for ci in self.data[:, 2]] ) self.plot.addItem(self._scatter_item) 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.commit() def commit(self): if len(self.data) == 0: self.send("Data", None) return if self.hasAttr2: X, Y = self.data[:, :2], self.data[:, 2] attrs = (Orange.data.ContinuousVariable(self.attr1), Orange.data.ContinuousVariable(self.attr2)) else: X, Y = self.data[:, np.newaxis, 0], self.data[:, 2] attrs = (Orange.data.ContinuousVariable(self.attr1),) if len(np.unique(Y)) >= 2: domain = Orange.data.Domain( attrs, Orange.data.DiscreteVariable( "Class", values=list(self.class_model)) ) data = Orange.data.Table.from_numpy(domain, X, Y) else: domain = Orange.data.Domain(attrs) data = Orange.data.Table.from_numpy(domain, X) data.name = self.table_name self.send("Data", data) def sizeHint(self): sh = super().sizeHint() return sh.expandedTo(QSize(1200, 800)) def onDeleteWidget(self): 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()