diff --git a/src/napari_clusters_plotter/_algorithm_widget.py b/src/napari_clusters_plotter/_algorithm_widget.py index 7ae64310..a3519dc7 100644 --- a/src/napari_clusters_plotter/_algorithm_widget.py +++ b/src/napari_clusters_plotter/_algorithm_widget.py @@ -1,3 +1,5 @@ +import warnings + import pandas as pd from magicgui import magicgui from napari.layers import ( @@ -50,6 +52,13 @@ def _get_features(self): ].astype("category") return features.reset_index(drop=True) + def _clean_up(self): + """Determines what happens in case of no layer selected""" + + raise NotImplementedError( + "This function should be implemented in the subclass." + ) + @property def common_columns(self): if len(self.layers) == 0: @@ -137,6 +146,13 @@ def _update_features(self): return features def _wait_for_finish(self, worker): + # escape empty input data + if self.selected_algorithm_widget.data.value.empty: + warnings.warn( + "No features selected. Please select features before running the algorithm.", + stacklevel=1, + ) + return self.worker = worker self.worker.start() self.worker.returned.connect(self._process_result) @@ -164,6 +180,7 @@ def _on_update_layer_selection(self, layer): # don't do anything if no layer is selected if self.n_selected_layers == 0: + self._clean_up() return # check if the selected layers are of the correct type @@ -189,6 +206,16 @@ def _on_update_layer_selection(self, layer): self.feature_selection_widget.addItems(sorted(features_to_add.columns)) self._update_features() + def _clean_up(self): + """ + Clean up the widget when it is closed. + """ + + # block signals for feature selection + self.feature_selection_widget.blockSignals(True) + self.feature_selection_widget.clear() + self.feature_selection_widget.blockSignals(False) + @property def selected_algorithm(self): return self.algorithm_selection.currentText() diff --git a/src/napari_clusters_plotter/_new_plotter_widget.py b/src/napari_clusters_plotter/_new_plotter_widget.py index 7eddfd12..51cbc8b2 100644 --- a/src/napari_clusters_plotter/_new_plotter_widget.py +++ b/src/napari_clusters_plotter/_new_plotter_widget.py @@ -149,6 +149,9 @@ def _on_finish_draw(self, color_indices: np.ndarray): # if the hue axis is not set to MANUAL_CLUSTER_ID, set it to that # otherwise replot the data + if self.n_selected_layers == 0: + return + features = self._get_features() for layer in self.viewer.layers.selection: layer_indices = features[features["layer"] == layer.name].index @@ -335,6 +338,7 @@ def _on_update_layer_selection( """ # don't do anything if no layer is selected if self.n_selected_layers == 0: + self._clean_up() return # check if the selected layers are of the correct type @@ -362,6 +366,23 @@ def _on_update_layer_selection( for layer in self.layers: layer.events.features.connect(self._update_feature_selection) + def _clean_up(self): + """In case of empty layer selection""" + + # disconnect the events from the layers + for layer in self.viewer.layers.selection: + layer.events.features.disconnect(self._update_feature_selection) + + # reset the selected layers + self.layers = [] + + # reset the selectors + for dim in ["x", "y", "hue"]: + selector = self._selectors[dim] + selector.blockSignals(True) + selector.clear() + selector.blockSignals(False) + def _update_feature_selection( self, event: napari.utils.events.Event ) -> None: @@ -450,6 +471,10 @@ def _reset(self): """ Reset the selection in the current plotting widget. """ + + if self.n_selected_layers == 0: + return + self.plotting_widget.active_artist.color_indices = np.zeros( len(self._get_features()) ) diff --git a/src/napari_clusters_plotter/_tests/test_dimensionality_reduction.py b/src/napari_clusters_plotter/_tests/test_dimensionality_reduction.py index d117c311..e57f8b7d 100644 --- a/src/napari_clusters_plotter/_tests/test_dimensionality_reduction.py +++ b/src/napari_clusters_plotter/_tests/test_dimensionality_reduction.py @@ -77,6 +77,11 @@ def test_initialization(make_napari_viewer, widget_config): # check that all features are in reduce_wdiget.feature_selection_widget assert len(feature_selection_items) == len(layer.features.columns) + # clear layers to make sure cleanup works + viewer.layers.clear() + + assert widget.feature_selection_widget.count() == 0 + def test_layer_update(make_napari_viewer, widget_config): viewer = make_napari_viewer() diff --git a/src/napari_clusters_plotter/_tests/test_plotter.py b/src/napari_clusters_plotter/_tests/test_plotter.py index b0541a33..7ea56369 100644 --- a/src/napari_clusters_plotter/_tests/test_plotter.py +++ b/src/napari_clusters_plotter/_tests/test_plotter.py @@ -344,6 +344,45 @@ def test_categorical_handling(make_napari_viewer, create_sample_layers): assert categorical_columns[1] == "layer" +def test_empty_layer_clean_up(make_napari_viewer, n_samples: int = 100): + """ + This test checks what happenns when you add some layers, + do a manual clustering , then delete the layers and add some others + """ + from napari_clusters_plotter import PlotterWidget + + viewer = make_napari_viewer() + + points1, points2 = create_multi_point_layer(n_samples=n_samples) + vectors1, _ = create_multi_vectors_layer(n_samples=n_samples) + + # add points to viewer + viewer.add_layer(points1) + viewer.add_layer(points2) + + widget = PlotterWidget(viewer) + viewer.window.add_dock_widget(widget, area="right") + viewer.layers.selection.active = points1 + + # do a random drawing + assert "MANUAL_CLUSTER_ID" in points1.features.columns + random_cluster_indeces = np.random.randint(0, 2, len(points1.data)) + widget._on_finish_draw(random_cluster_indeces) + + # delete the layers + viewer.layers.clear() + + # check that all widget._selectros ('x', 'y', 'hue') are empty + assert widget._selectors["x"].currentText() == "" + assert widget._selectors["y"].currentText() == "" + assert widget._selectors["hue"].currentText() == "" + + widget._reset() + + # add vectors to viewer + viewer.add_layer(vectors1) + + @pytest.mark.parametrize( "create_sample_layers", [