Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added a Feature Histogram Widget #148

Merged
merged 26 commits into from
Aug 25, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
91f6d16
added a Feature Histogram Widget
jo-mueller Jun 1, 2023
94b4b72
Merge branch 'main' into add-feature-histogram
jo-mueller Jun 2, 2023
230a303
Update src/napari_matplotlib/scatter.py
jo-mueller Jun 26, 2023
e5cb943
Update src/napari_matplotlib/scatter.py
jo-mueller Jun 26, 2023
524fdbd
Update src/napari_matplotlib/scatter.py
jo-mueller Jun 26, 2023
4dcb43e
Merge branch 'add-feature-histogram' of https://github.com/jo-mueller…
jo-mueller Jun 26, 2023
f697618
moved new widget to `histogram.py`
jo-mueller Jun 26, 2023
98d84f6
put import at correct location
jo-mueller Jun 26, 2023
c3d1d01
introduced `FEATURES_LAYER_TYPES` variable to be used across widgets
jo-mueller Jun 26, 2023
622eb75
Merge branch 'add-feature-histogram' of https://github.com/jo-mueller…
jo-mueller Jun 26, 2023
e965cd3
used `SingleAxesWidget` in FeatureHistogram
jo-mueller Jun 26, 2023
6731648
Added optional `parent` argument
jo-mueller Jun 26, 2023
dcab365
removed unused ComboBox
jo-mueller Jun 26, 2023
d1207f0
updated tests
jo-mueller Jun 26, 2023
476e2f2
Used PyQt instead of magicgui
jo-mueller Jun 26, 2023
47ccb37
codestyle
jo-mueller Jun 26, 2023
a40faf2
codestyle
jo-mueller Jun 26, 2023
ed658cd
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Jun 26, 2023
9d6988e
added check for `_key_selection_widget`
jo-mueller Jun 30, 2023
a2b83ca
removed `reset_choices` from old widget
jo-mueller Jun 30, 2023
2c95034
added figure test
jo-mueller Jun 30, 2023
20300a4
added baseline image for histogram creation
jo-mueller Jul 4, 2023
ad86b38
Merge branch 'main' into add-feature-histogram
dstansby Aug 25, 2023
5e9604b
Fix test figure location
dstansby Aug 25, 2023
80ec91f
Run pre-commit and fix typing
dstansby Aug 25, 2023
e1ccfb1
Update docs
dstansby Aug 25, 2023
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
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
jo-mueller marked this conversation as resolved.
Show resolved Hide resolved
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,
)
jo-mueller marked this conversation as resolved.
Show resolved Hide resolved

def __init__(self, napari_viewer: napari.viewer.Viewer):
jo-mueller marked this conversation as resolved.
Show resolved Hide resolved
super().__init__(napari_viewer)
self.axes = self.canvas.figure.subplots()
jo-mueller marked this conversation as resolved.
Show resolved Hide resolved

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
jo-mueller marked this conversation as resolved.
Show resolved Hide resolved
) -> 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]:
jo-mueller marked this conversation as resolved.
Show resolved Hide resolved
"""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',
jo-mueller marked this conversation as resolved.
Show resolved Hide resolved
linewidth=0.3)

# # set ax labels
jo-mueller marked this conversation as resolved.
Show resolved Hide resolved
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()