0

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?

1 Answer 1

2

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:

  1. Avoid clearing the Plot every time. It significantly reduces the speed of GUI. Remove the use of ax.cla() to prevent unnecessary clearing of the plot.
  2. Avoid creating new lines every time the data is updated. Use Line2D object. Create it once during initialization and reuse it for all the updates.
  3. Instead of plotting all the data points from scratch on every update, use set_xdata() and set_ydata() to update the existing line with new data directly. This is reducing the processing time significantly.
  4. Use a fixed-sized window instead of accumulating the data indefinitely.
  5. One of the major reasons could be 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.

Sign up to request clarification or add additional context in comments.

1 Comment

Thank you. I implemented those changes and it sped it up considerably. However, I wanted at least several thousand points plotted still and the load was a bit too high. I modified it to update the plot for every n points received. In my case it seems to work fine and match pace if I update every 10-15 points.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.