Issue
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?
Solution
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:
- The first loop lets you control the overall figure
- The second loop
while len(pts) < 2:
lets you select the two cutoff values while the 3rd loopwhile True and close_flag == 0:
(nested in (2)) lets you freely zoom in / out and move the figure, thus avoiding the issue withplt.ginput()
. Note theplt.waitforbuttompress()
which serves as a trigger to exit this loop. - 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:
- 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
- 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. - 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
Answered By - greg salles
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.