Tkinter Matplotlib and Thread - python

In the main loop, a 2 x 2 tkinter grid is cretaed with
one label in each cell of the first line.
In the second line, two Matplotlib figures are crated with a subplot
Two functions are in charge to dynamicly refresh the grid.
They are running each one in a thread.
The first line (two labels) is well refresh by the two functions.
But, in the second line nothing ... no plot !
from tkinter import *
import threading
import time
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib import style
def read_api1():
n = 0
while 1:
n = n + 1
texte1.config(text="fig1 " + str(n))
ax1.plot([1,2], [12,14])
time.sleep(2)
ax1.cla()
def read_api2():
m = 0
while 1:
m = m + 1
texte2.config(text="fig2 " + str(m))
ax2.plot([.1,.2,.3], [2,4,3])
time.sleep(1)
main = Tk()
style.use("ggplot")
texte1 = Label(main, text="fig1")
texte1.grid(row=0,column=0)
fig1 = Figure(figsize=(2, 2), dpi=112)
ax1 = fig1.add_subplot()
fig1.set_tight_layout(True)
graph = FigureCanvasTkAgg(fig1, master=main)
canvas = graph.get_tk_widget()
canvas.grid(row=1, column=0)
texte2 = Label(main, text="fig2")
texte2.grid(row=0,column=1)
fig2 = Figure(figsize=(2, 2), dpi=112)
ax2 = fig2.add_subplot()
fig2.set_tight_layout(True)
graph = FigureCanvasTkAgg(fig2, master=main)
canvas = graph.get_tk_widget()
canvas.grid(row=1, column=1)
t = threading.Thread(target=read_api1)
t.start()
t = threading.Thread(target=read_api2)
t.start()
main.mainloop()
Any help would be appreciated :)
EDIT:
Some more details #furas:
read_api2 is supposed to get data from a WEB API at specific time. So what you recommend (after method) should work.
read_api1 is supposed to acquire data from a serial port (GPIO UART). So the thread will be waiting for data beeing available for reading.
In that case, I don't see how to use the after method
In other words, the question is : how to refresh a matplotlib plot in a tkinter environnement based on asynchronous input ? The asynchronous serial data read cannot be in the mainloop, so I put it in the thread but even with graph.draw(), it does not work. Any suggestion ?

There are two problems:
it needs graph.draw() to update/redraw plot
usually GUIs don't like to run in thread and it seems graph.draw() doesn't work in thread (at least on my Linux).
You may have to use main.after(1000, main_api1) to run the same function after 1000ms (1s) without using thread and without blocking mainloop()
import tkinter as tk # PEP8: `import *` is not preferred
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib import style
import random
# --- functions ---
def read_api1():
global n
n = n + 1
texte1.config(text="fig1 " + str(n))
ax1.cla()
ax1.plot([1,2], [random.randint(0,10),random.randint(0,10)])
graph1.draw()
main.after(1000, read_api1)
def read_api2():
global m
m = m + 1
texte2.config(text="fig2 " + str(m))
ax2.cla()
ax2.plot([.1,.2,.3], [random.randint(0,10),random.randint(0,10),random.randint(0,10)])
graph2.draw()
main.after(1000, read_api2)
# --- main ---
m = 0
n = 0
main = tk.Tk()
style.use("ggplot")
texte1 = tk.Label(main, text="fig1")
texte1.grid(row=0, column=0)
fig1 = Figure(figsize=(2, 2), dpi=112)
ax1 = fig1.add_subplot()
fig1.set_tight_layout(True)
graph1 = FigureCanvasTkAgg(fig1, master=main)
canvas1 = graph1.get_tk_widget()
canvas1.grid(row=1, column=0)
#graph1.draw()
texte2 = tk.Label(main, text="fig2")
texte2.grid(row=0,column=1)
fig2 = Figure(figsize=(2, 2), dpi=112)
ax2 = fig2.add_subplot()
fig2.set_tight_layout(True)
graph2 = FigureCanvasTkAgg(fig2, master=main)
canvas2 = graph2.get_tk_widget()
canvas2.grid(row=1, column=1)
#graph2.draw()
read_api1()
read_api2()
main.mainloop()
EDIT: Example which runs two threads. Every threads generate data in different speed and use two queues to send data to main thread. And main thread use two after() to check two queues and update two plots.
import tkinter as tk # PEP8: `import *` is not preferred
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib import style
import random
import threading
import queue
import time
# --- functions ---
def WEB_API(queue):
# it will run in thread
print('WEB_API: start')
while web_api_running:
value = random.randint(1, 3)
time.sleep(.1)
print('WEB_API:', value)
queue.put(value)
def GPIO_API(queue):
# it will run in thread
print('GPIO_API: start')
while gpio_api_running:
value = random.randint(1, 3)
time.sleep(value)
print('GPIO_API:', value)
queue.put(value)
def read_api1():
global n
global data1
if not queue1.empty():
value = queue1.get()
# remove first item and add new item at the the end
data1 = data1[1:] + [value]
n += 1
texte1.config(text="fig1 " + str(n))
ax1.cla()
ax1.plot(range(10), data1)
graph1.draw()
main.after(100, read_api1)
def read_api2():
global m
global data2
if not queue2.empty():
value = queue2.get()
# remove first item and add new item at the the end
data2 = data2[1:] + [value]
m = m + 1
texte2.config(text="fig2 " + str(m))
ax2.cla()
ax2.plot([.1,.2,.3], data2)
graph2.draw()
main.after(100, read_api2)
# --- before GUI ---
# default data at start (to add new value at the end and remove first value)
data1 = [0,0,0,0,0,0,0,0,0,0]
data2 = [0,0,0]
m = 0
n = 0
# queues to communicate with threads
queue1 = queue.Queue()
queue2 = queue.Queue()
# global variables to control loops in thread
web_api_running = True
gpio_api_running = True
# start threads and send queues as arguments
thread1 = threading.Thread(target=WEB_API, args=(queue1,))
thread1.start()
thread2 = threading.Thread(target=GPIO_API, args=(queue2,))
thread2.start()
# --- GUI ---
main = tk.Tk()
style.use("ggplot")
texte1 = tk.Label(main, text="fig1")
texte1.grid(row=0, column=0)
fig1 = Figure(figsize=(2, 2), dpi=112)
ax1 = fig1.add_subplot()
fig1.set_tight_layout(True)
graph1 = FigureCanvasTkAgg(fig1, master=main)
canvas1 = graph1.get_tk_widget()
canvas1.grid(row=1, column=0)
texte2 = tk.Label(main, text="fig2")
texte2.grid(row=0,column=1)
fig2 = Figure(figsize=(2, 2), dpi=112)
ax2 = fig2.add_subplot()
fig2.set_tight_layout(True)
graph2 = FigureCanvasTkAgg(fig2, master=main)
canvas2 = graph2.get_tk_widget()
canvas2.grid(row=1, column=1)
# draw plots first time
ax1.plot(range(10), data1)
ax2.plot([.1,.2,.3], data2)
# run after which will update data and redraw plots
read_api1()
read_api2()
main.mainloop()
# --- after GUI ---
# stop loops in threads
web_api_running = False
gpio_api_running = False
# wait for end of theads
thread1.join()
thread2.join()

Related

Python: threading and tkinter

I'm trying to continuously update matlibplots in tkinter GUI while being able to click on buttons to pause/continue/stop updating the plots. I've tried using threads, but they don't seem to be executing parallelly (e.g. data thread is being executed but the plots don't get updated + clicking on buttons is ignored). Why doesn't it work?
# Import Modules
import tkinter as tk
from threading import *
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)
from scipy.fft import fft
import numpy as np
import time
import random
# global variables
state = 1 # 0 starting state; 1 streaming; 2 pause; -1 end and save
x = [0]*12
y = [0]*12
# Thread buttons and plots separately
def threading():
state = 1
t_buttons = Thread(target = buttons)
t_plots = Thread(target = plots)
t_data = Thread(target = data)
t_buttons.start()
t_plots.start()
t_data.start()
def hex_to_dec(x, y):
for i in range(0, 12):
for j in range(0, len(y)):
x[i][j] = int(str(x[i][j]), 16)
y[i][j] = int(str(y[i][j]), 16)
def data():
fig1, axs1 = main_plot()
fig2, axs2 = FFT_plot()
# To be replaced with actual Arduino data
while(state!=-1):
for i in range(0, 12):
x[i] = [j for j in range(101)]
y[i] = [random.randint(0, 10) for j in range(-50, 51)]
for i in range(0, 12):
for j in range(0, len(y)):
x[i][j] = int(str(x[i][j]), 16)
y[i][j] = int(str(y[i][j]), 16)
# create buttons
def stream_clicked():
state = 1
print("clicked")
def pause_clicked():
state = 2
print("state")
def finish_clicked():
state = -1
def buttons():
continue_button = tk.Button(window, width = 30, text = "Stream data" ,
fg = "black", bg = '#98FB98', command = stream_clicked)
continue_button.place(x = window.winfo_screenwidth()*0.2, y = 0)
pause_button = tk.Button(window, width = 30, text = "Pause streaming data" ,
fg = "black", bg = '#FFA000', command = pause_clicked)
pause_button.place(x = window.winfo_screenwidth()*0.4, y = 0)
finish_button = tk.Button(window, width = 30, text = "End session and save",
fg = 'black', bg = '#FF4500', command = finish_clicked())
finish_button.place(x = window.winfo_screenwidth()*0.6, y = 0)
def plots():
fig1, axs1 = main_plot()
fig2, axs2 = FFT_plot()
if state==1:
print("update")
for i in range(0, 12):
axs1[i].plot(x[i], y[i], 'blue')
axs1[i].axes.get_yaxis().set_ticks([0], labels = ["channel " + str(i+1)])
axs1[i].grid(True)
axs1[i].margins(x = 0)
fig1.canvas.draw()
fig1.canvas.flush_events()
for i in range(0, 12):
axs1[i].clear()
for i in range(0, 12):
axs2.plot(x[i], fft(y[i]))
plt.title("FFT of all 12 channels", x = 0.5, y = 1)
fig2.canvas.draw()
fig2.canvas.flush_events()
axs2.clear()
def main_plot():
plt.ion()
fig1, axs1 = plt.subplots(12, figsize = (10, 9), sharex = True)
fig1.subplots_adjust(hspace = 0)
# Add fixed values for axis
canvas = FigureCanvasTkAgg(fig1, master = window)
canvas.draw()
canvas.get_tk_widget().pack()
canvas.get_tk_widget().place(x = 0, y = 35)
return fig1, axs1
def update_main_plot(fig1, axs1):
if state==1:
for i in range(0, 12):
axs1[i].plot(x[i], y[i], 'blue')
axs1[i].axes.get_yaxis().set_ticks([0], labels = ["channel " + str(i+1)])
axs1[i].grid(True)
axs1[i].margins(x = 0)
axs1[0].set_title("Plot recordings", x = 0.5, y = 1)
fig1.canvas.draw()
fig1.canvas.flush_events()
for i in range(0, 12):
axs1[i].clear()
def FFT_plot():
# Plot FFT figure
plt.ion()
fig2, axs2 = plt.subplots(1, figsize = (7, 9))
# Add fixed values for axis
canvas = FigureCanvasTkAgg(fig2, master = window)
canvas.draw()
canvas.get_tk_widget().pack()
canvas.get_tk_widget().place(x = window.winfo_screenwidth()*0.55, y = 35)
return fig2, axs2
def update_FFT_plot(fig2, axs2):
# Update FFT plot
for i in range(0, 12):
axs2.plot(x[i], fft(y[i]))
plt.title("FFT", x = 0.5, y = 1)
fig2.canvas.draw()
fig2.canvas.flush_events()
axs2.clear()
# create root window and set its properties
window = tk.Tk()
window.title("Data Displayer")
window.geometry("%dx%d" % (window.winfo_screenwidth(), window.winfo_screenheight()))
window.configure(background = 'white')
threading()
window.mainloop()
*** Sometimes it just doesn't work without any message and sometimes I also get "RuntimeError: main thread is not in main loop" ***
to be fair all functions in your code are very likely to cause a segmentation fault, and other functions that don't result in a segmentation fault simply don't work, it's hard to explain what's wrong.
define global variables as global if you are going to modify them
update GUI in your main thread by using the window.after method repeatedly.
only reading from your microcontroller should be done in separate thread.
creation of Tkinter objects should be done in the main thread, only updates are allowed in other threads, but it is not thread-safe, so while it may work it can lead to some weird behavior or errors sometimes.
calling matplotlib functions such as ion and flush_events causes errors because these are for matplotlib interactive canvas, not for tkinter canvas.
threading has a very tough learning curve so ask yourself "do i really need threads in here" and "is there any way to not use threads" before you attempt to use them as once you start using threads you are no longer using python's "safe code", despite all the efforts, threads are not safe to use for any task, it's up to you to make them safe, and to be honest threads are not needed here unless you are reading 1 GB/s from your microcontroller.
don't use numbers for states, it's not pythonic, and it confuses the readers, and it has no performance benefit over using Enums.
programs are built incrementally, not copy-paste from multiple working snippets, as it is harder to track where the error comes from when multiple parts of the code weren't verified to be working.
# Import Modules
import tkinter as tk
from threading import *
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)
from scipy.fft import fft
import numpy as np
import time
import random
from enum import Enum,auto
UPDATE_INTERVAL_MS = 300
class States(Enum):
STREAM = auto()
PAUSE = auto()
SAVE = auto()
START = auto()
# global variables
state = States.START # check States Enum
x = [[0]]*12
y = [[0]]*12
# Thread buttons and plots separately
def threading():
global state
global window
state = States.STREAM
buttons()
plots()
data()
t_grab_data = Thread(target=grab_data_loop,daemon=True)
t_grab_data.start()
# t_buttons = Thread(target=buttons)
# t_plots = Thread(target=plots)
# t_data = Thread(target=data)
#
# t_buttons.start()
# t_plots.start()
# t_data.start()
def hex_to_dec(x, y):
for i in range(0, 12):
for j in range(0, len(y)):
x[i][j] = int(str(x[i][j]), 16)
y[i][j] = int(str(y[i][j]), 16)
def data():
global fig1,axs1,fig2,axs2
fig1, axs1 = main_plot()
fig2, axs2 = FFT_plot()
# To be replaced with actual Arduino data
window.after(UPDATE_INTERVAL_MS,draw_data_loop)
def grab_data_loop():
while state != States.SAVE:
for i in range(0, 12):
x[i] = [j for j in range(101)]
y[i] = [random.randint(0, 10) for j in range(-50, 51)]
for i in range(0, 12):
for j in range(0, len(y)):
x[i][j] = int(str(x[i][j]), 16)
y[i][j] = int(str(y[i][j]), 16)
time.sleep(0.1) # because we are not reading from a microcontroller
def draw_data_loop():
if state == States.STREAM:
update_main_plot(fig1, axs1)
update_FFT_plot(fig2, axs2)
window.after(UPDATE_INTERVAL_MS,draw_data_loop)
# create buttons
def stream_clicked():
global state
state = States.STREAM
print("clicked")
def pause_clicked():
global state
state = States.PAUSE
print("state")
def finish_clicked():
global state
state = States.SAVE
window.destroy()
def buttons():
continue_button = tk.Button(window, width=30, text="Stream data",
fg="black", bg='#98FB98', command=stream_clicked)
continue_button.place(x=window.winfo_screenwidth() * 0.2, y=0)
pause_button = tk.Button(window, width=30, text="Pause streaming data",
fg="black", bg='#FFA000', command=pause_clicked)
pause_button.place(x=window.winfo_screenwidth() * 0.4, y=0)
finish_button = tk.Button(window, width=30, text="End session and save",
fg='black', bg='#FF4500', command=finish_clicked)
finish_button.place(x=window.winfo_screenwidth() * 0.6, y=0)
def plots():
global state
fig1, axs1 = main_plot()
fig2, axs2 = FFT_plot()
if state == States.STREAM:
print("update")
for i in range(0, 12):
axs1[i].plot(x[i], y[i], 'blue')
axs1[i].axes.get_yaxis().set_ticks([0], labels=["channel " + str(i + 1)])
axs1[i].grid(True)
axs1[i].margins(x=0)
# fig1.canvas.draw()
# fig1.canvas.flush_events()
# for i in range(0, 12):
# axs1[i].clear()
for i in range(0, 12):
axs2.plot(x[i], np.abs(fft(y[i])))
plt.title("FFT of all 12 channels", x=0.5, y=1)
# fig2.canvas.draw()
# fig2.canvas.flush_events()
# axs2.clear()
def main_plot():
# plt.ion()
global canvas1
fig1, axs1 = plt.subplots(12, figsize=(10, 9), sharex=True)
fig1.subplots_adjust(hspace=0)
# Add fixed values for axis
canvas1 = FigureCanvasTkAgg(fig1, master=window)
# canvas.draw()
canvas1.get_tk_widget().pack()
canvas1.get_tk_widget().place(x=0, y=35)
return fig1, axs1
def update_main_plot(fig1, axs1):
if state == States.STREAM:
for i in range(0, 12):
axs1[i].clear()
for i in range(0, 12):
axs1[i].plot(x[i], y[i], 'blue')
axs1[i].axes.get_yaxis().set_ticks([0], labels=["channel " + str(i + 1)])
axs1[i].grid(True)
axs1[i].margins(x=0)
axs1[0].set_title("Plot recordings", x=0.5, y=1)
canvas1.draw()
# fig1.canvas.draw()
# fig1.canvas.flush_events()
def FFT_plot():
# Plot FFT figure
# plt.ion()
global canvas2
fig2, axs2 = plt.subplots(1, figsize=(7, 9))
# Add fixed values for axis
canvas2 = FigureCanvasTkAgg(fig2, master=window)
# canvas.draw()
canvas2.get_tk_widget().pack()
canvas2.get_tk_widget().place(x=window.winfo_screenwidth() * 0.55, y=35)
return fig2, axs2
def update_FFT_plot(fig2, axs2):
# Update FFT plot
if state == States.STREAM:
axs2.clear()
for i in range(0, 12):
axs2.plot(x[i], np.abs(fft(y[i])))
plt.title("FFT", x=0.5, y=1)
canvas2.draw()
# fig2.canvas.draw()
# fig2.canvas.flush_events()
# axs2.clear()
# create root window and set its properties
window = tk.Tk()
window.title("Data Displayer")
window.geometry("%dx%d" % (window.winfo_screenwidth(), window.winfo_screenheight()))
window.configure(background='white')
threading()
window.mainloop()
# put saving logic here

Tkinter GUI in for loop

I'm doing a tkinter GUI for my program and I have to display some real time data. I made a simple program (below) to demonstrate my problem on a simple case. I'm actually plotting some data every iteration of my for loop so I can observe data while the program in still calculating. Note that the real program si calculating a bit slower and have more iterations.
Now I would like to add 2 buttons (one to pause the program and one to continue) and a label (diplay variable k so i know where my program is), but I am unable to do it.
I've already lost a lot of time on it so if anyone have a hint or a solution i would love to see it.
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import matplotlib.pyplot as plt
from matplotlib import style
def func_A(a, x):
import numpy
data_x = numpy.arange(0, x)
data_y = a * numpy.sin(data_x/5)
return data_x, data_y
a = 1
root = tk.Tk()
root.title("Graph")
root.geometry("800x400")
fig = plt.figure(figsize=(5, 5), dpi=100)
canvas = FigureCanvasTkAgg(fig, master=root) # A tk.DrawingArea.
canvas.draw()
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
toolbar = NavigationToolbar2Tk(canvas, root)
toolbar.update()
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
plt.grid("both")
style.use("ggplot")
for k in range(0, 100):
data_x, data_y = func_A(a, k)
print("iteration", k)
print("data_x", data_x)
print("data_y", data_y)
if k == 0:
ax1 = plt.subplot(111)
line1, = ax1.plot([0], [0])
else:
line1.set_xdata(data_x)
line1.set_ydata(data_y)
ax1.set_ylim([-1, 1])
ax1.set_xlim([0, 100])
plt.grid("both")
canvas.draw()
canvas.flush_events()
root.mainloop()
To add pause/resume function:
create a frame to hold the progress label and the two buttons: pause and resume
create a tkinter BooleanVar() to store the pause/resume state
move the update plot code inside a function, e.g. update_plot()
use .after() to replace the for loop to call update_plot() periodically
inside update_plot(), check the pause/resume state to determine whether to update the plot or not
Below is a modified example based on your code:
import tkinter as tk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import matplotlib.pyplot as plt
from matplotlib import style
import matplotlib
matplotlib.use("Agg")
root = tk.Tk()
root.title("Graph")
#root.geometry("800x400")
# progress label, pause and resume buttons
frame = tk.Frame(root)
frame.pack(fill="x", side=tk.TOP)
progress = tk.Label(frame)
progress.pack(side="left")
is_paused = tk.BooleanVar() # variable to hold the pause/resume state
tk.Button(frame, text="Pause", command=lambda: is_paused.set(True)).pack(side="right")
tk.Button(frame, text="Resume", command=lambda: is_paused.set(False)).pack(side="right")
# the plot
fig = plt.figure(figsize=(10, 5), dpi=100)
canvas = FigureCanvasTkAgg(fig, master=root)
toolbar = NavigationToolbar2Tk(canvas, root)
canvas.get_tk_widget().pack(side=tk.TOP, fill=tk.BOTH, expand=1)
plt.grid("both")
style.use("ggplot")
a = 1
ax1 = plt.subplot(111)
line1, = ax1.plot([0], [0])
def func_A(a, x):
import numpy
data_x = numpy.arange(0, x)
data_y = a * numpy.sin(data_x/5)
return data_x, data_y
# function to update ploat
def update_plot(k=0):
if not is_paused.get():
progress["text"] = f"iteration: {k}"
data_x, data_y = func_A(a, k)
#print("iteration", k)
#print("data_x", data_x)
#print("data_y", data_y)
line1.set_xdata(data_x)
line1.set_ydata(data_y)
ax1.set_ylim([-1, 1])
ax1.set_xlim([0, 100])
plt.grid("both")
canvas.draw()
canvas.flush_events()
k += 1
if k <= 100:
# update plot again after 10ms. You can change the delay to whatever you want
root.after(10, update_plot, k)
update_plot() # start updating plot
root.mainloop()

How to put multiple separate graphs into one Tkinter window?

I have been running this script:
from threading import Thread
import serial
import time
import collections
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import struct
import copy
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg, NavigationToolbar2Tk
import tkinter as Tk
from tkinter.ttk import Frame
class serialPlot:
def __init__(self, serialPort='/dev/ttyACM0', serialBaud=38400, plotLength=100, dataNumBytes=2, numPlots=4):
self.port = serialPort
self.baud = serialBaud
self.plotMaxLength = plotLength
self.dataNumBytes = dataNumBytes
self.numPlots = numPlots
self.rawData = bytearray(numPlots * dataNumBytes)
self.dataType = None
if dataNumBytes == 2:
self.dataType = 'h' # 2 byte integer
elif dataNumBytes == 4:
self.dataType = 'f' # 4 byte float
self.data = []
self.privateData = None
for i in range(numPlots): # give an array for each type of data and store them in a list
self.data.append(collections.deque([0] * plotLength, maxlen=plotLength))
self.isRun = True
self.isReceiving = False
self.thread = None
self.plotTimer = 0
self.previousTimer = 0
# self.csvData = []
print('Trying to connect to: ' + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')
try:
self.serialConnection = serial.Serial(serialPort, serialBaud, timeout=4)
print('Connected to ' + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')
except:
print("Failed to connect with " + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')
def readSerialStart(self):
if self.thread == None:
self.thread = Thread(target=self.backgroundThread)
self.thread.start()
# Block till we start receiving values
while self.isReceiving != True:
time.sleep(0.1)
def getSerialData(self, frame, lines, lineValueText, lineLabel, timeText, pltNumber):
if pltNumber == 0: # in order to make all the clocks show the same reading
currentTimer = time.perf_counter()
self.plotTimer = int((currentTimer - self.previousTimer) * 1000) # the first reading will be erroneous
self.previousTimer = currentTimer
self.privateData = copy.deepcopy(self.rawData) # so that the 3 values in our plots will be synchronized to the same sample time
timeText.set_text('' + str(self.plotTimer) + '')
data = self.privateData[(pltNumber*self.dataNumBytes):(self.dataNumBytes + pltNumber*self.dataNumBytes)]
value, = struct.unpack(self.dataType, data)
self.data[pltNumber].append(value) # we get the latest data point and append it to our array
lines.set_data(range(self.plotMaxLength), self.data[pltNumber])
lineValueText.set_text('[' + lineLabel + '] = ' + str(value))
def backgroundThread(self): # retrieve data
time.sleep(1.0) # give some buffer time for retrieving data
self.serialConnection.reset_input_buffer()
while (self.isRun):
self.serialConnection.readinto(self.rawData)
self.isReceiving = True
#print(self.rawData)
def sendSerialData(self, data):
self.serialConnection.write(data.encode('utf-8'))
def close(self):
self.isRun = False
self.thread.join()
self.serialConnection.close()
print('Disconnected...')
# df = pd.DataFrame(self.csvData)
# df.to_csv('/home/rikisenia/Desktop/data.csv')
class Window(Frame):
def __init__(self, figure, master, SerialReference):
Frame.__init__(self, master)
self.entry = []
self.setPoint = None
self.master = master # a reference to the master window
self.serialReference = SerialReference # keep a reference to our serial connection so that we can use it for bi-directional communicate from this class
self.initWindow(figure) # initialize the window with our settings
def initWindow(self, figure):
self.master.title("Haptic Feedback Grasping Controller")
canvas = FigureCanvasTkAgg(figure, master=self.master)
toolbar = NavigationToolbar2Tk(canvas, self.master)
canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1)
# create out widgets in the master frame
lbl1 = Tk.Label(self.master, text="Distance")
lbl1.pack(padx=5, pady=5)
self.entry = Tk.Entry(self.master)
self.entry.insert(0, '0') # (index, string)
self.entry.pack(padx=5)
SendButton = Tk.Button(self.master, text='Send', command=self.sendFactorToMCU)
SendButton.pack(padx=5)
def sendFactorToMCU(self):
self.serialReference.sendSerialData(self.entry.get() + '%') # '%' is our ending marker
def main():
# portName = 'COM5'
portName = '/dev/ttyACM0'
baudRate = 38400
maxPlotLength = 100 # number of points in x-axis of real time plot
dataNumBytes = 4 # number of bytes of 1 data point
numPlots = 1 # number of plots in 1 graph
s = serialPlot(portName, baudRate, maxPlotLength, dataNumBytes, numPlots) # initializes all required variables
s.readSerialStart() # starts background thread
# plotting starts below
pltInterval = 50 # Period at which the plot animation updates [ms]
xmin = 0
xmax = maxPlotLength
ymin = -(1)
ymax = 200
fig = plt.figure()
ax = plt.axes(xlim=(xmin, xmax), ylim=(float(ymin - (ymax - ymin) / 10), float(ymax + (ymax - ymin) / 10)))
ax.set_title('Strain Gauge/ToF')
ax.set_xlabel("Time")
ax.set_ylabel("Force/Distance")
# put our plot onto Tkinter's GUI
root = Tk.Tk()
app = Window(fig, root, s)
lineLabel = ['W', 'X', 'Y', 'Z']
style = ['y-', 'r-', 'c-', 'b-'] # linestyles for the different plots
timeText = ax.text(0.70, 0.95, '', transform=ax.transAxes)
lines = []
lineValueText = []
for i in range(numPlots):
lines.append(ax.plot([], [], style[i], label=lineLabel[i])[0])
lineValueText.append(ax.text(0.70, 0.90-i*0.05, '', transform=ax.transAxes))
anim = animation.FuncAnimation(fig, s.getSerialData, fargs=(lines, lineValueText, lineLabel, timeText), interval=pltInterval) # fargs has to be a tuple
plt.legend(loc="upper left")
root.mainloop() # use this instead of plt.show() since we are encapsulating everything in Tkinter
s.close()
if __name__ == '__main__':
main()
A window shows up with no data passing through it even though I have 4 sensors that have data coming from an Arduino. The window contains 1 graph with 4 plots in it currently. I want 4 graphs each with one plot all in one window. I have been using https://thepoorengineer.com/en/python-gui/ as a reference to make graphs within python. The code for the data transfer is within the link as well. I tried to combine his 2 different codes and debugging it to make 4 graphs each with one plot to work with one Tkinter GUI window but it doesn't work. I also get an error of TypeError: getSerialData() missing 1 required positional argument: 'pltNumber' . Not sure why I get this error if pltNumber is in the parentheses in the code. I'm a beginner at python. What should I change to make the code work?
Script that can generate 4 separate graphs each with one plot that are not within a Tkinter GUI(works with 4 sensors but I need them within a Tkinter window):
from threading import Thread
import serial
import time
import collections
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import struct
import copy
class serialPlot:
def __init__(self, serialPort='/dev/ttyACM0', serialBaud=38400, plotLength=100, dataNumBytes=2, numPlots=1):
self.port = serialPort
self.baud = serialBaud
self.plotMaxLength = plotLength
self.dataNumBytes = dataNumBytes
self.numPlots = numPlots
self.rawData = bytearray(numPlots * dataNumBytes)
self.dataType = None
if dataNumBytes == 2:
self.dataType = 'h' # 2 byte integer
elif dataNumBytes == 4:
self.dataType = 'f' # 4 byte float
self.data = []
self.privateData = None # for storing a copy of the data so all plots are synchronized
for i in range(numPlots): # give an array for each type of data and store them in a list
self.data.append(collections.deque([0] * plotLength, maxlen=plotLength))
self.isRun = True
self.isReceiving = False
self.thread = None
self.plotTimer = 0
self.previousTimer = 0
print('Trying to connect to: ' + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')
try:
self.serialConnection = serial.Serial(serialPort, serialBaud, timeout=4)
print('Connected to ' + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')
except:
print("Failed to connect with " + str(serialPort) + ' at ' + str(serialBaud) + ' BAUD.')
def readSerialStart(self):
if self.thread == None:
self.thread = Thread(target=self.backgroundThread)
self.thread.start()
# Block till we start receiving values
while self.isReceiving != True:
time.sleep(0.1)
def getSerialData(self, frame, lines, lineValueText, lineLabel, timeText, pltNumber):
if pltNumber == 0: # in order to make all the clocks show the same reading
currentTimer = time.perf_counter()
self.plotTimer = int((currentTimer - self.previousTimer) * 1000) # the first reading will be erroneous
self.previousTimer = currentTimer
self.privateData = copy.deepcopy(self.rawData) # so that the 3 values in our plots will be synchronized to the same sample time
timeText.set_text('' + str(self.plotTimer) + '')
data = self.privateData[(pltNumber*self.dataNumBytes):(self.dataNumBytes + pltNumber*self.dataNumBytes)]
value, = struct.unpack(self.dataType, data)
self.data[pltNumber].append(value) # we get the latest data point and append it to our array
lines.set_data(range(self.plotMaxLength), self.data[pltNumber])
lineValueText.set_text('[' + lineLabel + '] = ' + str(value))
def backgroundThread(self): # retrieve data
time.sleep(1.0) # give some buffer time for retrieving data
self.serialConnection.reset_input_buffer()
while (self.isRun):
self.serialConnection.readinto(self.rawData)
self.isReceiving = True
def close(self):
self.isRun = False
self.thread.join()
self.serialConnection.close()
print('Disconnected...')
def makeFigure(xLimit, yLimit, title):
xmin, xmax = xLimit
ymin, ymax = yLimit
fig = plt.figure()
ax = plt.axes(xlim=(xmin, xmax), ylim=(int(ymin - (ymax - ymin) / 10), int(ymax + (ymax - ymin) / 10)))
ax.set_title(title)
ax.set_xlabel("Time")
ax.set_ylabel("Force/Distance")
return fig, ax
def main():
# portName = 'COM5'
portName = '/dev/ttyACM0'
baudRate = 38400
maxPlotLength = 100 # number of points in x-axis of real time plot
dataNumBytes = 4 # number of bytes of 1 data point
numPlots = 4 # number of plots in 1 graph
s = serialPlot(portName, baudRate, maxPlotLength, dataNumBytes, numPlots) # initializes all required variables
s.readSerialStart() # starts background thread
# plotting starts below
pltInterval = 50 # Period at which the plot animation updates [ms]
lineLabelText = ['W', 'X', 'Y', 'Z']
title = ['Strain Gauge 1 Force', 'Strain Gauge 2 Force', 'ToF 1 Distance', 'ToF 2 Distance']
xLimit = [(0, maxPlotLength), (0, maxPlotLength), (0, maxPlotLength), (0, maxPlotLength)]
yLimit = [(-1, 1), (-1, 1), (-1, 1), (-1, 1)]
style = ['y-', 'r-', 'g-', 'b-'] # linestyles for the different plots
anim = []
for i in range(numPlots):
fig, ax = makeFigure(xLimit[i], yLimit[i], title[i])
lines = ax.plot([], [], style[i], label=lineLabelText[i])[0]
timeText = ax.text(0.50, 0.95, '', transform=ax.transAxes)
lineValueText = ax.text(0.50, 0.90, '', transform=ax.transAxes)
anim.append(animation.FuncAnimation(fig, s.getSerialData, fargs=(lines, lineValueText, lineLabelText[i], timeText, i), interval=pltInterval)) # fargs has to be a tuple
plt.legend(loc="upper left")
plt.show()
s.close()
if __name__ == '__main__':
main()
I think something like this can be useful.
A similar issue was addressed in this post too.
Here we can use the backend class of matplotlib namely FigureCanvasTkAgg.
It works like a tkinter canvas but with the additional ability to be able to plot figures into it.
This means that we can initialize multiple matplotlib figures, plot graphs on them and then plot those figures onto the canvas.
This allows us to plot multiple graphs on the same tkinter window.
To import this class -:
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
Then a figure object of matplotlib can be used to plot a graph on the canvas like so -:
from matplotlib.figure import Figure
fig = Figure(...) # Initializing the figure object.
canvas = FigureCanvasTkAgg(fig, master=root) # Initializing the FigureCanvasTkAgg Class Object.
tk_canvas = canvas.get_tk_widget() # Getting the Figure canvas as a tkinter widget.
tk_canvas.pack() # Packing it into it's master window.
canvas.draw() # Drawing the canvas onto the screen.
Similarly multiple canvases can be initialized and packed into the tk window, thus giving multiple plotted graphs.
The plotting of the figure object can be done using matplotlib methods.
The full code for two such figures will become -:
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
# FOR FIRST GRAPH
fig = Figure(...) # Initializing the figure object.
canvas = FigureCanvasTkAgg(fig, master=root) # Initializing the FigureCanvasTkAgg Class Object.
tk_canvas = canvas.get_tk_widget() # Getting the Figure canvas as a tkinter widget.
tk_canvas.pack() # Packing it into it's master window.
canvas.draw() # Drawing the canvas onto the screen.
# FOR SECOND GRAPH
fig_2 = Figure(...) # Initializing the second figure object.
canvas_2 = FigureCanvasTkAgg(fig_2, master=root) # Initializing the second FigureCanvasTkAgg Class Object.
tk_canvas_2 = canvas_2.get_tk_widget() # Getting the second Figure canvas as a tkinter widget.
tk_canvas_2.pack() # Packing it into it's master window.
canvas_2.draw() # Drawing the second canvas onto the screen.
# CAN BE REPEATED FOR MULTIPLE SUCH GRAPHS....
EDIT: Misunderstood question, but will still leave this here as it'll be useful for you in terms of manipulating the graphs
Here is some code that I use to generate 5 listboxes, and append them to a dictionary so I can reference them later in the code.
self.listboxes = []
for i in range(5):
self.lb = tk.Listbox(self.modeSelect)
self.lb.configure(background='#2f2a2d', exportselection='false', font='{Arial} 12 {}', foreground='#feffff', height='23')
self.lb.configure(relief='flat', width='9', justify='center', selectbackground='#feffff', selectforeground='#000000')
self.lb.pack(side='left')
self.listboxes.append(self.lb)
self.lb.bind("<Double-1>", self.getIndexLB)
You can then assign functions or call attributes by using
self.listboxes[0].get() #for example
That way you can assign points to each graph, it will also allow you to control all the graphs simultaneously by doing something like:
for child in self.modeSelect.winfo_children():
if isinstance(child, tk.Listbox):
child.delete(index)

Delete data from canvas in tkinter

how do i delete data from a figure when it is used in tktinter via FigureCanvasTkAgg?
I have tried to clear the figure with cla, clf and to delete the canvas with .delete, .clear.
My code: (If you have any advise how i can improve my code let me now. This is my first project in coding)
import tkinter as tk
from tkinter import *
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
#createplot
fig_CDF = Figure(figsize = (6, 5), facecolor = "white")
axis_CDF = fig_CDF.add_subplot(111)
canvas_CDF = FigureCanvasTkAgg(fig_CDF, master = window_main)
canvas_CDF._tkcanvas.pack(side = tk.TOP, fill = tk.BOTH, expand = 1)
canvas_CDF.get_tk_widget().place(x=400,y=50)
#plotdata
axis_CDF.plot(datax, datay, label = "data",marker=".", linestyle = "")
canvas_CDF.draw()
Thanks for your help!
In this code I added three buttons which shows three methods - all of them need canvas_CDF.draw() to redraw element in window.
first replaces data in plot and it removes points from chart but it still show axes.
second clears axis - so it remove data and set axis to 0..1
third clears figure - it removes even axis.
First version needs
plot = axis_CDF.plot(...)
to have access to data.
I use random only to create some data.
import tkinter as tk
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import random
# --- functions ---
def replace_data():
datax = []
datay = []
#datax = range(10)
#datay = [x/10 for x in random.sample(range(10), 10)]
plot[0].set_data(datax, datay)
canvas_CDF.draw()
def clear_axis():
#axis_CDF.clear()
axis_CDF.cla()
canvas_CDF.draw()
def clear_figure():
fig_CDF.clear()
#fig_CDF.clf()
canvas_CDF.draw()
# --- main ---
window_main = tk.Tk()
datax = range(10)
datay = [x/10 for x in random.sample(range(10), 10)]
# create plot
fig_CDF = Figure(figsize=(6, 5), facecolor="white")
axis_CDF = fig_CDF.add_subplot(111)
canvas_CDF = FigureCanvasTkAgg(fig_CDF, master=window_main)
canvas_CDF._tkcanvas.pack(side='top', fill='both', expand=True)
canvas_CDF.get_tk_widget().pack()
# plot data
plot = axis_CDF.plot(datax, datay, label="data", marker=".", linestyle="")
canvas_CDF.draw()
# buttons
b = tk.Button(window_main, text='Replace data', command=replace_data)
b.pack()
b = tk.Button(window_main, text='Clear axis', command=clear_axis)
b.pack()
b = tk.Button(window_main, text='Clear figure', command=clear_figure)
b.pack()
window_main.mainloop()

How to make simple GUI graph with Linear Equation

I am a beginner programmer. I have a task to make a GUI with a linear equation y=mx+b and a set of parameters where I can change the m and b values. I have both matplotlib and numpy. I also have tkinter for the GUI. This is what i have so far i edited my friends code on a coordinate GUI.
def onButtonValChange():
if X1.get() != '':
x[0] = float(X1.get())
if Y1.get() != '':
y[0] = float(Y1.get()
def createGraph(x,y):
graphRoot = Tk.Tk()
graphRoot.wm_title("Your Graph")
graphRoot.resizable(0,0)
f = Figure(figsize=(5, 4), dpi=100)
a = f.add_subplot(111)
a.plot(x, y)
canvas = FigureCanvasTkAgg(f, master=graphRoot)
canvas.show()
canvas.get_tk_widget().pack(side=Tk.TOP, fill=Tk.BOTH, expand=1)
x = [1]
y = [1]
ButtonChangeValues = Tk.Button(root, text="Submit Change", command=onButtonValChange)
ButtonChangeValues.grid(row=11, columnspan=3)
ButtonCreateGraph = Tk.Button(root, text="Create This Graph", command=lambda: createGraph(x, y))
ButtonCreateGraph.grid(row="15", columnspan=3)
Tk.mainloop()
So here is some code that i use its a moving line graph but you could adapt the code to move only when you change the equation. all you would have to do is populate the lists of x0Coords y0Coords and xy0Coords with the right mx+b formula
# Run from IDLE or LXTerminal not IDLE 3
# for import spidev to work, must run as python (v2) not python3
from Tkinter import * #to run on python 2, use Tkinter, for python3 use tkinter
import math
from datetime import datetime, timedelta
import numpy as np
import spidev
#--------------------- variables -------------------------
#---user defined settings
screenWidth = 450
resolution = 5 #number of pixels between data points, for visual purposes only
samplePeriod = 100 #milliseconds, time between data points written to txt file
timeRange = .5 #minutes
#---end user settings
baseTime = int(timeRange*60*1000/screenWidth)
startTime = datetime.now()
nl = "\n"
root = Tk()
root.title("Simple GUI")
root.geometry("500x300") #widthxheight
C = Canvas(root, bg = "gray", height = 250, width = screenWidth)
x0Coords = []
y0Coords = []
xy0Coords = []
coordLength = int(screenWidth/resolution)
#---initiation of lists
for i in range(0,coordLength):
x0Coords.append(i*resolution)
y0Coords.append(125)
xy0Coords.append(0)
xy0Coords.append(0)
#putting X and Y corrdinites in a list
def coordinate():
global x0Coords, y0Coords, xy0Coords
for i in range(0,coordLength*2,2):
xy0Coords[i] = x0Coords[i/2]
xy0Coords[i+1] = y0Coords[i/2]
#print(xy0Coords)
#---End initiation of lists
c0 = C.create_rectangle(0,0,20,50)
cl0 = C.create_line(xy0Coords)
pressure = Label(root, text="test")
pressure.pack()
spi_0 = spidev.SpiDev()
spi_0.open(0, 0)
#--------------------------- End of Variables -------------------------
#--------------------------- Definitions ------------------------------
#shifts y values down in index in array to represent time moved forward
def shiftCoords(nextValue):
global y0Coords, xy0Coords
y0Coords.pop(0)
y0Coords.append(nextValue)
coordinate()
#updates the GUI based on the new time
def move_time():
global c0,cl0,xy0Coords, resolution, baseTime
C.delete(c0)
C.delete(cl0)
c0 = C.create_rectangle(0,0,20,int(float(readadc_0(0))/1023*250))
shiftCoords(125-int(float(readadc_0(0))/1023*125))
cl0 = C.create_line(xy0Coords)
#print(float(readadc_0(0))/1023*250)
root.title("V= " + str(round(3.3*float(readadc_0(0))/1023,2)))
root.after(baseTime*resolution,move_time)
C.pack()
root.after(baseTime,move_time)
root.after(samplePeriod,writeData)
root.mainloop()

Categories

Resources