From a7b09d4ce297cad9dd6fa424403c8398ec20bc74 Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Thu, 11 Jan 2024 10:55:41 +0000 Subject: [PATCH 01/22] Add widgets to set bin parameters for histograms Also set np.linspace dtype based on image dtype --- src/napari_matplotlib/histogram.py | 130 +++++++++++++++++++++++++---- 1 file changed, 116 insertions(+), 14 deletions(-) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index 2db2f08..fd44d6f 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -4,12 +4,7 @@ import numpy as np import numpy.typing as npt from matplotlib.container import BarContainer -from qtpy.QtWidgets import ( - QComboBox, - QLabel, - QVBoxLayout, - QWidget, -) +from qtpy.QtWidgets import QComboBox, QLabel, QVBoxLayout, QWidget, QGroupBox, QFormLayout, QDoubleSpinBox, QSpinBox, QAbstractSpinBox from .base import SingleAxesWidget from .features import FEATURES_LAYER_TYPES @@ -34,6 +29,50 @@ def __init__( parent: Optional[QWidget] = None, ): super().__init__(napari_viewer, parent=parent) + + # Create widgets for setting bin parameters + bins_start = QDoubleSpinBox() + bins_start.setObjectName("bins start") + bins_start.setStepType(QAbstractSpinBox.AdaptiveDecimalStepType) + bins_start.setRange(-1e10, 1e10) + bins_start.setValue(0) + bins_start.setWrapping(True) + bins_start.setKeyboardTracking(False) + bins_start.setDecimals(2) + + bins_stop = QDoubleSpinBox() + bins_stop.setObjectName("bins stop") + bins_stop.setStepType(QAbstractSpinBox.AdaptiveDecimalStepType) + bins_stop.setRange(-1e10, 1e10) + bins_stop.setValue(100) + bins_stop.setKeyboardTracking(False) + bins_stop.setDecimals(2) + + bins_num = QSpinBox() + bins_num.setObjectName("bins num") + bins_num.setRange(1, 100_000) + bins_num.setValue(101) + bins_num.setWrapping(False) + bins_num.setKeyboardTracking(False) + + # Set bins widget layout + bins_selection_layout = QFormLayout() + bins_selection_layout.addRow("start", bins_start) + bins_selection_layout.addRow("stop", bins_stop) + bins_selection_layout.addRow("num", bins_num) + + # Group the widgets and add to main layout + bins_widget_group = QGroupBox("Bins") + bins_widget_group_layout = QVBoxLayout() + bins_widget_group_layout.addLayout(bins_selection_layout) + bins_widget_group.setLayout(bins_widget_group_layout) + self.layout().addWidget(bins_widget_group) + + # Add callbacks + bins_start.valueChanged.connect(self._draw) + bins_stop.valueChanged.connect(self._draw) + bins_num.valueChanged.connect(self._draw) + self._update_layers(None) self.viewer.events.theme.connect(self._on_napari_theme_changed) @@ -53,11 +92,47 @@ def _update_contrast_lims(self) -> None: self.figure.canvas.draw() - def draw(self) -> None: - """ - Clear the axes and histogram the currently selected layer/slice. - """ - layer = self.layers[0] + @property + def bins_start(self) -> float: + """Minimum bin edge""" + return self.findChild(QDoubleSpinBox, name="bins start").value() + + @bins_start.setter + def bins_start(self, start: int | float) -> None: + """Set the minimum bin edge""" + self.findChild(QDoubleSpinBox, name="bins start").setValue(start) + + @property + def bins_stop(self) -> float: + """Maximum bin edge""" + return self.findChild(QDoubleSpinBox, name="bins stop").value() + + @bins_stop.setter + def bins_stop(self, stop: int | float) -> None: + """Set the maximum bin edge""" + self.findChild(QDoubleSpinBox, name="bins stop").setValue(stop) + + @property + def bins_num(self) -> int: + """Number of bins to use""" + return self.findChild(QSpinBox, name="bins num").value() + + @bins_num.setter + def bins_num(self, num: int) -> None: + """Set the number of bins to use""" + self.findChild(QSpinBox, name="bins num").setValue(num) + + def autoset_widget_bins(self, data: npt.ArrayLike) -> None: + """Update widgets with bins determined from the image data""" + + bins = np.linspace(np.min(data), np.max(data), 100, dtype=data.dtype) + self.bins_start = bins[0] + self.bins_stop = bins[-1] + self.bins_num = bins.size + + + def _get_layer_data(self, layer) -> np.ndarray: + """Get the data associated with a given layer""" if layer.data.ndim - layer.rgb == 3: # 3D data, can be single channel or RGB @@ -65,18 +140,45 @@ def draw(self) -> None: self.axes.set_title(f"z={self.current_z}") else: data = layer.data + # Read data into memory if it's a dask array data = np.asarray(data) + return data + + def on_update_layers(self) -> None: + """ + Called when the layer selection changes by ``self._update_layers()``. + """ + + if not self.layers: + return + + # Reset to bin start, stop and step + layer_data = self._get_layer_data(self.layers[0]) + self.autoset_widget_bins(data=layer_data) + + # Only allow integer bins for integer data + n_decimals = 0 if np.issubdtype(layer_data.dtype, np.integer) else 2 + self.findChild(QDoubleSpinBox, name="bins start").setDecimals(n_decimals) + self.findChild(QDoubleSpinBox, name="bins stop").setDecimals(n_decimals) + + def draw(self) -> None: + """ + Clear the axes and histogram the currently selected layer/slice. + """ + layer = self.layers[0] + data = self._get_layer_data(layer) + # Important to calculate bins after slicing 3D data, to avoid reading # whole cube into memory. if data.dtype.kind in {"i", "u"}: # Make sure integer data types have integer sized bins - step = abs(np.max(data) - np.min(data)) // 100 + step = (self.bins_start - self.bins_stop) // self.bins_num step = max(1, step) - bins = np.arange(np.min(data), np.max(data) + step, step) + bins = np.arange(self.bins_start, self.bins_stop + step, step) else: - bins = np.linspace(np.min(data), np.max(data), 100) + bins = np.linspace(self.bins_start, self.bins_stop, self.bins_num) if layer.rgb: # Histogram RGB channels independently From 96b2f0cd3cc6f5400ba4f8eaca71ea9482a56a07 Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Thu, 11 Jan 2024 11:44:02 +0000 Subject: [PATCH 02/22] Add test for histogram widget when setting bin parameters --- .../tests/baseline/test_histogram_2D_bins.png | Bin 0 -> 21366 bytes src/napari_matplotlib/tests/test_histogram.py | 15 ++++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/napari_matplotlib/tests/baseline/test_histogram_2D_bins.png diff --git a/src/napari_matplotlib/tests/baseline/test_histogram_2D_bins.png b/src/napari_matplotlib/tests/baseline/test_histogram_2D_bins.png new file mode 100644 index 0000000000000000000000000000000000000000..eb43c3108147079f9410a40936975d94158ac6f0 GIT binary patch literal 21366 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 b-^Cj#YN-+8Uxv&S3Wb(bypnn8+Wr3z5b2@o literal 0 HcmV?d00001 diff --git a/src/napari_matplotlib/tests/test_histogram.py b/src/napari_matplotlib/tests/test_histogram.py index 1ceca51..b392ef5 100644 --- a/src/napari_matplotlib/tests/test_histogram.py +++ b/src/napari_matplotlib/tests/test_histogram.py @@ -9,6 +9,20 @@ assert_figures_not_equal, ) +@pytest.mark.mpl_image_compare +def test_histogram_2D_bins(make_napari_viewer, astronaut_data): + viewer = make_napari_viewer() + viewer.theme = "light" + viewer.add_image(astronaut_data[0], **astronaut_data[1]) + widget = HistogramWidget(viewer) + viewer.window.add_dock_widget(widget) + widget.bins_start = -50 + widget.bins_stop = 300 + widget.bins_num = 35 + fig = widget.figure + # Need to return a copy, as original figure is too eagerley garbage + # collected by the widget + return deepcopy(fig) @pytest.mark.mpl_image_compare def test_histogram_2D(make_napari_viewer, astronaut_data): @@ -20,7 +34,6 @@ def test_histogram_2D(make_napari_viewer, astronaut_data): # collected by the widget return deepcopy(fig) - @pytest.mark.mpl_image_compare def test_histogram_3D(make_napari_viewer, brain_data): viewer = make_napari_viewer() From b8623ededfa0dd4c922f507a38a9430b86665328 Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Thu, 11 Jan 2024 11:50:21 +0000 Subject: [PATCH 03/22] Update changelog --- docs/changelog.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/changelog.rst b/docs/changelog.rst index cb591f9..96b353d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,11 @@ Changes - Dropped support for Python 3.8, and added support for Python 3.11. - Histogram plots of points and vector layers are now coloured with their napari colourmap. - Added support for Matplotlib 3.8 +- Add widgets for setting histogram bin parameters + +Bug fixes +~~~~~~~~~ +- Use integer bin limits for integer images in ``HistogramWidget`` Bug fixes ~~~~~~~~~ From 4e4fb84b8dc7a1049dd7b1795425d06e117310f5 Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Thu, 11 Jan 2024 11:52:56 +0000 Subject: [PATCH 04/22] Make linters happy --- src/napari_matplotlib/histogram.py | 24 +++++++++++++------ src/napari_matplotlib/tests/test_histogram.py | 3 +++ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index fd44d6f..c527266 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -4,7 +4,17 @@ import numpy as np import numpy.typing as npt from matplotlib.container import BarContainer -from qtpy.QtWidgets import QComboBox, QLabel, QVBoxLayout, QWidget, QGroupBox, QFormLayout, QDoubleSpinBox, QSpinBox, QAbstractSpinBox +from qtpy.QtWidgets import ( + QAbstractSpinBox, + QComboBox, + QDoubleSpinBox, + QFormLayout, + QGroupBox, + QLabel, + QSpinBox, + QVBoxLayout, + QWidget, +) from .base import SingleAxesWidget from .features import FEATURES_LAYER_TYPES @@ -124,16 +134,13 @@ def bins_num(self, num: int) -> None: def autoset_widget_bins(self, data: npt.ArrayLike) -> None: """Update widgets with bins determined from the image data""" - bins = np.linspace(np.min(data), np.max(data), 100, dtype=data.dtype) self.bins_start = bins[0] self.bins_stop = bins[-1] self.bins_num = bins.size - def _get_layer_data(self, layer) -> np.ndarray: """Get the data associated with a given layer""" - if layer.data.ndim - layer.rgb == 3: # 3D data, can be single channel or RGB data = layer.data[self.current_z] @@ -150,7 +157,6 @@ def on_update_layers(self) -> None: """ Called when the layer selection changes by ``self._update_layers()``. """ - if not self.layers: return @@ -160,8 +166,12 @@ def on_update_layers(self) -> None: # Only allow integer bins for integer data n_decimals = 0 if np.issubdtype(layer_data.dtype, np.integer) else 2 - self.findChild(QDoubleSpinBox, name="bins start").setDecimals(n_decimals) - self.findChild(QDoubleSpinBox, name="bins stop").setDecimals(n_decimals) + self.findChild(QDoubleSpinBox, name="bins start").setDecimals( + n_decimals + ) + self.findChild(QDoubleSpinBox, name="bins stop").setDecimals( + n_decimals + ) def draw(self) -> None: """ diff --git a/src/napari_matplotlib/tests/test_histogram.py b/src/napari_matplotlib/tests/test_histogram.py index b392ef5..58acf23 100644 --- a/src/napari_matplotlib/tests/test_histogram.py +++ b/src/napari_matplotlib/tests/test_histogram.py @@ -9,6 +9,7 @@ assert_figures_not_equal, ) + @pytest.mark.mpl_image_compare def test_histogram_2D_bins(make_napari_viewer, astronaut_data): viewer = make_napari_viewer() @@ -24,6 +25,7 @@ def test_histogram_2D_bins(make_napari_viewer, astronaut_data): # collected by the widget return deepcopy(fig) + @pytest.mark.mpl_image_compare def test_histogram_2D(make_napari_viewer, astronaut_data): viewer = make_napari_viewer() @@ -34,6 +36,7 @@ def test_histogram_2D(make_napari_viewer, astronaut_data): # collected by the widget return deepcopy(fig) + @pytest.mark.mpl_image_compare def test_histogram_3D(make_napari_viewer, brain_data): viewer = make_napari_viewer() From 5ac1f712cfa8f760a5e4e9ad028fd8882d087ee4 Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Thu, 11 Jan 2024 12:18:55 +0000 Subject: [PATCH 05/22] Fix type hints --- src/napari_matplotlib/histogram.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index c527266..3615172 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, cast +from typing import Any, Optional, Union, cast import napari import numpy as np @@ -108,7 +108,7 @@ def bins_start(self) -> float: return self.findChild(QDoubleSpinBox, name="bins start").value() @bins_start.setter - def bins_start(self, start: int | float) -> None: + def bins_start(self, start: Union[int, float]) -> None: """Set the minimum bin edge""" self.findChild(QDoubleSpinBox, name="bins start").setValue(start) @@ -118,7 +118,7 @@ def bins_stop(self) -> float: return self.findChild(QDoubleSpinBox, name="bins stop").value() @bins_stop.setter - def bins_stop(self, stop: int | float) -> None: + def bins_stop(self, stop: Union[int, float]) -> None: """Set the maximum bin edge""" self.findChild(QDoubleSpinBox, name="bins stop").setValue(stop) @@ -132,14 +132,14 @@ def bins_num(self, num: int) -> None: """Set the number of bins to use""" self.findChild(QSpinBox, name="bins num").setValue(num) - def autoset_widget_bins(self, data: npt.ArrayLike) -> None: + def autoset_widget_bins(self, data: npt.NDArray[Any]) -> None: """Update widgets with bins determined from the image data""" bins = np.linspace(np.min(data), np.max(data), 100, dtype=data.dtype) self.bins_start = bins[0] self.bins_stop = bins[-1] self.bins_num = bins.size - def _get_layer_data(self, layer) -> np.ndarray: + def _get_layer_data(self, layer: napari.layers.Layer) -> npt.NDArray[Any]: """Get the data associated with a given layer""" if layer.data.ndim - layer.rgb == 3: # 3D data, can be single channel or RGB From fab29063cdb87b848c35002847656f663ac051ef Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Thu, 11 Jan 2024 12:51:53 +0000 Subject: [PATCH 06/22] Don't allow bins lower than 0 if dtype is unisgned --- examples/histogram.py | 2 +- src/napari_matplotlib/histogram.py | 19 ++++++++++++------- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/examples/histogram.py b/examples/histogram.py index ccda491..17111f5 100644 --- a/examples/histogram.py +++ b/examples/histogram.py @@ -5,7 +5,7 @@ import napari viewer = napari.Viewer() -viewer.open_sample("napari", "kidney") +viewer.open_sample("napari", "coins") viewer.window.add_plugin_dock_widget( plugin_name="napari-matplotlib", widget_name="Histogram" diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index 3615172..348a506 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -46,7 +46,7 @@ def __init__( bins_start.setStepType(QAbstractSpinBox.AdaptiveDecimalStepType) bins_start.setRange(-1e10, 1e10) bins_start.setValue(0) - bins_start.setWrapping(True) + bins_start.setWrapping(False) bins_start.setKeyboardTracking(False) bins_start.setDecimals(2) @@ -55,6 +55,7 @@ def __init__( bins_stop.setStepType(QAbstractSpinBox.AdaptiveDecimalStepType) bins_stop.setRange(-1e10, 1e10) bins_stop.setValue(100) + bins_start.setWrapping(False) bins_stop.setKeyboardTracking(False) bins_stop.setDecimals(2) @@ -165,13 +166,17 @@ def on_update_layers(self) -> None: self.autoset_widget_bins(data=layer_data) # Only allow integer bins for integer data + # And only allow values greater than 0 for unsigned integers n_decimals = 0 if np.issubdtype(layer_data.dtype, np.integer) else 2 - self.findChild(QDoubleSpinBox, name="bins start").setDecimals( - n_decimals - ) - self.findChild(QDoubleSpinBox, name="bins stop").setDecimals( - n_decimals - ) + is_unsigned = layer_data.dtype.kind == "u" + minimum_value = 0 if is_unsigned else -1e10 + + bins_start = self.findChild(QDoubleSpinBox, name="bins start") + bins_stop = self.findChild(QDoubleSpinBox, name="bins stop") + bins_start.setDecimals(n_decimals) + bins_stop.setDecimals(n_decimals) + bins_start.setMinimum(minimum_value) + bins_stop.setMinimum(minimum_value) def draw(self) -> None: """ From 55531747ef1e8c5597df9c081c44bc92a6dc3d3c Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Thu, 11 Jan 2024 13:12:56 +0000 Subject: [PATCH 07/22] Update changelog --- docs/changelog.rst | 4 ---- 1 file changed, 4 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 96b353d..226cbb5 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -13,10 +13,6 @@ Bug fixes ~~~~~~~~~ - Use integer bin limits for integer images in ``HistogramWidget`` -Bug fixes -~~~~~~~~~ -- Use integer bin limits for integer images in ``HistogramWidget`` - 1.1.0 ----- Additions From e86d4f692a5252881924a3e35c317d1cb24a1723 Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Thu, 11 Jan 2024 13:13:17 +0000 Subject: [PATCH 08/22] Undo changes to example of HistogramWidget --- examples/histogram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/histogram.py b/examples/histogram.py index 17111f5..ccda491 100644 --- a/examples/histogram.py +++ b/examples/histogram.py @@ -5,7 +5,7 @@ import napari viewer = napari.Viewer() -viewer.open_sample("napari", "coins") +viewer.open_sample("napari", "kidney") viewer.window.add_plugin_dock_widget( plugin_name="napari-matplotlib", widget_name="Histogram" From c5e08861b18084217eb297bb042996ea46f44cc3 Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Mon, 15 Jan 2024 15:59:34 +0000 Subject: [PATCH 09/22] Fix autosetting bins from data --- src/napari_matplotlib/histogram.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index 348a506..b0a04f1 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -135,7 +135,19 @@ def bins_num(self, num: int) -> None: def autoset_widget_bins(self, data: npt.NDArray[Any]) -> None: """Update widgets with bins determined from the image data""" - bins = np.linspace(np.min(data), np.max(data), 100, dtype=data.dtype) + if data.dtype.kind in {"i", "u"}: + # Make sure integer data types have integer sized bins + # We can't use unsigned ints when calculating the step, otherwise + # the following warning is raised: + # 'RuntimeWarning: overflow encountered in scalar subtract' + step = ( + abs(np.min(data).astype(int) - np.max(data).astype(int)) // 100 + ) + step = max(1, step) + bins = np.arange(np.min(data), np.max(data) + step, step) + else: + bins = np.linspace(np.min(data), np.max(data), 100) + self.bins_start = bins[0] self.bins_stop = bins[-1] self.bins_num = bins.size From 127d325d37133447dd9ba462c5a4a2f806ae86c4 Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Mon, 15 Jan 2024 16:05:24 +0000 Subject: [PATCH 10/22] remove duplicate on_update_layers method --- src/napari_matplotlib/histogram.py | 84 ++++++++++++++---------------- 1 file changed, 40 insertions(+), 44 deletions(-) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index b0a04f1..4371ea9 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -95,6 +95,26 @@ def on_update_layers(self) -> None: for layer in self.viewer.layers: layer.events.contrast_limits.connect(self._update_contrast_lims) + if not self.layers: + return + + # Reset to bin start, stop and step + layer_data = self._get_layer_data(self.layers[0]) + self.autoset_widget_bins(data=layer_data) + + # Only allow integer bins for integer data + # And only allow values greater than 0 for unsigned integers + n_decimals = 0 if np.issubdtype(layer_data.dtype, np.integer) else 2 + is_unsigned = layer_data.dtype.kind == "u" + minimum_value = 0 if is_unsigned else -1e10 + + bins_start = self.findChild(QDoubleSpinBox, name="bins start") + bins_stop = self.findChild(QDoubleSpinBox, name="bins stop") + bins_start.setDecimals(n_decimals) + bins_stop.setDecimals(n_decimals) + bins_start.setMinimum(minimum_value) + bins_stop.setMinimum(minimum_value) + def _update_contrast_lims(self) -> None: for lim, line in zip( self.layers[0].contrast_limits, self._contrast_lines @@ -103,6 +123,25 @@ def _update_contrast_lims(self) -> None: self.figure.canvas.draw() + def autoset_widget_bins(self, data: npt.NDArray[Any]) -> None: + """Update widgets with bins determined from the image data""" + + if data.dtype.kind in {"i", "u"}: + # Make sure integer data types have integer sized bins + # We can't use unsigned ints when calculating the step, otherwise + # the following warning is raised: + # 'RuntimeWarning: overflow encountered in scalar subtract' + step = abs(np.min(data).astype(int) - np.max(data).astype(int) // 100) + step = max(1, step) + bins = np.arange(np.min(data), np.max(data) + step, step) + else: + bins = np.linspace(np.min(data), np.max(data), 100) + + self.bins_start = bins[0] + self.bins_stop = bins[-1] + self.bins_num = bins.size + + @property def bins_start(self) -> float: """Minimum bin edge""" @@ -133,25 +172,6 @@ def bins_num(self, num: int) -> None: """Set the number of bins to use""" self.findChild(QSpinBox, name="bins num").setValue(num) - def autoset_widget_bins(self, data: npt.NDArray[Any]) -> None: - """Update widgets with bins determined from the image data""" - if data.dtype.kind in {"i", "u"}: - # Make sure integer data types have integer sized bins - # We can't use unsigned ints when calculating the step, otherwise - # the following warning is raised: - # 'RuntimeWarning: overflow encountered in scalar subtract' - step = ( - abs(np.min(data).astype(int) - np.max(data).astype(int)) // 100 - ) - step = max(1, step) - bins = np.arange(np.min(data), np.max(data) + step, step) - else: - bins = np.linspace(np.min(data), np.max(data), 100) - - self.bins_start = bins[0] - self.bins_stop = bins[-1] - self.bins_num = bins.size - def _get_layer_data(self, layer: napari.layers.Layer) -> npt.NDArray[Any]: """Get the data associated with a given layer""" if layer.data.ndim - layer.rgb == 3: @@ -166,30 +186,6 @@ def _get_layer_data(self, layer: napari.layers.Layer) -> npt.NDArray[Any]: return data - def on_update_layers(self) -> None: - """ - Called when the layer selection changes by ``self._update_layers()``. - """ - if not self.layers: - return - - # Reset to bin start, stop and step - layer_data = self._get_layer_data(self.layers[0]) - self.autoset_widget_bins(data=layer_data) - - # Only allow integer bins for integer data - # And only allow values greater than 0 for unsigned integers - n_decimals = 0 if np.issubdtype(layer_data.dtype, np.integer) else 2 - is_unsigned = layer_data.dtype.kind == "u" - minimum_value = 0 if is_unsigned else -1e10 - - bins_start = self.findChild(QDoubleSpinBox, name="bins start") - bins_stop = self.findChild(QDoubleSpinBox, name="bins stop") - bins_start.setDecimals(n_decimals) - bins_stop.setDecimals(n_decimals) - bins_start.setMinimum(minimum_value) - bins_stop.setMinimum(minimum_value) - def draw(self) -> None: """ Clear the axes and histogram the currently selected layer/slice. @@ -201,7 +197,7 @@ def draw(self) -> None: # whole cube into memory. if data.dtype.kind in {"i", "u"}: # Make sure integer data types have integer sized bins - step = (self.bins_start - self.bins_stop) // self.bins_num + step = abs((self.bins_start - self.bins_stop) // self.bins_num) step = max(1, step) bins = np.arange(self.bins_start, self.bins_stop + step, step) else: From b8ffdb79a7c20fe0ceaf7b9a0dbd8174c52c63ec Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Mon, 15 Jan 2024 16:05:35 +0000 Subject: [PATCH 11/22] Make linters happy --- src/napari_matplotlib/histogram.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index 4371ea9..294e509 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -125,13 +125,14 @@ def _update_contrast_lims(self) -> None: def autoset_widget_bins(self, data: npt.NDArray[Any]) -> None: """Update widgets with bins determined from the image data""" - if data.dtype.kind in {"i", "u"}: # Make sure integer data types have integer sized bins # We can't use unsigned ints when calculating the step, otherwise # the following warning is raised: # 'RuntimeWarning: overflow encountered in scalar subtract' - step = abs(np.min(data).astype(int) - np.max(data).astype(int) // 100) + step = abs( + np.min(data).astype(int) - np.max(data).astype(int) // 100 + ) step = max(1, step) bins = np.arange(np.min(data), np.max(data) + step, step) else: @@ -141,7 +142,6 @@ def autoset_widget_bins(self, data: npt.NDArray[Any]) -> None: self.bins_stop = bins[-1] self.bins_num = bins.size - @property def bins_start(self) -> float: """Minimum bin edge""" From 88760cf203f89df667bb3711a47e1a588626c5b9 Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Mon, 15 Jan 2024 16:21:15 +0000 Subject: [PATCH 12/22] Add HistogramWidget._bin_widgets attribute for storing bin widgets --- src/napari_matplotlib/histogram.py | 46 ++++++++++++++++++++---------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index 294e509..b846af6 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -42,7 +42,6 @@ def __init__( # Create widgets for setting bin parameters bins_start = QDoubleSpinBox() - bins_start.setObjectName("bins start") bins_start.setStepType(QAbstractSpinBox.AdaptiveDecimalStepType) bins_start.setRange(-1e10, 1e10) bins_start.setValue(0) @@ -51,7 +50,6 @@ def __init__( bins_start.setDecimals(2) bins_stop = QDoubleSpinBox() - bins_stop.setObjectName("bins stop") bins_stop.setStepType(QAbstractSpinBox.AdaptiveDecimalStepType) bins_stop.setRange(-1e10, 1e10) bins_stop.setValue(100) @@ -60,7 +58,6 @@ def __init__( bins_stop.setDecimals(2) bins_num = QSpinBox() - bins_num.setObjectName("bins num") bins_num.setRange(1, 100_000) bins_num.setValue(101) bins_num.setWrapping(False) @@ -84,6 +81,13 @@ def __init__( bins_stop.valueChanged.connect(self._draw) bins_num.valueChanged.connect(self._draw) + # Store widgets for later usage + self._bin_widgets = { + "start": bins_start, + "stop": bins_stop, + "num": bins_num, + } + self._update_layers(None) self.viewer.events.theme.connect(self._on_napari_theme_changed) @@ -108,12 +112,17 @@ def on_update_layers(self) -> None: is_unsigned = layer_data.dtype.kind == "u" minimum_value = 0 if is_unsigned else -1e10 - bins_start = self.findChild(QDoubleSpinBox, name="bins start") - bins_stop = self.findChild(QDoubleSpinBox, name="bins stop") - bins_start.setDecimals(n_decimals) - bins_stop.setDecimals(n_decimals) - bins_start.setMinimum(minimum_value) - bins_stop.setMinimum(minimum_value) + # Disable callbacks whilst widget values might change + for widget in self._bin_widgets.values(): + widget.blockSignals(True) + + self._bin_widgets["start"].setDecimals(n_decimals) + self._bin_widgets["stop"].setDecimals(n_decimals) + self._bin_widgets["start"].setMinimum(minimum_value) + self._bin_widgets["stop"].setMinimum(minimum_value) + + for widget in self._bin_widgets.values(): + widget.blockSignals(False) def _update_contrast_lims(self) -> None: for lim, line in zip( @@ -138,39 +147,46 @@ def autoset_widget_bins(self, data: npt.NDArray[Any]) -> None: else: bins = np.linspace(np.min(data), np.max(data), 100) + # Disable callbacks whilst setting widget values + for widget in self._bin_widgets.values(): + widget.blockSignals(True) + self.bins_start = bins[0] self.bins_stop = bins[-1] self.bins_num = bins.size + for widget in self._bin_widgets.values(): + widget.blockSignals(False) + @property def bins_start(self) -> float: """Minimum bin edge""" - return self.findChild(QDoubleSpinBox, name="bins start").value() + return self._bin_widgets["start"].value() @bins_start.setter def bins_start(self, start: Union[int, float]) -> None: """Set the minimum bin edge""" - self.findChild(QDoubleSpinBox, name="bins start").setValue(start) + self._bin_widgets["start"].setValue(start) @property def bins_stop(self) -> float: """Maximum bin edge""" - return self.findChild(QDoubleSpinBox, name="bins stop").value() + return self._bin_widgets["stop"].value() @bins_stop.setter def bins_stop(self, stop: Union[int, float]) -> None: """Set the maximum bin edge""" - self.findChild(QDoubleSpinBox, name="bins stop").setValue(stop) + self._bin_widgets["stop"].setValue(stop) @property def bins_num(self) -> int: """Number of bins to use""" - return self.findChild(QSpinBox, name="bins num").value() + return self._bin_widgets["num"].value() @bins_num.setter def bins_num(self, num: int) -> None: """Set the number of bins to use""" - self.findChild(QSpinBox, name="bins num").setValue(num) + self._bin_widgets["num"].setValue(num) def _get_layer_data(self, layer: napari.layers.Layer) -> npt.NDArray[Any]: """Get the data associated with a given layer""" From d56942b72abd8e65f821ba07feb99ff6f0a25526 Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Mon, 15 Jan 2024 16:33:50 +0000 Subject: [PATCH 13/22] Fix calculation of bins from widget values --- src/napari_matplotlib/histogram.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index b846af6..3215b14 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -136,12 +136,7 @@ def autoset_widget_bins(self, data: npt.NDArray[Any]) -> None: """Update widgets with bins determined from the image data""" if data.dtype.kind in {"i", "u"}: # Make sure integer data types have integer sized bins - # We can't use unsigned ints when calculating the step, otherwise - # the following warning is raised: - # 'RuntimeWarning: overflow encountered in scalar subtract' - step = abs( - np.min(data).astype(int) - np.max(data).astype(int) // 100 - ) + step = abs(np.max(data) - np.min(data)) // 100 step = max(1, step) bins = np.arange(np.min(data), np.max(data) + step, step) else: @@ -213,7 +208,7 @@ def draw(self) -> None: # whole cube into memory. if data.dtype.kind in {"i", "u"}: # Make sure integer data types have integer sized bins - step = abs((self.bins_start - self.bins_stop) // self.bins_num) + step = abs(self.bins_stop - self.bins_start) // self.bins_num step = max(1, step) bins = np.arange(self.bins_start, self.bins_stop + step, step) else: From 11590b251846ab9b6023663bacb6d286c3e5a15f Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Mon, 15 Jan 2024 16:51:22 +0000 Subject: [PATCH 14/22] Calculate step using n_bins-1 n_bins corresponds to number of bin edges rather than bins --- src/napari_matplotlib/histogram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index 3215b14..4ce6ebc 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -208,7 +208,7 @@ def draw(self) -> None: # whole cube into memory. if data.dtype.kind in {"i", "u"}: # Make sure integer data types have integer sized bins - step = abs(self.bins_stop - self.bins_start) // self.bins_num + step = abs(self.bins_stop - self.bins_start) // (self.bins_num - 1) step = max(1, step) bins = np.arange(self.bins_start, self.bins_stop + step, step) else: From 08a5086f345e5db2418f2f4e1f5f4e5b169567a4 Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Mon, 15 Jan 2024 16:52:16 +0000 Subject: [PATCH 15/22] cannot use negative start bin for uint data --- .../tests/baseline/test_histogram_2D_bins.png | Bin 21366 -> 19894 bytes src/napari_matplotlib/tests/test_histogram.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/napari_matplotlib/tests/baseline/test_histogram_2D_bins.png b/src/napari_matplotlib/tests/baseline/test_histogram_2D_bins.png index eb43c3108147079f9410a40936975d94158ac6f0..db401612c44fb7eb92dc1ae9f2fd66a7bbd69fd6 100644 GIT binary patch literal 19894 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 BUXB0& literal 21366 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 b-^Cj#YN-+8Uxv&S3Wb(bypnn8+Wr3z5b2@o diff --git a/src/napari_matplotlib/tests/test_histogram.py b/src/napari_matplotlib/tests/test_histogram.py index 58acf23..399db3d 100644 --- a/src/napari_matplotlib/tests/test_histogram.py +++ b/src/napari_matplotlib/tests/test_histogram.py @@ -17,8 +17,8 @@ def test_histogram_2D_bins(make_napari_viewer, astronaut_data): viewer.add_image(astronaut_data[0], **astronaut_data[1]) widget = HistogramWidget(viewer) viewer.window.add_dock_widget(widget) - widget.bins_start = -50 - widget.bins_stop = 300 + widget.bins_start = 0 + widget.bins_stop = 350 widget.bins_num = 35 fig = widget.figure # Need to return a copy, as original figure is too eagerley garbage From 65e84f8692e7ad42586304a643621b02dab95670 Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Mon, 15 Jan 2024 16:56:00 +0000 Subject: [PATCH 16/22] Make HistogramWidget bins_num widget correspond to number of bins rather than number of bin edges --- 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 4ce6ebc..728dfc8 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -148,7 +148,7 @@ def autoset_widget_bins(self, data: npt.NDArray[Any]) -> None: self.bins_start = bins[0] self.bins_stop = bins[-1] - self.bins_num = bins.size + self.bins_num = bins.size - 1 for widget in self._bin_widgets.values(): widget.blockSignals(False) @@ -208,11 +208,13 @@ def draw(self) -> None: # whole cube into memory. if data.dtype.kind in {"i", "u"}: # Make sure integer data types have integer sized bins - step = abs(self.bins_stop - self.bins_start) // (self.bins_num - 1) + step = abs(self.bins_stop - self.bins_start) // (self.bins_num) step = max(1, step) bins = np.arange(self.bins_start, self.bins_stop + step, step) else: - bins = np.linspace(self.bins_start, self.bins_stop, self.bins_num) + bins = np.linspace( + self.bins_start, self.bins_stop, self.bins_num + 1 + ) if layer.rgb: # Histogram RGB channels independently From 8a7d9e46da49492172df8f36e0f579c995407a65 Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Mon, 15 Jan 2024 17:07:02 +0000 Subject: [PATCH 17/22] fix typo in comment about using 128 bins for float data --- src/napari_matplotlib/histogram.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index 2329c1a..39bcfa4 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -31,7 +31,7 @@ def _get_bins(data: npt.NDArray[Any]) -> npt.NDArray[Any]: step = np.ceil(np.ptp(data) / 100) return np.arange(np.min(data), np.max(data) + step, step) else: - # For other data types, just have 128 evenly spaced bins + # For other data types, just have 99 evenly spaced bins return np.linspace(np.min(data), np.max(data), 100) From 7c4cdc87328480e53bd07847e79fcf27b9fef697 Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Mon, 15 Jan 2024 17:10:07 +0000 Subject: [PATCH 18/22] Update changelog --- docs/changelog.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 673a0ef..f4caaf3 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -20,6 +20,7 @@ Other changes - The ``HistogramWidget`` now has two vertical lines showing the contrast limits used to render the selected layer in the main napari window. - Added an example gallery for the ``FeaturesHistogramWidget``. +- Add widgets for setting bin parameters for ``HistogramWidget``. 1.2.0 ----- @@ -28,7 +29,6 @@ Changes - Dropped support for Python 3.8, and added support for Python 3.11. - Histogram plots of points and vector layers are now coloured with their napari colourmap. - Added support for Matplotlib 3.8 -- Add widgets for setting histogram bin parameters 1.1.0 ----- From 6261f4c36fb531146cb270f3ad2bf9fc5d62ac1c Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Wed, 14 Feb 2024 10:54:40 +0000 Subject: [PATCH 19/22] Add 'num_bins, 'start', and 'stop' parameters to '_get_bins' --- src/napari_matplotlib/histogram.py | 56 ++++++++++++++++++++++-------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index bec5c61..5036071 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -27,15 +27,44 @@ _COLORS = {"r": "tab:red", "g": "tab:green", "b": "tab:blue"} -def _get_bins(data: npt.NDArray[Any]) -> npt.NDArray[Any]: +def _get_bins( + data: npt.NDArray[Any], + num_bins: int = 100, + start: Optional[Union[int, float]] = None, + stop: Optional[Union[int, float]] = None, +) -> npt.NDArray[Any]: + """Create evenly spaced bins with a given interval. + + If `start` or `stop` are `None`, they will be set based on the minimum + and maximum values, respectively, of the data. + + Parameters + ---------- + data : napari.layers.Layer.data + Napari layer data. + num_bins : integer, optional + Number of evenly-spaced bins to create. + start : integer or real, optional + Start bin edge. Defaults to the minimum value of `data`. + stop : integer or real, optional + Stop bin edge. Defaults to the maximum value of `data`. + + Returns + ------- + bin_edges : numpy.ndarray + Array of evenly spaced bin edges. + """ + start = np.min(data) if start is None else start + stop = np.max(data) if stop is None else stop + if data.dtype.kind in {"i", "u"}: # Make sure integer data types have integer sized bins - step = np.ceil(np.ptp(data) / 100) - return np.arange(np.min(data), np.max(data) + step, step) + step = np.ceil((stop - start) / num_bins) + return np.arange(start, stop + step, step) else: - # For other data types, just have 100 evenly spaced bins - # (and 101 bin edges) - return np.linspace(np.min(data), np.max(data), 101) + # For other data types we can use exactly `num_bins` bins + # (and `num_bins` + 1 bin edges) + return np.linspace(start, stop, num_bins + 1) class HistogramWidget(SingleAxesWidget): @@ -217,15 +246,12 @@ def draw(self) -> None: # Important to calculate bins after slicing 3D data, to avoid reading # whole cube into memory. - if data.dtype.kind in {"i", "u"}: - # Make sure integer data types have integer sized bins - step = abs(self.bins_stop - self.bins_start) // (self.bins_num) - step = max(1, step) - bins = np.arange(self.bins_start, self.bins_stop + step, step) - else: - bins = np.linspace( - self.bins_start, self.bins_stop, self.bins_num + 1 - ) + bins = _get_bins( + data, + num_bins=self.bins_num, + start=self.bins_start, + stop=self.bins_stop, + ) if layer.rgb: # Histogram RGB channels independently From 426a0f5f8401300b1c8c0e93c479e5559e1dec97 Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Sat, 25 May 2024 10:36:31 +0100 Subject: [PATCH 20/22] use '| None' rather than Optional[Union[...]] for type hints --- src/napari_matplotlib/histogram.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index 042ef8f..fe281c4 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -1,4 +1,4 @@ -from typing import Any, Optional, Union, cast +from typing import Any, cast import napari import numpy as np @@ -30,8 +30,8 @@ def _get_bins( data: npt.NDArray[Any], num_bins: int = 100, - start: Optional[Union[int, float]] = None, - stop: Optional[Union[int, float]] = None, + start: int | float | None = None, + stop: int | float | None = None, ) -> npt.NDArray[Any]: """Create evenly spaced bins with a given interval. @@ -195,7 +195,7 @@ def bins_start(self) -> float: return self._bin_widgets["start"].value() @bins_start.setter - def bins_start(self, start: Union[int, float]) -> None: + def bins_start(self, start: int | float) -> None: """Set the minimum bin edge""" self._bin_widgets["start"].setValue(start) @@ -205,7 +205,7 @@ def bins_stop(self) -> float: return self._bin_widgets["stop"].value() @bins_stop.setter - def bins_stop(self, stop: Union[int, float]) -> None: + def bins_stop(self, stop: int | float) -> None: """Set the maximum bin edge""" self._bin_widgets["stop"].setValue(stop) From 67a864101d2d7d037bfbe8f9f3e1b55569cd125e Mon Sep 17 00:00:00 2001 From: Paul Smith Date: Sat, 25 May 2024 11:02:24 +0100 Subject: [PATCH 21/22] remove widgest to set start and stop values for histogram bins --- src/napari_matplotlib/histogram.py | 145 +++--------------- .../tests/baseline/test_histogram_2D_bins.png | Bin 19894 -> 19832 bytes src/napari_matplotlib/tests/test_histogram.py | 4 +- 3 files changed, 24 insertions(+), 125 deletions(-) diff --git a/src/napari_matplotlib/histogram.py b/src/napari_matplotlib/histogram.py index fe281c4..aeef841 100644 --- a/src/napari_matplotlib/histogram.py +++ b/src/napari_matplotlib/histogram.py @@ -7,9 +7,7 @@ from napari.layers import Image from napari.layers._multiscale_data import MultiScaleData from qtpy.QtWidgets import ( - QAbstractSpinBox, QComboBox, - QDoubleSpinBox, QFormLayout, QGroupBox, QLabel, @@ -30,41 +28,29 @@ def _get_bins( data: npt.NDArray[Any], num_bins: int = 100, - start: int | float | None = None, - stop: int | float | None = None, ) -> npt.NDArray[Any]: """Create evenly spaced bins with a given interval. - If `start` or `stop` are `None`, they will be set based on the minimum - and maximum values, respectively, of the data. - Parameters ---------- data : napari.layers.Layer.data Napari layer data. num_bins : integer, optional - Number of evenly-spaced bins to create. - start : integer or real, optional - Start bin edge. Defaults to the minimum value of `data`. - stop : integer or real, optional - Stop bin edge. Defaults to the maximum value of `data`. + Number of evenly-spaced bins to create. Defaults to 100. Returns ------- bin_edges : numpy.ndarray Array of evenly spaced bin edges. """ - start = np.min(data) if start is None else start - stop = np.max(data) if stop is None else stop - if data.dtype.kind in {"i", "u"}: # Make sure integer data types have integer sized bins - step = np.ceil((stop - start) / num_bins) - return np.arange(start, stop + step, step) + step = np.ceil(np.ptp(data) / num_bins) + return np.arange(np.min(data), np.max(data) + step, step) else: # For other data types we can use exactly `num_bins` bins # (and `num_bins` + 1 bin edges) - return np.linspace(start, stop, num_bins + 1) + return np.linspace(np.min(data), np.max(data), num_bins + 1) class HistogramWidget(SingleAxesWidget): @@ -82,53 +68,28 @@ def __init__( ): super().__init__(napari_viewer, parent=parent) - # Create widgets for setting bin parameters - bins_start = QDoubleSpinBox() - bins_start.setStepType(QAbstractSpinBox.AdaptiveDecimalStepType) - bins_start.setRange(-1e10, 1e10) - bins_start.setValue(0) - bins_start.setWrapping(False) - bins_start.setKeyboardTracking(False) - bins_start.setDecimals(2) - - bins_stop = QDoubleSpinBox() - bins_stop.setStepType(QAbstractSpinBox.AdaptiveDecimalStepType) - bins_stop.setRange(-1e10, 1e10) - bins_stop.setValue(100) - bins_start.setWrapping(False) - bins_stop.setKeyboardTracking(False) - bins_stop.setDecimals(2) - - bins_num = QSpinBox() - bins_num.setRange(1, 100_000) - bins_num.setValue(101) - bins_num.setWrapping(False) - bins_num.setKeyboardTracking(False) + num_bins_widget = QSpinBox() + num_bins_widget.setRange(1, 100_000) + num_bins_widget.setValue(101) + num_bins_widget.setWrapping(False) + num_bins_widget.setKeyboardTracking(False) # Set bins widget layout bins_selection_layout = QFormLayout() - bins_selection_layout.addRow("start", bins_start) - bins_selection_layout.addRow("stop", bins_stop) - bins_selection_layout.addRow("num", bins_num) + bins_selection_layout.addRow("num bins", num_bins_widget) # Group the widgets and add to main layout - bins_widget_group = QGroupBox("Bins") - bins_widget_group_layout = QVBoxLayout() - bins_widget_group_layout.addLayout(bins_selection_layout) - bins_widget_group.setLayout(bins_widget_group_layout) - self.layout().addWidget(bins_widget_group) + params_widget_group = QGroupBox("Params") + params_widget_group_layout = QVBoxLayout() + params_widget_group_layout.addLayout(bins_selection_layout) + params_widget_group.setLayout(params_widget_group_layout) + self.layout().addWidget(params_widget_group) # Add callbacks - bins_start.valueChanged.connect(self._draw) - bins_stop.valueChanged.connect(self._draw) - bins_num.valueChanged.connect(self._draw) + num_bins_widget.valueChanged.connect(self._draw) # Store widgets for later usage - self._bin_widgets = { - "start": bins_start, - "stop": bins_stop, - "num": bins_num, - } + self.num_bins_widget = num_bins_widget self._update_layers(None) self.viewer.events.theme.connect(self._on_napari_theme_changed) @@ -144,27 +105,9 @@ def on_update_layers(self) -> None: if not self.layers: return - # Reset the bin start, stop and step values based on new layer data + # Reset the num bins based on new layer data layer_data = self._get_layer_data(self.layers[0]) - self.autoset_widget_bins(data=layer_data) - - # Only allow integer bins for integer data - # And only allow values greater than 0 for unsigned integers - n_decimals = 0 if np.issubdtype(layer_data.dtype, np.integer) else 2 - is_unsigned = layer_data.dtype.kind == "u" - minimum_value = 0 if is_unsigned else -1e10 - - # Disable callbacks whilst widget values might change - for widget in self._bin_widgets.values(): - widget.blockSignals(True) - - self._bin_widgets["start"].setDecimals(n_decimals) - self._bin_widgets["stop"].setDecimals(n_decimals) - self._bin_widgets["start"].setMinimum(minimum_value) - self._bin_widgets["stop"].setMinimum(minimum_value) - - for widget in self._bin_widgets.values(): - widget.blockSignals(False) + self._set_widget_nums_bins(data=layer_data) def _update_contrast_lims(self) -> None: for lim, line in zip( @@ -174,50 +117,10 @@ def _update_contrast_lims(self) -> None: self.figure.canvas.draw() - def autoset_widget_bins(self, data: npt.NDArray[Any]) -> None: - """Update widgets with bins determined from the image data""" + def _set_widget_nums_bins(self, data: npt.NDArray[Any]) -> None: + """Update num_bins widget with bins determined from the image data""" bins = _get_bins(data) - - # Disable callbacks whilst setting widget values - for widget in self._bin_widgets.values(): - widget.blockSignals(True) - - self.bins_start = bins[0] - self.bins_stop = bins[-1] - self.bins_num = bins.size - 1 - - for widget in self._bin_widgets.values(): - widget.blockSignals(False) - - @property - def bins_start(self) -> float: - """Minimum bin edge""" - return self._bin_widgets["start"].value() - - @bins_start.setter - def bins_start(self, start: int | float) -> None: - """Set the minimum bin edge""" - self._bin_widgets["start"].setValue(start) - - @property - def bins_stop(self) -> float: - """Maximum bin edge""" - return self._bin_widgets["stop"].value() - - @bins_stop.setter - def bins_stop(self, stop: int | float) -> None: - """Set the maximum bin edge""" - self._bin_widgets["stop"].setValue(stop) - - @property - def bins_num(self) -> int: - """Number of bins to use""" - return self._bin_widgets["num"].value() - - @bins_num.setter - def bins_num(self, num: int) -> None: - """Set the number of bins to use""" - self._bin_widgets["num"].setValue(num) + self.num_bins_widget.setValue(bins.size - 1) def _get_layer_data(self, layer: napari.layers.Layer) -> npt.NDArray[Any]: """Get the data associated with a given layer""" @@ -248,9 +151,7 @@ def draw(self) -> None: # whole cube into memory. bins = _get_bins( data, - num_bins=self.bins_num, - start=self.bins_start, - stop=self.bins_stop, + num_bins=self.num_bins_widget.value(), ) if layer.rgb: diff --git a/src/napari_matplotlib/tests/baseline/test_histogram_2D_bins.png b/src/napari_matplotlib/tests/baseline/test_histogram_2D_bins.png index db401612c44fb7eb92dc1ae9f2fd66a7bbd69fd6..98e3cde1254b75ffc92e7786f8106fe4cce3c16f 100644 GIT binary patch literal 19832 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 literal 19894 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 BUXB0& diff --git a/src/napari_matplotlib/tests/test_histogram.py b/src/napari_matplotlib/tests/test_histogram.py index 399db3d..435973b 100644 --- a/src/napari_matplotlib/tests/test_histogram.py +++ b/src/napari_matplotlib/tests/test_histogram.py @@ -17,9 +17,7 @@ def test_histogram_2D_bins(make_napari_viewer, astronaut_data): viewer.add_image(astronaut_data[0], **astronaut_data[1]) widget = HistogramWidget(viewer) viewer.window.add_dock_widget(widget) - widget.bins_start = 0 - widget.bins_stop = 350 - widget.bins_num = 35 + widget.num_bins_widget.setValue(25) fig = widget.figure # Need to return a copy, as original figure is too eagerley garbage # collected by the widget From 8fe7c7f0b75da3a8018ebead7ddfc4823c338b35 Mon Sep 17 00:00:00 2001 From: David Stansby Date: Fri, 12 Jul 2024 16:24:48 +0200 Subject: [PATCH 22/22] Update changelog --- docs/changelog.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 4c2509f..60dd72b 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -1,6 +1,12 @@ Changelog ========= +2.1.0 +----- +New features +~~~~~~~~~~~~ +- Added a GUI element to manually set the number of bins in the histogram widgets. + 2.0.3 ----- Bug fixes @@ -48,7 +54,6 @@ Other changes - The ``HistogramWidget`` now has two vertical lines showing the contrast limits used to render the selected layer in the main napari window. - Added an example gallery for the ``FeaturesHistogramWidget``. -- Add widgets for setting bin parameters for ``HistogramWidget``. 1.2.0 -----