Skip to content

Add blitting to Slider widgets #23439

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: main
Choose a base branch
from
Draft
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
107 changes: 87 additions & 20 deletions lib/matplotlib/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ class SliderBase(AxesWidget):
For the slider to remain responsive you must maintain a reference to it.
"""
def __init__(self, ax, orientation, closedmin, closedmax,
valmin, valmax, valfmt, dragging, valstep):
valmin, valmax, valfmt, dragging, valstep, useblit):
if ax.name == '3d':
raise ValueError('Sliders cannot be added to 3D Axes')

Expand Down Expand Up @@ -265,6 +265,11 @@ def __init__(self, ax, orientation, closedmin, closedmax,
ax.set_axis_off()
ax.set_navigate(False)

self._useblit = useblit and self.canvas.supports_blit
if self._useblit:
self._background = None
self.connect_event("draw_event", self._clear)

self.connect_event("button_press_event", self._update)
self.connect_event("button_release_event", self._update)
if dragging:
Expand Down Expand Up @@ -301,6 +306,18 @@ def reset(self):
if np.any(self.val != self.valinit):
self.set_val(self.valinit)

def _blit_draw(self, artists):
if not self.drawon:
return
if not self._useblit or self._background is None:
self.ax.figure.canvas.draw_idle()
return

self.canvas.restore_region(self._background)
for a in artists:
self.ax.draw_artist(a)
self.canvas.blit(self.ax.get_tightbbox())


class Slider(SliderBase):
"""
Expand All @@ -320,7 +337,8 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt=None,
closedmin=True, closedmax=True, slidermin=None,
slidermax=None, dragging=True, valstep=None,
orientation='horizontal', *, initcolor='r',
track_color='lightgrey', handle_style=None, **kwargs):
track_color='lightgrey', handle_style=None, useblit=False,
**kwargs):
"""
Parameters
----------
Expand Down Expand Up @@ -390,6 +408,13 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt=None,
`~.Line2D` constructor. e.g. ``handle_style = {'style'='x'}`` will
result in ``markerstyle = 'x'``.

useblit : bool, default: False
Use blitting for faster drawing if supported by the backend.
See the tutorial :doc:`/tutorials/advanced/blitting` for details.

If you enable blitting, you should set *valfmt* to a fixed-width
format, or there may be artifacts if the text changes width.

Notes
-----
Additional kwargs are passed on to ``self.poly`` which is the
Expand All @@ -398,7 +423,7 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt=None,
``edgecolor``, ``alpha``, etc.).
"""
super().__init__(ax, orientation, closedmin, closedmax,
valmin, valmax, valfmt, dragging, valstep)
valmin, valmax, valfmt, dragging, valstep, useblit)

if slidermin is not None and not hasattr(slidermin, 'val'):
raise ValueError(
Expand All @@ -419,6 +444,7 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt=None,
marker_props = {
f'marker{k}': v for k, v in {**defaults, **handle_style}.items()
}
animated_style = {'animated': True} if self._useblit else {}

if orientation == 'vertical':
self.track = Rectangle(
Expand All @@ -427,11 +453,14 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt=None,
facecolor=track_color
)
ax.add_patch(self.track)
self.poly = ax.axhspan(valmin, valinit, .25, .75, **kwargs)
self.poly = ax.axhspan(valmin, valinit, .25, .75, **animated_style,
**kwargs)
# Drawing a longer line and clipping it to the track avoids
# pixelation-related asymmetries.
self.hline = ax.axhline(valinit, 0, 1, color=initcolor, lw=1,
clip_path=TransformedPatchPath(self.track))
self._line = self.hline = ax.axhline(
valinit, 0, 1, color=initcolor, lw=1,
clip_path=TransformedPatchPath(self.track),
**animated_style)
handleXY = [[0.5], [valinit]]
else:
self.track = Rectangle(
Expand All @@ -440,15 +469,19 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt=None,
facecolor=track_color
)
ax.add_patch(self.track)
self.poly = ax.axvspan(valmin, valinit, .25, .75, **kwargs)
self.vline = ax.axvline(valinit, 0, 1, color=initcolor, lw=1,
clip_path=TransformedPatchPath(self.track))
self.poly = ax.axvspan(valmin, valinit, .25, .75, **animated_style,
**kwargs)
self._line = self.vline = ax.axvline(
valinit, 0, 1, color=initcolor, lw=1,
clip_path=TransformedPatchPath(self.track),
**animated_style)
handleXY = [[valinit], [0.5]]
self._handle, = ax.plot(
*handleXY,
"o",
**marker_props,
clip_on=False
clip_on=False,
**animated_style
)

if orientation == 'vertical':
Expand All @@ -459,7 +492,8 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt=None,
self.valtext = ax.text(0.5, -0.02, self._format(valinit),
transform=ax.transAxes,
verticalalignment='top',
horizontalalignment='center')
horizontalalignment='center',
**animated_style)
else:
self.label = ax.text(-0.02, 0.5, label, transform=ax.transAxes,
verticalalignment='center',
Expand All @@ -468,10 +502,22 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt=None,
self.valtext = ax.text(1.02, 0.5, self._format(valinit),
transform=ax.transAxes,
verticalalignment='center',
horizontalalignment='left')
horizontalalignment='left',
**animated_style)

self.set_val(valinit)

def _clear(self, event):
"""Internal event handler to refresh the blitting background."""
if self.ignore(event):
return
area = self.ax.get_tightbbox()
self._background = self.canvas.copy_from_bbox(area)
artists = [self.poly, self._line, self.valtext, self._handle]
for a in artists:
self.ax.draw_artist(a)
self.canvas.blit(self.ax.get_tightbbox())

def _value_in_bounds(self, val):
"""Makes sure *val* is with given bounds."""
val = self._stepped_value(val)
Expand Down Expand Up @@ -549,8 +595,7 @@ def set_val(self, val):
self._handle.set_xdata([val])
self.poly.xy = xy
self.valtext.set_text(self._format(val))
if self.drawon:
self.ax.figure.canvas.draw_idle()
self._blit_draw([self.poly, self._line, self.valtext, self._handle])
self.val = val
if self.eventson:
self._observers.process('changed', val)
Expand Down Expand Up @@ -603,6 +648,7 @@ def __init__(
orientation="horizontal",
track_color='lightgrey',
handle_style=None,
useblit=False,
**kwargs,
):
"""
Expand Down Expand Up @@ -662,6 +708,13 @@ def __init__(
`~.Line2D` constructor. e.g. ``handle_style = {'style'='x'}`` will
result in ``markerstyle = 'x'``.

useblit : bool, default: False
Use blitting for faster drawing if supported by the backend.
See the tutorial :doc:`/tutorials/advanced/blitting` for details.

If you enable blitting, you should set *valfmt* to a fixed-width
format, or there may be artifacts if the text changes width.

Notes
-----
Additional kwargs are passed on to ``self.poly`` which is the
Expand All @@ -670,7 +723,7 @@ def __init__(
``edgecolor``, ``alpha``, etc.).
"""
super().__init__(ax, orientation, closedmin, closedmax,
valmin, valmax, valfmt, dragging, valstep)
valmin, valmax, valfmt, dragging, valstep, useblit)

# Set a value to allow _value_in_bounds() to work.
self.val = [valmin, valmax]
Expand All @@ -689,6 +742,7 @@ def __init__(
marker_props = {
f'marker{k}': v for k, v in {**defaults, **handle_style}.items()
}
animated_style = {'animated': True} if self._useblit else {}

if orientation == "vertical":
self.track = Rectangle(
Expand All @@ -710,7 +764,7 @@ def __init__(
poly_transform = self.ax.get_xaxis_transform(which="grid")
handleXY_1 = [valinit[0], .5]
handleXY_2 = [valinit[1], .5]
self.poly = Polygon(np.zeros([5, 2]), **kwargs)
self.poly = Polygon(np.zeros([5, 2]), **animated_style, **kwargs)
self._update_selection_poly(*valinit)
self.poly.set_transform(poly_transform)
self.poly.get_path()._interpolation_steps = 100
Expand All @@ -721,13 +775,15 @@ def __init__(
*handleXY_1,
"o",
**marker_props,
clip_on=False
clip_on=False,
**animated_style
)[0],
ax.plot(
*handleXY_2,
"o",
**marker_props,
clip_on=False
clip_on=False,
**animated_style
)[0]
]

Expand All @@ -748,6 +804,7 @@ def __init__(
transform=ax.transAxes,
verticalalignment="top",
horizontalalignment="center",
**animated_style
)
else:
self.label = ax.text(
Expand All @@ -766,11 +823,22 @@ def __init__(
transform=ax.transAxes,
verticalalignment="center",
horizontalalignment="left",
**animated_style
)

self._active_handle = None
self.set_val(valinit)

def _clear(self, event):
"""Internal event handler to refresh the blitting background."""
if self.ignore(event):
return
artists = [self.poly, self.valtext, *self._handles]
self._background = self.canvas.copy_from_bbox(self.ax.get_tightbbox())
for a in artists:
self.ax.draw_artist(a)
self.canvas.blit(self.ax.get_tightbbox())

def _update_selection_poly(self, vmin, vmax):
"""
Update the vertices of the *self.poly* slider in-place
Expand Down Expand Up @@ -931,8 +999,7 @@ def set_val(self, val):

self.valtext.set_text(self._format((vmin, vmax)))

if self.drawon:
self.ax.figure.canvas.draw_idle()
self._blit_draw([self.poly, self.valtext, *self._handles])
self.val = (vmin, vmax)
if self.eventson:
self._observers.process("changed", (vmin, vmax))
Expand Down