diff --git a/Orange/widgets/visualize/owscatterplotgraph.py b/Orange/widgets/visualize/owscatterplotgraph.py index 96783a0a95d..4497abadfa9 100644 --- a/Orange/widgets/visualize/owscatterplotgraph.py +++ b/Orange/widgets/visualize/owscatterplotgraph.py @@ -6,7 +6,7 @@ import numpy as np -from AnyQt.QtCore import Qt, QRectF, QSize +from AnyQt.QtCore import Qt, QRectF, QSize, QTimer from AnyQt.QtGui import ( QStaticText, QColor, QPen, QBrush, QPainterPath, QTransform, QPainter ) @@ -405,6 +405,8 @@ def __init__(self, scatter_widget, parent=None, view_box=ViewBox): self.plot_widget.scene().installEventFilter(self._tooltip_delegate) self.view_box.sigTransformChanged.connect(self.update_density) + self.timer = None + def _create_legend(self, anchor): legend = LegendItem() legend.setParentItem(self.plot_widget.getViewBox()) @@ -485,6 +487,9 @@ def clear(self): self.plot_widget.clear() self.density_img = None + if self.timer is not None and self.timer.isActive(): + self.timer.stop() + self.timer = None self.scatterplot_item = None self.scatterplot_item_sel = None self.labels = [] @@ -704,8 +709,10 @@ def get_sizes(self): self.MinShapeSize + (5 + self.point_width) * 0.5) size_column = self._filter_visible(size_column) size_column = size_column.copy() - size_column -= np.nanmin(size_column) - mx = np.nanmax(size_column) + with warnings.catch_warnings(): + warnings.simplefilter("ignore", category=RuntimeWarning) + size_column -= np.nanmin(size_column) + mx = np.nanmax(size_column) if mx > 0: size_column /= mx else: @@ -725,8 +732,42 @@ def update_sizes(self): size_imputer = getattr( self.master, "impute_sizes", self.default_impute_sizes) size_imputer(size_data) - self.scatterplot_item.setSize(size_data) - self.scatterplot_item_sel.setSize(size_data + SELECTION_WIDTH) + + if self.timer is not None and self.timer.isActive(): + self.timer.stop() + self.timer = None + + current_size_data = self.scatterplot_item.data["size"].copy() + diff = size_data - current_size_data + widget = self + + class Timeout: + # 0.5 - np.cos(np.arange(0.17, 1, 0.17) * np.pi) / 2 + factors = [0.07, 0.26, 0.52, 0.77, 0.95, 1] + + def __init__(self): + self._counter = 0 + + def __call__(self): + factor = self.factors[self._counter] + self._counter += 1 + size = current_size_data + diff * factor + if len(self.factors) == self._counter: + widget.timer.stop() + widget.timer = None + size = size_data + widget.scatterplot_item.setSize(size) + widget.scatterplot_item_sel.setSize(size + SELECTION_WIDTH) + + if np.sum(current_size_data) / self.n_valid != -1 and np.sum(diff): + # If encountered any strange behaviour when updating sizes, + # implement it with threads + self.timer = QTimer(self.scatterplot_item, interval=50) + self.timer.timeout.connect(Timeout()) + self.timer.start() + else: + self.scatterplot_item.setSize(size_data) + self.scatterplot_item_sel.setSize(size_data + SELECTION_WIDTH) update_point_size = update_sizes # backward compatibility (needed?!) update_size = update_sizes diff --git a/Orange/widgets/visualize/tests/test_owscatterplotbase.py b/Orange/widgets/visualize/tests/test_owscatterplotbase.py index e966a4fc664..dd6cd626b41 100644 --- a/Orange/widgets/visualize/tests/test_owscatterplotbase.py +++ b/Orange/widgets/visualize/tests/test_owscatterplotbase.py @@ -8,7 +8,8 @@ from pyqtgraph import mkPen -from Orange.widgets.tests.base import GuiTest +from Orange.widgets.settings import SettingProvider +from Orange.widgets.tests.base import WidgetTest from Orange.widgets.utils.colorpalette import ColorPaletteGenerator, \ ContinuousPaletteGenerator, NAN_GREY from Orange.widgets.visualize.owscatterplotgraph import OWScatterPlotBase, \ @@ -34,6 +35,9 @@ class MockWidget(OWWidget): combined_legend = Mock(return_value=False) selection_changed = Mock(return_value=None) + GRAPH_CLASS = OWScatterPlotBase + graph = SettingProvider(OWScatterPlotBase) + def get_palette(self): if self.is_continuous_color(): return ContinuousPaletteGenerator(Qt.white, Qt.black, False) @@ -41,7 +45,7 @@ def get_palette(self): return ColorPaletteGenerator(12) -class TestOWScatterPlotBase(GuiTest): +class TestOWScatterPlotBase(WidgetTest): def setUp(self): self.master = MockWidget() self.graph = OWScatterPlotBase(self.master) @@ -160,6 +164,8 @@ def test_sampling(self): master.get_label_data = lambda: \ np.array([str(x) for x in d], dtype=object) graph.reset_graph() + self.process_events(until=lambda: not ( + self.graph.timer is not None and self.graph.timer.isActive())) # Check proper sampling scatterplot_item = graph.scatterplot_item @@ -187,6 +193,8 @@ def test_sampling(self): # Check that sample is extended when sample size is changed graph.set_sample_size(4) + self.process_events(until=lambda: not ( + self.graph.timer is not None and self.graph.timer.isActive())) scatterplot_item = graph.scatterplot_item x, y = scatterplot_item.getData() data = scatterplot_item.data @@ -226,6 +234,8 @@ def test_sampling(self): # Enable sampling when data is already present and not sampled graph.set_sample_size(3) + self.process_events(until=lambda: not ( + self.graph.timer is not None and self.graph.timer.isActive())) scatterplot_item = graph.scatterplot_item x, y = scatterplot_item.getData() data = scatterplot_item.data @@ -261,6 +271,8 @@ def test_sampling(self): np.arange(100, 105, dtype=float)) d = self.xy[0] - 100 graph.reset_graph() + self.process_events(until=lambda: not ( + self.graph.timer is not None and self.graph.timer.isActive())) scatterplot_item = graph.scatterplot_item x, y = scatterplot_item.getData() self.assertEqual(len(x), 3) @@ -365,6 +377,8 @@ def test_size_with_nans(self): d[4] = np.nan graph.update_sizes() + self.process_events(until=lambda: not ( + self.graph.timer is not None and self.graph.timer.isActive())) sizes2 = scatterplot_item.data["size"] self.assertEqual(sizes[1] - sizes[0], sizes2[1] - sizes2[0]) diff --git a/Orange/widgets/visualize/utils/widget.py b/Orange/widgets/visualize/utils/widget.py index 6362cad4cea..d05c6a33225 100644 --- a/Orange/widgets/visualize/utils/widget.py +++ b/Orange/widgets/visualize/utils/widget.py @@ -583,6 +583,7 @@ def onDeleteWidget(self): super().onDeleteWidget() self.graph.plot_widget.getViewBox().deleteLater() self.graph.plot_widget.clear() + self.graph.clear() class OWAnchorProjectionWidget(OWDataProjectionWidget):