Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit dc1f3bb

Browse filesBrowse files
authored
Merge pull request #148 from jo-mueller/add-feature-histogram
added a Feature Histogram Widget
2 parents 3a8261a + e1ccfb1 commit dc1f3bb
Copy full SHA for dc1f3bb

10 files changed

+231
-12
lines changed

‎baseline/test_feature_histogram2.png

Copy file name to clipboard
12.6 KB
Loading

‎docs/changelog.rst

Copy file name to clipboardExpand all lines: docs/changelog.rst
+5-1Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
Changelog
22
=========
3-
1.0.3
3+
1.1.0
44
-----
5+
Additions
6+
~~~~~~~~~
7+
- Added a widget to draw a histogram of features.
8+
59
Changes
610
~~~~~~~
711
- The slice widget is now limited to slicing along the x/y dimensions. Support

‎docs/user_guide.rst

Copy file name to clipboardExpand all lines: docs/user_guide.rst
+1Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ These widgets plot the data stored in the ``.features`` attribute of individual
3030
Currently available are:
3131

3232
- 2D scatter plots of two features against each other.
33+
- Histograms of individual features.
3334

3435
To use these:
3536

‎src/napari_matplotlib/features.py

Copy file name to clipboard
+9Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
from napari.layers import Labels, Points, Shapes, Tracks, Vectors
2+
3+
FEATURES_LAYER_TYPES = (
4+
Labels,
5+
Points,
6+
Shapes,
7+
Tracks,
8+
Vectors,
9+
)

‎src/napari_matplotlib/histogram.py

Copy file name to clipboardExpand all lines: src/napari_matplotlib/histogram.py
+114-3Lines changed: 114 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,15 @@
1-
from typing import Optional
1+
from typing import Any, List, Optional, Tuple
22

33
import napari
44
import numpy as np
5-
from qtpy.QtWidgets import QWidget
5+
import numpy.typing as npt
6+
from qtpy.QtWidgets import QComboBox, QLabel, QVBoxLayout, QWidget
67

78
from .base import SingleAxesWidget
9+
from .features import FEATURES_LAYER_TYPES
810
from .util import Interval
911

10-
__all__ = ["HistogramWidget"]
12+
__all__ = ["HistogramWidget", "FeaturesHistogramWidget"]
1113

1214
_COLORS = {"r": "tab:red", "g": "tab:green", "b": "tab:blue"}
1315

@@ -61,3 +63,112 @@ def draw(self) -> None:
6163
self.axes.hist(data.ravel(), bins=bins, label=layer.name)
6264

6365
self.axes.legend()
66+
67+
68+
class FeaturesHistogramWidget(SingleAxesWidget):
69+
"""
70+
Display a histogram of selected feature attached to selected layer.
71+
"""
72+
73+
n_layers_input = Interval(1, 1)
74+
# All layers that have a .features attributes
75+
input_layer_types = FEATURES_LAYER_TYPES
76+
77+
def __init__(
78+
self,
79+
napari_viewer: napari.viewer.Viewer,
80+
parent: Optional[QWidget] = None,
81+
):
82+
super().__init__(napari_viewer, parent=parent)
83+
84+
self.layout().addLayout(QVBoxLayout())
85+
self._key_selection_widget = QComboBox()
86+
self.layout().addWidget(QLabel("Key:"))
87+
self.layout().addWidget(self._key_selection_widget)
88+
89+
self._key_selection_widget.currentTextChanged.connect(
90+
self._set_axis_keys
91+
)
92+
93+
self._update_layers(None)
94+
95+
@property
96+
def x_axis_key(self) -> Optional[str]:
97+
"""Key to access x axis data from the FeaturesTable"""
98+
return self._x_axis_key
99+
100+
@x_axis_key.setter
101+
def x_axis_key(self, key: Optional[str]) -> None:
102+
self._x_axis_key = key
103+
self._draw()
104+
105+
def _set_axis_keys(self, x_axis_key: str) -> None:
106+
"""Set both axis keys and then redraw the plot"""
107+
self._x_axis_key = x_axis_key
108+
self._draw()
109+
110+
def _get_valid_axis_keys(self) -> List[str]:
111+
"""
112+
Get the valid axis keys from the layer FeatureTable.
113+
114+
Returns
115+
-------
116+
axis_keys : List[str]
117+
The valid axis keys in the FeatureTable. If the table is empty
118+
or there isn't a table, returns an empty list.
119+
"""
120+
if len(self.layers) == 0 or not (hasattr(self.layers[0], "features")):
121+
return []
122+
else:
123+
return self.layers[0].features.keys()
124+
125+
def _get_data(self) -> Tuple[Optional[npt.NDArray[Any]], str]:
126+
"""Get the plot data.
127+
128+
Returns
129+
-------
130+
data : List[np.ndarray]
131+
List contains X and Y columns from the FeatureTable. Returns
132+
an empty array if nothing to plot.
133+
x_axis_name : str
134+
The title to display on the x axis. Returns
135+
an empty string if nothing to plot.
136+
"""
137+
if not hasattr(self.layers[0], "features"):
138+
# if the selected layer doesn't have a featuretable,
139+
# skip draw
140+
return None, ""
141+
142+
feature_table = self.layers[0].features
143+
144+
if (len(feature_table) == 0) or (self.x_axis_key is None):
145+
return None, ""
146+
147+
data = feature_table[self.x_axis_key]
148+
x_axis_name = self.x_axis_key.replace("_", " ")
149+
150+
return data, x_axis_name
151+
152+
def on_update_layers(self) -> None:
153+
"""
154+
Called when the layer selection changes by ``self.update_layers()``.
155+
"""
156+
# reset the axis keys
157+
self._x_axis_key = None
158+
159+
# Clear combobox
160+
self._key_selection_widget.clear()
161+
self._key_selection_widget.addItems(self._get_valid_axis_keys())
162+
163+
def draw(self) -> None:
164+
"""Clear the axes and histogram the currently selected layer/slice."""
165+
data, x_axis_name = self._get_data()
166+
167+
if data is None:
168+
return
169+
170+
self.axes.hist(data, bins=50, edgecolor="white", linewidth=0.3)
171+
172+
# set ax labels
173+
self.axes.set_xlabel(x_axis_name)
174+
self.axes.set_ylabel("Counts [#]")

‎src/napari_matplotlib/napari.yaml

Copy file name to clipboardExpand all lines: src/napari_matplotlib/napari.yaml
+7Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,10 @@ contributions:
1414
python_name: napari_matplotlib:FeaturesScatterWidget
1515
title: Make a scatter plot of layer features
1616

17+
- id: napari-matplotlib.features_histogram
18+
python_name: napari_matplotlib:FeaturesHistogramWidget
19+
title: Plot feature histograms
20+
1721
- id: napari-matplotlib.slice
1822
python_name: napari_matplotlib:SliceWidget
1923
title: Plot a 1D slice
@@ -28,5 +32,8 @@ contributions:
2832
- command: napari-matplotlib.features_scatter
2933
display_name: FeaturesScatter
3034

35+
- command: napari-matplotlib.features_histogram
36+
display_name: FeaturesHistogram
37+
3138
- command: napari-matplotlib.slice
3239
display_name: 1D slice

‎src/napari_matplotlib/scatter.py

Copy file name to clipboardExpand all lines: src/napari_matplotlib/scatter.py
+2-7Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from qtpy.QtWidgets import QComboBox, QLabel, QVBoxLayout, QWidget
66

77
from .base import SingleAxesWidget
8+
from .features import FEATURES_LAYER_TYPES
89
from .util import Interval
910

1011
__all__ = ["ScatterBaseWidget", "ScatterWidget", "FeaturesScatterWidget"]
@@ -94,13 +95,7 @@ class FeaturesScatterWidget(ScatterBaseWidget):
9495

9596
n_layers_input = Interval(1, 1)
9697
# All layers that have a .features attributes
97-
input_layer_types = (
98-
napari.layers.Labels,
99-
napari.layers.Points,
100-
napari.layers.Shapes,
101-
napari.layers.Tracks,
102-
napari.layers.Vectors,
103-
)
98+
input_layer_types = FEATURES_LAYER_TYPES
10499

105100
def __init__(
106101
self,
Loading
Loading

‎src/napari_matplotlib/tests/test_histogram.py

Copy file name to clipboardExpand all lines: src/napari_matplotlib/tests/test_histogram.py
+93-1Lines changed: 93 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,13 @@
11
from copy import deepcopy
22

3+
import numpy as np
34
import pytest
45

5-
from napari_matplotlib import HistogramWidget
6+
from napari_matplotlib import FeaturesHistogramWidget, HistogramWidget
7+
from napari_matplotlib.tests.helpers import (
8+
assert_figures_equal,
9+
assert_figures_not_equal,
10+
)
611

712

813
@pytest.mark.mpl_image_compare
@@ -28,3 +33,90 @@ def test_histogram_3D(make_napari_viewer, brain_data):
2833
# Need to return a copy, as original figure is too eagerley garbage
2934
# collected by the widget
3035
return deepcopy(fig)
36+
37+
38+
def test_feature_histogram(make_napari_viewer):
39+
n_points = 1000
40+
random_points = np.random.random((n_points, 3)) * 10
41+
feature1 = np.random.random(n_points)
42+
feature2 = np.random.normal(size=n_points)
43+
44+
viewer = make_napari_viewer()
45+
viewer.add_points(
46+
random_points,
47+
properties={"feature1": feature1, "feature2": feature2},
48+
name="points1",
49+
)
50+
viewer.add_points(
51+
random_points,
52+
properties={"feature1": feature1, "feature2": feature2},
53+
name="points2",
54+
)
55+
56+
widget = FeaturesHistogramWidget(viewer)
57+
viewer.window.add_dock_widget(widget)
58+
59+
# Check whether changing the selected key changes the plot
60+
widget._set_axis_keys("feature1")
61+
fig1 = deepcopy(widget.figure)
62+
63+
widget._set_axis_keys("feature2")
64+
assert_figures_not_equal(widget.figure, fig1)
65+
66+
# check whether selecting a different layer produces the same plot
67+
viewer.layers.selection.clear()
68+
viewer.layers.selection.add(viewer.layers[1])
69+
assert_figures_equal(widget.figure, fig1)
70+
71+
72+
@pytest.mark.mpl_image_compare
73+
def test_feature_histogram2(make_napari_viewer):
74+
import numpy as np
75+
76+
np.random.seed(0)
77+
n_points = 1000
78+
random_points = np.random.random((n_points, 3)) * 10
79+
feature1 = np.random.random(n_points)
80+
feature2 = np.random.normal(size=n_points)
81+
82+
viewer = make_napari_viewer()
83+
viewer.add_points(
84+
random_points,
85+
properties={"feature1": feature1, "feature2": feature2},
86+
name="points1",
87+
)
88+
viewer.add_points(
89+
random_points,
90+
properties={"feature1": feature1, "feature2": feature2},
91+
name="points2",
92+
)
93+
94+
widget = FeaturesHistogramWidget(viewer)
95+
viewer.window.add_dock_widget(widget)
96+
widget._set_axis_keys("feature1")
97+
98+
fig = FeaturesHistogramWidget(viewer).figure
99+
return deepcopy(fig)
100+
101+
102+
def test_change_layer(make_napari_viewer, brain_data, astronaut_data):
103+
viewer = make_napari_viewer()
104+
widget = HistogramWidget(viewer)
105+
106+
viewer.add_image(brain_data[0], **brain_data[1])
107+
viewer.add_image(astronaut_data[0], **astronaut_data[1])
108+
109+
# Select first layer
110+
viewer.layers.selection.clear()
111+
viewer.layers.selection.add(viewer.layers[0])
112+
fig1 = deepcopy(widget.figure)
113+
114+
# Re-selecting first layer should produce identical plot
115+
viewer.layers.selection.clear()
116+
viewer.layers.selection.add(viewer.layers[0])
117+
assert_figures_equal(widget.figure, fig1)
118+
119+
# Plotting the second layer should produce a different plot
120+
viewer.layers.selection.clear()
121+
viewer.layers.selection.add(viewer.layers[1])
122+
assert_figures_not_equal(widget.figure, fig1)

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.