def _create_legend(self, anchor): legend = LegendItem() legend.setParentItem(self.view_box) legend.restoreAnchor(anchor) legend.hide() return legend
class OWLinePlot(OWWidget): name = "Line Plot" description = "Visualization of data profiles (e.g., time series)." icon = "icons/LinePlot.svg" priority = 1030 class Inputs: data = Input("Data", Table, default=True) data_subset = Input("Data Subset", Table) class Outputs: selected_data = Output("Selected Data", Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) settingsHandler = settings.PerfectDomainContextHandler() group_var = settings.ContextSetting(None) display_index = settings.Setting(LinePlotDisplay.RANGE_WITH_MEAN) display_quartiles = settings.Setting(False) auto_commit = settings.Setting(True) selection = settings.ContextSetting([]) class Information(OWWidget.Information): not_enough_attrs = Msg("Need at least one continuous feature.") def __init__(self, parent=None): super().__init__(parent) self.__groups = None self.__profiles = None self.data = None self.data_subset = None self.subset_selection = [] self.graph_variables = [] # Setup GUI infobox = gui.widgetBox(self.controlArea, "Info") self.infoLabel = gui.widgetLabel(infobox, "No data on input.") displaybox = gui.widgetBox(self.controlArea, "Display") radiobox = gui.radioButtons(displaybox, self, "display_index", callback=self.__update_visibility) gui.appendRadioButton(radiobox, "Range (min-max) with mean") gui.appendRadioButton(radiobox, "Line plot") gui.appendRadioButton(radiobox, "Mean") gui.appendRadioButton(radiobox, "Line plot with mean") showbox = gui.widgetBox(self.controlArea, "Show") gui.checkBox(showbox, self, "display_quartiles", "Error bars", callback=self.__update_visibility) self.group_vars = DomainModel( placeholder="None", separators=False, valid_types=DiscreteVariable) self.group_view = gui.listView( self.controlArea, self, "group_var", box="Group by", model=self.group_vars, callback=self.__group_var_changed) self.group_view.setEnabled(False) self.group_view.setMinimumSize(QSize(30, 100)) self.group_view.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Ignored) self.gui = OWPlotGUI(self) self.box_zoom_select(self.controlArea) gui.rubber(self.controlArea) gui.auto_commit(self.controlArea, self, "auto_commit", "Send Selection", "Send Automatically") self.graph = LinePlotGraph(self) self.graph.getPlotItem().buttonsHidden = True self.graph.setRenderHint(QPainter.Antialiasing, True) self.mainArea.layout().addWidget(self.graph) self._legend = LegendItem() self._legend.setParentItem(self.graph.getPlotItem().vb) self._legend.hide() self._legend.anchor((1, 0), (1, 0)) @property def has_selection(self): return bool(self.selection) @property def has_subset(self): return bool(self.data_subset) 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.SimpleSelect].clicked.connect(self.select_button_clicked) buttons[g.Pan].clicked.connect(self.pan_button_clicked) buttons[g.Zoom].clicked.connect(self.zoom_button_clicked) buttons[g.ZoomReset].clicked.connect(self.reset_button_clicked) return box_zoom_select def select_button_clicked(self): self.graph.state = SELECT self.graph.getViewBox().setMouseMode(self.graph.getViewBox().RectMode) def pan_button_clicked(self): self.graph.state = PANNING self.graph.getViewBox().setMouseMode(self.graph.getViewBox().PanMode) def zoom_button_clicked(self): self.graph.state = ZOOMING self.graph.getViewBox().setMouseMode(self.graph.getViewBox().RectMode) def reset_button_clicked(self): self.graph.getViewBox().autoRange() def selection_changed(self): self.selection = list(self.graph.selection) self.__update_visibility() self.commit() def sizeHint(self): return QSize(1000, 562) def clear(self): """ Clear/reset the widget state. """ self.__groups = None self.__profiles = None self.graph_variables = [] self.graph.reset() self.infoLabel.setText("No data on input.") self.group_vars.set_domain(None) self.group_view.setEnabled(False) self._legend.hide() @Inputs.data def set_data(self, data): """ Set the input profile dataset. """ self.closeContext() self.clear() self.clear_messages() self.data = data self.selection = [] if data is not None: self.group_vars.set_domain(data.domain) self.group_view.setEnabled(len(self.group_vars) > 1) self.group_var = data.domain.class_var if \ data.domain.class_var and data.domain.class_var.is_discrete \ else None self.infoLabel.setText("%i instances on input\n%i attributes" % ( len(data), len(data.domain.attributes))) self.graph_variables = [var for var in data.domain.attributes if var.is_continuous] if len(self.graph_variables) < 1: self.data = None self.Information.not_enough_attrs() self.commit() return self.openContext(data) self._setup_plot() self.commit() @Inputs.data_subset def set_data_subset(self, subset): """ Set the supplementary input subset dataset. """ self.data_subset = subset if len(self.subset_selection): self.graph.deselect_subset() def handleNewSignals(self): self.subset_selection = [] if self.data is not None and self.has_subset and \ len(self.graph_variables): intersection = set(self.data.ids).intersection( set(self.data_subset.ids)) self.subset_selection = intersection if self.__profiles is not None: self.graph.select_subset(self.subset_selection) if self.data is not None: self.__update_visibility() def _setup_plot(self): """Setup the plot with new curve data.""" if self.data is None: return ticks = [[(i + 1, str(a)) for i, a in enumerate(self.graph_variables)]] self.graph.getAxis('bottom').setTicks(ticks) self.graph.getViewBox().enableAutoRange() if self.display_index in (LinePlotDisplay.INSTANCES, LinePlotDisplay.INSTANCES_WITH_MEAN): self._plot_profiles() self._plot_groups() self.__update_visibility() self._setup_legend() def _setup_legend(self): self._legend.clear() self._legend.hide() if self.group_var: for index, name in enumerate(self.group_var.values): color = self.__get_line_color(None, index) self._legend.addItem( ScatterPlotItem(pen=color, brush=color, size=10, shape="s"), escape(name) ) self._legend.show() def _plot_profiles(self): X = np.arange(1, len(self.graph_variables) + 1) data = self.data[:, self.graph_variables] self.__profiles = [] for index, inst in zip(range(len(self.data)), data): color = self.__get_line_color(index) profile = LinePlotItem(self, index, inst.id, X, inst.x, color) profile.sigClicked.connect(self.graph.select_by_click) self.graph.add_line_plot_item(profile) self.__profiles.append(profile) self.graph.finished_adding() self.__select_data_instances() def _plot_groups(self): if self.__groups is not None: for group in self.__groups: if group is not None: self.graph.getViewBox().removeItem(group.mean) self.graph.getViewBox().removeItem(group.range) self.graph.getViewBox().removeItem(group.error_bar) self.__groups = [] X = np.arange(1, len(self.graph_variables) + 1) if self.group_var is None: self.__plot_group(X, self.data[:, self.graph_variables]) else: class_col_data, _ = self.data.get_column_view(self.group_var) group_indices = [np.flatnonzero(class_col_data == i) for i in range(len(self.group_var.values))] for index, indices in enumerate(group_indices): if len(indices) == 0: self.__groups.append(None) else: group_data = self.data[indices, self.graph_variables] self.__plot_group(X, group_data, index) def __plot_group(self, X, data, index=None): p = QPen(self.__get_line_color(None, index), LinePlotStyle.MEAN_WIDTH) p.setCosmetic(True) mean = np.nanmean(data.X, axis=0) mean_curve = self.get_mean_curve(X, mean, p) range_curve = self.get_range_curve(X, data, p) error_bar = self.get_error_bar(X, data, mean) self.graph.addItem(mean_curve) self.graph.addItem(range_curve) self.graph.addItem(error_bar) self.__groups.append(namespace(mean=mean_curve, range=range_curve, error_bar=error_bar)) @staticmethod def get_mean_curve(X, mean, pen): return pg.PlotDataItem(x=X, y=mean, pen=pen, antialias=True) @staticmethod def get_range_curve(X, data, pen): color = QColor(pen.color()) color.setAlpha(LinePlotStyle.RANGE_ALPHA) max_curve = pg.PlotDataItem(x=X, y=np.nanmax(data.X, axis=0)) min_curve = pg.PlotDataItem(x=X, y=np.nanmin(data.X, axis=0)) return pg.FillBetweenItem(max_curve, min_curve, brush=color) @staticmethod def get_error_bar(X, data, mean): q1, q2, q3 = np.nanpercentile(data.X, [25, 50, 75], axis=0) bottom = np.clip(mean - q1, 0, mean - q1) top = np.clip(q3 - mean, 0, q3 - mean) return pg.ErrorBarItem(x=X, y=mean, bottom=bottom, top=top, beam=0.01) def __update_visibility(self): self.__update_visibility_profiles() self.__update_visibility_groups() def __update_visibility_groups(self): show_mean = self.display_index in (LinePlotDisplay.MEAN, LinePlotDisplay.INSTANCES_WITH_MEAN, LinePlotDisplay.RANGE_WITH_MEAN) show_range = self.display_index == LinePlotDisplay.RANGE_WITH_MEAN if self.__groups is not None: for group in self.__groups: if group is not None: group.mean.setVisible(show_mean) group.range.setVisible(show_range) group.error_bar.setVisible(self.display_quartiles) def __update_visibility_profiles(self): show_selection = self.display_index == LinePlotDisplay.RANGE_WITH_MEAN show_inst = self.display_index in (LinePlotDisplay.INSTANCES, LinePlotDisplay.INSTANCES_WITH_MEAN) if self.__profiles is None and (show_inst or show_selection) \ and self.data is not None: self._plot_profiles() self.graph.select_subset(self.subset_selection) if self.__profiles is not None: selection = set(chain( self.selection, self.data_subset.ids if self.has_subset else [] )) for profile in self.__profiles: selected = profile.index in selection profile.setVisible(show_inst or selected and show_selection) profile.reset_pen() def __group_var_changed(self): if self.data is None or not len(self.graph_variables): return self.__color_profiles() self._plot_groups() self.__update_visibility() self._setup_legend() def __color_profiles(self): if self.__profiles is not None: for profile in self.__profiles: profile.setColor(self.__get_line_color(profile.index)) def __select_data_instances(self): if self.data is None or not len(self.data) or not self.has_selection: return if max(self.selection) >= len(self.data): self.selection = [] self.graph.select(self.selection) def __get_line_color(self, data_index=None, mean_index=None): color = QColor(LinePlotStyle.DEFAULT_COLOR) if self.group_var is not None: if data_index is not None: value = self.data[data_index][self.group_var] if np.isnan(value): return color index = int(value) if data_index is not None else mean_index color = LinePlotStyle()(len(self.group_var.values))[index] return color.darker(110) if data_index is None else color def commit(self): selected = self.data[self.selection] \ if self.data is not None and self.has_selection else None annotated = create_annotated_table(self.data, self.selection) self.Outputs.selected_data.send(selected) self.Outputs.annotated_data.send(annotated)