Skip to content

Commit fb47952

Browse files
committed
added a Feature Histogram Widget
1 parent 308134b commit fb47952

File tree

3 files changed

+149
-2
lines changed

3 files changed

+149
-2
lines changed

Diff for: src/napari_matplotlib/napari.yaml

+7
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

Diff for: src/napari_matplotlib/scatter.py

+122-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
from .base import NapariMPLWidget
1010
from .util import Interval
1111

12-
__all__ = ["ScatterWidget", "FeaturesScatterWidget"]
12+
__all__ = ["ScatterWidget", "FeaturesScatterWidget", "FeaturesHistogramWidget"]
1313

1414

1515
class ScatterBaseWidget(NapariMPLWidget):
@@ -222,3 +222,124 @@ def _on_update_layers(self) -> None:
222222
# reset the axis keys
223223
self._x_axis_key = None
224224
self._y_axis_key = None
225+
226+
227+
class FeaturesHistogramWidget(NapariMPLWidget):
228+
n_layers_input = Interval(1, 1)
229+
# All layers that have a .features attributes
230+
input_layer_types = (
231+
napari.layers.Labels,
232+
napari.layers.Points,
233+
napari.layers.Shapes,
234+
napari.layers.Tracks,
235+
napari.layers.Vectors,
236+
)
237+
238+
def __init__(self, napari_viewer: napari.viewer.Viewer):
239+
super().__init__(napari_viewer)
240+
self.axes = self.canvas.figure.subplots()
241+
242+
self._key_selection_widget = magicgui(
243+
self._set_axis_keys,
244+
x_axis_key={"choices": self._get_valid_axis_keys},
245+
call_button="plot",
246+
)
247+
self.layout().addWidget(self._key_selection_widget.native)
248+
249+
self.update_layers(None)
250+
251+
def clear(self) -> None:
252+
"""
253+
Clear the axes.
254+
"""
255+
self.axes.clear()
256+
257+
self.layout().addWidget(self._key_selection_widget.native)
258+
259+
@property
260+
def x_axis_key(self) -> Optional[str]:
261+
"""Key to access x axis data from the FeaturesTable"""
262+
return self._x_axis_key
263+
264+
@x_axis_key.setter
265+
def x_axis_key(self, key: Optional[str]) -> None:
266+
self._x_axis_key = key
267+
self._draw()
268+
269+
def _set_axis_keys(self, x_axis_key: str) -> None:
270+
"""Set both axis keys and then redraw the plot"""
271+
self._x_axis_key = x_axis_key
272+
self._draw()
273+
274+
def _get_valid_axis_keys(
275+
self, combo_widget: Optional[ComboBox] = None
276+
) -> List[str]:
277+
"""
278+
Get the valid axis keys from the layer FeatureTable.
279+
280+
Returns
281+
-------
282+
axis_keys : List[str]
283+
The valid axis keys in the FeatureTable. If the table is empty
284+
or there isn't a table, returns an empty list.
285+
"""
286+
if len(self.layers) == 0 or not (hasattr(self.layers[0], "features")):
287+
return []
288+
else:
289+
return self.layers[0].features.keys()
290+
291+
def _get_data(self) -> Tuple[List[np.ndarray], str, str]:
292+
"""Get the plot data.
293+
294+
Returns
295+
-------
296+
data : List[np.ndarray]
297+
List contains X and Y columns from the FeatureTable. Returns
298+
an empty array if nothing to plot.
299+
x_axis_name : str
300+
The title to display on the x axis. Returns
301+
an empty string if nothing to plot.
302+
"""
303+
if not hasattr(self.layers[0], "features"):
304+
# if the selected layer doesn't have a featuretable,
305+
# skip draw
306+
return [], ""
307+
308+
feature_table = self.layers[0].features
309+
310+
if (
311+
(len(feature_table) == 0)
312+
or (self.x_axis_key is None)
313+
):
314+
return [], ""
315+
316+
data = feature_table[self.x_axis_key]
317+
x_axis_name = self.x_axis_key.replace("_", " ")
318+
319+
return data, x_axis_name
320+
321+
def _on_update_layers(self) -> None:
322+
"""
323+
This is called when the layer selection changes by
324+
``self.update_layers()``.
325+
"""
326+
if hasattr(self, "_key_selection_widget"):
327+
self._key_selection_widget.reset_choices()
328+
329+
# reset the axis keys
330+
self._x_axis_key = None
331+
332+
def draw(self) -> None:
333+
"""Clear the axes and histogram the currently selected layer/slice."""
334+
335+
data, x_axis_name = self._get_data()
336+
337+
if len(data) == 0:
338+
return
339+
340+
_, _, _ = self.axes.hist(data, bins=50, edgecolor='white',
341+
linewidth=0.3)
342+
343+
# # set ax labels
344+
self.axes.set_xlabel(x_axis_name)
345+
self.axes.set_ylabel('Counts [#]')

Diff for: src/napari_matplotlib/tests/test_histogram.py

+20-1
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,27 @@
1-
from napari_matplotlib import HistogramWidget
1+
from napari_matplotlib import HistogramWidget, FeaturesHistogramWidget
22

33

44
def test_example_q_widget(make_napari_viewer, image_data):
55
# Smoke test adding a histogram widget
66
viewer = make_napari_viewer()
77
viewer.add_image(image_data[0], **image_data[1])
88
HistogramWidget(viewer)
9+
10+
def test_feature_histogram(make_napari_viewer):
11+
12+
import numpy as np
13+
14+
n_points = 1000
15+
random_points = np.random.random((n_points,3))*10
16+
feature1 = np.random.random(n_points)
17+
feature2 = np.random.normal(size=n_points)
18+
19+
viewer = make_napari_viewer()
20+
viewer.add_points(random_points, properties={'feature1': feature1, 'feature2': feature2}, face_color='feature1', size=1)
21+
22+
widget = FeaturesHistogramWidget(viewer)
23+
viewer.window.add_dock_widget(widget)
24+
widget._set_axis_keys('feature1')
25+
widget._key_selection_widget()
26+
widget._set_axis_keys('feature2')
27+
widget._key_selection_widget()

0 commit comments

Comments
 (0)