How to interactively change the animation arguments

Question:

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()
Asked By: Francesco

||

Answers:

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)
Answered By: medium-dimensional