-
Notifications
You must be signed in to change notification settings - Fork 50
Graphic features refactor #511
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
Conversation
@almarklein if you have time it would be great to get your thoughts, I've summarized what this does here since it's a massive diff: The majority of this PR implements this: #389 (comment) I decided to go for explict properties instead of descriptors because it shows up better for typing and docstring popups. API comparisionBasic features (no buffer)# before this PR, setting and getting a feature value
>>> line_graphic.thickness = 3.5
>>> line_graphic.thickness()
3.5
# this PR, getter & setter are symmetric when possible
>>> line_graphic.thickness = 3.5
>>> line_graphic.thickness
3.5 Sliceable features (manages a buffer)# setting and getting the "entire value"
>>> line_graphic.colors = "r"
>>> line_graphic.colors
<__repr__ prints array>
# this PR, similar but value can be directly access if desired
>>> line_graphic.colors = "r"
>>> line_graphic.colors
<__repr__ prints array>
>> line_graphic.colors.value
<returns the actual array> Sharing buffers 😄 ! # let's say you have a line with some vertex colors
>>> line1.colors
<returns array for red>
>>> line2 = subplot.add_line(data, colors=line1.colors) # share buffer :D !
# modify colors
>>> line2.colors[50:] = "b"
# change of colors is reflected in BOTH graphics :D The implementation is very simple, for example in class PositionsGraphic(Graphic):
def __init__(self, data, colors, ...):
if isinstance(data, VertexPositions):
self._data = data
else:
self._data = VertexPositions(data, isolated_buffer=isolated_buffer) As we can see, it actually shares the Events# before this PR
line_graphic.thickness.add_event_handler(<callable>)
# this PR
line_graphic.add_event_handler(<callable>, "thickness")
# can also use decorators :D
@line_graphic.add_event_handler("thickness")
def thickness_changed_handler(ev):
pass
# exactly the same for features that subclass BufferManager
@line_graphic.add_event_handler("colors", "data")
def line_colors_or_data_changed(ev):
if ev.type == "colors":
print(
f"colors changed for graphic: <{ev.graphic}>
F"with slice: {ev.info['key']} and new values: {ev.info['value']}"
)
if ev.type == "data":
print(
f"data changed for graphic: <{ev.graphic}>
F"with slice: {ev.info['key']} and new values: {ev.info['value']}"
)
# rendering engine events added in exactly the same way
@image_graphic.add_event_handler("click")
def image_clicked(ev):
ev.graphic # the graphic that caused the event The event adding and handling is managed here, mostly copy-pasted from pygfx. fastplotlib/fastplotlib/graphics/_base.py Lines 209 to 332 in 47cecfa
We handle the events in a wrapper method which appends the Before this PR, the event info for "graphic features" was a mess that we probably don't need to dig into 😆 . Now every
The "info" dict for all "simple" features has one key: "value" - the user passed new value. Example if
|
dict key | value type | value description |
---|---|---|
key | int | slice | np.ndarray[int | bool] | tuple[slice, ...] |
key at which colors were indexed/sliced |
value | np.ndarray |
new color values for points that were changed, shape is [n_points_changed, RGBA] |
user_value | str | np.ndarray | tuple[float] | list[float] | list[str] |
user input value that was parsed into the RGBA array |
data (points data, line or scatter)
dict key | value type | value description |
---|---|---|
key | int | slice | np.ndarray[int | bool] | tuple[slice, ...] |
key at which vertex positions data were indexed/sliced |
value | np.ndarray | float | list[float] |
new data values for points that were changed, shape depends on the indices that were set |
Selectors
Adding event handlers to a selector is identical to how they're added for "regular" graphics:
# one of the selectors will change the line colors when it moves
@selector.add_event_handler("selection")
def set_color_at_index(ev):
# changes the color at the index where the slider is
ix = ev.get_selected_index()
g = ev.graphic.parent
g.colors[ix] = "green"
Selector feature event structure
LinearSelector
"selection"
additional event attributes:
attribute | type | description |
---|---|---|
get_selected_index | callable | returns indices under the selector |
info dict:
dict key | value type | value description |
---|---|---|
value | np.ndarray | new x or y value of selection |
LinearRegioonSelector
additional event attributes:
attribute | type | description |
---|---|---|
get_selected_indices | callable | returns indices under the selector |
get_selected_data | callable | returns data under the selector |
info dict:
dict key | value type | value description |
---|---|---|
value | np.ndarray | new [min, max] of selection |
selector
is the LinearSelector
or LinearRegionSelector
that the feature manages, and it has the get_selected_index
, or get_selected_indices
and get_selected_data
methods for LinearRegionSelector
Graphics
Some major refactor to graphics not mentioned above:
- line and scatter graphics now inherit from a common
PositionsGraphic
that handles things that are common between lines and scatters (data, colors, etc.)
Bug when adding an existing graphic to another plot
yields
also, even just trying to access a graphic
I think we are passing around the weakrefs everywhere and then we end up trying to weakref a weakref |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@almarklein if you have time it would be great to get your thoughts, I've summarized what this does here since it's a massive diff
The description you posted sounds very good!
Sharing buffers
Hooray! Feels nice and simple.
Adding event handlers to a selector is identical to how they're added for "regular" graphics:
Nice, less stuff to learn for users.
I also glanced over the diff. Looks nice from a birds-eye view. Made a few minor comments/suggestions.
failing because camera stuff was recently merged in |
You mean pygfx/pygfx#778? I was under the impression that that change was backwards compatible. |
It's specific to how we allow changing the controller of an existing plot area: https://github.com/fastplotlib/fastplotlib/actions/runs/9492712816/job/26160279159?pr=511#step:9:895 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
YAYAY!!!
Everything passing on our ends locally, merging. Need to wait for pygfx to fix pygfx/pygfx#790 and for things to catch up with numpy v2 before all our CI works again. |
WIP
closes #389, closes #489, closes #499, closes #456, closes #369, closes #322, closes #316, closes #309, closes #283, closes #174, closes #147, closes #126
closes #507, closes #449, closes #123, closes #214
Remaining TODOs
ImageGraphic
GraphicCollection
GraphicCollection
implementation:line_stack.name = [f"line-{i}" for i in range(len(line_stack))]
TextGraphic
with the feature changes.Summary
Graphic Features
GraphicFeature
. #389 for the original inspiration of this refactor, thanks @tlambert03 !!GraphicFeatures
, basically every graphic property which makes sense to be evented is aGraphicFeature
, see Scope of graphic features #489. These features are common among all graphics:Name
:str
, an optional name for the graphicOffset
:np.ndarray, [x, y, z]
, offset from the origin, NOT related to the data shown in the graphic, renameposition_x
,position_y
,position_z
to offsets #499LineStack
, lines are stacked along the y-axis w.r.t. to the rendering engine. This is done by incrementing theOffset
. We DO NOT add to the actual data buffer that the graphic uses!Rotation
:np.ndarray
, quaternionVisible
:bool
Deleted
:bool
- indicates if a aPlotArea
has requested the deletion of a graphic. This is useful because you can add event handlers for"deleted"
, and this event handler can perform any necessary cleanup (disconnted events, etc.) so that the RAM for the graphic (i.e. the buffers) can be cleared.ImageCmap
. It was just cleaner this way and makes more sense because RGBA images have no cmap, but they can have vmin and vmax.Graphics
do not keepcollection_index
as an attribute.Graphic Features that are sliceable, subclasses
BufferManager
Graphic.attach_feature("feature_name")
andGraphic.detach_feature("feature_name")
BufferManager.__init__
manages whether the user's passed array should be isolated or not, the default is to create an isolated buffer. Makes much more sense to manage it here rather than in eachSubGraphic.__init__
. need to improve how we handle buffers #147float32
, Choose dtype for isolated buffer #369Selector tools
WorldObjects
are used to get the position/region under the selection. Since the offsets match those of the parentGraphic
, we don't need to do anything more."selection"
feature now returns value(s) in data space, NOT INDICES. This makes more sense since for example, you would want to set or get the bounds of aLinearRegionSelector
in terms of actual(data xmin, data xmax)
. Likewise forLinearSelector
.LinearRegionSelector
behaves more intuitively when at/close to the limits. It is not actually possible to "smash" the selector right up against the limits and it actually touches the limit, whereas before if the delta was a bit larger it would be stuck a few pixels away from the limit.API comparision
Basic features (no buffer)
Sliceable features (manages a buffer)
Sharing buffers 😄 !
The implementation is very simple, for example in
PositionsGraphic.__init__
:As we can see, it actually shares the
BufferManager
(i.e. feature) instance. This is simpler and makes it so changing the buffer causes events to be emitted even if it's from another graphic. In the above example, if an event handler is added toline1
, i.e.line1.add_event_handler(<callable>, "colors")
, then changing the colors ofline2
would also trigger the event which I assume is what you would want sinceline1
colors have also changed.Events
The event adding and handling is managed here:
fastplotlib/fastplotlib/graphics/_base.py
Lines 173 to 278 in 1d6adc6
Before this PR, the event info was a mess that we probably don't need to dig into 😆 . Now every
FeatureEvent
inherits frompygfx.Event
and has the following attributes:str
Graphic
dict
pygfx.WorldObject
float
The "info" dict for all "simple" features has one key, "value", which is the user passed new value. Example if
line_graphic.name
is changed, then the info dict will be:{"value": "new_name_str"}
BufferManager
example info dicts:colors (vertex colors, line or scatter)
int
|slice
|np.ndarray[int | bool]
|tuple[slice, ...]
np.ndarray
str
|np.ndarray
|tuple[float]
|list[float]
|list[str]
data (points data, line or scatter)
int
|slice
|np.ndarray[int | bool]
|tuple[slice, ...]
np.ndarray
|float
|list[float]
Selectors
Adding event handlers to a selector is identical to how they're added for "regular" graphics:
Selector feature event structure
LinearSelector
"selection"
additional event attributes:
info dict:
LinearRegioonSelector
additional event attributes:
info dict:
[min, max]
of selectionThis is how it's implemented:
fastplotlib/fastplotlib/graphics/_features/_selection_features.py
Lines 57 to 76 in 1d6adc6
selector
is theLinearSelector
(in this case) orLinearRegionSelector
that the feature manages, and it has theget_selected_index
, orget_selected_indices
andget_selected_data
methods forLinearRegionSelector
Graphics
Some major refactor to graphics not mentioned above:
PositionsGraphic
that handles things that are common between lines and scatters (data, colors, etc.)