2

I would like to interactively change a matplotlib.animation argument depending on the value provided by a GUI.

Example:

  • I prepared an example code which I show below, where I am trying to change the interval argument of animation based on a value provided by the user through a spinBox created with tkinter.

Problem:

  • In order to be able to update its argument, I want to call my animation into the call back function called by the spinbox. But if I do that, I get the following error message " UserWarning: Animation was deleted without rendering anything. This is most likely unintended. To prevent deletion, assign the Animation to a variable that exists for as long as you need the Animation."
  • If I call my animation into the main code, then I won't be able to interactively change its arguments

Question:

  • How can I change an animation argument interactively, i.e. based on a value which the user can set in a tkinter widget?

Example code:

import tkinter as tk
from random import randint
import matplotlib as plt
import matplotlib.animation as animation
import matplotlib.backends.backend_tkagg as tkagg


#Creating an instance of the Tk class
win = tk.Tk() 

#Creating an instance of the figure class
fig = plt.figure.Figure() 
#Create a Canvas containing fig into win
aCanvas =tkagg.FigureCanvasTkAgg(fig, master=win)
#Making the canvas a tkinter widget
aFigureWidget=aCanvas.get_tk_widget()
#Showing the figure into win as if it was a normal tkinter widget
aFigureWidget.grid(row=0, column=0)


#Defining the animation
ax = fig.add_subplot(xlim=(0, 1),  ylim=(0, 1))
(line,) = ax.plot([],[], '-')
CumulativeX, CumulativeY = [], []



# Providing the input data for the plot for each animation step
def update(i):
    CumulativeX.append(randint(0, 10) / 10)
    CumulativeY.append(randint(0, 10) / 10)
    return line.set_data(CumulativeX, CumulativeY)

spinBoxValue=1000
    
#When the button is pushed, get the value
def button():
    spinBoxValue=aSpinbox.get()

#Running the animation
ani=animation.FuncAnimation(fig, update, interval=spinBoxValue,  repeat=True)

#Creating an instance of the Spinbox class
aSpinbox = tk.Spinbox(master=win,from_=0, to=1000, command=button)
#Placing the button
aSpinbox .grid(row=2, column=0)


#run the GUI
win.mainloop()
Francesco
  • 111
  • 9
  • Could you clarify if you want to just start animation from the beginning when a new input is obtained or is there any other relationship of button with the animation? – medium-dimensional Oct 21 '22 at 16:30
  • In my full code, I use tkinter to provide a number of inputs. Once I click a tkinter button, I want to a) clear completely the previous animation, b) I want to use some of the inputs to compute new lists that will modify the line.set_data (the values in CumulativeX and CumulativeY, if you wish) and c) the remaining inputs have to change the arguments of the animate function itself. – Francesco Oct 21 '22 at 20:25

1 Answers1

2

We have to redraw the animation using fig.canvas.draw() when the animation is created inside the function button:

def button():
    global spinBoxValue, CumulativeX, CumulativeY, ani
    spinBoxValue = aSpinbox.get()
    CumulativeX, CumulativeY = [], [] # This is optional
    
    # To stop the background animation
    ani.event_source.stop()
    
    # Unlink/delete the reference to the previous animation 
    # del ani

    ani=animation.FuncAnimation(fig, update, interval=int(spinBoxValue) * 1000,  repeat=False)
    fig.canvas.draw()

In the code provided, it was drawing the lines too fast when it was recreating animation using the value from aSpinbox.get(), so I changed the input to integer to draw the animation at a slower rate using interval=int(spinBoxvalue) * 1000 inside the button function.

On deleting the animation

Since we have to stop the background animation and also run the newly generated animation when the button is pressed, and because an animation must be stored in a variable as long as it runs, we will have to refer to the previous and the latest animation by the same variable name.

We can delete the animation stored in the global variable ani, using del ani after ani.event_source.stop(), which would lose the reference to the animation stored in memory before the button was pressed, but we can't really free the memory address where the reference by ani was made (I am guessing this would be true as long as we are using default garbage collection method in Python).


EDIT

Jumping to a new animation will not update/remove any variables created on the axes here - we will have to take care of it explicitly. To update variables only once after pressing the button, first create those variables in the global scope of code, and delete them inside button function and recreate/define them before/after using fig.canvas.draw:

# Defined in global scope
text = ax.text(0.7, 0.5, "text")

def button():
    global spinBoxValue, CumulativeX, CumulativeY, ani, text
    spinBoxValue = int(aSpinbox.get())

    # To stop the background animation
    ani.event_source.stop()

    CumulativeX, CumulativeY = [], []   
    
    # Unlink/delete the reference to the previous animation 
    # del ani

    text.remove()
    text = ax.text(0.7 * spinBoxValue/10 , 0.5, "text")

    ani=animation.FuncAnimation(fig, update, interval=spinBoxValue*1000, repeat=False)
    fig.canvas.draw()

The same logic can be applied to use update function to redraw text after every button press or after every frame while using the function button provided at the very top:

text = ax.text(0.7, 0.5, "text")

# Providing the input data for the plot for each animation step
def update(i):
    global text 
    text.remove()

    # Update text after button press
    # "text" is drawn at (0.7 * 1000/10, 0.5) when button = 0
    text = ax.text(0.7 * spinBoxValue/10 , 0.5, "text")

    # Comment previous line and uncomment next line to redraw text at every frame
    # text = ax.text(0.7 * i/10 , 0.5, "text")

    CumulativeX.append(randint(0, 10) / 10)
    CumulativeY.append(randint(0, 10) / 10)
    print(CumulativeX)
    return line.set_data(CumulativeX, CumulativeY)
medium-dimensional
  • 1,974
  • 10
  • 19
  • 1
    Thanks. How would I completely refresh the animation once I click the spinbox? The code I provided is a simplified case, where I can just clear the 2 lists (CumulativeX and CumulativeY). However, in my complete code, I cannot simply do that. (when I try, the first animation keeps running in the background together with the new one). How can I destroy the old animation, before running the new one? Thanks – Francesco Oct 21 '22 at 20:12
  • @Francesco Can you check if the updated `button` function in the answer now stops the background animation? I am still looking for *deleting* the old animation, but the latest answer might just solve the issue. – medium-dimensional Oct 22 '22 at 01:28
  • 1
    Thanks a lot for your help. Unfortunately, your edited button does not work. Well, actually for the simple case, we are working on, it does, but that is only because we are clearing the CumulativeX and Y lists. So, it looks like it is working, but actually we have not cleared the animation, but only what the animation plots. For example, if you add ax.text( 0.7,0.5,'test') to the button function, then you will see that the 'test' txt is added on top of each other every time you press the button, so the animation has not really restarted. Any further help, please? – Francesco Oct 22 '22 at 21:02
  • Hi medium-dimensional. What you suggested worked on the small example, but not on my main code. However, I applied your approach to fig (i.e. I deleted the instance of the matplotlib class "figure" everytime the button is pushed and I defined it again) and this seems to work fine! Thanks again! I really appreciate it! – Francesco Oct 23 '22 at 12:43
  • Actually, it does not work well. If I click on the button once, twice or three times, then it works ok. However, when I push the button more than that, then the animation becomes very slow and moves jerkily. This makes me think that actually somewhere in the background, an operation is still running and that I have not really killed all of the processes. Now, this cannot be seen in the example code provided, so I uploaded my work code in the main question. I tried to strip the code as much as possible, but in order to show the slow down, I had to leave the main core of the code. – Francesco Oct 23 '22 at 13:47
  • Do you think it would be a good idea to post it as a new question? Please see [this](https://meta.stackexchange.com/a/106250) and the answer below it. – medium-dimensional Oct 23 '22 at 13:54
  • You are right. I posted a new question here: https://stackoverflow.com/questions/74171926/how-to-properly-interrupt-an-animation – Francesco Oct 23 '22 at 14:28