Skip to content

Commit 9b41cb8

Browse files
committed
bases for graphic collection, indexable along with features, all implemented with line collection
1 parent cbff3f4 commit 9b41cb8

10 files changed

Lines changed: 676 additions & 49 deletions

File tree

examples/collection.ipynb

Lines changed: 377 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
from typing import *
2+
3+
import numpy as np
4+
5+
from pygfx import Group
6+
7+
from ._base import BaseGraphic, Graphic
8+
from .features._base import GraphicFeature, GraphicFeatureIndexable, cleanup_slice
9+
10+
11+
class GraphicCollection(BaseGraphic):
12+
"""Graphic Collection base class"""
13+
def __init__(self, name: str = None):
14+
self.name = name
15+
16+
@property
17+
def world_object(self) -> Group:
18+
return self._world_object
19+
20+
@property
21+
def items(self) -> Tuple[Graphic]:
22+
"""Get the Graphic instances within this collection"""
23+
return tuple(self._items)
24+
25+
def add_graphic(self, graphic: Graphic):
26+
"""Add a graphic to the collection"""
27+
self._items.append(graphic)
28+
self.world_object.add(graphic)
29+
30+
def remove_graphic(self, graphic: Graphic):
31+
"""Remove a graphic from the collection"""
32+
self._items.remove(graphic)
33+
self.world_object.remove(graphic)
34+
35+
def __getitem__(self, key):
36+
if isinstance(key, int):
37+
key = [key]
38+
39+
if isinstance(key, slice):
40+
key = cleanup_slice(key, upper_bound=len(self))
41+
selection_indices = range(key.start, key.stop, key.step)
42+
selection = self._items[key]
43+
44+
# fancy-ish indexing
45+
elif isinstance(key, (tuple, list)):
46+
selection = list()
47+
for ix in key:
48+
selection.append(self._items[ix])
49+
50+
selection_indices = key
51+
else:
52+
raise TypeError("Graphic Collection indexing supports int, slice, tuple or list of integers")
53+
return CollectionIndexer(
54+
parent=self,
55+
selection=selection,
56+
selection_indices=selection_indices
57+
)
58+
59+
60+
class CollectionIndexer:
61+
"""Collection Indexer"""
62+
def __init__(
63+
self,
64+
parent: GraphicCollection,
65+
selection: List[Graphic],
66+
selection_indices: Union[list, range],
67+
):
68+
"""
69+
70+
Parameters
71+
----------
72+
parent
73+
selection
74+
selection_indices: Union[list, range]
75+
"""
76+
self._selection = selection
77+
self._selection_indices = selection_indices
78+
79+
for attr_name in self._selection[0].__dict__.keys():
80+
attr = getattr(self._selection[0], attr_name)
81+
if isinstance(attr, GraphicFeature):
82+
collection_feature = CollectionFeature(
83+
parent,
84+
self._selection,
85+
selection_indices=selection_indices,
86+
feature=attr_name
87+
)
88+
collection_feature.__doc__ = f"indexable {attr_name} feature for collection"
89+
setattr(self, attr_name, collection_feature)
90+
91+
def __setattr__(self, key, value):
92+
if hasattr(self, key):
93+
attr = getattr(self, key)
94+
if isinstance(attr, CollectionFeature):
95+
attr._set(value)
96+
return
97+
98+
super().__setattr__(key, value)
99+
100+
def __repr__(self):
101+
return f"{self.__class__.__name__} @ {hex(id(self))}\n" \
102+
f"Collection of <{len(self._selection)}> {self._selection[0].__class__.__name__}"
103+
104+
105+
class CollectionFeature:
106+
"""Collection Feature"""
107+
def __init__(
108+
self,
109+
parent: GraphicCollection,
110+
selection: List[Graphic],
111+
selection_indices, feature: str
112+
):
113+
self._selection = selection
114+
self._selection_indices = selection_indices
115+
self._feature = feature
116+
117+
self._feature_instances: List[GraphicFeature] = list()
118+
119+
for graphic in self._selection:
120+
fi = getattr(graphic, self._feature)
121+
self._feature_instances.append(fi)
122+
123+
if isinstance(fi, GraphicFeatureIndexable):
124+
self._indexable = True
125+
else:
126+
self._indexable = False
127+
128+
def _set(self, value):
129+
self[:] = value
130+
131+
def __getitem__(self, item):
132+
# only for indexable graphic features
133+
return [fi[item] for fi in self._feature_instances]
134+
135+
def __setitem__(self, key, value):
136+
if self._indexable:
137+
for fi in self._feature_instances:
138+
fi[key] = value
139+
140+
else:
141+
for fi in self._feature_instances:
142+
fi._set(value)
143+
key = None
144+
145+
def add_event_handler(self, handler: callable):
146+
for fi in self._feature_instances:
147+
fi.add_event_handler(handler)
148+
149+
def remove_event_handler(self, handler: callable):
150+
for fi in self._feature_instances:
151+
fi.remove_event_handler(handler)
152+
153+
def __repr__(self):
154+
return f"Collection feature for: <{self._feature}>"

fastplotlib/graphics/_graphic_attribute.py

Lines changed: 0 additions & 3 deletions
This file was deleted.

fastplotlib/graphics/features/_base.py

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,26 @@ def __repr__(self):
2828

2929

3030
class GraphicFeature(ABC):
31-
def __init__(self, parent, data: Any):
31+
def __init__(self, parent, data: Any, collection_index: int = None):
32+
"""
33+
34+
Parameters
35+
----------
36+
parent
37+
38+
data: Any
39+
40+
collection_index: int
41+
if part of a collection, index of this graphic within the collection
42+
43+
"""
3244
self._parent = parent
3345
if isinstance(data, np.ndarray):
3446
data = data.astype(np.float32)
3547

3648
self._data = data
49+
50+
self._collection_index = collection_index
3751
self._event_handlers = list()
3852

3953
@property
@@ -71,6 +85,12 @@ def add_event_handler(self, handler: callable):
7185

7286
self._event_handlers.append(handler)
7387

88+
def remove_event_handler(self, handler: callable):
89+
if handler not in self._event_handlers:
90+
raise KeyError(f"event handler {handler} not registered.")
91+
92+
self._event_handlers.pop(handler)
93+
7494
#TODO: maybe this can be implemented right here in the base class
7595
@abstractmethod
7696
def _feature_changed(self, key: Union[int, slice, Tuple[slice]], new_data: Any):

fastplotlib/graphics/features/_colors.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ def __getitem__(self, item):
1616
def __repr__(self):
1717
return repr(self._buffer.data)
1818

19-
def __init__(self, parent, colors, n_colors: int, alpha: float = 1.0):
19+
def __init__(self, parent, colors, n_colors: int, alpha: float = 1.0, collection_index: int = None):
2020
"""
2121
ColorFeature
2222
@@ -91,7 +91,7 @@ def __init__(self, parent, colors, n_colors: int, alpha: float = 1.0):
9191
if alpha != 1.0:
9292
data[:, -1] = alpha
9393

94-
super(ColorFeature, self).__init__(parent, data)
94+
super(ColorFeature, self).__init__(parent, data, collection_index=collection_index)
9595

9696
def __setitem__(self, key, value):
9797
# parse numerical slice indices
@@ -184,6 +184,7 @@ def _feature_changed(self, key, new_data):
184184

185185
pick_info = {
186186
"index": indices,
187+
"collection-index": self._collection_index,
187188
"world_object": self._parent.world_object,
188189
"new_data": new_data,
189190
}

fastplotlib/graphics/features/_data.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@ class PointsDataFeature(GraphicFeatureIndexable):
1818
Access to the vertex buffer data shown in the graphic.
1919
Supports fancy indexing if the data array also supports it.
2020
"""
21-
def __init__(self, parent, data: Any):
21+
def __init__(self, parent, data: Any, collection_index: int = None):
2222
data = self._fix_data(data, parent)
23-
super(PointsDataFeature, self).__init__(parent, data)
23+
super(PointsDataFeature, self).__init__(parent, data, collection_index=collection_index)
2424

2525
@property
2626
def _buffer(self) -> Buffer:
@@ -83,6 +83,7 @@ def _feature_changed(self, key, new_data):
8383

8484
pick_info = {
8585
"index": indices,
86+
"collection-index": self._collection_index,
8687
"world_object": self._parent.world_object,
8788
"new_data": new_data
8889
}

fastplotlib/graphics/features/_present.py

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
from ._base import GraphicFeature, FeatureEvent
2-
from pygfx import Scene
2+
from pygfx import Scene, Group
33

44

55
class PresentFeature(GraphicFeature):
@@ -8,14 +8,17 @@ class PresentFeature(GraphicFeature):
88
Useful for computing bounding boxes from the Scene to only include graphics
99
that are present
1010
"""
11-
def __init__(self, parent, present: bool = True):
11+
def __init__(self, parent, present: bool = True, collection_index: int = False):
1212
self._scene = None
13-
super(PresentFeature, self).__init__(parent, present)
13+
super(PresentFeature, self).__init__(parent, present, collection_index)
1414

1515
def _set(self, present: bool):
1616
i = 0
17-
while not isinstance(self._scene, Scene):
18-
self._scene = self._parent.world_object.parent
17+
wo = self._parent.world_object
18+
while not isinstance(self._scene, (Group, Scene)):
19+
wo_parent = wo.parent
20+
self._scene = wo_parent
21+
wo = wo_parent
1922
i += 1
2023

2124
if i > 100:
@@ -42,6 +45,7 @@ def _feature_changed(self, key, new_data):
4245

4346
pick_info = {
4447
"index": None,
48+
"collection-index": self._collection_index,
4549
"world_object": self._parent.world_object,
4650
"new_data": new_data
4751
}

fastplotlib/graphics/line.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,8 @@ def __init__(
1515
colors: Union[str, np.ndarray, Iterable] = "w",
1616
alpha: float = 1.0,
1717
cmap: str = None,
18-
z_position: float = 0.0,
18+
z_position: float = None,
19+
collection_index: int = None,
1920
*args,
2021
**kwargs
2122
):
@@ -52,12 +53,19 @@ def __init__(
5253
5354
"""
5455

55-
self.data = PointsDataFeature(self, data)
56+
self.data = PointsDataFeature(self, data, collection_index=collection_index)
5657

5758
if cmap is not None:
5859
colors = get_colors(n_colors=self.data.feature_data.shape[0], cmap=cmap, alpha=alpha)
5960

60-
self.colors = ColorFeature(self, colors, n_colors=self.data.feature_data.shape[0], alpha=alpha)
61+
self.colors = ColorFeature(
62+
self,
63+
colors,
64+
n_colors=self.data.feature_data.shape[0],
65+
alpha=alpha,
66+
collection_index=collection_index
67+
)
68+
6169
self.cmap = CmapFeature(self, self.colors.feature_data)
6270

6371
super(LineGraphic, self).__init__(*args, **kwargs)
@@ -73,4 +81,5 @@ def __init__(
7381
material=material(thickness=size, vertex_colors=True)
7482
)
7583

76-
self.world_object.position.z = z_position
84+
if z_position is not None:
85+
self.world_object.position.z = z_position

0 commit comments

Comments
 (0)