continuously updating graph in tkinter - python

I have the following python code (in PyCharm) that I use to take readings from an Arduino board. The readings themselves are fine. I have the two following problems with the tkinter part of the code:
The code begins to read in values from Arduino as soon as it is
launched, whereas I want to initiate this on a button click
('read_data'); as long as I don't press the 'read_data' button, the
graph is not displayed, but readings are taken; I can see that when
I open the graph several seconds after I begin running the code;
When I close the plot close_plot, the graph window is indeed
closed, only to be re-opened a short while later.
I think the problem lies in Top.after as it is continuously run within the mainloop() and, therefore, the read_data function is continuously updated. How can I get around this?
import serial
from tkinter import *
from matplotlib import pyplot as plt
Top = Tk()
ser = serial.Serial('COM3', baudrate=9600, timeout=1)
x = []
y = []
def read_data():
plt.ion()
new_value = ser.readline().decode('ascii')
if new_value == '':
pass
else:
y.append(eval(new_value[:-2]))
x.append(len(y) - 1)
plt.plot(x, y, 'r-')
plt.show()
plt.pause(0.0001)
Top.after(100, read_data)
def close_plot():
plt.close()
global x, y
x = []
y = []
def quit():
Top.destroy()
Button(Top, text='Read', command=read_data).pack()
Button(Top, text='Close plot', command=close_plot).pack()
Button(Top, text='Quit', command=quit).pack()
Top.after(100, read_data)
mainloop()
Edit: when pressing the read_data button, I get the following warning:
C:\ProgramData\Anaconda3\lib\site-packages\matplotlib\backend_bases.py:2445: MatplotlibDeprecationWarning: Using default event loop until function specific to this GUI is implemented warnings.warn(str, mplDeprecation)

First, remove the line:
Top.after(100, read_data)
that comes immediately before mainloop() like furas suggested.
Then add after_cancel method to stop calling read_data every 100 ms, but for that to work, we need to be assigning after we use inside the method to a global variable first:
func_id = Top.after(100, read_data)
and then finally call after_cancel in close_plot:
Top.after_cancel(func_id)
Your code should be exactly like below:
import serial
from tkinter import *
from matplotlib import pyplot as plt
Top = Tk()
ser = serial.Serial('COM3', baudrate=9600, timeout=1)
x = []
y = []
func_id = None
def read_data():
global func_id
plt.ion()
new_value = ser.readline().decode('ascii')
if new_value == '':
pass
else:
y.append(eval(new_value[:-2]))
x.append(len(y) - 1)
plt.plot(x, y, 'r-')
plt.show()
plt.pause(0.0001)
func_id = Top.after(100, read_data)
def close_plot():
global func_id
#to no longer update the plot
Top.after_cancel(func_id)
plt.close()
global x, y
del x[:]
del y[:]
def quit():
Top.destroy()
Button(Top, text='Read', command=read_data).pack()
Button(Top, text='Close plot', command=close_plot).pack()
Button(Top, text='Quit', command=quit).pack()
mainloop()

Related

Python ProgressBar & GUI frozen while calculation for plot is going on

Can somebody help me out with threading in python and getting a progress bar working?
Even research gives quite a lot results, i cannot get it working.
I never did threading before and i cannot tell where to put things correctly.
For testing purposes i prepared a simple GUI with a button and a progress bar:
After clicking the button a simple 3d plot will pop-up.
Such plotting may take some computation time and while a user need to wait, id like to have the GUI not frozen and the progressbar animated.
At the moment the GUI freezes until the plot will show up. And after that the progress bar start the animation.
I have read a lot about threading and to put calculations and gui to different threads?
But im just to noobish to get it working.
Is somebody able to explain me more, direct me to similar problems or documentations?
Or maybe, in case is quickly solved, overtake the simple code and correct it the way it should be?
Thanks in advance for any kind of help.
Python script so far:
from time import sleep
from tkinter import EW
import ttkbootstrap as ttk
import numpy as np
import matplotlib.pyplot as plt
def main():
def plot_sample():
sleep(5) # simulate calculation times
x = np.outer(np.linspace(-2, 2, 30), np.ones(30))
y = x.copy().T # transpose
z = np.cos(x ** 2 + y ** 2)
fig = plt.figure()
ax = plt.axes(projection='3d')
ax.plot_surface(x, y, z,cmap='viridis', edgecolor='none')
ax.set_title('Surface plot')
plt.show()
def progressbar_start():
progressbar.grid(row=1, column=0, sticky=EW, padx=10, pady=10) # let progressbar appear in GUI
progressbar.start(interval=10)
def progressbar_stop():
progressbar.stop()
progressbar.grid_forget() # hide progressbar when not needed
def button_clicked():
progressbar_start() # start progressbar before computation begins
plot_sample() # plotting
progressbar_stop() # stop progressbar after plot is done
# GUI
# window size and settings
root = ttk.Window()
# Basic settings for the main window
root.title('Test progressbar')
root.minsize(300, 200)
root.resizable(True, True)
root.configure(bg='white')
# grid settings for the main window in which LabelFrames are sitting in
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
root.rowconfigure(1, weight=1)
# Button fto show 3d-plot
button_calc_3dplot = ttk.Button(root, text='Calculate 3D Plot', command=button_clicked)
button_calc_3dplot.grid(row=0, column=0, padx=5, pady=5)
progressbar = ttk.Progressbar(style='success-striped', mode='indeterminate')
# end of GUI
root.mainloop()
if __name__ == "__main__":
main()
I have one solution. The main problem is, that You can't run the plt.show() function in a Thread. From this follows, that you can't run the window and the plt.show function at the same time.
from time import sleep
from tkinter import EW
import ttkbootstrap as ttk
import numpy as np
import matplotlib.pyplot as plt
from threading import Thread
x, y, z = None, None, None
def main():
def calculate_xyz():
sleep(5) # simulate calculation times
global x, y, z
x = np.outer(np.linspace(-2, 2, 30), np.ones(30))
y = x.copy().T # transpose
z = np.cos(x ** 2 + y ** 2)
def progressbar_start():
progressbar.grid(row=1, column=0, sticky=EW, padx=10, pady=10) # let progressbar appear in GUI
progressbar.start(interval=10)
def progressbar_stop():
progressbar.stop()
progressbar.grid_forget() # hide progressbar when not needed
def button_clicked():
progressbar_start() # start progressbar before computation begins
calculate_xyz_thread = Thread(target=calculate_xyz)
calculate_xyz_thread.start() # calculate x, y and z
root.after(500, wait_for_plotting) # wait for plotting and stop animation after finishing
def wait_for_plotting():
if x is None or y is None or z is None:
root.after(500, wait_for_plotting) # run self again
return # wait for calculation
# calculation is finished
progressbar_stop() # stop progressbar "after" plot is done
ax = plt.axes(projection='3d')
ax.plot_surface(x, y, z, cmap='viridis', edgecolor='none')
ax.set_title('Surface plot')
root.after(500, plot_3d)
def plot_3d():
plt.show()
# GUI
# window size and settings
root = ttk.Window()
# Basic settings for the main window
root.title('Test progressbar')
root.minsize(300, 200)
root.resizable(True, True)
root.configure(bg='white')
# grid settings for the main window in which LabelFrames are sitting in
root.columnconfigure(0, weight=1)
root.rowconfigure(0, weight=1)
root.rowconfigure(1, weight=1)
# Button fto show 3d-plot
button_calc_3dplot = ttk.Button(root, text='Calculate 3D Plot', command=button_clicked)
button_calc_3dplot.grid(row=0, column=0, padx=5, pady=5)
progressbar = ttk.Progressbar(style='success-striped', mode='indeterminate')
# end of GUI
root.mainloop()
if __name__ == "__main__":
main()

Matplotlib FuncAnimation fails to run and is bypassed in tkinter GUI

I am trying to have two daemonic threads run alongside my tkinter main thread. One of the threads generates data and the other thread runs FuncAnimation that plots the data that is generated. However, I am having issues with getting FuncAnimation to run, I can see that whenever I click the button both print statements are shown in the console meaning FuncAnimation appears to have been skipped or ran but exited due to some missing or bad parameter. I checked the matplotlib documentation to ensure I'm not missing anything and I think I have created all the objects and called the right methods to make this happen but something is obviously amiss. Any help is greatly appreciated.
import tkinter as tk
import time
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)
from matplotlib.figure import Figure
from matplotlib import animation
import threading
root = tk.Tk()
class dynamicChart():
def __init__(self):
self.x = []
self.y = []
self.buildChart()
def buildChart(self):
self.figure = Figure(figsize=(5, 5), dpi=100)
ax = self.figure.add_subplot(111)
self.scatter = ax.scatter(self.x,self.y)
canvas = FigureCanvasTkAgg(self.figure, root)
canvas.draw()
canvas.get_tk_widget().pack(pady=10)
toolbar = NavigationToolbar2Tk(canvas, root)
toolbar.update()
canvas.get_tk_widget().pack()
def animate(self,i):
print("Enter animate")
self.scatter.set_offsets([self.x, self.y])
def Ani(self):
print("Enter Ani")
self.animation = animation.FuncAnimation(self.figure, self.animate, interval = 2000)
print("Exit Ani")
class genData():
def __init__(self):
self.chart = dynamicChart()
def generate(self):
X = 2
Y = 2
i = 1
while True:
time.sleep(1)
X = X*i
Y = Y + i
self.chart.x.append(X)
self.chart.y.append(Y)
i+=1
StartDataGen_CreateFigureAxes = genData()
genData = threading.Thread(target = StartDataGen_CreateFigureAxes.generate, daemon = True)
genData.start()
ani = threading.Thread(target = StartDataGen_CreateFigureAxes.chart.Ani, daemon = True)
button = tk.Button(root, text="Graph It", command = lambda: ani.start())
button.pack()
root.mainloop()

Update Matplotlib plot in Tkinter frame

I'm trying to plot data from serial port in a tkinter gui. The plot should be plotted and updated only when a specific pack comes from serial.
I can parse the new incoming data and update the GUI (text area). But when I call the "plot()" function from the "update_gui" thread, the program quit and I get the
"Process finished with exit code -1073741819 (0xC0000005)"
message.
Instead, if I call "plot()" from somewhere else (command button, or simply before mainloop()), the plot is generated and shown.
Here is the relevant part of the code:
import threading
import tkinter as tk
import tkinter.scrolledtext as st
import rx_seriale as rx_ser
from matplotlib.figure import Figure
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)
import queue
running = True
serial_data = ''
filter_data = ''
update_period = 5
serial_object = None
gui = tk.Tk()
gui.title("UART Interface")
my_queue = queue.Queue()
t1 = ''
def plot(valori):
global frame_plot1
try:
# the figure that will contain the plot
fig = Figure(figsize=(4.5, 4), dpi=100)
# adding the subplot
plot1 = fig.add_subplot(111)
#dummy values
y = [i ** 2 for i in range(101)]
# plotting the graph
plot1.plot(y)
# creating the Tkinter canvas
# containing the Matplotlib figure
canvas = FigureCanvasTkAgg(fig, master=frame_plot1)
canvas.draw()
# placing the canvas on the Tkinter window
canvas.get_tk_widget().pack()
# creating the Matplotlib toolbar
#toolbar = NavigationToolbar2Tk(canvas, frame_plot1)
#toolbar.update()
# placing the toolbar on the Tkinter window
#canvas.get_tk_widget().pack()
except Exception as e:
print('Errore:' + str(e))
def update_gui():
global filter_data
global update_period
global my_queue
global type_test, test_status,flag
while (1):
data = my_queue.get(block=True)
text.insert('end', test_status[data[0]] + " - " + type_test[data[1]])
text.insert('end', '\n')
text.see('end')
if (data[1] == 6):
plot(1)
if __name__ == "__main__":
'''
...
all the stuff for design TK windows
...
'''
# threads
t2 = threading.Thread(target=update_gui)
t2.daemon = True
t2.start()
# mainloop
gui.geometry('1000x500')
gui.mainloop()
What goes wrong? Thank you.

Is there any way I can resume running by clicking the start button?

I'm new to Python.
I have three buttons in the code:
the first one is to start running the chart, the second one is to stop running. But, I don't know what I should do if I want to resume running when I press the start button again.
Once I press start button, none of the buttons work.
import matplotlib.pyplot as plt
import numpy as np
import tkinter
running=True
def btn1():
plt.clf()
while running==True:
x=np.arange(1,51)
f=np.random.choice(x,15,replace=True, p=None)
x_pos = [i for i, _ in enumerate(f)]
plt.bar(x_pos, f, color='green')
print(f)
plt.xticks(x_pos, x)
plt.plot()
plt.pause(0.5)
plt.ioff()
plt.show()
def stop():
global running
running = False
def closewindow():
window.destroy()
window = tkinter.Tk()
screensize = 500, 500
size = str(screensize[0])+'x'+str(screensize[1])
window.geometry(size)
but1 = tkinter.Button(window, text="start", command=btn1).grid(column = 1, row = 1)
but2=tkinter.Button(window, text="end", command=stop).grid(column = 1, row =2)
ext = tkinter.Button(window, text="exit", command=closewindow).grid(column = 1, row =3 )
window.mainloop()
Since you're using while running == True inside btn1(), the other buttons wouldn't work as the program execution will be stuck inside the loop. You'll need to create a thread for this, that executes your start action, so that the other buttons (and consequently the remainder of your program) continue executing properly.

Why does Python Tkinter Canvas draw causes a freeze? [duplicate]

I have written a piece of code where I have a simple GUI with an canvas. On this canvas I draw a Matplot. The Matplot is updated every second with data from an SQ Lite DB which I fill with some fake Sensor information (just for testing at the moment).
My Problem was that the redrawing of the canvas causes my window/gui to lag every second. I even tried to update the plot in another thread. But even there I get an lag.
With my newest Code i got most of my things working. Threading helps to prevent my GUI/Window from freezing while the Canvas is updated.
The last thing I miss is to make it Thread safe.
This is the message I get:
RuntimeError: main thread is not in main loop
Here is my newest working code with threading:
from tkinter import *
import random
from random import randint
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import time
import threading
from datetime import datetime
continuePlotting = False
def change_state():
global continuePlotting
if continuePlotting == True:
continuePlotting = False
else:
continuePlotting = True
def data_points():
yList = []
for x in range (0, 20):
yList.append(random.randint(0, 100))
return yList
def app():
# initialise a window and creating the GUI
root = Tk()
root.config(background='white')
root.geometry("1000x700")
lab = Label(root, text="Live Plotting", bg = 'white').pack()
fig = Figure()
ax = fig.add_subplot(111)
ax.set_ylim(0,100)
ax.set_xlim(1,30)
ax.grid()
graph = FigureCanvasTkAgg(fig, master=root)
graph.get_tk_widget().pack(side="top",fill='both',expand=True)
# Updated the Canvas
def plotter():
while continuePlotting:
ax.cla()
ax.grid()
ax.set_ylim(0,100)
ax.set_xlim(1,20)
dpts = data_points()
ax.plot(range(20), dpts, marker='o', color='orange')
graph.draw()
time.sleep(1)
def gui_handler():
change_state()
threading.Thread(target=plotter).start()
b = Button(root, text="Start/Stop", command=gui_handler, bg="red", fg="white")
b.pack()
root.mainloop()
if __name__ == '__main__':
app()
Here the idea without a thread:
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import tkinter as tk
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
import sqlite3
from datetime import datetime
from random import randint
class MainApplication(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
root.update_idletasks()
f = Figure(figsize=(5,5), dpi=100)
x=1
ax = f.add_subplot(111)
line = ax.plot(x, np.sin(x))
def animate(i):
# Open Database
conn = sqlite3.connect('Sensor_Data.db')
c = conn.cursor()
# Create some fake Sensor Data
NowIs = datetime.now()
Temperature = randint(0, 100)
Humidity = randint(0, 100)
# Add Data to the Database
c = conn.cursor()
# Insert a row of data
c.execute("insert into Sensor_Stream_1 (Date, Temperature, Humidity) values (?, ?, ?)",
(NowIs, Temperature, Humidity))
# Save (commit) the changes
conn.commit()
# Select Data from the Database
c.execute("SELECT Temperature FROM Sensor_Stream_1 LIMIT 10 OFFSET (SELECT COUNT(*) FROM Sensor_Stream_1)-10")
# Gives a list of all temperature values
x = 1
Temperatures = []
for record in c.fetchall():
Temperatures.append(str(x)+','+str(record[0]))
x+=1
# Setting up the Plot with X and Y Values
xList = []
yList = []
for eachLine in Temperatures:
if len(eachLine) > 1:
x, y = eachLine.split(',')
xList.append(int(x))
yList.append(int(y))
ax.clear()
ax.plot(xList, yList)
ax.set_ylim(0,100)
ax.set_xlim(1,10)
ax.grid(b=None, which='major', axis='both', **kwargs)
label = tk.Label(root,text="Temperature / Humidity").pack(side="top", fill="both", expand=True)
canvas = FigureCanvasTkAgg(f, master=root)
canvas.get_tk_widget().pack(side="left", fill="both", expand=True)
root.ani = animation.FuncAnimation(f, animate, interval=1000)
if __name__ == "__main__":
root = tk.Tk()
MainApplication(root).pack(side="top", fill="both", expand=True)
root.mainloop()
Here is my DB Schema:
CREATE TABLE `Sensor_Stream_1` (
`Date` TEXT,
`Temperature` INTEGER,
`Humidity` INTEGER
);
Your GUI process must not run in any thread. only the dataacquisition must be threaded.
When needed , the data acquired are transfered to the gui process (or the gui process notified from new data available) . I may need to use a mutex to share data resource between acquisition thread and gui (when copying)
the mainloop will look like :
running = True
while running:
root.update()
if data_available:
copydata_to_gui()
root.quit()
I had the same problem with tkinter and using pypubsub events was my solution.
As comments above suggested, you have to run your calculation in another thread, then send it to the gui thread.
import time
import tkinter as tk
import threading
from pubsub import pub
lock = threading.Lock()
class MainApplication(tk.Frame):
def __init__(self, parent, *args, **kwargs):
tk.Frame.__init__(self, parent, *args, **kwargs)
self.parent = parent
self.label = tk.Label(root, text="Temperature / Humidity")
self.label.pack(side="top", fill="both", expand=True)
def listener(self, plot_data):
with lock:
"""do your plot drawing things here"""
self.label.configure(text=plot_data)
class WorkerThread(threading.Thread):
def __init__(self):
super(WorkerThread, self).__init__()
self.daemon = True # do not keep thread after app exit
self._stop = False
def run(self):
"""calculate your plot data here"""
for i in range(100):
if self._stop:
break
time.sleep(1)
pub.sendMessage('listener', text=str(i))
if __name__ == "__main__":
root = tk.Tk()
root.wm_geometry("320x240+100+100")
main = MainApplication(root)
main.pack(side="top", fill="both", expand=True)
pub.subscribe(main.listener, 'listener')
wt = WorkerThread()
wt.start()
root.mainloop()
This function is called every second, and it is outside the normal refresh.
def start(self,parent):
self.close=False
self.Refresh(parent)
def Refresh(self,parent):
'''your code'''
if(self.close == False):
frame.after( UpdateDelay*1000, self.Refresh, parent)
The function is called alone, and everything that happens inside it does not block the normal operation of the interface.

Categories

Resources