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) + pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy