Our naive approaches to crosshair cursors https://matplotlib.org/devdocs/gallery/widgets/cursor.html have the disadvantage that the crosshair is drawn on the figure. This means an expensive redraw of the the figure for every cursor position change and results in a laggy behavior.
Proposed solution
We could grow an API on Canvas to draw a crosshair with GUI native lines. That should be relatively easy. We only need the size of the Axes-under-cursor in figure coordinates to determine the length of the crosshair.
The goal would be to add a crosshair tool in addtion to Pan and Zoom.
This tool should be much easier to use than a Cursor widget and much more efficient.
Hi! My name is Ayush Kumar. I found this issue while reading the GSoC ideas and I’m interested in working on it. I’ve started exploring the related code (especially event handling and redraw flow in FigureCanvasBase) to understand where overlays would fit in the pipeline.
Before I begin a small PR, could you please confirm whether the intended direction is to let overlay elements update independently of the full draw() cycle?
Hello,
My name is Sireesha and I’m interested in contributing to Matplotlib for GSoC 2026.
I’ve been reading through the discussion on the GUI-native crosshair cursor and exploring the backend and widget code to understand the current redraw pipeline.
I’m particularly interested in the overlay layer idea and how it could allow interactive elements like crosshairs to update independently from the main draw() cycle.
I’d love to contribute and start exploring this area. Please let me know if there are beginner tasks or related parts of the codebase I should study first.
Thank you!
@konangisireesha3-del We do not have concrete tasks. For GSoC, we expect candiates to be able to work indepently to a certain extent. As a first step, you should understand the draw mechanism and the backend concept. As pointed out in #30953 (comment), there are two possible approaches to the topic. We have not yet decided which way to go. Aanalyzing both approaches and identifying their benefits, limitations and pain points is a good next step. Showing that you are able to analyze the situation on your own and have well-founded solution ideas is a stong proposal for GSoC.
Hi @timhoffm,
I spent some time going through this issue and the related discussions about overlays and the crosshair cursor. To understand the problem better, I also explored parts of Matplotlib’s rendering pipeline to see where the performance limitations might arise.
From what I understand, the main challenge is that interactive elements like crosshair cursors are currently implemented as Artists that participate in the normal draw pipeline. Because of this, every cursor movement triggers the entire rendering process (FigureCanvasBase.draw -Figure.draw-Axes.draw-Artists).
For elements that update very frequently, such as cursors or selectors, this can become quite expensive and can introduce noticeable lag.
The overlay concept seems intended to address this by separating the rendering of the static figure from rapidly changing interactive elements.
Based on the discussion, it looks like there are two possible directions for implementing this.
(a) GUI-native overlay primitives:
One option would be for each backend to draw overlay primitives directly using the GUI toolkit’s native drawing capabilities.
Advantages:
1)Very fast updates since this bypasses Matplotlib’s main rendering pipeline.
2)The figure itself does not need to be redrawn.
Limitations:
1)Each backend would need its own implementation.
2)Maintaining consistent behavior across backends could become challenging.
3)It may also reduce flexibility if overlays need deeper integration with Matplotlib objects.
(b) A Matplotlib-managed overlay layer:
Another approach would be to introduce a dedicated overlay layer within Matplotlib itself. This layer would be rendered separately from the main figure image and composited on top of it.
Advantages:
1)More backend-independent design.
2)Could potentially evolve into a more general layered rendering architecture.
3)Might unify or simplify existing mechanisms such as widget blitting or animation updates.
Challenges:
1)It would require careful design around layer coordination (coordinate systems, event handling, and draw order).
2)Overlay updates would still need to remain lightweight to ensure smooth interaction.
The idea of expanding this into a general layered drawing system seems particularly interesting. For instance, we could imagine a structure like:
a static background layer for expensive plots
a dynamic overlay layer for elements like crosshairs, selectors, or cursors
a foreground layer for annotations or temporary UI elements
With this setup, only the dynamic layer would need to be updated during interaction, while the other layers remain unchanged. This might also help simplify the current blitting logic used by widgets and animations.
As a next step, I plan to continue looking into the backend canvas implementations and the draw pipeline to understand where a layered system like this could integrate most cleanly.
One question I had while thinking about the architecture:
Would it make more sense for the overlay system to live at the canvas level (inside FigureCanvasBase), acting as a composition layer, or closer to the Figure level, but rendered separately from the main draw pass?
Thanks for the guidance -this is a really interesting architectural topic, and I’m looking forward to exploring it further.
Following up on the discussion about the two approaches, I was curious about the actual performance hit of Approach (b)—specifically the alpha compositing overhead if Matplotlib tries to manage the overlay layer itself.
I wrote a quick script to benchmark full-canvas blending (1920x1080) in pure NumPy versus just offloading a transparent QWidget to the OS window manager using PyQt6.
The difference is pretty massive. Doing the array blending in NumPy takes about ~31.7 ms per frame on my machine (roughly 31 FPS), which would definitely feel laggy for a fast-moving crosshair. On the other hand, the Qt hardware compositing is basically free, taking only ~0.035 ms per frame.
It seems like a pure Matplotlib-managed layer (Approach B) is going to be too slow out of the box unless we restrict the pixel blending to a small bounding box around the cursor. But wouldn't that just reintroduce the same region-tracking headaches we already deal with using blitting?
Given this performance gap, do you think Approach (a) (GUI-native primitives) is the more realistic path forward, even though it means writing separate implementations for each backend?
Thanks for sharing the benchmark results. The comparison between NumPy compositing and Qt’s hardware compositing is really helpful, and it gives a clearer picture of the performance limitations that a purely Matplotlib-managed overlay layer might face for high-frequency interactions like crosshair cursors.
From the results, it seems that doing a full-canvas alpha blending step in Python/NumPy could introduce noticeable latency, especially for larger figures. That makes it clear why GUI-native rendering could provide much smoother updates for interactive overlays.
At the same time, I was wondering if a hybrid approach might work.
For example, Matplotlib could provide a general overlay API at the canvas level, while each backend implements the rendering using the most efficient approach available.
In that case:
GUI backends such as Qt, GTK, or Tk could render overlays using native drawing primitives or hardware compositing.
Non-interactive backends could fall back to a lightweight Matplotlib-based implementation if needed.
This might allow Matplotlib to keep a consistent interface while still benefiting from hardware-accelerated rendering in GUI environments.
Right now I’m continuing to explore the backend implementations (starting with the Qt backend) to better understand how the draw pipeline and event handling interact inside FigureCanvasBase. I also plan to look more closely at how blitting and partial redraws are currently used by widgets and animations, to see if similar region-based updates could help keep overlay updates lightweight.
Thanks again for sharing the benchmark -it’s very helpful for understanding the trade-offs between the different approaches.
We have to be a bit careful to not mix different aspects in the performance evaluation. Who draws primitives (GUI-native vs. matplotlib) and whether one draws directly or uses a separate layer "canvas" and alpha-composites the whole layer are not necessarily coupled.
What @kathanCodes has shown is that alpha-compositing complete layers using numpy is expensive for lage images. We may get a bit faster than a pure numpy implementation, but in the end a lot of computation is involved.
This indicates that pre-rendering layers and alpha-compositing is typically not efficient. One could still keep a layer model with the idea that if a change to a layer is needed all layers above that need re-rendering. Each layer could be free to decide how to render: using primitives GUI primitives or matplotlib primitives or compositing a pre-rendered image (can also be just a fraction of the figure similar to what we currently can do with blitting).
On a general note: Please share if and how you use AI tools, in the process (e.g. understanding the code base, idea finding implementing prototypes etc.)
Hi @timhoffm,
Thank you for the clarification - that distinction is very helpful. I see now that the two questions are somewhat orthogonal:
Who renders the primitives. (GUI-native primitives vs Matplotlib primitives).
How the rendering is organized. (direct drawing vs layered rendering with compositing).
The benchmark shared earlier mainly demonstrates that full-frame alpha compositing with NumPy is expensive for large images, which suggests that a design relying heavily on pre-rendered layers that are recomposited each frame may not scale well.
Your point about keeping a "logical layer model", where only layers affected by a change are re-rendered, makes a lot of sense. In that case, each layer could choose its own rendering strategy depending on what it contains.
for example:
drawing primitives directly (via GUI or Matplotlib)
rendering a cached image region
or updating only a small bounding region similar to the current blitting approach
That seems like a flexible architecture which could allow interactive elements (like crosshairs or selectors) to remain lightweight while avoiding the cost of full-frame compositing.
As a next step, I plan to look more closely at how the current "blitting mechanism and partial redraw logic" work in the backends and in "FigureCanvasBase", since that may already contain some of the infrastructure needed for efficient updates.
Regarding AI tools: I do use them occasionally to help understand unfamiliar parts of the codebase or explore possible design ideas. However, I make sure to read the relevant source code and documentation myself and verify any suggestions before using them in my analysis or experiments.
Thanks again for the guidance - this discussion is helping me understand the rendering architecture much better.
I've been exploring transforms.py and the backend_bases.py, and I'd love your thoughts on a specific approach for calculating the "dirty" regions to avoid full-canvas compositing.
What if we moved the caching state out of the widgets and introduced a LayerManager (living in Figure or FigureCanvasBase)? The workflow could look like this:
Separation & Invalidation: Artists are assigned to layers (e.g., Layer 0: Static plot, Layer 1: Interactive crosshair). When a widget updates its coordinates, it naturally triggers self.invalidate().
The Math: The LayerManager intercepts this and calculates the exact changed region using Bbox.union([old_bbox, new_bbox]).
Smart Compositing: During the draw pass, the manager skips rendering for valid layers (reusing their cached pixel buffers). It commands the backend to re-render the dirty layers only inside that specific Bbox union, and then alpha-composites just that tiny fraction of the screen.
This seems like it would completely eliminate the manual blitting hacks (restore_region, etc.) currently scattered across the widget library. Widgets would only need to manage their data, and the LayerManager would gracefully handle edge cases like window resizing or background animations.
Regarding your question about AI tools: I want to be fully transparent. I use LLMs as an interactive learning accelerator. Because Matplotlib's codebase is massive, I use AI to help me quickly locate relevant files (like transforms.py), explain GUI-specific rendering jargon, and act as a sounding board for high-level architectural ideas.
Hi @timhoffm,
Thanks for the clarification about separating the rendering strategy from the layer organization.
I’m currently exploring the existing redraw and blitting mechanisms in "FigureCanvasBase" and the backends to better understand how partial updates are handled today.
As part of this exploration, I’m planning to implement a small prototype to experiment with efficient cursor updates (for example using blitting or small region updates). My goal is mainly to understand the current rendering pipeline and where a future overlay system might integrate cleanly.
I’ll share any findings or small experiments once I have something working.
@kathanCodes The fundamental idea sounds good. I would say that the detecting the changed region is optional for a start. Assuming that the upper layer typically only contains few Artists, completely redrawing it is fine most of the time; e.g. for the crosshair the changed region is always the bbox of the Axes, but you have to redraw the whole crossghair anyway. The benefit is rather that you have cheap access to the full background image of the lower layer.
Hi @timhoffm,
Following the earlier discussion about efficient cursor updates, I created a small prototype example using Matplotlib’s existing blitting mechanism to experiment with updating a crosshair cursor without triggering a full figure redraw.
The example caches the axes background and redraws only the crosshair artists during mouse movement using "restore_region" and "blit".
I opened a small PR with the prototype example here: #31310
The goal was mainly to better understand the current rendering pipeline and how partial updates work in practice.
I’d be interested to know whether experiments like this are useful when thinking about possible overlay or layered rendering designs.
Activity
PoC: GUI-native crosshair cursor
PoC: GUI-native crosshair cursor [skip ci]
ayushjariyal commented on Feb 16, 2026
Hi! My name is Ayush Kumar. I found this issue while reading the GSoC ideas and I’m interested in working on it. I’ve started exploring the related code (especially event handling and redraw flow in FigureCanvasBase) to understand where overlays would fit in the pipeline.
Before I begin a small PR, could you please confirm whether the intended direction is to let overlay elements update independently of the full draw() cycle?
timhoffm commented on Feb 16, 2026
@ayushjariyal Thanks for your interest. Yes, overlay elements should be able to update independently of the full draw cycle.
Note that crosshair is a specific application and we will likely go to a more general concept right away. See #30953
konangisireesha3-del commented on Mar 8, 2026
Hello,
My name is Sireesha and I’m interested in contributing to Matplotlib for GSoC 2026.
I’ve been reading through the discussion on the GUI-native crosshair cursor and exploring the backend and widget code to understand the current redraw pipeline.
I’m particularly interested in the overlay layer idea and how it could allow interactive elements like crosshairs to update independently from the main draw() cycle.
I’d love to contribute and start exploring this area. Please let me know if there are beginner tasks or related parts of the codebase I should study first.
Thank you!
timhoffm commented on Mar 8, 2026
@konangisireesha3-del We do not have concrete tasks. For GSoC, we expect candiates to be able to work indepently to a certain extent. As a first step, you should understand the draw mechanism and the backend concept. As pointed out in #30953 (comment), there are two possible approaches to the topic. We have not yet decided which way to go. Aanalyzing both approaches and identifying their benefits, limitations and pain points is a good next step. Showing that you are able to analyze the situation on your own and have well-founded solution ideas is a stong proposal for GSoC.
konangisireesha3-del commented on Mar 9, 2026
Hi @timhoffm,
I spent some time going through this issue and the related discussions about overlays and the crosshair cursor. To understand the problem better, I also explored parts of Matplotlib’s rendering pipeline to see where the performance limitations might arise.
From what I understand, the main challenge is that interactive elements like crosshair cursors are currently implemented as Artists that participate in the normal draw pipeline. Because of this, every cursor movement triggers the entire rendering process (FigureCanvasBase.draw -Figure.draw-Axes.draw-Artists).
For elements that update very frequently, such as cursors or selectors, this can become quite expensive and can introduce noticeable lag.
The overlay concept seems intended to address this by separating the rendering of the static figure from rapidly changing interactive elements.
Based on the discussion, it looks like there are two possible directions for implementing this.
(a) GUI-native overlay primitives:
One option would be for each backend to draw overlay primitives directly using the GUI toolkit’s native drawing capabilities.
Advantages:
1)Very fast updates since this bypasses Matplotlib’s main rendering pipeline.
2)The figure itself does not need to be redrawn.
Limitations:
1)Each backend would need its own implementation.
2)Maintaining consistent behavior across backends could become challenging.
3)It may also reduce flexibility if overlays need deeper integration with Matplotlib objects.
(b) A Matplotlib-managed overlay layer:
Another approach would be to introduce a dedicated overlay layer within Matplotlib itself. This layer would be rendered separately from the main figure image and composited on top of it.
Advantages:
1)More backend-independent design.
2)Could potentially evolve into a more general layered rendering architecture.
3)Might unify or simplify existing mechanisms such as widget blitting or animation updates.
Challenges:
1)It would require careful design around layer coordination (coordinate systems, event handling, and draw order).
2)Overlay updates would still need to remain lightweight to ensure smooth interaction.
The idea of expanding this into a general layered drawing system seems particularly interesting. For instance, we could imagine a structure like:
With this setup, only the dynamic layer would need to be updated during interaction, while the other layers remain unchanged. This might also help simplify the current blitting logic used by widgets and animations.
As a next step, I plan to continue looking into the backend canvas implementations and the draw pipeline to understand where a layered system like this could integrate most cleanly.
One question I had while thinking about the architecture:
Would it make more sense for the overlay system to live at the canvas level (inside FigureCanvasBase), acting as a composition layer, or closer to the Figure level, but rendered separately from the main draw pass?
Thanks for the guidance -this is a really interesting architectural topic, and I’m looking forward to exploring it further.
kathanCodes commented on Mar 9, 2026
Hi @timhoffm,
Following up on the discussion about the two approaches, I was curious about the actual performance hit of Approach (b)—specifically the alpha compositing overhead if Matplotlib tries to manage the overlay layer itself.
I wrote a quick script to benchmark full-canvas blending (1920x1080) in pure NumPy versus just offloading a transparent QWidget to the OS window manager using PyQt6.
The difference is pretty massive. Doing the array blending in NumPy takes about ~31.7 ms per frame on my machine (roughly 31 FPS), which would definitely feel laggy for a fast-moving crosshair. On the other hand, the Qt hardware compositing is basically free, taking only ~0.035 ms per frame.
It seems like a pure Matplotlib-managed layer (Approach B) is going to be too slow out of the box unless we restrict the pixel blending to a small bounding box around the cursor. But wouldn't that just reintroduce the same region-tracking headaches we already deal with using blitting?
Given this performance gap, do you think Approach (a) (GUI-native primitives) is the more realistic path forward, even though it means writing separate implementations for each backend?
konangisireesha3-del commented on Mar 10, 2026
Hi @timhoffm and @kathanCodes,
Thanks for sharing the benchmark results. The comparison between NumPy compositing and Qt’s hardware compositing is really helpful, and it gives a clearer picture of the performance limitations that a purely Matplotlib-managed overlay layer might face for high-frequency interactions like crosshair cursors.
From the results, it seems that doing a full-canvas alpha blending step in Python/NumPy could introduce noticeable latency, especially for larger figures. That makes it clear why GUI-native rendering could provide much smoother updates for interactive overlays.
At the same time, I was wondering if a hybrid approach might work.
For example, Matplotlib could provide a general overlay API at the canvas level, while each backend implements the rendering using the most efficient approach available.
In that case:
GUI backends such as Qt, GTK, or Tk could render overlays using native drawing primitives or hardware compositing.
Non-interactive backends could fall back to a lightweight Matplotlib-based implementation if needed.
This might allow Matplotlib to keep a consistent interface while still benefiting from hardware-accelerated rendering in GUI environments.
Right now I’m continuing to explore the backend implementations (starting with the Qt backend) to better understand how the draw pipeline and event handling interact inside FigureCanvasBase. I also plan to look more closely at how blitting and partial redraws are currently used by widgets and animations, to see if similar region-based updates could help keep overlay updates lightweight.
Thanks again for sharing the benchmark -it’s very helpful for understanding the trade-offs between the different approaches.
timhoffm commented on Mar 11, 2026
@kathanCodes @konangisireesha3-del Thanks for looking into this.
We have to be a bit careful to not mix different aspects in the performance evaluation. Who draws primitives (GUI-native vs. matplotlib) and whether one draws directly or uses a separate layer "canvas" and alpha-composites the whole layer are not necessarily coupled.
What @kathanCodes has shown is that alpha-compositing complete layers using numpy is expensive for lage images. We may get a bit faster than a pure numpy implementation, but in the end a lot of computation is involved.
This indicates that pre-rendering layers and alpha-compositing is typically not efficient. One could still keep a layer model with the idea that if a change to a layer is needed all layers above that need re-rendering. Each layer could be free to decide how to render: using primitives GUI primitives or matplotlib primitives or compositing a pre-rendered image (can also be just a fraction of the figure similar to what we currently can do with blitting).
On a general note: Please share if and how you use AI tools, in the process (e.g. understanding the code base, idea finding implementing prototypes etc.)
konangisireesha3-del commented on Mar 11, 2026
Hi @timhoffm,
Thank you for the clarification - that distinction is very helpful. I see now that the two questions are somewhat orthogonal:
The benchmark shared earlier mainly demonstrates that full-frame alpha compositing with NumPy is expensive for large images, which suggests that a design relying heavily on pre-rendered layers that are recomposited each frame may not scale well.
Your point about keeping a "logical layer model", where only layers affected by a change are re-rendered, makes a lot of sense. In that case, each layer could choose its own rendering strategy depending on what it contains.
for example:
drawing primitives directly (via GUI or Matplotlib)
rendering a cached image region
or updating only a small bounding region similar to the current blitting approach
That seems like a flexible architecture which could allow interactive elements (like crosshairs or selectors) to remain lightweight while avoiding the cost of full-frame compositing.
As a next step, I plan to look more closely at how the current "blitting mechanism and partial redraw logic" work in the backends and in "FigureCanvasBase", since that may already contain some of the infrastructure needed for efficient updates.
Regarding AI tools: I do use them occasionally to help understand unfamiliar parts of the codebase or explore possible design ideas. However, I make sure to read the relevant source code and documentation myself and verify any suggestions before using them in my analysis or experiments.
Thanks again for the guidance - this discussion is helping me understand the rendering architecture much better.
kathanCodes commented on Mar 12, 2026
Hi @timhoffm,
I've been exploring transforms.py and the backend_bases.py, and I'd love your thoughts on a specific approach for calculating the "dirty" regions to avoid full-canvas compositing.
What if we moved the caching state out of the widgets and introduced a LayerManager (living in Figure or FigureCanvasBase)? The workflow could look like this:
This seems like it would completely eliminate the manual blitting hacks (restore_region, etc.) currently scattered across the widget library. Widgets would only need to manage their data, and the LayerManager would gracefully handle edge cases like window resizing or background animations.
Regarding your question about AI tools: I want to be fully transparent. I use LLMs as an interactive learning accelerator. Because Matplotlib's codebase is massive, I use AI to help me quickly locate relevant files (like transforms.py), explain GUI-specific rendering jargon, and act as a sounding board for high-level architectural ideas.
konangisireesha3-del commented on Mar 13, 2026
Hi @timhoffm,
Thanks for the clarification about separating the rendering strategy from the layer organization.
I’m currently exploring the existing redraw and blitting mechanisms in "FigureCanvasBase" and the backends to better understand how partial updates are handled today.
As part of this exploration, I’m planning to implement a small prototype to experiment with efficient cursor updates (for example using blitting or small region updates). My goal is mainly to understand the current rendering pipeline and where a future overlay system might integrate cleanly.
I’ll share any findings or small experiments once I have something working.
Thanks again for the guidance!
timhoffm commented on Mar 13, 2026
@kathanCodes The fundamental idea sounds good. I would say that the detecting the changed region is optional for a start. Assuming that the upper layer typically only contains few Artists, completely redrawing it is fine most of the time; e.g. for the crosshair the changed region is always the bbox of the Axes, but you have to redraw the whole crossghair anyway. The benefit is rather that you have cheap access to the full background image of the lower layer.
konangisireesha3-del commented on Mar 15, 2026
Hi @timhoffm,
Following the earlier discussion about efficient cursor updates, I created a small prototype example using Matplotlib’s existing blitting mechanism to experiment with updating a crosshair cursor without triggering a full figure redraw.
The example caches the axes background and redraws only the crosshair artists during mouse movement using "restore_region" and "blit".
I opened a small PR with the prototype example here:
#31310
The goal was mainly to better understand the current rendering pipeline and how partial updates work in practice.
I’d be interested to know whether experiments like this are useful when thinking about possible overlay or layered rendering designs.
Thanks!