Interactive plot based on Tkinter and matplotlib - python

Dear programmming communauty,
I am trying to perform a "interactive plot" based on Tkinter and pylab.plot in order to plot 1D values. The abssissa are a 1D numpy array x and the ordonates values are in a multidimension array Y, eg.
import numpy
x = numpy.arange(0.0,3.0,0.01)
y = numpy.sin(2*numpy.pi*x)
Y = numpy.vstack((y,y/2))
I want to display y or y/2 (the elements of Y matrix) according to x and change between them with 2 buttons left and right (in order to go to more complex cases). Usually I create some functions like the following to plot graphs.
import pylab
def graphic_plot(n):
fig = pylab.figure(figsize=(8,5))
pylab.plot(x,Y[n,:],'x',markersize=2)
pylab.show()
To add two buttons to change the value of nparameter, I have tried this without success :
import Tkinter
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
class App:
def __init__(self,master):
# Create a container
frame = Tkinter.Frame(master)
frame.pack()
# Create 2 buttons
self.button_left = Tkinter.Button(frame,text="<",command=self.decrease)
self.button_left.pack(side="left")
self.button_right = Tkinter.Button(frame,text=">",command=self.increase)
self.button_right.pack(side="left")
self.canvas = FigureCanvasTkAgg(fig,master=self)
self.canvas.show()
def decrease(self):
print "Decrease"
def increase(self):
print "Increase"
root = Tkinter.Tk()
app = App(root)
root.mainloop()
Can someone help me to understand how to perform such kind of feature ? Many thanks.

To change the y-values of the line, save the object that's returned when you plot it (line, = ax.plot(...)) and then use line.set_ydata(...). To redraw the plot, use canvas.draw().
As a more complete example based on your code:
import Tkinter
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
class App:
def __init__(self, master):
# Create a container
frame = Tkinter.Frame(master)
# Create 2 buttons
self.button_left = Tkinter.Button(frame,text="< Decrease Slope",
command=self.decrease)
self.button_left.pack(side="left")
self.button_right = Tkinter.Button(frame,text="Increase Slope >",
command=self.increase)
self.button_right.pack(side="left")
fig = Figure()
ax = fig.add_subplot(111)
self.line, = ax.plot(range(10))
self.canvas = FigureCanvasTkAgg(fig,master=master)
self.canvas.show()
self.canvas.get_tk_widget().pack(side='top', fill='both', expand=1)
frame.pack()
def decrease(self):
x, y = self.line.get_data()
self.line.set_ydata(y - 0.2 * x)
self.canvas.draw()
def increase(self):
x, y = self.line.get_data()
self.line.set_ydata(y + 0.2 * x)
self.canvas.draw()
root = Tkinter.Tk()
app = App(root)
root.mainloop()

Related

How can an interactive subplot in Tkinter be improved in terms of speed?

I am currently working on a program that I am creating with Tkinter. Thereby large matrices (signals) are read in, which I represent as an image. In addition, I would like to display the signal at the point X (red vertical line) in the adjacent plot (interactive).
# Imports
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import matplotlib.pyplot as plt
import numpy as np
from tkinter import *
# Global: Selected points with cursor
points = []
# Cursor
class Cursor:
def __init__(self, ax):
self.ax = ax
self.background = None
self.horizontal_line = ax.axhline(color='r', lw=0.8, ls='--')
self.vertical_line = ax.axvline(color='r', lw=0.8, ls='--')
self._creating_background = False
ax.figure.canvas.mpl_connect('draw_event', self.on_draw)
def on_draw(self, event):
self.create_new_background()
def set_cross_hair_visible(self, visible):
need_redraw = self.horizontal_line.get_visible() != visible
self.horizontal_line.set_visible(visible)
self.vertical_line.set_visible(visible)
return need_redraw
def create_new_background(self):
if self._creating_background:
return
self._creating_background = True
self.set_cross_hair_visible(False)
self.ax.figure.canvas.draw_idle()
self.background = self.ax.figure.canvas.copy_from_bbox(self.ax.bbox)
self.set_cross_hair_visible(True)
self._creating_background = False
def on_mouse_move(self, event, mode: str, matrix=None):
if self.background is None:
self.create_new_background()
if not event.inaxes:
need_redraw = self.set_cross_hair_visible(False)
if need_redraw:
self.ax.figure.canvas.restore_region(self.background)
self.ax.figure.canvas.blit(self.ax.bbox)
else:
self.set_cross_hair_visible(True)
x, y = event.xdata, event.ydata
if mode == "both":
self.horizontal_line.set_ydata(y)
self.vertical_line.set_xdata(x)
self.ax.figure.canvas.restore_region(self.background)
self.ax.draw_artist(self.horizontal_line)
self.ax.draw_artist(self.vertical_line)
elif mode == "horizontal":
self.ax.cla()
self.ax.plot(matrix[:, int(x)], range(0, matrix.shape[0], 1))
self.ax.figure.canvas.draw_idle()
self.horizontal_line.set_ydata(y)
self.ax.figure.canvas.restore_region(self.background)
self.ax.draw_artist(self.horizontal_line)
self.ax.figure.canvas.blit(self.ax.bbox)
# Graphical User Interface
class ToolGUI:
def __init__(self, master):
self.master = master
# Matrix (Example)
self.matrix = np.random.rand(3000, 5000)
# Subplots
self.fig = plt.figure(constrained_layout=True)
self.spec = self.fig.add_gridspec(5, 6)
self.ax_main = self.fig.add_subplot(self.spec[:, :-1])
self.ax_main.imshow(self.matrix, cmap='gray', aspect='auto')
self.ax_right = self.fig.add_subplot(self.spec[:, -1:], sharey=self.ax_main)
self.ax_right.get_yaxis().set_visible(False)
# Canvas - Drawing Area
self.canvas = FigureCanvasTkAgg(self.fig, master=master)
self.canvas.get_tk_widget().grid(column=0, row=0, sticky=NSEW)
# Cursor with crosshair
self.cursor_main = Cursor(self.ax_main)
self.fig.canvas.mpl_connect('motion_notify_event', lambda event: self.cursor_main.on_mouse_move(event, mode="both"))
self.cursor_right = Cursor(self.ax_right)
self.fig.canvas.mpl_connect('motion_notify_event', lambda event: self.cursor_right.on_mouse_move(event, mode="horizontal", matrix=self.matrix))
# Update Canvas
self.canvas.draw() # Update canvas
# Create root window
root = Tk()
# Root window title
root.title("Tool")
# Create GUI
my_gui = ToolGUI(root)
# Execute Tkinter
root.mainloop()
This example is only a small part of my program. In my full program, for example, certain points are picked out manually. With the help of the interactive plot a more exact selection of such points is possible.
Unfortunately, the program runs very slowly due to the use of this interactive plot. Since I haven't been working with Python for too long, I would appreciate any suggestions for improvement!
Thanks in advance! - Stefan

Why does my matplotlib graph embedded in a tkinter frame not display its x-axis values and gridlines until after I pan it?

I have been stuck on this for a while and cannot work out the issue, so was hoping somebody on here could assist.
I am attempting to create a program to graph some data stored in a .csv file. My program starts with an empty graph and requires the user to first select a file, and then prompts the user to specify which columns from the csv file should be plotted. For the purposes of getting assistance, I have created a smaller program to outline the issue that I am experiencing.
As part of the graphing process, I want to have 5 gridlines displayed on the x-axis at all times, with the interval between these lines determined by the current viewable x-axis limits. For this reason, I have created a 'CustomFormatter' class which inherits from matplotlib.ticker's Formatter class, and overridden the 'call' method to try and get the desired behaviour. I believe that I need to do this in order to keep track of the current viewable xlimits of the graph, which then enable me to calculate the required intervals.
When I run my code, the following GUI is generated, which seems to be working as intended:
enter image description here
However, once I click the 'Load Data' button, my specified data is loaded into the graph, but the graph's x-axis labels and gridlines disappear, as shown in the below image.
enter image description here
It is not until I use the built-in pan tool to pan the graph that these gridlines and labels reappear, as shown in the below image.
enter image description here
The same thing happens when I zoom in on the graph. After zooming, it is not until I pan again that the x-axis labels and gridlines re-calibrate themselves to the desired positions.
Any help that anyone is willing to provide to help me solve this issue would be greatly appreciated. Hopefully it is just something basic or silly that I am overlooking.
Cheers.
The code I used to generate my matplotlib graph embedded in a tkinter frame is shown below:
import tkinter as tk
import matplotlib
import matplotlib.dates as mdates
from matplotlib.ticker import Formatter
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
class CustomFormatter(Formatter):
def __init__(self, ax, graph):
super().__init__()
self.set_axis(ax)
self.ax = ax
self.graph = graph
def __call__(self, x, pos=None):
xlims = self.ax.get_xlim()
ylims = self.ax.get_ylim()
xmin, xmax = xlims[0], xlims[1]
xmin = mdates.num2date(xmin)
xmax = mdates.num2date(xmax)
x = mdates.num2date(x)
interval = (xmax - xmin) / 6
self.ax.set_xticks([xmin, xmin + 1 * interval, xmin + 2 * interval, xmin + 3 * interval, xmin + 4 * interval, xmin + 5 * interval, xmax])
self.ax.grid(b=True, which='major', axis='both')
return f"{x.hour}:{x.minute}:{x.second}\n{x.day}/{x.month}/{x.year}"
class App(object):
def __init__(self, master):
initialFrame = tk.Frame(master)
initialFrame.pack(fill=tk.BOTH, expand=1)
master.title('Tkinter Graph Example')
self.master = master
self._load_data_button = tk.Button(initialFrame, takefocus=0, text='Load data')
self._load_data_button.bind('<Button-1>', self.load_data)
self._load_data_button.pack(side=tk.TOP, fill=tk.BOTH, padx=2, pady=2, ipady=5)
self.graphFrame = tk.Frame(initialFrame, borderwidth=5, relief=tk.SUNKEN)
self.graphFrame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
self.f=Figure(figsize=(5,5), dpi=100)
self.a=self.f.add_subplot(111)
self.graph = FigureCanvasTkAgg(self.f, self.graphFrame)
self.graph.get_tk_widget().pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)
self.toolbar = NavigationToolbar2Tk(self.graph, self.graphFrame)
self.toolbar.update()
self.graph._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
self.a.set_xlabel('Time')
self.a.set_ylabel('Y-Axis')
self.a.set_title('Test')
self.a.grid(b=True)
self.a.xaxis.set_major_formatter(CustomFormatter(self.a, self.graph))#, self.df, self.toPlot, self.index_dict, self.name_dict))
self.graph.draw()
def load_data(self, event):
data = [1, 4, 2, 7, 4, 6]
time = [18820.410517624932, 18820.414807940015, 18820.41623804504, 18820.41766815007, 18820.41862155342, 18820.420528360122]
self.a.plot(time, data)
self.a.set_xlim(xmin=time[0], xmax=time[-1])
self.graph.draw()
def main():
root = tk.Tk()
app = App(root)
root.geometry('%dx%d+0+0'%(800,600))
root.mainloop()
if __name__ == '__main__':
main()
In the end I managed to figure out a solution to my issue. I added the following two lines of code to my App's _init method, along with two new function definitions.
self.a.callbacks.connect('xlim_changed', self.on_xlims_change)
self.a.callbacks.connect('ylim_changed', self.on_ylims_change)
def on_xlims_change(self, event_ax):
self.graph.draw()
def on_ylims_change(self, event_ax):
self.graph.draw()
These event handlers enabled me to redraw the graph whenever the x or y limits are changed (as they are during zoom or pan operations). I am now observing my desired behaviour.
Full code is shown below.
import tkinter as tk
import matplotlib
import matplotlib.dates as mdates
from matplotlib.ticker import Formatter
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
from matplotlib.figure import Figure
class CustomFormatter(Formatter):
def __init__(self, ax, graph):
super().__init__()
self.set_axis(ax)
self.ax = ax
self.graph = graph
def __call__(self, x, pos=None):
xlims = self.ax.get_xlim()
ylims = self.ax.get_ylim()
xmin, xmax = xlims[0], xlims[1]
xmin = mdates.num2date(xmin)
xmax = mdates.num2date(xmax)
x = mdates.num2date(x)
interval = (xmax - xmin) / 6
self.ax.set_xticks([xmin, xmin + 1 * interval, xmin + 2 * interval, xmin + 3 * interval, xmin + 4 * interval, xmin + 5 * interval, xmax])
self.ax.grid(b=True, which='major', axis='both')
return f"{x.hour}:{x.minute}:{x.second}\n{x.day}/{x.month}/{x.year}"
class App(object):
def __init__(self, master):
initialFrame = tk.Frame(master)
initialFrame.pack(fill=tk.BOTH, expand=1)
master.title('Tkinter Graph Example')
self.master = master
self._load_data_button = tk.Button(initialFrame, takefocus=0, text='Load data')
self._load_data_button.bind('<Button-1>', self.load_data)
self._load_data_button.pack(side=tk.TOP, fill=tk.BOTH, padx=2, pady=2, ipady=5)
self.graphFrame = tk.Frame(initialFrame, borderwidth=5, relief=tk.SUNKEN)
self.graphFrame.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
self.f=Figure(figsize=(5,5), dpi=100)
self.a=self.f.add_subplot(111)
self.graph = FigureCanvasTkAgg(self.f, self.graphFrame)
self.graph.get_tk_widget().pack(side=tk.BOTTOM, fill=tk.BOTH, expand=True)
self.toolbar = NavigationToolbar2Tk(self.graph, self.graphFrame)
self.toolbar.update()
self.graph._tkcanvas.pack(side=tk.TOP, fill=tk.BOTH, expand=True)
self.a.set_xlabel('Time')
self.a.set_ylabel('Y-Axis')
self.a.set_title('Test')
self.a.grid(b=True)
self.a.callbacks.connect('xlim_changed', self.on_xlims_change)
self.a.callbacks.connect('ylim_changed', self.on_ylims_change)
self.a.xaxis.set_major_formatter(CustomFormatter(self.a, self.graph))#, self.df, self.toPlot, self.index_dict, self.name_dict))
self.graph.draw()
def on_xlims_change(self, event_ax):
self.graph.draw()
def on_ylims_change(self, event_ax):
self.graph.draw()
def load_data(self, event):
data = [1, 4, 2, 7, 4, 6]
time = [18820.410517624932, 18820.414807940015, 18820.41623804504, 18820.41766815007, 18820.41862155342, 18820.420528360122]
self.a.plot(time, data)
self.a.set_xlim(xmin=time[0], xmax=time[-1])
self.graph.draw()
def main():
root = tk.Tk()
app = App(root)
root.geometry('%dx%d+0+0'%(800,600))
root.mainloop()
if __name__ == '__main__':
main()

Python Matplot animation embedding in Tkinter

I'm currently working on a visualisation of different sorting algorithms and am now attempting to embed the animations in a tkinker gui for ease of use. The animations are done with matplotlib and use bar charts. I'm currently struggling to get the graphs to be animated. I had it working without the gui but currently I get a static graph in the gui. Please could I have some advice on how to rectify this. Full code so far can be found at https://github.com/MGedney1/Sorting_Algorithm_Visualisation
Area of interest code:
import random
import time
import matplotlib
import matplotlib.pyplot as plt
import matplotlib.animation as animation
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import tkinter as tk
from tkinter import Frame,Label,Entry,Button
def bubble_sort(lst): #Bubble Sort
index = len(lst) - 1
while index >= 0:
test_index = index
while test_index >= 0:
if lst[index] < lst[test_index]:
temp = lst[index]
lst[index] = lst[test_index]
lst[test_index] = temp
test_index -= 1
yield lst
index -= 1
class Window(Frame):
def __init__(self, master = None):
Frame.__init__(self,master)
self.master = master
self.set_up_window()
def update_fig(self): #Update fig function
for self.rect, val in zip(self.rects,self.unordered): #Setting height of the rectangles
self.rect.set_height(val)
self.iteration[0] += 1
text.set_text("# of operations: {}".format(iteration[0]))
def set_up_window(self):
n,self.unordered = create_array()
title = 'Test'
generator = bubble_sort(self.unordered)
self.fig,self.ax = plt.subplots() #Creating axis and figure
self.bar_rects = self.ax.bar(range(len(self.unordered)), self.unordered, align="edge") #Creating the rectangular bars
self.ax.set_xlim(0, n) #Axis limits
self.ax.set_ylim(0, int(1.07 * n))
self.text = self.ax.text(0.02, 0.95, "", transform=self.ax.transAxes) #Number of operations counter
self.iteration = [0]
self.anim = animation.FuncAnimation(self.fig, func=self.update_fig, frames=generator, interval=1,repeat=False) #Creating the animatio
self.canvas = FigureCanvasTkAgg(self.fig, master=root)
self.canvas.get_tk_widget().grid(column=0,row=1)
if __name__ == '__main__':
root = tk.Tk()
root.geometry("700x400")
app = Window(root)
tk.mainloop()

How to set the x and y Scale of a matplotlib Plot in a tkinter canvas

I have the following code:
from tkinter import *
import matplotlib
matplotlib.use("TkAgg")
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
from matplotlib.figure import Figure
class tkPlot:
def __init__(self,master):
frame = Frame(master)
frame.pack()
canvasSubFrame=Frame(frame)
canvasSubFrame.grid(row=0, column=0)
f = Figure(figsize=(8,8), dpi=100)
subplotA = f.add_subplot(211)
subplotA.set_title("Plot A")
subplotB = f.add_subplot(212)
subplotB.set_title("Plot B")
subplotA.plot([1,2,3,4,5,6],[1,4,9,16,25,36])
subplotB.plot([1,2,3,4,5,6],[1,1/2,1/3,1/4,1/5,1/6])
canvas = FigureCanvasTkAgg(f, master=canvasSubFrame)
canvas.draw()
canvas.get_tk_widget().pack(expand=False)
if __name__ == "__main__":
root = Tk()
app = tkPlot(root)
root.mainloop()
Ideally, i would like to have a textbox or spin control besides every top and bottom of each subplot to set the y axes min and max scales (y limits)
What is the proper way to do this. It should work for any number of subplots.

Tkinter updating matplotlib figure from different tk.Button in another frame

I'm building a GUI using Tkinter for the first time and have run into a problem updating the data in a Matplotlib Figure using a button in a different frame. Below is some generalized code to show the error I'm getting.
from Tkinter import *
import numpy as np
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2TkAgg
from matplotlib.figure import Figure
class testApp():
def app(self):
self.root = Tk()
self.create_frame1()
self.create_frame2()
self.root.mainloop()
def create_frame1(self):
frame1 = Frame(self.root)
frame1.grid(row=0)
(x, y) = self.create_data()
f = plt.Figure(figsize = (5,2), dpi=100)
a = f.add_subplot(111)
lines1, = a.plot(x, y)
f.tight_layout()
canvas = FigureCanvasTkAgg(f, frame1)
canvas.get_tk_widget().grid()
def create_frame2(self):
frame2 = Frame(self.root)
frame2.grid(row=1)
reEval = Button(frame2, text = "Reevaluate", command = lambda: self.reRand()).grid(sticky = "W")
def reRand(self):
(x, y) = self.create_data()
ax = self.root.frame1.lines1
ax.set_data(x, y)
ax.set_xlim(x.min(), x.max())
ax.set_ylim(y.min(), y.max())
self.root.frame1.canvas.draw()
def create_data(self):
y = np.random.uniform(1,10,[25])
x = np.arange(0,25,1)
return (x, y)
if __name__ == "__main__":
test = testApp()
test.app()
When I run this code, I get an error:
AttributeError: frame1
I think my problem stems from how I am referencing the frame containing the figure, itself, so I'm fairly certain this problem is arising from my lack of Tkinter experience. Any help would be greatly appreciated.
This is more to do with using Python classes than Tkinter. All you really need to do is change all frame1 in create_frame1 to be self.frame1. Similarly in reRand, self.root.frame1 becomes self.frame1.
As it is the name "frame1" doesn't exist beyond the end of create_frame1, but if you save it as an attribute of self you can access it later.

Categories

Resources