Repository: biolab/orange3

Issue:
Eliminate sizing by overlap in scatter plot (and other projections)
Projections show data instances as symbols of different sizes, colors and shapes, and marked with labels. We also have jittering, backgrounds colored by majority and add-ons for particular projections, such as regression lines in the scatter plot. Subset can be represented with filled symbols.

When overlap is enabled, projections show data symbols of different sizes<strike>, colors and shapes, and marked with labels. We also have jittering, backgrounds colored by majority and add-ons for particular projections, such as regression lines in the scatter plot. Subset can be represented with filled symbols.</strike>

It's a completely different beast. Trying to implement this properly is like stuffing another visualization into an existing one. All methods have to have a special condition that overrides the default behaviour. (One particular annoyance is that even the number of points change from len(data) to the number of combinations.) Yes, colors can be replaced with majority colors, but this is wrong anyway. (Or, yes, we can use pie charts, but consider that the plot's basic ingredient is then no longer a ScatterplotItem but a list of QGraphicsEllipseItems - it's another plot altogether.) Shapes and labels must be disabled, and jittering too. Background must be computed from distributions. Regression lines and similar stuff must be disabled. There are no more subsets. 

The cost is great: it makes the current neat implementation a hodgepodge of special cases. But the benefit is zero. Symbol sizes are not a good visualization for numerical quantities. For pairs of discrete features, we have box plots (or Mosaic, to add colors). If we want to have discrete + numeric, let's add violin plots. For numeric vs. numeric, there is either no overlap or they can be discretized or scatter plot with jittering should do the job.

If, however, scatter plots of pie charts seem useful, let us have another widget for that. Overlaps in current projections must be gone asap.


Discussion:

Resolving patch:
diff --git a/Orange/widgets/utils/plot/owplotgui.py b/Orange/widgets/utils/plot/owplotgui.py
--- a/Orange/widgets/utils/plot/owplotgui.py
+++ b/Orange/widgets/utils/plot/owplotgui.py
@@ -28,10 +28,8 @@
 import os
 
 import unicodedata
-from functools import reduce
-from operator import itemgetter
 
-from Orange.data import ContinuousVariable, DiscreteVariable, Variable
+from Orange.data import ContinuousVariable, DiscreteVariable
 from Orange.widgets import gui
 from Orange.widgets.utils import itemmodels
 from Orange.widgets.utils.listfilter import variables_filter
@@ -430,19 +428,16 @@ class OWPlotGUI:
 
     def __init__(self, plot):
         self._plot = plot
-        self.color_model = DomainModel(placeholder="(Same color)",
-                                       valid_types=DomainModel.PRIMITIVE)
-        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.color_model = DomainModel(
+            placeholder="(Same color)", valid_types=DomainModel.PRIMITIVE)
+        self.shape_model = DomainModel(
+            placeholder="(Same shape)", valid_types=DiscreteVariable)
+        self.size_model = DomainModel(
+            placeholder="(Same size)", 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/utils/widget.py b/Orange/widgets/visualize/utils/widget.py
--- a/Orange/widgets/visualize/utils/widget.py
+++ b/Orange/widgets/visualize/utils/widget.py
@@ -1,6 +1,4 @@
-from collections import Counter, defaultdict
 from xml.sax.saxutils import escape
-from math import log2
 
 import numpy as np
 
@@ -22,7 +20,6 @@
 from Orange.widgets.utils.colorpalette import (
     ColorPaletteGenerator, ContinuousPaletteGenerator, DefaultRGBColors
 )
-from Orange.widgets.utils.plot import OWPlotGUI
 from Orange.widgets.utils.sql import check_sql_input
 from Orange.widgets.visualize.owscatterplotgraph import OWScatterPlotBase
 from Orange.widgets.visualize.utils.component import OWGraphWithAnchors
@@ -98,13 +95,6 @@ def get_subset_mask(self):
         """
         return None
 
-    @staticmethod
-    def __get_overlap_groups(x, y):
-        coord_to_id = defaultdict(list)
-        for i, xy in enumerate(zip(x, y)):
-            coord_to_id[xy].append(i)
-        return coord_to_id
-
     def get_column(self, attr, filter_valid=True,
                    merge_infrequent=False, return_labels=False):
         """
@@ -172,11 +162,6 @@ def get_column(self, attr, filter_valid=True,
     # Sizes
     def get_size_data(self):
         """Return the column corresponding to `attr_size`"""
-        if self.attr_size == OWPlotGUI.SizeByOverlap:
-            x, y = self.get_coordinates_data()
-            coord_to_id = self.__get_overlap_groups(x, y)
-            overlaps = [len(coord_to_id[xy]) for xy in zip(x, y)]
-            return [1 + log2(o) for o in overlaps]
         return self.get_column(self.attr_size)
 
     def impute_sizes(self, size_data):
@@ -195,22 +180,11 @@ def impute_sizes(self, size_data):
 
     def sizes_changed(self):
         self.graph.update_sizes()
-        self.graph.update_colors()  # Needed for overlapping
 
     # Colors
     def get_color_data(self):
         """Return the column corresponding to color data"""
-        colors = self.get_column(self.attr_color, merge_infrequent=True)
-        if self.attr_size == OWPlotGUI.SizeByOverlap:
-            # color overlapping points by most frequent color
-            x, y = self.get_coordinates_data()
-            coord_to_id = self.__get_overlap_groups(x, y)
-            majority_colors = np.empty(len(x))
-            for i, xy in enumerate(zip(x, y)):
-                cnt = Counter(colors[j] for j in coord_to_id[xy])
-                majority_colors[i] = cnt.most_common(1)[0][0]
-            return majority_colors
-        return colors
+        return self.get_column(self.attr_color, merge_infrequent=True)
 
     def get_color_labels(self):
         """


Test patch:
diff --git a/Orange/widgets/tests/base.py b/Orange/widgets/tests/base.py
--- a/Orange/widgets/tests/base.py
+++ b/Orange/widgets/tests/base.py
@@ -848,7 +848,7 @@ def test_attr_models(self):
         controls = self.widget.controls
         self.assertEqual(len(controls.attr_color.model()), 8)
         self.assertEqual(len(controls.attr_shape.model()), 3)
-        self.assertTrue(7 < len(controls.attr_size.model()) < 10)
+        self.assertTrue(5 < len(controls.attr_size.model()) < 8)
         self.assertEqual(len(controls.attr_label.model()), 8)
 
         # color and label should contain all variables
@@ -864,14 +864,6 @@ def test_attr_models(self):
                 self.assertNotIn(var, controls.attr_size.model())
                 self.assertIn(var, controls.attr_shape.model())
 
-    def test_overlap(self):
-        """Test option 'Overlap' in 'Size' combo box"""
-        self.send_signal(self.widget.Inputs.data, self.data)
-        self.assertEqual(len(set(self.widget.graph.get_sizes())), 1)
-        simulate.combobox_activate_item(self.widget.controls.attr_size,
-                                        OWPlotGUI.SizeByOverlap)
-        self.assertGreater(len(set(self.widget.graph.get_sizes())), 1)
-
     def test_attr_label_metas(self, timeout=DEFAULT_TIMEOUT):
         """Set 'Label' from string meta attribute"""
         cont = Continuize(multinomial_treatment=Continuize.AsOrdinal)
diff --git a/Orange/widgets/unsupervised/tests/test_owmds.py b/Orange/widgets/unsupervised/tests/test_owmds.py
--- a/Orange/widgets/unsupervised/tests/test_owmds.py
+++ b/Orange/widgets/unsupervised/tests/test_owmds.py
@@ -7,8 +7,6 @@
 
 import numpy as np
 
-from AnyQt.QtTest import QSignalSpy
-
 from Orange.data import Table
 from Orange.distance import Euclidean
 from Orange.misc import DistMatrix
@@ -18,7 +16,6 @@
 )
 from Orange.widgets.tests.utils import simulate
 from Orange.widgets.unsupervised.owmds import OWMDS
-from Orange.widgets.utils.plot import OWPlotGUI
 
 
 class TestOWMDS(WidgetTest, ProjectionWidgetTestMixin,
@@ -275,16 +272,6 @@ def test_saved_matrix_and_data(self):
         self.send_signal(self.widget.Inputs.data, towns_data)
         self.assertIn(towns_data.domain["label"], attr_label.model())
 
-    def test_overlap(self):
-        self.send_signal(self.signal_name, self.signal_data)
-        if self.widget.isBlocking():
-            spy = QSignalSpy(self.widget.blockingStateChanged)
-            self.assertTrue(spy.wait(5000))
-        self.assertEqual(len(set(self.widget.graph.get_sizes())), 1)
-        simulate.combobox_activate_item(self.widget.controls.attr_size,
-                                        OWPlotGUI.SizeByOverlap)
-        self.assertEqual(len(set(self.widget.graph.get_sizes())), 1)
-
 
 if __name__ == "__main__":
     unittest.main()