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()
Related
I'm trying to create a GUI that will plot data from an array that is updated when I click a button (in this case move a slider), I can get the data to replot, however, the scale doesn't update and I can't work out how to do it. does anyone have any ideas?
the data is created in a function dataMaker and just returns 2 arrays, one of the time increments and the other of random data.
many thanks
import tkinter
from matplotlib.backends.backend_tkagg import (
FigureCanvasTkAgg, NavigationToolbar2Tk)
# Implement the default Matplotlib key bindings.
from matplotlib.backend_bases import key_press_handler
from matplotlib.figure import Figure
import numpy as np
import dataMaker as DM
root = tkinter.Tk()
root.wm_title("Embedding in Tk")
fig = Figure(figsize=(5, 4), dpi=100)
rt_data = DM.comboP(1, 1000)
sample = rt_data[1]
torque = rt_data[2]
ax = fig.add_subplot()
line, = ax.plot(sample, torque)
canvas = FigureCanvasTkAgg(fig, master=root) # A tk.DrawingArea.
canvas.draw()
# pack_toolbar=False will make it easier to use a layout manager later on.
toolbar = NavigationToolbar2Tk(canvas, root, pack_toolbar=False)
toolbar.update()
canvas.mpl_connect(
"key_press_event", lambda event: print(f"you pressed {event.key}"))
canvas.mpl_connect("key_press_event", key_press_handler)
button_quit = tkinter.Button(master=root, text="Quit", command=root.destroy)
def update_frequency(new_val):
rt_data = DM.comboP(1, 1000)
sample = rt_data[1]
torque = rt_data[2]
line.set_data(sample, torque)
ax.autoscale_view()
# required to update canvas and attached toolbar!
canvas.draw()
slider_update = tkinter.Scale(root, from_=1, to=5, orient=tkinter.HORIZONTAL,
command=update_frequency, label="Frequency [Hz]")
# Packing order is important. Widgets are processed sequentially and if there
# is no space left, because the window is too small, they are not displayed.
# The canvas is rather flexible in its size, so we pack it last which makes
# sure the UI controls are displayed as long as possible.
button_quit.pack(side=tkinter.BOTTOM)
slider_update.pack(side=tkinter.BOTTOM)
toolbar.pack(side=tkinter.BOTTOM, fill=tkinter.X)
canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=True)
tkinter.mainloop()
I've searched the internet but can't find a solution, id like the scale the change to fit the data that's in the array
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.
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()
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
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()