Matplotlib FuncAnimation in Tkinter - Axes labels not being displayed/retained - python

I am using Python with TKinter as GUI; and Matplotlib's FuncAnimation, to display 2 graphs next to each other. These graphs are representing sensor readings (temperature, humidity), and I would like to label both the x-axis and y-axis. (The matplotlib graph title is not required, but I am including it for testing purposes)
I'll try not to divulge unnecessary details; but in simple terms, I have 2 classes representing the sensors, "temperature" and "humidity", and another class "main_window" to combine both the classes into the Tkinter GUI.
This is a preview of the main.py file; including the main_window class.
class main_window(tk.Tk):
def __init__(self, *args, **kwargs):
tk.Tk.__init__(self, *args, **kwargs)
self.wm_title("Indoor Monitoring System")
frame1 = tk.Frame(self)
frame1.grid(column=0, row=0)
frame2 = tk.Frame(self)
frame2.grid(column=1, row=0)
temperature(frame1, self)
humidity(frame2, self)
if __name__ == "__main__":
runObj = main_window()
runObj.mainloop()
and this is a preview of the temperature class
class temperature(tk.Frame):
def __init__(self, parent, controller):
pd.set_option('display.float_format', lambda x: '%.2f' % x)
plt.style.use('fivethirtyeight')
self.x_values = []
self.y_values = []
self.counter = 0
self.fig = plt.figure(figsize=(8, 4), dpi=100)
self.ax1 = self.fig.add_subplot(111)
self.ax1.set(xlabel="Reading Counter", ylabel="Degrees Celcius")
label = tk.Label(parent, text="Temperature Graph", font=("Arial", 12)).grid(column=0, row=0)
canvas = FigureCanvasTkAgg(self.fig, master=parent)
canvas.get_tk_widget().grid(column=0, row=1)
self.ani1 = FuncAnimation(self.fig, self.animate, interval=10000, blit=False)
def animate(self, i):
df = pd.read_csv('C:\\log.csv')
x_temporary_values = df["Counter"]
y_temp_values = df["Temp"]
currrow = x_temporary_values[self.counter]
if currrow is not None:
self.x_values.append(currrow)
self.y_values.append(y_temp_values[self.counter])
self.ax1.plot(self.x_values, self.y_values)
# self.ax1.set(xlabel="Reading Counter", ylabel="Degrees Celcius")
self.counter += 1
The humidity class is very similar.
I am trying to set the xlabel and ylabel as such:
self.ax1.set(xlabel="Reading Counter", ylabel="Degrees Celcius", title="Temperature")
I've also tried:
self.ax1.set_title("Temperature")
self.ax1.set_xlabel("Reading Counter")
self.ax1.set_ylabel("Degrees Celcius")
The title and y_label are initially displayed. After refreshing the graph via the "animate" funxction, the title is retained, the y_label is not. The x_label is never shown.
I have also tried to re-call the self.ax1.set() in the animation function. Does not make a difference.
What am I doing wrong?
Note: that this setup is the only one that worked for me, in the context of trying to animate the graphs. Most other setups that I was more accustomed to, do not work with FuncAnimation.
Also, note that the UI is not refined by any means (especially font sizes). I am concerned with fixing the label issues first.

Related

Python tkinter hangs when I try to close graphs

I've tried using Tk to make a function that will allow users to look at a graph and select if points are wrong. I had this working before, but now my code is hanging every time I run it and try to go to the next graph. Specifically when I click "next graph" and it should run
def _quit(self):
self.master.destroy() # stops mainloop
When I manually stop the code I get this message:
File "C:\Users\laura\Anaconda3\lib\tkinter\__init__.py", line 1429, in mainloop
self.tk.mainloop(n)
KeyboardInterrupt
Any suggestions? I've read using root.destroy() but I haven't been able to get that to work
Thanks!
def config_plot():
tk, ax = plt.subplots()
ax.set(title='Are the peaks okay?')
return (tk, ax)
class matplotlibSwitchGraphs:
def __init__(self, master):
self.master = master
self.frame = Frame(self.master)
self.frame.pack(expand=YES, fill=BOTH)
self.fig = Figure(figsize=(12,7))
self.ax = self.fig.gca()
self.canvas = FigureCanvasTkAgg(self.fig, self.master)
self.config_window()
self.graphIndex = 0
self.draw_graph_one()
def config_window(self):
toolbar = NavigationToolbar2Tk(self.canvas, self.master)
toolbar.update()
self.canvas.get_tk_widget().pack(side=TOP, fill=BOTH, expand=1)
print('connect')
self.canvas.mpl_connect("button_press_event", self.on_button_press)
self.button = Button(self.master, text="YES, next graph", command=self._quit) #this is where it gets stuck!
self.button.pack(side=BOTTOM)
self.button_switch = Button(self.master, text="NO", command=self.switch_graphs)
self.button_switch.pack(side=BOTTOM)
def draw_graph_one(self):
self.ax.clear() # clear current axes
self.ax.plot(data)
self.ax.plot(peaks,loc_peaks, marker='o', linestyle='none')
# self.ax.set(xlim=(touchdown_cut[-15]-50,toeoff_cut[-5]+50))
self.ax.set(title='Check the data')
self.canvas.draw()
def draw_graph_two(self):
self.ax.clear()
self.ax.plot(data)
self.ax.set(title='Click all the incorrect peaks')
self.ax.plot(peaks,loc_peaks, marker='o', linestyle='none')
self.canvas.draw()
def on_button_press(self, event):
print('xdata, ydata:', event.xdata, event.ydata)
# return (event.xdata)
global ix
ix = event.xdata
global clicks
clicks.append((ix))
array1 = np.asarray(peaks)
idx1 = (np.abs(array1 - event.xdata)).argmin()
global peaks_adj
global loc_peaks_adj
peaks_adj = np.delete(peaks, idx1)
loc_peaks_adj = np.delete(loc_peaks, idx1)
self.canvas.flush_events()
self.ax.clear()
self.ax.plot(data)
self.ax.set(title='Click all the incorrect peaks')
self.ax.plot(peaks_adj,loc_peaks_adj, marker='o', linestyle='none')
self.canvas.draw()
return (clicks)
def _quit(self):
self.master.destroy() # stops mainloop
def switch_graphs(self):
# Need to call the correct draw, whether we're on graph one or two
self.graphIndex = (self.graphIndex + 1 ) % 2
if self.graphIndex == 0:
self.draw_graph_one()
else:
self.draw_graph_two()
def main():
root = Tk()
matplotlibSwitchGraphs(root)
root.mainloop()
if __name__ == '__main__':
main()
Try changing root.destroy() to self.destroy()as you are working with classes. If using function through a button remove the()after command likecommand=self.destroy`.
Also don't use global, use self. to create a class variable if you want to access the variable in multiple function.
As far as for lags, I would recommend you to use update and update_idletasks.

Is there a way to delay the on_changed() call for matplotlib sliders?

I am currently working to create a tkinter GUI which shows a matplotlib figure with two sliders. The sliders are to be used for selecting a range of data (they are connected to vertical lines on the plot).
The issue I am running into is that when I change the sliders quickly a few times the GUI freezes. I believe this is due to the on_changed() call I am using to make the sliders update. When you move the slider quickly I believe on_changed() is being called multiple times at a faster rate than the program can keep up with, thus causing the freeze.
Is there any way to add a delay to the on_changed() call so it only looks to see if the slider has been changed every given interval of time (ex. 100 ms)?
Below is example code I found which freezes in the way I described.
import tkinter as tk
from tkinter.ttk import Notebook
from tkinter import Canvas
from tkinter import messagebox as msg
import numpy as np
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.widgets import Slider, Button, RadioButtons
class LukeOutline(tk.Tk):
def __init__(self):
# Inherit from tk.Tk
super().__init__()
# Title and size of the window
self.title('Luke Outline')
self.geometry('600x400')
# Create the drop down menus
self.menu = tk.Menu(self,bg='lightgrey',fg='black')
self.file_menu = tk.Menu(self.menu,tearoff=0,bg='lightgrey',fg='black')
self.file_menu.add_command(label='Add Project',command=self.unfinished)
self.menu.add_cascade(label='File',menu=self.file_menu)
self.config(menu=self.menu)
# Create the tabs (Graph, File Explorer, etc.)
self.notebook = Notebook(self)
graph_tab = tk.Frame(self.notebook)
file_explorer_tab = tk.Frame(self.notebook)
# Sets the Graph Tab as a Canvas where figures, images, etc. can be added
self.graph_tab = tk.Canvas(graph_tab)
self.graph_tab.pack(side=tk.TOP, expand=1)
# Sets the file explorer tab as a text box (change later)
self.file_explorer_tab = tk.Text(file_explorer_tab,bg='white',fg='black')
self.file_explorer_tab.pack(side=tk.TOP, expand=1)
# Add the tabs to the GUI
self.notebook.add(graph_tab, text='Graph')
self.notebook.add(file_explorer_tab, text='Files')
self.notebook.pack(fill=tk.BOTH, expand=1)
# Add the graph to the graph tab
self.fig = Figure()
graph = FigureCanvasTkAgg(self.fig,self.graph_tab)
graph.get_tk_widget().pack(side='top',fill='both',expand=True)
EllipseSlider(self.fig)
#------------------------------------------------------
def quit(self):
'''
Quit the program
'''
self.destroy()
#------------------------------------------------------
def unfinished(self):
'''
Messagebox for unfinished items
'''
msg.showinfo('Unfinished','This feature has not been finished')
#------------------------------------------------------
def random_graph(self):
x = list(range(0,10))
y = [i**3 for i in x]
fig = Figure()
axes = fig.add_subplot(111)
axes.plot(x,y,label=r'$x^3$')
axes.legend()
return fig
#----------------------------------------------------------
class EllipseSlider():
#------------------------------------------------------
def __init__(self,fig):
self.fig = fig
# Initial values
self.u = 0. #x-position of the center
self.v = 0. #y-position of the center
self.a = 2. #radius on the x-axis
self.b = 1.5 #radius on the y-axis
# Points to plot against
self.t = np.linspace(0, 2*np.pi, 100)
# Set up figure with centered axes and grid
self.ax = self.fig.add_subplot(111)
self.ax.set_aspect(aspect='equal')
self.ax.spines['left'].set_position('center')
self.ax.spines['bottom'].set_position('center')
self.ax.spines['right'].set_color('none')
self.ax.spines['top'].set_color('none')
self.ax.xaxis.set_ticks_position('bottom')
self.ax.yaxis.set_ticks_position('left')
self.ax.set_xlim(-2,2)
self.ax.set_ylim(-2,2)
self.ax.grid(color='lightgray',linestyle='--')
# Initial plot
self.l, = self.ax.plot(self.u+self.a*np.cos(self.t),
self.v+self.b*np.sin(self.t),'k')
# Slider setup
self.axcolor = 'lightgoldenrodyellow'
self.axb = self.fig.add_axes([0.25, 0.1, 0.65, 0.03], facecolor=self.axcolor)
self.axa = self.fig.add_axes([0.25, 0.15, 0.65, 0.03], facecolor=self.axcolor)
self.sb = Slider(self.axb, 'Y Radius', 0.1, 2.0, valinit=self.b)
self.sa = Slider(self.axa, 'X Radius', 0.1, 2.0, valinit=self.a)
# Call update as slider is changed
self.sb.on_changed(self.update)
self.sa.on_changed(self.update)
# Reset if reset button is pushed
self.resetax = self.fig.add_axes([0.8,0.025,0.1,0.04])
self.button = Button(self.resetax, 'Reset', color=self.axcolor, hovercolor='0.975')
self.button.on_clicked(self.reset)
# Color button setup
self.rax = self.fig.add_axes([0.025, 0.5, 0.15, 0.15], facecolor=self.axcolor)
self.radio = RadioButtons(self.rax, ('red', 'blue', 'green'), active=0)
self.radio.on_clicked(self.colorfunc)
#------------------------------------------------------
def update(self, val):
'''
Updates the plot as sliders are moved
'''
self.a = self.sa.val
self.b = self.sb.val
self.l.set_xdata(self.u+self.a*np.cos(self.t))
self.l.set_ydata(self.u+self.b*np.sin(self.t))
#------------------------------------------------------
def reset(self, event):
'''
Resets everything if reset button clicked
'''
self.sb.reset()
self.sa.reset()
#------------------------------------------------------
def colorfunc(self, label):
'''
Changes color of the plot when button clicked
'''
self.l.set_color(label)
self.fig.canvas.draw_idle()
#----------------------------------------------------------
if __name__ == '__main__':
luke_gui = LukeOutline()
luke_gui.mainloop()
es = EllipseSlider()
(UPDATE):
I have implemented the correction you showed and the code looks to work as intended except the vertical sliders I am using now do not update their position until I move the slider a second time. Please see the section from my code below:
# Call update if slider is changed
self.sa.on_changed(self.schedule_update)
self.sb.on_changed(self.schedule_update)
def update(self, value):
print(value)
self.vert_a.set_xdata(value)
self.vert_b.set_xdata(self.sb.val)
root.update()
def schedule_update(self, new_value):
if self.after_id:
root.after_cancel(self.after_id)
self.after_id = root.after(1000, self.update, new_value)
root = tk.Tk()
mainapp = MainApp(root)
root.mainloop()
Tkinter has a method named after for delaying the execution of a command. Your slider can use this to delay the effect of the slider for a second or two. If the slider moves, it can cancel the previously scheduled command and submit a new one.
Here's an example using plain tkinter for simplicity. Run the code and then move the slider quickly or slowly. You'll see that the label will only update after you haven't moved the slider for a full second.
import tkinter as tk
after_id = None
def schedule_update(new_value):
global after_id
if after_id:
root.after_cancel(after_id)
after_id = root.after(1000, update_label, new_value)
def update_label(new_value):
label.configure(text=f"New value: {new_value}")
root = tk.Tk()
label = tk.Label(root, text="", width=20)
scale = tk.Scale(
root, from_=1, to=100,
orient="horizontal", command=schedule_update
)
label.pack(side="top", fill="x", padx=20, pady=(10,0))
scale.pack(side="bottom", fill="x", padx=20, pady=10)
root.mainloop()

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 to prevent multiple graph plot in tkinter canvas

Im using function plot_graph() which is triggered everytime you open Graph window, my question is: How to plot graph with this function everytime on same place?
plot_graph function looks like this:
def plot_graph(self,event):
df_1 = self.controller.df_1
df_2 = self.controller.df_2
df_3 = self.controller.df_3
df_4 = self.controller.df_4
f = Figure(figsize=(5, 5), dpi=100)
a = f.add_subplot(111)
a.plot(df_1['mean'])
a.plot(df_2['mean'])
a.plot(df_3['mean'])
a.plot(df_4['mean'])
a.legend(['bsl morning', '1st expo', 'bsl noon', '2nd expo'])
canvas = FigureCanvasTkAgg(f, self)
canvas.draw()
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=True)
And im calling this function with self.bind("<<ShowGraph>>", self.plot_graph)
After second call of this function program starts create second graph under first one, on and on. Output of program, as you can see on image.I want to prevent this and have only one graph.
Thank you for help!
I believe you have two choices:
destroy the canvas before creating a new one. See Using tkinter -- How to clear FigureCanvasTkAgg object if exists or similar?
(I think this is a better method) create the figure/canvas in the init section, and reuse those objects to plot new data:
The following demonstrates option #2:
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import tkinter
import numpy as np
class MyClass:
def __init__(self, frame):
self.frame = frame
self.fig = Figure(figsize=(5, 5), dpi=100)
self.ax = self.fig.add_subplot(111)
self.canvas = FigureCanvasTkAgg(self.fig, self.frame)
self.canvas.draw()
self.canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=True)
self.button = tkinter.Button(self.frame, text="plot", command=self.plot_graph)
self.button.pack()
def plot_graph(self):
x, y = np.random.random(size=(2, 10))
self.ax.cla()
self.ax.plot(x, y)
self.canvas.draw()
root = tkinter.Tk()
MyFrame = tkinter.Frame(root)
MyClass(MyFrame)
MyFrame.pack()
root.mainloop()

matplotlib's zoom functionality inside a tkinter canvas

I have been trying to transfer some scripts behind a GUI (using Tkinter) and so far have made it thus far that any data that is opened is shown in a Tkinter canvas (using matplotlib to draw it).
The only problem that I have with this is that the standard zoom/scrolling that are in matplotlib (using left mouse button to 'move' the plot and right mouse button to 'zoom') are not accessible in the canvas, basically the functionality of the '4 pointed cross' in the matplotlib plot window.
I think this would require creating my own handlers but I would assume that there has to be a way to use the default handlers of matplotlib? I have also looked at 'scrolling' canvas options as mentioned in this question but those only seem to change the size of the plot area instead of zooming in/out on the data, also I do not want to add any further buttons just to manipulate the plot area.
The bare minimum code that I have currently:
#! /usr/bin/env python
from Tkinter import *
import matplotlib.pyplot as plt
import matplotlib.backends.backend_tkagg as tkagg
import tkFileDialog
class App():
def __init__(self,master):
# VARIABLES
self.inputFile = ""
self.fig = plt.Figure()
self.canvas = tkagg.FigureCanvasTkAgg(self.fig, master = master)
self.canvas.get_tk_widget().pack()
self.canvas.draw()
# FRAME
frame = Frame(master)
master.title("MassyTools 0.1.1 (Alpha)")
# VARIABLE ENTRIES
# BUTTONS
# MENU
menu = Menu(root)
root.config(menu = menu)
filemenu = Menu(menu)
menu.add_cascade(label="File", menu=filemenu)
filemenu.add_command(label="Open Input File", command = self.openFile)
calibmenu = Menu(menu)
menu.add_cascade(label="Calibrate",menu=calibmenu)
calibmenu.add_command(label="Open Calibration File", command = self.openCalibrationFile)
calibmenu.add_command(label="Calibrate", command = self.calibrateData)
def openFile(self):
file_path = tkFileDialog.askopenfilename()
setattr(self,'inputFile',file_path)
data = self.readData()
self.plotData(data)
def openCalibrationFile(self):
print "Place holder for selection of the calibration file"
def calibrateData(self):
print "Place holder for actual calibration"
def readData(self):
x_array = []
y_array = []
with open(self.inputFile,'r') as fr:
for line in fr:
line = line.rstrip('\n')
values = line.split()
x_array.append(float(values[0]))
y_array.append(float(values[1]))
return zip(x_array,y_array)
def plotData(self,data):
x_array = []
y_array = []
for i in data:
x_array.append(i[0])
y_array.append(i[1])
self.fig.clear()
self.axes = self.fig.add_subplot(111)
self.line, = self.axes.plot(x_array,y_array)
self.canvas.draw()
# Stuff that is not being used now but can be useful
"""def openFile(self,number):
name = tkFileDialog.askopenfilename()
ops = {
1: 'deglycoData',
2: 'peptideFile',
3: 'mzML'
}
setattr(self,ops[number],name)
"""
# End of 'stuff'
root = Tk()
app = App(root)
root.mainloop()
So you can affix a NavigationToolbar2TkAgg object to your canvas that will give you all the normal matplotlib methods and tools.
import matplotlib.backends.backend_tkagg as tkagg
# canvas is your canvas, and root is your parent (Frame, TopLevel, Tk instance etc.)
tkagg.NavigationToolbar2TkAgg(canvas, root)
A good example of its usage can be found here: Updating a graphs coordinates in matplotlib.
And an example of how to add custom methods to it can be found here (see class NavSelectToolbar(NavigationToolbar2TkAgg)).

Categories

Resources