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