Skip to content

Commit 25f224f

Browse files
authored
Merge pull request #3506 from janezd/label-subset
[ENH] Label subset
2 parents b9597a8 + 8bc0b3d commit 25f224f

File tree

3 files changed

+230
-20
lines changed

3 files changed

+230
-20
lines changed

Orange/widgets/utils/plot/owplotgui.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,7 @@ def regression_line_check_box(self, widget):
616616

617617
def label_only_selected_check_box(self, widget):
618618
self._check_box(widget=widget, value="label_only_selected",
619-
label="Label only selected points",
619+
label="Label only selection and subset",
620620
cb_name=self._plot.update_labels)
621621

622622
def filled_symbols_check_box(self, widget):

Orange/widgets/visualize/owscatterplotgraph.py

Lines changed: 34 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1024,25 +1024,22 @@ def update_labels(self):
10241024
for label in self.labels:
10251025
self.plot_widget.removeItem(label)
10261026
self.labels = []
1027-
if self.scatterplot_item is None \
1028-
or self.label_only_selected and self.selection is None:
1029-
self._signal_too_many_labels(False)
1030-
return
1031-
labels = self.get_labels()
1032-
if labels is None:
1033-
self._signal_too_many_labels(False)
1034-
return
1035-
(x0, x1), (y0, y1) = self.view_box.viewRange()
1036-
x, y = self.scatterplot_item.getData()
1037-
mask = np.logical_and(
1038-
np.logical_and(x >= x0, x <= x1),
1039-
np.logical_and(y >= y0, y <= y1))
1040-
if self.label_only_selected:
1041-
mask = np.logical_and(
1042-
mask, self._filter_visible(self.selection) != 0)
1043-
if mask.sum() > self.MAX_VISIBLE_LABELS:
1044-
self._signal_too_many_labels(True)
1027+
1028+
mask = None
1029+
if self.scatterplot_item is not None:
1030+
x, y = self.scatterplot_item.getData()
1031+
mask = self._label_mask(x, y)
1032+
1033+
if mask is not None:
1034+
labels = self.get_labels()
1035+
if labels is None:
1036+
mask = None
1037+
1038+
self._signal_too_many_labels(
1039+
mask is not None and mask.sum() > self.MAX_VISIBLE_LABELS)
1040+
if self._too_many_labels or mask is None or not np.any(mask):
10451041
return
1042+
10461043
black = pg.mkColor(0, 0, 0)
10471044
labels = labels[mask]
10481045
x = x[mask]
@@ -1052,13 +1049,31 @@ def update_labels(self):
10521049
ti.setPos(xp, yp)
10531050
self.plot_widget.addItem(ti)
10541051
self.labels.append(ti)
1055-
self._signal_too_many_labels(False)
10561052

10571053
def _signal_too_many_labels(self, too_many):
10581054
if self._too_many_labels != too_many:
10591055
self._too_many_labels = too_many
10601056
self.too_many_labels.emit(too_many)
10611057

1058+
def _label_mask(self, x, y):
1059+
(x0, x1), (y0, y1) = self.view_box.viewRange()
1060+
mask = np.logical_and(
1061+
np.logical_and(x >= x0, x <= x1),
1062+
np.logical_and(y >= y0, y <= y1))
1063+
if self.label_only_selected:
1064+
sub_mask = self._filter_visible(self.master.get_subset_mask())
1065+
if self.selection is None:
1066+
if sub_mask is None:
1067+
return None
1068+
else:
1069+
sel_mask = sub_mask
1070+
else:
1071+
sel_mask = self._filter_visible(self.selection) != 0
1072+
if sub_mask is not None:
1073+
sel_mask = np.logical_or(sel_mask, sub_mask)
1074+
mask = np.logical_and(mask, sel_mask)
1075+
return mask
1076+
10621077
# Shapes
10631078
def get_shapes(self):
10641079
"""

Orange/widgets/visualize/tests/test_owscatterplotbase.py

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
# pylint: disable=missing-docstring,too-many-lines,too-many-public-methods
2+
# pylint: disable=protected-access
23
from unittest.mock import patch, Mock
34
import numpy as np
45

@@ -729,6 +730,200 @@ def test_labels(self):
729730
self.assertEqual(label.x(), x[ind])
730731
self.assertEqual(label.y(), y[ind])
731732

733+
def test_label_mask_all_visible(self):
734+
graph = self.graph
735+
736+
x, y = np.arange(10) / 10, np.arange(10) / 10
737+
sel = np.array(
738+
[True, True, False, False, False, True, True, True, False, False])
739+
subset = np.array(
740+
[True, False, True, True, False, True, True, False, False, False])
741+
trues = np.ones(10, dtype=bool)
742+
743+
np.testing.assert_equal(graph._label_mask(x, y), trues)
744+
745+
# Selection present, subset is None
746+
graph.selection = sel
747+
graph.master.get_subset_mask = lambda: None
748+
749+
graph.label_only_selected = False
750+
np.testing.assert_equal(graph._label_mask(x, y), trues)
751+
752+
graph.label_only_selected = True
753+
np.testing.assert_equal(graph._label_mask(x, y), sel)
754+
755+
# Selection and subset present
756+
graph.selection = sel
757+
graph.master.get_subset_mask = lambda: subset
758+
759+
graph.label_only_selected = False
760+
np.testing.assert_equal(graph._label_mask(x, y), trues)
761+
762+
graph.label_only_selected = True
763+
np.testing.assert_equal(graph._label_mask(x, y), np.array(
764+
[True, True, True, True, False, True, True, True, False, False]
765+
))
766+
767+
# No selection, subset present
768+
graph.selection = None
769+
graph.master.get_subset_mask = lambda: subset
770+
771+
graph.label_only_selected = False
772+
np.testing.assert_equal(graph._label_mask(x, y), trues)
773+
774+
graph.label_only_selected = True
775+
np.testing.assert_equal(graph._label_mask(x, y), subset)
776+
777+
# No selection, no subset
778+
graph.selection = None
779+
graph.master.get_subset_mask = lambda: None
780+
781+
graph.label_only_selected = False
782+
np.testing.assert_equal(graph._label_mask(x, y), trues)
783+
784+
graph.label_only_selected = True
785+
self.assertIsNone(graph._label_mask(x, y))
786+
787+
def test_label_mask_with_invisible(self):
788+
graph = self.graph
789+
790+
x, y = np.arange(5, 10) / 10, np.arange(5, 10) / 10
791+
sel = np.array(
792+
[True, True, False, False, False, # these 5 are not in the sample
793+
True, True, True, False, False])
794+
subset = np.array(
795+
[True, False, True, True, False, # these 5 are not in the sample
796+
True, True, False, False, True])
797+
graph.sample_indices = np.arange(5, 10, dtype=int)
798+
trues = np.ones(5, dtype=bool)
799+
800+
np.testing.assert_equal(graph._label_mask(x, y), trues)
801+
802+
# Selection present, subset is None
803+
graph.selection = sel
804+
graph.master.get_subset_mask = lambda: None
805+
806+
graph.label_only_selected = False
807+
np.testing.assert_equal(graph._label_mask(x, y), trues)
808+
809+
graph.label_only_selected = True
810+
np.testing.assert_equal(graph._label_mask(x, y), sel[5:])
811+
812+
# Selection and subset present
813+
graph.selection = sel
814+
graph.master.get_subset_mask = lambda: subset
815+
816+
graph.label_only_selected = False
817+
np.testing.assert_equal(graph._label_mask(x, y), trues)
818+
819+
graph.label_only_selected = True
820+
np.testing.assert_equal(
821+
graph._label_mask(x, y),
822+
np.array([True, True, True, False, True]))
823+
824+
# No selection, subset present
825+
graph.selection = None
826+
graph.master.get_subset_mask = lambda: subset
827+
828+
graph.label_only_selected = False
829+
np.testing.assert_equal(graph._label_mask(x, y), trues)
830+
831+
graph.label_only_selected = True
832+
np.testing.assert_equal(graph._label_mask(x, y), subset[5:])
833+
834+
# No selection, no subset
835+
graph.selection = None
836+
graph.master.get_subset_mask = lambda: None
837+
838+
graph.label_only_selected = False
839+
np.testing.assert_equal(graph._label_mask(x, y), trues)
840+
841+
graph.label_only_selected = True
842+
self.assertIsNone(graph._label_mask(x, y))
843+
844+
def test_label_mask_with_invisible_and_view(self):
845+
graph = self.graph
846+
847+
x, y = np.arange(5, 10) / 10, np.arange(5) / 10
848+
sel = np.array(
849+
[True, True, False, False, False, # these 5 are not in the sample
850+
True, True, True, False, False]) # first and last out of the view
851+
subset = np.array(
852+
[True, False, True, True, False, # these 5 are not in the sample
853+
True, True, False, True, True]) # first and last out of the view
854+
graph.sample_indices = np.arange(5, 10, dtype=int)
855+
graph.view_box.viewRange = lambda: ((0.6, 1), (0, 0.3))
856+
viewed = np.array([False, True, True, True, False])
857+
858+
np.testing.assert_equal(graph._label_mask(x, y), viewed)
859+
860+
# Selection present, subset is None
861+
graph.selection = sel
862+
graph.master.get_subset_mask = lambda: None
863+
864+
graph.label_only_selected = False
865+
np.testing.assert_equal(graph._label_mask(x, y), viewed)
866+
867+
graph.label_only_selected = True
868+
np.testing.assert_equal(
869+
graph._label_mask(x, y),
870+
np.array([False, True, True, False, False]))
871+
872+
# Selection and subset present
873+
graph.selection = sel
874+
graph.master.get_subset_mask = lambda: subset
875+
876+
graph.label_only_selected = False
877+
np.testing.assert_equal(graph._label_mask(x, y), viewed)
878+
879+
graph.label_only_selected = True
880+
np.testing.assert_equal(
881+
graph._label_mask(x, y),
882+
np.array([False, True, True, True, False]))
883+
884+
# No selection, subset present
885+
graph.selection = None
886+
graph.master.get_subset_mask = lambda: subset
887+
888+
graph.label_only_selected = False
889+
np.testing.assert_equal(graph._label_mask(x, y), viewed)
890+
891+
graph.label_only_selected = True
892+
np.testing.assert_equal(
893+
graph._label_mask(x, y),
894+
np.array([False, True, False, True, False]))
895+
896+
# No selection, no subset
897+
graph.selection = None
898+
graph.master.get_subset_mask = lambda: None
899+
900+
graph.label_only_selected = False
901+
np.testing.assert_equal(graph._label_mask(x, y), viewed)
902+
903+
graph.label_only_selected = True
904+
self.assertIsNone(graph._label_mask(x, y))
905+
906+
def test_labels_observes_mask(self):
907+
graph = self.graph
908+
get_label_data = graph.master.get_label_data
909+
graph.reset_graph()
910+
911+
self.assertEqual(graph.labels, [])
912+
913+
get_label_data.reset_mock()
914+
graph._label_mask = lambda *_: None
915+
graph.update_labels()
916+
get_label_data.assert_not_called()
917+
918+
self.master.get_label_data = lambda: \
919+
np.array([str(x) for x in range(10)], dtype=object)
920+
graph._label_mask = \
921+
lambda *_: np.array([False, True, True] + [False] * 7)
922+
graph.update_labels()
923+
self.assertEqual(
924+
[label.textItem.toPlainText() for label in graph.labels],
925+
["1", "2"])
926+
732927
def test_labels_update_coordinates(self):
733928
graph = self.graph
734929
self.master.get_label_data = lambda: \

0 commit comments

Comments
 (0)