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

Skip to content

auto connect a ipywidget slider to a linear and linear region selector #298

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Sep 11, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 23 additions & 3 deletions examples/notebooks/linear_region_selector.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
"source": [
"import fastplotlib as fpl\n",
"import numpy as np\n",
"\n",
"from ipywidgets import IntRangeSlider, FloatRangeSlider, VBox\n",
"\n",
"gp = fpl.GridPlot((2, 2))\n",
"\n",
Expand Down Expand Up @@ -83,7 +83,27 @@
"ls_x.selection.add_event_handler(set_zoom_x)\n",
"ls_y.selection.add_event_handler(set_zoom_y)\n",
"\n",
"gp.show()"
"# make some ipywidget sliders too\n",
"# these are not necessary, it's just to show how they can be connected\n",
"x_range_slider = IntRangeSlider(\n",
" value=ls_x.selection(),\n",
" min=ls_x.limits[0],\n",
" max=ls_x.limits[1],\n",
" description=\"x\"\n",
")\n",
"\n",
"y_range_slider = FloatRangeSlider(\n",
" value=ls_y.selection(),\n",
" min=ls_y.limits[0],\n",
" max=ls_y.limits[1],\n",
" description=\"x\"\n",
")\n",
"\n",
"# connect the region selector to the ipywidget slider\n",
"ls_x.add_ipywidget_handler(x_range_slider, step=5)\n",
"ls_y.add_ipywidget_handler(y_range_slider, step=0.1)\n",
"\n",
"VBox([gp.show(), x_range_slider, y_range_slider])"
]
},
{
Expand Down Expand Up @@ -281,7 +301,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
"version": "3.11.4"
}
},
"nbformat": 4,
Expand Down
26 changes: 11 additions & 15 deletions examples/notebooks/linear_selector.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@
"from fastplotlib.graphics.selectors import Synchronizer\n",
"\n",
"import numpy as np\n",
"from ipywidgets import VBox\n",
"from ipywidgets import VBox, IntSlider, FloatSlider\n",
"\n",
"plot = fpl.Plot()\n",
"\n",
Expand Down Expand Up @@ -49,22 +49,18 @@
"\n",
"# fastplotlib LineSelector can make an ipywidget slider and return it :D \n",
"ipywidget_slider = selector.make_ipywidget_slider()\n",
"ipywidget_slider.description = \"slider1\"\n",
"\n",
"# or you can make your own ipywidget sliders and connect them to the linear selector\n",
"ipywidget_slider2 = IntSlider(min=0, max=100, description=\"slider2\")\n",
"ipywidget_slider3 = FloatSlider(min=0, max=100, description=\"slider3\")\n",
"\n",
"selector2.add_ipywidget_handler(ipywidget_slider2, step=5)\n",
"selector3.add_ipywidget_handler(ipywidget_slider3, step=0.1)\n",
"\n",
"plot.auto_scale()\n",
"plot.show()\n",
"VBox([plot.show(), ipywidget_slider])"
]
},
{
"cell_type": "code",
"execution_count": null,
"id": "a632c8ee-2d4c-44fc-9391-7b2880223fdb",
"metadata": {
"tags": []
},
"outputs": [],
"source": [
"selector.step = 0.1"
"VBox([plot.show(), ipywidget_slider, ipywidget_slider2, ipywidget_slider3])"
]
},
{
Expand Down Expand Up @@ -135,7 +131,7 @@
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.11.3"
"version": "3.11.4"
}
},
"nbformat": 4,
Expand Down
12 changes: 6 additions & 6 deletions fastplotlib/graphics/_features/_selection_features.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,14 +150,14 @@ class LinearSelectionFeature(GraphicFeature):
def __init__(self, parent, axis: str, value: float, limits: Tuple[int, int]):
super(LinearSelectionFeature, self).__init__(parent, data=value)

self.axis = axis
self.limits = limits
self._axis = axis
self._limits = limits

def _set(self, value: float):
if not (self.limits[0] <= value <= self.limits[1]):
if not (self._limits[0] <= value <= self._limits[1]):
return

if self.axis == "x":
if self._axis == "x":
self._parent.position_x = value
else:
self._parent.position_y = value
Expand Down Expand Up @@ -219,7 +219,7 @@ def __init__(
super(LinearRegionSelectionFeature, self).__init__(parent, data=selection)

self._axis = axis
self.limits = limits
self._limits = limits

self._set(selection)

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

# make sure bounds not exceeded
for v in value:
if not (self.limits[0] <= v <= self.limits[1]):
if not (self._limits[0] <= v <= self._limits[1]):
return

# make sure `selector width >= 2`, left edge must not move past right edge!
Expand Down
5 changes: 5 additions & 0 deletions fastplotlib/graphics/selectors/_base_selector.py
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,9 @@ def __init__(

self._move_info: MoveInfo = None

# sets to `True` on "pointer_down", sets to `False` on "pointer_up"
self._moving = False #: indicates if the selector is currently being moved

# used to disable fill area events if the edge is being actively hovered
# otherwise annoying and requires too much accuracy to move just an edge
self._edge_hovered: bool = False
Expand Down Expand Up @@ -189,6 +192,7 @@ def _move_start(self, event_source: WorldObject, ev):
last_position = self._plot_area.map_screen_to_world(ev)

self._move_info = MoveInfo(last_position=last_position, source=event_source)
self._moving = True

def _move(self, ev):
"""
Expand Down Expand Up @@ -231,6 +235,7 @@ def _move_graphic(self, delta: np.ndarray):

def _move_end(self, ev):
self._move_info = None
self._moving = False
self._plot_area.controller.enabled = True

def _move_to_pointer(self, ev):
Expand Down
129 changes: 100 additions & 29 deletions fastplotlib/graphics/selectors/_linear.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
from typing import *
import math
from numbers import Real

import numpy as np

Expand All @@ -18,6 +19,21 @@


class LinearSelector(Graphic, BaseSelector):
@property
def limits(self) -> Tuple[float, float]:
return self._limits

@limits.setter
def limits(self, values: Tuple[float, float]):
# check that `values` is an iterable of two real numbers
# using `Real` here allows it to work with builtin `int` and `float` types, and numpy scaler types
if len(values) != 2 or not all(map(lambda v: isinstance(v, Real), values)):
raise TypeError(
"limits must be an iterable of two numeric values"
)
self._limits = tuple(map(round, values)) # if values are close to zero things get weird so round them
self.selection._limits = self._limits

# TODO: make `selection` arg in graphics data space not world space
def __init__(
self,
Expand All @@ -27,7 +43,6 @@ def __init__(
parent: Graphic = None,
end_points: Tuple[int, int] = None,
arrow_keys_modifier: str = "Shift",
ipywidget_slider=None,
thickness: float = 2.5,
color: Any = "w",
name: str = None,
Expand Down Expand Up @@ -57,9 +72,6 @@ def __init__(
"Control", "Shift", "Alt" or ``None``. Double click on the selector first to enable the
arrow key movements, or set the attribute ``arrow_key_events_enabled = True``

ipywidget_slider: IntSlider, optional
ipywidget slider to associate with this graphic

thickness: float, default 2.5
thickness of the slider

Expand All @@ -84,7 +96,8 @@ def __init__(
if len(limits) != 2:
raise ValueError("limits must be a tuple of 2 integers, i.e. (int, int)")

limits = tuple(map(round, limits))
self._limits = tuple(map(round, limits))

selection = round(selection)

if axis == "x":
Expand Down Expand Up @@ -141,21 +154,18 @@ def __init__(
self.position_y = selection

self.selection = LinearSelectionFeature(
self, axis=axis, value=selection, limits=limits
self, axis=axis, value=selection, limits=self._limits
)

self.ipywidget_slider = ipywidget_slider

if self.ipywidget_slider is not None:
self._setup_ipywidget_slider(ipywidget_slider)

self._move_info: dict = None
self._pygfx_event = None

self.parent = parent

self._block_ipywidget_call = False

self._handled_widgets = list()

# init base selector
BaseSelector.__init__(
self,
Expand All @@ -166,29 +176,50 @@ def __init__(
)

def _setup_ipywidget_slider(self, widget):
# setup ipywidget slider with callbacks to this LinearSelector
widget.value = int(self.selection())
# setup an ipywidget slider with bidirectional callbacks to this LinearSelector
value = self.selection()

if isinstance(widget, ipywidgets.IntSlider):
value = int(value)

widget.value = value

# user changes widget -> linear selection changes
widget.observe(self._ipywidget_callback, "value")
self.selection.add_event_handler(self._update_ipywidget)

# user changes linear selection -> widget changes
self.selection.add_event_handler(self._update_ipywidgets)

self._plot_area.renderer.add_event_handler(self._set_slider_layout, "resize")

def _update_ipywidget(self, ev):
# update the ipywidget slider value when LinearSelector value changes
self._block_ipywidget_call = True
self.ipywidget_slider.value = int(ev.pick_info["new_data"])
self._handled_widgets.append(widget)

def _update_ipywidgets(self, ev):
# update the ipywidget sliders when LinearSelector value changes
self._block_ipywidget_call = True # prevent infinite recursion

value = ev.pick_info["new_data"]
# update all the handled slider widgets
for widget in self._handled_widgets:
if isinstance(widget, ipywidgets.IntSlider):
widget.value = int(value)
else:
widget.value = value

self._block_ipywidget_call = False

def _ipywidget_callback(self, change):
# update the LinearSelector if the ipywidget value changes
if self._block_ipywidget_call:
if self._block_ipywidget_call or self._moving:
return

self.selection = change["new"]

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

self.ipywidget_slider.layout = ipywidgets.Layout(width=f"{w}px")
for widget in self._handled_widgets:
widget.layout = ipywidgets.Layout(width=f"{w}px")

def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs):
"""
Expand All @@ -197,7 +228,7 @@ def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs):
Parameters
----------
kind: str
"IntSlider" or "FloatSlider"
"IntSlider", "FloatSlider" or "FloatLogSlider"

kwargs
passed to the ipywidget slider constructor
Expand All @@ -207,28 +238,68 @@ def make_ipywidget_slider(self, kind: str = "IntSlider", **kwargs):
ipywidgets.Intslider or ipywidgets.FloatSlider

"""
if self.ipywidget_slider is not None:
raise AttributeError("Already has ipywidget slider")

if not HAS_IPYWIDGETS:
raise ImportError(
"Must installed `ipywidgets` to use `make_ipywidget_slider()`"
)

if kind not in ["IntSlider", "FloatSlider", "FloatLogSlider"]:
raise TypeError(
f"`kind` must be one of: 'IntSlider', 'FloatSlider' or 'FloatLogSlider'\n"
f"You have passed: '{kind}'"
)

cls = getattr(ipywidgets, kind)

value = self.selection()
if "Int" in kind:
value = int(self.selection())

slider = cls(
min=self.selection.limits[0],
max=self.selection.limits[1],
value=int(self.selection()),
step=1,
min=self.limits[0],
max=self.limits[1],
value=value,
**kwargs,
)
self.ipywidget_slider = slider
self._setup_ipywidget_slider(slider)
self.add_ipywidget_handler(slider)

return slider

def add_ipywidget_handler(
self,
widget,
step: Union[int, float] = None
):
"""
Bidirectionally connect events with a ipywidget slider

Parameters
----------
widget: ipywidgets.IntSlider, ipywidgets.FloatSlider, or ipywidgets.FloatLogSlider
ipywidget slider to connect to

step: int or float, default ``None``
step size, if ``None`` 100 steps are created

"""

if not isinstance(widget, (ipywidgets.IntSlider, ipywidgets.FloatSlider, ipywidgets.FloatLogSlider)):
raise TypeError(
f"`widget` must be one of: ipywidgets.IntSlider, ipywidgets.FloatSlider, or ipywidgets.FloatLogSlider\n"
f"You have passed a: <{type(widget)}"
)

if step is None:
step = (self.limits[1] - self.limits[0]) / 100

if isinstance(widget, ipywidgets.IntSlider):
step = int(step)

widget.step = step

self._setup_ipywidget_slider(widget)

def get_selected_index(self, graphic: Graphic = None) -> Union[int, List[int]]:
"""
Data index the slider is currently at w.r.t. the Graphic data. With LineGraphic data, the geometry x or y
Expand Down
Loading