diff --git a/Orange/widgets/utils/plot/owplotgui.py b/Orange/widgets/utils/plot/owplotgui.py index 2d74e5abd7b..eaa49305c3d 100644 --- 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 index 785da69e399..2f474a3f311 100644 --- 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): """ diff --git a/Orange/widgets/visualize/tests/test_owscatterplotbase.py b/Orange/widgets/visualize/tests/test_owscatterplotbase.py index 498ee82b3ef..abe4f3dd7d7 100644 --- 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: \