def update_data(self, x_attr, y_attr, i, j): """ Update graph :param x_attr: x attributes :param y_attr: y attributes :return: None """ OWScatterPlotGraph.update_data(self, x_attr, y_attr) x_index, y_index = i, j X = self.original_data[x_index] Y = self.original_data[y_index] valid = self.get_valid_list([x_index, y_index]) X = X[valid] Y = Y[valid] x_min, x_max = self.attr_values[x_attr] X = numpy.array([numpy.ones_like(X), X]).T try: beta, _, _, _ = numpy.linalg.lstsq(X, Y) except numpy.linalg.LinAlgError: beta = [0, 0] angle = atan2(beta[1], 1) * 180 / pi ti = InfiniteLine((-999, beta[0] + -999 * beta[1]), angle=angle, pen='r') # TODO FIX self.plot_widget.addItem(ti) if self.last_line is not None: self.plot_widget.removeItem(self.last_line) self.last_line = ti self.plot_widget.replot()
class OWScatterPlot(OWWidget): """Scatterplot visualization with explorative analysis and intelligent data visualization enhancements.""" name = 'Scatter Plot' description = "Interactive scatter plot visualization with " \ "intelligent data visualization enhancements." icon = "icons/ScatterPlot.svg" priority = 140 class Inputs: data = Input("Data", Table, default=True) data_subset = Input("Data Subset", Table) features = Input("Features", AttributeList) class Outputs: selected_data = Output("Selected Data", Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) features = Output("Features", AttributeList, dynamic=False) settings_version = 2 settingsHandler = DomainContextHandler() auto_send_selection = Setting(True) auto_sample = Setting(True) toolbar_selection = Setting(0) attr_x = ContextSetting(None) attr_y = ContextSetting(None) #: Serialized selection state to be restored selection_group = Setting(None, schema_only=True) graph = SettingProvider(OWScatterPlotGraph) jitter_sizes = [0, 0.1, 0.5, 1, 2, 3, 4, 5, 7, 10] graph_name = "graph.plot_widget.plotItem" class Information(OWWidget.Information): sampled_sql = Msg("Large SQL table; showing a sample.") def __init__(self): super().__init__() box = gui.vBox(self.mainArea, True, margin=0) self.graph = OWScatterPlotGraph(self, box, "ScatterPlot") box.layout().addWidget(self.graph.plot_widget) plot = self.graph.plot_widget axispen = QPen(self.palette().color(QPalette.Text)) axis = plot.getAxis("bottom") axis.setPen(axispen) axis = plot.getAxis("left") axis.setPen(axispen) self.data = None # Orange.data.Table self.subset_data = None # Orange.data.Table self.sql_data = None # Orange.data.sql.table.SqlTable self.attribute_selection_list = None # list of Orange.data.Variable self.__timer = QTimer(self, interval=1200) self.__timer.timeout.connect(self.add_data) #: Remember the saved state to restore self.__pending_selection_restore = self.selection_group self.selection_group = None common_options = dict( labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str) box = gui.vBox(self.controlArea, "Axis Data") dmod = DomainModel self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE) self.cb_attr_x = gui.comboBox( box, self, "attr_x", label="Axis x:", callback=self.update_attr, model=self.xy_model, **common_options) self.cb_attr_y = gui.comboBox( box, self, "attr_y", label="Axis y:", callback=self.update_attr, model=self.xy_model, **common_options) vizrank_box = gui.hBox(box) gui.separator(vizrank_box, width=common_options["labelWidth"]) self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank( vizrank_box, self, "Find Informative Projections", self.set_attr) gui.separator(box) g = self.graph.gui g.add_widgets([g.JitterSizeSlider, g.JitterNumericValues], box) self.sampling = gui.auto_commit( self.controlArea, self, "auto_sample", "Sample", box="Sampling", callback=self.switch_sampling, commit=lambda: self.add_data(1)) self.sampling.setVisible(False) g.point_properties_box(self.controlArea) self.models = [self.xy_model] + g.points_models box_plot_prop = gui.vBox(self.controlArea, "Plot Properties") g.add_widgets([g.ShowLegend, g.ShowGridLines, g.ToolTipShowsAll, g.ClassDensity, g.RegressionLine, g.LabelOnlySelected], box_plot_prop) self.graph.box_zoom_select(self.controlArea) self.controlArea.layout().addStretch(100) self.icons = gui.attributeIconDict p = self.graph.plot_widget.palette() self.graph.set_palette(p) gui.auto_commit(self.controlArea, self, "auto_send_selection", "Send Selection", "Send Automatically") self.graph.zoom_actions(self) def keyPressEvent(self, event): super().keyPressEvent(event) self.graph.update_tooltip(event.modifiers()) def keyReleaseEvent(self, event): super().keyReleaseEvent(event) self.graph.update_tooltip(event.modifiers()) def reset_graph_data(self, *_): if self.data is not None: self.graph.rescale_data() self.update_graph() def _vizrank_color_change(self): self.vizrank.initialize() is_enabled = self.data is not None and not self.data.is_sparse() and \ len([v for v in chain(self.data.domain.variables, self.data.domain.metas) if v.is_primitive]) > 2\ and len(self.data) > 1 self.vizrank_button.setEnabled( is_enabled and self.graph.attr_color is not None and not np.isnan(self.data.get_column_view(self.graph.attr_color)[0].astype(float)).all()) if is_enabled and self.graph.attr_color is None: self.vizrank_button.setToolTip("Color variable has to be selected.") else: self.vizrank_button.setToolTip("") @Inputs.data def set_data(self, data): self.clear_messages() self.Information.sampled_sql.clear() self.__timer.stop() self.sampling.setVisible(False) self.sql_data = None if isinstance(data, SqlTable): if data.approx_len() < 4000: data = Table(data) else: self.Information.sampled_sql() self.sql_data = data data_sample = data.sample_time(0.8, no_cache=True) data_sample.download_data(2000, partial=True) data = Table(data_sample) self.sampling.setVisible(True) if self.auto_sample: self.__timer.start() if data is not None and (len(data) == 0 or len(data.domain) == 0): data = None if self.data and data and self.data.checksum() == data.checksum(): return self.closeContext() same_domain = (self.data and data and data.domain.checksum() == self.data.domain.checksum()) self.data = data if not same_domain: self.init_attr_values() self.openContext(self.data) self._vizrank_color_change() def findvar(name, iterable): """Find a Orange.data.Variable in `iterable` by name""" for el in iterable: if isinstance(el, Orange.data.Variable) and el.name == name: return el return None # handle restored settings from < 3.3.9 when attr_* were stored # by name if isinstance(self.attr_x, str): self.attr_x = findvar(self.attr_x, self.xy_model) if isinstance(self.attr_y, str): self.attr_y = findvar(self.attr_y, self.xy_model) if isinstance(self.graph.attr_label, str): self.graph.attr_label = findvar( self.graph.attr_label, self.graph.gui.label_model) if isinstance(self.graph.attr_color, str): self.graph.attr_color = findvar( self.graph.attr_color, self.graph.gui.color_model) if isinstance(self.graph.attr_shape, str): self.graph.attr_shape = findvar( self.graph.attr_shape, self.graph.gui.shape_model) if isinstance(self.graph.attr_size, str): self.graph.attr_size = findvar( self.graph.attr_size, self.graph.gui.size_model) def add_data(self, time=0.4): if self.data and len(self.data) > 2000: return self.__timer.stop() data_sample = self.sql_data.sample_time(time, no_cache=True) if data_sample: data_sample.download_data(2000, partial=True) data = Table(data_sample) self.data = Table.concatenate((self.data, data), axis=0) self.handleNewSignals() def switch_sampling(self): self.__timer.stop() if self.auto_sample and self.sql_data: self.add_data() self.__timer.start() @Inputs.data_subset def set_subset_data(self, subset_data): self.warning() if isinstance(subset_data, SqlTable): if subset_data.approx_len() < AUTO_DL_LIMIT: subset_data = Table(subset_data) else: self.warning("Data subset does not support large Sql tables") subset_data = None self.subset_data = subset_data self.controls.graph.alpha_value.setEnabled(subset_data is None) # called when all signals are received, so the graph is updated only once def handleNewSignals(self): self.graph.new_data(self.data, self.subset_data) if self.attribute_selection_list and self.graph.domain and \ all(attr in self.graph.domain for attr in self.attribute_selection_list): self.attr_x = self.attribute_selection_list[0] self.attr_y = self.attribute_selection_list[1] self.attribute_selection_list = None self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.cb_reg_line.setEnabled(self.graph.can_draw_regresssion_line()) if self.data is not None and self.__pending_selection_restore is not None: self.apply_selection(self.__pending_selection_restore) self.__pending_selection_restore = None self.unconditional_commit() def apply_selection(self, selection): """Apply `selection` to the current plot.""" if self.data is not None: self.graph.selection = np.zeros(len(self.data), dtype=np.uint8) self.selection_group = [x for x in selection if x[0] < len(self.data)] selection_array = np.array(self.selection_group).T self.graph.selection[selection_array[0]] = selection_array[1] self.graph.update_colors(keep_colors=True) @Inputs.features def set_shown_attributes(self, attributes): if attributes and len(attributes) >= 2: self.attribute_selection_list = attributes[:2] else: self.attribute_selection_list = None def init_attr_values(self): domain = self.data and self.data.domain for model in self.models: model.set_domain(domain) self.attr_x = self.xy_model[0] if self.xy_model else None self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \ else self.attr_x self.graph.attr_color = self.data.domain.class_var if domain else None self.graph.attr_shape = None self.graph.attr_size = None self.graph.attr_label = None def set_attr(self, attr_x, attr_y): self.attr_x, self.attr_y = attr_x, attr_y self.update_attr() def update_attr(self): self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.cb_reg_line.setEnabled(self.graph.can_draw_regresssion_line()) self.send_features() def update_colors(self): self._vizrank_color_change() self.cb_class_density.setEnabled(self.graph.can_draw_density()) def update_density(self): self.update_graph(reset_view=False) def update_regression_line(self): self.update_graph(reset_view=False) def update_graph(self, reset_view=True, **_): self.graph.zoomStack = [] if self.graph.data is None: return self.graph.update_data(self.attr_x, self.attr_y, reset_view) def selection_changed(self): # Store current selection in a setting that is stored in workflow if isinstance(self.data, SqlTable): selection = None elif self.data is not None: selection = self.graph.get_selection() else: selection = None if selection is not None and len(selection): self.selection_group = list(zip(selection, self.graph.selection[selection])) else: self.selection_group = None self.commit() def send_data(self): # TODO: Implement selection for sql data def _get_selected(): if not len(selection): return None return create_groups_table(data, graph.selection, False, "Group") def _get_annotated(): if graph.selection is not None and np.max(graph.selection) > 1: return create_groups_table(data, graph.selection) else: return create_annotated_table(data, selection) graph = self.graph data = self.data selection = graph.get_selection() self.Outputs.annotated_data.send(_get_annotated()) self.Outputs.selected_data.send(_get_selected()) def send_features(self): features = [attr for attr in [self.attr_x, self.attr_y] if attr] self.Outputs.features.send(features or None) def commit(self): self.send_data() self.send_features() def get_widget_name_extension(self): if self.data is not None: return "{} vs {}".format(self.attr_x.name, self.attr_y.name) def send_report(self): if self.data is None: return def name(var): return var and var.name caption = report.render_items_vert(( ("Color", name(self.graph.attr_color)), ("Label", name(self.graph.attr_label)), ("Shape", name(self.graph.attr_shape)), ("Size", name(self.graph.attr_size)), ("Jittering", (self.attr_x.is_discrete or self.attr_y.is_discrete or self.graph.jitter_continuous) and self.graph.jitter_size))) self.report_plot() if caption: self.report_caption(caption) def onDeleteWidget(self): super().onDeleteWidget() self.graph.plot_widget.getViewBox().deleteLater() self.graph.plot_widget.clear() @classmethod def migrate_settings(cls, settings, version): if version < 2 and "selection" in settings and settings["selection"]: settings["selection_group"] = [(a, 1) for a in settings["selection"]]
class OWScatterPlot(OWWidget): """Scatterplot visualization with explorative analysis and intelligent data visualization enhancements.""" name = 'Scatter Plot' description = "Interactive scatter plot visualization with " \ "intelligent data visualization enhancements." icon = "icons/ScatterPlot.svg" priority = 140 class Inputs: data = Input("Data", Table, default=True) data_subset = Input("Data Subset", Table) features = Input("Features", AttributeList) class Outputs: selected_data = Output("Selected Data", Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) features = Output("Features", Table, dynamic=False) settingsHandler = DomainContextHandler() auto_send_selection = Setting(True) auto_sample = Setting(True) toolbar_selection = Setting(0) attr_x = ContextSetting(None) attr_y = ContextSetting(None) selection = Setting(None, schema_only=True) graph = SettingProvider(OWScatterPlotGraph) jitter_sizes = [0, 0.1, 0.5, 1, 2, 3, 4, 5, 7, 10] graph_name = "graph.plot_widget.plotItem" class Information(OWWidget.Information): sampled_sql = Msg("Large SQL table; showing a sample.") def __init__(self): super().__init__() box = gui.vBox(self.mainArea, True, margin=0) self.graph = OWScatterPlotGraph(self, box, "ScatterPlot") box.layout().addWidget(self.graph.plot_widget) plot = self.graph.plot_widget axispen = QPen(self.palette().color(QPalette.Text)) axis = plot.getAxis("bottom") axis.setPen(axispen) axis = plot.getAxis("left") axis.setPen(axispen) self.data = None # Orange.data.Table self.subset_data = None # Orange.data.Table self.data_metas_X = None # self.data, where primitive metas are moved to X self.sql_data = None # Orange.data.sql.table.SqlTable self.attribute_selection_list = None # list of Orange.data.Variable self.__timer = QTimer(self, interval=1200) self.__timer.timeout.connect(self.add_data) common_options = dict( labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str) box = gui.vBox(self.controlArea, "Axis Data") dmod = DomainModel self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE) self.cb_attr_x = gui.comboBox( box, self, "attr_x", label="Axis x:", callback=self.update_attr, model=self.xy_model, **common_options) self.cb_attr_y = gui.comboBox( box, self, "attr_y", label="Axis y:", callback=self.update_attr, model=self.xy_model, **common_options) vizrank_box = gui.hBox(box) gui.separator(vizrank_box, width=common_options["labelWidth"]) self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank( vizrank_box, self, "Find Informative Projections", self.set_attr) gui.separator(box) gui.valueSlider( box, self, value='graph.jitter_size', label='Jittering: ', values=self.jitter_sizes, callback=self.reset_graph_data, labelFormat=lambda x: "None" if x == 0 else ("%.1f %%" if x < 1 else "%d %%") % x) gui.checkBox( gui.indentedBox(box), self, 'graph.jitter_continuous', 'Jitter numeric values', callback=self.reset_graph_data) self.sampling = gui.auto_commit( self.controlArea, self, "auto_sample", "Sample", box="Sampling", callback=self.switch_sampling, commit=lambda: self.add_data(1)) self.sampling.setVisible(False) g = self.graph.gui g.point_properties_box(self.controlArea) self.models = [self.xy_model] + g.points_models box = gui.vBox(self.controlArea, "Plot Properties") g.add_widgets([g.ShowLegend, g.ShowGridLines], box) gui.checkBox( box, self, value='graph.tooltip_shows_all', label='Show all data on mouse hover') self.cb_class_density = gui.checkBox( box, self, value='graph.class_density', label='Show class density', callback=self.update_density) self.cb_reg_line = gui.checkBox( box, self, value='graph.show_reg_line', label='Show regression line', callback=self.update_regression_line) gui.checkBox( box, self, 'graph.label_only_selected', 'Label only selected points', callback=self.graph.update_labels) self.zoom_select_toolbar = g.zoom_select_toolbar( gui.vBox(self.controlArea, "Zoom/Select"), nomargin=True, buttons=[g.StateButtonsBegin, g.SimpleSelect, g.Pan, g.Zoom, g.StateButtonsEnd, g.ZoomReset] ) buttons = self.zoom_select_toolbar.buttons buttons[g.Zoom].clicked.connect(self.graph.zoom_button_clicked) buttons[g.Pan].clicked.connect(self.graph.pan_button_clicked) buttons[g.SimpleSelect].clicked.connect(self.graph.select_button_clicked) buttons[g.ZoomReset].clicked.connect(self.graph.reset_button_clicked) self.controlArea.layout().addStretch(100) self.icons = gui.attributeIconDict p = self.graph.plot_widget.palette() self.graph.set_palette(p) gui.auto_commit(self.controlArea, self, "auto_send_selection", "Send Selection", "Send Automatically") def zoom(s): """Zoom in/out by factor `s`.""" viewbox = plot.getViewBox() # scaleBy scales the view's bounds (the axis range) viewbox.scaleBy((1 / s, 1 / s)) def fit_to_view(): viewbox = plot.getViewBox() viewbox.autoRange() zoom_in = QAction( "Zoom in", self, triggered=lambda: zoom(1.25) ) zoom_in.setShortcuts([QKeySequence(QKeySequence.ZoomIn), QKeySequence(self.tr("Ctrl+="))]) zoom_out = QAction( "Zoom out", self, shortcut=QKeySequence.ZoomOut, triggered=lambda: zoom(1 / 1.25) ) zoom_fit = QAction( "Fit in view", self, shortcut=QKeySequence(Qt.ControlModifier | Qt.Key_0), triggered=fit_to_view ) self.addActions([zoom_in, zoom_out, zoom_fit]) def keyPressEvent(self, event): super().keyPressEvent(event) self.graph.update_tooltip(event.modifiers()) def keyReleaseEvent(self, event): super().keyReleaseEvent(event) self.graph.update_tooltip(event.modifiers()) # def settingsFromWidgetCallback(self, handler, context): # context.selectionPolygons = [] # for curve in self.graph.selectionCurveList: # xs = [curve.x(i) for i in range(curve.dataSize())] # ys = [curve.y(i) for i in range(curve.dataSize())] # context.selectionPolygons.append((xs, ys)) # def settingsToWidgetCallback(self, handler, context): # selections = getattr(context, "selectionPolygons", []) # for (xs, ys) in selections: # c = SelectionCurve("") # c.setData(xs,ys) # c.attach(self.graph) # self.graph.selectionCurveList.append(c) def reset_graph_data(self, *_): if self.data is not None: self.graph.rescale_data() self.update_graph() @Inputs.data def set_data(self, data): self.clear_messages() self.Information.sampled_sql.clear() self.__timer.stop() self.sampling.setVisible(False) self.sql_data = None if isinstance(data, SqlTable): if data.approx_len() < 4000: data = Table(data) else: self.Information.sampled_sql() self.sql_data = data data_sample = data.sample_time(0.8, no_cache=True) data_sample.download_data(2000, partial=True) data = Table(data_sample) self.sampling.setVisible(True) if self.auto_sample: self.__timer.start() if data is not None and (len(data) == 0 or len(data.domain) == 0): data = None if self.data and data and self.data.checksum() == data.checksum(): return self.closeContext() same_domain = (self.data and data and data.domain.checksum() == self.data.domain.checksum()) self.data = data self.data_metas_X = self.move_primitive_metas_to_X(data) if not same_domain: self.init_attr_values() self.vizrank.initialize() self.vizrank.attrs = self.data.domain.attributes if self.data is not None else [] self.vizrank_button.setEnabled( self.data is not None and not self.data.is_sparse() and self.data.domain.class_var is not None and len(self.data.domain.attributes) > 1 and len(self.data) > 1) if self.data is not None and self.data.domain.class_var is None \ and len(self.data.domain.attributes) > 1 and len(self.data) > 1: self.vizrank_button.setToolTip( "Data with a class variable is required.") else: self.vizrank_button.setToolTip("") self.openContext(self.data) def findvar(name, iterable): """Find a Orange.data.Variable in `iterable` by name""" for el in iterable: if isinstance(el, Orange.data.Variable) and el.name == name: return el return None # handle restored settings from < 3.3.9 when attr_* were stored # by name if isinstance(self.attr_x, str): self.attr_x = findvar(self.attr_x, self.xy_model) if isinstance(self.attr_y, str): self.attr_y = findvar(self.attr_y, self.xy_model) if isinstance(self.graph.attr_label, str): self.graph.attr_label = findvar( self.graph.attr_label, self.graph.gui.label_model) if isinstance(self.graph.attr_color, str): self.graph.attr_color = findvar( self.graph.attr_color, self.graph.gui.color_model) if isinstance(self.graph.attr_shape, str): self.graph.attr_shape = findvar( self.graph.attr_shape, self.graph.gui.shape_model) if isinstance(self.graph.attr_size, str): self.graph.attr_size = findvar( self.graph.attr_size, self.graph.gui.size_model) def add_data(self, time=0.4): if self.data and len(self.data) > 2000: return self.__timer.stop() data_sample = self.sql_data.sample_time(time, no_cache=True) if data_sample: data_sample.download_data(2000, partial=True) data = Table(data_sample) self.data = Table.concatenate((self.data, data), axis=0) self.data_metas_X = self.move_primitive_metas_to_X(self.data) self.handleNewSignals() def switch_sampling(self): self.__timer.stop() if self.auto_sample and self.sql_data: self.add_data() self.__timer.start() def move_primitive_metas_to_X(self, data): if data is not None: new_attrs = [a for a in data.domain.attributes + data.domain.metas if a.is_primitive()] new_metas = [m for m in data.domain.metas if not m.is_primitive()] new_domain = Domain(new_attrs, data.domain.class_vars, new_metas) data = data.transform(new_domain) return data @Inputs.data_subset def set_subset_data(self, subset_data): self.warning() if isinstance(subset_data, SqlTable): if subset_data.approx_len() < AUTO_DL_LIMIT: subset_data = Table(subset_data) else: self.warning("Data subset does not support large Sql tables") subset_data = None self.subset_data = self.move_primitive_metas_to_X(subset_data) self.controls.graph.alpha_value.setEnabled(subset_data is None) # called when all signals are received, so the graph is updated only once def handleNewSignals(self): self.graph.new_data(self.sparse_to_dense(self.data_metas_X), self.sparse_to_dense(self.subset_data)) if self.attribute_selection_list and self.graph.domain and \ all(attr in self.graph.domain for attr in self.attribute_selection_list): self.attr_x = self.attribute_selection_list[0] self.attr_y = self.attribute_selection_list[1] self.attribute_selection_list = None self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.cb_reg_line.setEnabled(self.graph.can_draw_regresssion_line()) self.apply_selection() self.unconditional_commit() def prepare_data(self): """ Only when dealing with sparse matrices. GH-2152 """ self.graph.new_data(self.sparse_to_dense(self.data_metas_X), self.sparse_to_dense(self.subset_data), new=False) def sparse_to_dense(self, input_data=None): if input_data is None or not input_data.is_sparse(): return input_data keys = [] attrs = {self.attr_x, self.attr_y, self.graph.attr_color, self.graph.attr_shape, self.graph.attr_size, self.graph.attr_label} for i, attr in enumerate(input_data.domain): if attr in attrs: keys.append(i) new_domain = input_data.domain.select_columns(keys) dmx = input_data.transform(new_domain) dmx.X = dmx.X.toarray() # TODO: remove once we make sure Y is always dense. if sp.issparse(dmx.Y): dmx.Y = dmx.Y.toarray() return dmx def apply_selection(self): """Apply selection saved in workflow.""" if self.data is not None and self.selection is not None: self.graph.selection = np.zeros(len(self.data), dtype=np.uint8) self.selection = [x for x in self.selection if x < len(self.data)] self.graph.selection[self.selection] = 1 self.graph.update_colors(keep_colors=True) @Inputs.features def set_shown_attributes(self, attributes): if attributes and len(attributes) >= 2: self.attribute_selection_list = attributes[:2] else: self.attribute_selection_list = None def get_shown_attributes(self): return self.attr_x, self.attr_y def init_attr_values(self): domain = self.data and self.data.domain for model in self.models: model.set_domain(domain) self.attr_x = self.xy_model[0] if self.xy_model else None self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \ else self.attr_x self.graph.attr_color = domain and self.data.domain.class_var or None self.graph.attr_shape = None self.graph.attr_size = None self.graph.attr_label = None def set_attr(self, attr_x, attr_y): self.attr_x, self.attr_y = attr_x, attr_y self.update_attr() def update_attr(self): self.prepare_data() self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.cb_reg_line.setEnabled(self.graph.can_draw_regresssion_line()) self.send_features() def update_colors(self): self.prepare_data() self.cb_class_density.setEnabled(self.graph.can_draw_density()) def update_density(self): self.update_graph(reset_view=False) def update_regression_line(self): self.update_graph(reset_view=False) def update_graph(self, reset_view=True, **_): self.graph.zoomStack = [] if self.graph.data is None: return self.graph.update_data(self.attr_x, self.attr_y, reset_view) def selection_changed(self): self.send_data() @staticmethod def create_groups_table(data, selection): if data is None: return None names = [var.name for var in data.domain.variables + data.domain.metas] name = get_next_name(names, "Selection group") metas = data.domain.metas + ( DiscreteVariable( name, ["Unselected"] + ["G{}".format(i + 1) for i in range(np.max(selection))]), ) domain = Domain(data.domain.attributes, data.domain.class_vars, metas) table = data.transform(domain) table.metas[:, len(data.domain.metas):] = \ selection.reshape(len(data), 1) return table def send_data(self): selected = None selection = None # TODO: Implement selection for sql data graph = self.graph if isinstance(self.data, SqlTable): selected = self.data elif self.data is not None: selection = graph.get_selection() if len(selection) > 0: selected = self.data[selection] if graph.selection is not None and np.max(graph.selection) > 1: annotated = self.create_groups_table(self.data, graph.selection) else: annotated = create_annotated_table(self.data, selection) self.Outputs.selected_data.send(selected) self.Outputs.annotated_data.send(annotated) # Store current selection in a setting that is stored in workflow if self.selection is not None and len(selection): self.selection = list(selection) def send_features(self): features = None if self.attr_x or self.attr_y: dom = Domain([], metas=(StringVariable(name="feature"),)) features = Table(dom, [[self.attr_x], [self.attr_y]]) features.name = "Features" self.Outputs.features.send(features) def commit(self): self.send_data() self.send_features() def get_widget_name_extension(self): if self.data is not None: return "{} vs {}".format(self.attr_x.name, self.attr_y.name) def send_report(self): if self.data is None: return def name(var): return var and var.name caption = report.render_items_vert(( ("Color", name(self.graph.attr_color)), ("Label", name(self.graph.attr_label)), ("Shape", name(self.graph.attr_shape)), ("Size", name(self.graph.attr_size)), ("Jittering", (self.attr_x.is_discrete or self.attr_y.is_discrete or self.graph.jitter_continuous) and self.graph.jitter_size))) self.report_plot() if caption: self.report_caption(caption) def onDeleteWidget(self): super().onDeleteWidget() self.graph.plot_widget.getViewBox().deleteLater() self.graph.plot_widget.clear()
class OWScatterPlot(OWWidget): """Scatterplot visualization with explorative analysis and intelligent data visualization enhancements.""" name = 'Scatter Plot' description = "Interactive scatter plot visualization with " \ "intelligent data visualization enhancements." icon = "icons/ScatterPlot.svg" priority = 140 inputs = [("Data", Table, "set_data", Default), ("Data Subset", Table, "set_subset_data"), ("Features", AttributeList, "set_shown_attributes")] outputs = [("Selected Data", Table, Default), ("Other Data", Table), ("Features", Table)] settingsHandler = DomainContextHandler() auto_send_selection = Setting(True) auto_sample = Setting(True) toolbar_selection = Setting(0) attr_x = ContextSetting("") attr_y = ContextSetting("") graph = SettingProvider(OWScatterPlotGraph) jitter_sizes = [0, 0.1, 0.5, 1, 2, 3, 4, 5, 7, 10] graph_name = "graph.plot_widget.plotItem" class Information(OWWidget.Information): sampled_sql = Msg("Large SQL table; showing a sample.") def __init__(self): super().__init__() box = gui.vBox(self.mainArea, True, margin=0) self.graph = OWScatterPlotGraph(self, box, "ScatterPlot") box.layout().addWidget(self.graph.plot_widget) plot = self.graph.plot_widget axispen = QtGui.QPen(self.palette().color(QtGui.QPalette.Text)) axis = plot.getAxis("bottom") axis.setPen(axispen) axis = plot.getAxis("left") axis.setPen(axispen) self.data = None # Orange.data.Table self.subset_data = None # Orange.data.Table self.data_metas_X = None # self.data, where primitive metas are moved to X self.sql_data = None # Orange.data.sql.table.SqlTable self.attribute_selection_list = None # list of Orange.data.Variable self.__timer = QTimer(self, interval=1200) self.__timer.timeout.connect(self.add_data) common_options = dict( labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str) box = gui.vBox(self.controlArea, "Axis Data") self.cb_attr_x = gui.comboBox(box, self, "attr_x", label="Axis x:", callback=self.update_attr, **common_options) self.cb_attr_y = gui.comboBox(box, self, "attr_y", label="Axis y:", callback=self.update_attr, **common_options) vizrank_box = gui.hBox(box) gui.separator(vizrank_box, width=common_options["labelWidth"]) self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank( vizrank_box, self, "Find Informative Projections", self.set_attr) gui.separator(box) gui.valueSlider( box, self, value='graph.jitter_size', label='Jittering: ', values=self.jitter_sizes, callback=self.reset_graph_data, labelFormat=lambda x: "None" if x == 0 else ("%.1f %%" if x < 1 else "%d %%") % x) gui.checkBox( gui.indentedBox(box), self, 'graph.jitter_continuous', 'Jitter continuous values', callback=self.reset_graph_data) self.sampling = gui.auto_commit( self.controlArea, self, "auto_sample", "Sample", box="Sampling", callback=self.switch_sampling, commit=lambda: self.add_data(1)) self.sampling.setVisible(False) box = gui.vBox(self.controlArea, "Points") self.cb_attr_color = gui.comboBox( box, self, "graph.attr_color", label="Color:", emptyString="(Same color)", callback=self.update_colors, **common_options) self.cb_attr_label = gui.comboBox( box, self, "graph.attr_label", label="Label:", emptyString="(No labels)", callback=self.graph.update_labels, **common_options) self.cb_attr_shape = gui.comboBox( box, self, "graph.attr_shape", label="Shape:", emptyString="(Same shape)", callback=self.graph.update_shapes, **common_options) self.cb_attr_size = gui.comboBox( box, self, "graph.attr_size", label="Size:", emptyString="(Same size)", callback=self.graph.update_sizes, **common_options) g = self.graph.gui box = gui.vBox(self.controlArea, "Plot Properties") g.add_widgets([g.ShowLegend, g.ShowGridLines], box) gui.checkBox( box, self, value='graph.tooltip_shows_all', label='Show all data on mouse hover') self.cb_class_density = gui.checkBox( box, self, value='graph.class_density', label='Show class density', callback=self.update_density) gui.checkBox( box, self, 'graph.label_only_selected', 'Label only selected points', callback=self.graph.update_labels) self.zoom_select_toolbar = g.zoom_select_toolbar( gui.vBox(self.controlArea, "Zoom/Select"), nomargin=True, buttons=[g.StateButtonsBegin, g.SimpleSelect, g.Pan, g.Zoom, g.StateButtonsEnd, g.ZoomReset] ) buttons = self.zoom_select_toolbar.buttons buttons[g.Zoom].clicked.connect(self.graph.zoom_button_clicked) buttons[g.Pan].clicked.connect(self.graph.pan_button_clicked) buttons[g.SimpleSelect].clicked.connect(self.graph.select_button_clicked) buttons[g.ZoomReset].clicked.connect(self.graph.reset_button_clicked) self.controlArea.layout().addStretch(100) self.icons = gui.attributeIconDict p = self.graph.plot_widget.palette() self.graph.set_palette(p) gui.auto_commit(self.controlArea, self, "auto_send_selection", "Send Selection", "Send Automatically") def zoom(s): """Zoom in/out by factor `s`.""" viewbox = plot.getViewBox() # scaleBy scales the view's bounds (the axis range) viewbox.scaleBy((1 / s, 1 / s)) def fit_to_view(): viewbox = plot.getViewBox() viewbox.autoRange() zoom_in = QtGui.QAction( "Zoom in", self, triggered=lambda: zoom(1.25) ) zoom_in.setShortcuts([QtGui.QKeySequence(QtGui.QKeySequence.ZoomIn), QtGui.QKeySequence(self.tr("Ctrl+="))]) zoom_out = QtGui.QAction( "Zoom out", self, shortcut=QtGui.QKeySequence.ZoomOut, triggered=lambda: zoom(1 / 1.25) ) zoom_fit = QtGui.QAction( "Fit in view", self, shortcut=QtGui.QKeySequence(Qt.ControlModifier | Qt.Key_0), triggered=fit_to_view ) self.addActions([zoom_in, zoom_out, zoom_fit]) # def settingsFromWidgetCallback(self, handler, context): # context.selectionPolygons = [] # for curve in self.graph.selectionCurveList: # xs = [curve.x(i) for i in range(curve.dataSize())] # ys = [curve.y(i) for i in range(curve.dataSize())] # context.selectionPolygons.append((xs, ys)) # def settingsToWidgetCallback(self, handler, context): # selections = getattr(context, "selectionPolygons", []) # for (xs, ys) in selections: # c = SelectionCurve("") # c.setData(xs,ys) # c.attach(self.graph) # self.graph.selectionCurveList.append(c) def reset_graph_data(self, *_): self.graph.rescale_data() self.update_graph() def set_data(self, data): self.Information.sampled_sql.clear() self.__timer.stop() self.sampling.setVisible(False) self.sql_data = None if isinstance(data, SqlTable): if data.approx_len() < 4000: data = Table(data) else: self.Information.sampled_sql() self.sql_data = data data_sample = data.sample_time(0.8, no_cache=True) data_sample.download_data(2000, partial=True) data = Table(data_sample) self.sampling.setVisible(True) if self.auto_sample: self.__timer.start() if data is not None and (len(data) == 0 or len(data.domain) == 0): data = None if self.data and data and self.data.checksum() == data.checksum(): return self.closeContext() same_domain = (self.data and data and data.domain.checksum() == self.data.domain.checksum()) self.data = data self.data_metas_X = self.move_primitive_metas_to_X(data) if not same_domain: self.init_attr_values() self.vizrank.initialize() self.vizrank_button.setEnabled( self.data is not None and self.data.domain.class_var is not None and len(self.data.domain.attributes) > 1 and len(self.data) > 1) if self.data is not None and self.data.domain.class_var is None \ and len(self.data.domain.attributes) > 1 and len(self.data) > 1: self.vizrank_button.setToolTip( "Data with a class variable is required.") else: self.vizrank_button.setToolTip("") self.openContext(self.data) def add_data(self, time=0.4): if self.data and len(self.data) > 2000: return self.__timer.stop() data_sample = self.sql_data.sample_time(time, no_cache=True) if data_sample: data_sample.download_data(2000, partial=True) data = Table(data_sample) self.data = Table.concatenate((self.data, data), axis=0) self.data_metas_X = self.move_primitive_metas_to_X(self.data) self.handleNewSignals() def switch_sampling(self): self.__timer.stop() if self.auto_sample and self.sql_data: self.add_data() self.__timer.start() def move_primitive_metas_to_X(self, data): if data is not None: new_attrs = [a for a in data.domain.attributes + data.domain.metas if a.is_primitive()] new_metas = [m for m in data.domain.metas if not m.is_primitive()] data = Table.from_table(Domain(new_attrs, data.domain.class_vars, new_metas), data) return data def set_subset_data(self, subset_data): self.warning() if isinstance(subset_data, SqlTable): if subset_data.approx_len() < AUTO_DL_LIMIT: subset_data = Table(subset_data) else: self.warning("Data subset does not support large Sql tables") subset_data = None self.subset_data = self.move_primitive_metas_to_X(subset_data) # called when all signals are received, so the graph is updated only once def handleNewSignals(self): self.graph.new_data(self.data_metas_X, self.subset_data) if self.attribute_selection_list and \ all(attr in self.graph.data_domain for attr in self.attribute_selection_list): self.attr_x = self.attribute_selection_list[0].name self.attr_y = self.attribute_selection_list[1].name self.attribute_selection_list = None self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.unconditional_commit() def set_shown_attributes(self, attributes): if attributes and len(attributes) >= 2: self.attribute_selection_list = attributes[:2] else: self.attribute_selection_list = None def get_shown_attributes(self): return self.attr_x, self.attr_y def init_attr_values(self): self.cb_attr_x.clear() self.attr_x = None self.cb_attr_y.clear() self.attr_y = None self.cb_attr_color.clear() self.cb_attr_color.addItem("(Same color)") self.graph.attr_color = None self.cb_attr_label.clear() self.cb_attr_label.addItem("(No labels)") self.graph.attr_label = None self.cb_attr_shape.clear() self.cb_attr_shape.addItem("(Same shape)") self.graph.attr_shape = None self.cb_attr_size.clear() self.cb_attr_size.addItem("(Same size)") self.graph.attr_size = None if not self.data: return for var in self.data.domain.metas: if not var.is_primitive(): self.cb_attr_label.addItem(self.icons[var], var.name) for attr in self.data.domain.variables: self.cb_attr_x.addItem(self.icons[attr], attr.name) self.cb_attr_y.addItem(self.icons[attr], attr.name) self.cb_attr_color.addItem(self.icons[attr], attr.name) if attr.is_discrete: self.cb_attr_shape.addItem(self.icons[attr], attr.name) else: self.cb_attr_size.addItem(self.icons[attr], attr.name) self.cb_attr_label.addItem(self.icons[attr], attr.name) for var in self.data.domain.metas: if var.is_primitive(): self.cb_attr_x.addItem(self.icons[var], var.name) self.cb_attr_y.addItem(self.icons[var], var.name) self.cb_attr_color.addItem(self.icons[var], var.name) if var.is_discrete: self.cb_attr_shape.addItem(self.icons[var], var.name) else: self.cb_attr_size.addItem(self.icons[var], var.name) self.cb_attr_label.addItem(self.icons[var], var.name) self.attr_x = self.cb_attr_x.itemText(0) if self.cb_attr_y.count() > 1: self.attr_y = self.cb_attr_y.itemText(1) else: self.attr_y = self.cb_attr_y.itemText(0) if self.data.domain.class_var: self.graph.attr_color = self.data.domain.class_var.name else: self.graph.attr_color = "" self.graph.attr_shape = "" self.graph.attr_size = "" self.graph.attr_label = "" def set_attr(self, attr_x, attr_y): self.attr_x, self.attr_y = attr_x.name, attr_y.name self.update_attr() def update_attr(self): self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.send_features() def update_colors(self): self.graph.update_colors() self.cb_class_density.setEnabled(self.graph.can_draw_density()) def update_density(self): self.update_graph(reset_view=False) def update_graph(self, reset_view=True, **_): self.graph.zoomStack = [] if not self.graph.have_data: return self.graph.update_data(self.attr_x, self.attr_y, reset_view) def selection_changed(self): self.send_data() def send_data(self): selected = unselected = None # TODO: Implement selection for sql data if isinstance(self.data, SqlTable): selected = unselected = self.data elif self.data is not None: selection = self.graph.get_selection() if len(selection) == 0: self.send("Selected Data", None) self.send("Other Data", self.data) return selected = self.data[selection] unselection = np.full(len(self.data), True, dtype=bool) unselection[selection] = False unselected = self.data[unselection] self.send("Selected Data", selected) if unselected is None or len(unselected) == 0: self.send("Other Data", None) else: self.send("Other Data", unselected) def send_features(self): features = None if self.attr_x or self.attr_y: dom = Domain([], metas=(StringVariable(name="feature"),)) features = Table(dom, [[self.attr_x], [self.attr_y]]) features.name = "Features" self.send("Features", features) def commit(self): self.send_data() self.send_features() def get_widget_name_extension(self): if self.data is not None: return "{} vs {}".format(self.combo_value(self.cb_attr_x), self.combo_value(self.cb_attr_y)) def send_report(self): disc_attr = False if self.data: domain = self.data.domain disc_attr = domain[self.attr_x].is_discrete or \ domain[self.attr_y].is_discrete caption = report.render_items_vert(( ("Color", self.combo_value(self.cb_attr_color)), ("Label", self.combo_value(self.cb_attr_label)), ("Shape", self.combo_value(self.cb_attr_shape)), ("Size", self.combo_value(self.cb_attr_size)), ("Jittering", (self.graph.jitter_continuous or disc_attr) and self.graph.jitter_size))) self.report_plot() if caption: self.report_caption(caption) def onDeleteWidget(self): super().onDeleteWidget() self.graph.plot_widget.getViewBox().deleteLater() self.graph.plot_widget.clear()
class OWScatterPlot(OWWidget): name = 'Scatter Plot' description = 'Scatter plot visualization.' icon = "icons/ScatterPlot.svg" inputs = [("Data", Table, "set_data", Default), ("Data Subset", Table, "set_subset_data"), ("Features", AttributeList, "set_shown_attributes")] outputs = [("Selected Data", Table, Default), ("Other Data", Table), ("Features", Table)] settingsHandler = DomainContextHandler() auto_send_selection = Setting(True) toolbar_selection = Setting(0) color_settings = Setting(None) selected_schema_index = Setting(0) attr_x = ContextSetting("") attr_y = ContextSetting("") graph = SettingProvider(OWScatterPlotGraph) zoom_select_toolbar = SettingProvider(ZoomSelectToolbar) jitter_sizes = [0, 0.1, 0.5, 1, 2, 3, 4, 5, 7, 10] want_graph = True def __init__(self): super().__init__() box = gui.widgetBox(self.mainArea, True, margin=0) self.graph = OWScatterPlotGraph(self, box, "ScatterPlot") box.layout().addWidget(self.graph.plot_widget) plot = self.graph.plot_widget axispen = QtGui.QPen(self.palette().color(QtGui.QPalette.Text)) axis = plot.getAxis("bottom") axis.setPen(axispen) axis = plot.getAxis("left") axis.setPen(axispen) self.data = None # Orange.data.Table self.subset_data = None # Orange.data.Table self.data_metas_X = None # self.data, where primitive metas are moved to X self.attribute_selection_list = None # list of Orange.data.Variable common_options = {"labelWidth": 50, "orientation": "horizontal", "sendSelectedValue": True, "valueType": str} box = gui.widgetBox(self.controlArea, "Axis Data") self.cb_attr_x = gui.comboBox(box, self, "attr_x", label="Axis x:", callback=self.update_attr, **common_options) self.cb_attr_y = gui.comboBox(box, self, "attr_y", label="Axis y:", callback=self.update_attr, **common_options) self.vizrank = self.VizRank(self) vizrank_box = gui.widgetBox(box, None, orientation='horizontal') gui.separator(vizrank_box, width=common_options["labelWidth"]) self.vizrank_button = gui.button( vizrank_box, self, "Rank projections", callback=self.vizrank.reshow, tooltip="Find projections with good class separation") self.vizrank_button.setEnabled(False) gui.separator(box) gui.valueSlider( box, self, value='graph.jitter_size', label='Jittering: ', values=self.jitter_sizes, callback=self.reset_graph_data, labelFormat=lambda x: "None" if x == 0 else ("%.1f %%" if x < 1 else "%d %%") % x) gui.checkBox( gui.indentedBox(box), self, 'graph.jitter_continuous', 'Jitter continuous values', callback=self.reset_graph_data) box = gui.widgetBox(self.controlArea, "Points") self.cb_attr_color = gui.comboBox( box, self, "graph.attr_color", label="Color:", emptyString="(Same color)", callback=self.update_colors, **common_options) self.cb_attr_label = gui.comboBox( box, self, "graph.attr_label", label="Label:", emptyString="(No labels)", callback=self.graph.update_labels, **common_options) self.cb_attr_shape = gui.comboBox( box, self, "graph.attr_shape", label="Shape:", emptyString="(Same shape)", callback=self.graph.update_shapes, **common_options) self.cb_attr_size = gui.comboBox( box, self, "graph.attr_size", label="Size:", emptyString="(Same size)", callback=self.graph.update_sizes, **common_options) g = self.graph.gui box2 = g.point_properties_box(self.controlArea, box) gui.button(box2, self, "Set Colors", self.set_colors) box = gui.widgetBox(self.controlArea, "Plot Properties") g.add_widgets([g.ShowLegend, g.ShowGridLines], box) gui.checkBox(box, self, value='graph.tooltip_shows_all', label='Show all data on mouse hover') self.cb_class_density = gui.checkBox( box, self, value='graph.class_density', label='Show class density', callback=self.update_density) self.zoom_select_toolbar = g.zoom_select_toolbar( gui.widgetBox(self.controlArea, "Zoom/Select"), nomargin=True, buttons=[g.StateButtonsBegin, g.SimpleSelect, g.Pan, g.Zoom, g.StateButtonsEnd, g.ZoomReset] ) buttons = self.zoom_select_toolbar.buttons buttons[g.Zoom].clicked.connect(self.graph.zoom_button_clicked) buttons[g.Pan].clicked.connect(self.graph.pan_button_clicked) buttons[g.SimpleSelect].clicked.connect(self.graph.select_button_clicked) buttons[g.ZoomReset].clicked.connect(self.graph.reset_button_clicked) self.controlArea.layout().addStretch(100) self.icons = gui.attributeIconDict dlg = self.create_color_dialog() self.graph.continuous_palette = dlg.getContinuousPalette("contPalette") self.graph.discrete_palette = dlg.getDiscretePalette("discPalette") dlg.deleteLater() p = self.graph.plot_widget.palette() self.graph.set_palette(p) gui.auto_commit(self.controlArea, self, "auto_send_selection", "Send Selection") def zoom(s): """Zoom in/out by factor `s`.""" viewbox = plot.getViewBox() # scaleBy scales the view's bounds (the axis range) viewbox.scaleBy((1 / s, 1 / s)) def fit_to_view(): viewbox = plot.getViewBox() viewbox.autoRange() zoom_in = QtGui.QAction( "Zoom in", self, triggered=lambda: zoom(1.25) ) zoom_in.setShortcuts([QtGui.QKeySequence(QtGui.QKeySequence.ZoomIn), QtGui.QKeySequence(self.tr("Ctrl+="))]) zoom_out = QtGui.QAction( "Zoom out", self, shortcut=QtGui.QKeySequence.ZoomOut, triggered=lambda: zoom(1 / 1.25) ) zoom_fit = QtGui.QAction( "Fit in view", self, shortcut=QtGui.QKeySequence(Qt.ControlModifier | Qt.Key_0), triggered=fit_to_view ) self.addActions([zoom_in, zoom_out, zoom_fit]) self.graphButton.clicked.connect(self.save_graph) # def settingsFromWidgetCallback(self, handler, context): # context.selectionPolygons = [] # for curve in self.graph.selectionCurveList: # xs = [curve.x(i) for i in range(curve.dataSize())] # ys = [curve.y(i) for i in range(curve.dataSize())] # context.selectionPolygons.append((xs, ys)) # def settingsToWidgetCallback(self, handler, context): # selections = getattr(context, "selectionPolygons", []) # for (xs, ys) in selections: # c = SelectionCurve("") # c.setData(xs,ys) # c.attach(self.graph) # self.graph.selectionCurveList.append(c) def reset_graph_data(self, *_): self.graph.rescale_data() self.update_graph() def set_data(self, data): self.information(1) if isinstance(data, SqlTable): if data.approx_len() < 4000: data = Table(data) else: self.information(1, "Data has been sampled") data_sample = data.sample_time(1, no_cache=True) data_sample.download_data(2000, partial=True) data = Table(data_sample) if data is not None and (len(data) == 0 or len(data.domain) == 0): data = None if self.data and data and self.data.checksum() == data.checksum(): return self.closeContext() same_domain = \ self.data and data and \ data.domain.checksum() == self.data.domain.checksum() self.data = data self.data_metas_X = self.move_primitive_metas_to_X(data) # TODO: adapt scatter plot to work on SqlTables (avoid use of X and Y) if isinstance(self.data, SqlTable): self.data.download_data() if not same_domain: self.init_attr_values() self.vizrank._initialize() self.vizrank_button.setEnabled( self.data is not None and self.data.domain.class_var is not None and len(self.data.domain.attributes) > 1) self.openContext(self.data) def move_primitive_metas_to_X(self, data): if data is not None: new_attrs = [a for a in data.domain.attributes + data.domain.metas if a.is_primitive()] new_metas = [m for m in data.domain.metas if not m.is_primitive()] data = Table.from_table(Domain(new_attrs, data.domain.class_vars, new_metas), data) return data def set_subset_data(self, subset_data): self.subset_data = self.move_primitive_metas_to_X(subset_data) # called when all signals are received, so the graph is updated only once def handleNewSignals(self): self.graph.new_data(self.data_metas_X, self.subset_data) if self.attribute_selection_list and \ all(attr.name in self.graph.attribute_name_index for attr in self.attribute_selection_list): self.attr_x = self.attribute_selection_list[0].name self.attr_y = self.attribute_selection_list[1].name self.attribute_selection_list = None self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.unconditional_commit() def set_shown_attributes(self, attributes): if attributes and len(attributes) >= 2: self.attribute_selection_list = attributes[:2] else: self.attribute_selection_list = None def get_shown_attributes(self): return self.attr_x, self.attr_y def init_attr_values(self): self.cb_attr_x.clear() self.cb_attr_y.clear() self.attr_x = None self.attr_y = None self.cb_attr_color.clear() self.cb_attr_color.addItem("(Same color)") self.cb_attr_label.clear() self.cb_attr_label.addItem("(No labels)") self.cb_attr_shape.clear() self.cb_attr_shape.addItem("(Same shape)") self.cb_attr_size.clear() self.cb_attr_size.addItem("(Same size)") if not self.data: return for var in self.data.domain.metas: if not var.is_primitive(): self.cb_attr_label.addItem(self.icons[var], var.name) for attr in self.data.domain.variables: self.cb_attr_x.addItem(self.icons[attr], attr.name) self.cb_attr_y.addItem(self.icons[attr], attr.name) self.cb_attr_color.addItem(self.icons[attr], attr.name) if attr.is_discrete: self.cb_attr_shape.addItem(self.icons[attr], attr.name) else: self.cb_attr_size.addItem(self.icons[attr], attr.name) self.cb_attr_label.addItem(self.icons[attr], attr.name) for var in self.data.domain.metas: if var.is_primitive(): self.cb_attr_x.addItem(self.icons[var], var.name) self.cb_attr_y.addItem(self.icons[var], var.name) self.cb_attr_color.addItem(self.icons[var], var.name) if var.is_discrete: self.cb_attr_shape.addItem(self.icons[var], var.name) else: self.cb_attr_size.addItem(self.icons[var], var.name) self.cb_attr_label.addItem(self.icons[var], var.name) self.attr_x = self.cb_attr_x.itemText(0) if self.cb_attr_y.count() > 1: self.attr_y = self.cb_attr_y.itemText(1) else: self.attr_y = self.cb_attr_y.itemText(0) if self.data.domain.class_var: self.graph.attr_color = self.data.domain.class_var.name else: self.graph.attr_color = "" self.graph.attr_shape = "" self.graph.attr_size = "" self.graph.attr_label = "" def update_attr(self, attributes=None): self.update_graph(attributes=attributes) self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.send_features() def update_colors(self): self.graph.update_colors() self.cb_class_density.setEnabled(self.graph.can_draw_density()) def update_density(self): self.update_graph(reset_view=False) def update_graph(self, attributes=None, reset_view=True, **_): self.graph.zoomStack = [] if attributes and len(attributes) == 2: self.attr_x, self.attr_y = attributes if not self.graph.have_data: return self.graph.update_data(self.attr_x, self.attr_y, reset_view) def selection_changed(self): self.send_data() def send_data(self): selected = unselected = None # TODO: Implement selection for sql data if isinstance(self.data, SqlTable): selected = unselected = self.data elif self.data is not None: selection = self.graph.get_selection() selected = self.data[selection] unselection = np.full(len(self.data), True, dtype=bool) unselection[selection] = False unselected = self.data[unselection] self.send("Selected Data", selected) self.send("Other Data", unselected) def send_features(self): features = None if self.attr_x or self.attr_y: dom = Domain([], metas=(StringVariable(name="feature"),)) features = Table(dom, [[self.attr_x], [self.attr_y]]) features.name = "Features" self.send("Features", features) def commit(self): self.send_data() self.send_features() def set_colors(self): dlg = self.create_color_dialog() if dlg.exec_(): self.color_settings = dlg.getColorSchemas() self.selected_schema_index = dlg.selectedSchemaIndex self.graph.continuous_palette = dlg.getContinuousPalette("contPalette") self.graph.discrete_palette = dlg.getDiscretePalette("discPalette") self.update_graph() def create_color_dialog(self): c = ColorPaletteDlg(self, "Color Palette") c.createDiscretePalette("discPalette", "Discrete Palette") c.createContinuousPalette("contPalette", "Continuous Palette") c.setColorSchemas(self.color_settings, self.selected_schema_index) return c def closeEvent(self, ce): self.vizrank.close() super().closeEvent(ce) def hideEvent(self, he): self.vizrank.hide() super().hideEvent(he) def sendReport(self): self.startReport( "%s [%s - %s]" % (self.windowTitle(), self.attr_x, self.attr_y)) self.reportSettings( "Visualized attributes", [("X", self.attr_x), ("Y", self.attr_y), self.graph.attr_color and ("Color", self.graph.attr_color), self.graph.attr_label and ("Label", self.graph.attr_label), self.graph.attr_shape and ("Shape", self.graph.attr_shape), self.graph.attr_size and ("Size", self.graph.attr_size)]) self.reportSettings( "Settings", [("Symbol size", self.graph.point_width), ("Opacity", self.graph.alpha_value), ("Jittering", self.graph.jitter_size), ("Jitter continuous attributes", gui.YesNo[self.graph.jitter_continuous])]) self.reportSection("Graph") self.reportImage(self.graph.save_to_file, QSize(400, 400)) def save_graph(self): from Orange.widgets.data.owsave import OWSave save_img = OWSave(data=self.graph.plot_widget.plotItem, file_formats=FileFormat.img_writers) save_img.exec_() def onDeleteWidget(self): super().onDeleteWidget() self.graph.plot_widget.getViewBox().deleteLater() self.graph.plot_widget.clear() class VizRank(OWWidget): name = "Rank projections (Scatter Plot)" def __init__(self, parent_widget): super().__init__() self.parent_widget = parent_widget self.want_control_area = False self.running = False self.progress = None self.k = 10 self.projectionTable = QTableView() self.mainArea.layout().addWidget(self.projectionTable) self.projectionTable.setSelectionBehavior(QTableView.SelectRows) self.projectionTable.setSelectionMode(QTableView.SingleSelection) self.projectionTable.setSortingEnabled(True) self.projectionTableModel = QStandardItemModel(self) self.projectionTable.setModel(self.projectionTableModel) self.projectionTable.selectionModel().selectionChanged.connect( self.on_selection_changed) self.button = gui.button(self.mainArea, self, "Start evaluation", callback=self.toggle, default=True) self.resize(380, 512) self._initialize() def _initialize(self): self.running = False self.projectionTableModel.clear() self.projectionTableModel.setHorizontalHeaderLabels( ["Score", "Feature 1", "Feature 2"]) self.projectionTable.setColumnWidth(0, 60) self.projectionTable.setColumnWidth(1, 120) self.projectionTable.setColumnWidth(2, 120) self.button.setText("Start evaluation") self.button.setEnabled(False) self.pause = False self.data = None self.attrs = [] self.scores = [] self.i, self.j = 0, 0 if self.progress: self.progress.finish() self.progress = None self.information(0) if self.parent_widget.data: if not self.parent_widget.data.domain.class_var: self.information( 0, "Data with a class variable is required.") return if len(self.parent_widget.data.domain.attributes) < 2: self.information( 0, 'At least 2 unique features are needed.') return self.button.setEnabled(True) def on_selection_changed(self, selected, deselected): """Called when the ranks view selection changes.""" a1 = selected.indexes()[1].data() a2 = selected.indexes()[2].data() self.parent_widget.update_attr(attributes=(a1, a2)) def toggle(self): self.running ^= 1 if self.running: self.button.setText("Pause") self.run() else: self.button.setText("Continue") self.button.setEnabled(False) def run(self): graph = self.parent_widget.graph y_full = self.parent_widget.data.Y norm = 1 / (len(y_full) * self.k) if not self.attrs: self.attrs = self.score_heuristic() if not self.progress: self.progress = gui.ProgressBar( self, len(self.attrs) * (len(self.attrs) - 1) / 2) for i in range(self.i, len(self.attrs)): ind1 = graph.attribute_name_index[self.attrs[i]] for j in range(self.j, i): if not self.running: self.i, self.j = i, j if not self.projectionTable.selectedIndexes(): self.projectionTable.selectRow(0) self.button.setEnabled(True) return ind2 = graph.attribute_name_index[self.attrs[j]] X = graph.scaled_data[[ind1, ind2], :] valid = graph.get_valid_list([ind1, ind2]) X = X[:, valid].T y = y_full[valid] knn = NearestNeighbors(n_neighbors=self.k).fit(X) ind = knn.kneighbors(return_distance=False) score = norm * np.sum(y[ind] == y.reshape(-1, 1)) pos = bisect_left(self.scores, score) self.projectionTableModel.insertRow( len(self.scores) - pos, [QStandardItem("{:.4f}".format(score)), QStandardItem(self.attrs[j]), QStandardItem(self.attrs[i])]) self.scores.insert(pos, score) self.progress.advance() self.j = 0 self.progress.finish() if not self.projectionTable.selectedIndexes(): self.projectionTable.selectRow(0) self.button.setText("Finished") self.button.setEnabled(False) def score_heuristic(self): X = self.parent_widget.graph.scaled_data.T Y = self.parent_widget.data.Y dom = Orange.data.Domain([ContinuousVariable(str(i)) for i in range(X.shape[1])], self.parent_widget.data.domain.class_vars) data = Orange.data.Table(dom, X, Y) weights = ReliefF(n_iterations=100, k_nearest=self.k)(data) attrs = sorted( zip(weights, (x.name for x in self.parent_widget.data.domain.attributes) ), reverse=True) return [a for _, a in attrs]
class OWScatterPlot(OWWidget): name = 'Scatter plot' description = 'Scatter plot visualization' inputs = [("Data", Table, "set_data", Default), ("Data Subset", Table, "set_subset_data"), ("Features", AttributeList, "set_shown_attributes")] outputs = [("Selected Data", Table), ("Other Data", Table)] settingsHandler = DomainContextHandler() auto_send_selection = Setting(True) toolbar_selection = Setting(0) color_settings = Setting(None) selected_schema_index = Setting(0) attr_x = ContextSetting("") attr_y = ContextSetting("") graph = SettingProvider(OWScatterPlotGraph) zoom_select_toolbar = SettingProvider(ZoomSelectToolbar) jitter_sizes = [0, 0.1, 0.5, 1, 2, 3, 4, 5, 7, 10] def __init__(self): super().__init__() box = gui.widgetBox(self.mainArea, True, margin=0) self.graph = OWScatterPlotGraph(self, box, "ScatterPlot") box.layout().addWidget(self.graph.plot_widget) self.data = None # Orange.data.Table self.subset_data = None # Orange.data.Table self.attribute_selection_list = None # list of Orange.data.Variable self.selection_dirty = False common_options = { "labelWidth": 50, "orientation": "horizontal", "sendSelectedValue": True, "valueType": str } box = gui.widgetBox(self.controlArea, "Axis Data") self.cb_attr_x = gui.comboBox(box, self, "attr_x", label="Axis x:", callback=self.major_graph_update, **common_options) self.cb_attr_y = gui.comboBox(box, self, "attr_y", label="Axis y:", callback=self.major_graph_update, **common_options) gui.valueSlider(box, self, value='graph.jitter_size', label='Jittering: ', values=self.jitter_sizes, callback=self.reset_graph_data, labelFormat=lambda x: "None" if x == 0 else ("%.1f %%" if x < 1 else "%d %%") % x) gui.checkBox(gui.indentedBox(box), self, 'graph.jitter_continuous', 'Jitter continuous values', callback=self.reset_graph_data) box = gui.widgetBox(self.controlArea, "Points") self.cb_attr_color = gui.comboBox(box, self, "graph.attr_color", label="Color:", emptyString="(Same color)", callback=self.graph.update_colors, **common_options) self.cb_attr_label = gui.comboBox(box, self, "graph.attr_label", label="Label:", emptyString="(No labels)", callback=self.graph.update_labels, **common_options) self.cb_attr_shape = gui.comboBox(box, self, "graph.attr_shape", label="Shape:", emptyString="(Same shape)", callback=self.graph.update_shapes, **common_options) self.cb_attr_size = gui.comboBox(box, self, "graph.attr_size", label="Size:", emptyString="(Same size)", callback=self.graph.update_sizes, **common_options) g = self.graph.gui box2 = g.point_properties_box(self.controlArea, box) gui.button(box2, self, "Set Colors", self.set_colors) box = gui.widgetBox(self.controlArea, "Plot Properties") g.add_widgets([g.ShowLegend, g.ShowGridLines], box) gui.checkBox(box, self, value='graph.tooltip_shows_all', label='Show all data on mouse hover') gui.separator(self.controlArea, 8, 8) self.zoom_select_toolbar = g.zoom_select_toolbar( self.controlArea, nomargin=True, buttons=[ g.StateButtonsBegin, g.SimpleSelect, g.Pan, g.Zoom, g.StateButtonsEnd, g.ZoomReset, g.Spacing, g.SendSelection ]) buttons = self.zoom_select_toolbar.buttons buttons[g.SendSelection].clicked.connect(self.send_selection) buttons[g.Zoom].clicked.connect(self.graph.zoom_button_clicked) buttons[g.Pan].clicked.connect(self.graph.pan_button_clicked) buttons[g.SimpleSelect].clicked.connect( self.graph.select_button_clicked) buttons[g.ZoomReset].clicked.connect(self.graph.reset_button_clicked) cb_auto_send = gui.checkBox(box, self, 'auto_send_selection', 'Send selection on change') gui.setStopper(self, buttons[g.SendSelection], cb_auto_send, "selection_dirty", self.send_selection) self.controlArea.layout().addStretch(100) self.icons = gui.attributeIconDict dlg = self.create_color_dialog() self.graph.continuous_palette = dlg.getContinuousPalette("contPalette") self.graph.discrete_palette = dlg.getDiscretePalette("discPalette") p = self.graph.plot_widget.palette() self.graph.set_palette(p) self.zoom_select_toolbar.buttons[OWPlotGUI.SendSelection].setEnabled( not self.auto_send_selection) self.mainArea.setMinimumWidth(700) self.mainArea.setMinimumHeight(550) # self.vizrank = OWVizRank(self, self.signalManager, self.graph, # orngVizRank.SCATTERPLOT, "ScatterPlot") # self.optimizationDlg = self.vizrank # def settingsFromWidgetCallback(self, handler, context): # context.selectionPolygons = [] # for curve in self.graph.selectionCurveList: # xs = [curve.x(i) for i in range(curve.dataSize())] # ys = [curve.y(i) for i in range(curve.dataSize())] # context.selectionPolygons.append((xs, ys)) # def settingsToWidgetCallback(self, handler, context): # selections = getattr(context, "selectionPolygons", []) # for (xs, ys) in selections: # c = SelectionCurve("") # c.setData(xs,ys) # c.attach(self.graph) # self.graph.selectionCurveList.append(c) def reset_graph_data(self, *_): self.graph.rescale_data() self.major_graph_update() def set_data(self, data: Orange.data.Table): if data is not None and (len(data) == 0 or len(data.domain) == 0): data = None if self.data and data and self.data.checksum() == data.checksum(): return self.closeContext() same_domain = \ self.data and data and \ data.domain.checksum() == self.data.domain.checksum() self.data = data # TODO: adapt scatter plot to work on SqlTables (avoid use of X and Y) if isinstance(self.data, SqlTable): self.data.X = np.empty( (len(self.data), len(self.data.domain.attributes))) self.data.Y = np.empty( (len(self.data), len(self.data.domain.class_vars))) for i, row in enumerate(data): self.data.X[i] = [ row[attr] for attr in self.data.domain.attributes ] if self.data.domain.class_vars: self.data.Y[i] = [ row[cv] for cv in self.data.domain.class_vars ] # self.vizrank.clearResults() if not same_domain: self.init_attr_values() self.openContext(self.data) def set_subset_data(self, subset_data): self.subset_data = subset_data # self.vizrank.clearArguments() # called when all signals are received, so the graph is updated only once def handleNewSignals(self): self.graph.set_data(self.data, self.subset_data) # self.vizrank.resetDialog() if self.attribute_selection_list and \ all(attr in self.graph.attribute_name_index for attr in self.attribute_selection_list): self.attr_x = self.attribute_selection_list[0] self.attr_y = self.attribute_selection_list[1] self.attribute_selection_list = None self.update_graph() self.send_selection() def set_shown_attributes(self, attributes): if attributes and len(attributes) >= 2: self.attribute_selection_list = attributes[:2] else: self.attribute_selection_list = None # Callback from VizRank dialog def show_selected_attributes(self): val = self.vizrank.get_selected_projection() if not val: return if self.data.domain.class_var: self.graph.attr_color = self.data.domain.class_var.name self.major_graph_update(val[3]) def get_shown_attributes(self): return self.attr_x, self.attr_y def init_attr_values(self): self.cb_attr_x.clear() self.cb_attr_y.clear() self.cb_attr_color.clear() self.cb_attr_color.addItem("(Same color)") self.cb_attr_label.clear() self.cb_attr_label.addItem("(No labels)") self.cb_attr_shape.clear() self.cb_attr_shape.addItem("(Same shape)") self.cb_attr_size.clear() self.cb_attr_size.addItem("(Same size)") if not self.data: return for var in self.data.domain.metas: self.cb_attr_label.addItem(self.icons[var], var.name) for attr in self.data.domain.variables: self.cb_attr_x.addItem(self.icons[attr], attr.name) self.cb_attr_y.addItem(self.icons[attr], attr.name) self.cb_attr_color.addItem(self.icons[attr], attr.name) if isinstance(attr, DiscreteVariable): self.cb_attr_shape.addItem(self.icons[attr], attr.name) else: self.cb_attr_size.addItem(self.icons[attr], attr.name) self.cb_attr_label.addItem(self.icons[attr], attr.name) self.attr_x = self.cb_attr_x.itemText(0) if self.cb_attr_y.count() > 1: self.attr_y = self.cb_attr_y.itemText(1) else: self.attr_y = self.cb_attr_y.itemText(0) if self.data.domain.class_var: self.graph.attr_color = self.data.domain.class_var.name else: self.graph.attr_color = "" self.graph.attr_shape = "" self.graph.attr_size = "" self.graph.attr_label = "" def major_graph_update(self, attributes=None, inside_colors=None, **args): self.update_graph(attributes, inside_colors, **args) def update_graph(self, attributes=None, inside_colors=None, **_): self.graph.zoomStack = [] if not self.graph.have_data: return if attributes and len(attributes) == 2: self.attr_x, self.attr_y = attributes self.graph.update_data(self.attr_x, self.attr_y) def saveSettings(self): OWWidget.saveSettings(self) # self.vizrank.saveSettings() """ def auto_selection_changed(self): self.zoom_select_toolbar.buttons[OWPlotGUI.SendSelection].setEnabled( not self.auto_send_selection) if self.auto_send_selection: self.send_selection() """ def selection_changed(self): if self.auto_send_selection: self.send_selection() else: self.selection_dirty = True def send_selection(self): self.selection_dirty = False selection = self.graph.get_selection() selected = self.data[selection] unselection = np.full(len(self.data), True, dtype=bool) unselection[selection] = False unselected = self.data[unselection] self.send("Selected Data", selected) self.send("Other Data", unselected) def set_colors(self): dlg = self.create_color_dialog() if dlg.exec_(): self.color_settings = dlg.getColorSchemas() self.selected_schema_index = dlg.selectedSchemaIndex self.graph.continuous_palette = dlg.getContinuousPalette( "contPalette") self.graph.discrete_palette = dlg.getDiscretePalette("discPalette") self.update_graph() def create_color_dialog(self): c = ColorPaletteDlg(self, "Color Palette") c.createDiscretePalette("discPalette", "Discrete Palette") c.createContinuousPalette("contPalette", "Continuous Palette") c.setColorSchemas(self.color_settings, self.selected_schema_index) return c def closeEvent(self, ce): # self.vizrank.close() super().closeEvent(ce) def sendReport(self): self.startReport("%s [%s - %s]" % (self.windowTitle(), self.attr_x, self.attr_y)) self.reportSettings( "Visualized attributes", [("X", self.attr_x), ("Y", self.attr_y), self.graph.attr_color and ("Color", self.graph.attr_color), self.graph.attr_label and ("Label", self.graph.attr_label), self.graph.attr_shape and ("Shape", self.graph.attr_shape), self.graph.attr_size and ("Size", self.graph.attr_size)]) self.reportSettings("Settings", [("Symbol size", self.graph.point_width), ("Opacity", self.graph.alpha_value), ("Jittering", self.graph.jitter_size), ("Jitter continuous attributes", gui.YesNo[self.graph.jitter_continuous])]) self.reportSection("Graph") self.reportImage(self.graph.save_to_file, QSize(400, 400))
class OWScatterPlot(OWWidget): name = 'Scatter Plot' description = 'Scatter plot visualization.' icon = "icons/ScatterPlot.svg" inputs = [("Data", Table, "set_data", Default), ("Data Subset", Table, "set_subset_data"), ("Features", AttributeList, "set_shown_attributes")] outputs = [("Selected Data", Table, Default), ("Other Data", Table), ("Features", Table)] settingsHandler = DomainContextHandler() auto_send_selection = Setting(True) auto_sample = Setting(True) toolbar_selection = Setting(0) attr_x = ContextSetting("") attr_y = ContextSetting("") graph = SettingProvider(OWScatterPlotGraph) jitter_sizes = [0, 0.1, 0.5, 1, 2, 3, 4, 5, 7, 10] graph_name = "graph.plot_widget.plotItem" def __init__(self): super().__init__() box = gui.vBox(self.mainArea, True, margin=0) self.graph = OWScatterPlotGraph(self, box, "ScatterPlot") box.layout().addWidget(self.graph.plot_widget) plot = self.graph.plot_widget axispen = QtGui.QPen(self.palette().color(QtGui.QPalette.Text)) axis = plot.getAxis("bottom") axis.setPen(axispen) axis = plot.getAxis("left") axis.setPen(axispen) self.data = None # Orange.data.Table self.subset_data = None # Orange.data.Table self.data_metas_X = None # self.data, where primitive metas are moved to X self.sql_data = None # Orange.data.sql.table.SqlTable self.attribute_selection_list = None # list of Orange.data.Variable self.__timer = QTimer(self, interval=1200) self.__timer.timeout.connect(self.add_data) common_options = dict(labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str) box = gui.vBox(self.controlArea, "Axis Data") self.cb_attr_x = gui.comboBox(box, self, "attr_x", label="Axis x:", callback=self.update_attr, **common_options) self.cb_attr_y = gui.comboBox(box, self, "attr_y", label="Axis y:", callback=self.update_attr, **common_options) self.vizrank = self.VizRank(self) vizrank_box = gui.hBox(box) gui.separator(vizrank_box, width=common_options["labelWidth"]) self.vizrank_button = gui.button( vizrank_box, self, "Rank projections", callback=self.vizrank.reshow, tooltip="Find projections with good class separation") self.vizrank_button.setEnabled(False) gui.separator(box) gui.valueSlider(box, self, value='graph.jitter_size', label='Jittering: ', values=self.jitter_sizes, callback=self.reset_graph_data, labelFormat=lambda x: "None" if x == 0 else ("%.1f %%" if x < 1 else "%d %%") % x) gui.checkBox(gui.indentedBox(box), self, 'graph.jitter_continuous', 'Jitter continuous values', callback=self.reset_graph_data) self.sampling = gui.auto_commit(self.controlArea, self, "auto_sample", "Sample", box="Sampling", callback=self.switch_sampling, commit=lambda: self.add_data(1)) self.sampling.setVisible(False) box = gui.vBox(self.controlArea, "Points") self.cb_attr_color = gui.comboBox(box, self, "graph.attr_color", label="Color:", emptyString="(Same color)", callback=self.update_colors, **common_options) self.cb_attr_label = gui.comboBox(box, self, "graph.attr_label", label="Label:", emptyString="(No labels)", callback=self.graph.update_labels, **common_options) self.cb_attr_shape = gui.comboBox(box, self, "graph.attr_shape", label="Shape:", emptyString="(Same shape)", callback=self.graph.update_shapes, **common_options) self.cb_attr_size = gui.comboBox(box, self, "graph.attr_size", label="Size:", emptyString="(Same size)", callback=self.graph.update_sizes, **common_options) g = self.graph.gui box2 = g.point_properties_box(self.controlArea, box) box = gui.vBox(self.controlArea, "Plot Properties") g.add_widgets([g.ShowLegend, g.ShowGridLines], box) gui.checkBox(box, self, value='graph.tooltip_shows_all', label='Show all data on mouse hover') self.cb_class_density = gui.checkBox(box, self, value='graph.class_density', label='Show class density', callback=self.update_density) self.zoom_select_toolbar = g.zoom_select_toolbar( gui.vBox(self.controlArea, "Zoom/Select"), nomargin=True, buttons=[ g.StateButtonsBegin, g.SimpleSelect, g.Pan, g.Zoom, g.StateButtonsEnd, g.ZoomReset ]) buttons = self.zoom_select_toolbar.buttons buttons[g.Zoom].clicked.connect(self.graph.zoom_button_clicked) buttons[g.Pan].clicked.connect(self.graph.pan_button_clicked) buttons[g.SimpleSelect].clicked.connect( self.graph.select_button_clicked) buttons[g.ZoomReset].clicked.connect(self.graph.reset_button_clicked) self.controlArea.layout().addStretch(100) self.icons = gui.attributeIconDict p = self.graph.plot_widget.palette() self.graph.set_palette(p) gui.auto_commit(self.controlArea, self, "auto_send_selection", "Send Selection") def zoom(s): """Zoom in/out by factor `s`.""" viewbox = plot.getViewBox() # scaleBy scales the view's bounds (the axis range) viewbox.scaleBy((1 / s, 1 / s)) def fit_to_view(): viewbox = plot.getViewBox() viewbox.autoRange() zoom_in = QtGui.QAction("Zoom in", self, triggered=lambda: zoom(1.25)) zoom_in.setShortcuts([ QtGui.QKeySequence(QtGui.QKeySequence.ZoomIn), QtGui.QKeySequence(self.tr("Ctrl+=")) ]) zoom_out = QtGui.QAction("Zoom out", self, shortcut=QtGui.QKeySequence.ZoomOut, triggered=lambda: zoom(1 / 1.25)) zoom_fit = QtGui.QAction("Fit in view", self, shortcut=QtGui.QKeySequence(Qt.ControlModifier | Qt.Key_0), triggered=fit_to_view) self.addActions([zoom_in, zoom_out, zoom_fit]) # def settingsFromWidgetCallback(self, handler, context): # context.selectionPolygons = [] # for curve in self.graph.selectionCurveList: # xs = [curve.x(i) for i in range(curve.dataSize())] # ys = [curve.y(i) for i in range(curve.dataSize())] # context.selectionPolygons.append((xs, ys)) # def settingsToWidgetCallback(self, handler, context): # selections = getattr(context, "selectionPolygons", []) # for (xs, ys) in selections: # c = SelectionCurve("") # c.setData(xs,ys) # c.attach(self.graph) # self.graph.selectionCurveList.append(c) def reset_graph_data(self, *_): self.graph.rescale_data() self.update_graph() def set_data(self, data): self.information(1) self.__timer.stop() self.sampling.setVisible(False) self.sql_data = None if isinstance(data, SqlTable): if data.approx_len() < 4000: data = Table(data) else: self.information(1, "Large SQL table (showing a sample)") self.sql_data = data data_sample = data.sample_time(0.8, no_cache=True) data_sample.download_data(2000, partial=True) data = Table(data_sample) self.sampling.setVisible(True) if self.auto_sample: self.__timer.start() if data is not None and (len(data) == 0 or len(data.domain) == 0): data = None if self.data and data and self.data.checksum() == data.checksum(): return self.closeContext() same_domain = (self.data and data and data.domain.checksum() == self.data.domain.checksum()) self.data = data self.data_metas_X = self.move_primitive_metas_to_X(data) if not same_domain: self.init_attr_values() self.vizrank._initialize() self.vizrank_button.setEnabled( self.data is not None and self.data.domain.class_var is not None and len(self.data.domain.attributes) > 1 and len(self.data) > 1) self.openContext(self.data) def add_data(self, time=0.4): if self.data and len(self.data) > 2000: return self.__timer.stop() data_sample = self.sql_data.sample_time(time, no_cache=True) if data_sample: data_sample.download_data(2000, partial=True) data = Table(data_sample) self.data = Table.concatenate((self.data, data), axis=0) self.data_metas_X = self.move_primitive_metas_to_X(self.data) self.handleNewSignals() def switch_sampling(self): self.__timer.stop() if self.auto_sample and self.sql_data: self.add_data() self.__timer.start() def move_primitive_metas_to_X(self, data): if data is not None: new_attrs = [ a for a in data.domain.attributes + data.domain.metas if a.is_primitive() ] new_metas = [m for m in data.domain.metas if not m.is_primitive()] data = Table.from_table( Domain(new_attrs, data.domain.class_vars, new_metas), data) return data def set_subset_data(self, subset_data): self.warning(0) if isinstance(subset_data, SqlTable): if subset_data.approx_len() < AUTO_DL_LIMIT: subset_data = Table(subset_data) else: self.warning(0, "Data subset does not support large Sql tables") subset_data = None self.subset_data = self.move_primitive_metas_to_X(subset_data) # called when all signals are received, so the graph is updated only once def handleNewSignals(self): self.graph.new_data(self.data_metas_X, self.subset_data) if self.attribute_selection_list and \ all(attr.name in self.graph.attribute_name_index for attr in self.attribute_selection_list): self.attr_x = self.attribute_selection_list[0].name self.attr_y = self.attribute_selection_list[1].name self.attribute_selection_list = None self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.unconditional_commit() def set_shown_attributes(self, attributes): if attributes and len(attributes) >= 2: self.attribute_selection_list = attributes[:2] else: self.attribute_selection_list = None def get_shown_attributes(self): return self.attr_x, self.attr_y def init_attr_values(self): self.cb_attr_x.clear() self.cb_attr_y.clear() self.attr_x = None self.attr_y = None self.cb_attr_color.clear() self.cb_attr_color.addItem("(Same color)") self.cb_attr_label.clear() self.cb_attr_label.addItem("(No labels)") self.cb_attr_shape.clear() self.cb_attr_shape.addItem("(Same shape)") self.cb_attr_size.clear() self.cb_attr_size.addItem("(Same size)") if not self.data: return for var in self.data.domain.metas: if not var.is_primitive(): self.cb_attr_label.addItem(self.icons[var], var.name) for attr in self.data.domain.variables: self.cb_attr_x.addItem(self.icons[attr], attr.name) self.cb_attr_y.addItem(self.icons[attr], attr.name) self.cb_attr_color.addItem(self.icons[attr], attr.name) if attr.is_discrete: self.cb_attr_shape.addItem(self.icons[attr], attr.name) else: self.cb_attr_size.addItem(self.icons[attr], attr.name) self.cb_attr_label.addItem(self.icons[attr], attr.name) for var in self.data.domain.metas: if var.is_primitive(): self.cb_attr_x.addItem(self.icons[var], var.name) self.cb_attr_y.addItem(self.icons[var], var.name) self.cb_attr_color.addItem(self.icons[var], var.name) if var.is_discrete: self.cb_attr_shape.addItem(self.icons[var], var.name) else: self.cb_attr_size.addItem(self.icons[var], var.name) self.cb_attr_label.addItem(self.icons[var], var.name) self.attr_x = self.cb_attr_x.itemText(0) if self.cb_attr_y.count() > 1: self.attr_y = self.cb_attr_y.itemText(1) else: self.attr_y = self.cb_attr_y.itemText(0) if self.data.domain.class_var: self.graph.attr_color = self.data.domain.class_var.name else: self.graph.attr_color = "" self.graph.attr_shape = "" self.graph.attr_size = "" self.graph.attr_label = "" def update_attr(self, attributes=None): self.update_graph(attributes=attributes) self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.send_features() def update_colors(self): self.graph.update_colors() self.cb_class_density.setEnabled(self.graph.can_draw_density()) def update_density(self): self.update_graph(reset_view=False) def update_graph(self, attributes=None, reset_view=True, **_): self.graph.zoomStack = [] if attributes and len(attributes) == 2: self.attr_x, self.attr_y = attributes if not self.graph.have_data: return self.graph.update_data(self.attr_x, self.attr_y, reset_view) def selection_changed(self): self.send_data() def send_data(self): selected = unselected = None # TODO: Implement selection for sql data if isinstance(self.data, SqlTable): selected = unselected = self.data elif self.data is not None: selection = self.graph.get_selection() selected = self.data[selection] unselection = np.full(len(self.data), True, dtype=bool) unselection[selection] = False unselected = self.data[unselection] self.send("Selected Data", selected) self.send("Other Data", unselected) def send_features(self): features = None if self.attr_x or self.attr_y: dom = Domain([], metas=(StringVariable(name="feature"), )) features = Table(dom, [[self.attr_x], [self.attr_y]]) features.name = "Features" self.send("Features", features) def commit(self): self.send_data() self.send_features() def closeEvent(self, ce): self.vizrank.close() super().closeEvent(ce) def hideEvent(self, he): self.vizrank.hide() super().hideEvent(he) def get_widget_name_extension(self): if self.data is not None: return "{} vs {}".format(self.combo_value(self.cb_attr_x), self.combo_value(self.cb_attr_y)) def send_report(self): disc_attr = False if self.data: domain = self.data.domain disc_attr = domain[self.attr_x].is_discrete or \ domain[self.attr_y].is_discrete caption = report.render_items_vert( (("Color", self.combo_value(self.cb_attr_color)), ("Label", self.combo_value(self.cb_attr_label)), ("Shape", self.combo_value(self.cb_attr_shape)), ("Size", self.combo_value(self.cb_attr_size)), ("Jittering", (self.graph.jitter_continuous or disc_attr) and self.graph.jitter_size))) self.report_plot() if caption: self.report_caption(caption) def onDeleteWidget(self): super().onDeleteWidget() self.graph.plot_widget.getViewBox().deleteLater() self.graph.plot_widget.clear() class VizRank(OWWidget): name = "Rank projections (Scatter Plot)" want_control_area = False def __init__(self, parent_widget): super().__init__() self.parent_widget = parent_widget self.running = False self.progress = None self.k = 10 self.projectionTable = QTableView() self.mainArea.layout().addWidget(self.projectionTable) self.projectionTable.setSelectionBehavior(QTableView.SelectRows) self.projectionTable.setSelectionMode(QTableView.SingleSelection) self.projectionTable.setSortingEnabled(True) self.projectionTableModel = QStandardItemModel(self) self.projectionTable.setModel(self.projectionTableModel) self.projectionTable.selectionModel().selectionChanged.connect( self.on_selection_changed) self.button = gui.button(self.mainArea, self, "Start evaluation", callback=self.toggle, default=True) self.resize(380, 512) self._initialize() def _initialize(self): self.running = False self.projectionTableModel.clear() self.projectionTableModel.setHorizontalHeaderLabels( ["Score", "Feature 1", "Feature 2"]) self.projectionTable.setColumnWidth(0, 60) self.projectionTable.setColumnWidth(1, 120) self.projectionTable.setColumnWidth(2, 120) self.button.setText("Start evaluation") self.button.setEnabled(False) self.pause = False self.data = None self.attrs = [] self.scores = [] self.i, self.j = 0, 0 if self.progress: self.progress.finish() self.progress = None self.information(0) if self.parent_widget.data: if not self.parent_widget.data.domain.class_var: self.information( 0, "Data with a class variable is required.") return if len(self.parent_widget.data.domain.attributes) < 2: self.information(0, 'At least 2 unique features are needed.') return if len(self.parent_widget.data) < 2: self.information(0, 'At least 2 instances are needed.') return self.button.setEnabled(True) def on_selection_changed(self, selected, deselected): """Called when the ranks view selection changes.""" a1 = selected.indexes()[1].data() a2 = selected.indexes()[2].data() self.parent_widget.update_attr(attributes=(a1, a2)) def toggle(self): self.running ^= 1 if self.running: self.button.setText("Pause") self.run() else: self.button.setText("Continue") self.button.setEnabled(False) def run(self): graph = self.parent_widget.graph y_full = self.parent_widget.data.Y if not self.attrs: self.attrs = self.score_heuristic() if not self.progress: self.progress = gui.ProgressBar( self, len(self.attrs) * (len(self.attrs) - 1) / 2) for i in range(self.i, len(self.attrs)): ind1 = graph.attribute_name_index[self.attrs[i]] for j in range(self.j, i): if not self.running: self.i, self.j = i, j if not self.projectionTable.selectedIndexes(): self.projectionTable.selectRow(0) self.button.setEnabled(True) return ind2 = graph.attribute_name_index[self.attrs[j]] X = graph.scaled_data[[ind1, ind2], :] valid = graph.get_valid_list([ind1, ind2]) X = X[:, valid].T if X.shape[0] < self.k: self.progress.advance() continue y = y_full[valid] n_neighbors = min(self.k, len(X) - 1) knn = NearestNeighbors(n_neighbors=n_neighbors).fit(X) ind = knn.kneighbors(return_distance=False) if self.parent_widget.data.domain.has_discrete_class: score = np.sum(y[ind] == y.reshape(-1, 1)) / ( len(y_full) * n_neighbors) else: score = r2_score(y, np.mean( y[ind], axis=1)) * (len(y) / len(y_full)) pos = bisect_left(self.scores, score) self.projectionTableModel.insertRow( len(self.scores) - pos, [ QStandardItem("{:.4f}".format(score)), QStandardItem(self.attrs[j]), QStandardItem(self.attrs[i]) ]) self.scores.insert(pos, score) self.progress.advance() self.j = 0 self.progress.finish() if not self.projectionTable.selectedIndexes(): self.projectionTable.selectRow(0) self.button.setText("Finished") self.button.setEnabled(False) def score_heuristic(self): X = self.parent_widget.graph.scaled_data.T Y = self.parent_widget.data.Y dom = Domain( [ContinuousVariable(str(i)) for i in range(X.shape[1])], self.parent_widget.data.domain.class_vars) data = Table(dom, X, Y) relief = ReliefF if isinstance(dom.class_var, DiscreteVariable) else RReliefF weights = relief(n_iterations=100, k_nearest=self.k)(data) attrs = sorted(zip( weights, (x.name for x in self.parent_widget.data.domain.attributes)), reverse=True) return [a for _, a in attrs]
class OWScatterPlot(OWWidget): """Scatterplot visualization with explorative analysis and intelligent data visualization enhancements.""" name = 'Scatter Plot' description = "Interactive scatter plot visualization with " \ "intelligent data visualization enhancements." icon = "icons/ScatterPlot.svg" priority = 140 class Inputs: data = Input("Data", Table, default=True) data_subset = Input("Data Subset", Table) features = Input("Features", AttributeList) class Outputs: selected_data = Output("Selected Data", Table, default=True) annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table) features = Output("Features", AttributeList, dynamic=False) settings_version = 2 settingsHandler = DomainContextHandler() auto_send_selection = Setting(True) auto_sample = Setting(True) toolbar_selection = Setting(0) attr_x = ContextSetting(None) attr_y = ContextSetting(None) selection_group = Setting(None, schema_only=True) graph = SettingProvider(OWScatterPlotGraph) jitter_sizes = [0, 0.1, 0.5, 1, 2, 3, 4, 5, 7, 10] graph_name = "graph.plot_widget.plotItem" class Information(OWWidget.Information): sampled_sql = Msg("Large SQL table; showing a sample.") def __init__(self): super().__init__() box = gui.vBox(self.mainArea, True, margin=0) self.graph = OWScatterPlotGraph(self, box, "ScatterPlot") box.layout().addWidget(self.graph.plot_widget) plot = self.graph.plot_widget axispen = QPen(self.palette().color(QPalette.Text)) axis = plot.getAxis("bottom") axis.setPen(axispen) axis = plot.getAxis("left") axis.setPen(axispen) self.data = None # Orange.data.Table self.subset_data = None # Orange.data.Table self.data_metas_X = None # self.data, where primitive metas are moved to X self.sql_data = None # Orange.data.sql.table.SqlTable self.attribute_selection_list = None # list of Orange.data.Variable self.__timer = QTimer(self, interval=1200) self.__timer.timeout.connect(self.add_data) common_options = dict(labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str) box = gui.vBox(self.controlArea, "Axis Data") dmod = DomainModel self.xy_model = DomainModel(dmod.MIXED, valid_types=dmod.PRIMITIVE) self.cb_attr_x = gui.comboBox(box, self, "attr_x", label="Axis x:", callback=self.update_attr, model=self.xy_model, **common_options) self.cb_attr_y = gui.comboBox(box, self, "attr_y", label="Axis y:", callback=self.update_attr, model=self.xy_model, **common_options) vizrank_box = gui.hBox(box) gui.separator(vizrank_box, width=common_options["labelWidth"]) self.vizrank, self.vizrank_button = ScatterPlotVizRank.add_vizrank( vizrank_box, self, "Find Informative Projections", self.set_attr) gui.separator(box) g = self.graph.gui g.add_widgets([g.JitterSizeSlider, g.JitterNumericValues], box) self.sampling = gui.auto_commit(self.controlArea, self, "auto_sample", "Sample", box="Sampling", callback=self.switch_sampling, commit=lambda: self.add_data(1)) self.sampling.setVisible(False) g.point_properties_box(self.controlArea) self.models = [self.xy_model] + g.points_models box_plot_prop = gui.vBox(self.controlArea, "Plot Properties") g.add_widgets([ g.ShowLegend, g.ShowGridLines, g.ToolTipShowsAll, g.ClassDensity, g.RegressionLine, g.LabelOnlySelected ], box_plot_prop) self.graph.box_zoom_select(self.controlArea) self.controlArea.layout().addStretch(100) self.icons = gui.attributeIconDict p = self.graph.plot_widget.palette() self.graph.set_palette(p) gui.auto_commit(self.controlArea, self, "auto_send_selection", "Send Selection", "Send Automatically") self.graph.zoom_actions(self) def keyPressEvent(self, event): super().keyPressEvent(event) self.graph.update_tooltip(event.modifiers()) def keyReleaseEvent(self, event): super().keyReleaseEvent(event) self.graph.update_tooltip(event.modifiers()) def reset_graph_data(self, *_): if self.data is not None: self.graph.rescale_data() self.update_graph() @Inputs.data def set_data(self, data): self.clear_messages() self.Information.sampled_sql.clear() self.__timer.stop() self.sampling.setVisible(False) self.sql_data = None if isinstance(data, SqlTable): if data.approx_len() < 4000: data = Table(data) else: self.Information.sampled_sql() self.sql_data = data data_sample = data.sample_time(0.8, no_cache=True) data_sample.download_data(2000, partial=True) data = Table(data_sample) self.sampling.setVisible(True) if self.auto_sample: self.__timer.start() if data is not None and (len(data) == 0 or len(data.domain) == 0): data = None if self.data and data and self.data.checksum() == data.checksum(): return self.closeContext() same_domain = (self.data and data and data.domain.checksum() == self.data.domain.checksum()) self.data = data self.data_metas_X = self.move_primitive_metas_to_X(data) if not same_domain: self.init_attr_values() self.vizrank.initialize() self.vizrank.attrs = self.data.domain.attributes if self.data is not None else [] self.vizrank_button.setEnabled( self.data is not None and not self.data.is_sparse() and self.data.domain.class_var is not None and len(self.data.domain.attributes) > 1 and len(self.data) > 1) if self.data is not None and self.data.domain.class_var is None \ and len(self.data.domain.attributes) > 1 and len(self.data) > 1: self.vizrank_button.setToolTip( "Data with a class variable is required.") else: self.vizrank_button.setToolTip("") self.openContext(self.data) def findvar(name, iterable): """Find a Orange.data.Variable in `iterable` by name""" for el in iterable: if isinstance(el, Orange.data.Variable) and el.name == name: return el return None # handle restored settings from < 3.3.9 when attr_* were stored # by name if isinstance(self.attr_x, str): self.attr_x = findvar(self.attr_x, self.xy_model) if isinstance(self.attr_y, str): self.attr_y = findvar(self.attr_y, self.xy_model) if isinstance(self.graph.attr_label, str): self.graph.attr_label = findvar(self.graph.attr_label, self.graph.gui.label_model) if isinstance(self.graph.attr_color, str): self.graph.attr_color = findvar(self.graph.attr_color, self.graph.gui.color_model) if isinstance(self.graph.attr_shape, str): self.graph.attr_shape = findvar(self.graph.attr_shape, self.graph.gui.shape_model) if isinstance(self.graph.attr_size, str): self.graph.attr_size = findvar(self.graph.attr_size, self.graph.gui.size_model) def add_data(self, time=0.4): if self.data and len(self.data) > 2000: return self.__timer.stop() data_sample = self.sql_data.sample_time(time, no_cache=True) if data_sample: data_sample.download_data(2000, partial=True) data = Table(data_sample) self.data = Table.concatenate((self.data, data), axis=0) self.data_metas_X = self.move_primitive_metas_to_X(self.data) self.handleNewSignals() def switch_sampling(self): self.__timer.stop() if self.auto_sample and self.sql_data: self.add_data() self.__timer.start() def move_primitive_metas_to_X(self, data): if data is not None: new_attrs = [ a for a in data.domain.attributes + data.domain.metas if a.is_primitive() ] new_metas = [m for m in data.domain.metas if not m.is_primitive()] new_domain = Domain(new_attrs, data.domain.class_vars, new_metas) data = data.transform(new_domain) return data @Inputs.data_subset def set_subset_data(self, subset_data): self.warning() if isinstance(subset_data, SqlTable): if subset_data.approx_len() < AUTO_DL_LIMIT: subset_data = Table(subset_data) else: self.warning("Data subset does not support large Sql tables") subset_data = None self.subset_data = self.move_primitive_metas_to_X(subset_data) self.controls.graph.alpha_value.setEnabled(subset_data is None) # called when all signals are received, so the graph is updated only once def handleNewSignals(self): self.graph.new_data(self.data_metas_X, self.subset_data) if self.attribute_selection_list and self.graph.domain and \ all(attr in self.graph.domain for attr in self.attribute_selection_list): self.attr_x = self.attribute_selection_list[0] self.attr_y = self.attribute_selection_list[1] self.attribute_selection_list = None self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.cb_reg_line.setEnabled(self.graph.can_draw_regresssion_line()) self.apply_selection() self.unconditional_commit() def apply_selection(self): """Apply selection saved in workflow.""" if self.data is not None and self.selection_group is not None: self.graph.selection = np.zeros(len(self.data), dtype=np.uint8) self.selection_group = [ x for x in self.selection_group if x[0] < len(self.data) ] selection_array = np.array(self.selection_group).T self.graph.selection[selection_array[0]] = selection_array[1] self.graph.update_colors(keep_colors=True) @Inputs.features def set_shown_attributes(self, attributes): if attributes and len(attributes) >= 2: self.attribute_selection_list = attributes[:2] else: self.attribute_selection_list = None def init_attr_values(self): domain = self.data and self.data.domain for model in self.models: model.set_domain(domain) self.attr_x = self.xy_model[0] if self.xy_model else None self.attr_y = self.xy_model[1] if len(self.xy_model) >= 2 \ else self.attr_x self.graph.attr_color = self.data.domain.class_var if domain else None self.graph.attr_shape = None self.graph.attr_size = None self.graph.attr_label = None def set_attr(self, attr_x, attr_y): self.attr_x, self.attr_y = attr_x, attr_y self.update_attr() def update_attr(self): self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.cb_reg_line.setEnabled(self.graph.can_draw_regresssion_line()) self.send_features() def update_colors(self): self.cb_class_density.setEnabled(self.graph.can_draw_density()) def update_density(self): self.update_graph(reset_view=False) def update_regression_line(self): self.update_graph(reset_view=False) def update_graph(self, reset_view=True, **_): self.graph.zoomStack = [] if self.graph.data is None: return self.graph.update_data(self.attr_x, self.attr_y, reset_view) def selection_changed(self): self.commit() def send_data(self): # TODO: Implement selection for sql data def _get_selected(): if not len(selection): return None return create_groups_table(data, graph.selection, False, "Group") def _get_annotated(): if graph.selection is not None and np.max(graph.selection) > 1: return create_groups_table(data, graph.selection) else: return create_annotated_table(data, selection) graph = self.graph data = self.data selection = graph.get_selection() self.Outputs.annotated_data.send(_get_annotated()) self.Outputs.selected_data.send(_get_selected()) # Store current selection in a setting that is stored in workflow if len(selection): self.selection_group = list( zip(selection, graph.selection[selection])) else: self.selection_group = None def send_features(self): features = [attr for attr in [self.attr_x, self.attr_y] if attr] self.Outputs.features.send(features or None) def commit(self): self.send_data() self.send_features() def get_widget_name_extension(self): if self.data is not None: return "{} vs {}".format(self.attr_x.name, self.attr_y.name) def send_report(self): if self.data is None: return def name(var): return var and var.name caption = report.render_items_vert( (("Color", name(self.graph.attr_color)), ("Label", name(self.graph.attr_label)), ("Shape", name(self.graph.attr_shape)), ("Size", name(self.graph.attr_size)), ("Jittering", (self.attr_x.is_discrete or self.attr_y.is_discrete or self.graph.jitter_continuous) and self.graph.jitter_size))) self.report_plot() if caption: self.report_caption(caption) def onDeleteWidget(self): super().onDeleteWidget() self.graph.plot_widget.getViewBox().deleteLater() self.graph.plot_widget.clear() @classmethod def migrate_settings(cls, settings, version): if version < 2 and "selection" in settings and settings["selection"]: settings["selection_group"] = [(a, 1) for a in settings["selection"]]
class OWScatterPlot(OWWidget): name = 'Scatter Plot' description = "Interactive scatter plot visualization with " \ "intelligent data visualization enhancements." icon = "icons/ScatterPlot.svg" priority = 210 inputs = [("Data", Table, "set_data", Default), ("Data Subset", Table, "set_subset_data"), ("Features", AttributeList, "set_shown_attributes")] outputs = [("Selected Data", Table, Default), ("Other Data", Table), ("Features", Table)] settingsHandler = DomainContextHandler() auto_send_selection = Setting(True) auto_sample = Setting(True) toolbar_selection = Setting(0) attr_x = ContextSetting("") attr_y = ContextSetting("") graph = SettingProvider(OWScatterPlotGraph) jitter_sizes = [0, 0.1, 0.5, 1, 2, 3, 4, 5, 7, 10] graph_name = "graph.plot_widget.plotItem" class Information(OWWidget.Information): sampled_sql = Msg("Large SQL table; showing a sample.") def __init__(self): super().__init__() box = gui.vBox(self.mainArea, True, margin=0) self.graph = OWScatterPlotGraph(self, box, "ScatterPlot") box.layout().addWidget(self.graph.plot_widget) plot = self.graph.plot_widget axispen = QtGui.QPen(self.palette().color(QtGui.QPalette.Text)) axis = plot.getAxis("bottom") axis.setPen(axispen) axis = plot.getAxis("left") axis.setPen(axispen) self.data = None # Orange.data.Table self.subset_data = None # Orange.data.Table self.data_metas_X = None # self.data, where primitive metas are moved to X self.sql_data = None # Orange.data.sql.table.SqlTable self.attribute_selection_list = None # list of Orange.data.Variable self.__timer = QTimer(self, interval=1200) self.__timer.timeout.connect(self.add_data) common_options = dict(labelWidth=50, orientation=Qt.Horizontal, sendSelectedValue=True, valueType=str) box = gui.vBox(self.controlArea, "Axis Data") self.cb_attr_x = gui.comboBox(box, self, "attr_x", label="Axis x:", callback=self.update_attr, **common_options) self.cb_attr_y = gui.comboBox(box, self, "attr_y", label="Axis y:", callback=self.update_attr, **common_options) self.vizrank = ScatterPlotVizRank(self) vizrank_box = gui.hBox(box) gui.separator(vizrank_box, width=common_options["labelWidth"]) self.vizrank_button_tooltip = "Find informative projections" self.vizrank_button = gui.button(vizrank_box, self, "Score Plots", callback=self.vizrank.reshow, tooltip=self.vizrank_button_tooltip, enabled=False) self.vizrank.pairSelected.connect(self.set_attr) gui.separator(box) gui.valueSlider(box, self, value='graph.jitter_size', label='Jittering: ', values=self.jitter_sizes, callback=self.reset_graph_data, labelFormat=lambda x: "None" if x == 0 else ("%.1f %%" if x < 1 else "%d %%") % x) gui.checkBox(gui.indentedBox(box), self, 'graph.jitter_continuous', 'Jitter continuous values', callback=self.reset_graph_data) self.sampling = gui.auto_commit(self.controlArea, self, "auto_sample", "Sample", box="Sampling", callback=self.switch_sampling, commit=lambda: self.add_data(1)) self.sampling.setVisible(False) box = gui.vBox(self.controlArea, "Points") self.cb_attr_color = gui.comboBox(box, self, "graph.attr_color", label="Color:", emptyString="(Same color)", callback=self.update_colors, **common_options) self.cb_attr_label = gui.comboBox(box, self, "graph.attr_label", label="Label:", emptyString="(No labels)", callback=self.graph.update_labels, **common_options) self.cb_attr_shape = gui.comboBox(box, self, "graph.attr_shape", label="Shape:", emptyString="(Same shape)", callback=self.graph.update_shapes, **common_options) self.cb_attr_size = gui.comboBox(box, self, "graph.attr_size", label="Size:", emptyString="(Same size)", callback=self.graph.update_sizes, **common_options) g = self.graph.gui box2 = g.point_properties_box(self.controlArea, box) box = gui.vBox(self.controlArea, "Plot Properties") g.add_widgets([g.ShowLegend, g.ShowGridLines], box) gui.checkBox(box, self, value='graph.tooltip_shows_all', label='Show all data on mouse hover') self.cb_class_density = gui.checkBox(box, self, value='graph.class_density', label='Show class density', callback=self.update_density) gui.checkBox(box, self, 'graph.label_only_selected', 'Label only selected points', callback=self.graph.update_labels) self.zoom_select_toolbar = g.zoom_select_toolbar( gui.vBox(self.controlArea, "Zoom/Select"), nomargin=True, buttons=[ g.StateButtonsBegin, g.SimpleSelect, g.Pan, g.Zoom, g.StateButtonsEnd, g.ZoomReset ]) buttons = self.zoom_select_toolbar.buttons buttons[g.Zoom].clicked.connect(self.graph.zoom_button_clicked) buttons[g.Pan].clicked.connect(self.graph.pan_button_clicked) buttons[g.SimpleSelect].clicked.connect( self.graph.select_button_clicked) buttons[g.ZoomReset].clicked.connect(self.graph.reset_button_clicked) self.controlArea.layout().addStretch(100) self.icons = gui.attributeIconDict p = self.graph.plot_widget.palette() self.graph.set_palette(p) gui.auto_commit(self.controlArea, self, "auto_send_selection", "Send Selection", "Send Automatically") def zoom(s): """Zoom in/out by factor `s`.""" viewbox = plot.getViewBox() # scaleBy scales the view's bounds (the axis range) viewbox.scaleBy((1 / s, 1 / s)) def fit_to_view(): viewbox = plot.getViewBox() viewbox.autoRange() zoom_in = QtGui.QAction("Zoom in", self, triggered=lambda: zoom(1.25)) zoom_in.setShortcuts([ QtGui.QKeySequence(QtGui.QKeySequence.ZoomIn), QtGui.QKeySequence(self.tr("Ctrl+=")) ]) zoom_out = QtGui.QAction("Zoom out", self, shortcut=QtGui.QKeySequence.ZoomOut, triggered=lambda: zoom(1 / 1.25)) zoom_fit = QtGui.QAction("Fit in view", self, shortcut=QtGui.QKeySequence(Qt.ControlModifier | Qt.Key_0), triggered=fit_to_view) self.addActions([zoom_in, zoom_out, zoom_fit]) # def settingsFromWidgetCallback(self, handler, context): # context.selectionPolygons = [] # for curve in self.graph.selectionCurveList: # xs = [curve.x(i) for i in range(curve.dataSize())] # ys = [curve.y(i) for i in range(curve.dataSize())] # context.selectionPolygons.append((xs, ys)) # def settingsToWidgetCallback(self, handler, context): # selections = getattr(context, "selectionPolygons", []) # for (xs, ys) in selections: # c = SelectionCurve("") # c.setData(xs,ys) # c.attach(self.graph) # self.graph.selectionCurveList.append(c) def reset_graph_data(self, *_): self.graph.rescale_data() self.update_graph() def set_data(self, data): self.Information.sampled_sql.clear() self.__timer.stop() self.sampling.setVisible(False) self.sql_data = None if isinstance(data, SqlTable): if data.approx_len() < 4000: data = Table(data) else: self.Information.sampled_sql() self.sql_data = data data_sample = data.sample_time(0.8, no_cache=True) data_sample.download_data(2000, partial=True) data = Table(data_sample) self.sampling.setVisible(True) if self.auto_sample: self.__timer.start() if data is not None and (len(data) == 0 or len(data.domain) == 0): data = None if self.data and data and self.data.checksum() == data.checksum(): return self.closeContext() same_domain = (self.data and data and data.domain.checksum() == self.data.domain.checksum()) self.data = data self.data_metas_X = self.move_primitive_metas_to_X(data) if not same_domain: self.init_attr_values() self.vizrank.initialize() self.vizrank_button.setEnabled( self.data is not None and self.data.domain.class_var is not None and len(self.data.domain.attributes) > 1 and len(self.data) > 1) if self.data is not None and self.data.domain.class_var is None \ and len(self.data.domain.attributes) > 1 and len(self.data) > 1: self.vizrank_button.setToolTip( "Data with a class variable is required.") else: self.vizrank_button.setToolTip(self.vizrank_button_tooltip) self.openContext(self.data) def add_data(self, time=0.4): if self.data and len(self.data) > 2000: return self.__timer.stop() data_sample = self.sql_data.sample_time(time, no_cache=True) if data_sample: data_sample.download_data(2000, partial=True) data = Table(data_sample) self.data = Table.concatenate((self.data, data), axis=0) self.data_metas_X = self.move_primitive_metas_to_X(self.data) self.handleNewSignals() def switch_sampling(self): self.__timer.stop() if self.auto_sample and self.sql_data: self.add_data() self.__timer.start() def move_primitive_metas_to_X(self, data): if data is not None: new_attrs = [ a for a in data.domain.attributes + data.domain.metas if a.is_primitive() ] new_metas = [m for m in data.domain.metas if not m.is_primitive()] data = Table.from_table( Domain(new_attrs, data.domain.class_vars, new_metas), data) return data def set_subset_data(self, subset_data): self.warning() if isinstance(subset_data, SqlTable): if subset_data.approx_len() < AUTO_DL_LIMIT: subset_data = Table(subset_data) else: self.warning("Data subset does not support large Sql tables") subset_data = None self.subset_data = self.move_primitive_metas_to_X(subset_data) # called when all signals are received, so the graph is updated only once def handleNewSignals(self): self.graph.new_data(self.data_metas_X, self.subset_data) if self.attribute_selection_list and \ all(attr in self.graph.data_domain for attr in self.attribute_selection_list): self.attr_x = self.attribute_selection_list[0].name self.attr_y = self.attribute_selection_list[1].name self.attribute_selection_list = None self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.unconditional_commit() def set_shown_attributes(self, attributes): if attributes and len(attributes) >= 2: self.attribute_selection_list = attributes[:2] else: self.attribute_selection_list = None def get_shown_attributes(self): return self.attr_x, self.attr_y def init_attr_values(self): self.cb_attr_x.clear() self.attr_x = None self.cb_attr_y.clear() self.attr_y = None self.cb_attr_color.clear() self.cb_attr_color.addItem("(Same color)") self.graph.attr_color = None self.cb_attr_label.clear() self.cb_attr_label.addItem("(No labels)") self.graph.attr_label = None self.cb_attr_shape.clear() self.cb_attr_shape.addItem("(Same shape)") self.graph.attr_shape = None self.cb_attr_size.clear() self.cb_attr_size.addItem("(Same size)") self.graph.attr_size = None if not self.data: return for var in self.data.domain.metas: if not var.is_primitive(): self.cb_attr_label.addItem(self.icons[var], var.name) for attr in self.data.domain.variables: self.cb_attr_x.addItem(self.icons[attr], attr.name) self.cb_attr_y.addItem(self.icons[attr], attr.name) self.cb_attr_color.addItem(self.icons[attr], attr.name) if attr.is_discrete: self.cb_attr_shape.addItem(self.icons[attr], attr.name) else: self.cb_attr_size.addItem(self.icons[attr], attr.name) self.cb_attr_label.addItem(self.icons[attr], attr.name) for var in self.data.domain.metas: if var.is_primitive(): self.cb_attr_x.addItem(self.icons[var], var.name) self.cb_attr_y.addItem(self.icons[var], var.name) self.cb_attr_color.addItem(self.icons[var], var.name) if var.is_discrete: self.cb_attr_shape.addItem(self.icons[var], var.name) else: self.cb_attr_size.addItem(self.icons[var], var.name) self.cb_attr_label.addItem(self.icons[var], var.name) self.attr_x = self.cb_attr_x.itemText(0) if self.cb_attr_y.count() > 1: self.attr_y = self.cb_attr_y.itemText(1) else: self.attr_y = self.cb_attr_y.itemText(0) if self.data.domain.class_var: self.graph.attr_color = self.data.domain.class_var.name else: self.graph.attr_color = "" self.graph.attr_shape = "" self.graph.attr_size = "" self.graph.attr_label = "" def set_attr(self, attr_x, attr_y): self.attr_x, self.attr_y = attr_x.name, attr_y.name self.update_attr() def update_attr(self): self.update_graph() self.cb_class_density.setEnabled(self.graph.can_draw_density()) self.send_features() def update_colors(self): self.graph.update_colors() self.cb_class_density.setEnabled(self.graph.can_draw_density()) def update_density(self): self.update_graph(reset_view=False) def update_graph(self, reset_view=True, **_): self.graph.zoomStack = [] if not self.graph.have_data: return self.graph.update_data(self.attr_x, self.attr_y, reset_view) def selection_changed(self): self.send_data() def send_data(self): selected = unselected = None # TODO: Implement selection for sql data if isinstance(self.data, SqlTable): selected = unselected = self.data elif self.data is not None: selection = self.graph.get_selection() if len(selection) == 0: self.send("Selected Data", None) self.send("Other Data", self.data) return selected = self.data[selection] unselection = np.full(len(self.data), True, dtype=bool) unselection[selection] = False unselected = self.data[unselection] self.send("Selected Data", selected) if unselected is None or len(unselected) == 0: self.send("Other Data", None) else: self.send("Other Data", unselected) def send_features(self): features = None if self.attr_x or self.attr_y: dom = Domain([], metas=(StringVariable(name="feature"), )) features = Table(dom, [[self.attr_x], [self.attr_y]]) features.name = "Features" self.send("Features", features) def commit(self): self.send_data() self.send_features() def closeEvent(self, ce): self.vizrank.close() super().closeEvent(ce) def hideEvent(self, he): self.vizrank.hide() super().hideEvent(he) def get_widget_name_extension(self): if self.data is not None: return "{} vs {}".format(self.combo_value(self.cb_attr_x), self.combo_value(self.cb_attr_y)) def send_report(self): disc_attr = False if self.data: domain = self.data.domain disc_attr = domain[self.attr_x].is_discrete or \ domain[self.attr_y].is_discrete caption = report.render_items_vert( (("Color", self.combo_value(self.cb_attr_color)), ("Label", self.combo_value(self.cb_attr_label)), ("Shape", self.combo_value(self.cb_attr_shape)), ("Size", self.combo_value(self.cb_attr_size)), ("Jittering", (self.graph.jitter_continuous or disc_attr) and self.graph.jitter_size))) self.report_plot() if caption: self.report_caption(caption) def onDeleteWidget(self): super().onDeleteWidget() self.graph.plot_widget.getViewBox().deleteLater() self.graph.plot_widget.clear()
class OWScatterPlot(OWWidget): name = 'Scatter Plot' description = 'Scatter plot visualization.' icon = "icons/ScatterPlot.svg" inputs = [("Data", Table, "set_data", Default), ("Data Subset", Table, "set_subset_data"), ("Features", AttributeList, "set_shown_attributes")] outputs = [("Selected Data", Table), ("Other Data", Table)] settingsHandler = DomainContextHandler() auto_send_selection = Setting(True) toolbar_selection = Setting(0) color_settings = Setting(None) selected_schema_index = Setting(0) attr_x = ContextSetting("") attr_y = ContextSetting("") graph = SettingProvider(OWScatterPlotGraph) zoom_select_toolbar = SettingProvider(ZoomSelectToolbar) jitter_sizes = [0, 0.1, 0.5, 1, 2, 3, 4, 5, 7, 10] def __init__(self): super().__init__() box = gui.widgetBox(self.mainArea, True, margin=0) self.graph = OWScatterPlotGraph(self, box, "ScatterPlot") box.layout().addWidget(self.graph.plot_widget) plot = self.graph.plot_widget axisfont = font_resize(self.font(), 0.8, minsize=11) axispen = QtGui.QPen(self.palette().color(QtGui.QPalette.Text)) axis = plot.getAxis("bottom") axis.setPen(axispen) axis = plot.getAxis("left") axis.setPen(axispen) self.data = None # Orange.data.Table self.subset_data = None # Orange.data.Table self.attribute_selection_list = None # list of Orange.data.Variable common_options = {"labelWidth": 50, "orientation": "horizontal", "sendSelectedValue": True, "valueType": str} box = gui.widgetBox(self.controlArea, "Axis Data") self.cb_attr_x = gui.comboBox(box, self, "attr_x", label="Axis x:", callback=self.major_graph_update, **common_options) self.cb_attr_y = gui.comboBox(box, self, "attr_y", label="Axis y:", callback=self.major_graph_update, **common_options) gui.valueSlider( box, self, value='graph.jitter_size', label='Jittering: ', values=self.jitter_sizes, callback=self.reset_graph_data, labelFormat=lambda x: "None" if x == 0 else ("%.1f %%" if x < 1 else "%d %%") % x) gui.checkBox( gui.indentedBox(box), self, 'graph.jitter_continuous', 'Jitter continuous values', callback=self.reset_graph_data) box = gui.widgetBox(self.controlArea, "Points") self.cb_attr_color = gui.comboBox( box, self, "graph.attr_color", label="Color:", emptyString="(Same color)", callback=self.graph.update_colors, **common_options) self.cb_attr_label = gui.comboBox( box, self, "graph.attr_label", label="Label:", emptyString="(No labels)", callback=self.graph.update_labels, **common_options) self.cb_attr_shape = gui.comboBox( box, self, "graph.attr_shape", label="Shape:", emptyString="(Same shape)", callback=self.graph.update_shapes, **common_options) self.cb_attr_size = gui.comboBox( box, self, "graph.attr_size", label="Size:", emptyString="(Same size)", callback=self.graph.update_sizes, **common_options) g = self.graph.gui box2 = g.point_properties_box(self.controlArea, box) gui.button(box2, self, "Set Colors", self.set_colors) box = gui.widgetBox(self.controlArea, "Plot Properties") g.add_widgets([g.ShowLegend, g.ShowGridLines], box) gui.checkBox(box, self, value='graph.tooltip_shows_all', label='Show all data on mouse hover') gui.separator(self.controlArea, 8, 8) self.zoom_select_toolbar = g.zoom_select_toolbar( self.controlArea, nomargin=True, buttons=[g.StateButtonsBegin, g.SimpleSelect, g.Pan, g.Zoom, g.StateButtonsEnd, g.ZoomReset] ) buttons = self.zoom_select_toolbar.buttons buttons[g.Zoom].clicked.connect(self.graph.zoom_button_clicked) buttons[g.Pan].clicked.connect(self.graph.pan_button_clicked) buttons[g.SimpleSelect].clicked.connect(self.graph.select_button_clicked) buttons[g.ZoomReset].clicked.connect(self.graph.reset_button_clicked) self.controlArea.layout().addStretch(100) self.icons = gui.attributeIconDict dlg = self.create_color_dialog() self.graph.continuous_palette = dlg.getContinuousPalette("contPalette") self.graph.discrete_palette = dlg.getDiscretePalette("discPalette") dlg.deleteLater() p = self.graph.plot_widget.palette() self.graph.set_palette(p) gui.auto_commit(self.controlArea, self, "auto_send_selection", "Send Selection") def zoom(s): """Zoom in/out by factor `s`.""" viewbox = plot.getViewBox() # scaleBy scales the view's bounds (the axis range) viewbox.scaleBy((1 / s, 1 / s)) def fit_to_view(): viewbox = plot.getViewBox() viewbox.autoRange() zoom_in = QtGui.QAction( "Zoom in", self, triggered=lambda: zoom(1.25) ) zoom_in.setShortcuts([QtGui.QKeySequence(QtGui.QKeySequence.ZoomIn), QtGui.QKeySequence(self.tr("Ctrl+="))]) zoom_out = QtGui.QAction( "Zoom out", self, shortcut=QtGui.QKeySequence.ZoomOut, triggered=lambda: zoom(1 / 1.25) ) zoom_fit = QtGui.QAction( "Fit in view", self, shortcut=QtGui.QKeySequence(Qt.ControlModifier | Qt.Key_0), triggered=fit_to_view ) self.addActions([zoom_in, zoom_out, zoom_fit]) # self.vizrank = OWVizRank(self, self.signalManager, self.graph, # orngVizRank.SCATTERPLOT, "ScatterPlot") # self.optimizationDlg = self.vizrank # def settingsFromWidgetCallback(self, handler, context): # context.selectionPolygons = [] # for curve in self.graph.selectionCurveList: # xs = [curve.x(i) for i in range(curve.dataSize())] # ys = [curve.y(i) for i in range(curve.dataSize())] # context.selectionPolygons.append((xs, ys)) # def settingsToWidgetCallback(self, handler, context): # selections = getattr(context, "selectionPolygons", []) # for (xs, ys) in selections: # c = SelectionCurve("") # c.setData(xs,ys) # c.attach(self.graph) # self.graph.selectionCurveList.append(c) def reset_graph_data(self, *_): self.graph.rescale_data() self.major_graph_update() def set_data(self, data): if type(data) == SqlTable and data.approx_len() > LARGE_TABLE: data = data.sample_time(DEFAULT_SAMPLE_TIME) if data is not None and (len(data) == 0 or len(data.domain) == 0): data = None if self.data and data and self.data.checksum() == data.checksum(): return self.closeContext() same_domain = \ self.data and data and \ data.domain.checksum() == self.data.domain.checksum() self.data = data # TODO: adapt scatter plot to work on SqlTables (avoid use of X and Y) if isinstance(self.data, SqlTable): self.data.download_data() # self.vizrank.clearResults() if not same_domain: self.init_attr_values() self.openContext(self.data) def set_subset_data(self, subset_data): self.subset_data = subset_data # self.vizrank.clearArguments() # called when all signals are received, so the graph is updated only once def handleNewSignals(self): self.graph.new_data(self.data, self.subset_data) # self.vizrank.resetDialog() if self.attribute_selection_list and \ all(attr.name in self.graph.attribute_name_index for attr in self.attribute_selection_list): self.attr_x = self.attribute_selection_list[0].name self.attr_y = self.attribute_selection_list[1].name self.attribute_selection_list = None self.update_graph() self.unconditional_commit() def set_shown_attributes(self, attributes): if attributes and len(attributes) >= 2: self.attribute_selection_list = attributes[:2] else: self.attribute_selection_list = None # Callback from VizRank dialog def show_selected_attributes(self): val = self.vizrank.get_selected_projection() if not val: return if self.data.domain.class_var: self.graph.attr_color = self.data.domain.class_var.name self.major_graph_update(val[3]) def get_shown_attributes(self): return self.attr_x, self.attr_y def init_attr_values(self): self.cb_attr_x.clear() self.cb_attr_y.clear() self.cb_attr_color.clear() self.cb_attr_color.addItem("(Same color)") self.cb_attr_label.clear() self.cb_attr_label.addItem("(No labels)") self.cb_attr_shape.clear() self.cb_attr_shape.addItem("(Same shape)") self.cb_attr_size.clear() self.cb_attr_size.addItem("(Same size)") if not self.data: return for var in self.data.domain.metas: self.cb_attr_label.addItem(self.icons[var], var.name) for attr in self.data.domain.variables: self.cb_attr_x.addItem(self.icons[attr], attr.name) self.cb_attr_y.addItem(self.icons[attr], attr.name) self.cb_attr_color.addItem(self.icons[attr], attr.name) if attr.is_discrete: self.cb_attr_shape.addItem(self.icons[attr], attr.name) else: self.cb_attr_size.addItem(self.icons[attr], attr.name) self.cb_attr_label.addItem(self.icons[attr], attr.name) self.attr_x = self.cb_attr_x.itemText(0) if self.cb_attr_y.count() > 1: self.attr_y = self.cb_attr_y.itemText(1) else: self.attr_y = self.cb_attr_y.itemText(0) if self.data.domain.class_var: self.graph.attr_color = self.data.domain.class_var.name else: self.graph.attr_color = "" self.graph.attr_shape = "" self.graph.attr_size = "" self.graph.attr_label = "" def major_graph_update(self, attributes=None, inside_colors=None, **args): self.update_graph(attributes, inside_colors, **args) def update_graph(self, attributes=None, inside_colors=None, **_): self.graph.zoomStack = [] if not self.graph.have_data: return if attributes and len(attributes) == 2: self.attr_x, self.attr_y = attributes self.graph.update_data(self.attr_x, self.attr_y) def saveSettings(self): OWWidget.saveSettings(self) # self.vizrank.saveSettings() def selection_changed(self): self.commit() def commit(self): selected = unselected = None # TODO: Implement selection for sql data if isinstance(self.data, SqlTable): selected = unselected = self.data elif self.data is not None: selection = self.graph.get_selection() selected = self.data[selection] unselection = np.full(len(self.data), True, dtype=bool) unselection[selection] = False unselected = self.data[unselection] self.send("Selected Data", selected) self.send("Other Data", unselected) def set_colors(self): dlg = self.create_color_dialog() if dlg.exec_(): self.color_settings = dlg.getColorSchemas() self.selected_schema_index = dlg.selectedSchemaIndex self.graph.continuous_palette = dlg.getContinuousPalette("contPalette") self.graph.discrete_palette = dlg.getDiscretePalette("discPalette") self.update_graph() def create_color_dialog(self): c = ColorPaletteDlg(self, "Color Palette") c.createDiscretePalette("discPalette", "Discrete Palette") c.createContinuousPalette("contPalette", "Continuous Palette") c.setColorSchemas(self.color_settings, self.selected_schema_index) return c def closeEvent(self, ce): # self.vizrank.close() super().closeEvent(ce) def sendReport(self): self.startReport( "%s [%s - %s]" % (self.windowTitle(), self.attr_x, self.attr_y)) self.reportSettings( "Visualized attributes", [("X", self.attr_x), ("Y", self.attr_y), self.graph.attr_color and ("Color", self.graph.attr_color), self.graph.attr_label and ("Label", self.graph.attr_label), self.graph.attr_shape and ("Shape", self.graph.attr_shape), self.graph.attr_size and ("Size", self.graph.attr_size)]) self.reportSettings( "Settings", [("Symbol size", self.graph.point_width), ("Opacity", self.graph.alpha_value), ("Jittering", self.graph.jitter_size), ("Jitter continuous attributes", gui.YesNo[self.graph.jitter_continuous])]) self.reportSection("Graph") self.reportImage(self.graph.save_to_file, QSize(400, 400))
class OWScatterPlot(OWWidget): name = 'Scatter plot' description = 'Scatter plot visualization' inputs = [("Data", Table, "set_data", Default), ("Data Subset", Table, "set_subset_data"), ("Features", AttributeList, "set_shown_attributes")] outputs = [("Selected Data", Table), ("Other Data", Table)] settingsHandler = DomainContextHandler() auto_send_selection = Setting(True) toolbar_selection = Setting(0) color_settings = Setting(None) selected_schema_index = Setting(0) attr_x = ContextSetting("") attr_y = ContextSetting("") graph = SettingProvider(OWScatterPlotGraph) zoom_select_toolbar = SettingProvider(ZoomSelectToolbar) jitter_sizes = [0, 0.1, 0.5, 1, 2, 3, 4, 5, 7, 10] def __init__(self): super().__init__() box = gui.widgetBox(self.mainArea, True, margin=0) self.graph = OWScatterPlotGraph(self, box, "ScatterPlot") box.layout().addWidget(self.graph.plot_widget) self.data = None # Orange.data.Table self.subset_data = None # Orange.data.Table self.attribute_selection_list = None # list of Orange.data.Variable self.selection_dirty = False common_options = {"labelWidth": 50, "orientation": "horizontal", "sendSelectedValue": True, "valueType": str} box = gui.widgetBox(self.controlArea, "Axis Data") self.cb_attr_x = gui.comboBox(box, self, "attr_x", label="Axis x:", callback=self.major_graph_update, **common_options) self.cb_attr_y = gui.comboBox(box, self, "attr_y", label="Axis y:", callback=self.major_graph_update, **common_options) gui.valueSlider( box, self, value='graph.jitter_size', label='Jittering: ', values=self.jitter_sizes, callback=self.reset_graph_data, labelFormat=lambda x: "None" if x == 0 else ("%.1f %%" if x < 1 else "%d %%") % x) gui.checkBox( gui.indentedBox(box), self, 'graph.jitter_continuous', 'Jitter continuous values', callback=self.reset_graph_data) box = gui.widgetBox(self.controlArea, "Points") self.cb_attr_color = gui.comboBox( box, self, "graph.attr_color", label="Color:", emptyString="(Same color)", callback=self.graph.update_colors, **common_options) self.cb_attr_label = gui.comboBox( box, self, "graph.attr_label", label="Label:", emptyString="(No labels)", callback=self.graph.update_labels, **common_options) self.cb_attr_shape = gui.comboBox( box, self, "graph.attr_shape", label="Shape:", emptyString="(Same shape)", callback=self.graph.update_shapes, **common_options) self.cb_attr_size = gui.comboBox( box, self, "graph.attr_size", label="Size:", emptyString="(Same size)", callback=self.graph.update_sizes, **common_options) g = self.graph.gui box2 = g.point_properties_box(self.controlArea, box) gui.button(box2, self, "Set Colors", self.set_colors) box = gui.widgetBox(self.controlArea, "Plot Properties") g.add_widgets([g.ShowLegend, g.ShowGridLines], box) gui.checkBox(box, self, value='graph.tooltip_shows_all', label='Show all data on mouse hover') gui.separator(self.controlArea, 8, 8) self.zoom_select_toolbar = g.zoom_select_toolbar( self.controlArea, nomargin=True, buttons=[g.StateButtonsBegin, g.SimpleSelect, g.Pan, g.Zoom, g.StateButtonsEnd, g.ZoomReset, g.Spacing, g.SendSelection] ) buttons = self.zoom_select_toolbar.buttons buttons[g.SendSelection].clicked.connect(self.send_selection) buttons[g.Zoom].clicked.connect(self.graph.zoom_button_clicked) buttons[g.Pan].clicked.connect(self.graph.pan_button_clicked) buttons[g.SimpleSelect].clicked.connect(self.graph.select_button_clicked) buttons[g.ZoomReset].clicked.connect(self.graph.reset_button_clicked) cb_auto_send = gui.checkBox( box, self, 'auto_send_selection', 'Send selection on change') gui.setStopper(self, buttons[g.SendSelection], cb_auto_send, "selection_dirty", self.send_selection) self.controlArea.layout().addStretch(100) self.icons = gui.attributeIconDict dlg = self.create_color_dialog() self.graph.continuous_palette = dlg.getContinuousPalette("contPalette") self.graph.discrete_palette = dlg.getDiscretePalette("discPalette") p = self.graph.plot_widget.palette() self.graph.set_palette(p) self.zoom_select_toolbar.buttons[OWPlotGUI.SendSelection].setEnabled( not self.auto_send_selection) self.mainArea.setMinimumWidth(700) self.mainArea.setMinimumHeight(550) # self.vizrank = OWVizRank(self, self.signalManager, self.graph, # orngVizRank.SCATTERPLOT, "ScatterPlot") # self.optimizationDlg = self.vizrank # def settingsFromWidgetCallback(self, handler, context): # context.selectionPolygons = [] # for curve in self.graph.selectionCurveList: # xs = [curve.x(i) for i in range(curve.dataSize())] # ys = [curve.y(i) for i in range(curve.dataSize())] # context.selectionPolygons.append((xs, ys)) # def settingsToWidgetCallback(self, handler, context): # selections = getattr(context, "selectionPolygons", []) # for (xs, ys) in selections: # c = SelectionCurve("") # c.setData(xs,ys) # c.attach(self.graph) # self.graph.selectionCurveList.append(c) def reset_graph_data(self, *_): self.graph.rescale_data() self.major_graph_update() def set_data(self, data: Orange.data.Table): if data is not None and (len(data) == 0 or len(data.domain) == 0): data = None if self.data and data and self.data.checksum() == data.checksum(): return self.closeContext() same_domain = \ self.data and data and \ data.domain.checksum() == self.data.domain.checksum() self.data = data # TODO: adapt scatter plot to work on SqlTables (avoid use of X and Y) if isinstance(self.data, SqlTable): self.data.X = np.empty((len(self.data), len(self.data.domain.attributes))) self.data.Y = np.empty((len(self.data), len(self.data.domain.class_vars))) for i, row in enumerate(data): self.data.X[i] = [row[attr] for attr in self.data.domain.attributes] if self.data.domain.class_vars: self.data.Y[i] = [row[cv] for cv in self.data.domain.class_vars] # self.vizrank.clearResults() if not same_domain: self.init_attr_values() self.openContext(self.data) def set_subset_data(self, subset_data): self.subset_data = subset_data # self.vizrank.clearArguments() # called when all signals are received, so the graph is updated only once def handleNewSignals(self): self.graph.set_data(self.data, self.subset_data) # self.vizrank.resetDialog() if self.attribute_selection_list and \ all(attr in self.graph.attribute_name_index for attr in self.attribute_selection_list): self.attr_x = self.attribute_selection_list[0] self.attr_y = self.attribute_selection_list[1] self.attribute_selection_list = None self.update_graph() self.send_selection() def set_shown_attributes(self, attributes): if attributes and len(attributes) >= 2: self.attribute_selection_list = attributes[:2] else: self.attribute_selection_list = None # Callback from VizRank dialog def show_selected_attributes(self): val = self.vizrank.get_selected_projection() if not val: return if self.data.domain.class_var: self.graph.attr_color = self.data.domain.class_var.name self.major_graph_update(val[3]) def get_shown_attributes(self): return self.attr_x, self.attr_y def init_attr_values(self): self.cb_attr_x.clear() self.cb_attr_y.clear() self.cb_attr_color.clear() self.cb_attr_color.addItem("(Same color)") self.cb_attr_label.clear() self.cb_attr_label.addItem("(No labels)") self.cb_attr_shape.clear() self.cb_attr_shape.addItem("(Same shape)") self.cb_attr_size.clear() self.cb_attr_size.addItem("(Same size)") if not self.data: return for var in self.data.domain.metas: self.cb_attr_label.addItem(self.icons[var], var.name) for attr in self.data.domain.variables: self.cb_attr_x.addItem(self.icons[attr], attr.name) self.cb_attr_y.addItem(self.icons[attr], attr.name) self.cb_attr_color.addItem(self.icons[attr], attr.name) if isinstance(attr, DiscreteVariable): self.cb_attr_shape.addItem(self.icons[attr], attr.name) else: self.cb_attr_size.addItem(self.icons[attr], attr.name) self.cb_attr_label.addItem(self.icons[attr], attr.name) self.attr_x = self.cb_attr_x.itemText(0) if self.cb_attr_y.count() > 1: self.attr_y = self.cb_attr_y.itemText(1) else: self.attr_y = self.cb_attr_y.itemText(0) if self.data.domain.class_var: self.graph.attr_color = self.data.domain.class_var.name else: self.graph.attr_color = "" self.graph.attr_shape = "" self.graph.attr_size = "" self.graph.attr_label = "" def major_graph_update(self, attributes=None, inside_colors=None, **args): self.update_graph(attributes, inside_colors, **args) def update_graph(self, attributes=None, inside_colors=None, **_): self.graph.zoomStack = [] if not self.graph.have_data: return if attributes and len(attributes) == 2: self.attr_x, self.attr_y = attributes self.graph.update_data(self.attr_x, self.attr_y) def saveSettings(self): OWWidget.saveSettings(self) # self.vizrank.saveSettings() """ def auto_selection_changed(self): self.zoom_select_toolbar.buttons[OWPlotGUI.SendSelection].setEnabled( not self.auto_send_selection) if self.auto_send_selection: self.send_selection() """ def selection_changed(self): if self.auto_send_selection: self.send_selection() else: self.selection_dirty = True def send_selection(self): self.selection_dirty = False selection = self.graph.get_selection() selected = self.data[selection] unselection = np.full(len(self.data), True, dtype=bool) unselection[selection] = False unselected = self.data[unselection] self.send("Selected Data", selected) self.send("Other Data", unselected) def set_colors(self): dlg = self.create_color_dialog() if dlg.exec_(): self.color_settings = dlg.getColorSchemas() self.selected_schema_index = dlg.selectedSchemaIndex self.graph.continuous_palette = dlg.getContinuousPalette("contPalette") self.graph.discrete_palette = dlg.getDiscretePalette("discPalette") self.update_graph() def create_color_dialog(self): c = ColorPaletteDlg(self, "Color Palette") c.createDiscretePalette("discPalette", "Discrete Palette") c.createContinuousPalette("contPalette", "Continuous Palette") c.setColorSchemas(self.color_settings, self.selected_schema_index) return c def closeEvent(self, ce): # self.vizrank.close() super().closeEvent(ce) def sendReport(self): self.startReport( "%s [%s - %s]" % (self.windowTitle(), self.attr_x, self.attr_y)) self.reportSettings( "Visualized attributes", [("X", self.attr_x), ("Y", self.attr_y), self.graph.attr_color and ("Color", self.graph.attr_color), self.graph.attr_label and ("Label", self.graph.attr_label), self.graph.attr_shape and ("Shape", self.graph.attr_shape), self.graph.attr_size and ("Size", self.graph.attr_size)]) self.reportSettings( "Settings", [("Symbol size", self.graph.point_width), ("Opacity", self.graph.alpha_value), ("Jittering", self.graph.jitter_size), ("Jitter continuous attributes", gui.YesNo[self.graph.jitter_continuous])]) self.reportSection("Graph") self.reportImage(self.graph.save_to_file, QSize(400, 400))
class OWScatterPlot(OWWidget): name = 'Scatter plot' description = 'Scatter plot visualization' icon = "icons/ScatterPlot.svg" inputs = [("Data", Table, "set_data", Default), ("Data Subset", Table, "set_subset_data"), ("Features", AttributeList, "set_shown_attributes")] outputs = [("Selected Data", Table), ("Other Data", Table)] settingsHandler = DomainContextHandler() auto_send_selection = Setting(True) toolbar_selection = Setting(0) color_settings = Setting(None) selected_schema_index = Setting(0) attr_x = ContextSetting("") attr_y = ContextSetting("") graph = SettingProvider(OWScatterPlotGraph) zoom_select_toolbar = SettingProvider(ZoomSelectToolbar) jitter_sizes = [0, 0.1, 0.5, 1, 2, 3, 4, 5, 7, 10] def __init__(self): super().__init__() box = gui.widgetBox(self.mainArea, True, margin=0) self.graph = OWScatterPlotGraph(self, box, "ScatterPlot") box.layout().addWidget(self.graph.plot_widget) plot = self.graph.plot_widget axisfont = font_resize(self.font(), 0.8, minsize=11) axispen = QtGui.QPen(self.palette().color(QtGui.QPalette.Text)) axis = plot.getAxis("bottom") axis.setTickFont(axisfont) axis.setPen(axispen) axis = plot.getAxis("left") axis.setTickFont(axisfont) axis.setPen(axispen) self.data = None # Orange.data.Table self.subset_data = None # Orange.data.Table self.attribute_selection_list = None # list of Orange.data.Variable common_options = { "labelWidth": 50, "orientation": "horizontal", "sendSelectedValue": True, "valueType": str } box = gui.widgetBox(self.controlArea, "Axis Data") self.cb_attr_x = gui.comboBox(box, self, "attr_x", label="Axis x:", callback=self.major_graph_update, **common_options) self.cb_attr_y = gui.comboBox(box, self, "attr_y", label="Axis y:", callback=self.major_graph_update, **common_options) gui.valueSlider(box, self, value='graph.jitter_size', label='Jittering: ', values=self.jitter_sizes, callback=self.reset_graph_data, labelFormat=lambda x: "None" if x == 0 else ("%.1f %%" if x < 1 else "%d %%") % x) gui.checkBox(gui.indentedBox(box), self, 'graph.jitter_continuous', 'Jitter continuous values', callback=self.reset_graph_data) box = gui.widgetBox(self.controlArea, "Points") self.cb_attr_color = gui.comboBox(box, self, "graph.attr_color", label="Color:", emptyString="(Same color)", callback=self.graph.update_colors, **common_options) self.cb_attr_label = gui.comboBox(box, self, "graph.attr_label", label="Label:", emptyString="(No labels)", callback=self.graph.update_labels, **common_options) self.cb_attr_shape = gui.comboBox(box, self, "graph.attr_shape", label="Shape:", emptyString="(Same shape)", callback=self.graph.update_shapes, **common_options) self.cb_attr_size = gui.comboBox(box, self, "graph.attr_size", label="Size:", emptyString="(Same size)", callback=self.graph.update_sizes, **common_options) g = self.graph.gui box2 = g.point_properties_box(self.controlArea, box) gui.button(box2, self, "Set Colors", self.set_colors) box = gui.widgetBox(self.controlArea, "Plot Properties") g.add_widgets([g.ShowLegend, g.ShowGridLines], box) gui.checkBox(box, self, value='graph.tooltip_shows_all', label='Show all data on mouse hover') gui.separator(self.controlArea, 8, 8) self.zoom_select_toolbar = g.zoom_select_toolbar( self.controlArea, nomargin=True, buttons=[ g.StateButtonsBegin, g.SimpleSelect, g.Pan, g.Zoom, g.StateButtonsEnd, g.ZoomReset ]) buttons = self.zoom_select_toolbar.buttons buttons[g.Zoom].clicked.connect(self.graph.zoom_button_clicked) buttons[g.Pan].clicked.connect(self.graph.pan_button_clicked) buttons[g.SimpleSelect].clicked.connect( self.graph.select_button_clicked) buttons[g.ZoomReset].clicked.connect(self.graph.reset_button_clicked) self.controlArea.layout().addStretch(100) self.icons = gui.attributeIconDict dlg = self.create_color_dialog() self.graph.continuous_palette = dlg.getContinuousPalette("contPalette") self.graph.discrete_palette = dlg.getDiscretePalette("discPalette") dlg.deleteLater() p = self.graph.plot_widget.palette() self.graph.set_palette(p) gui.auto_commit(self.controlArea, self, "auto_send_selection", "Send Selection") def zoom(s): """Zoom in/out by factor `s`.""" viewbox = plot.getViewBox() # scaleBy scales the view's bounds (the axis range) viewbox.scaleBy((1 / s, 1 / s)) def fit_to_view(): viewbox = plot.getViewBox() viewbox.autoRange() zoom_in = QtGui.QAction("Zoom in", self, triggered=lambda: zoom(1.25)) zoom_in.setShortcuts([ QtGui.QKeySequence(QtGui.QKeySequence.ZoomIn), QtGui.QKeySequence(self.tr("Ctrl+=")) ]) zoom_out = QtGui.QAction("Zoom out", self, shortcut=QtGui.QKeySequence.ZoomOut, triggered=lambda: zoom(1 / 1.25)) zoom_fit = QtGui.QAction("Fit in view", self, shortcut=QtGui.QKeySequence(Qt.ControlModifier | Qt.Key_0), triggered=fit_to_view) self.addActions([zoom_in, zoom_out, zoom_fit]) # self.vizrank = OWVizRank(self, self.signalManager, self.graph, # orngVizRank.SCATTERPLOT, "ScatterPlot") # self.optimizationDlg = self.vizrank # def settingsFromWidgetCallback(self, handler, context): # context.selectionPolygons = [] # for curve in self.graph.selectionCurveList: # xs = [curve.x(i) for i in range(curve.dataSize())] # ys = [curve.y(i) for i in range(curve.dataSize())] # context.selectionPolygons.append((xs, ys)) # def settingsToWidgetCallback(self, handler, context): # selections = getattr(context, "selectionPolygons", []) # for (xs, ys) in selections: # c = SelectionCurve("") # c.setData(xs,ys) # c.attach(self.graph) # self.graph.selectionCurveList.append(c) def reset_graph_data(self, *_): self.graph.rescale_data() self.major_graph_update() def set_data(self, data): if type(data) == SqlTable and data.approx_len() > LARGE_TABLE: data = data.sample_time(DEFAULT_SAMPLE_TIME) if data is not None and (len(data) == 0 or len(data.domain) == 0): data = None if self.data and data and self.data.checksum() == data.checksum(): return self.closeContext() same_domain = \ self.data and data and \ data.domain.checksum() == self.data.domain.checksum() self.data = data # TODO: adapt scatter plot to work on SqlTables (avoid use of X and Y) if isinstance(self.data, SqlTable): self.data.download_data() # self.vizrank.clearResults() if not same_domain: self.init_attr_values() self.openContext(self.data) def set_subset_data(self, subset_data): self.subset_data = subset_data # self.vizrank.clearArguments() # called when all signals are received, so the graph is updated only once def handleNewSignals(self): self.graph.new_data(self.data, self.subset_data) # self.vizrank.resetDialog() if self.attribute_selection_list and \ all(attr.name in self.graph.attribute_name_index for attr in self.attribute_selection_list): self.attr_x = self.attribute_selection_list[0].name self.attr_y = self.attribute_selection_list[1].name self.attribute_selection_list = None self.update_graph() self.unconditional_commit() def set_shown_attributes(self, attributes): if attributes and len(attributes) >= 2: self.attribute_selection_list = attributes[:2] else: self.attribute_selection_list = None # Callback from VizRank dialog def show_selected_attributes(self): val = self.vizrank.get_selected_projection() if not val: return if self.data.domain.class_var: self.graph.attr_color = self.data.domain.class_var.name self.major_graph_update(val[3]) def get_shown_attributes(self): return self.attr_x, self.attr_y def init_attr_values(self): self.cb_attr_x.clear() self.cb_attr_y.clear() self.cb_attr_color.clear() self.cb_attr_color.addItem("(Same color)") self.cb_attr_label.clear() self.cb_attr_label.addItem("(No labels)") self.cb_attr_shape.clear() self.cb_attr_shape.addItem("(Same shape)") self.cb_attr_size.clear() self.cb_attr_size.addItem("(Same size)") if not self.data: return for var in self.data.domain.metas: self.cb_attr_label.addItem(self.icons[var], var.name) for attr in self.data.domain.variables: self.cb_attr_x.addItem(self.icons[attr], attr.name) self.cb_attr_y.addItem(self.icons[attr], attr.name) self.cb_attr_color.addItem(self.icons[attr], attr.name) if isinstance(attr, DiscreteVariable): self.cb_attr_shape.addItem(self.icons[attr], attr.name) else: self.cb_attr_size.addItem(self.icons[attr], attr.name) self.cb_attr_label.addItem(self.icons[attr], attr.name) self.attr_x = self.cb_attr_x.itemText(0) if self.cb_attr_y.count() > 1: self.attr_y = self.cb_attr_y.itemText(1) else: self.attr_y = self.cb_attr_y.itemText(0) if self.data.domain.class_var: self.graph.attr_color = self.data.domain.class_var.name else: self.graph.attr_color = "" self.graph.attr_shape = "" self.graph.attr_size = "" self.graph.attr_label = "" def major_graph_update(self, attributes=None, inside_colors=None, **args): self.update_graph(attributes, inside_colors, **args) def update_graph(self, attributes=None, inside_colors=None, **_): self.graph.zoomStack = [] if not self.graph.have_data: return if attributes and len(attributes) == 2: self.attr_x, self.attr_y = attributes self.graph.update_data(self.attr_x, self.attr_y) def saveSettings(self): OWWidget.saveSettings(self) # self.vizrank.saveSettings() def selection_changed(self): self.commit() def commit(self): selected = unselected = None # TODO: Implement selection for sql data if isinstance(self.data, SqlTable): selected = unselected = self.data elif self.data is not None: selection = self.graph.get_selection() selected = self.data[selection] unselection = np.full(len(self.data), True, dtype=bool) unselection[selection] = False unselected = self.data[unselection] self.send("Selected Data", selected) self.send("Other Data", unselected) def set_colors(self): dlg = self.create_color_dialog() if dlg.exec_(): self.color_settings = dlg.getColorSchemas() self.selected_schema_index = dlg.selectedSchemaIndex self.graph.continuous_palette = dlg.getContinuousPalette( "contPalette") self.graph.discrete_palette = dlg.getDiscretePalette("discPalette") self.update_graph() def create_color_dialog(self): c = ColorPaletteDlg(self, "Color Palette") c.createDiscretePalette("discPalette", "Discrete Palette") c.createContinuousPalette("contPalette", "Continuous Palette") c.setColorSchemas(self.color_settings, self.selected_schema_index) return c def closeEvent(self, ce): # self.vizrank.close() super().closeEvent(ce) def sendReport(self): self.startReport("%s [%s - %s]" % (self.windowTitle(), self.attr_x, self.attr_y)) self.reportSettings( "Visualized attributes", [("X", self.attr_x), ("Y", self.attr_y), self.graph.attr_color and ("Color", self.graph.attr_color), self.graph.attr_label and ("Label", self.graph.attr_label), self.graph.attr_shape and ("Shape", self.graph.attr_shape), self.graph.attr_size and ("Size", self.graph.attr_size)]) self.reportSettings("Settings", [("Symbol size", self.graph.point_width), ("Opacity", self.graph.alpha_value), ("Jittering", self.graph.jitter_size), ("Jitter continuous attributes", gui.YesNo[self.graph.jitter_continuous])]) self.reportSection("Graph") self.reportImage(self.graph.save_to_file, QSize(400, 400))