GSoC 2026 — First PR merged, looking for guidance on project ideas

Hi everyone,

I am Shourya Soneji, a first-year B.Tech AI & ML student from Mumbai, and I have just had my first matplotlib PR merged — a warning for mismatched handles and labels in legend._parse_legend_args().

Getting that first PR merged taught me more about the codebase than weeks of reading docs. I am now actively looking for my next contribution while simultaneously working on my GSoC 2026 proposal.

I am reviewing the ideas page and would genuinely appreciate guidance from maintainers or experienced contributors on which project idea would be a good fit for someone at my stage. I want to write a proposal that is technically credible and realistically scoped — not just ambitious on paper.

Happy to share my draft proposal here for feedback once it is ready.

My GitHub: EncryptedDoom · GitHub

The projects are intended to have roughly around the same level of technical difficulty. What I (and I think many of the other maintainers) suggest is you go for the project where you have intrinsic motivation to see it through b/c you want/need the feature.

also congrats on your first PR!

Thank you so much, that means a lot!

After going through both ideas carefully, I am leaning towards the overlay layer project as mentioned on the matplotlib ideas page. I have personally felt the lag when using interactive plots and I genuinely want to see a fast, smooth crosshair cursor exist in matplotlib, and I know this will genuinely improve user experience for a lot of developers — so this project feels like the right fit.

I will go through PR #30515 to understand the existing groundwork and start drafting my proposal. I may come back with questions as I dig deeper — hope that is okay!

1 Like

As part of preparing my GSoC proposal for the overlay layer project, I have been studying the existing Cursor widget in widgets.py and the blitting infrastructure in backend_bases.py to understand the current limitations.

I built a small proof-of-concept that demonstrates the core architectural idea — separating the logical state of overlay elements from the rendering layer, so the overlay can redraw independently without triggering a full figure draw. It also demonstrates crosshair propagation across multiple shared axes, which the current Cursor widget does not support.

python

import matplotlib
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
import numpy as np


class OverlayCrosshair:
    """
    Logical representation of a crosshair — backend-independent.
    Stores position and visibility state only.
    Rendering is handled by OverlayLayer.
    """
    def __init__(self, color='red', linewidth=0.8, linestyle='--'):
        self.x = None
        self.y = None
        self.visible = False
        self.color = color
        self.linewidth = linewidth
        self.linestyle = linestyle

    def update(self, x, y):
        self.x = x
        self.y = y
        self.visible = True

    def hide(self):
        self.visible = False


class OverlayLayer:
    """
    A layer that sits on top of the figure and redraws independently.

    Responsibilities:
    - Maintain awareness of all Axes on the figure
    - Cache the background (figure without overlay elements)
    - On each update: restore background + redraw only overlay elements
    - Invalidate cache on resize or full redraw
    """
    def __init__(self, fig):
        self.fig = fig
        self.canvas = fig.canvas
        self._background = None
        self._elements = []
        self._artists = []

        self.canvas.mpl_connect('draw_event', self._on_draw)
        self.canvas.mpl_connect('resize_event', self._on_resize)

    def add_element(self, element):
        self._elements.append(element)

    def _get_all_axes(self):
        """
        Figure-level Axes awareness.
        This is what allows the overlay to propagate
        to all axes, not just the one under the cursor.
        """
        return self.fig.get_axes()

    def _build_artists(self):
        """
        Create one pair of crosshair lines per Axes per element.
        Artists are marked animated=True so they are excluded
        from the normal draw cycle.
        """
        for artist in self._artists:
            artist.remove()
        self._artists = []

        for element in self._elements:
            if isinstance(element, OverlayCrosshair):
                for ax in self._get_all_axes():
                    hline = ax.axhline(
                        y=0,
                        color=element.color,
                        linewidth=element.linewidth,
                        linestyle=element.linestyle,
                        animated=True,
                        visible=False
                    )
                    vline = ax.axvline(
                        x=0,
                        color=element.color,
                        linewidth=element.linewidth,
                        linestyle=element.linestyle,
                        animated=True,
                        visible=False
                    )
                    self._artists.extend([hline, vline])

    def _cache_background(self):
        """
        Capture the figure WITHOUT overlay elements as a pixel buffer.
        This is restored on every mouse move instead of redrawing
        the full figure.
        """
        for artist in self._artists:
            artist.set_visible(False)
        self._background = self.canvas.copy_from_bbox(self.fig.bbox)

    def _on_draw(self, event):
        self._build_artists()
        self._cache_background()

    def _on_resize(self, event):
        self._background = None

    def render(self):
        """
        Core render loop — called on every mouse move.
        Never triggers a full figure redraw.
        """
        if self._background is None:
            return

        self.canvas.restore_region(self._background)

        axes = self._get_all_axes()
        artist_idx = 0

        for element in self._elements:
            if isinstance(element, OverlayCrosshair):
                for ax in axes:
                    hline = self._artists[artist_idx]
                    vline = self._artists[artist_idx + 1]
                    artist_idx += 2

                    if element.visible and element.x is not None:
                        hline.set_ydata([element.y])
                        vline.set_xdata([element.x])
                        hline.set_visible(True)
                        vline.set_visible(True)
                        ax.draw_artist(hline)
                        ax.draw_artist(vline)
                    else:
                        hline.set_visible(False)
                        vline.set_visible(False)

        self.canvas.blit(self.fig.bbox)


class OverlayCrosshairTool:
    """
    Connects mouse events to the OverlayLayer.
    In the full implementation this would be a NavigationToolbar2
    Tool, toggleable from the toolbar like Pan and Zoom.
    """
    def __init__(self, fig, overlay, crosshair):
        self.fig = fig
        self.overlay = overlay
        self.crosshair = crosshair
        fig.canvas.mpl_connect('motion_notify_event', self._on_move)

    def _on_move(self, event):
        if event.inaxes:
            self.crosshair.update(event.xdata, event.ydata)
        else:
            self.crosshair.hide()
        self.overlay.render()


# ── Demo: two shared axes, crosshair propagates to both ──────────────────────

fig, (ax1, ax2) = plt.subplots(2, 1, sharex=True)
fig.suptitle("Overlay PoC — crosshair propagates to both axes", fontsize=10)

for ax in (ax1, ax2):
    for i in range(30):
        ax.plot(np.random.rand(500), np.random.rand(500),
                '.', markersize=2, alpha=0.3)

overlay = OverlayLayer(fig)
crosshair = OverlayCrosshair(color='red', linewidth=0.8, linestyle='--')
overlay.add_element(crosshair)
tool = OverlayCrosshairTool(fig, overlay, crosshair)

plt.tight_layout()
plt.show()

I am sharing this to get early feedback on whether this architectural direction aligns with what is envisioned for the project before I build it into my proposal.

A few specific questions:

  • Is the separation between logical element state (OverlayCrosshair) and the rendering layer (OverlayLayer) the right abstraction to build on?

  • Should the overlay layer ultimately live inside the backend itself, or is a Python-level implementation viable as a starting point?

  • For the full implementation, which backends should be prioritised first — Qt, Tk, or both?