8
8
from napari .layers ._multiscale_data import MultiScaleData
9
9
from qtpy .QtWidgets import (
10
10
QComboBox ,
11
+ QFormLayout ,
12
+ QGroupBox ,
11
13
QLabel ,
14
+ QSpinBox ,
12
15
QVBoxLayout ,
13
16
QWidget ,
14
17
)
22
25
_COLORS = {"r" : "tab:red" , "g" : "tab:green" , "b" : "tab:blue" }
23
26
24
27
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
+ """
26
46
if data .dtype .kind in {"i" , "u" }:
27
47
# 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 )
29
49
return np .arange (np .min (data ), np .max (data ) + step , step )
30
50
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 )
34
54
35
55
36
56
class HistogramWidget (SingleAxesWidget ):
@@ -47,6 +67,30 @@ def __init__(
47
67
parent : QWidget | None = None ,
48
68
):
49
69
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
+
50
94
self ._update_layers (None )
51
95
self .viewer .events .theme .connect (self ._on_napari_theme_changed )
52
96
@@ -60,6 +104,13 @@ def on_update_layers(self) -> None:
60
104
self ._update_contrast_lims
61
105
)
62
106
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
+
63
114
def _update_contrast_lims (self ) -> None :
64
115
for lim , line in zip (
65
116
self .layers [0 ].contrast_limits , self ._contrast_lines , strict = False
@@ -68,11 +119,13 @@ def _update_contrast_lims(self) -> None:
68
119
69
120
self .figure .canvas .draw ()
70
121
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"""
76
129
data = layer .data
77
130
78
131
if isinstance (layer .data , MultiScaleData ):
@@ -87,9 +140,21 @@ def draw(self) -> None:
87
140
# Read data into memory if it's a dask array
88
141
data = np .asarray (data )
89
142
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
+
90
152
# Important to calculate bins after slicing 3D data, to avoid reading
91
153
# whole cube into memory.
92
- bins = _get_bins (data )
154
+ bins = _get_bins (
155
+ data ,
156
+ num_bins = self .num_bins_widget .value (),
157
+ )
93
158
94
159
if layer .rgb :
95
160
# Histogram RGB channels independently
0 commit comments