Matplotlib in Tkinter - python

I am trying to plot a graph using Matplotlib in tkinter. Here the graph should plot all the values of 'a' in the range 0-24. My code is as follows
import math
import matplotlib
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from tkinter import *
def att_func(d=0, n=0, z=0):
# Getting User inputs from the UI
d = d_user.get()
n = n_user.get()
z = z_user.get()
a = (-9.87 * math.sin(2 * ((2 * math.pi * (d - 81)) / 365)) + n * z)
a_label.configure(text=a)
return (a)
#Plotting the graph
class App:
def __init__(self, master):
frame = tkinter.Frame(master)
self.nbutton_graph = tkinter.Button(frame, text="Show Graph", command=self.graph)
self.nbutton_graph.pack()
f = Figure(figsize=(5, 5), dpi=100)
ab = f.add_subplot(111)
self.line, = ab.plot(range(24))
self.canvas = FigureCanvasTkAgg(f, self)
self.canvas.show()
self.canvas.get_tk_widget().pack()
def graph(self):
day_elevation_hrs = []
for i in range(24):
day_elevation_hrs.append(att_func(i, 0, 0)[0])
self.canvas.draw()
return
root = tkinter.Tk()
app = App(root)
# User Inputs
d_user = IntVar()
n_user = DoubleVar()
z_user = DoubleVar()
nlabel_d = Label(text="Enter d").pack()
nEntry_d = Entry(root, textvariable=d_user).pack()
nlabel_n = Label(text="Enter n").pack()
nEntry_n = Entry(root, textvariable=n_user).pack()
nlabel_z = Label(text="Enter z").pack()
nEntry_z = Entry(root, textvariable=z_user).pack()
# Displaying results
nlabel_a = Label(text="a is").pack()
a_label = Label(root, text="")
a_label.pack()
root.mainloop()
Here I am able to calculate what i need. But when I am trying to plot the same, I am unable to. I tried as many modification I could. But seems to be in a stale mate. I am sure that I am going wrong somewhere. but can't figure out where.
when i try to plot the same graph with matplotlib, with out tkinter, it works. but when I try to do it in a UI with tkinter i am unable to.. Here is the code for plotting graph in matplotlib without tkinter.
import matplotlib.pylab as pl
day_elevation_hrs=[]
for i in range(24):
day_elevation_hrs.append(att_func(i, 0, 0)[0])
pl.title("Elevation of a in range i")
pl.plot(day_elevation_hrs)

Your code won't run as posted, but I can see two definite problems.
First, your App is a frame that contains a canvas, but you never add the frame to the root window. Because of that, your canvas will be invisible.
Add the following code after you create an instance of App:
app.pack(side="top", fill="both", expand=True)
Second, you are making a common mistake when defining the button to display the graph. The command attribute takes a reference to a function. However, you are calling the graph() function, and using the result as the value of the command attribute.
In other words, change this:
self.nbutton_graph = Tk.Button(self, text="Show Graph", command=self.graph())
to this:
self.nbutton_graph = Tk.Button(self, text="Show Graph", command=self.graph)
Notice the lack of () after self.graph. This could be the reason why you are seeing errors like 'App' object has no attribute 'line', since you are calling the graph function before you fully initialize all your variables.

This documentation shows that the second explicit parameter for FigureCanvasTkAgg.__init__ should be the master. (It's really a keyword parameter.)
So, have you tried changing that line to ...
self.canvas = FigureCanvasTkAgg(f, master=master)

Related

How to imbed figure object when plotting occurs outside tkinter environment

There are plenty of web examples (1,2,3,4) and threads (1,2,3) about imbedding a plot into a tkinter window, but very few that address plotting in a separate environment and importing the resulting graph to the tkinter window.
In a nutshell, I have a program that calculates many different values, and exports those values to a separate file that creates a large number of plots. My tkinter application accepts parameters in Entry boxes, before applying them to the main file that does all the calculations. Typically, I would just follow the examples I linked, but with such a large number of plots being generated and the need to be able to select any particular graph I need at a given time, this would be inefficient and time consuming to brute-force. There must be a better way!
Below is a simplified example of how I am trying to accomplish this task:
import tkinter as tk
from matplotlib import pyplot as plt
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg,
NavigationToolbar2Tk)
import numpy as np
def example_plot(A):
# Plot generated outside of tkinter environment, but controlled by
# variable within tkinter window.
x = np.linspace(0, 10, 50)
y = A*x**2
fig, ax = plt.subplots()
ax.plot(x,y)
ax.set_xlabel('x')
ax.set_ylabel('y')
return fig
window = tk.Tk()
window.geometry('256x256')
variableEntry = tk.Entry(width = 10)
variableLabel = tk.Label(window, text = "A")
variableEntry.grid(row = 0, column = 0)
variableLabel.grid(row = 0, column = 1)
def plotButton():
A = variableEntry.get()
A = int(A)
figure = Figure(figsize = (1,1), dpi = 128)
add = figure.add_subplot(1,1,1)
example = example_plot(A)
add.imshow(example)
canvas = FigureCanvasTkAgg(figure)
canvas.get_tk_widget().grid(row = 2, column = 0)
toolbar = NavigationToolbar2Tk(canvas)
toolbar.update()
canvas._tkcanvas.grid(row = 3 , column = 0)
canvas.show()
applyButton = tk.Button(master = window, text = "Apply", command = plotButton)
applyButton.grid(row = 1,column = 0)
window.mainloop()
When I run this, set A to some integer and press apply, I get an error
TypeError: Image data of dtype object cannot be converted to float
It seems that add.imshow() doesn't like that I fed it the figure. Is there some way to obtain the figure (ie: example = example_plot(A)) and store it to display later?
Try this:
import tkinter as tk
from matplotlib import pyplot as plt
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, \
NavigationToolbar2Tk
import numpy as np
def example_plot(A):
# Plot generated outside of tkinter environment, but controlled by
# variable within tkinter window.
x = np.linspace(0, 10, 50)
y = A*x*x # This should run slightly faster
fig, ax = plt.subplots()
ax.plot(x,y)
ax.set_xlabel("x")
ax.set_ylabel("y")
return fig
window = tk.Tk()
frame = tk.Frame(window)
frame.pack()
variable_entry = tk.Entry(frame, width=10)
variable_label = tk.Label(frame, text="A")
variable_entry.pack(side="left", fill="x")
variable_label.pack(side="left")
def plot():
A = int(variable_entry.get())
figure = Figure(figsize=(1, 1), dpi=128)
add = figure.add_subplot(1, 1, 1)
figure = example_plot(A)
canvas = FigureCanvasTkAgg(figure)
canvas.get_tk_widget().pack()
toolbar = NavigationToolbar2Tk(canvas, window)
toolbar.update()
canvas.get_tk_widget().pack()
# canvas.show() # There is no need for this
apply_button = tk.Button(window, text="Apply", command=plot)
apply_button.pack(fill="x")
window.mainloop()
Your example_plot returns a Figure so you can use figure = example_plot(A) and then FigureCanvasTkAgg(figure). I also added a frame and tried to make everything look better.

How can i avoid making two tkinter windows?

I have made a GUI in which on user values a line is drawn, and when you click on the line another window opens where you can select something. And when you press ok it should print on the terminal. I think because i am creating a new tkinter window when i click on the line, i am unable to retrieve the user selection.How can i solve this problem? Your help is greatly appreciated.
import matplotlib
matplotlib.use('TkAgg')
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from matplotlib import style
import tkinter as tk
from tkinter import ttk
from tkinter import *
import random
import numpy as np
LARGE_FONT = ('Verdana',12)
style.use('ggplot')
from matplotlib import pyplot as plt
class PageOne(tk.Tk):
def __init__(self):
tk.Tk.__init__(self)
label = tk.Label(self,text='Experiment', font = LARGE_FONT)
label.pack(pady=10,padx=10)
self.adding_widgets()
button2= ttk.Button(self,text='Validate', command=self.draw)
button2.pack()
button3= ttk.Button(self,text='Erase', command=self.dlet)
button3.pack()
self.lines_pts=[]
self.f1= Figure(figsize=(5,5),dpi=100)
self.b=self.f1.add_subplot(1,1,1)
self.b.set_xlim([0,10])
self.b.set_ylim([0,10])
self.canvas = FigureCanvasTkAgg(self.f1, self)
self.canvas.draw()
self.canvas.get_tk_widget().pack(side=tk.TOP,fill=tk.BOTH,expand=True)
self.f1.canvas.mpl_connect('pick_event', self.on_pick)
self.lines =[]
def adding_widgets(self,*args):
labfram=ttk.LabelFrame(self,width=100,height=100,text='Enter Member Coordinates',labelanchor=N)
labfram.pack()
l1=ttk.Label(labfram,text='x0')
l1.pack(side='left')
self.x0=Entry(labfram,width=10,)
self.x0.pack(side='left')
l2=ttk.Label(labfram,text='y0')
l2.pack(side='left')
self.y0=Entry(labfram,width=10)
self.y0.pack(side='left')
l3=ttk.Label(labfram,text='x1')
l3.pack(side='left')
self.x1=Entry(labfram,width=10)
self.x1.pack(side='left')
l4=ttk.Label(labfram,text='y1')
l4.pack(side='left')
self.y1=Entry(labfram,width=10)
self.y1.pack(side='left')
def draw(self):
p0 = float(self.x0.get()), float(self.y0.get())
p1 = float(self.x1.get()), float(self.y1.get())
self.lines_pts.append((p0,p1))
for p0,p1 in self.lines_pts:
x0, y0, x1, y1 = *p0, *p1
X = x0,x1
Y = y0,y1
ax = self.b.plot(X,Y, 'r', linewidth=4,picker=5 )
self.lines.append(ax)
self.canvas.draw()
def dlet(self):
self.b.lines.remove(self.b.lines[-1])
self.canvas.draw()
def on_pick(self,event):
w=Tk()
w.title('Channel Select')
w.geometry('250x50')
n = StringVar()
ch = ttk.Combobox(w, width = 15 , textvariable = n)
ch['values'] = ('CH1','CH2','CH3','CH4','CH5','CH6','CH7','CH8','CH9','CH10')
ch.grid(column=1,row=0)
ch_label = ttk.Label(w,text='Select Your Channel')
ch_label.grid(column=0,row=0)
ch_button = ttk.Button(w,text='OK',command=lambda: print ('value is:'+ n.get()))
ch_button.grid(column=1,row=1)
for line in self.lines:
line = event.artist
xdata, ydata = line.get_data()
ind = event.ind
print('on pick line:', np.array([xdata[ind], ydata[ind]]).T)
app = PageOne()
app.mainloop()
It's here do you really want to create a new window?
def on_pick(self,event):
w=Tk()
w.title('Channel Select')
w.geometry('250x50')
Take note that when you do this w=Tk() you are creating an instance of an object a NEW OBJECT which is Tk() so that's why it creates a new window. What is your goal are you really trying to pop up a window whenever you click cause if not you I think you can remove this.
----UPDATE---
So the reason why your window keeps popping out is because on your function you create an instance of an object with it. Then I tried putting the w=Tk() away from the function but still it would create or show that object cause it is within the mainloop.
Alternative solution is you can check if once that window exists or its state is normal then you can just do a focus.
Here is the code that I've added only on your on_pick function. I also added on your __init__ method a self.w = None to just set the w variable to None initially.
OVERALL this are only the changes made
def on_pick(self,event):
try:
if self.w.state() == "normal":
self.w.focus()
except BaseException as on_pick_error:
self.w=Toplevel()
self.w.title('Channel Select')
self.w.geometry('250x50')
n = StringVar()
ch = ttk.Combobox(self.w, width = 15 , textvariable = n)
ch['values'] = ('CH1','CH2','CH3','CH4','CH5','CH6','CH7','CH8','CH9','CH10')
ch.grid(column=1,row=0)
ch_label = ttk.Label(self.w,text='Select Your Channel')
ch_label.grid(column=0,row=0)
ch_button = ttk.Button(self.w,text='OK',command=lambda: print ('value is:'+ n.get()))
ch_button.grid(column=1,row=1)
for line in self.lines:
line = event.artist
xdata, ydata = line.get_data()
ind = event.ind
print('on pick line:', np.array([xdata[ind], ydata[ind]]).T)
You might wonder what this does, it just checks if the state of your self.w which happens to be your window that pops out, it just checks if its state is equal to "normal" meaning that it is active, then when it is it will just do a .focus() and focus on that current window. Else it would create a new window which is this self.w=Toplevel() and so on.
try:
if self.w.state() == "normal":
self.w.focus()
except BaseException as on_pick_error:
self.w=Toplevel()
self.w.title('Channel Select')
self.w.geometry('250x50')
Why is it Toplevel instead of Tk?
I would suggest to have it Toplevel however it is up to you to decide since Toplevel and Tk I think might just have the same properties but do note they are not the same so it's up to you.

Correct scrolling/resizing behavior of FigureCanvasTkAgg canvas widget when the contained figure is resized

I am trying to make an interactive plotting GUI using Tkinter and matplotlib (python 3.7 and matplotlib 3.0.0) I want the user to be able to resize the figure as it is displayed on the screen without resizing the window, and have achieved this by editing the dpi, width, and height properties of the figure. So far, this works, but if the figure is bigger than the display area, I want the user to be able to scroll to see the whole figure. And if the figure is smaller than the display area, I want the scrollbars to be disabled.
I have tried applying scrollbars directly to the FigureCanvasTkAgg object itself as well as embedding the FigureCanvasTkAgg canvas inside a second scrollable canvas, but it seems that the problem is that the drawable area of the FigureCanvasTkAgg widget doesn't change when the figure size changes. Minimal code reproducing the problem is below. Is there some property of the FigureCanvasTkAgg object that I'm missing that makes this work?
import tkinter as tk
from tkinter import ttk
from tkinter.simpledialog import askfloat
from matplotlib.figure import Figure
from matplotlib.axes import Axes
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
class InteractivePlot(tk.Frame):
def __init__(self,master,**kwargs):
super().__init__(master,**kwargs)
self._figure = Figure(dpi=150)
self._canvas = FigureCanvasTkAgg(self._figure, master=self)
self._sizebutton = tk.Button(self,text="Size (in.)", command=self._change_size)
self._axis = self._figure.add_subplot(111)
# Plot some data just to have something to look at.
self._axis.plot([0,1,2,3,4,5],[1,1,3,3,5,5],label='Dummy Data')
self._cwidg = self._canvas.get_tk_widget()
self._scrx = ttk.Scrollbar(self,orient="horizontal", command=self._cwidg.xview)
self._scry = ttk.Scrollbar(self,orient="vertical", command=self._cwidg.yview)
self._cwidg.configure(yscrollcommand=self._scry.set, xscrollcommand=self._scrx.set)
self._cwidg.bind(
"<Configure>",
lambda e: self._cwidg.configure(
scrollregion=self._cwidg.bbox("all")
)
)
self._sizebutton.grid(row=0,column=0,sticky='w')
self._cwidg. grid(row=1,column=0,sticky='news')
self._scrx. grid(row=2,column=0,sticky='ew')
self._scry. grid(row=1,column=1,sticky='ns')
self.rowconfigure(1,weight=1)
self.columnconfigure(0,weight=1)
self._canvas.draw()
def _change_size(self):
newsize = askfloat('Size','Input new size in inches')
if newsize is None:
return
w = newsize
h = newsize/1.8
self._figure.set_figwidth(w)
self._figure.set_figheight(h)
self._canvas.draw()
root = tk.Tk()
plt = InteractivePlot(root,width=400,height=400)
plt.pack(fill=tk.BOTH,expand=True)
root.mainloop()
The main issue here is that the matplotlib figure is meant to resize with self._cwidg. Because the figure is assumed to always be of the same size as self._cwidg, matplotlib only redraws the portion of the figure which is visible in self._cwidg and the latter is not resized when the figure size changes.
A work around is to use an extra canvas self._scroll_canvas and embed self._cwidg as a window inside it. Then I modified _change_size() in the following way:
def _change_size(self):
newsize = askfloat('Size', 'Input new size in inches')
if newsize is None:
return
w = newsize
h = newsize/1.8
self._cwidg.configure(width=int(w*self._conv_ratio), height=int(h*self._conv_ratio))
self._scroll_canvas.configure(scrollregion=self._scroll_canvas.bbox("all"))
I directly resize self._cwidg which in turns resizes the figure, ensuring that every part of it is redrawn. Then I update the scrollregion. Here is the full code:
import tkinter as tk
from tkinter import ttk
from tkinter.simpledialog import askfloat
from matplotlib.figure import Figure
from matplotlib.axes import Axes
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
class InteractivePlot(tk.Frame):
def __init__(self, master, **kwargs):
super().__init__(master, **kwargs)
self._scroll_canvas = tk.Canvas(self)
self._figure = Figure(dpi=150)
self._canvas = FigureCanvasTkAgg(self._figure, master=self._scroll_canvas)
self._sizebutton = tk.Button(self, text="Size (in.)", command=self._change_size)
self._axis = self._figure.add_subplot(111)
# Plot some data just to have something to look at.
self._axis.plot([0, 1, 2, 3, 4, 5], [1, 1, 3, 3, 5, 5], label='Dummy Data')
self._cwidg = self._canvas.get_tk_widget()
self._scroll_canvas.create_window(0, 0, anchor='nw', window=self._cwidg)
self._scrx = ttk.Scrollbar(self, orient="horizontal", command=self._scroll_canvas.xview)
self._scry = ttk.Scrollbar(self, orient="vertical", command=self._scroll_canvas.yview)
self._scroll_canvas.configure(yscrollcommand=self._scry.set, xscrollcommand=self._scrx.set)
self._sizebutton.grid(row=0, column=0, sticky='w')
self._scroll_canvas.grid(row=1, column=0, sticky='news')
self._scrx.grid(row=2, column=0, sticky='ew')
self._scry.grid(row=1, column=1, sticky='ns')
self.rowconfigure(1, weight=1)
self.columnconfigure(0, weight=1)
self._canvas.draw()
wi = self._figure.get_figwidth()
wp = self._cwidg.winfo_reqwidth(),
self._conv_ratio = wp / wi # get inch to pixel conversion factor
self._scroll_canvas.configure(width=wp, height=self._cwidg.winfo_reqheight())
self._scroll_canvas.configure(scrollregion=self._scroll_canvas.bbox("all"))
def _change_size(self):
newsize = askfloat('Size', 'Input new size in inches')
if newsize is None:
return
w = newsize
h = newsize/1.8
self._cwidg.configure(width=int(w*self._conv_ratio), height=int(h*self._conv_ratio))
self._scroll_canvas.configure(scrollregion=self._scroll_canvas.bbox("all"))
root = tk.Tk()
plt = InteractivePlot(root, width=400, height=400)
plt.pack(fill=tk.BOTH, expand=True)
root.mainloop()
Thanks to j_4321's answer, and after digging around in the source code for the FigureCanvasTk object, I came up with the following solution that does everything I need it to.
It looks like what the canvas object does is use tkinter.Canvas.create_image to generate a new image of the figure every time the canvas widget is resized, keeping the DPI of the figure constant and setting the width and height of the figure to keep it the same size as the canvas widget. Since I want the canvas widget to resize to the figure, I calculate the width and height from the figure properties and then pass a custom Event object to FigureCanvasTk.resize, the callback function for the canvas widget's <Configure> event.
The last trick is to set the scrollregion of the canvas to be only the size of the last canvas item created. It looks like previous iterations of the figure are not deleted from the canvas (seems like a memory leak?), so if you try to set the scrollregion to Canvas.bbox('all'), it will set the scrollregion to the size of the largest version of the figure, not the current version of the figure.
Here is the full example code:
import tkinter as tk
from tkinter import ttk
from tkinter.simpledialog import askfloat
from matplotlib.figure import Figure
from matplotlib.axes import Axes
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
class InteractivePlot(tk.Frame):
def __init__(self,master,**kwargs):
super().__init__(master,**kwargs)
self._figure = Figure(dpi=150)
self._canvas = FigureCanvasTkAgg(self._figure, master=self)
buttonframe = tk.Frame(self)
self._sizebutton = tk.Button(buttonframe,text="Size (in.)", command=self._change_size)
self._dpibutton = tk.Button(buttonframe,text="DPI", command=self._change_dpi)
self._axis = self._figure.add_subplot(111)
# Plot some data just to have something to look at.
self._axis.plot([0,1,2,3,4,5],[1,1,3,3,5,5],label='Dummy Data')
self._cwidg = self._canvas.get_tk_widget()
self._scrx = ttk.Scrollbar(self,orient="horizontal", command=self._cwidg.xview)
self._scry = ttk.Scrollbar(self,orient="vertical", command=self._cwidg.yview)
self._cwidg.configure(yscrollcommand=self._scry.set, xscrollcommand=self._scrx.set)
self._cwidg.bind("<Configure>",self._refresh)
self._sizebutton.grid(row=0,column=0,sticky='w')
self._dpibutton.grid(row=0,column=1,sticky='w')
buttonframe.grid(row=0,column=0,columnspan=2,sticky='W')
self._cwidg. grid(row=1,column=0,sticky='news')
self._scrx. grid(row=2,column=0,sticky='ew')
self._scry. grid(row=1,column=1,sticky='ns')
self.rowconfigure(1,weight=1)
self.columnconfigure(0,weight=1)
# Refresh the canvas to show the new plot
self._canvas.draw()
# Figure size change button callback
def _change_size(self):
newsize = askfloat('Size','Input new size in inches')
if newsize is None:
return
w = newsize
h = newsize/1.8
self._figure.set_figwidth(w)
self._figure.set_figheight(h)
self._refresh()
# Figure DPI change button callback
def _change_dpi(self):
newdpi = askfloat('DPI', 'Input a new DPI for the figure')
if newdpi is None:
return
self._figure.set_dpi(newdpi)
self._refresh()
# Refresh function to make the figure canvas widget display the entire figure
def _refresh(self,event=None):
# Get the width and height of the *figure* in pixels
w = self._figure.get_figwidth()*self._figure.get_dpi()
h = self._figure.get_figheight()*self._figure.get_dpi()
# Generate a blank tkinter Event object
evnt = tk.Event()
# Set the "width" and "height" values of the event
evnt.width = w
evnt.height = h
# Set the width and height of the canvas widget
self._cwidg.configure(width=w,height=h)
self._cwidg.update_idletasks()
# Pass the generated event object to the FigureCanvasTk.resize() function
self._canvas.resize(evnt)
# Set the scroll region to *only* the area of the last canvas item created.
# Otherwise, the scrollregion will always be the size of the largest iteration
# of the figure.
self._cwidg.configure(scrollregion=self._cwidg.bbox(self._cwidg.find_all()[-1]))
root = tk.Tk()
plt = InteractivePlot(root,width=400,height=400)
plt.pack(fill=tk.BOTH,expand=True)
root.mainloop()

Embedding a MatPlotLib Graph in Tkinter [.grid method], and Customizing MatPlotLib's Navigation Toolbar

I've been having problems with embedding my MatPlotLib Graph in Tkinter, and after doing some searching on Google, and the MatPlotLib website, the best I could get was the standard method:
import tkinter
from matplotlib.backends.backend_tkagg import (
FigureCanvasTkAgg, NavigationToolbar2Tk)
fig = Figure(figsize=(5, 4), dpi=100)
canvas = FigureCanvasTkAgg(fig, master=root)
canvas.draw()
canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=1)
toolbar = NavigationToolbar2Tk(canvas, root) toolbar.update()
canvas.get_tk_widget().pack(side=tkinter.TOP, fill=tkinter.BOTH, expand=1)
Now if I try to replace the packing layout with a .grid (and remove the .pack() parameters), I get a bunch of errors, and no matter how many Google searches I have tried, all of the methods of embedding a MatPlotLib graph in Tkinter are only using the pack method. Can someone help me out with this? I want to embed the graph, but using the grid method, as the rest of the layout of my GUI application is .grid layout.
Another problem I'm having with the navigation toolbar in Tkinter is the fact that the navigation toolbar can apparently be customized (At least according to SentDex [5:18]). He doesn't seem to go over how I can do this, which makes it difficult for me, because I'm not very happy with MatPlotLib's buttons (They look very archaic and outdated).
Can someone please help me out with this? When I only put the graph in, it seems to work just fine, but I get issues when trying to put in the Navigation Toolbar with the graph as well. Any help on this would be appreciated. Thanks!
Here is a simple plot and navigation toolbar inside tkinter window using grid geometry manager only.
import tkinter as tk
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)
window = tk.Tk()
btn = tk.Label(window, text='A simple plot')
btn.grid(row=0, column=0, padx=20, pady=10)
x = ['Col A', 'Col B', 'Col C']
y = [50, 20, 80]
fig = plt.figure(figsize=(4, 5))
plt.bar(x=x, height=y)
# You can make your x axis labels vertical using the rotation
plt.xticks(x, rotation=90)
# specify the window as master
canvas = FigureCanvasTkAgg(fig, master=window)
canvas.draw()
canvas.get_tk_widget().grid(row=1, column=0, ipadx=40, ipady=20)
# navigation toolbar
toolbarFrame = tk.Frame(master=window)
toolbarFrame.grid(row=2,column=0)
toolbar = NavigationToolbar2Tk(canvas, toolbarFrame)
window.mainloop()
Output GUI
I have not worked on customizing the navigation toolbar so I haven't included any solution for that part. But I'll look into it surely and update you if I find something useful. Hope you find this helpful.
I managed to get a simple app to control a matplotlib graph with window resizing.
I haven't used the navigation bar feature so that might be an addition to this framework
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
from google.cloud import bigquery
import os, json, sys
from time import time
import pandas as pd
class SliderGraph:
def __init__(self, master):
self.master = master
# with open(self.resource_path(config_file)) as fp:
# self.config = json.load(fp)
# os.environ['GOOGLE_APPLICATION_CREDENTIALS'] = self.resource_path(creds_file)
# self.projectID = self.config['projectID']
# self.datasetID = self.config['datasetID']
# last_used_freq = self.config['last_used_frequency']
# last_used_delta = self.config['last_used_delta']
self.bounds = 1
self.frame = tk.Frame(master)
self.fig = Figure()
row = 0
self.ax = self.fig.add_subplot(111)
self.ax.set_xlabel("Run Numbers")
self.ax.set_ylabel("UDD")
self.ax.set_ylim([-self.bounds,self.bounds])
self.canvas = FigureCanvasTkAgg(self.fig, master=master) # , width=win_width, height=(win_height-50))
self.canvas.draw()
self.canvas.get_tk_widget().grid(row=row, columnspan=2, sticky='nsew')
row+=1
self.table_label = tk.Label(master, text="Enter BigQuery Table Name")
self.table_label.grid(row=row, column=0)
# row += 1
self.table_name = tk.Entry(master)
# self.table_name.insert(0,self.config['last_used_table'])
self.table_name.grid(row=row, column=1, sticky='ew')
row += 1
self.get_table_button = tk.Button(master, text="Get Table Data and Plot", command=self.plot_data)
self.get_table_button.grid(row=row, columnspan=2)
row += 1
self.frequency_slider = tk.Scale(master, from_=400, to=4500, orient=tk.HORIZONTAL, command=self.update_plot)
# self.frequency_slider.set(last_used_freq)
self.frequency_slider.grid(row=row,columnspan=2, sticky="nsew")
row += 1
self.frequency_entry = tk.Entry(master)
# self.frequency_entry.insert(0,last_used_freq)
self.frequency_entry.grid(row=row, columnspan=2)
row += 1
self.delta_slider = tk.Scale(master, from_=-500, to=500, orient=tk.HORIZONTAL, command=self.update_plot)
# self.delta_slider.set(last_used_delta)
self.delta_slider.grid(row=row, columnspan=2, sticky="ensw")
row += 1
self.delta_entry = tk.Entry(master)
# self.delta_entry.insert(0, last_used_delta)
self.delta_entry.grid(row=row, columnspan=2)
row += 1
self.get_table_button = tk.Button(master, text="Autoscale", command=self.autoscale)
self.get_table_button.grid(row=row,columnspan=2)
row += 1
tk.Grid.columnconfigure(master, 0, weight=1)
tk.Grid.columnconfigure(master, 1, weight=1)
tk.Grid.rowconfigure(master, 0, weight=5)
for x in range(1,row):
tk.Grid.rowconfigure(master, x, weight=0)
master.protocol('WM_DELETE_WINDOW', self.close)
self.df = None
self.frequency_list = []
self.series = None
self.elapsed = time()*1000
def plot_data(self):
self.ax.clear()
self.client = bigquery.Client(project=self.projectID)
self.tableID = f"`{self.datasetID}.{self.table_name.get()}`"
QUERY = (
f"SELECT * EXCEPT (testCount,elapsed_run_time_ms_,moleculeValue,onboardTemp1,onboardTemp2,temp,tempControl,\
logamp, txrx,timestamp) FROM {self.tableID} ORDER BY runCount ASC;"
)
# query_job = self.client.query(QUERY)
# rows = query_job.result()
self.df = pd.read_gbq(QUERY,self.projectID)
for col in self.df.columns:
if 'runCount' in col:
continue
self.frequency_list.append(int(col.replace('_','')))
self.frequency_slider.configure(from_=min(self.frequency_list),to=max(self.frequency_list))
self.delta_slider.configure(from_=-max(self.frequency_list),to=max(self.frequency_list))
freq = f'_{self.frequency_slider.get()}'
freq2 = f'_{self.frequency_slider.get() + self.delta_slider.get()}'
self.series = self.df[freq] - self.df[freq2]
self.series.plot(ax=self.ax)
self.ax.set_ylim([-self.bounds,self.bounds])
self.fig.canvas.draw()
self.fig.canvas.flush_events()
pass
def update_plot(self, newslider):
try:
self.ax.clear()
freq = f'_{self.frequency_slider.get()}'
freq2 = f'_{self.frequency_slider.get() + self.delta_slider.get()}'
self.series = self.df[freq] - self.df[freq2]
self.series.plot(ax=self.ax)
zero = self.series.mean()
self.ax.set_ylim([zero-self.bounds, zero+self.bounds])
self.fig.canvas.draw()
self.fig.canvas.flush_events()
# self.master.update_idletasks()
if ((time()*1000)-self.elapsed > 100):
self.elapsed = time()*1000
self.frequency_entry.delete(0,'end')
self.frequency_entry.insert(0,freq.replace('_',''))
self.delta_entry.delete(0,'end')
self.delta_entry.insert(0,self.delta_slider.get())
except:
pass
def play_runs(self):
pass
def autoscale(self):
self.ax.clear()
self.series.plot(ax=self.ax)
self.ax.relim()
self.ax.autoscale()
zero = self.series.mean()
self.bounds = self.series.max() - self.series.min()
self.ax.set_ylim([zero-self.bounds,zero+self.bounds])
self.fig.canvas.draw()
self.fig.canvas.flush_events()
def close(self):
self.master.destroy()
if __name__=="__main__":
root = tk.Tk()
slider_fig = SliderGraph(root)
root.mainloop()

How to update value in Tkinter plot of brain slices to be able to page through different brain volumes from a 4D image

I am trying to design a python gui to be able to assess impacts of motion by plotting brain slices, the framewise displacement timeseries, and different outputs of motion detection algorithms. I want to be able to slide through each of the brain volumes individually (180 volumes per scan) so that I can compare the FD timecourse to what the actual brain data looks like.
I've been using tkinter and I can plot several slices of one brain volume, but I'm having updating volume that is selected. I've tried creating buttons to advance and go back, and also using a tkinter Scale.
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
from matplotlib.figure import Figure
from matplotlib import pyplot as plt
import os
import nibabel
from nilearn import plotting
from nilearn import image
from matplotlib.widgets import Slider, Button, RadioButtons
data_path = os.getcwd()
file='sub-HV01baseline_task-EA1_bold.nii'
file_path = os.path.join(data_path,file)
EA1=nibabel.load(file_path)
struct_arr2 = EA1.get_data()
vol=1
import tkinter as tk
from tkinter import ttk
fig = plt.Figure(figsize=(10,5), dpi=100)
class App:
def __init__(self, master):
self.event_num = 1
frame = tk.Frame(master)
frame.pack()
self.txt = tk.Entry(frame,width=10)
self.txt.pack(side="bottom")
self.lbl = tk.Label(frame, text="FD Value")
self.lbl.pack(side="bottom")
self.btn = tk.Button(frame, text = "Update",command=self.clicked)
self.btn.pack(side="bottom")
self.txt.focus()
self.var =tk.IntVar(frame)
self.var.set(0)
self.vol_scale=tk.Scale(frame,from_=0, to=180,orient="horizontal",sliderlength=20,command=self.show_slices(fig))
self.increase_btn = tk.Button(frame, text = "Increase",command=self.show_slices(fig))
self.increase_btn.pack(side="bottom")
self.vol_scale.pack(side="bottom")
#self.spin = tk.Spinbox(frame, from_=0, to=180, width=5, textvariable=self.var)
#self.spin.pack(side="bottom")
self.canvas = FigureCanvasTkAgg(fig,master=master)
self.canvas.get_tk_widget().pack(side=tk.TOP)
def clicked(self):
res = "FD = " + self.txt.get()
self.lbl.configure(text = res)
def show_slices(self,fig):
vol = self.vol_scale.get()
slice_0 = struct_arr2[:, :, 10,vol]
slice_1 = struct_arr2[:, : , 15,vol]
slice_2 = struct_arr2[:, :, 20,vol]
slice_3 = struct_arr2[:, :, 25,vol]
slice_4 = struct_arr2[:, : , 30,vol]
slices=[slice_0, slice_1, slice_2, slice_3, slice_4]
axes = fig.subplots(1, len(slices))
#svol = Slider(axes, 'Vol', 0, 180, valinit=0, valstep=1)
fig.subplots_adjust(hspace=0, wspace=0)
for i, slice in enumerate(slices):
axes[i].xaxis.set_major_locator(plt.NullLocator())
axes[i].yaxis.set_major_locator(plt.NullLocator())
axes[i].imshow(slice.T, origin="lower")
root=tk.Tk()
app = App(root)
root.mainloop()
Currently I'm getting an error that "App has no attribute 'vol_scale'" even though I've defined it above.

Categories