diff --git a/Orange/widgets/tests/base.py b/Orange/widgets/tests/base.py index c0b9b7ec85c..fab6e0fe98a 100644 --- a/Orange/widgets/tests/base.py +++ b/Orange/widgets/tests/base.py @@ -894,13 +894,22 @@ def test_none_data(self): """Test widget for empty dataset""" self.send_signal(self.widget.Inputs.data, self.data[:0]) - def test_subset_data(self, timeout=DEFAULT_TIMEOUT): - """Test widget for subset data""" + def test_plot_once(self, timeout=DEFAULT_TIMEOUT): + """Test if data is plotted only once but committed on every input change""" + self.widget.setup_plot = Mock() + self.widget.commit = Mock() self.send_signal(self.widget.Inputs.data, self.data) + self.widget.setup_plot.assert_called_once() + self.widget.commit.assert_called_once() + if self.widget.isBlocking(): spy = QSignalSpy(self.widget.blockingStateChanged) self.assertTrue(spy.wait(timeout)) + + self.widget.commit.reset_mock() self.send_signal(self.widget.Inputs.data_subset, self.data[::10]) + self.widget.setup_plot.assert_called_once() + self.widget.commit.assert_called_once() def test_class_density(self, timeout=DEFAULT_TIMEOUT): """Check class density update""" @@ -932,6 +941,24 @@ def test_sparse_data(self, timeout=DEFAULT_TIMEOUT): self.send_signal(self.widget.Inputs.data_subset, table[::30]) self.assertEqual(len(self.widget.subset_indices), 5) + def test_invalidated_embedding(self, timeout=DEFAULT_TIMEOUT): + """Check if graph has been replotted when sending same data""" + self.widget.graph.update_coordinates = Mock() + self.widget.graph.update_point_props = Mock() + self.send_signal(self.widget.Inputs.data, self.data) + self.widget.graph.update_coordinates.assert_called_once() + self.widget.graph.update_point_props.assert_called_once() + + if self.widget.isBlocking(): + spy = QSignalSpy(self.widget.blockingStateChanged) + self.assertTrue(spy.wait(timeout)) + + self.widget.graph.update_coordinates.reset_mock() + self.widget.graph.update_point_props.reset_mock() + self.send_signal(self.widget.Inputs.data, self.data) + self.widget.graph.update_coordinates.assert_not_called() + self.widget.graph.update_point_props.assert_called_once() + def test_send_report(self, timeout=DEFAULT_TIMEOUT): """Test report """ self.send_signal(self.widget.Inputs.data, self.data) diff --git a/Orange/widgets/unsupervised/owmds.py b/Orange/widgets/unsupervised/owmds.py index fce83ea83ae..ff1cf2c484a 100644 --- a/Orange/widgets/unsupervised/owmds.py +++ b/Orange/widgets/unsupervised/owmds.py @@ -156,7 +156,7 @@ def __init__(self): #: Input data table self.signal_data = None - self._invalidated = False + self.__invalidated = True self.embedding = None self.effective_matrix = None @@ -212,15 +212,6 @@ def set_data(self, data): self.signal_data = data - if self.matrix is not None and data is not None and \ - len(self.matrix) == len(data): - self.closeContext() - self.data = data - self.init_attr_values() - self.openContext(data) - else: - self._invalidated = True - @Inputs.distances def set_disimilarity(self, matrix): """Set the dissimilarity (distance) matrix. @@ -238,30 +229,33 @@ def set_disimilarity(self, matrix): self.matrix = matrix self.matrix_data = matrix.row_items if matrix is not None else None - self._invalidated = True def clear(self): super().clear() self.embedding = None - self.effective_matrix = None self.graph.set_effective_matrix(None) self.__set_update_loop(None) self.__state = OWMDS.Waiting def _initialize(self): + matrix_existed = self.effective_matrix is not None + effective_matrix = self.effective_matrix + self.__invalidated = True + self.data = None + self.effective_matrix = None self.closeContext() - self.clear() self.clear_messages() # if no data nor matrix is present reset plot if self.signal_data is None and self.matrix is None: - self.data = None + self.clear() self.init_attr_values() return if self.signal_data is not None and self.matrix is not None and \ len(self.signal_data) != len(self.matrix): self.Error.mismatching_dimensions() + self.clear() self.init_attr_values() return @@ -279,11 +273,18 @@ def _initialize(self): self.effective_matrix = Euclidean(preprocessed_data) else: self.Error.no_attributes() + self.clear() self.init_attr_values() return self.init_attr_values() self.openContext(self.data) + self.__invalidated = not (matrix_existed and + self.effective_matrix is not None and + np.array_equal(effective_matrix, + self.effective_matrix)) + if self.__invalidated: + self.clear() self.graph.set_effective_matrix(self.effective_matrix) def _toggle_run(self): @@ -407,7 +408,6 @@ def __next_step(self): self.__set_update_loop(None) self.unconditional_commit() self.graph.resume_drawing_pairs() - self.graph.update_coordinates() except MemoryError: self.Error.out_of_memory() self.__set_update_loop(None) @@ -446,6 +446,7 @@ def jitter_coord(part): self.__set_update_loop(None) if self.effective_matrix is None: + self.graph.reset_graph() return X = self.effective_matrix @@ -477,15 +478,16 @@ def __invalidate_refresh(self): self.__start() def handleNewSignals(self): - if self._invalidated: + self._initialize() + if self.__invalidated: self.graph.pause_drawing_pairs() - self._invalidated = False - self._initialize() + self.__invalidated = False self.__invalidate_embedding() self.cb_class_density.setEnabled(self.can_draw_density()) self.start() - - super().handleNewSignals() + else: + self.graph.update_point_props() + self.commit() def _invalidate_output(self): self.commit() diff --git a/Orange/widgets/unsupervised/owtsne.py b/Orange/widgets/unsupervised/owtsne.py index db09b162e9e..7e9ad043974 100644 --- a/Orange/widgets/unsupervised/owtsne.py +++ b/Orange/widgets/unsupervised/owtsne.py @@ -66,7 +66,6 @@ def __init__(self): super().__init__() self.pca_data = None self.projection = None - self.__invalidated = True self.__update_loop = None # timer for scheduling updates self.__timer = QTimer(self, singleShot=True, interval=1, @@ -112,11 +111,6 @@ def _add_controls_start_box(self): gui.hSlider(box, self, "pca_components", label="PCA components:", minValue=2, maxValue=50, step=1) - def set_data(self, data): - self.__invalidated = not (self.data and data and - np.array_equal(self.data.X, data.X)) - super().set_data(data) - def check_data(self): def error(err): err() @@ -251,14 +245,9 @@ def __next_step(self): self.__in_next_step = False - def handleNewSignals(self): - if self.__invalidated: - self.__invalidated = False - self.setup_plot() - self.start() - else: - self.graph.update_point_props() - self.commit() + def setup_plot(self): + super().setup_plot() + self.start() def commit(self): super().commit() @@ -281,12 +270,11 @@ def send_preprocessor(self): self.Outputs.preprocessor.send(prep) def clear(self): - if self.__invalidated: - super().clear() - self.__set_update_loop(None) - self.__state = OWtSNE.Waiting - self.pca_data = None - self.projection = None + super().clear() + self.__set_update_loop(None) + self.__state = OWtSNE.Waiting + self.pca_data = None + self.projection = None @classmethod def migrate_settings(cls, settings, version): diff --git a/Orange/widgets/unsupervised/tests/test_owtsne.py b/Orange/widgets/unsupervised/tests/test_owtsne.py index e735cba82ea..05e15d901f4 100644 --- a/Orange/widgets/unsupervised/tests/test_owtsne.py +++ b/Orange/widgets/unsupervised/tests/test_owtsne.py @@ -1,8 +1,6 @@ import unittest import numpy as np -from AnyQt.QtTest import QSignalSpy - from Orange.data import DiscreteVariable, ContinuousVariable, Domain, Table from Orange.preprocess import Preprocess from Orange.widgets.tests.base import ( @@ -98,23 +96,6 @@ def test_output_preprocessor(self): self.assertEqual([a.name for a in transformed.domain.attributes], [m.name for m in output.domain.metas[:2]]) - def test_invalidated_embedding(self): - self.widget.graph.update_coordinates = unittest.mock.Mock() - self.widget.graph.update_point_props = unittest.mock.Mock() - self.send_signal(self.widget.Inputs.data, self.data) - self.widget.graph.update_coordinates.assert_called_once() - self.widget.graph.update_point_props.assert_called_once() - - if self.widget.isBlocking(): - spy = QSignalSpy(self.widget.blockingStateChanged) - self.assertTrue(spy.wait(5000)) - - self.widget.graph.update_coordinates.reset_mock() - self.widget.graph.update_point_props.reset_mock() - self.send_signal(self.widget.Inputs.data, self.data) - self.widget.graph.update_coordinates.assert_not_called() - self.widget.graph.update_point_props.assert_called_once() - if __name__ == '__main__': unittest.main() diff --git a/Orange/widgets/utils/itemmodels.py b/Orange/widgets/utils/itemmodels.py index 6f22918243a..c33d3aad805 100644 --- a/Orange/widgets/utils/itemmodels.py +++ b/Orange/widgets/utils/itemmodels.py @@ -496,6 +496,7 @@ class PyListModel(QAbstractListModel): """ MIME_TYPE = "application/x-Orange-PyListModelData" Separator = object() + removed = Signal() def __init__(self, iterable=None, parent=None, flags=Qt.ItemIsSelectable | Qt.ItemIsEnabled, @@ -616,6 +617,7 @@ def removeRows(self, row, count, parent=QModelIndex()): """ if not parent.isValid(): del self[row:row + count] + self.removed.emit() return True else: return False diff --git a/Orange/widgets/utils/plot/owplotgui.py b/Orange/widgets/utils/plot/owplotgui.py index e3fa8c1bdd9..aee0471331f 100644 --- a/Orange/widgets/utils/plot/owplotgui.py +++ b/Orange/widgets/utils/plot/owplotgui.py @@ -31,7 +31,7 @@ from AnyQt.QtWidgets import QWidget, QToolButton, QVBoxLayout, QHBoxLayout, QGridLayout, QMenu, QAction,\ QDialog, QSizePolicy, QPushButton, QListView, QLabel from AnyQt.QtGui import QIcon, QKeySequence -from AnyQt.QtCore import Qt, pyqtSignal, QPoint, QSize +from AnyQt.QtCore import Qt, pyqtSignal, QPoint, QSize, QObject from Orange.data import ContinuousVariable, DiscreteVariable from Orange.widgets import gui @@ -52,6 +52,8 @@ class AddVariablesDialog(QDialog): + add = pyqtSignal() + def __init__(self, master, model): QDialog.__init__(self) @@ -134,10 +136,16 @@ def add_variables(self): del model[i] self.master.model_selected.extend(variables) + self.add.emit() + +class VariablesSelection(QObject): + added = pyqtSignal() + removed = pyqtSignal() -class VariablesSelection: - def __init__(self, master, model_selected, model_other, widget=None): + def __init__(self, master, model_selected, model_other, + widget=None, parent=None): + super().__init__(parent) self.master = master self.model_selected = model_selected self.model_other = model_other @@ -197,9 +205,11 @@ def __deactivate_selection(self): del model[i] self.model_other.extend(variables) + self.removed.emit() def _action_add(self): self.add_variables_dialog = AddVariablesDialog(self, self.model_other) + self.add_variables_dialog.add.connect(lambda: self.added.emit()) class OrientedWidget(QWidget): diff --git a/Orange/widgets/visualize/owlinearprojection.py b/Orange/widgets/visualize/owlinearprojection.py index da2ba198bdc..67e354a0f00 100644 --- a/Orange/widgets/visualize/owlinearprojection.py +++ b/Orange/widgets/visualize/owlinearprojection.py @@ -271,8 +271,7 @@ class Error(OWAnchorProjectionWidget.Error): def __init__(self): self.model_selected = VariableListModel(enable_dnd=True) - self.model_selected.rowsInserted.connect(self.__model_selected_changed) - self.model_selected.rowsRemoved.connect(self.__model_selected_changed) + self.model_selected.removed.connect(self.__model_selected_changed) self.model_other = VariableListModel(enable_dnd=True) self.vizrank, self.btn_vizrank = LinearProjectionVizRank.add_vizrank( @@ -296,6 +295,8 @@ def _add_controls_variables(self): self.variables_selection = VariablesSelection( self, self.model_selected, self.model_other, self.controlArea ) + self.variables_selection.added.connect(self.__model_selected_changed) + self.variables_selection.removed.connect(self.__model_selected_changed) self.variables_selection.add_remove.layout().addWidget( self.btn_vizrank ) @@ -328,6 +329,7 @@ def __vizrank_set_attrs(self, attrs): self.model_selected[:] = attrs[:] self.model_other[:] = [var for var in self.continuous_variables if var not in attrs] + self.__model_selected_changed() def __model_selected_changed(self): self.selected_vars = [(var.name, vartype(var)) for var @@ -368,6 +370,7 @@ def set_data(self, data): self._check_options() self._init_vizrank() + self.init_projection() def _check_options(self): buttons = self.radio_placement.buttons @@ -423,8 +426,6 @@ def init_attr_values(self): self.selected_vars = [] def init_projection(self): - if not len(self.effective_variables): - return if self.placement == self.Placement.Circular: self.projector = CircularPlacement() elif self.placement == self.Placement.LDA: diff --git a/Orange/widgets/visualize/owradviz.py b/Orange/widgets/visualize/owradviz.py index 630e0e0e178..2cbd6b141d5 100644 --- a/Orange/widgets/visualize/owradviz.py +++ b/Orange/widgets/visualize/owradviz.py @@ -287,8 +287,7 @@ class Error(OWAnchorProjectionWidget.Error): def __init__(self): self.model_selected = VariableListModel(enable_dnd=True) - self.model_selected.rowsInserted.connect(self.__model_selected_changed) - self.model_selected.rowsRemoved.connect(self.__model_selected_changed) + self.model_selected.removed.connect(self.__model_selected_changed) self.model_other = VariableListModel(enable_dnd=True) self.vizrank, self.btn_vizrank = RadvizVizRank.add_vizrank( @@ -300,6 +299,8 @@ def _add_controls(self): self.variables_selection = VariablesSelection( self, self.model_selected, self.model_other, self.controlArea ) + self.variables_selection.added.connect(self.__model_selected_changed) + self.variables_selection.removed.connect(self.__model_selected_changed) self.variables_selection.add_remove.layout().addWidget( self.btn_vizrank ) @@ -325,6 +326,7 @@ def __vizrank_set_attrs(self, attrs): self.model_selected[:] = attrs[:] self.model_other[:] = [var for var in self.primitive_variables if var not in attrs] + self.__model_selected_changed() def __model_selected_changed(self): self.selected_vars = [(var.name, vartype(var)) for var @@ -359,6 +361,7 @@ def set_data(self, data): self.model_other[:] = variables[5:] + class_var self._init_vizrank() + self.init_projection() def _init_vizrank(self): is_enabled = self.data is not None and \ diff --git a/Orange/widgets/visualize/owscatterplot.py b/Orange/widgets/visualize/owscatterplot.py index 687a62f24b4..0ba5d78decd 100644 --- a/Orange/widgets/visualize/owscatterplot.py +++ b/Orange/widgets/visualize/owscatterplot.py @@ -262,6 +262,10 @@ def _add_controls_sampling(self): callback=self.switch_sampling, commit=lambda: self.add_data(1)) self.sampling.setVisible(False) + @property + def effective_variables(self): + return [self.attr_x, self.attr_y] + def _vizrank_color_change(self): self.vizrank.initialize() is_enabled = self.data is not None and not self.data.is_sparse() and \ diff --git a/Orange/widgets/visualize/tests/test_owlinearprojection.py b/Orange/widgets/visualize/tests/test_owlinearprojection.py index d56b75c4d27..d314302220e 100644 --- a/Orange/widgets/visualize/tests/test_owlinearprojection.py +++ b/Orange/widgets/visualize/tests/test_owlinearprojection.py @@ -169,8 +169,6 @@ class LinProjVizRankTests(WidgetTest): def setUpClass(cls): super().setUpClass() cls.data = Table("iris") - # dom = Domain(cls.iris.domain.attributes, []) - # cls.iris_no_class = Table(dom, cls.iris) def setUp(self): self.widget = self.create_widget(OWLinearProjection) @@ -192,6 +190,7 @@ def test_continuous_class(self): def test_set_attrs(self): self.send_signal(self.widget.Inputs.data, self.data) model_selected = self.widget.model_selected[:] + c1 = self.get_output(self.widget.Outputs.components) self.vizrank.toggle() self.process_events(until=lambda: not self.vizrank.keep_running) self.assertEqual(len(self.vizrank.scores), self.vizrank.state_count()) @@ -200,3 +199,5 @@ def test_set_attrs(self): QItemSelectionModel.ClearAndSelect ) self.assertNotEqual(self.widget.model_selected[:], model_selected) + c2 = self.get_output(self.widget.Outputs.components) + self.assertNotEqual(c1.domain.attributes, c2.domain.attributes) diff --git a/Orange/widgets/visualize/tests/test_owradviz.py b/Orange/widgets/visualize/tests/test_owradviz.py index b2ed908295b..84ba6fbe996 100644 --- a/Orange/widgets/visualize/tests/test_owradviz.py +++ b/Orange/widgets/visualize/tests/test_owradviz.py @@ -63,10 +63,12 @@ def test_not_enough_instances(self): def test_saved_features(self): self.send_signal(self.widget.Inputs.data, self.data) self.widget.model_selected.pop(0) + self.widget.variables_selection.removed.emit() + selected = [a.name for a in self.widget.model_selected] + settings = self.widget.settingsHandler.pack_data(self.widget) w = self.create_widget(OWRadviz, stored_settings=settings) - self.send_signal(self.widget.Inputs.data, self.data, widget=w) - selected = [a.name for a in self.widget.model_selected] + self.send_signal(w.Inputs.data, self.data, widget=w) self.assertListEqual(selected, [a.name for a in w.model_selected]) self.send_signal(self.widget.Inputs.data, self.heart_disease) selected = [a.name for a in self.widget.model_selected] diff --git a/Orange/widgets/visualize/utils/widget.py b/Orange/widgets/visualize/utils/widget.py index 93b7e167178..866a0ddc6c5 100644 --- a/Orange/widgets/visualize/utils/widget.py +++ b/Orange/widgets/visualize/utils/widget.py @@ -373,6 +373,7 @@ def __init__(self): self.subset_data = None self.subset_indices = None self.__pending_selection = self.selection + self.__invalidated = True self.setup_gui() # GUI @@ -395,22 +396,39 @@ def _add_controls(self): gui.auto_commit(self.controlArea, self, "auto_commit", "Send Selection", "Send Automatically") + @property + def effective_variables(self): + return self.data.domain.attributes + + @property + def effective_data(self): + return self.data.transform(Domain(self.effective_variables, + self.data.domain.class_vars, + self.data.domain.metas)) + # Input @Inputs.data @check_sql_input def set_data(self, data): - same_domain = (self.data and data and + data_existed = self.data is not None + effective_data = self.effective_data if data_existed else None + same_domain = (data_existed and data is not None and data.domain.checksum() == self.data.domain.checksum()) self.closeContext() - self.clear() self.data = data self.check_data() if not same_domain: self.init_attr_values() self.openContext(self.data) + self.__invalidated = not (data_existed and self.data is not None and + np.array_equal(effective_data.X, + self.effective_data.X)) + if self.__invalidated: + self.clear() self.cb_class_density.setEnabled(self.can_draw_density()) def check_data(self): + self.valid_data = None self.clear_messages() @Inputs.data_subset @@ -422,7 +440,11 @@ def set_subset_data(self, subset): self.controls.graph.alpha_value.setEnabled(subset is None) def handleNewSignals(self): - self.setup_plot() + if self.__invalidated: + self.__invalidated = False + self.setup_plot() + else: + self.graph.update_point_props() self.commit() def get_subset_mask(self): @@ -439,7 +461,7 @@ def get_embedding(self): should return embedding for all data (valid and invalid). Invalid data embedding coordinates should be set to 0 (in some cases to Nan). - The method should also sets self.valid_data. + The method should also set self.valid_data. Returns: np.array: Array of embedding coordinates with shape @@ -552,8 +574,6 @@ def sizeHint(self): return QSize(1132, 708) def clear(self): - self.data = None - self.valid_data = None self.selection = None self.graph.selection = None @@ -587,16 +607,6 @@ def __init__(self): self.graph.view_box.moved.connect(self._manual_move) self.graph.view_box.finished.connect(self._manual_move_finish) - @property - def effective_variables(self): - return self.data.domain.attributes - - @property - def effective_data(self): - return self.data.transform(Domain(self.effective_variables, - self.data.domain.class_vars, - self.data.domain.metas)) - def check_data(self): def error(err): err() @@ -615,7 +625,7 @@ def error(err): def init_projection(self): self.projection = None - if not len(self.effective_variables): + if not self.effective_variables: return try: self.projection = self.projector(self.effective_data)