Skip to content

Commit 251d333

Browse files
authored
Merge pull request #242 from p-j-smith/feat/hist-bin-params
Add widgets for setting histogram bins
2 parents 0501fab + 8fe7c7f commit 251d333

File tree

4 files changed

+96
-11
lines changed

4 files changed

+96
-11
lines changed

docs/changelog.rst

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
Changelog
22
=========
33

4+
2.1.0
5+
-----
6+
New features
7+
~~~~~~~~~~~~
8+
- Added a GUI element to manually set the number of bins in the histogram widgets.
9+
410
2.0.3
511
-----
612
Bug fixes

src/napari_matplotlib/histogram.py

Lines changed: 76 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,10 @@
88
from napari.layers._multiscale_data import MultiScaleData
99
from qtpy.QtWidgets import (
1010
QComboBox,
11+
QFormLayout,
12+
QGroupBox,
1113
QLabel,
14+
QSpinBox,
1215
QVBoxLayout,
1316
QWidget,
1417
)
@@ -22,15 +25,32 @@
2225
_COLORS = {"r": "tab:red", "g": "tab:green", "b": "tab:blue"}
2326

2427

25-
def _get_bins(data: npt.NDArray[Any]) -> npt.NDArray[Any]:
28+
def _get_bins(
29+
data: npt.NDArray[Any],
30+
num_bins: int = 100,
31+
) -> npt.NDArray[Any]:
32+
"""Create evenly spaced bins with a given interval.
33+
34+
Parameters
35+
----------
36+
data : napari.layers.Layer.data
37+
Napari layer data.
38+
num_bins : integer, optional
39+
Number of evenly-spaced bins to create. Defaults to 100.
40+
41+
Returns
42+
-------
43+
bin_edges : numpy.ndarray
44+
Array of evenly spaced bin edges.
45+
"""
2646
if data.dtype.kind in {"i", "u"}:
2747
# Make sure integer data types have integer sized bins
28-
step = np.ceil(np.ptp(data) / 100)
48+
step = np.ceil(np.ptp(data) / num_bins)
2949
return np.arange(np.min(data), np.max(data) + step, step)
3050
else:
31-
# For other data types, just have 100 evenly spaced bins
32-
# (and 101 bin edges)
33-
return np.linspace(np.min(data), np.max(data), 101)
51+
# For other data types we can use exactly `num_bins` bins
52+
# (and `num_bins` + 1 bin edges)
53+
return np.linspace(np.min(data), np.max(data), num_bins + 1)
3454

3555

3656
class HistogramWidget(SingleAxesWidget):
@@ -47,6 +67,30 @@ def __init__(
4767
parent: QWidget | None = None,
4868
):
4969
super().__init__(napari_viewer, parent=parent)
70+
71+
num_bins_widget = QSpinBox()
72+
num_bins_widget.setRange(1, 100_000)
73+
num_bins_widget.setValue(101)
74+
num_bins_widget.setWrapping(False)
75+
num_bins_widget.setKeyboardTracking(False)
76+
77+
# Set bins widget layout
78+
bins_selection_layout = QFormLayout()
79+
bins_selection_layout.addRow("num bins", num_bins_widget)
80+
81+
# Group the widgets and add to main layout
82+
params_widget_group = QGroupBox("Params")
83+
params_widget_group_layout = QVBoxLayout()
84+
params_widget_group_layout.addLayout(bins_selection_layout)
85+
params_widget_group.setLayout(params_widget_group_layout)
86+
self.layout().addWidget(params_widget_group)
87+
88+
# Add callbacks
89+
num_bins_widget.valueChanged.connect(self._draw)
90+
91+
# Store widgets for later usage
92+
self.num_bins_widget = num_bins_widget
93+
5094
self._update_layers(None)
5195
self.viewer.events.theme.connect(self._on_napari_theme_changed)
5296

@@ -60,6 +104,13 @@ def on_update_layers(self) -> None:
60104
self._update_contrast_lims
61105
)
62106

107+
if not self.layers:
108+
return
109+
110+
# Reset the num bins based on new layer data
111+
layer_data = self._get_layer_data(self.layers[0])
112+
self._set_widget_nums_bins(data=layer_data)
113+
63114
def _update_contrast_lims(self) -> None:
64115
for lim, line in zip(
65116
self.layers[0].contrast_limits, self._contrast_lines, strict=False
@@ -68,11 +119,13 @@ def _update_contrast_lims(self) -> None:
68119

69120
self.figure.canvas.draw()
70121

71-
def draw(self) -> None:
72-
"""
73-
Clear the axes and histogram the currently selected layer/slice.
74-
"""
75-
layer: Image = self.layers[0]
122+
def _set_widget_nums_bins(self, data: npt.NDArray[Any]) -> None:
123+
"""Update num_bins widget with bins determined from the image data"""
124+
bins = _get_bins(data)
125+
self.num_bins_widget.setValue(bins.size - 1)
126+
127+
def _get_layer_data(self, layer: napari.layers.Layer) -> npt.NDArray[Any]:
128+
"""Get the data associated with a given layer"""
76129
data = layer.data
77130

78131
if isinstance(layer.data, MultiScaleData):
@@ -87,9 +140,21 @@ def draw(self) -> None:
87140
# Read data into memory if it's a dask array
88141
data = np.asarray(data)
89142

143+
return data
144+
145+
def draw(self) -> None:
146+
"""
147+
Clear the axes and histogram the currently selected layer/slice.
148+
"""
149+
layer: Image = self.layers[0]
150+
data = self._get_layer_data(layer)
151+
90152
# Important to calculate bins after slicing 3D data, to avoid reading
91153
# whole cube into memory.
92-
bins = _get_bins(data)
154+
bins = _get_bins(
155+
data,
156+
num_bins=self.num_bins_widget.value(),
157+
)
93158

94159
if layer.rgb:
95160
# Histogram RGB channels independently

src/napari_matplotlib/tests/test_histogram.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,20 @@
1010
)
1111

1212

13+
@pytest.mark.mpl_image_compare
14+
def test_histogram_2D_bins(make_napari_viewer, astronaut_data):
15+
viewer = make_napari_viewer()
16+
viewer.theme = "light"
17+
viewer.add_image(astronaut_data[0], **astronaut_data[1])
18+
widget = HistogramWidget(viewer)
19+
viewer.window.add_dock_widget(widget)
20+
widget.num_bins_widget.setValue(25)
21+
fig = widget.figure
22+
# Need to return a copy, as original figure is too eagerley garbage
23+
# collected by the widget
24+
return deepcopy(fig)
25+
26+
1327
@pytest.mark.mpl_image_compare
1428
def test_histogram_2D(make_napari_viewer, astronaut_data):
1529
viewer = make_napari_viewer()

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy