def _on_view_context_menu(self, pos): widget = self.scene.widget if widget is None: return assert isinstance(widget, HeatmapGridWidget) menu = QMenu(self.view.viewport()) menu.setAttribute(Qt.WA_DeleteOnClose) menu.addActions(self.view.actions()) menu.addSeparator() menu.addActions([self.__font_inc, self.__font_dec]) menu.addSeparator() a = QAction("Keep aspect ratio", menu, checkable=True) a.setChecked(self.keep_aspect) def ontoggled(state): self.keep_aspect = state self.__aspect_mode_changed() a.toggled.connect(ontoggled) menu.addAction(a) menu.popup(self.view.viewport().mapToGlobal(pos))
class SplitterResizer(QObject): """ An object able to control the size of a widget in a QSplitter instance. """ def __init__(self, parent=None, **kwargs): # type: (Optional[QObject], Any) -> None super().__init__(parent, **kwargs) self.__splitter = None # type: Optional[QSplitter] self.__widget = None # type: Optional[QWidget] self.__updateOnShow = True # Need __update on next show event self.__animationEnabled = True self.__size = -1 self.__expanded = False self.__animation = QPropertyAnimation( self, b"size_", self, duration=200 ) self.__action = QAction("toggle-expanded", self, checkable=True) self.__action.triggered[bool].connect(self.setExpanded) def setSize(self, size): # type: (int) -> None """ Set the size of the controlled widget (either width or height depending on the orientation). .. note:: The controlled widget's size is only updated when it it is shown. """ if self.__size != size: self.__size = size self.__update() def size(self): # type: () -> int """ Return the size of the widget in the splitter (either height of width) depending on the splitter orientation. """ if self.__splitter and self.__widget: index = self.__splitter.indexOf(self.__widget) sizes = self.__splitter.sizes() return sizes[index] else: return -1 size_ = Property(int, fget=size, fset=setSize) def setAnimationEnabled(self, enable): # type: (bool) -> None """Enable/disable animation.""" self.__animation.setDuration(0 if enable else 200) def animationEnabled(self): # type: () -> bool return self.__animation.duration() == 0 def setSplitterAndWidget(self, splitter, widget): # type: (QSplitter, QWidget) -> None """Set the QSplitter and QWidget instance the resizer should control. .. note:: the widget must be in the splitter. """ if splitter and widget and not splitter.indexOf(widget) > 0: raise ValueError("Widget must be in a splitter.") if self.__widget is not None: self.__widget.removeEventFilter(self) if self.__splitter is not None: self.__splitter.removeEventFilter(self) self.__splitter = splitter self.__widget = widget if widget is not None: widget.installEventFilter(self) if splitter is not None: splitter.installEventFilter(self) self.__update() size = self.size() if self.__expanded and size == 0: self.open() elif not self.__expanded and size > 0: self.close() def toggleExpandedAction(self): # type: () -> QAction """Return a QAction that can be used to toggle expanded state. """ return self.__action def toogleExpandedAction(self): warnings.warn( "'toogleExpandedAction is deprecated, use 'toggleExpandedAction' " "instead.", DeprecationWarning, stacklevel=2 ) return self.toggleExpandedAction() def open(self): # type: () -> None """Open the controlled widget (expand it to sizeHint). """ self.__expanded = True self.__action.setChecked(True) if self.__splitter is None or self.__widget is None: return hint = self.__widget.sizeHint() if self.__splitter.orientation() == Qt.Vertical: end = hint.height() else: end = hint.width() self.__animation.setStartValue(0) self.__animation.setEndValue(end) self.__animation.start() def close(self): # type: () -> None """Close the controlled widget (shrink to size 0). """ self.__expanded = False self.__action.setChecked(False) if self.__splitter is None or self.__widget is None: return self.__animation.setStartValue(self.size()) self.__animation.setEndValue(0) self.__animation.start() def setExpanded(self, expanded): # type: (bool) -> None """Set the expanded state.""" if self.__expanded != expanded: if expanded: self.open() else: self.close() def expanded(self): # type: () -> bool """Return the expanded state.""" return self.__expanded def __update(self): # type: () -> None """Update the splitter sizes.""" if self.__splitter and self.__widget: if sum(self.__splitter.sizes()) == 0: # schedule update on next show event self.__updateOnShow = True return splitter = self.__splitter index = splitter.indexOf(self.__widget) sizes = splitter.sizes() current = sizes[index] diff = current - self.__size sizes[index] = self.__size sizes[index - 1] = sizes[index - 1] + diff self.__splitter.setSizes(sizes) def eventFilter(self, obj, event): # type: (QObject, QEvent) -> bool if event.type() == QEvent.Resize and obj is self.__widget and \ self.__animation.state() == QPropertyAnimation.Stopped: # Update the expanded state when the user opens/closes the widget # by dragging the splitter handle. assert self.__splitter is not None assert isinstance(event, QResizeEvent) if self.__splitter.orientation() == Qt.Vertical: size = event.size().height() else: size = event.size().width() if self.__expanded and size == 0: self.__action.setChecked(False) self.__expanded = False elif not self.__expanded and size > 0: self.__action.setChecked(True) self.__expanded = True if event.type() == QEvent.Show and obj is self.__splitter and \ self.__updateOnShow: # Update the splitter state after receiving valid geometry self.__updateOnShow = False self.__update() return super().eventFilter(obj, event)
class CurvePlot(QWidget, OWComponent): label_title = Setting("") label_xaxis = Setting("") label_yaxis = Setting("") range_x1 = Setting(None) range_x2 = Setting(None) range_y1 = Setting(None) range_y2 = Setting(None) color_attr = ContextSetting(0) invertX = Setting(False) selected_indices = Setting(set()) data_size = Setting(None) # to invalidate selected_indices def __init__(self, parent=None, select=SELECTNONE): QWidget.__init__(self) OWComponent.__init__(self, parent) self.parent = parent self.selection_type = select self.saving_enabled = hasattr(self.parent, "save_graph") self.clear_data(init=True) self.plotview = pg.PlotWidget(background="w", viewBox=InteractiveViewBox(self)) self.plot = self.plotview.getPlotItem() self.plot.setDownsampling(auto=True, mode="peak") self.markings = [] self.vLine = pg.InfiniteLine(angle=90, movable=False) self.hLine = pg.InfiniteLine(angle=0, movable=False) self.proxy = pg.SignalProxy(self.plot.scene().sigMouseMoved, rateLimit=20, slot=self.mouseMoved, delay=0.1) self.plot.scene().sigMouseMoved.connect(self.plot.vb.mouseMovedEvent) self.plot.vb.sigRangeChanged.connect(self.resized) self.pen_mouse = pg.mkPen(color=(0, 0, 255), width=2) self.pen_normal = defaultdict(lambda: pg.mkPen(color=(200, 200, 200, 127), width=1)) self.pen_subset = defaultdict(lambda: pg.mkPen(color=(0, 0, 0, 127), width=1)) self.pen_selected = defaultdict(lambda: pg.mkPen(color=(0, 0, 0, 127), width=2, style=Qt.DotLine)) self.label = pg.TextItem("", anchor=(1, 0)) self.label.setText("", color=(0, 0, 0)) self.discrete_palette = None QPixmapCache.setCacheLimit(max(QPixmapCache.cacheLimit(), 100 * 1024)) self.curves_cont = PlotCurvesItem() self.important_decimals = 4, 4 self.MOUSE_RADIUS = 20 self.clear_graph() #interface settings self.location = True #show current position self.markclosest = True #mark self.crosshair = True self.crosshair_hidden = True self.viewtype = INDIVIDUAL layout = QVBoxLayout() self.setLayout(layout) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self.plotview) actions = [] zoom_in = QAction( "Zoom in", self, triggered=self.plot.vb.set_mode_zooming ) zoom_in.setShortcuts([Qt.Key_Z, QKeySequence(QKeySequence.ZoomIn)]) actions.append(zoom_in) zoom_fit = QAction( "Zoom to fit", self, triggered=lambda x: (self.plot.vb.autoRange(), self.plot.vb.set_mode_panning()) ) zoom_fit.setShortcuts([Qt.Key_Backspace, QKeySequence(Qt.ControlModifier | Qt.Key_0)]) actions.append(zoom_fit) rescale_y = QAction( "Rescale Y to fit", self, shortcut=Qt.Key_D, triggered=self.rescale_current_view_y ) actions.append(rescale_y) view_individual = QAction( "Show individual", self, shortcut=Qt.Key_I, triggered=lambda x: self.show_individual() ) actions.append(view_individual) view_average = QAction( "Show averages", self, shortcut=Qt.Key_A, triggered=lambda x: self.show_average() ) actions.append(view_average) self.show_grid = False self.show_grid_a = QAction( "Show grid", self, shortcut=Qt.Key_G, checkable=True, triggered=self.grid_changed ) actions.append(self.show_grid_a) self.invertX_menu = QAction( "Invert X", self, shortcut=Qt.Key_X, checkable=True, triggered=self.invertX_changed ) actions.append(self.invertX_menu) if self.selection_type == SELECTMANY: select_curves = QAction( "Select (line)", self, triggered=self.plot.vb.set_mode_select, ) select_curves.setShortcuts([Qt.Key_S]) actions.append(select_curves) if self.saving_enabled: save_graph = QAction( "Save graph", self, triggered=self.save_graph, ) save_graph.setShortcuts([QKeySequence(Qt.ControlModifier | Qt.Key_S)]) actions.append(save_graph) range_menu = MenuFocus("Define view range", self) range_action = QWidgetAction(self) layout = QGridLayout() range_box = gui.widgetBox(self, margin=5, orientation=layout) range_box.setFocusPolicy(Qt.TabFocus) self.range_e_x1 = lineEditFloatOrNone(None, self, "range_x1", label="e") range_box.setFocusProxy(self.range_e_x1) self.range_e_x2 = lineEditFloatOrNone(None, self, "range_x2", label="e") layout.addWidget(QLabel("X"), 0, 0, Qt.AlignRight) layout.addWidget(self.range_e_x1, 0, 1) layout.addWidget(QLabel("-"), 0, 2) layout.addWidget(self.range_e_x2, 0, 3) self.range_e_y1 = lineEditFloatOrNone(None, self, "range_y1", label="e") self.range_e_y2 = lineEditFloatOrNone(None, self, "range_y2", label="e") layout.addWidget(QLabel("Y"), 1, 0, Qt.AlignRight) layout.addWidget(self.range_e_y1, 1, 1) layout.addWidget(QLabel("-"), 1, 2) layout.addWidget(self.range_e_y2, 1, 3) b = gui.button(None, self, "Apply", callback=self.set_limits) layout.addWidget(b, 2, 3, Qt.AlignRight) range_action.setDefaultWidget(range_box) range_menu.addAction(range_action) layout = QGridLayout() self.plotview.setLayout(layout) self.button = QPushButton("View", self.plotview) self.button.setAutoDefault(False) layout.setRowStretch(1, 1) layout.setColumnStretch(1, 1) layout.addWidget(self.button, 0, 0) view_menu = MenuFocus(self) self.button.setMenu(view_menu) view_menu.addActions(actions) view_menu.addMenu(range_menu) self.addActions(actions) choose_color_action = QWidgetAction(self) choose_color_box = gui.hBox(self) choose_color_box.setFocusPolicy(Qt.TabFocus) model = VariableListModel() self.attrs = [] model.wrap(self.attrs) label = gui.label(choose_color_box, self, "Color by") label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) self.attrCombo = gui.comboBox( choose_color_box, self, value="color_attr", contentsLength=12, callback=self.change_color_attr) self.attrCombo.setModel(model) choose_color_box.setFocusProxy(self.attrCombo) choose_color_action.setDefaultWidget(choose_color_box) view_menu.addAction(choose_color_action) labels_action = QWidgetAction(self) layout = QGridLayout() labels_box = gui.widgetBox(self, margin=0, orientation=layout) t = gui.lineEdit(None, self, "label_title", label="Title:", callback=self.labels_changed, callbackOnType=self.labels_changed) layout.addWidget(QLabel("Title:"), 0, 0, Qt.AlignRight) layout.addWidget(t, 0, 1) t = gui.lineEdit(None, self, "label_xaxis", label="X-axis:", callback=self.labels_changed, callbackOnType=self.labels_changed) layout.addWidget(QLabel("X-axis:"), 1, 0, Qt.AlignRight) layout.addWidget(t, 1, 1) t = gui.lineEdit(None, self, "label_yaxis", label="Y-axis:", callback=self.labels_changed, callbackOnType=self.labels_changed) layout.addWidget(QLabel("Y-axis:"), 2, 0, Qt.AlignRight) layout.addWidget(t, 2, 1) labels_action.setDefaultWidget(labels_box) view_menu.addAction(labels_action) self.labels_changed() # apply saved labels self.invertX_apply() self.plot.vb.set_mode_panning() self.reports = {} # current reports self.viewhelpers_show() def report(self, reporter, contents): self.reports[id(reporter)] = contents def report_finished(self, reporter): try: self.reports.pop(id(reporter)) except KeyError: pass # ok if it was already removed if not self.reports: pass def set_limits(self): vr = self.plot.vb.viewRect() x1 = self.range_x1 if self.range_x1 is not None else vr.left() x2 = self.range_x2 if self.range_x2 is not None else vr.right() y1 = self.range_y1 if self.range_y1 is not None else vr.top() y2 = self.range_y2 if self.range_y2 is not None else vr.bottom() self.plot.vb.setXRange(x1, x2) self.plot.vb.setYRange(y1, y2) def labels_changed(self): self.plot.setTitle(self.label_title) if not self.label_title: self.plot.setTitle(None) self.plot.setLabels(bottom=self.label_xaxis) self.plot.showLabel("bottom", bool(self.label_xaxis)) self.plot.getAxis("bottom").resizeEvent() # align text self.plot.setLabels(left=self.label_yaxis) self.plot.showLabel("left", bool(self.label_yaxis)) self.plot.getAxis("left").resizeEvent() # align text def grid_changed(self): self.show_grid = not self.show_grid self.grid_apply() def grid_apply(self): self.plot.showGrid(self.show_grid, self.show_grid, alpha=0.3) self.show_grid_a.setChecked(self.show_grid) def invertX_changed(self): self.invertX = not self.invertX self.invertX_apply() def invertX_apply(self): self.plot.vb.invertX(self.invertX) self.resized() # force redraw of axes (to avoid a pyqtgraph bug) vr = self.plot.vb.viewRect() self.plot.vb.setRange(xRange=(0,1), yRange=(0,1)) self.plot.vb.setRange(rect=vr) self.invertX_menu.setChecked(self.invertX) def save_graph(self): self.viewhelpers_hide() self.plot.showAxis("top", True) self.plot.showAxis("right", True) self.parent.save_graph() self.plot.showAxis("top", False) self.plot.showAxis("right", False) self.viewhelpers_show() def clear_data(self, init=True): self.subset_ids = set() self.data = None self.data_x = None self.data_ys = None self.sampled_indices = [] self.sampled_indices_inverse = {} self.sampling = None if not init: self.selection_changed() self.discrete_palette = None def clear_graph(self): # reset caching. if not, it is not cleared when view changing when zoomed self.highlighted = None self.curves_cont.setCacheMode(QGraphicsItem.NoCache) self.curves_cont.setCacheMode(QGraphicsItem.DeviceCoordinateCache) self.plot.vb.disableAutoRange() self.curves_cont.clear() self.curves_cont.update() self.plotview.clear() self.curves_plotted = [] # currently plotted elements (for rescale) self.curves = [] # for finding closest curve self.plotview.addItem(self.label, ignoreBounds=True) self.highlighted_curve = pg.PlotCurveItem(pen=self.pen_mouse) self.highlighted_curve.setZValue(10) self.highlighted_curve.hide() self.selection_line = pg.PlotCurveItem() self.selection_line.setPen(pg.mkPen(color=QColor(Qt.black), width=2, style=Qt.DotLine)) self.selection_line.setZValue(1e9) self.selection_line.hide() self.plot.addItem(self.highlighted_curve) self.plot.addItem(self.vLine, ignoreBounds=True) self.plot.addItem(self.hLine, ignoreBounds=True) self.viewhelpers = True self.plot.addItem(self.selection_line, ignoreBounds=True) self.plot.addItem(self.curves_cont) for m in self.markings: self.plot.addItem(m, ignoreBounds=True) def resized(self): vr = self.plot.vb.viewRect() xpixel, ypixel = self.plot.vb.viewPixelSize() def important_decimals(n): return max(-int(math.floor(math.log10(n))) + 1, 0) self.important_decimals = important_decimals(xpixel), important_decimals(ypixel) if self.invertX: self.label.setPos(vr.bottomLeft()) else: self.label.setPos(vr.bottomRight()) xd, yd = self.important_decimals self.range_e_x1.setPlaceholderText(("%0." + str(xd) + "f") % vr.left()) self.range_e_x2.setPlaceholderText(("%0." + str(xd) + "f") % vr.right()) self.range_e_y1.setPlaceholderText(("%0." + str(yd) + "f") % vr.top()) self.range_e_y2.setPlaceholderText(("%0." + str(yd) + "f") % vr.bottom()) def make_selection(self, data_indices, add=False): selected_indices = self.selected_indices oldids = selected_indices.copy() invd = self.sampled_indices_inverse if data_indices is None: if not add: selected_indices.clear() self.set_curve_pens([invd[a] for a in oldids if a in invd]) else: if add: selected_indices.update(data_indices) self.set_curve_pens([invd[a] for a in data_indices if a in invd]) else: selected_indices.clear() selected_indices.update(data_indices) self.set_curve_pens([invd[a] for a in (oldids | selected_indices) if a in invd]) self.selection_changed() def selection_changed(self): if self.selection_type: self.parent.selection_changed() def viewhelpers_hide(self): self.label.hide() self.vLine.hide() self.hLine.hide() def viewhelpers_show(self): self.label.show() if self.crosshair and not self.crosshair_hidden: self.vLine.show() self.hLine.show() else: self.vLine.hide() self.hLine.hide() def mouseMoved(self, evt): pos = evt[0] if self.plot.sceneBoundingRect().contains(pos): mousePoint = self.plot.vb.mapSceneToView(pos) posx, posy = mousePoint.x(), mousePoint.y() labels = [] for a, vs in sorted(self.reports.items()): for v in vs: if isinstance(v, tuple) and len(v) == 2: if v[0] == "x": labels.append(("%0." + str(self.important_decimals[0]) + "f") % v[1]) continue labels.append(str(v)) labels = " ".join(labels) self.crosshair_hidden = bool(labels) if self.location and not labels: fs = "%0." + str(self.important_decimals[0]) + "f %0." + str(self.important_decimals[1]) + "f" labels = fs % (posx, posy) self.label.setText(labels, color=(0, 0, 0)) if self.curves and len(self.curves[0][0]): #need non-zero x axis! cache = {} bd = None if self.markclosest and self.plot.vb.action != ZOOMING: xpixel, ypixel = self.plot.vb.viewPixelSize() distances = distancetocurves(self.curves[0], posx, posy, xpixel, ypixel, r=self.MOUSE_RADIUS, cache=cache) try: bd = np.nanargmin(distances) bd = (bd, distances[bd]) except ValueError: # if all distances are NaN pass if self.highlighted is not None: self.highlighted = None self.highlighted_curve.hide() if bd and bd[1] < self.MOUSE_RADIUS: self.highlighted = bd[0] x = self.curves[0][0] y = self.curves[0][1][self.highlighted] self.highlighted_curve.setData(x=x,y=y) self.highlighted_curve.show() self.vLine.setPos(posx) self.hLine.setPos(posy) self.viewhelpers_show() else: self.viewhelpers_hide() def set_curve_pen(self, idc): idcdata = self.sampled_indices[idc] insubset = not self.subset_ids or self.data[idcdata].id in self.subset_ids inselected = self.selection_type and idcdata in self.selected_indices thispen = self.pen_subset if insubset else self.pen_normal if inselected: thispen = self.pen_selected color_var = self._current_color_var() value = None if isinstance(color_var, str) else str(self.data[idcdata][color_var]) self.curves_cont.objs[idc].setPen(thispen[value]) self.curves_cont.objs[idc].setZValue(int(insubset) + int(inselected)) def set_curve_pens(self, curves=None): if self.viewtype == INDIVIDUAL and self.curves: curves = range(len(self.curves[0][1])) if curves is None else curves for i in curves: self.set_curve_pen(i) self.curves_cont.update() def add_marking(self, item): self.markings.append(item) self.plot.addItem(item, ignoreBounds=True) def remove_marking(self, item): self.plot.removeItem(item) self.markings.remove(item) def clear_markings(self): for m in self.markings: self.plot.removeItem(m) self.markings = [] def add_curves(self, x, ys, addc=True): """ Add multiple curves with the same x domain. """ if len(ys) > MAX_INSTANCES_DRAWN: self.sampled_indices = sorted(random.Random(0).sample(range(len(ys)), MAX_INSTANCES_DRAWN)) self.sampling = True else: self.sampled_indices = list(range(len(ys))) random.Random(0).shuffle(self.sampled_indices) #for sequential classes# self.sampled_indices_inverse = {s: i for i, s in enumerate(self.sampled_indices)} ys = ys[self.sampled_indices] self.curves.append((x, ys)) for y in ys: c = pg.PlotCurveItem(x=x, y=y, pen=self.pen_normal[None]) self.curves_cont.add_curve(c) self.curves_plotted = self.curves def add_curve(self, x, y, pen=None): c = pg.PlotCurveItem(x=x, y=y, pen=pen if pen else self.pen_normal[None]) self.curves_cont.add_curve(c) # for rescale to work correctly self.curves_plotted.append((x, np.array([y]))) def add_fill_curve(self, x, ylow, yhigh, pen): phigh = pg.PlotCurveItem(x, yhigh, pen=pen) plow = pg.PlotCurveItem(x, ylow, pen=pen) color = pen.color() color.setAlphaF(0.2) cc = pg.mkBrush(color) pfill = pg.FillBetweenItem(plow, phigh, brush=cc) pfill.setZValue(10) self.curves_cont.add_curve(pfill) # for zoom to work correctly self.curves_plotted.append((x, np.array([ylow, yhigh]))) def _current_color_var(self): color_var = "(Same color)" try: color_var = self.attrs[self.color_attr] except IndexError: pass return color_var def change_color_attr(self): self.set_pen_colors() self.update_view() def set_pen_colors(self): self.pen_normal.clear() self.pen_subset.clear() self.pen_selected.clear() color_var = self._current_color_var() if color_var != "(Same color)": colors = color_var.colors discrete_palette = ColorPaletteGenerator( number_of_colors=len(colors), rgb_colors=colors) for v in color_var.values: basecolor = discrete_palette[color_var.to_val(v)] basecolor = QColor(basecolor) basecolor.setAlphaF(0.9) self.pen_subset[v] = pg.mkPen(color=basecolor, width=1) self.pen_selected[v] = pg.mkPen(color=basecolor, width=2, style=Qt.DotLine) notselcolor = basecolor.lighter(150) notselcolor.setAlphaF(0.5) self.pen_normal[v] = pg.mkPen(color=notselcolor, width=1) def show_individual(self): if not self.data: return self.viewtype = INDIVIDUAL self.clear_graph() self.add_curves(self.data_x, self.data_ys) self.set_curve_pens() self.curves_cont.update() def rescale_current_view_y(self): if self.curves_plotted: cache = {} qrect = self.plot.vb.targetRect() bleft = qrect.left() bright = qrect.right() ymax = max(np.max(ys[:, searchsorted_cached(cache, x, bleft): searchsorted_cached(cache, x, bright, side="right")]) for x, ys in self.curves_plotted) ymin = min(np.min(ys[:, searchsorted_cached(cache, x, bleft): searchsorted_cached(cache, x, bright, side="right")]) for x, ys in self.curves_plotted) self.plot.vb.setYRange(ymin, ymax, padding=0.0) self.plot.vb.pad_current_view_y() def _split_by_color_value(self, data): color_var = self._current_color_var() rd = defaultdict(list) for i, inst in enumerate(data): value = None if isinstance(color_var, str) else str(inst[color_var]) rd[value].append(i) return rd def show_average(self): if not self.data: return self.viewtype = AVERAGE self.clear_graph() x = self.data_x if self.data: ysall = [] subset_indices = [i for i, id in enumerate(self.data.ids) if id in self.subset_ids] dsplit = self._split_by_color_value(self.data) for colorv, indices in dsplit.items(): for part in ["everything", "subset", "selection"]: if part == "everything": ys = self.data_ys[indices] pen = self.pen_normal if subset_indices else self.pen_subset elif part == "selection" and self.selection_type: current_selected = sorted(set(self.selected_indices) & set(indices)) if not current_selected: continue ys = self.data_ys[current_selected] pen = self.pen_selected elif part == "subset": current_subset = sorted(set(subset_indices) & set(indices)) if not current_subset: continue ys = self.data_ys[current_subset] pen = self.pen_subset std = np.std(ys, axis=0) mean = np.mean(ys, axis=0) ysall.append(mean) penc = QPen(pen[colorv]) penc.setWidth(3) self.add_curve(x, mean, pen=penc) self.add_fill_curve(x, mean + std, mean - std, pen=penc) self.curves.append((x, np.array(ysall))) self.curves_cont.update() def update_view(self): if self.viewtype == INDIVIDUAL: self.show_individual() elif self.viewtype == AVERAGE: self.show_average() def set_data(self, data, rescale="auto"): self.clear_graph() self.clear_data() self.attrs[:] = [] if data is not None: self.attrs[:] = ["(Same color)"] + [ var for var in chain(data.domain, data.domain.metas) if isinstance(var, str) or var.is_discrete] self.color_attr = 0 self.set_pen_colors() if data is not None: if rescale == "auto": if self.data: rescale = not data.domain == self.data.domain else: rescale = True self.data = data # reset selection if dataset sizes do not match if self.selected_indices and \ (max(self.selected_indices) >= len(self.data) or self.data_size != len(self.data)): self.selected_indices.clear() self.data_size = len(self.data) # get and sort input data x = getx(self.data) xsind = np.argsort(x) self.data_x = x[xsind] self.data_ys = data.X[:, xsind] self.update_view() if rescale == True: self.plot.vb.autoRange() def update_display(self): self.curves_cont.update() def set_data_subset(self, ids): self.subset_ids = set(ids) if ids is not None else set() self.set_curve_pens() self.update_view()
class SplitterResizer(QObject): """ An object able to control the size of a widget in a QSplitter instance. """ def __init__(self, parent=None): super().__init__(parent) self.__splitter = None self.__widget = None self.__updateOnShow = True # Need __update on next show event self.__animationEnabled = True self.__size = -1 self.__expanded = False self.__animation = QPropertyAnimation( self, b"size_", self, duration=200 ) self.__action = QAction("toogle-expanded", self, checkable=True) self.__action.triggered[bool].connect(self.setExpanded) def setSize(self, size): """Set the size of the controlled widget (either width or height depending on the orientation). .. note:: The controlled widget's size is only updated when it it is shown. """ if self.__size != size: self.__size = size self.__update() def size(self): """Return the size of the widget in the splitter (either height of width) depending on the splitter orientation. """ if self.__splitter and self.__widget: index = self.__splitter.indexOf(self.__widget) sizes = self.__splitter.sizes() return sizes[index] else: return -1 size_ = Property(int, fget=size, fset=setSize) def setAnimationEnabled(self, enable): """Enable/disable animation. """ self.__animation.setDuration(0 if enable else 200) def animationEnabled(self): return self.__animation.duration() == 0 def setSplitterAndWidget(self, splitter, widget): """Set the QSplitter and QWidget instance the resizer should control. .. note:: the widget must be in the splitter. """ if splitter and widget and not splitter.indexOf(widget) > 0: raise ValueError("Widget must be in a spliter.") if self.__widget is not None: self.__widget.removeEventFilter(self) if self.__splitter is not None: self.__splitter.removeEventFilter(self) self.__splitter = splitter self.__widget = widget if widget is not None: widget.installEventFilter(self) if splitter is not None: splitter.installEventFilter(self) self.__update() size = self.size() if self.__expanded and size == 0: self.open() elif not self.__expanded and size > 0: self.close() def toogleExpandedAction(self): """Return a QAction that can be used to toggle expanded state. """ return self.__action def open(self): """Open the controlled widget (expand it to sizeHint). """ self.__expanded = True self.__action.setChecked(True) if self.__splitter is None or self.__widget is None: return hint = self.__widget.sizeHint() if self.__splitter.orientation() == Qt.Vertical: end = hint.height() else: end = hint.width() self.__animation.setStartValue(0) self.__animation.setEndValue(end) self.__animation.start() def close(self): """Close the controlled widget (shrink to size 0). """ self.__expanded = False self.__action.setChecked(False) if self.__splitter is None or self.__widget is None: return self.__animation.setStartValue(self.size()) self.__animation.setEndValue(0) self.__animation.start() def setExpanded(self, expanded): """Set the expanded state. """ if self.__expanded != expanded: if expanded: self.open() else: self.close() def expanded(self): """Return the expanded state. """ return self.__expanded def __update(self): """Update the splitter sizes. """ if self.__splitter and self.__widget: if sum(self.__splitter.sizes()) == 0: # schedule update on next show event self.__updateOnShow = True return splitter = self.__splitter index = splitter.indexOf(self.__widget) sizes = splitter.sizes() current = sizes[index] diff = current - self.__size sizes[index] = self.__size sizes[index - 1] = sizes[index - 1] + diff self.__splitter.setSizes(sizes) def eventFilter(self, obj, event): if event.type() == QEvent.Resize and obj is self.__widget and \ self.__animation.state() == QPropertyAnimation.Stopped: # Update the expanded state when the user opens/closes the widget # by dragging the splitter handle. if self.__splitter.orientation() == Qt.Vertical: size = event.size().height() else: size = event.size().width() if self.__expanded and size == 0: self.__action.setChecked(False) self.__expanded = False elif not self.__expanded and size > 0: self.__action.setChecked(True) self.__expanded = True if event.type() == QEvent.Show and obj is self.__splitter and \ self.__updateOnShow: # Update the splitter state after receiving valid geometry self.__updateOnShow = False self.__update() return super().eventFilter(obj, event)
class CurvePlot(QWidget, OWComponent): sample_seed = Setting(0, schema_only=True) label_title = Setting("") label_xaxis = Setting("") label_yaxis = Setting("") range_x1 = Setting(None) range_x2 = Setting(None) range_y1 = Setting(None) range_y2 = Setting(None) feature_color = ContextSetting(None) invertX = Setting(False) selected_indices = Setting(set()) data_size = Setting(None) # to invalidate selected_indices viewtype = Setting(INDIVIDUAL) def __init__(self, parent=None, select=SELECTNONE): QWidget.__init__(self) OWComponent.__init__(self, parent) self.parent = parent self.selection_type = select self.saving_enabled = hasattr(self.parent, "save_graph") self.clear_data(init=True) self.subset = None # current subset input self.subset_indices = None # boolean index array with indices in self.data self.plotview = pg.PlotWidget(background="w", viewBox=InteractiveViewBoxC(self)) self.plot = self.plotview.getPlotItem() self.plot.setDownsampling(auto=True, mode="peak") self.markings = [] self.vLine = pg.InfiniteLine(angle=90, movable=False) self.hLine = pg.InfiniteLine(angle=0, movable=False) self.proxy = pg.SignalProxy(self.plot.scene().sigMouseMoved, rateLimit=20, slot=self.mouseMoved, delay=0.1) self.plot.scene().sigMouseMoved.connect(self.plot.vb.mouseMovedEvent) self.plot.vb.sigRangeChanged.connect(self.resized) self.pen_mouse = pg.mkPen(color=(0, 0, 255), width=2) self.pen_normal = defaultdict( lambda: pg.mkPen(color=(200, 200, 200, 127), width=1)) self.pen_subset = defaultdict( lambda: pg.mkPen(color=(0, 0, 0, 127), width=1)) self.pen_selected = defaultdict( lambda: pg.mkPen(color=(0, 0, 0, 127), width=2, style=Qt.DotLine)) self.label = pg.TextItem("", anchor=(1, 0)) self.label.setText("", color=(0, 0, 0)) self.discrete_palette = None QPixmapCache.setCacheLimit(max(QPixmapCache.cacheLimit(), 100 * 1024)) self.curves_cont = PlotCurvesItem() self.important_decimals = 4, 4 self.plot.scene().installEventFilter( HelpEventDelegate(self.help_event, self)) # whether to rescale at next update self.rescale_next = True self.MOUSE_RADIUS = 20 self.clear_graph() # interface settings self.location = True # show current position self.markclosest = True # mark self.crosshair = True self.crosshair_hidden = True layout = QVBoxLayout() self.setLayout(layout) self.layout().setContentsMargins(0, 0, 0, 0) self.layout().addWidget(self.plotview) actions = [] resample_curves = QAction( "Resample curves", self, shortcut=Qt.Key_R, triggered=lambda x: self.resample_curves(self.sample_seed + 1)) actions.append(resample_curves) reset_curves = QAction("Resampling reset", self, shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_R), triggered=lambda x: self.resample_curves(0)) actions.append(reset_curves) zoom_in = QAction("Zoom in", self, triggered=self.plot.vb.set_mode_zooming) zoom_in.setShortcuts([Qt.Key_Z, QKeySequence(QKeySequence.ZoomIn)]) zoom_in.setShortcutContext(Qt.WidgetWithChildrenShortcut) actions.append(zoom_in) zoom_fit = QAction( "Zoom to fit", self, triggered=lambda x: (self.plot.vb.autoRange(), self.plot.vb.set_mode_panning())) zoom_fit.setShortcuts( [Qt.Key_Backspace, QKeySequence(Qt.ControlModifier | Qt.Key_0)]) zoom_fit.setShortcutContext(Qt.WidgetWithChildrenShortcut) actions.append(zoom_fit) rescale_y = QAction("Rescale Y to fit", self, shortcut=Qt.Key_D, triggered=self.rescale_current_view_y) actions.append(rescale_y) self.view_average_menu = QAction( "Show averages", self, shortcut=Qt.Key_A, checkable=True, triggered=lambda x: self.viewtype_changed()) actions.append(self.view_average_menu) self.show_grid = False self.show_grid_a = QAction("Show grid", self, shortcut=Qt.Key_G, checkable=True, triggered=self.grid_changed) actions.append(self.show_grid_a) self.invertX_menu = QAction("Invert X", self, shortcut=Qt.Key_X, checkable=True, triggered=self.invertX_changed) actions.append(self.invertX_menu) if self.selection_type == SELECTMANY: select_curves = QAction( "Select (line)", self, triggered=self.line_select_start, ) select_curves.setShortcuts([Qt.Key_S]) select_curves.setShortcutContext(Qt.WidgetWithChildrenShortcut) actions.append(select_curves) if self.saving_enabled: save_graph = QAction( "Save graph", self, triggered=self.save_graph, ) save_graph.setShortcuts( [QKeySequence(Qt.ControlModifier | Qt.Key_S)]) actions.append(save_graph) range_menu = MenuFocus("Define view range", self) range_action = QWidgetAction(self) layout = QGridLayout() range_box = gui.widgetBox(self, margin=5, orientation=layout) range_box.setFocusPolicy(Qt.TabFocus) self.range_e_x1 = lineEditFloatOrNone(None, self, "range_x1", label="e") range_box.setFocusProxy(self.range_e_x1) self.range_e_x2 = lineEditFloatOrNone(None, self, "range_x2", label="e") layout.addWidget(QLabel("X"), 0, 0, Qt.AlignRight) layout.addWidget(self.range_e_x1, 0, 1) layout.addWidget(QLabel("-"), 0, 2) layout.addWidget(self.range_e_x2, 0, 3) self.range_e_y1 = lineEditFloatOrNone(None, self, "range_y1", label="e") self.range_e_y2 = lineEditFloatOrNone(None, self, "range_y2", label="e") layout.addWidget(QLabel("Y"), 1, 0, Qt.AlignRight) layout.addWidget(self.range_e_y1, 1, 1) layout.addWidget(QLabel("-"), 1, 2) layout.addWidget(self.range_e_y2, 1, 3) b = gui.button(None, self, "Apply", callback=self.set_limits) layout.addWidget(b, 2, 3, Qt.AlignRight) range_action.setDefaultWidget(range_box) range_menu.addAction(range_action) layout = QGridLayout() self.plotview.setLayout(layout) self.button = QPushButton("View", self.plotview) self.button.setAutoDefault(False) layout.setRowStretch(1, 1) layout.setColumnStretch(1, 1) layout.addWidget(self.button, 0, 0) view_menu = MenuFocus(self) self.button.setMenu(view_menu) view_menu.addActions(actions) view_menu.addMenu(range_menu) self.addActions(actions) choose_color_action = QWidgetAction(self) choose_color_box = gui.hBox(self) choose_color_box.setFocusPolicy(Qt.TabFocus) label = gui.label(choose_color_box, self, "Color by") label.setAlignment(Qt.AlignRight | Qt.AlignVCenter) self.feature_color_model = DomainModel( DomainModel.METAS | DomainModel.CLASSES, valid_types=(DiscreteVariable, ), placeholder="None") self.feature_color_combo = gui.comboBox(choose_color_box, self, "feature_color", callback=self.update_view, model=self.feature_color_model, valueType=str) choose_color_box.setFocusProxy(self.feature_color_combo) choose_color_action.setDefaultWidget(choose_color_box) view_menu.addAction(choose_color_action) cycle_colors = QShortcut(Qt.Key_C, self, self.cycle_color_attr, context=Qt.WidgetWithChildrenShortcut) labels_action = QWidgetAction(self) layout = QGridLayout() labels_box = gui.widgetBox(self, margin=0, orientation=layout) t = gui.lineEdit(None, self, "label_title", label="Title:", callback=self.labels_changed, callbackOnType=self.labels_changed) layout.addWidget(QLabel("Title:"), 0, 0, Qt.AlignRight) layout.addWidget(t, 0, 1) t = gui.lineEdit(None, self, "label_xaxis", label="X-axis:", callback=self.labels_changed, callbackOnType=self.labels_changed) layout.addWidget(QLabel("X-axis:"), 1, 0, Qt.AlignRight) layout.addWidget(t, 1, 1) t = gui.lineEdit(None, self, "label_yaxis", label="Y-axis:", callback=self.labels_changed, callbackOnType=self.labels_changed) layout.addWidget(QLabel("Y-axis:"), 2, 0, Qt.AlignRight) layout.addWidget(t, 2, 1) labels_action.setDefaultWidget(labels_box) view_menu.addAction(labels_action) self.labels_changed() # apply saved labels self.invertX_apply() self.plot.vb.set_mode_panning() self.reports = {} # current reports self.viewhelpers_show() def line_select_start(self): if self.viewtype == INDIVIDUAL: self.plot.vb.set_mode_select() def help_event(self, ev): text = "" if self.highlighted is not None: if self.viewtype == INDIVIDUAL: index = self.sampled_indices[self.highlighted] variables = self.data.domain.metas + self.data.domain.class_vars text += "".join( '{} = {}\n'.format(attr.name, self.data[index][attr]) for attr in variables) elif self.viewtype == AVERAGE: c = self.multiple_curves_info[self.highlighted] nc = sum(c[2]) if c[0] is not None: text += str(c[0]) + " " if c[1]: text += "({})".format(c[1]) if text: text += "\n" text += "{} curves".format(nc) if text: text = text.rstrip() text = ('<span style="white-space:pre">{}</span>'.format( escape(text))) QToolTip.showText(ev.screenPos(), text, widget=self.plotview) return True else: return False def report(self, reporter, contents): self.reports[id(reporter)] = contents def report_finished(self, reporter): try: self.reports.pop(id(reporter)) except KeyError: pass # ok if it was already removed if not self.reports: pass def cycle_color_attr(self): elements = [(a.name if isinstance(a, Variable) else a) for a in self.feature_color_model] currentind = 0 try: currentind = elements.index(self.feature_color) except ValueError: pass next = (currentind + 1) % len(self.feature_color_model) self.feature_color = elements[next] self.update_view() def set_limits(self): vr = self.plot.vb.viewRect() x1 = self.range_x1 if self.range_x1 is not None else vr.left() x2 = self.range_x2 if self.range_x2 is not None else vr.right() y1 = self.range_y1 if self.range_y1 is not None else vr.top() y2 = self.range_y2 if self.range_y2 is not None else vr.bottom() self.plot.vb.setXRange(x1, x2) self.plot.vb.setYRange(y1, y2) def labels_changed(self): self.plot.setTitle(self.label_title) if not self.label_title: self.plot.setTitle(None) self.plot.setLabels(bottom=self.label_xaxis) self.plot.showLabel("bottom", bool(self.label_xaxis)) self.plot.getAxis("bottom").resizeEvent() # align text self.plot.setLabels(left=self.label_yaxis) self.plot.showLabel("left", bool(self.label_yaxis)) self.plot.getAxis("left").resizeEvent() # align text def grid_changed(self): self.show_grid = not self.show_grid self.grid_apply() def grid_apply(self): self.plot.showGrid(self.show_grid, self.show_grid, alpha=0.3) self.show_grid_a.setChecked(self.show_grid) def invertX_changed(self): self.invertX = not self.invertX self.invertX_apply() def invertX_apply(self): self.plot.vb.invertX(self.invertX) self.resized() # force redraw of axes (to avoid a pyqtgraph bug) vr = self.plot.vb.viewRect() self.plot.vb.setRange(xRange=(0, 1), yRange=(0, 1)) self.plot.vb.setRange(rect=vr) self.invertX_menu.setChecked(self.invertX) def save_graph(self): self.viewhelpers_hide() self.plot.showAxis("top", True) self.plot.showAxis("right", True) self.parent.save_graph() self.plot.showAxis("top", False) self.plot.showAxis("right", False) self.viewhelpers_show() def clear_data(self, init=True): self.data = None self.data_x = None # already sorted x-axis self.data_xsind = None # sorting indices for x-axis self.sampled_indices = [] self.sampled_indices_inverse = {} self.sampling = None if not init: self.selection_changed() self.discrete_palette = None def clear_graph(self): # reset caching. if not, it is not cleared when view changing when zoomed self.highlighted = None self.curves_cont.setCacheMode(QGraphicsItem.NoCache) self.curves_cont.setCacheMode(QGraphicsItem.DeviceCoordinateCache) self.plot.vb.disableAutoRange() self.curves_cont.clear() self.curves_cont.update() self.plotview.clear() self.multiple_curves_info = [] self.curves_plotted = [] # currently plotted elements (for rescale) self.curves = [] # for finding closest curve self.plotview.addItem(self.label, ignoreBounds=True) self.highlighted_curve = pg.PlotCurveItem(pen=self.pen_mouse) self.highlighted_curve.setZValue(10) self.highlighted_curve.hide() self.plot.addItem(self.highlighted_curve) self.plot.addItem(self.vLine, ignoreBounds=True) self.plot.addItem(self.hLine, ignoreBounds=True) self.viewhelpers = True self.plot.addItem(self.curves_cont) for m in self.markings: self.plot.addItem(m, ignoreBounds=True) def resized(self): vr = self.plot.vb.viewRect() xpixel, ypixel = self.plot.vb.viewPixelSize() def important_decimals(n): return max(-int(math.floor(math.log10(n))) + 1, 0) self.important_decimals = important_decimals( xpixel), important_decimals(ypixel) if self.invertX: self.label.setPos(vr.bottomLeft()) else: self.label.setPos(vr.bottomRight()) xd, yd = self.important_decimals self.range_e_x1.setPlaceholderText(("%0." + str(xd) + "f") % vr.left()) self.range_e_x2.setPlaceholderText( ("%0." + str(xd) + "f") % vr.right()) self.range_e_y1.setPlaceholderText(("%0." + str(yd) + "f") % vr.top()) self.range_e_y2.setPlaceholderText( ("%0." + str(yd) + "f") % vr.bottom()) def make_selection(self, data_indices, add=False): selected_indices = self.selected_indices oldids = selected_indices.copy() invd = self.sampled_indices_inverse if data_indices is None: if not add: selected_indices.clear() self.set_curve_pens([invd[a] for a in oldids if a in invd]) else: if add: selected_indices.update(data_indices) self.set_curve_pens( [invd[a] for a in data_indices if a in invd]) else: selected_indices.clear() selected_indices.update(data_indices) self.set_curve_pens([ invd[a] for a in (oldids | selected_indices) if a in invd ]) self.selection_changed() def selection_changed(self): if self.selection_type: self.parent.selection_changed() def viewhelpers_hide(self): self.label.hide() self.vLine.hide() self.hLine.hide() def viewhelpers_show(self): self.label.show() if self.crosshair and not self.crosshair_hidden: self.vLine.show() self.hLine.show() else: self.vLine.hide() self.hLine.hide() def mouseMoved(self, evt): pos = evt[0] if self.plot.sceneBoundingRect().contains(pos): mousePoint = self.plot.vb.mapSceneToView(pos) posx, posy = mousePoint.x(), mousePoint.y() labels = [] for a, vs in sorted(self.reports.items()): for v in vs: if isinstance(v, tuple) and len(v) == 2: if v[0] == "x": labels.append( ("%0." + str(self.important_decimals[0]) + "f") % v[1]) continue labels.append(str(v)) labels = " ".join(labels) self.crosshair_hidden = bool(labels) if self.location and not labels: fs = "%0." + str(self.important_decimals[0]) + "f %0." + str( self.important_decimals[1]) + "f" labels = fs % (posx, posy) self.label.setText(labels, color=(0, 0, 0)) if self.curves and len(self.curves[0][0]): # need non-zero x axis! cache = {} bd = None if self.markclosest and self.plot.vb.action != ZOOMING: xpixel, ypixel = self.plot.vb.viewPixelSize() distances = distancetocurves(self.curves[0], posx, posy, xpixel, ypixel, r=self.MOUSE_RADIUS, cache=cache) try: mindi = np.nanargmin(distances) if distances[mindi] < self.MOUSE_RADIUS: bd = mindi except ValueError: # if all distances are NaN pass if self.highlighted != bd: QToolTip.hideText() if self.highlighted is not None and bd is None: self.highlighted = None self.highlighted_curve.hide() if bd is not None: self.highlighted = bd x = self.curves[0][0] y = self.curves[0][1][self.highlighted] self.highlighted_curve.setData(x=x, y=y) self.highlighted_curve.show() self.vLine.setPos(posx) self.hLine.setPos(posy) self.viewhelpers_show() else: self.viewhelpers_hide() def set_curve_pen(self, idc): idcdata = self.sampled_indices[idc] insubset = self.subset_indices[idcdata] inselected = self.selection_type and idcdata in self.selected_indices have_subset = np.any(self.subset_indices) thispen = self.pen_subset if insubset or not have_subset else self.pen_normal if inselected: thispen = self.pen_selected color_var = self._current_color_var() value = None if color_var is None else str( self.data[idcdata][color_var]) self.curves_cont.objs[idc].setPen(thispen[value]) self.curves_cont.objs[idc].setZValue(int(insubset) + int(inselected)) def set_curve_pens(self, curves=None): if self.viewtype == INDIVIDUAL and self.curves: curves = range(len( self.curves[0][1])) if curves is None else curves for i in curves: self.set_curve_pen(i) self.curves_cont.update() def add_marking(self, item): self.markings.append(item) self.plot.addItem(item, ignoreBounds=True) def remove_marking(self, item): self.plot.removeItem(item) self.markings.remove(item) def clear_markings(self): for m in self.markings: self.plot.removeItem(m) self.markings = [] def add_curves(self, x, ys, addc=True): """ Add multiple curves with the same x domain. """ if len(ys) > MAX_INSTANCES_DRAWN: sample_selection = random.Random(self.sample_seed).sample( range(len(ys)), MAX_INSTANCES_DRAWN) # with random selection also show at most MAX_INSTANCES_DRAW elements from the subset subset = set(np.where(self.subset_indices)[0]) subset_to_show = subset - set(sample_selection) subset_additional = MAX_INSTANCES_DRAWN - (len(subset) - len(subset_to_show)) if len(subset_to_show) > subset_additional: subset_to_show = random.Random(self.sample_seed).sample( subset_to_show, subset_additional) self.sampled_indices = sorted(sample_selection + list(subset_to_show)) self.sampling = True else: self.sampled_indices = list(range(len(ys))) random.Random(self.sample_seed).shuffle( self.sampled_indices) # for sequential classes# self.sampled_indices_inverse = { s: i for i, s in enumerate(self.sampled_indices) } ys = self.data.X[self.sampled_indices][:, self.data_xsind] self.curves.append((x, ys)) for y in ys: c = pg.PlotCurveItem(x=x, y=y, pen=self.pen_normal[None]) self.curves_cont.add_curve(c) self.curves_plotted = self.curves def add_curve(self, x, y, pen=None): c = pg.PlotCurveItem(x=x, y=y, pen=pen if pen else self.pen_normal[None]) self.curves_cont.add_curve(c) # for rescale to work correctly self.curves_plotted.append((x, np.array([y]))) def add_fill_curve(self, x, ylow, yhigh, pen): phigh = pg.PlotCurveItem(x, yhigh, pen=pen) plow = pg.PlotCurveItem(x, ylow, pen=pen) color = pen.color() color.setAlphaF(0.2) cc = pg.mkBrush(color) pfill = pg.FillBetweenItem(plow, phigh, brush=cc) pfill.setZValue(10) self.curves_cont.add_bounds(phigh) self.curves_cont.add_bounds(plow) self.curves_cont.add_curve(pfill, ignore_bounds=True) # for zoom to work correctly self.curves_plotted.append((x, np.array([ylow, yhigh]))) def _current_color_var(self): color_var = None if self.feature_color and self.data: color_var = self.data.domain[self.feature_color] return color_var def set_pen_colors(self): self.pen_normal.clear() self.pen_subset.clear() self.pen_selected.clear() color_var = self._current_color_var() if color_var is not None: colors = color_var.colors discrete_palette = ColorPaletteGenerator( number_of_colors=len(colors), rgb_colors=colors) for v in color_var.values: basecolor = discrete_palette[color_var.to_val(v)] basecolor = QColor(basecolor) basecolor.setAlphaF(0.9) self.pen_subset[v] = pg.mkPen(color=basecolor, width=1) self.pen_selected[v] = pg.mkPen(color=basecolor, width=2, style=Qt.DotLine) notselcolor = basecolor.lighter(150) notselcolor.setAlphaF(0.5) self.pen_normal[v] = pg.mkPen(color=notselcolor, width=1) def show_individual(self): self.view_average_menu.setChecked(False) self.set_pen_colors() self.clear_graph() self.viewtype = INDIVIDUAL if not self.data: return self.add_curves(self.data_x, self.data.X) self.set_curve_pens() self.curves_cont.update() self.plot.vb.set_mode_panning() def resample_curves(self, seed): self.sample_seed = seed self.update_view() def rescale_current_view_y(self): if self.curves_plotted: cache = {} qrect = self.plot.vb.targetRect() bleft = qrect.left() bright = qrect.right() ymax = max( np.max(ys[:, searchsorted_cached(cache, x, bleft): searchsorted_cached(cache, x, bright, side="right")]) for x, ys in self.curves_plotted) ymin = min( np.min(ys[:, searchsorted_cached(cache, x, bleft): searchsorted_cached(cache, x, bright, side="right")]) for x, ys in self.curves_plotted) self.plot.vb.setYRange(ymin, ymax, padding=0.0) self.plot.vb.pad_current_view_y() def _split_by_color_value(self, data): color_var = self._current_color_var() rd = {} if color_var is None: rd[None] = np.full((len(data.X), ), True, dtype=bool) else: cvd = Orange.data.Table(Orange.data.Domain([color_var]), data) for v in range(len(color_var.values)): v1 = np.in1d(cvd.X, v) if np.any(v1): rd[color_var.values[v]] = v1 nanind = np.isnan(cvd.X) if np.any(nanind): rd[None] = nanind return rd def viewtype_changed(self): if self.viewtype == AVERAGE: self.viewtype = INDIVIDUAL else: self.viewtype = AVERAGE self.update_view() def show_average(self): self.view_average_menu.setChecked(True) self.set_pen_colors() self.clear_graph() self.viewtype = AVERAGE if not self.data: return x = self.data_x if self.data: ysall = [] cinfo = [] selected_indices = np.full(self.data_size, False, dtype=bool) selected_indices[list(self.selected_indices)] = True dsplit = self._split_by_color_value(self.data) for colorv, indices in dsplit.items(): for part in [None, "subset", "selection"]: if part is None: part_selection = indices pen = self.pen_normal if np.any( self.subset_indices) else self.pen_subset elif part == "selection" and self.selection_type: part_selection = indices & selected_indices pen = self.pen_selected elif part == "subset": part_selection = indices & self.subset_indices pen = self.pen_subset if np.any(part_selection): ys = self.data.X[part_selection] std = np.nanstd(ys, axis=0) mean = np.nanmean(ys, axis=0) std = std[self.data_xsind] mean = mean[self.data_xsind] ysall.append(mean) penc = QPen(pen[colorv]) penc.setWidth(3) self.add_curve(x, mean, pen=penc) self.add_fill_curve(x, mean + std, mean - std, pen=penc) cinfo.append((colorv, part, part_selection)) self.curves.append((x, np.array(ysall))) self.multiple_curves_info = cinfo self.curves_cont.update() self.plot.vb.set_mode_panning() def update_view(self): if self.viewtype == INDIVIDUAL: self.show_individual() elif self.viewtype == AVERAGE: self.show_average() if self.rescale_next: self.plot.vb.autoRange() def set_data(self, data): old_domain = self.data.domain if self.data else None self.clear_data() domain = data.domain if data is not None else None self.feature_color_model.set_domain(domain) if old_domain and domain != old_domain: # do not reset feature_color self.feature_color = self.feature_color_model[ 0] if self.feature_color_model else None if data is not None: if self.data: self.rescale_next = not data.domain == self.data.domain else: self.rescale_next = True self.data = data # reset selection if dataset sizes do not match if self.selected_indices and \ (max(self.selected_indices) >= len(self.data) or self.data_size != len(self.data)): self.selected_indices.clear() self.data_size = len(self.data) # get and sort input data x = getx(self.data) xsind = np.argsort(x) self.data_x = x[xsind] self.data_xsind = xsind self._set_subset_indices( ) # refresh subset indices according to the current subset def _set_subset_indices(self): ids = self.subset if ids is None: ids = [] if self.data: self.subset_indices = np.in1d(self.data.ids, ids) def set_data_subset(self, ids): self.subset = ids self._set_subset_indices() self.update_view() def select_by_click(self, pos, add): clicked_curve = self.highlighted if clicked_curve is not None: if self.viewtype == INDIVIDUAL: self.make_selection([self.sampled_indices[clicked_curve]], add) elif self.viewtype == AVERAGE: sel = np.where(self.multiple_curves_info[clicked_curve][2])[0] self.make_selection(sel, add) else: self.make_selection(None, add) if self.viewtype == AVERAGE: # reset average view self.show_average() def select_line(self, startp, endp, add): intersected = self.intersect_curves((startp.x(), startp.y()), (endp.x(), endp.y())) self.make_selection(intersected if len(intersected) else None, add) def intersect_curves(self, q1, q2): x, ys = self.data_x, self.data.X if len(x) < 2: return [] x1, x2 = min(q1[0], q2[0]), max(q1[0], q2[0]) xmin = closestindex(x, x1) xmax = closestindex(x, x2, side="right") xmin = max(0, xmin - 1) xmax = xmax + 2 sel = np.flatnonzero( intersect_curves_chunked(x, ys, self.data_xsind, q1, q2, xmin, xmax)) return sel