Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4df9ff1
added context menu for subcluster export
jo-mueller Jan 29, 2025
6375f97
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 4, 2025
e7dd056
added basic testing sample data functions for testing
jo-mueller Feb 4, 2025
360c4c3
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 4, 2025
6229de3
fixed points sample data creation
jo-mueller Feb 4, 2025
cbbf829
added sample data function for labels
jo-mueller Feb 4, 2025
e71ad91
added test function for subcluster export
jo-mueller Feb 4, 2025
9237fdc
Merge branch 'add-context-menu-to-plotter-for-layer-export' of https:…
jo-mueller Feb 4, 2025
d46540e
removed print statement
jo-mueller Feb 4, 2025
4c27f03
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 4, 2025
fe7dfbd
added Shapes to layer exports possibilities
jo-mueller Feb 4, 2025
d35941b
Merge branch 'add-context-menu-to-plotter-for-layer-export' of https:…
jo-mueller Feb 4, 2025
b9cc73e
Merge branch 'v0.9.0' into add-context-menu-to-plotter-for-layer-export
jo-mueller Feb 4, 2025
705a19f
Merge branch 'v0.9.0' into add-context-menu-to-plotter-for-layer-export
jo-mueller Feb 4, 2025
3de4d90
fixed export of shapes layer
jo-mueller Feb 6, 2025
d4d476e
fix tests
jo-mueller Feb 6, 2025
a3f965a
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Feb 6, 2025
14ed475
Merge branch 'v0.9.0' into add-context-menu-to-plotter-for-layer-export
jo-mueller Apr 13, 2025
a1f0942
Merge branch 'control-coloring' into add-context-menu-to-plotter-for-…
jo-mueller Apr 13, 2025
126ffb9
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Apr 13, 2025
3f9d452
Fixed lavbels layer LUT creation
jo-mueller Apr 14, 2025
42a9788
added default colors for non-sequential label images
jo-mueller Apr 14, 2025
f948a91
Merge branch 'v0.9.0' into add-context-menu-to-plotter-for-layer-export
jo-mueller Apr 14, 2025
720c9d7
removed unnecessary dict comprehension
jo-mueller Apr 14, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
150 changes: 119 additions & 31 deletions src/napari_clusters_plotter/_new_plotter_widget.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
from qtpy import uic
from qtpy.QtCore import Qt, Signal
from qtpy.QtGui import QColor
from qtpy.QtWidgets import QComboBox, QVBoxLayout, QWidget
from qtpy.QtWidgets import QComboBox, QMenu, QVBoxLayout, QWidget

from ._algorithm_widget import BaseWidget

Expand Down Expand Up @@ -64,6 +64,13 @@
ArtistType.SCATTER
]

# Context menu
self.context_menu = QMenu(self.plotting_widget)
self.export_clusters = self.context_menu.addAction(
"Export selected cluster to new layer"
)
self.export_clusters.triggered.connect(self._on_export_clusters)

# Add plot and options as widgets
self.layout.addWidget(self.plotting_widget)
self.layout.addWidget(self.control_widget)
Expand All @@ -85,6 +92,30 @@
self.control_widget.bins_settings_container.setVisible(False)
self.control_widget.log_scale_container.setVisible(False)

def contextMenuEvent(self, event):
self.context_menu.exec_(event.globalPos())

Check warning on line 96 in src/napari_clusters_plotter/_new_plotter_widget.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_new_plotter_widget.py#L96

Added line #L96 was not covered by tests

def _on_export_clusters(self):
"""
Export the selected cluster to a new layer.
"""

# get currently selected cluster from plotting widget
selected_cluster = self.plotting_widget.class_spinbox.value
indices = (
self.plotting_widget.active_artist.color_indices
== selected_cluster
)

# get the layer to export from
layer = self.layers[0]

export_layer = _export_cluster_to_layer(
layer, indices, subcluster_index=selected_cluster
)
if export_layer is not None:
self.viewer.add_layer(export_layer)

def _setup_callbacks(self):
"""
Set up the callbacks for the widget.
Expand Down Expand Up @@ -449,33 +480,90 @@
"""
from napari.utils import DirectLabelColormap

color_mapping = {
napari.layers.Points: lambda _layer, _color: setattr(
_layer, "face_color", _color
),
napari.layers.Vectors: lambda _layer, _color: setattr(
_layer, "edge_color", _color
),
napari.layers.Surface: lambda _layer, _color: setattr(
_layer, "vertex_colors", _color
),
napari.layers.Shapes: lambda _layer, _color: setattr(
_layer, "face_color", _color
),
napari.layers.Labels: lambda _layer, _color: setattr(
_layer,
"colormap",
DirectLabelColormap(
color_dict={
label: _color[label] for label in np.unique(_layer.data)
}
),
),
}

if type(layer) in color_mapping:
if type(layer) is napari.layers.Labels:
# add a color for the background at the first index
colors = np.insert(colors, 0, [0, 0, 0, 0], axis=0)
color_mapping[type(layer)](layer, colors)
layer.refresh()
if isinstance(layer, napari.layers.Points):
layer.face_color = colors

elif isinstance(layer, napari.layers.Vectors):
layer.edge_color = colors

Check warning on line 487 in src/napari_clusters_plotter/_new_plotter_widget.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_new_plotter_widget.py#L487

Added line #L487 was not covered by tests

elif isinstance(layer, napari.layers.Surface):
layer.vertex_colors = colors

Check warning on line 490 in src/napari_clusters_plotter/_new_plotter_widget.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_new_plotter_widget.py#L490

Added line #L490 was not covered by tests

elif isinstance(layer, napari.layers.Shapes):
layer.face_color = colors

elif isinstance(layer, napari.layers.Labels):

Check warning on line 495 in src/napari_clusters_plotter/_new_plotter_widget.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_new_plotter_widget.py#L495

Added line #L495 was not covered by tests

colors = np.insert(colors, 0, [0, 0, 0, 0], axis=0)
color_dict = dict(zip(np.unique(layer.data), colors))

Check warning on line 498 in src/napari_clusters_plotter/_new_plotter_widget.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_new_plotter_widget.py#L497-L498

Added lines #L497 - L498 were not covered by tests

# Insert default colors for labels that are not in the color_dict
# Relevant for non-sequential label images
if max(color_dict.keys()) > len(colors):
for i in range(1, max(color_dict.keys()) - 1):
color_dict[i] = [0, 0, 0, 0]

Check warning on line 504 in src/napari_clusters_plotter/_new_plotter_widget.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_new_plotter_widget.py#L502-L504

Added lines #L502 - L504 were not covered by tests
# Add a color for the background at the first index
layer.colormap = DirectLabelColormap(color_dict=color_dict)

Check warning on line 506 in src/napari_clusters_plotter/_new_plotter_widget.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_new_plotter_widget.py#L506

Added line #L506 was not covered by tests

layer.refresh()


def _export_cluster_to_layer(layer, indices, subcluster_index: int = None):
"""
Export the selected cluster to a new layer.

Parameters
----------
layer : napari.layers.Layer
The layer to export the cluster from.

indices : np.ndarray
The indices of the cluster to export.

subcluster_index : str
The name of the new layer. If not provided, the name of the layer will be used.

Returns
-------
napari.layers.Layer
The new layer with the selected cluster.
"""

if isinstance(layer, napari.layers.Labels):
LUT = np.arange(layer.data.max() + 1)
LUT[1:][~indices] = 0
new_layer = napari.layers.Labels(LUT[layer.data])

Check warning on line 535 in src/napari_clusters_plotter/_new_plotter_widget.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_new_plotter_widget.py#L533-L535

Added lines #L533 - L535 were not covered by tests

elif isinstance(layer, napari.layers.Points):
new_layer = napari.layers.Points(layer.data[indices])
new_layer.size = layer.size[indices]

elif isinstance(layer, napari.layers.Shapes):
new_shapes = [shape for shape, i in zip(layer.data, indices) if i]
new_layer = napari.layers.Shapes(new_shapes)

elif isinstance(layer, napari.layers.Surface):

Check warning on line 545 in src/napari_clusters_plotter/_new_plotter_widget.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_new_plotter_widget.py#L545

Added line #L545 was not covered by tests
# TODO implement surface export
return None

Check warning on line 547 in src/napari_clusters_plotter/_new_plotter_widget.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_new_plotter_widget.py#L547

Added line #L547 was not covered by tests

elif isinstance(layer, napari.layers.Vectors):
new_layer = napari.layers.Vectors(layer.data[indices])

Check warning on line 550 in src/napari_clusters_plotter/_new_plotter_widget.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_new_plotter_widget.py#L549-L550

Added lines #L549 - L550 were not covered by tests

else:
return None

Check warning on line 553 in src/napari_clusters_plotter/_new_plotter_widget.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_new_plotter_widget.py#L553

Added line #L553 was not covered by tests

new_layer.scale = layer.scale
new_layer.translate = layer.translate
new_layer.rotate = layer.rotate

if not subcluster_index:
new_layer.name = f"{layer.name} subcluster"
else:
new_layer.name = f"{layer.name} subcluster {subcluster_index}"

Check warning on line 562 in src/napari_clusters_plotter/_new_plotter_widget.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_new_plotter_widget.py#L562

Added line #L562 was not covered by tests

# copy features to new layer if available and drop cluster column
new_layer.features = layer.features.iloc[indices].copy()
if "cluster" in new_layer.features.columns:
new_layer.features.drop(columns=["cluster"], inplace=True)

Check warning on line 567 in src/napari_clusters_plotter/_new_plotter_widget.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_new_plotter_widget.py#L567

Added line #L567 was not covered by tests

return new_layer
161 changes: 159 additions & 2 deletions src/napari_clusters_plotter/_tests/test_plotter.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,137 @@
import numpy as np
import pandas as pd
import pytest
from napari.layers import Labels, Points, Shapes


def create_points(n_samples=100, loc=5):

loc = 5
n_timeframes = 5
frame = np.arange(n_timeframes).repeat(n_samples // n_timeframes)
# make some random points with random features
points = np.random.random((n_samples, 4))
points2 = np.random.random((n_samples - 1, 4))

points[:, 0] = frame
points2[:, 0] = frame[:-1]

features = pd.DataFrame(
{
"frame": frame,
"feature1": np.random.normal(size=n_samples, loc=loc),
"feature2": np.random.normal(size=n_samples, loc=loc),
"feature3": np.random.normal(size=n_samples, loc=loc),
"feature4": np.random.normal(size=n_samples, loc=loc),
}
)

features2 = pd.DataFrame(
{
"frame": frame[:-1],
"feature2": np.random.normal(size=n_samples - 1, loc=-loc),
"feature3": np.random.normal(size=n_samples - 1, loc=-loc),
"feature4": np.random.normal(size=n_samples - 1, loc=-loc),
}
)

layer1 = Points(
points, features=features, size=0.1, blending="translucent_no_depth"
)
layer2 = Points(
points2,
features=features2,
size=0.1,
translate=(0, 0, 2),
blending="translucent_no_depth",
)

return layer1, layer2


def create_shapes(n_samples=100):

# create 100 random anchors
np.random.seed(0)
anchors = np.random.random((n_samples, 2)) * 100

# create 100 random widths and heights
widths = np.random.random(n_samples) * 10
heights = np.random.random(n_samples) * 10

# combine into lists of corner coordinates
corner1 = anchors - np.c_[widths, heights] / 2
corner2 = anchors + np.c_[widths, -heights] / 2
corner3 = anchors + np.c_[widths, heights] / 2
corner4 = anchors + np.c_[-widths, heights] / 2

# create a list of polygons
polygons = np.stack([corner1, corner2, corner3, corner4], axis=1)

layer1 = Shapes(polygons[:49], shape_type="polygon")
layer2 = Shapes(polygons[50:], shape_type="polygon")
features1 = pd.DataFrame(
{
"feature1": np.random.normal(size=49),
"feature2": np.random.normal(size=49),
"feature3": np.random.normal(size=49),
"feature4": np.random.normal(size=49),
}
)

features2 = pd.DataFrame(
{
"feature1": np.random.normal(size=50),
"feature2": np.random.normal(size=50),
"feature3": np.random.normal(size=50),
"feature4": np.random.normal(size=50),
}
)

layer1.features = features1
layer2.features = features2

return layer1, layer2


def create_labels(n_samples=100):
from skimage import data, measure

Check warning on line 98 in src/napari_clusters_plotter/_tests/test_plotter.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_tests/test_plotter.py#L98

Added line #L98 was not covered by tests

binary_image1 = data.binary_blobs(length=128, n_dim=3, volume_fraction=0.1)
binary_image2 = data.binary_blobs(length=128, n_dim=3, volume_fraction=0.1)

Check warning on line 101 in src/napari_clusters_plotter/_tests/test_plotter.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_tests/test_plotter.py#L100-L101

Added lines #L100 - L101 were not covered by tests

labels1 = measure.label(binary_image1)
labels2 = measure.label(binary_image2)

Check warning on line 104 in src/napari_clusters_plotter/_tests/test_plotter.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_tests/test_plotter.py#L103-L104

Added lines #L103 - L104 were not covered by tests

n_labels1 = len(np.unique(labels1))
n_labels2 = len(np.unique(labels2))

Check warning on line 107 in src/napari_clusters_plotter/_tests/test_plotter.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_tests/test_plotter.py#L106-L107

Added lines #L106 - L107 were not covered by tests

features1 = pd.DataFrame(

Check warning on line 109 in src/napari_clusters_plotter/_tests/test_plotter.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_tests/test_plotter.py#L109

Added line #L109 was not covered by tests
{
"feature1": np.random.normal(size=n_labels1),
"feature2": np.random.normal(size=n_labels1),
"feature3": np.random.normal(size=n_labels1),
"feature4": np.random.normal(size=n_labels1),
}
)

features2 = pd.DataFrame(

Check warning on line 118 in src/napari_clusters_plotter/_tests/test_plotter.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_tests/test_plotter.py#L118

Added line #L118 was not covered by tests
{
"feature1": np.random.normal(size=n_labels2),
"feature2": np.random.normal(size=n_labels2),
"feature3": np.random.normal(size=n_labels2),
"feature4": np.random.normal(size=n_labels2),
}
)

layer1 = Labels(labels1, features=features1)
layer2 = Labels(labels2, features=features2)

Check warning on line 128 in src/napari_clusters_plotter/_tests/test_plotter.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_tests/test_plotter.py#L127-L128

Added lines #L127 - L128 were not covered by tests

return layer1, layer2

Check warning on line 130 in src/napari_clusters_plotter/_tests/test_plotter.py

View check run for this annotation

Codecov / codecov/patch

src/napari_clusters_plotter/_tests/test_plotter.py#L130

Added line #L130 was not covered by tests


def create_multi_point_layer(n_samples: int = 100):
import pandas as pd
from napari.layers import Points

loc = 5
n_timeframes = 5
Expand Down Expand Up @@ -71,7 +199,36 @@
viewer.add_image(random_image)
viewer.add_labels(sample_labels)

#

@pytest.mark.parametrize("create_data", [create_points, create_shapes])
def test_cluster_export(make_napari_viewer, create_data):
from napari_clusters_plotter import PlotterWidget

viewer = make_napari_viewer()
widget = PlotterWidget(viewer)
viewer.window.add_dock_widget(widget, area="right")

layer1, _ = create_data()
viewer.add_layer(layer1)

# select some random features in the plotting widget
n_samples = layer1.features.shape[0]
selected_clusters = np.zeros(n_samples, dtype=int)
selected_clusters[:5] = 1

widget.plotting_widget.active_artist.color_indices = selected_clusters
widget._on_export_clusters()

assert len(viewer.layers) == 2

if isinstance(layer1, Points):
assert (
viewer.layers[-1].data.shape[0] == n_samples - 5
) # selected cluster is 0, by default.
assert np.array_equal(
viewer.layers[-1].data,
layer1.data[~selected_clusters.astype(bool)],
)


def test_cluster_memorization(make_napari_viewer, n_samples: int = 100):
Expand Down