diff --git a/doc/release/next_whats_new/animation_paused_start.rst b/doc/release/next_whats_new/animation_paused_start.rst new file mode 100644 index 000000000000..6d8a3303e969 --- /dev/null +++ b/doc/release/next_whats_new/animation_paused_start.rst @@ -0,0 +1,14 @@ +Animations can now be created in a paused state +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +'.Animation' now accepts a 'paused' keyword argument. When set to 'True', +the animation will not automatically start when the figure is first drawn. +This allows starting an animation in a paused state without needing to +access private API. Additionally, '.TimerBase.is_running' was added to +query whether the underlying event source is currently running. + +:: + + ani = FuncAnimation(fig, update, frames=10, paused=True) + ani.event_source.is_running() # False + ani.resume() + ani.event_source.is_running() # True diff --git a/lib/matplotlib/animation.py b/lib/matplotlib/animation.py index 7146dc28fcc9..153867acc0a3 100644 --- a/lib/matplotlib/animation.py +++ b/lib/matplotlib/animation.py @@ -883,9 +883,9 @@ class Animation: FuncAnimation, ArtistAnimation """ - def __init__(self, fig, event_source, blit=False): + def __init__(self, fig, event_source, blit=False, *, paused=False): self._draw_was_started = False - + self._paused_at_start = paused self._fig = fig # Disables blitting for backends that don't support it. This # allows users to request it if available, but still have a @@ -934,7 +934,8 @@ def _start(self, *args): # Now do any initial draw self._init_draw() # Actually start the event_source. - self.event_source.start() + if not self._paused_at_start: + self.event_source.start() def _stop(self, *args): # On stop we disconnect all of our events. diff --git a/lib/matplotlib/animation.pyi b/lib/matplotlib/animation.pyi index e90a0103aefd..52da52b9a92c 100644 --- a/lib/matplotlib/animation.pyi +++ b/lib/matplotlib/animation.pyi @@ -162,7 +162,12 @@ class Animation: frame_seq: Iterable[Artist] event_source: EventSourceProtocol | None # TODO: We should remove None def __init__( - self, fig: Figure, event_source: EventSourceProtocol, blit: bool = ... + self, + fig: Figure, + event_source: EventSourceProtocol, + blit: bool = ..., + *, + paused: bool = ..., ) -> None: ... def __del__(self) -> None: ... def save( diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index 27c3752858a7..99e472e41d86 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -1072,6 +1072,7 @@ def __init__(self, interval=None, callbacks=None): # Set .interval and not ._interval to go through the property setter. self.interval = 1000 if interval is None else interval self.single_shot = False + self._running = False def __del__(self): """Need to stop timer and possibly disconnect timer.""" @@ -1080,10 +1081,16 @@ def __del__(self): def start(self): """Start the timer.""" self._timer_start() + self._running = True def stop(self): """Stop the timer.""" self._timer_stop() + self._running = False + + def is_running(self): + """Return whether the timer is currently running.""" + return self._running def _timer_start(self): pass diff --git a/lib/matplotlib/backend_bases.pyi b/lib/matplotlib/backend_bases.pyi index 94a8522717cd..f63bf517cf99 100644 --- a/lib/matplotlib/backend_bases.pyi +++ b/lib/matplotlib/backend_bases.pyi @@ -200,6 +200,7 @@ class TimerBase: def __del__(self) -> None: ... def start(self) -> None: ... def stop(self) -> None: ... + def is_running(self) -> bool: ... @property def interval(self) -> int: ... @interval.setter diff --git a/lib/matplotlib/tests/test_animation.py b/lib/matplotlib/tests/test_animation.py index a00adcdf95f0..3bc3c34e13ac 100644 --- a/lib/matplotlib/tests/test_animation.py +++ b/lib/matplotlib/tests/test_animation.py @@ -558,3 +558,33 @@ def test_animation_with_transparency(): # Check that the alpha channel is not 255, so frame has transparency assert frame.getextrema()[3][0] < 255 plt.close(fig) + + +def test_animation_paused_start(): + """Test that paused=True prevents the animation from auto-starting.""" + fig, ax = plt.subplots() + line, = ax.plot([], []) + + def update(frame): + return line, + + ani = animation.FuncAnimation(fig, update, frames=5, paused=True) + fig.canvas.draw() # triggers the first draw_event / _start callback + + assert not ani.event_source.is_running() + plt.close(fig) + + +def test_animation_default_starts(): + """Test that the default paused=False still auto-starts.""" + fig, ax = plt.subplots() + line, = ax.plot([], []) + + def update(frame): + return line, + + ani = animation.FuncAnimation(fig, update, frames=5) + fig.canvas.draw() + + assert ani.event_source.is_running() + plt.close(fig)