Implement Figure-level overlay architecture with two-pass drawing by Vikash-Kumar-23 · Pull Request #32199 · matplotlib/matplotlib · GitHub
Skip to content
Open
83 changes: 67 additions & 16 deletions lib/matplotlib/backends/backend_qtagg.py
137 changes: 107 additions & 30 deletions lib/matplotlib/figure.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,15 @@

def _stale_figure_callback(self, val):
if (fig := self.get_figure(root=False)) is not None:
if val and hasattr(fig, '_stale_layers'):
for layer_name, artists in fig._children_by_layer.items():
if self in artists:
fig._stale_layers[layer_name] = val
break
else:
if (self in getattr(fig, '_localaxes', []) or
self in getattr(fig, 'subfigs', [])):
fig._stale_layers["base"] = val
fig.stale = val


Expand Down Expand Up @@ -238,14 +247,8 @@ def patches(self):
def texts(self):
return _FigureArtistList(self, 'texts', valid_types=Text)

def _get_draw_artists(self, renderer):
"""Also runs apply_aspect"""
artists = self.get_children()

artists.remove(self.patch)
artists = sorted(
(artist for artist in artists if not artist.get_animated()),
key=lambda artist: artist.get_zorder())
def _apply_aspects(self, renderer):
"""Apply aspect ratios to all axes and children."""
for ax in self._localaxes:
locator = ax.get_axes_locator()
ax.apply_aspect(locator(ax, renderer) if locator else None)
Expand All @@ -255,8 +258,37 @@ def _get_draw_artists(self, renderer):
locator = child.get_axes_locator()
child.apply_aspect(
locator(child, renderer) if locator else None)

def _get_draw_artists(self, renderer, layer):
artists = self.get_children(layer=layer)
artists = sorted(
(artist for artist in artists if not artist.get_animated()),
key=lambda artist: artist.get_zorder())
return artists

def _draw_layer(self, renderer, layer):
"""
Draw the specified layer of artists.

Parameters
----------
renderer : `.RendererBase`
layer : str
The layer to draw.
"""
try:
artists = self._get_draw_artists(renderer, layer=layer)
if not artists:
return

renderer.open_group(layer)
mimage._draw_list_compositing_images(
renderer, self, artists, self.suppressComposite)
renderer.close_group(layer)
finally:
if hasattr(self, '_stale_layers'):
self._stale_layers[layer] = False

def autofmt_xdate(
self, bottom=0.2, rotation=30, ha='right', which='major'):
"""
Expand Down Expand Up @@ -302,17 +334,41 @@ def autofmt_xdate(
self.subplots_adjust(bottom=bottom)
self.stale = True

def get_children(self):
def get_children(self, *, layer=None):
Comment thread
ksunden marked this conversation as resolved.
"""Get a list of artists contained in the figure."""
return [self.patch,
*self.artists,
*self._localaxes,
*self.lines,
*self.patches,
*self.texts,
*self.images,
*self.legends,
*self.subfigs]
layers = list(self._children_by_layer) if layer is None else [layer]
result = []

# Define the types in the exact order they should be appended
ordered_types = (Line2D, Patch, Text, mimage.FigureImage, mlegend.Legend)

for name in layers:
children = self._children_by_layer.get(name, [])

buckets = {cls: [] for cls in ordered_types}
artists = []

for a in children:
# Route the artist to its bucket, or artists
matched = next(
(cls for cls in ordered_types if isinstance(a, cls)), None
)
if matched:
buckets[matched].append(a)
else:
artists.append(a)

result += artists
if name == "base":
result += self._localaxes

for cls in ordered_types:
result += buckets[cls]

if name == "base":
result += self.subfigs

return result

def get_figure(self, root=None):
"""
Expand Down Expand Up @@ -585,7 +641,7 @@ def set_frameon(self, b):

frameon = property(get_frameon, set_frameon)

def add_artist(self, artist, clip=False):
def add_artist(self, artist, clip=False, *, layer=None):
Comment thread
Vikash-Kumar-23 marked this conversation as resolved.
"""
Add an `.Artist` to the figure.

Expand All @@ -601,15 +657,22 @@ def add_artist(self, artist, clip=False):
``figure.transSubfigure``.
clip : bool, default: False
Whether the added artist should be clipped by the figure patch.
layer : str, default: None
The layer to add the artist to. If None, the base layer is used.

Returns
-------
`~matplotlib.artist.Artist`
The added artist.
"""
artist.set_figure(self)
self._children.append(artist)
artist._remove_method = self._children.remove
resolved_layer = layer or "base"
target = self._children_by_layer.setdefault(resolved_layer, [])
target.append(artist)
artist._remove_method = target.remove

Comment thread
ksunden marked this conversation as resolved.
self._stale_layers[resolved_layer] = True
artist.stale_callback = _stale_figure_callback

if not artist.is_transform_set():
artist.set_transform(self.transSubfigure)
Expand Down Expand Up @@ -1076,6 +1139,11 @@ def clear(self, keep_observers=False):
self.delaxes(ax) # Remove ax from self._axstack.

self._children = []
self._children_by_layer = {
"patch": [self.patch],
"base": self._children,
}
Comment thread
Vikash-Kumar-23 marked this conversation as resolved.
self._stale_layers = {"patch": True, "base": True}
self.subplotpars.reset()
if not keep_observers:
self._axobservers = cbook.CallbackRegistry()
Expand Down Expand Up @@ -2385,6 +2453,13 @@ def __init__(self, parent, subplotspec, *,
in_layout=False, transform=self.transSubfigure)
self._set_artist_props(self.patch)
self.patch.set_antialiased(False)
# Rebuild the dict with "patch" as the first key so that iterating
# _children_by_layer in insertion order always draws patch first.
self._children_by_layer = {
"patch": [self.patch],
"base": self._children,
}
self._stale_layers = {"patch": True, "base": True}

@property
def canvas(self):
Expand Down Expand Up @@ -2494,13 +2569,13 @@ def draw(self, renderer):
if not self.get_visible():
return

artists = self._get_draw_artists(renderer)

try:
renderer.open_group('subfigure', gid=self.get_gid())
self.patch.draw(renderer)
mimage._draw_list_compositing_images(
renderer, self, artists, self.get_figure(root=True).suppressComposite)
self._apply_aspects(renderer)
# Draw all layers in dict insertion order:
# patch (background) → base → any additional layers.
for _layer in self._children_by_layer:
self._draw_layer(renderer, _layer)
renderer.close_group('subfigure')

finally:
Expand Down Expand Up @@ -3348,7 +3423,6 @@ def draw(self, renderer):

with self._render_lock:

artists = self._get_draw_artists(renderer)
try:
renderer.open_group('figure', gid=self.get_gid())
if self.axes and self.get_layout_engine() is not None:
Expand All @@ -3358,9 +3432,12 @@ def draw(self, renderer):
pass
# ValueError can occur when resizing a window.

self.patch.draw(renderer)
mimage._draw_list_compositing_images(
renderer, self, artists, self.suppressComposite)
self._apply_aspects(renderer)

# Draw all layers in dict insertion order:
# patch (background) → base → any additional layers.
for _layer in self._children_by_layer:
self._draw_layer(renderer, _layer)

renderer.close_group('figure')
finally:
Expand Down
4 changes: 2 additions & 2 deletions lib/matplotlib/figure.pyi
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading