I would like to properly interrupt an animation.
Background:
I have a matplotlib figure (an animation) encapsulated into a tkinter instance. I want that when the user presses a tkinter button, the animation must stop, be deleted and be restarted. I am interrupting the old animation, by using del fig at the beginning of the call back function (called by the button) which deletes the old instance of the figure class and after creates a new one.
Problem:
I think that the old animations are still somehow running in the background, as I noticed that when I click the button like 5 times, the animation gets slow and moves jerkily.
Code:
import matplotlib
import matplotlib.backends.backend_tkagg as tkagg
import matplotlib.animation as animation
import numpy as np
import tkinter as tk
fig = matplotlib.figure.Figure()
ani = []
#callback function
def solve():
#I want the old animation to disappear, every time the button is pushed
global fig, ani
del fig, ani
#Creating an instance of the figure class
fig = matplotlib.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=3, rowspan=10)
# create a time array
ti, tf, dt, delay=0, 10, 0.01, 15
N=(tf-ti)/dt
t = np.arange(ti, tf, dt)
x1=np.sin(t)
N=len(t)
#Creating a sub plot
ax2 = fig.add_subplot(xlim=(0, t[N-1]), ylim=(min(x1), max(x1)))
#Printing a legend with time info
time_template = 'time = %.1fs'
time_text = ax2.text(0.05, 0.95, '', transform=ax2.transAxes, bbox=dict(facecolor='white', edgecolor='black', boxstyle='round,pad=1'))
#I want to create a live plot for x(t). I am using place-holders where the t and x will be
massmotion, = ax2.plot([], [], '-')
CumulativeX1, CumulativeT=[], []
#I am defining what is going to go into the brackets above (I am filling the placeholders)
def animate(i):
CumulativeX1.append(x1[i]), CumulativeT.append(i*dt)
time_text.set_text(time_template % (i*dt))
massmotion.set_data(CumulativeT, CumulativeX1 ) #Update the placeholders for the live plot
return time_text, massmotion
ani = animation.FuncAnimation(fig, animate, np.arange(1, N), interval=delay, blit=True)
fig.canvas.draw()
#Creating the GUI
#Creating an instance of the Tk class
win = tk.Tk()
#Creating an instance of the Button class
#run the solve function when I click the button
aButton=tk.Button(win, text='Start Animation', command=solve)
#Placing the Button instance
aButton.grid(column=0, row=10, columnspan=2)
#Starting the event loop
win.mainloop()
Related
I'm currently programming a GUI in which a diagram with data from a CSV file should be displayed. Since new data is always being added to the CSV, I want to display a current history after each button click.
My problem:
After every button click, a new diagram appears instead of a new course in the old diagram
Code:
def graph():
# List 1
liste_1_x = ["06:15", "15:30"]
liste_1_y = [0,16000]
# List 2
list_2_x = []
list_2_y = []
#------------------------------------------------------------------------
import csv
with open("testdata.csv", "r") as filedata:
lines = csv.reader(filedata, delimiter=",")
for row in lines:
list_2_x.append(row[0])
list_2_y.append(int(row[1]))
#------------------------------------------------------------------------
import matplotlib.pyplot as plt
import tkinter
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)
fig = Figure(figsize=(6,4), dpi=100)
plot1 = fig.add_subplot(111)
plot1.plot(liste_1_x,liste_1_y, color="b", marker="o")
plot1.plot(list_2_x, list_2_y, color="r", marker="o")
plot1.grid()
plot1.set_ylabel("Amount")
plot1.set_xlabel("Time")
canvas = FigureCanvasTkAgg(fig, master=tkFenster)
canvas.draw()
canvas.get_tk_widget().pack(side=tkinter.BOTTOM)
toolbar = NavigationToolbar2Tk(canvas, tkFenster)
toolbar.update()
canvas.get_tk_widget().pack(side=tkinter.LEFT, expand = 1)
Thanks for the help!
Overview
I am in the process of embedding a Matplotlib plot in a Tkinter window. I need to use the Matplotlib event handler functions (described here). When run as a standalone Matplotlib figure, I get the correct behavior: the event handlers perform their correct function on the correct user action. But when embedding in a Tkinter window, the Matplotlib event handlers are no longer being called.
Expected Behavior
The Matplotlib event handler should be called when the figure is embedded in a Tkinter window.
Current Behavior
The Matplotlib event handler is not being called.
Minimal Code Snippet
Without Tkinter
import matplotlib.pyplot as plt
def onpick(event):
print("You selected the line!")
if __name__=='__main__':
### MATPLOTLIB SETUP ###
xs = [0,1,2] #x-values of the graph
ys = [4,3,2] #y-values of the graph
fig, ax = plt.subplots(1)
ax.plot(xs, ys, picker=True)
fig.canvas.mpl_connect('pick_event', onpick)
plt.show()
When this code is run, you can select the line on the plot and the onpick method is called, printing the text to stdout. This is the desired output.
Embedded in Tkinter
import tkinter
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
def onpick(event):
print("You selected the line!")
if __name__=='__main__':
### MATPLOTLIB SETUP ###
xs = [0,1,2] #x-values of the graph
ys = [4,3,2] #y-values of the graph
fig, ax = plt.subplots(1)
ax.plot(xs, ys, picker=True)
fig.canvas.mpl_connect('pick_event', onpick)
### TKINTER SETUP ###
root = tkinter.Tk()
canvas = FigureCanvasTkAgg(fig, master=root)
canvas.draw()
canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=1)
tkinter.mainloop()
When this code is run and you try to click on the line, the text is never printed meaning the onpick method is never being called.
Versions
python : 3.6.1
matplotlib : 3.3.4
tkinter : 8.6.6
The event listeners of the Matplotlib Figure object cannot be called when embedded in Tkinter. Instead, you have to add the listeners to the FigureCanvasTkAgg (or similar) object. So a working example built on the previous minimal example would be:
import tkinter
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
def onpick(event):
print("You selected the line!")
if __name__=='__main__':
### MATPLOTLIB SETUP ###
xs = [0,1,2] #x-values of the graph
ys = [4,3,2] #y-values of the graph
fig, ax = plt.subplots(1)
ax.plot(xs, ys, picker=True)
#plt.show()
### TKINTER SETUP ###
root = tkinter.Tk()
canvas = FigureCanvasTkAgg(fig, master=root)
canvas.draw()
canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=1)
canvas.mpl_connect("pick_event", onpick) # Add the listener using this command
tkinter.mainloop()
I have a troubleing bug that i just could not understands it's origin. Several days of attempts and still no luck.
I'm trying to create a line cursor that correspond to played audio with FuncAnimation and for some reason, the animation is created twice ONLY when the callback (line_select_callback) that activates the function is triggered from RectangleSelector widget after drawing wiith the mouse. when I use a standard TK button to activate the SAME function (line_select_callback), it operates well.
some debugging code with reevant prints is present.
I've created minimal working example.
My guess is it has something to do with the figure that is not attached to the tk window, and is silently activated in addition to the embedded figure, I'm not really sure.
Any help will be very much appreciated, Thanks! :)
import os
import threading
import tkinter as tk
from matplotlib.backends.backend_tkagg import (
FigureCanvasTkAgg)
from matplotlib.widgets import RectangleSelector
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
from matplotlib import animation
class LineAnimation:
def __init__(self, fig, ax):
print(' enter LineAnimation ctor')
# Parameters
self.ax = ax
self.fig = fig
self.xdata, self.ydata = [], []
self.ln, = plt.plot([], [], 'ro')
# Print figures list
figures = [manager.canvas.figure
for manager in matplotlib._pylab_helpers.Gcf.get_all_fig_managers()]
print('figures BEFORE animation: ', figures)
self.animation = animation.FuncAnimation(fig=self.fig,
func=self.update,
init_func=self.init,
frames=np.linspace(0, 2 * np.pi, 128),
interval=25,
blit=True, repeat=False,
cache_frame_data=False)
self.fig.canvas.draw()
# Print figures list
figures = [manager.canvas.figure
for manager in matplotlib._pylab_helpers.Gcf.get_all_fig_managers()]
print('figures AFTER animation: ', figures, '\n')
def init(self):
# Prints for debugging
print('\nenter init animate')
print('Thread id: ', threading.get_ident())
print('Process id: ', os.getpid(), '\n')
# Init
self.ax.set_xlim(0, 2*np.pi)
self.ax.set_ylim(-1, 1)
return self.ln,
def update(self, frame):
self.xdata.append(frame)
self.ydata.append(np.sin(frame))
self.ln.set_data(self.xdata, self.ydata)
return self.ln,
class Example:
def __init__(self):
# init window
self.root = tk.Tk(className=' Species segmentation')
self.fig, self.ax = plt.subplots()
# init sine audio file
self.fs = 44100
self.dur = 2
self.freq = 440
self.x = np.sin(2*np.pi*np.arange(self.fs*self.dur)*self.freq/self.fs)
# plt.ion()
# Embedd in tk
self.canvas = FigureCanvasTkAgg(self.fig, master=self.root) # A tk.DrawingArea.
self.canvas.draw()
self.canvas.get_tk_widget().grid()
# Plot something
self.N = 100000
self.xp = np.linspace(0, 10, self.N)
self.ax.plot(self.xp, np.sin(2*np.pi*self.xp))
self.ax.set_title(
"Plot for demonstration purpuse")
# init Rectangle Selector
self.RS = RectangleSelector(self.ax, self.line_select_callback,
drawtype='box', useblit=True,
button=[1, 3], # avoid using middle button
minspanx=5, minspany=5,
spancoords='pixels', interactive=True,
rectprops={'facecolor': 'yellow', 'edgecolor': 'black', 'alpha': 0.15, 'fill': True})
self.canvas.draw()
# plt.show()
tk.mainloop()
def line_select_callback(self, eclick, erelease):
print('enter line_select_callback')
self.anim = LineAnimation(
self.fig,
self.ax)
self.fig.canvas.draw()
# plt.show()
Example()
I managed to isolate the cause for this issue: The presence of the
rectangle selector (which uses blitting) and the use of animation (which also uses blitting) on the same axes.
I've managed to create the animation properly, but only when I disabled the rectangle selector
self.RS.set_active(False)
self.RS.update()
self.canvas.flush_events()
and removed his artists (i needed to do that manually in my code) using:
for a in self.RS.artists:
a.set_visible(False)
after that, The animation worked properly.
I'm doing a tkinter GUI for my program and I have to display some real time data. I made a simple program (below) to demonstrate my problem on a simple case. I'm actually plotting some data every iteration of my for loop so I can observe data while the program in still calculating. Note that the real program si calculating a bit slower and have more iterations.
Now I would like to add 2 buttons (one to pause the program and one to continue) and a label (diplay variable k so i know where my program is), but I am unable to do it.
I've already lost a lot of time on it so if anyone have a hint or a solution i would love to see it.
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import matplotlib.pyplot as plt
from matplotlib import style
def func_A(a, x):
import numpy
data_x = numpy.arange(0, x)
data_y = a * numpy.sin(data_x/5)
return data_x, data_y
a = 1
root = tk.Tk()
root.title("Graph")
root.geometry("800x400")
fig = plt.figure(figsize=(5, 5), dpi=100)
canvas = FigureCanvasTkAgg(fig, master=root) # A tk.DrawingArea.
canvas.draw()
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
toolbar = NavigationToolbar2Tk(canvas, root)
toolbar.update()
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
plt.grid("both")
style.use("ggplot")
for k in range(0, 100):
data_x, data_y = func_A(a, k)
print("iteration", k)
print("data_x", data_x)
print("data_y", data_y)
if k == 0:
ax1 = plt.subplot(111)
line1, = ax1.plot([0], [0])
else:
line1.set_xdata(data_x)
line1.set_ydata(data_y)
ax1.set_ylim([-1, 1])
ax1.set_xlim([0, 100])
plt.grid("both")
canvas.draw()
canvas.flush_events()
root.mainloop()
To add pause/resume function:
create a frame to hold the progress label and the two buttons: pause and resume
create a tkinter BooleanVar() to store the pause/resume state
move the update plot code inside a function, e.g. update_plot()
use .after() to replace the for loop to call update_plot() periodically
inside update_plot(), check the pause/resume state to determine whether to update the plot or not
Below is a modified example based on your code:
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import matplotlib.pyplot as plt
from matplotlib import style
import matplotlib
matplotlib.use("Agg")
root = tk.Tk()
root.title("Graph")
#root.geometry("800x400")
# progress label, pause and resume buttons
frame = tk.Frame(root)
frame.pack(fill="x", side=tk.TOP)
progress = tk.Label(frame)
progress.pack(side="left")
is_paused = tk.BooleanVar() # variable to hold the pause/resume state
tk.Button(frame, text="Pause", command=lambda: is_paused.set(True)).pack(side="right")
tk.Button(frame, text="Resume", command=lambda: is_paused.set(False)).pack(side="right")
# the plot
fig = plt.figure(figsize=(10, 5), dpi=100)
canvas = FigureCanvasTkAgg(fig, master=root)
toolbar = NavigationToolbar2Tk(canvas, root)
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
plt.grid("both")
style.use("ggplot")
a = 1
ax1 = plt.subplot(111)
line1, = ax1.plot([0], [0])
def func_A(a, x):
import numpy
data_x = numpy.arange(0, x)
data_y = a * numpy.sin(data_x/5)
return data_x, data_y
# function to update ploat
def update_plot(k=0):
if not is_paused.get():
progress["text"] = f"iteration: {k}"
data_x, data_y = func_A(a, k)
#print("iteration", k)
#print("data_x", data_x)
#print("data_y", data_y)
line1.set_xdata(data_x)
line1.set_ydata(data_y)
ax1.set_ylim([-1, 1])
ax1.set_xlim([0, 100])
plt.grid("both")
canvas.draw()
canvas.flush_events()
k += 1
if k <= 100:
# update plot again after 10ms. You can change the delay to whatever you want
root.after(10, update_plot, k)
update_plot() # start updating plot
root.mainloop()
Here is a tkinter program, boiled down from a GUI I am working on:
import tkinter as tk
import matplotlib.pyplot as plt
import numpy as np
from matplotlib.backends.backend_tkagg import (
FigureCanvasTkAgg, NavigationToolbar2Tk)
class App(tk.Tk):
def __init__(self):
super(App, self).__init__()
self.main_frame = tk.Frame(self,)
self.main_frame.pack(fill=tk.BOTH, expand=1)
self.plot1_button = tk.Button(self.main_frame, text='Plot 1',
command=self.draw_plot1)
self.plot1_button.pack(fill=tk.X,expand=1)
self.plot2_button = tk.Button(self.main_frame, text='Plot 2',
command=self.draw_plot2)
self.plot2_button.pack(fill=tk.X,expand=1)
self.FIG, self.AX = plt.subplots()
self.canvas = FigureCanvasTkAgg(self.FIG, master=self.main_frame)
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
self.toolbar = NavigationToolbar2Tk(self.canvas, self.main_frame)
self.canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=1)
def draw_plot1(self):
self.clear_axes()
fig = self.AX.plot(np.random.rand(10),np.random.rand(10), color='red')
self.canvas.draw_idle()
self.toolbar.update()
def draw_plot2(self):
self.clear_axes()
im = self.AX.matshow(np.random.rand(100,100))
self.canvas.draw_idle()
self.toolbar.update()
cb = plt.colorbar(im, ax=self.AX)
def clear_axes(self):
for ax in self.FIG.axes:
ax.clear()
if ax != self.AX:
ax.remove()
root = App()
root.resizable(False, False)
root.mainloop()
The Plot 1 button draws a random line plot, while the Plot 2 button draws a random heatmap with a colorbar. The Plot 1 button can be clicked repeatedly, creating new random line plots as expected. After 10 clicks, the display looks fine:
:
But the Plot 2 button causes the figure to shrink each time it is clicked. After 10 clicks, the graph is uninterpretable:
Additionally, the figure size persists when clicking Plot 1 again:
These are the .png files saved from the application's toolbar, but the same can be seen in the GUI window. I have tried add updates to the GUI/canvas (e.g. self.update(), self.canvas.draw_idle()) at different locations but haven't found anything that affects the issue. I added the clear_axes() function because in the real GUI I have some figures with multiple axes and this removes them, but apparently it does not help here.
I have found that if the color bar is removed, the problem disappears (i.e. comment out cb = plt.colorbar(im, ax=self.AX)), but I would like to have this as part of the figure. Can anyone shed light on what is going on, or can anyone suggest a fix? I'm on matplotlib 3.2.1.
The problem is you are not clearing the colorbar when you clear the axes.
class App(tk.Tk):
def __init__(self):
super(App, self).__init__()
self.main_frame = tk.Frame(self,)
...
self.cb = None
...
def draw_plot2(self):
self.clear_axes()
im = self.AX.matshow(np.random.rand(100,100))
self.canvas.draw_idle()
self.toolbar.update()
self.cb = plt.colorbar(im, ax=self.AX)
def clear_axes(self):
if self.cb:
self.cb.remove()
self.cb = None
for ax in self.FIG.axes:
ax.clear()
if ax != self.AX:
ax.remove()
Also note that you should use matplotlib.figure.Figure instead of pyplot when working with tkinter. See this for the official sample.