16
16
}
17
17
18
18
19
- def calc_gridshape (n ):
19
+ def _calc_gridshape (n ):
20
20
sr = np .sqrt (n )
21
21
return (
22
22
int (np .ceil (sr )),
23
23
int (np .round (sr ))
24
24
)
25
25
26
26
27
- def is_arraylike (obj ) -> bool :
27
+ def _is_arraylike (obj ) -> bool :
28
28
"""
29
29
Checks if the object is array-like.
30
30
For now just checks if obj has `__getitem__()`
@@ -41,6 +41,66 @@ def is_arraylike(obj) -> bool:
41
41
42
42
43
43
class ImageWidget :
44
+ @property
45
+ def plot (self ) -> Union [Plot , GridPlot ]:
46
+ """
47
+ The plotter used by the ImageWidget. Either a simple ``Plot`` or ``GridPlot``.
48
+ """
49
+ return self ._plot
50
+
51
+ @property
52
+ def data (self ) -> List [np .ndarray ]:
53
+ """data currently displayed in the widget"""
54
+ return self ._data
55
+
56
+ @property
57
+ def ndim (self ) -> int :
58
+ """number of dimensions in the image data displayed in the widget"""
59
+ return self ._ndim
60
+
61
+ @property
62
+ def dims_order (self ) -> List [str ]:
63
+ """dimension order of the data displayed in the widget"""
64
+ return self ._dims_order
65
+
66
+ @property
67
+ def sliders (self ) -> List [IntSlider ]:
68
+ """the slider instances used by the widget for indexing the desired dimensions"""
69
+ return self ._sliders
70
+
71
+ @property
72
+ def slider_dims (self ) -> List [str ]:
73
+ """the dimensions that the sliders index"""
74
+ return self ._slider_dims
75
+
76
+ @property
77
+ def current_index (self ) -> Dict [str , int ]:
78
+ return self ._current_index
79
+
80
+ @current_index .setter
81
+ def current_index (self , index : Dict [str , int ]):
82
+ """
83
+ Set the current index
84
+
85
+ Parameters
86
+ ----------
87
+ index: Dict[str, int]
88
+ | ``dict`` for indexing each dimension, provide a ``dict`` with indices for all dimensions used by sliders
89
+ or only a subset of dimensions used by the sliders.
90
+ | example: if you have sliders for dims "t" and "z", you can pass either ``{"t": 10}`` to index to position
91
+ 10 on dimension "t" or ``{"t": 5, "z": 20}`` to index to position 5 on dimension "t" and position 20 on
92
+ dimension "z" simultaneously.
93
+ """
94
+ if not set (index .keys ()).issubset (set (self ._current_index .keys ())):
95
+ raise KeyError (
96
+ f"All dimension keys for setting `current_index` must be present in the widget sliders. "
97
+ f"The dimensions currently used for sliders are: { list (self .current_index .keys ())} "
98
+ )
99
+ self ._current_index .update (index )
100
+ for ig , data in zip (self .image_graphics , self .data ):
101
+ frame = self ._get_2d_slice (data , self ._current_index )
102
+ ig .update_data (frame )
103
+
44
104
def __init__ (
45
105
self ,
46
106
data : Union [np .ndarray , List [np .ndarray ]],
@@ -75,14 +135,15 @@ def __init__(
75
135
dims_order: Optional[Union[str, Dict[np.ndarray, str]]]
76
136
| ``str`` or a dict mapping to indicate dimension order
77
137
| a single ``str`` if ``data`` is a single array, or a list of arrays with the same dimension order
138
+ | examples: ``"xyt"``, ``"tzxy"``
78
139
| ``dict`` mapping of ``{array: axis_order}`` if specific arrays have a non-default axes order.
79
- | examples: "xyt ", "tzxy"
140
+ | examples: ``{some_array: "tzxy ", another_array: "xytz"}``
80
141
81
142
slider_dims: Optional[Union[str, int, List[Union[str, int]]]]
82
143
| The dimensions for which to create a slider
83
144
| can be a single ``str`` such as **"t"**, **"z"** or a numerical ``int`` that indexes the desired dimension
84
145
| can also be a list of ``str`` or ``int`` if multiple sliders are desired for multiple dimensions
85
- | examples: "t", ["t", "z"]
146
+ | examples: `` "t"``, `` ["t", "z"]``
86
147
87
148
slice_avg: Dict[Union[int, str], int]
88
149
| average one or more dimensions using a given window
@@ -101,13 +162,13 @@ def __init__(
101
162
"""
102
163
if isinstance (data , list ):
103
164
# verify that it's a list of np.ndarray
104
- if all ([is_arraylike (d ) for d in data ]):
165
+ if all ([_is_arraylike (d ) for d in data ]):
105
166
if grid_shape is None :
106
- grid_shape = calc_gridshape (len (data ))
167
+ grid_shape = _calc_gridshape (len (data ))
107
168
108
169
# verify that user-specified grid shape is large enough for the number of image arrays passed
109
170
elif grid_shape [0 ] * grid_shape [1 ] < len (data ):
110
- grid_shape = calc_gridshape (len (data ))
171
+ grid_shape = _calc_gridshape (len (data ))
111
172
warn (f"Invalid `grid_shape` passed, setting grid shape to: { grid_shape } " )
112
173
113
174
_ndim = [d .ndim for d in data ]
@@ -119,22 +180,22 @@ def __init__(
119
180
f"Number of dimensions of all data arrays must match, your ndims are: { _ndim } "
120
181
)
121
182
122
- self .data : List [np .ndarray ] = data
123
- self .ndim = self .data [0 ].ndim # all ndim must be same
183
+ self ._data : List [np .ndarray ] = data
184
+ self ._ndim = self .data [0 ].ndim # all ndim must be same
124
185
125
- self .plot_type = "grid"
186
+ self ._plot_type = "grid"
126
187
127
188
else :
128
189
raise TypeError (
129
190
f"If passing a list to `data` all elements must be an "
130
191
f"array-like type representing an n-dimensional image"
131
192
)
132
193
133
- elif is_arraylike (data ):
134
- self .data = [data ]
135
- self .ndim = self .data [0 ].ndim
194
+ elif _is_arraylike (data ):
195
+ self ._data = [data ]
196
+ self ._ndim = self .data [0 ].ndim
136
197
137
- self .plot_type = "single"
198
+ self ._plot_type = "single"
138
199
else :
139
200
raise TypeError (
140
201
f"`data` must be an array-like type representing an n-dimensional image "
@@ -143,12 +204,12 @@ def __init__(
143
204
144
205
# default dims order if not passed
145
206
if dims_order is None :
146
- self .dims_order : List [str ] = [DEFAULT_DIMS_ORDER [self .ndim ]] * len (self .data )
207
+ self ._dims_order : List [str ] = [DEFAULT_DIMS_ORDER [self .ndim ]] * len (self .data )
147
208
148
209
elif isinstance (dims_order , str ):
149
- self .dims_order : List [str ] = [dims_order ] * len (self .data )
210
+ self ._dims_order : List [str ] = [dims_order ] * len (self .data )
150
211
elif isinstance (dims_order , dict ):
151
- self .dims_order : List [str ] = [DEFAULT_DIMS_ORDER [self .ndim ]] * len (self .data )
212
+ self ._dims_order : List [str ] = [DEFAULT_DIMS_ORDER [self .ndim ]] * len (self .data )
152
213
153
214
# dict of {array: dims_order_str}
154
215
for array in list (dims_order .keys ()):
@@ -208,7 +269,7 @@ def __init__(
208
269
f"`dims_order` for all arrays must be identical if passing in a <int> `slider_dims` argument. "
209
270
f"Pass in a <str> argument if the `dims_order` are different for each array."
210
271
)
211
- self .slider_dims : List [str ] = [self .dims_order [0 ][slider_dims ]]
272
+ self ._slider_dims : List [str ] = [self .dims_order [0 ][slider_dims ]]
212
273
213
274
# if dimension specified by str
214
275
elif isinstance (slider_dims , str ):
@@ -217,11 +278,11 @@ def __init__(
217
278
f"if `slider_dims` is a <str>, it must be a character found in `dims_order`. "
218
279
f"Your `dims_order` characters are: { set (self .dims_order [0 ])} ."
219
280
)
220
- self .slider_dims : List [str ] = [slider_dims ]
281
+ self ._slider_dims : List [str ] = [slider_dims ]
221
282
222
283
# multiple sliders, one for each dimension
223
284
elif isinstance (slider_dims , list ):
224
- self .slider_dims : List [str ] = list ()
285
+ self ._slider_dims : List [str ] = list ()
225
286
226
287
# make sure slice_avg and frame_apply are dicts if multiple sliders are desired
227
288
if (not isinstance (slice_avg , dict )) and (slice_avg is not None ):
@@ -265,73 +326,73 @@ def __init__(
265
326
self ._slice_avg = None
266
327
self .slice_avg = slice_avg
267
328
268
- self .sliders = list ()
269
- self .vertical_sliders = list ()
270
- self .horizontal_sliders = list ()
329
+ self ._sliders : List [ IntSlider ] = list ()
330
+ self ._vertical_sliders = list ()
331
+ self ._horizontal_sliders = list ()
271
332
272
333
# current_index stores {dimension_index: slice_index} for every dimension
273
- self .current_index : Dict [str , int ] = {sax : 0 for sax in self .slider_dims }
334
+ self ._current_index : Dict [str , int ] = {sax : 0 for sax in self .slider_dims }
274
335
275
336
# get max bound for all data arrays for all dimensions
276
- self .dims_max_bounds : Dict [str , int ] = {k : np .inf for k in self .slider_dims }
277
- for _dim in list (self .dims_max_bounds .keys ()):
337
+ self ._dims_max_bounds : Dict [str , int ] = {k : np .inf for k in self .slider_dims }
338
+ for _dim in list (self ._dims_max_bounds .keys ()):
278
339
for array , order in zip (self .data , self .dims_order ):
279
- self .dims_max_bounds [_dim ] = min (self .dims_max_bounds [_dim ], array .shape [order .index (_dim )])
340
+ self ._dims_max_bounds [_dim ] = min (self ._dims_max_bounds [_dim ], array .shape [order .index (_dim )])
280
341
281
- if self .plot_type == "single" :
282
- self .plot : Plot = Plot ()
342
+ if self ._plot_type == "single" :
343
+ self ._plot : Plot = Plot ()
283
344
284
345
if slice_avg is not None :
285
346
pass
286
347
287
- frame = self .get_2d_slice (self .data [0 ], slice_indices = self .current_index )
348
+ frame = self ._get_2d_slice (self .data [0 ], slice_indices = self ._current_index )
288
349
289
350
self .image_graphics : List [Image ] = [self .plot .image (data = frame , ** kwargs )]
290
351
291
- elif self .plot_type == "grid" :
292
- self .plot : GridPlot = GridPlot (shape = grid_shape , controllers = "sync" )
352
+ elif self ._plot_type == "grid" :
353
+ self ._plot : GridPlot = GridPlot (shape = grid_shape , controllers = "sync" )
293
354
294
355
self .image_graphics = list ()
295
356
for d , subplot in zip (self .data , self .plot ):
296
- frame = self .get_2d_slice (d , slice_indices = self .current_index )
357
+ frame = self ._get_2d_slice (d , slice_indices = self ._current_index )
297
358
ig = Image (frame , ** kwargs )
298
359
subplot .add_graphic (ig )
299
360
self .image_graphics .append (ig )
300
361
301
- self .plot .renderer .add_event_handler (self .set_slider_layout , "resize" )
362
+ self .plot .renderer .add_event_handler (self ._set_slider_layout , "resize" )
302
363
303
364
for sdm in self .slider_dims :
304
365
if sdm == "z" :
305
- # TODO: once ipywidgets plays nicely with HBox and jupyter-rfb can use vertical
366
+ # TODO: once ipywidgets plays nicely with HBox and jupyter-rfb, use vertical
306
367
# orientation = "vertical"
307
368
orientation = "horizontal"
308
369
else :
309
370
orientation = "horizontal"
310
371
311
372
slider = IntSlider (
312
373
min = 0 ,
313
- max = self .dims_max_bounds [sdm ] - 1 ,
374
+ max = self ._dims_max_bounds [sdm ] - 1 ,
314
375
step = 1 ,
315
376
value = 0 ,
316
377
description = f"dimension: { sdm } " ,
317
378
orientation = orientation
318
379
)
319
380
320
381
slider .observe (
321
- partial (self .slider_value_changed , sdm ),
382
+ partial (self ._slider_value_changed , sdm ),
322
383
names = "value"
323
384
)
324
385
325
- self .sliders .append (slider )
386
+ self ._sliders .append (slider )
326
387
if orientation == "horizontal" :
327
- self .horizontal_sliders .append (slider )
388
+ self ._horizontal_sliders .append (slider )
328
389
elif orientation == "vertical" :
329
- self .vertical_sliders .append (slider )
390
+ self ._vertical_sliders .append (slider )
330
391
331
392
# TODO: So just stack everything vertically for now
332
393
self .widget = VBox ([
333
394
self .plot .canvas ,
334
- * self .sliders
395
+ * self ._sliders
335
396
])
336
397
337
398
# TODO: there is currently an issue with ipywidgets or jupyter-rfb and HBox doesn't work with RFB canvas
@@ -391,7 +452,7 @@ def slice_avg(self, sa: Union[int, Dict[str, int]]):
391
452
f"You have passed a { type (sa )} . See the docstring."
392
453
)
393
454
394
- def get_2d_slice (
455
+ def _get_2d_slice (
395
456
self ,
396
457
array : np .ndarray ,
397
458
slice_indices : dict [Union [int , str ], int ]
@@ -485,30 +546,34 @@ def _process_dim_index(self, data_ix, dim, indices_dim):
485
546
486
547
hw = int ((sa - 1 ) / 2 ) # half-window size
487
548
# get the max bound for that dimension
488
- max_bound = self .dims_max_bounds [dim_str ]
549
+ max_bound = self ._dims_max_bounds [dim_str ]
489
550
indices_dim = range (max (0 , ix - hw ), min (max_bound , ix + hw ))
490
551
return indices_dim
491
552
492
- def slider_value_changed (
553
+ def _slider_value_changed (
493
554
self ,
494
- dimension : int ,
555
+ dimension : str ,
495
556
change : dict
496
557
):
497
- self .current_index [ dimension ] = change ["new" ]
558
+ self .current_index = { dimension : change ["new" ]}
498
559
499
- for ig , data in zip (self .image_graphics , self .data ):
500
- frame = self .get_2d_slice (data , self .current_index )
501
- ig .update_data (frame )
502
-
503
- def set_slider_layout (self , * args ):
560
+ def _set_slider_layout (self , * args ):
504
561
w , h = self .plot .renderer .logical_size
505
- for hs in self .horizontal_sliders :
562
+ for hs in self ._horizontal_sliders :
506
563
hs .layout = Layout (width = f"{ w } px" )
507
564
508
- for vs in self .vertical_sliders :
565
+ for vs in self ._vertical_sliders :
509
566
vs .layout = Layout (height = f"{ h } px" )
510
567
511
568
def show (self ):
569
+ """
570
+ Show the widget
571
+
572
+ Returns
573
+ -------
574
+ VBox
575
+ ``ipywidgets.VBox`` stacking the plotter and sliders in a vertical layout
576
+ """
512
577
# start render loop
513
578
self .plot .show ()
514
579
0 commit comments