Skip to content

Commit cf5b17f

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

File tree

1 file changed

+119
-12
lines changed

1 file changed

+119
-12
lines changed

src/napari_matplotlib/histogram.py

Lines changed: 119 additions & 12 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,26 +29,138 @@ 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

39-
def draw(self) -> None:
40-
"""
41-
Clear the axes and histogram the currently selected layer/slice.
42-
"""
43-
layer = self.layers[0]
78+
@property
79+
def bins_start(self) -> float:
80+
"""Minimum bin edge"""
81+
return self.findChild(QDoubleSpinBox, name="bins start").value()
82+
83+
@bins_start.setter
84+
def bins_start(self, start: int | float) -> None:
85+
"""Set the minimum bin edge"""
86+
self.findChild(QDoubleSpinBox, name="bins start").setValue(start)
87+
88+
@property
89+
def bins_stop(self) -> float:
90+
"""Maximum bin edge"""
91+
return self.findChild(QDoubleSpinBox, name="bins stop").value()
92+
93+
@bins_stop.setter
94+
def bins_stop(self, stop: int | float) -> None:
95+
"""Set the maximum bin edge"""
96+
self.findChild(QDoubleSpinBox, name="bins stop").setValue(stop)
97+
98+
@property
99+
def bins_num(self) -> int:
100+
"""Number of bins to use"""
101+
return self.findChild(QSpinBox, name="bins num").value()
102+
103+
@bins_num.setter
104+
def bins_num(self, num: int) -> None:
105+
"""Set the number of bins to use"""
106+
self.findChild(QSpinBox, name="bins num").setValue(num)
107+
108+
def autoset_widget_bins(self, data: npt.ArrayLike) -> None:
109+
"""Update widgets with bins determined from the image data"""
110+
111+
bins = np.linspace(np.min(data), np.max(data), 100, dtype=data.dtype)
112+
self.bins_start = bins[0]
113+
self.bins_stop = bins[-1]
114+
self.bins_num = bins.size
115+
116+
117+
def _get_layer_data(self, layer) -> np.ndarray:
118+
"""Get the data associated with a given layer"""
44119

45120
if layer.data.ndim - layer.rgb == 3:
46121
# 3D data, can be single channel or RGB
47122
data = layer.data[self.current_z]
48123
self.axes.set_title(f"z={self.current_z}")
49124
else:
50125
data = layer.data
126+
51127
# Read data into memory if it's a dask array
52128
data = np.asarray(data)
53129

130+
return data
131+
132+
def on_update_layers(self) -> None:
133+
"""
134+
Called when the layer selection changes by ``self._update_layers()``.
135+
"""
136+
137+
if not self.layers:
138+
return
139+
140+
# Reset to bin start, stop and step
141+
layer_data = self._get_layer_data(self.layers[0])
142+
self.autoset_widget_bins(data=layer_data)
143+
144+
# Only allow integer bins for integer data
145+
n_decimals = 0 if np.issubdtype(layer_data.dtype, np.integer) else 2
146+
self.findChild(QDoubleSpinBox, name="bins start").setDecimals(n_decimals)
147+
self.findChild(QDoubleSpinBox, name="bins stop").setDecimals(n_decimals)
148+
149+
def draw(self) -> None:
150+
"""
151+
Clear the axes and histogram the currently selected layer/slice.
152+
"""
153+
layer = self.layers[0]
154+
data = self._get_layer_data(layer)
155+
54156
# Important to calculate bins after slicing 3D data, to avoid reading
55157
# whole cube into memory.
56-
bins = np.linspace(np.min(data), np.max(data), 100, dtype=data.dtype)
158+
bins = np.linspace(
159+
self.bins_start,
160+
self.bins_stop,
161+
self.bins_num,
162+
dtype=data.dtype,
163+
)
57164

58165
if layer.rgb:
59166
# Histogram RGB channels independently

0 commit comments

Comments
 (0)