1

TL;DR:

matplotlib.backends.backend_qt5.TimerQT seeems to hold a reference to animation objects that have been previously run, even after using animation.event_source.stop(). Upon resizing of the application window, the animation loop resumes from where it was left. del self.animation does not help. How I can avoid this?

Context

I'm writing a GUI Python application (PyQt5, Python 3.8, Matplotlib 3.3.4) which allows the user to plot and analyze some data. Part of the analysis requires the user to select a range of the plotted data. I'm using matplotlib animation to show in realtime the selected data points and other relevant info:

Everything works as expected: once the user has ended the interaction with the plot, the animation stops. Unfortunately, if the user resizes the window, the animation loop resumes where it was left (checked by printing the ith frame number). Here is a short gif to show to illustrate problematic behaviour.

If multiple animations have been performed before resizing, upon window resize all of these animations will start running concurrently.

Sample Code

Here is a code snippet that can be run to reproduce the described behaviour:

import sys
import matplotlib
from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg
from matplotlib.figure import Figure
import matplotlib.animation as animation

from PyQt5 import QtCore, QtWidgets

matplotlib.use("Qt5Agg")


class MplCanvas(FigureCanvasQTAgg):
    def __init__(self, parent=None, width=5, height=4, dpi=100):
        fig = Figure(figsize=(width, height), dpi=dpi)
        self.axes = fig.add_subplot(111)

        super().__init__(fig)

        self._ani = None
        self.plots = None
        self.loop = None

    def animation(self):
        self.plot = [self.axes.plot([], [])[0]]

        self._ani = animation.FuncAnimation(
            self.figure,
            self._animate_test,
            init_func=self._init_test,
            interval=40,
            blit=True,
        )
        self.figure.canvas.mpl_connect("button_press_event", self._on_mouse_click)

        self.loop = QtCore.QEventLoop()
        self.loop.exec()

        self._ani.event_source.stop()

    def _init_test(self):
        return self.plot

    def _animate_test(self, i):
        print(f"Running animation with frame {i}")

        return self.plot

    def _on_mouse_click(self, event):
        self.loop.quit()


class MainWindow(QtWidgets.QMainWindow):
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)
        self.setObjectName("MainWindow")
        self.resize(1000, 600)

        self.centralwidget = QtWidgets.QWidget()
        self.horizontal_layout = QtWidgets.QHBoxLayout()

        self.centralwidget.setLayout(self.horizontal_layout)

        self.mpl_canvas = MplCanvas(self.centralwidget)
        self.button = QtWidgets.QToolButton(self.centralwidget)
        self.button.setText("Test button")

        self.horizontal_layout.addWidget(self.mpl_canvas)
        self.horizontal_layout.addWidget(self.button)

        self.button.clicked.connect(self._button_clicked)

        self.setCentralWidget(self.centralwidget)

    def _button_clicked(self):
        self.mpl_canvas.animation()


app = QtWidgets.QApplication(sys.argv)

window = MainWindow()
window.show()
app.exec()

sys.exit()

After you run this snippet, you can reproduce the behaviour as follows:

  1. click on the test button: an animation will start and the frame number will be printed to the console;
  2. click on the plot to stop the animation: frame number will stop printing;
  3. Resize the windows: the frame number should resume printing starting from the last frame from the previous animation.

My hypothesis

I suspect matplotlib.backends.backend_qt5.TimerQT holds a reference (weakref?) to the animation that have been previously started. Upon a window resize event, perhaps a redraw is requested and all previous animations restart. It is my understanding from a previous question that the self._ani.event_source.stop() should de-register the animation object from the timer callback, but for some reason here it's not working.

What I have tried

  1. I tried to remove the QtCore.QEventLoop(), but the behaviour persists;
  2. I tried to del self._ani after self._ani.event_source.stop(), but the behaviour still persists. Even if self._ani was deleted (checked with hasattr), upon window resize the animation loop restarts at the frame where it was left;
  3. I tried to save the cid of mpl_connect and then mpl_disconnect after the QEventLoop has ended, but the behaviour persists;
  4. Searched Stackoverflow for similar questions but could not find any similar case except the one cited above.

Help

I do not know what else to try at the moment, any suggestion would be appreciated! I'm quite new to Qt, Python in general and Stackoverlow, so please let me know if I should clarify anything further. I can provide the full code of the application if needed and also objgraph inspection of the animation object after self._ani.event_source.stop().

  • please provide a [mre] – eyllanesc Apr 25 '21 at 18:21
  • Hi @eyllanesc, thank you for the comment. I was just doing that and I have updated the question accordingly! Thanks and let me know if I can provide any further information. – Mattia Felice Palermo Apr 25 '21 at 18:27
  • I have a question: what do you use the QEventLoop for? – eyllanesc Apr 25 '21 at 18:29
  • I'm using it to wait for the user to complete some interaction with the plot. In this way the output of the interaction is then returned from the animation function. Perhaps there is a more elegant way? For troubleshooting I tried to remove the QEventLoop and the problem still persists. – Mattia Felice Palermo Apr 25 '21 at 18:32
  • I just tested the code you provide and it doesn't show anything, have you tested that code? – eyllanesc Apr 25 '21 at 18:33
  • I just copied/pasted the code back to a new file in my spyder editor and run it, and it works... I don't know where the issue could be? – Mattia Felice Palermo Apr 25 '21 at 18:34
  • I get the following: https://i.imgur.com/GevSQOI.png and it prints something like: `Running animation with frame ...` – eyllanesc Apr 25 '21 at 18:36
  • Exactly, the test code is not expected to show anything in the plot, as the animate function is empty. If you click the test button, an (empty) animation should start and "Running animation with frame ..." starts printing. Then, if you click on the empty plot, the (empty) animation should stop and "Running animation with frame..." should stop. If you then try to resize the window, the animation and the printing resumes by itself - and this is not expected! Let me know if something is not clear and thank you for trying this out. – Mattia Felice Palermo Apr 25 '21 at 18:42
  • I already understood your MRE. And I have an explanation for the bug: The animation has a generator that returns a new step of the animation every time the canvas is repainted, and what happens is that just when you change the size it happens. I don't think it's a bug since I don't think FuncAnimation was created to stop or pause the animation (and that's what you try). I have minimized the effect in the following script: https://gist.github.com/eyllanesc/ce5b14a03550cb4c1a4607d26e935f47, – eyllanesc Apr 25 '21 at 18:59
  • but my recommendation is that you implement a custom FuncAnimation for your needs where the pause is implemented. – eyllanesc Apr 25 '21 at 18:59
  • @eyllanesc thank you for the answer. I have tried your code but it seems the issue is still present? I believe the animation start/pause is already implemented in Funcanimation, with animation.event_source.start() and animation.event_source.stop() (see here https://stackoverflow.com/questions/32280140/cannot-delete-matplotlib-animation-funcanimation-objects). In fact, I can pause the animation in the sample snippet. I think something more subtle is going on here, since even if I delete the animation object, the animation restarts upon window resizing. What do you think? Thanks a lot. – Mattia Felice Palermo Apr 25 '21 at 19:14
  • I have implemented an animation that if it pauses (not only pauses the timer but the iterator): https://gist.github.com/eyllanesc/ce5b14a03550cb4c1a4607d26e935f47. Some feedback? – eyllanesc Apr 25 '21 at 19:23
  • @eyllanesc wow thanks so much, it definitely works! It also allows to resize the window during an animation without running into strange bugs, great! Unfortunately I do not understand *how* it works, cause my knowledge of the inner workings of matplotlib (and Qt) is rather scarce. I guess it would be great, if you have the time and the will, to post an answer where you also explain how this works :) But many thanks in any case! P.s.: wouldn't it be great to implement this modification directly in the matplotlib library? Or perhaps I am misunderstanding how FuncAnimation should be used? – Mattia Felice Palermo Apr 25 '21 at 20:45

2 Answers2

1

Had the same problem, my workaround was to create a global flag pausePlot which I set whenever user presses the pause button in my GUI which I then read in my updatePlot function:

    def _animate_test(self, i):
        global pausePlot
        if not pausePlot:
            print(f"Running animation with frame {i}")
        return self.plot

This way even though the animation is always running, my update function does not update any plot lines when the pause flag is asserted and I can resize the window without the plot animation auto-resuming.

Michael
  • 11
  • 2
0

I know it is been a while but I have encounter the exact same problem and it was turning me crazy. To give some context, I have a class that reads a bunch of csv files, each of them with some interesting information to fill a series of scans (like frames in a video). Then I have a player very inspired in this answer. The key point is that the player is a class that inherits from FuncAnimation (the same that you use).

Now, whenever you resize your window, matplotlib backend have to perform a set of operations to guarantee that everything remains as it is supposed to. More precisely, the FuncAnimation has (well, a parent of it) a specific method (_on_resize(self, event)) that is called every time you resize the window. It basically stops the animation, clear the cache, do some stuffs with the events handlers and starts again the animation (from the beginning). The implementation of this functions reads as follow (see the source code for mor info):

    def _on_resize(self, event):
        # On resize, we need to disable the resize event handling so we don't
        # get too many events. Also stop the animation events, so that
        # 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.event_source.stop()
        self._blit_cache.clear()
        self._init_draw()
        self._resize_id = self._fig.canvas.mpl_connect('draw_event',
                                                       self._end_redraw)
    
    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()
        self._fig.canvas.mpl_disconnect(self._resize_id)
        self._resize_id = self._fig.canvas.mpl_connect('resize_event',
                                                       self._on_resize)

So the way I got around the problem of keeping the state of the player after resizing was redefining this methods in my own player class. This will be different for your case but should not be hard. You have to somehow be able to store the state of your animation. For instance, in my implementation I have an attribute in the class storing the frame I am in (self.i) and also a method to do a fresh plot of that frame (self.set_pos(i) which also sets internally self.i = i). With this into consideration here is how I changed the methods to restore the state after each resizing:

def _on_resize(self, event):
    i = self.i
    self._fig.canvas.mpl_disconnect(self._resize_id)
    self.event_source.stop()
    self._blit_cache.clear()
    self._init_draw()
    if self.paused:
        self.pause()
    self.set_pos(i)
    self._resize_id = self._fig.canvas.mpl_connect('draw_event',
                                                   self._end_redraw)

def _end_redraw(self, event):
    i = self.i
    self._post_draw(None, False)
    self.event_source.start()
    if self.paused:
        self.pause()
    self.set_pos(i)
    self._fig.canvas.mpl_disconnect(self._resize_id)
    self._resize_id = self._fig.canvas.mpl_connect('resize_event',
                                                   self._on_resize)

I wish this can help other people finding the same issue in the future!

Txema
  • 16
  • 1