Search code examples
pythonmatplotlibsleeppause

Way to wait until user deletes Matplotlib figure before adding more?


So I have a function that scatter-plots some data and does so by creating new figures. The maximum amount of figures allowed at a time is 20 to avoid memory overload. If the user wants to plot a data-set with 6 variables to be exact, then there would be 30 different figures. Is there a way to wait until the user deletes the necessary amount of figures before adding more?

This is what I've though of:

import matplolib.pyplot as plt
... # some code
# this below is inside a loop structure
f = plt.figure
# add some stuff to the figure
plt.show(block=False)
Halt()  # checks to see if there are too many figures

Where Halt() is defined as such:

def halt():
    first = True
    while plt.gcf().number > 20:  # are there more than 20 figures
        if first:
            # show message
            first  = False
            # time.sleep(100)

The only problem with this is that it "freezes" the program, not allowing the user to exit out of any of the figures, as it is "not responding". I've also tried the time.sleep() but that does not seem work either.

Does anyone know of a good way to loop until a condition is met?


Solution

  • https://matplotlib.org/api/_as_gen/matplotlib.pyplot.show.html says:

    If False ensure that all windows are displayed and return immediately. In this case, you are responsible for ensuring that the event loop is running to have responsive figures.

    How to do this, you ask? Well, the documentation is at https://matplotlib.org/users/interactive_guide.html#explicitly-spinning-the-event-loop .

    After some fiddling around, I made the following which plots 20 figures with maximum 5 at the same time:

    import matplotlib.pyplot as plt
    import numpy as np
    from time import sleep
    
    def plot_stuff(exponent, titlenum):
        x = np.linspace(0.0, 1.0)
        f = plt.figure()
        ax = f.add_subplot(1, 1, 1)
        ax.set_title('{} - {}'.format(titlenum, exponent))
        ax.plot(x, x**exponent)
    
    def get_fighandles():
        fignumbers = plt.get_fignums()
        return [plt.figure(fign) for fign in fignumbers]
    
    N_figs_eventually_plotted = 20
    N_figs_max_simultaneous = 5
    
    N=0
    while N < N_figs_eventually_plotted:
        if len(get_fighandles()) < N_figs_max_simultaneous:
            N += 1
            # put here whichever update is needed when you can add new figures
            plot_stuff(np.random.random(), N)
            plt.show(block=False)
        print('hi')
        for fig in get_fighandles():
            print(fig.canvas)
            fig.canvas.flush_events()
            fig.canvas.draw_idle() # might not be needed, but here it's fast
        sleep(0.1)
    
    # note: solution terminates when the last figure is plotted, you might want something to prevent this (for instance a plt.show(block=True) when the last figure is plotted)
    

    There might be some subtle concurrency bugs (for instance, if you close a figure after the loop reads the figure handles but before it flushes the events), but I do not see how you can avoid that for your use case.