You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

2816 lines
92 KiB

4 years ago
  1. """
  2. GUI neutral widgets
  3. ===================
  4. Widgets that are designed to work for any of the GUI backends.
  5. All of these widgets require you to predefine a :class:`matplotlib.axes.Axes`
  6. instance and pass that as the first arg. matplotlib doesn't try to
  7. be too smart with respect to layout -- you will have to figure out how
  8. wide and tall you want your Axes to be to accommodate your widget.
  9. """
  10. import copy
  11. from numbers import Integral
  12. import numpy as np
  13. from . import rcParams
  14. from .lines import Line2D
  15. from .patches import Circle, Rectangle, Ellipse
  16. from .transforms import blended_transform_factory
  17. class LockDraw(object):
  18. """
  19. Some widgets, like the cursor, draw onto the canvas, and this is not
  20. desirable under all circumstances, like when the toolbar is in zoom-to-rect
  21. mode and drawing a rectangle. To avoid this, a widget can acquire a
  22. canvas' lock with ``canvas.widgetlock(widget)`` before drawing on the
  23. canvas; this will prevent other widgets from doing so at the same time (if
  24. they also try to acquire the lock first).
  25. """
  26. def __init__(self):
  27. self._owner = None
  28. def __call__(self, o):
  29. """Reserve the lock for *o*."""
  30. if not self.available(o):
  31. raise ValueError('already locked')
  32. self._owner = o
  33. def release(self, o):
  34. """Release the lock from *o*."""
  35. if not self.available(o):
  36. raise ValueError('you do not own this lock')
  37. self._owner = None
  38. def available(self, o):
  39. """Return whether drawing is available to *o*."""
  40. return not self.locked() or self.isowner(o)
  41. def isowner(self, o):
  42. """Return whether *o* owns this lock."""
  43. return self._owner is o
  44. def locked(self):
  45. """Return whether the lock is currently held by an owner."""
  46. return self._owner is not None
  47. class Widget(object):
  48. """
  49. Abstract base class for GUI neutral widgets
  50. """
  51. drawon = True
  52. eventson = True
  53. _active = True
  54. def set_active(self, active):
  55. """Set whether the widget is active.
  56. """
  57. self._active = active
  58. def get_active(self):
  59. """Get whether the widget is active.
  60. """
  61. return self._active
  62. # set_active is overridden by SelectorWidgets.
  63. active = property(get_active, lambda self, active: self.set_active(active),
  64. doc="Is the widget active?")
  65. def ignore(self, event):
  66. """Return True if event should be ignored.
  67. This method (or a version of it) should be called at the beginning
  68. of any event callback.
  69. """
  70. return not self.active
  71. class AxesWidget(Widget):
  72. """Widget that is connected to a single
  73. :class:`~matplotlib.axes.Axes`.
  74. To guarantee that the widget remains responsive and not garbage-collected,
  75. a reference to the object should be maintained by the user.
  76. This is necessary because the callback registry
  77. maintains only weak-refs to the functions, which are member
  78. functions of the widget. If there are no references to the widget
  79. object it may be garbage collected which will disconnect the
  80. callbacks.
  81. Attributes:
  82. *ax* : :class:`~matplotlib.axes.Axes`
  83. The parent axes for the widget
  84. *canvas* : :class:`~matplotlib.backend_bases.FigureCanvasBase` subclass
  85. The parent figure canvas for the widget.
  86. *active* : bool
  87. If False, the widget does not respond to events.
  88. """
  89. def __init__(self, ax):
  90. self.ax = ax
  91. self.canvas = ax.figure.canvas
  92. self.cids = []
  93. def connect_event(self, event, callback):
  94. """Connect callback with an event.
  95. This should be used in lieu of `figure.canvas.mpl_connect` since this
  96. function stores callback ids for later clean up.
  97. """
  98. cid = self.canvas.mpl_connect(event, callback)
  99. self.cids.append(cid)
  100. def disconnect_events(self):
  101. """Disconnect all events created by this widget."""
  102. for c in self.cids:
  103. self.canvas.mpl_disconnect(c)
  104. class Button(AxesWidget):
  105. """
  106. A GUI neutral button.
  107. For the button to remain responsive you must keep a reference to it.
  108. Call :meth:`on_clicked` to connect to the button.
  109. Attributes
  110. ----------
  111. ax :
  112. The :class:`matplotlib.axes.Axes` the button renders into.
  113. label :
  114. A :class:`matplotlib.text.Text` instance.
  115. color :
  116. The color of the button when not hovering.
  117. hovercolor :
  118. The color of the button when hovering.
  119. """
  120. def __init__(self, ax, label, image=None,
  121. color='0.85', hovercolor='0.95'):
  122. """
  123. Parameters
  124. ----------
  125. ax : matplotlib.axes.Axes
  126. The :class:`matplotlib.axes.Axes` instance the button
  127. will be placed into.
  128. label : str
  129. The button text. Accepts string.
  130. image : array, mpl image, Pillow Image
  131. The image to place in the button, if not *None*.
  132. Can be any legal arg to imshow (numpy array,
  133. matplotlib Image instance, or Pillow Image).
  134. color : color
  135. The color of the button when not activated
  136. hovercolor : color
  137. The color of the button when the mouse is over it
  138. """
  139. AxesWidget.__init__(self, ax)
  140. if image is not None:
  141. ax.imshow(image)
  142. self.label = ax.text(0.5, 0.5, label,
  143. verticalalignment='center',
  144. horizontalalignment='center',
  145. transform=ax.transAxes)
  146. self.cnt = 0
  147. self.observers = {}
  148. self.connect_event('button_press_event', self._click)
  149. self.connect_event('button_release_event', self._release)
  150. self.connect_event('motion_notify_event', self._motion)
  151. ax.set_navigate(False)
  152. ax.set_facecolor(color)
  153. ax.set_xticks([])
  154. ax.set_yticks([])
  155. self.color = color
  156. self.hovercolor = hovercolor
  157. self._lastcolor = color
  158. def _click(self, event):
  159. if self.ignore(event):
  160. return
  161. if event.inaxes != self.ax:
  162. return
  163. if not self.eventson:
  164. return
  165. if event.canvas.mouse_grabber != self.ax:
  166. event.canvas.grab_mouse(self.ax)
  167. def _release(self, event):
  168. if self.ignore(event):
  169. return
  170. if event.canvas.mouse_grabber != self.ax:
  171. return
  172. event.canvas.release_mouse(self.ax)
  173. if not self.eventson:
  174. return
  175. if event.inaxes != self.ax:
  176. return
  177. for cid, func in self.observers.items():
  178. func(event)
  179. def _motion(self, event):
  180. if self.ignore(event):
  181. return
  182. if event.inaxes == self.ax:
  183. c = self.hovercolor
  184. else:
  185. c = self.color
  186. if c != self._lastcolor:
  187. self.ax.set_facecolor(c)
  188. self._lastcolor = c
  189. if self.drawon:
  190. self.ax.figure.canvas.draw()
  191. def on_clicked(self, func):
  192. """
  193. When the button is clicked, call this *func* with event.
  194. A connection id is returned. It can be used to disconnect
  195. the button from its callback.
  196. """
  197. cid = self.cnt
  198. self.observers[cid] = func
  199. self.cnt += 1
  200. return cid
  201. def disconnect(self, cid):
  202. """remove the observer with connection id *cid*"""
  203. try:
  204. del self.observers[cid]
  205. except KeyError:
  206. pass
  207. class Slider(AxesWidget):
  208. """
  209. A slider representing a floating point range.
  210. Create a slider from *valmin* to *valmax* in axes *ax*. For the slider to
  211. remain responsive you must maintain a reference to it. Call
  212. :meth:`on_changed` to connect to the slider event.
  213. Attributes
  214. ----------
  215. val : float
  216. Slider value.
  217. """
  218. def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt='%1.2f',
  219. closedmin=True, closedmax=True, slidermin=None,
  220. slidermax=None, dragging=True, valstep=None, **kwargs):
  221. """
  222. Parameters
  223. ----------
  224. ax : Axes
  225. The Axes to put the slider in.
  226. label : str
  227. Slider label.
  228. valmin : float
  229. The minimum value of the slider.
  230. valmax : float
  231. The maximum value of the slider.
  232. valinit : float, optional, default: 0.5
  233. The slider initial position.
  234. valfmt : str, optional, default: "%1.2f"
  235. Used to format the slider value, fprint format string.
  236. closedmin : bool, optional, default: True
  237. Indicate whether the slider interval is closed on the bottom.
  238. closedmax : bool, optional, default: True
  239. Indicate whether the slider interval is closed on the top.
  240. slidermin : Slider, optional, default: None
  241. Do not allow the current slider to have a value less than
  242. the value of the Slider `slidermin`.
  243. slidermax : Slider, optional, default: None
  244. Do not allow the current slider to have a value greater than
  245. the value of the Slider `slidermax`.
  246. dragging : bool, optional, default: True
  247. If True the slider can be dragged by the mouse.
  248. valstep : float, optional, default: None
  249. If given, the slider will snap to multiples of `valstep`.
  250. Notes
  251. -----
  252. Additional kwargs are passed on to ``self.poly`` which is the
  253. :class:`~matplotlib.patches.Rectangle` that draws the slider
  254. knob. See the :class:`~matplotlib.patches.Rectangle` documentation for
  255. valid property names (e.g., `facecolor`, `edgecolor`, `alpha`).
  256. """
  257. AxesWidget.__init__(self, ax)
  258. if slidermin is not None and not hasattr(slidermin, 'val'):
  259. raise ValueError("Argument slidermin ({}) has no 'val'"
  260. .format(type(slidermin)))
  261. if slidermax is not None and not hasattr(slidermax, 'val'):
  262. raise ValueError("Argument slidermax ({}) has no 'val'"
  263. .format(type(slidermax)))
  264. self.closedmin = closedmin
  265. self.closedmax = closedmax
  266. self.slidermin = slidermin
  267. self.slidermax = slidermax
  268. self.drag_active = False
  269. self.valmin = valmin
  270. self.valmax = valmax
  271. self.valstep = valstep
  272. valinit = self._value_in_bounds(valinit)
  273. if valinit is None:
  274. valinit = valmin
  275. self.val = valinit
  276. self.valinit = valinit
  277. self.poly = ax.axvspan(valmin, valinit, 0, 1, **kwargs)
  278. self.vline = ax.axvline(valinit, 0, 1, color='r', lw=1)
  279. self.valfmt = valfmt
  280. ax.set_yticks([])
  281. ax.set_xlim((valmin, valmax))
  282. ax.set_xticks([])
  283. ax.set_navigate(False)
  284. self.connect_event('button_press_event', self._update)
  285. self.connect_event('button_release_event', self._update)
  286. if dragging:
  287. self.connect_event('motion_notify_event', self._update)
  288. self.label = ax.text(-0.02, 0.5, label, transform=ax.transAxes,
  289. verticalalignment='center',
  290. horizontalalignment='right')
  291. self.valtext = ax.text(1.02, 0.5, valfmt % valinit,
  292. transform=ax.transAxes,
  293. verticalalignment='center',
  294. horizontalalignment='left')
  295. self.cnt = 0
  296. self.observers = {}
  297. self.set_val(valinit)
  298. def _value_in_bounds(self, val):
  299. """ Makes sure self.val is with given bounds."""
  300. if self.valstep:
  301. val = np.round((val - self.valmin)/self.valstep)*self.valstep
  302. val += self.valmin
  303. if val <= self.valmin:
  304. if not self.closedmin:
  305. return
  306. val = self.valmin
  307. elif val >= self.valmax:
  308. if not self.closedmax:
  309. return
  310. val = self.valmax
  311. if self.slidermin is not None and val <= self.slidermin.val:
  312. if not self.closedmin:
  313. return
  314. val = self.slidermin.val
  315. if self.slidermax is not None and val >= self.slidermax.val:
  316. if not self.closedmax:
  317. return
  318. val = self.slidermax.val
  319. return val
  320. def _update(self, event):
  321. """update the slider position"""
  322. if self.ignore(event):
  323. return
  324. if event.button != 1:
  325. return
  326. if event.name == 'button_press_event' and event.inaxes == self.ax:
  327. self.drag_active = True
  328. event.canvas.grab_mouse(self.ax)
  329. if not self.drag_active:
  330. return
  331. elif ((event.name == 'button_release_event') or
  332. (event.name == 'button_press_event' and
  333. event.inaxes != self.ax)):
  334. self.drag_active = False
  335. event.canvas.release_mouse(self.ax)
  336. return
  337. val = self._value_in_bounds(event.xdata)
  338. if val not in [None, self.val]:
  339. self.set_val(val)
  340. def set_val(self, val):
  341. """
  342. Set slider value to *val*
  343. Parameters
  344. ----------
  345. val : float
  346. """
  347. xy = self.poly.xy
  348. xy[2] = val, 1
  349. xy[3] = val, 0
  350. self.poly.xy = xy
  351. self.valtext.set_text(self.valfmt % val)
  352. if self.drawon:
  353. self.ax.figure.canvas.draw_idle()
  354. self.val = val
  355. if not self.eventson:
  356. return
  357. for cid, func in self.observers.items():
  358. func(val)
  359. def on_changed(self, func):
  360. """
  361. When the slider value is changed call *func* with the new
  362. slider value
  363. Parameters
  364. ----------
  365. func : callable
  366. Function to call when slider is changed.
  367. The function must accept a single float as its arguments.
  368. Returns
  369. -------
  370. cid : int
  371. Connection id (which can be used to disconnect *func*)
  372. """
  373. cid = self.cnt
  374. self.observers[cid] = func
  375. self.cnt += 1
  376. return cid
  377. def disconnect(self, cid):
  378. """
  379. Remove the observer with connection id *cid*
  380. Parameters
  381. ----------
  382. cid : int
  383. Connection id of the observer to be removed
  384. """
  385. try:
  386. del self.observers[cid]
  387. except KeyError:
  388. pass
  389. def reset(self):
  390. """Reset the slider to the initial value"""
  391. if self.val != self.valinit:
  392. self.set_val(self.valinit)
  393. class CheckButtons(AxesWidget):
  394. """
  395. A GUI neutral set of check buttons.
  396. For the check buttons to remain responsive you must keep a
  397. reference to this object.
  398. The following attributes are exposed
  399. *ax*
  400. The :class:`matplotlib.axes.Axes` instance the buttons are
  401. located in
  402. *labels*
  403. List of :class:`matplotlib.text.Text` instances
  404. *lines*
  405. List of (line1, line2) tuples for the x's in the check boxes.
  406. These lines exist for each box, but have ``set_visible(False)``
  407. when its box is not checked.
  408. *rectangles*
  409. List of :class:`matplotlib.patches.Rectangle` instances
  410. Connect to the CheckButtons with the :meth:`on_clicked` method
  411. """
  412. def __init__(self, ax, labels, actives=None):
  413. """
  414. Add check buttons to :class:`matplotlib.axes.Axes` instance *ax*
  415. Parameters
  416. ----------
  417. ax : `~matplotlib.axes.Axes`
  418. The parent axes for the widget.
  419. labels : List[str]
  420. The labels of the check buttons.
  421. actives : List[bool], optional
  422. The initial check states of the buttons. The list must have the
  423. same length as *labels*. If not given, all buttons are unchecked.
  424. """
  425. AxesWidget.__init__(self, ax)
  426. ax.set_xticks([])
  427. ax.set_yticks([])
  428. ax.set_navigate(False)
  429. if actives is None:
  430. actives = [False] * len(labels)
  431. if len(labels) > 1:
  432. dy = 1. / (len(labels) + 1)
  433. ys = np.linspace(1 - dy, dy, len(labels))
  434. else:
  435. dy = 0.25
  436. ys = [0.5]
  437. axcolor = ax.get_facecolor()
  438. self.labels = []
  439. self.lines = []
  440. self.rectangles = []
  441. lineparams = {'color': 'k', 'linewidth': 1.25,
  442. 'transform': ax.transAxes, 'solid_capstyle': 'butt'}
  443. for y, label, active in zip(ys, labels, actives):
  444. t = ax.text(0.25, y, label, transform=ax.transAxes,
  445. horizontalalignment='left',
  446. verticalalignment='center')
  447. w, h = dy / 2, dy / 2
  448. x, y = 0.05, y - h / 2
  449. p = Rectangle(xy=(x, y), width=w, height=h, edgecolor='black',
  450. facecolor=axcolor, transform=ax.transAxes)
  451. l1 = Line2D([x, x + w], [y + h, y], **lineparams)
  452. l2 = Line2D([x, x + w], [y, y + h], **lineparams)
  453. l1.set_visible(active)
  454. l2.set_visible(active)
  455. self.labels.append(t)
  456. self.rectangles.append(p)
  457. self.lines.append((l1, l2))
  458. ax.add_patch(p)
  459. ax.add_line(l1)
  460. ax.add_line(l2)
  461. self.connect_event('button_press_event', self._clicked)
  462. self.cnt = 0
  463. self.observers = {}
  464. def _clicked(self, event):
  465. if self.ignore(event) or event.button != 1 or event.inaxes != self.ax:
  466. return
  467. for i, (p, t) in enumerate(zip(self.rectangles, self.labels)):
  468. if (t.get_window_extent().contains(event.x, event.y) or
  469. p.get_window_extent().contains(event.x, event.y)):
  470. self.set_active(i)
  471. break
  472. def set_active(self, index):
  473. """
  474. Directly (de)activate a check button by index.
  475. *index* is an index into the original label list
  476. that this object was constructed with.
  477. Raises ValueError if *index* is invalid.
  478. Callbacks will be triggered if :attr:`eventson` is True.
  479. """
  480. if 0 > index >= len(self.labels):
  481. raise ValueError("Invalid CheckButton index: %d" % index)
  482. l1, l2 = self.lines[index]
  483. l1.set_visible(not l1.get_visible())
  484. l2.set_visible(not l2.get_visible())
  485. if self.drawon:
  486. self.ax.figure.canvas.draw()
  487. if not self.eventson:
  488. return
  489. for cid, func in self.observers.items():
  490. func(self.labels[index].get_text())
  491. def get_status(self):
  492. """
  493. returns a tuple of the status (True/False) of all of the check buttons
  494. """
  495. return [l1.get_visible() for (l1, l2) in self.lines]
  496. def on_clicked(self, func):
  497. """
  498. When the button is clicked, call *func* with button label
  499. A connection id is returned which can be used to disconnect
  500. """
  501. cid = self.cnt
  502. self.observers[cid] = func
  503. self.cnt += 1
  504. return cid
  505. def disconnect(self, cid):
  506. """remove the observer with connection id *cid*"""
  507. try:
  508. del self.observers[cid]
  509. except KeyError:
  510. pass
  511. class TextBox(AxesWidget):
  512. """
  513. A GUI neutral text input box.
  514. For the text box to remain responsive you must keep a reference to it.
  515. The following attributes are accessible:
  516. *ax*
  517. The :class:`matplotlib.axes.Axes` the button renders into.
  518. *label*
  519. A :class:`matplotlib.text.Text` instance.
  520. *color*
  521. The color of the text box when not hovering.
  522. *hovercolor*
  523. The color of the text box when hovering.
  524. Call :meth:`on_text_change` to be updated whenever the text changes.
  525. Call :meth:`on_submit` to be updated whenever the user hits enter or
  526. leaves the text entry field.
  527. """
  528. def __init__(self, ax, label, initial='',
  529. color='.95', hovercolor='1', label_pad=.01):
  530. """
  531. Parameters
  532. ----------
  533. ax : matplotlib.axes.Axes
  534. The :class:`matplotlib.axes.Axes` instance the button
  535. will be placed into.
  536. label : str
  537. Label for this text box. Accepts string.
  538. initial : str
  539. Initial value in the text box
  540. color : color
  541. The color of the box
  542. hovercolor : color
  543. The color of the box when the mouse is over it
  544. label_pad : float
  545. the distance between the label and the right side of the textbox
  546. """
  547. AxesWidget.__init__(self, ax)
  548. self.DIST_FROM_LEFT = .05
  549. self.params_to_disable = [key for key in rcParams if 'keymap' in key]
  550. self.text = initial
  551. self.label = ax.text(-label_pad, 0.5, label,
  552. verticalalignment='center',
  553. horizontalalignment='right',
  554. transform=ax.transAxes)
  555. self.text_disp = self._make_text_disp(self.text)
  556. self.cnt = 0
  557. self.change_observers = {}
  558. self.submit_observers = {}
  559. # If these lines are removed, the cursor won't appear the first
  560. # time the box is clicked:
  561. self.ax.set_xlim(0, 1)
  562. self.ax.set_ylim(0, 1)
  563. self.cursor_index = 0
  564. # Because this is initialized, _render_cursor
  565. # can assume that cursor exists.
  566. self.cursor = self.ax.vlines(0, 0, 0)
  567. self.cursor.set_visible(False)
  568. self.connect_event('button_press_event', self._click)
  569. self.connect_event('button_release_event', self._release)
  570. self.connect_event('motion_notify_event', self._motion)
  571. self.connect_event('key_press_event', self._keypress)
  572. self.connect_event('resize_event', self._resize)
  573. ax.set_navigate(False)
  574. ax.set_facecolor(color)
  575. ax.set_xticks([])
  576. ax.set_yticks([])
  577. self.color = color
  578. self.hovercolor = hovercolor
  579. self._lastcolor = color
  580. self.capturekeystrokes = False
  581. def _make_text_disp(self, string):
  582. return self.ax.text(self.DIST_FROM_LEFT, 0.5, string,
  583. verticalalignment='center',
  584. horizontalalignment='left',
  585. transform=self.ax.transAxes)
  586. def _rendercursor(self):
  587. # this is a hack to figure out where the cursor should go.
  588. # we draw the text up to where the cursor should go, measure
  589. # and save its dimensions, draw the real text, then put the cursor
  590. # at the saved dimensions
  591. widthtext = self.text[:self.cursor_index]
  592. no_text = False
  593. if(widthtext == "" or widthtext == " " or widthtext == " "):
  594. no_text = widthtext == ""
  595. widthtext = ","
  596. wt_disp = self._make_text_disp(widthtext)
  597. self.ax.figure.canvas.draw()
  598. bb = wt_disp.get_window_extent()
  599. inv = self.ax.transData.inverted()
  600. bb = inv.transform(bb)
  601. wt_disp.set_visible(False)
  602. if no_text:
  603. bb[1, 0] = bb[0, 0]
  604. # hack done
  605. self.cursor.set_visible(False)
  606. self.cursor = self.ax.vlines(bb[1, 0], bb[0, 1], bb[1, 1])
  607. self.ax.figure.canvas.draw()
  608. def _notify_submit_observers(self):
  609. for cid, func in self.submit_observers.items():
  610. func(self.text)
  611. def _release(self, event):
  612. if self.ignore(event):
  613. return
  614. if event.canvas.mouse_grabber != self.ax:
  615. return
  616. event.canvas.release_mouse(self.ax)
  617. def _keypress(self, event):
  618. if self.ignore(event):
  619. return
  620. if self.capturekeystrokes:
  621. key = event.key
  622. if(len(key) == 1):
  623. self.text = (self.text[:self.cursor_index] + key +
  624. self.text[self.cursor_index:])
  625. self.cursor_index += 1
  626. elif key == "right":
  627. if self.cursor_index != len(self.text):
  628. self.cursor_index += 1
  629. elif key == "left":
  630. if self.cursor_index != 0:
  631. self.cursor_index -= 1
  632. elif key == "home":
  633. self.cursor_index = 0
  634. elif key == "end":
  635. self.cursor_index = len(self.text)
  636. elif(key == "backspace"):
  637. if self.cursor_index != 0:
  638. self.text = (self.text[:self.cursor_index - 1] +
  639. self.text[self.cursor_index:])
  640. self.cursor_index -= 1
  641. elif(key == "delete"):
  642. if self.cursor_index != len(self.text):
  643. self.text = (self.text[:self.cursor_index] +
  644. self.text[self.cursor_index + 1:])
  645. self.text_disp.remove()
  646. self.text_disp = self._make_text_disp(self.text)
  647. self._rendercursor()
  648. self._notify_change_observers()
  649. if key == "enter":
  650. self._notify_submit_observers()
  651. def set_val(self, val):
  652. newval = str(val)
  653. if self.text == newval:
  654. return
  655. self.text = newval
  656. self.text_disp.remove()
  657. self.text_disp = self._make_text_disp(self.text)
  658. self._rendercursor()
  659. self._notify_change_observers()
  660. self._notify_submit_observers()
  661. def _notify_change_observers(self):
  662. for cid, func in self.change_observers.items():
  663. func(self.text)
  664. def begin_typing(self, x):
  665. self.capturekeystrokes = True
  666. # disable command keys so that the user can type without
  667. # command keys causing figure to be saved, etc
  668. self.reset_params = {}
  669. for key in self.params_to_disable:
  670. self.reset_params[key] = rcParams[key]
  671. rcParams[key] = []
  672. def stop_typing(self):
  673. notifysubmit = False
  674. # because _notify_submit_users might throw an error in the
  675. # user's code, we only want to call it once we've already done
  676. # our cleanup.
  677. if self.capturekeystrokes:
  678. # since the user is no longer typing,
  679. # reactivate the standard command keys
  680. for key in self.params_to_disable:
  681. rcParams[key] = self.reset_params[key]
  682. notifysubmit = True
  683. self.capturekeystrokes = False
  684. self.cursor.set_visible(False)
  685. self.ax.figure.canvas.draw()
  686. if notifysubmit:
  687. self._notify_submit_observers()
  688. def position_cursor(self, x):
  689. # now, we have to figure out where the cursor goes.
  690. # approximate it based on assuming all characters the same length
  691. if len(self.text) == 0:
  692. self.cursor_index = 0
  693. else:
  694. bb = self.text_disp.get_window_extent()
  695. trans = self.ax.transData
  696. inv = self.ax.transData.inverted()
  697. bb = trans.transform(inv.transform(bb))
  698. text_start = bb[0, 0]
  699. text_end = bb[1, 0]
  700. ratio = (x - text_start) / (text_end - text_start)
  701. if ratio < 0:
  702. ratio = 0
  703. if ratio > 1:
  704. ratio = 1
  705. self.cursor_index = int(len(self.text) * ratio)
  706. self._rendercursor()
  707. def _click(self, event):
  708. if self.ignore(event):
  709. return
  710. if event.inaxes != self.ax:
  711. self.stop_typing()
  712. return
  713. if not self.eventson:
  714. return
  715. if event.canvas.mouse_grabber != self.ax:
  716. event.canvas.grab_mouse(self.ax)
  717. if not self.capturekeystrokes:
  718. self.begin_typing(event.x)
  719. self.position_cursor(event.x)
  720. def _resize(self, event):
  721. self.stop_typing()
  722. def _motion(self, event):
  723. if self.ignore(event):
  724. return
  725. if event.inaxes == self.ax:
  726. c = self.hovercolor
  727. else:
  728. c = self.color
  729. if c != self._lastcolor:
  730. self.ax.set_facecolor(c)
  731. self._lastcolor = c
  732. if self.drawon:
  733. self.ax.figure.canvas.draw()
  734. def on_text_change(self, func):
  735. """
  736. When the text changes, call this *func* with event.
  737. A connection id is returned which can be used to disconnect.
  738. """
  739. cid = self.cnt
  740. self.change_observers[cid] = func
  741. self.cnt += 1
  742. return cid
  743. def on_submit(self, func):
  744. """
  745. When the user hits enter or leaves the submission box, call this
  746. *func* with event.
  747. A connection id is returned which can be used to disconnect.
  748. """
  749. cid = self.cnt
  750. self.submit_observers[cid] = func
  751. self.cnt += 1
  752. return cid
  753. def disconnect(self, cid):
  754. """Remove the observer with connection id *cid*."""
  755. for reg in [self.change_observers, self.submit_observers]:
  756. try:
  757. del reg[cid]
  758. except KeyError:
  759. pass
  760. class RadioButtons(AxesWidget):
  761. """
  762. A GUI neutral radio button.
  763. For the buttons to remain responsive
  764. you must keep a reference to this object.
  765. The following attributes are exposed:
  766. *ax*
  767. The :class:`matplotlib.axes.Axes` instance the buttons are in
  768. *activecolor*
  769. The color of the button when clicked
  770. *labels*
  771. A list of :class:`matplotlib.text.Text` instances
  772. *circles*
  773. A list of :class:`matplotlib.patches.Circle` instances
  774. *value_selected*
  775. A string listing the current value selected
  776. Connect to the RadioButtons with the :meth:`on_clicked` method
  777. """
  778. def __init__(self, ax, labels, active=0, activecolor='blue'):
  779. """
  780. Add radio buttons to :class:`matplotlib.axes.Axes` instance *ax*
  781. *labels*
  782. A len(buttons) list of labels as strings
  783. *active*
  784. The index into labels for the button that is active
  785. *activecolor*
  786. The color of the button when clicked
  787. """
  788. AxesWidget.__init__(self, ax)
  789. self.activecolor = activecolor
  790. self.value_selected = None
  791. ax.set_xticks([])
  792. ax.set_yticks([])
  793. ax.set_navigate(False)
  794. dy = 1. / (len(labels) + 1)
  795. ys = np.linspace(1 - dy, dy, len(labels))
  796. cnt = 0
  797. axcolor = ax.get_facecolor()
  798. # scale the radius of the circle with the spacing between each one
  799. circle_radius = (dy / 2) - 0.01
  800. # defaul to hard-coded value if the radius becomes too large
  801. if(circle_radius > 0.05):
  802. circle_radius = 0.05
  803. self.labels = []
  804. self.circles = []
  805. for y, label in zip(ys, labels):
  806. t = ax.text(0.25, y, label, transform=ax.transAxes,
  807. horizontalalignment='left',
  808. verticalalignment='center')
  809. if cnt == active:
  810. self.value_selected = label
  811. facecolor = activecolor
  812. else:
  813. facecolor = axcolor
  814. p = Circle(xy=(0.15, y), radius=circle_radius, edgecolor='black',
  815. facecolor=facecolor, transform=ax.transAxes)
  816. self.labels.append(t)
  817. self.circles.append(p)
  818. ax.add_patch(p)
  819. cnt += 1
  820. self.connect_event('button_press_event', self._clicked)
  821. self.cnt = 0
  822. self.observers = {}
  823. def _clicked(self, event):
  824. if self.ignore(event) or event.button != 1 or event.inaxes != self.ax:
  825. return
  826. xy = self.ax.transAxes.inverted().transform_point((event.x, event.y))
  827. pclicked = np.array([xy[0], xy[1]])
  828. for i, (p, t) in enumerate(zip(self.circles, self.labels)):
  829. if (t.get_window_extent().contains(event.x, event.y)
  830. or np.linalg.norm(pclicked - p.center) < p.radius):
  831. self.set_active(i)
  832. break
  833. def set_active(self, index):
  834. """
  835. Trigger which radio button to make active.
  836. *index* is an index into the original label list
  837. that this object was constructed with.
  838. Raise ValueError if the index is invalid.
  839. Callbacks will be triggered if :attr:`eventson` is True.
  840. """
  841. if 0 > index >= len(self.labels):
  842. raise ValueError("Invalid RadioButton index: %d" % index)
  843. self.value_selected = self.labels[index].get_text()
  844. for i, p in enumerate(self.circles):
  845. if i == index:
  846. color = self.activecolor
  847. else:
  848. color = self.ax.get_facecolor()
  849. p.set_facecolor(color)
  850. if self.drawon:
  851. self.ax.figure.canvas.draw()
  852. if not self.eventson:
  853. return
  854. for cid, func in self.observers.items():
  855. func(self.labels[index].get_text())
  856. def on_clicked(self, func):
  857. """
  858. When the button is clicked, call *func* with button label
  859. A connection id is returned which can be used to disconnect
  860. """
  861. cid = self.cnt
  862. self.observers[cid] = func
  863. self.cnt += 1
  864. return cid
  865. def disconnect(self, cid):
  866. """remove the observer with connection id *cid*"""
  867. try:
  868. del self.observers[cid]
  869. except KeyError:
  870. pass
  871. class SubplotTool(Widget):
  872. """
  873. A tool to adjust the subplot params of a :class:`matplotlib.figure.Figure`.
  874. """
  875. def __init__(self, targetfig, toolfig):
  876. """
  877. *targetfig*
  878. The figure instance to adjust.
  879. *toolfig*
  880. The figure instance to embed the subplot tool into. If
  881. *None*, a default figure will be created. If you are using
  882. this from the GUI
  883. """
  884. # FIXME: The docstring seems to just abruptly end without...
  885. self.targetfig = targetfig
  886. toolfig.subplots_adjust(left=0.2, right=0.9)
  887. class toolbarfmt:
  888. def __init__(self, slider):
  889. self.slider = slider
  890. def __call__(self, x, y):
  891. fmt = '%s=%s' % (self.slider.label.get_text(),
  892. self.slider.valfmt)
  893. return fmt % x
  894. self.axleft = toolfig.add_subplot(711)
  895. self.axleft.set_title('Click on slider to adjust subplot param')
  896. self.axleft.set_navigate(False)
  897. self.sliderleft = Slider(self.axleft, 'left',
  898. 0, 1, targetfig.subplotpars.left,
  899. closedmax=False)
  900. self.sliderleft.on_changed(self.funcleft)
  901. self.axbottom = toolfig.add_subplot(712)
  902. self.axbottom.set_navigate(False)
  903. self.sliderbottom = Slider(self.axbottom,
  904. 'bottom', 0, 1,
  905. targetfig.subplotpars.bottom,
  906. closedmax=False)
  907. self.sliderbottom.on_changed(self.funcbottom)
  908. self.axright = toolfig.add_subplot(713)
  909. self.axright.set_navigate(False)
  910. self.sliderright = Slider(self.axright, 'right', 0, 1,
  911. targetfig.subplotpars.right,
  912. closedmin=False)
  913. self.sliderright.on_changed(self.funcright)
  914. self.axtop = toolfig.add_subplot(714)
  915. self.axtop.set_navigate(False)
  916. self.slidertop = Slider(self.axtop, 'top', 0, 1,
  917. targetfig.subplotpars.top,
  918. closedmin=False)
  919. self.slidertop.on_changed(self.functop)
  920. self.axwspace = toolfig.add_subplot(715)
  921. self.axwspace.set_navigate(False)
  922. self.sliderwspace = Slider(self.axwspace, 'wspace',
  923. 0, 1, targetfig.subplotpars.wspace,
  924. closedmax=False)
  925. self.sliderwspace.on_changed(self.funcwspace)
  926. self.axhspace = toolfig.add_subplot(716)
  927. self.axhspace.set_navigate(False)
  928. self.sliderhspace = Slider(self.axhspace, 'hspace',
  929. 0, 1, targetfig.subplotpars.hspace,
  930. closedmax=False)
  931. self.sliderhspace.on_changed(self.funchspace)
  932. # constraints
  933. self.sliderleft.slidermax = self.sliderright
  934. self.sliderright.slidermin = self.sliderleft
  935. self.sliderbottom.slidermax = self.slidertop
  936. self.slidertop.slidermin = self.sliderbottom
  937. bax = toolfig.add_axes([0.8, 0.05, 0.15, 0.075])
  938. self.buttonreset = Button(bax, 'Reset')
  939. sliders = (self.sliderleft, self.sliderbottom, self.sliderright,
  940. self.slidertop, self.sliderwspace, self.sliderhspace,)
  941. def func(event):
  942. thisdrawon = self.drawon
  943. self.drawon = False
  944. # store the drawon state of each slider
  945. bs = []
  946. for slider in sliders:
  947. bs.append(slider.drawon)
  948. slider.drawon = False
  949. # reset the slider to the initial position
  950. for slider in sliders:
  951. slider.reset()
  952. # reset drawon
  953. for slider, b in zip(sliders, bs):
  954. slider.drawon = b
  955. # draw the canvas
  956. self.drawon = thisdrawon
  957. if self.drawon:
  958. toolfig.canvas.draw()
  959. self.targetfig.canvas.draw()
  960. # during reset there can be a temporary invalid state
  961. # depending on the order of the reset so we turn off
  962. # validation for the resetting
  963. validate = toolfig.subplotpars.validate
  964. toolfig.subplotpars.validate = False
  965. self.buttonreset.on_clicked(func)
  966. toolfig.subplotpars.validate = validate
  967. def funcleft(self, val):
  968. self.targetfig.subplots_adjust(left=val)
  969. if self.drawon:
  970. self.targetfig.canvas.draw()
  971. def funcright(self, val):
  972. self.targetfig.subplots_adjust(right=val)
  973. if self.drawon:
  974. self.targetfig.canvas.draw()
  975. def funcbottom(self, val):
  976. self.targetfig.subplots_adjust(bottom=val)
  977. if self.drawon:
  978. self.targetfig.canvas.draw()
  979. def functop(self, val):
  980. self.targetfig.subplots_adjust(top=val)
  981. if self.drawon:
  982. self.targetfig.canvas.draw()
  983. def funcwspace(self, val):
  984. self.targetfig.subplots_adjust(wspace=val)
  985. if self.drawon:
  986. self.targetfig.canvas.draw()
  987. def funchspace(self, val):
  988. self.targetfig.subplots_adjust(hspace=val)
  989. if self.drawon:
  990. self.targetfig.canvas.draw()
  991. class Cursor(AxesWidget):
  992. """
  993. A horizontal and vertical line that spans the axes and moves with
  994. the pointer. You can turn off the hline or vline respectively with
  995. the following attributes:
  996. *horizOn*
  997. Controls the visibility of the horizontal line
  998. *vertOn*
  999. Controls the visibility of the horizontal line
  1000. and the visibility of the cursor itself with the *visible* attribute.
  1001. For the cursor to remain responsive you must keep a reference to
  1002. it.
  1003. """
  1004. def __init__(self, ax, horizOn=True, vertOn=True, useblit=False,
  1005. **lineprops):
  1006. """
  1007. Add a cursor to *ax*. If ``useblit=True``, use the backend-dependent
  1008. blitting features for faster updates. *lineprops* is a dictionary of
  1009. line properties.
  1010. """
  1011. AxesWidget.__init__(self, ax)
  1012. self.connect_event('motion_notify_event', self.onmove)
  1013. self.connect_event('draw_event', self.clear)
  1014. self.visible = True
  1015. self.horizOn = horizOn
  1016. self.vertOn = vertOn
  1017. self.useblit = useblit and self.canvas.supports_blit
  1018. if self.useblit:
  1019. lineprops['animated'] = True
  1020. self.lineh = ax.axhline(ax.get_ybound()[0], visible=False, **lineprops)
  1021. self.linev = ax.axvline(ax.get_xbound()[0], visible=False, **lineprops)
  1022. self.background = None
  1023. self.needclear = False
  1024. def clear(self, event):
  1025. """clear the cursor"""
  1026. if self.ignore(event):
  1027. return
  1028. if self.useblit:
  1029. self.background = self.canvas.copy_from_bbox(self.ax.bbox)
  1030. self.linev.set_visible(False)
  1031. self.lineh.set_visible(False)
  1032. def onmove(self, event):
  1033. """on mouse motion draw the cursor if visible"""
  1034. if self.ignore(event):
  1035. return
  1036. if not self.canvas.widgetlock.available(self):
  1037. return
  1038. if event.inaxes != self.ax:
  1039. self.linev.set_visible(False)
  1040. self.lineh.set_visible(False)
  1041. if self.needclear:
  1042. self.canvas.draw()
  1043. self.needclear = False
  1044. return
  1045. self.needclear = True
  1046. if not self.visible:
  1047. return
  1048. self.linev.set_xdata((event.xdata, event.xdata))
  1049. self.lineh.set_ydata((event.ydata, event.ydata))
  1050. self.linev.set_visible(self.visible and self.vertOn)
  1051. self.lineh.set_visible(self.visible and self.horizOn)
  1052. self._update()
  1053. def _update(self):
  1054. if self.useblit:
  1055. if self.background is not None:
  1056. self.canvas.restore_region(self.background)
  1057. self.ax.draw_artist(self.linev)
  1058. self.ax.draw_artist(self.lineh)
  1059. self.canvas.blit(self.ax.bbox)
  1060. else:
  1061. self.canvas.draw_idle()
  1062. return False
  1063. class MultiCursor(Widget):
  1064. """
  1065. Provide a vertical (default) and/or horizontal line cursor shared between
  1066. multiple axes.
  1067. For the cursor to remain responsive you must keep a reference to
  1068. it.
  1069. Example usage::
  1070. from matplotlib.widgets import MultiCursor
  1071. import matplotlib.pyplot as plt
  1072. import numpy as np
  1073. fig, (ax1, ax2) = plt.subplots(nrows=2, sharex=True)
  1074. t = np.arange(0.0, 2.0, 0.01)
  1075. ax1.plot(t, np.sin(2*np.pi*t))
  1076. ax2.plot(t, np.sin(4*np.pi*t))
  1077. multi = MultiCursor(fig.canvas, (ax1, ax2), color='r', lw=1,
  1078. horizOn=False, vertOn=True)
  1079. plt.show()
  1080. """
  1081. def __init__(self, canvas, axes, useblit=True, horizOn=False, vertOn=True,
  1082. **lineprops):
  1083. self.canvas = canvas
  1084. self.axes = axes
  1085. self.horizOn = horizOn
  1086. self.vertOn = vertOn
  1087. xmin, xmax = axes[-1].get_xlim()
  1088. ymin, ymax = axes[-1].get_ylim()
  1089. xmid = 0.5 * (xmin + xmax)
  1090. ymid = 0.5 * (ymin + ymax)
  1091. self.visible = True
  1092. self.useblit = useblit and self.canvas.supports_blit
  1093. self.background = None
  1094. self.needclear = False
  1095. if self.useblit:
  1096. lineprops['animated'] = True
  1097. if vertOn:
  1098. self.vlines = [ax.axvline(xmid, visible=False, **lineprops)
  1099. for ax in axes]
  1100. else:
  1101. self.vlines = []
  1102. if horizOn:
  1103. self.hlines = [ax.axhline(ymid, visible=False, **lineprops)
  1104. for ax in axes]
  1105. else:
  1106. self.hlines = []
  1107. self.connect()
  1108. def connect(self):
  1109. """connect events"""
  1110. self._cidmotion = self.canvas.mpl_connect('motion_notify_event',
  1111. self.onmove)
  1112. self._ciddraw = self.canvas.mpl_connect('draw_event', self.clear)
  1113. def disconnect(self):
  1114. """disconnect events"""
  1115. self.canvas.mpl_disconnect(self._cidmotion)
  1116. self.canvas.mpl_disconnect(self._ciddraw)
  1117. def clear(self, event):
  1118. """clear the cursor"""
  1119. if self.ignore(event):
  1120. return
  1121. if self.useblit:
  1122. self.background = (
  1123. self.canvas.copy_from_bbox(self.canvas.figure.bbox))
  1124. for line in self.vlines + self.hlines:
  1125. line.set_visible(False)
  1126. def onmove(self, event):
  1127. if self.ignore(event):
  1128. return
  1129. if event.inaxes is None:
  1130. return
  1131. if not self.canvas.widgetlock.available(self):
  1132. return
  1133. self.needclear = True
  1134. if not self.visible:
  1135. return
  1136. if self.vertOn:
  1137. for line in self.vlines:
  1138. line.set_xdata((event.xdata, event.xdata))
  1139. line.set_visible(self.visible)
  1140. if self.horizOn:
  1141. for line in self.hlines:
  1142. line.set_ydata((event.ydata, event.ydata))
  1143. line.set_visible(self.visible)
  1144. self._update()
  1145. def _update(self):
  1146. if self.useblit:
  1147. if self.background is not None:
  1148. self.canvas.restore_region(self.background)
  1149. if self.vertOn:
  1150. for ax, line in zip(self.axes, self.vlines):
  1151. ax.draw_artist(line)
  1152. if self.horizOn:
  1153. for ax, line in zip(self.axes, self.hlines):
  1154. ax.draw_artist(line)
  1155. self.canvas.blit(self.canvas.figure.bbox)
  1156. else:
  1157. self.canvas.draw_idle()
  1158. class _SelectorWidget(AxesWidget):
  1159. def __init__(self, ax, onselect, useblit=False, button=None,
  1160. state_modifier_keys=None):
  1161. AxesWidget.__init__(self, ax)
  1162. self.visible = True
  1163. self.onselect = onselect
  1164. self.useblit = useblit and self.canvas.supports_blit
  1165. self.connect_default_events()
  1166. self.state_modifier_keys = dict(move=' ', clear='escape',
  1167. square='shift', center='control')
  1168. self.state_modifier_keys.update(state_modifier_keys or {})
  1169. self.background = None
  1170. self.artists = []
  1171. if isinstance(button, Integral):
  1172. self.validButtons = [button]
  1173. else:
  1174. self.validButtons = button
  1175. # will save the data (position at mouseclick)
  1176. self.eventpress = None
  1177. # will save the data (pos. at mouserelease)
  1178. self.eventrelease = None
  1179. self._prev_event = None
  1180. self.state = set()
  1181. def set_active(self, active):
  1182. AxesWidget.set_active(self, active)
  1183. if active:
  1184. self.update_background(None)
  1185. def update_background(self, event):
  1186. """force an update of the background"""
  1187. # If you add a call to `ignore` here, you'll want to check edge case:
  1188. # `release` can call a draw event even when `ignore` is True.
  1189. if self.useblit:
  1190. self.background = self.canvas.copy_from_bbox(self.ax.bbox)
  1191. def connect_default_events(self):
  1192. """Connect the major canvas events to methods."""
  1193. self.connect_event('motion_notify_event', self.onmove)
  1194. self.connect_event('button_press_event', self.press)
  1195. self.connect_event('button_release_event', self.release)
  1196. self.connect_event('draw_event', self.update_background)
  1197. self.connect_event('key_press_event', self.on_key_press)
  1198. self.connect_event('key_release_event', self.on_key_release)
  1199. self.connect_event('scroll_event', self.on_scroll)
  1200. def ignore(self, event):
  1201. """return *True* if *event* should be ignored"""
  1202. if not self.active or not self.ax.get_visible():
  1203. return True
  1204. # If canvas was locked
  1205. if not self.canvas.widgetlock.available(self):
  1206. return True
  1207. if not hasattr(event, 'button'):
  1208. event.button = None
  1209. # Only do rectangle selection if event was triggered
  1210. # with a desired button
  1211. if self.validButtons is not None:
  1212. if event.button not in self.validButtons:
  1213. return True
  1214. # If no button was pressed yet ignore the event if it was out
  1215. # of the axes
  1216. if self.eventpress is None:
  1217. return event.inaxes != self.ax
  1218. # If a button was pressed, check if the release-button is the
  1219. # same.
  1220. if event.button == self.eventpress.button:
  1221. return False
  1222. # If a button was pressed, check if the release-button is the
  1223. # same.
  1224. return (event.inaxes != self.ax or
  1225. event.button != self.eventpress.button)
  1226. def update(self):
  1227. """draw using newfangled blit or oldfangled draw depending on
  1228. useblit
  1229. """
  1230. if not self.ax.get_visible():
  1231. return False
  1232. if self.useblit:
  1233. if self.background is not None:
  1234. self.canvas.restore_region(self.background)
  1235. for artist in self.artists:
  1236. self.ax.draw_artist(artist)
  1237. self.canvas.blit(self.ax.bbox)
  1238. else:
  1239. self.canvas.draw_idle()
  1240. return False
  1241. def _get_data(self, event):
  1242. """Get the xdata and ydata for event, with limits"""
  1243. if event.xdata is None:
  1244. return None, None
  1245. x0, x1 = self.ax.get_xbound()
  1246. y0, y1 = self.ax.get_ybound()
  1247. xdata = max(x0, event.xdata)
  1248. xdata = min(x1, xdata)
  1249. ydata = max(y0, event.ydata)
  1250. ydata = min(y1, ydata)
  1251. return xdata, ydata
  1252. def _clean_event(self, event):
  1253. """Clean up an event
  1254. Use prev event if there is no xdata
  1255. Limit the xdata and ydata to the axes limits
  1256. Set the prev event
  1257. """
  1258. if event.xdata is None:
  1259. event = self._prev_event
  1260. else:
  1261. event = copy.copy(event)
  1262. event.xdata, event.ydata = self._get_data(event)
  1263. self._prev_event = event
  1264. return event
  1265. def press(self, event):
  1266. """Button press handler and validator"""
  1267. if not self.ignore(event):
  1268. event = self._clean_event(event)
  1269. self.eventpress = event
  1270. self._prev_event = event
  1271. key = event.key or ''
  1272. key = key.replace('ctrl', 'control')
  1273. # move state is locked in on a button press
  1274. if key == self.state_modifier_keys['move']:
  1275. self.state.add('move')
  1276. self._press(event)
  1277. return True
  1278. return False
  1279. def _press(self, event):
  1280. """Button press handler"""
  1281. pass
  1282. def release(self, event):
  1283. """Button release event handler and validator"""
  1284. if not self.ignore(event) and self.eventpress:
  1285. event = self._clean_event(event)
  1286. self.eventrelease = event
  1287. self._release(event)
  1288. self.eventpress = None
  1289. self.eventrelease = None
  1290. self.state.discard('move')
  1291. return True
  1292. return False
  1293. def _release(self, event):
  1294. """Button release event handler"""
  1295. pass
  1296. def onmove(self, event):
  1297. """Cursor move event handler and validator"""
  1298. if not self.ignore(event) and self.eventpress:
  1299. event = self._clean_event(event)
  1300. self._onmove(event)
  1301. return True
  1302. return False
  1303. def _onmove(self, event):
  1304. """Cursor move event handler"""
  1305. pass
  1306. def on_scroll(self, event):
  1307. """Mouse scroll event handler and validator"""
  1308. if not self.ignore(event):
  1309. self._on_scroll(event)
  1310. def _on_scroll(self, event):
  1311. """Mouse scroll event handler"""
  1312. pass
  1313. def on_key_press(self, event):
  1314. """Key press event handler and validator for all selection widgets"""
  1315. if self.active:
  1316. key = event.key or ''
  1317. key = key.replace('ctrl', 'control')
  1318. if key == self.state_modifier_keys['clear']:
  1319. for artist in self.artists:
  1320. artist.set_visible(False)
  1321. self.update()
  1322. return
  1323. for (state, modifier) in self.state_modifier_keys.items():
  1324. if modifier in key:
  1325. self.state.add(state)
  1326. self._on_key_press(event)
  1327. def _on_key_press(self, event):
  1328. """Key press event handler - use for widget-specific key press actions.
  1329. """
  1330. pass
  1331. def on_key_release(self, event):
  1332. """Key release event handler and validator"""
  1333. if self.active:
  1334. key = event.key or ''
  1335. for (state, modifier) in self.state_modifier_keys.items():
  1336. if modifier in key:
  1337. self.state.discard(state)
  1338. self._on_key_release(event)
  1339. def _on_key_release(self, event):
  1340. """Key release event handler"""
  1341. pass
  1342. def set_visible(self, visible):
  1343. """ Set the visibility of our artists """
  1344. self.visible = visible
  1345. for artist in self.artists:
  1346. artist.set_visible(visible)
  1347. class SpanSelector(_SelectorWidget):
  1348. """
  1349. Visually select a min/max range on a single axis and call a function with
  1350. those values.
  1351. To guarantee that the selector remains responsive, keep a reference to it.
  1352. In order to turn off the SpanSelector, set `span_selector.active=False`. To
  1353. turn it back on, set `span_selector.active=True`.
  1354. Parameters
  1355. ----------
  1356. ax : :class:`matplotlib.axes.Axes` object
  1357. onselect : func(min, max), min/max are floats
  1358. direction : "horizontal" or "vertical"
  1359. The axis along which to draw the span selector
  1360. minspan : float, default is None
  1361. If selection is less than *minspan*, do not call *onselect*
  1362. useblit : bool, default is False
  1363. If True, use the backend-dependent blitting features for faster
  1364. canvas updates.
  1365. rectprops : dict, default is None
  1366. Dictionary of :class:`matplotlib.patches.Patch` properties
  1367. onmove_callback : func(min, max), min/max are floats, default is None
  1368. Called on mouse move while the span is being selected
  1369. span_stays : bool, default is False
  1370. If True, the span stays visible after the mouse is released
  1371. button : int or list of ints
  1372. Determines which mouse buttons activate the span selector
  1373. 1 = left mouse button\n
  1374. 2 = center mouse button (scroll wheel)\n
  1375. 3 = right mouse button\n
  1376. Examples
  1377. --------
  1378. >>> import matplotlib.pyplot as plt
  1379. >>> import matplotlib.widgets as mwidgets
  1380. >>> fig, ax = plt.subplots()
  1381. >>> ax.plot([1, 2, 3], [10, 50, 100])
  1382. >>> def onselect(vmin, vmax):
  1383. ... print(vmin, vmax)
  1384. >>> rectprops = dict(facecolor='blue', alpha=0.5)
  1385. >>> span = mwidgets.SpanSelector(ax, onselect, 'horizontal',
  1386. ... rectprops=rectprops)
  1387. >>> fig.show()
  1388. See also: :doc:`/gallery/widgets/span_selector`
  1389. """
  1390. def __init__(self, ax, onselect, direction, minspan=None, useblit=False,
  1391. rectprops=None, onmove_callback=None, span_stays=False,
  1392. button=None):
  1393. _SelectorWidget.__init__(self, ax, onselect, useblit=useblit,
  1394. button=button)
  1395. if rectprops is None:
  1396. rectprops = dict(facecolor='red', alpha=0.5)
  1397. rectprops['animated'] = self.useblit
  1398. if direction not in ['horizontal', 'vertical']:
  1399. raise ValueError("direction must be 'horizontal' or 'vertical'")
  1400. self.direction = direction
  1401. self.rect = None
  1402. self.pressv = None
  1403. self.rectprops = rectprops
  1404. self.onmove_callback = onmove_callback
  1405. self.minspan = minspan
  1406. self.span_stays = span_stays
  1407. # Needed when dragging out of axes
  1408. self.prev = (0, 0)
  1409. # Reset canvas so that `new_axes` connects events.
  1410. self.canvas = None
  1411. self.new_axes(ax)
  1412. def new_axes(self, ax):
  1413. """Set SpanSelector to operate on a new Axes"""
  1414. self.ax = ax
  1415. if self.canvas is not ax.figure.canvas:
  1416. if self.canvas is not None:
  1417. self.disconnect_events()
  1418. self.canvas = ax.figure.canvas
  1419. self.connect_default_events()
  1420. if self.direction == 'horizontal':
  1421. trans = blended_transform_factory(self.ax.transData,
  1422. self.ax.transAxes)
  1423. w, h = 0, 1
  1424. else:
  1425. trans = blended_transform_factory(self.ax.transAxes,
  1426. self.ax.transData)
  1427. w, h = 1, 0
  1428. self.rect = Rectangle((0, 0), w, h,
  1429. transform=trans,
  1430. visible=False,
  1431. **self.rectprops)
  1432. if self.span_stays:
  1433. self.stay_rect = Rectangle((0, 0), w, h,
  1434. transform=trans,
  1435. visible=False,
  1436. **self.rectprops)
  1437. self.stay_rect.set_animated(False)
  1438. self.ax.add_patch(self.stay_rect)
  1439. self.ax.add_patch(self.rect)
  1440. self.artists = [self.rect]
  1441. def ignore(self, event):
  1442. """return *True* if *event* should be ignored"""
  1443. return _SelectorWidget.ignore(self, event) or not self.visible
  1444. def _press(self, event):
  1445. """on button press event"""
  1446. self.rect.set_visible(self.visible)
  1447. if self.span_stays:
  1448. self.stay_rect.set_visible(False)
  1449. # really force a draw so that the stay rect is not in
  1450. # the blit background
  1451. if self.useblit:
  1452. self.canvas.draw()
  1453. xdata, ydata = self._get_data(event)
  1454. if self.direction == 'horizontal':
  1455. self.pressv = xdata
  1456. else:
  1457. self.pressv = ydata
  1458. self._set_span_xy(event)
  1459. return False
  1460. def _release(self, event):
  1461. """on button release event"""
  1462. if self.pressv is None:
  1463. return
  1464. self.buttonDown = False
  1465. self.rect.set_visible(False)
  1466. if self.span_stays:
  1467. self.stay_rect.set_x(self.rect.get_x())
  1468. self.stay_rect.set_y(self.rect.get_y())
  1469. self.stay_rect.set_width(self.rect.get_width())
  1470. self.stay_rect.set_height(self.rect.get_height())
  1471. self.stay_rect.set_visible(True)
  1472. self.canvas.draw_idle()
  1473. vmin = self.pressv
  1474. xdata, ydata = self._get_data(event)
  1475. if self.direction == 'horizontal':
  1476. vmax = xdata or self.prev[0]
  1477. else:
  1478. vmax = ydata or self.prev[1]
  1479. if vmin > vmax:
  1480. vmin, vmax = vmax, vmin
  1481. span = vmax - vmin
  1482. if self.minspan is not None and span < self.minspan:
  1483. return
  1484. self.onselect(vmin, vmax)
  1485. self.pressv = None
  1486. return False
  1487. def _onmove(self, event):
  1488. """on motion notify event"""
  1489. if self.pressv is None:
  1490. return
  1491. self._set_span_xy(event)
  1492. if self.onmove_callback is not None:
  1493. vmin = self.pressv
  1494. xdata, ydata = self._get_data(event)
  1495. if self.direction == 'horizontal':
  1496. vmax = xdata or self.prev[0]
  1497. else:
  1498. vmax = ydata or self.prev[1]
  1499. if vmin > vmax:
  1500. vmin, vmax = vmax, vmin
  1501. self.onmove_callback(vmin, vmax)
  1502. self.update()
  1503. return False
  1504. def _set_span_xy(self, event):
  1505. """Setting the span coordinates"""
  1506. x, y = self._get_data(event)
  1507. if x is None:
  1508. return
  1509. self.prev = x, y
  1510. if self.direction == 'horizontal':
  1511. v = x
  1512. else:
  1513. v = y
  1514. minv, maxv = v, self.pressv
  1515. if minv > maxv:
  1516. minv, maxv = maxv, minv
  1517. if self.direction == 'horizontal':
  1518. self.rect.set_x(minv)
  1519. self.rect.set_width(maxv - minv)
  1520. else:
  1521. self.rect.set_y(minv)
  1522. self.rect.set_height(maxv - minv)
  1523. class ToolHandles(object):
  1524. """Control handles for canvas tools.
  1525. Parameters
  1526. ----------
  1527. ax : :class:`matplotlib.axes.Axes`
  1528. Matplotlib axes where tool handles are displayed.
  1529. x, y : 1D arrays
  1530. Coordinates of control handles.
  1531. marker : str
  1532. Shape of marker used to display handle. See `matplotlib.pyplot.plot`.
  1533. marker_props : dict
  1534. Additional marker properties. See :class:`matplotlib.lines.Line2D`.
  1535. """
  1536. def __init__(self, ax, x, y, marker='o', marker_props=None, useblit=True):
  1537. self.ax = ax
  1538. props = dict(marker=marker, markersize=7, mfc='w', ls='none',
  1539. alpha=0.5, visible=False, label='_nolegend_')
  1540. props.update(marker_props if marker_props is not None else {})
  1541. self._markers = Line2D(x, y, animated=useblit, **props)
  1542. self.ax.add_line(self._markers)
  1543. self.artist = self._markers
  1544. @property
  1545. def x(self):
  1546. return self._markers.get_xdata()
  1547. @property
  1548. def y(self):
  1549. return self._markers.get_ydata()
  1550. def set_data(self, pts, y=None):
  1551. """Set x and y positions of handles"""
  1552. if y is not None:
  1553. x = pts
  1554. pts = np.array([x, y])
  1555. self._markers.set_data(pts)
  1556. def set_visible(self, val):
  1557. self._markers.set_visible(val)
  1558. def set_animated(self, val):
  1559. self._markers.set_animated(val)
  1560. def closest(self, x, y):
  1561. """Return index and pixel distance to closest index."""
  1562. pts = np.transpose((self.x, self.y))
  1563. # Transform data coordinates to pixel coordinates.
  1564. pts = self.ax.transData.transform(pts)
  1565. diff = pts - ((x, y))
  1566. if diff.ndim == 2:
  1567. dist = np.sqrt(np.sum(diff ** 2, axis=1))
  1568. return np.argmin(dist), np.min(dist)
  1569. else:
  1570. return 0, np.sqrt(np.sum(diff ** 2))
  1571. class RectangleSelector(_SelectorWidget):
  1572. """
  1573. Select a rectangular region of an axes.
  1574. For the cursor to remain responsive you must keep a reference to
  1575. it.
  1576. Example usage::
  1577. import numpy as np
  1578. import matplotlib.pyplot as plt
  1579. from matplotlib.widgets import RectangleSelector
  1580. def onselect(eclick, erelease):
  1581. "eclick and erelease are matplotlib events at press and release."
  1582. print('startposition: (%f, %f)' % (eclick.xdata, eclick.ydata))
  1583. print('endposition : (%f, %f)' % (erelease.xdata, erelease.ydata))
  1584. print('used button : ', eclick.button)
  1585. def toggle_selector(event):
  1586. print('Key pressed.')
  1587. if event.key in ['Q', 'q'] and toggle_selector.RS.active:
  1588. print('RectangleSelector deactivated.')
  1589. toggle_selector.RS.set_active(False)
  1590. if event.key in ['A', 'a'] and not toggle_selector.RS.active:
  1591. print('RectangleSelector activated.')
  1592. toggle_selector.RS.set_active(True)
  1593. x = np.arange(100.) / 99
  1594. y = np.sin(x)
  1595. fig, ax = plt.subplots()
  1596. ax.plot(x, y)
  1597. toggle_selector.RS = RectangleSelector(ax, onselect, drawtype='line')
  1598. fig.canvas.connect('key_press_event', toggle_selector)
  1599. plt.show()
  1600. """
  1601. _shape_klass = Rectangle
  1602. def __init__(self, ax, onselect, drawtype='box',
  1603. minspanx=None, minspany=None, useblit=False,
  1604. lineprops=None, rectprops=None, spancoords='data',
  1605. button=None, maxdist=10, marker_props=None,
  1606. interactive=False, state_modifier_keys=None):
  1607. """
  1608. Create a selector in *ax*. When a selection is made, clear
  1609. the span and call onselect with::
  1610. onselect(pos_1, pos_2)
  1611. and clear the drawn box/line. The ``pos_1`` and ``pos_2`` are
  1612. arrays of length 2 containing the x- and y-coordinate.
  1613. If *minspanx* is not *None* then events smaller than *minspanx*
  1614. in x direction are ignored (it's the same for y).
  1615. The rectangle is drawn with *rectprops*; default::
  1616. rectprops = dict(facecolor='red', edgecolor = 'black',
  1617. alpha=0.2, fill=True)
  1618. The line is drawn with *lineprops*; default::
  1619. lineprops = dict(color='black', linestyle='-',
  1620. linewidth = 2, alpha=0.5)
  1621. Use *drawtype* if you want the mouse to draw a line,
  1622. a box or nothing between click and actual position by setting
  1623. ``drawtype = 'line'``, ``drawtype='box'`` or ``drawtype = 'none'``.
  1624. Drawing a line would result in a line from vertex A to vertex C in
  1625. a rectangle ABCD.
  1626. *spancoords* is one of 'data' or 'pixels'. If 'data', *minspanx*
  1627. and *minspanx* will be interpreted in the same coordinates as
  1628. the x and y axis. If 'pixels', they are in pixels.
  1629. *button* is a list of integers indicating which mouse buttons should
  1630. be used for rectangle selection. You can also specify a single
  1631. integer if only a single button is desired. Default is *None*,
  1632. which does not limit which button can be used.
  1633. Note, typically:
  1634. 1 = left mouse button
  1635. 2 = center mouse button (scroll wheel)
  1636. 3 = right mouse button
  1637. *interactive* will draw a set of handles and allow you interact
  1638. with the widget after it is drawn.
  1639. *state_modifier_keys* are keyboard modifiers that affect the behavior
  1640. of the widget.
  1641. The defaults are:
  1642. dict(move=' ', clear='escape', square='shift', center='ctrl')
  1643. Keyboard modifiers, which:
  1644. 'move': Move the existing shape.
  1645. 'clear': Clear the current shape.
  1646. 'square': Makes the shape square.
  1647. 'center': Make the initial point the center of the shape.
  1648. 'square' and 'center' can be combined.
  1649. """
  1650. _SelectorWidget.__init__(self, ax, onselect, useblit=useblit,
  1651. button=button,
  1652. state_modifier_keys=state_modifier_keys)
  1653. self.to_draw = None
  1654. self.visible = True
  1655. self.interactive = interactive
  1656. if drawtype == 'none':
  1657. drawtype = 'line' # draw a line but make it
  1658. self.visible = False # invisible
  1659. if drawtype == 'box':
  1660. if rectprops is None:
  1661. rectprops = dict(facecolor='red', edgecolor='black',
  1662. alpha=0.2, fill=True)
  1663. rectprops['animated'] = self.useblit
  1664. self.rectprops = rectprops
  1665. self.to_draw = self._shape_klass((0, 0), 0, 1, visible=False,
  1666. **self.rectprops)
  1667. self.ax.add_patch(self.to_draw)
  1668. if drawtype == 'line':
  1669. if lineprops is None:
  1670. lineprops = dict(color='black', linestyle='-',
  1671. linewidth=2, alpha=0.5)
  1672. lineprops['animated'] = self.useblit
  1673. self.lineprops = lineprops
  1674. self.to_draw = Line2D([0, 0], [0, 0], visible=False,
  1675. **self.lineprops)
  1676. self.ax.add_line(self.to_draw)
  1677. self.minspanx = minspanx
  1678. self.minspany = minspany
  1679. if spancoords not in ('data', 'pixels'):
  1680. raise ValueError("'spancoords' must be 'data' or 'pixels'")
  1681. self.spancoords = spancoords
  1682. self.drawtype = drawtype
  1683. self.maxdist = maxdist
  1684. if rectprops is None:
  1685. props = dict(mec='r')
  1686. else:
  1687. props = dict(mec=rectprops.get('edgecolor', 'r'))
  1688. self._corner_order = ['NW', 'NE', 'SE', 'SW']
  1689. xc, yc = self.corners
  1690. self._corner_handles = ToolHandles(self.ax, xc, yc, marker_props=props,
  1691. useblit=self.useblit)
  1692. self._edge_order = ['W', 'N', 'E', 'S']
  1693. xe, ye = self.edge_centers
  1694. self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s',
  1695. marker_props=props,
  1696. useblit=self.useblit)
  1697. xc, yc = self.center
  1698. self._center_handle = ToolHandles(self.ax, [xc], [yc], marker='s',
  1699. marker_props=props,
  1700. useblit=self.useblit)
  1701. self.active_handle = None
  1702. self.artists = [self.to_draw, self._center_handle.artist,
  1703. self._corner_handles.artist,
  1704. self._edge_handles.artist]
  1705. if not self.interactive:
  1706. self.artists = [self.to_draw]
  1707. self._extents_on_press = None
  1708. def _press(self, event):
  1709. """on button press event"""
  1710. # make the drawed box/line visible get the click-coordinates,
  1711. # button, ...
  1712. if self.interactive and self.to_draw.get_visible():
  1713. self._set_active_handle(event)
  1714. else:
  1715. self.active_handle = None
  1716. if self.active_handle is None or not self.interactive:
  1717. # Clear previous rectangle before drawing new rectangle.
  1718. self.update()
  1719. if not self.interactive:
  1720. x = event.xdata
  1721. y = event.ydata
  1722. self.extents = x, x, y, y
  1723. self.set_visible(self.visible)
  1724. def _release(self, event):
  1725. """on button release event"""
  1726. if not self.interactive:
  1727. self.to_draw.set_visible(False)
  1728. # update the eventpress and eventrelease with the resulting extents
  1729. x1, x2, y1, y2 = self.extents
  1730. self.eventpress.xdata = x1
  1731. self.eventpress.ydata = y1
  1732. xy1 = self.ax.transData.transform_point([x1, y1])
  1733. self.eventpress.x, self.eventpress.y = xy1
  1734. self.eventrelease.xdata = x2
  1735. self.eventrelease.ydata = y2
  1736. xy2 = self.ax.transData.transform_point([x2, y2])
  1737. self.eventrelease.x, self.eventrelease.y = xy2
  1738. if self.spancoords == 'data':
  1739. xmin, ymin = self.eventpress.xdata, self.eventpress.ydata
  1740. xmax, ymax = self.eventrelease.xdata, self.eventrelease.ydata
  1741. # calculate dimensions of box or line get values in the right
  1742. # order
  1743. elif self.spancoords == 'pixels':
  1744. xmin, ymin = self.eventpress.x, self.eventpress.y
  1745. xmax, ymax = self.eventrelease.x, self.eventrelease.y
  1746. else:
  1747. raise ValueError('spancoords must be "data" or "pixels"')
  1748. if xmin > xmax:
  1749. xmin, xmax = xmax, xmin
  1750. if ymin > ymax:
  1751. ymin, ymax = ymax, ymin
  1752. spanx = xmax - xmin
  1753. spany = ymax - ymin
  1754. xproblems = self.minspanx is not None and spanx < self.minspanx
  1755. yproblems = self.minspany is not None and spany < self.minspany
  1756. # check if drawn distance (if it exists) is not too small in
  1757. # either x or y-direction
  1758. if self.drawtype != 'none' and (xproblems or yproblems):
  1759. for artist in self.artists:
  1760. artist.set_visible(False)
  1761. self.update()
  1762. return
  1763. # call desired function
  1764. self.onselect(self.eventpress, self.eventrelease)
  1765. self.update()
  1766. return False
  1767. def _onmove(self, event):
  1768. """on motion notify event if box/line is wanted"""
  1769. # resize an existing shape
  1770. if self.active_handle and not self.active_handle == 'C':
  1771. x1, x2, y1, y2 = self._extents_on_press
  1772. if self.active_handle in ['E', 'W'] + self._corner_order:
  1773. x2 = event.xdata
  1774. if self.active_handle in ['N', 'S'] + self._corner_order:
  1775. y2 = event.ydata
  1776. # move existing shape
  1777. elif (('move' in self.state or self.active_handle == 'C')
  1778. and self._extents_on_press is not None):
  1779. x1, x2, y1, y2 = self._extents_on_press
  1780. dx = event.xdata - self.eventpress.xdata
  1781. dy = event.ydata - self.eventpress.ydata
  1782. x1 += dx
  1783. x2 += dx
  1784. y1 += dy
  1785. y2 += dy
  1786. # new shape
  1787. else:
  1788. center = [self.eventpress.xdata, self.eventpress.ydata]
  1789. center_pix = [self.eventpress.x, self.eventpress.y]
  1790. dx = (event.xdata - center[0]) / 2.
  1791. dy = (event.ydata - center[1]) / 2.
  1792. # square shape
  1793. if 'square' in self.state:
  1794. dx_pix = abs(event.x - center_pix[0])
  1795. dy_pix = abs(event.y - center_pix[1])
  1796. if not dx_pix:
  1797. return
  1798. maxd = max(abs(dx_pix), abs(dy_pix))
  1799. if abs(dx_pix) < maxd:
  1800. dx *= maxd / (abs(dx_pix) + 1e-6)
  1801. if abs(dy_pix) < maxd:
  1802. dy *= maxd / (abs(dy_pix) + 1e-6)
  1803. # from center
  1804. if 'center' in self.state:
  1805. dx *= 2
  1806. dy *= 2
  1807. # from corner
  1808. else:
  1809. center[0] += dx
  1810. center[1] += dy
  1811. x1, x2, y1, y2 = (center[0] - dx, center[0] + dx,
  1812. center[1] - dy, center[1] + dy)
  1813. self.extents = x1, x2, y1, y2
  1814. @property
  1815. def _rect_bbox(self):
  1816. if self.drawtype == 'box':
  1817. x0 = self.to_draw.get_x()
  1818. y0 = self.to_draw.get_y()
  1819. width = self.to_draw.get_width()
  1820. height = self.to_draw.get_height()
  1821. return x0, y0, width, height
  1822. else:
  1823. x, y = self.to_draw.get_data()
  1824. x0, x1 = min(x), max(x)
  1825. y0, y1 = min(y), max(y)
  1826. return x0, y0, x1 - x0, y1 - y0
  1827. @property
  1828. def corners(self):
  1829. """Corners of rectangle from lower left, moving clockwise."""
  1830. x0, y0, width, height = self._rect_bbox
  1831. xc = x0, x0 + width, x0 + width, x0
  1832. yc = y0, y0, y0 + height, y0 + height
  1833. return xc, yc
  1834. @property
  1835. def edge_centers(self):
  1836. """Midpoint of rectangle edges from left, moving clockwise."""
  1837. x0, y0, width, height = self._rect_bbox
  1838. w = width / 2.
  1839. h = height / 2.
  1840. xe = x0, x0 + w, x0 + width, x0 + w
  1841. ye = y0 + h, y0, y0 + h, y0 + height
  1842. return xe, ye
  1843. @property
  1844. def center(self):
  1845. """Center of rectangle"""
  1846. x0, y0, width, height = self._rect_bbox
  1847. return x0 + width / 2., y0 + height / 2.
  1848. @property
  1849. def extents(self):
  1850. """Return (xmin, xmax, ymin, ymax)."""
  1851. x0, y0, width, height = self._rect_bbox
  1852. xmin, xmax = sorted([x0, x0 + width])
  1853. ymin, ymax = sorted([y0, y0 + height])
  1854. return xmin, xmax, ymin, ymax
  1855. @extents.setter
  1856. def extents(self, extents):
  1857. # Update displayed shape
  1858. self.draw_shape(extents)
  1859. # Update displayed handles
  1860. self._corner_handles.set_data(*self.corners)
  1861. self._edge_handles.set_data(*self.edge_centers)
  1862. self._center_handle.set_data(*self.center)
  1863. self.set_visible(self.visible)
  1864. self.update()
  1865. def draw_shape(self, extents):
  1866. x0, x1, y0, y1 = extents
  1867. xmin, xmax = sorted([x0, x1])
  1868. ymin, ymax = sorted([y0, y1])
  1869. xlim = sorted(self.ax.get_xlim())
  1870. ylim = sorted(self.ax.get_ylim())
  1871. xmin = max(xlim[0], xmin)
  1872. ymin = max(ylim[0], ymin)
  1873. xmax = min(xmax, xlim[1])
  1874. ymax = min(ymax, ylim[1])
  1875. if self.drawtype == 'box':
  1876. self.to_draw.set_x(xmin)
  1877. self.to_draw.set_y(ymin)
  1878. self.to_draw.set_width(xmax - xmin)
  1879. self.to_draw.set_height(ymax - ymin)
  1880. elif self.drawtype == 'line':
  1881. self.to_draw.set_data([xmin, xmax], [ymin, ymax])
  1882. def _set_active_handle(self, event):
  1883. """Set active handle based on the location of the mouse event"""
  1884. # Note: event.xdata/ydata in data coordinates, event.x/y in pixels
  1885. c_idx, c_dist = self._corner_handles.closest(event.x, event.y)
  1886. e_idx, e_dist = self._edge_handles.closest(event.x, event.y)
  1887. m_idx, m_dist = self._center_handle.closest(event.x, event.y)
  1888. if 'move' in self.state:
  1889. self.active_handle = 'C'
  1890. self._extents_on_press = self.extents
  1891. # Set active handle as closest handle, if mouse click is close enough.
  1892. elif m_dist < self.maxdist * 2:
  1893. self.active_handle = 'C'
  1894. elif c_dist > self.maxdist and e_dist > self.maxdist:
  1895. self.active_handle = None
  1896. return
  1897. elif c_dist < e_dist:
  1898. self.active_handle = self._corner_order[c_idx]
  1899. else:
  1900. self.active_handle = self._edge_order[e_idx]
  1901. # Save coordinates of rectangle at the start of handle movement.
  1902. x1, x2, y1, y2 = self.extents
  1903. # Switch variables so that only x2 and/or y2 are updated on move.
  1904. if self.active_handle in ['W', 'SW', 'NW']:
  1905. x1, x2 = x2, event.xdata
  1906. if self.active_handle in ['N', 'NW', 'NE']:
  1907. y1, y2 = y2, event.ydata
  1908. self._extents_on_press = x1, x2, y1, y2
  1909. @property
  1910. def geometry(self):
  1911. """
  1912. Returns numpy.ndarray of shape (2,5) containing
  1913. x (``RectangleSelector.geometry[1,:]``) and
  1914. y (``RectangleSelector.geometry[0,:]``)
  1915. coordinates of the four corners of the rectangle starting
  1916. and ending in the top left corner.
  1917. """
  1918. if hasattr(self.to_draw, 'get_verts'):
  1919. xfm = self.ax.transData.inverted()
  1920. y, x = xfm.transform(self.to_draw.get_verts()).T
  1921. return np.array([x, y])
  1922. else:
  1923. return np.array(self.to_draw.get_data())
  1924. class EllipseSelector(RectangleSelector):
  1925. """
  1926. Select an elliptical region of an axes.
  1927. For the cursor to remain responsive you must keep a reference to
  1928. it.
  1929. Example usage::
  1930. import numpy as np
  1931. import matplotlib.pyplot as plt
  1932. from matplotlib.widgets import EllipseSelector
  1933. def onselect(eclick, erelease):
  1934. "eclick and erelease are matplotlib events at press and release."
  1935. print('startposition: (%f, %f)' % (eclick.xdata, eclick.ydata))
  1936. print('endposition : (%f, %f)' % (erelease.xdata, erelease.ydata))
  1937. print('used button : ', eclick.button)
  1938. def toggle_selector(event):
  1939. print(' Key pressed.')
  1940. if event.key in ['Q', 'q'] and toggle_selector.ES.active:
  1941. print('EllipseSelector deactivated.')
  1942. toggle_selector.RS.set_active(False)
  1943. if event.key in ['A', 'a'] and not toggle_selector.ES.active:
  1944. print('EllipseSelector activated.')
  1945. toggle_selector.ES.set_active(True)
  1946. x = np.arange(100.) / 99
  1947. y = np.sin(x)
  1948. fig, ax = plt.subplots()
  1949. ax.plot(x, y)
  1950. toggle_selector.ES = EllipseSelector(ax, onselect, drawtype='line')
  1951. fig.canvas.connect('key_press_event', toggle_selector)
  1952. plt.show()
  1953. """
  1954. _shape_klass = Ellipse
  1955. def draw_shape(self, extents):
  1956. x1, x2, y1, y2 = extents
  1957. xmin, xmax = sorted([x1, x2])
  1958. ymin, ymax = sorted([y1, y2])
  1959. center = [x1 + (x2 - x1) / 2., y1 + (y2 - y1) / 2.]
  1960. a = (xmax - xmin) / 2.
  1961. b = (ymax - ymin) / 2.
  1962. if self.drawtype == 'box':
  1963. self.to_draw.center = center
  1964. self.to_draw.width = 2 * a
  1965. self.to_draw.height = 2 * b
  1966. else:
  1967. rad = np.deg2rad(np.arange(31) * 12)
  1968. x = a * np.cos(rad) + center[0]
  1969. y = b * np.sin(rad) + center[1]
  1970. self.to_draw.set_data(x, y)
  1971. @property
  1972. def _rect_bbox(self):
  1973. if self.drawtype == 'box':
  1974. x, y = self.to_draw.center
  1975. width = self.to_draw.width
  1976. height = self.to_draw.height
  1977. return x - width / 2., y - height / 2., width, height
  1978. else:
  1979. x, y = self.to_draw.get_data()
  1980. x0, x1 = min(x), max(x)
  1981. y0, y1 = min(y), max(y)
  1982. return x0, y0, x1 - x0, y1 - y0
  1983. class LassoSelector(_SelectorWidget):
  1984. """
  1985. Selection curve of an arbitrary shape.
  1986. For the selector to remain responsive you must keep a reference to it.
  1987. The selected path can be used in conjunction with `~.Path.contains_point`
  1988. to select data points from an image.
  1989. In contrast to `Lasso`, `LassoSelector` is written with an interface
  1990. similar to `RectangleSelector` and `SpanSelector`, and will continue to
  1991. interact with the axes until disconnected.
  1992. Example usage::
  1993. ax = subplot(111)
  1994. ax.plot(x,y)
  1995. def onselect(verts):
  1996. print(verts)
  1997. lasso = LassoSelector(ax, onselect)
  1998. Parameters
  1999. ----------
  2000. ax : :class:`~matplotlib.axes.Axes`
  2001. The parent axes for the widget.
  2002. onselect : function
  2003. Whenever the lasso is released, the *onselect* function is called and
  2004. passed the vertices of the selected path.
  2005. button : List[Int], optional
  2006. A list of integers indicating which mouse buttons should be used for
  2007. rectangle selection. You can also specify a single integer if only a
  2008. single button is desired. Default is ``None``, which does not limit
  2009. which button can be used.
  2010. Note, typically:
  2011. - 1 = left mouse button
  2012. - 2 = center mouse button (scroll wheel)
  2013. - 3 = right mouse button
  2014. """
  2015. def __init__(self, ax, onselect=None, useblit=True, lineprops=None,
  2016. button=None):
  2017. _SelectorWidget.__init__(self, ax, onselect, useblit=useblit,
  2018. button=button)
  2019. self.verts = None
  2020. if lineprops is None:
  2021. lineprops = dict()
  2022. if useblit:
  2023. lineprops['animated'] = True
  2024. self.line = Line2D([], [], **lineprops)
  2025. self.line.set_visible(False)
  2026. self.ax.add_line(self.line)
  2027. self.artists = [self.line]
  2028. def onpress(self, event):
  2029. self.press(event)
  2030. def _press(self, event):
  2031. self.verts = [self._get_data(event)]
  2032. self.line.set_visible(True)
  2033. def onrelease(self, event):
  2034. self.release(event)
  2035. def _release(self, event):
  2036. if self.verts is not None:
  2037. self.verts.append(self._get_data(event))
  2038. self.onselect(self.verts)
  2039. self.line.set_data([[], []])
  2040. self.line.set_visible(False)
  2041. self.verts = None
  2042. def _onmove(self, event):
  2043. if self.verts is None:
  2044. return
  2045. self.verts.append(self._get_data(event))
  2046. self.line.set_data(list(zip(*self.verts)))
  2047. self.update()
  2048. class PolygonSelector(_SelectorWidget):
  2049. """Select a polygon region of an axes.
  2050. Place vertices with each mouse click, and make the selection by completing
  2051. the polygon (clicking on the first vertex). Hold the *ctrl* key and click
  2052. and drag a vertex to reposition it (the *ctrl* key is not necessary if the
  2053. polygon has already been completed). Hold the *shift* key and click and
  2054. drag anywhere in the axes to move all vertices. Press the *esc* key to
  2055. start a new polygon.
  2056. For the selector to remain responsive you must keep a reference to
  2057. it.
  2058. Parameters
  2059. ----------
  2060. ax : :class:`~matplotlib.axes.Axes`
  2061. The parent axes for the widget.
  2062. onselect : function
  2063. When a polygon is completed or modified after completion,
  2064. the `onselect` function is called and passed a list of the vertices as
  2065. ``(xdata, ydata)`` tuples.
  2066. useblit : bool, optional
  2067. lineprops : dict, optional
  2068. The line for the sides of the polygon is drawn with the properties
  2069. given by `lineprops`. The default is ``dict(color='k', linestyle='-',
  2070. linewidth=2, alpha=0.5)``.
  2071. markerprops : dict, optional
  2072. The markers for the vertices of the polygon are drawn with the
  2073. properties given by `markerprops`. The default is ``dict(marker='o',
  2074. markersize=7, mec='k', mfc='k', alpha=0.5)``.
  2075. vertex_select_radius : float, optional
  2076. A vertex is selected (to complete the polygon or to move a vertex)
  2077. if the mouse click is within `vertex_select_radius` pixels of the
  2078. vertex. The default radius is 15 pixels.
  2079. Examples
  2080. --------
  2081. :doc:`/gallery/widgets/polygon_selector_demo`
  2082. """
  2083. def __init__(self, ax, onselect, useblit=False,
  2084. lineprops=None, markerprops=None, vertex_select_radius=15):
  2085. # The state modifiers 'move', 'square', and 'center' are expected by
  2086. # _SelectorWidget but are not supported by PolygonSelector
  2087. # Note: could not use the existing 'move' state modifier in-place of
  2088. # 'move_all' because _SelectorWidget automatically discards 'move'
  2089. # from the state on button release.
  2090. state_modifier_keys = dict(clear='escape', move_vertex='control',
  2091. move_all='shift', move='not-applicable',
  2092. square='not-applicable',
  2093. center='not-applicable')
  2094. _SelectorWidget.__init__(self, ax, onselect, useblit=useblit,
  2095. state_modifier_keys=state_modifier_keys)
  2096. self._xs, self._ys = [0], [0]
  2097. self._polygon_completed = False
  2098. if lineprops is None:
  2099. lineprops = dict(color='k', linestyle='-', linewidth=2, alpha=0.5)
  2100. lineprops['animated'] = self.useblit
  2101. self.line = Line2D(self._xs, self._ys, **lineprops)
  2102. self.ax.add_line(self.line)
  2103. if markerprops is None:
  2104. markerprops = dict(mec='k', mfc=lineprops.get('color', 'k'))
  2105. self._polygon_handles = ToolHandles(self.ax, self._xs, self._ys,
  2106. useblit=self.useblit,
  2107. marker_props=markerprops)
  2108. self._active_handle_idx = -1
  2109. self.vertex_select_radius = vertex_select_radius
  2110. self.artists = [self.line, self._polygon_handles.artist]
  2111. self.set_visible(True)
  2112. def _press(self, event):
  2113. """Button press event handler"""
  2114. # Check for selection of a tool handle.
  2115. if ((self._polygon_completed or 'move_vertex' in self.state)
  2116. and len(self._xs) > 0):
  2117. h_idx, h_dist = self._polygon_handles.closest(event.x, event.y)
  2118. if h_dist < self.vertex_select_radius:
  2119. self._active_handle_idx = h_idx
  2120. # Save the vertex positions at the time of the press event (needed to
  2121. # support the 'move_all' state modifier).
  2122. self._xs_at_press, self._ys_at_press = self._xs[:], self._ys[:]
  2123. def _release(self, event):
  2124. """Button release event handler"""
  2125. # Release active tool handle.
  2126. if self._active_handle_idx >= 0:
  2127. self._active_handle_idx = -1
  2128. # Complete the polygon.
  2129. elif (len(self._xs) > 3
  2130. and self._xs[-1] == self._xs[0]
  2131. and self._ys[-1] == self._ys[0]):
  2132. self._polygon_completed = True
  2133. # Place new vertex.
  2134. elif (not self._polygon_completed
  2135. and 'move_all' not in self.state
  2136. and 'move_vertex' not in self.state):
  2137. self._xs.insert(-1, event.xdata)
  2138. self._ys.insert(-1, event.ydata)
  2139. if self._polygon_completed:
  2140. self.onselect(self.verts)
  2141. def onmove(self, event):
  2142. """Cursor move event handler and validator"""
  2143. # Method overrides _SelectorWidget.onmove because the polygon selector
  2144. # needs to process the move callback even if there is no button press.
  2145. # _SelectorWidget.onmove include logic to ignore move event if
  2146. # eventpress is None.
  2147. if not self.ignore(event):
  2148. event = self._clean_event(event)
  2149. self._onmove(event)
  2150. return True
  2151. return False
  2152. def _onmove(self, event):
  2153. """Cursor move event handler"""
  2154. # Move the active vertex (ToolHandle).
  2155. if self._active_handle_idx >= 0:
  2156. idx = self._active_handle_idx
  2157. self._xs[idx], self._ys[idx] = event.xdata, event.ydata
  2158. # Also update the end of the polygon line if the first vertex is
  2159. # the active handle and the polygon is completed.
  2160. if idx == 0 and self._polygon_completed:
  2161. self._xs[-1], self._ys[-1] = event.xdata, event.ydata
  2162. # Move all vertices.
  2163. elif 'move_all' in self.state and self.eventpress:
  2164. dx = event.xdata - self.eventpress.xdata
  2165. dy = event.ydata - self.eventpress.ydata
  2166. for k in range(len(self._xs)):
  2167. self._xs[k] = self._xs_at_press[k] + dx
  2168. self._ys[k] = self._ys_at_press[k] + dy
  2169. # Do nothing if completed or waiting for a move.
  2170. elif (self._polygon_completed
  2171. or 'move_vertex' in self.state or 'move_all' in self.state):
  2172. return
  2173. # Position pending vertex.
  2174. else:
  2175. # Calculate distance to the start vertex.
  2176. x0, y0 = self.line.get_transform().transform((self._xs[0],
  2177. self._ys[0]))
  2178. v0_dist = np.sqrt((x0 - event.x) ** 2 + (y0 - event.y) ** 2)
  2179. # Lock on to the start vertex if near it and ready to complete.
  2180. if len(self._xs) > 3 and v0_dist < self.vertex_select_radius:
  2181. self._xs[-1], self._ys[-1] = self._xs[0], self._ys[0]
  2182. else:
  2183. self._xs[-1], self._ys[-1] = event.xdata, event.ydata
  2184. self._draw_polygon()
  2185. def _on_key_press(self, event):
  2186. """Key press event handler"""
  2187. # Remove the pending vertex if entering the 'move_vertex' or
  2188. # 'move_all' mode
  2189. if (not self._polygon_completed
  2190. and ('move_vertex' in self.state or 'move_all' in self.state)):
  2191. self._xs, self._ys = self._xs[:-1], self._ys[:-1]
  2192. self._draw_polygon()
  2193. def _on_key_release(self, event):
  2194. """Key release event handler"""
  2195. # Add back the pending vertex if leaving the 'move_vertex' or
  2196. # 'move_all' mode (by checking the released key)
  2197. if (not self._polygon_completed
  2198. and
  2199. (event.key == self.state_modifier_keys.get('move_vertex')
  2200. or event.key == self.state_modifier_keys.get('move_all'))):
  2201. self._xs.append(event.xdata)
  2202. self._ys.append(event.ydata)
  2203. self._draw_polygon()
  2204. # Reset the polygon if the released key is the 'clear' key.
  2205. elif event.key == self.state_modifier_keys.get('clear'):
  2206. event = self._clean_event(event)
  2207. self._xs, self._ys = [event.xdata], [event.ydata]
  2208. self._polygon_completed = False
  2209. self.set_visible(True)
  2210. def _draw_polygon(self):
  2211. """Redraw the polygon based on the new vertex positions."""
  2212. self.line.set_data(self._xs, self._ys)
  2213. # Only show one tool handle at the start and end vertex of the polygon
  2214. # if the polygon is completed or the user is locked on to the start
  2215. # vertex.
  2216. if (self._polygon_completed
  2217. or (len(self._xs) > 3
  2218. and self._xs[-1] == self._xs[0]
  2219. and self._ys[-1] == self._ys[0])):
  2220. self._polygon_handles.set_data(self._xs[:-1], self._ys[:-1])
  2221. else:
  2222. self._polygon_handles.set_data(self._xs, self._ys)
  2223. self.update()
  2224. @property
  2225. def verts(self):
  2226. """Get the polygon vertices.
  2227. Returns
  2228. -------
  2229. list
  2230. A list of the vertices of the polygon as ``(xdata, ydata)`` tuples.
  2231. """
  2232. return list(zip(self._xs[:-1], self._ys[:-1]))
  2233. class Lasso(AxesWidget):
  2234. """Selection curve of an arbitrary shape.
  2235. The selected path can be used in conjunction with
  2236. :func:`~matplotlib.path.Path.contains_point` to select data points
  2237. from an image.
  2238. Unlike :class:`LassoSelector`, this must be initialized with a starting
  2239. point `xy`, and the `Lasso` events are destroyed upon release.
  2240. Parameters
  2241. ----------
  2242. ax : `~matplotlib.axes.Axes`
  2243. The parent axes for the widget.
  2244. xy : (float, float)
  2245. Coordinates of the start of the lasso.
  2246. callback : callable
  2247. Whenever the lasso is released, the `callback` function is called and
  2248. passed the vertices of the selected path.
  2249. """
  2250. def __init__(self, ax, xy, callback=None, useblit=True):
  2251. AxesWidget.__init__(self, ax)
  2252. self.useblit = useblit and self.canvas.supports_blit
  2253. if self.useblit:
  2254. self.background = self.canvas.copy_from_bbox(self.ax.bbox)
  2255. x, y = xy
  2256. self.verts = [(x, y)]
  2257. self.line = Line2D([x], [y], linestyle='-', color='black', lw=2)
  2258. self.ax.add_line(self.line)
  2259. self.callback = callback
  2260. self.connect_event('button_release_event', self.onrelease)
  2261. self.connect_event('motion_notify_event', self.onmove)
  2262. def onrelease(self, event):
  2263. if self.ignore(event):
  2264. return
  2265. if self.verts is not None:
  2266. self.verts.append((event.xdata, event.ydata))
  2267. if len(self.verts) > 2:
  2268. self.callback(self.verts)
  2269. self.ax.lines.remove(self.line)
  2270. self.verts = None
  2271. self.disconnect_events()
  2272. def onmove(self, event):
  2273. if self.ignore(event):
  2274. return
  2275. if self.verts is None:
  2276. return
  2277. if event.inaxes != self.ax:
  2278. return
  2279. if event.button != 1:
  2280. return
  2281. self.verts.append((event.xdata, event.ydata))
  2282. self.line.set_data(list(zip(*self.verts)))
  2283. if self.useblit:
  2284. self.canvas.restore_region(self.background)
  2285. self.ax.draw_artist(self.line)
  2286. self.canvas.blit(self.ax.bbox)
  2287. else:
  2288. self.canvas.draw_idle()