class OWMDS(widget.OWWidget): name = "MDS" description = "Two-dimensional data projection by multidimensional " \ "scaling constructed from a distance matrix." icon = "icons/MDS.svg" inputs = [("Data", Orange.data.Table, "set_data", widget.Default), ("Distances", Orange.misc.DistMatrix, "set_disimilarity"), ("Data Subset", Orange.data.Table, "set_subset_data")] outputs = [("Selected Data", Orange.data.Table, widget.Default), ("Data", Orange.data.Table)] #: Initialization type PCA, Random = 0, 1 #: Refresh rate RefreshRate = [ ("Every iteration", 1), ("Every 5 steps", 5), ("Every 10 steps", 10), ("Every 25 steps", 25), ("Every 50 steps", 50), ("None", -1) ] JitterAmount = [ ("None", 0), ("0.1 %", 0.1), ("0.5 %", 0.5), ("1 %", 1.0), ("2 %", 2.0) ] #: Runtime state Running, Finished, Waiting = 1, 2, 3 settingsHandler = settings.DomainContextHandler() max_iter = settings.Setting(300) initialization = settings.Setting(PCA) refresh_rate = settings.Setting(3) # output embedding role. NoRole, AttrRole, AddAttrRole, MetaRole = 0, 1, 2, 3 output_embedding_role = settings.Setting(2) autocommit = settings.Setting(True) color_value = settings.ContextSetting("") shape_value = settings.ContextSetting("") size_value = settings.ContextSetting("") label_value = settings.ContextSetting("") label_only_selected = settings.Setting(False) symbol_size = settings.Setting(8) symbol_opacity = settings.Setting(230) connected_pairs = settings.Setting(5) jitter = settings.Setting(0) legend_anchor = settings.Setting(((1, 0), (1, 0))) graph_name = "plot.plotItem" def __init__(self): super().__init__() self.matrix = None self.data = None self.subset_data = None # type: Optional[Orange.data.Table] self.matrix_data = None self.signal_data = None self._pen_data = None self._brush_data = None self._shape_data = None self._size_data = None self._label_data = None self._similar_pairs = None self._scatter_item = None self._legend_item = None self._selection_mask = None self._subset_mask = None # type: Optional[numpy.ndarray] self._invalidated = False self._effective_matrix = None self.__update_loop = None self.__state = OWMDS.Waiting self.__in_next_step = False self.__draw_similar_pairs = False box = gui.vBox(self.controlArea, "MDS Optimization") form = QtGui.QFormLayout( labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft, fieldGrowthPolicy=QtGui.QFormLayout.AllNonFixedFieldsGrow, verticalSpacing=10 ) form.addRow("Max iterations:", gui.spin(box, self, "max_iter", 10, 10 ** 4, step=1)) form.addRow("Initialization:", gui.comboBox(box, self, "initialization", items=["PCA (Torgerson)", "Random"], callback=self.__invalidate_embedding)) box.layout().addLayout(form) form.addRow("Refresh:", gui.comboBox( box, self, "refresh_rate", items=[t for t, _ in OWMDS.RefreshRate], callback=self.__invalidate_refresh)) gui.separator(box, 10) self.runbutton = gui.button( box, self, "Run", callback=self._toggle_run) box = gui.vBox(self.controlArea, "Graph") self.colorvar_model = itemmodels.VariableListModel() common_options = dict( sendSelectedValue=True, valueType=str, orientation=Qt.Horizontal, labelWidth=50, contentsLength=12) self.cb_color_value = gui.comboBox( box, self, "color_value", label="Color:", callback=self._on_color_index_changed, **common_options) self.cb_color_value.setModel(self.colorvar_model) self.shapevar_model = itemmodels.VariableListModel() self.cb_shape_value = gui.comboBox( box, self, "shape_value", label="Shape:", callback=self._on_shape_index_changed, **common_options) self.cb_shape_value.setModel(self.shapevar_model) self.sizevar_model = itemmodels.VariableListModel() self.cb_size_value = gui.comboBox( box, self, "size_value", label="Size:", callback=self._on_size_index_changed, **common_options) self.cb_size_value.setModel(self.sizevar_model) self.labelvar_model = itemmodels.VariableListModel() self.cb_label_value = gui.comboBox( box, self, "label_value", label="Label:", callback=self._on_label_index_changed, **common_options) self.cb_label_value.setModel(self.labelvar_model) gui.checkBox( gui.indentedBox(box), self, 'label_only_selected', 'Label only selected points', callback=self._on_label_index_changed) form = QtGui.QFormLayout( labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft, fieldGrowthPolicy=QtGui.QFormLayout.AllNonFixedFieldsGrow, verticalSpacing=10 ) form.addRow("Symbol size:", gui.hSlider(box, self, "symbol_size", minValue=1, maxValue=20, callback=self._on_size_index_changed, createLabel=False)) form.addRow("Symbol opacity:", gui.hSlider(box, self, "symbol_opacity", minValue=100, maxValue=255, step=100, callback=self._on_color_index_changed, createLabel=False)) form.addRow("Show similar pairs:", gui.hSlider( gui.hBox(self.controlArea), self, "connected_pairs", minValue=0, maxValue=20, createLabel=False, callback=self._on_connected_changed)) form.addRow("Jitter:", gui.comboBox( box, self, "jitter", items=[text for text, _ in self.JitterAmount], callback=self._update_plot)) box.layout().addLayout(form) gui.rubber(self.controlArea) box = QtGui.QGroupBox("Zoom/Select", ) box.setLayout(QtGui.QHBoxLayout()) box.layout().setContentsMargins(2, 2, 2, 2) group = QtGui.QActionGroup(self, exclusive=True) def icon(name): path = "icons/Dlg_{}.png".format(name) path = pkg_resources.resource_filename(widget.__name__, path) return QtGui.QIcon(path) action_select = QtGui.QAction( "Select", self, checkable=True, checked=True, icon=icon("arrow"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_1)) action_zoom = QtGui.QAction( "Zoom", self, checkable=True, checked=False, icon=icon("zoom"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_2)) action_pan = QtGui.QAction( "Pan", self, checkable=True, checked=False, icon=icon("pan_hand"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_3)) action_reset_zoom = QtGui.QAction( "Zoom to fit", self, icon=icon("zoom_reset"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_0)) action_reset_zoom.triggered.connect( lambda: self.plot.autoRange(padding=0.1, items=[self._scatter_item])) group.addAction(action_select) group.addAction(action_zoom) group.addAction(action_pan) self.addActions(group.actions() + [action_reset_zoom]) action_select.setChecked(True) def button(action): b = QtGui.QToolButton() b.setToolButtonStyle(Qt.ToolButtonIconOnly) b.setDefaultAction(action) return b box.layout().addWidget(button(action_select)) box.layout().addWidget(button(action_zoom)) box.layout().addWidget(button(action_pan)) box.layout().addSpacing(4) box.layout().addWidget(button(action_reset_zoom)) box.layout().addStretch() self.controlArea.layout().addWidget(box) box = gui.vBox(self.controlArea, "Output") self.output_combo = gui.comboBox( box, self, "output_embedding_role", items=["Original features only", "Coordinates only", "Coordinates as features", "Coordinates as meta attributes"], callback=self._invalidate_output, addSpace=4) gui.auto_commit(box, self, "autocommit", "Send Selected", checkbox_label="Send selected automatically", box=None) self.plot = pg.PlotWidget(background="w", enableMenu=False) self.plot.setAspectLocked(True) self.plot.getPlotItem().hideAxis("bottom") self.plot.getPlotItem().hideAxis("left") self.plot.getPlotItem().hideButtons() self.plot.setRenderHint(QtGui.QPainter.Antialiasing) self.mainArea.layout().addWidget(self.plot) self.selection_tool = PlotSelectionTool(parent=self) self.zoom_tool = PlotZoomTool(parent=self) self.pan_tool = PlotPanTool(parent=self) self.pinch_tool = PlotPinchZoomTool(parent=self) self.pinch_tool.setViewBox(self.plot.getViewBox()) self.selection_tool.setViewBox(self.plot.getViewBox()) self.selection_tool.selectionFinished.connect(self.__selection_end) self.current_tool = self.selection_tool def activate_tool(action): self.current_tool.setViewBox(None) if action is action_select: active, cur = self.selection_tool, Qt.ArrowCursor elif action is action_zoom: active, cur = self.zoom_tool, Qt.ArrowCursor elif action is action_pan: active, cur = self.pan_tool, Qt.OpenHandCursor self.current_tool = active self.current_tool.setViewBox(self.plot.getViewBox()) self.plot.getViewBox().setCursor(QtGui.QCursor(cur)) group.triggered[QtGui.QAction].connect(activate_tool) self._initialize() @check_sql_input def set_data(self, data): """Set the input data set. Parameters ---------- data : Optional[Orange.data.Table] """ self.signal_data = data if self.matrix is not None and data is not None and len(self.matrix) == len(data): self.closeContext() self.data = data self.update_controls() self.openContext(data) else: self._invalidated = True self._selection_mask = None def set_disimilarity(self, matrix): """Set the dissimilarity (distance) matrix. Parameters ---------- matrix : Optional[Orange.misc.DistMatrix] """ self.matrix = matrix if matrix is not None and matrix.row_items: self.matrix_data = matrix.row_items if matrix is None: self.matrix_data = None self._invalidated = True self._selection_mask = None def set_subset_data(self, subset_data): """Set a subset of `data` input to highlight in the plot. Parameters ---------- subset_data: Optional[Orange.data.Table] """ self.subset_data = subset_data # invalidate the pen/brush when the subset is changed self._pen_data = self._brush_data = None self._subset_mask = None # type: Optional[numpy.ndarray] def _clear(self): self._pen_data = None self._brush_data = None self._shape_data = None self._size_data = None self._label_data = None self._similar_pairs = None self.colorvar_model[:] = ["Same color"] self.shapevar_model[:] = ["Same shape"] self.sizevar_model[:] = ["Same size"] self.labelvar_model[:] = ["No labels"] self.color_value = self.colorvar_model[0] self.shape_value = self.shapevar_model[0] self.size_value = self.sizevar_model[0] self.label_value = self.labelvar_model[0] self.__set_update_loop(None) self.__state = OWMDS.Waiting def _clear_plot(self): self.plot.clear() self._scatter_item = None if self._legend_item is not None: anchor = legend_anchor_pos(self._legend_item) if anchor is not None: self.legend_anchor = anchor if self._legend_item.scene() is not None: self._legend_item.scene().removeItem(self._legend_item) self._legend_item = None def update_controls(self): if self.data is None and getattr(self.matrix, 'axis', 1) == 0: # Column-wise distances attr = "Attribute names" self.labelvar_model[:] = ["No labels", attr] self.shapevar_model[:] = ["Same shape", attr] self.colorvar_model[:] = ["Same solor", attr] self.color_value = attr self.shape_value = attr else: # initialize the graph state from data domain = self.data.domain all_vars = list(filter_visible(domain.variables + domain.metas)) cd_vars = [var for var in all_vars if var.is_primitive()] disc_vars = [var for var in all_vars if var.is_discrete] cont_vars = [var for var in all_vars if var.is_continuous] shape_vars = [var for var in disc_vars if len(var.values) <= len(ScatterPlotItem.Symbols) - 1] self.colorvar_model[:] = chain(["Same color"], [self.colorvar_model.Separator] if cd_vars else [], cd_vars) self.shapevar_model[:] = chain(["Same shape"], [self.shapevar_model.Separator] if shape_vars else [], shape_vars) self.sizevar_model[:] = chain(["Same size", "Stress"], [self.sizevar_model.Separator] if cont_vars else [], cont_vars) self.labelvar_model[:] = chain(["No labels"], [self.labelvar_model.Separator] if all_vars else [], all_vars) if domain.class_var is not None: self.color_value = domain.class_var.name def _initialize(self): # clear everything self.closeContext() self._clear() self.data = None self._effective_matrix = None self.embedding = None # if no data nor matrix is present reset plot if self.signal_data is None and self.matrix is None: return if self.signal_data and self.matrix is not None and len(self.signal_data) != len(self.matrix): self.error(1, "Data and distances dimensions do not match.") self._update_plot() return self.error(1) if self.signal_data: self.data = self.signal_data elif self.matrix_data: self.data = self.matrix_data if self.matrix is not None: self._effective_matrix = self.matrix if self.matrix.axis == 0 and self.data is self.matrix_data: self.data = None else: preprocessed_data = Orange.projection.MDS().preprocess(self.data) self._effective_matrix = Orange.distance.Euclidean(preprocessed_data) self.update_controls() self.openContext(self.data) def _toggle_run(self): if self.__state == OWMDS.Running: self.stop() self._invalidate_output() else: self.start() def start(self): if self.__state == OWMDS.Running: return elif self.__state == OWMDS.Finished: # Resume/continue from a previous run self.__start() elif self.__state == OWMDS.Waiting and \ self._effective_matrix is not None: self.__start() def stop(self): if self.__state == OWMDS.Running: self.__set_update_loop(None) def __start(self): self.__draw_similar_pairs = False X = self._effective_matrix if self.embedding is not None: init = self.embedding elif self.initialization == OWMDS.PCA: init = torgerson(X, n_components=2) else: init = None # number of iterations per single GUI update step _, step_size = OWMDS.RefreshRate[self.refresh_rate] if step_size == -1: step_size = self.max_iter def update_loop(X, max_iter, step, init): """ return an iterator over successive improved MDS point embeddings. """ # NOTE: this code MUST NOT call into QApplication.processEvents done = False iterations_done = 0 oldstress = numpy.finfo(numpy.float).max while not done: step_iter = min(max_iter - iterations_done, step) mds = Orange.projection.MDS( dissimilarity="precomputed", n_components=2, n_init=1, max_iter=step_iter) mdsfit = mds.fit(X, init=init) iterations_done += step_iter embedding, stress = mdsfit.embedding_, mdsfit.stress_ stress /= numpy.sqrt(numpy.sum(embedding ** 2, axis=1)).sum() if iterations_done >= max_iter: done = True elif (oldstress - stress) < mds.params["eps"]: done = True init = embedding oldstress = stress yield embedding, mdsfit.stress_, iterations_done / max_iter self.__set_update_loop(update_loop(X, self.max_iter, step_size, init)) self.progressBarInit(processEvents=None) def __set_update_loop(self, loop): """ Set the update `loop` coroutine. The `loop` is a generator yielding `(embedding, stress, progress)` tuples where `embedding` is a `(N, 2) ndarray` of current updated MDS points, `stress` is the current stress and `progress` a float ratio (0 <= progress <= 1) If an existing update loop is already in palace it is interrupted (closed). .. note:: The `loop` must not explicitly yield control flow to the event loop (i.e. call `QApplication.processEvents`) """ if self.__update_loop is not None: self.__update_loop.close() self.__update_loop = None self.progressBarFinished(processEvents=None) self.__update_loop = loop if loop is not None: self.progressBarInit(processEvents=None) self.setStatusMessage("Running") self.runbutton.setText("Stop") self.__state = OWMDS.Running QtGui.QApplication.postEvent(self, QEvent(QEvent.User)) else: self.setStatusMessage("") self.runbutton.setText("Start") self.__state = OWMDS.Finished def __next_step(self): if self.__update_loop is None: return loop = self.__update_loop try: embedding, stress, progress = next(self.__update_loop) assert self.__update_loop is loop except StopIteration: self.__set_update_loop(None) self.unconditional_commit() self.__draw_similar_pairs = True self._update_plot() self.plot.autoRange(padding=0.1, items=[self._scatter_item]) else: self.progressBarSet(100.0 * progress, processEvents=None) self.embedding = embedding self._update_plot() self.plot.autoRange(padding=0.1, items=[self._scatter_item]) # schedule next update QtGui.QApplication.postEvent( self, QEvent(QEvent.User), Qt.LowEventPriority) def customEvent(self, event): if event.type() == QEvent.User and self.__update_loop is not None: if not self.__in_next_step: self.__in_next_step = True try: self.__next_step() finally: self.__in_next_step = False else: warnings.warn( "Re-entry in update loop detected. " "A rogue `proccessEvents` is on the loose.", RuntimeWarning) # re-schedule the update iteration. QtGui.QApplication.postEvent(self, QEvent(QEvent.User)) return super().customEvent(event) def __invalidate_embedding(self): # reset/invalidate the MDS embedding, to the default initialization # (Random or PCA), restarting the optimization if necessary. if self.embedding is None: return state = self.__state if self.__update_loop is not None: self.__set_update_loop(None) X = self._effective_matrix if self.initialization == OWMDS.PCA: self.embedding = torgerson(X) else: self.embedding = numpy.random.rand(len(X), 2) self._update_plot() self.plot.autoRange(padding=0.1, items=[self._scatter_item]) # restart the optimization if it was interrupted. if state == OWMDS.Running: self.__start() def __invalidate_refresh(self): state = self.__state if self.__update_loop is not None: self.__set_update_loop(None) # restart the optimization if it was interrupted. # TODO: decrease the max iteration count by the already # completed iterations count. if state == OWMDS.Running: self.__start() def handleNewSignals(self): if self._invalidated: self._invalidated = False self._initialize() self.start() self.__draw_similar_pairs = False if self._subset_mask is None and self.subset_data is not None and \ self.data is not None: self._subset_mask = numpy.in1d(self.data.ids, self.subset_data.ids) self._update_plot() self.plot.autoRange(padding=0.1) self.unconditional_commit() def _invalidate_output(self): self.commit() def _on_color_index_changed(self): self._pen_data = None self._update_plot() def _on_shape_index_changed(self): self._shape_data = None self._update_plot() def _on_size_index_changed(self): self._size_data = None self._update_plot() def _on_label_index_changed(self): self._label_data = None self._update_plot() def _on_connected_changed(self): self._similar_pairs = None self._update_plot() def _update_plot(self): self._clear_plot() if self.embedding is not None: self._setup_plot() def _setup_plot(self): have_data = self.data is not None have_matrix_transposed = self.matrix is not None and not self.matrix.axis plotstyle = mdsplotutils.plotstyle size = self._effective_matrix.shape[0] def column(data, variable): a, _ = data.get_column_view(variable) return a.ravel() def attributes(matrix): return matrix.row_items.domain.attributes def scale(a): dmin, dmax = numpy.nanmin(a), numpy.nanmax(a) if dmax - dmin > 0: return (a - dmin) / (dmax - dmin) else: return numpy.zeros_like(a) def jitter(x, factor=1, rstate=None): if rstate is None: rstate = numpy.random.RandomState() elif not isinstance(rstate, numpy.random.RandomState): rstate = numpy.random.RandomState(rstate) span = numpy.nanmax(x) - numpy.nanmin(x) if span < numpy.finfo(x.dtype).eps * 100: span = 1 a = factor * span / 100. return x + (rstate.random_sample(x.shape) - 0.5) * a if self._pen_data is None: if self._selection_mask is not None: pointflags = numpy.where( self._selection_mask, mdsplotutils.Selected, mdsplotutils.NoFlags) else: pointflags = None color_index = self.cb_color_value.currentIndex() if have_data and color_index > 0: color_var = self.colorvar_model[color_index] if color_var.is_discrete: palette = colorpalette.ColorPaletteGenerator( len(color_var.values) ) plotstyle = plotstyle.updated(discrete_palette=palette) else: palette = None color_data = mdsplotutils.color_data( self.data, color_var, plotstyle=plotstyle) color_data = numpy.hstack( (color_data, numpy.full((len(color_data), 1), self.symbol_opacity, dtype=float)) ) pen_data = mdsplotutils.pen_data(color_data * 0.8, pointflags) brush_data = mdsplotutils.brush_data(color_data) elif have_matrix_transposed and \ self.colorvar_model[color_index] == 'Attribute names': attr = attributes(self.matrix) palette = colorpalette.ColorPaletteGenerator(len(attr)) color_data = [palette.getRGB(i) for i in range(len(attr))] color_data = numpy.hstack(( color_data, numpy.full((len(color_data), 1), self.symbol_opacity, dtype=float)) ) pen_data = mdsplotutils.pen_data(color_data * 0.8, pointflags) brush_data = mdsplotutils.brush_data(color_data) else: pen_data = make_pen(QtGui.QColor(Qt.darkGray), cosmetic=True) if self._selection_mask is not None: pen_data = numpy.array( [pen_data, plotstyle.selected_pen]) pen_data = pen_data[self._selection_mask.astype(int)] else: pen_data = numpy.full(self._effective_matrix.dim, pen_data, dtype=object) brush_data = numpy.full( size, pg.mkColor((192, 192, 192, self.symbol_opacity)), dtype=object) if self._subset_mask is not None and have_data and \ self._subset_mask.shape == (size, ): # clear brush fill for non subset data brush_data[~self._subset_mask] = QtGui.QBrush(Qt.NoBrush) self._pen_data = pen_data self._brush_data = brush_data if self._shape_data is None: shape_index = self.cb_shape_value.currentIndex() if have_data and shape_index > 0: Symbols = ScatterPlotItem.Symbols symbols = numpy.array(list(Symbols.keys())) shape_var = self.shapevar_model[shape_index] data = column(self.data, shape_var).astype(numpy.float) data = data % (len(Symbols) - 1) data[numpy.isnan(data)] = len(Symbols) - 1 shape_data = symbols[data.astype(int)] elif have_matrix_transposed and \ self.shapevar_model[shape_index] == 'Attribute names': Symbols = ScatterPlotItem.Symbols symbols = numpy.array(list(Symbols.keys())) attr = [i % (len(Symbols) - 1) for i, _ in enumerate(attributes(self.matrix))] shape_data = symbols[attr] else: shape_data = "o" self._shape_data = shape_data if self._size_data is None: MinPointSize = 3 point_size = self.symbol_size + MinPointSize size_index = self.cb_size_value.currentIndex() if have_data and size_index == 1: # size by stress size_data = stress(self.embedding, self._effective_matrix) size_data = scale(size_data) size_data = MinPointSize + size_data * point_size elif have_data and size_index > 0: size_var = self.sizevar_model[size_index] size_data = column(self.data, size_var) size_data = scale(size_data) size_data = MinPointSize + size_data * point_size else: size_data = point_size self._size_data = size_data if self._label_data is None: label_index = self.cb_label_value.currentIndex() if have_data and label_index > 0: label_var = self.labelvar_model[label_index] label_data = column(self.data, label_var) label_data = [label_var.str_val(val) for val in label_data] label_items = [pg.TextItem(text, anchor=(0.5, 0), color=0.0) for text in label_data] elif have_matrix_transposed and \ self.labelvar_model[label_index] == 'Attribute names': attr = attributes(self.matrix) label_items = [pg.TextItem(str(text), anchor=(0.5, 0)) for text in attr] else: label_items = None self._label_data = label_items emb_x, emb_y = self.embedding[:, 0], self.embedding[:, 1] if self.jitter > 0: _, jitter_factor = self.JitterAmount[self.jitter] emb_x = jitter(emb_x, jitter_factor, rstate=42) emb_y = jitter(emb_y, jitter_factor, rstate=667) if self.connected_pairs and self.__draw_similar_pairs: if self._similar_pairs is None: # This code requires storing lower triangle of X (n x n / 2 # doubles), n x n / 2 * 2 indices to X, n x n / 2 indices for # argsort result. If this becomes an issue, it can be reduced to # n x n argsort indices by argsorting the entire X. Then we # take the first n + 2 * p indices. We compute their coordinates # i, j in the original matrix. We keep those for which i < j. # n + 2 * p will suffice to exclude the diagonal (i = j). If the # number of those for which i < j is smaller than p, we instead # take i > j. Among those that remain, we take the first p. # Assuming that MDS can't show so many points that memory could # become an issue, I preferred using simpler code. m = self._effective_matrix n = len(m) p = (n * (n - 1) // 2 * self.connected_pairs) // 100 indcs = numpy.triu_indices(n, 1) sorted = numpy.argsort(m[indcs])[:p] self._similar_pairs = fpairs = numpy.empty(2 * p, dtype=int) fpairs[::2] = indcs[0][sorted] fpairs[1::2] = indcs[1][sorted] for i in range(int(len(emb_x[self._similar_pairs]) / 2)): item = QtGui.QGraphicsLineItem( emb_x[self._similar_pairs][i * 2], emb_y[self._similar_pairs][i * 2], emb_x[self._similar_pairs][i * 2 + 1], emb_y[self._similar_pairs][i * 2 + 1] ) pen = QtGui.QPen(QtGui.QBrush(QtGui.QColor(204, 204, 204)), 2) pen.setCosmetic(True) item.setPen(pen) self.plot.addItem(item) data = numpy.arange(size) self._scatter_item = item = ScatterPlotItem( x=emb_x, y=emb_y, pen=self._pen_data, brush=self._brush_data, symbol=self._shape_data, size=self._size_data, data=data, antialias=True ) self.plot.addItem(item) if self._label_data is not None: if self.label_only_selected: if self._selection_mask is not None: for (x, y), text_item, selected \ in zip(self.embedding, self._label_data, self._selection_mask): if selected: self.plot.addItem(text_item) text_item.setPos(x, y) else: for (x, y), text_item in zip(self.embedding, self._label_data): self.plot.addItem(text_item) text_item.setPos(x, y) self._legend_item = LegendItem() viewbox = self.plot.getViewBox() self._legend_item.setParentItem(self.plot.getViewBox()) self._legend_item.setZValue(viewbox.zValue() + 10) self._legend_item.restoreAnchor(self.legend_anchor) color_var = shape_var = None color_index = self.cb_color_value.currentIndex() if have_data and 1 <= color_index < len(self.colorvar_model): color_var = self.colorvar_model[color_index] assert isinstance(color_var, Orange.data.Variable) shape_index = self.cb_shape_value.currentIndex() if have_data and 1 <= shape_index < len(self.shapevar_model): shape_var = self.shapevar_model[shape_index] assert isinstance(shape_var, Orange.data.Variable) if shape_var is not None or \ (color_var is not None and color_var.is_discrete): legend_data = mdsplotutils.legend_data( color_var, shape_var, plotstyle=plotstyle) for color, symbol, text in legend_data: self._legend_item.addItem( ScatterPlotItem(pen=color, brush=color, symbol=symbol, size=10), escape(text) ) else: self._legend_item.hide() def commit(self): if self.embedding is not None: output = embedding = Orange.data.Table.from_numpy( Orange.data.Domain([Orange.data.ContinuousVariable("X"), Orange.data.ContinuousVariable("Y")]), self.embedding ) else: output = embedding = None if self.embedding is not None and self.data is not None: domain = self.data.domain attrs = domain.attributes class_vars = domain.class_vars metas = domain.metas if self.output_embedding_role == OWMDS.AttrRole: attrs = embedding.domain.attributes elif self.output_embedding_role == OWMDS.AddAttrRole: attrs = domain.attributes + embedding.domain.attributes elif self.output_embedding_role == OWMDS.MetaRole: metas += embedding.domain.attributes domain = Orange.data.Domain(attrs, class_vars, metas) output = Orange.data.Table.from_table(domain, self.data) if self.output_embedding_role == OWMDS.AttrRole: output.X[:] = embedding.X if self.output_embedding_role == OWMDS.AddAttrRole: output.X[:, -2:] = embedding.X elif self.output_embedding_role == OWMDS.MetaRole: output.metas[:, -2:] = embedding.X self.send("Data", output) if output is not None and self._selection_mask is not None and \ numpy.any(self._selection_mask): subset = output[self._selection_mask] else: subset = None self.send("Selected Data", subset) def onDeleteWidget(self): super().onDeleteWidget() self._clear_plot() self._clear() def __selection_end(self, path): self.select(path) self._pen_data = None self._update_plot() self._invalidate_output() def select(self, region): item = self._scatter_item if item is None: return indices = numpy.array( [spot.data() for spot in item.points() if region.contains(spot.pos())], dtype=int) if not QtGui.QApplication.keyboardModifiers(): self._selection_mask = None self.select_indices(indices, QtGui.QApplication.keyboardModifiers()) def select_indices(self, indices, modifiers=Qt.NoModifier): if self.data is None: return if self._selection_mask is None or \ not modifiers & (Qt.ControlModifier | Qt.ShiftModifier | Qt.AltModifier): self._selection_mask = numpy.zeros(len(self.data), dtype=bool) if modifiers & Qt.AltModifier: self._selection_mask[indices] = False elif modifiers & Qt.ControlModifier: self._selection_mask[indices] = ~self._selection_mask[indices] else: self._selection_mask[indices] = True def send_report(self): if self.data is None: return self.report_plot() caption = report.render_items_vert(( ("Color", self.color_value != "Same color" and self.color_value), ("Shape", self.shape_value != "Same shape" and self.shape_value), ("Size", self.size_value != "Same size" and self.size_value), ("Labels", self.label_value != "No labels" and self.label_value))) if caption: self.report_caption(caption) self.report_items((("Output", self.output_combo.currentText()),))
def __init__(self): super().__init__() self.matrix = None self.data = None self.subset_data = None # type: Optional[Orange.data.Table] self.matrix_data = None self.signal_data = None self._pen_data = None self._brush_data = None self._shape_data = None self._size_data = None self._label_data = None self._similar_pairs = None self._scatter_item = None self._legend_item = None self._selection_mask = None self._subset_mask = None # type: Optional[numpy.ndarray] self._invalidated = False self._effective_matrix = None self.__update_loop = None self.__state = OWMDS.Waiting self.__in_next_step = False self.__draw_similar_pairs = False box = gui.vBox(self.controlArea, "MDS Optimization") form = QtGui.QFormLayout( labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft, fieldGrowthPolicy=QtGui.QFormLayout.AllNonFixedFieldsGrow, verticalSpacing=10 ) form.addRow("Max iterations:", gui.spin(box, self, "max_iter", 10, 10 ** 4, step=1)) form.addRow("Initialization:", gui.comboBox(box, self, "initialization", items=["PCA (Torgerson)", "Random"], callback=self.__invalidate_embedding)) box.layout().addLayout(form) form.addRow("Refresh:", gui.comboBox( box, self, "refresh_rate", items=[t for t, _ in OWMDS.RefreshRate], callback=self.__invalidate_refresh)) gui.separator(box, 10) self.runbutton = gui.button( box, self, "Run", callback=self._toggle_run) box = gui.vBox(self.controlArea, "Graph") self.colorvar_model = itemmodels.VariableListModel() common_options = dict( sendSelectedValue=True, valueType=str, orientation=Qt.Horizontal, labelWidth=50, contentsLength=12) self.cb_color_value = gui.comboBox( box, self, "color_value", label="Color:", callback=self._on_color_index_changed, **common_options) self.cb_color_value.setModel(self.colorvar_model) self.shapevar_model = itemmodels.VariableListModel() self.cb_shape_value = gui.comboBox( box, self, "shape_value", label="Shape:", callback=self._on_shape_index_changed, **common_options) self.cb_shape_value.setModel(self.shapevar_model) self.sizevar_model = itemmodels.VariableListModel() self.cb_size_value = gui.comboBox( box, self, "size_value", label="Size:", callback=self._on_size_index_changed, **common_options) self.cb_size_value.setModel(self.sizevar_model) self.labelvar_model = itemmodels.VariableListModel() self.cb_label_value = gui.comboBox( box, self, "label_value", label="Label:", callback=self._on_label_index_changed, **common_options) self.cb_label_value.setModel(self.labelvar_model) gui.checkBox( gui.indentedBox(box), self, 'label_only_selected', 'Label only selected points', callback=self._on_label_index_changed) form = QtGui.QFormLayout( labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft, fieldGrowthPolicy=QtGui.QFormLayout.AllNonFixedFieldsGrow, verticalSpacing=10 ) form.addRow("Symbol size:", gui.hSlider(box, self, "symbol_size", minValue=1, maxValue=20, callback=self._on_size_index_changed, createLabel=False)) form.addRow("Symbol opacity:", gui.hSlider(box, self, "symbol_opacity", minValue=100, maxValue=255, step=100, callback=self._on_color_index_changed, createLabel=False)) form.addRow("Show similar pairs:", gui.hSlider( gui.hBox(self.controlArea), self, "connected_pairs", minValue=0, maxValue=20, createLabel=False, callback=self._on_connected_changed)) form.addRow("Jitter:", gui.comboBox( box, self, "jitter", items=[text for text, _ in self.JitterAmount], callback=self._update_plot)) box.layout().addLayout(form) gui.rubber(self.controlArea) box = QtGui.QGroupBox("Zoom/Select", ) box.setLayout(QtGui.QHBoxLayout()) box.layout().setContentsMargins(2, 2, 2, 2) group = QtGui.QActionGroup(self, exclusive=True) def icon(name): path = "icons/Dlg_{}.png".format(name) path = pkg_resources.resource_filename(widget.__name__, path) return QtGui.QIcon(path) action_select = QtGui.QAction( "Select", self, checkable=True, checked=True, icon=icon("arrow"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_1)) action_zoom = QtGui.QAction( "Zoom", self, checkable=True, checked=False, icon=icon("zoom"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_2)) action_pan = QtGui.QAction( "Pan", self, checkable=True, checked=False, icon=icon("pan_hand"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_3)) action_reset_zoom = QtGui.QAction( "Zoom to fit", self, icon=icon("zoom_reset"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_0)) action_reset_zoom.triggered.connect( lambda: self.plot.autoRange(padding=0.1, items=[self._scatter_item])) group.addAction(action_select) group.addAction(action_zoom) group.addAction(action_pan) self.addActions(group.actions() + [action_reset_zoom]) action_select.setChecked(True) def button(action): b = QtGui.QToolButton() b.setToolButtonStyle(Qt.ToolButtonIconOnly) b.setDefaultAction(action) return b box.layout().addWidget(button(action_select)) box.layout().addWidget(button(action_zoom)) box.layout().addWidget(button(action_pan)) box.layout().addSpacing(4) box.layout().addWidget(button(action_reset_zoom)) box.layout().addStretch() self.controlArea.layout().addWidget(box) box = gui.vBox(self.controlArea, "Output") self.output_combo = gui.comboBox( box, self, "output_embedding_role", items=["Original features only", "Coordinates only", "Coordinates as features", "Coordinates as meta attributes"], callback=self._invalidate_output, addSpace=4) gui.auto_commit(box, self, "autocommit", "Send Selected", checkbox_label="Send selected automatically", box=None) self.plot = pg.PlotWidget(background="w", enableMenu=False) self.plot.setAspectLocked(True) self.plot.getPlotItem().hideAxis("bottom") self.plot.getPlotItem().hideAxis("left") self.plot.getPlotItem().hideButtons() self.plot.setRenderHint(QtGui.QPainter.Antialiasing) self.mainArea.layout().addWidget(self.plot) self.selection_tool = PlotSelectionTool(parent=self) self.zoom_tool = PlotZoomTool(parent=self) self.pan_tool = PlotPanTool(parent=self) self.pinch_tool = PlotPinchZoomTool(parent=self) self.pinch_tool.setViewBox(self.plot.getViewBox()) self.selection_tool.setViewBox(self.plot.getViewBox()) self.selection_tool.selectionFinished.connect(self.__selection_end) self.current_tool = self.selection_tool def activate_tool(action): self.current_tool.setViewBox(None) if action is action_select: active, cur = self.selection_tool, Qt.ArrowCursor elif action is action_zoom: active, cur = self.zoom_tool, Qt.ArrowCursor elif action is action_pan: active, cur = self.pan_tool, Qt.OpenHandCursor self.current_tool = active self.current_tool.setViewBox(self.plot.getViewBox()) self.plot.getViewBox().setCursor(QtGui.QCursor(cur)) group.triggered[QtGui.QAction].connect(activate_tool) self._initialize()
class OWMDS(widget.OWWidget): name = "MDS" description = "Two-dimensional data projection by multidimensional " \ "scaling constructed from a distance matrix." icon = "icons/MDS.svg" inputs = [("Data", Orange.data.Table, "set_data"), ("Distances", Orange.misc.DistMatrix, "set_disimilarity")] outputs = [("Data", Orange.data.Table, widget.Default), ("Data Subset", Orange.data.Table)] #: Initialization type PCA, Random = 0, 1 #: Refresh rate RefreshRate = [("Every iteration", 1), ("Every 5 steps", 5), ("Every 10 steps", 10), ("Every 25 steps", 25), ("Every 50 steps", 50), ("None", -1)] #: Runtime state Running, Finished, Waiting = 1, 2, 3 settingsHandler = settings.DomainContextHandler() max_iter = settings.Setting(300) initialization = settings.Setting(PCA) refresh_rate = settings.Setting(3) # output embedding role. NoRole, AttrRole, MetaRole = 0, 1, 2 output_embedding_role = settings.Setting(1) autocommit = settings.Setting(True) color_index = settings.ContextSetting(0, not_attribute=True) shape_index = settings.ContextSetting(0, not_attribute=True) size_index = settings.ContextSetting(0, not_attribute=True) label_index = settings.ContextSetting(0, not_attribute=True) symbol_size = settings.Setting(8) symbol_opacity = settings.Setting(230) def __init__(self, parent=None): super().__init__(parent) self.matrix = None self.data = None self.matrix_data = None self.signal_data = None self._pen_data = None self._shape_data = None self._size_data = None self._label_data = None self._scatter_item = None self._selection_item = None self._selection_mask = None self._invalidated = False self._effective_matrix = None self.__update_loop = None self.__state = OWMDS.Waiting self.__in_next_step = False box = gui.widgetBox(self.controlArea, "MDS Optimization") form = QtGui.QFormLayout( labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft, fieldGrowthPolicy=QtGui.QFormLayout.AllNonFixedFieldsGrow, ) form.addRow("Max iterations:", gui.spin(box, self, "max_iter", 10, 10**4, step=1)) form.addRow( "Initialization", gui.comboBox(box, self, "initialization", items=["PCA (Torgerson)", "Random"], callback=self.__invalidate_embedding)) box.layout().addLayout(form) form.addRow( "Refresh", gui.comboBox(box, self, "refresh_rate", items=[t for t, _ in OWMDS.RefreshRate], callback=self.__invalidate_refresh)) self.runbutton = gui.button(box, self, "Run", callback=self._toggle_run) box = gui.widgetBox(self.controlArea, "Graph") self.colorvar_model = itemmodels.VariableListModel() cb = gui.comboBox(box, self, "color_index", box="Color", callback=self._on_color_index_changed) cb.setModel(self.colorvar_model) cb.box.setFlat(True) self.shapevar_model = itemmodels.VariableListModel() cb = gui.comboBox(box, self, "shape_index", box="Shape", callback=self._on_shape_index_changed) cb.setModel(self.shapevar_model) cb.box.setFlat(True) self.sizevar_model = itemmodels.VariableListModel() cb = gui.comboBox(box, self, "size_index", "Size", callback=self._on_size_index_changed) cb.setModel(self.sizevar_model) cb.box.setFlat(True) self.labelvar_model = itemmodels.VariableListModel() cb = gui.comboBox(box, self, "label_index", "Label", callback=self._on_label_index_changed) cb.setModel(self.labelvar_model) cb.box.setFlat(True) form = QtGui.QFormLayout( labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft, fieldGrowthPolicy=QtGui.QFormLayout.AllNonFixedFieldsGrow, ) form.addRow( "Symbol size", gui.hSlider(box, self, "symbol_size", minValue=1, maxValue=20, callback=self._on_size_index_changed, createLabel=False)) form.addRow( "Symbol opacity", gui.hSlider(box, self, "symbol_opacity", minValue=100, maxValue=255, step=100, callback=self._on_color_index_changed, createLabel=False)) box.layout().addLayout(form) box = QtGui.QGroupBox("Zoom/Select", ) box.setLayout(QtGui.QHBoxLayout()) group = QtGui.QActionGroup(self, exclusive=True) def icon(name): path = "icons/Dlg_{}.png".format(name) path = pkg_resources.resource_filename(widget.__name__, path) return QtGui.QIcon(path) action_select = QtGui.QAction( "Select", self, checkable=True, checked=True, icon=icon("arrow"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_1)) action_zoom = QtGui.QAction( "Zoom", self, checkable=True, checked=False, icon=icon("zoom"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_2)) action_pan = QtGui.QAction( "Pan", self, checkable=True, checked=False, icon=icon("pan_hand"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_3)) action_reset_zoom = QtGui.QAction( "Zoom to fit", self, icon=icon("zoom_reset"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_0)) action_reset_zoom.triggered.connect(lambda: self.plot.autoRange()) group.addAction(action_select) group.addAction(action_zoom) group.addAction(action_pan) self.addActions(group.actions() + [action_reset_zoom]) action_select.setChecked(True) def button(action): b = QtGui.QToolButton() b.setToolButtonStyle(Qt.ToolButtonIconOnly) b.setDefaultAction(action) return b box.layout().addWidget(button(action_select)) box.layout().addWidget(button(action_zoom)) box.layout().addWidget(button(action_pan)) box.layout().addSpacing(4) box.layout().addWidget(button(action_reset_zoom)) box.layout().addStretch() self.controlArea.layout().addWidget(box) gui.rubber(self.controlArea) box = gui.widgetBox(self.controlArea, "Output") cb = gui.comboBox(box, self, "output_embedding_role", box="Append coordinates", items=["Do not append", "As attributes", "As metas"], callback=self._invalidate_output) cb.box.setFlat(True) gui.auto_commit(box, self, "autocommit", "Send data", checkbox_label="Send after any change", box=None) self.plot = pg.PlotWidget(background="w", enableMenu=False) self.mainArea.layout().addWidget(self.plot) self.selection_tool = PlotSelectionTool( parent=self, selectionMode=PlotSelectionTool.Lasso) self.zoom_tool = PlotZoomTool(parent=self) self.pan_tool = PlotPanTool(parent=self) self.pinch_tool = PlotPinchZoomTool(parent=self) self.pinch_tool.setViewBox(self.plot.getViewBox()) self.selection_tool.setViewBox(self.plot.getViewBox()) self.selection_tool.selectionFinished.connect(self.__selection_end) self.current_tool = self.selection_tool def activate_tool(action): self.current_tool.setViewBox(None) if action is action_select: active, cur = self.selection_tool, Qt.ArrowCursor elif action is action_zoom: active, cur = self.zoom_tool, Qt.ArrowCursor elif action is action_pan: active, cur = self.pan_tool, Qt.OpenHandCursor self.current_tool = active self.current_tool.setViewBox(self.plot.getViewBox()) self.plot.getViewBox().setCursor(QtGui.QCursor(cur)) group.triggered[QtGui.QAction].connect(activate_tool) def set_data(self, data): self.signal_data = data if self.matrix and data is not None and len( self.matrix.X) == len(data): self.closeContext() self.data = data self.update_controls() self.openContext(data) else: self._invalidated = True self._selection_mask = None def set_disimilarity(self, matrix): self.matrix = matrix if matrix and matrix.row_items: self.matrix_data = matrix.row_items if matrix is None: self.matrix_data = None self._invalidated = True self._selection_mask = None def _clear(self): self._pen_data = None self._shape_data = None self._size_data = None self._label_data = None self.colorvar_model[:] = ["Same color"] self.shapevar_model[:] = ["Same shape"] self.sizevar_model[:] = ["Same size"] self.labelvar_model[:] = ["No labels"] self.color_index = 0 self.shape_index = 0 self.size_index = 0 self.label_index = 0 self.__set_update_loop(None) self.__state = OWMDS.Waiting def update_controls(self): if getattr(self.matrix, 'axis', 1) == 0: # Column-wise distances attr = "Attribute names" self.labelvar_model[:] = ["No labels", attr] self.shapevar_model[:] = ["Same shape", attr] self.colorvar_model[:] = ["Same color", attr] self.color_index = list(self.colorvar_model).index(attr) self.shape_index = list(self.shapevar_model).index(attr) else: # initialize the graph state from data domain = self.data.domain all_vars = list(domain.variables + domain.metas) cd_vars = [var for var in all_vars if var.is_primitive()] disc_vars = [var for var in all_vars if var.is_discrete] cont_vars = [var for var in all_vars if var.is_continuous] str_vars = [ var for var in all_vars if var.is_discrete or var.is_string ] def set_separator(model, index): index = model.index(index, 0) model.setData(index, "separator", Qt.AccessibleDescriptionRole) model.setData(index, Qt.NoItemFlags, role="flags") self.colorvar_model[:] = ["Same color", ""] + cd_vars set_separator(self.colorvar_model, 1) self.shapevar_model[:] = ["Same shape", ""] + disc_vars set_separator(self.shapevar_model, 1) self.sizevar_model[:] = ["Same size", "Stress", ""] + cont_vars set_separator(self.sizevar_model, 2) self.labelvar_model[:] = ["No labels", ""] + str_vars set_separator(self.labelvar_model, 1) if domain.class_var is not None: self.color_index = list(self.colorvar_model).index( domain.class_var) def _initialize(self): # clear everything self.closeContext() self._clear() self.data = None self._effective_matrix = None self.embedding = None # if no data nor matrix is present reset plot if self.signal_data is None and self.matrix is None: self._update_plot() return if self.signal_data and self.matrix_data and len( self.signal_data) != len(self.matrix_data): self.error(1, "Data and distances dimensions do not match.") self._update_plot() return self.error(1) if self.signal_data: self.data = self.signal_data elif self.matrix_data: self.data = self.matrix_data if self.matrix: self._effective_matrix = self.matrix if self.matrix.axis == 0: self.data = None else: self._effective_matrix = Orange.distance.Euclidean(self.data) self.update_controls() self.openContext(self.data) def _toggle_run(self): if self.__state == OWMDS.Running: self.stop() self._invalidate_output() else: self.start() def start(self): if self.__state == OWMDS.Running: return elif self.__state == OWMDS.Finished: # Resume/continue from a previous run self.__start() elif self.__state == OWMDS.Waiting and \ self._effective_matrix is not None: self.__start() def stop(self): if self.__state == OWMDS.Running: self.__set_update_loop(None) def __start(self): X = self._effective_matrix.X if self.embedding is not None: init = self.embedding elif self.initialization == OWMDS.PCA: init = torgerson(X, n_components=2) else: init = None # number of iterations per single GUI update step _, step_size = OWMDS.RefreshRate[self.refresh_rate] if step_size == -1: step_size = self.max_iter def update_loop(X, max_iter, step, init): """ return an iterator over successive improved MDS point embeddings. """ # NOTE: this code MUST NOT call into QApplication.processEvents done = False iterations_done = 0 oldstress = numpy.finfo(numpy.float).max while not done: step_iter = min(max_iter - iterations_done, step) mds = Orange.projection.MDS(dissimilarity="precomputed", n_components=2, n_init=1, max_iter=step_iter) mdsfit = mds.fit(X, init=init) iterations_done += step_iter embedding, stress = mdsfit.embedding_, mdsfit.stress_ stress /= numpy.sqrt(numpy.sum(embedding**2, axis=1)).sum() if iterations_done >= max_iter: done = True elif (oldstress - stress) < mds.params["eps"]: done = True init = embedding oldstress = stress yield embedding, mdsfit.stress_, iterations_done / max_iter self.__set_update_loop(update_loop(X, self.max_iter, step_size, init)) self.progressBarInit(processEvents=None) def __set_update_loop(self, loop): """ Set the update `loop` coroutine. The `loop` is a generator yielding `(embedding, stress, progress)` tuples where `embedding` is a `(N, 2) ndarray` of current updated MDS points, `stress` is the current stress and `progress` a float ratio (0 <= progress <= 1) If an existing update loop is already in palace it is interrupted (closed). .. note:: The `loop` must not explicitly yield control flow to the event loop (i.e. call `QApplication.proceesEvents`) """ if self.__update_loop is not None: self.__update_loop.close() self.__update_loop = None self.progressBarFinished(processEvents=None) self.__update_loop = loop if loop is not None: self.progressBarInit(processEvents=None) self.setStatusMessage("Running") self.runbutton.setText("Stop") self.__state = OWMDS.Running QtGui.QApplication.postEvent(self, QEvent(QEvent.User)) else: self.setStatusMessage("") self.runbutton.setText("Start") self.__state = OWMDS.Finished def __next_step(self): if self.__update_loop is None: return loop = self.__update_loop try: embedding, stress, progress = next(self.__update_loop) assert self.__update_loop is loop except StopIteration: self.__set_update_loop(None) self.unconditional_commit() else: self.progressBarSet(100.0 * progress, processEvents=None) self.embedding = embedding self._update_plot() # schedule next update QtGui.QApplication.postEvent(self, QEvent(QEvent.User), Qt.LowEventPriority) def customEvent(self, event): if event.type() == QEvent.User and self.__update_loop is not None: if not self.__in_next_step: self.__in_next_step = True try: self.__next_step() finally: self.__in_next_step = False else: warnings.warn( "Re-entry in update loop detected. " "A rogue `proccessEvents` is on the loose.", RuntimeWarning) # re-schedule the update iteration. QtGui.QApplication.postEvent(self, QEvent(QEvent.User)) return super().customEvent(event) def __invalidate_embedding(self): state = self.__state if self.__update_loop is not None: self.__set_update_loop(None) X = self._effective_matrix.X if self.initialization == OWMDS.PCA: self.embedding = torgerson(X) else: self.embedding = numpy.random.rand(len(X), 2) self._update_plot() # restart the optimization if it was interrupted. if state == OWMDS.Running: self.__start() def __invalidate_refresh(self): state = self.__state if self.__update_loop is not None: self.__set_update_loop(None) # restart the optimization if it was interrupted. # TODO: decrease the max iteration count by the already # completed iterations count. if state == OWMDS.Running: self.__start() def handleNewSignals(self): if self._invalidated: self._invalidated = False self._initialize() self.start() self._update_plot() self.unconditional_commit() def _invalidate_output(self): self.commit() def _on_color_index_changed(self): self._pen_data = None self._update_plot() def _on_shape_index_changed(self): self._shape_data = None self._update_plot() def _on_size_index_changed(self): self._size_data = None self._update_plot() def _on_label_index_changed(self): self._label_data = None self._update_plot() def _update_plot(self): self.plot.clear() self._scatter_item = None self._selection_item = None if self.embedding is not None: self._setup_plot() def _setup_plot(self): have_data = self.data is not None have_matrix_transposed = self.matrix is not None and not self.matrix.axis def column(data, variable): a, _ = data.get_column_view(variable) return a.ravel() def attributes(matrix): return matrix.row_items.domain.attributes def scale(a): dmin, dmax = numpy.nanmin(a), numpy.nanmax(a) if dmax - dmin > 0: return (a - dmin) / (dmax - dmin) else: return numpy.zeros_like(a) if self._pen_data is None: if self._selection_mask is not None: pointflags = numpy.where(self._selection_mask, mdsplotutils.Selected, mdsplotutils.NoFlags) else: pointflags = None if have_data and self.color_index > 0: color_var = self.colorvar_model[self.color_index] if color_var.is_discrete: palette = colorpalette.ColorPaletteGenerator( len(color_var.values)) else: palette = None color_data = mdsplotutils.color_data( self.data, color_var, plotstyle=mdsplotutils.plotstyle) color_data = numpy.hstack((color_data, numpy.full((len(color_data), 1), self.symbol_opacity))) pen_data = mdsplotutils.pen_data(color_data, pointflags) elif have_matrix_transposed and self.colorvar_model[ self.color_index] == 'Attribute names': attr = attributes(self.matrix) palette = colorpalette.ColorPaletteGenerator(len(attr)) color_data = [palette.getRGB(i) for i in range(len(attr))] color_data = numpy.hstack( color_data, numpy.full((len(color_data), 1), self.symbol_opacity)) pen_data = mdsplotutils.pen_data(color_data, pointflags) else: pen_data = make_pen(QtGui.QColor(Qt.darkGray), cosmetic=True) pen_data = numpy.full(len(self.data), pen_data, dtype=object) self._pen_data = pen_data if self._shape_data is None: if have_data and self.shape_index > 0: Symbols = ScatterPlotItem.Symbols symbols = numpy.array(list(Symbols.keys())) shape_var = self.shapevar_model[self.shape_index] data = column(self.data, shape_var) data = data % (len(Symbols) - 1) data[numpy.isnan(data)] = len(Symbols) - 1 shape_data = symbols[data.astype(int)] elif have_matrix_transposed and self.shapevar_model[ self.shape_index] == 'Attribute names': Symbols = ScatterPlotItem.Symbols symbols = numpy.array(list(Symbols.keys())) attr = [ i % (len(Symbols) - 1) for i, _ in enumerate(attributes(self.matrix)) ] shape_data = symbols[attr] else: shape_data = "o" self._shape_data = shape_data if self._size_data is None: MinPointSize = 3 point_size = self.symbol_size + MinPointSize if have_data and self.size_index == 1: # size by stress size_data = stress(self.embedding, self._effective_matrix.X) size_data = scale(size_data) size_data = MinPointSize + size_data * point_size elif have_data and self.size_index > 0: size_var = self.sizevar_model[self.size_index] size_data = column(self.data, size_var) size_data = scale(size_data) size_data = MinPointSize + size_data * point_size else: size_data = point_size if self._label_data is None: if have_data and self.label_index > 0: label_var = self.labelvar_model[self.label_index] label_data = column(self.data, label_var) label_data = [label_var.repr_val(val) for val in label_data] label_items = [ pg.TextItem(text, anchor=(0.5, 0)) for text in label_data ] elif have_matrix_transposed and self.labelvar_model[ self.label_index] == 'Attribute names': attr = attributes(self.matrix) label_items = [ pg.TextItem(str(text), anchor=(0.5, 0)) for text in attr ] else: label_items = None self._label_data = label_items self._scatter_item = item = ScatterPlotItem( x=self.embedding[:, 0], y=self.embedding[:, 1], pen=self._pen_data, symbol=self._shape_data, brush=QtGui.QBrush(Qt.transparent), size=size_data, data=numpy.arange(len(self.data)), antialias=True) self.plot.addItem(item) if self._label_data is not None: for (x, y), text_item in zip(self.embedding, self._label_data): self.plot.addItem(text_item) text_item.setPos(x, y) def commit(self): if self.embedding is not None: output = embedding = Orange.data.Table.from_numpy( Orange.data.Domain([ Orange.data.ContinuousVariable("X"), Orange.data.ContinuousVariable("Y") ]), self.embedding) else: output = embedding = None if self.embedding is not None and self.data is not None: domain = self.data.domain attrs = domain.attributes class_vars = domain.class_vars metas = domain.metas if self.output_embedding_role == OWMDS.AttrRole: attrs = attrs + embedding.domain.attributes elif self.output_embedding_role == OWMDS.MetaRole: metas = metas + embedding.domain.attributes domain = Orange.data.Domain(attrs, class_vars, metas) output = Orange.data.Table.from_table(domain, self.data) if self.output_embedding_role == OWMDS.AttrRole: output.X[:, -2:] = embedding.X elif self.output_embedding_role == OWMDS.MetaRole: output.metas[:, -2:] = embedding.X self.send("Data", output) if output is not None and self._selection_mask is not None and \ numpy.any(self._selection_mask): subset = output[self._selection_mask] else: subset = None self.send("Data Subset", subset) def onDeleteWidget(self): super().onDeleteWidget() self.plot.clear() self._selection_item = self._scatter_item = None self._clear() def __selection_end(self, path): self.select(path) self._pen_data = None self._update_plot() self._invalidate_output() def select(self, region): item = self._scatter_item if item is None: return indices = numpy.array([ spot.data() for spot in item.points() if region.contains(spot.pos()) ], dtype=int) if not QtGui.QApplication.keyboardModifiers() & Qt.ControlModifier: self._selection_mask = None self.select_indices(indices) def select_indices(self, indices): if self.data is None: return if self._selection_mask is None: self._selection_mask = numpy.zeros(len(self.data), dtype=bool) self._selection_mask[indices] = True
def __init__(self, parent=None): super().__init__(parent) self.matrix = None self.data = None self.matrix_data = None self.signal_data = None self._pen_data = None self._shape_data = None self._size_data = None self._label_data = None self._scatter_item = None self._selection_item = None self._selection_mask = None self._invalidated = False self._effective_matrix = None self.__update_loop = None self.__state = OWMDS.Waiting self.__in_next_step = False box = gui.widgetBox(self.controlArea, "MDS Optimization") form = QtGui.QFormLayout( labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft, fieldGrowthPolicy=QtGui.QFormLayout.AllNonFixedFieldsGrow, ) form.addRow("Max iterations:", gui.spin(box, self, "max_iter", 10, 10**4, step=1)) form.addRow( "Initialization", gui.comboBox(box, self, "initialization", items=["PCA (Torgerson)", "Random"], callback=self.__invalidate_embedding)) box.layout().addLayout(form) form.addRow( "Refresh", gui.comboBox(box, self, "refresh_rate", items=[t for t, _ in OWMDS.RefreshRate], callback=self.__invalidate_refresh)) self.runbutton = gui.button(box, self, "Run", callback=self._toggle_run) box = gui.widgetBox(self.controlArea, "Graph") self.colorvar_model = itemmodels.VariableListModel() cb = gui.comboBox(box, self, "color_index", box="Color", callback=self._on_color_index_changed) cb.setModel(self.colorvar_model) cb.box.setFlat(True) self.shapevar_model = itemmodels.VariableListModel() cb = gui.comboBox(box, self, "shape_index", box="Shape", callback=self._on_shape_index_changed) cb.setModel(self.shapevar_model) cb.box.setFlat(True) self.sizevar_model = itemmodels.VariableListModel() cb = gui.comboBox(box, self, "size_index", "Size", callback=self._on_size_index_changed) cb.setModel(self.sizevar_model) cb.box.setFlat(True) self.labelvar_model = itemmodels.VariableListModel() cb = gui.comboBox(box, self, "label_index", "Label", callback=self._on_label_index_changed) cb.setModel(self.labelvar_model) cb.box.setFlat(True) form = QtGui.QFormLayout( labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft, fieldGrowthPolicy=QtGui.QFormLayout.AllNonFixedFieldsGrow, ) form.addRow( "Symbol size", gui.hSlider(box, self, "symbol_size", minValue=1, maxValue=20, callback=self._on_size_index_changed, createLabel=False)) form.addRow( "Symbol opacity", gui.hSlider(box, self, "symbol_opacity", minValue=100, maxValue=255, step=100, callback=self._on_color_index_changed, createLabel=False)) box.layout().addLayout(form) box = QtGui.QGroupBox("Zoom/Select", ) box.setLayout(QtGui.QHBoxLayout()) group = QtGui.QActionGroup(self, exclusive=True) def icon(name): path = "icons/Dlg_{}.png".format(name) path = pkg_resources.resource_filename(widget.__name__, path) return QtGui.QIcon(path) action_select = QtGui.QAction( "Select", self, checkable=True, checked=True, icon=icon("arrow"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_1)) action_zoom = QtGui.QAction( "Zoom", self, checkable=True, checked=False, icon=icon("zoom"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_2)) action_pan = QtGui.QAction( "Pan", self, checkable=True, checked=False, icon=icon("pan_hand"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_3)) action_reset_zoom = QtGui.QAction( "Zoom to fit", self, icon=icon("zoom_reset"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_0)) action_reset_zoom.triggered.connect(lambda: self.plot.autoRange()) group.addAction(action_select) group.addAction(action_zoom) group.addAction(action_pan) self.addActions(group.actions() + [action_reset_zoom]) action_select.setChecked(True) def button(action): b = QtGui.QToolButton() b.setToolButtonStyle(Qt.ToolButtonIconOnly) b.setDefaultAction(action) return b box.layout().addWidget(button(action_select)) box.layout().addWidget(button(action_zoom)) box.layout().addWidget(button(action_pan)) box.layout().addSpacing(4) box.layout().addWidget(button(action_reset_zoom)) box.layout().addStretch() self.controlArea.layout().addWidget(box) gui.rubber(self.controlArea) box = gui.widgetBox(self.controlArea, "Output") cb = gui.comboBox(box, self, "output_embedding_role", box="Append coordinates", items=["Do not append", "As attributes", "As metas"], callback=self._invalidate_output) cb.box.setFlat(True) gui.auto_commit(box, self, "autocommit", "Send data", checkbox_label="Send after any change", box=None) self.plot = pg.PlotWidget(background="w", enableMenu=False) self.mainArea.layout().addWidget(self.plot) self.selection_tool = PlotSelectionTool( parent=self, selectionMode=PlotSelectionTool.Lasso) self.zoom_tool = PlotZoomTool(parent=self) self.pan_tool = PlotPanTool(parent=self) self.pinch_tool = PlotPinchZoomTool(parent=self) self.pinch_tool.setViewBox(self.plot.getViewBox()) self.selection_tool.setViewBox(self.plot.getViewBox()) self.selection_tool.selectionFinished.connect(self.__selection_end) self.current_tool = self.selection_tool def activate_tool(action): self.current_tool.setViewBox(None) if action is action_select: active, cur = self.selection_tool, Qt.ArrowCursor elif action is action_zoom: active, cur = self.zoom_tool, Qt.ArrowCursor elif action is action_pan: active, cur = self.pan_tool, Qt.OpenHandCursor self.current_tool = active self.current_tool.setViewBox(self.plot.getViewBox()) self.plot.getViewBox().setCursor(QtGui.QCursor(cur)) group.triggered[QtGui.QAction].connect(activate_tool)
class OWMDS(widget.OWWidget): name = "MDS" description = "Two-dimensional data projection by multidimensional " \ "scaling constructed from a distance matrix." icon = "icons/MDS.svg" inputs = [("Data", Orange.data.Table, "set_data"), ("Distances", Orange.misc.DistMatrix, "set_disimilarity")] outputs = [("Data", Orange.data.Table, widget.Default), ("Data Subset", Orange.data.Table)] #: Initialization type PCA, Random = 0, 1 #: Refresh rate RefreshRate = [ ("Every iteration", 1), ("Every 5 steps", 5), ("Every 10 steps", 10), ("Every 25 steps", 25), ("Every 50 steps", 50), ("None", -1) ] #: Runtime state Running, Finished, Waiting = 1, 2, 3 settingsHandler = settings.DomainContextHandler() max_iter = settings.Setting(300) initialization = settings.Setting(PCA) refresh_rate = settings.Setting(3) # output embedding role. NoRole, AttrRole, MetaRole = 0, 1, 2 output_embedding_role = settings.Setting(1) autocommit = settings.Setting(True) color_index = settings.ContextSetting(0, not_attribute=True) shape_index = settings.ContextSetting(0, not_attribute=True) size_index = settings.ContextSetting(0, not_attribute=True) label_index = settings.ContextSetting(0, not_attribute=True) symbol_size = settings.Setting(8) symbol_opacity = settings.Setting(230) legend_anchor = settings.Setting(((1, 0), (1, 0))) def __init__(self, parent=None): super().__init__(parent) self.matrix = None self.data = None self.matrix_data = None self.signal_data = None self._pen_data = None self._shape_data = None self._size_data = None self._label_data = None self._scatter_item = None self._legend_item = None self._selection_mask = None self._invalidated = False self._effective_matrix = None self.__update_loop = None self.__state = OWMDS.Waiting self.__in_next_step = False box = gui.widgetBox(self.controlArea, "MDS Optimization") form = QtGui.QFormLayout( labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft, fieldGrowthPolicy=QtGui.QFormLayout.AllNonFixedFieldsGrow, ) form.addRow("Max iterations:", gui.spin(box, self, "max_iter", 10, 10 ** 4, step=1)) form.addRow("Initialization", gui.comboBox(box, self, "initialization", items=["PCA (Torgerson)", "Random"], callback=self.__invalidate_embedding)) box.layout().addLayout(form) form.addRow("Refresh", gui.comboBox( box, self, "refresh_rate", items=[t for t, _ in OWMDS.RefreshRate], callback=self.__invalidate_refresh)) self.runbutton = gui.button( box, self, "Run", callback=self._toggle_run) box = gui.widgetBox(self.controlArea, "Graph") self.colorvar_model = itemmodels.VariableListModel() cb = gui.comboBox(box, self, "color_index", box="Color", callback=self._on_color_index_changed) cb.setModel(self.colorvar_model) cb.box.setFlat(True) self.shapevar_model = itemmodels.VariableListModel() cb = gui.comboBox(box, self, "shape_index", box="Shape", callback=self._on_shape_index_changed) cb.setModel(self.shapevar_model) cb.box.setFlat(True) self.sizevar_model = itemmodels.VariableListModel() cb = gui.comboBox(box, self, "size_index", "Size", callback=self._on_size_index_changed) cb.setModel(self.sizevar_model) cb.box.setFlat(True) self.labelvar_model = itemmodels.VariableListModel() cb = gui.comboBox(box, self, "label_index", "Label", callback=self._on_label_index_changed) cb.setModel(self.labelvar_model) cb.box.setFlat(True) form = QtGui.QFormLayout( labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft, fieldGrowthPolicy=QtGui.QFormLayout.AllNonFixedFieldsGrow, ) form.addRow("Symbol size", gui.hSlider(box, self, "symbol_size", minValue=1, maxValue=20, callback=self._on_size_index_changed, createLabel=False)) form.addRow("Symbol opacity", gui.hSlider(box, self, "symbol_opacity", minValue=100, maxValue=255, step=100, callback=self._on_color_index_changed, createLabel=False)) box.layout().addLayout(form) box = QtGui.QGroupBox("Zoom/Select", ) box.setLayout(QtGui.QHBoxLayout()) group = QtGui.QActionGroup(self, exclusive=True) def icon(name): path = "icons/Dlg_{}.png".format(name) path = pkg_resources.resource_filename(widget.__name__, path) return QtGui.QIcon(path) action_select = QtGui.QAction( "Select", self, checkable=True, checked=True, icon=icon("arrow"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_1)) action_zoom = QtGui.QAction( "Zoom", self, checkable=True, checked=False, icon=icon("zoom"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_2)) action_pan = QtGui.QAction( "Pan", self, checkable=True, checked=False, icon=icon("pan_hand"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_3)) action_reset_zoom = QtGui.QAction( "Zoom to fit", self, icon=icon("zoom_reset"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_0)) action_reset_zoom.triggered.connect( lambda: self.plot.autoRange()) group.addAction(action_select) group.addAction(action_zoom) group.addAction(action_pan) self.addActions(group.actions() + [action_reset_zoom]) action_select.setChecked(True) def button(action): b = QtGui.QToolButton() b.setToolButtonStyle(Qt.ToolButtonIconOnly) b.setDefaultAction(action) return b box.layout().addWidget(button(action_select)) box.layout().addWidget(button(action_zoom)) box.layout().addWidget(button(action_pan)) box.layout().addSpacing(4) box.layout().addWidget(button(action_reset_zoom)) box.layout().addStretch() self.controlArea.layout().addWidget(box) gui.rubber(self.controlArea) box = gui.widgetBox(self.controlArea, "Output") cb = gui.comboBox(box, self, "output_embedding_role", box="Append coordinates", items=["Do not append", "As attributes", "As metas"], callback=self._invalidate_output) cb.box.setFlat(True) gui.auto_commit(box, self, "autocommit", "Send data", checkbox_label="Send after any change", box=None) self.plot = pg.PlotWidget(background="w", enableMenu=False) self.mainArea.layout().addWidget(self.plot) self.selection_tool = PlotSelectionTool( parent=self, selectionMode=PlotSelectionTool.Lasso) self.zoom_tool = PlotZoomTool(parent=self) self.pan_tool = PlotPanTool(parent=self) self.pinch_tool = PlotPinchZoomTool(parent=self) self.pinch_tool.setViewBox(self.plot.getViewBox()) self.selection_tool.setViewBox(self.plot.getViewBox()) self.selection_tool.selectionFinished.connect(self.__selection_end) self.current_tool = self.selection_tool def activate_tool(action): self.current_tool.setViewBox(None) if action is action_select: active, cur = self.selection_tool, Qt.ArrowCursor elif action is action_zoom: active, cur = self.zoom_tool, Qt.ArrowCursor elif action is action_pan: active, cur = self.pan_tool, Qt.OpenHandCursor self.current_tool = active self.current_tool.setViewBox(self.plot.getViewBox()) self.plot.getViewBox().setCursor(QtGui.QCursor(cur)) group.triggered[QtGui.QAction].connect(activate_tool) def set_data(self, data): self.signal_data = data if self.matrix and data is not None and len(self.matrix.X) == len(data): self.closeContext() self.data = data self.update_controls() self.openContext(data) else: self._invalidated = True self._selection_mask = None def set_disimilarity(self, matrix): self.matrix = matrix if matrix and matrix.row_items: self.matrix_data = matrix.row_items if matrix is None: self.matrix_data = None self._invalidated = True self._selection_mask = None def _clear(self): self._pen_data = None self._shape_data = None self._size_data = None self._label_data = None self.colorvar_model[:] = ["Same color"] self.shapevar_model[:] = ["Same shape"] self.sizevar_model[:] = ["Same size"] self.labelvar_model[:] = ["No labels"] self.color_index = 0 self.shape_index = 0 self.size_index = 0 self.label_index = 0 self.__set_update_loop(None) self.__state = OWMDS.Waiting def _clear_plot(self): self.plot.clear() self._scatter_item = None if self._legend_item is not None: anchor = legend_anchor_pos(self._legend_item) if anchor is not None: self.legend_anchor = anchor if self._legend_item.scene() is not None: self._legend_item.scene().removeItem(self._legend_item) self._legend_item = None def update_controls(self): if getattr(self.matrix, 'axis', 1) == 0: # Column-wise distances attr = "Attribute names" self.labelvar_model[:] = ["No labels", attr] self.shapevar_model[:] = ["Same shape", attr] self.colorvar_model[:] = ["Same color", attr] self.color_index = list(self.colorvar_model).index(attr) self.shape_index = list(self.shapevar_model).index(attr) else: # initialize the graph state from data domain = self.data.domain all_vars = list(domain.variables + domain.metas) cd_vars = [var for var in all_vars if var.is_primitive()] disc_vars = [var for var in all_vars if var.is_discrete] cont_vars = [var for var in all_vars if var.is_continuous] str_vars = [var for var in all_vars if var.is_discrete or var.is_string] self.colorvar_model[:] = chain(["Same color"], [self.colorvar_model.Separator], cd_vars) self.shapevar_model[:] = chain(["Same shape"], [self.shapevar_model.Separator], disc_vars) self.sizevar_model[:] = chain(["Same size", "Stress"], [self.sizevar_model.Separator], cont_vars) self.labelvar_model[:] = chain(["No labels"], [self.labelvar_model.Separator], str_vars) if domain.class_var is not None: self.color_index = list(self.colorvar_model).index(domain.class_var) def _initialize(self): # clear everything self.closeContext() self._clear() self.data = None self._effective_matrix = None self.embedding = None # if no data nor matrix is present reset plot if self.signal_data is None and self.matrix is None: self._update_plot() return if self.signal_data and self.matrix_data and len(self.signal_data) != len(self.matrix_data): self.error(1, "Data and distances dimensions do not match.") self._update_plot() return self.error(1) if self.signal_data: self.data = self.signal_data elif self.matrix_data: self.data = self.matrix_data if self.matrix: self._effective_matrix = self.matrix if self.matrix.axis == 0: self.data = None else: self._effective_matrix = Orange.distance.Euclidean(self.data) self.update_controls() self.openContext(self.data) def _toggle_run(self): if self.__state == OWMDS.Running: self.stop() self._invalidate_output() else: self.start() def start(self): if self.__state == OWMDS.Running: return elif self.__state == OWMDS.Finished: # Resume/continue from a previous run self.__start() elif self.__state == OWMDS.Waiting and \ self._effective_matrix is not None: self.__start() def stop(self): if self.__state == OWMDS.Running: self.__set_update_loop(None) def __start(self): X = self._effective_matrix.X if self.embedding is not None: init = self.embedding elif self.initialization == OWMDS.PCA: init = torgerson(X, n_components=2) else: init = None # number of iterations per single GUI update step _, step_size = OWMDS.RefreshRate[self.refresh_rate] if step_size == -1: step_size = self.max_iter def update_loop(X, max_iter, step, init): """ return an iterator over successive improved MDS point embeddings. """ # NOTE: this code MUST NOT call into QApplication.processEvents done = False iterations_done = 0 oldstress = numpy.finfo(numpy.float).max while not done: step_iter = min(max_iter - iterations_done, step) mds = Orange.projection.MDS( dissimilarity="precomputed", n_components=2, n_init=1, max_iter=step_iter) mdsfit = mds.fit(X, init=init) iterations_done += step_iter embedding, stress = mdsfit.embedding_, mdsfit.stress_ stress /= numpy.sqrt(numpy.sum(embedding ** 2, axis=1)).sum() if iterations_done >= max_iter: done = True elif (oldstress - stress) < mds.params["eps"]: done = True init = embedding oldstress = stress yield embedding, mdsfit.stress_, iterations_done / max_iter self.__set_update_loop(update_loop(X, self.max_iter, step_size, init)) self.progressBarInit(processEvents=None) def __set_update_loop(self, loop): """ Set the update `loop` coroutine. The `loop` is a generator yielding `(embedding, stress, progress)` tuples where `embedding` is a `(N, 2) ndarray` of current updated MDS points, `stress` is the current stress and `progress` a float ratio (0 <= progress <= 1) If an existing update loop is already in palace it is interrupted (closed). .. note:: The `loop` must not explicitly yield control flow to the event loop (i.e. call `QApplication.proceesEvents`) """ if self.__update_loop is not None: self.__update_loop.close() self.__update_loop = None self.progressBarFinished(processEvents=None) self.__update_loop = loop if loop is not None: self.progressBarInit(processEvents=None) self.setStatusMessage("Running") self.runbutton.setText("Stop") self.__state = OWMDS.Running QtGui.QApplication.postEvent(self, QEvent(QEvent.User)) else: self.setStatusMessage("") self.runbutton.setText("Start") self.__state = OWMDS.Finished def __next_step(self): if self.__update_loop is None: return loop = self.__update_loop try: embedding, stress, progress = next(self.__update_loop) assert self.__update_loop is loop except StopIteration: self.__set_update_loop(None) self.unconditional_commit() else: self.progressBarSet(100.0 * progress, processEvents=None) self.embedding = embedding self._update_plot() # schedule next update QtGui.QApplication.postEvent( self, QEvent(QEvent.User), Qt.LowEventPriority) def customEvent(self, event): if event.type() == QEvent.User and self.__update_loop is not None: if not self.__in_next_step: self.__in_next_step = True try: self.__next_step() finally: self.__in_next_step = False else: warnings.warn( "Re-entry in update loop detected. " "A rogue `proccessEvents` is on the loose.", RuntimeWarning) # re-schedule the update iteration. QtGui.QApplication.postEvent(self, QEvent(QEvent.User)) return super().customEvent(event) def __invalidate_embedding(self): state = self.__state if self.__update_loop is not None: self.__set_update_loop(None) X = self._effective_matrix.X if self.initialization == OWMDS.PCA: self.embedding = torgerson(X) else: self.embedding = numpy.random.rand(len(X), 2) self._update_plot() # restart the optimization if it was interrupted. if state == OWMDS.Running: self.__start() def __invalidate_refresh(self): state = self.__state if self.__update_loop is not None: self.__set_update_loop(None) # restart the optimization if it was interrupted. # TODO: decrease the max iteration count by the already # completed iterations count. if state == OWMDS.Running: self.__start() def handleNewSignals(self): if self._invalidated: self._invalidated = False self._initialize() self.start() self._update_plot() self.unconditional_commit() def _invalidate_output(self): self.commit() def _on_color_index_changed(self): self._pen_data = None self._update_plot() def _on_shape_index_changed(self): self._shape_data = None self._update_plot() def _on_size_index_changed(self): self._size_data = None self._update_plot() def _on_label_index_changed(self): self._label_data = None self._update_plot() def _update_plot(self): self._clear_plot() if self.embedding is not None: self._setup_plot() def _setup_plot(self): have_data = self.data is not None have_matrix_transposed = self.matrix is not None and not self.matrix.axis def column(data, variable): a, _ = data.get_column_view(variable) return a.ravel() def attributes(matrix): return matrix.row_items.domain.attributes def scale(a): dmin, dmax = numpy.nanmin(a), numpy.nanmax(a) if dmax - dmin > 0: return (a - dmin) / (dmax - dmin) else: return numpy.zeros_like(a) if self._pen_data is None: if self._selection_mask is not None: pointflags = numpy.where( self._selection_mask, mdsplotutils.Selected, mdsplotutils.NoFlags) else: pointflags = None if have_data and self.color_index > 0: color_var = self.colorvar_model[self.color_index] if color_var.is_discrete: palette = colorpalette.ColorPaletteGenerator( len(color_var.values) ) else: palette = None color_data = mdsplotutils.color_data( self.data, color_var, plotstyle=mdsplotutils.plotstyle) color_data = numpy.hstack( (color_data, numpy.full((len(color_data), 1), self.symbol_opacity)) ) pen_data = mdsplotutils.pen_data(color_data, pointflags) elif have_matrix_transposed and self.colorvar_model[self.color_index] == 'Attribute names': attr = attributes(self.matrix) palette = colorpalette.ColorPaletteGenerator(len(attr)) color_data = [palette.getRGB(i) for i in range(len(attr))] color_data = numpy.hstack( color_data, numpy.full((len(color_data), 1), self.symbol_opacity) ) pen_data = mdsplotutils.pen_data(color_data, pointflags) else: pen_data = make_pen(QtGui.QColor(Qt.darkGray), cosmetic=True) pen_data = numpy.full(len(self.data), pen_data, dtype=object) self._pen_data = pen_data if self._shape_data is None: if have_data and self.shape_index > 0: Symbols = ScatterPlotItem.Symbols symbols = numpy.array(list(Symbols.keys())) shape_var = self.shapevar_model[self.shape_index] data = column(self.data, shape_var) data = data % (len(Symbols) - 1) data[numpy.isnan(data)] = len(Symbols) - 1 shape_data = symbols[data.astype(int)] elif have_matrix_transposed and self.shapevar_model[self.shape_index] == 'Attribute names': Symbols = ScatterPlotItem.Symbols symbols = numpy.array(list(Symbols.keys())) attr = [i % (len(Symbols) - 1) for i, _ in enumerate(attributes(self.matrix))] shape_data = symbols[attr] else: shape_data = "o" self._shape_data = shape_data if self._size_data is None: MinPointSize = 3 point_size = self.symbol_size + MinPointSize if have_data and self.size_index == 1: # size by stress size_data = stress(self.embedding, self._effective_matrix.X) size_data = scale(size_data) size_data = MinPointSize + size_data * point_size elif have_data and self.size_index > 0: size_var = self.sizevar_model[self.size_index] size_data = column(self.data, size_var) size_data = scale(size_data) size_data = MinPointSize + size_data * point_size else: size_data = point_size if self._label_data is None: if have_data and self.label_index > 0: label_var = self.labelvar_model[self.label_index] label_data = column(self.data, label_var) label_data = [label_var.repr_val(val) for val in label_data] label_items = [pg.TextItem(text, anchor=(0.5, 0)) for text in label_data] elif have_matrix_transposed and self.labelvar_model[self.label_index] == 'Attribute names': attr = attributes(self.matrix) label_items = [pg.TextItem(str(text), anchor=(0.5, 0)) for text in attr] else: label_items = None self._label_data = label_items self._scatter_item = item = ScatterPlotItem( x=self.embedding[:, 0], y=self.embedding[:, 1], pen=self._pen_data, symbol=self._shape_data, brush=QtGui.QBrush(Qt.transparent), size=size_data, data=numpy.arange(len(self.data)), antialias=True ) self.plot.addItem(item) if self._label_data is not None: for (x, y), text_item in zip(self.embedding, self._label_data): self.plot.addItem(text_item) text_item.setPos(x, y) self._legend_item = LegendItem() self._legend_item.setParentItem(self.plot.getViewBox()) self._legend_item.anchor(*self.legend_anchor) color_var = shape_var = None if have_data and 1 <= self.color_index < len(self.colorvar_model): color_var = self.colorvar_model[self.color_index] assert isinstance(color_var, Orange.data.Variable) if have_data and 1 <= self.shape_index < len(self.shapevar_model): shape_var = self.shapevar_model[self.shape_index] assert isinstance(shape_var, Orange.data.Variable) if shape_var is not None or \ (color_var is not None and color_var.is_discrete): legend_data = mdsplotutils.legend_data( color_var, shape_var, plotstyle=mdsplotutils.plotstyle) for color, symbol, text in legend_data: self._legend_item.addItem( ScatterPlotItem(pen=color, brush=color, symbol=symbol, size=10), text ) else: self._legend_item.hide() def commit(self): if self.embedding is not None: output = embedding = Orange.data.Table.from_numpy( Orange.data.Domain([Orange.data.ContinuousVariable("X"), Orange.data.ContinuousVariable("Y")]), self.embedding ) else: output = embedding = None if self.embedding is not None and self.data is not None: domain = self.data.domain attrs = domain.attributes class_vars = domain.class_vars metas = domain.metas if self.output_embedding_role == OWMDS.AttrRole: attrs = attrs + embedding.domain.attributes elif self.output_embedding_role == OWMDS.MetaRole: metas = metas + embedding.domain.attributes domain = Orange.data.Domain(attrs, class_vars, metas) output = Orange.data.Table.from_table(domain, self.data) if self.output_embedding_role == OWMDS.AttrRole: output.X[:, -2:] = embedding.X elif self.output_embedding_role == OWMDS.MetaRole: output.metas[:, -2:] = embedding.X self.send("Data", output) if output is not None and self._selection_mask is not None and \ numpy.any(self._selection_mask): subset = output[self._selection_mask] else: subset = None self.send("Data Subset", subset) def onDeleteWidget(self): super().onDeleteWidget() self._clear_plot() self._clear() def __selection_end(self, path): self.select(path) self._pen_data = None self._update_plot() self._invalidate_output() def select(self, region): item = self._scatter_item if item is None: return indices = numpy.array( [spot.data() for spot in item.points() if region.contains(spot.pos())], dtype=int) if not QtGui.QApplication.keyboardModifiers() & Qt.ControlModifier: self._selection_mask = None self.select_indices(indices) def select_indices(self, indices): if self.data is None: return if self._selection_mask is None: self._selection_mask = numpy.zeros(len(self.data), dtype=bool) self._selection_mask[indices] = True
def __init__(self, parent=None): super().__init__(parent) self.matrix = None self.data = None self.matrix_data = None self.signal_data = None self._pen_data = None self._shape_data = None self._size_data = None self._label_data = None self._scatter_item = None self._legend_item = None self._selection_mask = None self._invalidated = False self._effective_matrix = None self.__update_loop = None self.__state = OWMDS.Waiting self.__in_next_step = False box = gui.widgetBox(self.controlArea, "MDS Optimization") form = QtGui.QFormLayout( labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft, fieldGrowthPolicy=QtGui.QFormLayout.AllNonFixedFieldsGrow, ) form.addRow("Max iterations:", gui.spin(box, self, "max_iter", 10, 10 ** 4, step=1)) form.addRow("Initialization", gui.comboBox(box, self, "initialization", items=["PCA (Torgerson)", "Random"], callback=self.__invalidate_embedding)) box.layout().addLayout(form) form.addRow("Refresh", gui.comboBox( box, self, "refresh_rate", items=[t for t, _ in OWMDS.RefreshRate], callback=self.__invalidate_refresh)) self.runbutton = gui.button( box, self, "Run", callback=self._toggle_run) box = gui.widgetBox(self.controlArea, "Graph") self.colorvar_model = itemmodels.VariableListModel() cb = gui.comboBox(box, self, "color_index", box="Color", callback=self._on_color_index_changed) cb.setModel(self.colorvar_model) cb.box.setFlat(True) self.shapevar_model = itemmodels.VariableListModel() cb = gui.comboBox(box, self, "shape_index", box="Shape", callback=self._on_shape_index_changed) cb.setModel(self.shapevar_model) cb.box.setFlat(True) self.sizevar_model = itemmodels.VariableListModel() cb = gui.comboBox(box, self, "size_index", "Size", callback=self._on_size_index_changed) cb.setModel(self.sizevar_model) cb.box.setFlat(True) self.labelvar_model = itemmodels.VariableListModel() cb = gui.comboBox(box, self, "label_index", "Label", callback=self._on_label_index_changed) cb.setModel(self.labelvar_model) cb.box.setFlat(True) form = QtGui.QFormLayout( labelAlignment=Qt.AlignLeft, formAlignment=Qt.AlignLeft, fieldGrowthPolicy=QtGui.QFormLayout.AllNonFixedFieldsGrow, ) form.addRow("Symbol size", gui.hSlider(box, self, "symbol_size", minValue=1, maxValue=20, callback=self._on_size_index_changed, createLabel=False)) form.addRow("Symbol opacity", gui.hSlider(box, self, "symbol_opacity", minValue=100, maxValue=255, step=100, callback=self._on_color_index_changed, createLabel=False)) box.layout().addLayout(form) box = QtGui.QGroupBox("Zoom/Select", ) box.setLayout(QtGui.QHBoxLayout()) group = QtGui.QActionGroup(self, exclusive=True) def icon(name): path = "icons/Dlg_{}.png".format(name) path = pkg_resources.resource_filename(widget.__name__, path) return QtGui.QIcon(path) action_select = QtGui.QAction( "Select", self, checkable=True, checked=True, icon=icon("arrow"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_1)) action_zoom = QtGui.QAction( "Zoom", self, checkable=True, checked=False, icon=icon("zoom"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_2)) action_pan = QtGui.QAction( "Pan", self, checkable=True, checked=False, icon=icon("pan_hand"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_3)) action_reset_zoom = QtGui.QAction( "Zoom to fit", self, icon=icon("zoom_reset"), shortcut=QtGui.QKeySequence(Qt.ControlModifier + Qt.Key_0)) action_reset_zoom.triggered.connect( lambda: self.plot.autoRange()) group.addAction(action_select) group.addAction(action_zoom) group.addAction(action_pan) self.addActions(group.actions() + [action_reset_zoom]) action_select.setChecked(True) def button(action): b = QtGui.QToolButton() b.setToolButtonStyle(Qt.ToolButtonIconOnly) b.setDefaultAction(action) return b box.layout().addWidget(button(action_select)) box.layout().addWidget(button(action_zoom)) box.layout().addWidget(button(action_pan)) box.layout().addSpacing(4) box.layout().addWidget(button(action_reset_zoom)) box.layout().addStretch() self.controlArea.layout().addWidget(box) gui.rubber(self.controlArea) box = gui.widgetBox(self.controlArea, "Output") cb = gui.comboBox(box, self, "output_embedding_role", box="Append coordinates", items=["Do not append", "As attributes", "As metas"], callback=self._invalidate_output) cb.box.setFlat(True) gui.auto_commit(box, self, "autocommit", "Send data", checkbox_label="Send after any change", box=None) self.plot = pg.PlotWidget(background="w", enableMenu=False) self.mainArea.layout().addWidget(self.plot) self.selection_tool = PlotSelectionTool( parent=self, selectionMode=PlotSelectionTool.Lasso) self.zoom_tool = PlotZoomTool(parent=self) self.pan_tool = PlotPanTool(parent=self) self.pinch_tool = PlotPinchZoomTool(parent=self) self.pinch_tool.setViewBox(self.plot.getViewBox()) self.selection_tool.setViewBox(self.plot.getViewBox()) self.selection_tool.selectionFinished.connect(self.__selection_end) self.current_tool = self.selection_tool def activate_tool(action): self.current_tool.setViewBox(None) if action is action_select: active, cur = self.selection_tool, Qt.ArrowCursor elif action is action_zoom: active, cur = self.zoom_tool, Qt.ArrowCursor elif action is action_pan: active, cur = self.pan_tool, Qt.OpenHandCursor self.current_tool = active self.current_tool.setViewBox(self.plot.getViewBox()) self.plot.getViewBox().setCursor(QtGui.QCursor(cur)) group.triggered[QtGui.QAction].connect(activate_tool)