Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Matplotlib: responding to click events

I want to do the following, using matplotlib:

  1. Create a line between two points, by doing the following: i. Double click on canvas using Left button (first point created) ii. Either drag mouse to (or simply click on) second point ii. Have line drawn between first and second point

  2. Place a green (or any other color) circle on the canvas, by doing the following: i. Double clicking on canvas, using RIGHT button

  3. Since I'm likely to make mistakes when double clicking, I want to be able to select a plotted circle (or line), and press the delete button to delete the selected item.

Back in the good old days of VB, this was a 15 minute job. After wasting several hours on this, I have run out of ideas.

This is what I have so far:

import matplotlib.pyplot as plt


class LineDrawer(object):
    lines = []
    def draw_line(self):
        ax = plt.gca()
        xy = plt.ginput(2)

        x = [p[0] for p in xy]
        y = [p[1] for p in xy]
        line = plt.plot(x,y)
        ax.figure.canvas.draw()

        self.lines.append(line)


def onclick(event):
    if event.dblclick:
        if event.button == 1:
            # Draw line
            ld = LineDrawer()
            ld.draw_line() # here you click on the plot
        elif event.button == 3:
            # Write to figure
            plt.figtext(3, 8, 'boxed italics text in data coords', style='italic', bbox={'facecolor':'red', 'alpha':0.5, 'pad':10})
            circ = plt.Circle((event.x, event.y), radius=0.07, color='g')
            ax.add_patch(circ)
            plt.draw()
        else:
            pass # Do nothing


def onpick(event):
    thisline = event.artist
    xdata = thisline.get_xdata()
    ydata = thisline.get_ydata()
    ind = event.ind
    print ('onpick points:', zip(xdata[ind], ydata[ind]))



fig, ax = plt.subplots()

connection_id = fig.canvas.mpl_connect('button_press_event', onclick)
fig.canvas.mpl_connect('pick_event', onpick)


plt.tight_layout()

plt.show()

Apart from the delete functionality, which I haven't even got round to yet, why is my code not performing requirements 1 and 2?

What am I doing wrong?, more importantly, how do I fix the code to get the required functionality?

like image 314
Homunculus Reticulli Avatar asked Nov 06 '15 15:11

Homunculus Reticulli


People also ask

What are Matplotlib events and where are they stored?

All Matplotlib events inherit from the base class matplotlib.backend_bases.Event, which stores the attributes: The most common events that are the bread and butter of event handling are key press/release events and mouse press/release and movement events.

Is Matplotlib event handling GUI neutral?

Although the event handling API is GUI neutral, it is based on the GTK model, which was the first user interface Matplotlib supported. The events that are triggered are also a bit richer vis-a-vis Matplotlib than standard GUI events, including information like which Axes the event occurred in.

Why are the key_press_event and key_release_event events not working in Matplotlib?

When connecting to 'key_press_event' and 'key_release_event' events, you may encounter inconsistencies between the different user interface toolkits that Matplotlib works with. This is due to inconsistencies/limitations of the user interface toolkit.

How do I create a line between two points in Matplotlib?

I want to do the following, using matplotlib: Create a line between two points, by doing the following: i. Double click on canvas using Left button (first point created) ii. Either drag mouse to (or simply click on) second point ii.


1 Answers

You're almost there, but your logic sends the code to draw a line on a double click without storing where the double click was, so it then requires two single clicks to draw a line. Also, you needed to draw the canvas in the circle code. Here's a minimally revised version that does requirement 1 and 2:

import matplotlib.pyplot as plt


class LineDrawer(object):
    lines = []
    def draw_line(self, startx,starty):
        ax = plt.gca()
        xy = plt.ginput(1)
        x = [startx,xy[0][0]]
        y = [starty,xy[0][1]]
        line = plt.plot(x,y)
        ax.figure.canvas.draw()

        self.lines.append(line)


def onclick(event):
    if event.dblclick:
        if event.button == 1:
            # Draw line
            ld = LineDrawer()
            ld.draw_line(event.xdata,event.ydata) # here you click on the plot
        elif event.button == 3:
            # Write to figure
            plt.figtext(3, 8, 'boxed italics text in data coords', style='italic', bbox={'facecolor':'red', 'alpha':0.5, 'pad':10})
            circ = plt.Circle((event.xdata, event.ydata), radius=0.07, color='g')
            ax.add_patch(circ)
            ax.figure.canvas.draw()
        else:
            pass # Do nothing


def onpick(event):
    thisline = event.artist
    xdata = thisline.get_xdata()
    ydata = thisline.get_ydata()
    ind = event.ind
    print ('onpick points:', zip(xdata[ind], ydata[ind]))



fig, ax = plt.subplots()

connection_id = fig.canvas.mpl_connect('button_press_event', onclick)
fig.canvas.mpl_connect('pick_event', onpick)


plt.tight_layout()

plt.show()

Note that matplotlib might not be the best or easiest way to implement these requirements - also the axis will auto rescale on drawing the first line as it stands. You can alter this by fixing the xlim and ylim. e.g. as follows:

ax.set_xlim([0,2])
ax.set_ylim([0,2])

To implement requirement 3, you're going to have to store the picked object and listen for a keypress matching delete to remove it. Here's a version combining all the above. I've tried to stick to your design as much as possible. I store the reference to the picked object in the relevant axes object. You might want to implement your own data structure to store the picked object if you don't like inserting it into the current axis. I've tested it a bit, but there are probably click / keypress sequences that could confuse the logic.

import matplotlib.pyplot as plt

# function to draw lines - from matplotlib examples.  Note you don't need
# to keep a reference to the lines drawn, so I've removed the class as it
# is overkill for your purposes
def draw_line(startx,starty):
        ax = plt.gca()
        xy = plt.ginput(1)
        x = [startx,xy[0][0]]
        y = [starty,xy[0][1]]
        line = ax.plot(x,y, picker=5) # note that picker=5 means a click within 5 pixels will "pick" the Line2D object
        ax.figure.canvas.draw()        

def onclick(event):
    """
    This implements click functionality.  If it's a double click do something,
    else ignore.
    Once in the double click block, if its a left click, wait for a further 
    click and draw a line between the double click co-ordinates and that click
    (using ginput(1) - the 1 means wait for one mouse input - a higher number
    is used to get multiple clicks to define a polyline)
    If the double click was a right click, draw the fixed radius circle

    """
    if event.dblclick:
        if event.button == 1:
            # Draw line
            draw_line(event.xdata,event.ydata) # here you click on the plot
        elif event.button == 3:
            # Write to figure
            plt.figtext(3, 8, 'boxed italics text in data coords', style='italic', bbox={'facecolor':'red', 'alpha':0.5, 'pad':10})
            circ = plt.Circle((event.xdata, event.ydata), radius=0.07, color='g', picker = True)
            ax.add_patch(circ)
            ax.figure.canvas.draw()
        else:
            pass # Do nothing


def onpick(event):    
    """
    Handles the pick event - if an object has been picked, store a
    reference to it.  We do this by simply adding a reference to it
    named 'stored_pick' to the axes object.  Note that in python we
    can dynamically add an attribute variable (stored_pick) to an 
    existing object - even one that is produced by a library as in this
    case
    """
    this_artist = event.artist #the picked object is available as event.artist
    # print(this_artist) #For debug just to show you which object is picked
    plt.gca().picked_object = this_artist

def on_key(event):
    """
    Function to be bound to the key press event
    If the key pressed is delete and there is a picked object,
    remove that object from the canvas
    """
    if event.key == u'delete':
        ax = plt.gca()
        if ax.picked_object:
            ax.picked_object.remove()
            ax.picked_object = None
            ax.figure.canvas.draw()


fig, ax = plt.subplots()

#First we need to catch three types of event, clicks, "picks" (a specialised
#type of click to select an object on a matplotlib canvas) and key presses.
#The logic is - if it's a right double click, wait for the next click and draw
#a line, if its a right double click draw a fixed radius circle.  If it's a
#pick, store a reference to the picked item until the next keypress.  If it's
#a keypress - test if it's delete and if so, remove the picked object.
#The functions (defined above) bound to the events implement this logic
connection_id = fig.canvas.mpl_connect('button_press_event', onclick)
fig.canvas.mpl_connect('pick_event', onpick)
cid = fig.canvas.mpl_connect('key_press_event', on_key)

#set the size of the matplotlib figure in data units, so that it doesn't
#auto-resize (which it will be default on the first drawn item)
ax.set_xlim([0,2])
ax.set_ylim([0,2])
ax.aspect = 1
plt.tight_layout()

plt.show()
like image 75
J Richard Snape Avatar answered Oct 12 '22 12:10

J Richard Snape