Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit adcc13f

Browse filesBrowse files
committed
regular features and refactor line and scatter into positions graphic
1 parent 3a58feb commit adcc13f
Copy full SHA for adcc13f

File tree

9 files changed

+238
-143
lines changed
Filter options

9 files changed

+238
-143
lines changed

‎fastplotlib/graphics/_base.py

Copy file name to clipboardExpand all lines: fastplotlib/graphics/_base.py
+140-24Lines changed: 140 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@
1212

1313
import pygfx
1414

15-
from ._features import GraphicFeature, BufferManager, GraphicFeatureDescriptor, Deleted, PointsDataFeature, ColorFeature, PointsSizesFeature, Name, Offset, Rotation, Visible
16-
15+
from ._features import GraphicFeature, BufferManager, Deleted, VertexPositions, VertexColors, PointsSizesFeature, Name, Offset, Rotation, Visible, UniformColor
16+
from ..utils import parse_cmap_values
1717

1818
HexStr: TypeAlias = str
1919

@@ -38,33 +38,71 @@
3838
]
3939

4040

41-
class BaseGraphic:
41+
class Graphic:
42+
features = {}
43+
44+
@property
45+
def name(self) -> str | None:
46+
"""Graphic name"""
47+
return self._name.value
48+
49+
@name.setter
50+
def name(self, value: str):
51+
self._name.set_value(self, value)
52+
53+
@property
54+
def offset(self) -> tuple:
55+
"""Offset position of the graphic, [x, y, z]"""
56+
return self._offset.value
57+
58+
@offset.setter
59+
def offset(self, value: tuple[float, float, float]):
60+
self._offset.set_value(self, value)
61+
62+
@property
63+
def rotation(self) -> np.ndarray:
64+
"""Orientation of the graphic as a quaternion"""
65+
return self._rotation.value
66+
67+
@rotation.setter
68+
def rotation(self, value: tuple[float, float, float, float]):
69+
self._rotation.set_value(self, value)
70+
71+
@property
72+
def visible(self) -> bool:
73+
"""Whether the graphic is visible"""
74+
return self._visible.value
75+
76+
@visible.setter
77+
def visible(self, value: bool):
78+
self._visible.set_value(self, value)
79+
80+
@property
81+
def deleted(self) -> bool:
82+
"""used to emit an event after the graphic is deleted"""
83+
return self._deleted.value
84+
85+
@deleted.setter
86+
def deleted(self, value: bool):
87+
self._deleted.set_value(self, value)
88+
4289
def __init_subclass__(cls, **kwargs):
43-
"""set the type of the graphic in lower case like "image", "line_collection", etc."""
90+
# set the type of the graphic in lower case like "image", "line_collection", etc.
4491
cls.type = (
4592
cls.__name__.lower()
4693
.replace("graphic", "")
4794
.replace("collection", "_collection")
4895
.replace("stack", "_stack")
4996
)
5097

51-
super().__init_subclass__(**kwargs)
52-
53-
54-
class Graphic(BaseGraphic):
55-
features = {}
56-
57-
def __init_subclass__(cls, **kwargs):
58-
super().__init_subclass__(**kwargs)
98+
# set of all features
5999
cls.features = {*cls.features, "name", "offset", "rotation", "visible", "deleted"}
60-
61-
# graphic feature class attributes
62-
for f in cls.features:
63-
setattr(cls, f, GraphicFeatureDescriptor(f))
100+
super().__init_subclass__(**kwargs)
64101

65102
def __init__(
66103
self,
67104
name: str = None,
105+
offset: tuple[float] = (0., 0., 0.),
68106
metadata: Any = None,
69107
collection_index: int = None,
70108
):
@@ -82,7 +120,6 @@ def __init__(
82120
if (name is not None) and (not isinstance(name, str)):
83121
raise TypeError("Graphic `name` must be of type <str>")
84122

85-
self._name = Name(name)
86123
self.metadata = metadata
87124
self.collection_index = collection_index
88125
self.registered_callbacks = dict()
@@ -91,8 +128,6 @@ def __init__(
91128
# store hex id str of Graphic instance mem location
92129
self._fpl_address: HexStr = hex(id(self))
93130

94-
self._deleted = Deleted(False)
95-
96131
self._plot_area = None
97132

98133
# event handlers
@@ -101,6 +136,13 @@ def __init__(
101136
# maps callbacks to their partials
102137
self._event_handler_wrappers = defaultdict(set)
103138

139+
# all the common features
140+
self._name = Name(name)
141+
self._deleted = Deleted(False)
142+
self._rotation = None # set later when world object is set
143+
self._offset = Offset(offset)
144+
self._visible = Visible(True)
145+
104146
@property
105147
def world_object(self) -> pygfx.WorldObject:
106148
"""Associated pygfx WorldObject. Always returns a proxy, real object cannot be accessed directly."""
@@ -110,6 +152,8 @@ def world_object(self) -> pygfx.WorldObject:
110152
def _set_world_object(self, wo: pygfx.WorldObject):
111153
WORLD_OBJECTS[self._fpl_address] = wo
112154

155+
self._rotation = Rotation(self.world_object.world.rotation[:])
156+
113157
def detach_feature(self, feature: str):
114158
raise NotImplementedError
115159

@@ -203,7 +247,8 @@ def _handle_event(self, callback, event: pygfx.Event):
203247
# for feature events
204248
event._target = self.world_object
205249

206-
callback(event)
250+
with log_exception(f"Error during handling {event.type} event"):
251+
callback(event)
207252

208253
def remove_event_handler(self, callback, *types):
209254
# remove from our record first
@@ -315,6 +360,77 @@ def rotate(self, alpha: float, axis: Literal["x", "y", "z"] = "y"):
315360
class PositionsGraphic(Graphic):
316361
"""Base class for LineGraphic and ScatterGraphic"""
317362

363+
@property
364+
def data(self) -> VertexPositions:
365+
"""Get or set the vertex positions data"""
366+
return self._data
367+
368+
@data.setter
369+
def data(self, value):
370+
self._data[:] = value
371+
372+
@property
373+
def colors(self) -> VertexColors | pygfx.Color:
374+
"""Get or set the colors data"""
375+
if isinstance(self._colors, VertexColors):
376+
return self._colors
377+
378+
elif isinstance(self._colors, UniformColor):
379+
return self._colors.value
380+
381+
@colors.setter
382+
def colors(self, value):
383+
if isinstance(self._colors, VertexColors):
384+
self._colors[:] = value
385+
386+
elif isinstance(self._colors, UniformColor):
387+
self._colors.set_value(self, value)
388+
389+
def __init__(
390+
self,
391+
data: Any,
392+
colors: str | np.ndarray | tuple[float] | list[float] | list[str] = "w",
393+
uniform_colors: bool = False,
394+
alpha: float = 1.0,
395+
cmap: str = None,
396+
cmap_values: np.ndarray = None,
397+
isolated_buffer: bool = True,
398+
*args,
399+
**kwargs,
400+
):
401+
self._data = VertexPositions(data, isolated_buffer=isolated_buffer)
402+
403+
if cmap is not None:
404+
if uniform_colors:
405+
raise TypeError(
406+
"Cannot use cmap if uniform_colors=True"
407+
)
408+
409+
n_datapoints = self._data.value.shape[0]
410+
411+
colors = parse_cmap_values(
412+
n_colors=n_datapoints, cmap_name=cmap, cmap_values=cmap_values
413+
)
414+
415+
if isinstance(colors, VertexColors):
416+
if uniform_colors:
417+
raise TypeError(
418+
"Cannot use vertex colors from existing instance if uniform_colors=True"
419+
)
420+
self._colors = colors
421+
self._colors._shared += 1
422+
else:
423+
if uniform_colors:
424+
self._colors = UniformColor(colors)
425+
else:
426+
self._colors = VertexColors(
427+
colors,
428+
n_colors=self._data.value.shape[0],
429+
alpha=alpha,
430+
)
431+
432+
super().__init__(*args, **kwargs)
433+
318434
def detach_feature(self, feature: str):
319435
if not isinstance(feature, str):
320436
raise TypeError
@@ -323,7 +439,7 @@ def detach_feature(self, feature: str):
323439
if f.shared == 0:
324440
raise BufferError("Cannot detach an independent buffer")
325441

326-
if feature == "colors":
442+
if feature == "colors" and isinstance(feature, VertexColors):
327443
self._colors._buffer = pygfx.Buffer(self._colors.value.copy())
328444
self.world_object.geometry.colors = self._colors.buffer
329445
self._colors._shared -= 1
@@ -338,16 +454,16 @@ def detach_feature(self, feature: str):
338454
self.world_object.geometry.positions = self._sizes.buffer
339455
self._sizes._shared -= 1
340456

341-
def attach_feature(self, feature: PointsDataFeature | ColorFeature | PointsSizesFeature):
342-
if isinstance(feature, PointsDataFeature):
457+
def attach_feature(self, feature: VertexPositions | VertexColors | PointsSizesFeature):
458+
if isinstance(feature, VertexPositions):
343459
# TODO: check if this causes a memory leak
344460
self._data._shared -= 1
345461

346462
self._data = feature
347463
self._data._shared += 1
348464
self.world_object.geometry.positions = self._data.buffer
349465

350-
elif isinstance(feature, ColorFeature):
466+
elif isinstance(feature, VertexColors):
351467
self._colors._shared -= 1
352468

353469
self._colors = feature

‎fastplotlib/graphics/_features/__init__.py

Copy file name to clipboardExpand all lines: fastplotlib/graphics/_features/__init__.py
+2-3Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
from ._colors import ColorFeature#, CmapFeature, ImageCmapFeature, HeatmapCmapFeature
2-
from ._data import PointsDataFeature#, ImageDataFeature, HeatmapDataFeature
1+
from ._positions_graphics import VertexColors, UniformColor, \
2+
VertexPositions # , CmapFeature, ImageCmapFeature, HeatmapCmapFeature
33
from ._sizes import PointsSizesFeature
44
# from ._present import PresentFeature
55
# from ._thickness import ThicknessFeature
66
from ._base import (
77
GraphicFeature,
88
BufferManager,
9-
GraphicFeatureDescriptor,
109
FeatureEvent,
1110
to_gpu_supported_dtype,
1211
)

‎fastplotlib/graphics/_features/_base.py

Copy file name to clipboardExpand all lines: fastplotlib/graphics/_features/_base.py
-25Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
from abc import abstractmethod
2-
from inspect import getfullargspec
31
from warnings import warn
42
from typing import Any, Literal
53

@@ -292,26 +290,3 @@ def _emit_event(self, type: str, key, value):
292290
def __repr__(self):
293291
return f"{self.__class__.__name__} buffer data:\n" \
294292
f"{self.value.__repr__()}"
295-
296-
297-
class GraphicFeatureDescriptor:
298-
def __init__(self, feature_name):
299-
self.feature_name = feature_name
300-
301-
def _get_feature(self, instance):
302-
feature: GraphicFeature = getattr(instance, f"_{self.feature_name}")
303-
return feature
304-
305-
def __get__(self, graphic, owner):
306-
f = self._get_feature(graphic)
307-
if isinstance(f, BufferManager):
308-
return f
309-
else:
310-
return f.value
311-
312-
def __set__(self, graphic, value):
313-
feature = self._get_feature(graphic)
314-
if isinstance(feature, BufferManager):
315-
feature[:] = value
316-
else:
317-
feature.set_value(graphic, value)

‎fastplotlib/graphics/_features/_common.py

Copy file name to clipboardExpand all lines: fastplotlib/graphics/_features/_common.py
+1-1Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ def __init__(self, value: str):
1111
def value(self) -> str:
1212
return self._value
1313

14-
def set_value(self, graphic, value: bool):
14+
def set_value(self, graphic, value: str):
1515
if not isinstance(value, str):
1616
raise TypeError("`Graphic` name must be of type <str>")
1717

‎fastplotlib/graphics/_features/_data.py

Copy file name to clipboardExpand all lines: fastplotlib/graphics/_features/_data.py
-52Lines changed: 0 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -1,55 +1,3 @@
1-
from typing import *
2-
3-
import numpy as np
4-
5-
import pygfx
6-
7-
from ._base import (
8-
BufferManager,
9-
FeatureEvent,
10-
to_gpu_supported_dtype,
11-
)
12-
13-
14-
class PointsDataFeature(BufferManager):
15-
"""
16-
Access to the vertex buffer data shown in the graphic.
17-
Supports fancy indexing if the data array also supports it.
18-
"""
19-
20-
def __init__(self, data: Any, isolated_buffer: bool = True):
21-
data = self._fix_data(data)
22-
super().__init__(data, isolated_buffer=isolated_buffer)
23-
24-
def _fix_data(self, data):
25-
# data = to_gpu_supported_dtype(data)
26-
27-
if data.ndim == 1:
28-
# if user provides a 1D array, assume these are y-values
29-
data = np.column_stack([np.arange(data.size, dtype=data.dtype), data])
30-
31-
if data.shape[1] != 3:
32-
if data.shape[1] != 2:
33-
raise ValueError(f"Must pass 1D, 2D or 3D data")
34-
35-
# zeros for z
36-
zs = np.zeros(data.shape[0], dtype=data.dtype)
37-
38-
# column stack [x, y, z] to make data of shape [n_points, 3]
39-
data = np.column_stack([data[:, 0], data[:, 1], zs])
40-
41-
return to_gpu_supported_dtype(data)
42-
43-
def __setitem__(self, key: int | slice | range | np.ndarray[int | bool] | tuple[slice, ...] | tuple[range, ...], value):
44-
# directly use the key to slice the buffer
45-
self.buffer.data[key] = value
46-
47-
# _update_range handles parsing the key to
48-
# determine offset and size for GPU upload
49-
self._update_range(key)
50-
51-
self._emit_event("data", key, value)
52-
531
#
542
# class ImageDataFeature(GraphicFeatureIndexable):
553
# """

0 commit comments

Comments
0 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.