Repository: biolab/orange3

Issue:
Projections: Show labels for subset
In visualizations, "Label only selected points" could also apply to points in the subset. Or there could be a separate check box.


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
@@ -616,7 +616,7 @@ def regression_line_check_box(self, widget):
 
     def label_only_selected_check_box(self, widget):
         self._check_box(widget=widget, value="label_only_selected",
-                        label="Label only selected points",
+                        label="Label only selection and subset",
                         cb_name=self._plot.update_labels)
 
     def filled_symbols_check_box(self, widget):
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
@@ -1024,25 +1024,22 @@ def update_labels(self):
         for label in self.labels:
             self.plot_widget.removeItem(label)
         self.labels = []
-        if self.scatterplot_item is None \
-                or self.label_only_selected and self.selection is None:
-            self._signal_too_many_labels(False)
-            return
-        labels = self.get_labels()
-        if labels is None:
-            self._signal_too_many_labels(False)
-            return
-        (x0, x1), (y0, y1) = self.view_box.viewRange()
-        x, y = self.scatterplot_item.getData()
-        mask = np.logical_and(
-            np.logical_and(x >= x0, x <= x1),
-            np.logical_and(y >= y0, y <= y1))
-        if self.label_only_selected:
-            mask = np.logical_and(
-                mask, self._filter_visible(self.selection) != 0)
-        if mask.sum() > self.MAX_VISIBLE_LABELS:
-            self._signal_too_many_labels(True)
+
+        mask = None
+        if self.scatterplot_item is not None:
+            x, y = self.scatterplot_item.getData()
+            mask = self._label_mask(x, y)
+
+        if mask is not None:
+            labels = self.get_labels()
+            if labels is None:
+                mask = None
+
+        self._signal_too_many_labels(
+            mask is not None and mask.sum() > self.MAX_VISIBLE_LABELS)
+        if self._too_many_labels or mask is None or not np.any(mask):
             return
+
         black = pg.mkColor(0, 0, 0)
         labels = labels[mask]
         x = x[mask]
@@ -1052,13 +1049,31 @@ def update_labels(self):
             ti.setPos(xp, yp)
             self.plot_widget.addItem(ti)
             self.labels.append(ti)
-        self._signal_too_many_labels(False)
 
     def _signal_too_many_labels(self, too_many):
         if self._too_many_labels != too_many:
             self._too_many_labels = too_many
             self.too_many_labels.emit(too_many)
 
+    def _label_mask(self, x, y):
+        (x0, x1), (y0, y1) = self.view_box.viewRange()
+        mask = np.logical_and(
+            np.logical_and(x >= x0, x <= x1),
+            np.logical_and(y >= y0, y <= y1))
+        if self.label_only_selected:
+            sub_mask = self._filter_visible(self.master.get_subset_mask())
+            if self.selection is None:
+                if sub_mask is None:
+                    return None
+                else:
+                    sel_mask = sub_mask
+            else:
+                sel_mask = self._filter_visible(self.selection) != 0
+                if sub_mask is not None:
+                    sel_mask = np.logical_or(sel_mask, sub_mask)
+            mask = np.logical_and(mask, sel_mask)
+        return mask
+
     # Shapes
     def get_shapes(self):
         """


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
@@ -1,4 +1,5 @@
 # pylint: disable=missing-docstring,too-many-lines,too-many-public-methods
+# pylint: disable=protected-access
 from unittest.mock import patch, Mock
 import numpy as np
 
@@ -729,6 +730,200 @@ def test_labels(self):
             self.assertEqual(label.x(), x[ind])
             self.assertEqual(label.y(), y[ind])
 
+    def test_label_mask_all_visible(self):
+        graph = self.graph
+
+        x, y = np.arange(10) / 10, np.arange(10) / 10
+        sel = np.array(
+            [True, True, False, False, False, True, True, True, False, False])
+        subset = np.array(
+            [True, False, True, True, False, True, True, False, False, False])
+        trues = np.ones(10, dtype=bool)
+
+        np.testing.assert_equal(graph._label_mask(x, y), trues)
+
+        # Selection present, subset is None
+        graph.selection = sel
+        graph.master.get_subset_mask = lambda: None
+
+        graph.label_only_selected = False
+        np.testing.assert_equal(graph._label_mask(x, y), trues)
+
+        graph.label_only_selected = True
+        np.testing.assert_equal(graph._label_mask(x, y), sel)
+
+        # Selection and subset present
+        graph.selection = sel
+        graph.master.get_subset_mask = lambda: subset
+
+        graph.label_only_selected = False
+        np.testing.assert_equal(graph._label_mask(x, y), trues)
+
+        graph.label_only_selected = True
+        np.testing.assert_equal(graph._label_mask(x, y), np.array(
+            [True, True, True, True, False, True, True, True, False, False]
+        ))
+
+        # No selection, subset present
+        graph.selection = None
+        graph.master.get_subset_mask = lambda: subset
+
+        graph.label_only_selected = False
+        np.testing.assert_equal(graph._label_mask(x, y), trues)
+
+        graph.label_only_selected = True
+        np.testing.assert_equal(graph._label_mask(x, y), subset)
+
+        # No selection, no subset
+        graph.selection = None
+        graph.master.get_subset_mask = lambda: None
+
+        graph.label_only_selected = False
+        np.testing.assert_equal(graph._label_mask(x, y), trues)
+
+        graph.label_only_selected = True
+        self.assertIsNone(graph._label_mask(x, y))
+
+    def test_label_mask_with_invisible(self):
+        graph = self.graph
+
+        x, y = np.arange(5, 10) / 10, np.arange(5, 10) / 10
+        sel = np.array(
+            [True, True, False, False, False,  # these 5 are not in the sample
+             True, True, True, False, False])
+        subset = np.array(
+            [True, False, True, True, False,  # these 5 are not in the sample
+             True, True, False, False, True])
+        graph.sample_indices = np.arange(5, 10, dtype=int)
+        trues = np.ones(5, dtype=bool)
+
+        np.testing.assert_equal(graph._label_mask(x, y), trues)
+
+        # Selection present, subset is None
+        graph.selection = sel
+        graph.master.get_subset_mask = lambda: None
+
+        graph.label_only_selected = False
+        np.testing.assert_equal(graph._label_mask(x, y), trues)
+
+        graph.label_only_selected = True
+        np.testing.assert_equal(graph._label_mask(x, y), sel[5:])
+
+        # Selection and subset present
+        graph.selection = sel
+        graph.master.get_subset_mask = lambda: subset
+
+        graph.label_only_selected = False
+        np.testing.assert_equal(graph._label_mask(x, y), trues)
+
+        graph.label_only_selected = True
+        np.testing.assert_equal(
+            graph._label_mask(x, y),
+            np.array([True, True, True, False, True]))
+
+        # No selection, subset present
+        graph.selection = None
+        graph.master.get_subset_mask = lambda: subset
+
+        graph.label_only_selected = False
+        np.testing.assert_equal(graph._label_mask(x, y), trues)
+
+        graph.label_only_selected = True
+        np.testing.assert_equal(graph._label_mask(x, y), subset[5:])
+
+        # No selection, no subset
+        graph.selection = None
+        graph.master.get_subset_mask = lambda: None
+
+        graph.label_only_selected = False
+        np.testing.assert_equal(graph._label_mask(x, y), trues)
+
+        graph.label_only_selected = True
+        self.assertIsNone(graph._label_mask(x, y))
+
+    def test_label_mask_with_invisible_and_view(self):
+        graph = self.graph
+
+        x, y = np.arange(5, 10) / 10, np.arange(5) / 10
+        sel = np.array(
+            [True, True, False, False, False,  # these 5 are not in the sample
+             True, True, True, False, False])  # first and last out of the view
+        subset = np.array(
+            [True, False, True, True, False,  # these 5 are not in the sample
+             True, True, False, True, True])  # first and last out of the view
+        graph.sample_indices = np.arange(5, 10, dtype=int)
+        graph.view_box.viewRange = lambda: ((0.6, 1), (0, 0.3))
+        viewed = np.array([False, True, True, True, False])
+
+        np.testing.assert_equal(graph._label_mask(x, y), viewed)
+
+        # Selection present, subset is None
+        graph.selection = sel
+        graph.master.get_subset_mask = lambda: None
+
+        graph.label_only_selected = False
+        np.testing.assert_equal(graph._label_mask(x, y), viewed)
+
+        graph.label_only_selected = True
+        np.testing.assert_equal(
+            graph._label_mask(x, y),
+            np.array([False, True, True, False, False]))
+
+        # Selection and subset present
+        graph.selection = sel
+        graph.master.get_subset_mask = lambda: subset
+
+        graph.label_only_selected = False
+        np.testing.assert_equal(graph._label_mask(x, y), viewed)
+
+        graph.label_only_selected = True
+        np.testing.assert_equal(
+            graph._label_mask(x, y),
+            np.array([False, True, True, True, False]))
+
+        # No selection, subset present
+        graph.selection = None
+        graph.master.get_subset_mask = lambda: subset
+
+        graph.label_only_selected = False
+        np.testing.assert_equal(graph._label_mask(x, y), viewed)
+
+        graph.label_only_selected = True
+        np.testing.assert_equal(
+            graph._label_mask(x, y),
+            np.array([False, True, False, True, False]))
+
+        # No selection, no subset
+        graph.selection = None
+        graph.master.get_subset_mask = lambda: None
+
+        graph.label_only_selected = False
+        np.testing.assert_equal(graph._label_mask(x, y), viewed)
+
+        graph.label_only_selected = True
+        self.assertIsNone(graph._label_mask(x, y))
+
+    def test_labels_observes_mask(self):
+        graph = self.graph
+        get_label_data = graph.master.get_label_data
+        graph.reset_graph()
+
+        self.assertEqual(graph.labels, [])
+
+        get_label_data.reset_mock()
+        graph._label_mask = lambda *_: None
+        graph.update_labels()
+        get_label_data.assert_not_called()
+
+        self.master.get_label_data = lambda: \
+            np.array([str(x) for x in range(10)], dtype=object)
+        graph._label_mask = \
+            lambda *_: np.array([False, True, True] + [False] * 7)
+        graph.update_labels()
+        self.assertEqual(
+            [label.textItem.toPlainText() for label in graph.labels],
+            ["1", "2"])
+
     def test_labels_update_coordinates(self):
         graph = self.graph
         self.master.get_label_data = lambda: \