From d0d5bd140cbf0412d52e43727df9d547f454967e Mon Sep 17 00:00:00 2001 From: Mikael Tulldahl Date: Sun, 27 Nov 2022 22:08:41 +0100 Subject: [PATCH 1/3] FIX: Animation shouldn't start by itself after resizing window If event_source was stopped, it should remain stopped after resize event --- lib/matplotlib/animation.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index aba35659d882..13399372e7c8 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -1215,6 +1215,7 @@ def _on_resize(self, event): # we're paused. Reset the cache and re-init. Set up an event handler # to catch once the draw has actually taken place. self._fig.canvas.mpl_disconnect(self._resize_id) + self._was_stopped = self.event_source._timer is None self.event_source.stop() self._blit_cache.clear() self._init_draw() @@ -1225,7 +1226,8 @@ def _end_redraw(self, event): # Now that the redraw has happened, do the post draw flushing and # blit handling. Then re-enable all of the original events. self._post_draw(None, False) - self.event_source.start() + if not self._was_stopped: + self.event_source.start() self._fig.canvas.mpl_disconnect(self._resize_id) self._resize_id = self._fig.canvas.mpl_connect('resize_event', self._on_resize) From 3df1db3b2d02db90b2b4569813d8f32a56c19605 Mon Sep 17 00:00:00 2001 From: Mikael Tulldahl Date: Sun, 27 Nov 2022 22:17:33 +0100 Subject: [PATCH 2/3] Make Button and Slider support blitting --- lib/matplotlib/widgets.py | 89 ++++++++++++++++++++++++++++++++++++--- 1 file changed, 83 insertions(+), 6 deletions(-) diff --git a/lib/matplotlib/widgets.py b/lib/matplotlib/widgets.py index 24a5bcad98d5..055e80068e01 100644 --- a/lib/matplotlib/widgets.py +++ b/lib/matplotlib/widgets.py @@ -152,7 +152,7 @@ class Button(AxesWidget): """ def __init__(self, ax, label, image=None, - color='0.85', hovercolor='0.95'): + color='0.85', hovercolor='0.95', useblit=False): """ Parameters ---------- @@ -182,6 +182,8 @@ def __init__(self, ax, label, image=None, self.connect_event('button_press_event', self._click) self.connect_event('button_release_event', self._release) self.connect_event('motion_notify_event', self._motion) + self.connect_event('draw_event', self._draw) + self.connect_event('resize_event', self._clear) ax.set_navigate(False) ax.set_facecolor(color) ax.set_xticks([]) @@ -189,6 +191,20 @@ def __init__(self, ax, label, image=None, self.color = color self.hovercolor = hovercolor + self.useblit = useblit and self.canvas.supports_blit + if self.useblit: + self.label.set_animated(True) + self.ax.patch.set_animated(True) + self.background = self.canvas.copy_from_bbox(self.ax.bbox) + + def _clear(self, event): + if self.ignore(event): + return + if self.useblit: + self.label.set_visible(False) + self.ax.patch.set_visible(False) + self.background = self.canvas.copy_from_bbox(self.ax.bbox) + def _click(self, event): if self.ignore(event) or event.inaxes != self.ax or not self.eventson: return @@ -201,6 +217,7 @@ def _release(self, event): event.canvas.release_mouse(self.ax) if self.eventson and event.inaxes == self.ax: self._observers.process('clicked', event) + self._draw() def _motion(self, event): if self.ignore(event): @@ -208,8 +225,20 @@ def _motion(self, event): c = self.hovercolor if event.inaxes == self.ax else self.color if not colors.same_color(c, self.ax.get_facecolor()): self.ax.set_facecolor(c) - if self.drawon: - self.ax.figure.canvas.draw() + self._draw() + + def _draw(self, event=None): + if self.ignore(event): + return + self.label.set_visible(True) + self.ax.patch.set_visible(True) + if self.useblit: + self.canvas.restore_region(self.background) + self.ax.draw_artist(self.ax.patch) + self.ax.draw_artist(self.label) + self.canvas.blit(self.ax.bbox) + else: + self.canvas.draw_idle() def on_clicked(self, func): """ @@ -320,7 +349,7 @@ 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 ---------- @@ -400,6 +429,8 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt=None, super().__init__(ax, orientation, closedmin, closedmax, valmin, valmax, valfmt, dragging, valstep) + self.useblit = useblit and self.canvas.supports_blit + if slidermin is not None and not hasattr(slidermin, 'val'): raise ValueError( f"Argument slidermin ({type(slidermin)}) has no 'val'") @@ -420,18 +451,22 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt=None, f'marker{k}': v for k, v in {**defaults, **handle_style}.items() } + self.artists = [] if orientation == 'vertical': self.track = Rectangle( (.25, 0), .5, 1, transform=ax.transAxes, facecolor=track_color ) + self.artists.append(self.track) ax.add_patch(self.track) self.poly = ax.axhspan(valmin, valinit, .25, .75, **kwargs) # Drawing a longer line and clipping it to the track avoids # pixelation-related asymmetries. + self.artists.append(self.poly) self.hline = ax.axhline(valinit, 0, 1, color=initcolor, lw=1, clip_path=TransformedPatchPath(self.track)) + self.artists.append(self.hline) handleXY = [[0.5], [valinit]] else: self.track = Rectangle( @@ -439,10 +474,13 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt=None, transform=ax.transAxes, facecolor=track_color ) + self.artists.append(self.track) ax.add_patch(self.track) self.poly = ax.axvspan(valmin, valinit, .25, .75, **kwargs) + self.artists.append(self.poly) self.vline = ax.axvline(valinit, 0, 1, color=initcolor, lw=1, clip_path=TransformedPatchPath(self.track)) + self.artists.append(self.vline) handleXY = [[valinit], [0.5]] self._handle, = ax.plot( *handleXY, @@ -450,27 +488,48 @@ def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt=None, **marker_props, clip_on=False ) + self.artists.append(self._handle) if orientation == 'vertical': self.label = ax.text(0.5, 1.02, label, transform=ax.transAxes, verticalalignment='bottom', horizontalalignment='center') + self.artists.append(self.label) self.valtext = ax.text(0.5, -0.02, self._format(valinit), transform=ax.transAxes, verticalalignment='top', horizontalalignment='center') + self.artists.append(self.valtext) else: self.label = ax.text(-0.02, 0.5, label, transform=ax.transAxes, verticalalignment='center', horizontalalignment='right') + self.artists.append(self.label) self.valtext = ax.text(1.02, 0.5, self._format(valinit), transform=ax.transAxes, verticalalignment='center', horizontalalignment='left') + self.artists.append(self.valtext) + + self.connect_event('draw_event', self._draw) + self.connect_event('resize_event', self._clear) + if self.useblit: + for artist in self.artists: + artist.set_animated(True) + self.background = self.canvas.copy_from_bbox(self.ax.bbox) + else: + self.set_val(valinit) + + def _clear(self, event): + if self.ignore(event): + return + if self.useblit: + for artist in self.artists: + artist.set_visible(False) + self.background = self.canvas.copy_from_bbox(self.ax.bbox) - self.set_val(valinit) def _value_in_bounds(self, val): """Makes sure *val* is with given bounds.""" @@ -538,6 +597,7 @@ def set_val(self, val): ---------- val : float """ + print(f"slider set_value {val}") xy = self.poly.xy if self.orientation == 'vertical': xy[1] = .25, val @@ -549,11 +609,12 @@ def set_val(self, val): self._handle.set_xdata([val]) self.poly.xy = xy self.valtext.set_text(self._format(val)) - if self.drawon: + if self.drawon and not self.useblit: self.ax.figure.canvas.draw_idle() self.val = val if self.eventson: self._observers.process('changed', val) + self._draw() def on_changed(self, func): """ @@ -572,6 +633,22 @@ def on_changed(self, func): """ return self._observers.connect('changed', lambda val: func(val)) + def _draw(self, event=None): + if self.ignore(event): + return + print(f"blit slider _draw() {self.val}") + if event is not None: + print(event) + for artist in self.artists: + artist.set_visible(True) + if self.useblit: + self.canvas.restore_region(self.background) + for artist in self.artists: + self.ax.draw_artist(artist) + self.canvas.blit(self.ax.bbox) + else: + self.canvas.draw_idle() + class RangeSlider(SliderBase): """ From 6e5456ad6c71138ee66235b2dbd7892aee75d362 Mon Sep 17 00:00:00 2001 From: Mikael Tulldahl Date: Sun, 27 Nov 2022 22:18:18 +0100 Subject: [PATCH 3/3] Added PlayerAnimation with example --- examples/animation/animate_decay_blit.py | 51 +++++++++++ lib/matplotlib/animation.py | 107 ++++++++++++++++++++++- 2 files changed, 157 insertions(+), 1 deletion(-) create mode 100644 examples/animation/animate_decay_blit.py diff --git a/examples/animation/animate_decay_blit.py b/examples/animation/animate_decay_blit.py new file mode 100644 index 000000000000..670630c25df6 --- /dev/null +++ b/examples/animation/animate_decay_blit.py @@ -0,0 +1,51 @@ +""" +===== +Decay +===== + +This example showcases: +- using PlayerAnimation +- using blitting +- changing axes limits during an animation. +""" + +import itertools + +import numpy as np +import matplotlib.pyplot as plt + +from matplotlib.animation import PlayerAnimation, FuncAnimation +import math + +def data_gen(): + for cnt in itertools.count(): + yield cnt + +def init(val): + global line + print(f"init with val: {val}") + if not "line" in globals(): + line, = ax.plot([], [], lw=2) + ax.grid() + ax.set_ylim(-1.1, 1.1) + ax.set_xlim(0, 1 + math.floor(val / 10)) + line.set_data([], []) + return [line] + +fig, ax = plt.subplots() + +def update_plot(i): + # update the data + xdata = np.linspace(-10, 10, 1000) + ydata = np.sin(xdata + i*0.1) + _, xmax = ax.get_xlim() + new_xmax = 1 + math.floor(i / 10) + if xmax != new_xmax: + ax.set_xlim(0, new_xmax) + ax.figure.canvas.draw_idle() + line.set_data(xdata, ydata) + + return [line] + +animation = PlayerAnimation(fig=fig, func=update_plot, init_func=init, interval=100, blit=True, valstep=0.5) +plt.show() diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 13399372e7c8..698629665894 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -40,6 +40,8 @@ DISPLAY_TEMPLATE, INCLUDED_FRAMES, JS_INCLUDE, STYLE_INCLUDE) from matplotlib import _api, cbook import matplotlib.colors as mcolors +from matplotlib.widgets import Button,Slider +import mpl_toolkits.axes_grid1 _log = logging.getLogger(__name__) @@ -1225,7 +1227,7 @@ def _on_resize(self, event): def _end_redraw(self, event): # Now that the redraw has happened, do the post draw flushing and # blit handling. Then re-enable all of the original events. - self._post_draw(None, False) + self._post_draw(None, self._blit) if not self._was_stopped: self.event_source.start() self._fig.canvas.mpl_disconnect(self._resize_id) @@ -1779,3 +1781,106 @@ def _draw_frame(self, framedata): for a in self._drawn_artists: a.set_animated(self._blit) + +class PlayerAnimation(FuncAnimation): + # inspired from https://stackoverflow.com/a/46327978/3949028 + PLAY_SYMBOL = "$\u25B6$" + STOP_SYMBOL = "$\u25A0$" + PAUSE_SYMBOL = "$\u23F8$" #TODO use instead of STOP_SYMBOL, but doesn't work in Button.label + ONE_BACK_SYMBOL = "$\u29CF$" + ONE_FORWARD_SYMBOL = "$\u29D0$" + + def __init__(self, func, init_func, min_value=0, max_value=100, + pos=(0.125, 0.92), valstep=1, **kwargs): + self.val = min_value + self.min = min_value + self.max = max_value + self.direction = 1 + self.caller_func = func + self.valstep = valstep + self._player_initiated = False + #https://github.com/matplotlib/matplotlib/issues/17685 + + def init_func_wrapper(): + return init_func(self.val) + + super().__init__(func=self._func_wrapper, frames=self._frame_generator, + init_func=init_func_wrapper, **kwargs) + + self._setup_player(pos) + + def _setup_player(self, pos): + if not self._player_initiated: + self._player_initiated = True + playerax = self._fig.add_axes([pos[0], pos[1], 0.64, 0.04]) + divider = mpl_toolkits.axes_grid1.make_axes_locatable(playerax) + sax = divider.append_axes("right", size="80%", pad=0.05) + ofax = divider.append_axes("right", size="100%", pad=0.05) + sliderax = divider.append_axes("right", size="500%", pad=0.07) + self.button_oneback = Button(playerax, label=self.ONE_BACK_SYMBOL, useblit=self._blit) + self.play_pause_button = Button(sax, label=self.STOP_SYMBOL, useblit=self._blit) + self.button_oneforward = Button(ofax, label=self.ONE_FORWARD_SYMBOL, useblit=self._blit) + self.button_oneback.on_clicked(self.onebackward) + self.play_pause_button.on_clicked(self.play_pause) + self.button_oneforward.on_clicked(self.oneforward) + self.slider = Slider(sliderax, '', self.min, self.max, valinit=self.min, valstep=self.valstep, useblit=self._blit) + self.slider.on_changed(self.set_pos) + + def _frame_generator(self): + while True: + next = self.val + self.direction*self.valstep + if next >= self.min and next <= self.max: + self.val = next + print(f"yield: {self.val}") + yield self.val + else: + self.pause() + print(f"pause, yield: {self.val}") + yield self.val + + def pause(self, event=None): + super().pause() + self.direction = 0 + self.play_pause_button.label.set_text(self.PLAY_SYMBOL) + self.play_pause_button._draw() + + def resume(self, event=None): + self.direction = 1 + self.play_pause_button.label.set_text(self.STOP_SYMBOL) + self.play_pause_button._draw() + super().resume() + + def play_pause(self, event=None): + if self.direction == 0: + self.resume() + else: + self.pause() + + def oneforward(self, event=None): + self.direction = 1 + self.trigger_step() + + def onebackward(self, event=None): + self.direction = -1 + self.trigger_step() + + def set_pos(self, val): + if isinstance(self.valstep, int): + val = int(val) # slider gives float event if valstep is int + if self.val != val: + print(f"slider set_pos: {val}") + self.val = val + self.direction = 0 + self.trigger_step() + + def trigger_step(self): + for a in self._drawn_artists: + a.set_animated(True) + self._step() + self.pause() + + def _func_wrapper(self, val): + print(f"player _func_wrapper: {val}") + self.slider.set_val(val) + return self.caller_func(self.val) +