Hide keyboard shortcuts

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=================== 

4 

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""" 

11 

12from contextlib import ExitStack 

13import copy 

14from numbers import Integral 

15 

16import numpy as np 

17 

18from . import cbook, rcParams 

19from .lines import Line2D 

20from .patches import Circle, Rectangle, Ellipse 

21from .transforms import blended_transform_factory 

22 

23 

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 """ 

33 

34 def __init__(self): 

35 self._owner = None 

36 

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 

42 

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 

48 

49 def available(self, o): 

50 """Return whether drawing is available to *o*.""" 

51 return not self.locked() or self.isowner(o) 

52 

53 def isowner(self, o): 

54 """Return whether *o* owns this lock.""" 

55 return self._owner is o 

56 

57 def locked(self): 

58 """Return whether the lock is currently held by an owner.""" 

59 return self._owner is not None 

60 

61 

62class Widget: 

63 """ 

64 Abstract base class for GUI neutral widgets 

65 """ 

66 drawon = True 

67 eventson = True 

68 _active = True 

69 

70 def set_active(self, active): 

71 """Set whether the widget is active. 

72 """ 

73 self._active = active 

74 

75 def get_active(self): 

76 """Get whether the widget is active. 

77 """ 

78 return self._active 

79 

80 # set_active is overridden by SelectorWidgets. 

81 active = property(get_active, set_active, doc="Is the widget active?") 

82 

83 def ignore(self, event): 

84 """ 

85 Return whether *event* should be ignored. 

86 

87 This method should be called at the beginning of any event callback. 

88 """ 

89 return not self.active 

90 

91 

92class AxesWidget(Widget): 

93 """ 

94 Widget that is connected to a single `~matplotlib.axes.Axes`. 

95 

96 To guarantee that the widget remains responsive and not garbage-collected, 

97 a reference to the object should be maintained by the user. 

98 

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. 

103 

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 = [] 

117 

118 def connect_event(self, event, callback): 

119 """ 

120 Connect callback with an event. 

121 

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) 

127 

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) 

132 

133 

134class Button(AxesWidget): 

135 """ 

136 A GUI neutral button. 

137 

138 For the button to remain responsive you must keep a reference to it. 

139 Call `.on_clicked` to connect to the button. 

140 

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 """ 

152 

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) 

171 

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) 

178 

179 self.cnt = 0 

180 self.observers = {} 

181 

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 

191 

192 self._lastcolor = color 

193 

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) 

201 

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) 

212 

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() 

225 

226 def on_clicked(self, func): 

227 """ 

228 Connect the callback function *func* to button click events. 

229 

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 

236 

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 

243 

244 

245class Slider(AxesWidget): 

246 """ 

247 A slider representing a floating point range. 

248 

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. 

252 

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. 

267 

268 label : str 

269 Slider label. 

270 

271 valmin : float 

272 The minimum value of the slider. 

273 

274 valmax : float 

275 The maximum value of the slider. 

276 

277 valinit : float, optional, default: 0.5 

278 The slider initial position. 

279 

280 valfmt : str, optional, default: "%1.2f" 

281 Used to format the slider value, fprint format string. 

282 

283 closedmin : bool, optional, default: True 

284 Whether the slider interval is closed on the bottom. 

285 

286 closedmax : bool, optional, default: True 

287 Whether the slider interval is closed on the top. 

288 

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`. 

292 

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`. 

296 

297 dragging : bool, optional, default: True 

298 If True the slider can be dragged by the mouse. 

299 

300 valstep : float, optional, default: None 

301 If given, the slider will snap to multiples of `valstep`. 

302 

303 orientation : {'horizontal', 'vertical'}, default: 'horizontal' 

304 The orientation of the slider. 

305 

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') 

315 

316 AxesWidget.__init__(self, ax) 

317 

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)) 

327 

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) 

348 

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) 

357 

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') 

366 

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') 

375 

376 self.valtext = ax.text(1.02, 0.5, valfmt % valinit, 

377 transform=ax.transAxes, 

378 verticalalignment='center', 

379 horizontalalignment='left') 

380 

381 self.cnt = 0 

382 self.observers = {} 

383 

384 self.set_val(valinit) 

385 

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) 

391 

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 

400 

401 if self.slidermin is not None and val <= self.slidermin.val: 

402 if not self.closedmin: 

403 return 

404 val = self.slidermin.val 

405 

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 

411 

412 def _update(self, event): 

413 """Update the slider position.""" 

414 if self.ignore(event) or event.button != 1: 

415 return 

416 

417 if event.name == 'button_press_event' and event.inaxes == self.ax: 

418 self.drag_active = True 

419 event.canvas.grab_mouse(self.ax) 

420 

421 if not self.drag_active: 

422 return 

423 

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) 

436 

437 def set_val(self, val): 

438 """ 

439 Set slider value to *val* 

440 

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) 

461 

462 def on_changed(self, func): 

463 """ 

464 When the slider value is changed call *func* with the new 

465 slider value 

466 

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. 

472 

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 

482 

483 def disconnect(self, cid): 

484 """ 

485 Remove the observer with connection id *cid* 

486 

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 

496 

497 def reset(self): 

498 """Reset the slider to the initial value""" 

499 if self.val != self.valinit: 

500 self.set_val(self.valinit) 

501 

502 

503class CheckButtons(AxesWidget): 

504 r""" 

505 A GUI neutral set of check buttons. 

506 

507 For the check buttons to remain responsive you must keep a 

508 reference to this object. 

509 

510 Connect to the CheckButtons with the :meth:`on_clicked` method 

511 

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 """ 

525 

526 def __init__(self, ax, labels, actives=None): 

527 """ 

528 Add check buttons to `matplotlib.axes.Axes` instance *ax* 

529 

530 Parameters 

531 ---------- 

532 ax : `~matplotlib.axes.Axes` 

533 The parent axes for the widget. 

534 

535 labels : list of str 

536 The labels of the check buttons. 

537 

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) 

543 

544 ax.set_xticks([]) 

545 ax.set_yticks([]) 

546 ax.set_navigate(False) 

547 

548 if actives is None: 

549 actives = [False] * len(labels) 

550 

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] 

557 

558 axcolor = ax.get_facecolor() 

559 

560 self.labels = [] 

561 self.lines = [] 

562 self.rectangles = [] 

563 

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') 

570 

571 w, h = dy / 2, dy / 2 

572 x, y = 0.05, y - h / 2 

573 

574 p = Rectangle(xy=(x, y), width=w, height=h, edgecolor='black', 

575 facecolor=axcolor, transform=ax.transAxes) 

576 

577 l1 = Line2D([x, x + w], [y + h, y], **lineparams) 

578 l2 = Line2D([x, x + w], [y, y + h], **lineparams) 

579 

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) 

588 

589 self.connect_event('button_press_event', self._clicked) 

590 

591 self.cnt = 0 

592 self.observers = {} 

593 

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 

602 

603 def set_active(self, index): 

604 """ 

605 Directly (de)activate a check button by index. 

606 

607 *index* is an index into the original label list 

608 that this object was constructed with. 

609 Raises ValueError if *index* is invalid. 

610 

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) 

615 

616 l1, l2 = self.lines[index] 

617 l1.set_visible(not l1.get_visible()) 

618 l2.set_visible(not l2.get_visible()) 

619 

620 if self.drawon: 

621 self.ax.figure.canvas.draw() 

622 

623 if not self.eventson: 

624 return 

625 for cid, func in self.observers.items(): 

626 func(self.labels[index].get_text()) 

627 

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] 

633 

634 def on_clicked(self, func): 

635 """ 

636 Connect the callback function *func* to button click events. 

637 

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 

644 

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 

651 

652 

653class TextBox(AxesWidget): 

654 """ 

655 A GUI neutral text input box. 

656 

657 For the text box to remain responsive you must keep a reference to it. 

658 

659 Call :meth:`on_text_change` to be updated whenever the text changes. 

660 

661 Call :meth:`on_submit` to be updated whenever the user hits enter or 

662 leaves the text entry field. 

663 

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 """ 

675 

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) 

695 

696 self.DIST_FROM_LEFT = .05 

697 

698 self.params_to_disable = [key for key in rcParams if 'keymap' in key] 

699 

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) 

706 

707 self.cnt = 0 

708 self.change_observers = {} 

709 self.submit_observers = {} 

710 

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) 

715 

716 self.cursor_index = 0 

717 

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) 

722 

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 

734 

735 self._lastcolor = color 

736 

737 self.capturekeystrokes = False 

738 

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) 

744 

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 

750 

751 widthtext = self.text[:self.cursor_index] 

752 no_text = False 

753 if widthtext in ["", " ", " "]: 

754 no_text = widthtext == "" 

755 widthtext = "," 

756 

757 wt_disp = self._make_text_disp(widthtext) 

758 

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) 

768 

769 self.cursor = self.ax.vlines(bb[1, 0], bb[0, 1], bb[1, 1]) 

770 self.ax.figure.canvas.draw() 

771 

772 def _notify_submit_observers(self): 

773 if self.eventson: 

774 for cid, func in self.submit_observers.items(): 

775 func(self.text) 

776 

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) 

783 

784 def _keypress(self, event): 

785 if self.ignore(event): 

786 return 

787 if self.capturekeystrokes: 

788 key = event.key 

789 

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:]) 

813 

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() 

820 

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() 

831 

832 def _notify_change_observers(self): 

833 if self.eventson: 

834 for cid, func in self.change_observers.items(): 

835 func(self.text) 

836 

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) 

849 

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() 

870 

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() 

878 

879 trans = self.ax.transData 

880 inv = self.ax.transData.inverted() 

881 bb = trans.transform(inv.transform(bb)) 

882 

883 text_start = bb[0, 0] 

884 text_end = bb[1, 0] 

885 

886 ratio = (x - text_start) / (text_end - text_start) 

887 

888 if ratio < 0: 

889 ratio = 0 

890 if ratio > 1: 

891 ratio = 1 

892 

893 self.cursor_index = int(len(self.text) * ratio) 

894 

895 self._rendercursor() 

896 

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) 

910 

911 def _resize(self, event): 

912 self.stop_typing() 

913 

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() 

926 

927 def on_text_change(self, func): 

928 """ 

929 When the text changes, call this *func* with event. 

930 

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 

937 

938 def on_submit(self, func): 

939 """ 

940 When the user hits enter or leaves the submission box, call this 

941 *func* with event. 

942 

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 

949 

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 

957 

958 

959class RadioButtons(AxesWidget): 

960 """ 

961 A GUI neutral radio button. 

962 

963 For the buttons to remain responsive you must keep a reference to this 

964 object. 

965 

966 Connect to the RadioButtons with the :meth:`on_clicked` method. 

967 

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 """ 

981 

982 def __init__(self, ax, labels, active=0, activecolor='blue'): 

983 """ 

984 Add radio buttons to an `~.axes.Axes`. 

985 

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 

1000 

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() 

1008 

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) 

1013 

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') 

1020 

1021 if cnt == active: 

1022 self.value_selected = label 

1023 facecolor = activecolor 

1024 else: 

1025 facecolor = axcolor 

1026 

1027 p = Circle(xy=(0.15, y), radius=circle_radius, edgecolor='black', 

1028 facecolor=facecolor, transform=ax.transAxes) 

1029 

1030 self.labels.append(t) 

1031 self.circles.append(p) 

1032 ax.add_patch(p) 

1033 cnt += 1 

1034 

1035 self.connect_event('button_press_event', self._clicked) 

1036 

1037 self.cnt = 0 

1038 self.observers = {} 

1039 

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) 

1052 

1053 def set_active(self, index): 

1054 """ 

1055 Select button with number *index*. 

1056 

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) 

1061 

1062 self.value_selected = self.labels[index].get_text() 

1063 

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) 

1070 

1071 if self.drawon: 

1072 self.ax.figure.canvas.draw() 

1073 

1074 if not self.eventson: 

1075 return 

1076 for cid, func in self.observers.items(): 

1077 func(self.labels[index].get_text()) 

1078 

1079 def on_clicked(self, func): 

1080 """ 

1081 Connect the callback function *func* to button click events. 

1082 

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 

1089 

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 

1096 

1097 

1098class SubplotTool(Widget): 

1099 """ 

1100 A tool to adjust the subplot params of a `matplotlib.figure.Figure`. 

1101 """ 

1102 

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 """ 

1112 

1113 self.targetfig = targetfig 

1114 toolfig.subplots_adjust(left=0.2, right=0.9) 

1115 

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) 

1119 

1120 self.sliderleft = Slider(self.axleft, 'left', 

1121 0, 1, targetfig.subplotpars.left, 

1122 closedmax=False) 

1123 self.sliderleft.on_changed(self.funcleft) 

1124 

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) 

1132 

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) 

1139 

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) 

1146 

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) 

1153 

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) 

1160 

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 

1166 

1167 bax = toolfig.add_axes([0.8, 0.05, 0.15, 0.075]) 

1168 self.buttonreset = Button(bax, 'Reset') 

1169 

1170 sliders = (self.sliderleft, self.sliderbottom, self.sliderright, 

1171 self.slidertop, self.sliderwspace, self.sliderhspace,) 

1172 

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() 

1187 

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 

1195 

1196 def funcleft(self, val): 

1197 self.targetfig.subplots_adjust(left=val) 

1198 if self.drawon: 

1199 self.targetfig.canvas.draw() 

1200 

1201 def funcright(self, val): 

1202 self.targetfig.subplots_adjust(right=val) 

1203 if self.drawon: 

1204 self.targetfig.canvas.draw() 

1205 

1206 def funcbottom(self, val): 

1207 self.targetfig.subplots_adjust(bottom=val) 

1208 if self.drawon: 

1209 self.targetfig.canvas.draw() 

1210 

1211 def functop(self, val): 

1212 self.targetfig.subplots_adjust(top=val) 

1213 if self.drawon: 

1214 self.targetfig.canvas.draw() 

1215 

1216 def funcwspace(self, val): 

1217 self.targetfig.subplots_adjust(wspace=val) 

1218 if self.drawon: 

1219 self.targetfig.canvas.draw() 

1220 

1221 def funchspace(self, val): 

1222 self.targetfig.subplots_adjust(hspace=val) 

1223 if self.drawon: 

1224 self.targetfig.canvas.draw() 

1225 

1226 

1227class Cursor(AxesWidget): 

1228 """ 

1229 A crosshair cursor that spans the axes and moves with mouse cursor. 

1230 

1231 For the cursor to remain responsive you must keep a reference to it. 

1232 

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. 

1243 

1244 Other Parameters 

1245 ---------------- 

1246 **lineprops 

1247 `.Line2D` properties that control the appearance of the lines. 

1248 See also `~.Axes.axhline`. 

1249 

1250 Examples 

1251 -------- 

1252 See :doc:`/gallery/widgets/cursor`. 

1253 """ 

1254 

1255 def __init__(self, ax, horizOn=True, vertOn=True, useblit=False, 

1256 **lineprops): 

1257 AxesWidget.__init__(self, ax) 

1258 

1259 self.connect_event('motion_notify_event', self.onmove) 

1260 self.connect_event('draw_event', self.clear) 

1261 

1262 self.visible = True 

1263 self.horizOn = horizOn 

1264 self.vertOn = vertOn 

1265 self.useblit = useblit and self.canvas.supports_blit 

1266 

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) 

1271 

1272 self.background = None 

1273 self.needclear = False 

1274 

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) 

1283 

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) 

1293 

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)) 

1302 

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) 

1306 

1307 self._update() 

1308 

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 

1319 

1320 

1321class MultiCursor(Widget): 

1322 """ 

1323 Provide a vertical (default) and/or horizontal line cursor shared between 

1324 multiple axes. 

1325 

1326 For the cursor to remain responsive you must keep a reference to it. 

1327 

1328 Example usage:: 

1329 

1330 from matplotlib.widgets import MultiCursor 

1331 import matplotlib.pyplot as plt 

1332 import numpy as np 

1333 

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)) 

1338 

1339 multi = MultiCursor(fig.canvas, (ax1, ax2), color='r', lw=1, 

1340 horizOn=False, vertOn=True) 

1341 plt.show() 

1342 

1343 """ 

1344 def __init__(self, canvas, axes, useblit=True, horizOn=False, vertOn=True, 

1345 **lineprops): 

1346 

1347 self.canvas = canvas 

1348 self.axes = axes 

1349 self.horizOn = horizOn 

1350 self.vertOn = vertOn 

1351 

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) 

1356 

1357 self.visible = True 

1358 self.useblit = useblit and self.canvas.supports_blit 

1359 self.background = None 

1360 self.needclear = False 

1361 

1362 if self.useblit: 

1363 lineprops['animated'] = True 

1364 

1365 if vertOn: 

1366 self.vlines = [ax.axvline(xmid, visible=False, **lineprops) 

1367 for ax in axes] 

1368 else: 

1369 self.vlines = [] 

1370 

1371 if horizOn: 

1372 self.hlines = [ax.axhline(ymid, visible=False, **lineprops) 

1373 for ax in axes] 

1374 else: 

1375 self.hlines = [] 

1376 

1377 self.connect() 

1378 

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) 

1384 

1385 def disconnect(self): 

1386 """disconnect events""" 

1387 self.canvas.mpl_disconnect(self._cidmotion) 

1388 self.canvas.mpl_disconnect(self._ciddraw) 

1389 

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) 

1399 

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() 

1419 

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() 

1433 

1434 

1435class _SelectorWidget(AxesWidget): 

1436 

1437 def __init__(self, ax, onselect, useblit=False, button=None, 

1438 state_modifier_keys=None): 

1439 AxesWidget.__init__(self, ax) 

1440 

1441 self.visible = True 

1442 self.onselect = onselect 

1443 self.useblit = useblit and self.canvas.supports_blit 

1444 self.connect_default_events() 

1445 

1446 self.state_modifier_keys = dict(move=' ', clear='escape', 

1447 square='shift', center='control') 

1448 self.state_modifier_keys.update(state_modifier_keys or {}) 

1449 

1450 self.background = None 

1451 self.artists = [] 

1452 

1453 if isinstance(button, Integral): 

1454 self.validButtons = [button] 

1455 else: 

1456 self.validButtons = button 

1457 

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() 

1464 

1465 def set_active(self, active): 

1466 AxesWidget.set_active(self, active) 

1467 if active: 

1468 self.update_background(None) 

1469 

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) 

1476 

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) 

1486 

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) 

1511 

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 

1527 

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 

1539 

1540 def _clean_event(self, event): 

1541 """Clean up an event 

1542 

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) 

1552 

1553 self._prev_event = event 

1554 return event 

1555 

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 

1570 

1571 def _press(self, event): 

1572 """Button press handler""" 

1573 

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 

1585 

1586 def _release(self, event): 

1587 """Button release event handler""" 

1588 

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 

1596 

1597 def _onmove(self, event): 

1598 """Cursor move event handler""" 

1599 

1600 def on_scroll(self, event): 

1601 """Mouse scroll event handler and validator""" 

1602 if not self.ignore(event): 

1603 self._on_scroll(event) 

1604 

1605 def _on_scroll(self, event): 

1606 """Mouse scroll event handler""" 

1607 

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) 

1622 

1623 def _on_key_press(self, event): 

1624 """Key press event handler - use for widget-specific key press actions. 

1625 """ 

1626 

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) 

1635 

1636 def _on_key_release(self, event): 

1637 """Key release event handler.""" 

1638 

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) 

1644 

1645 

1646class SpanSelector(_SelectorWidget): 

1647 """ 

1648 Visually select a min/max range on a single axis and call a function with 

1649 those values. 

1650 

1651 To guarantee that the selector remains responsive, keep a reference to it. 

1652 

1653 In order to turn off the SpanSelector, set `span_selector.active=False`. To 

1654 turn it back on, set `span_selector.active=True`. 

1655 

1656 Parameters 

1657 ---------- 

1658 ax : `matplotlib.axes.Axes` object 

1659 

1660 onselect : func(min, max), min/max are floats 

1661 

1662 direction : {"horizontal", "vertical"} 

1663 The direction along which to draw the span selector. 

1664 

1665 minspan : float, default is None 

1666 If selection is less than *minspan*, do not call *onselect*. 

1667 

1668 useblit : bool, default is False 

1669 If True, use the backend-dependent blitting features for faster 

1670 canvas updates. 

1671 

1672 rectprops : dict, default is None 

1673 Dictionary of `matplotlib.patches.Patch` properties. 

1674 

1675 onmove_callback : func(min, max), min/max are floats, default is None 

1676 Called on mouse move while the span is being selected. 

1677 

1678 span_stays : bool, default is False 

1679 If True, the span stays visible after the mouse is released. 

1680 

1681 button : `.MouseButton` or list of `.MouseButton` 

1682 The mouse buttons which activate the span selector. 

1683 

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() 

1696 

1697 See also: :doc:`/gallery/widgets/span_selector` 

1698 """ 

1699 

1700 def __init__(self, ax, onselect, direction, minspan=None, useblit=False, 

1701 rectprops=None, onmove_callback=None, span_stays=False, 

1702 button=None): 

1703 

1704 _SelectorWidget.__init__(self, ax, onselect, useblit=useblit, 

1705 button=button) 

1706 

1707 if rectprops is None: 

1708 rectprops = dict(facecolor='red', alpha=0.5) 

1709 

1710 rectprops['animated'] = self.useblit 

1711 

1712 cbook._check_in_list(['horizontal', 'vertical'], direction=direction) 

1713 self.direction = direction 

1714 

1715 self.rect = None 

1716 self.pressv = None 

1717 

1718 self.rectprops = rectprops 

1719 self.onmove_callback = onmove_callback 

1720 self.minspan = minspan 

1721 self.span_stays = span_stays 

1722 

1723 # Needed when dragging out of axes 

1724 self.prev = (0, 0) 

1725 

1726 # Reset canvas so that `new_axes` connects events. 

1727 self.canvas = None 

1728 self.new_axes(ax) 

1729 

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() 

1736 

1737 self.canvas = ax.figure.canvas 

1738 self.connect_default_events() 

1739 

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) 

1759 

1760 self.ax.add_patch(self.rect) 

1761 self.artists = [self.rect] 

1762 

1763 def ignore(self, event): 

1764 # docstring inherited 

1765 return _SelectorWidget.ignore(self, event) or not self.visible 

1766 

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 

1781 

1782 self._set_span_xy(event) 

1783 return False 

1784 

1785 def _release(self, event): 

1786 """on button release event""" 

1787 if self.pressv is None: 

1788 return 

1789 

1790 self.rect.set_visible(False) 

1791 

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) 

1798 

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] 

1806 

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 

1815 

1816 @cbook.deprecated("3.1") 

1817 @property 

1818 def buttonDown(self): 

1819 return False 

1820 

1821 def _onmove(self, event): 

1822 """on motion notify event""" 

1823 if self.pressv is None: 

1824 return 

1825 

1826 self._set_span_xy(event) 

1827 

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] 

1835 

1836 if vmin > vmax: 

1837 vmin, vmax = vmax, vmin 

1838 self.onmove_callback(vmin, vmax) 

1839 

1840 self.update() 

1841 return False 

1842 

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 

1848 

1849 self.prev = x, y 

1850 if self.direction == 'horizontal': 

1851 v = x 

1852 else: 

1853 v = y 

1854 

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) 

1864 

1865 

1866class ToolHandles: 

1867 """ 

1868 Control handles for canvas tools. 

1869 

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 """ 

1881 

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 

1890 

1891 @property 

1892 def x(self): 

1893 return self._markers.get_xdata() 

1894 

1895 @property 

1896 def y(self): 

1897 return self._markers.get_ydata() 

1898 

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) 

1905 

1906 def set_visible(self, val): 

1907 self._markers.set_visible(val) 

1908 

1909 def set_animated(self, val): 

1910 self._markers.set_animated(val) 

1911 

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] 

1921 

1922 

1923class RectangleSelector(_SelectorWidget): 

1924 """ 

1925 Select a rectangular region of an axes. 

1926 

1927 For the cursor to remain responsive you must keep a reference to it. 

1928 

1929 Example usage:: 

1930 

1931 import numpy as np 

1932 import matplotlib.pyplot as plt 

1933 from matplotlib.widgets import RectangleSelector 

1934 

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) 

1940 

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) 

1949 

1950 x = np.arange(100.) / 99 

1951 y = np.sin(x) 

1952 fig, ax = plt.subplots() 

1953 ax.plot(x, y) 

1954 

1955 toggle_selector.RS = RectangleSelector(ax, onselect, drawtype='line') 

1956 fig.canvas.mpl_connect('key_press_event', toggle_selector) 

1957 plt.show() 

1958 """ 

1959 

1960 _shape_klass = Rectangle 

1961 

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:: 

1970 

1971 onselect(pos_1, pos_2) 

1972 

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. 

1975 

1976 If *minspanx* is not *None* then events smaller than *minspanx* 

1977 in x direction are ignored (it's the same for y). 

1978 

1979 The rectangle is drawn with *rectprops*; default:: 

1980 

1981 rectprops = dict(facecolor='red', edgecolor = 'black', 

1982 alpha=0.2, fill=True) 

1983 

1984 The line is drawn with *lineprops*; default:: 

1985 

1986 lineprops = dict(color='black', linestyle='-', 

1987 linewidth = 2, alpha=0.5) 

1988 

1989 Use *drawtype* if you want the mouse to draw a line, 

1990 a box or nothing between click and actual position by setting 

1991 

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. 

1995 

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. 

1999 

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. 

2004 

2005 Note, typically: 

2006 1 = left mouse button 

2007 2 = center mouse button (scroll wheel) 

2008 3 = right mouse button 

2009 

2010 *interactive* will draw a set of handles and allow you interact 

2011 with the widget after it is drawn. 

2012 

2013 *state_modifier_keys* are keyboard modifiers that affect the behavior 

2014 of the widget. 

2015 

2016 The defaults are: 

2017 dict(move=' ', clear='escape', square='shift', center='ctrl') 

2018 

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) 

2029 

2030 self.to_draw = None 

2031 self.visible = True 

2032 self.interactive = interactive 

2033 

2034 if drawtype == 'none': # draw a line but make it invisible 

2035 drawtype = 'line' 

2036 self.visible = False 

2037 

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) 

2056 

2057 self.minspanx = minspanx 

2058 self.minspany = minspany 

2059 

2060 cbook._check_in_list(['data', 'pixels'], spancoords=spancoords) 

2061 self.spancoords = spancoords 

2062 self.drawtype = drawtype 

2063 

2064 self.maxdist = maxdist 

2065 

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) 

2074 

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) 

2080 

2081 xc, yc = self.center 

2082 self._center_handle = ToolHandles(self.ax, [xc], [yc], marker='s', 

2083 marker_props=props, 

2084 useblit=self.useblit) 

2085 

2086 self.active_handle = None 

2087 

2088 self.artists = [self.to_draw, self._center_handle.artist, 

2089 self._corner_handles.artist, 

2090 self._edge_handles.artist] 

2091 

2092 if not self.interactive: 

2093 self.artists = [self.to_draw] 

2094 

2095 self._extents_on_press = None 

2096 

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 

2105 

2106 if self.active_handle is None or not self.interactive: 

2107 # Clear previous rectangle before drawing new rectangle. 

2108 self.update() 

2109 

2110 if not self.interactive: 

2111 x = event.xdata 

2112 y = event.ydata 

2113 self.extents = x, x, y, y 

2114 

2115 self.set_visible(self.visible) 

2116 

2117 def _release(self, event): 

2118 """on button release event""" 

2119 if not self.interactive: 

2120 self.to_draw.set_visible(False) 

2121 

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 

2128 

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 

2133 

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) 

2144 

2145 if xmin > xmax: 

2146 xmin, xmax = xmax, xmin 

2147 if ymin > ymax: 

2148 ymin, ymax = ymax, ymin 

2149 

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 

2154 

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 

2162 

2163 # call desired function 

2164 self.onselect(self.eventpress, self.eventrelease) 

2165 self.update() 

2166 

2167 return False 

2168 

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 

2178 

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 

2189 

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. 

2196 

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) 

2208 

2209 # from center 

2210 if 'center' in self.state: 

2211 dx *= 2 

2212 dy *= 2 

2213 

2214 # from corner 

2215 else: 

2216 center[0] += dx 

2217 center[1] += dy 

2218 

2219 x1, x2, y1, y2 = (center[0] - dx, center[0] + dx, 

2220 center[1] - dy, center[1] + dy) 

2221 

2222 self.extents = x1, x2, y1, y2 

2223 

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 

2237 

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 

2245 

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 

2255 

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. 

2261 

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 

2269 

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() 

2280 

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()) 

2287 

2288 xmin = max(xlim[0], xmin) 

2289 ymin = max(ylim[0], ymin) 

2290 xmax = min(xmax, xlim[1]) 

2291 ymax = min(ymax, ylim[1]) 

2292 

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) 

2298 

2299 elif self.drawtype == 'line': 

2300 self.to_draw.set_data([xmin, xmax], [ymin, ymax]) 

2301 

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) 

2308 

2309 if 'move' in self.state: 

2310 self.active_handle = 'C' 

2311 self._extents_on_press = self.extents 

2312 

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] 

2323 

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 

2332 

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()) 

2348 

2349 

2350class EllipseSelector(RectangleSelector): 

2351 """ 

2352 Select an elliptical region of an axes. 

2353 

2354 For the cursor to remain responsive you must keep a reference to it. 

2355 

2356 Example usage:: 

2357 

2358 import numpy as np 

2359 import matplotlib.pyplot as plt 

2360 from matplotlib.widgets import EllipseSelector 

2361 

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) 

2367 

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) 

2376 

2377 x = np.arange(100.) / 99 

2378 y = np.sin(x) 

2379 fig, ax = plt.subplots() 

2380 ax.plot(x, y) 

2381 

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 

2387 

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. 

2395 

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) 

2405 

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 

2418 

2419 

2420class LassoSelector(_SelectorWidget): 

2421 """ 

2422 Selection curve of an arbitrary shape. 

2423 

2424 For the selector to remain responsive you must keep a reference to it. 

2425 

2426 The selected path can be used in conjunction with `~.Path.contains_point` 

2427 to select data points from an image. 

2428 

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. 

2432 

2433 Example usage:: 

2434 

2435 ax = subplot(111) 

2436 ax.plot(x, y) 

2437 

2438 def onselect(verts): 

2439 print(verts) 

2440 lasso = LassoSelector(ax, onselect) 

2441 

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 """ 

2453 

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] 

2466 

2467 def onpress(self, event): 

2468 self.press(event) 

2469 

2470 def _press(self, event): 

2471 self.verts = [self._get_data(event)] 

2472 self.line.set_visible(True) 

2473 

2474 def onrelease(self, event): 

2475 self.release(event) 

2476 

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 

2484 

2485 def _onmove(self, event): 

2486 if self.verts is None: 

2487 return 

2488 self.verts.append(self._get_data(event)) 

2489 

2490 self.line.set_data(list(zip(*self.verts))) 

2491 

2492 self.update() 

2493 

2494 

2495class PolygonSelector(_SelectorWidget): 

2496 """ 

2497 Select a polygon region of an axes. 

2498 

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. 

2505 

2506 For the selector to remain responsive you must keep a reference to 

2507 it. 

2508 

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. 

2530 

2531 Examples 

2532 -------- 

2533 :doc:`/gallery/widgets/polygon_selector_demo` 

2534 """ 

2535 

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) 

2549 

2550 self._xs, self._ys = [0], [0] 

2551 self._polygon_completed = False 

2552 

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) 

2558 

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) 

2564 

2565 self._active_handle_idx = -1 

2566 self.vertex_select_radius = vertex_select_radius 

2567 

2568 self.artists = [self.line, self._polygon_handles.artist] 

2569 self.set_visible(True) 

2570 

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[:] 

2582 

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 

2588 

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 

2594 

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) 

2601 

2602 if self._polygon_completed: 

2603 self.onselect(self.verts) 

2604 

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 

2616 

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 

2627 

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 

2635 

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 

2640 

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 

2652 

2653 self._draw_polygon() 

2654 

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() 

2663 

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) 

2681 

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() 

2696 

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])) 

2701 

2702 

2703class Lasso(AxesWidget): 

2704 """ 

2705 Selection curve of an arbitrary shape. 

2706 

2707 The selected path can be used in conjunction with 

2708 `~matplotlib.path.Path.contains_point` to select data points from an image. 

2709 

2710 Unlike `LassoSelector`, this must be initialized with a starting 

2711 point `xy`, and the `Lasso` events are destroyed upon release. 

2712 

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 """ 

2723 

2724 def __init__(self, ax, xy, callback=None, useblit=True): 

2725 AxesWidget.__init__(self, ax) 

2726 

2727 self.useblit = useblit and self.canvas.supports_blit 

2728 if self.useblit: 

2729 self.background = self.canvas.copy_from_bbox(self.ax.bbox) 

2730 

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) 

2738 

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() 

2749 

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)) 

2760 

2761 self.line.set_data(list(zip(*self.verts))) 

2762 

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()