Skip to content

Commit a7b09d4

Browse files
committed
Add widgets to set bin parameters for histograms
Also set np.linspace dtype based on image dtype
1 parent 06d4b17 commit a7b09d4

File tree

1 file changed

+116
-14
lines changed

1 file changed

+116
-14
lines changed

src/napari_matplotlib/histogram.py

Lines changed: 116 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,7 @@
44
import numpy as np
55
import numpy.typing as npt
66
from matplotlib.container import BarContainer
7-
from qtpy.QtWidgets import (
8-
QComboBox,
9-
QLabel,
10-
QVBoxLayout,
11-
QWidget,
12-
)
7+
from qtpy.QtWidgets import QComboBox, QLabel, QVBoxLayout, QWidget, QGroupBox, QFormLayout, QDoubleSpinBox, QSpinBox, QAbstractSpinBox
138

149
from .base import SingleAxesWidget
1510
from .features import FEATURES_LAYER_TYPES
@@ -34,6 +29,50 @@ def __init__(
3429
parent: Optional[QWidget] = None,
3530
):
3631
super().__init__(napari_viewer, parent=parent)
32+
33+
# Create widgets for setting bin parameters
34+
bins_start = QDoubleSpinBox()
35+
bins_start.setObjectName("bins start")
36+
bins_start.setStepType(QAbstractSpinBox.AdaptiveDecimalStepType)
37+
bins_start.setRange(-1e10, 1e10)
38+
bins_start.setValue(0)
39+
bins_start.setWrapping(True)
40+
bins_start.setKeyboardTracking(False)
41+
bins_start.setDecimals(2)
42+
43+
bins_stop = QDoubleSpinBox()
44+
bins_stop.setObjectName("bins stop")
45+
bins_stop.setStepType(QAbstractSpinBox.AdaptiveDecimalStepType)
46+
bins_stop.setRange(-1e10, 1e10)
47+
bins_stop.setValue(100)
48+
bins_stop.setKeyboardTracking(False)
49+
bins_stop.setDecimals(2)
50+
51+
bins_num = QSpinBox()
52+
bins_num.setObjectName("bins num")
53+
bins_num.setRange(1, 100_000)
54+
bins_num.setValue(101)
55+
bins_num.setWrapping(False)
56+
bins_num.setKeyboardTracking(False)
57+
58+
# Set bins widget layout
59+
bins_selection_layout = QFormLayout()
60+
bins_selection_layout.addRow("start", bins_start)
61+
bins_selection_layout.addRow("stop", bins_stop)
62+
bins_selection_layout.addRow("num", bins_num)
63+
64+
# Group the widgets and add to main layout
65+
bins_widget_group = QGroupBox("Bins")
66+
bins_widget_group_layout = QVBoxLayout()
67+
bins_widget_group_layout.addLayout(bins_selection_layout)
68+
bins_widget_group.setLayout(bins_widget_group_layout)
69+
self.layout().addWidget(bins_widget_group)
70+
71+
# Add callbacks
72+
bins_start.valueChanged.connect(self._draw)
73+
bins_stop.valueChanged.connect(self._draw)
74+
bins_num.valueChanged.connect(self._draw)
75+
3776
self._update_layers(None)
3877
self.viewer.events.theme.connect(self._on_napari_theme_changed)
3978

@@ -53,30 +92,93 @@ def _update_contrast_lims(self) -> None:
5392

5493
self.figure.canvas.draw()
5594

56-
def draw(self) -> None:
57-
"""
58-
Clear the axes and histogram the currently selected layer/slice.
59-
"""
60-
layer = self.layers[0]
95+
@property
96+
def bins_start(self) -> float:
97+
"""Minimum bin edge"""
98+
return self.findChild(QDoubleSpinBox, name="bins start").value()
99+
100+
@bins_start.setter
101+
def bins_start(self, start: int | float) -> None:
102+
"""Set the minimum bin edge"""
103+
self.findChild(QDoubleSpinBox, name="bins start").setValue(start)
104+
105+
@property
106+
def bins_stop(self) -> float:
107+
"""Maximum bin edge"""
108+
return self.findChild(QDoubleSpinBox, name="bins stop").value()
109+
110+
@bins_stop.setter
111+
def bins_stop(self, stop: int | float) -> None:
112+
"""Set the maximum bin edge"""
113+
self.findChild(QDoubleSpinBox, name="bins stop").setValue(stop)
114+
115+
@property
116+
def bins_num(self) -> int:
117+
"""Number of bins to use"""
118+
return self.findChild(QSpinBox, name="bins num").value()
119+
120+
@bins_num.setter
121+
def bins_num(self, num: int) -> None:
122+
"""Set the number of bins to use"""
123+
self.findChild(QSpinBox, name="bins num").setValue(num)
124+
125+
def autoset_widget_bins(self, data: npt.ArrayLike) -> None:
126+
"""Update widgets with bins determined from the image data"""
127+
128+
bins = np.linspace(np.min(data), np.max(data), 100, dtype=data.dtype)
129+
self.bins_start = bins[0]
130+
self.bins_stop = bins[-1]
131+
self.bins_num = bins.size
132+
133+
134+
def _get_layer_data(self, layer) -> np.ndarray:
135+
"""Get the data associated with a given layer"""
61136

62137
if layer.data.ndim - layer.rgb == 3:
63138
# 3D data, can be single channel or RGB
64139
data = layer.data[self.current_z]
65140
self.axes.set_title(f"z={self.current_z}")
66141
else:
67142
data = layer.data
143+
68144
# Read data into memory if it's a dask array
69145
data = np.asarray(data)
70146

147+
return data
148+
149+
def on_update_layers(self) -> None:
150+
"""
151+
Called when the layer selection changes by ``self._update_layers()``.
152+
"""
153+
154+
if not self.layers:
155+
return
156+
157+
# Reset to bin start, stop and step
158+
layer_data = self._get_layer_data(self.layers[0])
159+
self.autoset_widget_bins(data=layer_data)
160+
161+
# Only allow integer bins for integer data
162+
n_decimals = 0 if np.issubdtype(layer_data.dtype, np.integer) else 2
163+
self.findChild(QDoubleSpinBox, name="bins start").setDecimals(n_decimals)
164+
self.findChild(QDoubleSpinBox, name="bins stop").setDecimals(n_decimals)
165+
166+
def draw(self) -> None:
167+
"""
168+
Clear the axes and histogram the currently selected layer/slice.
169+
"""
170+
layer = self.layers[0]
171+
data = self._get_layer_data(layer)
172+
71173
# Important to calculate bins after slicing 3D data, to avoid reading
72174
# whole cube into memory.
73175
if data.dtype.kind in {"i", "u"}:
74176
# Make sure integer data types have integer sized bins
75-
step = abs(np.max(data) - np.min(data)) // 100
177+
step = (self.bins_start - self.bins_stop) // self.bins_num
76178
step = max(1, step)
77-
bins = np.arange(np.min(data), np.max(data) + step, step)
179+
bins = np.arange(self.bins_start, self.bins_stop + step, step)
78180
else:
79-
bins = np.linspace(np.min(data), np.max(data), 100)
181+
bins = np.linspace(self.bins_start, self.bins_stop, self.bins_num)
80182

81183
if layer.rgb:
82184
# Histogram RGB channels independently

0 commit comments

Comments
 (0)