Thanks to visit codestin.com
Credit goes to github.com

Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 67 additions & 16 deletions lib/matplotlib/backends/backend_qtagg.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,60 @@


from matplotlib.transforms import Bbox
from matplotlib.backend_bases import DrawEvent

from .qt_compat import QT_API, QtCore, QtGui
from .backend_agg import FigureCanvasAgg
from .backend_agg import FigureCanvasAgg, RendererAgg
from .backend_qt import _BackendQT, FigureCanvasQT
from .backend_qt import ( # noqa: F401 # pylint: disable=W0611
FigureManagerQT, NavigationToolbar2QT)


class FigureCanvasQTAgg(FigureCanvasAgg, FigureCanvasQT):

def __init__(self, figure=None):
super().__init__(figure=figure)
self._layer_renderers = {}
self._renderer_key = None

def draw(self):
"""
Render the figure using the per-layer caching optimization.
"""
fig = self.figure
w, h = self.get_width_height(physical=True)
dpi = fig.dpi

# Run layout engine once before drawing
if fig.axes and fig.get_layout_engine() is not None:
try:
fig.get_layout_engine().execute(fig)
except ValueError:
pass

key = (w, h, dpi)
is_resized = self._renderer_key != key
if is_resized:
self._renderer_key = key

for layer_name in fig._children_by_layer:
layer_stale = fig._stale_layers.get(layer_name, True)

# Re-render if: layer is stale OR canvas was resized
if layer_stale or is_resized:
layer_renderer = RendererAgg(w, h, dpi)
fig._draw_layer(layer_renderer, layer_name)

self._layer_renderers[layer_name] = layer_renderer

fig.stale = False

# Fire the draw event
base_renderer = self._layer_renderers.get("base")
DrawEvent("draw_event", self, base_renderer)._process()

self.update()

def paintEvent(self, event):
"""
Copy the image from the Agg canvas to the qt.drawable.
Expand All @@ -23,9 +67,8 @@ def paintEvent(self, event):
"""
self._draw_idle() # Only does something if a draw is pending.

# If the canvas does not have a renderer, then give up and wait for
# FigureCanvasAgg.draw(self) to be called.
if not hasattr(self, 'renderer'):
# If the layers haven't been rendered yet, give up and wait for draw()
if not self._layer_renderers:
return

painter = QtGui.QPainter(self)
Expand All @@ -46,21 +89,29 @@ def paintEvent(self, event):
right = left + width
# create a buffer using the image bounding box
bbox = Bbox([[left, bottom], [right, top]])
buf = memoryview(self.copy_from_bbox(bbox))

if QT_API == "PyQt6":
from PyQt6 import sip
ptr = int(sip.voidptr(buf))
else:
ptr = buf

painter.eraseRect(rect) # clear the widget canvas
qimage = QtGui.QImage(ptr, buf.shape[1], buf.shape[0],
QtGui.QImage.Format.Format_RGBA8888)
qimage.setDevicePixelRatio(self.device_pixel_ratio)
# set origin using original QT coordinates
origin = QtCore.QPoint(rect.left(), rect.top())
painter.drawImage(origin, qimage)

for layer_name in self.figure._children_by_layer:
if layer_name in self._layer_renderers:
layer_renderer = self._layer_renderers[layer_name]

buf = memoryview(layer_renderer.copy_from_bbox(bbox))

if QT_API == "PyQt6":
from PyQt6 import sip
ptr = int(sip.voidptr(buf))
else:
ptr = buf

qimage = QtGui.QImage(ptr, buf.shape[1], buf.shape[0],
QtGui.QImage.Format.Format_RGBA8888)
qimage.setDevicePixelRatio(self.device_pixel_ratio)

# Qt's QPainter natively handles alpha blending!
painter.drawImage(origin, qimage)

self._draw_rect_callback(painter)
finally:
painter.end()
Expand Down
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
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class FigureBase(Artist):
@property
def texts(self) -> ArtistList[Text]: ...

def get_children(self) -> list[Artist]: ...
def get_children(self, *, layer: str | None = ...) -> list[Artist]: ...
def contains(self, mouseevent: MouseEvent) -> tuple[bool, dict[Any, Any]]: ...
def suptitle(self, t: str, **kwargs) -> Text: ...
def get_suptitle(self) -> str: ...
Expand All @@ -81,7 +81,7 @@ class FigureBase(Artist):
def frameon(self) -> bool: ...
@frameon.setter
def frameon(self, b: bool) -> None: ...
def add_artist(self, artist: Artist, clip: bool = ...) -> Artist: ...
def add_artist(self, artist: Artist, clip: bool = ..., *, layer: str | None = ...) -> Artist: ...
@overload
def add_axes(self, ax: Axes) -> Axes: ...
@overload
Expand Down
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