I have a GUI using PySimpleGUI with multiple plots which receives data from a bluetooth device. I'd like to plot the received data in real-time ideally as fast as the points are received. Right now this is about 1 point every 20ms. However, in it's current state, it is agonizingly slow. The hardware is long done with it's measurement before the GUI can catch up. The entire GUI is bogged down and doesn't even register the device has completed its tasks.
class DataPlot:
#stuff
def update(self, plot_func):
self.ax.cla()
self.ax.grid()
plot_func()
self.ax.set_title(self.title)
self.ax.set_xlabel(self.x_label)
self.ax.set_ylabel(self.y_label)
plt.legend(loc="lower right", fontsize="5", ncol=2)
self.figure_agg.draw()
class View:
#stuff
def update_demo_plots(
self, calibration_sweeps: List[SensorSweep], test_sweeps: List[SensorSweep]
):
def demo_well1_update(calibration_sweeps, test_sweeps):
for c_num in range(8):
cal_x = [sweep.applied_voltages[c_num] for sweep in calibration_sweeps]
cal_y = [sweep.calculated_resistances[c_num] for sweep in calibration_sweeps]
self.well1_plot.ax.plot(
cal_x,
cal_y,
"s",
color="blue",
markersize=2,
label="Calibration" if c_num == 0 else None,
)
self.well1_plot.update(
lambda: demo_well1_update(calibration_sweeps, test_sweeps)
)
#other plotting
Every time a point is received, update_demo_plots() is called. This completely clears and replots all data for every point.
I've determined the mere call to self.figure_agg.draw() with everything else commented out is enough to slow down the GUI considerably. How can I improve and get around this?
Your data is too fast for this code to plot because it is doing a lot of stuff on every data received. Here are some solutions to fix your issues:
ax.cla()
to prevent unnecessary clearing of the plot.Line2D
object. Create it once during initialization and reuse it for all the updates.set_xdata()
and set_ydata()
to update the existing line with new data directly. This is reducing the processing time significantly.self.figure_agg.draw()
as it may block the main thread resulting in slow down the GUI. Use self.figure_agg.draw_idle()
instead.Bonus: Make a dynamically adjusted axis limits using set_xlim()
and set_ylim()
to keep the plot centered on the latest data that enhances the visual experience.
I did not have a proper setup to generate this data from a Bluetooth device so I simulated the data using Python only with the help of AI. This is the final code that includes all the above-mentioned points.
# Importing necessary libraries
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import PySimpleGUI as sg
import random
import threading
import time
# Class for managing data plotting
class DataPlot:
def __init__(self, ax, title, x_label, y_label, window_size=50):
# Setting up the plot with given parameters
self.ax = ax
self.title = title
self.x_label = x_label
self.y_label = y_label
self.window_size = window_size # Controls the number of points shown in the moving window
self.line, = self.ax.plot([], [], 'b-', linewidth=1) # Initialize the plot line
self.ax.grid()
self.ax.set_title(self.title)
self.ax.set_xlabel(self.x_label)
self.ax.set_ylabel(self.y_label)
# Initialize empty lists to hold data
self.x_data = []
self.y_data = []
def update(self, new_x, new_y):
# Add new data points to the current list
self.x_data.extend(new_x)
self.y_data.extend(new_y)
# Keep only the latest `window_size` number of points
if len(self.x_data) > self.window_size:
self.x_data = self.x_data[-self.window_size:]
self.y_data = self.y_data[-self.window_size:]
# Update the line data
self.line.set_xdata(self.x_data)
self.line.set_ydata(self.y_data)
# Adjust the plot limits to keep the new data centered
self.ax.set_xlim(min(self.x_data), max(self.x_data))
self.ax.set_ylim(min(self.y_data) - 10, max(self.y_data) + 10) # Dynamically adjust y-limits
# Redraw the plot without blocking the GUI
self.line.figure.canvas.draw_idle()
class View:
def __init__(self, figure, ax):
self.figure = figure
self.ax = ax
self.well1_plot = DataPlot(ax, "Moving Line Plot", "Time", "Value")
def update_demo_plots(self, calibration_sweeps):
# Prepare data for plotting
cal_x = [i for i in range(len(calibration_sweeps))] # Simulating time on the x-axis
cal_y = [sweep['resistance'] for sweep in calibration_sweeps] # Plotting resistance values
self.well1_plot.update(cal_x, cal_y)
# Function to simulate incoming data
def data_generator(view):
calibration_sweeps = [] # This list will store incoming data points
while True:
time.sleep(0.02) # Simulate data arrival every 20 milliseconds
# Generate random data points
new_data = {'voltage': random.uniform(0, 5), 'resistance': random.uniform(10, 100)}
calibration_sweeps.append(new_data)
# Limiting the number of data points to keep the plot responsive
if len(calibration_sweeps) > 50: # Keep the last 50 points
calibration_sweeps.pop(0)
# Updating the plot with new data
view.update_demo_plots(calibration_sweeps)
# Setting up the GUI window with PySimpleGUI
layout = [[sg.Canvas(key='-CANVAS-')], [sg.Button('Exit')]]
window = sg.Window('Real-time Plotting', layout, finalize=True)
fig, ax = plt.subplots()
view = View(fig, ax)
figure_agg = FigureCanvasTkAgg(fig, window['-CANVAS-'].TKCanvas)
figure_agg.draw()
figure_agg.get_tk_widget().pack(side='top', fill='both', expand=1)
thread = threading.Thread(target=data_generator, args=(view,), daemon=True)
thread.start()
while True:
event, values = window.read(timeout=10)
if event == sg.WIN_CLOSED or event == 'Exit':
break
window.close()
This updates the plot very quickly. Let me know if anything is not clear or breaks down during your implementation.