Skip to content

Commit 4b0f7a9

Browse files
authored
LinearRegionSelector (#164)
LinearRegionSelector, can select on x or y axis entire selection responds to mouse events edges allow resizing with mouse events, edges change color and thickness with interaction `LinearSelector.get_selected_data()` returns a view of the data, or a list of views if from a collection such as LineStack `LinearBoundsFeature` manages events. `LineGraphic.add_linear_region_selector()`, allows adding multiple selectors onto the same graphic `LineCollection.add_linear_region_selector()`
1 parent 4b763ab commit 4b0f7a9

File tree

8 files changed

+1078
-7
lines changed

8 files changed

+1078
-7
lines changed

examples/linear_selector.ipynb

Lines changed: 301 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,301 @@
1+
{
2+
"cells": [
3+
{
4+
"cell_type": "markdown",
5+
"id": "40bf515f-7ca3-4f16-8ec9-31076e8d4bde",
6+
"metadata": {},
7+
"source": [
8+
"# `LinearSelector` with single lines"
9+
]
10+
},
11+
{
12+
"cell_type": "code",
13+
"execution_count": null,
14+
"id": "41f4e1d0-9ae9-4e59-9883-d9339d985afe",
15+
"metadata": {
16+
"tags": []
17+
},
18+
"outputs": [],
19+
"source": [
20+
"import fastplotlib as fpl\n",
21+
"import numpy as np\n",
22+
"\n",
23+
"\n",
24+
"gp = fpl.GridPlot((2, 2))\n",
25+
"\n",
26+
"# preallocated size for zoomed data\n",
27+
"zoomed_prealloc = 1_000\n",
28+
"\n",
29+
"# data to plot\n",
30+
"xs = np.linspace(0, 100, 1_000)\n",
31+
"sine = np.sin(xs) * 20\n",
32+
"\n",
33+
"# make sine along x axis\n",
34+
"sine_graphic_x = gp[0, 0].add_line(sine)\n",
35+
"\n",
36+
"# just something that looks different for line along y-axis\n",
37+
"sine_y = sine\n",
38+
"sine_y[sine_y > 0] = 0\n",
39+
"\n",
40+
"# sine along y axis\n",
41+
"sine_graphic_y = gp[0, 1].add_line(np.column_stack([sine_y, xs]))\n",
42+
"\n",
43+
"# offset the position of the graphic to demonstrate `get_selected_data()` later\n",
44+
"sine_graphic_y.position.set_x(50)\n",
45+
"sine_graphic_y.position.set_y(50)\n",
46+
"\n",
47+
"# add linear selectors\n",
48+
"ls_x = sine_graphic_x.add_linear_region_selector() # default axis is \"x\"\n",
49+
"ls_y = sine_graphic_y.add_linear_region_selector(axis=\"y\")\n",
50+
"\n",
51+
"# preallocate array for storing zoomed in data\n",
52+
"zoomed_init = np.column_stack([np.arange(zoomed_prealloc), np.random.rand(zoomed_prealloc)])\n",
53+
"\n",
54+
"# make line graphics for displaying zoomed data\n",
55+
"zoomed_x = gp[1, 0].add_line(zoomed_init)\n",
56+
"zoomed_y = gp[1, 1].add_line(zoomed_init)\n",
57+
"\n",
58+
"\n",
59+
"def interpolate(subdata: np.ndarray, axis: int):\n",
60+
" \"\"\"1D interpolation to display within the preallocated data array\"\"\"\n",
61+
" x = np.arange(0, zoomed_prealloc)\n",
62+
" xp = np.linspace(0, zoomed_prealloc, subdata.shape[0])\n",
63+
" \n",
64+
" # interpolate to preallocated size\n",
65+
" return np.interp(x, xp, fp=subdata[:, axis]) # use the y-values\n",
66+
"\n",
67+
"\n",
68+
"def set_zoom_x(ev):\n",
69+
" \"\"\"sets zoomed x selector data\"\"\"\n",
70+
" selected_data = ev.pick_info[\"selected_data\"]\n",
71+
" zoomed_x.data = interpolate(selected_data, axis=1) # use the y-values\n",
72+
" gp[1, 0].auto_scale()\n",
73+
"\n",
74+
"\n",
75+
"def set_zoom_y(ev):\n",
76+
" \"\"\"sets zoomed y selector data\"\"\"\n",
77+
" selected_data = ev.pick_info[\"selected_data\"]\n",
78+
" zoomed_y.data = -interpolate(selected_data, axis=0) # use the x-values\n",
79+
" gp[1, 1].auto_scale()\n",
80+
"\n",
81+
"\n",
82+
"# update zoomed plots when bounds change\n",
83+
"ls_x.bounds.add_event_handler(set_zoom_x)\n",
84+
"ls_y.bounds.add_event_handler(set_zoom_y)\n",
85+
"\n",
86+
"gp.show()"
87+
]
88+
},
89+
{
90+
"cell_type": "markdown",
91+
"id": "66b1c599-42c0-4223-b33e-37c1ef077204",
92+
"metadata": {},
93+
"source": [
94+
"### On the x-axis we have a 1-1 mapping from the data that we have passed and the line geometry positions. So the `bounds` min max corresponds directly to the data indices."
95+
]
96+
},
97+
{
98+
"cell_type": "code",
99+
"execution_count": null,
100+
"id": "8b26a37d-aa1d-478e-ad77-99f68a2b7d0c",
101+
"metadata": {
102+
"tags": []
103+
},
104+
"outputs": [],
105+
"source": [
106+
"ls_x.bounds()"
107+
]
108+
},
109+
{
110+
"cell_type": "code",
111+
"execution_count": null,
112+
"id": "c2be060c-8f87-4b5c-8262-619768f6e6af",
113+
"metadata": {
114+
"tags": []
115+
},
116+
"outputs": [],
117+
"source": [
118+
"ls_x.get_selected_indices()"
119+
]
120+
},
121+
{
122+
"cell_type": "markdown",
123+
"id": "d1bef432-d764-4841-bd6d-9b9e4c86ff62",
124+
"metadata": {},
125+
"source": [
126+
"### However, for the y-axis line we have passed a 2D array where we've used a linspace, so there is not a 1-1 mapping from the data to the line geometry positions. Use `get_selected_indices()` to get the indices of the data bounded by the current selection. In addition the position of the Graphic is not `(0, 0)`. You must use `get_selected_indices()` whenever you want the indices of the selected data."
127+
]
128+
},
129+
{
130+
"cell_type": "code",
131+
"execution_count": null,
132+
"id": "c370d6d7-d92a-4680-8bf0-2f9d541028be",
133+
"metadata": {
134+
"tags": []
135+
},
136+
"outputs": [],
137+
"source": [
138+
"ls_y.bounds()"
139+
]
140+
},
141+
{
142+
"cell_type": "code",
143+
"execution_count": null,
144+
"id": "cdf351e1-63a2-4f5a-8199-8ac3f70909c1",
145+
"metadata": {
146+
"tags": []
147+
},
148+
"outputs": [],
149+
"source": [
150+
"ls_y.get_selected_indices()"
151+
]
152+
},
153+
{
154+
"cell_type": "code",
155+
"execution_count": null,
156+
"id": "6fd608ad-9732-4f50-9d43-8630603c86d0",
157+
"metadata": {
158+
"tags": []
159+
},
160+
"outputs": [],
161+
"source": [
162+
"import fastplotlib as fpl\n",
163+
"import numpy as np\n",
164+
"\n",
165+
"# data to plot\n",
166+
"xs = np.linspace(0, 100, 1_000)\n",
167+
"sine = np.sin(xs) * 20\n",
168+
"cosine = np.cos(xs) * 20\n",
169+
"\n",
170+
"plot = fpl.GridPlot((5, 1))\n",
171+
"\n",
172+
"# sines and cosines\n",
173+
"sines = [sine] * 2\n",
174+
"cosines = [cosine] * 2\n",
175+
"\n",
176+
"# make line stack\n",
177+
"line_stack = plot[0, 0].add_line_stack(sines + cosines, separation=50)\n",
178+
"\n",
179+
"# make selector\n",
180+
"selector = line_stack.add_linear_region_selector()\n",
181+
"\n",
182+
"# populate subplots with preallocated graphics\n",
183+
"for i, subplot in enumerate(plot):\n",
184+
" if i == 0:\n",
185+
" # skip the first one\n",
186+
" continue\n",
187+
" # make line graphics for displaying zoomed data\n",
188+
" subplot.add_line(zoomed_init, name=\"zoomed\")\n",
189+
"\n",
190+
"\n",
191+
"def update_zoomed_subplots(ev):\n",
192+
" \"\"\"update the zoomed subplots\"\"\"\n",
193+
" zoomed_data = selector.get_selected_data()\n",
194+
" \n",
195+
" for i in range(len(zoomed_data)):\n",
196+
" data = interpolate(zoomed_data[i], axis=1)\n",
197+
" plot[i + 1, 0][\"zoomed\"].data = data\n",
198+
" plot[i + 1, 0].auto_scale()\n",
199+
"\n",
200+
"\n",
201+
"selector.bounds.add_event_handler(update_zoomed_subplots)\n",
202+
"plot.show()"
203+
]
204+
},
205+
{
206+
"cell_type": "markdown",
207+
"id": "63acd2b6-958e-458d-bf01-903037644cfe",
208+
"metadata": {},
209+
"source": [
210+
"# Large line stack with selector"
211+
]
212+
},
213+
{
214+
"cell_type": "code",
215+
"execution_count": null,
216+
"id": "20e53223-6ccd-4145-bf67-32eb409d3b0a",
217+
"metadata": {
218+
"tags": []
219+
},
220+
"outputs": [],
221+
"source": [
222+
"import fastplotlib as fpl\n",
223+
"import numpy as np\n",
224+
"\n",
225+
"# data to plot\n",
226+
"xs = np.linspace(0, 250, 10_000)\n",
227+
"sine = np.sin(xs) * 20\n",
228+
"cosine = np.cos(xs) * 20\n",
229+
"\n",
230+
"plot = fpl.GridPlot((1, 2))\n",
231+
"\n",
232+
"# sines and cosines\n",
233+
"sines = [sine] * 1_00\n",
234+
"cosines = [cosine] * 1_00\n",
235+
"\n",
236+
"# make line stack\n",
237+
"line_stack = plot[0, 0].add_line_stack(sines + cosines, separation=50)\n",
238+
"\n",
239+
"# make selector\n",
240+
"stack_selector = line_stack.add_linear_region_selector(padding=200)\n",
241+
"\n",
242+
"zoomed_line_stack = plot[0, 1].add_line_stack([zoomed_init] * 2_000, separation=50, name=\"zoomed\")\n",
243+
" \n",
244+
"def update_zoomed_stack(ev):\n",
245+
" \"\"\"update the zoomed subplots\"\"\"\n",
246+
" zoomed_data = stack_selector.get_selected_data()\n",
247+
" \n",
248+
" for i in range(len(zoomed_data)):\n",
249+
" data = interpolate(zoomed_data[i], axis=1)\n",
250+
" zoomed_line_stack.graphics[i].data = data\n",
251+
" \n",
252+
" plot[0, 1].auto_scale()\n",
253+
"\n",
254+
"\n",
255+
"stack_selector.bounds.add_event_handler(update_zoomed_stack)\n",
256+
"plot.show()"
257+
]
258+
},
259+
{
260+
"cell_type": "code",
261+
"execution_count": null,
262+
"id": "3fa61ffd-43d5-42d0-b3e1-5541f58185cd",
263+
"metadata": {
264+
"tags": []
265+
},
266+
"outputs": [],
267+
"source": [
268+
"plot[0, 0].auto_scale()"
269+
]
270+
},
271+
{
272+
"cell_type": "code",
273+
"execution_count": null,
274+
"id": "80e276ba-23b3-43d0-9e0c-86acab79ac67",
275+
"metadata": {},
276+
"outputs": [],
277+
"source": []
278+
}
279+
],
280+
"metadata": {
281+
"kernelspec": {
282+
"display_name": "Python 3 (ipykernel)",
283+
"language": "python",
284+
"name": "python3"
285+
},
286+
"language_info": {
287+
"codemirror_mode": {
288+
"name": "ipython",
289+
"version": 3
290+
},
291+
"file_extension": ".py",
292+
"mimetype": "text/x-python",
293+
"name": "python",
294+
"nbconvert_exporter": "python",
295+
"pygments_lexer": "ipython3",
296+
"version": "3.11.3"
297+
}
298+
},
299+
"nbformat": 4,
300+
"nbformat_minor": 5
301+
}

fastplotlib/graphics/features/_base.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -145,7 +145,7 @@ def _call_event_handlers(self, event_data: FeatureEvent):
145145
func(event_data)
146146
else:
147147
func()
148-
except:
148+
except TypeError:
149149
warn(f"Event handler {func} has an unresolvable argspec, calling it without arguments")
150150
func()
151151

0 commit comments

Comments
 (0)