88from napari .layers ._multiscale_data import MultiScaleData
99from qtpy .QtWidgets import (
1010 QComboBox ,
11+ QFormLayout ,
12+ QGroupBox ,
1113 QLabel ,
14+ QSpinBox ,
1215 QVBoxLayout ,
1316 QWidget ,
1417)
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
3656class 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
0 commit comments