1

In a project I am currently working on, I need to draw a figure using matplotlib and let the user interact with said figure to define some cutoff values at the start and end of a signal. These cutoff values are then supposed to be kept in memory for a later usage.

Having never done it before, I looked up the documentation and the examples proposed here as well as some answers found on StackOverflow (here). From them, I was able to more or less obtain a satisfactory result and let the user interact with the figure to select the cutoff values.

Now my question is: How do I exit the event loop when the figure is closed?

My environment uses Python 3.9.16 and Matplotlib 3.7.0. I am working on Windows 10.

Based on the previous links, I wrote the following example:

import matplotlib.pyplot as plt
import numpy as np

close_flag = 0

# Creating a random dataset
x = np.arange(0, np.pi * 2 * 5, 0.1)
y = np.sin(x)

# Shamelessly stolen from Matplotlib documentation
def tellme(s):
    plt.title(s, fontsize=16)
    plt.draw()

def close_event(event):
    global close_flag
    close_flag = 1

# Creating the figure
fig, axs = plt.subplots()
fig.canvas.mpl_connect('close_event', close_event)

plt.plot(x, y)
plt.grid()

while close_flag == 0:
    pts = [] # Empty list to keep my cutoff values in memory
    _vlines = [] 
    
    # Updating the plot title
    tellme('Select the first cutoff value with the mouse') 
    
    # First cutoff value
    pt = plt.ginput(1, timeout=-1)[0][0] # We only need the value along the x axis
    pts.append(pt)
    
    # Ploting the first cutoff value as a vertical line
    tmp = plt.vlines(pt, ymin=y.min(), ymax=y.max(), colors='k', linestyles='-.')
    _vlines.append(tmp)
    
    
    # Updating the plot title a second time
    tellme('Select the second cutoff dates with the mouse')
    
    # Second cutoff value
    pt = plt.ginput(1, timeout=-1)[0][0]
    pts.append(pt)
    
    # Ploting the second cutoff value as a vertical line
    tmp = plt.vlines(pt, ymin=y.min(), ymax=y.max(), colors='k', linestyles='-.')
    _vlines.append(tmp)

    # Updating the title a third time to inform the user both cutoff values are set
    tellme('Happy? Key click for yes, mouse click for no')

    # First control to let the user escape the loop
    if plt.waitforbuttonpress():
        break

    # First control returned False, we remove the vertical lines and let the user select new values
    for v in _vlines:
        v.remove()
        
    # Second control used to check if the Figure was closed, if so we should exit the loop
    if close_flag == 1:
        break

# Rest of the script where I am using the selected values

From the example, we can see that script is interacting with the user as expected: the values can be defined by clicking on the figure, it can be done until the user is happy about the result, and the script correctly exits the loop if a key is pressed once the values are defined. However, when I close the Figure, the script keeps on running as if stuck in endless loop.

Using the print() function, I could confirm the script enters the close_event() function but nothing else seems to happen and I have to abort the script using ctrl + C.

How should I proceed to exit the loop when the figure is closed and let the script continue running?

4
  • That script ends for me. I'd look at "Rest of the script" for the problem. Commented Nov 23, 2023 at 17:10
  • Hi Jody, thanks for your time. The "rest of the script" is, for now, only a print('I am out of the loop'). It shouldn't be an issue. And yes the script works but I am not able to escape the loop when the figure is closed manually (i.e., by clicking on the close button on the upper right corner) Commented Nov 23, 2023 at 21:36
  • I see - thats because you pause your script waiting for a ginput or waitforbuttonpress so the break at close_flag never gets run. You can check this by adding print statements. You probably should wrap your user inputs in timeout loops that periodically check if the figure has been closed Commented Nov 23, 2023 at 22:02
  • Yep that's also what I am thinking ... I am currently trying out some solutions where a timeout value is fed to plt.ginput(). I still need some time to work out a proper solution but I am now able to close the figure and keep the script running. I'll edit my question once I am satisfied Commented Nov 23, 2023 at 22:12

1 Answer 1

1

After tweaking around with matplotlib functionalities, I came to a solution I find satisfying (though not elegant). I'll leave it here in case anyone needs it in the future and close the question in a few days if no other answer is given.

As pointed by Jody, the main issue was indeed the plt.ginput() and plt.waitforbuttonpress() commands. Both commands need to be "refreshed" in a while loop to make sure the script doesn't end up stuck, waiting for the user to interact with the figure

import matplotlib.pyplot as plt
import numpy as np


# Defining the dataset
x = np.arange(0, 2*np.pi*5, 0.1)
y = np.sin(x)

# Pre-defining the flags    
global close_flag # See note (1)
close_flag = 0

global key_event_end # See note (1)
key_event_end = 0

# Shamelessly stolen from Matplotlib documentation
def tellme(s):
    plt.title(s, fontsize=13)
    plt.draw()

# Figure closing event
def close_event(event):
    global close_flag
    close_flag = 1

# Key pressing event
def key_event(event):
    if event.key == 'enter':
        global key_event_end
        key_event_end = 1

# Creating the figure
fig, axs = plt.subplots()
fig.canvas.mpl_connect('close_event', close_event)
fig.canvas.mpl_connect('key_press_event', key_event)

# Ploting the data
plt.plot(x, y)
plt.grid()

# Event loop
while close_flag == 0:
    pts = []     # Empty list to keep my cutoff values in memory
    _vlines = [] # Empty list to keep the vlines (artist objects)

    # Second loop - Used to define the cutoff dates
    while len(pts) < 2:
        # No point in asking for new inputs from the user if the figure is closed
        if close_flag == 1:
            break
        
        # Estimating the cutoffs
        try:
            # Leaving the option to the user to zoom in / out / move the figure
            update_title = True
            
            while True and close_flag == 0:
                # Updating the plot title
                if update_title:
                    tellme('Zoom in to select the cutoff value. Press any key to continue')
                    update_title = False
                                    
                if plt.waitforbuttonpress(0.1):
                    break
                
            # No point in asking for new inputs from the user if the figure is closed
            if close_flag == 1:
                break
                
            # Updating the title to let the user know they can select the value
            tellme('Select the cutoff value with the mouse')     
            
            # Keeping the user's input
            pt = plt.ginput(1, timeout=-1)[0][0] # We only need the value along the x axis
            
            # Converting the position clicked into the nearest value on the x-axis
            ind = np.argmin(abs( (x - pt ) ))
            pts.append(x[ind])

            # Ploting the cutoff value as a vertical line
            tmp = plt.vlines(x[ind], ymin=y.min(), ymax=y.max(), colors='k', linestyles='-.')
            _vlines.append(tmp)
            
            del pt
        
        # Handling the IndexError happening if no value is selected before the end of the timeout
        # or if the window is closed
        except IndexError:     
            continue

    # Updating the title a third time to inform the user both cutoff values are set
    if close_flag == 0:
        update_title = True
        key_event_end = 0
        
        # Looping to check if the user wish to change the values
        while True and close_flag == 0:
            if update_title:
                tellme('Press Enter to confirm or any other key to select new values')
                update_title = False
            
            # Breaking out the loop whenever a key is pressed
            if plt.waitforbuttonpress(0.1):
                break


    # Check if the Figure was closed or if the user confirmed it want to exit the loop
    if close_flag == 1 or key_event_end == 1:
        break

    # First control returned False, we remove the vertical lines and let the user select new values
    for v in _vlines:
        v.remove()

    
    plt.pause(0.1)

# Remove the global variables as they are now useless
del key_event_end
del close_flag
plt.close() # Make sure the figure is closed
pts.sort()  # Make sure your values are sorted in ascending order
            

Going over my code, here is what is happening:

  1. The first loop lets you control the overall figure
  2. The second loop while len(pts) < 2: lets you select the two cutoff values while the 3rd loop while True and close_flag == 0: (nested in (2)) lets you freely zoom in / out and move the figure, thus avoiding the issue with plt.ginput(). Note the plt.waitforbuttompress() which serves as a trigger to exit this loop.
  3. The last loop let the user confirm the values previously defined. Pressing enter will let break out of the main loop while any other key will trigger a new iteration.

In addition, there are a few interesting / tricky points:

  1. Pre-Defining the variables as global is not needed if you only run the script as is. However it seems to be required if the script is wrapped in a function
  2. Using plt.ginput() to get the values works as you would expect, except if one the axis comports datetime values. See here for an explanation. After some tests, I found the datum was 1970-01-01 00:00:00 instead of the one proposed in tacaswell's answer. Maybe it is version specific.
  3. While plt.ginput() is called, trying to zoom out/in or move the figure with the widgets will trigger the function. Different solutions exist, I drew inspiration from there
Sign up to request clarification or add additional context in comments.

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.