connect napari-sided object selection in plotter widget#441
connect napari-sided object selection in plotter widget#441ClementCaporal wants to merge 13 commits intoBiAPoL:mainfrom
Conversation
for more information, see https://pre-commit.ci
…include SELECTED_DATA_LAYER_CLUSTER_ID
for more information, see https://pre-commit.ci
| def _get_selected_objects(self, layer: napari.layers.Layer) -> List[int]: | ||
| """ | ||
| Retrieve id of selected object on napari canvas" | ||
| """ | ||
| if isinstance(layer, napari.layers.Points): | ||
| return list(layer.selected_data) | ||
| elif isinstance(layer, napari.layers.Labels): | ||
| return [layer.selected_label] | ||
| else: | ||
| raise TypeError( | ||
| f"Layer type {type(layer)} is not supported for selection." | ||
| ) |
There was a problem hiding this comment.
This doesn't necessarily have to be a member of the PlotterWidget - none of the functionality relies on self. Originally, some other functionality in here (e.g., _set_layer_color) weren't member functions but I think that was lost somewhere along the way.
Still - moving it to _utilities.py could help to keep this widget a bit cleaner here :)
There was a problem hiding this comment.
Ok I will move it!
| def _update_layer_selected_data_feature( | ||
| self, layer: napari.layers.Layer | ||
| ) -> None: | ||
| """ | ||
| Update the layer selected_data to feature. | ||
| """ | ||
| selected_data = self._get_selected_objects(layer) | ||
| cluster = np.zeros(len(layer.features)) | ||
| cluster[list(selected_data)] = 1 | ||
| # set categorical to be selectable in "Hue" dropdown | ||
| layer.features["SELECTED_DATA_LAYER_CLUSTER_ID"] = pd.Categorical( | ||
| cluster | ||
| ) | ||
| self.plot_needs_update.emit() |
There was a problem hiding this comment.
Not sure - does it make sense to iterate over all the layers and collect the selected objects inside this function? That way, you could save yourself the lambda callback connection above. Somethhing like this:
for layer in self.layers:
selected_data = self._get_selected_objects(layer)
cluster = np.zeros(len(layer.features))
cluster[list(selected_data)] = 1
# set categorical to be selectable in "Hue" dropdown
layer.features["SELECTED_DATA_LAYER_CLUSTER_ID"] = pd.Categorical(cluster)edit: Avoiding the lambda function would probably also make pre-commit happy :)
|
Hi @ClementCaporal , just gave it a try, it looks pretty much how I envisioned it to work for selectable objects :)
I was wondering why it was so slow for labels 😅
Maybe we could just mute/block this event while updating the colormap? I just tried to add the following into the elif isinstance(layer, napari.layers.Labels):
from napari.utils import DirectLabelColormap
from ._utilities import _get_unique_values
# Ensure the first color is transparent for the background
colors = np.insert(colors, 0, [0, 0, 0, 0], axis=0)
color_dict = dict(zip(_get_unique_values(layer), colors))
layer.events.selected_label.block() # avoid triggering the infinite loop
layer.colormap = DirectLabelColormap(color_dict=color_dict)
layer.events.selected_label.unblock() # restore functionality for whatever purpose it serves |
Thank you for this workaround! I think I will still open an issue on the napari side because I don't understand why this event is needed
Co-authored-by: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com>
Follow-up on #366
Apologies for the delay in getting back to you!
Since I couldn't copy-paste your comments directly, here's a summary of what I’ve addressed based on your feedback:
your _is_selectableand_get_selected_objectsas private functions and applied them where needed._VIEWPORT_CLUSTER_ID: I kept the original nameSELECTED_DATA_LAYER_CLUSTER_IDto stay consistent with the naming used in the napari layer selected_data. I don't have a strong opinion on this, this is just a proposition, I can change it.This PR is not ready yet (and it doesn't pass all the tests) because :
I’m still facing an issue related to selection on the labels layer that I think are out of scope of this PR.
To handle label selection, we need to listen to the
selected_labelevent:https://github.com/napari/napari/blob/86e6d27c547f3fad22ddfe8835c4869d3f0931ed/src/napari/layers/labels/labels.py#L382
From there, we update the layer features and emit
self.plot_needs_update. This eventually leads to updatinglayer.colormap, which unfortunately triggers theselected_labelevent again:https://github.com/napari/napari/blob/86e6d27c547f3fad22ddfe8835c4869d3f0931ed/src/napari/layers/labels/labels.py#L571
This creates an unintended recursive loop.
I think this might be a bug on napari’s side (it's unclear to me why changing the colormap should emit a selected_label event). Unless I’ve missed something, I’m will open an issue on their repo based on your review.
Thanks again for your time and for the excellent plugin!