1
1
from typing import *
2
2
import math
3
+ from numbers import Real
3
4
4
5
import numpy as np
5
6
18
19
19
20
20
21
class LinearSelector (Graphic , BaseSelector ):
22
+ @property
23
+ def limits (self ) -> Tuple [float , float ]:
24
+ return self ._limits
25
+
26
+ @limits .setter
27
+ def limits (self , values : Tuple [float , float ]):
28
+ # check that `values` is an iterable of two real numbers
29
+ # using `Real` here allows it to work with builtin `int` and `float` types, and numpy scaler types
30
+ if len (values ) != 2 or not all (map (lambda v : isinstance (v , Real ), values )):
31
+ raise TypeError (
32
+ "limits must be an iterable of two numeric values"
33
+ )
34
+ self ._limits = tuple (map (round , values )) # if values are close to zero things get weird so round them
35
+ self .selection ._limits = self ._limits
36
+
21
37
# TODO: make `selection` arg in graphics data space not world space
22
38
def __init__ (
23
39
self ,
@@ -27,7 +43,6 @@ def __init__(
27
43
parent : Graphic = None ,
28
44
end_points : Tuple [int , int ] = None ,
29
45
arrow_keys_modifier : str = "Shift" ,
30
- ipywidget_slider = None ,
31
46
thickness : float = 2.5 ,
32
47
color : Any = "w" ,
33
48
name : str = None ,
@@ -57,9 +72,6 @@ def __init__(
57
72
"Control", "Shift", "Alt" or ``None``. Double click on the selector first to enable the
58
73
arrow key movements, or set the attribute ``arrow_key_events_enabled = True``
59
74
60
- ipywidget_slider: IntSlider, optional
61
- ipywidget slider to associate with this graphic
62
-
63
75
thickness: float, default 2.5
64
76
thickness of the slider
65
77
@@ -84,7 +96,8 @@ def __init__(
84
96
if len (limits ) != 2 :
85
97
raise ValueError ("limits must be a tuple of 2 integers, i.e. (int, int)" )
86
98
87
- limits = tuple (map (round , limits ))
99
+ self ._limits = tuple (map (round , limits ))
100
+
88
101
selection = round (selection )
89
102
90
103
if axis == "x" :
@@ -141,21 +154,18 @@ def __init__(
141
154
self .position_y = selection
142
155
143
156
self .selection = LinearSelectionFeature (
144
- self , axis = axis , value = selection , limits = limits
157
+ self , axis = axis , value = selection , limits = self . _limits
145
158
)
146
159
147
- self .ipywidget_slider = ipywidget_slider
148
-
149
- if self .ipywidget_slider is not None :
150
- self ._setup_ipywidget_slider (ipywidget_slider )
151
-
152
160
self ._move_info : dict = None
153
161
self ._pygfx_event = None
154
162
155
163
self .parent = parent
156
164
157
165
self ._block_ipywidget_call = False
158
166
167
+ self ._handled_widgets = list ()
168
+
159
169
# init base selector
160
170
BaseSelector .__init__ (
161
171
self ,
@@ -166,29 +176,50 @@ def __init__(
166
176
)
167
177
168
178
def _setup_ipywidget_slider (self , widget ):
169
- # setup ipywidget slider with callbacks to this LinearSelector
170
- widget .value = int (self .selection ())
179
+ # setup an ipywidget slider with bidirectional callbacks to this LinearSelector
180
+ value = self .selection ()
181
+
182
+ if isinstance (widget , ipywidgets .IntSlider ):
183
+ value = int (value )
184
+
185
+ widget .value = value
186
+
187
+ # user changes widget -> linear selection changes
171
188
widget .observe (self ._ipywidget_callback , "value" )
172
- self .selection .add_event_handler (self ._update_ipywidget )
189
+
190
+ # user changes linear selection -> widget changes
191
+ self .selection .add_event_handler (self ._update_ipywidgets )
192
+
173
193
self ._plot_area .renderer .add_event_handler (self ._set_slider_layout , "resize" )
174
194
175
- def _update_ipywidget (self , ev ):
176
- # update the ipywidget slider value when LinearSelector value changes
177
- self ._block_ipywidget_call = True
178
- self .ipywidget_slider .value = int (ev .pick_info ["new_data" ])
195
+ self ._handled_widgets .append (widget )
196
+
197
+ def _update_ipywidgets (self , ev ):
198
+ # update the ipywidget sliders when LinearSelector value changes
199
+ self ._block_ipywidget_call = True # prevent infinite recursion
200
+
201
+ value = ev .pick_info ["new_data" ]
202
+ # update all the handled slider widgets
203
+ for widget in self ._handled_widgets :
204
+ if isinstance (widget , ipywidgets .IntSlider ):
205
+ widget .value = int (value )
206
+ else :
207
+ widget .value = value
208
+
179
209
self ._block_ipywidget_call = False
180
210
181
211
def _ipywidget_callback (self , change ):
182
212
# update the LinearSelector if the ipywidget value changes
183
- if self ._block_ipywidget_call :
213
+ if self ._block_ipywidget_call or self . _moving :
184
214
return
185
215
186
216
self .selection = change ["new" ]
187
217
188
218
def _set_slider_layout (self , * args ):
189
219
w , h = self ._plot_area .renderer .logical_size
190
220
191
- self .ipywidget_slider .layout = ipywidgets .Layout (width = f"{ w } px" )
221
+ for widget in self ._handled_widgets :
222
+ widget .layout = ipywidgets .Layout (width = f"{ w } px" )
192
223
193
224
def make_ipywidget_slider (self , kind : str = "IntSlider" , ** kwargs ):
194
225
"""
@@ -197,7 +228,7 @@ def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs):
197
228
Parameters
198
229
----------
199
230
kind: str
200
- "IntSlider" or "FloatSlider "
231
+ "IntSlider", "FloatSlider" or "FloatLogSlider "
201
232
202
233
kwargs
203
234
passed to the ipywidget slider constructor
@@ -207,28 +238,68 @@ def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs):
207
238
ipywidgets.Intslider or ipywidgets.FloatSlider
208
239
209
240
"""
210
- if self .ipywidget_slider is not None :
211
- raise AttributeError ("Already has ipywidget slider" )
212
241
213
242
if not HAS_IPYWIDGETS :
214
243
raise ImportError (
215
244
"Must installed `ipywidgets` to use `make_ipywidget_slider()`"
216
245
)
217
246
247
+ if kind not in ["IntSlider" , "FloatSlider" , "FloatLogSlider" ]:
248
+ raise TypeError (
249
+ f"`kind` must be one of: 'IntSlider', 'FloatSlider' or 'FloatLogSlider'\n "
250
+ f"You have passed: '{ kind } '"
251
+ )
252
+
218
253
cls = getattr (ipywidgets , kind )
219
254
255
+ value = self .selection ()
256
+ if "Int" in kind :
257
+ value = int (self .selection ())
258
+
220
259
slider = cls (
221
- min = self .selection .limits [0 ],
222
- max = self .selection .limits [1 ],
223
- value = int (self .selection ()),
224
- step = 1 ,
260
+ min = self .limits [0 ],
261
+ max = self .limits [1 ],
262
+ value = value ,
225
263
** kwargs ,
226
264
)
227
- self .ipywidget_slider = slider
228
- self ._setup_ipywidget_slider (slider )
265
+ self .add_ipywidget_handler (slider )
229
266
230
267
return slider
231
268
269
+ def add_ipywidget_handler (
270
+ self ,
271
+ widget ,
272
+ step : Union [int , float ] = None
273
+ ):
274
+ """
275
+ Bidirectionally connect events with a ipywidget slider
276
+
277
+ Parameters
278
+ ----------
279
+ widget: ipywidgets.IntSlider, ipywidgets.FloatSlider, or ipywidgets.FloatLogSlider
280
+ ipywidget slider to connect to
281
+
282
+ step: int or float, default ``None``
283
+ step size, if ``None`` 100 steps are created
284
+
285
+ """
286
+
287
+ if not isinstance (widget , (ipywidgets .IntSlider , ipywidgets .FloatSlider , ipywidgets .FloatLogSlider )):
288
+ raise TypeError (
289
+ f"`widget` must be one of: ipywidgets.IntSlider, ipywidgets.FloatSlider, or ipywidgets.FloatLogSlider\n "
290
+ f"You have passed a: <{ type (widget )} "
291
+ )
292
+
293
+ if step is None :
294
+ step = (self .limits [1 ] - self .limits [0 ]) / 100
295
+
296
+ if isinstance (widget , ipywidgets .IntSlider ):
297
+ step = int (step )
298
+
299
+ widget .step = step
300
+
301
+ self ._setup_ipywidget_slider (widget )
302
+
232
303
def get_selected_index (self , graphic : Graphic = None ) -> Union [int , List [int ]]:
233
304
"""
234
305
Data index the slider is currently at w.r.t. the Graphic data. With LineGraphic data, the geometry x or y
0 commit comments