Repository: biolab/orange3

Issue:
Scatterplotgraph is too eager to combine legends
When checking results of prediction models (typically coming from Predictions widget), we may get this:

<img width="265" alt="Screenshot 2019-09-30 at 14 43 23" src="https://user-images.githubusercontent.com/2387315/65875935-f9f6af00-e390-11e9-9449-d223b876c612.png">

Points belonging to C1 are blue and they are circles, and those is C2 are red and crosses. But the legend implies there are only blue circles and red crosses, which is evidently not the case.

Scatterplotgraph decides whether to combine the color legend and the shape legen into a single legend based on whether they have the same labels:

```
        shape_labels = self.master.get_shape_labels()
        color_labels = self.master.get_color_labels()
        if shape_labels == color_labels and shape_labels is not None:
            self._update_combined_legend(shape_labels)
```

A proper way would be something like

```
        if shape_attr is color_attr and shape_attr is not None:
            self._update_combined_legend(...)
```

except that scatterplotgraph does not have access to `shape_attr` and `color_attr`. Something like `color_var` may even not exist. Scatterplotgraph is the base class for all kinds of projections, and the colors of points need not represent values of any variable but can be compute from, for instance, the stress (in MDS).

Hence only the `master`, that is, the widget, can reliably tell the scatterplotgraph whether to combine the color and shape legends. I would hate to have an API with too many methods, but it looks like we will have to add this one.



Discussion:
Scatterplotgraph actually has a way to do it without widget's help: it can check whether the two columns are the same. If they are, there is nothing wrong if it combines legends even if they come from different attributes.

Resolving patch:
diff --git a/Orange/widgets/visualize/owscatterplotgraph.py b/Orange/widgets/visualize/owscatterplotgraph.py
--- a/Orange/widgets/visualize/owscatterplotgraph.py
+++ b/Orange/widgets/visualize/owscatterplotgraph.py
@@ -1208,7 +1208,15 @@ def update_legends(self):
         cont_color = self.master.is_continuous_color()
         shape_labels = self.master.get_shape_labels()
         color_labels = self.master.get_color_labels()
-        if shape_labels == color_labels and shape_labels is not None:
+        if not cont_color and shape_labels is not None \
+                and shape_labels == color_labels:
+            colors = self.master.get_color_data()
+            shapes = self.master.get_shape_data()
+            mask = np.isfinite(colors) * np.isfinite(shapes)
+            combined = (colors == shapes)[mask].all()
+        else:
+            combined = False
+        if combined:
             self._update_combined_legend(shape_labels)
         else:
             self._update_shape_legend(shape_labels)


Test patch:
diff --git a/Orange/widgets/visualize/tests/test_owscatterplotbase.py b/Orange/widgets/visualize/tests/test_owscatterplotbase.py
--- a/Orange/widgets/visualize/tests/test_owscatterplotbase.py
+++ b/Orange/widgets/visualize/tests/test_owscatterplotbase.py
@@ -1105,6 +1105,10 @@ def test_show_legend_no_data(self):
     def test_legend_combine(self):
         master = self.master
         graph = self.graph
+
+        master.get_shape_data = lambda: np.arange(10, dtype=float) % 3
+        master.get_color_data = lambda: 2 * np.arange(10, dtype=float) % 3
+
         graph.reset_graph()
 
         shape_legend = self.graph.shape_legend.setVisible = Mock()
@@ -1119,6 +1123,12 @@ def test_legend_combine(self):
         master.get_color_labels = lambda: ["a", "b"]
         graph.update_legends()
         self.assertTrue(shape_legend.call_args[0][0])
+        self.assertTrue(color_legend.call_args[0][0])
+        self.assertEqual(len(graph.shape_legend.items), 2)
+
+        master.get_color_data = lambda: np.arange(10, dtype=float) % 3
+        graph.update_legends()
+        self.assertTrue(shape_legend.call_args[0][0])
         self.assertFalse(color_legend.call_args[0][0])
         self.assertEqual(len(graph.shape_legend.items), 2)