From e8e97775f0cc239b5ab9289fca688f6221eb0605 Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Sun, 17 Jul 2022 18:19:56 -0500 Subject: [PATCH] Add blitting to Slider widgets --- lib/matplotlib/widgets.py | 107 +++++++++++++++++++++++++++++++------- 1 file changed, 87 insertions(+), 20 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 56267d12cf55..6a088103a92c 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -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') @@ -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: @@ -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): """ @@ -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 ---------- @@ -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 @@ -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( @@ -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( @@ -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( @@ -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': @@ -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', @@ -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) @@ -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) @@ -603,6 +648,7 @@ def __init__( orientation="horizontal", track_color='lightgrey', handle_style=None, + useblit=False, **kwargs, ): """ @@ -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 @@ -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] @@ -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( @@ -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 @@ -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] ] @@ -748,6 +804,7 @@ def __init__( transform=ax.transAxes, verticalalignment="top", horizontalalignment="center", + **animated_style ) else: self.label = ax.text( @@ -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 @@ -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))