Beispiel #1
0
class OWScatterPlotGraph(gui.OWComponent, ScaleScatterPlotData):
    attr_color = ContextSetting(None, required=ContextSetting.OPTIONAL)
    attr_label = ContextSetting(None, required=ContextSetting.OPTIONAL)
    attr_shape = ContextSetting(None, required=ContextSetting.OPTIONAL)
    attr_size = ContextSetting(None, required=ContextSetting.OPTIONAL)
    label_only_selected = Setting(False)

    point_width = Setting(10)
    alpha_value = Setting(128)
    show_grid = Setting(False)
    show_legend = Setting(True)
    tooltip_shows_all = Setting(False)
    class_density = Setting(False)
    show_reg_line = Setting(False)
    resolution = 256

    CurveSymbols = np.array("o x t + d s t2 t3 p h star ?".split())
    MinShapeSize = 6
    DarkerValue = 120
    UnknownColor = (168, 50, 168)

    def __init__(self,
                 scatter_widget,
                 parent=None,
                 _="None",
                 view_box=InteractiveViewBox):
        gui.OWComponent.__init__(self, scatter_widget)
        self.view_box = view_box(self)
        self.plot_widget = pg.PlotWidget(viewBox=self.view_box,
                                         parent=parent,
                                         background="w")
        self.plot_widget.getPlotItem().buttonsHidden = True
        self.plot_widget.setAntialiasing(True)
        self.plot_widget.sizeHint = lambda: QSize(500, 500)
        scene = self.plot_widget.scene()
        self._create_drag_tooltip(scene)
        self._data = None  # Original Table as passed from widget to new_data before transformations

        self.replot = self.plot_widget.replot
        ScaleScatterPlotData.__init__(self)
        self.density_img = None
        self.scatterplot_item = None
        self.scatterplot_item_sel = None
        self.reg_line_item = None

        self.labels = []

        self.master = scatter_widget
        self.master.Warning.add_message(
            "missing_coords",
            "Plot cannot be displayed because '{}' or '{}' is missing for "
            "all data points")
        self.master.Information.add_message(
            "missing_coords",
            "Points with missing '{}' or '{}' are not displayed")
        self.master.Information.add_message(
            "missing_size",
            "Points with undefined '{}' are shown in smaller size")
        self.master.Information.add_message(
            "missing_shape",
            "Points with undefined '{}' are shown as crossed circles")
        self.shown_attribute_indices = []
        self.shown_x = self.shown_y = None
        self.pen_colors = self.brush_colors = None

        self.valid_data = None  # np.ndarray
        self.selection = None  # np.ndarray
        self.n_points = 0

        self.gui = OWPlotGUI(self)
        self.continuous_palette = ContinuousPaletteGenerator(
            QColor(255, 255, 0), QColor(0, 0, 255), True)
        self.discrete_palette = ColorPaletteGenerator()

        self.selection_behavior = 0

        self.legend = self.color_legend = None
        self.__legend_anchor = (1, 0), (1, 0)
        self.__color_legend_anchor = (1, 1), (1, 1)

        self.scale = None  # DiscretizedScale

        self.subset_indices = None

        # self.setMouseTracking(True)
        # self.grabGesture(QPinchGesture)
        # self.grabGesture(QPanGesture)

        self.update_grid()

        self._tooltip_delegate = HelpEventDelegate(self.help_event)
        self.plot_widget.scene().installEventFilter(self._tooltip_delegate)

    def _create_drag_tooltip(self, scene):
        tip_parts = [(Qt.ShiftModifier, "Shift: Add group"),
                     (Qt.ShiftModifier + Qt.ControlModifier,
                      "Shift-{}: Append to group".format(
                          "Cmd" if sys.platform == "darwin" else "Ctrl")),
                     (Qt.AltModifier, "Alt: Remove")]
        all_parts = ", ".join(part for _, part in tip_parts)
        self.tiptexts = {
            int(modifier): all_parts.replace(part, "<b>{}</b>".format(part))
            for modifier, part in tip_parts
        }
        self.tiptexts[0] = all_parts

        self.tip_textitem = text = QGraphicsTextItem()
        # Set to the longest text
        text.setHtml(self.tiptexts[Qt.ShiftModifier + Qt.ControlModifier])
        text.setPos(4, 2)
        r = text.boundingRect()
        rect = QGraphicsRectItem(0, 0, r.width() + 8, r.height() + 4)
        rect.setBrush(QColor(224, 224, 224, 212))
        rect.setPen(QPen(Qt.NoPen))
        self.update_tooltip(Qt.NoModifier)

        scene.drag_tooltip = scene.createItemGroup([rect, text])
        scene.drag_tooltip.hide()

    def update_tooltip(self, modifiers):
        modifiers &= Qt.ShiftModifier + Qt.ControlModifier + Qt.AltModifier
        text = self.tiptexts.get(int(modifiers), self.tiptexts[0])
        self.tip_textitem.setHtml(text)

    def new_data(self, data, subset_data=None, new=True, **args):
        if new:
            self.plot_widget.clear()
            self.remove_legend()

            self.density_img = None
            self.scatterplot_item = None
            self.scatterplot_item_sel = None
            self.reg_line_item = None
            self.labels = []
            self.selection = None
            self.valid_data = None

        self.subset_indices = set(
            e.id for e in subset_data) if subset_data else None

        self._data = data
        data = self.sparse_to_dense()
        self.set_data(data, **args)

    def set_domain(self, data):
        domain = data.domain if data and len(data) else None
        for attr in ("attr_color", "attr_shape", "attr_size", "attr_label"):
            getattr(self.controls, attr).model().set_domain(domain)
            setattr(self, attr, None)
        if domain is not None:
            self.attr_color = domain.class_var

    def sparse_to_dense(self):
        data = self._data
        if data is None or not data.is_sparse():
            return data

        attrs = {
            self.shown_x, self.shown_y, self.attr_color, self.attr_shape,
            self.attr_size, self.attr_label
        }
        domain = data.domain
        all_attrs = domain.variables + domain.metas
        attrs = list(set(all_attrs) & attrs)
        selected_data = data[:, attrs].to_dense()
        return selected_data

    def _clear_plot_widget(self):
        self.remove_legend()
        if self.density_img:
            self.plot_widget.removeItem(self.density_img)
            self.density_img = None
        if self.scatterplot_item:
            self.plot_widget.removeItem(self.scatterplot_item)
            self.scatterplot_item = None
        if self.scatterplot_item_sel:
            self.plot_widget.removeItem(self.scatterplot_item_sel)
            self.scatterplot_item_sel = None
        if self.reg_line_item:
            self.plot_widget.removeItem(self.reg_line_item)
            self.reg_line_item = None
        for label in self.labels:
            self.plot_widget.removeItem(label)
        self.labels = []
        self.set_axis_title("bottom", "")
        self.set_axis_title("left", "")

    def update_data(self, attr_x, attr_y, reset_view=True):
        self.master.Warning.missing_coords.clear()
        self.master.Information.missing_coords.clear()
        self._clear_plot_widget()

        if self.shown_y != attr_y:
            # 'reset' the axis text width estimation. Without this the left
            # axis tick labels space only ever expands
            yaxis = self.plot_widget.getAxis("left")
            yaxis.textWidth = 30

        self.shown_x, self.shown_y = attr_x, attr_y
        if attr_x not in self.data.domain or attr_y not in self.data.domain:
            data = self.sparse_to_dense()
            self.set_data(data)

        if self.jittered_data is None or not len(self.jittered_data):
            self.valid_data = None
        else:
            self.valid_data = self.get_valid_list([attr_x, attr_y])
            if not np.any(self.valid_data):
                self.valid_data = None
        if self.valid_data is None:
            self.selection = None
            self.n_points = 0
            self.master.Warning.missing_coords(self.shown_x.name,
                                               self.shown_y.name)
            return

        x_data, y_data = self.get_xy_data_positions(attr_x, attr_y,
                                                    self.valid_data)
        self.n_points = len(x_data)

        if reset_view:
            min_x, max_x = np.nanmin(x_data), np.nanmax(x_data)
            min_y, max_y = np.nanmin(y_data), np.nanmax(y_data)
            self.view_box.setRange(QRectF(min_x, min_y, max_x - min_x,
                                          max_y - min_y),
                                   padding=0.025)
            self.view_box.init_history()
            self.view_box.tag_history()
        [min_x, max_x], [min_y, max_y] = self.view_box.viewRange()

        for axis, var in (("bottom", attr_x), ("left", attr_y)):
            self.set_axis_title(axis, var)
            if var.is_discrete:
                self.set_labels(axis, get_variable_values_sorted(var))
            else:
                self.set_labels(axis, None)

        color_data, brush_data = self.compute_colors()
        color_data_sel, brush_data_sel = self.compute_colors_sel()
        size_data = self.compute_sizes()
        shape_data = self.compute_symbols()

        if self.should_draw_density():
            rgb_data = [pen.color().getRgb()[:3] for pen in color_data]
            self.density_img = classdensity.class_density_image(
                min_x, max_x, min_y, max_y, self.resolution, x_data, y_data,
                rgb_data)
            self.plot_widget.addItem(self.density_img)

        self.data_indices = np.flatnonzero(self.valid_data)
        if len(self.data_indices) != len(self.data):
            self.master.Information.missing_coords(self.shown_x.name,
                                                   self.shown_y.name)

        self.scatterplot_item = ScatterPlotItem(x=x_data,
                                                y=y_data,
                                                data=self.data_indices,
                                                symbol=shape_data,
                                                size=size_data,
                                                pen=color_data,
                                                brush=brush_data)
        self.scatterplot_item_sel = ScatterPlotItem(x=x_data,
                                                    y=y_data,
                                                    data=self.data_indices,
                                                    symbol=shape_data,
                                                    size=size_data +
                                                    SELECTION_WIDTH,
                                                    pen=color_data_sel,
                                                    brush=brush_data_sel)
        self.plot_widget.addItem(self.scatterplot_item_sel)
        self.plot_widget.addItem(self.scatterplot_item)

        self.scatterplot_item.selected_points = []
        self.scatterplot_item.sigClicked.connect(self.select_by_click)
        if self.show_reg_line:
            _x_data = self.data.get_column_view(self.shown_x)[0]
            _y_data = self.data.get_column_view(self.shown_y)[0]
            _x_data = _x_data[self.valid_data]
            _y_data = _y_data[self.valid_data]
            assert _x_data.size
            assert _y_data.size
            self.draw_regression_line(_x_data, _y_data, np.min(_x_data),
                                      np.max(_y_data))

        self.update_labels()
        self.make_legend()
        self.plot_widget.replot()

    def draw_regression_line(self, x_data, y_data, min_x, max_x):
        if self.show_reg_line and self.can_draw_regresssion_line():
            slope, intercept, rvalue, _, _ = linregress(x_data, y_data)
            start_y = min_x * slope + intercept
            end_y = max_x * slope + intercept
            angle = np.degrees(np.arctan((end_y - start_y) / (max_x - min_x)))
            rotate = ((angle + 45) % 180) - 45 > 90
            color = QColor("#505050")
            l_opts = dict(color=color,
                          position=abs(int(rotate) - 0.85),
                          rotateAxis=(1, 0),
                          movable=True)
            self.reg_line_item = InfiniteLine(
                pos=QPointF(min_x, start_y),
                pen=pg.mkPen(color=color, width=1),
                angle=angle,
                label="r = {:.2f}".format(rvalue),
                labelOpts=l_opts)
            if rotate:
                self.reg_line_item.label.angle = 180
                self.reg_line_item.label.updateTransform()
            self.plot_widget.addItem(self.reg_line_item)

    def can_draw_density(self):
        return self.domain is not None and \
            self.attr_color is not None and \
            self.attr_color.is_discrete and \
            self.shown_x.is_continuous and \
            self.shown_y.is_continuous

    def should_draw_density(self):
        return self.class_density and self.n_points > 1 and self.can_draw_density(
        )

    def can_draw_regresssion_line(self):
        return self.domain is not None and \
               self.shown_x.is_continuous and \
               self.shown_y.is_continuous

    def set_labels(self, axis, labels):
        axis = self.plot_widget.getAxis(axis)
        if labels:
            ticks = [[(i, labels[i]) for i in range(len(labels))]]
            axis.setTicks(ticks)
        else:
            axis.setTicks(None)

    def set_axis_title(self, axis, title):
        self.plot_widget.setLabel(axis=axis, text=title)

    def compute_sizes(self):
        self.master.Information.missing_size.clear()
        if self.attr_size is None:
            size_data = np.full((self.n_points, ),
                                self.point_width,
                                dtype=float)
        else:
            size_data = \
                self.MinShapeSize + \
                self.scaled_data.get_column_view(self.attr_size)[0][self.valid_data] * \
                self.point_width
        nans = np.isnan(size_data)
        if np.any(nans):
            size_data[nans] = self.MinShapeSize - 2
            self.master.Information.missing_size(self.attr_size)
        return size_data

    def update_sizes(self):
        self.set_data(self.sparse_to_dense())
        self.update_point_size()

    def update_point_size(self):
        if self.scatterplot_item:
            size_data = self.compute_sizes()
            self.scatterplot_item.setSize(size_data)
            self.scatterplot_item_sel.setSize(size_data + SELECTION_WIDTH)

    def get_color(self):
        if self.attr_color is None:
            return None
        colors = self.attr_color.colors
        if self.attr_color.is_discrete:
            self.discrete_palette = ColorPaletteGenerator(
                number_of_colors=min(len(colors), MAX),
                rgb_colors=colors if len(colors) <= MAX else DefaultRGBColors)
        else:
            self.continuous_palette = ContinuousPaletteGenerator(*colors)
        return self.attr_color

    def compute_colors_sel(self, keep_colors=False):
        if not keep_colors:
            self.pen_colors_sel = self.brush_colors_sel = None

        nopen = QPen(Qt.NoPen)
        if self.selection is not None:
            sels = np.max(self.selection)
            if sels == 1:
                pens = [
                    nopen,
                    _make_pen(QColor(255, 190, 0, 255), SELECTION_WIDTH + 1.)
                ]
            else:
                # Start with the first color so that the colors of the
                # additional attribute in annotation (which start with 0,
                # unselected) will match these colors
                palette = ColorPaletteGenerator(number_of_colors=sels + 1)
                pens = [nopen] + \
                       [_make_pen(palette[i + 1], SELECTION_WIDTH + 1.)
                        for i in range(sels)]
            pen = [pens[a] for a in self.selection[self.valid_data]]
        else:
            pen = [nopen] * self.n_points
        brush = [QBrush(QColor(255, 255, 255, 0))] * self.n_points
        return pen, brush

    def _reduce_values(self, attr):
        """
        If discrete variable has more than maximium allowed values,
        less used values are joined as "Other"
        """
        c_data = self.data.get_column_view(attr)[0][self.valid_data]
        if attr.is_continuous or len(attr.values) <= MAX:
            return None, c_data
        values_to_replace = Counter(c_data)
        values_to_replace = sorted(values_to_replace,
                                   key=values_to_replace.get,
                                   reverse=True)
        return values_to_replace, c_data

    def _get_values(self, attr):
        if len(attr.values) <= MAX:
            return attr.values
        values_to_replace, _ = self._reduce_values(attr)
        return [
            attr.values[int(i)] for i in values_to_replace if not np.isnan(i)
        ][:MAX - 1] + ["Other"]

    def _get_data(self, attr):
        values_to_replace, c_data = self._reduce_values(attr)
        if values_to_replace is not None:
            c_data_2 = c_data.copy()
            for i, v in enumerate(values_to_replace):
                c_data[c_data_2 == v] = i if i < MAX - 1 else MAX - 1
        return c_data

    def compute_colors(self, keep_colors=False):
        if not keep_colors:
            self.pen_colors = self.brush_colors = None
        self.get_color()

        subset = None
        if self.subset_indices:
            subset = np.array([
                ex.id in self.subset_indices
                for ex in self.data[self.valid_data]
            ])

        if self.attr_color is None:  # same color
            color = self.plot_widget.palette().color(OWPalette.Data)
            pen = [_make_pen(color, 1.5)] * self.n_points
            if subset is not None:
                brush = [(QBrush(QColor(128, 128, 128,
                                        0)), QBrush(QColor(128, 128, 128,
                                                           255)))[s]
                         for s in subset]
            else:
                brush = [QBrush(QColor(128, 128, 128, self.alpha_value))] \
                        * self.n_points
            return pen, brush

        c_data = self._get_data(self.attr_color)
        if self.attr_color.is_continuous:
            if self.pen_colors is None:
                self.scale = DiscretizedScale(np.nanmin(c_data),
                                              np.nanmax(c_data))
                c_data -= self.scale.offset
                c_data /= self.scale.width
                c_data = np.floor(c_data) + 0.5
                c_data /= self.scale.bins
                c_data = np.clip(c_data, 0, 1)
                palette = self.continuous_palette
                self.pen_colors = palette.getRGB(c_data)
                self.brush_colors = np.hstack([
                    self.pen_colors,
                    np.full((self.n_points, 1), self.alpha_value, dtype=int)
                ])
                self.pen_colors *= 100
                self.pen_colors //= self.DarkerValue
                self.pen_colors = [
                    _make_pen(QColor(*col), 1.5)
                    for col in self.pen_colors.tolist()
                ]
            if subset is not None:
                self.brush_colors[:, 3] = 0
                self.brush_colors[subset, 3] = 255
            else:
                self.brush_colors[:, 3] = self.alpha_value
            pen = self.pen_colors
            brush = np.array(
                [QBrush(QColor(*col)) for col in self.brush_colors.tolist()])
        else:
            if self.pen_colors is None:
                palette = self.discrete_palette
                n_colors = palette.number_of_colors
                c_data = c_data.copy()
                c_data[np.isnan(c_data)] = n_colors
                c_data = c_data.astype(int)
                colors = np.r_[palette.getRGB(np.arange(n_colors)),
                               [[128, 128, 128]]]
                pens = np.array([
                    _make_pen(QColor(*col).darker(self.DarkerValue), 1.5)
                    for col in colors
                ])
                self.pen_colors = pens[c_data]
                alpha = self.alpha_value if subset is None else 255
                self.brush_colors = np.array([[
                    QBrush(QColor(0, 0, 0, 0)),
                    QBrush(QColor(col[0], col[1], col[2], alpha))
                ] for col in colors])
                self.brush_colors = self.brush_colors[c_data]
            if subset is not None:
                brush = np.where(subset, self.brush_colors[:, 1],
                                 self.brush_colors[:, 0])
            else:
                brush = self.brush_colors[:, 1]
            pen = self.pen_colors
        return pen, brush

    def update_colors(self, keep_colors=False):
        self.master.update_colors()
        self.set_data(self.sparse_to_dense())
        self.update_alpha_value(keep_colors)

    def update_alpha_value(self, keep_colors=False):
        if self.scatterplot_item:
            pen_data, brush_data = self.compute_colors(keep_colors)
            pen_data_sel, brush_data_sel = self.compute_colors_sel(keep_colors)
            self.scatterplot_item.setPen(pen_data, update=False, mask=None)
            self.scatterplot_item.setBrush(brush_data, mask=None)
            self.scatterplot_item_sel.setPen(pen_data_sel,
                                             update=False,
                                             mask=None)
            self.scatterplot_item_sel.setBrush(brush_data_sel, mask=None)
            if not keep_colors:
                self.make_legend()

                if self.should_draw_density():
                    self.update_data(self.shown_x, self.shown_y)
                elif self.density_img:
                    self.plot_widget.removeItem(self.density_img)

    def create_labels(self):
        for x, y in zip(*self.scatterplot_item.getData()):
            ti = TextItem()
            self.plot_widget.addItem(ti)
            ti.setPos(x, y)
            self.labels.append(ti)

    def _create_label_column(self):
        if self.attr_label in self.data.domain:
            label_column = self.data.get_column_view(self.attr_label)[0]
        else:
            label_column = self.master.data.get_column_view(self.attr_label)[0]
        return label_column[self.data_indices]

    def update_labels(self):
        if self.attr_label is None or \
                self.label_only_selected and self.selection is None:
            for label in self.labels:
                label.setText("")
            return
        self.assure_attribute_present(self.attr_label)
        if not self.labels:
            self.create_labels()
        label_column = self._create_label_column()
        formatter = self.attr_label.str_val
        label_data = map(formatter, label_column)
        black = pg.mkColor(0, 0, 0)
        selection = self.selection[
            self.valid_data] if self.selection is not None else []
        if self.label_only_selected:
            for label, text, selected \
                    in zip(self.labels, label_data, selection):
                label.setText(text if selected else "", black)
        else:
            for label, text in zip(self.labels, label_data):
                label.setText(text, black)

    def compute_symbols(self):
        self.master.Information.missing_shape.clear()
        if self.attr_shape is None:
            shape_data = self.CurveSymbols[np.zeros(self.n_points, dtype=int)]
        else:
            shape_data = self._get_data(self.attr_shape)
            nans = np.isnan(shape_data)
            if np.any(nans):
                shape_data[nans] = len(self.CurveSymbols) - 1
                self.master.Information.missing_shape(self.attr_shape)
            shape_data = self.CurveSymbols[shape_data.astype(int)]
        return shape_data

    def update_shapes(self):
        self.assure_attribute_present(self.attr_shape)
        if self.scatterplot_item:
            shape_data = self.compute_symbols()
            self.scatterplot_item.setSymbol(shape_data)
        self.make_legend()

    def assure_attribute_present(self, attr):
        if self.data is not None and attr not in self.data.domain:
            self.set_data(self.sparse_to_dense())

    def update_grid(self):
        self.plot_widget.showGrid(x=self.show_grid, y=self.show_grid)

    def update_legend(self):
        if self.legend:
            self.legend.setVisible(self.show_legend)

    def create_legend(self):
        self.legend = LegendItem()
        self.legend.setParentItem(self.plot_widget.getViewBox())
        self.legend.restoreAnchor(self.__legend_anchor)

    def remove_legend(self):
        if self.legend:
            anchor = legend_anchor_pos(self.legend)
            if anchor is not None:
                self.__legend_anchor = anchor
            self.legend.setParent(None)
            self.legend = None
        if self.color_legend:
            anchor = legend_anchor_pos(self.color_legend)
            if anchor is not None:
                self.__color_legend_anchor = anchor
            self.color_legend.setParent(None)
            self.color_legend = None

    def make_legend(self):
        self.remove_legend()
        self.make_color_legend()
        self.make_shape_legend()
        self.update_legend()

    def make_color_legend(self):
        if self.attr_color is None:
            return
        use_shape = self.attr_shape == self.get_color()
        if self.attr_color.is_discrete:
            if not self.legend:
                self.create_legend()
            palette = self.discrete_palette
            for i, value in enumerate(self._get_values(self.attr_color)):
                color = QColor(*palette.getRGB(i))
                pen = _make_pen(color.darker(self.DarkerValue), 1.5)
                color.setAlpha(
                    self.alpha_value if self.subset_indices is None else 255)
                brush = QBrush(color)
                self.legend.addItem(
                    ScatterPlotItem(
                        pen=pen,
                        brush=brush,
                        size=10,
                        symbol=self.CurveSymbols[i] if use_shape else "o"),
                    escape(value))
        else:
            legend = self.color_legend = LegendItem()
            legend.setParentItem(self.plot_widget.getViewBox())
            legend.restoreAnchor(self.__color_legend_anchor)

            label = PaletteItemSample(self.continuous_palette, self.scale)
            legend.addItem(label, "")
            legend.setGeometry(label.boundingRect())

    def make_shape_legend(self):
        if self.attr_shape is None or self.attr_shape == self.get_color():
            return
        if not self.legend:
            self.create_legend()
        color = QColor(0, 0, 0)
        color.setAlpha(self.alpha_value)
        for i, value in enumerate(self._get_values(self.attr_shape)):
            self.legend.addItem(
                ScatterPlotItem(pen=color,
                                brush=color,
                                size=10,
                                symbol=self.CurveSymbols[i]), escape(value))

    def zoom_button_clicked(self):
        self.plot_widget.getViewBox().setMouseMode(
            self.plot_widget.getViewBox().RectMode)

    def pan_button_clicked(self):
        self.plot_widget.getViewBox().setMouseMode(
            self.plot_widget.getViewBox().PanMode)

    def select_button_clicked(self):
        self.plot_widget.getViewBox().setMouseMode(
            self.plot_widget.getViewBox().RectMode)

    def reset_button_clicked(self):
        self.update_data(self.shown_x, self.shown_y,
                         reset_view=True)  # also redraw density image
        # self.view_box.autoRange()

    def select_by_click(self, _, points):
        if self.scatterplot_item is not None:
            self.select(points)

    def select_by_rectangle(self, value_rect):
        if self.scatterplot_item is not None:
            points = [
                point for point in self.scatterplot_item.points()
                if value_rect.contains(QPointF(point.pos()))
            ]
            self.select(points)

    def unselect_all(self):
        self.selection = None
        self.update_colors(keep_colors=True)
        if self.label_only_selected:
            self.update_labels()
        self.master.selection_changed()

    def select(self, points):
        # noinspection PyArgumentList
        if self.data is None:
            return
        if self.selection is None:
            self.selection = np.zeros(len(self.data), dtype=np.uint8)
        indices = [p.data() for p in points]
        keys = QApplication.keyboardModifiers()
        # Remove from selection
        if keys & Qt.AltModifier:
            self.selection[indices] = 0
        # Append to the last group
        elif keys & Qt.ShiftModifier and keys & Qt.ControlModifier:
            self.selection[indices] = np.max(self.selection)
        # Create a new group
        elif keys & Qt.ShiftModifier:
            self.selection[indices] = np.max(self.selection) + 1
        # No modifiers: new selection
        else:
            self.selection = np.zeros(len(self.data), dtype=np.uint8)
            self.selection[indices] = 1
        self.update_colors(keep_colors=True)
        if self.label_only_selected:
            self.update_labels()
        self.master.selection_changed()

    def get_selection(self):
        if self.selection is None:
            return np.array([], dtype=np.uint8)
        else:
            return np.flatnonzero(self.selection)

    def set_palette(self, p):
        self.plot_widget.setPalette(p)

    def save_to_file(self, size):
        pass

    def help_event(self, event):
        if self.scatterplot_item is None:
            return False

        domain = self.data.domain
        PARTS = (("Class", "Classes", 4, domain.class_vars),
                 ("Meta", "Metas", 4, domain.metas), ("Feature", "Features",
                                                      10, domain.attributes))

        def format_val(var, point_data, bold=False):
            text = escape('{} = {}'.format(var.name, point_data[var]))
            if bold:
                text = "<b>{}</b>".format(text)
            return text

        def show_part(point_data, singular, plural, max_shown, vars):
            cols = [
                format_val(var, point_data) for var in vars[:max_shown + 2]
                if vars == domain.class_vars or var not in (self.shown_x,
                                                            self.shown_y)
            ][:max_shown]
            if not cols:
                return ""
            n_vars = len(vars)
            if n_vars > max_shown:
                cols[-1] = "... and {} others".format(n_vars - max_shown + 1)
            return \
                "<br/><b>{}</b>:<br/>".format(singular if n_vars < 2
                                              else plural) \
                + "<br/>".join(cols)

        def point_data(p):
            point_data = self.data[p.data()]
            text = "<br/>".join(
                format_val(var, point_data, bold=self.tooltip_shows_all)
                for var in (self.shown_x, self.shown_y))
            if self.tooltip_shows_all:
                text += "<br/>" + \
                        "".join(show_part(point_data, *columns)
                                for columns in PARTS)
            return text

        act_pos = self.scatterplot_item.mapFromScene(event.scenePos())
        points = self.scatterplot_item.pointsAt(act_pos)

        if len(points):
            if len(points) > MAX_POINTS_IN_TOOLTIP:
                text = "{} instances<hr/>{}<hr/>...".format(
                    len(points), "<hr/>".join(
                        point_data(point)
                        for point in points[:MAX_POINTS_IN_TOOLTIP]))
            else:
                text = "<hr/>".join(point_data(point) for point in points)

            QToolTip.showText(event.screenPos(), text, widget=self.plot_widget)
            return True
        else:
            return False

    def box_zoom_select(self, parent):
        g = self.gui
        box_zoom_select = gui.vBox(parent, "Zoom/Select")
        zoom_select_toolbar = g.zoom_select_toolbar(box_zoom_select,
                                                    nomargin=True,
                                                    buttons=[
                                                        g.StateButtonsBegin,
                                                        g.SimpleSelect, g.Pan,
                                                        g.Zoom,
                                                        g.StateButtonsEnd,
                                                        g.ZoomReset
                                                    ])
        buttons = zoom_select_toolbar.buttons
        buttons[g.Zoom].clicked.connect(self.zoom_button_clicked)
        buttons[g.Pan].clicked.connect(self.pan_button_clicked)
        buttons[g.SimpleSelect].clicked.connect(self.select_button_clicked)
        buttons[g.ZoomReset].clicked.connect(self.reset_button_clicked)
        return box_zoom_select

    def zoom_actions(self, parent):
        def zoom(s):
            """
            Zoom in/out by factor `s`.
            scaleBy scales the view's bounds (the axis range)
            """
            self.view_box.scaleBy((1 / s, 1 / s))

        def fit_to_view():
            self.viewbox.autoRange()

        zoom_in = QAction("Zoom in", parent, triggered=lambda: zoom(1.25))
        zoom_in.setShortcuts([
            QKeySequence(QKeySequence.ZoomIn),
            QKeySequence(parent.tr("Ctrl+="))
        ])
        zoom_out = QAction("Zoom out",
                           parent,
                           shortcut=QKeySequence.ZoomOut,
                           triggered=lambda: zoom(1 / 1.25))
        zoom_fit = QAction("Fit in view",
                           parent,
                           shortcut=QKeySequence(Qt.ControlModifier
                                                 | Qt.Key_0),
                           triggered=fit_to_view)
        parent.addActions([zoom_in, zoom_out, zoom_fit])
class OWScatterPlotGraph(gui.OWComponent, ScaleScatterPlotData):
    attr_color = ContextSetting("", ContextSetting.OPTIONAL)
    attr_label = ContextSetting("", ContextSetting.OPTIONAL)
    attr_shape = ContextSetting("", ContextSetting.OPTIONAL)
    attr_size = ContextSetting("", ContextSetting.OPTIONAL)

    point_width = Setting(10)
    alpha_value = Setting(128)
    show_grid = Setting(False)
    show_legend = Setting(True)
    tooltip_shows_all = Setting(False)
    class_density = Setting(False)
    resolution = 256

    CurveSymbols = np.array("o x t + d s ?".split())
    MinShapeSize = 6
    DarkerValue = 120
    UnknownColor = (168, 50, 168)

    ID_MISSING_COORDS, ID_MISSING_SIZE, ID_MISSING_SHAPE = range(1, 4)

    def __init__(self, scatter_widget, parent=None, _="None"):
        gui.OWComponent.__init__(self, scatter_widget)
        self.view_box = InteractiveViewBox(self)
        self.plot_widget = pg.PlotWidget(viewBox=self.view_box, parent=parent,
                                         background="w")
        self.plot_widget.getPlotItem().buttonsHidden = True
        self.plot_widget.setAntialiasing(True)
        self.plot_widget.sizeHint = lambda: QtCore.QSize(500, 500)

        self.replot = self.plot_widget.replot
        ScaleScatterPlotData.__init__(self)
        self.density_img = None
        self.scatterplot_item = None
        self.scatterplot_item_sel = None

        self.labels = []

        self.master = scatter_widget
        self.shown_attribute_indices = []
        self.shown_x = ""
        self.shown_y = ""
        self.pen_colors = self.brush_colors = None

        self.valid_data = None  # np.ndarray
        self.selection = None  # np.ndarray
        self.n_points = 0

        self.gui = OWPlotGUI(self)
        self.continuous_palette = ContinuousPaletteGenerator(
            QColor(255, 255, 0), QColor(0, 0, 255), True)
        self.discrete_palette = ColorPaletteGenerator()

        self.selection_behavior = 0

        self.legend = self.color_legend = None
        self.__legend_anchor = (1, 0), (1, 0)
        self.__color_legend_anchor = (1, 1), (1, 1)

        self.scale = None  # DiscretizedScale

        self.subset_indices = None

        # self.setMouseTracking(True)
        # self.grabGesture(QPinchGesture)
        # self.grabGesture(QPanGesture)

        self.update_grid()

        self._tooltip_delegate = HelpEventDelegate(self.help_event)
        self.plot_widget.scene().installEventFilter(self._tooltip_delegate)

    def new_data(self, data, subset_data=None, **args):
        self.plot_widget.clear()

        self.density_img = None
        self.scatterplot_item = None
        self.scatterplot_item_sel = None
        self.labels = []
        self.selection = None
        self.valid_data = None

        self.subset_indices = set(e.id for e in subset_data) if subset_data else None

        self.set_data(data, **args)

    def _clear_plot_widget(self):
        self.remove_legend()
        if self.density_img:
            self.plot_widget.removeItem(self.density_img)
            self.density_img = None
        if self.scatterplot_item:
            self.plot_widget.removeItem(self.scatterplot_item)
            self.scatterplot_item = None
        if self.scatterplot_item_sel:
            self.plot_widget.removeItem(self.scatterplot_item_sel)
            self.scatterplot_item_sel = None
        for label in self.labels:
            self.plot_widget.removeItem(label)
        self.labels = []
        self.set_axis_title("bottom", "")
        self.set_axis_title("left", "")

    def update_data(self, attr_x, attr_y, reset_view=True):
        self.master.warning(self.ID_MISSING_COORDS)
        self.master.information(self.ID_MISSING_COORDS)
        self._clear_plot_widget()

        self.shown_x = attr_x
        self.shown_y = attr_y

        if self.scaled_data is None or not len(self.scaled_data):
            self.valid_data = None
        else:
            index_x = self.attribute_name_index[attr_x]
            index_y = self.attribute_name_index[attr_y]
            self.valid_data = self.get_valid_list([index_x, index_y],
                                                  also_class_if_exists=False)
            if not np.any(self.valid_data):
                self.valid_data = None
        if self.valid_data is None:
            self.selection = None
            self.n_points = 0
            self.master.warning(
                self.ID_MISSING_COORDS,
                "Plot cannot be displayed because '{}' or '{}' is missing for "
                "all data points".format(self.shown_x, self.shown_y))
            return

        x_data, y_data = self.get_xy_data_positions(
            attr_x, attr_y, self.valid_data)
        self.n_points = len(x_data)

        if reset_view:
            min_x, max_x = np.nanmin(x_data), np.nanmax(x_data)
            min_y, max_y = np.nanmin(y_data), np.nanmax(y_data)
            self.view_box.setRange(
                QRectF(min_x, min_y, max_x - min_x, max_y - min_y),
                padding=0.025)
            self.view_box.init_history()
            self.view_box.tag_history()
        [min_x, max_x], [min_y, max_y] = self.view_box.viewRange()

        for axis, name, index in (("bottom", attr_x, index_x),
                                  ("left", attr_y, index_y)):
            self.set_axis_title(axis, name)
            var = self.data_domain[index]
            if var.is_discrete:
                self.set_labels(axis, get_variable_values_sorted(var))
            else:
                self.set_labels(axis, None)

        color_data, brush_data = self.compute_colors()
        color_data_sel, brush_data_sel = self.compute_colors_sel()
        size_data = self.compute_sizes()
        shape_data = self.compute_symbols()

        if self.should_draw_density():
            rgb_data = [pen.color().getRgb()[:3] for pen in color_data]
            self.density_img = classdensity.class_density_image(
                min_x, max_x, min_y, max_y, self.resolution,
                x_data, y_data, rgb_data)
            self.plot_widget.addItem(self.density_img)

        data_indices = np.flatnonzero(self.valid_data)
        if len(data_indices) != self.original_data.shape[1]:
            self.master.information(
                self.ID_MISSING_COORDS,
                "Points with missing '{}' or '{}' are not displayed".
                format(self.shown_x, self.shown_y))

        self.scatterplot_item = ScatterPlotItem(
            x=x_data, y=y_data, data=data_indices,
            symbol=shape_data, size=size_data, pen=color_data, brush=brush_data
        )
        self.scatterplot_item_sel = ScatterPlotItem(
            x=x_data, y=y_data, data=data_indices,
            symbol=shape_data, size=size_data + SELECTION_WIDTH,
            pen=color_data_sel, brush=brush_data_sel
        )
        self.plot_widget.addItem(self.scatterplot_item_sel)
        self.plot_widget.addItem(self.scatterplot_item)

        self.scatterplot_item.selected_points = []
        self.scatterplot_item.sigClicked.connect(self.select_by_click)

        self.update_labels()
        self.make_legend()
        self.plot_widget.replot()

    def can_draw_density(self):
        if self.data_domain is None:
            return False
        discrete_color = False
        attr_color = self.attr_color
        if attr_color != "" and attr_color != "(Same color)":
            color_var = self.data_domain[attr_color]
            discrete_color = color_var.is_discrete
        continuous_x = False
        continuous_y = False
        if self.shown_x and self.shown_y:
            continuous_x = self.data_domain[self.shown_x].is_continuous
            continuous_y = self.data_domain[self.shown_y].is_continuous
        return discrete_color and continuous_x and continuous_y

    def should_draw_density(self):
        return self.class_density and self.n_points > 1 and self.can_draw_density()

    def set_labels(self, axis, labels):
        axis = self.plot_widget.getAxis(axis)
        if labels:
            ticks = [[(i, labels[i]) for i in range(len(labels))]]
            axis.setTicks(ticks)
        else:
            axis.setTicks(None)

    def set_axis_title(self, axis, title):
        self.plot_widget.setLabel(axis=axis, text=title)

    def get_size_index(self):
        size_index = -1
        attr_size = self.attr_size
        if attr_size != "" and attr_size != "(Same size)":
            size_index = self.attribute_name_index[attr_size]
        return size_index

    def compute_sizes(self):
        self.master.information(self.ID_MISSING_SIZE)
        size_index = self.get_size_index()
        if size_index == -1:
            size_data = np.full((self.n_points,), self.point_width)
        else:
            size_data = \
                self.MinShapeSize + \
                self.no_jittering_scaled_data[size_index, self.valid_data] * \
                self.point_width
        nans = np.isnan(size_data)
        if np.any(nans):
            size_data[nans] = self.MinShapeSize - 2
            self.master.information(
                self.ID_MISSING_SIZE,
                "Points with undefined '{}' are shown in smaller size".
                format(self.attr_size))
        return size_data

    def update_sizes(self):
        if self.scatterplot_item:
            size_data = self.compute_sizes()
            self.scatterplot_item.setSize(size_data)
            self.scatterplot_item_sel.setSize(size_data + SELECTION_WIDTH)

    update_point_size = update_sizes

    def get_color_index(self):
        color_index = -1
        attr_color = self.attr_color
        if attr_color != "" and attr_color != "(Same color)":
            color_index = self.attribute_name_index[attr_color]
            color_var = self.data_domain[attr_color]
            colors = color_var.colors
            if color_var.is_discrete:
                self.discrete_palette = ColorPaletteGenerator(
                    number_of_colors=len(colors), rgb_colors=colors)
            else:
                self.continuous_palette = ContinuousPaletteGenerator(*colors)
        return color_index

    def compute_colors_sel(self, keep_colors=False):
        if not keep_colors:
            self.pen_colors_sel = self.brush_colors_sel = None

        def make_pen(color, width):
            p = QPen(color, width)
            p.setCosmetic(True)
            return p

        pens = [QPen(Qt.NoPen),
                make_pen(QColor(255, 190, 0, 255), SELECTION_WIDTH + 1.)]
        if self.selection is not None:
            pen = [pens[a] for a in self.selection[self.valid_data]]
        else:
            pen = [pens[0]] * self.n_points
        brush = [QBrush(QColor(255, 255, 255, 0))] * self.n_points
        return pen, brush

    def compute_colors(self, keep_colors=False):
        if not keep_colors:
            self.pen_colors = self.brush_colors = None
        color_index = self.get_color_index()

        def make_pen(color, width):
            p = QPen(color, width)
            p.setCosmetic(True)
            return p

        subset = None
        if self.subset_indices:
            subset = np.array([ex.id in self.subset_indices
                               for ex in self.raw_data[self.valid_data]])

        if color_index == -1:  # same color
            color = self.plot_widget.palette().color(OWPalette.Data)
            pen = [make_pen(color, 1.5)] * self.n_points
            if subset is not None:
                brush = [(QBrush(QColor(128, 128, 128, 0)),
                          QBrush(QColor(128, 128, 128, self.alpha_value)))[s]
                         for s in subset]
            else:
                brush = [QBrush(QColor(128, 128, 128, self.alpha_value))] \
                        * self.n_points
            return pen, brush

        c_data = self.original_data[color_index, self.valid_data]
        if self.data_domain[color_index].is_continuous:
            if self.pen_colors is None:
                self.scale = DiscretizedScale(np.nanmin(c_data), np.nanmax(c_data))
                c_data -= self.scale.offset
                c_data /= self.scale.width
                c_data = np.floor(c_data) + 0.5
                c_data /= self.scale.bins
                c_data = np.clip(c_data, 0, 1)
                palette = self.continuous_palette
                self.pen_colors = palette.getRGB(c_data)
                self.brush_colors = np.hstack(
                    [self.pen_colors,
                     np.full((self.n_points, 1), self.alpha_value)])
                self.pen_colors *= 100 // self.DarkerValue
                self.pen_colors = [make_pen(QColor(*col), 1.5)
                                   for col in self.pen_colors.tolist()]
            if subset is not None:
                self.brush_colors[:, 3] = 0
                self.brush_colors[subset, 3] = self.alpha_value
            else:
                self.brush_colors[:, 3] = self.alpha_value
            pen = self.pen_colors
            brush = np.array([QBrush(QColor(*col))
                              for col in self.brush_colors.tolist()])
        else:
            if self.pen_colors is None:
                palette = self.discrete_palette
                n_colors = palette.number_of_colors
                c_data = c_data.copy()
                c_data[np.isnan(c_data)] = n_colors
                c_data = c_data.astype(int)
                colors = np.r_[palette.getRGB(np.arange(n_colors)),
                               [[128, 128, 128]]]
                pens = np.array(
                    [make_pen(QColor(*col).darker(self.DarkerValue), 1.5)
                     for col in colors])
                self.pen_colors = pens[c_data]
                self.brush_colors = np.array([
                    [QBrush(QColor(0, 0, 0, 0)),
                     QBrush(QColor(col[0], col[1], col[2], self.alpha_value))]
                    for col in colors])
                self.brush_colors = self.brush_colors[c_data]
            if subset is not None:
                brush = np.where(
                    subset,
                    self.brush_colors[:, 1], self.brush_colors[:, 0])
            else:
                brush = self.brush_colors[:, 1]
            pen = self.pen_colors
        return pen, brush

    def update_colors(self, keep_colors=False):
        if self.scatterplot_item:
            pen_data, brush_data = self.compute_colors(keep_colors)
            pen_data_sel, brush_data_sel = self.compute_colors_sel(keep_colors)
            self.scatterplot_item.setPen(pen_data, update=False, mask=None)
            self.scatterplot_item.setBrush(brush_data, mask=None)
            self.scatterplot_item_sel.setPen(pen_data_sel, update=False, mask=None)
            self.scatterplot_item_sel.setBrush(brush_data_sel, mask=None)
            if not keep_colors:
                self.make_legend()

                if self.should_draw_density():
                    self.update_data(self.shown_x, self.shown_y)
                elif self.density_img:
                    self.plot_widget.removeItem(self.density_img)

    update_alpha_value = update_colors

    def create_labels(self):
        for x, y in zip(*self.scatterplot_item.getData()):
            ti = TextItem()
            self.plot_widget.addItem(ti)
            ti.setPos(x, y)
            self.labels.append(ti)

    def update_labels(self):
        if not self.attr_label:
            for label in self.labels:
                label.setText("")
            return
        if not self.labels:
            self.create_labels()
        label_column = self.raw_data.get_column_view(self.attr_label)[0]
        formatter = self.raw_data.domain[self.attr_label].str_val
        label_data = map(formatter, label_column)
        black = pg.mkColor(0, 0, 0)
        for label, text in zip(self.labels, label_data):
            label.setText(text, black)

    def get_shape_index(self):
        shape_index = -1
        attr_shape = self.attr_shape
        if attr_shape and attr_shape != "(Same shape)" and \
                len(self.data_domain[attr_shape].values) <= \
                len(self.CurveSymbols):
            shape_index = self.attribute_name_index[attr_shape]
        return shape_index

    def compute_symbols(self):
        self.master.information(self.ID_MISSING_SHAPE)
        shape_index = self.get_shape_index()
        if shape_index == -1:
            shape_data = self.CurveSymbols[np.zeros(self.n_points, dtype=int)]
        else:
            shape_data = self.original_data[shape_index, self.valid_data]
            nans = np.isnan(shape_data)
            if np.any(nans):
                shape_data[nans] = len(self.CurveSymbols) - 1
                self.master.information(
                    self.ID_MISSING_SHAPE,
                    "Points with undefined '{}' are shown as crossed circles".
                    format(self.attr_shape))
            shape_data = self.CurveSymbols[shape_data.astype(int)]
        return shape_data

    def update_shapes(self):
        if self.scatterplot_item:
            shape_data = self.compute_symbols()
            self.scatterplot_item.setSymbol(shape_data)
        self.make_legend()

    def update_grid(self):
        self.plot_widget.showGrid(x=self.show_grid, y=self.show_grid)

    def update_legend(self):
        if self.legend:
            self.legend.setVisible(self.show_legend)

    def create_legend(self):
        self.legend = LegendItem()
        self.legend.setParentItem(self.plot_widget.getViewBox())
        self.legend.restoreAnchor(self.__legend_anchor)

    def remove_legend(self):
        if self.legend:
            anchor = legend_anchor_pos(self.legend)
            if anchor is not None:
                self.__legend_anchor = anchor
            self.legend.setParent(None)
            self.legend = None
        if self.color_legend:
            anchor = legend_anchor_pos(self.color_legend)
            if anchor is not None:
                self.__color_legend_anchor = anchor
            self.color_legend.setParent(None)
            self.color_legend = None

    def make_legend(self):
        self.remove_legend()
        self.make_color_legend()
        self.make_shape_legend()
        self.update_legend()

    def make_color_legend(self):
        color_index = self.get_color_index()
        if color_index == -1:
            return
        color_var = self.data_domain[color_index]
        use_shape = self.get_shape_index() == color_index
        if color_var.is_discrete:
            if not self.legend:
                self.create_legend()
            palette = self.discrete_palette
            for i, value in enumerate(color_var.values):
                color = QColor(*palette.getRGB(i))
                brush = color.lighter(self.DarkerValue)
                self.legend.addItem(
                    ScatterPlotItem(
                        pen=color, brush=brush, size=10,
                        symbol=self.CurveSymbols[i] if use_shape else "o"),
                    escape(value))
        else:
            legend = self.color_legend = LegendItem()
            legend.setParentItem(self.plot_widget.getViewBox())
            legend.restoreAnchor(self.__color_legend_anchor)

            label = PaletteItemSample(self.continuous_palette, self.scale)
            legend.addItem(label, "")
            legend.setGeometry(label.boundingRect())

    def make_shape_legend(self):
        shape_index = self.get_shape_index()
        if shape_index == -1 or shape_index == self.get_color_index():
            return
        if not self.legend:
            self.create_legend()
        shape_var = self.data_domain[shape_index]
        color = self.plot_widget.palette().color(OWPalette.Data)
        pen = QPen(color.darker(self.DarkerValue))
        color.setAlpha(self.alpha_value)
        for i, value in enumerate(shape_var.values):
            self.legend.addItem(
                ScatterPlotItem(pen=pen, brush=color, size=10,
                                symbol=self.CurveSymbols[i]), escape(value))

    def zoom_button_clicked(self):
        self.plot_widget.getViewBox().setMouseMode(
            self.plot_widget.getViewBox().RectMode)

    def pan_button_clicked(self):
        self.plot_widget.getViewBox().setMouseMode(
            self.plot_widget.getViewBox().PanMode)

    def select_button_clicked(self):
        self.plot_widget.getViewBox().setMouseMode(
            self.plot_widget.getViewBox().RectMode)

    def reset_button_clicked(self):
        self.update_data(self.shown_x, self.shown_y, reset_view=True)  # also redraw density image
        # self.view_box.autoRange()

    def select_by_click(self, _, points):
        if self.scatterplot_item is not None:
            self.select(points)

    def select_by_rectangle(self, value_rect):
        if self.scatterplot_item is not None:
            points = [point
                      for point in self.scatterplot_item.points()
                      if value_rect.contains(QPointF(point.pos()))]
            self.select(points)

    def unselect_all(self):
        self.selection = None
        self.update_colors(keep_colors=True)
        self.master.selection_changed()

    def select(self, points):
        # noinspection PyArgumentList
        if self.raw_data is None:
            return
        keys = QApplication.keyboardModifiers()
        if self.selection is None or not keys & (
                Qt.ShiftModifier + Qt.ControlModifier + Qt.AltModifier):
            self.selection = np.full(len(self.raw_data), False, dtype=np.bool)
        indices = [p.data() for p in points]
        if keys & Qt.AltModifier:
            self.selection[indices] = False
        elif keys & Qt.ControlModifier:
            self.selection[indices] = ~self.selection[indices]
        else:  # Handle shift and no modifiers
            self.selection[indices] = True
        self.update_colors(keep_colors=True)
        self.master.selection_changed()

    def get_selection(self):
        if self.selection is None:
            return np.array([], dtype=int)
        else:
            return np.flatnonzero(self.selection)

    def set_palette(self, p):
        self.plot_widget.setPalette(p)

    def save_to_file(self, size):
        pass

    def help_event(self, event):
        if self.scatterplot_item is None:
            return False

        act_pos = self.scatterplot_item.mapFromScene(event.scenePos())
        points = self.scatterplot_item.pointsAt(act_pos)
        text = ""
        if len(points):
            for i, p in enumerate(points):
                index = p.data()
                text += "Attributes:\n"
                if self.tooltip_shows_all and \
                        len(self.data_domain.attributes) < 30:
                    text += "".join(
                        '   {} = {}\n'.format(attr.name,
                                              self.raw_data[index][attr])
                        for attr in self.data_domain.attributes)
                else:
                    text += '   {} = {}\n   {} = {}\n'.format(
                        self.shown_x, self.raw_data[index][self.shown_x],
                        self.shown_y, self.raw_data[index][self.shown_y])
                    if self.tooltip_shows_all:
                        text += "   ... and {} others\n\n".format(
                            len(self.data_domain.attributes) - 2)
                if self.data_domain.class_var:
                    text += 'Class:\n   {} = {}\n'.format(
                        self.data_domain.class_var.name,
                        self.raw_data[index][self.raw_data.domain.class_var])
                if i < len(points) - 1:
                    text += '------------------\n'

            text = ('<span style="white-space:pre">{}</span>'
                    .format(escape(text)))

            QToolTip.showText(event.screenPos(), text, widget=self.plot_widget)
            return True
        else:
            return False
Beispiel #3
0
class OWScatterPlotGraph(gui.OWComponent, ScaleScatterPlotData):
    attr_color = ContextSetting(None,
                                ContextSetting.OPTIONAL,
                                exclude_metas=False)
    attr_label = ContextSetting(None,
                                ContextSetting.OPTIONAL,
                                exclude_metas=False)
    attr_shape = ContextSetting(None,
                                ContextSetting.OPTIONAL,
                                exclude_metas=False)
    attr_size = ContextSetting(None,
                               ContextSetting.OPTIONAL,
                               exclude_metas=False)
    label_only_selected = Setting(False)

    point_width = Setting(10)
    alpha_value = Setting(128)
    show_grid = Setting(False)
    show_legend = Setting(True)
    tooltip_shows_all = Setting(False)
    class_density = Setting(False)
    resolution = 256

    CurveSymbols = np.array("o x t + d s ?".split())
    MinShapeSize = 6
    DarkerValue = 120
    UnknownColor = (168, 50, 168)

    def __init__(self, scatter_widget, parent=None, _="None"):
        gui.OWComponent.__init__(self, scatter_widget)
        self.view_box = InteractiveViewBox(self)
        self.plot_widget = pg.PlotWidget(viewBox=self.view_box,
                                         parent=parent,
                                         background="w")
        self.plot_widget.getPlotItem().buttonsHidden = True
        self.plot_widget.setAntialiasing(True)
        self.plot_widget.sizeHint = lambda: QtCore.QSize(500, 500)

        self.replot = self.plot_widget.replot
        ScaleScatterPlotData.__init__(self)
        self.density_img = None
        self.scatterplot_item = None
        self.scatterplot_item_sel = None

        self.labels = []

        self.master = scatter_widget
        self.master.Warning.add_message(
            "missing_coords",
            "Plot cannot be displayed because '{}' or '{}' is missing for "
            "all data points")
        self.master.Information.add_message(
            "missing_coords",
            "Points with missing '{}' or '{}' are not displayed")
        self.master.Information.add_message(
            "missing_size",
            "Points with undefined '{}' are shown in smaller size")
        self.master.Information.add_message(
            "missing_shape",
            "Points with undefined '{}' are shown as crossed circles")
        self.shown_attribute_indices = []
        self.shown_x = self.shown_y = None
        self.pen_colors = self.brush_colors = None

        self.valid_data = None  # np.ndarray
        self.selection = None  # np.ndarray
        self.n_points = 0

        self.gui = OWPlotGUI(self)
        self.continuous_palette = ContinuousPaletteGenerator(
            QColor(255, 255, 0), QColor(0, 0, 255), True)
        self.discrete_palette = ColorPaletteGenerator()

        self.selection_behavior = 0

        self.legend = self.color_legend = None
        self.__legend_anchor = (1, 0), (1, 0)
        self.__color_legend_anchor = (1, 1), (1, 1)

        self.scale = None  # DiscretizedScale

        self.subset_indices = None

        # self.setMouseTracking(True)
        # self.grabGesture(QPinchGesture)
        # self.grabGesture(QPanGesture)

        self.update_grid()

        self._tooltip_delegate = HelpEventDelegate(self.help_event)
        self.plot_widget.scene().installEventFilter(self._tooltip_delegate)

    def new_data(self, data, subset_data=None, **args):
        self.plot_widget.clear()
        self.remove_legend()

        self.density_img = None
        self.scatterplot_item = None
        self.scatterplot_item_sel = None
        self.labels = []
        self.selection = None
        self.valid_data = None

        self.subset_indices = set(
            e.id for e in subset_data) if subset_data else None

        self.set_data(data, **args)

    def _clear_plot_widget(self):
        self.remove_legend()
        if self.density_img:
            self.plot_widget.removeItem(self.density_img)
            self.density_img = None
        if self.scatterplot_item:
            self.plot_widget.removeItem(self.scatterplot_item)
            self.scatterplot_item = None
        if self.scatterplot_item_sel:
            self.plot_widget.removeItem(self.scatterplot_item_sel)
            self.scatterplot_item_sel = None
        for label in self.labels:
            self.plot_widget.removeItem(label)
        self.labels = []
        self.set_axis_title("bottom", "")
        self.set_axis_title("left", "")

    def update_data(self, attr_x, attr_y, reset_view=True):
        self.master.Warning.missing_coords.clear()
        self.master.Information.missing_coords.clear()
        self._clear_plot_widget()

        self.shown_x, self.shown_y = attr_x, attr_y

        if self.jittered_data is None or not len(self.jittered_data):
            self.valid_data = None
        else:
            index_x = self.domain.index(attr_x)
            index_y = self.domain.index(attr_y)
            self.valid_data = self.get_valid_list([index_x, index_y])
            if not np.any(self.valid_data):
                self.valid_data = None
        if self.valid_data is None:
            self.selection = None
            self.n_points = 0
            self.master.Warning.missing_coords(self.shown_x.name,
                                               self.shown_y.name)
            return

        x_data, y_data = self.get_xy_data_positions(attr_x, attr_y,
                                                    self.valid_data)
        self.n_points = len(x_data)

        if reset_view:
            min_x, max_x = np.nanmin(x_data), np.nanmax(x_data)
            min_y, max_y = np.nanmin(y_data), np.nanmax(y_data)
            self.view_box.setRange(QRectF(min_x, min_y, max_x - min_x,
                                          max_y - min_y),
                                   padding=0.025)
            self.view_box.init_history()
            self.view_box.tag_history()
        [min_x, max_x], [min_y, max_y] = self.view_box.viewRange()

        for axis, name, index in (("bottom", attr_x, index_x), ("left", attr_y,
                                                                index_y)):
            self.set_axis_title(axis, name)
            var = self.domain[index]
            if var.is_discrete:
                self.set_labels(axis, get_variable_values_sorted(var))
            else:
                self.set_labels(axis, None)

        color_data, brush_data = self.compute_colors()
        color_data_sel, brush_data_sel = self.compute_colors_sel()
        size_data = self.compute_sizes()
        shape_data = self.compute_symbols()

        if self.should_draw_density():
            rgb_data = [pen.color().getRgb()[:3] for pen in color_data]
            self.density_img = classdensity.class_density_image(
                min_x, max_x, min_y, max_y, self.resolution, x_data, y_data,
                rgb_data)
            self.plot_widget.addItem(self.density_img)

        data_indices = np.flatnonzero(self.valid_data)
        if len(data_indices) != self.original_data.shape[1]:
            self.master.Information.missing_coords(self.shown_x.name,
                                                   self.shown_y.name)

        self.scatterplot_item = ScatterPlotItem(x=x_data,
                                                y=y_data,
                                                data=data_indices,
                                                symbol=shape_data,
                                                size=size_data,
                                                pen=color_data,
                                                brush=brush_data)
        self.scatterplot_item_sel = ScatterPlotItem(x=x_data,
                                                    y=y_data,
                                                    data=data_indices,
                                                    symbol=shape_data,
                                                    size=size_data +
                                                    SELECTION_WIDTH,
                                                    pen=color_data_sel,
                                                    brush=brush_data_sel)
        self.plot_widget.addItem(self.scatterplot_item_sel)
        self.plot_widget.addItem(self.scatterplot_item)

        self.scatterplot_item.selected_points = []
        self.scatterplot_item.sigClicked.connect(self.select_by_click)

        self.update_labels()
        self.make_legend()
        self.plot_widget.replot()

    def can_draw_density(self):
        return self.domain is not None and \
            self.attr_color is not None and \
            self.attr_color.is_discrete and \
            self.shown_x.is_continuous and \
            self.shown_y.is_continuous

    def should_draw_density(self):
        return self.class_density and self.n_points > 1 and self.can_draw_density(
        )

    def set_labels(self, axis, labels):
        axis = self.plot_widget.getAxis(axis)
        if labels:
            ticks = [[(i, labels[i]) for i in range(len(labels))]]
            axis.setTicks(ticks)
        else:
            axis.setTicks(None)

    def set_axis_title(self, axis, title):
        self.plot_widget.setLabel(axis=axis, text=title)

    def get_size_index(self):
        if self.attr_size is None:
            return -1
        return self.domain.index(self.attr_size)

    def compute_sizes(self):
        self.master.Information.missing_size.clear()
        size_index = self.get_size_index()
        if size_index == -1:
            size_data = np.full((self.n_points, ), self.point_width)
        else:
            size_data = \
                self.MinShapeSize + \
                self.scaled_data[size_index, self.valid_data] * \
                self.point_width
        nans = np.isnan(size_data)
        if np.any(nans):
            size_data[nans] = self.MinShapeSize - 2
            self.master.Information.missing_size(self.attr_size)
        return size_data

    def update_sizes(self):
        if self.scatterplot_item:
            size_data = self.compute_sizes()
            self.scatterplot_item.setSize(size_data)
            self.scatterplot_item_sel.setSize(size_data + SELECTION_WIDTH)

    update_point_size = update_sizes

    def get_color_index(self):
        if self.attr_color is None:
            return -1
        colors = self.attr_color.colors
        if self.attr_color.is_discrete:
            self.discrete_palette = ColorPaletteGenerator(
                number_of_colors=len(colors), rgb_colors=colors)
        else:
            self.continuous_palette = ContinuousPaletteGenerator(*colors)
        return self.domain.index(self.attr_color)

    def compute_colors_sel(self, keep_colors=False):
        if not keep_colors:
            self.pen_colors_sel = self.brush_colors_sel = None

        def make_pen(color, width):
            p = QPen(color, width)
            p.setCosmetic(True)
            return p

        pens = [
            QPen(Qt.NoPen),
            make_pen(QColor(255, 190, 0, 255), SELECTION_WIDTH + 1.)
        ]
        if self.selection is not None:
            pen = [pens[a] for a in self.selection[self.valid_data]]
        else:
            pen = [pens[0]] * self.n_points
        brush = [QBrush(QColor(255, 255, 255, 0))] * self.n_points
        return pen, brush

    def compute_colors(self, keep_colors=False):
        if not keep_colors:
            self.pen_colors = self.brush_colors = None
        color_index = self.get_color_index()

        def make_pen(color, width):
            p = QPen(color, width)
            p.setCosmetic(True)
            return p

        subset = None
        if self.subset_indices:
            subset = np.array([
                ex.id in self.subset_indices
                for ex in self.data[self.valid_data]
            ])

        if color_index == -1:  # same color
            color = self.plot_widget.palette().color(OWPalette.Data)
            pen = [make_pen(color, 1.5)] * self.n_points
            if subset is not None:
                brush = [(QBrush(QColor(128, 128, 128,
                                        0)), QBrush(QColor(128, 128, 128,
                                                           255)))[s]
                         for s in subset]
            else:
                brush = [QBrush(QColor(128, 128, 128, self.alpha_value))] \
                        * self.n_points
            return pen, brush

        c_data = self.original_data[color_index, self.valid_data]
        if self.domain[color_index].is_continuous:
            if self.pen_colors is None:
                self.scale = DiscretizedScale(np.nanmin(c_data),
                                              np.nanmax(c_data))
                c_data -= self.scale.offset
                c_data /= self.scale.width
                c_data = np.floor(c_data) + 0.5
                c_data /= self.scale.bins
                c_data = np.clip(c_data, 0, 1)
                palette = self.continuous_palette
                self.pen_colors = palette.getRGB(c_data)
                self.brush_colors = np.hstack([
                    self.pen_colors,
                    np.full((self.n_points, 1), self.alpha_value)
                ])
                self.pen_colors *= 100 // self.DarkerValue
                self.pen_colors = [
                    make_pen(QColor(*col), 1.5)
                    for col in self.pen_colors.tolist()
                ]
            if subset is not None:
                self.brush_colors[:, 3] = 0
                self.brush_colors[subset, 3] = 255
            else:
                self.brush_colors[:, 3] = self.alpha_value
            pen = self.pen_colors
            brush = np.array(
                [QBrush(QColor(*col)) for col in self.brush_colors.tolist()])
        else:
            if self.pen_colors is None:
                palette = self.discrete_palette
                n_colors = palette.number_of_colors
                c_data = c_data.copy()
                c_data[np.isnan(c_data)] = n_colors
                c_data = c_data.astype(int)
                colors = np.r_[palette.getRGB(np.arange(n_colors)),
                               [[128, 128, 128]]]
                pens = np.array([
                    make_pen(QColor(*col).darker(self.DarkerValue), 1.5)
                    for col in colors
                ])
                self.pen_colors = pens[c_data]
                alpha = self.alpha_value if subset is None else 255
                self.brush_colors = np.array([[
                    QBrush(QColor(0, 0, 0, 0)),
                    QBrush(QColor(col[0], col[1], col[2], alpha))
                ] for col in colors])
                self.brush_colors = self.brush_colors[c_data]
            if subset is not None:
                brush = np.where(subset, self.brush_colors[:, 1],
                                 self.brush_colors[:, 0])
            else:
                brush = self.brush_colors[:, 1]
            pen = self.pen_colors
        return pen, brush

    def update_colors(self, keep_colors=False):
        if self.scatterplot_item:
            pen_data, brush_data = self.compute_colors(keep_colors)
            pen_data_sel, brush_data_sel = self.compute_colors_sel(keep_colors)
            self.scatterplot_item.setPen(pen_data, update=False, mask=None)
            self.scatterplot_item.setBrush(brush_data, mask=None)
            self.scatterplot_item_sel.setPen(pen_data_sel,
                                             update=False,
                                             mask=None)
            self.scatterplot_item_sel.setBrush(brush_data_sel, mask=None)
            if not keep_colors:
                self.make_legend()

                if self.should_draw_density():
                    self.update_data(self.shown_x, self.shown_y)
                elif self.density_img:
                    self.plot_widget.removeItem(self.density_img)

    update_alpha_value = update_colors

    def create_labels(self):
        for x, y in zip(*self.scatterplot_item.getData()):
            ti = TextItem()
            self.plot_widget.addItem(ti)
            ti.setPos(x, y)
            self.labels.append(ti)

    def update_labels(self):
        if self.attr_label is None or \
                self.label_only_selected and self.selection is None:
            for label in self.labels:
                label.setText("")
            return
        if not self.labels:
            self.create_labels()
        label_column = self.data.get_column_view(self.attr_label)[0]
        formatter = self.attr_label.str_val
        label_data = map(formatter, label_column)
        black = pg.mkColor(0, 0, 0)
        if self.label_only_selected:
            for label, text, selected \
                    in zip(self.labels, label_data, self.selection):
                label.setText(text if selected else "", black)
        else:
            for label, text in zip(self.labels, label_data):
                label.setText(text, black)

    def get_shape_index(self):
        if self.attr_shape is None or \
                len(self.attr_shape.values) > len(self.CurveSymbols):
            return -1
        return self.domain.index(self.attr_shape)

    def compute_symbols(self):
        self.master.Information.missing_shape.clear()
        shape_index = self.get_shape_index()
        if shape_index == -1:
            shape_data = self.CurveSymbols[np.zeros(self.n_points, dtype=int)]
        else:
            shape_data = self.original_data[shape_index, self.valid_data]
            nans = np.isnan(shape_data)
            if np.any(nans):
                shape_data[nans] = len(self.CurveSymbols) - 1
                self.master.Information.missing_shape(self.attr_shape)
            shape_data = self.CurveSymbols[shape_data.astype(int)]
        return shape_data

    def update_shapes(self):
        if self.scatterplot_item:
            shape_data = self.compute_symbols()
            self.scatterplot_item.setSymbol(shape_data)
        self.make_legend()

    def update_grid(self):
        self.plot_widget.showGrid(x=self.show_grid, y=self.show_grid)

    def update_legend(self):
        if self.legend:
            self.legend.setVisible(self.show_legend)

    def create_legend(self):
        self.legend = LegendItem()
        self.legend.setParentItem(self.plot_widget.getViewBox())
        self.legend.restoreAnchor(self.__legend_anchor)

    def remove_legend(self):
        if self.legend:
            anchor = legend_anchor_pos(self.legend)
            if anchor is not None:
                self.__legend_anchor = anchor
            self.legend.setParent(None)
            self.legend = None
        if self.color_legend:
            anchor = legend_anchor_pos(self.color_legend)
            if anchor is not None:
                self.__color_legend_anchor = anchor
            self.color_legend.setParent(None)
            self.color_legend = None

    def make_legend(self):
        self.remove_legend()
        self.make_color_legend()
        self.make_shape_legend()
        self.update_legend()

    def make_color_legend(self):
        color_index = self.get_color_index()
        if color_index == -1:
            return
        color_var = self.domain[color_index]
        use_shape = self.get_shape_index() == color_index
        if color_var.is_discrete:
            if not self.legend:
                self.create_legend()
            palette = self.discrete_palette
            for i, value in enumerate(color_var.values):
                color = QColor(*palette.getRGB(i))
                brush = color.lighter(self.DarkerValue)
                self.legend.addItem(
                    ScatterPlotItem(
                        pen=color,
                        brush=brush,
                        size=10,
                        symbol=self.CurveSymbols[i] if use_shape else "o"),
                    escape(value))
        else:
            legend = self.color_legend = LegendItem()
            legend.setParentItem(self.plot_widget.getViewBox())
            legend.restoreAnchor(self.__color_legend_anchor)

            label = PaletteItemSample(self.continuous_palette, self.scale)
            legend.addItem(label, "")
            legend.setGeometry(label.boundingRect())

    def make_shape_legend(self):
        shape_index = self.get_shape_index()
        if shape_index == -1 or shape_index == self.get_color_index():
            return
        if not self.legend:
            self.create_legend()
        shape_var = self.domain[shape_index]
        color = self.plot_widget.palette().color(OWPalette.Data)
        pen = QPen(color.darker(self.DarkerValue))
        color.setAlpha(self.alpha_value)
        for i, value in enumerate(shape_var.values):
            self.legend.addItem(
                ScatterPlotItem(pen=pen,
                                brush=color,
                                size=10,
                                symbol=self.CurveSymbols[i]), escape(value))

    def zoom_button_clicked(self):
        self.plot_widget.getViewBox().setMouseMode(
            self.plot_widget.getViewBox().RectMode)

    def pan_button_clicked(self):
        self.plot_widget.getViewBox().setMouseMode(
            self.plot_widget.getViewBox().PanMode)

    def select_button_clicked(self):
        self.plot_widget.getViewBox().setMouseMode(
            self.plot_widget.getViewBox().RectMode)

    def reset_button_clicked(self):
        self.update_data(self.shown_x, self.shown_y,
                         reset_view=True)  # also redraw density image
        # self.view_box.autoRange()

    def select_by_click(self, _, points):
        if self.scatterplot_item is not None:
            self.select(points)

    def select_by_rectangle(self, value_rect):
        if self.scatterplot_item is not None:
            points = [
                point for point in self.scatterplot_item.points()
                if value_rect.contains(QPointF(point.pos()))
            ]
            self.select(points)

    def unselect_all(self):
        self.selection = None
        self.update_colors(keep_colors=True)
        if self.label_only_selected:
            self.update_labels()
        self.master.selection_changed()

    def select(self, points):
        # noinspection PyArgumentList
        if self.data is None:
            return
        keys = QApplication.keyboardModifiers()
        if self.selection is None or not keys & (
                Qt.ShiftModifier + Qt.ControlModifier + Qt.AltModifier):
            self.selection = np.full(len(self.data), False, dtype=np.bool)
        indices = [p.data() for p in points]
        if keys & Qt.AltModifier:
            self.selection[indices] = False
        elif keys & Qt.ControlModifier:
            self.selection[indices] = ~self.selection[indices]
        else:  # Handle shift and no modifiers
            self.selection[indices] = True
        self.update_colors(keep_colors=True)
        if self.label_only_selected:
            self.update_labels()
        self.master.selection_changed()

    def get_selection(self):
        if self.selection is None:
            return np.array([], dtype=int)
        else:
            return np.flatnonzero(self.selection)

    def set_palette(self, p):
        self.plot_widget.setPalette(p)

    def save_to_file(self, size):
        pass

    def help_event(self, event):
        if self.scatterplot_item is None:
            return False

        act_pos = self.scatterplot_item.mapFromScene(event.scenePos())
        points = self.scatterplot_item.pointsAt(act_pos)
        text = ""
        if len(points):
            for i, p in enumerate(points):
                index = p.data()
                text += "Attributes:\n"
                if self.tooltip_shows_all and \
                        len(self.domain.attributes) < 30:
                    text += "".join('   {} = {}\n'.format(
                        attr.name, self.data[index][attr])
                                    for attr in self.domain.attributes)
                else:
                    text += '   {} = {}\n   {} = {}\n'.format(
                        self.shown_x, self.data[index][self.shown_x],
                        self.shown_y, self.data[index][self.shown_y])
                    if self.tooltip_shows_all:
                        text += "   ... and {} others\n\n".format(
                            len(self.domain.attributes) - 2)
                if self.domain.class_var:
                    text += 'Class:\n   {} = {}\n'.format(
                        self.domain.class_var.name,
                        self.data[index][self.data.domain.class_var])
                if i < len(points) - 1:
                    text += '------------------\n'

            text = ('<span style="white-space:pre">{}</span>'.format(
                escape(text)))

            QToolTip.showText(event.screenPos(), text, widget=self.plot_widget)
            return True
        else:
            return False
Beispiel #4
0
class OWScatterPlotGraph(gui.OWComponent, ScaleScatterPlotData):
    attr_color = ContextSetting(None, required=ContextSetting.OPTIONAL)
    attr_label = ContextSetting(None, required=ContextSetting.OPTIONAL)
    attr_shape = ContextSetting(None, required=ContextSetting.OPTIONAL)
    attr_size = ContextSetting(None, required=ContextSetting.OPTIONAL)
    label_only_selected = Setting(False)

    point_width = Setting(10)
    alpha_value = Setting(128)
    show_grid = Setting(False)
    show_legend = Setting(True)
    tooltip_shows_all = Setting(False)
    class_density = Setting(False)
    show_reg_line = Setting(False)
    resolution = 256

    CurveSymbols = np.array("o x t + d s t2 t3 p h star ?".split())
    MinShapeSize = 6
    DarkerValue = 120
    UnknownColor = (168, 50, 168)

    def __init__(self, scatter_widget, parent=None, _="None", view_box=InteractiveViewBox):
        gui.OWComponent.__init__(self, scatter_widget)
        self.view_box = view_box(self)
        self.plot_widget = pg.PlotWidget(viewBox=self.view_box, parent=parent,
                                         background="w")
        self.plot_widget.getPlotItem().buttonsHidden = True
        self.plot_widget.setAntialiasing(True)
        self.plot_widget.sizeHint = lambda: QSize(500, 500)
        scene = self.plot_widget.scene()
        self._create_drag_tooltip(scene)
        self._data = None  # Original Table as passed from widget to new_data before transformations

        self.replot = self.plot_widget.replot
        ScaleScatterPlotData.__init__(self)
        self.density_img = None
        self.scatterplot_item = None
        self.scatterplot_item_sel = None
        self.reg_line_item = None

        self.labels = []

        self.master = scatter_widget
        self.master.Warning.add_message(
            "missing_coords",
            "Plot cannot be displayed because '{}' or '{}' is missing for "
            "all data points")
        self.master.Information.add_message(
            "missing_coords",
            "Points with missing '{}' or '{}' are not displayed")
        self.master.Information.add_message(
            "missing_size",
            "Points with undefined '{}' are shown in smaller size")
        self.master.Information.add_message(
            "missing_shape",
            "Points with undefined '{}' are shown as crossed circles")
        self.shown_attribute_indices = []
        self.shown_x = self.shown_y = None
        self.pen_colors = self.brush_colors = None

        self.valid_data = None  # np.ndarray
        self.selection = None  # np.ndarray
        self.n_points = 0

        self.gui = OWPlotGUI(self)
        self.continuous_palette = ContinuousPaletteGenerator(
            QColor(255, 255, 0), QColor(0, 0, 255), True)
        self.discrete_palette = ColorPaletteGenerator()

        self.selection_behavior = 0

        self.legend = self.color_legend = None
        self.__legend_anchor = (1, 0), (1, 0)
        self.__color_legend_anchor = (1, 1), (1, 1)

        self.scale = None  # DiscretizedScale

        self.subset_indices = None

        # self.setMouseTracking(True)
        # self.grabGesture(QPinchGesture)
        # self.grabGesture(QPanGesture)

        self.update_grid()

        self._tooltip_delegate = HelpEventDelegate(self.help_event)
        self.plot_widget.scene().installEventFilter(self._tooltip_delegate)

    def _create_drag_tooltip(self, scene):
        tip_parts = [
            (Qt.ShiftModifier, "Shift: Add group"),
            (Qt.ShiftModifier + Qt.ControlModifier,
             "Shift-{}: Append to group".
             format("Cmd" if sys.platform == "darwin" else "Ctrl")),
            (Qt.AltModifier, "Alt: Remove")
        ]
        all_parts = ", ".join(part for _, part in tip_parts)
        self.tiptexts = {
            int(modifier): all_parts.replace(part, "<b>{}</b>".format(part))
            for modifier, part in tip_parts
        }
        self.tiptexts[0] = all_parts

        self.tip_textitem = text = QGraphicsTextItem()
        # Set to the longest text
        text.setHtml(self.tiptexts[Qt.ShiftModifier + Qt.ControlModifier])
        text.setPos(4, 2)
        r = text.boundingRect()
        rect = QGraphicsRectItem(0, 0, r.width() + 8, r.height() + 4)
        rect.setBrush(QColor(224, 224, 224, 212))
        rect.setPen(QPen(Qt.NoPen))
        self.update_tooltip(Qt.NoModifier)

        scene.drag_tooltip = scene.createItemGroup([rect, text])
        scene.drag_tooltip.hide()

    def update_tooltip(self, modifiers):
        modifiers &= Qt.ShiftModifier + Qt.ControlModifier + Qt.AltModifier
        text = self.tiptexts.get(int(modifiers), self.tiptexts[0])
        self.tip_textitem.setHtml(text)

    def new_data(self, data, subset_data=None, new=True, **args):
        if new:
            self.plot_widget.clear()
            self.remove_legend()

            self.density_img = None
            self.scatterplot_item = None
            self.scatterplot_item_sel = None
            self.reg_line_item = None
            self.labels = []
            self.selection = None
            self.valid_data = None

        self.subset_indices = set(e.id for e in subset_data) if subset_data else None

        self._data = data
        data = self.sparse_to_dense()
        self.set_data(data, **args)

    def set_domain(self, data):
        domain = data.domain if data and len(data) else None
        for attr in ("attr_color", "attr_shape", "attr_size", "attr_label"):
            getattr(self.controls, attr).model().set_domain(domain)
            setattr(self, attr, None)
        if domain is not None:
            self.attr_color = domain.class_var

    def sparse_to_dense(self):
        data = self._data
        if data is None or not data.is_sparse():
            return data

        attrs = {self.shown_x,
                 self.shown_y,
                 self.attr_color,
                 self.attr_shape,
                 self.attr_size,
                 self.attr_label}
        domain = data.domain
        all_attrs = domain.variables + domain.metas
        attrs = list(set(all_attrs) & attrs)
        selected_data = data[:, attrs].to_dense()
        return selected_data

    def _clear_plot_widget(self):
        self.remove_legend()
        if self.density_img:
            self.plot_widget.removeItem(self.density_img)
            self.density_img = None
        if self.scatterplot_item:
            self.plot_widget.removeItem(self.scatterplot_item)
            self.scatterplot_item = None
        if self.scatterplot_item_sel:
            self.plot_widget.removeItem(self.scatterplot_item_sel)
            self.scatterplot_item_sel = None
        if self.reg_line_item:
            self.plot_widget.removeItem(self.reg_line_item)
            self.reg_line_item = None
        for label in self.labels:
            self.plot_widget.removeItem(label)
        self.labels = []
        self.set_axis_title("bottom", "")
        self.set_axis_title("left", "")

    def update_data(self, attr_x, attr_y, reset_view=True):
        self.master.Warning.missing_coords.clear()
        self.master.Information.missing_coords.clear()
        self._clear_plot_widget()

        if self.shown_y != attr_y:
            # 'reset' the axis text width estimation. Without this the left
            # axis tick labels space only ever expands
            yaxis = self.plot_widget.getAxis("left")
            yaxis.textWidth = 30

        self.shown_x, self.shown_y = attr_x, attr_y
        if attr_x not in self.data.domain or attr_y not in self.data.domain:
            data = self.sparse_to_dense()
            self.set_data(data)

        if self.jittered_data is None or not len(self.jittered_data):
            self.valid_data = None
        else:
            self.valid_data = self.get_valid_list([attr_x, attr_y])
            if not np.any(self.valid_data):
                self.valid_data = None
        if self.valid_data is None:
            self.selection = None
            self.n_points = 0
            self.master.Warning.missing_coords(
                self.shown_x.name, self.shown_y.name)
            return

        x_data, y_data = self.get_xy_data_positions(
            attr_x, attr_y, self.valid_data)
        self.n_points = len(x_data)

        if reset_view:
            min_x, max_x = np.nanmin(x_data), np.nanmax(x_data)
            min_y, max_y = np.nanmin(y_data), np.nanmax(y_data)
            self.view_box.setRange(
                QRectF(min_x, min_y, max_x - min_x, max_y - min_y),
                padding=0.025)
            self.view_box.init_history()
            self.view_box.tag_history()
        [min_x, max_x], [min_y, max_y] = self.view_box.viewRange()

        for axis, var in (("bottom", attr_x), ("left", attr_y)):
            self.set_axis_title(axis, var)
            if var.is_discrete:
                self.set_labels(axis, get_variable_values_sorted(var))
            else:
                self.set_labels(axis, None)

        color_data, brush_data = self.compute_colors()
        color_data_sel, brush_data_sel = self.compute_colors_sel()
        size_data = self.compute_sizes()
        shape_data = self.compute_symbols()

        if self.should_draw_density():
            rgb_data = [pen.color().getRgb()[:3] for pen in color_data]
            self.density_img = classdensity.class_density_image(
                min_x, max_x, min_y, max_y, self.resolution,
                x_data, y_data, rgb_data)
            self.plot_widget.addItem(self.density_img)

        self.data_indices = np.flatnonzero(self.valid_data)
        if len(self.data_indices) != len(self.data):
            self.master.Information.missing_coords(
                self.shown_x.name, self.shown_y.name)

        self.scatterplot_item = ScatterPlotItem(
            x=x_data, y=y_data, data=self.data_indices,
            symbol=shape_data, size=size_data, pen=color_data, brush=brush_data
        )
        self.scatterplot_item_sel = ScatterPlotItem(
            x=x_data, y=y_data, data=self.data_indices,
            symbol=shape_data, size=size_data + SELECTION_WIDTH,
            pen=color_data_sel, brush=brush_data_sel
        )
        self.plot_widget.addItem(self.scatterplot_item_sel)
        self.plot_widget.addItem(self.scatterplot_item)

        self.scatterplot_item.selected_points = []
        self.scatterplot_item.sigClicked.connect(self.select_by_click)
        if self.show_reg_line:
            _x_data = self.data.get_column_view(self.shown_x)[0]
            _y_data = self.data.get_column_view(self.shown_y)[0]
            _x_data = _x_data[self.valid_data]
            _y_data = _y_data[self.valid_data]
            assert _x_data.size
            assert _y_data.size
            self.draw_regression_line(
                _x_data, _y_data, np.min(_x_data), np.max(_y_data))

        self.update_labels()
        self.make_legend()
        self.plot_widget.replot()

    def draw_regression_line(self, x_data, y_data, min_x, max_x):
        if self.show_reg_line and self.can_draw_regresssion_line():
            slope, intercept, rvalue, _, _ = linregress(x_data, y_data)
            start_y = min_x * slope + intercept
            end_y = max_x * slope + intercept
            angle = np.degrees(np.arctan((end_y - start_y) / (max_x - min_x)))
            rotate = ((angle + 45) % 180) - 45 > 90
            color = QColor("#505050")
            l_opts = dict(color=color, position=abs(int(rotate) - 0.85),
                          rotateAxis=(1, 0), movable=True)
            self.reg_line_item = InfiniteLine(
                pos=QPointF(min_x, start_y), pen=pg.mkPen(color=color, width=1),
                angle=angle, label="r = {:.2f}".format(rvalue), labelOpts=l_opts)
            if rotate:
                self.reg_line_item.label.angle = 180
                self.reg_line_item.label.updateTransform()
            self.plot_widget.addItem(self.reg_line_item)

    def can_draw_density(self):
        return self.domain is not None and \
            self.attr_color is not None and \
            self.attr_color.is_discrete and \
            self.shown_x.is_continuous and \
            self.shown_y.is_continuous

    def should_draw_density(self):
        return self.class_density and self.n_points > 1 and self.can_draw_density()

    def can_draw_regresssion_line(self):
        return self.domain is not None and \
               self.shown_x.is_continuous and \
               self.shown_y.is_continuous

    def set_labels(self, axis, labels):
        axis = self.plot_widget.getAxis(axis)
        if labels:
            ticks = [[(i, labels[i]) for i in range(len(labels))]]
            axis.setTicks(ticks)
        else:
            axis.setTicks(None)

    def set_axis_title(self, axis, title):
        self.plot_widget.setLabel(axis=axis, text=title)

    def compute_sizes(self):
        self.master.Information.missing_size.clear()
        if self.attr_size is None:
            size_data = np.full((self.n_points,), self.point_width,
                                dtype=float)
        else:
            size_data = \
                self.MinShapeSize + \
                self.scaled_data.get_column_view(self.attr_size)[0][self.valid_data] * \
                self.point_width
        nans = np.isnan(size_data)
        if np.any(nans):
            size_data[nans] = self.MinShapeSize - 2
            self.master.Information.missing_size(self.attr_size)
        return size_data

    def update_sizes(self):
        self.set_data(self.sparse_to_dense())
        self.update_point_size()

    def update_point_size(self):
        if self.scatterplot_item:
            size_data = self.compute_sizes()
            self.scatterplot_item.setSize(size_data)
            self.scatterplot_item_sel.setSize(size_data + SELECTION_WIDTH)

    def get_color(self):
        if self.attr_color is None:
            return None
        colors = self.attr_color.colors
        if self.attr_color.is_discrete:
            self.discrete_palette = ColorPaletteGenerator(
                number_of_colors=min(len(colors), MAX), rgb_colors=colors if len(colors) <= MAX
                else DefaultRGBColors)
        else:
            self.continuous_palette = ContinuousPaletteGenerator(*colors)
        return self.attr_color

    def compute_colors_sel(self, keep_colors=False):
        if not keep_colors:
            self.pen_colors_sel = self.brush_colors_sel = None

        nopen = QPen(Qt.NoPen)
        if self.selection is not None:
            sels = np.max(self.selection)
            if sels == 1:
                pens = [nopen,
                        _make_pen(QColor(255, 190, 0, 255),
                                  SELECTION_WIDTH + 1.)]
            else:
                palette = ColorPaletteGenerator(number_of_colors=sels + 1)
                pens = [nopen] + \
                       [_make_pen(palette[i], SELECTION_WIDTH + 1.)
                        for i in range(sels)]
            pen = [pens[a] for a in self.selection[self.valid_data]]
        else:
            pen = [nopen] * self.n_points
        brush = [QBrush(QColor(255, 255, 255, 0))] * self.n_points
        return pen, brush

    def _reduce_values(self, attr):
        """
        If discrete variable has more than maximium allowed values,
        less used values are joined as "Other"
        """
        c_data = self.data.get_column_view(attr)[0][self.valid_data]
        if attr.is_continuous or len(attr.values) <= MAX:
            return None, c_data
        values_to_replace = Counter(c_data)
        values_to_replace = sorted(
            values_to_replace, key=values_to_replace.get, reverse=True
        )
        return values_to_replace, c_data

    def _get_values(self, attr):
        if len(attr.values) <= MAX:
            return attr.values
        values_to_replace, _ = self._reduce_values(attr)
        return [attr.values[int(i)] for i in values_to_replace
                if not np.isnan(i)][:MAX - 1] + ["Other"]

    def _get_data(self, attr):
        values_to_replace, c_data = self._reduce_values(attr)
        if values_to_replace is not None:
            c_data_2 = c_data.copy()
            for i, v in enumerate(values_to_replace):
                c_data[c_data_2 == v] = i if i < MAX - 1 else MAX - 1
        return c_data

    def compute_colors(self, keep_colors=False):
        if not keep_colors:
            self.pen_colors = self.brush_colors = None
        self.get_color()

        subset = None
        if self.subset_indices:
            subset = np.array([ex.id in self.subset_indices
                               for ex in self.data[self.valid_data]])

        if self.attr_color is None:  # same color
            color = self.plot_widget.palette().color(OWPalette.Data)
            pen = [_make_pen(color, 1.5)] * self.n_points
            if subset is not None:
                brush = [(QBrush(QColor(128, 128, 128, 0)),
                          QBrush(QColor(128, 128, 128, 255)))[s]
                         for s in subset]
            else:
                brush = [QBrush(QColor(128, 128, 128, self.alpha_value))] \
                        * self.n_points
            return pen, brush

        c_data = self._get_data(self.attr_color)
        if self.attr_color.is_continuous:
            if self.pen_colors is None:
                self.scale = DiscretizedScale(np.nanmin(c_data), np.nanmax(c_data))
                c_data -= self.scale.offset
                c_data /= self.scale.width
                c_data = np.floor(c_data) + 0.5
                c_data /= self.scale.bins
                c_data = np.clip(c_data, 0, 1)
                palette = self.continuous_palette
                self.pen_colors = palette.getRGB(c_data)
                self.brush_colors = np.hstack(
                    [self.pen_colors,
                     np.full((self.n_points, 1), self.alpha_value, dtype=int)])
                self.pen_colors *= 100
                self.pen_colors //= self.DarkerValue
                self.pen_colors = [_make_pen(QColor(*col), 1.5)
                                   for col in self.pen_colors.tolist()]
            if subset is not None:
                self.brush_colors[:, 3] = 0
                self.brush_colors[subset, 3] = 255
            else:
                self.brush_colors[:, 3] = self.alpha_value
            pen = self.pen_colors
            brush = np.array([QBrush(QColor(*col))
                              for col in self.brush_colors.tolist()])
        else:
            if self.pen_colors is None:
                palette = self.discrete_palette
                n_colors = palette.number_of_colors
                c_data = c_data.copy()
                c_data[np.isnan(c_data)] = n_colors
                c_data = c_data.astype(int)
                colors = np.r_[palette.getRGB(np.arange(n_colors)),
                               [[128, 128, 128]]]
                pens = np.array(
                    [_make_pen(QColor(*col).darker(self.DarkerValue), 1.5)
                     for col in colors])
                self.pen_colors = pens[c_data]
                alpha = self.alpha_value if subset is None else 255
                self.brush_colors = np.array([
                    [QBrush(QColor(0, 0, 0, 0)),
                     QBrush(QColor(col[0], col[1], col[2], alpha))]
                    for col in colors])
                self.brush_colors = self.brush_colors[c_data]
            if subset is not None:
                brush = np.where(
                    subset,
                    self.brush_colors[:, 1], self.brush_colors[:, 0])
            else:
                brush = self.brush_colors[:, 1]
            pen = self.pen_colors
        return pen, brush

    def update_colors(self, keep_colors=False):
        self.master.update_colors()
        self.set_data(self.sparse_to_dense())
        self.update_alpha_value(keep_colors)

    def update_alpha_value(self, keep_colors=False):
        if self.scatterplot_item:
            pen_data, brush_data = self.compute_colors(keep_colors)
            pen_data_sel, brush_data_sel = self.compute_colors_sel(keep_colors)
            self.scatterplot_item.setPen(pen_data, update=False, mask=None)
            self.scatterplot_item.setBrush(brush_data, mask=None)
            self.scatterplot_item_sel.setPen(pen_data_sel, update=False, mask=None)
            self.scatterplot_item_sel.setBrush(brush_data_sel, mask=None)
            if not keep_colors:
                self.make_legend()

                if self.should_draw_density():
                    self.update_data(self.shown_x, self.shown_y)
                elif self.density_img:
                    self.plot_widget.removeItem(self.density_img)

    def create_labels(self):
        for x, y in zip(*self.scatterplot_item.getData()):
            ti = TextItem()
            self.plot_widget.addItem(ti)
            ti.setPos(x, y)
            self.labels.append(ti)

    def _create_label_column(self):
        if self.attr_label in self.data.domain:
            label_column = self.data.get_column_view(self.attr_label)[0]
        else:
            label_column = self.master.data.get_column_view(self.attr_label)[0]
        return label_column[self.data_indices]


    def update_labels(self):
        if self.attr_label is None or \
                self.label_only_selected and self.selection is None:
            for label in self.labels:
                label.setText("")
            return
        self.assure_attribute_present(self.attr_label)
        if not self.labels:
            self.create_labels()
        label_column = self._create_label_column()
        formatter = self.attr_label.str_val
        label_data = map(formatter, label_column)
        black = pg.mkColor(0, 0, 0)
        selection = self.selection[self.valid_data] if self.selection is not None else []
        if self.label_only_selected:
            for label, text, selected \
                    in zip(self.labels, label_data, selection):
                label.setText(text if selected else "", black)
        else:
            for label, text in zip(self.labels, label_data):
                label.setText(text, black)

    def compute_symbols(self):
        self.master.Information.missing_shape.clear()
        if self.attr_shape is None:
            shape_data = self.CurveSymbols[np.zeros(self.n_points, dtype=int)]
        else:
            shape_data = self._get_data(self.attr_shape)
            nans = np.isnan(shape_data)
            if np.any(nans):
                shape_data[nans] = len(self.CurveSymbols) - 1
                self.master.Information.missing_shape(self.attr_shape)
            shape_data = self.CurveSymbols[shape_data.astype(int)]
        return shape_data

    def update_shapes(self):
        self.assure_attribute_present(self.attr_shape)
        if self.scatterplot_item:
            shape_data = self.compute_symbols()
            self.scatterplot_item.setSymbol(shape_data)
        self.make_legend()

    def assure_attribute_present(self, attr):
        if self.data is not None and attr not in self.data.domain:
            self.set_data(self.sparse_to_dense())

    def update_grid(self):
        self.plot_widget.showGrid(x=self.show_grid, y=self.show_grid)

    def update_legend(self):
        if self.legend:
            self.legend.setVisible(self.show_legend)

    def create_legend(self):
        self.legend = LegendItem()
        self.legend.setParentItem(self.plot_widget.getViewBox())
        self.legend.restoreAnchor(self.__legend_anchor)

    def remove_legend(self):
        if self.legend:
            anchor = legend_anchor_pos(self.legend)
            if anchor is not None:
                self.__legend_anchor = anchor
            self.legend.setParent(None)
            self.legend = None
        if self.color_legend:
            anchor = legend_anchor_pos(self.color_legend)
            if anchor is not None:
                self.__color_legend_anchor = anchor
            self.color_legend.setParent(None)
            self.color_legend = None

    def make_legend(self):
        self.remove_legend()
        self.make_color_legend()
        self.make_shape_legend()
        self.update_legend()

    def make_color_legend(self):
        if self.attr_color is None:
            return
        use_shape = self.attr_shape == self.get_color()
        if self.attr_color.is_discrete:
            if not self.legend:
                self.create_legend()
            palette = self.discrete_palette
            for i, value in enumerate(self._get_values(self.attr_color)):
                color = QColor(*palette.getRGB(i))
                pen = _make_pen(color.darker(self.DarkerValue), 1.5)
                color.setAlpha(self.alpha_value if self.subset_indices is None else 255)
                brush = QBrush(color)
                self.legend.addItem(
                    ScatterPlotItem(
                        pen=pen, brush=brush, size=10,
                        symbol=self.CurveSymbols[i] if use_shape else "o"),
                    escape(value))
        else:
            legend = self.color_legend = LegendItem()
            legend.setParentItem(self.plot_widget.getViewBox())
            legend.restoreAnchor(self.__color_legend_anchor)

            label = PaletteItemSample(self.continuous_palette, self.scale)
            legend.addItem(label, "")
            legend.setGeometry(label.boundingRect())

    def make_shape_legend(self):
        if self.attr_shape is None or self.attr_shape == self.get_color():
            return
        if not self.legend:
            self.create_legend()
        color = QColor(0, 0, 0)
        color.setAlpha(self.alpha_value)
        for i, value in enumerate(self._get_values(self.attr_shape)):
            self.legend.addItem(
                ScatterPlotItem(pen=color, brush=color, size=10,
                                symbol=self.CurveSymbols[i]), escape(value))

    def zoom_button_clicked(self):
        self.plot_widget.getViewBox().setMouseMode(
            self.plot_widget.getViewBox().RectMode)

    def pan_button_clicked(self):
        self.plot_widget.getViewBox().setMouseMode(
            self.plot_widget.getViewBox().PanMode)

    def select_button_clicked(self):
        self.plot_widget.getViewBox().setMouseMode(
            self.plot_widget.getViewBox().RectMode)

    def reset_button_clicked(self):
        self.update_data(self.shown_x, self.shown_y, reset_view=True)  # also redraw density image
        # self.view_box.autoRange()

    def select_by_click(self, _, points):
        if self.scatterplot_item is not None:
            self.select(points)

    def select_by_rectangle(self, value_rect):
        if self.scatterplot_item is not None:
            points = [point
                      for point in self.scatterplot_item.points()
                      if value_rect.contains(QPointF(point.pos()))]
            self.select(points)

    def unselect_all(self):
        self.selection = None
        self.update_colors(keep_colors=True)
        if self.label_only_selected:
            self.update_labels()
        self.master.selection_changed()

    def select(self, points):
        # noinspection PyArgumentList
        if self.data is None:
            return
        if self.selection is None:
            self.selection = np.zeros(len(self.data), dtype=np.uint8)
        indices = [p.data() for p in points]
        keys = QApplication.keyboardModifiers()
        # Remove from selection
        if keys & Qt.AltModifier:
            self.selection[indices] = 0
        # Append to the last group
        elif keys & Qt.ShiftModifier and keys & Qt.ControlModifier:
            self.selection[indices] = np.max(self.selection)
        # Create a new group
        elif keys & Qt.ShiftModifier:
            self.selection[indices] = np.max(self.selection) + 1
        # No modifiers: new selection
        else:
            self.selection = np.zeros(len(self.data), dtype=np.uint8)
            self.selection[indices] = 1
        self.update_colors(keep_colors=True)
        if self.label_only_selected:
            self.update_labels()
        self.master.selection_changed()

    def get_selection(self):
        if self.selection is None:
            return np.array([], dtype=np.uint8)
        else:
            return np.flatnonzero(self.selection)

    def set_palette(self, p):
        self.plot_widget.setPalette(p)

    def save_to_file(self, size):
        pass

    def help_event(self, event):
        if self.scatterplot_item is None:
            return False

        domain = self.data.domain
        PARTS = (("Class", "Classes", 4, domain.class_vars),
                 ("Meta", "Metas", 4, domain.metas),
                 ("Feature", "Features", 10, domain.attributes))

        def format_val(var, point_data, bold=False):
            text = escape('{} = {}'.format(var.name, point_data[var]))
            if bold:
                text = "<b>{}</b>".format(text)
            return text

        def show_part(point_data, singular, plural, max_shown, vars):
            cols = [format_val(var, point_data)
                    for var in vars[:max_shown + 2]
                    if vars == domain.class_vars
                    or var not in (self.shown_x, self.shown_y)][:max_shown]
            if not cols:
                return ""
            n_vars = len(vars)
            if n_vars > max_shown:
                cols[-1] = "... and {} others".format(n_vars - max_shown + 1)
            return \
                "<br/><b>{}</b>:<br/>".format(singular if n_vars < 2
                                              else plural) \
                + "<br/>".join(cols)

        def point_data(p):
            point_data = self.data[p.data()]
            text = "<br/>".join(
                format_val(var, point_data, bold=self.tooltip_shows_all)
                for var in (self.shown_x, self.shown_y))
            if self.tooltip_shows_all:
                text += "<br/>" + \
                        "".join(show_part(point_data, *columns)
                                for columns in PARTS)
            return text

        act_pos = self.scatterplot_item.mapFromScene(event.scenePos())
        points = self.scatterplot_item.pointsAt(act_pos)

        if len(points):
            if len(points) > MAX_POINTS_IN_TOOLTIP:
                text = "{} instances<hr/>{}<hr/>...".format(
                    len(points),
                    "<hr/>".join(point_data(point) for point in points[:MAX_POINTS_IN_TOOLTIP])
                )
            else:
                text = "<hr/>".join(point_data(point) for point in points)

            QToolTip.showText(event.screenPos(), text, widget=self.plot_widget)
            return True
        else:
            return False

    def box_zoom_select(self, parent):
        g = self.gui
        box_zoom_select = gui.vBox(parent, "Zoom/Select")
        zoom_select_toolbar = g.zoom_select_toolbar(
            box_zoom_select, nomargin=True,
            buttons=[g.StateButtonsBegin, g.SimpleSelect, g.Pan, g.Zoom,
                     g.StateButtonsEnd, g.ZoomReset]
        )
        buttons = zoom_select_toolbar.buttons
        buttons[g.Zoom].clicked.connect(self.zoom_button_clicked)
        buttons[g.Pan].clicked.connect(self.pan_button_clicked)
        buttons[g.SimpleSelect].clicked.connect(self.select_button_clicked)
        buttons[g.ZoomReset].clicked.connect(self.reset_button_clicked)
        return box_zoom_select

    def zoom_actions(self, parent):
        def zoom(s):
            """
            Zoom in/out by factor `s`.
            scaleBy scales the view's bounds (the axis range)
            """
            self.view_box.scaleBy((1 / s, 1 / s))

        def fit_to_view():
            self.viewbox.autoRange()

        zoom_in = QAction(
            "Zoom in", parent, triggered=lambda: zoom(1.25)
        )
        zoom_in.setShortcuts([QKeySequence(QKeySequence.ZoomIn),
                              QKeySequence(parent.tr("Ctrl+="))])
        zoom_out = QAction(
            "Zoom out", parent, shortcut=QKeySequence.ZoomOut,
            triggered=lambda: zoom(1 / 1.25)
        )
        zoom_fit = QAction(
            "Fit in view", parent,
            shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0),
            triggered=fit_to_view
        )
        parent.addActions([zoom_in, zoom_out, zoom_fit])
Beispiel #5
0
class OWScatterPlotGraph(gui.OWComponent, ScaleScatterPlotData):
    attr_color = ContextSetting("", ContextSetting.OPTIONAL)
    attr_label = ContextSetting("", ContextSetting.OPTIONAL)
    attr_shape = ContextSetting("", ContextSetting.OPTIONAL)
    attr_size = ContextSetting("", ContextSetting.OPTIONAL)

    point_width = Setting(10)
    alpha_value = Setting(255)
    show_grid = Setting(False)
    show_legend = Setting(True)
    tooltip_shows_all = Setting(False)
    square_granularity = Setting(3)
    space_between_cells = Setting(True)

    CurveSymbols = np.array("o x t + d s ?".split())
    MinShapeSize = 6
    DarkerValue = 120
    UnknownColor = (168, 50, 168)

    def __init__(self, scatter_widget, parent=None, _="None"):
        gui.OWComponent.__init__(self, scatter_widget)
        self.view_box = InteractiveViewBox(self)
        self.plot_widget = pg.PlotWidget(viewBox=self.view_box, parent=parent)
        self.plot_widget.setAntialiasing(True)
        self.replot = self.plot_widget
        ScaleScatterPlotData.__init__(self)
        self.scatterplot_item = None

        self.tooltip_data = []
        self.tooltip = TextItem(
            border=pg.mkPen(200, 200, 200), fill=pg.mkBrush(250, 250, 200, 220))
        self.tooltip.hide()

        self.labels = []

        self.master = scatter_widget
        self.shown_attribute_indices = []
        self.shown_x = ""
        self.shown_y = ""
        self.pen_colors = self.brush_colors = None

        self.valid_data = None  # np.ndarray
        self.selection = None  # np.ndarray
        self.n_points = 0

        self.gui = OWPlotGUI(self)
        self.continuous_palette = ContinuousPaletteGenerator(
            QColor(255, 255, 0), QColor(0, 0, 255), True)
        self.discrete_palette = ColorPaletteGenerator()

        self.selection_behavior = 0

        self.legend = self.color_legend = None
        self.scale = None  # DiscretizedScale

        self.tips = TooltipManager(self)
        # self.setMouseTracking(True)
        # self.grabGesture(QPinchGesture)
        # self.grabGesture(QPanGesture)

        self.update_grid()

    def set_data(self, data, subset_data=None, **args):
        self.plot_widget.clear()
        ScaleScatterPlotData.set_data(self, data, subset_data, **args)

    def update_data(self, attr_x, attr_y):
        self.shown_x = attr_x
        self.shown_y = attr_y

        self.remove_legend()
        if self.scatterplot_item:
            self.plot_widget.removeItem(self.scatterplot_item)
        for label in self.labels:
            self.plot_widget.removeItem(label)
        self.labels = []
        self.tooltip_data = []
        self.set_axis_title("bottom", "")
        self.set_axis_title("left", "")

        if self.scaled_data is None or not len(self.scaled_data):
            self.valid_data = None
            self.n_points = 0
            return

        index_x = self.attribute_name_index[attr_x]
        index_y = self.attribute_name_index[attr_y]
        self.valid_data = self.get_valid_list([index_x, index_y])
        x_data, y_data = self.get_xy_data_positions(
            attr_x, attr_y, self.valid_data)
        x_data = x_data[self.valid_data]
        y_data = y_data[self.valid_data]
        self.n_points = len(x_data)

        for axis, name, index in (("bottom", attr_x, index_x),
                                  ("left", attr_y, index_y)):
            self.set_axis_title(axis, name)
            var = self.data_domain[index]
            if isinstance(var, DiscreteVariable):
                self.set_labels(axis, get_variable_values_sorted(var))

        color_data, brush_data = self.compute_colors()
        size_data = self.compute_sizes()
        shape_data = self.compute_symbols()
        self.scatterplot_item = ScatterPlotItem(
            x=x_data, y=y_data, data=np.arange(self.n_points),
            symbol=shape_data, size=size_data, pen=color_data, brush=brush_data)
        self.plot_widget.addItem(self.scatterplot_item)
        self.plot_widget.addItem(self.tooltip)
        self.scatterplot_item.selected_points = []
        self.scatterplot_item.sigClicked.connect(self.select_by_click)
        self.scatterplot_item.scene().sigMouseMoved.connect(self.mouseMoved)

        self.update_labels()
        self.make_legend()
        self.plot_widget.replot()

    def set_labels(self, axis, labels):
        axis = self.plot_widget.getAxis(axis)
        if labels:
            ticks = [[(i, labels[i]) for i in range(len(labels))]]
            axis.setTicks(ticks)
        else:
            axis.setTicks(None)

    def set_axis_title(self, axis, title):
        self.plot_widget.setLabel(axis=axis, text=title)

    def get_size_index(self):
        size_index = -1
        attr_size = self.attr_size
        if attr_size != "" and attr_size != "(Same size)":
            size_index = self.attribute_name_index[attr_size]
        return size_index

    def compute_sizes(self):
        size_index = self.get_size_index()
        if size_index == -1:
            size_data = np.full((self.n_points,), self.point_width)
        else:
            size_data = \
                self.MinShapeSize + \
                self.no_jittering_scaled_data[size_index] * self.point_width
        size_data[np.isnan(size_data)] = self.MinShapeSize - 2
        return size_data

    def update_sizes(self):
        if self.scatterplot_item:
            size_data = self.compute_sizes()
            self.scatterplot_item.setSize(size_data)

    update_point_size = update_sizes

    def get_color_index(self):
        color_index = -1
        attr_color = self.attr_color
        if attr_color != "" and attr_color != "(Same color)":
            color_index = self.attribute_name_index[attr_color]
            color_var = self.data_domain[attr_color]
            if isinstance(color_var, DiscreteVariable):
                self.discrete_palette.set_number_of_colors(
                    len(color_var.values))
        return color_index

    def compute_colors(self, keep_colors=False):
        if not keep_colors:
            self.pen_colors = self.brush_colors = None
        color_index = self.get_color_index()
        if color_index == -1:
            color = self.plot_widget.palette().color(OWPalette.Data)
            pen = [QPen(QBrush(color), 1.5)] * self.n_points
            if self.selection is not None:
                brush = [(QBrush(QColor(128, 128, 128, 255)),
                          QBrush(QColor(128, 128, 128)))[s]
                         for s in self.selection]
            else:
                brush = [QBrush(QColor(128, 128, 128))] * self.n_points
            return pen, brush

        c_data = self.original_data[color_index, self.valid_data]
        if isinstance(self.data_domain[color_index], ContinuousVariable):
            if self.pen_colors is None:
                self.scale = DiscretizedScale(np.min(c_data), np.max(c_data))
                c_data -= self.scale.offset
                c_data /= self.scale.width
                c_data = np.floor(c_data) + 0.5
                c_data /= self.scale.bins
                c_data = np.clip(c_data, 0, 1)
                palette = self.continuous_palette
                self.pen_colors = palette.getRGB(c_data)
                self.brush_colors = np.hstack(
                    [self.pen_colors,
                     np.full((self.n_points, 1), self.alpha_value)])
                self.pen_colors *= 100 / self.DarkerValue
                self.pen_colors = [QPen(QBrush(QColor(*col)), 1.5)
                                   for col in self.pen_colors.tolist()]
            if self.selection is not None:
                self.brush_colors[:, 3] = 0
                self.brush_colors[self.selection, 3] = self.alpha_value
            else:
                self.brush_colors[:, 3] = self.alpha_value
            pen = self.pen_colors
            brush = np.array([QBrush(QColor(*col))
                              for col in self.brush_colors.tolist()])
        else:
            if self.pen_colors is None:
                palette = self.discrete_palette
                n_colors = palette.number_of_colors
                c_data = c_data.copy()
                c_data[np.isnan(c_data)] = n_colors
                c_data = c_data.astype(int)
                colors = palette.getRGB(np.arange(n_colors + 1))
                colors[n_colors] = (128, 128, 128)
                pens = np.array(
                    [QPen(QBrush(QColor(*col).darker(self.DarkerValue)), 1.5)
                     for col in colors])
                self.pen_colors = pens[c_data]
                self.brush_colors = np.array([
                    [QBrush(QColor(0, 0, 0, 0)),
                     QBrush(QColor(col[0], col[1], col[2], self.alpha_value))]
                    for col in colors])
                self.brush_colors = self.brush_colors[c_data]
            if self.selection is not None:
                brush = np.where(
                    self.selection,
                    self.brush_colors[:, 1], self.brush_colors[:, 0])
            else:
                brush = self.brush_colors[:, 1]
            pen = self.pen_colors
        return pen, brush

    def update_colors(self, keep_colors=False):
        if self.scatterplot_item:
            pen_data, brush_data = self.compute_colors(keep_colors)
            self.scatterplot_item.setPen(pen_data, update=False, mask=None)
            self.scatterplot_item.setBrush(brush_data, mask=None)
            if not keep_colors:
                self.make_legend()

    update_alpha_value = update_colors

    def create_labels(self):
        for x, y in zip(*self.scatterplot_item.getData()):
            ti = TextItem()
            self.plot_widget.addItem(ti)
            ti.setPos(x, y)
            self.labels.append(ti)

    def update_labels(self):
        if not self.attr_label:
            for label in self.labels:
                label.setText("")
            return
        if not self.labels:
            self.create_labels()
        label_column = self.raw_data.get_column_view(self.attr_label)[0]
        formatter = self.raw_data.domain[self.attr_label].str_val
        label_data = map(formatter, label_column)
        black = pg.mkColor(0, 0, 0)
        for label, text in zip(self.labels, label_data):
            label.setText(text, black)

    def get_shape_index(self):
        shape_index = -1
        attr_shape = self.attr_shape
        if attr_shape and attr_shape != "(Same shape)" and \
                len(self.data_domain[attr_shape].values) <= \
                len(self.CurveSymbols):
            shape_index = self.attribute_name_index[attr_shape]
        return shape_index

    def compute_symbols(self):
        shape_index = self.get_shape_index()
        if shape_index == -1:
            shape_data = self.CurveSymbols[np.zeros(self.n_points, dtype=int)]
        else:
            shape_data = self.original_data[shape_index]
            shape_data[np.isnan(shape_data)] = len(self.CurveSymbols) - 1
            shape_data = self.CurveSymbols[shape_data.astype(int)]
        return shape_data

    def update_shapes(self):
        if self.scatterplot_item:
            shape_data = self.compute_symbols()
            self.scatterplot_item.setSymbol(shape_data)
        self.make_legend()

    def update_grid(self):
        self.plot_widget.showGrid(x=self.show_grid, y=self.show_grid)

    def update_legend(self):
        if self.legend:
            self.legend.setVisible(self.show_legend)

    def create_legend(self):
        self.legend = PositionedLegendItem(self.plot_widget.plotItem, self)

    def remove_legend(self):
        if self.legend:
            self.legend.setParent(None)
            self.legend = None
        if self.color_legend:
            self.color_legend.setParent(None)
            self.color_legend = None

    def make_legend(self):
        self.remove_legend()
        self.make_color_legend()
        self.make_shape_legend()
        self.update_legend()

    def make_color_legend(self):
        color_index = self.get_color_index()
        if color_index == -1:
            return
        color_var = self.data_domain[color_index]
        use_shape = self.get_shape_index() == color_index
        if isinstance(color_var, DiscreteVariable):
            if not self.legend:
                self.create_legend()
            palette = self.discrete_palette
            for i, value in enumerate(color_var.values):
                color = QColor(*palette.getRGB(i))
                brush = color.lighter(self.DarkerValue)
                self.legend.addItem(
                    ScatterPlotItem(
                        pen=color, brush=brush, size=10,
                        symbol=self.CurveSymbols[i] if use_shape else "o"),
                    value)
        else:
            legend = self.color_legend = PositionedLegendItem(
                self.plot_widget.plotItem,
                self, legend_id="colors", at_bottom=True)
            label = PaletteItemSample(self.continuous_palette, self.scale)
            legend.addItem(label, "")
            legend.setGeometry(label.boundingRect())

    def make_shape_legend(self):
        shape_index = self.get_shape_index()
        if shape_index == -1 or shape_index == self.get_color_index():
            return
        if not self.legend:
            self.create_legend()
        shape_var = self.data_domain[shape_index]
        color = self.plot_widget.palette().color(OWPalette.Data)
        pen = QPen(color.darker(self.DarkerValue))
        color.setAlpha(self.alpha_value)
        for i, value in enumerate(shape_var.values):
            self.legend.addItem(
                ScatterPlotItem(pen=pen, brush=color, size=10,
                                symbol=self.CurveSymbols[i]), value)

    # noinspection PyPep8Naming
    def mouseMoved(self, pos):
        act_pos = self.scatterplot_item.mapFromScene(pos)
        points = self.scatterplot_item.pointsAt(act_pos)
        text = ""
        if len(points):
            for i, p in enumerate(points):
                index = p.data()
                text += "Attributes:\n"
                if self.tooltip_shows_all:
                    text += "".join(
                        '   {} = {}\n'.format(attr.name,
                                              self.raw_data[index][attr])
                        for attr in self.data_domain.attributes)
                else:
                    text += '   {} = {}\n   {} = {}\n'.format(
                        self.shown_x, self.raw_data[index][self.shown_x],
                        self.shown_y, self.raw_data[index][self.shown_y])
                if self.data_domain.class_var:
                    text += 'Class:\n   {} = {}\n'.format(
                        self.data_domain.class_var.name,
                        self.raw_data[index][self.raw_data.domain.class_var])
                if i < len(points) - 1:
                    text += '------------------\n'
            self.tooltip.setText(text, color=(0, 0, 0))
            self.tooltip.setPos(act_pos)
            self.tooltip.show()
            self.tooltip.setZValue(10)
        else:
            self.tooltip.hide()

    def zoom_button_clicked(self):
        self.scatterplot_item.getViewBox().setMouseMode(
            self.scatterplot_item.getViewBox().RectMode)

    def pan_button_clicked(self):
        self.scatterplot_item.getViewBox().setMouseMode(
            self.scatterplot_item.getViewBox().PanMode)

    def select_button_clicked(self):
        self.scatterplot_item.getViewBox().setMouseMode(
            self.scatterplot_item.getViewBox().RectMode)

    def reset_button_clicked(self):
        self.view_box.autoRange()

    def select_by_click(self, _, points):
        self.select(points)

    def select_by_rectangle(self, value_rect):
        points = [point
                  for point in self.scatterplot_item.points()
                  if value_rect.contains(QPointF(point.pos()))]
        self.select(points)

    def unselect_all(self):
        self.selection = None
        self.update_colors(keep_colors=True)

    def select(self, points):
        # noinspection PyArgumentList
        keys = QApplication.keyboardModifiers()
        if self.selection is None or not keys & (
                        Qt.ShiftModifier + Qt.ControlModifier + Qt.AltModifier):
            self.selection = np.full(self.n_points, False, dtype=np.bool)
        indices = [p.data() for p in points]
        if keys & Qt.ControlModifier:
            self.selection[indices] = False
        elif keys & Qt.AltModifier:
            self.selection[indices] = 1 - self.selection[indices]
        else:  # Handle shift and no modifiers
            self.selection[indices] = True
        self.update_colors(keep_colors=True)
        self.master.selection_changed()

    def get_selection(self):
        if self.selection is None:
            return np.array([], dtype=int)
        else:
            return np.arange(len(self.raw_data)
                )[self.valid_data][self.selection]

    def set_palette(self, p):
        self.plot_widget.setPalette(p)

    def save_to_file(self, size):
        pass
Beispiel #6
0
class OWScatterPlotGraph(gui.OWComponent, ScaleScatterPlotData):
    attr_color = ContextSetting("", ContextSetting.OPTIONAL)
    attr_label = ContextSetting("", ContextSetting.OPTIONAL)
    attr_shape = ContextSetting("", ContextSetting.OPTIONAL)
    attr_size = ContextSetting("", ContextSetting.OPTIONAL)

    point_width = Setting(10)
    alpha_value = Setting(255)
    show_grid = Setting(False)
    show_legend = Setting(True)
    tooltip_shows_all = Setting(False)
    square_granularity = Setting(3)
    space_between_cells = Setting(True)

    CurveSymbols = np.array("o x t + d s ?".split())
    MinShapeSize = 6
    DarkerValue = 120
    UnknownColor = (168, 50, 168)

    def __init__(self, scatter_widget, parent=None, _="None"):
        gui.OWComponent.__init__(self, scatter_widget)
        self.view_box = InteractiveViewBox(self)
        self.plot_widget = pg.PlotWidget(viewBox=self.view_box,
                                         parent=parent,
                                         background="w")
        self.plot_widget.setAntialiasing(True)
        self.plot_widget.sizeHint = lambda: QtCore.QSize(500, 500)

        self.replot = self.plot_widget.replot
        ScaleScatterPlotData.__init__(self)
        self.scatterplot_item = None

        self.labels = []

        self.master = scatter_widget
        self.shown_attribute_indices = []
        self.shown_x = ""
        self.shown_y = ""
        self.pen_colors = self.brush_colors = None

        self.valid_data = None  # np.ndarray
        self.selection = None  # np.ndarray
        self.n_points = 0

        self.gui = OWPlotGUI(self)
        self.continuous_palette = ContinuousPaletteGenerator(
            QColor(255, 255, 0), QColor(0, 0, 255), True)
        self.discrete_palette = ColorPaletteGenerator()

        self.selection_behavior = 0

        self.legend = self.color_legend = None
        self.scale = None  # DiscretizedScale

        # self.setMouseTracking(True)
        # self.grabGesture(QPinchGesture)
        # self.grabGesture(QPanGesture)

        self.update_grid()

        self._tooltip_delegate = HelpEventDelegate(self.help_event)
        self.plot_widget.scene().installEventFilter(self._tooltip_delegate)

    def set_data(self, data, subset_data=None, **args):
        self.plot_widget.clear()
        ScaleScatterPlotData.set_data(self, data, subset_data, **args)

    def update_data(self, attr_x, attr_y):
        self.shown_x = attr_x
        self.shown_y = attr_y

        self.remove_legend()
        if self.scatterplot_item:
            self.plot_widget.removeItem(self.scatterplot_item)
        for label in self.labels:
            self.plot_widget.removeItem(label)
        self.labels = []
        self.set_axis_title("bottom", "")
        self.set_axis_title("left", "")

        if self.scaled_data is None or not len(self.scaled_data):
            self.valid_data = None
            self.n_points = 0
            return

        index_x = self.attribute_name_index[attr_x]
        index_y = self.attribute_name_index[attr_y]
        self.valid_data = self.get_valid_list([index_x, index_y])
        x_data, y_data = self.get_xy_data_positions(attr_x, attr_y,
                                                    self.valid_data)
        x_data = x_data[self.valid_data]
        y_data = y_data[self.valid_data]
        self.n_points = len(x_data)

        for axis, name, index in (("bottom", attr_x, index_x), ("left", attr_y,
                                                                index_y)):
            self.set_axis_title(axis, name)
            var = self.data_domain[index]
            if isinstance(var, DiscreteVariable):
                self.set_labels(axis, get_variable_values_sorted(var))
            else:
                self.set_labels(axis, None)

        color_data, brush_data = self.compute_colors()
        size_data = self.compute_sizes()
        shape_data = self.compute_symbols()
        self.scatterplot_item = ScatterPlotItem(x=x_data,
                                                y=y_data,
                                                data=np.arange(self.n_points),
                                                symbol=shape_data,
                                                size=size_data,
                                                pen=color_data,
                                                brush=brush_data)

        self.plot_widget.addItem(self.scatterplot_item)

        self.scatterplot_item.selected_points = []
        self.scatterplot_item.sigClicked.connect(self.select_by_click)

        self.update_labels()
        self.make_legend()
        self.plot_widget.replot()

    def set_labels(self, axis, labels):
        axis = self.plot_widget.getAxis(axis)
        if labels:
            ticks = [[(i, labels[i]) for i in range(len(labels))]]
            axis.setTicks(ticks)
        else:
            axis.setTicks(None)

    def set_axis_title(self, axis, title):
        self.plot_widget.setLabel(axis=axis, text=title)

    def get_size_index(self):
        size_index = -1
        attr_size = self.attr_size
        if attr_size != "" and attr_size != "(Same size)":
            size_index = self.attribute_name_index[attr_size]
        return size_index

    def compute_sizes(self):
        size_index = self.get_size_index()
        if size_index == -1:
            size_data = np.full((self.n_points, ), self.point_width)
        else:
            size_data = \
                self.MinShapeSize + \
                self.no_jittering_scaled_data[size_index] * self.point_width
        size_data[np.isnan(size_data)] = self.MinShapeSize - 2
        return size_data

    def update_sizes(self):
        if self.scatterplot_item:
            size_data = self.compute_sizes()
            self.scatterplot_item.setSize(size_data)

    update_point_size = update_sizes

    def get_color_index(self):
        color_index = -1
        attr_color = self.attr_color
        if attr_color != "" and attr_color != "(Same color)":
            color_index = self.attribute_name_index[attr_color]
            color_var = self.data_domain[attr_color]
            if isinstance(color_var, DiscreteVariable):
                self.discrete_palette.set_number_of_colors(
                    len(color_var.values))
        return color_index

    def compute_colors(self, keep_colors=False):
        if not keep_colors:
            self.pen_colors = self.brush_colors = None
        color_index = self.get_color_index()
        if color_index == -1:
            color = self.plot_widget.palette().color(OWPalette.Data)
            pen = [QPen(QBrush(color), 1.5)] * self.n_points
            if self.selection is not None:
                brush = [(QBrush(QColor(128, 128, 128,
                                        255)), QBrush(QColor(128, 128,
                                                             128)))[s]
                         for s in self.selection]
            else:
                brush = [QBrush(QColor(128, 128, 128))] * self.n_points
            return pen, brush

        c_data = self.original_data[color_index, self.valid_data]
        if isinstance(self.data_domain[color_index], ContinuousVariable):
            if self.pen_colors is None:
                self.scale = DiscretizedScale(np.min(c_data), np.max(c_data))
                c_data -= self.scale.offset
                c_data /= self.scale.width
                c_data = np.floor(c_data) + 0.5
                c_data /= self.scale.bins
                c_data = np.clip(c_data, 0, 1)
                palette = self.continuous_palette
                self.pen_colors = palette.getRGB(c_data)
                self.brush_colors = np.hstack([
                    self.pen_colors,
                    np.full((self.n_points, 1), self.alpha_value)
                ])
                self.pen_colors *= 100 / self.DarkerValue
                self.pen_colors = [
                    QPen(QBrush(QColor(*col)), 1.5)
                    for col in self.pen_colors.tolist()
                ]
            if self.selection is not None:
                self.brush_colors[:, 3] = 0
                self.brush_colors[self.selection, 3] = self.alpha_value
            else:
                self.brush_colors[:, 3] = self.alpha_value
            pen = self.pen_colors
            brush = np.array(
                [QBrush(QColor(*col)) for col in self.brush_colors.tolist()])
        else:
            if self.pen_colors is None:
                palette = self.discrete_palette
                n_colors = palette.number_of_colors
                c_data = c_data.copy()
                c_data[np.isnan(c_data)] = n_colors
                c_data = c_data.astype(int)
                colors = palette.getRGB(np.arange(n_colors + 1))
                colors[n_colors] = (128, 128, 128)
                pens = np.array([
                    QPen(QBrush(QColor(*col).darker(self.DarkerValue)), 1.5)
                    for col in colors
                ])
                self.pen_colors = pens[c_data]
                self.brush_colors = np.array([[
                    QBrush(QColor(0, 0, 0, 0)),
                    QBrush(QColor(col[0], col[1], col[2], self.alpha_value))
                ] for col in colors])
                self.brush_colors = self.brush_colors[c_data]
            if self.selection is not None:
                brush = np.where(self.selection, self.brush_colors[:, 1],
                                 self.brush_colors[:, 0])
            else:
                brush = self.brush_colors[:, 1]
            pen = self.pen_colors
        return pen, brush

    def update_colors(self, keep_colors=False):
        if self.scatterplot_item:
            pen_data, brush_data = self.compute_colors(keep_colors)
            self.scatterplot_item.setPen(pen_data, update=False, mask=None)
            self.scatterplot_item.setBrush(brush_data, mask=None)
            if not keep_colors:
                self.make_legend()

    update_alpha_value = update_colors

    def create_labels(self):
        for x, y in zip(*self.scatterplot_item.getData()):
            ti = TextItem()
            self.plot_widget.addItem(ti)
            ti.setPos(x, y)
            self.labels.append(ti)

    def update_labels(self):
        if not self.attr_label:
            for label in self.labels:
                label.setText("")
            return
        if not self.labels:
            self.create_labels()
        label_column = self.raw_data.get_column_view(self.attr_label)[0]
        formatter = self.raw_data.domain[self.attr_label].str_val
        label_data = map(formatter, label_column)
        black = pg.mkColor(0, 0, 0)
        for label, text in zip(self.labels, label_data):
            label.setText(text, black)

    def get_shape_index(self):
        shape_index = -1
        attr_shape = self.attr_shape
        if attr_shape and attr_shape != "(Same shape)" and \
                len(self.data_domain[attr_shape].values) <= \
                len(self.CurveSymbols):
            shape_index = self.attribute_name_index[attr_shape]
        return shape_index

    def compute_symbols(self):
        shape_index = self.get_shape_index()
        if shape_index == -1:
            shape_data = self.CurveSymbols[np.zeros(self.n_points, dtype=int)]
        else:
            shape_data = self.original_data[shape_index]
            shape_data[np.isnan(shape_data)] = len(self.CurveSymbols) - 1
            shape_data = self.CurveSymbols[shape_data.astype(int)]
        return shape_data

    def update_shapes(self):
        if self.scatterplot_item:
            shape_data = self.compute_symbols()
            self.scatterplot_item.setSymbol(shape_data)
        self.make_legend()

    def update_grid(self):
        self.plot_widget.showGrid(x=self.show_grid, y=self.show_grid)

    def update_legend(self):
        if self.legend:
            self.legend.setVisible(self.show_legend)

    def create_legend(self):
        self.legend = PositionedLegendItem(self.plot_widget.plotItem, self)

    def remove_legend(self):
        if self.legend:
            self.legend.setParent(None)
            self.legend = None
        if self.color_legend:
            self.color_legend.setParent(None)
            self.color_legend = None

    def make_legend(self):
        self.remove_legend()
        self.make_color_legend()
        self.make_shape_legend()
        self.update_legend()

    def make_color_legend(self):
        color_index = self.get_color_index()
        if color_index == -1:
            return
        color_var = self.data_domain[color_index]
        use_shape = self.get_shape_index() == color_index
        if isinstance(color_var, DiscreteVariable):
            if not self.legend:
                self.create_legend()
            palette = self.discrete_palette
            for i, value in enumerate(color_var.values):
                color = QColor(*palette.getRGB(i))
                brush = color.lighter(self.DarkerValue)
                self.legend.addItem(
                    ScatterPlotItem(
                        pen=color,
                        brush=brush,
                        size=10,
                        symbol=self.CurveSymbols[i] if use_shape else "o"),
                    value)
        else:
            legend = self.color_legend = PositionedLegendItem(
                self.plot_widget.plotItem,
                self,
                legend_id="colors",
                at_bottom=True)
            label = PaletteItemSample(self.continuous_palette, self.scale)
            legend.addItem(label, "")
            legend.setGeometry(label.boundingRect())

    def make_shape_legend(self):
        shape_index = self.get_shape_index()
        if shape_index == -1 or shape_index == self.get_color_index():
            return
        if not self.legend:
            self.create_legend()
        shape_var = self.data_domain[shape_index]
        color = self.plot_widget.palette().color(OWPalette.Data)
        pen = QPen(color.darker(self.DarkerValue))
        color.setAlpha(self.alpha_value)
        for i, value in enumerate(shape_var.values):
            self.legend.addItem(
                ScatterPlotItem(pen=pen,
                                brush=color,
                                size=10,
                                symbol=self.CurveSymbols[i]), value)

    def zoom_button_clicked(self):
        self.scatterplot_item.getViewBox().setMouseMode(
            self.scatterplot_item.getViewBox().RectMode)

    def pan_button_clicked(self):
        self.scatterplot_item.getViewBox().setMouseMode(
            self.scatterplot_item.getViewBox().PanMode)

    def select_button_clicked(self):
        self.scatterplot_item.getViewBox().setMouseMode(
            self.scatterplot_item.getViewBox().RectMode)

    def reset_button_clicked(self):
        self.view_box.autoRange()

    def select_by_click(self, _, points):
        self.select(points)

    def select_by_rectangle(self, value_rect):
        points = [
            point for point in self.scatterplot_item.points()
            if value_rect.contains(QPointF(point.pos()))
        ]
        self.select(points)

    def unselect_all(self):
        self.selection = None
        self.update_colors(keep_colors=True)

    def select(self, points):
        # noinspection PyArgumentList
        keys = QApplication.keyboardModifiers()
        if self.selection is None or not keys & (
                Qt.ShiftModifier + Qt.ControlModifier + Qt.AltModifier):
            self.selection = np.full(self.n_points, False, dtype=np.bool)
        indices = [p.data() for p in points]
        if keys & Qt.ControlModifier:
            self.selection[indices] = False
        elif keys & Qt.AltModifier:
            self.selection[indices] = 1 - self.selection[indices]
        else:  # Handle shift and no modifiers
            self.selection[indices] = True
        self.update_colors(keep_colors=True)
        self.master.selection_changed()

    def get_selection(self):
        if self.selection is None:
            return np.array([], dtype=int)
        else:
            return np.arange(len(
                self.raw_data))[self.valid_data][self.selection]

    def set_palette(self, p):
        self.plot_widget.setPalette(p)

    def save_to_file(self, size):
        pass

    def help_event(self, event):
        if self.scatterplot_item is None:
            return False

        act_pos = self.scatterplot_item.mapFromScene(event.scenePos())
        points = self.scatterplot_item.pointsAt(act_pos)
        text = ""
        if len(points):
            for i, p in enumerate(points):
                index = p.data()
                text += "Attributes:\n"
                if self.tooltip_shows_all:
                    text += "".join('   {} = {}\n'.format(
                        attr.name, self.raw_data[index][attr])
                                    for attr in self.data_domain.attributes)
                else:
                    text += '   {} = {}\n   {} = {}\n'.format(
                        self.shown_x, self.raw_data[index][self.shown_x],
                        self.shown_y, self.raw_data[index][self.shown_y])
                if self.data_domain.class_var:
                    text += 'Class:\n   {} = {}\n'.format(
                        self.data_domain.class_var.name,
                        self.raw_data[index][self.raw_data.domain.class_var])
                if i < len(points) - 1:
                    text += '------------------\n'

            text = ('<span style="white-space:pre">{}</span>'.format(
                escape(text)))

            QToolTip.showText(event.screenPos(), text, widget=self.plot_widget)
            return True
        else:
            return False