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_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 onZipfPlot(self): before = time.time() pg.setConfigOptions(antialias=True) dialog= QtGui.QDialog(self) plotWidget = pg.PlotWidget(name='Zipf\'s Law Plot') logx = self.model.getLogX() ab = self.model.getPolyFit() s = ScatterPlotItem(logx, self.model.getLogfreqDist(), size=4, pen=None, brush=pg.mkBrush(255, 255, 255)) s.addPoints(logx, self.model.getLogfreqDist()) plot = plotWidget.plot(logx, [self.model.getPoly(x) for x in logx], pen=(0,255,255), size=4) legend = LegendItem((130,60), offset=(500,30)) legend.setParentItem(plotWidget.getPlotItem()) legend.addItem(s, 'Corpus data') legend.addItem(plot, str(round(ab[0], 3)) + 'x + ' +str(round(ab[1], 3))) plotWidget.addItem(s) lay = QtGui.QVBoxLayout() lay.addWidget(plotWidget) dialog.setLayout(lay) dialog.show() self.fillTimeData("creating Zipf plot", time.time() - before)
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])
def create_legend(self): self.legend = LegendItem() self.legend.setParentItem(self.plot_widget.getViewBox()) self.legend.restoreAnchor(self.__legend_anchor)
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
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
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])
def __init__(self, offset, text_size='9pt'): self.text_size = text_size LegendItem.__init__(self, None, offset)