Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/matplotlib/widgets.py : 13%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2GUI neutral widgets
3===================
5Widgets that are designed to work for any of the GUI backends.
6All of these widgets require you to predefine a `matplotlib.axes.Axes`
7instance and pass that as the first parameter. Matplotlib doesn't try to
8be too smart with respect to layout -- you will have to figure out how
9wide and tall you want your Axes to be to accommodate your widget.
10"""
12from contextlib import ExitStack
13import copy
14from numbers import Integral
16import numpy as np
18from . import cbook, rcParams
19from .lines import Line2D
20from .patches import Circle, Rectangle, Ellipse
21from .transforms import blended_transform_factory
24class LockDraw:
25 """
26 Some widgets, like the cursor, draw onto the canvas, and this is not
27 desirable under all circumstances, like when the toolbar is in zoom-to-rect
28 mode and drawing a rectangle. To avoid this, a widget can acquire a
29 canvas' lock with ``canvas.widgetlock(widget)`` before drawing on the
30 canvas; this will prevent other widgets from doing so at the same time (if
31 they also try to acquire the lock first).
32 """
34 def __init__(self):
35 self._owner = None
37 def __call__(self, o):
38 """Reserve the lock for *o*."""
39 if not self.available(o):
40 raise ValueError('already locked')
41 self._owner = o
43 def release(self, o):
44 """Release the lock from *o*."""
45 if not self.available(o):
46 raise ValueError('you do not own this lock')
47 self._owner = None
49 def available(self, o):
50 """Return whether drawing is available to *o*."""
51 return not self.locked() or self.isowner(o)
53 def isowner(self, o):
54 """Return whether *o* owns this lock."""
55 return self._owner is o
57 def locked(self):
58 """Return whether the lock is currently held by an owner."""
59 return self._owner is not None
62class Widget:
63 """
64 Abstract base class for GUI neutral widgets
65 """
66 drawon = True
67 eventson = True
68 _active = True
70 def set_active(self, active):
71 """Set whether the widget is active.
72 """
73 self._active = active
75 def get_active(self):
76 """Get whether the widget is active.
77 """
78 return self._active
80 # set_active is overridden by SelectorWidgets.
81 active = property(get_active, set_active, doc="Is the widget active?")
83 def ignore(self, event):
84 """
85 Return whether *event* should be ignored.
87 This method should be called at the beginning of any event callback.
88 """
89 return not self.active
92class AxesWidget(Widget):
93 """
94 Widget that is connected to a single `~matplotlib.axes.Axes`.
96 To guarantee that the widget remains responsive and not garbage-collected,
97 a reference to the object should be maintained by the user.
99 This is necessary because the callback registry
100 maintains only weak-refs to the functions, which are member
101 functions of the widget. If there are no references to the widget
102 object it may be garbage collected which will disconnect the callbacks.
104 Attributes
105 ----------
106 ax : `~matplotlib.axes.Axes`
107 The parent axes for the widget.
108 canvas : `~matplotlib.backend_bases.FigureCanvasBase` subclass
109 The parent figure canvas for the widget.
110 active : bool
111 If False, the widget does not respond to events.
112 """
113 def __init__(self, ax):
114 self.ax = ax
115 self.canvas = ax.figure.canvas
116 self.cids = []
118 def connect_event(self, event, callback):
119 """
120 Connect callback with an event.
122 This should be used in lieu of `figure.canvas.mpl_connect` since this
123 function stores callback ids for later clean up.
124 """
125 cid = self.canvas.mpl_connect(event, callback)
126 self.cids.append(cid)
128 def disconnect_events(self):
129 """Disconnect all events created by this widget."""
130 for c in self.cids:
131 self.canvas.mpl_disconnect(c)
134class Button(AxesWidget):
135 """
136 A GUI neutral button.
138 For the button to remain responsive you must keep a reference to it.
139 Call `.on_clicked` to connect to the button.
141 Attributes
142 ----------
143 ax
144 The `matplotlib.axes.Axes` the button renders into.
145 label
146 A `matplotlib.text.Text` instance.
147 color
148 The color of the button when not hovering.
149 hovercolor
150 The color of the button when hovering.
151 """
153 def __init__(self, ax, label, image=None,
154 color='0.85', hovercolor='0.95'):
155 """
156 Parameters
157 ----------
158 ax : `~matplotlib.axes.Axes`
159 The `~.axes.Axes` instance the button will be placed into.
160 label : str
161 The button text. Accepts string.
162 image : array-like or PIL image
163 The image to place in the button, if not *None*.
164 Supported inputs are the same as for `.Axes.imshow`.
165 color : color
166 The color of the button when not activated.
167 hovercolor : color
168 The color of the button when the mouse is over it.
169 """
170 AxesWidget.__init__(self, ax)
172 if image is not None:
173 ax.imshow(image)
174 self.label = ax.text(0.5, 0.5, label,
175 verticalalignment='center',
176 horizontalalignment='center',
177 transform=ax.transAxes)
179 self.cnt = 0
180 self.observers = {}
182 self.connect_event('button_press_event', self._click)
183 self.connect_event('button_release_event', self._release)
184 self.connect_event('motion_notify_event', self._motion)
185 ax.set_navigate(False)
186 ax.set_facecolor(color)
187 ax.set_xticks([])
188 ax.set_yticks([])
189 self.color = color
190 self.hovercolor = hovercolor
192 self._lastcolor = color
194 def _click(self, event):
195 if (self.ignore(event)
196 or event.inaxes != self.ax
197 or not self.eventson):
198 return
199 if event.canvas.mouse_grabber != self.ax:
200 event.canvas.grab_mouse(self.ax)
202 def _release(self, event):
203 if (self.ignore(event)
204 or event.canvas.mouse_grabber != self.ax):
205 return
206 event.canvas.release_mouse(self.ax)
207 if (not self.eventson
208 or event.inaxes != self.ax):
209 return
210 for cid, func in self.observers.items():
211 func(event)
213 def _motion(self, event):
214 if self.ignore(event):
215 return
216 if event.inaxes == self.ax:
217 c = self.hovercolor
218 else:
219 c = self.color
220 if c != self._lastcolor:
221 self.ax.set_facecolor(c)
222 self._lastcolor = c
223 if self.drawon:
224 self.ax.figure.canvas.draw()
226 def on_clicked(self, func):
227 """
228 Connect the callback function *func* to button click events.
230 Returns a connection id, which can be used to disconnect the callback.
231 """
232 cid = self.cnt
233 self.observers[cid] = func
234 self.cnt += 1
235 return cid
237 def disconnect(self, cid):
238 """Remove the callback function with connection id *cid*."""
239 try:
240 del self.observers[cid]
241 except KeyError:
242 pass
245class Slider(AxesWidget):
246 """
247 A slider representing a floating point range.
249 Create a slider from *valmin* to *valmax* in axes *ax*. For the slider to
250 remain responsive you must maintain a reference to it. Call
251 :meth:`on_changed` to connect to the slider event.
253 Attributes
254 ----------
255 val : float
256 Slider value.
257 """
258 def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt='%1.2f',
259 closedmin=True, closedmax=True, slidermin=None,
260 slidermax=None, dragging=True, valstep=None,
261 orientation='horizontal', **kwargs):
262 """
263 Parameters
264 ----------
265 ax : Axes
266 The Axes to put the slider in.
268 label : str
269 Slider label.
271 valmin : float
272 The minimum value of the slider.
274 valmax : float
275 The maximum value of the slider.
277 valinit : float, optional, default: 0.5
278 The slider initial position.
280 valfmt : str, optional, default: "%1.2f"
281 Used to format the slider value, fprint format string.
283 closedmin : bool, optional, default: True
284 Whether the slider interval is closed on the bottom.
286 closedmax : bool, optional, default: True
287 Whether the slider interval is closed on the top.
289 slidermin : Slider, optional, default: None
290 Do not allow the current slider to have a value less than
291 the value of the Slider `slidermin`.
293 slidermax : Slider, optional, default: None
294 Do not allow the current slider to have a value greater than
295 the value of the Slider `slidermax`.
297 dragging : bool, optional, default: True
298 If True the slider can be dragged by the mouse.
300 valstep : float, optional, default: None
301 If given, the slider will snap to multiples of `valstep`.
303 orientation : {'horizontal', 'vertical'}, default: 'horizontal'
304 The orientation of the slider.
306 Notes
307 -----
308 Additional kwargs are passed on to ``self.poly`` which is the
309 `~matplotlib.patches.Rectangle` that draws the slider knob. See the
310 `.Rectangle` documentation for valid property names (``facecolor``,
311 ``edgecolor``, ``alpha``, etc.).
312 """
313 if ax.name == '3d':
314 raise ValueError('Sliders cannot be added to 3D Axes')
316 AxesWidget.__init__(self, ax)
318 if slidermin is not None and not hasattr(slidermin, 'val'):
319 raise ValueError("Argument slidermin ({}) has no 'val'"
320 .format(type(slidermin)))
321 if slidermax is not None and not hasattr(slidermax, 'val'):
322 raise ValueError("Argument slidermax ({}) has no 'val'"
323 .format(type(slidermax)))
324 if orientation not in ['horizontal', 'vertical']:
325 raise ValueError("Argument orientation ({}) must be either"
326 "'horizontal' or 'vertical'".format(orientation))
328 self.orientation = orientation
329 self.closedmin = closedmin
330 self.closedmax = closedmax
331 self.slidermin = slidermin
332 self.slidermax = slidermax
333 self.drag_active = False
334 self.valmin = valmin
335 self.valmax = valmax
336 self.valstep = valstep
337 valinit = self._value_in_bounds(valinit)
338 if valinit is None:
339 valinit = valmin
340 self.val = valinit
341 self.valinit = valinit
342 if orientation == 'vertical':
343 self.poly = ax.axhspan(valmin, valinit, 0, 1, **kwargs)
344 self.hline = ax.axhline(valinit, 0, 1, color='r', lw=1)
345 else:
346 self.poly = ax.axvspan(valmin, valinit, 0, 1, **kwargs)
347 self.vline = ax.axvline(valinit, 0, 1, color='r', lw=1)
349 self.valfmt = valfmt
350 ax.set_yticks([])
351 if orientation == 'vertical':
352 ax.set_ylim((valmin, valmax))
353 else:
354 ax.set_xlim((valmin, valmax))
355 ax.set_xticks([])
356 ax.set_navigate(False)
358 self.connect_event('button_press_event', self._update)
359 self.connect_event('button_release_event', self._update)
360 if dragging:
361 self.connect_event('motion_notify_event', self._update)
362 if orientation == 'vertical':
363 self.label = ax.text(0.5, 1.02, label, transform=ax.transAxes,
364 verticalalignment='bottom',
365 horizontalalignment='center')
367 self.valtext = ax.text(0.5, -0.02, valfmt % valinit,
368 transform=ax.transAxes,
369 verticalalignment='top',
370 horizontalalignment='center')
371 else:
372 self.label = ax.text(-0.02, 0.5, label, transform=ax.transAxes,
373 verticalalignment='center',
374 horizontalalignment='right')
376 self.valtext = ax.text(1.02, 0.5, valfmt % valinit,
377 transform=ax.transAxes,
378 verticalalignment='center',
379 horizontalalignment='left')
381 self.cnt = 0
382 self.observers = {}
384 self.set_val(valinit)
386 def _value_in_bounds(self, val):
387 """Makes sure *val* is with given bounds."""
388 if self.valstep:
389 val = (self.valmin
390 + round((val - self.valmin) / self.valstep) * self.valstep)
392 if val <= self.valmin:
393 if not self.closedmin:
394 return
395 val = self.valmin
396 elif val >= self.valmax:
397 if not self.closedmax:
398 return
399 val = self.valmax
401 if self.slidermin is not None and val <= self.slidermin.val:
402 if not self.closedmin:
403 return
404 val = self.slidermin.val
406 if self.slidermax is not None and val >= self.slidermax.val:
407 if not self.closedmax:
408 return
409 val = self.slidermax.val
410 return val
412 def _update(self, event):
413 """Update the slider position."""
414 if self.ignore(event) or event.button != 1:
415 return
417 if event.name == 'button_press_event' and event.inaxes == self.ax:
418 self.drag_active = True
419 event.canvas.grab_mouse(self.ax)
421 if not self.drag_active:
422 return
424 elif ((event.name == 'button_release_event') or
425 (event.name == 'button_press_event' and
426 event.inaxes != self.ax)):
427 self.drag_active = False
428 event.canvas.release_mouse(self.ax)
429 return
430 if self.orientation == 'vertical':
431 val = self._value_in_bounds(event.ydata)
432 else:
433 val = self._value_in_bounds(event.xdata)
434 if val not in [None, self.val]:
435 self.set_val(val)
437 def set_val(self, val):
438 """
439 Set slider value to *val*
441 Parameters
442 ----------
443 val : float
444 """
445 xy = self.poly.xy
446 if self.orientation == 'vertical':
447 xy[1] = 0, val
448 xy[2] = 1, val
449 else:
450 xy[2] = val, 1
451 xy[3] = val, 0
452 self.poly.xy = xy
453 self.valtext.set_text(self.valfmt % val)
454 if self.drawon:
455 self.ax.figure.canvas.draw_idle()
456 self.val = val
457 if not self.eventson:
458 return
459 for cid, func in self.observers.items():
460 func(val)
462 def on_changed(self, func):
463 """
464 When the slider value is changed call *func* with the new
465 slider value
467 Parameters
468 ----------
469 func : callable
470 Function to call when slider is changed.
471 The function must accept a single float as its arguments.
473 Returns
474 -------
475 cid : int
476 Connection id (which can be used to disconnect *func*)
477 """
478 cid = self.cnt
479 self.observers[cid] = func
480 self.cnt += 1
481 return cid
483 def disconnect(self, cid):
484 """
485 Remove the observer with connection id *cid*
487 Parameters
488 ----------
489 cid : int
490 Connection id of the observer to be removed
491 """
492 try:
493 del self.observers[cid]
494 except KeyError:
495 pass
497 def reset(self):
498 """Reset the slider to the initial value"""
499 if self.val != self.valinit:
500 self.set_val(self.valinit)
503class CheckButtons(AxesWidget):
504 r"""
505 A GUI neutral set of check buttons.
507 For the check buttons to remain responsive you must keep a
508 reference to this object.
510 Connect to the CheckButtons with the :meth:`on_clicked` method
512 Attributes
513 ----------
514 ax
515 The `matplotlib.axes.Axes` the button are located in.
516 labels
517 A list of `matplotlib.text.Text`\ s.
518 lines
519 List of (line1, line2) tuples for the x's in the check boxes.
520 These lines exist for each box, but have ``set_visible(False)``
521 when its box is not checked.
522 rectangles
523 A list of `matplotlib.patches.Rectangle`\ s.
524 """
526 def __init__(self, ax, labels, actives=None):
527 """
528 Add check buttons to `matplotlib.axes.Axes` instance *ax*
530 Parameters
531 ----------
532 ax : `~matplotlib.axes.Axes`
533 The parent axes for the widget.
535 labels : list of str
536 The labels of the check buttons.
538 actives : list of bool, optional
539 The initial check states of the buttons. The list must have the
540 same length as *labels*. If not given, all buttons are unchecked.
541 """
542 AxesWidget.__init__(self, ax)
544 ax.set_xticks([])
545 ax.set_yticks([])
546 ax.set_navigate(False)
548 if actives is None:
549 actives = [False] * len(labels)
551 if len(labels) > 1:
552 dy = 1. / (len(labels) + 1)
553 ys = np.linspace(1 - dy, dy, len(labels))
554 else:
555 dy = 0.25
556 ys = [0.5]
558 axcolor = ax.get_facecolor()
560 self.labels = []
561 self.lines = []
562 self.rectangles = []
564 lineparams = {'color': 'k', 'linewidth': 1.25,
565 'transform': ax.transAxes, 'solid_capstyle': 'butt'}
566 for y, label, active in zip(ys, labels, actives):
567 t = ax.text(0.25, y, label, transform=ax.transAxes,
568 horizontalalignment='left',
569 verticalalignment='center')
571 w, h = dy / 2, dy / 2
572 x, y = 0.05, y - h / 2
574 p = Rectangle(xy=(x, y), width=w, height=h, edgecolor='black',
575 facecolor=axcolor, transform=ax.transAxes)
577 l1 = Line2D([x, x + w], [y + h, y], **lineparams)
578 l2 = Line2D([x, x + w], [y, y + h], **lineparams)
580 l1.set_visible(active)
581 l2.set_visible(active)
582 self.labels.append(t)
583 self.rectangles.append(p)
584 self.lines.append((l1, l2))
585 ax.add_patch(p)
586 ax.add_line(l1)
587 ax.add_line(l2)
589 self.connect_event('button_press_event', self._clicked)
591 self.cnt = 0
592 self.observers = {}
594 def _clicked(self, event):
595 if self.ignore(event) or event.button != 1 or event.inaxes != self.ax:
596 return
597 for i, (p, t) in enumerate(zip(self.rectangles, self.labels)):
598 if (t.get_window_extent().contains(event.x, event.y) or
599 p.get_window_extent().contains(event.x, event.y)):
600 self.set_active(i)
601 break
603 def set_active(self, index):
604 """
605 Directly (de)activate a check button by index.
607 *index* is an index into the original label list
608 that this object was constructed with.
609 Raises ValueError if *index* is invalid.
611 Callbacks will be triggered if :attr:`eventson` is True.
612 """
613 if not 0 <= index < len(self.labels):
614 raise ValueError("Invalid CheckButton index: %d" % index)
616 l1, l2 = self.lines[index]
617 l1.set_visible(not l1.get_visible())
618 l2.set_visible(not l2.get_visible())
620 if self.drawon:
621 self.ax.figure.canvas.draw()
623 if not self.eventson:
624 return
625 for cid, func in self.observers.items():
626 func(self.labels[index].get_text())
628 def get_status(self):
629 """
630 Return a tuple of the status (True/False) of all of the check buttons.
631 """
632 return [l1.get_visible() for (l1, l2) in self.lines]
634 def on_clicked(self, func):
635 """
636 Connect the callback function *func* to button click events.
638 Returns a connection id, which can be used to disconnect the callback.
639 """
640 cid = self.cnt
641 self.observers[cid] = func
642 self.cnt += 1
643 return cid
645 def disconnect(self, cid):
646 """remove the observer with connection id *cid*"""
647 try:
648 del self.observers[cid]
649 except KeyError:
650 pass
653class TextBox(AxesWidget):
654 """
655 A GUI neutral text input box.
657 For the text box to remain responsive you must keep a reference to it.
659 Call :meth:`on_text_change` to be updated whenever the text changes.
661 Call :meth:`on_submit` to be updated whenever the user hits enter or
662 leaves the text entry field.
664 Attributes
665 ----------
666 ax
667 The `matplotlib.axes.Axes` the button renders into.
668 label
669 A `matplotlib.text.Text` instance.
670 color
671 The color of the button when not hovering.
672 hovercolor
673 The color of the button when hovering.
674 """
676 def __init__(self, ax, label, initial='',
677 color='.95', hovercolor='1', label_pad=.01):
678 """
679 Parameters
680 ----------
681 ax : `~matplotlib.axes.Axes`
682 The `~.axes.Axes` instance the button will be placed into.
683 label : str
684 Label for this text box.
685 initial : str
686 Initial value in the text box.
687 color : color
688 The color of the box.
689 hovercolor : color
690 The color of the box when the mouse is over it.
691 label_pad : float
692 The distance between the label and the right side of the textbox.
693 """
694 AxesWidget.__init__(self, ax)
696 self.DIST_FROM_LEFT = .05
698 self.params_to_disable = [key for key in rcParams if 'keymap' in key]
700 self.text = initial
701 self.label = ax.text(-label_pad, 0.5, label,
702 verticalalignment='center',
703 horizontalalignment='right',
704 transform=ax.transAxes)
705 self.text_disp = self._make_text_disp(self.text)
707 self.cnt = 0
708 self.change_observers = {}
709 self.submit_observers = {}
711 # If these lines are removed, the cursor won't appear the first
712 # time the box is clicked:
713 self.ax.set_xlim(0, 1)
714 self.ax.set_ylim(0, 1)
716 self.cursor_index = 0
718 # Because this is initialized, _render_cursor
719 # can assume that cursor exists.
720 self.cursor = self.ax.vlines(0, 0, 0)
721 self.cursor.set_visible(False)
723 self.connect_event('button_press_event', self._click)
724 self.connect_event('button_release_event', self._release)
725 self.connect_event('motion_notify_event', self._motion)
726 self.connect_event('key_press_event', self._keypress)
727 self.connect_event('resize_event', self._resize)
728 ax.set_navigate(False)
729 ax.set_facecolor(color)
730 ax.set_xticks([])
731 ax.set_yticks([])
732 self.color = color
733 self.hovercolor = hovercolor
735 self._lastcolor = color
737 self.capturekeystrokes = False
739 def _make_text_disp(self, string):
740 return self.ax.text(self.DIST_FROM_LEFT, 0.5, string,
741 verticalalignment='center',
742 horizontalalignment='left',
743 transform=self.ax.transAxes)
745 def _rendercursor(self):
746 # this is a hack to figure out where the cursor should go.
747 # we draw the text up to where the cursor should go, measure
748 # and save its dimensions, draw the real text, then put the cursor
749 # at the saved dimensions
751 widthtext = self.text[:self.cursor_index]
752 no_text = False
753 if widthtext in ["", " ", " "]:
754 no_text = widthtext == ""
755 widthtext = ","
757 wt_disp = self._make_text_disp(widthtext)
759 self.ax.figure.canvas.draw()
760 bb = wt_disp.get_window_extent()
761 inv = self.ax.transData.inverted()
762 bb = inv.transform(bb)
763 wt_disp.set_visible(False)
764 if no_text:
765 bb[1, 0] = bb[0, 0]
766 # hack done
767 self.cursor.set_visible(False)
769 self.cursor = self.ax.vlines(bb[1, 0], bb[0, 1], bb[1, 1])
770 self.ax.figure.canvas.draw()
772 def _notify_submit_observers(self):
773 if self.eventson:
774 for cid, func in self.submit_observers.items():
775 func(self.text)
777 def _release(self, event):
778 if self.ignore(event):
779 return
780 if event.canvas.mouse_grabber != self.ax:
781 return
782 event.canvas.release_mouse(self.ax)
784 def _keypress(self, event):
785 if self.ignore(event):
786 return
787 if self.capturekeystrokes:
788 key = event.key
790 if len(key) == 1:
791 self.text = (self.text[:self.cursor_index] + key +
792 self.text[self.cursor_index:])
793 self.cursor_index += 1
794 elif key == "right":
795 if self.cursor_index != len(self.text):
796 self.cursor_index += 1
797 elif key == "left":
798 if self.cursor_index != 0:
799 self.cursor_index -= 1
800 elif key == "home":
801 self.cursor_index = 0
802 elif key == "end":
803 self.cursor_index = len(self.text)
804 elif key == "backspace":
805 if self.cursor_index != 0:
806 self.text = (self.text[:self.cursor_index - 1] +
807 self.text[self.cursor_index:])
808 self.cursor_index -= 1
809 elif key == "delete":
810 if self.cursor_index != len(self.text):
811 self.text = (self.text[:self.cursor_index] +
812 self.text[self.cursor_index + 1:])
814 self.text_disp.remove()
815 self.text_disp = self._make_text_disp(self.text)
816 self._rendercursor()
817 self._notify_change_observers()
818 if key == "enter":
819 self._notify_submit_observers()
821 def set_val(self, val):
822 newval = str(val)
823 if self.text == newval:
824 return
825 self.text = newval
826 self.text_disp.remove()
827 self.text_disp = self._make_text_disp(self.text)
828 self._rendercursor()
829 self._notify_change_observers()
830 self._notify_submit_observers()
832 def _notify_change_observers(self):
833 if self.eventson:
834 for cid, func in self.change_observers.items():
835 func(self.text)
837 def begin_typing(self, x):
838 self.capturekeystrokes = True
839 # Check for toolmanager handling the keypress
840 if self.ax.figure.canvas.manager.key_press_handler_id is not None:
841 # disable command keys so that the user can type without
842 # command keys causing figure to be saved, etc
843 self.reset_params = {}
844 for key in self.params_to_disable:
845 self.reset_params[key] = rcParams[key]
846 rcParams[key] = []
847 else:
848 self.ax.figure.canvas.manager.toolmanager.keypresslock(self)
850 def stop_typing(self):
851 notifysubmit = False
852 # Because _notify_submit_users might throw an error in the user's code,
853 # we only want to call it once we've already done our cleanup.
854 if self.capturekeystrokes:
855 # Check for toolmanager handling the keypress
856 if self.ax.figure.canvas.manager.key_press_handler_id is not None:
857 # since the user is no longer typing,
858 # reactivate the standard command keys
859 for key in self.params_to_disable:
860 rcParams[key] = self.reset_params[key]
861 else:
862 toolmanager = self.ax.figure.canvas.manager.toolmanager
863 toolmanager.keypresslock.release(self)
864 notifysubmit = True
865 self.capturekeystrokes = False
866 self.cursor.set_visible(False)
867 self.ax.figure.canvas.draw()
868 if notifysubmit:
869 self._notify_submit_observers()
871 def position_cursor(self, x):
872 # now, we have to figure out where the cursor goes.
873 # approximate it based on assuming all characters the same length
874 if len(self.text) == 0:
875 self.cursor_index = 0
876 else:
877 bb = self.text_disp.get_window_extent()
879 trans = self.ax.transData
880 inv = self.ax.transData.inverted()
881 bb = trans.transform(inv.transform(bb))
883 text_start = bb[0, 0]
884 text_end = bb[1, 0]
886 ratio = (x - text_start) / (text_end - text_start)
888 if ratio < 0:
889 ratio = 0
890 if ratio > 1:
891 ratio = 1
893 self.cursor_index = int(len(self.text) * ratio)
895 self._rendercursor()
897 def _click(self, event):
898 if self.ignore(event):
899 return
900 if event.inaxes != self.ax:
901 self.stop_typing()
902 return
903 if not self.eventson:
904 return
905 if event.canvas.mouse_grabber != self.ax:
906 event.canvas.grab_mouse(self.ax)
907 if not self.capturekeystrokes:
908 self.begin_typing(event.x)
909 self.position_cursor(event.x)
911 def _resize(self, event):
912 self.stop_typing()
914 def _motion(self, event):
915 if self.ignore(event):
916 return
917 if event.inaxes == self.ax:
918 c = self.hovercolor
919 else:
920 c = self.color
921 if c != self._lastcolor:
922 self.ax.set_facecolor(c)
923 self._lastcolor = c
924 if self.drawon:
925 self.ax.figure.canvas.draw()
927 def on_text_change(self, func):
928 """
929 When the text changes, call this *func* with event.
931 A connection id is returned which can be used to disconnect.
932 """
933 cid = self.cnt
934 self.change_observers[cid] = func
935 self.cnt += 1
936 return cid
938 def on_submit(self, func):
939 """
940 When the user hits enter or leaves the submission box, call this
941 *func* with event.
943 A connection id is returned which can be used to disconnect.
944 """
945 cid = self.cnt
946 self.submit_observers[cid] = func
947 self.cnt += 1
948 return cid
950 def disconnect(self, cid):
951 """Remove the observer with connection id *cid*."""
952 for reg in [self.change_observers, self.submit_observers]:
953 try:
954 del reg[cid]
955 except KeyError:
956 pass
959class RadioButtons(AxesWidget):
960 """
961 A GUI neutral radio button.
963 For the buttons to remain responsive you must keep a reference to this
964 object.
966 Connect to the RadioButtons with the :meth:`on_clicked` method.
968 Attributes
969 ----------
970 ax
971 The containing `~.axes.Axes` instance.
972 activecolor
973 The color of the selected button.
974 labels
975 A list of `~.text.Text` instances containing the button labels.
976 circles
977 A list of `~.patches.Circle` instances defining the buttons.
978 value_selected : str
979 The label text of the currently selected button.
980 """
982 def __init__(self, ax, labels, active=0, activecolor='blue'):
983 """
984 Add radio buttons to an `~.axes.Axes`.
986 Parameters
987 ----------
988 ax : `~matplotlib.axes.Axes`
989 The axes to add the buttons to.
990 labels : list of str
991 The button labels.
992 active : int
993 The index of the initially selected button.
994 activecolor : color
995 The color of the selected button.
996 """
997 AxesWidget.__init__(self, ax)
998 self.activecolor = activecolor
999 self.value_selected = None
1001 ax.set_xticks([])
1002 ax.set_yticks([])
1003 ax.set_navigate(False)
1004 dy = 1. / (len(labels) + 1)
1005 ys = np.linspace(1 - dy, dy, len(labels))
1006 cnt = 0
1007 axcolor = ax.get_facecolor()
1009 # scale the radius of the circle with the spacing between each one
1010 circle_radius = dy / 2 - 0.01
1011 # default to hard-coded value if the radius becomes too large
1012 circle_radius = min(circle_radius, 0.05)
1014 self.labels = []
1015 self.circles = []
1016 for y, label in zip(ys, labels):
1017 t = ax.text(0.25, y, label, transform=ax.transAxes,
1018 horizontalalignment='left',
1019 verticalalignment='center')
1021 if cnt == active:
1022 self.value_selected = label
1023 facecolor = activecolor
1024 else:
1025 facecolor = axcolor
1027 p = Circle(xy=(0.15, y), radius=circle_radius, edgecolor='black',
1028 facecolor=facecolor, transform=ax.transAxes)
1030 self.labels.append(t)
1031 self.circles.append(p)
1032 ax.add_patch(p)
1033 cnt += 1
1035 self.connect_event('button_press_event', self._clicked)
1037 self.cnt = 0
1038 self.observers = {}
1040 def _clicked(self, event):
1041 if self.ignore(event) or event.button != 1 or event.inaxes != self.ax:
1042 return
1043 pclicked = self.ax.transAxes.inverted().transform((event.x, event.y))
1044 distances = {}
1045 for i, (p, t) in enumerate(zip(self.circles, self.labels)):
1046 if (t.get_window_extent().contains(event.x, event.y)
1047 or np.linalg.norm(pclicked - p.center) < p.radius):
1048 distances[i] = np.linalg.norm(pclicked - p.center)
1049 if len(distances) > 0:
1050 closest = min(distances, key=distances.get)
1051 self.set_active(closest)
1053 def set_active(self, index):
1054 """
1055 Select button with number *index*.
1057 Callbacks will be triggered if :attr:`eventson` is True.
1058 """
1059 if 0 > index >= len(self.labels):
1060 raise ValueError("Invalid RadioButton index: %d" % index)
1062 self.value_selected = self.labels[index].get_text()
1064 for i, p in enumerate(self.circles):
1065 if i == index:
1066 color = self.activecolor
1067 else:
1068 color = self.ax.get_facecolor()
1069 p.set_facecolor(color)
1071 if self.drawon:
1072 self.ax.figure.canvas.draw()
1074 if not self.eventson:
1075 return
1076 for cid, func in self.observers.items():
1077 func(self.labels[index].get_text())
1079 def on_clicked(self, func):
1080 """
1081 Connect the callback function *func* to button click events.
1083 Returns a connection id, which can be used to disconnect the callback.
1084 """
1085 cid = self.cnt
1086 self.observers[cid] = func
1087 self.cnt += 1
1088 return cid
1090 def disconnect(self, cid):
1091 """Remove the observer with connection id *cid*."""
1092 try:
1093 del self.observers[cid]
1094 except KeyError:
1095 pass
1098class SubplotTool(Widget):
1099 """
1100 A tool to adjust the subplot params of a `matplotlib.figure.Figure`.
1101 """
1103 def __init__(self, targetfig, toolfig):
1104 """
1105 Parameters
1106 ----------
1107 targetfig : `.Figure`
1108 The figure instance to adjust.
1109 toolfig : `.Figure`
1110 The figure instance to embed the subplot tool into.
1111 """
1113 self.targetfig = targetfig
1114 toolfig.subplots_adjust(left=0.2, right=0.9)
1116 self.axleft = toolfig.add_subplot(711)
1117 self.axleft.set_title('Click on slider to adjust subplot param')
1118 self.axleft.set_navigate(False)
1120 self.sliderleft = Slider(self.axleft, 'left',
1121 0, 1, targetfig.subplotpars.left,
1122 closedmax=False)
1123 self.sliderleft.on_changed(self.funcleft)
1125 self.axbottom = toolfig.add_subplot(712)
1126 self.axbottom.set_navigate(False)
1127 self.sliderbottom = Slider(self.axbottom,
1128 'bottom', 0, 1,
1129 targetfig.subplotpars.bottom,
1130 closedmax=False)
1131 self.sliderbottom.on_changed(self.funcbottom)
1133 self.axright = toolfig.add_subplot(713)
1134 self.axright.set_navigate(False)
1135 self.sliderright = Slider(self.axright, 'right', 0, 1,
1136 targetfig.subplotpars.right,
1137 closedmin=False)
1138 self.sliderright.on_changed(self.funcright)
1140 self.axtop = toolfig.add_subplot(714)
1141 self.axtop.set_navigate(False)
1142 self.slidertop = Slider(self.axtop, 'top', 0, 1,
1143 targetfig.subplotpars.top,
1144 closedmin=False)
1145 self.slidertop.on_changed(self.functop)
1147 self.axwspace = toolfig.add_subplot(715)
1148 self.axwspace.set_navigate(False)
1149 self.sliderwspace = Slider(self.axwspace, 'wspace',
1150 0, 1, targetfig.subplotpars.wspace,
1151 closedmax=False)
1152 self.sliderwspace.on_changed(self.funcwspace)
1154 self.axhspace = toolfig.add_subplot(716)
1155 self.axhspace.set_navigate(False)
1156 self.sliderhspace = Slider(self.axhspace, 'hspace',
1157 0, 1, targetfig.subplotpars.hspace,
1158 closedmax=False)
1159 self.sliderhspace.on_changed(self.funchspace)
1161 # constraints
1162 self.sliderleft.slidermax = self.sliderright
1163 self.sliderright.slidermin = self.sliderleft
1164 self.sliderbottom.slidermax = self.slidertop
1165 self.slidertop.slidermin = self.sliderbottom
1167 bax = toolfig.add_axes([0.8, 0.05, 0.15, 0.075])
1168 self.buttonreset = Button(bax, 'Reset')
1170 sliders = (self.sliderleft, self.sliderbottom, self.sliderright,
1171 self.slidertop, self.sliderwspace, self.sliderhspace,)
1173 def func(event):
1174 with ExitStack() as stack:
1175 # Temporarily disable drawing on self and self's sliders.
1176 stack.enter_context(cbook._setattr_cm(self, drawon=False))
1177 for slider in sliders:
1178 stack.enter_context(
1179 cbook._setattr_cm(slider, drawon=False))
1180 # Reset the slider to the initial position.
1181 for slider in sliders:
1182 slider.reset()
1183 # Draw the canvas.
1184 if self.drawon:
1185 toolfig.canvas.draw()
1186 self.targetfig.canvas.draw()
1188 # during reset there can be a temporary invalid state
1189 # depending on the order of the reset so we turn off
1190 # validation for the resetting
1191 validate = toolfig.subplotpars.validate
1192 toolfig.subplotpars.validate = False
1193 self.buttonreset.on_clicked(func)
1194 toolfig.subplotpars.validate = validate
1196 def funcleft(self, val):
1197 self.targetfig.subplots_adjust(left=val)
1198 if self.drawon:
1199 self.targetfig.canvas.draw()
1201 def funcright(self, val):
1202 self.targetfig.subplots_adjust(right=val)
1203 if self.drawon:
1204 self.targetfig.canvas.draw()
1206 def funcbottom(self, val):
1207 self.targetfig.subplots_adjust(bottom=val)
1208 if self.drawon:
1209 self.targetfig.canvas.draw()
1211 def functop(self, val):
1212 self.targetfig.subplots_adjust(top=val)
1213 if self.drawon:
1214 self.targetfig.canvas.draw()
1216 def funcwspace(self, val):
1217 self.targetfig.subplots_adjust(wspace=val)
1218 if self.drawon:
1219 self.targetfig.canvas.draw()
1221 def funchspace(self, val):
1222 self.targetfig.subplots_adjust(hspace=val)
1223 if self.drawon:
1224 self.targetfig.canvas.draw()
1227class Cursor(AxesWidget):
1228 """
1229 A crosshair cursor that spans the axes and moves with mouse cursor.
1231 For the cursor to remain responsive you must keep a reference to it.
1233 Parameters
1234 ----------
1235 ax : `matplotlib.axes.Axes`
1236 The `~.axes.Axes` to attach the cursor to.
1237 horizOn : bool, optional, default: True
1238 Whether to draw the horizontal line.
1239 vertOn : bool, optional, default: True
1240 Whether to draw the vertical line.
1241 useblit : bool, optional, default: False
1242 Use blitting for faster drawing if supported by the backend.
1244 Other Parameters
1245 ----------------
1246 **lineprops
1247 `.Line2D` properties that control the appearance of the lines.
1248 See also `~.Axes.axhline`.
1250 Examples
1251 --------
1252 See :doc:`/gallery/widgets/cursor`.
1253 """
1255 def __init__(self, ax, horizOn=True, vertOn=True, useblit=False,
1256 **lineprops):
1257 AxesWidget.__init__(self, ax)
1259 self.connect_event('motion_notify_event', self.onmove)
1260 self.connect_event('draw_event', self.clear)
1262 self.visible = True
1263 self.horizOn = horizOn
1264 self.vertOn = vertOn
1265 self.useblit = useblit and self.canvas.supports_blit
1267 if self.useblit:
1268 lineprops['animated'] = True
1269 self.lineh = ax.axhline(ax.get_ybound()[0], visible=False, **lineprops)
1270 self.linev = ax.axvline(ax.get_xbound()[0], visible=False, **lineprops)
1272 self.background = None
1273 self.needclear = False
1275 def clear(self, event):
1276 """Internal event handler to clear the cursor."""
1277 if self.ignore(event):
1278 return
1279 if self.useblit:
1280 self.background = self.canvas.copy_from_bbox(self.ax.bbox)
1281 self.linev.set_visible(False)
1282 self.lineh.set_visible(False)
1284 def onmove(self, event):
1285 """Internal event handler to draw the cursor when the mouse moves."""
1286 if self.ignore(event):
1287 return
1288 if not self.canvas.widgetlock.available(self):
1289 return
1290 if event.inaxes != self.ax:
1291 self.linev.set_visible(False)
1292 self.lineh.set_visible(False)
1294 if self.needclear:
1295 self.canvas.draw()
1296 self.needclear = False
1297 return
1298 self.needclear = True
1299 if not self.visible:
1300 return
1301 self.linev.set_xdata((event.xdata, event.xdata))
1303 self.lineh.set_ydata((event.ydata, event.ydata))
1304 self.linev.set_visible(self.visible and self.vertOn)
1305 self.lineh.set_visible(self.visible and self.horizOn)
1307 self._update()
1309 def _update(self):
1310 if self.useblit:
1311 if self.background is not None:
1312 self.canvas.restore_region(self.background)
1313 self.ax.draw_artist(self.linev)
1314 self.ax.draw_artist(self.lineh)
1315 self.canvas.blit(self.ax.bbox)
1316 else:
1317 self.canvas.draw_idle()
1318 return False
1321class MultiCursor(Widget):
1322 """
1323 Provide a vertical (default) and/or horizontal line cursor shared between
1324 multiple axes.
1326 For the cursor to remain responsive you must keep a reference to it.
1328 Example usage::
1330 from matplotlib.widgets import MultiCursor
1331 import matplotlib.pyplot as plt
1332 import numpy as np
1334 fig, (ax1, ax2) = plt.subplots(nrows=2, sharex=True)
1335 t = np.arange(0.0, 2.0, 0.01)
1336 ax1.plot(t, np.sin(2*np.pi*t))
1337 ax2.plot(t, np.sin(4*np.pi*t))
1339 multi = MultiCursor(fig.canvas, (ax1, ax2), color='r', lw=1,
1340 horizOn=False, vertOn=True)
1341 plt.show()
1343 """
1344 def __init__(self, canvas, axes, useblit=True, horizOn=False, vertOn=True,
1345 **lineprops):
1347 self.canvas = canvas
1348 self.axes = axes
1349 self.horizOn = horizOn
1350 self.vertOn = vertOn
1352 xmin, xmax = axes[-1].get_xlim()
1353 ymin, ymax = axes[-1].get_ylim()
1354 xmid = 0.5 * (xmin + xmax)
1355 ymid = 0.5 * (ymin + ymax)
1357 self.visible = True
1358 self.useblit = useblit and self.canvas.supports_blit
1359 self.background = None
1360 self.needclear = False
1362 if self.useblit:
1363 lineprops['animated'] = True
1365 if vertOn:
1366 self.vlines = [ax.axvline(xmid, visible=False, **lineprops)
1367 for ax in axes]
1368 else:
1369 self.vlines = []
1371 if horizOn:
1372 self.hlines = [ax.axhline(ymid, visible=False, **lineprops)
1373 for ax in axes]
1374 else:
1375 self.hlines = []
1377 self.connect()
1379 def connect(self):
1380 """connect events"""
1381 self._cidmotion = self.canvas.mpl_connect('motion_notify_event',
1382 self.onmove)
1383 self._ciddraw = self.canvas.mpl_connect('draw_event', self.clear)
1385 def disconnect(self):
1386 """disconnect events"""
1387 self.canvas.mpl_disconnect(self._cidmotion)
1388 self.canvas.mpl_disconnect(self._ciddraw)
1390 def clear(self, event):
1391 """clear the cursor"""
1392 if self.ignore(event):
1393 return
1394 if self.useblit:
1395 self.background = (
1396 self.canvas.copy_from_bbox(self.canvas.figure.bbox))
1397 for line in self.vlines + self.hlines:
1398 line.set_visible(False)
1400 def onmove(self, event):
1401 if self.ignore(event):
1402 return
1403 if event.inaxes is None:
1404 return
1405 if not self.canvas.widgetlock.available(self):
1406 return
1407 self.needclear = True
1408 if not self.visible:
1409 return
1410 if self.vertOn:
1411 for line in self.vlines:
1412 line.set_xdata((event.xdata, event.xdata))
1413 line.set_visible(self.visible)
1414 if self.horizOn:
1415 for line in self.hlines:
1416 line.set_ydata((event.ydata, event.ydata))
1417 line.set_visible(self.visible)
1418 self._update()
1420 def _update(self):
1421 if self.useblit:
1422 if self.background is not None:
1423 self.canvas.restore_region(self.background)
1424 if self.vertOn:
1425 for ax, line in zip(self.axes, self.vlines):
1426 ax.draw_artist(line)
1427 if self.horizOn:
1428 for ax, line in zip(self.axes, self.hlines):
1429 ax.draw_artist(line)
1430 self.canvas.blit()
1431 else:
1432 self.canvas.draw_idle()
1435class _SelectorWidget(AxesWidget):
1437 def __init__(self, ax, onselect, useblit=False, button=None,
1438 state_modifier_keys=None):
1439 AxesWidget.__init__(self, ax)
1441 self.visible = True
1442 self.onselect = onselect
1443 self.useblit = useblit and self.canvas.supports_blit
1444 self.connect_default_events()
1446 self.state_modifier_keys = dict(move=' ', clear='escape',
1447 square='shift', center='control')
1448 self.state_modifier_keys.update(state_modifier_keys or {})
1450 self.background = None
1451 self.artists = []
1453 if isinstance(button, Integral):
1454 self.validButtons = [button]
1455 else:
1456 self.validButtons = button
1458 # will save the data (position at mouseclick)
1459 self.eventpress = None
1460 # will save the data (pos. at mouserelease)
1461 self.eventrelease = None
1462 self._prev_event = None
1463 self.state = set()
1465 def set_active(self, active):
1466 AxesWidget.set_active(self, active)
1467 if active:
1468 self.update_background(None)
1470 def update_background(self, event):
1471 """force an update of the background"""
1472 # If you add a call to `ignore` here, you'll want to check edge case:
1473 # `release` can call a draw event even when `ignore` is True.
1474 if self.useblit:
1475 self.background = self.canvas.copy_from_bbox(self.ax.bbox)
1477 def connect_default_events(self):
1478 """Connect the major canvas events to methods."""
1479 self.connect_event('motion_notify_event', self.onmove)
1480 self.connect_event('button_press_event', self.press)
1481 self.connect_event('button_release_event', self.release)
1482 self.connect_event('draw_event', self.update_background)
1483 self.connect_event('key_press_event', self.on_key_press)
1484 self.connect_event('key_release_event', self.on_key_release)
1485 self.connect_event('scroll_event', self.on_scroll)
1487 def ignore(self, event):
1488 # docstring inherited
1489 if not self.active or not self.ax.get_visible():
1490 return True
1491 # If canvas was locked
1492 if not self.canvas.widgetlock.available(self):
1493 return True
1494 if not hasattr(event, 'button'):
1495 event.button = None
1496 # Only do rectangle selection if event was triggered
1497 # with a desired button
1498 if (self.validButtons is not None
1499 and event.button not in self.validButtons):
1500 return True
1501 # If no button was pressed yet ignore the event if it was out
1502 # of the axes
1503 if self.eventpress is None:
1504 return event.inaxes != self.ax
1505 # If a button was pressed, check if the release-button is the same.
1506 if event.button == self.eventpress.button:
1507 return False
1508 # If a button was pressed, check if the release-button is the same.
1509 return (event.inaxes != self.ax or
1510 event.button != self.eventpress.button)
1512 def update(self):
1513 """
1514 Draw using blit() or draw_idle() depending on ``self.useblit``.
1515 """
1516 if not self.ax.get_visible():
1517 return False
1518 if self.useblit:
1519 if self.background is not None:
1520 self.canvas.restore_region(self.background)
1521 for artist in self.artists:
1522 self.ax.draw_artist(artist)
1523 self.canvas.blit(self.ax.bbox)
1524 else:
1525 self.canvas.draw_idle()
1526 return False
1528 def _get_data(self, event):
1529 """Get the xdata and ydata for event, with limits"""
1530 if event.xdata is None:
1531 return None, None
1532 x0, x1 = self.ax.get_xbound()
1533 y0, y1 = self.ax.get_ybound()
1534 xdata = max(x0, event.xdata)
1535 xdata = min(x1, xdata)
1536 ydata = max(y0, event.ydata)
1537 ydata = min(y1, ydata)
1538 return xdata, ydata
1540 def _clean_event(self, event):
1541 """Clean up an event
1543 Use prev event if there is no xdata
1544 Limit the xdata and ydata to the axes limits
1545 Set the prev event
1546 """
1547 if event.xdata is None:
1548 event = self._prev_event
1549 else:
1550 event = copy.copy(event)
1551 event.xdata, event.ydata = self._get_data(event)
1553 self._prev_event = event
1554 return event
1556 def press(self, event):
1557 """Button press handler and validator"""
1558 if not self.ignore(event):
1559 event = self._clean_event(event)
1560 self.eventpress = event
1561 self._prev_event = event
1562 key = event.key or ''
1563 key = key.replace('ctrl', 'control')
1564 # move state is locked in on a button press
1565 if key == self.state_modifier_keys['move']:
1566 self.state.add('move')
1567 self._press(event)
1568 return True
1569 return False
1571 def _press(self, event):
1572 """Button press handler"""
1574 def release(self, event):
1575 """Button release event handler and validator"""
1576 if not self.ignore(event) and self.eventpress:
1577 event = self._clean_event(event)
1578 self.eventrelease = event
1579 self._release(event)
1580 self.eventpress = None
1581 self.eventrelease = None
1582 self.state.discard('move')
1583 return True
1584 return False
1586 def _release(self, event):
1587 """Button release event handler"""
1589 def onmove(self, event):
1590 """Cursor move event handler and validator"""
1591 if not self.ignore(event) and self.eventpress:
1592 event = self._clean_event(event)
1593 self._onmove(event)
1594 return True
1595 return False
1597 def _onmove(self, event):
1598 """Cursor move event handler"""
1600 def on_scroll(self, event):
1601 """Mouse scroll event handler and validator"""
1602 if not self.ignore(event):
1603 self._on_scroll(event)
1605 def _on_scroll(self, event):
1606 """Mouse scroll event handler"""
1608 def on_key_press(self, event):
1609 """Key press event handler and validator for all selection widgets"""
1610 if self.active:
1611 key = event.key or ''
1612 key = key.replace('ctrl', 'control')
1613 if key == self.state_modifier_keys['clear']:
1614 for artist in self.artists:
1615 artist.set_visible(False)
1616 self.update()
1617 return
1618 for (state, modifier) in self.state_modifier_keys.items():
1619 if modifier in key:
1620 self.state.add(state)
1621 self._on_key_press(event)
1623 def _on_key_press(self, event):
1624 """Key press event handler - use for widget-specific key press actions.
1625 """
1627 def on_key_release(self, event):
1628 """Key release event handler and validator."""
1629 if self.active:
1630 key = event.key or ''
1631 for (state, modifier) in self.state_modifier_keys.items():
1632 if modifier in key:
1633 self.state.discard(state)
1634 self._on_key_release(event)
1636 def _on_key_release(self, event):
1637 """Key release event handler."""
1639 def set_visible(self, visible):
1640 """Set the visibility of our artists."""
1641 self.visible = visible
1642 for artist in self.artists:
1643 artist.set_visible(visible)
1646class SpanSelector(_SelectorWidget):
1647 """
1648 Visually select a min/max range on a single axis and call a function with
1649 those values.
1651 To guarantee that the selector remains responsive, keep a reference to it.
1653 In order to turn off the SpanSelector, set `span_selector.active=False`. To
1654 turn it back on, set `span_selector.active=True`.
1656 Parameters
1657 ----------
1658 ax : `matplotlib.axes.Axes` object
1660 onselect : func(min, max), min/max are floats
1662 direction : {"horizontal", "vertical"}
1663 The direction along which to draw the span selector.
1665 minspan : float, default is None
1666 If selection is less than *minspan*, do not call *onselect*.
1668 useblit : bool, default is False
1669 If True, use the backend-dependent blitting features for faster
1670 canvas updates.
1672 rectprops : dict, default is None
1673 Dictionary of `matplotlib.patches.Patch` properties.
1675 onmove_callback : func(min, max), min/max are floats, default is None
1676 Called on mouse move while the span is being selected.
1678 span_stays : bool, default is False
1679 If True, the span stays visible after the mouse is released.
1681 button : `.MouseButton` or list of `.MouseButton`
1682 The mouse buttons which activate the span selector.
1684 Examples
1685 --------
1686 >>> import matplotlib.pyplot as plt
1687 >>> import matplotlib.widgets as mwidgets
1688 >>> fig, ax = plt.subplots()
1689 >>> ax.plot([1, 2, 3], [10, 50, 100])
1690 >>> def onselect(vmin, vmax):
1691 ... print(vmin, vmax)
1692 >>> rectprops = dict(facecolor='blue', alpha=0.5)
1693 >>> span = mwidgets.SpanSelector(ax, onselect, 'horizontal',
1694 ... rectprops=rectprops)
1695 >>> fig.show()
1697 See also: :doc:`/gallery/widgets/span_selector`
1698 """
1700 def __init__(self, ax, onselect, direction, minspan=None, useblit=False,
1701 rectprops=None, onmove_callback=None, span_stays=False,
1702 button=None):
1704 _SelectorWidget.__init__(self, ax, onselect, useblit=useblit,
1705 button=button)
1707 if rectprops is None:
1708 rectprops = dict(facecolor='red', alpha=0.5)
1710 rectprops['animated'] = self.useblit
1712 cbook._check_in_list(['horizontal', 'vertical'], direction=direction)
1713 self.direction = direction
1715 self.rect = None
1716 self.pressv = None
1718 self.rectprops = rectprops
1719 self.onmove_callback = onmove_callback
1720 self.minspan = minspan
1721 self.span_stays = span_stays
1723 # Needed when dragging out of axes
1724 self.prev = (0, 0)
1726 # Reset canvas so that `new_axes` connects events.
1727 self.canvas = None
1728 self.new_axes(ax)
1730 def new_axes(self, ax):
1731 """Set SpanSelector to operate on a new Axes."""
1732 self.ax = ax
1733 if self.canvas is not ax.figure.canvas:
1734 if self.canvas is not None:
1735 self.disconnect_events()
1737 self.canvas = ax.figure.canvas
1738 self.connect_default_events()
1740 if self.direction == 'horizontal':
1741 trans = blended_transform_factory(self.ax.transData,
1742 self.ax.transAxes)
1743 w, h = 0, 1
1744 else:
1745 trans = blended_transform_factory(self.ax.transAxes,
1746 self.ax.transData)
1747 w, h = 1, 0
1748 self.rect = Rectangle((0, 0), w, h,
1749 transform=trans,
1750 visible=False,
1751 **self.rectprops)
1752 if self.span_stays:
1753 self.stay_rect = Rectangle((0, 0), w, h,
1754 transform=trans,
1755 visible=False,
1756 **self.rectprops)
1757 self.stay_rect.set_animated(False)
1758 self.ax.add_patch(self.stay_rect)
1760 self.ax.add_patch(self.rect)
1761 self.artists = [self.rect]
1763 def ignore(self, event):
1764 # docstring inherited
1765 return _SelectorWidget.ignore(self, event) or not self.visible
1767 def _press(self, event):
1768 """on button press event"""
1769 self.rect.set_visible(self.visible)
1770 if self.span_stays:
1771 self.stay_rect.set_visible(False)
1772 # really force a draw so that the stay rect is not in
1773 # the blit background
1774 if self.useblit:
1775 self.canvas.draw()
1776 xdata, ydata = self._get_data(event)
1777 if self.direction == 'horizontal':
1778 self.pressv = xdata
1779 else:
1780 self.pressv = ydata
1782 self._set_span_xy(event)
1783 return False
1785 def _release(self, event):
1786 """on button release event"""
1787 if self.pressv is None:
1788 return
1790 self.rect.set_visible(False)
1792 if self.span_stays:
1793 self.stay_rect.set_x(self.rect.get_x())
1794 self.stay_rect.set_y(self.rect.get_y())
1795 self.stay_rect.set_width(self.rect.get_width())
1796 self.stay_rect.set_height(self.rect.get_height())
1797 self.stay_rect.set_visible(True)
1799 self.canvas.draw_idle()
1800 vmin = self.pressv
1801 xdata, ydata = self._get_data(event)
1802 if self.direction == 'horizontal':
1803 vmax = xdata or self.prev[0]
1804 else:
1805 vmax = ydata or self.prev[1]
1807 if vmin > vmax:
1808 vmin, vmax = vmax, vmin
1809 span = vmax - vmin
1810 if self.minspan is not None and span < self.minspan:
1811 return
1812 self.onselect(vmin, vmax)
1813 self.pressv = None
1814 return False
1816 @cbook.deprecated("3.1")
1817 @property
1818 def buttonDown(self):
1819 return False
1821 def _onmove(self, event):
1822 """on motion notify event"""
1823 if self.pressv is None:
1824 return
1826 self._set_span_xy(event)
1828 if self.onmove_callback is not None:
1829 vmin = self.pressv
1830 xdata, ydata = self._get_data(event)
1831 if self.direction == 'horizontal':
1832 vmax = xdata or self.prev[0]
1833 else:
1834 vmax = ydata or self.prev[1]
1836 if vmin > vmax:
1837 vmin, vmax = vmax, vmin
1838 self.onmove_callback(vmin, vmax)
1840 self.update()
1841 return False
1843 def _set_span_xy(self, event):
1844 """Setting the span coordinates"""
1845 x, y = self._get_data(event)
1846 if x is None:
1847 return
1849 self.prev = x, y
1850 if self.direction == 'horizontal':
1851 v = x
1852 else:
1853 v = y
1855 minv, maxv = v, self.pressv
1856 if minv > maxv:
1857 minv, maxv = maxv, minv
1858 if self.direction == 'horizontal':
1859 self.rect.set_x(minv)
1860 self.rect.set_width(maxv - minv)
1861 else:
1862 self.rect.set_y(minv)
1863 self.rect.set_height(maxv - minv)
1866class ToolHandles:
1867 """
1868 Control handles for canvas tools.
1870 Parameters
1871 ----------
1872 ax : `matplotlib.axes.Axes`
1873 Matplotlib axes where tool handles are displayed.
1874 x, y : 1D arrays
1875 Coordinates of control handles.
1876 marker : str
1877 Shape of marker used to display handle. See `matplotlib.pyplot.plot`.
1878 marker_props : dict
1879 Additional marker properties. See `matplotlib.lines.Line2D`.
1880 """
1882 def __init__(self, ax, x, y, marker='o', marker_props=None, useblit=True):
1883 self.ax = ax
1884 props = dict(marker=marker, markersize=7, mfc='w', ls='none',
1885 alpha=0.5, visible=False, label='_nolegend_')
1886 props.update(marker_props if marker_props is not None else {})
1887 self._markers = Line2D(x, y, animated=useblit, **props)
1888 self.ax.add_line(self._markers)
1889 self.artist = self._markers
1891 @property
1892 def x(self):
1893 return self._markers.get_xdata()
1895 @property
1896 def y(self):
1897 return self._markers.get_ydata()
1899 def set_data(self, pts, y=None):
1900 """Set x and y positions of handles"""
1901 if y is not None:
1902 x = pts
1903 pts = np.array([x, y])
1904 self._markers.set_data(pts)
1906 def set_visible(self, val):
1907 self._markers.set_visible(val)
1909 def set_animated(self, val):
1910 self._markers.set_animated(val)
1912 def closest(self, x, y):
1913 """Return index and pixel distance to closest index."""
1914 pts = np.column_stack([self.x, self.y])
1915 # Transform data coordinates to pixel coordinates.
1916 pts = self.ax.transData.transform(pts)
1917 diff = pts - [x, y]
1918 dist = np.hypot(*diff.T)
1919 min_index = np.argmin(dist)
1920 return min_index, dist[min_index]
1923class RectangleSelector(_SelectorWidget):
1924 """
1925 Select a rectangular region of an axes.
1927 For the cursor to remain responsive you must keep a reference to it.
1929 Example usage::
1931 import numpy as np
1932 import matplotlib.pyplot as plt
1933 from matplotlib.widgets import RectangleSelector
1935 def onselect(eclick, erelease):
1936 "eclick and erelease are matplotlib events at press and release."
1937 print('startposition: (%f, %f)' % (eclick.xdata, eclick.ydata))
1938 print('endposition : (%f, %f)' % (erelease.xdata, erelease.ydata))
1939 print('used button : ', eclick.button)
1941 def toggle_selector(event):
1942 print('Key pressed.')
1943 if event.key in ['Q', 'q'] and toggle_selector.RS.active:
1944 print('RectangleSelector deactivated.')
1945 toggle_selector.RS.set_active(False)
1946 if event.key in ['A', 'a'] and not toggle_selector.RS.active:
1947 print('RectangleSelector activated.')
1948 toggle_selector.RS.set_active(True)
1950 x = np.arange(100.) / 99
1951 y = np.sin(x)
1952 fig, ax = plt.subplots()
1953 ax.plot(x, y)
1955 toggle_selector.RS = RectangleSelector(ax, onselect, drawtype='line')
1956 fig.canvas.mpl_connect('key_press_event', toggle_selector)
1957 plt.show()
1958 """
1960 _shape_klass = Rectangle
1962 def __init__(self, ax, onselect, drawtype='box',
1963 minspanx=None, minspany=None, useblit=False,
1964 lineprops=None, rectprops=None, spancoords='data',
1965 button=None, maxdist=10, marker_props=None,
1966 interactive=False, state_modifier_keys=None):
1967 """
1968 Create a selector in *ax*. When a selection is made, clear
1969 the span and call onselect with::
1971 onselect(pos_1, pos_2)
1973 and clear the drawn box/line. The ``pos_1`` and ``pos_2`` are
1974 arrays of length 2 containing the x- and y-coordinate.
1976 If *minspanx* is not *None* then events smaller than *minspanx*
1977 in x direction are ignored (it's the same for y).
1979 The rectangle is drawn with *rectprops*; default::
1981 rectprops = dict(facecolor='red', edgecolor = 'black',
1982 alpha=0.2, fill=True)
1984 The line is drawn with *lineprops*; default::
1986 lineprops = dict(color='black', linestyle='-',
1987 linewidth = 2, alpha=0.5)
1989 Use *drawtype* if you want the mouse to draw a line,
1990 a box or nothing between click and actual position by setting
1992 ``drawtype = 'line'``, ``drawtype='box'`` or ``drawtype = 'none'``.
1993 Drawing a line would result in a line from vertex A to vertex C in
1994 a rectangle ABCD.
1996 *spancoords* is one of 'data' or 'pixels'. If 'data', *minspanx*
1997 and *minspanx* will be interpreted in the same coordinates as
1998 the x and y axis. If 'pixels', they are in pixels.
2000 *button* is a list of integers indicating which mouse buttons should
2001 be used for rectangle selection. You can also specify a single
2002 integer if only a single button is desired. Default is *None*,
2003 which does not limit which button can be used.
2005 Note, typically:
2006 1 = left mouse button
2007 2 = center mouse button (scroll wheel)
2008 3 = right mouse button
2010 *interactive* will draw a set of handles and allow you interact
2011 with the widget after it is drawn.
2013 *state_modifier_keys* are keyboard modifiers that affect the behavior
2014 of the widget.
2016 The defaults are:
2017 dict(move=' ', clear='escape', square='shift', center='ctrl')
2019 Keyboard modifiers, which:
2020 'move': Move the existing shape.
2021 'clear': Clear the current shape.
2022 'square': Makes the shape square.
2023 'center': Make the initial point the center of the shape.
2024 'square' and 'center' can be combined.
2025 """
2026 _SelectorWidget.__init__(self, ax, onselect, useblit=useblit,
2027 button=button,
2028 state_modifier_keys=state_modifier_keys)
2030 self.to_draw = None
2031 self.visible = True
2032 self.interactive = interactive
2034 if drawtype == 'none': # draw a line but make it invisible
2035 drawtype = 'line'
2036 self.visible = False
2038 if drawtype == 'box':
2039 if rectprops is None:
2040 rectprops = dict(facecolor='red', edgecolor='black',
2041 alpha=0.2, fill=True)
2042 rectprops['animated'] = self.useblit
2043 self.rectprops = rectprops
2044 self.to_draw = self._shape_klass((0, 0), 0, 1, visible=False,
2045 **self.rectprops)
2046 self.ax.add_patch(self.to_draw)
2047 if drawtype == 'line':
2048 if lineprops is None:
2049 lineprops = dict(color='black', linestyle='-',
2050 linewidth=2, alpha=0.5)
2051 lineprops['animated'] = self.useblit
2052 self.lineprops = lineprops
2053 self.to_draw = Line2D([0, 0], [0, 0], visible=False,
2054 **self.lineprops)
2055 self.ax.add_line(self.to_draw)
2057 self.minspanx = minspanx
2058 self.minspany = minspany
2060 cbook._check_in_list(['data', 'pixels'], spancoords=spancoords)
2061 self.spancoords = spancoords
2062 self.drawtype = drawtype
2064 self.maxdist = maxdist
2066 if rectprops is None:
2067 props = dict(mec='r')
2068 else:
2069 props = dict(mec=rectprops.get('edgecolor', 'r'))
2070 self._corner_order = ['NW', 'NE', 'SE', 'SW']
2071 xc, yc = self.corners
2072 self._corner_handles = ToolHandles(self.ax, xc, yc, marker_props=props,
2073 useblit=self.useblit)
2075 self._edge_order = ['W', 'N', 'E', 'S']
2076 xe, ye = self.edge_centers
2077 self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s',
2078 marker_props=props,
2079 useblit=self.useblit)
2081 xc, yc = self.center
2082 self._center_handle = ToolHandles(self.ax, [xc], [yc], marker='s',
2083 marker_props=props,
2084 useblit=self.useblit)
2086 self.active_handle = None
2088 self.artists = [self.to_draw, self._center_handle.artist,
2089 self._corner_handles.artist,
2090 self._edge_handles.artist]
2092 if not self.interactive:
2093 self.artists = [self.to_draw]
2095 self._extents_on_press = None
2097 def _press(self, event):
2098 """on button press event"""
2099 # make the drawed box/line visible get the click-coordinates,
2100 # button, ...
2101 if self.interactive and self.to_draw.get_visible():
2102 self._set_active_handle(event)
2103 else:
2104 self.active_handle = None
2106 if self.active_handle is None or not self.interactive:
2107 # Clear previous rectangle before drawing new rectangle.
2108 self.update()
2110 if not self.interactive:
2111 x = event.xdata
2112 y = event.ydata
2113 self.extents = x, x, y, y
2115 self.set_visible(self.visible)
2117 def _release(self, event):
2118 """on button release event"""
2119 if not self.interactive:
2120 self.to_draw.set_visible(False)
2122 # update the eventpress and eventrelease with the resulting extents
2123 x1, x2, y1, y2 = self.extents
2124 self.eventpress.xdata = x1
2125 self.eventpress.ydata = y1
2126 xy1 = self.ax.transData.transform([x1, y1])
2127 self.eventpress.x, self.eventpress.y = xy1
2129 self.eventrelease.xdata = x2
2130 self.eventrelease.ydata = y2
2131 xy2 = self.ax.transData.transform([x2, y2])
2132 self.eventrelease.x, self.eventrelease.y = xy2
2134 if self.spancoords == 'data':
2135 xmin, ymin = self.eventpress.xdata, self.eventpress.ydata
2136 xmax, ymax = self.eventrelease.xdata, self.eventrelease.ydata
2137 # calculate dimensions of box or line get values in the right order
2138 elif self.spancoords == 'pixels':
2139 xmin, ymin = self.eventpress.x, self.eventpress.y
2140 xmax, ymax = self.eventrelease.x, self.eventrelease.y
2141 else:
2142 cbook._check_in_list(['data', 'pixels'],
2143 spancoords=self.spancoords)
2145 if xmin > xmax:
2146 xmin, xmax = xmax, xmin
2147 if ymin > ymax:
2148 ymin, ymax = ymax, ymin
2150 spanx = xmax - xmin
2151 spany = ymax - ymin
2152 xproblems = self.minspanx is not None and spanx < self.minspanx
2153 yproblems = self.minspany is not None and spany < self.minspany
2155 # check if drawn distance (if it exists) is not too small in
2156 # either x or y-direction
2157 if self.drawtype != 'none' and (xproblems or yproblems):
2158 for artist in self.artists:
2159 artist.set_visible(False)
2160 self.update()
2161 return
2163 # call desired function
2164 self.onselect(self.eventpress, self.eventrelease)
2165 self.update()
2167 return False
2169 def _onmove(self, event):
2170 """on motion notify event if box/line is wanted"""
2171 # resize an existing shape
2172 if self.active_handle and self.active_handle != 'C':
2173 x1, x2, y1, y2 = self._extents_on_press
2174 if self.active_handle in ['E', 'W'] + self._corner_order:
2175 x2 = event.xdata
2176 if self.active_handle in ['N', 'S'] + self._corner_order:
2177 y2 = event.ydata
2179 # move existing shape
2180 elif (('move' in self.state or self.active_handle == 'C')
2181 and self._extents_on_press is not None):
2182 x1, x2, y1, y2 = self._extents_on_press
2183 dx = event.xdata - self.eventpress.xdata
2184 dy = event.ydata - self.eventpress.ydata
2185 x1 += dx
2186 x2 += dx
2187 y1 += dy
2188 y2 += dy
2190 # new shape
2191 else:
2192 center = [self.eventpress.xdata, self.eventpress.ydata]
2193 center_pix = [self.eventpress.x, self.eventpress.y]
2194 dx = (event.xdata - center[0]) / 2.
2195 dy = (event.ydata - center[1]) / 2.
2197 # square shape
2198 if 'square' in self.state:
2199 dx_pix = abs(event.x - center_pix[0])
2200 dy_pix = abs(event.y - center_pix[1])
2201 if not dx_pix:
2202 return
2203 maxd = max(abs(dx_pix), abs(dy_pix))
2204 if abs(dx_pix) < maxd:
2205 dx *= maxd / (abs(dx_pix) + 1e-6)
2206 if abs(dy_pix) < maxd:
2207 dy *= maxd / (abs(dy_pix) + 1e-6)
2209 # from center
2210 if 'center' in self.state:
2211 dx *= 2
2212 dy *= 2
2214 # from corner
2215 else:
2216 center[0] += dx
2217 center[1] += dy
2219 x1, x2, y1, y2 = (center[0] - dx, center[0] + dx,
2220 center[1] - dy, center[1] + dy)
2222 self.extents = x1, x2, y1, y2
2224 @property
2225 def _rect_bbox(self):
2226 if self.drawtype == 'box':
2227 x0 = self.to_draw.get_x()
2228 y0 = self.to_draw.get_y()
2229 width = self.to_draw.get_width()
2230 height = self.to_draw.get_height()
2231 return x0, y0, width, height
2232 else:
2233 x, y = self.to_draw.get_data()
2234 x0, x1 = min(x), max(x)
2235 y0, y1 = min(y), max(y)
2236 return x0, y0, x1 - x0, y1 - y0
2238 @property
2239 def corners(self):
2240 """Corners of rectangle from lower left, moving clockwise."""
2241 x0, y0, width, height = self._rect_bbox
2242 xc = x0, x0 + width, x0 + width, x0
2243 yc = y0, y0, y0 + height, y0 + height
2244 return xc, yc
2246 @property
2247 def edge_centers(self):
2248 """Midpoint of rectangle edges from left, moving clockwise."""
2249 x0, y0, width, height = self._rect_bbox
2250 w = width / 2.
2251 h = height / 2.
2252 xe = x0, x0 + w, x0 + width, x0 + w
2253 ye = y0 + h, y0, y0 + h, y0 + height
2254 return xe, ye
2256 @property
2257 def center(self):
2258 """Center of rectangle"""
2259 x0, y0, width, height = self._rect_bbox
2260 return x0 + width / 2., y0 + height / 2.
2262 @property
2263 def extents(self):
2264 """Return (xmin, xmax, ymin, ymax)."""
2265 x0, y0, width, height = self._rect_bbox
2266 xmin, xmax = sorted([x0, x0 + width])
2267 ymin, ymax = sorted([y0, y0 + height])
2268 return xmin, xmax, ymin, ymax
2270 @extents.setter
2271 def extents(self, extents):
2272 # Update displayed shape
2273 self.draw_shape(extents)
2274 # Update displayed handles
2275 self._corner_handles.set_data(*self.corners)
2276 self._edge_handles.set_data(*self.edge_centers)
2277 self._center_handle.set_data(*self.center)
2278 self.set_visible(self.visible)
2279 self.update()
2281 def draw_shape(self, extents):
2282 x0, x1, y0, y1 = extents
2283 xmin, xmax = sorted([x0, x1])
2284 ymin, ymax = sorted([y0, y1])
2285 xlim = sorted(self.ax.get_xlim())
2286 ylim = sorted(self.ax.get_ylim())
2288 xmin = max(xlim[0], xmin)
2289 ymin = max(ylim[0], ymin)
2290 xmax = min(xmax, xlim[1])
2291 ymax = min(ymax, ylim[1])
2293 if self.drawtype == 'box':
2294 self.to_draw.set_x(xmin)
2295 self.to_draw.set_y(ymin)
2296 self.to_draw.set_width(xmax - xmin)
2297 self.to_draw.set_height(ymax - ymin)
2299 elif self.drawtype == 'line':
2300 self.to_draw.set_data([xmin, xmax], [ymin, ymax])
2302 def _set_active_handle(self, event):
2303 """Set active handle based on the location of the mouse event"""
2304 # Note: event.xdata/ydata in data coordinates, event.x/y in pixels
2305 c_idx, c_dist = self._corner_handles.closest(event.x, event.y)
2306 e_idx, e_dist = self._edge_handles.closest(event.x, event.y)
2307 m_idx, m_dist = self._center_handle.closest(event.x, event.y)
2309 if 'move' in self.state:
2310 self.active_handle = 'C'
2311 self._extents_on_press = self.extents
2313 # Set active handle as closest handle, if mouse click is close enough.
2314 elif m_dist < self.maxdist * 2:
2315 self.active_handle = 'C'
2316 elif c_dist > self.maxdist and e_dist > self.maxdist:
2317 self.active_handle = None
2318 return
2319 elif c_dist < e_dist:
2320 self.active_handle = self._corner_order[c_idx]
2321 else:
2322 self.active_handle = self._edge_order[e_idx]
2324 # Save coordinates of rectangle at the start of handle movement.
2325 x1, x2, y1, y2 = self.extents
2326 # Switch variables so that only x2 and/or y2 are updated on move.
2327 if self.active_handle in ['W', 'SW', 'NW']:
2328 x1, x2 = x2, event.xdata
2329 if self.active_handle in ['N', 'NW', 'NE']:
2330 y1, y2 = y2, event.ydata
2331 self._extents_on_press = x1, x2, y1, y2
2333 @property
2334 def geometry(self):
2335 """
2336 Return an array of shape (2, 5) containing the
2337 x (``RectangleSelector.geometry[1, :]``) and
2338 y (``RectangleSelector.geometry[0, :]``) coordinates
2339 of the four corners of the rectangle starting and ending
2340 in the top left corner.
2341 """
2342 if hasattr(self.to_draw, 'get_verts'):
2343 xfm = self.ax.transData.inverted()
2344 y, x = xfm.transform(self.to_draw.get_verts()).T
2345 return np.array([x, y])
2346 else:
2347 return np.array(self.to_draw.get_data())
2350class EllipseSelector(RectangleSelector):
2351 """
2352 Select an elliptical region of an axes.
2354 For the cursor to remain responsive you must keep a reference to it.
2356 Example usage::
2358 import numpy as np
2359 import matplotlib.pyplot as plt
2360 from matplotlib.widgets import EllipseSelector
2362 def onselect(eclick, erelease):
2363 "eclick and erelease are matplotlib events at press and release."
2364 print('startposition: (%f, %f)' % (eclick.xdata, eclick.ydata))
2365 print('endposition : (%f, %f)' % (erelease.xdata, erelease.ydata))
2366 print('used button : ', eclick.button)
2368 def toggle_selector(event):
2369 print(' Key pressed.')
2370 if event.key in ['Q', 'q'] and toggle_selector.ES.active:
2371 print('EllipseSelector deactivated.')
2372 toggle_selector.RS.set_active(False)
2373 if event.key in ['A', 'a'] and not toggle_selector.ES.active:
2374 print('EllipseSelector activated.')
2375 toggle_selector.ES.set_active(True)
2377 x = np.arange(100.) / 99
2378 y = np.sin(x)
2379 fig, ax = plt.subplots()
2380 ax.plot(x, y)
2382 toggle_selector.ES = EllipseSelector(ax, onselect, drawtype='line')
2383 fig.canvas.mpl_connect('key_press_event', toggle_selector)
2384 plt.show()
2385 """
2386 _shape_klass = Ellipse
2388 def draw_shape(self, extents):
2389 x1, x2, y1, y2 = extents
2390 xmin, xmax = sorted([x1, x2])
2391 ymin, ymax = sorted([y1, y2])
2392 center = [x1 + (x2 - x1) / 2., y1 + (y2 - y1) / 2.]
2393 a = (xmax - xmin) / 2.
2394 b = (ymax - ymin) / 2.
2396 if self.drawtype == 'box':
2397 self.to_draw.center = center
2398 self.to_draw.width = 2 * a
2399 self.to_draw.height = 2 * b
2400 else:
2401 rad = np.deg2rad(np.arange(31) * 12)
2402 x = a * np.cos(rad) + center[0]
2403 y = b * np.sin(rad) + center[1]
2404 self.to_draw.set_data(x, y)
2406 @property
2407 def _rect_bbox(self):
2408 if self.drawtype == 'box':
2409 x, y = self.to_draw.center
2410 width = self.to_draw.width
2411 height = self.to_draw.height
2412 return x - width / 2., y - height / 2., width, height
2413 else:
2414 x, y = self.to_draw.get_data()
2415 x0, x1 = min(x), max(x)
2416 y0, y1 = min(y), max(y)
2417 return x0, y0, x1 - x0, y1 - y0
2420class LassoSelector(_SelectorWidget):
2421 """
2422 Selection curve of an arbitrary shape.
2424 For the selector to remain responsive you must keep a reference to it.
2426 The selected path can be used in conjunction with `~.Path.contains_point`
2427 to select data points from an image.
2429 In contrast to `Lasso`, `LassoSelector` is written with an interface
2430 similar to `RectangleSelector` and `SpanSelector`, and will continue to
2431 interact with the axes until disconnected.
2433 Example usage::
2435 ax = subplot(111)
2436 ax.plot(x, y)
2438 def onselect(verts):
2439 print(verts)
2440 lasso = LassoSelector(ax, onselect)
2442 Parameters
2443 ----------
2444 ax : `~matplotlib.axes.Axes`
2445 The parent axes for the widget.
2446 onselect : function
2447 Whenever the lasso is released, the *onselect* function is called and
2448 passed the vertices of the selected path.
2449 button : `.MouseButton` or list of `.MouseButton`, optional
2450 The mouse buttons used for rectangle selection. Default is ``None``,
2451 which corresponds to all buttons.
2452 """
2454 def __init__(self, ax, onselect=None, useblit=True, lineprops=None,
2455 button=None):
2456 _SelectorWidget.__init__(self, ax, onselect, useblit=useblit,
2457 button=button)
2458 self.verts = None
2459 if lineprops is None:
2460 lineprops = dict()
2461 # self.useblit may be != useblit, if the canvas doesn't support blit.
2462 lineprops.update(animated=self.useblit, visible=False)
2463 self.line = Line2D([], [], **lineprops)
2464 self.ax.add_line(self.line)
2465 self.artists = [self.line]
2467 def onpress(self, event):
2468 self.press(event)
2470 def _press(self, event):
2471 self.verts = [self._get_data(event)]
2472 self.line.set_visible(True)
2474 def onrelease(self, event):
2475 self.release(event)
2477 def _release(self, event):
2478 if self.verts is not None:
2479 self.verts.append(self._get_data(event))
2480 self.onselect(self.verts)
2481 self.line.set_data([[], []])
2482 self.line.set_visible(False)
2483 self.verts = None
2485 def _onmove(self, event):
2486 if self.verts is None:
2487 return
2488 self.verts.append(self._get_data(event))
2490 self.line.set_data(list(zip(*self.verts)))
2492 self.update()
2495class PolygonSelector(_SelectorWidget):
2496 """
2497 Select a polygon region of an axes.
2499 Place vertices with each mouse click, and make the selection by completing
2500 the polygon (clicking on the first vertex). Hold the *ctrl* key and click
2501 and drag a vertex to reposition it (the *ctrl* key is not necessary if the
2502 polygon has already been completed). Hold the *shift* key and click and
2503 drag anywhere in the axes to move all vertices. Press the *esc* key to
2504 start a new polygon.
2506 For the selector to remain responsive you must keep a reference to
2507 it.
2509 Parameters
2510 ----------
2511 ax : `~matplotlib.axes.Axes`
2512 The parent axes for the widget.
2513 onselect : function
2514 When a polygon is completed or modified after completion,
2515 the `onselect` function is called and passed a list of the vertices as
2516 ``(xdata, ydata)`` tuples.
2517 useblit : bool, optional
2518 lineprops : dict, optional
2519 The line for the sides of the polygon is drawn with the properties
2520 given by `lineprops`. The default is ``dict(color='k', linestyle='-',
2521 linewidth=2, alpha=0.5)``.
2522 markerprops : dict, optional
2523 The markers for the vertices of the polygon are drawn with the
2524 properties given by `markerprops`. The default is ``dict(marker='o',
2525 markersize=7, mec='k', mfc='k', alpha=0.5)``.
2526 vertex_select_radius : float, optional
2527 A vertex is selected (to complete the polygon or to move a vertex)
2528 if the mouse click is within `vertex_select_radius` pixels of the
2529 vertex. The default radius is 15 pixels.
2531 Examples
2532 --------
2533 :doc:`/gallery/widgets/polygon_selector_demo`
2534 """
2536 def __init__(self, ax, onselect, useblit=False,
2537 lineprops=None, markerprops=None, vertex_select_radius=15):
2538 # The state modifiers 'move', 'square', and 'center' are expected by
2539 # _SelectorWidget but are not supported by PolygonSelector
2540 # Note: could not use the existing 'move' state modifier in-place of
2541 # 'move_all' because _SelectorWidget automatically discards 'move'
2542 # from the state on button release.
2543 state_modifier_keys = dict(clear='escape', move_vertex='control',
2544 move_all='shift', move='not-applicable',
2545 square='not-applicable',
2546 center='not-applicable')
2547 _SelectorWidget.__init__(self, ax, onselect, useblit=useblit,
2548 state_modifier_keys=state_modifier_keys)
2550 self._xs, self._ys = [0], [0]
2551 self._polygon_completed = False
2553 if lineprops is None:
2554 lineprops = dict(color='k', linestyle='-', linewidth=2, alpha=0.5)
2555 lineprops['animated'] = self.useblit
2556 self.line = Line2D(self._xs, self._ys, **lineprops)
2557 self.ax.add_line(self.line)
2559 if markerprops is None:
2560 markerprops = dict(mec='k', mfc=lineprops.get('color', 'k'))
2561 self._polygon_handles = ToolHandles(self.ax, self._xs, self._ys,
2562 useblit=self.useblit,
2563 marker_props=markerprops)
2565 self._active_handle_idx = -1
2566 self.vertex_select_radius = vertex_select_radius
2568 self.artists = [self.line, self._polygon_handles.artist]
2569 self.set_visible(True)
2571 def _press(self, event):
2572 """Button press event handler"""
2573 # Check for selection of a tool handle.
2574 if ((self._polygon_completed or 'move_vertex' in self.state)
2575 and len(self._xs) > 0):
2576 h_idx, h_dist = self._polygon_handles.closest(event.x, event.y)
2577 if h_dist < self.vertex_select_radius:
2578 self._active_handle_idx = h_idx
2579 # Save the vertex positions at the time of the press event (needed to
2580 # support the 'move_all' state modifier).
2581 self._xs_at_press, self._ys_at_press = self._xs[:], self._ys[:]
2583 def _release(self, event):
2584 """Button release event handler"""
2585 # Release active tool handle.
2586 if self._active_handle_idx >= 0:
2587 self._active_handle_idx = -1
2589 # Complete the polygon.
2590 elif (len(self._xs) > 3
2591 and self._xs[-1] == self._xs[0]
2592 and self._ys[-1] == self._ys[0]):
2593 self._polygon_completed = True
2595 # Place new vertex.
2596 elif (not self._polygon_completed
2597 and 'move_all' not in self.state
2598 and 'move_vertex' not in self.state):
2599 self._xs.insert(-1, event.xdata)
2600 self._ys.insert(-1, event.ydata)
2602 if self._polygon_completed:
2603 self.onselect(self.verts)
2605 def onmove(self, event):
2606 """Cursor move event handler and validator"""
2607 # Method overrides _SelectorWidget.onmove because the polygon selector
2608 # needs to process the move callback even if there is no button press.
2609 # _SelectorWidget.onmove include logic to ignore move event if
2610 # eventpress is None.
2611 if not self.ignore(event):
2612 event = self._clean_event(event)
2613 self._onmove(event)
2614 return True
2615 return False
2617 def _onmove(self, event):
2618 """Cursor move event handler"""
2619 # Move the active vertex (ToolHandle).
2620 if self._active_handle_idx >= 0:
2621 idx = self._active_handle_idx
2622 self._xs[idx], self._ys[idx] = event.xdata, event.ydata
2623 # Also update the end of the polygon line if the first vertex is
2624 # the active handle and the polygon is completed.
2625 if idx == 0 and self._polygon_completed:
2626 self._xs[-1], self._ys[-1] = event.xdata, event.ydata
2628 # Move all vertices.
2629 elif 'move_all' in self.state and self.eventpress:
2630 dx = event.xdata - self.eventpress.xdata
2631 dy = event.ydata - self.eventpress.ydata
2632 for k in range(len(self._xs)):
2633 self._xs[k] = self._xs_at_press[k] + dx
2634 self._ys[k] = self._ys_at_press[k] + dy
2636 # Do nothing if completed or waiting for a move.
2637 elif (self._polygon_completed
2638 or 'move_vertex' in self.state or 'move_all' in self.state):
2639 return
2641 # Position pending vertex.
2642 else:
2643 # Calculate distance to the start vertex.
2644 x0, y0 = self.line.get_transform().transform((self._xs[0],
2645 self._ys[0]))
2646 v0_dist = np.hypot(x0 - event.x, y0 - event.y)
2647 # Lock on to the start vertex if near it and ready to complete.
2648 if len(self._xs) > 3 and v0_dist < self.vertex_select_radius:
2649 self._xs[-1], self._ys[-1] = self._xs[0], self._ys[0]
2650 else:
2651 self._xs[-1], self._ys[-1] = event.xdata, event.ydata
2653 self._draw_polygon()
2655 def _on_key_press(self, event):
2656 """Key press event handler"""
2657 # Remove the pending vertex if entering the 'move_vertex' or
2658 # 'move_all' mode
2659 if (not self._polygon_completed
2660 and ('move_vertex' in self.state or 'move_all' in self.state)):
2661 self._xs, self._ys = self._xs[:-1], self._ys[:-1]
2662 self._draw_polygon()
2664 def _on_key_release(self, event):
2665 """Key release event handler"""
2666 # Add back the pending vertex if leaving the 'move_vertex' or
2667 # 'move_all' mode (by checking the released key)
2668 if (not self._polygon_completed
2669 and
2670 (event.key == self.state_modifier_keys.get('move_vertex')
2671 or event.key == self.state_modifier_keys.get('move_all'))):
2672 self._xs.append(event.xdata)
2673 self._ys.append(event.ydata)
2674 self._draw_polygon()
2675 # Reset the polygon if the released key is the 'clear' key.
2676 elif event.key == self.state_modifier_keys.get('clear'):
2677 event = self._clean_event(event)
2678 self._xs, self._ys = [event.xdata], [event.ydata]
2679 self._polygon_completed = False
2680 self.set_visible(True)
2682 def _draw_polygon(self):
2683 """Redraw the polygon based on the new vertex positions."""
2684 self.line.set_data(self._xs, self._ys)
2685 # Only show one tool handle at the start and end vertex of the polygon
2686 # if the polygon is completed or the user is locked on to the start
2687 # vertex.
2688 if (self._polygon_completed
2689 or (len(self._xs) > 3
2690 and self._xs[-1] == self._xs[0]
2691 and self._ys[-1] == self._ys[0])):
2692 self._polygon_handles.set_data(self._xs[:-1], self._ys[:-1])
2693 else:
2694 self._polygon_handles.set_data(self._xs, self._ys)
2695 self.update()
2697 @property
2698 def verts(self):
2699 """The polygon vertices, as a list of ``(x, y)`` pairs."""
2700 return list(zip(self._xs[:-1], self._ys[:-1]))
2703class Lasso(AxesWidget):
2704 """
2705 Selection curve of an arbitrary shape.
2707 The selected path can be used in conjunction with
2708 `~matplotlib.path.Path.contains_point` to select data points from an image.
2710 Unlike `LassoSelector`, this must be initialized with a starting
2711 point `xy`, and the `Lasso` events are destroyed upon release.
2713 Parameters
2714 ----------
2715 ax : `~matplotlib.axes.Axes`
2716 The parent axes for the widget.
2717 xy : (float, float)
2718 Coordinates of the start of the lasso.
2719 callback : callable
2720 Whenever the lasso is released, the `callback` function is called and
2721 passed the vertices of the selected path.
2722 """
2724 def __init__(self, ax, xy, callback=None, useblit=True):
2725 AxesWidget.__init__(self, ax)
2727 self.useblit = useblit and self.canvas.supports_blit
2728 if self.useblit:
2729 self.background = self.canvas.copy_from_bbox(self.ax.bbox)
2731 x, y = xy
2732 self.verts = [(x, y)]
2733 self.line = Line2D([x], [y], linestyle='-', color='black', lw=2)
2734 self.ax.add_line(self.line)
2735 self.callback = callback
2736 self.connect_event('button_release_event', self.onrelease)
2737 self.connect_event('motion_notify_event', self.onmove)
2739 def onrelease(self, event):
2740 if self.ignore(event):
2741 return
2742 if self.verts is not None:
2743 self.verts.append((event.xdata, event.ydata))
2744 if len(self.verts) > 2:
2745 self.callback(self.verts)
2746 self.ax.lines.remove(self.line)
2747 self.verts = None
2748 self.disconnect_events()
2750 def onmove(self, event):
2751 if self.ignore(event):
2752 return
2753 if self.verts is None:
2754 return
2755 if event.inaxes != self.ax:
2756 return
2757 if event.button != 1:
2758 return
2759 self.verts.append((event.xdata, event.ydata))
2761 self.line.set_data(list(zip(*self.verts)))
2763 if self.useblit:
2764 self.canvas.restore_region(self.background)
2765 self.ax.draw_artist(self.line)
2766 self.canvas.blit(self.ax.bbox)
2767 else:
2768 self.canvas.draw_idle()