diff --git a/docs/changelog.rst b/docs/changelog.rst index 2304fecf..6f77e0c3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,7 +1,11 @@ Changelog ========= -1.0.3 +1.1.0 ----- +Additions +~~~~~~~~~ +- Added a widget to draw a histogram of features. + Changes ~~~~~~~ - The slice widget is now limited to slicing along the x/y dimensions. Support diff --git a/docs/user_guide.rst b/docs/user_guide.rst index 0872e540..fbd48db1 100644 --- a/docs/user_guide.rst +++ b/docs/user_guide.rst @@ -30,6 +30,7 @@ These widgets plot the data stored in the ``.features`` attribute of individual Currently available are: - 2D scatter plots of two features against each other. +- Histograms of individual features. To use these: diff --git a/src/napari_matplotlib/__init__.py b/src/napari_matplotlib/__init__.py index 2d0e7e71..803a1b8d 100644 --- a/src/napari_matplotlib/__init__.py +++ b/src/napari_matplotlib/__init__.py @@ -4,6 +4,7 @@ __version__ = "unknown" +from .feature_histogram import * # NoQA from .histogram import * # NoQA from .scatter import * # NoQA from .slice import * # NoQA diff --git a/src/napari_matplotlib/feature_histogram.py b/src/napari_matplotlib/feature_histogram.py new file mode 100644 index 00000000..935d32c8 --- /dev/null +++ b/src/napari_matplotlib/feature_histogram.py @@ -0,0 +1,91 @@ +from typing import Optional + +import numpy as np +from qtpy.QtWidgets import QCheckBox, QComboBox, QLabel, QWidget + +from napari_matplotlib.base import SingleAxesWidget + +__all__ = ["FeatureHistogramWidget"] + +import napari + +from .util import Interval + + +class FeatureHistogramWidget(SingleAxesWidget): + """ + Display a histogram of the features stored in the currently selected layer. + """ + + n_layers_input = Interval(1, 1) + input_layer_types = ( + napari.layers.Image, + napari.layers.Labels, + napari.layers.Points, + napari.layers.Surface, + ) + + def __init__( + self, + napari_viewer: napari.viewer.Viewer, + parent: Optional[QWidget] = None, + ): + super().__init__(napari_viewer) + + # Feature selection + self.layout().addWidget(QLabel("Feature:")) + self.plot_column_name = QComboBox() + self.plot_column_name.currentIndexChanged.connect(self._draw) + self.layout().addWidget(self.plot_column_name) + + # Logarithmic plot yes/no + self.logarithmic_plot = QCheckBox("Logarithmic") + self.logarithmic_plot.stateChanged.connect(self._draw) + self.layout().addWidget(self.logarithmic_plot) + + # listen to laer changed + napari_viewer.layers.selection.events.changed.connect( + self.update_available_columns + ) + + # setup GUI + self._update_layers(None) + self.update_available_columns() + + def update_available_columns(self) -> None: + """ + Update the feature list pulldown as soon as the user changes the selected layer + """ + selected_layer = self.layers[0] + self.plot_column_name.currentIndex() + + if selected_layer is not None: + features = selected_layer.features + if features is not None: + self.plot_column_name.clear() + feats = list(features.keys()) + print(f"Updating features list: {feats}") + self.plot_column_name.addItems(feats) + self.plot_column_name.setCurrentIndex(0) + + def draw(self) -> None: + """ + Clear the axes and histogram the currently selected feature. + """ + layer = self.layers[0] + if layer is None: + self.clear() + return + + selected_column = self.plot_column_name.currentText() + if selected_column is not None and len(selected_column) > 0: + data = layer.features[selected_column] + bins = np.linspace(np.min(data), np.max(data), 100) + self.clear() + self.axes.hist( + data, + bins=bins, + label=layer.name + " / " + selected_column, + log=self.logarithmic_plot.isChecked(), + ) + self.axes.legend() diff --git a/src/napari_matplotlib/napari.yaml b/src/napari_matplotlib/napari.yaml index b736592b..b5ea3017 100644 --- a/src/napari_matplotlib/napari.yaml +++ b/src/napari_matplotlib/napari.yaml @@ -2,6 +2,10 @@ name: napari-matplotlib display_name: napari Matplotlib contributions: commands: + - id: napari-matplotlib.feature_histogram + python_name: napari_matplotlib:FeatureHistogramWidget + title: Make a feature histogram + - id: napari-matplotlib.histogram python_name: napari_matplotlib:HistogramWidget title: Make a histogram @@ -19,6 +23,9 @@ contributions: title: Plot a 1D slice widgets: + - command: napari-matplotlib.feature_histogram + display_name: Feature Histogram + - command: napari-matplotlib.histogram display_name: Histogram diff --git a/src/napari_matplotlib/tests/baseline/test_feature_histogram.png b/src/napari_matplotlib/tests/baseline/test_feature_histogram.png new file mode 100644 index 00000000..4c3c9d49 Binary files /dev/null and b/src/napari_matplotlib/tests/baseline/test_feature_histogram.png differ diff --git a/src/napari_matplotlib/tests/test_feature_histogram.py b/src/napari_matplotlib/tests/test_feature_histogram.py new file mode 100644 index 00000000..75500c73 --- /dev/null +++ b/src/napari_matplotlib/tests/test_feature_histogram.py @@ -0,0 +1,26 @@ +from copy import deepcopy + +import numpy as np +import pytest + +from napari_matplotlib import FeatureHistogramWidget + + +@pytest.mark.mpl_image_compare +def test_feature_histogram(make_napari_viewer): + # Smoke test adding a histogram widget + viewer = make_napari_viewer() + viewer.theme = "light" + + image = np.asarray([[0, 1], [2, 1]]) + + viewer.add_image(image) + labels_layer = viewer.add_labels(image.astype(int)) + labels_layer.features = { + "labels": [1, 2], + "area": [2, 1], + "aspect_ratio": [2, 1], + } + + fig = FeatureHistogramWidget(viewer).figure + return deepcopy(fig)