3

I'm creating a GUI to allow users to see a "live view" of a spectrometer where data is taken from the spectrometer and plotted in Matplotlib to be displayed in the GUI window. The GUI also has a few other buttons which allow the user to go through other functions (irrelevant but just background).

I've gotten the live view to work in matplotlib using a while loop and clearing the data to re-plot:

while True:
    data = ccs.take_data(num_avg=3) # spectrometer function
    norm = (data[0]-dark[0])/(light[0]-dark[0]) # some calcs.
    plt.plot(data[1],norm)
    plt.axis([400,740,0,1.1])  
    plt.grid(color='w', linestyle='--')       
    plt.xlabel('Wavelength [nm]')
    plt.ylabel('Normalized Intesity')          
    plt.pause(0.1)
    plt.cla()

Next step was to show this figure in PySimpleGUI. Harder than expexted... I was able to use a few demo codes from PySimpleGUI to get a single figure to appear and update if user presses 'update' button:

from instrumental.drivers.spectrometers import thorlabs_ccs
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import PySimpleGUI as sg
import matplotlib, time, threading
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt


def fig_maker(ccs, dark, sub):
    plt.clf()
    plt.close()
    data = ccs.take_data(num_avg=3)
    norm = (data[0]-dark[0])/(sub[0]-dark[0])
    plt.plot(data[1],norm,c='r')
    plt.axis([400,750,0,1.1])  
    plt.grid(color='w', linestyle='--')       
    plt.xlabel('Wavelength [nm]')
    plt.ylabel('Normalized Intesity')  

return plt.gcf() 


def draw_figure(canvas, figure, loc=(0, 0)):
    figure_canvas_agg = FigureCanvasTkAgg(figure, canvas)
    figure_canvas_agg.draw()
    figure_canvas_agg.get_tk_widget().pack(side='top', fill='both', expand=1)
    return figure_canvas_agg


def delete_fig_agg(fig_agg):
    fig_agg.get_tk_widget().forget()
    plt.close('all')


if __name__ == '__main__':
    
    ... some code ...

    # define the window layout
    layout = [[sg.Button('update')],
              [sg.Text('Plot test', font='Any 18')],             
              [sg.Canvas(size=(500,500), key='canvas')] ]

    # create the form and show it without the plot
    window = sg.Window('Demo Application - Embedding Matplotlib In PySimpleGUI',
                       layout, finalize=True)

    fig_agg = None
    while True:
        event, values = window.read()
        if event is None:  # if user closes window
            break         
        if event == "update":
            if fig_agg is not None:
                delete_fig_agg(fig_agg)
            fig = fig_maker(ccs,dark,sub)
            fig_agg = draw_figure(window['canvas'].TKCanvas, fig) 
    window.close()            

Now for the fun part (I can't seem to get it to work). I would like the plot to always be updating similar to how I did it using just matplotlib so that the user doesn't have to press 'update'. Using PySimpleGUI long_task threaded example is where my program starts to fail. I don't actually get any errors thrown except for a print to the Debug I/O stating *** Faking Timeout *** before Python closes the script.

I even just tried to do a for loop of 10 iterations instead of continuous while loop:

from instrumental.drivers.spectrometers import thorlabs_ccs
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import PySimpleGUI as sg
import matplotlib, time, threading
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt


def long_function_thread(window, ccs, dark, sub):
    for i in range(10):
        fig = fig_maker(ccs, dark, sub)
        fig_agg = draw_figure(window['canvas'].TKCanvas, fig) 
        window.write_event_value('-THREAD PROGRESS-', i)
        time.sleep(1)
        delete_fig_agg(fig_agg)
        time.sleep(0.1)

    window.write_event_value('-THREAD DONE-', '')


def long_function(window, ccs, dark, sub):
    print('In long_function')
    threading.Thread(target=long_function_thread, args=(window, ccs, dark, sub), daemon=True).start()


def fig_maker(ccs, dark, sub):
    plt.clf()
    plt.close()
    data = ccs.take_data(num_avg=3)
    norm = (data[0]-dark[0])/(sub[0]-dark[0])
    plt.plot(data[1],norm,c='r')
    plt.axis([400,750,0,1.1])  
    plt.grid(color='w', linestyle='--')       
    plt.xlabel('Wavelength [nm]')
    plt.ylabel('Normalized Intesity')  
    
    return plt.gcf() 


def draw_figure(canvas, figure, loc=(0, 0)):
    figure_canvas_agg = FigureCanvasTkAgg(figure, canvas)
    figure_canvas_agg.draw()
    figure_canvas_agg.get_tk_widget().pack(side='top', fill='both', expand=1)
    return figure_canvas_agg


def delete_fig_agg(fig_agg):
    fig_agg.get_tk_widget().forget()
    plt.close('all')


if __name__ == '__main__':
    
     ... some code ...

    # define the window layout
    layout = [[sg.Button('Go')],
              [sg.Text('Plot test', font='Any 18')],            
              [sg.Canvas(size=(500,500), key='canvas')] ]

    # create the form and show it without the plot
    window = sg.Window('Demo Application - Embedding Matplotlib In PySimpleGUI',
                       layout, finalize=True)

    fig_agg = None
    while True:
        event, values = window.read()
        if event is None or event == 'Exit':
            break
        if event == 'Go':
            print('Calling plotter')
            long_function(window, ccs, dark, sub)
            print('Long function has returned from starting')            
        elif event == '-THREAD DONE-':
            print('Your long operation completed')

window.close()

Appologies on the long description and code dump but I thought this is the easiest way to explain. Any help or links on this issue would be greatly appreciated.

If someone wants to try and run my script this should just produce a random plot instead

def random_fig_maker():
   plt.scatter(np.random.rand(1,10),np.random.rand(1,10))
   return plt.gcf()

2 Answers 2

2

You need to use two additional PySimpleGUI features: window.Refresh() and window.write_event_value(). When you deleted figg_agg and the new plot is ready, call window.Refresh(). This will redraw the window, but also introduces a problem: the main event (while) loop will keep running forever. To address this, you also need to add window.write_event_value('-THREAD-', 'some message.') to one of the functions that is called from within the event loop. This will act as an external trigger for the event loop to keep running, but this will also keep the window responsive, so you can change some other window element (here I used a radio switch) to stop the loop.

For bonus points, you can also run the "trigger function" as a separate thread. Then, time.sleep() in that function will not affect GUI responsiveness. Because of this, I would use some data gathering function that only returns some lists or tuples as the trigger for restarting the loop. In this case, matplotlib was unhappy about being called from an external thread, so I just added a delay in the event loop to keep the plot visible.

from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import PySimpleGUI as sg
import matplotlib, time, threading
matplotlib.use('TkAgg')
import matplotlib.pyplot as plt
import numpy as np


def fig_maker(window): # this should be called as a thread, then time.sleep() here would not freeze the GUI
    plt.scatter(np.random.rand(1,10),np.random.rand(1,10))
    window.write_event_value('-THREAD-', 'done.')
    time.sleep(1)
    return plt.gcf()


def draw_figure(canvas, figure, loc=(0, 0)):
    figure_canvas_agg = FigureCanvasTkAgg(figure, canvas)
    figure_canvas_agg.draw()
    figure_canvas_agg.get_tk_widget().pack(side='top', fill='both', expand=1)
    return figure_canvas_agg


def delete_fig_agg(fig_agg):
    fig_agg.get_tk_widget().forget()
    plt.close('all')


if __name__ == '__main__':
    # define the window layout
    layout = [[sg.Button('update'), sg.Button('Stop', key="-STOP-"), sg.Button('Exit', key="-EXIT-")],
              [sg.Radio('Keep looping', "RADIO1", default=True, size=(12,3),key="-LOOP-"),sg.Radio('Stop looping', "RADIO1", size=(12,3), key='-NOLOOP-')],
              [sg.Text('Plot test', font='Any 18')],             
              [sg.Canvas(size=(500,500), key='canvas')]]

    # create the form and show it without the plot
    window = sg.Window('Demo Application - Embedding Matplotlib In PySimpleGUI',
                       layout, finalize=True)

    fig_agg = None
    while True:
        event, values = window.read()
        if event is None:  # if user closes window
            break
        
        if event == "update":
            if fig_agg is not None:
                    delete_fig_agg(fig_agg)
            fig = fig_maker(window)
            fig_agg = draw_figure(window['canvas'].TKCanvas, fig)

        if event == "-THREAD-":
            print('Acquisition: ', values[event])
            time.sleep(1)
            if values['-LOOP-'] == True:
                if fig_agg is not None:
                    delete_fig_agg(fig_agg)
                fig = fig_maker(window)
                fig_agg = draw_figure(window['canvas'].TKCanvas, fig)
                window.Refresh()
        
        if event == "-STOP-":
            window['-NOLOOP-'].update(True)
        
        if event == "-EXIT-":
            break
            
    
    window.close()            
Sign up to request clarification or add additional context in comments.

Comments

2

It's not exactly connected but I had a similar problem. Does this help....

import PySimpleGUI as sg
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
import numpy as np

class updateable_matplotlib_plot():
    def __init__(self, canvas) -> None:
        self.fig_agg = None
        self.figure = None
        self.canvas = canvas

    def plot(self, data):
        self.data = data
        self.figure_controller()
        self.figure_drawer()

    #put all of your normal matplotlib stuff in here
    def figure_controller(self):
        #first run....
        if self.figure is None:
            self.figure = plt.figure()
            self.axes = self.figure.add_subplot(111)
            self.line, = self.axes.plot(self.data)
            self.axes.set_title("Example of a Matplotlib plot updating in PySimpleGUI")
        #all other runs
        else:            
            self.line.set_ydata(self.data)#update data            
            self.axes.relim() #scale the y scale
            self.axes.autoscale_view() #scale the y scale

    #finally draw the figure on a canvas
    def figure_drawer(self):
        if self.fig_agg is not None: self.fig_agg.get_tk_widget().forget()
        self.fig_agg = FigureCanvasTkAgg(self.figure, self.canvas.TKCanvas)
        self.fig_agg.get_tk_widget().pack(side='top', fill='both', expand=1)
        self.fig_agg.draw()

def getGUI():
    # All the stuff inside your window.
    layout = [  [sg.Canvas(size=(500,500), key='canvas')],
                [sg.Button('Update', key='update'), sg.Button('Close')] ]

    # Create the Window
    window = sg.Window('Updating a plot example....', layout)
    return window


if __name__ == '__main__':
    window = getGUI()
    spectraPlot = updateable_matplotlib_plot(window['canvas']) #what canvas are you plotting it on
    window.finalize() #show the window
    spectraPlot.plot(np.zeros(1024)) # plot an empty plot    
    while True:
        event, values = window.read()
        if event == "update":
             some_spectrum = np.random.random(1024) # data to be plotted
             spectraPlot.plot(some_spectrum) #plot the data           
        if event == sg.WIN_CLOSED or event == 'Close': break # if user closes window or clicks cancel

    window.close()

enter image description here

Comments

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.