Skip to content

Commit

Permalink
Merge pull request #3469 from janezd/hide-too-many-labels
Browse files Browse the repository at this point in the history
OWScatterPlotBase: Hide labels if there are too many
  • Loading branch information
VesnaT authored Dec 18, 2018
2 parents 9614cbc + ae720d0 commit c5528ed
Show file tree
Hide file tree
Showing 4 changed files with 113 additions and 28 deletions.
56 changes: 36 additions & 20 deletions Orange/widgets/visualize/owscatterplotgraph.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@
from math import log10, floor, ceil

import numpy as np

from AnyQt.QtCore import Qt, QRectF, QSize, QTimer
from AnyQt.QtCore import Qt, QRectF, QSize, QTimer, pyqtSignal as Signal, \
QObject
from AnyQt.QtGui import (
QStaticText, QColor, QPen, QBrush, QPainterPath, QTransform, QPainter
)
Expand Down Expand Up @@ -251,7 +251,7 @@ def _make_pen(color, width):
return p


class OWScatterPlotBase(gui.OWComponent):
class OWScatterPlotBase(gui.OWComponent, QObject):
"""
Provide a graph component for widgets that show any kind of point plot
Expand Down Expand Up @@ -338,6 +338,8 @@ def get_size_data(self):
to the entire set etc. Internally, sampling happens as early as possible
(in methods `get_<something>`).
"""
too_many_labels = Signal(bool)

label_only_selected = Setting(False)
point_width = Setting(10)
alpha_value = Setting(128)
Expand All @@ -357,8 +359,11 @@ def get_size_data(self):
COLOR_SUBSET = (128, 128, 128, 255)
COLOR_DEFAULT = (128, 128, 128, 0)

MAX_VISIBLE_LABELS = 500

def __init__(self, scatter_widget, parent=None, view_box=ViewBox):
super().__init__(scatter_widget)
QObject.__init__(self)
gui.OWComponent.__init__(self, scatter_widget)

self.subset_is_shown = False

Expand Down Expand Up @@ -393,6 +398,7 @@ def __init__(self, scatter_widget, parent=None, view_box=ViewBox):
self.update_legend_visibility()

self.scale = None # DiscretizedScale
self._too_many_labels = False

# self.setMouseTracking(True)
# self.grabGesture(QPinchGesture)
Expand All @@ -403,6 +409,7 @@ def __init__(self, scatter_widget, parent=None, view_box=ViewBox):
self._tooltip_delegate = EventDelegate(self.help_event)
self.plot_widget.scene().installEventFilter(self._tooltip_delegate)
self.view_box.sigTransformChanged.connect(self.update_density)
self.view_box.sigRangeChangedManually.connect(self.update_labels)

self.timer = None

Expand Down Expand Up @@ -463,7 +470,7 @@ def update_jittering(self):
return
self._update_plot_coordinates(self.scatterplot_item, x, y)
self._update_plot_coordinates(self.scatterplot_item_sel, x, y)
self._update_label_coords(x, y)
self.update_labels()

# TODO: Rename to remove_plot_items
def clear(self):
Expand Down Expand Up @@ -492,6 +499,7 @@ def clear(self):
self.scatterplot_item = None
self.scatterplot_item_sel = None
self.labels = []
self._signal_too_many_labels(False)
self.view_box.init_history()
self.view_box.tag_history()

Expand Down Expand Up @@ -686,8 +694,8 @@ def update_coordinates(self):
else:
self._update_plot_coordinates(self.scatterplot_item, x, y)
self._update_plot_coordinates(self.scatterplot_item_sel, x, y)
self.update_labels()

self._update_label_coords(x, y)
self.update_density() # Todo: doesn't work: try MDS with density on
self._reset_view(x, y)

Expand Down Expand Up @@ -1007,7 +1015,7 @@ def get_labels(self):

def update_labels(self):
"""
Trigger an updaet of labels
Trigger an update of labels
The method calls `get_labels` which in turn calls the widget's
`get_label_data`. The obtained labels are shown if the corresponding
Expand All @@ -1018,31 +1026,38 @@ def update_labels(self):
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
black = pg.mkColor(0, 0, 0)
(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:
selected = np.nonzero(self._filter_visible(self.selection))
labels = labels[selected]
x = x[selected]
y = y[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)
return
black = pg.mkColor(0, 0, 0)
labels = labels[mask]
x = x[mask]
y = y[mask]
for label, xp, yp in zip(labels, x, y):
ti = TextItem(label, black)
ti.setPos(xp, yp)
self.plot_widget.addItem(ti)
self.labels.append(ti)
self._signal_too_many_labels(False)

def _update_label_coords(self, x, y):
"""Update label coordinates"""
if self.label_only_selected:
selected = np.nonzero(self._filter_visible(self.selection))
x = x[selected]
y = y[selected]
for label, xp, yp in zip(self.labels, x, y):
label.setPos(xp, yp)
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)

# Shapes
def get_shapes(self):
Expand Down Expand Up @@ -1194,6 +1209,7 @@ def select_button_clicked(self):

def reset_button_clicked(self):
self.plot_widget.getViewBox().autoRange()
self.update_labels()

def select_by_click(self, _, points):
if self.scatterplot_item is not None:
Expand Down
10 changes: 10 additions & 0 deletions Orange/widgets/visualize/tests/test_owprojectionwidget.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,3 +154,13 @@ def test_saved_selection(self):
self.send_signal(self.widget.Inputs.data, self.data, widget=w)
self.assertEqual(np.sum(w.graph.selection), 15)
np.testing.assert_equal(self.widget.graph.selection, w.graph.selection)

def test_too_many_labels(self):
w = self.widget.Warning.too_many_labels
self.assertFalse(w.is_shown())

self.widget.graph.too_many_labels.emit(True)
self.assertTrue(w.is_shown())

self.widget.graph.too_many_labels.emit(False)
self.assertFalse(w.is_shown())
69 changes: 61 additions & 8 deletions Orange/widgets/visualize/tests/test_owscatterplotbase.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# Test methods with long descriptive names can omit docstrings
# pylint: disable=missing-docstring
# pylint: disable=missing-docstring,too-many-lines,too-many-public-methods
from unittest.mock import patch, Mock
import numpy as np

from AnyQt.QtCore import QRectF, Qt
from AnyQt.QtGui import QColor
from AnyQt.QtTest import QSignalSpy

from pyqtgraph import mkPen

Expand Down Expand Up @@ -104,18 +104,21 @@ def test_update_coordinates(self):

def test_update_coordinates_and_labels(self):
graph = self.graph
xy = self.xy = (np.array([1, 2]), np.array([3, 4]))
self.master.get_label_data = lambda: ["a", "b"]
xy = self.xy = (np.array([1., 2]), np.array([3, 4]))
self.master.get_label_data = lambda: np.array(["a", "b"])
graph.reset_graph()
self.assertEqual(graph.labels[0].pos().x(), 1)
xy[0][0] = 0
xy[0][0] = 1.5
graph.update_coordinates()
self.assertEqual(graph.labels[0].pos().x(), 0)
self.assertEqual(graph.labels[0].pos().x(), 1.5)
xy[0][0] = 0 # This label goes out of the range
graph.update_coordinates()
self.assertEqual(graph.labels[0].pos().x(), 2)

def test_update_coordinates_and_density(self):
graph = self.graph
xy = self.xy = (np.array([1, 2]), np.array([3, 4]))
self.master.get_label_data = lambda: ["a", "b"]
self.master.get_label_data = lambda: np.array(["a", "b"])
graph.reset_graph()
self.assertEqual(graph.labels[0].pos().x(), 1)
xy[0][0] = 0
Expand All @@ -127,7 +130,7 @@ def test_update_coordinates_reset_view(self):
graph = self.graph
graph.view_box.setRange = self.setRange
xy = self.xy = (np.array([2, 1]), np.array([3, 10]))
self.master.get_label_data = lambda: ["a", "b"]
self.master.get_label_data = lambda: np.array(["a", "b"])
graph.reset_graph()
self.assertEqual(self.last_setRange, [[1, 2], [3, 10]])

Expand Down Expand Up @@ -969,6 +972,56 @@ def test_unselect_all(self):
graph.update_labels.assert_not_called()
self.master.selection_changed.assert_not_called()

def test_hiding_too_many_labels(self):
spy = QSignalSpy(self.graph.too_many_labels)
self.graph.MAX_VISIBLE_LABELS = 5

graph = self.graph
coords = np.array(
[(x, 0) for x in range(10)], dtype=float).T
self.master.get_coordinates_data = lambda: coords
graph.reset_graph()

self.assertFalse(spy and spy[-1][0])

self.master.get_label_data = lambda: \
np.array([str(x) for x in range(10)], dtype=object)
graph.update_labels()
self.assertTrue(spy[-1][0])
self.assertFalse(bool(self.graph.labels))

graph.view_box.setRange(QRectF(1, -1, 4, 4))
graph.view_box.sigRangeChangedManually.emit(((1, 5), (-1, 3)))
self.assertFalse(spy[-1][0])
self.assertTrue(bool(self.graph.labels))

graph.view_box.setRange(QRectF(1, -1, 8, 8))
graph.view_box.sigRangeChangedManually.emit(((1, 9), (-1, 7)))
self.assertTrue(spy[-1][0])
self.assertFalse(bool(self.graph.labels))

graph.label_only_selected = True
graph.update_labels()
self.assertFalse(spy[-1][0])
self.assertFalse(bool(self.graph.labels))

graph.selection_select([1, 2, 3, 4, 5, 6])
self.assertTrue(spy[-1][0])
self.assertFalse(bool(self.graph.labels))

graph.selection_select([1, 2, 3])
self.assertFalse(spy[-1][0])
self.assertTrue(bool(self.graph.labels))

graph.label_only_selected = False
graph.update_labels()
self.assertTrue(spy[-1][0])
self.assertFalse(bool(self.graph.labels))

graph.clear()
self.assertFalse(spy[-1][0])
self.assertFalse(bool(self.graph.labels))


if __name__ == "__main__":
import unittest
Expand Down
6 changes: 6 additions & 0 deletions Orange/widgets/visualize/utils/widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,10 @@ class Outputs:
selected_data = Output("Selected Data", Table, default=True)
annotated_data = Output(ANNOTATED_DATA_SIGNAL_NAME, Table)

class Warning(OWProjectionWidgetBase.Warning):
too_many_labels = Msg(
"Too many labels to show (zoom in or label only selected)")

settingsHandler = DomainContextHandler()
selection = Setting(None, schema_only=True)
auto_commit = Setting(True)
Expand All @@ -386,6 +390,8 @@ def _add_graph(self):
box = gui.vBox(self.mainArea, True, margin=0)
self.graph = self.GRAPH_CLASS(self, box)
box.layout().addWidget(self.graph.plot_widget)
self.graph.too_many_labels.connect(
lambda too_many: self.Warning.too_many_labels(shown=too_many))

def _add_controls(self):
self.gui = OWPlotGUI(self)
Expand Down

0 comments on commit c5528ed

Please sign in to comment.