-
Notifications
You must be signed in to change notification settings - Fork 64
Expand file tree
/
Copy path_linear.py
More file actions
303 lines (241 loc) · 9.5 KB
/
_linear.py
File metadata and controls
303 lines (241 loc) · 9.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
import math
from numbers import Real
from typing import Sequence
import numpy as np
import pygfx
from ...utils.enums import RenderQueue
from .._base import Graphic
from .._collection_base import GraphicCollection
from ..features._selection_features import LinearSelectionFeature
from ._base_selector import BaseSelector, MoveInfo
class LinearSelector(BaseSelector):
_features = {"selection": LinearSelectionFeature}
@property
def parent(self) -> Graphic:
return self._parent
@property
def selection(self) -> float:
"""
x or y value of selector's current position
"""
return self._selection.value
@selection.setter
def selection(self, value: int):
graphic = self._parent
if isinstance(graphic, GraphicCollection):
pass
self._selection.set_value(self, value)
@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
@property
def edge_color(self) -> pygfx.Color:
"""Returns the color of the linear selector."""
return self._edge_color
@edge_color.setter
def edge_color(self, color: str | Sequence[float]):
"""
Set the color of the linear selector.
Parameters
----------
color : str | Sequence[float]
String or sequence of floats that gets converted into a ``pygfx.Color`` object.
"""
color = pygfx.Color(color)
# only want to change inner line color
self._edges[0].material.color = color
self._original_colors[self._edges[0]] = color
self._edge_color = color
# TODO: make `selection` arg in graphics data space not world space
def __init__(
self,
selection: float,
limits: Sequence[float],
axis: str = "x",
parent: Graphic = None,
edge_color: str | Sequence[float] | np.ndarray = "yellow",
thickness: float = 1.0,
arrow_keys_modifier: str = "Shift",
extra_width: float = 14.0,
name: str = None,
):
"""
Create a horizontal or vertical line that can be used to select a value along an axis.
Parameters
----------
selection: int
initial x or y selected position for the selector, in data space
limits: (int, int)
(min, max) limits along the x or y-axis for the selector, in data space
axis: str, default "x"
"x" | "y", the axis along which the selector can move
parent: Graphic
parent graphic for this LinearSelector
arrow_keys_modifier: str
modifier key that must be pressed to initiate movement using arrow keys, must be one of:
"Control", "Shift", "Alt" or ``None``. Double-click the selector first to enable the
arrow key movements, or set the attribute ``arrow_key_events_enabled = True``
thickness: float, default 2.5
thickness of the selector
edge_color: str | tuple | np.ndarray, default "w"
color of the selector
extra_width: float, default 14.0
the width around the selector which is responsive to mouse events, in logical pixels
name: str, optional
name of linear selector
"""
self._fill_color = None
self._edge_color = pygfx.Color(edge_color)
self._vertex_color = None
if len(limits) != 2:
raise ValueError("limits must be a tuple of 2 integers, i.e. (int, int)")
self._limits = np.asarray(limits)
if axis == "x":
xs = np.array([selection, selection], dtype=np.float32)
ys = np.array([0, 1], dtype=np.float32)
zs = np.zeros(2, dtype=np.float32)
elif axis == "y":
xs = np.array([0, 1], dtype=np.float32)
ys = np.array([selection, selection], dtype=np.float32)
zs = np.zeros(2, dtype=np.float32)
else:
raise ValueError("`axis` must be one of 'x' | 'y'")
line_data = np.column_stack([xs, ys, zs])
material = pygfx.LineInfiniteSegmentMaterial
line_inner = pygfx.Line(
# self.data.feature_data because data is a Buffer
geometry=pygfx.Geometry(positions=line_data),
material=material(
thickness=thickness,
color=edge_color,
alpha_mode="blend",
aa=True,
render_queue=RenderQueue.selector,
depth_test=False,
depth_write=False,
pick_write=True,
),
)
line_outer = pygfx.Line(
geometry=line_inner.geometry,
material=material(
thickness=thickness + extra_width,
color=pygfx.Color([0, 0, 0]),
opacity=0,
alpha_mode="blend",
aa=True,
render_queue=RenderQueue.selector,
depth_test=False,
depth_write=False,
pick_write=True,
),
)
# Inner line goes on top of the outer line
line_inner.render_order = 1
world_object = pygfx.Group()
world_object.add(line_outer)
world_object.add(line_inner)
if parent is None:
offset = (0, 0, 0)
else:
if axis == "x":
offset = (parent.offset[0], 0, 0)
elif axis == "y":
offset = (0, parent.offset[1], 0)
# init base selector
BaseSelector.__init__(
self,
edges=(line_inner,),
outer_edges=(line_outer,),
hover_responsive=(line_inner,),
arrow_keys_modifier=arrow_keys_modifier,
axis=axis,
parent=parent,
name=name,
offset=offset,
)
self._set_world_object(world_object)
self._selection = LinearSelectionFeature(
axis=axis, value=selection, limits=self._limits
)
if self._parent is not None:
self.selection = selection
else:
self._selection.set_value(self, selection)
def get_selected_index(self, graphic: Graphic = None) -> int | list[int]:
"""
Data index the selector is currently at w.r.t. the Graphic data.
With LineGraphic data, the geometry x or y position is not always the data position, for example if plotting
data using np.linspace. Use this to get the data index of the selector.
Parameters
----------
graphic: Graphic, optional
Graphic to get the selected data index from. Default is the parent graphic associated to the selector.
Returns
-------
int or List[int]
data index the selector is currently at, list of ``int`` if a Collection
"""
source = self._get_source(graphic)
if isinstance(source, GraphicCollection):
ixs = list()
for g in source.graphics:
ixs.append(self._get_selected_index(g))
return ixs
return self._get_selected_index(source)
def _get_selected_index(self, graphic):
# the array to search for the closest value along that axis
if self.axis == "x":
data = graphic.data[:, 0]
elif self.axis == "y":
data = graphic.data[:, 1]
if (
"Line" in graphic.__class__.__name__
or "Scatter" in graphic.__class__.__name__
):
# we want to find the index of the data closest to the selector position
find_value = self.selection
# get closest data index to the world space position of the selector
idx = np.searchsorted(data, find_value, side="left")
if idx > 0 and (
idx == len(data)
or math.fabs(find_value - data[idx - 1])
< math.fabs(find_value - data[idx])
):
return round(idx - 1)
else:
return round(idx)
if "Image" in graphic.__class__.__name__:
# indices map directly to grid geometry for image data buffer
index = self.selection
shape = graphic.data[:].shape
if self.axis == "x":
# assume selecting columns
upper_bound = shape[1] - 1
elif self.axis == "y":
# assume selecting rows
upper_bound = shape[0] - 1
return min(round(index), upper_bound)
def _move_graphic(self, move_info: MoveInfo):
"""
Moves the graphic
Parameters
----------
delta: np.ndarray
delta in world space
"""
# If this the first move in this drag, store initial selection
if move_info.start_selection is None:
move_info.start_selection = self.selection
delta = move_info.delta[0] if self.axis == "x" else move_info.delta[1]
self.selection = move_info.start_selection + delta