pythonmatplotlibonclickmouseevent

Storing mouse click event coordinates of graph with matplotlib


I am attempting to store coordinates from two mouse clicks in an interactive matplotlib graph in JupyterLab. I have managed to interact with the plot and get the x and y coordinates to print upon clicking. However, when I try to store those coordinates by appending to a list or numpy array (tried both), the values are not saving in my global variable coords. Since I can get the coordinates for one click successfully as ix and iy, I suppose I could have duplicate cells each with one click event each, but I'd prefer to have a single cell that can store both clicks' coordinates.

My original code was taken from this question. I have had to make a few adjustments to work with ipympl and ipywidgets.

Specifically, coords prints as empty even though my if statement regarding its length does seem to terminate the click event. Maybe I am missing something obvious (I am relatively new to Python from a MATLAB background). I'd appreciate any advice or alternative approaches to the issue.

%matplotlib ipympl

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from scipy.optimize import curve_fit
from scipy import stats
import os
import ipywidgets as widgets

out = widgets.Output()
coords = []

fig, ax = plt.subplots(figsize =(4, 4))
ax.plot(time,temp,'k-',linewidth = 1.5)
ax.set_xlabel("Time (sec)")
ax.set_ylabel(r"Temperature ($\degree$ C)")
ax.tick_params(axis="both",direction='in',top=True, right=True)

plt.show()

@out.capture()
def onclick(event):
    ix, iy = event.xdata, event.ydata
    print ('x = ',ix, 'y = ',iy)

    global coords
    coords.append(ix)
    coords.append(iy)
    
    # Disconnect after 2 clicks
    if len(coords) == 4:
        fig.canvas.mpl_disconnect(cid)
    
    return coords

cid = fig.canvas.mpl_connect('button_press_event', onclick)
display(out)
print(coords)

image of interactive figure and printed coordinates, for reference


Solution

  • Code in Jupyter (and other GUIs) doesn't work as you probably expect - code print(coords) in last line is execute at start, before you click anything, and system will NOT update this when you append values to coords.

    If you use print(coords) inside def onclick() then you should see that there are values on this list. And these values are in global variable.

    If you need it use it at once then do it directly in onclick().

    If you need it use little later then better create some button to execute code which you will click to execute code when you will already have both values.


    To displaying text you can also use w = widgets.Label(value="text") and later you can replace text in this widget using w.value = "new text"

    %matplotlib ipympl
    
    import pandas as pd
    import numpy as np
    import matplotlib.pyplot as plt
    from scipy.optimize import curve_fit
    from scipy import stats
    import os
    import ipywidgets as widgets
    
    out = widgets.Output()
    coords = []
    
    time = [1,2,3,4,5]
    temp = [1,3,2,5,1]
    
    fig, ax = plt.subplots(figsize =(4, 4))
    ax.plot(time,temp,'k-',linewidth = 1.5)
    ax.set_xlabel("Time (sec)")
    ax.set_ylabel(r"Temperature ($\degree$ C)")
    ax.tick_params(axis="both",direction='in',top=True, right=True)
    
    plt.show()
    
    @out.capture()
    def onclick(event):
        global coords   # PEP8: globals always at the beginning of function 
        
        ix, iy = event.xdata, event.ydata
        print('x = ',ix, 'y = ',iy)
    
        coords.append(ix)
        coords.append(iy)
    
        #print(coords) # display new text but it doesn't remove previous text
        text.value = f'coord: {coords}'  # update existing Label (it needs string) and remove previous text
        
        # Disconnect after 2 clicks
        if len(coords) == 4:
            fig.canvas.mpl_disconnect(cid)
            # ... here you could use coords in calculations ...
        
        #return coords
    
    cid = fig.canvas.mpl_connect('button_press_event', onclick)
    
    text = widgets.Label(value=f'coord: {coords}')  # create widget which can be updated later
    
    display(text, out)
    #print(coords)  # this is executed before you click and later it will not update it.