From 91f6d1635150cb752273378851e7e1079ea92fd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= <38459088+jo-mueller@users.noreply.github.com> Date: Thu, 1 Jun 2023 16:53:53 +0200 Subject: [PATCH 01/22] added a Feature Histogram Widget Co-Authored-By: Marcelo Zoccoler <26173597+zoccoler@users.noreply.github.com> --- src/napari_matplotlib/napari.yaml | 7 + src/napari_matplotlib/scatter.py | 123 +++++++++++++++++- src/napari_matplotlib/tests/test_histogram.py | 21 ++- 3 files changed, 149 insertions(+), 2 deletions(-) diff --git a/src/napari_matplotlib/napari.yaml b/src/napari_matplotlib/napari.yaml index b736592b..71af0ca6 100644 --- a/src/napari_matplotlib/napari.yaml +++ b/src/napari_matplotlib/napari.yaml @@ -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 @@ -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 diff --git a/src/napari_matplotlib/scatter.py b/src/napari_matplotlib/scatter.py index 3b0f918c..b3c1147b 100644 --- a/src/napari_matplotlib/scatter.py +++ b/src/napari_matplotlib/scatter.py @@ -9,7 +9,7 @@ from .base import NapariMPLWidget from .util import Interval -__all__ = ["ScatterWidget", "FeaturesScatterWidget"] +__all__ = ["ScatterWidget", "FeaturesScatterWidget", "FeaturesHistogramWidget"] class ScatterBaseWidget(NapariMPLWidget): @@ -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 [#]') diff --git a/src/napari_matplotlib/tests/test_histogram.py b/src/napari_matplotlib/tests/test_histogram.py index f497a1a9..54b3ad29 100644 --- a/src/napari_matplotlib/tests/test_histogram.py +++ b/src/napari_matplotlib/tests/test_histogram.py @@ -1,4 +1,4 @@ -from napari_matplotlib import HistogramWidget +from napari_matplotlib import HistogramWidget, FeaturesHistogramWidget def test_example_q_widget(make_napari_viewer, image_data): @@ -6,3 +6,22 @@ def test_example_q_widget(make_napari_viewer, image_data): 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() From 230a303bda3edb39bdb2f19e0649b28df96a1e59 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Mon, 26 Jun 2023 11:35:30 +0200 Subject: [PATCH 02/22] Update src/napari_matplotlib/scatter.py Co-authored-by: David Stansby --- src/napari_matplotlib/scatter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/napari_matplotlib/scatter.py b/src/napari_matplotlib/scatter.py index b3c1147b..84fd4e74 100644 --- a/src/napari_matplotlib/scatter.py +++ b/src/napari_matplotlib/scatter.py @@ -340,6 +340,6 @@ def draw(self) -> None: _, _, _ = self.axes.hist(data, bins=50, edgecolor='white', linewidth=0.3) - # # set ax labels + # set ax labels self.axes.set_xlabel(x_axis_name) self.axes.set_ylabel('Counts [#]') From e5cb943c7d457f4574a6ee13f569c86cea80d304 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Mon, 26 Jun 2023 11:35:39 +0200 Subject: [PATCH 03/22] Update src/napari_matplotlib/scatter.py Co-authored-by: David Stansby --- src/napari_matplotlib/scatter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/napari_matplotlib/scatter.py b/src/napari_matplotlib/scatter.py index 84fd4e74..80931c7d 100644 --- a/src/napari_matplotlib/scatter.py +++ b/src/napari_matplotlib/scatter.py @@ -337,7 +337,7 @@ def draw(self) -> None: if len(data) == 0: return - _, _, _ = self.axes.hist(data, bins=50, edgecolor='white', + self.axes.hist(data, bins=50, edgecolor='white', linewidth=0.3) # set ax labels From 524fdbd5064c6c635bf4815cd5ff986ca4b328c0 Mon Sep 17 00:00:00 2001 From: Johannes Soltwedel <38459088+jo-mueller@users.noreply.github.com> Date: Mon, 26 Jun 2023 11:35:53 +0200 Subject: [PATCH 04/22] Update src/napari_matplotlib/scatter.py Co-authored-by: David Stansby --- src/napari_matplotlib/scatter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/napari_matplotlib/scatter.py b/src/napari_matplotlib/scatter.py index 80931c7d..210a8053 100644 --- a/src/napari_matplotlib/scatter.py +++ b/src/napari_matplotlib/scatter.py @@ -288,7 +288,7 @@ def _get_valid_axis_keys( else: return self.layers[0].features.keys() - def _get_data(self) -> Tuple[List[np.ndarray], str, str]: + def _get_data(self) -> Tuple[np.ndarray, str]: """Get the plot data. Returns From f69761871a301a98d8fac475df6cf5a0bdf56c33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= <38459088+jo-mueller@users.noreply.github.com> Date: Mon, 26 Jun 2023 11:44:40 +0200 Subject: [PATCH 05/22] moved new widget to `histogram.py` --- src/napari_matplotlib/histogram.py | 126 ++++++++++++++++++++++++++++- src/napari_matplotlib/scatter.py | 121 --------------------------- 2 files changed, 124 insertions(+), 123 deletions(-) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index 1e273e70..6a07bdf5 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -1,13 +1,14 @@ -from typing import Optional +from typing import Optional, List, Tuple import napari import numpy as np from qtpy.QtWidgets import QWidget +from magicgui import magicgui, ComboBox from .base import NapariMPLWidget from .util import Interval -__all__ = ["HistogramWidget"] +__all__ = ["HistogramWidget", "FeaturesHistogramWidget"] _COLORS = {"r": "tab:red", "g": "tab:green", "b": "tab:blue"} @@ -63,3 +64,124 @@ def draw(self) -> None: self.axes.hist(data.ravel(), bins=bins, label=layer.name) self.axes.legend() + + +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[np.ndarray, 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 [#]') \ No newline at end of file diff --git a/src/napari_matplotlib/scatter.py b/src/napari_matplotlib/scatter.py index eb6e9d27..c3c40c16 100644 --- a/src/napari_matplotlib/scatter.py +++ b/src/napari_matplotlib/scatter.py @@ -248,124 +248,3 @@ def on_update_layers(self) -> None: self._selectors[dim].removeItem(0) # Add keys for newly selected layer self._selectors[dim].addItems(self._get_valid_axis_keys()) - - -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[np.ndarray, 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 [#]') From 98d84f604a2fdb8457b78591291ebff61ca486e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= <38459088+jo-mueller@users.noreply.github.com> Date: Mon, 26 Jun 2023 13:12:19 +0200 Subject: [PATCH 06/22] put import at correct location --- src/napari_matplotlib/scatter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/napari_matplotlib/scatter.py b/src/napari_matplotlib/scatter.py index c3c40c16..622a71fb 100644 --- a/src/napari_matplotlib/scatter.py +++ b/src/napari_matplotlib/scatter.py @@ -8,7 +8,7 @@ from .util import Interval __all__ = ["ScatterBaseWidget", "ScatterWidget", - "FeaturesScatterWidget", "FeaturesHistogramWidget"] + "FeaturesScatterWidget"] class ScatterBaseWidget(NapariMPLWidget): From c3d1d018502a90bba9f594fb10fe327a7812e509 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= <38459088+jo-mueller@users.noreply.github.com> Date: Mon, 26 Jun 2023 13:12:58 +0200 Subject: [PATCH 07/22] introduced `FEATURES_LAYER_TYPES` variable to be used across widgets --- src/napari_matplotlib/features.py | 9 +++++++++ src/napari_matplotlib/histogram.py | 9 ++------- src/napari_matplotlib/scatter.py | 9 ++------- 3 files changed, 13 insertions(+), 14 deletions(-) create mode 100644 src/napari_matplotlib/features.py diff --git a/src/napari_matplotlib/features.py b/src/napari_matplotlib/features.py new file mode 100644 index 00000000..16a2efe1 --- /dev/null +++ b/src/napari_matplotlib/features.py @@ -0,0 +1,9 @@ +from napari.layers import Labels, Points, Shapes, Tracks, Vectors + +FEATURES_LAYER_TYPES = ( + Labels, + Points, + Shapes, + Tracks, + Vectors, +) \ No newline at end of file diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index 6a07bdf5..1f346c5e 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -7,6 +7,7 @@ from .base import NapariMPLWidget from .util import Interval +from .features import FEATURES_LAYER_TYPES __all__ = ["HistogramWidget", "FeaturesHistogramWidget"] @@ -69,13 +70,7 @@ def draw(self) -> 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, - ) + input_layer_types = FEATURES_LAYER_TYPES def __init__(self, napari_viewer: napari.viewer.Viewer): super().__init__(napari_viewer) diff --git a/src/napari_matplotlib/scatter.py b/src/napari_matplotlib/scatter.py index 622a71fb..144c604b 100644 --- a/src/napari_matplotlib/scatter.py +++ b/src/napari_matplotlib/scatter.py @@ -6,6 +6,7 @@ from .base import NapariMPLWidget from .util import Interval +from .features import FEATURES_LAYER_TYPES __all__ = ["ScatterBaseWidget", "ScatterWidget", "FeaturesScatterWidget"] @@ -107,13 +108,7 @@ class FeaturesScatterWidget(ScatterBaseWidget): 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, - ) + input_layer_types = FEATURES_LAYER_TYPES def __init__( self, From e965cd32b2f927a1d5a6a559c327a4a4df907693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= <38459088+jo-mueller@users.noreply.github.com> Date: Mon, 26 Jun 2023 13:18:30 +0200 Subject: [PATCH 08/22] used `SingleAxesWidget` in FeatureHistogram --- src/napari_matplotlib/histogram.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index c04c2b17..048bf6cf 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -60,14 +60,13 @@ def draw(self) -> None: self.axes.legend() -class FeaturesHistogramWidget(NapariMPLWidget): +class FeaturesHistogramWidget(SingleAxesWidget): n_layers_input = Interval(1, 1) # All layers that have a .features attributes input_layer_types = FEATURES_LAYER_TYPES 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, @@ -78,14 +77,6 @@ def __init__(self, napari_viewer: napari.viewer.Viewer): 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""" From 6731648a9775f479d082ea6b4bfa0db59509182b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= <38459088+jo-mueller@users.noreply.github.com> Date: Mon, 26 Jun 2023 13:19:19 +0200 Subject: [PATCH 09/22] Added optional `parent` argument added parent to init --- src/napari_matplotlib/histogram.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index 048bf6cf..ac1d1bfd 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -65,11 +65,13 @@ class FeaturesHistogramWidget(SingleAxesWidget): # All layers that have a .features attributes input_layer_types = FEATURES_LAYER_TYPES - def __init__(self, napari_viewer: napari.viewer.Viewer): - super().__init__(napari_viewer) + def __init__( + self, + napari_viewer: napari.viewer.Viewer, + parent: Optional[QWidget] = None,): + super().__init__(napari_viewer, parent=parent) self._key_selection_widget = magicgui( - self._set_axis_keys, x_axis_key={"choices": self._get_valid_axis_keys}, call_button="plot", ) From dcab365f898cb945207395af2f0e84afd9170dff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= <38459088+jo-mueller@users.noreply.github.com> Date: Mon, 26 Jun 2023 13:21:43 +0200 Subject: [PATCH 10/22] removed unused ComboBox --- src/napari_matplotlib/histogram.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index ac1d1bfd..213a08b3 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -3,7 +3,7 @@ import napari import numpy as np from qtpy.QtWidgets import QWidget -from magicgui import magicgui, ComboBox +from magicgui import magicgui from .base import SingleAxesWidget from .util import Interval @@ -94,9 +94,7 @@ def _set_axis_keys(self, x_axis_key: str) -> None: self._x_axis_key = x_axis_key self._draw() - def _get_valid_axis_keys( - self, combo_widget: Optional[ComboBox] = None - ) -> List[str]: + def _get_valid_axis_keys(self) -> List[str]: """ Get the valid axis keys from the layer FeatureTable. From d1207f0c49d5e6bfe60f16cdd34d5e76822fba9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= <38459088+jo-mueller@users.noreply.github.com> Date: Mon, 26 Jun 2023 14:17:50 +0200 Subject: [PATCH 11/22] updated tests --- src/napari_matplotlib/tests/test_histogram.py | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/src/napari_matplotlib/tests/test_histogram.py b/src/napari_matplotlib/tests/test_histogram.py index 847235f1..1b17045f 100644 --- a/src/napari_matplotlib/tests/test_histogram.py +++ b/src/napari_matplotlib/tests/test_histogram.py @@ -44,14 +44,27 @@ def test_feature_histogram(make_napari_viewer): 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) + viewer.add_points(random_points, + properties={'feature1': feature1, 'feature2': feature2}, + name='points1') + viewer.add_points(random_points, + properties={'feature1': feature1, 'feature2': feature2}, + name='points2') widget = FeaturesHistogramWidget(viewer) viewer.window.add_dock_widget(widget) + + # Check whether changing the selected key changes the plot widget._set_axis_keys('feature1') - widget._key_selection_widget() + fig1 = deepcopy(widget.figure) + widget._set_axis_keys('feature2') - widget._key_selection_widget() + assert_figures_not_equal(widget.figure, fig1) + + #check whether selecting a different layer produces the same plot + viewer.layers.selection.clear() + viewer.layers.selection.add(viewer.layers[1]) + assert_figures_equal(widget.figure, fig1) def test_change_layer(make_napari_viewer, brain_data, astronaut_data): From 476e2f26a5ffd067e318ab36c5666b4b8f38b5ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= <38459088+jo-mueller@users.noreply.github.com> Date: Mon, 26 Jun 2023 14:18:02 +0200 Subject: [PATCH 12/22] Used PyQt instead of magicgui --- src/napari_matplotlib/histogram.py | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index 213a08b3..1af9a9e1 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -2,8 +2,7 @@ import napari import numpy as np -from qtpy.QtWidgets import QWidget -from magicgui import magicgui +from qtpy.QtWidgets import QComboBox, QLabel, QVBoxLayout, QWidget from .base import SingleAxesWidget from .util import Interval @@ -71,13 +70,14 @@ def __init__( parent: Optional[QWidget] = None,): super().__init__(napari_viewer, parent=parent) - self._key_selection_widget = magicgui( - x_axis_key={"choices": self._get_valid_axis_keys}, - call_button="plot", - ) - self.layout().addWidget(self._key_selection_widget.native) + self.layout().addLayout(QVBoxLayout()) + self._key_selection_widget = QComboBox() + self.layout().addWidget(QLabel("Key:")) + self.layout().addWidget(self._key_selection_widget) - self.update_layers(None) + self._key_selection_widget.currentTextChanged.connect(self._set_axis_keys) + + self._update_layers(None) @property def x_axis_key(self) -> Optional[str]: @@ -139,17 +139,17 @@ def _get_data(self) -> Tuple[np.ndarray, str]: return data, x_axis_name - def _on_update_layers(self) -> None: + def on_update_layers(self) -> None: """ - This is called when the layer selection changes by - ``self.update_layers()``. + 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 + # Clear combobox + self._key_selection_widget.clear() + self._key_selection_widget.addItems(self._get_valid_axis_keys()) + def draw(self) -> None: """Clear the axes and histogram the currently selected layer/slice.""" @@ -159,7 +159,7 @@ def draw(self) -> None: return self.axes.hist(data, bins=50, edgecolor='white', - linewidth=0.3) + linewidth=0.3) # set ax labels self.axes.set_xlabel(x_axis_name) From 47ccb37c55d4294a2deb6f7398e3327966cc1de8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= <38459088+jo-mueller@users.noreply.github.com> Date: Mon, 26 Jun 2023 14:18:44 +0200 Subject: [PATCH 13/22] codestyle --- src/napari_matplotlib/tests/test_histogram.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/napari_matplotlib/tests/test_histogram.py b/src/napari_matplotlib/tests/test_histogram.py index 1b17045f..7c71ff2c 100644 --- a/src/napari_matplotlib/tests/test_histogram.py +++ b/src/napari_matplotlib/tests/test_histogram.py @@ -39,7 +39,7 @@ def test_feature_histogram(make_napari_viewer): import numpy as np n_points = 1000 - random_points = np.random.random((n_points,3))*10 + random_points = np.random.random((n_points, 3)) * 10 feature1 = np.random.random(n_points) feature2 = np.random.normal(size=n_points) @@ -61,7 +61,7 @@ def test_feature_histogram(make_napari_viewer): widget._set_axis_keys('feature2') assert_figures_not_equal(widget.figure, fig1) - #check whether selecting a different layer produces the same plot + # check whether selecting a different layer produces the same plot viewer.layers.selection.clear() viewer.layers.selection.add(viewer.layers[1]) assert_figures_equal(widget.figure, fig1) From a40faf2d5e8f7bf72737cf0efd8b1f28232b9261 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= <38459088+jo-mueller@users.noreply.github.com> Date: Mon, 26 Jun 2023 14:39:27 +0200 Subject: [PATCH 14/22] codestyle --- src/napari_matplotlib/histogram.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index 1af9a9e1..f4bcc346 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -1,4 +1,5 @@ -from typing import Optional, List, Tuple +from typing import Optional, List, Tuple, Any +import numpy.typing as npt import napari import numpy as np @@ -60,6 +61,9 @@ def draw(self) -> None: class FeaturesHistogramWidget(SingleAxesWidget): + """ + Display a histogram of selected feature attached to selected layer. + """ n_layers_input = Interval(1, 1) # All layers that have a .features attributes input_layer_types = FEATURES_LAYER_TYPES @@ -109,7 +113,7 @@ def _get_valid_axis_keys(self) -> List[str]: else: return self.layers[0].features.keys() - def _get_data(self) -> Tuple[np.ndarray, str]: + def _get_data(self) -> Tuple[npt.NDArray[Any], str]: """Get the plot data. Returns @@ -163,4 +167,4 @@ def draw(self) -> None: # set ax labels self.axes.set_xlabel(x_axis_name) - self.axes.set_ylabel('Counts [#]') \ No newline at end of file + self.axes.set_ylabel('Counts [#]') From ed658cdbfce9aafa2ea0517cbaaee704a47b71da Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 26 Jun 2023 18:47:13 +0000 Subject: [PATCH 15/22] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/napari_matplotlib/features.py | 2 +- src/napari_matplotlib/histogram.py | 29 +++++++++---------- src/napari_matplotlib/scatter.py | 5 ++-- src/napari_matplotlib/tests/test_histogram.py | 23 ++++++++------- 4 files changed, 30 insertions(+), 29 deletions(-) diff --git a/src/napari_matplotlib/features.py b/src/napari_matplotlib/features.py index 16a2efe1..34abf104 100644 --- a/src/napari_matplotlib/features.py +++ b/src/napari_matplotlib/features.py @@ -6,4 +6,4 @@ Shapes, Tracks, Vectors, -) \ No newline at end of file +) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index f4bcc346..01c2c6a4 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -1,13 +1,13 @@ -from typing import Optional, List, Tuple, Any -import numpy.typing as npt +from typing import Any, List, Optional, Tuple import napari import numpy as np +import numpy.typing as npt from qtpy.QtWidgets import QComboBox, QLabel, QVBoxLayout, QWidget from .base import SingleAxesWidget -from .util import Interval from .features import FEATURES_LAYER_TYPES +from .util import Interval __all__ = ["HistogramWidget", "FeaturesHistogramWidget"] @@ -64,14 +64,16 @@ class FeaturesHistogramWidget(SingleAxesWidget): """ Display a histogram of selected feature attached to selected layer. """ + n_layers_input = Interval(1, 1) # All layers that have a .features attributes input_layer_types = FEATURES_LAYER_TYPES def __init__( - self, - napari_viewer: napari.viewer.Viewer, - parent: Optional[QWidget] = None,): + self, + napari_viewer: napari.viewer.Viewer, + parent: Optional[QWidget] = None, + ): super().__init__(napari_viewer, parent=parent) self.layout().addLayout(QVBoxLayout()) @@ -79,7 +81,9 @@ def __init__( self.layout().addWidget(QLabel("Key:")) self.layout().addWidget(self._key_selection_widget) - self._key_selection_widget.currentTextChanged.connect(self._set_axis_keys) + self._key_selection_widget.currentTextChanged.connect( + self._set_axis_keys + ) self._update_layers(None) @@ -132,10 +136,7 @@ def _get_data(self) -> Tuple[npt.NDArray[Any], str]: feature_table = self.layers[0].features - if ( - (len(feature_table) == 0) - or (self.x_axis_key is None) - ): + if (len(feature_table) == 0) or (self.x_axis_key is None): return [], "" data = feature_table[self.x_axis_key] @@ -156,15 +157,13 @@ def on_update_layers(self) -> 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) + 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 [#]') + self.axes.set_ylabel("Counts [#]") diff --git a/src/napari_matplotlib/scatter.py b/src/napari_matplotlib/scatter.py index 08464bc4..8a9f55ae 100644 --- a/src/napari_matplotlib/scatter.py +++ b/src/napari_matplotlib/scatter.py @@ -5,11 +5,10 @@ from qtpy.QtWidgets import QComboBox, QLabel, QVBoxLayout, QWidget from .base import SingleAxesWidget -from .util import Interval from .features import FEATURES_LAYER_TYPES +from .util import Interval -__all__ = ["ScatterBaseWidget", "ScatterWidget", - "FeaturesScatterWidget"] +__all__ = ["ScatterBaseWidget", "ScatterWidget", "FeaturesScatterWidget"] class ScatterBaseWidget(SingleAxesWidget): diff --git a/src/napari_matplotlib/tests/test_histogram.py b/src/napari_matplotlib/tests/test_histogram.py index 7c71ff2c..793c1a35 100644 --- a/src/napari_matplotlib/tests/test_histogram.py +++ b/src/napari_matplotlib/tests/test_histogram.py @@ -2,7 +2,7 @@ import pytest -from napari_matplotlib import HistogramWidget, FeaturesHistogramWidget +from napari_matplotlib import FeaturesHistogramWidget, HistogramWidget from napari_matplotlib.tests.helpers import ( assert_figures_equal, assert_figures_not_equal, @@ -35,7 +35,6 @@ def test_histogram_3D(make_napari_viewer, brain_data): def test_feature_histogram(make_napari_viewer): - import numpy as np n_points = 1000 @@ -44,21 +43,25 @@ def test_feature_histogram(make_napari_viewer): feature2 = np.random.normal(size=n_points) viewer = make_napari_viewer() - viewer.add_points(random_points, - properties={'feature1': feature1, 'feature2': feature2}, - name='points1') - viewer.add_points(random_points, - properties={'feature1': feature1, 'feature2': feature2}, - name='points2') + viewer.add_points( + random_points, + properties={"feature1": feature1, "feature2": feature2}, + name="points1", + ) + viewer.add_points( + random_points, + properties={"feature1": feature1, "feature2": feature2}, + name="points2", + ) widget = FeaturesHistogramWidget(viewer) viewer.window.add_dock_widget(widget) # Check whether changing the selected key changes the plot - widget._set_axis_keys('feature1') + widget._set_axis_keys("feature1") fig1 = deepcopy(widget.figure) - widget._set_axis_keys('feature2') + widget._set_axis_keys("feature2") assert_figures_not_equal(widget.figure, fig1) # check whether selecting a different layer produces the same plot From 9d6988e4bf3ca5982475381994275b92d567fce4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= <38459088+jo-mueller@users.noreply.github.com> Date: Fri, 30 Jun 2023 13:18:45 +0200 Subject: [PATCH 16/22] added check for `_key_selection_widget` removed `reset_choices()` statement Revert "removed `reset_choices()` statement" This reverts commit ed8fc0acd3a53f446ba71313be5209b0b589caff. removed `reset_choices()` from feature histogram From a2b83caea0f07446d4252d821f8f2f3cc3a27895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= <38459088+jo-mueller@users.noreply.github.com> Date: Fri, 30 Jun 2023 16:19:15 +0200 Subject: [PATCH 17/22] removed `reset_choices` from old widget --- src/napari_matplotlib/scatter.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/napari_matplotlib/scatter.py b/src/napari_matplotlib/scatter.py index 8a9f55ae..a4148bd2 100644 --- a/src/napari_matplotlib/scatter.py +++ b/src/napari_matplotlib/scatter.py @@ -217,13 +217,6 @@ def on_update_layers(self) -> None: """ 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 - self._y_axis_key = None - # Clear combobox for dim in ["x", "y"]: while self._selectors[dim].count() > 0: From 2c95034c4d70e73a49399ba522b83406dd073a34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Johannes=20M=C3=BCller?= <38459088+jo-mueller@users.noreply.github.com> Date: Fri, 30 Jun 2023 16:43:19 +0200 Subject: [PATCH 18/22] added figure test --- .../tests/baseline/test_feature_histogram.png | Bin 0 -> 9681 bytes src/napari_matplotlib/tests/test_histogram.py | 27 ++++++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 src/napari_matplotlib/tests/baseline/test_feature_histogram.png 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 0000000000000000000000000000000000000000..1892af4467a4aee2346385437d4fc0c45216eccd GIT binary patch literal 9681 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

G!zI=`*>W|$!N)+K$@ar zE%%zsaYfy3kEM-_<{W8L>&4+Dc0}=m0_=!*f^cH&#!iK*Ko~+KwXou(G(O@7oJ8`{ zm$nbc0<{}y+oHNR+V`)SqMeduPMCAEYT%5=72&RLmc8wR$Q$Gu(AW6jByvzjyy5*X z@9<&dxf^A56WTG?UXsL3%;)7VImxKiKy#A_2A}KO>uU+54KVv?r=-4D8Ql>`(61xe zCX6&-N37%@)e33I7S6k3^>j+q+GoU(BuXEH7eS(ryQp>#`EF-xBa%T!baJS19vK;z zHV$N0@v6s91I3RQ$t|ECDNU!33S$mIM$-6o*C5FXo&cJfSY*?($OmNYwei&6;$s_6 zD}plUIu;9MvO^s@TT|Gvsua(bf>9G)6tD{Oz(D%sZ@@tSRYw|6n(%l~>mg>LnwlB8(?GP!YfwvZH-m?r( zh9A~qVp|q0!i%Jc^SELWv}sC;6~Q#6v>*784{0Dqtb&H?n3oN2q>XTIVu-3l%lN{O z+vCqbKj;`VtH%sC)C%i}&EEpHRvZMJg?CZQjy0o^is%vkDCO3r^Ng$5P|fXA-60bN zLm^c%;imCS2ly;Dsqd(wt)ZJhNyK-pPtb|0^`F5M+~E7b2oSld3`_Dg@-@;~7)>7KXns`VW7UM| ze0J95q4nZXP1jGfFET>6M6EJ`y_?TeG&gib{pzkok}-xew_AbVz|}!u;#? zn$$c#u>5vsoP<*AU6Kk=AD~()hU*Z)M-DpLal^au5!|d6axAcXG1-kb=I2D|Kf}|c zSn#HgqV&D+CcvDJ5I-iwzSkeN!`T%PjQovdjt92T)U>rb9Kk0WC3-ui9*_u+1hwk3Z9U^cwZKZc?oE?vabU;BU@4YQbCyTfH=9c%A3@2x-zD6qe zZb#XowW1%dISPQB+M~zz5qd#%Dy;9F;>S4gI?8d*jO7cwuQ!9kg3nkB!K?1^Ok0LO zqsBY)SYX(Y^)O0K79U}hIQ#@rD{d>2DT!a-E91p(uDq7s3wTz64eY=yEb^l!c^^Co zs0aeO;BI@#WIpQD^pCv&8QRl2C#^y`fLwN#ahQJf%N4V Date: Tue, 4 Jul 2023 11:47:44 +0200 Subject: [PATCH 19/22] added baseline image for histogram creation --- baseline/test_feature_histogram2.png | Bin 0 -> 12860 bytes src/napari_matplotlib/tests/test_histogram.py | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 baseline/test_feature_histogram2.png diff --git a/baseline/test_feature_histogram2.png b/baseline/test_feature_histogram2.png new file mode 100644 index 0000000000000000000000000000000000000000..b7bb19e039d73adfd05f0d3860a5a1c4d0711f72 GIT binary patch literal 12860 zcmeHtX;f3`w(SPB(6Yf42&i;MO97Easep8$jzkm%Dqs+lMnHrBp^?4~r5->~z!Ez` zbEMTELHZt4nv^0S(gX=T(nbhkez4G=o%x)CTRrWH{1FhT#5I&;ST?t^un2WnYdl^^0m2cj~Li^ zxx2V|xe#roeeAD$65U)+D5xqZ%S$_YdAWP4D=L!yyg|Y3x`QI~`>o;dkZtZ~%smlA z(1!OP-!p6`5kZunpVj^Of?vX9PowKqV!)Bb(V)|tpAbIXD8GDM>7e45v#-p%-d~a4 zF3(Gk!Q)~(UOnYsWMi3ng|5gNv zy^HW8$W;?J)@{1So%XUQ5AlQP!Z^IH`9o!Arei$2;gX~Tu1|23rr2;?JD?~z|0;s? zKP(c_$&kgR36q$*=EC=kb+J2gHPs_=@i=CYLzDIUQHLspNVG#Kg1nOGyo3FP2uGmGwfSv0JuPFK-eb4dho7nE!hk8MKa}n@;DI1ld|k2yKG&Xoc?P9C ze`-gKm_cyW3%BaP{U{VpSH(bRZsom_Jc4v8Ulx?kYF96-!fuq&SZ4B7$!xp`<@UIJ z;rNTaVF${pVm#t)$!-U3!76sietby(De=XPpA2MIpTMdr-ohS&7MvpZO3Ipt>Z zBQ2PbFu0M{8Gx)jt*Nn3i?bz@*_D<1Wz&}z%Zpo7489y25ls?8%_vrHV0zaZ`|8;n z^S-Hx+vMOgOnS;<$m@;d7&4d5A+3+C&{abcvISqSKFxSOw^mpOugaG^>H}eA;FBoLgaeEB|UxFiL%H% z8C!kX)sP5cysb7O%y@=N{Y&a0{RwOyo1x? zQX(R#RNE@EgsE67VVsJAM2%W)n_IxCcX@X{y|`=6ymSM8;q$(O{|r+_%Hl7@GLfB^ z<&P6KBS?4gl3UTtne}$-%agw{iC=!Ik@yf@a*Pzg{1{fU-PglyV{B8z@JPuqisg0D zzgxUz_hsxVFH1DV|1M(=hC(KN`iDgN|Cj&RlZB$~h)*Ajff5j4l#kpuLRYQNYE$_T z$>V&ROA2q^j8JtevfP5a+`i*AY;7J&2SGwzHt$9d>HU0gSihe}_~7!VE;+LywR)$X zI<@F~#*vUe@(qs*Nh$`xobGn@FpoQE^h8GVii?Q7ES3*>rSRzz@+1QQsemM8kJr^l zZ79{wj>(7+MMzA^WYxNXS((U)$IaL7p>f4igYF4~pEwXix!F?iu9A%GEwLHTn-{#` z$8Lg;a;H{XkvuENiAaa$MWaB?32J1`Fn)exX%y}j?7O+IreJRvL05l#-VD2=rETHK zfT_2op1DZNLJj5}e1fYZOXF!~9EOXl{UE5k!%k%(;{`Duoi0r9);>ZY?5UB45P~HK zAm-T}Eejjt3~x)it#V96#CXY(G?^xW*g?PE-`{Tb0l*z#1|US{;rI+dk`_UV^!0sm zhGDjrv5JvVyI}hSyE;ZgFA(;?`SED*#`2H!DE`t%wDym$dyl8wgg4zRT9AyD za*9`vHf24|5FYkG&Jxvqdovy#(pFoam}}O)E2Tx23~v|~Ov=1nM4X~Q8T2~@Fv>|J z{(wSZ12~%6+%K({v5OsnIw50M+X#`u_U5PY!phO3n|@b|#y@NqkYbBklZCjm%)WX~ zZ|@WCEkW2=Gp5SuR5EwCxcV*+<+7y$o%ycDD@?VkJ&CoFP%LM*dC%9lkF48>*~Wh? z4c3K72q!uFPU~fPL%!3pWTMa+aVc!opY60MiMz7n8@tBB59|_r)Y8;taK_vh|1jNZ z$Jct$v#pZ7I#w2PBKaUj=;LTq-IJuddGdB-Hx$$f)2xN@LUx!i6c|;PW1$6%$D6GP z1c1#RPaNz{aG?AB4}3@{{oL2U`w!yrzx(AYM<)W9dk}=#FA|zKyXsy0`t?H2+UNTb ziq#b?r;@v(itB;=uS^bwzBJjj3*B4s$!AaZ9&QpcJuh}+3-E-05xh+wYFj8e3b~Gc zL)vTfCQ1~QW34AHZow`y(!RYs+pzX*YiQ<|!|b!D-ba4on|CMO`4cewj0TA(HgEqE zA^zb+t~qC2**PQ@%%CepD;t;y<51p&Iu}e7byZvRSWI*ZJKU4;$bPSs7PHy>IF#;l zm$yr^yhVr9PPew<9~hjM#W5CIq$KO}H!7b|C*H3_t*pOi;P9X2p{|r{f}8V;?o#EN zH{Z^19h@Anc~P#gC=zvIw3u|i93rM-Q zKTqCDGLDQhzA(H}I@1@jgh8VtBOK!okIX$$f;83``FTrhl@vgP!Q6&>R-TMqM-g7q z=)Bbb%4YnP_^yi6)h=bHP~Y)5T70vELz6S*u<5Wmuz^*Zh$3ZGKp>yGo1Fqz<1@?@ zVp4c@FsS4rJQ0NgFp8dqEYrvXM2IsMSBOF5A~z~4WfUKCXHEh6!wY8$wJOd56{+XZ zg__~n_cIwpNdo)rH$bnR`UdEdEl&!(0W9L8pD!dTZ-K@O&_Nq?7l%X6)L0 zM@(fqa{UMG3?jT?E|6^DW2rCu?&y2&avh_uwwW^@wLESV62!bNRq$_T?9NyxVb60c z{hSI`GBYdtYgVhd;nvB$m;35F!mgyp+4>hmdWhS(<(9ac_ul{8%&Dveb+m%jyKhvX zV^iLx96PbFXDqpoGYVsDC+1;e{h1zSjU#2m%dtag7kqsiKxO3u>MG>@PcD~Ig`O{-_?qQLZr zeak?k6D$2Z=8{|2ez52vSs8^aWK}SsI*ZPs(-LtCCJB zTK5JYn>N#WP4LaQY!VGOyuCanTaazR~?q3fl)YJmmB)}vPoJ@ z?BvX|EV*5e^jemJ3rOV;R^fLn4I!Oq)2z!{X3T=oFr&()+pLN1T&i5Ee1jPM9~sk4 z9>3Q3d>$Dut1e5q_5#kFmDi?L?(U+K9QXTYz*884bH~Ok@r+ z`jX)BuexEnqi%jc!6~?ypW}by44lkTrY!)Qt zXvkT*QAhiT6JVdIEPTEKLb`;sVXBc{U~a5x{DnQ4_{7CZQXn=iV|&igNWrD5uflDV zyr@|p;y<$7Ozy3{Xl<$4_N6W=;#&l*)ace`U1U^$hxAvK77u`wjj6(FVAu7Ulj)<$ z!2v9Kl}acGQE@s{BC8Segj+EnmdI|3)QK*(giC0dXx zfsjkS3l%steEiMXuL;AzKP2!f_o4`11UFxk;rqEq$C$?kk6mZ%M366k^W^(HIWRCT z9(&(k5?4E;Z)X33VF`auk$GpJ_|`9E8F=I?T+^0V5ToMs6ZhXRSPD?$W)p9Z6AJg< zXb-#H4>z-eDNhC-56)qAZ#i|DF#B1?|g6#8?;w z`|?boGfv4iUr#TvVdpmZ(9EGtd{^_u*-O@Li`$e(Hvx6I?Y#4~T;yMt{jN?p7lvli z*#e>AwQE1b{CaJ@w$cwZ^NkGB{vd-fk&+K7oDekHsxor9J%>h~gg_-Ll$=u~%^3}Q z{~FGctw`>f4}KfCUGVJ_WR~ex9aUMU89x#b7uS7P z`=fH_`Y@KFQ0f`&_h{I=)LTBrm0cdNYy`Xza@|t&7Vfgjz&kD zdUzFdojwK|aM1T~==*Dv*^(~`4k-e0yM2Q1sy2m`wF^q{wh#8OAOAT$O<+b$Th=Jm zsu?;g3XGR&gK44*ev-u8C;7;fl0G@uHobw5lCeA!Y@boJBwZN*OePWyx{t<6Wt9ag z{s1^4uCPd&9wFnmcpTCT*!(LMlBcvuMKxL4x%}xCY=wgtZRPlN_X%V7sdhg z?BX-lvUi6j{$j2EOjPplq{wXfd^1fxN{p_Iju*?6g~%~q+X#w{L!ajYDIWY2A$)tX zE~2Ve+ZS@8B9_0=bdGm-bvJ3CJ&R>6|{e&%?vV z*~?#>9G}jTO&6n~!58E3wL#iZ{_mTDbL1nJms1^cm_pc35vhMb%oHhGxvJ;`aI}8u{lcy-Ia0s(~HXKHTWCWOCj`q z*P>VcYn~^_W9Pk6a9P!xnD_F%0>s55T3eyV@xGzpn%s#?g4CS&~<_-$~hwTrZt3 zN|lL}X}CN|*!m?@R1s}e9j&HT!YRzL>hV8`xZ9})WB;d+`aPX5jW{8;I^bn))jy-I z7U4HHvdX9o@?Qe#X3#UgaVJPYP}*BauSSk1WPx_t?-gz2e=b-46mnI{hlrfCP&kOT zjf@y@3}XA6@;nmS;77!E_9X8M;Y3Kx&s4b8k6+${C(JyFg1^s>jeReOukC&F3#3bb zjrL2_K%G#ti9QPDa~g1lHvAEpozZJn&;$)aW2avB&8@EXRs1U?PE7ONkx*d1AZ^NM z;8&Kh;z1thO_<*Q&_E4hE5Y+ZrOF7%-mn%;uxR?WhOWMCI(`#+ZecWtR12(pK(u+`n9%E_A z>JTr~E%xaEPa(7iR$CJYX>n5(kTm@sWnNoN&4rbB+0_Ov?h_AJes}pj_l#^^0ME%% zjUI*iH=PC- zyUxC(QZI+!EuWtBbvtOAFLkz6MLD_o77G4)V3hxizN~4McO1cge3}Z`OpMRF6`I%C zW^78qm513x(AW?koqdibo!E#w5Q%uYF%Mp|ZxBEL6OzL`>gvN}!UsaQJi z7a@z8*4-1i&3E(VqR{okvX6B`Dm_j|=Cjnc?ZXyaatpAU2w2PVgv7k)NfSt;vBuUy z@+aDUoYR3qQN>Pu?xuQ&*yBx!^{Al3{wSv#An}Eyw79|Y;_wzsBT)-HabE2qw0$SW zkGyJ9^Ig)W6t*k`1F^SNPPXo>QQBcAo^1+^8;$Z6!!ZPqpinV&U z)%Dt<+67D-R%2tZ0gJCa;m*rifIZVn7HT2Si4)7ux?&(kLd0zS9CCp3q>$ntH!4V_ z0EA7SVkhc=Wg;&!XI8r;^xEiQ6b zh?s{jwTAgQCFxVhTd|{?WcWhM7YA{*f4I?8?OB1c;Syf07~!W9xs;@LtPv8Yh}#%d z{`_Zz&oAw(^HtjW6UZ~8V_z~T1y8q5M)WLPuu^qfEw(g8tyO!jjjH}<9@ICX{~zf8 z|HhX)&A)mDB?^i?$d?y?7QtR`D1zrsA{M_Q{I3i(zE-;c251g2DyD71u0SCF)3f^X z@@2~~)pX&nE?hkr{b&eMFtSIRB@*6C?Ae zy@9?x7N;+3R|hChaw!x@lZga$4FM5RqgFpKF#Bc>5FTuy zFU#G~%c2gt`wIv6=`o8=Hb4<`b_?kRmUdbuJ5Pp!$ z&={bqTRRa5$_7IsR4CA{g98GwLnkG19#w5AGU#+AL7oR!ds9%_;|GPvxjs1sbtp@k zW`fvIVfulg8-~$dEk8r8c-=K2XLb?)RXTe1os-4kUBYZDn?V8YBXb;@?Y=bAUM|cf ztIo7v99`PaWA{^0imS`_vE$9+Yb|Wem>h0HW^u@ua4DU4$02t;G^KdtD=&!4Ie&upTi$YI;DRvri zx7>D(@3Y^dIU#tGdqwdTrb;Gd&lLL@5HY$_Av6&IWV9)k>RcX=F)NbP(dLYOdeD?b zm0Mg|nkwA~TcbT$E6yVv9{+b1+kfK}`)6IS|N76FAc?x~fnyq&9ecx`WKZHOFFrZS z3u)UU{J-BWBBjVRhe0c)=mLmicT0^uIv|QNh((r=E$*i@R0R&D3(KaH9zmjazcO41 zm5(van#BVzQ#6ZXg4YoPB zvT7S;x79(+6uSxXtSkh&na!O3E5(=U)3U#k9jY9xxm;IFl z;Q!ldN6DZcHl&J$*)Y?2hgl%RcU6oI2I@$@x%|kSBNMZ@w3s3vX_n1+WVHs3l55o8 z=o2yAl0Khpn*3d;;`E&!m%DcR4k%1z`=UeVH%0_ANR(DPaBEvJ%F!n0TjzV7xkKdL zIwgr%PG7|p&%l59(fOfo{Z)r(24N2hHMGq6Hib}*(lJ&S)kY7_cgFEfu5*h9N(Y@@ zB25mG(l_qC4scERboVEhDD=lI2M#8RS4b!5>cV%SP~>R=vMBiJnQL#^r`IV5QeM!^ zM~~5Z>OFf$x-#bznv=9*uCv*p3xH~U|oH0WwES&{H>%Cam!N*cp30-Dx> z4cQJcqbJ=wCrL?jOWe%rsEEON$+e8gIEAu8vKc5^<#b`t8(7aEu`r&u46M+=Q~TV8 z6nB)nr+TFkOE(Ic9rEm3%c=%8JhPy>WO^coXL(Yy-JSp32gVHStCiZodNOC68^{ehHFZ(BaEk8Ya=k~=g~J;f_iqGs)CXbUF+vj5sww5YMk3F%b9X7L1BYpN+cgD_aQik?>WeMuL&2ghIsSA}t)P;#yMT<@RsIj1iRb|?U>|5-=B zzQXVZ`}trgXkr9uYgj$^@n3!YJI-h7;BPkKsZwDl&dp*BgxvZh>tRsI;n5)ujfDZJ z$i@Y)Z>O>3dLbFedx6fnYv#`eKF`*sLmO3W*7QMMl%aig^nceYrK}N zx_)E~Lvj?WH}maz;_M&b8fbJu7{_z^7yFCp44>c}ORWPqe8SXo)jM|Lk_tINBv50~ zCNL@1qC%0ppFtY}MV4!oW9R1PG2pGi*z|EJShx0x>r+rZM2Z+)He-B)n8gl%_HI@s zhXc`t$MmXNWXo(khA_eH0V48b8*#`tP>zZQ?p94?x4gx+zEs#Z={Q!Bhy<&GV! z(2_y-brT$7vU}@``zp0|7HaNAM)>O|1~7gU2< zKkKc%r3cJR^hK-8lf@|?*EXHe)x}%6PtN4>M&n#JOP0g=FP-$SadW!AnDiU7C^fZR zo$53vi!y2@^)?&pgJ-gXUgH*-7g5CmVxV`+sxWg9lXEKHynIDEjn%Z7;T!((Rk+Jk zV)K%Na2*V{$STAW@;tSq?|uBM<;_fTsMJ_(iHwzSrrtuR=C)#&Q|yUV4m1sLZiUx#>Ns(*lWL_4l;co6JkXYE*4K6m*B@COeE}FZd^!ch*h-5XhDtW`5 zf2O>zH7qF%4kXUlD_F|FGMRGAis607D!e2mPabWl)W-g7wvx1#nn>AXY-+!w=URTE zFW1qqqlMh3^cX^0u0c%E%#AEgmr?XPFzQi8+^?=|Xu?~9{PuyNZRJD_^=JwO$(t3 z=jSKO*pC_wWO02K%zHC>KR#Mth)Aj)KguOA1Lq4rO z7YOZjd_#VE&Qf8T62*a7=M>hOn!43r3gsKd%C$4%Y?}waGpaXW{sQwE`~Oy@WsyXy zXDm>3Z0$xe9l#ITvltzSU$arO#F1cdL>Tv?1L)@gnA#=plLy{Rle2@BeNusjX6CPc zm_V{d*W2=R2(P*s#i!@Sy~H^yJUaXOq9FZDS&hbj^|q`F(q1CA2zkQB91qGU zzmmN{rEZQrGLkX5)ew%&#myt=)CWC%$^a%XaC^=vqxS~P4^SKOt(v;)EL@1_9%PuXZj zuMZ#M!cdBa9-*yxW<-QvQMD+6Kq{3(B#-iK?%xJEecc}#fVIHyBGB(VrvqA5j#;HhYyMf_sWi4`|C zO{d9!okluAhsHMOAON@*9hP@$cFUq6m_E6Pzzoo+sfj+Xe2CRQ&S+e}S4M?$nYic$ zes*|6s%LZX@%eGU;nS8Ep*@?h!eI`%Ps4!9`yqg)$Mv$nQsRt*)?4ynjbS$3ZP&ZA z@B{*UMB74La(?3G-KSn6v#p!>r*#Vr_S@s@%UCHS%CmQJfU(~S`?r}|nMeqthkAOs zqH~V%jXT$1P+jAf ze10&bQt!Z!CJJTQM~4|qkoPH=r;`0Y!@(v!GuV12cM78=dgS%sp&(6WpN2$l8^!=A zS?;AAqvou4$9W?LCpJ?_@yk<_HULP#Z(wD>i7Ka3s8r?>Ce3T)qzRA2zyx$|aq)6h zua5~mpfGy1M087N_-k!xBq1Ie72xWD2!|qeIlstjUf{Q4`vnL2Nw;&FJyp zs(iDFloP)?m{^0lRe0emMpybM)^F3YiT~B}`|j|8nKg+aW1qvcB?L<+R;N^g5=wH; zPTG4cdgjlR^rp5?SS1ODmLCa4rYncUp{=08e)1|f&*=9u=B>C4qAey1qlHGnI{PmB zvm1Vb@1W{uYQItdb3={rkfO1Uj$EQid&7tt6qZGixf67gONrxrr7sl0zbbLO)usYw z{O+002`LeR9ot{k^besC!$cR6lD($hA(C_)y|-OTF(&X{RJ7YjR6=5A%TmpQQPKtb zz4rLpGL}Nc$f*VX*yNZfM9oe)xo%+KIFAekJKXL8Iv8&ZO>``p=l^&!>R@OV6uw7% z3VTh4tYkQm_6VS;aW^#u%rY3kbz~lX*wA>q43v_J0lcM@?gvBWwz-dar&|>mxEx$* z5(#Wf2EDjcWxa)U=vms($)hlg8;3by-%+Gddf?zI8XUeF3=Q}#x?oLN3*%vA9A2EA z-;$K1N+B%{j;#$O4wwbzUP?~f1pMa9F%e!kjE1{l5c$;SEa?72uOG$%AfllyPx~nb zY;s<@XL Date: Fri, 25 Aug 2023 16:58:28 +0100 Subject: [PATCH 20/22] Fix test figure location --- .../tests/baseline/test_feature_histogram2.png | Bin 0 -> 12860 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/napari_matplotlib/tests/baseline/test_feature_histogram2.png diff --git a/src/napari_matplotlib/tests/baseline/test_feature_histogram2.png b/src/napari_matplotlib/tests/baseline/test_feature_histogram2.png new file mode 100644 index 0000000000000000000000000000000000000000..b7bb19e039d73adfd05f0d3860a5a1c4d0711f72 GIT binary patch literal 12860 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 Date: Fri, 25 Aug 2023 17:00:10 +0100 Subject: [PATCH 21/22] Run pre-commit and fix typing --- src/napari_matplotlib/histogram.py | 8 ++++---- src/napari_matplotlib/tests/test_histogram.py | 6 +++--- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index c5eb9f41..66aa7acc 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -122,7 +122,7 @@ def _get_valid_axis_keys(self) -> List[str]: else: return self.layers[0].features.keys() - def _get_data(self) -> Tuple[npt.NDArray[Any], str]: + def _get_data(self) -> Tuple[Optional[npt.NDArray[Any]], str]: """Get the plot data. Returns @@ -137,12 +137,12 @@ def _get_data(self) -> Tuple[npt.NDArray[Any], str]: if not hasattr(self.layers[0], "features"): # if the selected layer doesn't have a featuretable, # skip draw - return [], "" + return None, "" feature_table = self.layers[0].features if (len(feature_table) == 0) or (self.x_axis_key is None): - return [], "" + return None, "" data = feature_table[self.x_axis_key] x_axis_name = self.x_axis_key.replace("_", " ") @@ -164,7 +164,7 @@ 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: + if data is None: return self.axes.hist(data, bins=50, edgecolor="white", linewidth=0.3) diff --git a/src/napari_matplotlib/tests/test_histogram.py b/src/napari_matplotlib/tests/test_histogram.py index a478a2c2..006c042f 100644 --- a/src/napari_matplotlib/tests/test_histogram.py +++ b/src/napari_matplotlib/tests/test_histogram.py @@ -1,5 +1,6 @@ from copy import deepcopy +import numpy as np import pytest from napari_matplotlib import FeaturesHistogramWidget, HistogramWidget @@ -35,8 +36,6 @@ def test_histogram_3D(make_napari_viewer, brain_data): 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) @@ -72,7 +71,8 @@ def test_feature_histogram(make_napari_viewer): @pytest.mark.mpl_image_compare def test_feature_histogram2(make_napari_viewer): - import numpy as np + import numpy as np + np.random.seed(0) n_points = 1000 random_points = np.random.random((n_points, 3)) * 10 From e1ccfb18d4c48abd14037490e1acd4bf3a27baa1 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Fri, 25 Aug 2023 16:55:09 +0100 Subject: [PATCH 22/22] Update docs --- docs/changelog.rst | 6 +++++- docs/user_guide.rst | 1 + 2 files changed, 6 insertions(+), 1 deletion(-) 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: