Skip to content

Commit

Permalink
added a Feature Histogram Widget
Browse files Browse the repository at this point in the history
  • Loading branch information
jo-mueller committed Jun 1, 2023
1 parent 308134b commit fb47952
Show file tree
Hide file tree
Showing 3 changed files with 149 additions and 2 deletions.
7 changes: 7 additions & 0 deletions src/napari_matplotlib/napari.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ contributions:
python_name: napari_matplotlib:FeaturesScatterWidget
title: Make a scatter plot of layer features

- id: napari-matplotlib.features_histogram
python_name: napari_matplotlib:FeaturesHistogramWidget
title: Plot feature histograms

- id: napari-matplotlib.slice
python_name: napari_matplotlib:SliceWidget
title: Plot a 1D slice
Expand All @@ -28,5 +32,8 @@ contributions:
- command: napari-matplotlib.features_scatter
display_name: FeaturesScatter

- command: napari-matplotlib.features_histogram
display_name: FeaturesHistogram

- command: napari-matplotlib.slice
display_name: 1D slice
123 changes: 122 additions & 1 deletion src/napari_matplotlib/scatter.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from .base import NapariMPLWidget
from .util import Interval

__all__ = ["ScatterWidget", "FeaturesScatterWidget"]
__all__ = ["ScatterWidget", "FeaturesScatterWidget", "FeaturesHistogramWidget"]


class ScatterBaseWidget(NapariMPLWidget):
Expand Down Expand Up @@ -222,3 +222,124 @@ def _on_update_layers(self) -> None:
# reset the axis keys
self._x_axis_key = None
self._y_axis_key = None


class FeaturesHistogramWidget(NapariMPLWidget):
n_layers_input = Interval(1, 1)
# All layers that have a .features attributes
input_layer_types = (
napari.layers.Labels,
napari.layers.Points,
napari.layers.Shapes,
napari.layers.Tracks,
napari.layers.Vectors,
)

def __init__(self, napari_viewer: napari.viewer.Viewer):
super().__init__(napari_viewer)
self.axes = self.canvas.figure.subplots()

self._key_selection_widget = magicgui(
self._set_axis_keys,
x_axis_key={"choices": self._get_valid_axis_keys},
call_button="plot",
)
self.layout().addWidget(self._key_selection_widget.native)

self.update_layers(None)

def clear(self) -> None:
"""
Clear the axes.
"""
self.axes.clear()

self.layout().addWidget(self._key_selection_widget.native)

@property
def x_axis_key(self) -> Optional[str]:
"""Key to access x axis data from the FeaturesTable"""
return self._x_axis_key

@x_axis_key.setter
def x_axis_key(self, key: Optional[str]) -> None:
self._x_axis_key = key
self._draw()

def _set_axis_keys(self, x_axis_key: str) -> None:
"""Set both axis keys and then redraw the plot"""
self._x_axis_key = x_axis_key
self._draw()

def _get_valid_axis_keys(
self, combo_widget: Optional[ComboBox] = None
) -> List[str]:
"""
Get the valid axis keys from the layer FeatureTable.
Returns
-------
axis_keys : List[str]
The valid axis keys in the FeatureTable. If the table is empty
or there isn't a table, returns an empty list.
"""
if len(self.layers) == 0 or not (hasattr(self.layers[0], "features")):
return []
else:
return self.layers[0].features.keys()

def _get_data(self) -> Tuple[List[np.ndarray], str, str]:
"""Get the plot data.
Returns
-------
data : List[np.ndarray]
List contains X and Y columns from the FeatureTable. Returns
an empty array if nothing to plot.
x_axis_name : str
The title to display on the x axis. Returns
an empty string if nothing to plot.
"""
if not hasattr(self.layers[0], "features"):
# if the selected layer doesn't have a featuretable,
# skip draw
return [], ""

feature_table = self.layers[0].features

if (
(len(feature_table) == 0)
or (self.x_axis_key is None)
):
return [], ""

data = feature_table[self.x_axis_key]
x_axis_name = self.x_axis_key.replace("_", " ")

return data, x_axis_name

def _on_update_layers(self) -> None:
"""
This is called when the layer selection changes by
``self.update_layers()``.
"""
if hasattr(self, "_key_selection_widget"):
self._key_selection_widget.reset_choices()

# reset the axis keys
self._x_axis_key = None

def draw(self) -> None:
"""Clear the axes and histogram the currently selected layer/slice."""

data, x_axis_name = self._get_data()

if len(data) == 0:
return

_, _, _ = self.axes.hist(data, bins=50, edgecolor='white',
linewidth=0.3)

# # set ax labels
self.axes.set_xlabel(x_axis_name)
self.axes.set_ylabel('Counts [#]')
21 changes: 20 additions & 1 deletion src/napari_matplotlib/tests/test_histogram.py
Original file line number Diff line number Diff line change
@@ -1,8 +1,27 @@
from napari_matplotlib import HistogramWidget
from napari_matplotlib import HistogramWidget, FeaturesHistogramWidget


def test_example_q_widget(make_napari_viewer, image_data):
# Smoke test adding a histogram widget
viewer = make_napari_viewer()
viewer.add_image(image_data[0], **image_data[1])
HistogramWidget(viewer)

def test_feature_histogram(make_napari_viewer):

import numpy as np

n_points = 1000
random_points = np.random.random((n_points,3))*10
feature1 = np.random.random(n_points)
feature2 = np.random.normal(size=n_points)

viewer = make_napari_viewer()
viewer.add_points(random_points, properties={'feature1': feature1, 'feature2': feature2}, face_color='feature1', size=1)

widget = FeaturesHistogramWidget(viewer)
viewer.window.add_dock_widget(widget)
widget._set_axis_keys('feature1')
widget._key_selection_widget()
widget._set_axis_keys('feature2')
widget._key_selection_widget()

0 comments on commit fb47952

Please sign in to comment.