Can I change the interval of a previously created matplotlib FuncAnimation? - python

I'm trying to figure out if is there any way for me to change the interval of an existing matplotlib FuncAnimation. I want to be able to adjust the speed of the animation according to the user input.
I found a similar question How do I change the interval between frames (python)?, but since it got no answer I thought I would ask it anyway.
A minimal example of what I need and have is:
"""
Based on Matplotlib Animation Example
author: Jake Vanderplas
https://stackoverflow.com/questions/35658472/animating-a-moving-dot
"""
from matplotlib import pyplot as plt
from matplotlib import animation
import Tkinter as tk
import numpy as np
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
class AnimationWindow(tk.Frame):
def __init__(self, parent):
tk.Frame.__init__(self, parent)
self.fig = plt.figure(0, figsize=(10, 10))
self.anim = None
self.speed = 2
self.canvas = FigureCanvasTkAgg(self.fig, self)
self.canvas.show()
self.canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
self.canvas.mpl_connect('resize_event', self.on_resize)
self.bar = tk.Scale(self, from_=0.25, to=10, resolution=0.25, command=self.change_play_speed, orient=tk.HORIZONTAL)
self.bar.pack(fill=tk.X)
def start_animation(self):
ax = plt.axes()
self.x = np.arange(0, 2 * np.pi, 0.01)
self.line, = ax.plot(self.x, np.sin(self.x))
# The return needs to be assigned to a variable in order to prevent the cleaning by the GC
self.anim = animation.FuncAnimation(self.fig, self.animation_update, frames=100,
interval=100/self.speed, blit=True, repeat=False)
def animation_update(self, i):
self.line.set_ydata(np.sin(self.x + i / 10.0)) # update the data
return self.line,
return tuple(self.annotation)
def change_play_speed(self, speed):
self.speed = float(speed)
# This works but I think somehow the previous animation remains
#self.anim = animation.FuncAnimation(self.fig, self.animation_update, frames=100, interval=100/self.speed, blit=True, repeat=False)
def on_resize(self, event):
"""This function runs when the window is resized.
It's used to clear the previous points from the animation which remain after resizing the windows."""
plt.cla()
def main():
root = tk.Tk()
rw = AnimationWindow(root)
rw.pack()
rw.start_animation()
root.mainloop()
if __name__ == '__main__':
main()
In the change speed function I have a commented solution to this problem. This solution presents two main problems: it is most likely very inefficient (I think); and I haven't figured out a way for me to delete the previous animation which results in flickering.

I would not recomment to delete the animation. One option for more complex animations is of course to program them manually. Using a timer which repeatedly calls the update function is actually not much more code than creating the FuncAnimation.
However in this case the solution is very simple. Just change the interval of the underlying event_source:
def change_play_speed(self, speed):
self.speed = float(speed)
self.anim.event_source.interval = 100./self.speed

Related

Python3 - Matplotlib FuncAnimation continuous memoryleak

EDIT2:
So after some more work I created the smallest sample which reproduces the Memoryleak pretty decent on every machine.
This code just creates a TKinter Window and a matplotlib canvas with 3 Sine waves, than tries to animate then. See that blit is true, so it shoudnt do anything because the drawing does not change.
I also tried following ways from this post
How can I release memory after creating matplotlib figures
gc.collect() - did nothing
axes.clear - it did stop endless memory to be leaked but also completely screws the picture
figure.clf - same as clear but worse.
Any other idea why FuncAnimation increases in memory usage over time?????
Thanks
import tkinter as tk
from threading import Thread
from numpy import sin, cos, pi
#--Mainwindow--
class Drehstromdemonstrator(tk.Tk):
def __init__(self):
#Predefine
tk.Tk.__init__(self)
tk.Tk.wm_title(self, "Minimal")
container = tk.Frame(self)
container.pack(side="top", fill="both", expand=True)
container.grid_rowconfigure(0, weight=1)
container.grid_columnconfigure(0, weight=1)
self.frame = plotSine(self,(tk.Tk.winfo_screenwidth(self)*0.7,tk.Tk.winfo_screenheight(self)*0.7))
import numpy,matplotlib
matplotlib.use('TkAgg')
from numpy import sin, cos, pi, deg2rad, linspace, arange
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import matplotlib.ticker as tck
import time
import math
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from math import ceil
global U1ausg, U2ausg, U3ausg, Counter_1, Counter_2, Counter_3
U1ausg = U2ausg = U3ausg = Counter_3 = 50
Counter_1 = 120
Counter_2 = 240
class plotSine:
def __init__(self,masterframe,size):
self._running = True
global U1ausg
global U2ausg
global U3ausg
(w,h) = size
inchsize = (w/25.5, h/25.4)
fig = self.figure = Figure(inchsize)
self.axes = fig.add_subplot(111)
self.axes.xaxis.set_ticks(arange(0,390,30))
self.axes.margins(x=0)
self.axes.yaxis.set_ticks(arange(-120,130,20))
self.axes.set_ylim(-120,120)
#create canvas as matplotlib drawing area
self.canvas = FigureCanvasTkAgg(fig, master=masterframe)
self.canvas.get_tk_widget().pack()
self.x = linspace(0,360,1000)
self.axes.grid()
#self.drawStuff()
#Draw the plot
def drawStuff(self,*args):
ax = self.axes
self.axes.legend(["U1","U2","U3"])
#Changed everything to degree instead of PI, better looking
ysin = int(ceil(U1ausg))*sin(50/Counter_3 * deg2rad(self.x))
ysin2 = int(ceil(U2ausg))*sin(50/Counter_3 * deg2rad(self.x) + deg2rad(Counter_1))
ysin3 = int(ceil(U3ausg))*sin(50/Counter_3 * deg2rad(self.x) + deg2rad(Counter_2))
lineU1 = ax.plot(self.x, ysin, "-r",label="U1")
lineU2 = ax.plot(self.x, ysin2, "-g",label="U2")
lineU3 = ax.plot(self.x, ysin3, "-b",label="U3")
return [*lineU1,*lineU2,*lineU3]
#Animation to redraw when value changed
def animate(self):
self.ani = animation.FuncAnimation(self.figure, self.drawStuff, interval=10, blit=True)
#--Run Mainprog
try:
app = Drehstromdemonstrator()
plt.show()
app.frame.animate()
app.mainloop
# Aufraeumarbeiten nachdem das Programm beendet wurde
except UnicodeDecodeError:
print("Interrupt aswell!")
lampensteuerung.terminate()
except KeyboardInterrupt:
print("Interrupt")
lampensteuerung.terminate()
except Exception as e:
print(e)
EDIT1:
By try-and-error I could figure out that the problem seems to be in my matplotlib, as if I disable it the memory increase stops.
So my task is to finish this project from someone else - it simulates a 3-phased system via a raspberry and some lights. When I disable the GUI the code works splendid, no problems, fast reaction. Sometimes the PI says he has low voltage but I just think that is some weird bug because the sourcepower is really enough to power it.
Full Code removed because not needed.

How do I highlight points by clicking on legend entries in matplotlib?

I am plotting a simple scatter plot with 3 groups of data, labeled A, B and C.
How do I write the code to allow clicking on legend entry A and highlighting points associated with A, while dimming points associated with labels B and C?
Is it possible to select (by click and drag, or click on multiple legend entries with ctrl key) and highlighting the associated points, while dimming points having other labels (labels not selected on the legend)?
Following the example here https://matplotlib.org/3.1.1/gallery/event_handling/legend_picking.html (which uses plot instead of scatter), I have made some progress, but can't quite get the code to work the way I want it. In the example, they set the picker property for each legend entry via leg.get_lines(), and I tried a similar thing (leg.get_patches()) which gave an empty dict. I can't make further progress, hopefully someone can help. Thanks in advance.
import numpy as np
import tkinter as tk
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
class MainApplication(tk.Frame):
def __init__(self, root, *args, **kwargs):
tk.Frame.__init__(self, root, *args, **kwargs)
self.root = root
self.frame = tk.Frame(self.root)
self.frame.grid(row=0, column=0)
self.plot_button = tk.Button(self.frame, text='Plot XY', command=self.xy_plot)
self.plot_button.grid(row=0, column=0)
self.figure = plt.figure(figsize=(5,5))
self.canvas = FigureCanvasTkAgg(self.figure, master=self.frame)
self.canvas.get_tk_widget().grid(row=1, column=0)
self.X = [np.random.rand(5), np.random.rand(5), np.random.rand(5)]
self.Y = [np.random.rand(5), np.random.rand(5), np.random.rand(5)]
self.labels = ['A','B','C']
self.figure.canvas.mpl_connect('pick_event', self.onpick)
def xy_plot(self):
ax = self.figure.add_subplot(111)
self.pts = []
for x, y, grp in zip(self.X, self.Y, self.labels):
self.pts.append(ax.scatter(x, y, label=grp))
leg = ax.legend(self.pts, self.labels, fontsize=12)
leg.set_picker(5) #should be set for individual entries? If so, how?
self.canvas.draw()
def onpick(self, event):
#obviously this function needs to be modified
self.pts[0].set_alpha(0.9)
self.pts[1].set_alpha(0.1
self.canvas.draw()
if __name__ == "__main__":
root = tk.Tk()
MainApplication(root)
root.mainloop()
All the examples that I came across involve looping over leg.get_lines() and setting the set_picker(value) for each element in leg.get_lines(). However, leg.get_lines() is empty for scatter plot. I tried looking at leg.dict['legendHandles'] and I see that it consists of 3 (in my case) collections.PathCollection objects. Do these objects have set_picker() methods? It seems to me that legend for a scatter is very different from legend for a plot. Can someone shed light on this?
By analogy to the example shown in the link in my question, I looped over the legendHandles and set the set_picker(5). This allows clicking on different legend entries and allows me to get the label via event.artist.get_label(). The modification to the onpick function seems like a hack, but it does what I want. Is there a more elegant and proper way to loop over the legendHandles than what I am doing? Any suggestions to improve the "solution" is welcome.
def xy_plot(self):
ax = self.figure.add_axes([0,0,1,1])
self.pts = []
for x, y, grp in zip(self.X, self.Y, self.labels):
self.pts.append(ax.scatter(x, y, label=grp))
self.leg = ax.legend(self.pts, self.labels)
for obj in self.leg.__dict__['legendHandles']:
obj.set_picker(5)
self.canvas.draw()
def onpick(self, event):
groups = {key: val for key, val in zip(self.labels, self.pts)}
label = event.artist.get_label()
for key in groups:
if key == label:
groups[key].set_alpha(1.0)
else:
groups[key].set_alpha(0.2)
self.canvas.draw()

How do I make tkinter mainloop wait for a matplotlib click event

I am buildin a GUI with an embedded plot using Tkinter and matplotlib. I have embedded a figure in my window and am now looking to use matplotlib's event handler to get two sets of x,y coordinates from the graph and then use these coordinates to create a straight line which is subtracted from the data in the graph. A simplified version of the code looks like this:
import matplotlib
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
import tkinter as tk
#ideally this uses matplotlib's event handler and also waits for a click before registering the cooridnates
def choose_points():
points = []
window.bind("<Button-1>", on_click)
points.append(graph_xy)
window.bind("<Button-1>", on_click)
points.append(graph_xy)
return points
def on_click(event):
window.unbind("<Button-1")
window.config(cursor="arrow")
graph_xy[0]=event.x
graph_xy[1]=event.y
def line(x1=0,y1=0,x2=1,y2=1000):
m=(y2-y1)/(x2-x1)
c=y2-m*x2
line_data=[]
for val in range(0,20):
line_data.append(val*m + c)
return line_data
def build_line():
points = []
points = choose_points()
#store line in line_list
line_list=line(points[0],points[1],points[2],points[3])
#lists needed
line_list=[]
graph_xy=[0,0]
#GUI
window=tk.Tk()
window.title("IPES Graphing Tool")
window.geometry('1150x840')
#Make a frame for the graph
plot_frame = tk.Frame(window)
plot_frame.pack(side = tk.TOP,padx=5,pady=5)
#Button for making the straight line
line_btn = ttk.Button(plot_frame,text="Build line", command = build_line)
line_btn.grid(row=4, column=2,sticky='w')
#make empty figure
fig1=plt.figure(figsize=(9,7))
ax= fig1.add_axes([0.1,0.1,0.65,0.75])
#embed matplotlib figure
canvas = FigureCanvasTkAgg(fig1, plot_frame)
mpl_canvas=canvas.get_tk_widget()
canvas.get_tk_widget().pack(padx=20,side=tk.BOTTOM, fill=tk.BOTH, expand=False)
toolbar = NavigationToolbar2Tk(canvas, plot_frame)
toolbar.update()
canvas._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=False)
window.mainloop()
Obviously this example does not plot or use the line in any way, nor are the coordinates correct, since they are not converted to the coordinates of the graph. I tried replacing the window.bind("<Button-1>",wait_click) with plt.connect('button_press_event',on_click) but this does not wait for the click, and so an error occurs since the program tries to access points but it is empty.
I would like to use the functionality of the matplotlib event handling, so that I can use methods such as event.xdata and event.inaxes to avoid unnecessary extra work.
Thank you.
You should use canvas.mpl_connect to trigger your events, and then retrieve the xdata and ydata to plot the line. See below for sample:
import matplotlib
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
import tkinter as tk
window=tk.Tk()
window.title("IPES Graphing Tool")
window.geometry('1150x840')
plot_frame = tk.Frame(window)
plot_frame.pack(side = tk.TOP,padx=5,pady=5)
fig1=Figure(figsize=(9,7))
ax= fig1.add_axes([0.1,0.1,0.65,0.75])
canvas = FigureCanvasTkAgg(fig1, window)
canvas.get_tk_widget().pack(padx=20,side=tk.TOP, fill=tk.BOTH, expand=False)
toolbar = NavigationToolbar2Tk(canvas, window)
toolbar.update()
class DrawLine: # a simple class to store previous cords
def __init__(self):
self.x = None
self.y = None
def get_cords(self, event):
if self.x and self.y:
ax.plot([self.x, event.xdata], [self.y, event.ydata])
canvas.draw_idle()
self.x, self.y = event.xdata, event.ydata
draw = DrawLine()
canvas.mpl_connect('button_press_event', draw.get_cords)
window.mainloop()
So, I managed to get the functionality I wanted by tweaking #Henry Yik 's answer. The issue was resolved by simply using canvas.mpl_disconnect(cid).
I basically used a button to use a function called choose_cords() which would take in an object called draw of type DrawLine containing the x and y coordinates to build the line. The function would then issue the command canvas.mpl_connect('button_press_event', draw.get_cords) to start listening for clicks on the graph. Once two clicks had been registered the matplotlib event handler would be disconnected from within the draw object. The code goes like this
import matplotlib
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
import tkinter as tk
window=tk.Tk()
window.title("IPES Graphing Tool")
window.geometry('1150x840')
plot_frame = tk.Frame(window)
plot_frame.pack(side = tk.TOP,padx=5,pady=5)
fig1=Figure(figsize=(9,7))
ax= fig1.add_axes([0.1,0.1,0.65,0.75])
canvas = FigureCanvasTkAgg(fig1, window)
canvas.get_tk_widget().pack(padx=20,side=tk.TOP, fill=tk.BOTH, expand=False)
toolbar = NavigationToolbar2Tk(canvas, window)
toolbar.update()
def choose_cords(draw):
draw.cid=canvas.mpl_connect('button_press_event', draw.get_cords)
class DrawLine: # a simple class to store previous cords
def __init__(self):
self.x = None
self.y = None
self.cid = None
def get_cords(self, event):
if self.x and self.y:
ax.plot([self.x, event.xdata], [self.y, event.ydata])
canvas.draw_idle()
canvas.mpl_disconnect(self.cid)
self.__init__()
return
self.x, self.y = event.xdata, event.ydata
draw = DrawLine()
draw_btn = tk.Button(window, text="Draw the Line", command=lambda:choose_cords(draw)).pack(side=tk.TOP,padx=5,pady=5)
window.mainloop()
I added a return after plotting the line because I want a new line every time, and not a continuation.
Thanks again Henry for your answer which spawend this one

Python matplotlib tkinter - button doesn't update graph

I am writing a small program, with the intention to update matplotlib graphs periodically throughout. For this I intend to use clear() and redraw the graph. The clear function does work when called from within the method that creates the graph, but it does not work, when called from a button, eventhough the graph is given as a Parameter.
Below is runnable code in it's most basic form to illustrate the problem.
In this case, clicking the "Update" button does nothing. How would I fix that button to clear the graph?
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import tkinter as tk
import numpy as np
class MainWindow(tk.Frame):
def __init__(self, master = None):
tk.Frame.__init__(self, master)
self.add_graph()
def add_graph(self):
fig_sig = plt.figure(figsize=(4,2))
graph = fig_sig.add_subplot(111)
y_values = [0,1,2,3,4,5]
x_values = [1,2,3,4,5,6]
graph.plot(x_values, y_values)
canvas = FigureCanvasTkAgg(fig_sig, master=root)
canvas_widget=canvas.get_tk_widget()
canvas_widget.grid(row = 1, column = 0, columnspan = 3)
canvas.draw()
self.add_widgets(root, graph)
#graph.clear() # Calling graph.clear() here does clear the graph
def add_widgets(self, parent, graph):
update_btn = tk.Button(parent, text = "Update", command = lambda: self.update_graph(graph))
update_btn.grid(row = 8, column = 3)
def update_graph(self, graph):
graph.clear() # calling graph.clear() here does nothing
root = tk.Tk()
oberflaeche = MainWindow(master = root)
oberflaeche.mainloop()
you need to "update" canvas in that case.
define your canvas as: self.canvas = FigureCanvasTkAgg(fig_sig, master=root)
and "update" it:
def update_graph(self, graph):
graph.clear() # calling graph.clear() here does nothing
self.canvas.draw()

Blitting for live update in Tkinter GUI - performance and image overlap issues

I've got some issues about blitting a matplotlib plot, which is itself embedded in a Tkinter GUI - the whole program will eventually run on a Raspberry Pi. The question involves various levels, this is my first question, so sorry in advance for any unclarities.
In few words, what I'm doing is this: I'm working on a Tk GUI to read out a number of sensors simultaneously and I'd like to have some real-time updating of the sensor data on said GUI.
I'd like to have each measurable quantity on a separate frame, which is why I decided to set up a class for each Sensor. One of the sensors is a Flow Sensor, which is read out and plotted as follows:
import Tkinter as Tk
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from Backend import Backend #self written data acquisition handler
#global variables
t0 = datetime.now() #start time of program
#Returns time difference in seconds
def time_difference(t1, t2):
delta = t2-t1
delta = delta.total_seconds()
return delta
# Define Class for Flow data display
class FlowFig():
def __init__(self, master): #master:Parent frame for plot
#Initialize plot data
self.t = []
self.Flow = []
#Initialize plot for FlowFig
self.fig = plt.figure(figsize=(4,4))
self.ax = self.fig.add_subplot(111)
self.ax.set_title("Flow Control Monitor")
self.ax.set_xlabel("Time")
self.ax.set_ylabel("Flow")
self.ax.axis([0,100,0,5])
self.line = self.ax.plot(self.t, self.Flow, '-')
#Set up canvas
self.canvas = FigureCanvasTkAgg(self.fig, master = master)
self.canvas.show()
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self.ax.grid(True)
# Initialize handler for data aqcuisition
self.Data= Backend()
self.Data.initialize()
#update figure
def update(self):
# get new data values
self.t.append(time_difference(t0, datetime.now()))
Flow,_,_ = self.Data.get_adc_val(1)
self.Flow.append(Flow)
# shorten data vector if too long
if len(self.t) > 200:
del self.t[0]
del self.Flow[0]
#adjust xlims, add new data to plot
self.ax.set_xlim([np.min(self.t), np.max(self.t)])
self.line[0].set_data(self.t, self.Flow)
#blit new data into old frame
self.canvas.restore_region(self.background)
self.ax.draw_artist(self.line[0])
self.canvas.blit(self.ax.bbox)
root.after(25, Flowmonitor.Flowdata.update) #Recursive update
#Flow Frame of GUI
class FlowPage(Tk.Frame):
def __init__(self, parent, controller):
Tk.Frame.__init__(self,parent)
self.parent = parent
self.FlowPlot = FlowFig(self)
self.FlowPlot.canvas.get_tk_widget().grid(row=0, column=0, rowspan=9, columnspan=9)
# Mainloop
root= Tk.Tk()
root.rowconfigure(0, weight=1)
root.columnconfigure(0, weight=1)
Flowmonitor = FlowPage(root, root)
Flowmonitor.grid(row =0, column=0, rowspan =10, columnspan=10)
Flowmonitor.rowconfigure(0, weight=1)
Flowmonitor.columnconfigure(0, weight=1)
root.after(25, Flowmonitor.FlowPlot.update)
root.mainloop()
What troubles me with my resulting images is this:
When I use the statement copy_from_bbox(self.ax.bbox) I get a graph like this
Obviously, the size of the blitted background doesn't fit the image into which it is blitted. So, I tried to blit the figure's bbox instead (copy_from_bbox(self.fig.bbox)) and got this
Versions of these shifts happen with all combinations of fig.bbox and ax.bbox,
So here come my actual questions:
Can anybody help me find the bug in my above code which causes the missmatch? I'm aware that it is probably very simple yet subtle misstake. It seems very much related to this thread, yet I can't quite glue it together, using bbox.expanded() in the argument of copy_from_bbox() doesn't do much of a difference
.blit() vs. .draw() has already been discussed here.
But since speed is of the essence for my application I think I have to blit. Redrawing the plot gives me framerates of fps=10, whereas blitting runs almost 10x faster. In any case - is there a way to update one of the axes (e.g. time axis) while using blit? (The answer to this is probably closely related to question No.1 )
Lastly, a rather basic question about my application alltogether: Since my sensordata is currently fetched within an infinite, recursive loop - is it possible to run several of such loops in parallel or should I rather go for threading instead, making my code considerably more complex? What are the risks of running infinite, recursive loops? Or should these be avoided in general?
After days of blitting back and forth I'm rather confused about the possibilities regarding ax/fig blitting, so any help regarding the matter is much, much appreciated^^
Please let me know if you need more info about anything, I hope I could illustrate my problem well.
Thanks a lot for your help!
In short: the solution
This was written in Python3, but it should be virtually the exact same in your version of Python2
import tkinter as Tk
import numpy as np
import matplotlib.pyplot as plt
from datetime import datetime
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
# from Backend import Backend #self written data acquisition handler
import random
#global variables
t0 = datetime.now() #start time of program
#Returns time difference in seconds
def time_difference(t1, t2):
delta = t2-t1
delta = delta.total_seconds()
return delta
# Define Class for Flow data display
class FlowFig():
def __init__(self, master): #master:Parent frame for plot
#Initialize plot data
self.t = []
self.Flow = []
#Initialize plot for FlowFig
self.fig = plt.figure(figsize=(4,4))
self.ax = self.fig.add_subplot(111)
self.ax.set_title("Flow Control Monitor")
self.ax.set_xlabel("Time")
self.ax.set_ylabel("Flow")
self.ax.axis([0,100,0,5])
self.line = self.ax.plot(self.t, self.Flow, '-')
#Set up canvas
self.canvas = FigureCanvasTkAgg(self.fig, master = master)
self.canvas.draw()
self.ax.add_line(self.line[0])
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self.ax.grid(True)
# # Initialize handler for data aqcuisition
# self.Data= Backend()
# self.Data.initialize()
#update figure
def update(self):
# get new data values
self.t.append(time_difference(t0, datetime.now()))
Flow = random.uniform(1, 5)
self.Flow.append(Flow)
# shorten data vector if too long
if len(self.t) > 200:
del self.t[0]
del self.Flow[0]
#adjust xlims, add new data to plot
self.ax.set_xlim([np.min(self.t), np.max(self.t)])
self.line[0].set_data(self.t, self.Flow)
#blit new data into old frame
self.canvas.restore_region(self.background)
self.ax.draw_artist(self.line[0])
self.canvas.blit(self.ax.bbox)
self.canvas.flush_events()
root.after(1,self.update)
#Flow Frame of GUI
class FlowPage(Tk.Frame):
def __init__(self, parent, controller):
Tk.Frame.__init__(self,parent)
self.parent = parent
self.FlowPlot = FlowFig(self)
self.FlowPlot.canvas.get_tk_widget().grid(row=0, column=0, rowspan=9, columnspan=9)
# Mainloop
root= Tk.Tk()
root.rowconfigure(0, weight=1)
root.columnconfigure(0, weight=1)
Flowmonitor = FlowPage(root, root)
Flowmonitor.grid(row =0, column=0, rowspan =10, columnspan=10)
Flowmonitor.rowconfigure(0, weight=1)
Flowmonitor.columnconfigure(0, weight=1)
root.after(25, Flowmonitor.FlowPlot.update)
root.mainloop()
The Changes
Moving to python3
I moved
import Tkinter as Tk
to
import tkinter as Tk
Simulating your adc values
I moved
from Backend import Backend
to
import random
Because I don't have access to the Backend, I just used a random number generator (obviously it isn't the best example of ADC readings, but it is good enough for a tester)
Setting up the canvas
I moved
self.canvas = FigureCanvasTkAgg(self.fig, master = master)
self.canvas.show()
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self.ax.grid(True)
to
self.canvas = FigureCanvasTkAgg(self.fig, master = master)
self.canvas.draw()
self.ax.add_line(self.line[0])
self.background = self.canvas.copy_from_bbox(self.ax.bbox)
self.ax.grid(True)
You must first draw() the canvas, because otherwise matplotlib will throw an error AttributeError: draw_artist can only be used after an initial draw which caches the renderer.
Then, we now add the self.line, with no values in it. Currently it is just an empty point sitting on the canvas.
Simulating ADC
From:
Flow,_,_ = self.Data.get_adc_val(1)
to
Flow = random.uniform(1, 5)
Obviously you could keep your own code for this
Looping with after
Your system of using the after function wasn't entirely correct, as you should have inherited the Flowmonitor.Flowdata from the pre-existing window. Otherwise you would be updating values that simply don't exist, hence, I replaced it with a self. function
root.after(25, Flowmonitor.Flowdata.update)
to
self.canvas.flush_events()
root.after(1,self.update)
I decreased the after value, to show that the window could continue plotting correctly when doing it even faster!
The flush_events() function causes the window to properly update and keep track of other things it's doing!
Answer to question 3
I'd thoroughly dissuade you from going down the threading route, because it is awful with tkinter. The amount of issues and loop-holes you have to jump through are awful, and quite often, even with threading, the program still begins to feel quite slow and unresponsive.

Categories

Resources