Tkinter and animation.FuncAnimation accumulating delay and freeze GUI

63 Views Asked by At

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

1

There are 1 best solutions below

0
Sai_MSP On BEST ANSWER

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,