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 i
th 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:
- click on the test button: an animation will start and the frame number will be printed to the console;
- click on the plot to stop the animation: frame number will stop printing;
- 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
- I tried to remove the
QtCore.QEventLoop()
, but the behaviour persists; - I tried to
del self._ani
afterself._ani.event_source.stop()
, but the behaviour still persists. Even ifself._ani
was deleted (checked withhasattr
), upon window resize the animation loop restarts at the frame where it was left; - I tried to save the
cid
ofmpl_connect
and thenmpl_disconnect
after theQEventLoop
has ended, but the behaviour persists; - 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()
.