Search code examples
pythonmatplotlibplotpysimplegui

How to modify code for GUI-based realtime plotting using matplotlib?


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?


Solution

  • 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.