52

I am trying to implement a simple mouse click event in matplotlib. I wish to plot a figure then use the mouse to select the lower and upper limits for integration. So far I am able to print the coordinates to screen but not store them for later use in the program. I would also like to exit the connection to the figure after the second mouse click.

Below is the code which currently plots and then prints the coordinates.

My Question(s):

How can I store coordinates from the figure to list? i.e. click = [xpos, ypos]

Is it possible to get two sets of x coordinates in order to do a simple integration over that section of line?

import numpy as np
import matplotlib.pyplot as plt

x = np.arange(-10,10)
y = x**2

fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(x,y)

def onclick(event):
    global ix, iy
    ix, iy = event.xdata, event.ydata
    print 'x = %d, y = %d'%(
        ix, iy)

    global coords
    coords = [ix, iy]

    return coords


for i in xrange(0,1):

    cid = fig.canvas.mpl_connect('button_press_event', onclick)


plt.show()
smashbro
  • 1,094
  • 1
  • 9
  • 19

3 Answers3

55

mpl_connect needs to be called just once to connect the event to event handler. It will start listening to click event until you disconnect. And you can use

fig.canvas.mpl_disconnect(cid)

to disconnect the event hook.

What you want to do is something like:

import numpy as np
import matplotlib.pyplot as plt

x = np.arange(-10,10)
y = x**2

fig = plt.figure()
ax = fig.add_subplot(111)
ax.plot(x,y)

coords = []

def onclick(event):
    global ix, iy
    ix, iy = event.xdata, event.ydata
    print (f'x = {ix}, y = {iy}')

    global coords
    coords.append((ix, iy))
    
    if len(coords) == 2:
        fig.canvas.mpl_disconnect(cid)

    return coords
cid = fig.canvas.mpl_connect('button_press_event', onclick)
otterb
  • 2,660
  • 2
  • 29
  • 48
  • 1
    You don't need to return `coords`, the value goes no where. it gets stored via closure over the global variable. – tacaswell Aug 27 '14 at 23:42
  • @tcaswell i just did a minimum change to the code. But, what is closure here? – otterb Aug 28 '14 at 09:07
  • Excellent thanks very much. I'll update the question with my solution which is a slightly tweaked version of yours. – smashbro Aug 29 '14 at 04:34
  • How to add a button to disconnect, instead of test if coords has len two? – Sigur Mar 14 '18 at 23:26
  • 1
    Why you use global as attibute types inside onclick function? – eduardosufan Aug 21 '18 at 13:17
  • @otterb I tried to implement this method, but even when I click the close button, it guesses it just like a normal mouse click. I will like it to know when it is clicking inside the graph window, how to do it? – Nisha Jan 17 '19 at 10:00
  • @eduardosufan: This is the principle how global Python variables work: You declare them at the top (global scope, no indent) as a 'normal' variable. If you use them in a method, you have to tell the method that you really want to access the global variable, otherwise it will create a local variable with the same name. In the local case, the coordinates would not be registered correctly. I know, quite other way round than in other languages, definitely caused me some headache - but actually good concept if you think about it twice :D – asti205 Jan 24 '21 at 11:51
6

Thanks to otterb for providing the answer! I've added in a little function taken from here... Find nearest value in numpy array

In all this code will plot, wait for selection of x points and then return the indices of the x array needed for any integration, summations etc.

Ta,

import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import trapz

def find_nearest(array,value):
    idx = (np.abs(array-value)).argmin()
    return array[idx]

# Simple mouse click function to store coordinates
def onclick(event):
    global ix, iy
    ix, iy = event.xdata, event.ydata

    # print 'x = %d, y = %d'%(
    #     ix, iy)

    # assign global variable to access outside of function
    global coords
    coords.append((ix, iy))

    # Disconnect after 2 clicks
    if len(coords) == 2:
        fig.canvas.mpl_disconnect(cid)
        plt.close(1)
    return


x = np.arange(-10,10)
y = x**2

fig = plt.figure(1)
ax = fig.add_subplot(111)
ax.plot(x,y)

coords = []

# Call click func
cid = fig.canvas.mpl_connect('button_press_event', onclick)

plt.show(1)


# limits for integration
ch1 = np.where(x == (find_nearest(x, coords[0][0])))
ch2 = np.where(x == (find_nearest(x, coords[1][0])))

# Calculate integral
y_int = trapz(y[ch1[0][0]:ch2[0][0]], x = x[ch1[0][0]:ch2[0][0]])

print ''
print 'Integral between '+str(coords[0][0])+ ' & ' +str(coords[1][0])
print y_int
Community
  • 1
  • 1
smashbro
  • 1,094
  • 1
  • 9
  • 19
6

I want to provide a different answer here since I recently tried to do event handling but the soulutions here do not distinguish between zooming, paning and clicking, everything gets messed up in my case. I found an extention for matplotlib called mpl_point_clicker that works really well for me and can be installed with pip (with python 3.X). Here is the basic usage from their documentation:

import numpy as np
import matplotlib.pyplot as plt
from mpl_point_clicker import clicker

fig, ax = plt.subplots(constrained_layout=True)
ax.plot(np.sin(np.arange(200)/(5*np.pi)))
klicker = clicker(ax, ["event"], markers=["x"])

plt.show()

print(klicker.get_positions())

The figure with 3 clicks and the output look like this

enter image description here

Output:

{'event': array([[ 24.22415481,   1.00237796],
       [ 74.19892948,  -0.99140661],
       [123.23078387,   1.00237796]])}
Vorgon
  • 104
  • 1
  • 4
  • Awesome answer. A related question: https://stackoverflow.com/questions/70947912/how-to-draw-lines-between-mouseclicks-on-a-matplotlib-plot – zabop Feb 01 '22 at 21:41
  • If you then want to use the (x,y) coordinates you simply call the array as vertices1 = (klicker.get_positions())['event'] But, watch out! This is an array, and depending on what you need this for, you might want to convert the array to a list. See my answer here: https://discourse.matplotlib.org/t/mpl-point-clicker-how-to-return-to-code-after-clicking/23743/2 – Hugo May 11 '23 at 02:40