GeezerVisonThe World Wide Web, restored to 800 pixels of glory. | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Home|About|Gaming cabinet|Archives|How it works|Starter sites | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
GeezerVison is showing github.com — Graphic features refactor by kushalkolar · Pull Request #511 · fastplotlib/fastplotlib Conversation
|
| @property | |
| def event_handlers(self) -> list[tuple[str, callable, ...]]: | |
| """ | |
| Registered event handlers. Read-only use ``add_event_handler()`` | |
| and ``remove_event_handler()`` to manage callbacks | |
| """ | |
| return list(self._event_handlers.items()) | |
| def add_event_handler(self, *args): | |
| """ | |
| Register an event handler. | |
| Parameters | |
| ---------- | |
| callback: callable, the first argument | |
| Event handler, must accept a single event argument | |
| *types: list of strings | |
| A list of event types, ex: "click", "data", "colors", "pointer_down" | |
| For the available renderer event types, see | |
| https://jupyter-rfb.readthedocs.io/en/stable/events.html | |
| All feature support events, i.e. ``graphic.features`` will give a set of | |
| all features that are evented | |
| Can also be used as a decorator. | |
| Example | |
| ------- | |
| .. code-block:: py | |
| def my_handler(event): | |
| print(event) | |
| graphic.add_event_handler(my_handler, "pointer_up", "pointer_down") | |
| Decorator usage example: | |
| .. code-block:: py | |
| @graphic.add_event_handler("click") | |
| def my_handler(event): | |
| print(event) | |
| """ | |
| decorating = not callable(args[0]) | |
| callback = None if decorating else args[0] | |
| types = args if decorating else args[1:] | |
| def decorator(_callback): | |
| _callback_injector = partial(self._handle_event, _callback) # adds graphic instance as attribute | |
| for t in types: | |
| # add to our record | |
| self._event_handlers[t].add(_callback) | |
| if t in self.features: | |
| # fpl feature event | |
| feature = getattr(self, f"_{t}") | |
| feature.add_event_handler(_callback_injector) | |
| else: | |
| # wrap pygfx event | |
| self.world_object._event_handlers[t].add(_callback_injector) | |
| # keep track of the partial too | |
| self._event_handler_wrappers[t].add((_callback, _callback_injector)) | |
| return _callback | |
| if decorating: | |
| return decorator | |
| return decorator(callback) | |
| def _handle_event(self, callback, event: pygfx.Event): | |
| """Wrap pygfx event to add graphic to pick_info""" | |
| event.graphic = self | |
| if event.type in self.features: | |
| # for feature events | |
| event._target = self.world_object | |
| with log_exception(f"Error during handling {event.type} event"): | |
| callback(event) | |
| def remove_event_handler(self, callback, *types): | |
| # remove from our record first | |
| for t in types: | |
| for wrapper_map in self._event_handler_wrappers[t]: | |
| # TODO: not sure if we can handle this mapping in a better way | |
| if wrapper_map[0] == callback: | |
| wrapper = wrapper_map[1] | |
| self._event_handler_wrappers[t].remove(wrapper_map) | |
| break | |
| else: | |
| raise KeyError(f"event type: {t} with callback: {callback} is not registered") | |
| self._event_handlers[t].remove(callback) | |
| # remove callback wrapper from world object if pygfx event | |
| if t in PYGFX_EVENTS: | |
| print("pygfx event") | |
| print(wrapper) | |
| self.world_object.remove_event_handler(wrapper, t) | |
| else: | |
| feature = getattr(self, f"_{t}") | |
| feature.remove_event_handler(wrapper) |
Before this PR, the event info was a mess that we probably don't need to dig into 😆 . Now every FeatureEvent inherits from pygfx.Event and has the following attributes:
| attribute | type | description |
|---|---|---|
| type | str |
name of the event, ex: "colors" |
| graphic | Graphic |
graphic instance that the event is from |
| info | dict |
event info dictionary (see below) |
| target | pygfx.WorldObject |
pygfx rendering engine object for the graphic |
| time_stamp | float |
time when the event occured, in ms |
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)
| 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 |
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 |
This is how it's implemented:
fastplotlib/fastplotlib/graphics/_features/_selection_features.py
Lines 57 to 76 in 1d6adc6
| def set_value(self, selector, value: float): | |
| # clip value between limits | |
| value = np.clip(value, self._limits[0], self._limits[1]) | |
| # set position | |
| if self._axis == "x": | |
| dim = 0 | |
| elif self._axis == "y": | |
| dim = 1 | |
| for edge in selector._edges: | |
| edge.geometry.positions.data[:, dim] = value | |
| edge.geometry.positions.update_range() | |
| self._value = value | |
| event = FeatureEvent("selection", {"value": value}) | |
| event.get_selected_index = selector.get_selected_index | |
| self._call_event_handlers(event) |
selector is the LinearSelector (in this case) or LinearRegionSelector that the feature manages, and it has the get_selected_index, or get_selected_indices and get_selected_data methods for LinearRegionSelector
Some major refactor to graphics not mentioned above:
PositionsGraphic that handles things that are common between lines and scatters (data, colors, etc.)
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
@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 |
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
Some major refactor to graphics not mentioned above:
PositionsGraphic that handles things that are common between lines and scatters (data, colors, etc.)
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
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 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem 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.
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
|
failing because camera stuff was recently merged in |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
You mean pygfx/pygfx#778? I was under the impression that that change was backwards compatible. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
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 |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
The reason will be displayed to describe this comment to others. Learn more.
YAYAY!!!
Sorry, something went wrong.
There was an error while loading. Please reload this page.
|
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. |
Sorry, something went wrong.
There was an error while loading. Please reload this page.
There was an error while loading. Please reload this page.
Successfully merging this pull request may close these issues.
probably get rid ofCollectionIndexer and CollectionFeature in favor of numpy arrays for graphic collections
Cleanup GraphicFeatures events pick_info
need to improve how we handle buffers
Look into sharing storage buffers
GraphicCollection feature data is not accessible
There was an error while loading. Please reload this page.