GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Creating types for all events, canvas events and fastplotlib graphic feature events · Issue #852 · fastplotlib/fastplotlib

Creating types for all events, canvas events and fastplotlib graphic feature events #852

New issue
Copy link
New issue
Copy link
Open
Open
Creating types for all events, canvas events and fastplotlib graphic feature events#852
Copy link

Description

@kushalkolar
kushalkolar
opened on Jun 3, 2025
Issue body actions

A common difficulty I hear from users is understanding the attributes and other contents of an Event instance. Currently the various canvas events are typed in pygfx (PointerEvent, KeyEvent, etc.). I'm wondering if we should wrap these in fastplotlib and provide all Event classes as top level imports so they can be used for typing, and easier documentation. The description in the user guide (copied from jupyter_rfb I think?) and the event tables isn't the clearest way to define this I think. Also pygfx plans to remove pygfx.Event and directly provide dicts like rendercanvas.

Suggestion:

  • Create a fpl.Event, and documented subclasses for every single event, such as fpl.PointerEvent, etc. that wrap the pygfx event.
  • For graphics, event classes as class attributes so they are easy to distinguish? Ex: ImageGraphic.DataEvent, and maybe even ImageGraphic.PointerEvent which has the pick_info unpacked as attributes so that everything can be picked up easily by IDEs without having to scour through documentation.
    • might be a better way to namespace this, we can decide those details, maybe like ImageGraphic.event.DataEvent, ImageGraphic.event.PointerEvent, etc.

In addition to making the event attrs recognized by the IDE, it becomes easy to auto-doc what every event looks like.

Example usecases:

@image.add_event_handler("click")
def image_clicked(ev: fpl.ImageGraphic.events.PointerEvent):
    ev.pick_info.  # IDE can display attributes

@image.add_event_handler("cmap")
def cmap_changed(ev: fpl.ImageGraphic.events.CmapEvent):
    ev.  # IDE can display attributes

@linear_selector.add_event_handler("selection")
def cmap_changed(ev: fpl.LinearSelector.events.SelectionEvent):
    ev.  # IDE can display attributes

@figure.add_event_handler("key_down")
def key_down_handler(ev: fpl.KeyEvent):
    ev. # IDE displays attrs

Activity

  1. almarklein commented on Jun 6, 2025

    @almarklein
    almarklein
    on Jun 6, 2025
    Collaborator
    More actions

    ccing @Korijn into this discussion about events.

    In rendercanvas we decided to keep events simple dict objects, and we planned to make pygfx' events simple dicts too. Simple dicts have a certain elegance, but as it turns out, it makes it pretty hard for downstream users to use such events. Using event types has a few clear benefits. IDE's helping users what properties the event has is a pretty big one.

    I'm pretty fond of the event-spec in jupyter-rfb, that has resulted in a completely uniform (gui-backend independent) way to handle events, to some degree across multiple libs (rendercanvas, wgpu-py, pygfx, fastplotlib), which is pretty powerful. That spec is easily mapped/transformed to a spec based on classes.

    I'm tempted to argue for using classes in rendercanvas too, so that the whole stack can use these events/classes, and subclass them for more specific events (e.g. with added data). At the same time I'd argue for keeping these classes dead simple, just using attributes, and in principle avoid adding methods to them (at least in the lower level libs).

    @Korijn I'm interested to hear your thoughts on this.

  2. almarklein commented on Jun 6, 2025

    @almarklein
    almarklein
    on Jun 6, 2025
    Collaborator
    More actions

    An alternative could be that rendercanvas and pygfx stick to simple dicts, and that fastplotlib turns them into typed objects. But I'm leaning to the former so we have more uniformity in the stack, and because it also benefits users in rendercanvas/pygfx.

  3. Korijn commented on Jun 6, 2025

    @Korijn
    Korijn
    on Jun 6, 2025
    More actions

    Since events are often high throughput, we need them to perform exceptionally well, or they'll bottleneck performance. I think for this reason in wgpu-py, rendercanvas and pygfx plain dicts make more sense; they're highly optimized, simple data structures in python. Another benefit is that everybody knows how to work with a dict. A class requires you to learn the class design.

    If autocompletion in IDEs is the issue, we could consider using TypedDict? Or named tuples? Immediately jumping to classes seems like a bit much just to have discovery in IDEs.

    TypedDict seems like a great fit because at runtime they're just plain dicts, so there's no performance penalty.

    For fastplotlib perhaps the extreme performance requirement is less strong and is trumped by convenience. Creating a class hierarchy might be better there. But I would advise against it.

  4. almarklein commented on Jun 6, 2025

    @almarklein
    almarklein
    on Jun 6, 2025
    Collaborator
    More actions

    Exploring options

    Another benefit is that everybody knows how to work with a dict. A class requires you to learn the class design.

    Yeah, but the point Kushal makes is that although users know what a dict is, they cannot see, in static code, what's inside, and they can with a real class. If we keep the class simple, users won't even have to read the docs.

    I looked into TypedDict. Pretty nice. Results in clean docs. VSCode does show me the options when I type event[, but not when I already started typing a string; I must admit that I find the autocompletion for attributes much more pleasant. So this could be an ok solution, but I do want to explore proper types a bit further... 😉

    Named tuples are super light, but read-only, and we want callbacks to set "stop_propagation".

    I also looked at dataclass. Technically it does not prevent adding methods and whatnot, but it signals devs that this is just a bunch of attributes on an object; like a dict, but with attributes instead of keys. And it autmatically creates an __init__ and repr.

    @dataclasses.dataclass(slots=True)
    class Event:
        event_type: str  #: the type of the event, see EventType
        time_stamp: float  #: the timestamp at which the event was created
    
    event = Event("pointer_down", time.time())

    But for the docs and intellisense to work, we could go as simple as this:

    class Event:
        def __init__(self, event_dict):
               self.__dict__.update(event_dict)  # either this
               self.__dict__ = event_dict  # or this 🚀 
    
        event_type: str  #: the type of the event, see EventType
        time_stamp: float  #: the timestamp at which the event was created

    Benchmarking

    Since events are often high throughput, we need them to perform exceptionally well

    I did a little benchmarking on dataclass vs dict:

    import dataclasses
    import time
    
    @dataclasses.dataclass(slots=True)
    class Event:
        """Base class for all events"""
    
        event_type: str  #: the type of the event, see EventType
        time_stamp: float  #: the timestamp at which the event was created
        _ = dataclasses.KW_ONLY
    
    @dataclasses.dataclass(slots=True)
    class PointerEvent(Event):
        button: int  #: the id of the button being pressed (1 left, 2 right, 3 middle, etc.)
        x: float
        y: float
        modifiers: list[str]
    
    @dataclasses.dataclass(slots=True)
    class PointerDownEvent(PointerEvent):
        pass
    
    n = 1000000
    
    t0 = time.perf_counter()
    for i in range(n):    
        event = {"event_type": 'pointer_down', "time_stamp": 0, "button":1, "x":10, "y":11, "modifiers":[]}
    print(f"literal dict: {time.perf_counter() - t0:0.2f} s")
    
    t0 = time.perf_counter()
    for i in range(n):
        event = dict(event_type='pointer_down', time_stamp = 0, button=1, x=10, y=11, modifiers=[])
    print(f"dict init: {time.perf_counter() - t0:0.2f} s")
    
    t0 = time.perf_counter()
    for i in range(n):
        event = PointerDownEvent(event_type='pointer_down', time_stamp = 0, button=1, x=10, y=11, modifiers=[])
    print(f"dataclass: {time.perf_counter() - t0:0.2f} s")

    Result:

    literal dict: 0.22 s
    dict init: 0.26 s
    dataclass: 0.41 s
    

    So dicts indeed are very efficient, instantiating a class takes twice as long.

    However, I also found that attribute access of a dataclass is faster than keying into a dict, so with enough handlers processing the events (even just checking event.event_type) it will even out a bit.

    BTW: checking the event_type attr/key is considerably faster than using isinstance 🤔

    Where does performance matter?

    Thinking about performance a bit more, some events can indeed fire at very high rates, but we only emit once per draw, and we merge successive events of the same type.

    So what if the backends produce dicts (max performance), but when we emit them we emit them as an object? This "wrapping the dict in a typed object" is a cheap operation. Yes it's extra time, but we only do it for a handful of events on each draw. Compared to all other things happening in one draw that seems marginal.

    I could try making some speed tests in a more real setting ...

  5. Korijn commented on Jun 7, 2025

    @Korijn
    Korijn
    on Jun 7, 2025
    More actions

    Maybe we can only perform the conversion when a user actually subscribes with an event handler?

    And maybe __slots__ can help?

  6. almarklein commented on Jun 7, 2025

    @almarklein
    almarklein
    on Jun 7, 2025
    Collaborator
    More actions

    Maybe we can only perform the conversion when a user actually subscribes with an event handler?

    👍

    And maybe __slots__ can help?

    Good point. I see two options, either we convert to a class with slots, copying the fields into it, or we convert to a normal class and just replace it's __dict__ with the event dict. The tightest way to wrap a dict in an object 😉 (Note that objects from a class with slots don't have __dict__.)

    I benchmarked the difference and it looks like the second option is just a bit faster.

  7. kushalkolar commented on Oct 18, 2025

    @kushalkolar
    kushalkolar
    on Oct 18, 2025
    MemberAuthor
    More actions

    I finally have time to do this. Is rendercanvas the best place to implement this?

  8. almarklein commented on Oct 21, 2025

    @almarklein
    almarklein
    on Oct 21, 2025
    Collaborator
    More actions

    Is rendercanvas the best place to implement this?

    Yes

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.