Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 2b97c9d

Browse files
authored
auto connect a ipywidget slider to a linear selector (#298)
* auto connect a ipywidget slider to a linear selector, not yet tested * add _moving attr, smooth bidirectional link between linear selector and ipywidget sliders * update linear selector example nb * ipywidget connector for LinearRegionSelector, limits is a settable property for linear and linearregion * comments
1 parent 36ed10c commit 2b97c9d

File tree

6 files changed

+300
-56
lines changed

6 files changed

+300
-56
lines changed

examples/notebooks/linear_region_selector.ipynb

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@
1919
"source": [
2020
"import fastplotlib as fpl\n",
2121
"import numpy as np\n",
22-
"\n",
22+
"from ipywidgets import IntRangeSlider, FloatRangeSlider, VBox\n",
2323
"\n",
2424
"gp = fpl.GridPlot((2, 2))\n",
2525
"\n",
@@ -83,7 +83,27 @@
8383
"ls_x.selection.add_event_handler(set_zoom_x)\n",
8484
"ls_y.selection.add_event_handler(set_zoom_y)\n",
8585
"\n",
86-
"gp.show()"
86+
"# make some ipywidget sliders too\n",
87+
"# these are not necessary, it's just to show how they can be connected\n",
88+
"x_range_slider = IntRangeSlider(\n",
89+
" value=ls_x.selection(),\n",
90+
" min=ls_x.limits[0],\n",
91+
" max=ls_x.limits[1],\n",
92+
" description=\"x\"\n",
93+
")\n",
94+
"\n",
95+
"y_range_slider = FloatRangeSlider(\n",
96+
" value=ls_y.selection(),\n",
97+
" min=ls_y.limits[0],\n",
98+
" max=ls_y.limits[1],\n",
99+
" description=\"x\"\n",
100+
")\n",
101+
"\n",
102+
"# connect the region selector to the ipywidget slider\n",
103+
"ls_x.add_ipywidget_handler(x_range_slider, step=5)\n",
104+
"ls_y.add_ipywidget_handler(y_range_slider, step=0.1)\n",
105+
"\n",
106+
"VBox([gp.show(), x_range_slider, y_range_slider])"
87107
]
88108
},
89109
{
@@ -281,7 +301,7 @@
281301
"name": "python",
282302
"nbconvert_exporter": "python",
283303
"pygments_lexer": "ipython3",
284-
"version": "3.11.3"
304+
"version": "3.11.4"
285305
}
286306
},
287307
"nbformat": 4,

examples/notebooks/linear_selector.ipynb

Lines changed: 11 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@
2121
"from fastplotlib.graphics.selectors import Synchronizer\n",
2222
"\n",
2323
"import numpy as np\n",
24-
"from ipywidgets import VBox\n",
24+
"from ipywidgets import VBox, IntSlider, FloatSlider\n",
2525
"\n",
2626
"plot = fpl.Plot()\n",
2727
"\n",
@@ -49,22 +49,18 @@
4949
"\n",
5050
"# fastplotlib LineSelector can make an ipywidget slider and return it :D \n",
5151
"ipywidget_slider = selector.make_ipywidget_slider()\n",
52+
"ipywidget_slider.description = \"slider1\"\n",
53+
"\n",
54+
"# or you can make your own ipywidget sliders and connect them to the linear selector\n",
55+
"ipywidget_slider2 = IntSlider(min=0, max=100, description=\"slider2\")\n",
56+
"ipywidget_slider3 = FloatSlider(min=0, max=100, description=\"slider3\")\n",
57+
"\n",
58+
"selector2.add_ipywidget_handler(ipywidget_slider2, step=5)\n",
59+
"selector3.add_ipywidget_handler(ipywidget_slider3, step=0.1)\n",
5260
"\n",
5361
"plot.auto_scale()\n",
5462
"plot.show()\n",
55-
"VBox([plot.show(), ipywidget_slider])"
56-
]
57-
},
58-
{
59-
"cell_type": "code",
60-
"execution_count": null,
61-
"id": "a632c8ee-2d4c-44fc-9391-7b2880223fdb",
62-
"metadata": {
63-
"tags": []
64-
},
65-
"outputs": [],
66-
"source": [
67-
"selector.step = 0.1"
63+
"VBox([plot.show(), ipywidget_slider, ipywidget_slider2, ipywidget_slider3])"
6864
]
6965
},
7066
{
@@ -135,7 +131,7 @@
135131
"name": "python",
136132
"nbconvert_exporter": "python",
137133
"pygments_lexer": "ipython3",
138-
"version": "3.11.3"
134+
"version": "3.11.4"
139135
}
140136
},
141137
"nbformat": 4,

fastplotlib/graphics/_features/_selection_features.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -150,14 +150,14 @@ class LinearSelectionFeature(GraphicFeature):
150150
def __init__(self, parent, axis: str, value: float, limits: Tuple[int, int]):
151151
super(LinearSelectionFeature, self).__init__(parent, data=value)
152152

153-
self.axis = axis
154-
self.limits = limits
153+
self._axis = axis
154+
self._limits = limits
155155

156156
def _set(self, value: float):
157-
if not (self.limits[0] <= value <= self.limits[1]):
157+
if not (self._limits[0] <= value <= self._limits[1]):
158158
return
159159

160-
if self.axis == "x":
160+
if self._axis == "x":
161161
self._parent.position_x = value
162162
else:
163163
self._parent.position_y = value
@@ -219,7 +219,7 @@ def __init__(
219219
super(LinearRegionSelectionFeature, self).__init__(parent, data=selection)
220220

221221
self._axis = axis
222-
self.limits = limits
222+
self._limits = limits
223223

224224
self._set(selection)
225225

@@ -238,7 +238,7 @@ def _set(self, value: Tuple[float, float]):
238238

239239
# make sure bounds not exceeded
240240
for v in value:
241-
if not (self.limits[0] <= v <= self.limits[1]):
241+
if not (self._limits[0] <= v <= self._limits[1]):
242242
return
243243

244244
# make sure `selector width >= 2`, left edge must not move past right edge!

fastplotlib/graphics/selectors/_base_selector.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,9 @@ def __init__(
8080

8181
self._move_info: MoveInfo = None
8282

83+
# sets to `True` on "pointer_down", sets to `False` on "pointer_up"
84+
self._moving = False #: indicates if the selector is currently being moved
85+
8386
# used to disable fill area events if the edge is being actively hovered
8487
# otherwise annoying and requires too much accuracy to move just an edge
8588
self._edge_hovered: bool = False
@@ -189,6 +192,7 @@ def _move_start(self, event_source: WorldObject, ev):
189192
last_position = self._plot_area.map_screen_to_world(ev)
190193

191194
self._move_info = MoveInfo(last_position=last_position, source=event_source)
195+
self._moving = True
192196

193197
def _move(self, ev):
194198
"""
@@ -231,6 +235,7 @@ def _move_graphic(self, delta: np.ndarray):
231235

232236
def _move_end(self, ev):
233237
self._move_info = None
238+
self._moving = False
234239
self._plot_area.controller.enabled = True
235240

236241
def _move_to_pointer(self, ev):

fastplotlib/graphics/selectors/_linear.py

Lines changed: 100 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
from typing import *
22
import math
3+
from numbers import Real
34

45
import numpy as np
56

@@ -18,6 +19,21 @@
1819

1920

2021
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+
2137
# TODO: make `selection` arg in graphics data space not world space
2238
def __init__(
2339
self,
@@ -27,7 +43,6 @@ def __init__(
2743
parent: Graphic = None,
2844
end_points: Tuple[int, int] = None,
2945
arrow_keys_modifier: str = "Shift",
30-
ipywidget_slider=None,
3146
thickness: float = 2.5,
3247
color: Any = "w",
3348
name: str = None,
@@ -57,9 +72,6 @@ def __init__(
5772
"Control", "Shift", "Alt" or ``None``. Double click on the selector first to enable the
5873
arrow key movements, or set the attribute ``arrow_key_events_enabled = True``
5974
60-
ipywidget_slider: IntSlider, optional
61-
ipywidget slider to associate with this graphic
62-
6375
thickness: float, default 2.5
6476
thickness of the slider
6577
@@ -84,7 +96,8 @@ def __init__(
8496
if len(limits) != 2:
8597
raise ValueError("limits must be a tuple of 2 integers, i.e. (int, int)")
8698

87-
limits = tuple(map(round, limits))
99+
self._limits = tuple(map(round, limits))
100+
88101
selection = round(selection)
89102

90103
if axis == "x":
@@ -141,21 +154,18 @@ def __init__(
141154
self.position_y = selection
142155

143156
self.selection = LinearSelectionFeature(
144-
self, axis=axis, value=selection, limits=limits
157+
self, axis=axis, value=selection, limits=self._limits
145158
)
146159

147-
self.ipywidget_slider = ipywidget_slider
148-
149-
if self.ipywidget_slider is not None:
150-
self._setup_ipywidget_slider(ipywidget_slider)
151-
152160
self._move_info: dict = None
153161
self._pygfx_event = None
154162

155163
self.parent = parent
156164

157165
self._block_ipywidget_call = False
158166

167+
self._handled_widgets = list()
168+
159169
# init base selector
160170
BaseSelector.__init__(
161171
self,
@@ -166,29 +176,50 @@ def __init__(
166176
)
167177

168178
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
171188
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+
173193
self._plot_area.renderer.add_event_handler(self._set_slider_layout, "resize")
174194

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+
179209
self._block_ipywidget_call = False
180210

181211
def _ipywidget_callback(self, change):
182212
# update the LinearSelector if the ipywidget value changes
183-
if self._block_ipywidget_call:
213+
if self._block_ipywidget_call or self._moving:
184214
return
185215

186216
self.selection = change["new"]
187217

188218
def _set_slider_layout(self, *args):
189219
w, h = self._plot_area.renderer.logical_size
190220

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")
192223

193224
def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs):
194225
"""
@@ -197,7 +228,7 @@ def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs):
197228
Parameters
198229
----------
199230
kind: str
200-
"IntSlider" or "FloatSlider"
231+
"IntSlider", "FloatSlider" or "FloatLogSlider"
201232
202233
kwargs
203234
passed to the ipywidget slider constructor
@@ -207,28 +238,68 @@ def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs):
207238
ipywidgets.Intslider or ipywidgets.FloatSlider
208239
209240
"""
210-
if self.ipywidget_slider is not None:
211-
raise AttributeError("Already has ipywidget slider")
212241

213242
if not HAS_IPYWIDGETS:
214243
raise ImportError(
215244
"Must installed `ipywidgets` to use `make_ipywidget_slider()`"
216245
)
217246

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+
218253
cls = getattr(ipywidgets, kind)
219254

255+
value = self.selection()
256+
if "Int" in kind:
257+
value = int(self.selection())
258+
220259
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,
225263
**kwargs,
226264
)
227-
self.ipywidget_slider = slider
228-
self._setup_ipywidget_slider(slider)
265+
self.add_ipywidget_handler(slider)
229266

230267
return slider
231268

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+
232303
def get_selected_index(self, graphic: Graphic = None) -> Union[int, List[int]]:
233304
"""
234305
Data index the slider is currently at w.r.t. the Graphic data. With LineGraphic data, the geometry x or y

0 commit comments

Comments
 (0)