1.. _event-handling-tutorial: 2 3************************** 4Event handling and picking 5************************** 6 7matplotlib works with a number of user interface toolkits (wxpython, 8tkinter, qt4, gtk, and macosx) and in order to support features like 9interactive panning and zooming of figures, it is helpful to the 10developers to have an API for interacting with the figure via key 11presses and mouse movements that is "GUI neutral" so we don't have to 12repeat a lot of code across the different user interfaces. Although 13the event handling API is GUI neutral, it is based on the GTK model, 14which was the first user interface matplotlib supported. The events 15that are triggered are also a bit richer vis-a-vis matplotlib than 16standard GUI events, including information like which 17:class:`matplotlib.axes.Axes` the event occurred in. The events also 18understand the matplotlib coordinate system, and report event 19locations in both pixel and data coordinates. 20 21.. _event-connections: 22 23Event connections 24================= 25 26To receive events, you need to write a callback function and then 27connect your function to the event manager, which is part of the 28:class:`~matplotlib.backend_bases.FigureCanvasBase`. Here is a simple 29example that prints the location of the mouse click and which button 30was pressed:: 31 32 fig, ax = plt.subplots() 33 ax.plot(np.random.rand(10)) 34 35 def onclick(event): 36 print('%s click: button=%d, x=%d, y=%d, xdata=%f, ydata=%f' % 37 ('double' if event.dblclick else 'single', event.button, 38 event.x, event.y, event.xdata, event.ydata)) 39 40 cid = fig.canvas.mpl_connect('button_press_event', onclick) 41 42The ``FigureCanvas`` method 43:meth:`~matplotlib.backend_bases.FigureCanvasBase.mpl_connect` returns 44a connection id which is simply an integer. When you want to 45disconnect the callback, just call:: 46 47 fig.canvas.mpl_disconnect(cid) 48 49.. note:: 50 The canvas retains only weak references to the callbacks. Therefore 51 if a callback is a method of a class instance, you need to retain 52 a reference to that instance. Otherwise the instance will be 53 garbage-collected and the callback will vanish. 54 55 56Here are the events that you can connect to, the class instances that 57are sent back to you when the event occurs, and the event descriptions 58 59 60======================= ============================================================================================= 61Event name Class and description 62======================= ============================================================================================= 63'button_press_event' :class:`~matplotlib.backend_bases.MouseEvent` - mouse button is pressed 64'button_release_event' :class:`~matplotlib.backend_bases.MouseEvent` - mouse button is released 65'draw_event' :class:`~matplotlib.backend_bases.DrawEvent` - canvas draw (but before screen update) 66'key_press_event' :class:`~matplotlib.backend_bases.KeyEvent` - key is pressed 67'key_release_event' :class:`~matplotlib.backend_bases.KeyEvent` - key is released 68'motion_notify_event' :class:`~matplotlib.backend_bases.MouseEvent` - mouse motion 69'pick_event' :class:`~matplotlib.backend_bases.PickEvent` - an object in the canvas is selected 70'resize_event' :class:`~matplotlib.backend_bases.ResizeEvent` - figure canvas is resized 71'scroll_event' :class:`~matplotlib.backend_bases.MouseEvent` - mouse scroll wheel is rolled 72'figure_enter_event' :class:`~matplotlib.backend_bases.LocationEvent` - mouse enters a new figure 73'figure_leave_event' :class:`~matplotlib.backend_bases.LocationEvent` - mouse leaves a figure 74'axes_enter_event' :class:`~matplotlib.backend_bases.LocationEvent` - mouse enters a new axes 75'axes_leave_event' :class:`~matplotlib.backend_bases.LocationEvent` - mouse leaves an axes 76======================= ============================================================================================= 77 78.. _event-attributes: 79 80Event attributes 81================ 82 83All matplotlib events inherit from the base class 84:class:`matplotlib.backend_bases.Event`, which store the attributes: 85 86 ``name`` 87 the event name 88 89 ``canvas`` 90 the FigureCanvas instance generating the event 91 92 ``guiEvent`` 93 the GUI event that triggered the matplotlib event 94 95 96The most common events that are the bread and butter of event handling 97are key press/release events and mouse press/release and movement 98events. The :class:`~matplotlib.backend_bases.KeyEvent` and 99:class:`~matplotlib.backend_bases.MouseEvent` classes that handle 100these events are both derived from the LocationEvent, which has the 101following attributes 102 103 ``x`` 104 x position - pixels from left of canvas 105 106 ``y`` 107 y position - pixels from bottom of canvas 108 109 ``inaxes`` 110 the :class:`~matplotlib.axes.Axes` instance if mouse is over axes 111 112 ``xdata`` 113 x coord of mouse in data coords 114 115 ``ydata`` 116 y coord of mouse in data coords 117 118Let's look a simple example of a canvas, where a simple line segment 119is created every time a mouse is pressed:: 120 121 from matplotlib import pyplot as plt 122 123 class LineBuilder: 124 def __init__(self, line): 125 self.line = line 126 self.xs = list(line.get_xdata()) 127 self.ys = list(line.get_ydata()) 128 self.cid = line.figure.canvas.mpl_connect('button_press_event', self) 129 130 def __call__(self, event): 131 print('click', event) 132 if event.inaxes!=self.line.axes: return 133 self.xs.append(event.xdata) 134 self.ys.append(event.ydata) 135 self.line.set_data(self.xs, self.ys) 136 self.line.figure.canvas.draw() 137 138 fig = plt.figure() 139 ax = fig.add_subplot(111) 140 ax.set_title('click to build line segments') 141 line, = ax.plot([0], [0]) # empty line 142 linebuilder = LineBuilder(line) 143 144 plt.show() 145 146 147The :class:`~matplotlib.backend_bases.MouseEvent` that we just used is a 148:class:`~matplotlib.backend_bases.LocationEvent`, so we have access to 149the data and pixel coordinates in event.x and event.xdata. In 150addition to the ``LocationEvent`` attributes, it has 151 152 ``button`` 153 button pressed None, 1, 2, 3, 'up', 'down' (up and down are used for scroll events) 154 155 ``key`` 156 the key pressed: None, any character, 'shift', 'win', or 'control' 157 158Draggable rectangle exercise 159---------------------------- 160 161Write draggable rectangle class that is initialized with a 162:class:`~matplotlib.patches.Rectangle` instance but will move its x,y 163location when dragged. Hint: you will need to store the original 164``xy`` location of the rectangle which is stored as rect.xy and 165connect to the press, motion and release mouse events. When the mouse 166is pressed, check to see if the click occurs over your rectangle (see 167:meth:`matplotlib.patches.Rectangle.contains`) and if it does, store 168the rectangle xy and the location of the mouse click in data coords. 169In the motion event callback, compute the deltax and deltay of the 170mouse movement, and add those deltas to the origin of the rectangle 171you stored. The redraw the figure. On the button release event, just 172reset all the button press data you stored as None. 173 174Here is the solution:: 175 176 import numpy as np 177 import matplotlib.pyplot as plt 178 179 class DraggableRectangle: 180 def __init__(self, rect): 181 self.rect = rect 182 self.press = None 183 184 def connect(self): 185 'connect to all the events we need' 186 self.cidpress = self.rect.figure.canvas.mpl_connect( 187 'button_press_event', self.on_press) 188 self.cidrelease = self.rect.figure.canvas.mpl_connect( 189 'button_release_event', self.on_release) 190 self.cidmotion = self.rect.figure.canvas.mpl_connect( 191 'motion_notify_event', self.on_motion) 192 193 def on_press(self, event): 194 'on button press we will see if the mouse is over us and store some data' 195 if event.inaxes != self.rect.axes: return 196 197 contains, attrd = self.rect.contains(event) 198 if not contains: return 199 print('event contains', self.rect.xy) 200 x0, y0 = self.rect.xy 201 self.press = x0, y0, event.xdata, event.ydata 202 203 def on_motion(self, event): 204 'on motion we will move the rect if the mouse is over us' 205 if self.press is None: return 206 if event.inaxes != self.rect.axes: return 207 x0, y0, xpress, ypress = self.press 208 dx = event.xdata - xpress 209 dy = event.ydata - ypress 210 #print('x0=%f, xpress=%f, event.xdata=%f, dx=%f, x0+dx=%f' % 211 # (x0, xpress, event.xdata, dx, x0+dx)) 212 self.rect.set_x(x0+dx) 213 self.rect.set_y(y0+dy) 214 215 self.rect.figure.canvas.draw() 216 217 218 def on_release(self, event): 219 'on release we reset the press data' 220 self.press = None 221 self.rect.figure.canvas.draw() 222 223 def disconnect(self): 224 'disconnect all the stored connection ids' 225 self.rect.figure.canvas.mpl_disconnect(self.cidpress) 226 self.rect.figure.canvas.mpl_disconnect(self.cidrelease) 227 self.rect.figure.canvas.mpl_disconnect(self.cidmotion) 228 229 fig = plt.figure() 230 ax = fig.add_subplot(111) 231 rects = ax.bar(range(10), 20*np.random.rand(10)) 232 drs = [] 233 for rect in rects: 234 dr = DraggableRectangle(rect) 235 dr.connect() 236 drs.append(dr) 237 238 plt.show() 239 240 241**Extra credit**: use the animation blit techniques discussed in the 242`animations recipe 243<https://scipy-cookbook.readthedocs.io/items/Matplotlib_Animations.html>`_ to 244make the animated drawing faster and smoother. 245 246Extra credit solution:: 247 248 # draggable rectangle with the animation blit techniques; see 249 # http://www.scipy.org/Cookbook/Matplotlib/Animations 250 import numpy as np 251 import matplotlib.pyplot as plt 252 253 class DraggableRectangle: 254 lock = None # only one can be animated at a time 255 def __init__(self, rect): 256 self.rect = rect 257 self.press = None 258 self.background = None 259 260 def connect(self): 261 'connect to all the events we need' 262 self.cidpress = self.rect.figure.canvas.mpl_connect( 263 'button_press_event', self.on_press) 264 self.cidrelease = self.rect.figure.canvas.mpl_connect( 265 'button_release_event', self.on_release) 266 self.cidmotion = self.rect.figure.canvas.mpl_connect( 267 'motion_notify_event', self.on_motion) 268 269 def on_press(self, event): 270 'on button press we will see if the mouse is over us and store some data' 271 if event.inaxes != self.rect.axes: return 272 if DraggableRectangle.lock is not None: return 273 contains, attrd = self.rect.contains(event) 274 if not contains: return 275 print('event contains', self.rect.xy) 276 x0, y0 = self.rect.xy 277 self.press = x0, y0, event.xdata, event.ydata 278 DraggableRectangle.lock = self 279 280 # draw everything but the selected rectangle and store the pixel buffer 281 canvas = self.rect.figure.canvas 282 axes = self.rect.axes 283 self.rect.set_animated(True) 284 canvas.draw() 285 self.background = canvas.copy_from_bbox(self.rect.axes.bbox) 286 287 # now redraw just the rectangle 288 axes.draw_artist(self.rect) 289 290 # and blit just the redrawn area 291 canvas.blit(axes.bbox) 292 293 def on_motion(self, event): 294 'on motion we will move the rect if the mouse is over us' 295 if DraggableRectangle.lock is not self: 296 return 297 if event.inaxes != self.rect.axes: return 298 x0, y0, xpress, ypress = self.press 299 dx = event.xdata - xpress 300 dy = event.ydata - ypress 301 self.rect.set_x(x0+dx) 302 self.rect.set_y(y0+dy) 303 304 canvas = self.rect.figure.canvas 305 axes = self.rect.axes 306 # restore the background region 307 canvas.restore_region(self.background) 308 309 # redraw just the current rectangle 310 axes.draw_artist(self.rect) 311 312 # blit just the redrawn area 313 canvas.blit(axes.bbox) 314 315 def on_release(self, event): 316 'on release we reset the press data' 317 if DraggableRectangle.lock is not self: 318 return 319 320 self.press = None 321 DraggableRectangle.lock = None 322 323 # turn off the rect animation property and reset the background 324 self.rect.set_animated(False) 325 self.background = None 326 327 # redraw the full figure 328 self.rect.figure.canvas.draw() 329 330 def disconnect(self): 331 'disconnect all the stored connection ids' 332 self.rect.figure.canvas.mpl_disconnect(self.cidpress) 333 self.rect.figure.canvas.mpl_disconnect(self.cidrelease) 334 self.rect.figure.canvas.mpl_disconnect(self.cidmotion) 335 336 fig = plt.figure() 337 ax = fig.add_subplot(111) 338 rects = ax.bar(range(10), 20*np.random.rand(10)) 339 drs = [] 340 for rect in rects: 341 dr = DraggableRectangle(rect) 342 dr.connect() 343 drs.append(dr) 344 345 plt.show() 346 347 348.. _enter-leave-events: 349 350Mouse enter and leave 351====================== 352 353If you want to be notified when the mouse enters or leaves a figure or 354axes, you can connect to the figure/axes enter/leave events. Here is 355a simple example that changes the colors of the axes and figure 356background that the mouse is over:: 357 358 """ 359 Illustrate the figure and axes enter and leave events by changing the 360 frame colors on enter and leave 361 """ 362 import matplotlib.pyplot as plt 363 364 def enter_axes(event): 365 print('enter_axes', event.inaxes) 366 event.inaxes.patch.set_facecolor('yellow') 367 event.canvas.draw() 368 369 def leave_axes(event): 370 print('leave_axes', event.inaxes) 371 event.inaxes.patch.set_facecolor('white') 372 event.canvas.draw() 373 374 def enter_figure(event): 375 print('enter_figure', event.canvas.figure) 376 event.canvas.figure.patch.set_facecolor('red') 377 event.canvas.draw() 378 379 def leave_figure(event): 380 print('leave_figure', event.canvas.figure) 381 event.canvas.figure.patch.set_facecolor('grey') 382 event.canvas.draw() 383 384 fig1 = plt.figure() 385 fig1.suptitle('mouse hover over figure or axes to trigger events') 386 ax1 = fig1.add_subplot(211) 387 ax2 = fig1.add_subplot(212) 388 389 fig1.canvas.mpl_connect('figure_enter_event', enter_figure) 390 fig1.canvas.mpl_connect('figure_leave_event', leave_figure) 391 fig1.canvas.mpl_connect('axes_enter_event', enter_axes) 392 fig1.canvas.mpl_connect('axes_leave_event', leave_axes) 393 394 fig2 = plt.figure() 395 fig2.suptitle('mouse hover over figure or axes to trigger events') 396 ax1 = fig2.add_subplot(211) 397 ax2 = fig2.add_subplot(212) 398 399 fig2.canvas.mpl_connect('figure_enter_event', enter_figure) 400 fig2.canvas.mpl_connect('figure_leave_event', leave_figure) 401 fig2.canvas.mpl_connect('axes_enter_event', enter_axes) 402 fig2.canvas.mpl_connect('axes_leave_event', leave_axes) 403 404 plt.show() 405 406 407.. _object-picking: 408 409Object picking 410============== 411 412You can enable picking by setting the ``picker`` property of an 413:class:`~matplotlib.artist.Artist` (e.g., a matplotlib 414:class:`~matplotlib.lines.Line2D`, :class:`~matplotlib.text.Text`, 415:class:`~matplotlib.patches.Patch`, :class:`~matplotlib.patches.Polygon`, 416:class:`~matplotlib.patches.AxesImage`, etc...) 417 418There are a variety of meanings of the ``picker`` property: 419 420 ``None`` 421 picking is disabled for this artist (default) 422 423 ``boolean`` 424 if True then picking will be enabled and the artist will fire a 425 pick event if the mouse event is over the artist 426 427 ``float`` 428 if picker is a number it is interpreted as an epsilon tolerance in 429 points and the artist will fire off an event if its data is 430 within epsilon of the mouse event. For some artists like lines 431 and patch collections, the artist may provide additional data to 432 the pick event that is generated, e.g., the indices of the data 433 within epsilon of the pick event. 434 435 ``function`` 436 if picker is callable, it is a user supplied function which 437 determines whether the artist is hit by the mouse event. The 438 signature is ``hit, props = picker(artist, mouseevent)`` to 439 determine the hit test. If the mouse event is over the artist, 440 return ``hit=True`` and props is a dictionary of properties you 441 want added to the :class:`~matplotlib.backend_bases.PickEvent` 442 attributes 443 444 445After you have enabled an artist for picking by setting the ``picker`` 446property, you need to connect to the figure canvas pick_event to get 447pick callbacks on mouse press events. e.g.:: 448 449 def pick_handler(event): 450 mouseevent = event.mouseevent 451 artist = event.artist 452 # now do something with this... 453 454 455The :class:`~matplotlib.backend_bases.PickEvent` which is passed to 456your callback is always fired with two attributes: 457 458 ``mouseevent`` the mouse event that generate the pick event. The 459 mouse event in turn has attributes like ``x`` and ``y`` (the 460 coords in display space, e.g., pixels from left, bottom) and xdata, 461 ydata (the coords in data space). Additionally, you can get 462 information about which buttons were pressed, which keys were 463 pressed, which :class:`~matplotlib.axes.Axes` the mouse is over, 464 etc. See :class:`matplotlib.backend_bases.MouseEvent` for 465 details. 466 467 ``artist`` 468 the :class:`~matplotlib.artist.Artist` that generated the pick 469 event. 470 471Additionally, certain artists like :class:`~matplotlib.lines.Line2D` 472and :class:`~matplotlib.collections.PatchCollection` may attach 473additional meta data like the indices into the data that meet the 474picker criteria (e.g., all the points in the line that are within the 475specified epsilon tolerance) 476 477Simple picking example 478---------------------- 479 480In the example below, we set the line picker property to a scalar, so 481it represents a tolerance in points (72 points per inch). The onpick 482callback function will be called when the pick event it within the 483tolerance distance from the line, and has the indices of the data 484vertices that are within the pick distance tolerance. Our onpick 485callback function simply prints the data that are under the pick 486location. Different matplotlib Artists can attach different data to 487the PickEvent. For example, ``Line2D`` attaches the ind property, 488which are the indices into the line data under the pick point. See 489:meth:`~matplotlib.lines.Line2D.pick` for details on the ``PickEvent`` 490properties of the line. Here is the code:: 491 492 import numpy as np 493 import matplotlib.pyplot as plt 494 495 fig = plt.figure() 496 ax = fig.add_subplot(111) 497 ax.set_title('click on points') 498 499 line, = ax.plot(np.random.rand(100), 'o', picker=5) # 5 points tolerance 500 501 def onpick(event): 502 thisline = event.artist 503 xdata = thisline.get_xdata() 504 ydata = thisline.get_ydata() 505 ind = event.ind 506 points = tuple(zip(xdata[ind], ydata[ind])) 507 print('onpick points:', points) 508 509 fig.canvas.mpl_connect('pick_event', onpick) 510 511 plt.show() 512 513 514Picking exercise 515---------------- 516 517Create a data set of 100 arrays of 1000 Gaussian random numbers and 518compute the sample mean and standard deviation of each of them (hint: 519numpy arrays have a mean and std method) and make a xy marker plot of 520the 100 means vs the 100 standard deviations. Connect the line 521created by the plot command to the pick event, and plot the original 522time series of the data that generated the clicked on points. If more 523than one point is within the tolerance of the clicked on point, you 524can use multiple subplots to plot the multiple time series. 525 526Exercise solution:: 527 528 """ 529 compute the mean and stddev of 100 data sets and plot mean vs stddev. 530 When you click on one of the mu, sigma points, plot the raw data from 531 the dataset that generated the mean and stddev 532 """ 533 import numpy as np 534 import matplotlib.pyplot as plt 535 536 X = np.random.rand(100, 1000) 537 xs = np.mean(X, axis=1) 538 ys = np.std(X, axis=1) 539 540 fig = plt.figure() 541 ax = fig.add_subplot(111) 542 ax.set_title('click on point to plot time series') 543 line, = ax.plot(xs, ys, 'o', picker=5) # 5 points tolerance 544 545 546 def onpick(event): 547 548 if event.artist!=line: return True 549 550 N = len(event.ind) 551 if not N: return True 552 553 554 figi = plt.figure() 555 for subplotnum, dataind in enumerate(event.ind): 556 ax = figi.add_subplot(N,1,subplotnum+1) 557 ax.plot(X[dataind]) 558 ax.text(0.05, 0.9, 'mu=%1.3f\nsigma=%1.3f'%(xs[dataind], ys[dataind]), 559 transform=ax.transAxes, va='top') 560 ax.set_ylim(-0.5, 1.5) 561 figi.show() 562 return True 563 564 fig.canvas.mpl_connect('pick_event', onpick) 565 566 plt.show() 567