44import numpy as np
55import numpy .typing as npt
66from 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
149from .base import SingleAxesWidget
1510from .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