Пример #1
0
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
Пример #2
0
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()),))
Пример #3
0
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