Skip to content
Merged
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
50 changes: 50 additions & 0 deletions lib/matplotlib/backend_bases.py
Original file line number Diff line number Diff line change
Expand Up @@ -1740,6 +1740,10 @@ class FigureCanvasBase:

filetypes = _default_filetypes

# global counter to assign unique ids to blit backgrounds
# see _get_blit_background_id()
_last_blit_background_id = 0

@_api.classproperty
def supports_blit(cls):
"""If this Canvas sub-class supports blitting."""
Expand All @@ -1765,6 +1769,7 @@ def __init__(self, figure=None):
# We don't want to scale up the figure DPI more than once.
figure._original_dpi = figure.dpi
self._device_pixel_ratio = 1
self._blit_backgrounds = {}
super().__init__() # Typically the GUI widget init (if any).

callbacks = property(lambda self: self.figure._canvas_callbacks)
Expand Down Expand Up @@ -1840,6 +1845,51 @@ def is_saving(self):
def blit(self, bbox=None):
"""Blit the canvas in bbox (default entire canvas)."""

@classmethod
def _get_blit_background_id(cls):
"""
Get a globally unique id that can be used to store a blit background.

Blitting support is canvas-dependent, so blitting mechanisms should
store their backgrounds in the canvas, more precisely in
``canvas._blit_backgrounds[id]``. The id must be obtained via this
function to ensure it is globally unique.

The content of ``canvas._blit_backgrounds[id]`` is not specified.
We leave this freedom to the blitting mechanism.

Blitting mechanisms must not expect that a background that they
have stored is still there at a later time. The canvas may have
been switched out, or we may add other mechanisms later that
invalidate blit backgrounds (e.g. dpi changes).
Therefore, always query as `_blit_backgrounds.get(id)` and be
prepared for a None return value.

Note: The blit background API is still experimental and may change
in the future without warning.
"""
cls._last_blit_background_id += 1
return cls._last_blit_background_id

def _release_blit_background_id(self, bb_id):
"""
Release a blit background id that is no longer needed.

This removes the respective entry from the internal storage, i.e.
the ``canvas._blit_backgrounds`` dict, and thus allows to free the
associated memory.

After releasing the id you must not use it anymore.

It is safe to release an id that has not been used with the canvas
or that has already been released.

Note: The blit background API is still experimental and may change
in the future without warning.
"""
if bb_id in self._blit_backgrounds:
del self._blit_backgrounds[bb_id]

def inaxes(self, xy):
"""
Return the topmost visible `~.axes.Axes` containing the point *xy*.
Expand Down
105 changes: 71 additions & 34 deletions lib/matplotlib/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,11 @@ class AxesWidget(Widget):
def __init__(self, ax):
self.ax = ax
self._cids = []
self._blit_background_id = None

def __del__(self):
if self._blit_background_id is not None:
self.canvas._release_blit_background_id(self._blit_background_id)

canvas = property(
lambda self: getattr(self.ax.get_figure(root=True), 'canvas', None)
Expand Down Expand Up @@ -155,6 +160,26 @@ def _set_cursor(self, cursor):
"""Update the canvas cursor."""
self.ax.get_figure(root=True).canvas.set_cursor(cursor)

def _save_blit_background(self, background):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to pass the bounding box in instead?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For symmetry reasons and to keep the changes within the PR minimal, I would leave the complete handling to the individual widgets for now.

They do

self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))

and

background = self._load_blit_background()
if background is not None:
    self.canvas.restore_region(background)

It may be reasonable to refactor in a follow-up and move that logic to the base class.

"""
Save a blit background.

The background is stored on the canvas in a uniquely identifiable way.
It should be read back via `._load_blit_background`. Be prepared that
some events may invalidate the background, in which case
`._load_blit_background` will return None.

This currently allows at most one background per widget, which is
good enough for all existing widgets.
"""
if self._blit_background_id is None:
self._blit_background_id = self.canvas._get_blit_background_id()
self.canvas._blit_backgrounds[self._blit_background_id] = background

def _load_blit_background(self):
"""Load a blit background; may be None at any time."""
return self.canvas._blit_backgrounds.get(self._blit_background_id)


class Button(AxesWidget):
"""
Expand Down Expand Up @@ -206,7 +231,7 @@ def __init__(self, ax, label, image=None,
horizontalalignment='center',
transform=ax.transAxes)

self._useblit = useblit and self.canvas.supports_blit
self._useblit = useblit

self._observers = cbook.CallbackRegistry(signals=["clicked"])

Expand Down Expand Up @@ -240,7 +265,7 @@ def _motion(self, event):
if not colors.same_color(c, self.ax.get_facecolor()):
self.ax.set_facecolor(c)
if self.drawon:
if self._useblit:
if self._useblit and self.canvas.supports_blit:
self.ax.draw_artist(self.ax)
self.canvas.blit(self.ax.bbox)
else:
Expand Down Expand Up @@ -1062,8 +1087,7 @@ def __init__(self, ax, labels, actives=None, *, useblit=True,
if actives is None:
actives = [False] * len(labels)

self._useblit = useblit and self.canvas.supports_blit
self._background = None
self._useblit = useblit and self.canvas.supports_blit # TODO: make dynamic
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this TODO necessary to complete before 3.11?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a follow-up and can land in 3.11 or later.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@timhoffm are you planning on addressing this TODO in this PR or is it a follow-up?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a follow-up and can land in 3.11 or later.


ys = np.linspace(1, 0, len(labels)+2)[1:-1]

Expand Down Expand Up @@ -1110,7 +1134,7 @@ def _clear(self, event):
"""Internal event handler to clear the buttons."""
if self.ignore(event) or self.canvas.is_saving():
return
self._background = self.canvas.copy_from_bbox(self.ax.bbox)
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))
self.ax.draw_artist(self._checks)

def _clicked(self, event):
Expand Down Expand Up @@ -1215,8 +1239,9 @@ def set_active(self, index, state=None):

if self.drawon:
if self._useblit:
if self._background is not None:
self.canvas.restore_region(self._background)
background = self._load_blit_background()
if background is not None:
self.canvas.restore_region(background)
self.ax.draw_artist(self._checks)
self.canvas.blit(self.ax.bbox)
else:
Expand Down Expand Up @@ -1649,8 +1674,7 @@ def __init__(self, ax, labels, active=0, activecolor=None, *,

ys = np.linspace(1, 0, len(labels) + 2)[1:-1]

self._useblit = useblit and self.canvas.supports_blit
self._background = None
self._useblit = useblit and self.canvas.supports_blit # TODO: make dynamic

label_props = _expand_text_props(label_props)
self.labels = [
Expand Down Expand Up @@ -1692,7 +1716,7 @@ def _clear(self, event):
"""Internal event handler to clear the buttons."""
if self.ignore(event) or self.canvas.is_saving():
return
self._background = self.canvas.copy_from_bbox(self.ax.bbox)
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))
self.ax.draw_artist(self._buttons)

def _clicked(self, event):
Expand Down Expand Up @@ -1785,8 +1809,9 @@ def set_active(self, index):

if self.drawon:
if self._useblit:
if self._background is not None:
self.canvas.restore_region(self._background)
background = self._load_blit_background()
if background is not None:
self.canvas.restore_region(background)
self.ax.draw_artist(self._buttons)
self.canvas.blit(self.ax.bbox)
else:
Expand Down Expand Up @@ -1935,22 +1960,21 @@ def __init__(self, ax, *, horizOn=True, vertOn=True, useblit=False,
self.visible = True
self.horizOn = horizOn
self.vertOn = vertOn
self.useblit = useblit and self.canvas.supports_blit
self.useblit = useblit and self.canvas.supports_blit # TODO: make dynamic

if self.useblit:
lineprops['animated'] = True
self.lineh = ax.axhline(ax.get_ybound()[0], visible=False, **lineprops)
self.linev = ax.axvline(ax.get_xbound()[0], visible=False, **lineprops)

self.background = None
self.needclear = False

def clear(self, event):
"""Internal event handler to clear the cursor."""
if self.ignore(event) or self.canvas.is_saving():
return
if self.useblit:
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))

def onmove(self, event):
"""Internal event handler to draw the cursor when the mouse moves."""
Expand All @@ -1975,8 +1999,9 @@ def onmove(self, event):
return
# Redraw.
if self.useblit:
if self.background is not None:
self.canvas.restore_region(self.background)
background = self._load_blit_background()
if background is not None:
self.canvas.restore_region(background)
self.ax.draw_artist(self.linev)
self.ax.draw_artist(self.lineh)
self.canvas.blit(self.ax.bbox)
Expand Down Expand Up @@ -2044,6 +2069,7 @@ def __init__(self, canvas, axes, *, useblit=True, horizOn=False, vertOn=True,
self.useblit = (
useblit
and all(canvas.supports_blit for canvas in self._canvas_infos))
# TODO: make dynamic

if self.useblit:
lineprops['animated'] = True
Expand Down Expand Up @@ -2128,7 +2154,7 @@ def __init__(self, ax, onselect=None, useblit=False, button=None,
self.onselect = lambda *args: None
else:
self.onselect = onselect
self.useblit = useblit and self.canvas.supports_blit
self._useblit = useblit
self.connect_default_events()

self._state_modifier_keys = dict(move=' ', clear='escape',
Expand All @@ -2137,8 +2163,6 @@ def __init__(self, ax, onselect=None, useblit=False, button=None,
self._state_modifier_keys.update(state_modifier_keys or {})
self._use_data_coordinates = use_data_coordinates

self.background = None

if isinstance(button, Integral):
self.validButtons = [button]
else:
Expand All @@ -2154,6 +2178,11 @@ def __init__(self, ax, onselect=None, useblit=False, button=None,
self._prev_event = None
self._state = set()

@property
def useblit(self):
"""Return whether blitting is used (requested and supported by canvas)."""
return self._useblit and self.canvas.supports_blit

def set_active(self, active):
super().set_active(active)
if active:
Expand Down Expand Up @@ -2194,7 +2223,7 @@ def update_background(self, event):
for artist in artists:
stack.enter_context(artist._cm_set(visible=False))
self.canvas.draw()
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self._save_blit_background(self.canvas.copy_from_bbox(self.ax.bbox))
if needs_redraw:
for artist in artists:
self.ax.draw_artist(artist)
Expand Down Expand Up @@ -2241,8 +2270,9 @@ def update(self):
self.ax.get_figure(root=True)._get_renderer() is None):
return
if self.useblit:
if self.background is not None:
self.canvas.restore_region(self.background)
background = self._load_blit_background()
if background is not None:
self.canvas.restore_region(background)
else:
self.update_background(None)
# We need to draw all artists, which are not included in the
Expand Down Expand Up @@ -2575,7 +2605,14 @@ def __init__(self, ax, onselect, direction, *, minspan=0, useblit=False,
if props is None:
props = dict(facecolor='red', alpha=0.5)

props['animated'] = self.useblit
# Note: We set this based on the user setting during ínitialization,
# not on the actual capability of blitting. But the value is
# irrelevant if the backend does not support blitting, so that
# we don't have to dynamically update this on the backend.
# This relies on the current behavior that the request for
# useblit is fixed during initialization and cannot be changed
# afterwards.
props['animated'] = self._useblit

self.direction = direction
self._extents_on_press = None
Expand Down Expand Up @@ -2641,7 +2678,7 @@ def _setup_edge_handles(self, props):
self._edge_handles = ToolLineHandles(self.ax, positions,
direction=self.direction,
line_props=props,
useblit=self.useblit)
useblit=self._useblit)

@property
def _handles_artists(self):
Expand Down Expand Up @@ -3215,7 +3252,7 @@ def __init__(self, ax, onselect=None, *, minspanx=0,
if props is None:
props = dict(facecolor='red', edgecolor='black',
alpha=0.2, fill=True)
props = {**props, 'animated': self.useblit}
props = {**props, 'animated': self._useblit}
self._visible = props.pop('visible', self._visible)
to_draw = self._init_shape(**props)
self.ax.add_patch(to_draw)
Expand All @@ -3240,18 +3277,18 @@ def __init__(self, ax, onselect=None, *, minspanx=0,
xc, yc = self.corners
self._corner_handles = ToolHandles(self.ax, xc, yc,
marker_props=self._handle_props,
useblit=self.useblit)
useblit=self._useblit)

self._edge_order = ['W', 'S', 'E', 'N']
xe, ye = self.edge_centers
self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s',
marker_props=self._handle_props,
useblit=self.useblit)
useblit=self._useblit)

xc, yc = self.center
self._center_handle = ToolHandles(self.ax, [xc], [yc], marker='s',
marker_props=self._handle_props,
useblit=self.useblit)
useblit=self._useblit)

self._active_handle = None

Expand Down Expand Up @@ -3758,7 +3795,7 @@ def __init__(self, ax, onselect=None, *, useblit=True, props=None, button=None):
**(props if props is not None else {}),
# Note that self.useblit may be != useblit, if the canvas doesn't
# support blitting.
'animated': self.useblit, 'visible': False,
'animated': self._useblit, 'visible': False,
}
line = Line2D([], [], **props)
self.ax.add_line(line)
Expand Down Expand Up @@ -3882,7 +3919,7 @@ def __init__(self, ax, onselect=None, *, useblit=False,

if props is None:
props = dict(color='k', linestyle='-', linewidth=2, alpha=0.5)
props = {**props, 'animated': self.useblit}
props = {**props, 'animated': self._useblit}
self._selection_artist = line = Line2D([], [], **props)
self.ax.add_line(line)

Expand All @@ -3891,7 +3928,7 @@ def __init__(self, ax, onselect=None, *, useblit=False,
markerfacecolor=props.get('color', 'k'))
self._handle_props = handle_props
self._polygon_handles = ToolHandles(self.ax, [], [],
useblit=self.useblit,
useblit=self._useblit,
marker_props=self._handle_props)

self._active_handle_idx = -1
Expand All @@ -3911,7 +3948,7 @@ def _get_bbox(self):

def _add_box(self):
self._box = RectangleSelector(self.ax,
useblit=self.useblit,
useblit=self._useblit,
grab_range=self.grab_range,
handle_props=self._box_handle_props,
props=self._box_props,
Expand Down Expand Up @@ -4191,7 +4228,7 @@ class Lasso(AxesWidget):
def __init__(self, ax, xy, callback, *, useblit=True, props=None):
super().__init__(ax)

self.useblit = useblit and self.canvas.supports_blit
self.useblit = useblit and self.canvas.supports_blit # TODO: Make dynamic
if self.useblit:
self.background = self.canvas.copy_from_bbox(self.ax.bbox)

Expand Down
Loading
Loading