From 67962a9b60ea54b403d844326b41f6ae5eaec95f Mon Sep 17 00:00:00 2001 From: Tomaz Hocevar Date: Fri, 27 Jul 2018 11:36:03 +0200 Subject: [PATCH 1/6] Indicate overlap by point size. --- .../widgets/visualize/owscatterplotgraph.py | 41 ++++++++++++++----- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/Orange/widgets/visualize/owscatterplotgraph.py b/Orange/widgets/visualize/owscatterplotgraph.py index aad048f62e5..0f8a1768fbb 100644 --- a/Orange/widgets/visualize/owscatterplotgraph.py +++ b/Orange/widgets/visualize/owscatterplotgraph.py @@ -1,15 +1,15 @@ -from collections import Counter +from collections import Counter, defaultdict import sys import itertools from xml.sax.saxutils import escape -from math import log10, floor, ceil +from math import log2, log10, floor, ceil import numpy as np from scipy.stats import linregress from AnyQt.QtCore import Qt, QObject, QEvent, QRectF, QPointF, QSize from AnyQt.QtGui import ( - QStaticText, QColor, QPen, QBrush, QPainterPath, QTransform, QPainter, QKeySequence) + QStaticText, QColor, QPen, QBrush, QPainterPath, QTransform, QPainter, QKeySequence, QConicalGradient) from AnyQt.QtWidgets import QApplication, QToolTip, QPinchGesture, \ QGraphicsTextItem, QGraphicsRectItem, QAction @@ -691,13 +691,13 @@ def update_data(self, attr_x, attr_y, reset_view=True): self.shown_x.name, self.shown_y.name) return - x_data, y_data = self.get_xy_data_positions( + self.x_data, self.y_data = self.get_xy_data_positions( attr_x, attr_y, self.valid_data) - self.n_points = len(x_data) + self.n_points = len(self.x_data) if reset_view: - min_x, max_x = np.nanmin(x_data), np.nanmax(x_data) - min_y, max_y = np.nanmin(y_data), np.nanmax(y_data) + min_x, max_x = np.nanmin(self.x_data), np.nanmax(self.x_data) + min_y, max_y = np.nanmin(self.y_data), np.nanmax(self.y_data) self.view_box.setRange( QRectF(min_x, min_y, max_x - min_x, max_y - min_y), padding=0.025) @@ -712,6 +712,14 @@ def update_data(self, attr_x, attr_y, reset_view=True): else: self.set_labels(axis, None) + # compute overlaps of points for use in compute_colors and compute_sizes + self.overlaps = [] + points = defaultdict(list) + for i, xy in enumerate(zip(self.x_data, self.y_data)): + points[xy].append(i) + self.overlaps = [len(points[xy]) for i, xy in enumerate(zip(self.x_data, self.y_data))] + self.overlap_factor = [1+log2(o) for o in self.overlaps] + color_data, brush_data = self.compute_colors() color_data_sel, brush_data_sel = self.compute_colors_sel() size_data = self.compute_sizes() @@ -721,7 +729,7 @@ def update_data(self, attr_x, attr_y, reset_view=True): rgb_data = [pen.color().getRgb()[:3] for pen in color_data] self.density_img = classdensity.class_density_image( min_x, max_x, min_y, max_y, self.resolution, - x_data, y_data, rgb_data) + self.x_data, self.y_data, rgb_data) self.plot_widget.addItem(self.density_img) self.data_indices = np.flatnonzero(self.valid_data) @@ -730,11 +738,11 @@ def update_data(self, attr_x, attr_y, reset_view=True): self.shown_x.name, self.shown_y.name) self.scatterplot_item = ScatterPlotItem( - x=x_data, y=y_data, data=self.data_indices, + x=self.x_data, y=self.y_data, data=self.data_indices, symbol=shape_data, size=size_data, pen=color_data, brush=brush_data ) self.scatterplot_item_sel = ScatterPlotItem( - x=x_data, y=y_data, data=self.data_indices, + x=self.x_data, y=self.y_data, data=self.data_indices, symbol=shape_data, size=size_data + SELECTION_WIDTH, pen=color_data_sel, brush=brush_data_sel ) @@ -815,6 +823,10 @@ def compute_sizes(self): if np.any(nans): size_data[nans] = self.MinShapeSize - 2 self.master.Information.missing_size(self.attr_size) + + # scale sizes because of overlaps + size_data = np.multiply(size_data, self.overlap_factor) + return size_data def update_sizes(self): @@ -957,6 +969,15 @@ def compute_colors(self, keep_colors=False): QBrush(QColor(col[0], col[1], col[2], alpha))] for col in colors]) self.brush_colors = self.brush_colors[c_data] + + # gray out overlapping points + for i, xy in enumerate(zip(self.x_data, self.y_data)): + if self.overlaps[i] > 1: + self.brush_colors[i] = [ + QBrush(QColor(0, 0, 0, 0)), + QBrush(QColor(128, 128, 128, alpha))] + self.pen_colors[i] = _make_pen(QColor(128, 128, 128).darker(self.DarkerValue), 1.5) + if subset is not None: brush = np.where( subset, From be74c412ddac53fe06355557d4226e574fe667e8 Mon Sep 17 00:00:00 2001 From: Tomaz Hocevar Date: Tue, 31 Jul 2018 10:18:24 +0200 Subject: [PATCH 2/6] Color overlapping points by most frequent color. --- .../widgets/visualize/owscatterplotgraph.py | 26 +++++++++---------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/Orange/widgets/visualize/owscatterplotgraph.py b/Orange/widgets/visualize/owscatterplotgraph.py index 0f8a1768fbb..75698e96c3c 100644 --- a/Orange/widgets/visualize/owscatterplotgraph.py +++ b/Orange/widgets/visualize/owscatterplotgraph.py @@ -9,7 +9,7 @@ from AnyQt.QtCore import Qt, QObject, QEvent, QRectF, QPointF, QSize from AnyQt.QtGui import ( - QStaticText, QColor, QPen, QBrush, QPainterPath, QTransform, QPainter, QKeySequence, QConicalGradient) + QStaticText, QColor, QPen, QBrush, QPainterPath, QTransform, QPainter, QKeySequence) from AnyQt.QtWidgets import QApplication, QToolTip, QPinchGesture, \ QGraphicsTextItem, QGraphicsRectItem, QAction @@ -714,10 +714,11 @@ def update_data(self, attr_x, attr_y, reset_view=True): # compute overlaps of points for use in compute_colors and compute_sizes self.overlaps = [] - points = defaultdict(list) + self.coord_to_id = defaultdict(list) for i, xy in enumerate(zip(self.x_data, self.y_data)): - points[xy].append(i) - self.overlaps = [len(points[xy]) for i, xy in enumerate(zip(self.x_data, self.y_data))] + self.coord_to_id[xy].append(i) + self.overlaps = [len(self.coord_to_id[xy]) + for i, xy in enumerate(zip(self.x_data, self.y_data))] self.overlap_factor = [1+log2(o) for o in self.overlaps] color_data, brush_data = self.compute_colors() @@ -959,24 +960,23 @@ def compute_colors(self, keep_colors=False): c_data = c_data.astype(int) colors = np.r_[palette.getRGB(np.arange(n_colors)), [[128, 128, 128]]] - pens = np.array( + pen_colors_palette = np.array( [_make_pen(QColor(*col).darker(self.DarkerValue), 1.5) for col in colors]) - self.pen_colors = pens[c_data] + self.pen_colors = pen_colors_palette[c_data] alpha = self.alpha_value if subset is None else 255 - self.brush_colors = np.array([ + brush_colors_palette = np.array([ [QBrush(QColor(0, 0, 0, 0)), QBrush(QColor(col[0], col[1], col[2], alpha))] for col in colors]) - self.brush_colors = self.brush_colors[c_data] + self.brush_colors = brush_colors_palette[c_data] - # gray out overlapping points + # color overlapping points by most frequent color for i, xy in enumerate(zip(self.x_data, self.y_data)): if self.overlaps[i] > 1: - self.brush_colors[i] = [ - QBrush(QColor(0, 0, 0, 0)), - QBrush(QColor(128, 128, 128, alpha))] - self.pen_colors[i] = _make_pen(QColor(128, 128, 128).darker(self.DarkerValue), 1.5) + c = Counter(c_data[j] for j in self.coord_to_id[xy]).most_common(1)[0][0] + self.brush_colors[i] = brush_colors_palette[c] + self.pen_colors[i] = pen_colors_palette[c] if subset is not None: brush = np.where( From a1f81e9740d5068f037a18bf0c0c3e96e47221c0 Mon Sep 17 00:00:00 2001 From: Tomaz Hocevar Date: Fri, 24 Aug 2018 11:15:35 +0200 Subject: [PATCH 3/6] Add overlap option to size parameter. --- Orange/widgets/utils/plot/owplotgui.py | 3 +++ Orange/widgets/visualize/owscatterplotgraph.py | 18 ++++++++++-------- .../source/widgets/visualize/scatterplot.rst | 3 ++- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/Orange/widgets/utils/plot/owplotgui.py b/Orange/widgets/utils/plot/owplotgui.py index 3c3bd29b722..7779ac2984c 100644 --- a/Orange/widgets/utils/plot/owplotgui.py +++ b/Orange/widgets/utils/plot/owplotgui.py @@ -465,11 +465,14 @@ def __init__(self, plot): self.shape_model = DomainModel(placeholder="(Same shape)", valid_types=DiscreteVariable) self.size_model = DomainModel(placeholder="(Same size)", + order=(self.SizeByOverlap,) + DomainModel.SEPARATED, valid_types=ContinuousVariable) self.label_model = DomainModel(placeholder="(No labels)") self.points_models = [self.color_model, self.shape_model, self.size_model, self.label_model] + SizeByOverlap = "Overlap" + Spacing = 0 ShowLegend = 2 diff --git a/Orange/widgets/visualize/owscatterplotgraph.py b/Orange/widgets/visualize/owscatterplotgraph.py index 75698e96c3c..776af76cb8e 100644 --- a/Orange/widgets/visualize/owscatterplotgraph.py +++ b/Orange/widgets/visualize/owscatterplotgraph.py @@ -812,7 +812,7 @@ def set_axis_title(self, axis, title): def compute_sizes(self): self.master.Information.missing_size.clear() - if self.attr_size is None: + if self.attr_size in [None, OWPlotGUI.SizeByOverlap]: size_data = np.full((self.n_points,), self.point_width, dtype=float) else: @@ -826,7 +826,8 @@ def compute_sizes(self): self.master.Information.missing_size(self.attr_size) # scale sizes because of overlaps - size_data = np.multiply(size_data, self.overlap_factor) + if self.attr_size == OWPlotGUI.SizeByOverlap: + size_data = np.multiply(size_data, self.overlap_factor) return size_data @@ -971,12 +972,13 @@ def compute_colors(self, keep_colors=False): for col in colors]) self.brush_colors = brush_colors_palette[c_data] - # color overlapping points by most frequent color - for i, xy in enumerate(zip(self.x_data, self.y_data)): - if self.overlaps[i] > 1: - c = Counter(c_data[j] for j in self.coord_to_id[xy]).most_common(1)[0][0] - self.brush_colors[i] = brush_colors_palette[c] - self.pen_colors[i] = pen_colors_palette[c] + if self.attr_size == OWPlotGUI.SizeByOverlap: + # color overlapping points by most frequent color + for i, xy in enumerate(zip(self.x_data, self.y_data)): + if self.overlaps[i] > 1: + c = Counter(c_data[j] for j in self.coord_to_id[xy]).most_common(1)[0][0] + self.brush_colors[i] = brush_colors_palette[c] + self.pen_colors[i] = pen_colors_palette[c] if subset is not None: brush = np.where( diff --git a/doc/visual-programming/source/widgets/visualize/scatterplot.rst b/doc/visual-programming/source/widgets/visualize/scatterplot.rst index 851edec85cc..631b558bdbd 100644 --- a/doc/visual-programming/source/widgets/visualize/scatterplot.rst +++ b/doc/visual-programming/source/widgets/visualize/scatterplot.rst @@ -41,7 +41,8 @@ the left side of the widget. A snapshot below shows the scatterplot of the 2. Set the color of the displayed points (you will get colors for discrete values and grey-scale points for continuous). Set label, shape and size to differentiate between points. Set symbol size and opacity for - all data points. Set the desired colors scale. + all data points. Set the desired colors scale. To visualize the number + of overlapping points use *Overlap* for size. 3. Adjust *plot properties*: - *Show legend* displays a legend on the right. Click and drag the legend to move it. From 03c4b0c6e6f8f19514d46defcde14688251f313c Mon Sep 17 00:00:00 2001 From: Tomaz Hocevar Date: Fri, 24 Aug 2018 12:16:00 +0200 Subject: [PATCH 4/6] Fix MDS --- Orange/widgets/unsupervised/owmds.py | 19 ++++++++----------- .../widgets/visualize/owscatterplotgraph.py | 3 ++- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/Orange/widgets/unsupervised/owmds.py b/Orange/widgets/unsupervised/owmds.py index bb4fc919b3e..5ad2427bc51 100644 --- a/Orange/widgets/unsupervised/owmds.py +++ b/Orange/widgets/unsupervised/owmds.py @@ -56,6 +56,12 @@ def update_data(self, attr_x, attr_y, reset_view=True): self.plot_widget.setAspectLocked(True, 1) def compute_sizes(self): + """Handle 'Stress' size option. + Everything else is passed to Scatterplot's compute_sizes""" + + if self.attr_size != "Stress": + return super().compute_sizes() + def scale(a): dmin, dmax = np.nanmin(a), np.nanmax(a) if dmax - dmin > 0: @@ -64,17 +70,8 @@ def scale(a): return np.zeros_like(a) self.master.Information.missing_size.clear() - if self.attr_size is None: - size_data = np.full((self.n_points,), self.point_width, - dtype=float) - elif self.attr_size == "Stress": - size_data = scale(stress(self.master.embedding, self.master.effective_matrix)) - size_data = self.MinShapeSize + size_data * self.point_width - else: - size_data = \ - self.MinShapeSize + \ - self.scaled_data.get_column_view(self.attr_size)[0][self.valid_data] * \ - self.point_width + size_data = scale(stress(self.master.embedding, self.master.effective_matrix)) + size_data = self.MinShapeSize + size_data * self.point_width nans = np.isnan(size_data) if np.any(nans): size_data[nans] = self.MinShapeSize - 2 diff --git a/Orange/widgets/visualize/owscatterplotgraph.py b/Orange/widgets/visualize/owscatterplotgraph.py index 776af76cb8e..8811d58a816 100644 --- a/Orange/widgets/visualize/owscatterplotgraph.py +++ b/Orange/widgets/visualize/owscatterplotgraph.py @@ -976,7 +976,8 @@ def compute_colors(self, keep_colors=False): # color overlapping points by most frequent color for i, xy in enumerate(zip(self.x_data, self.y_data)): if self.overlaps[i] > 1: - c = Counter(c_data[j] for j in self.coord_to_id[xy]).most_common(1)[0][0] + cnt = Counter(c_data[j] for j in self.coord_to_id[xy]) + c = cnt.most_common(1)[0][0] self.brush_colors[i] = brush_colors_palette[c] self.pen_colors[i] = pen_colors_palette[c] From e7e3bc9034ab65d94e43a1935e5a314da8f3b2b3 Mon Sep 17 00:00:00 2001 From: Tomaz Hocevar Date: Fri, 24 Aug 2018 12:31:41 +0200 Subject: [PATCH 5/6] Fix tests --- .../widgets/visualize/tests/test_owfreeviz.py | 8 ------- .../tests/test_owlinearprojection.py | 8 ------- .../widgets/visualize/tests/test_owradviz.py | 8 ------- .../visualize/tests/test_owscatterplot.py | 21 ++++++++++++++----- 4 files changed, 16 insertions(+), 29 deletions(-) diff --git a/Orange/widgets/visualize/tests/test_owfreeviz.py b/Orange/widgets/visualize/tests/test_owfreeviz.py index 43e1b54e0e0..28d3e86ddc3 100644 --- a/Orange/widgets/visualize/tests/test_owfreeviz.py +++ b/Orange/widgets/visualize/tests/test_owfreeviz.py @@ -25,14 +25,6 @@ def setUpClass(cls): def setUp(self): self.widget = self.create_widget(OWFreeViz) - def test_points_combo_boxes(self): - self.send_signal(self.widget.Inputs.data, self.heart_disease) - graph = self.widget.controls.graph - self.assertEqual(len(graph.attr_color.model()), 17) - self.assertEqual(len(graph.attr_shape.model()), 11) - self.assertEqual(len(graph.attr_size.model()), 8) - self.assertEqual(len(graph.attr_label.model()), 17) - def test_ugly_datasets(self): self.send_signal(self.widget.Inputs.data, Table(datasets.path("testing_dataset_cls"))) self.send_signal(self.widget.Inputs.data, Table(datasets.path("testing_dataset_reg"))) diff --git a/Orange/widgets/visualize/tests/test_owlinearprojection.py b/Orange/widgets/visualize/tests/test_owlinearprojection.py index 51cbbc0beba..06737cc0888 100644 --- a/Orange/widgets/visualize/tests/test_owlinearprojection.py +++ b/Orange/widgets/visualize/tests/test_owlinearprojection.py @@ -65,14 +65,6 @@ def test_nan_plot(self): with excepthook_catch(): simulate.combobox_activate_item(cb.attr_size, "X1") - def test_points_combo_boxes(self): - self.send_signal("Data", self.data) - graph = self.widget.controls.graph - self.assertEqual(len(graph.attr_color.model()), 8) - self.assertEqual(len(graph.attr_shape.model()), 3) - self.assertEqual(len(graph.attr_size.model()), 6) - self.assertEqual(len(graph.attr_label.model()), 8) - def test_buttons(self): for btn in self.widget.radio_placement.buttons[:3]: self.send_signal(self.widget.Inputs.data, self.data) diff --git a/Orange/widgets/visualize/tests/test_owradviz.py b/Orange/widgets/visualize/tests/test_owradviz.py index e88510f4cb2..79f66bb568c 100644 --- a/Orange/widgets/visualize/tests/test_owradviz.py +++ b/Orange/widgets/visualize/tests/test_owradviz.py @@ -22,14 +22,6 @@ def setUpClass(cls): def setUp(self): self.widget = self.create_widget(OWRadviz) - def test_points_combo_boxes(self): - self.send_signal(self.widget.Inputs.data, self.heart_disease) - graph = self.widget.controls.graph - self.assertEqual(len(graph.attr_color.model()), 17) - self.assertEqual(len(graph.attr_shape.model()), 11) - self.assertEqual(len(graph.attr_size.model()), 8) - self.assertEqual(len(graph.attr_label.model()), 17) - def test_ugly_datasets(self): self.send_signal(self.widget.Inputs.data, Table(datasets.path("testing_dataset_cls"))) self.send_signal(self.widget.Inputs.data, Table(datasets.path("testing_dataset_reg"))) diff --git a/Orange/widgets/visualize/tests/test_owscatterplot.py b/Orange/widgets/visualize/tests/test_owscatterplot.py index ddd1ac2cd6b..10d86561cd1 100644 --- a/Orange/widgets/visualize/tests/test_owscatterplot.py +++ b/Orange/widgets/visualize/tests/test_owscatterplot.py @@ -154,13 +154,24 @@ def test_regression_line(self): def test_points_combo_boxes(self): """Check Point box combo models and values""" self.send_signal(self.widget.Inputs.data, self.data) - self.assertEqual(len(self.widget.controls.graph.attr_color.model()), 8) - self.assertEqual(len(self.widget.controls.graph.attr_shape.model()), 3) - self.assertEqual(len(self.widget.controls.graph.attr_size.model()), 6) - self.assertEqual(len(self.widget.controls.graph.attr_label.model()), 8) + graph = self.widget.controls.graph + + # color and label should contain all variables + # size should contain only continuous variables + # shape should contain only discrete variables + for var in self.data.domain.variables + self.data.domain.metas: + self.assertIn(var, graph.attr_color.model()) + self.assertIn(var, graph.attr_label.model()) + if var.is_continuous: + self.assertIn(var, graph.attr_size.model()) + self.assertNotIn(var, graph.attr_shape.model()) + if var.is_discrete: + self.assertNotIn(var, graph.attr_size.model()) + self.assertIn(var, graph.attr_shape.model()) + other_widget = self.create_widget(OWScatterPlot) self.send_signal(self.widget.Inputs.data, self.data, widget=other_widget) - self.assertEqual(self.widget.graph.controls.attr_color.currentText(), + self.assertEqual(graph.attr_color.currentText(), self.data.domain.class_var.name) def test_group_selections(self): From a03e6ac9b3fe0ef1e15d484b918bd9ed5b7fdc72 Mon Sep 17 00:00:00 2001 From: Tomaz Hocevar Date: Fri, 24 Aug 2018 13:20:14 +0200 Subject: [PATCH 6/6] Add overlap test to scatterplot. --- Orange/widgets/visualize/tests/test_owscatterplot.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/Orange/widgets/visualize/tests/test_owscatterplot.py b/Orange/widgets/visualize/tests/test_owscatterplot.py index 10d86561cd1..f4779d4aec4 100644 --- a/Orange/widgets/visualize/tests/test_owscatterplot.py +++ b/Orange/widgets/visualize/tests/test_owscatterplot.py @@ -8,6 +8,7 @@ from AnyQt.QtWidgets import QToolTip from Orange.data import Table, Domain, ContinuousVariable, DiscreteVariable +from Orange.widgets.utils.plot import OWPlotGUI from Orange.widgets.visualize.owscatterplotgraph import MAX from Orange.widgets.widget import AttributeList from Orange.widgets.tests.base import WidgetTest, WidgetOutputsTestMixin, datasets @@ -174,6 +175,13 @@ def test_points_combo_boxes(self): self.assertEqual(graph.attr_color.currentText(), self.data.domain.class_var.name) + def test_overlap(self): + self.send_signal(self.widget.Inputs.data, Table("iris")) + self.assertEqual(len(set(self.widget.graph.compute_sizes())), 1) + simulate.combobox_activate_item(self.widget.controls.graph.attr_size, + OWPlotGUI.SizeByOverlap) + self.assertGreater(len(set(self.widget.graph.compute_sizes())), 1) + def test_group_selections(self): self.send_signal(self.widget.Inputs.data, self.data) graph = self.widget.graph