Search code examples
pythontkinterdelaymatplotlib-animation

Tkinter and animation.FuncAnimation accumulating delay and freeze GUI


I am working on a Tkinter project where I am plotting live data from different sensors. I am using Tkinter to create the GUI and matplotlib animation.FuncAnimation to create live plots that update every given time (i.e. 1 second). The code is written in python 3.

This work fine as long as the total number of point is small. As the number of points exceed 300-400 the system starts accumulating delay, slows down and eventually freezes. I.e. if I aim to have a reading every 1 second, in the beginning the system returns a reading every 1 second and few ms. However as the time goes by, it starts increasing the interval with a linear trend (please see image below)

I can reproduce the problem by creating a single plot having on the x-axis the number of iteration (i.e. readings) and on the y-axis the delta time between each iteration and updating the plot every second (even if I use a longer time interval the result is the same).

I have tried to put the animation function in an independent thread, as it was suggested in other posts, but it did not help at all.

If I do not create the plot but I use the generated data (i.e. delta time) to update labels in tkinter I do not have the issue, so it must be related with the creation/update of the plot.

Switching on/off blit does not help, and I would prefer to keep it off anyway.

Please see below a short version of the code to reproduce the error and the output image after 600 iterations.

import tkinter as tk

import matplotlib.pyplot as plt
from matplotlib.backends import backend_tkagg as bk
import matplotlib.animation as animation

import numpy as np

import time
import threading 

class Application(tk.Frame):
    def __init__(self, master=None, **kwargs):
        tk.Frame.__init__(self, master, **kwargs)
       
# =============================================================================
# # Test 1 flags initialization
# =============================================================================
        self.ani = None
        
# =============================================================================
# Canvas frame
# =============================================================================
        self.fig = plt.figure(figsize=(15,5))
        frm = tk.Frame(self)
        frm.pack()

        self.canvas = bk.FigureCanvasTkAgg(self.fig, master=frm)
        self.canvas.get_tk_widget().pack()
        
# =============================================================================
# # Figure initialization
# =============================================================================
        self.ax = self.fig.add_subplot(1,1,1)
        self.ax.plot([],[],'-k', label='delta time')
        self.ax.legend(loc='upper right')
        self.ax.set_xlabel('n of readings [-]')
        self.ax.set_ylabel('time difference between readings [s]')
        
# =============================================================================
# # Start/Quick button
# =============================================================================
        frm4 = tk.Frame(self)
        frm4.pack(side='top', fill='x')

        frm_acquisition = tk.Frame(frm4)
        frm_acquisition.pack()
        self.button= tk.Button(frm_acquisition, text="Start acquisition", command=lambda: self.on_click(), bg='green')
        self.button.grid(column = 0, row=0)

# =============================================================================
# # Methods
# =============================================================================
    def on_click(self):
        '''the button is a start/stop button ''' 
        if self.ani is None:
                self.button.config(text='Quit acquisition', bg='red')
                print('acquisition started')
                return self.start()
        else: 
            self.ani.event_source.stop()
            self.button.config(text='Start acquisition', bg='green')
            print('acquisition stopped')
            self.ani = None
            return
    
    def start(self):
        self.starting_time = time.time()
        self.time = np.array([]) 
        self.ani = animation.FuncAnimation(self.fig, self.animate_thread, interval =1000, blit=False, cache_frame_data=False) #interval in ms     
        self.ani._start()
        return  
    
    # Some post suggested to  put animate() in an indipendent thread, but did not solve the problem
    def animate_thread(self, *args):
        self.w = threading.Thread(target=self.animate)
        self.w.daemon = True  # Daemonize the thread so it exits when the main program finishes
        self.w.start()
        return self.w


    def animate(self, *args):
        self.time = np.append(self.time, time.time()-self.starting_time)
        if len(self.time) > 1:
            self.ax.scatter(len(self.time),self.time[-1]-self.time[-2], c='k')
        # root.update() # another post suggested root.update() but did not help either
        return self.ax,

if __name__ == "__main__":
    root = tk.Tk()
    app=Application(root)
    app.pack()
    root.mainloop()

Time delay plot:

Time delay plot


Solution

  • Most of the examples matplotlib animation suggests creating the plot axes during intialization of animation instead of creating inside animate function, as your above part of code shows.

    So a possible way can be create scatter object and hold it in a variable which is intialized once. it actually returns matplotlib collections. Something like this for example,

        self.ax = self.fig.add_subplot(1,1,1)
        self.ax.plot([],[],'-k', label='delta time')
        self.ax.legend(loc='upper right')
        self.ax.set_xlabel('n of readings [-]')
        self.ax.set_ylabel('time difference between readings [s]')
        self.scatplot = self.ax.scatter(x, y, c='k') #here x and y are array data initalized empty.
    

    And in animate function, you can utilize 'self.scatplot' like this below with proper data format and customization, Refer here How to animate a scatter plot and FuncAnimation

        def animate(self, *args):
            self.time = np.append(self.time, time.time()-self.starting_time)
            data = np.stack([len(self.time), self.time[-1]-self.time[-2]])
            if len(self.time) > 1:
                self.scatplot.set_offsets(data)
            return self.scatplot,