Skip to content

Commit efd598f

Browse files
authored
Merge branch 'main' into napari-05
2 parents 8104b4b + d0b03bb commit efd598f

File tree

7 files changed

+101
-16
lines changed

7 files changed

+101
-16
lines changed

.github/workflows/docs.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ jobs:
5050
if: contains(github.ref, 'tags')
5151
steps:
5252
- uses: actions/checkout@v3
53-
- uses: actions/download-artifact@v3
53+
- uses: actions/download-artifact@v4.1.7
5454
with:
5555
name: docs
5656

.pre-commit-config.yaml

+3-3
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ repos:
77
- id: trailing-whitespace
88

99
- repo: https://github.com/psf/black-pre-commit-mirror
10-
rev: 24.4.2
10+
rev: 24.8.0
1111
hooks:
1212
- id: black
1313

@@ -17,14 +17,14 @@ repos:
1717
- id: napari-plugin-checks
1818

1919
- repo: https://github.com/pre-commit/mirrors-mypy
20-
rev: v1.10.1
20+
rev: v1.11.2
2121
hooks:
2222
- id: mypy
2323
additional_dependencies: [numpy, matplotlib]
2424

2525
- repo: https://github.com/astral-sh/ruff-pre-commit
2626
# Ruff version.
27-
rev: 'v0.5.1'
27+
rev: 'v0.6.8'
2828
hooks:
2929
- id: ruff
3030

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ A plugin to create Matplotlib plots from napari layers
1515
## Introduction
1616
`napari-matplotlib` is a bridge between `napari` and `matplotlib`, making it easy to create publication quality `Matplotlib` plots based on the data loaded in `napari` layers.
1717

18-
Documentaiton can be found at https://napari-matplotlib.github.io/
18+
Documentation can be found at https://napari-matplotlib.github.io/
1919

2020
## Contributing
2121

docs/changelog.rst

+6
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

+76-11
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
Loading

src/napari_matplotlib/tests/test_histogram.py

+14
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)