Event handling and picking#
Matplotlib works with a number of user interface toolkits (wxpython,tkinter, qt, gtk, and macOS) and in order to support features likeinteractive panning and zooming of figures, it is helpful to thedevelopers to have an API for interacting with the figure via keypresses and mouse movements that is "GUI neutral" so we don't have torepeat a lot of code across the different user interfaces. Althoughthe event handling API is GUI neutral, it is based on the GTK model,which was the first user interface Matplotlib supported. The eventsthat are triggered are also a bit richer vis-a-vis Matplotlib thanstandard GUI events, including information like whichAxes the event occurred in. The events alsounderstand the Matplotlib coordinate system, and report eventlocations in both pixel and data coordinates.
Event connections#
To receive events, you need to write a callback function and thenconnect your function to the event manager, which is part of theFigureCanvasBase. Here is a simpleexample that prints the location of the mouse click and which buttonwas pressed:
fig,ax=plt.subplots()ax.plot(np.random.rand(10))defonclick(event):print('%s click: button=%d, x=%d, y=%d, xdata=%f, ydata=%f'%('double'ifevent.dblclickelse'single',event.button,event.x,event.y,event.xdata,event.ydata))cid=fig.canvas.mpl_connect('button_press_event',onclick)
TheFigureCanvasBase.mpl_connect method returns a connection id (aninteger), which can be used to disconnect the callback via
fig.canvas.mpl_disconnect(cid)
Note
The canvas retains only weak references to instance methods used ascallbacks. Therefore, you need to retain a reference to instances owningsuch methods. Otherwise the instance will be garbage-collected and thecallback will vanish.
This does not affect free functions used as callbacks.
Here are the events that you can connect to, the class instances thatare sent back to you when the event occurs, and the event descriptions:
Event name | Class | Description |
|---|---|---|
'button_press_event' | mouse button is pressed | |
'button_release_event' | mouse button is released | |
'close_event' | figure is closed | |
'draw_event' | canvas has been drawn (but screenwidget not updated yet) | |
'key_press_event' | key is pressed | |
'key_release_event' | key is released | |
'motion_notify_event' | mouse moves | |
'pick_event' | artist in the canvas is selected | |
'resize_event' | figure canvas is resized | |
'scroll_event' | mouse scroll wheel is rolled | |
'figure_enter_event' | mouse enters a new figure | |
'figure_leave_event' | mouse leaves a figure | |
'axes_enter_event' | mouse enters a new axes | |
'axes_leave_event' | mouse leaves an axes |
Note
When connecting to 'key_press_event' and 'key_release_event' events,you may encounter inconsistencies between the different user interfacetoolkits that Matplotlib works with. This is due to inconsistencies/limitationsof the user interface toolkit. The following table shows some basic examples ofwhat you may expect to receive as key(s) (using a QWERTY keyboard layout)from the different user interface toolkits, where a comma separates different keys:
Key(s) Pressed | Tkinter | Qt | macosx | WebAgg | GTK | WxPython |
|---|---|---|---|---|---|---|
Shift+2 | shift, @ | shift, @ | shift, @ | shift, @ | shift, @ | shift, shift+2 |
Shift+F1 | shift, shift+f1 | shift, shift+f1 | shift, shift+f1 | shift, shift+f1 | shift, shift+f1 | shift, shift+f1 |
Shift | shift | shift | shift | shift | shift | shift |
Control | control | control | control | control | control | control |
Alt | alt | alt | alt | alt | alt | alt |
AltGr | iso_level3_shift | nothing | alt | iso_level3_shift | nothing | |
CapsLock | caps_lock | caps_lock | caps_lock | caps_lock | caps_lock | caps_lock |
CapsLock+a | caps_lock, A | caps_lock, a | caps_lock, a | caps_lock, A | caps_lock, A | caps_lock, a |
a | a | a | a | a | a | a |
Shift+a | shift, A | shift, A | shift, A | shift, A | shift, A | shift, A |
CapsLock+Shift+a | caps_lock, shift, a | caps_lock, shift, A | caps_lock, shift, A | caps_lock, shift, a | caps_lock, shift, a | caps_lock, shift, A |
Ctrl+Shift+Alt | control, ctrl+shift, ctrl+meta | control, ctrl+shift, ctrl+meta | control, ctrl+shift, ctrl+alt+shift | control, ctrl+shift, ctrl+meta | control, ctrl+shift, ctrl+meta | control, ctrl+shift, ctrl+alt |
Ctrl+Shift+a | control, ctrl+shift, ctrl+a | control, ctrl+shift, ctrl+A | control, ctrl+shift, ctrl+A | control, ctrl+shift, ctrl+A | control, ctrl+shift, ctrl+A | control, ctrl+shift, ctrl+A |
F1 | f1 | f1 | f1 | f1 | f1 | f1 |
Ctrl+F1 | control, ctrl+f1 | control, ctrl+f1 | control,nothing | control, ctrl+f1 | control, ctrl+f1 | control, ctrl+f1 |
Matplotlib attaches some keypress callbacks by default for interactivity; theyare documented in theNavigation keyboard shortcuts section.
Event attributes#
All Matplotlib events inherit from the base classmatplotlib.backend_bases.Event, which stores the attributes:
namethe event name
canvasthe FigureCanvas instance generating the event
guiEventthe GUI event that triggered the Matplotlib event
The most common events that are the bread and butter of event handlingare key press/release events and mouse press/release and movementevents. TheKeyEvent andMouseEvent classes that handlethese events are both derived from the LocationEvent, which has thefollowing attributes
x,ymouse x and y position in pixels from left and bottom of canvas
inaxesthe
Axesinstance over which the mouse is, if any; else Nonexdata,ydatamouse x and y position in data coordinates, if the mouse is over anaxes
Let's look a simple example of a canvas, where a simple line segmentis created every time a mouse is pressed:
frommatplotlibimportpyplotaspltclassLineBuilder:def__init__(self,line):self.line=lineself.xs=list(line.get_xdata())self.ys=list(line.get_ydata())self.cid=line.figure.canvas.mpl_connect('button_press_event',self)def__call__(self,event):print('click',event)ifevent.inaxes!=self.line.axes:returnself.xs.append(event.xdata)self.ys.append(event.ydata)self.line.set_data(self.xs,self.ys)self.line.figure.canvas.draw()fig,ax=plt.subplots()ax.set_title('click to build line segments')line,=ax.plot([0],[0])# empty linelinebuilder=LineBuilder(line)plt.show()
TheMouseEvent that we just used is aLocationEvent, so we have access tothe data and pixel coordinates via(event.x,event.y) and(event.xdata,event.ydata). In addition to theLocationEvent attributes, it also has:
buttonthe button pressed: None,
MouseButton, 'up', or 'down' (up and down are used for scroll events)keythe key pressed: None, any character, 'shift', 'win', or 'control'
Draggable rectangle exercise#
Write a draggable rectangle class that is initialized with aRectangle instance but will move itsxylocation when dragged.
Hint: You will need to store the originalxy location of the rectangle which is stored asrect.xy andconnect to the press, motion and release mouse events. When the mouseis pressed, check to see if the click occurs over your rectangle (see.Rectangle.contains) and if it does, storethe rectangle xy and the location of the mouse click in data coordinates.In the motion event callback, compute the deltax and deltay of themouse movement, and add those deltas to the origin of the rectangleyou stored, then redraw the figure. On the button release event, justreset all the button press data you stored as None.
Here is the solution:
importnumpyasnpimportmatplotlib.pyplotaspltclassDraggableRectangle:def__init__(self,rect):self.rect=rectself.press=Nonedefconnect(self):"""Connect to all the events we need."""self.cidpress=self.rect.figure.canvas.mpl_connect('button_press_event',self.on_press)self.cidrelease=self.rect.figure.canvas.mpl_connect('button_release_event',self.on_release)self.cidmotion=self.rect.figure.canvas.mpl_connect('motion_notify_event',self.on_motion)defon_press(self,event):"""Check whether mouse is over us; if so, store some data."""ifevent.inaxes!=self.rect.axes:returncontains,attrd=self.rect.contains(event)ifnotcontains:returnprint('event contains',self.rect.xy)self.press=self.rect.xy,(event.xdata,event.ydata)defon_motion(self,event):"""Move the rectangle if the mouse is over us."""ifself.pressisNoneorevent.inaxes!=self.rect.axes:return(x0,y0),(xpress,ypress)=self.pressdx=event.xdata-xpressdy=event.ydata-ypress# print(f'x0={x0}, xpress={xpress}, event.xdata={event.xdata}, '# f'dx={dx}, x0+dx={x0+dx}')self.rect.set_x(x0+dx)self.rect.set_y(y0+dy)self.rect.figure.canvas.draw()defon_release(self,event):"""Clear button press information."""self.press=Noneself.rect.figure.canvas.draw()defdisconnect(self):"""Disconnect all callbacks."""self.rect.figure.canvas.mpl_disconnect(self.cidpress)self.rect.figure.canvas.mpl_disconnect(self.cidrelease)self.rect.figure.canvas.mpl_disconnect(self.cidmotion)fig,ax=plt.subplots()rects=ax.bar(range(10),20*np.random.rand(10))drs=[]forrectinrects:dr=DraggableRectangle(rect)dr.connect()drs.append(dr)plt.show()
Extra credit: Use blitting to make the animated drawing faster andsmoother.
Extra credit solution:
# Draggable rectangle with blitting.importnumpyasnpimportmatplotlib.pyplotaspltclassDraggableRectangle:lock=None# only one can be animated at a timedef__init__(self,rect):self.rect=rectself.press=Noneself.background=Nonedefconnect(self):"""Connect to all the events we need."""self.cidpress=self.rect.figure.canvas.mpl_connect('button_press_event',self.on_press)self.cidrelease=self.rect.figure.canvas.mpl_connect('button_release_event',self.on_release)self.cidmotion=self.rect.figure.canvas.mpl_connect('motion_notify_event',self.on_motion)defon_press(self,event):"""Check whether mouse is over us; if so, store some data."""if(event.inaxes!=self.rect.axesorDraggableRectangle.lockisnotNone):returncontains,attrd=self.rect.contains(event)ifnotcontains:returnprint('event contains',self.rect.xy)self.press=self.rect.xy,(event.xdata,event.ydata)DraggableRectangle.lock=self# draw everything but the selected rectangle and store the pixel buffercanvas=self.rect.figure.canvasaxes=self.rect.axesself.rect.set_animated(True)canvas.draw()self.background=canvas.copy_from_bbox(self.rect.axes.bbox)# now redraw just the rectangleaxes.draw_artist(self.rect)# and blit just the redrawn areacanvas.blit(axes.bbox)defon_motion(self,event):"""Move the rectangle if the mouse is over us."""if(event.inaxes!=self.rect.axesorDraggableRectangle.lockisnotself):return(x0,y0),(xpress,ypress)=self.pressdx=event.xdata-xpressdy=event.ydata-ypressself.rect.set_x(x0+dx)self.rect.set_y(y0+dy)canvas=self.rect.figure.canvasaxes=self.rect.axes# restore the background regioncanvas.restore_region(self.background)# redraw just the current rectangleaxes.draw_artist(self.rect)# blit just the redrawn areacanvas.blit(axes.bbox)defon_release(self,event):"""Clear button press information."""ifDraggableRectangle.lockisnotself:returnself.press=NoneDraggableRectangle.lock=None# turn off the rect animation property and reset the backgroundself.rect.set_animated(False)self.background=None# redraw the full figureself.rect.figure.canvas.draw()defdisconnect(self):"""Disconnect all callbacks."""self.rect.figure.canvas.mpl_disconnect(self.cidpress)self.rect.figure.canvas.mpl_disconnect(self.cidrelease)self.rect.figure.canvas.mpl_disconnect(self.cidmotion)fig,ax=plt.subplots()rects=ax.bar(range(10),20*np.random.rand(10))drs=[]forrectinrects:dr=DraggableRectangle(rect)dr.connect()drs.append(dr)plt.show()
Mouse enter and leave#
If you want to be notified when the mouse enters or leaves a figure oraxes, you can connect to the figure/axes enter/leave events. Here isa simple example that changes the colors of the axes and figurebackground that the mouse is over:
"""Illustrate the figure and axes enter and leave events by changing theframe colors on enter and leave"""importmatplotlib.pyplotaspltdefenter_axes(event):print('enter_axes',event.inaxes)event.inaxes.patch.set_facecolor('yellow')event.canvas.draw()defleave_axes(event):print('leave_axes',event.inaxes)event.inaxes.patch.set_facecolor('white')event.canvas.draw()defenter_figure(event):print('enter_figure',event.canvas.figure)event.canvas.figure.patch.set_facecolor('red')event.canvas.draw()defleave_figure(event):print('leave_figure',event.canvas.figure)event.canvas.figure.patch.set_facecolor('grey')event.canvas.draw()fig1,axs=plt.subplots(2)fig1.suptitle('mouse hover over figure or axes to trigger events')fig1.canvas.mpl_connect('figure_enter_event',enter_figure)fig1.canvas.mpl_connect('figure_leave_event',leave_figure)fig1.canvas.mpl_connect('axes_enter_event',enter_axes)fig1.canvas.mpl_connect('axes_leave_event',leave_axes)fig2,axs=plt.subplots(2)fig2.suptitle('mouse hover over figure or axes to trigger events')fig2.canvas.mpl_connect('figure_enter_event',enter_figure)fig2.canvas.mpl_connect('figure_leave_event',leave_figure)fig2.canvas.mpl_connect('axes_enter_event',enter_axes)fig2.canvas.mpl_connect('axes_leave_event',leave_axes)plt.show()
Object picking#
You can enable picking by setting thepicker property of anArtist (suchasLine2D,Text,Patch,Polygon,AxesImage, etc.)
Thepicker property can be set using various types:
NonePicking is disabled for this artist (default).
booleanIf True, then picking will be enabled and the artist will fire apick event if the mouse event is over the artist.
callableIf picker is a callable, it is a user supplied function whichdetermines whether the artist is hit by the mouse event. Thesignature is
hit,props=picker(artist,mouseevent)todetermine the hit test. If the mouse event is over the artist,returnhit=True;propsis a dictionary of properties thatbecome additional attributes on thePickEvent.
The artist'spickradius property can additionally be set to a tolerancevalue in points (there are 72 points per inch) that determines how far themouse can be and still trigger a mouse event.
After you have enabled an artist for picking by setting thepickerproperty, you need to connect a handler to the figure canvas pick_event to getpick callbacks on mouse press events. The handler typically looks like
defpick_handler(event):mouseevent=event.mouseeventartist=event.artist# now do something with this...
ThePickEvent passed to your callback always has the following attributes:
mouseeventThe
MouseEventthat generate the pick event. Seeevent-attributesfor a list of useful attributes on the mouse event.artistThe
Artistthat generated the pick event.
Additionally, certain artists likeLine2D andPatchCollection may attachadditional metadata, like the indices of the data that meet thepicker criteria (e.g., all the points in the line that are within thespecifiedpickradius tolerance).
Simple picking example#
In the example below, we enable picking on the line and set a pick radiustolerance in points. Theonpickcallback function will be called when the pick event it within thetolerance distance from the line, and has the indices of the datavertices that are within the pick distance tolerance. Ouronpickcallback function simply prints the data that are under the picklocation. Different Matplotlib Artists can attach different data tothe PickEvent. For example,Line2D attaches the ind property,which are the indices into the line data under the pick point. See.Line2D.pick for details on thePickEvent properties of the line.
importnumpyasnpimportmatplotlib.pyplotaspltfig,ax=plt.subplots()ax.set_title('click on points')line,=ax.plot(np.random.rand(100),'o',picker=True,pickradius=5)# 5 points tolerancedefonpick(event):thisline=event.artistxdata=thisline.get_xdata()ydata=thisline.get_ydata()ind=event.indpoints=tuple(zip(xdata[ind],ydata[ind]))print('onpick points:',points)fig.canvas.mpl_connect('pick_event',onpick)plt.show()
Picking exercise#
Create a data set of 100 arrays of 1000 Gaussian random numbers andcompute the sample mean and standard deviation of each of them (hint:NumPy arrays have a mean and std method) and make a xy marker plot ofthe 100 means vs. the 100 standard deviations. Connect the linecreated by the plot command to the pick event, and plot the originaltime series of the data that generated the clicked on points. If morethan one point is within the tolerance of the clicked on point, youcan use multiple subplots to plot the multiple time series.
Exercise solution:
"""Compute the mean and stddev of 100 data sets and plot mean vs. stddev.When you click on one of the (mean, stddev) points, plot the raw datasetthat generated that point."""importnumpyasnpimportmatplotlib.pyplotaspltX=np.random.rand(100,1000)xs=np.mean(X,axis=1)ys=np.std(X,axis=1)fig,ax=plt.subplots()ax.set_title('click on point to plot time series')line,=ax.plot(xs,ys,'o',picker=True,pickradius=5)# 5 points tolerancedefonpick(event):ifevent.artist!=line:returnn=len(event.ind)ifnotn:returnfig,axs=plt.subplots(n,squeeze=False)fordataind,axinzip(event.ind,axs.flat):ax.plot(X[dataind])ax.text(0.05,0.9,f"$\\mu$={xs[dataind]:1.3f}\n$\\sigma$={ys[dataind]:1.3f}",transform=ax.transAxes,verticalalignment='top')ax.set_ylim(-0.5,1.5)fig.show()returnTruefig.canvas.mpl_connect('pick_event',onpick)plt.show()