Coverage for /usr/lib/python3/dist-packages/matplotlib/widgets.py: 17%

1811 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-06-14 15:25 +0200

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, Number 

15 

16import numpy as np 

17 

18import matplotlib as mpl 

19from . import (_api, _docstring, backend_tools, cbook, colors, ticker, 

20 transforms) 

21from .lines import Line2D 

22from .patches import Circle, Rectangle, Ellipse, Polygon 

23from .transforms import TransformedPatchPath, Affine2D 

24 

25 

26class LockDraw: 

27 """ 

28 Some widgets, like the cursor, draw onto the canvas, and this is not 

29 desirable under all circumstances, like when the toolbar is in zoom-to-rect 

30 mode and drawing a rectangle. To avoid this, a widget can acquire a 

31 canvas' lock with ``canvas.widgetlock(widget)`` before drawing on the 

32 canvas; this will prevent other widgets from doing so at the same time (if 

33 they also try to acquire the lock first). 

34 """ 

35 

36 def __init__(self): 

37 self._owner = None 

38 

39 def __call__(self, o): 

40 """Reserve the lock for *o*.""" 

41 if not self.available(o): 

42 raise ValueError('already locked') 

43 self._owner = o 

44 

45 def release(self, o): 

46 """Release the lock from *o*.""" 

47 if not self.available(o): 

48 raise ValueError('you do not own this lock') 

49 self._owner = None 

50 

51 def available(self, o): 

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

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

54 

55 def isowner(self, o): 

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

57 return self._owner is o 

58 

59 def locked(self): 

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

61 return self._owner is not None 

62 

63 

64class Widget: 

65 """ 

66 Abstract base class for GUI neutral widgets. 

67 """ 

68 drawon = True 

69 eventson = True 

70 _active = True 

71 

72 def set_active(self, active): 

73 """Set whether the widget is active.""" 

74 self._active = active 

75 

76 def get_active(self): 

77 """Get whether the widget is active.""" 

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

109 The parent figure canvas for the widget. 

110 active : bool 

111 If False, the widget does not respond to events. 

112 """ 

113 

114 def __init__(self, ax): 

115 self.ax = ax 

116 self.canvas = ax.figure.canvas 

117 self._cids = [] 

118 

119 def connect_event(self, event, callback): 

120 """ 

121 Connect a callback function with an event. 

122 

123 This should be used in lieu of ``figure.canvas.mpl_connect`` since this 

124 function stores callback ids for later clean up. 

125 """ 

126 cid = self.canvas.mpl_connect(event, callback) 

127 self._cids.append(cid) 

128 

129 def disconnect_events(self): 

130 """Disconnect all events created by this widget.""" 

131 for c in self._cids: 

132 self.canvas.mpl_disconnect(c) 

133 

134 

135class Button(AxesWidget): 

136 """ 

137 A GUI neutral button. 

138 

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

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

141 

142 Attributes 

143 ---------- 

144 ax 

145 The `matplotlib.axes.Axes` the button renders into. 

146 label 

147 A `matplotlib.text.Text` instance. 

148 color 

149 The color of the button when not hovering. 

150 hovercolor 

151 The color of the button when hovering. 

152 """ 

153 

154 def __init__(self, ax, label, image=None, 

155 color='0.85', hovercolor='0.95'): 

156 """ 

157 Parameters 

158 ---------- 

159 ax : `~matplotlib.axes.Axes` 

160 The `~.axes.Axes` instance the button will be placed into. 

161 label : str 

162 The button text. 

163 image : array-like or PIL Image 

164 The image to place in the button, if not *None*. The parameter is 

165 directly forwarded to `~matplotlib.axes.Axes.imshow`. 

166 color : color 

167 The color of the button when not activated. 

168 hovercolor : color 

169 The color of the button when the mouse is over it. 

170 """ 

171 super().__init__(ax) 

172 

173 if image is not None: 

174 ax.imshow(image) 

175 self.label = ax.text(0.5, 0.5, label, 

176 verticalalignment='center', 

177 horizontalalignment='center', 

178 transform=ax.transAxes) 

179 

180 self._observers = cbook.CallbackRegistry(signals=["clicked"]) 

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 def _click(self, event): 

193 if self.ignore(event) or event.inaxes != self.ax or not self.eventson: 

194 return 

195 if event.canvas.mouse_grabber != self.ax: 

196 event.canvas.grab_mouse(self.ax) 

197 

198 def _release(self, event): 

199 if self.ignore(event) or event.canvas.mouse_grabber != self.ax: 

200 return 

201 event.canvas.release_mouse(self.ax) 

202 if self.eventson and event.inaxes == self.ax: 

203 self._observers.process('clicked', event) 

204 

205 def _motion(self, event): 

206 if self.ignore(event): 

207 return 

208 c = self.hovercolor if event.inaxes == self.ax else self.color 

209 if not colors.same_color(c, self.ax.get_facecolor()): 

210 self.ax.set_facecolor(c) 

211 if self.drawon: 

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

213 

214 def on_clicked(self, func): 

215 """ 

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

217 

218 Returns a connection id, which can be used to disconnect the callback. 

219 """ 

220 return self._observers.connect('clicked', lambda event: func(event)) 

221 

222 def disconnect(self, cid): 

223 """Remove the callback function with connection id *cid*.""" 

224 self._observers.disconnect(cid) 

225 

226 

227class SliderBase(AxesWidget): 

228 """ 

229 The base class for constructing Slider widgets. Not intended for direct 

230 usage. 

231 

232 For the slider to remain responsive you must maintain a reference to it. 

233 """ 

234 def __init__(self, ax, orientation, closedmin, closedmax, 

235 valmin, valmax, valfmt, dragging, valstep): 

236 if ax.name == '3d': 

237 raise ValueError('Sliders cannot be added to 3D Axes') 

238 

239 super().__init__(ax) 

240 _api.check_in_list(['horizontal', 'vertical'], orientation=orientation) 

241 

242 self.orientation = orientation 

243 self.closedmin = closedmin 

244 self.closedmax = closedmax 

245 self.valmin = valmin 

246 self.valmax = valmax 

247 self.valstep = valstep 

248 self.drag_active = False 

249 self.valfmt = valfmt 

250 

251 if orientation == "vertical": 

252 ax.set_ylim((valmin, valmax)) 

253 axis = ax.yaxis 

254 else: 

255 ax.set_xlim((valmin, valmax)) 

256 axis = ax.xaxis 

257 

258 self._fmt = axis.get_major_formatter() 

259 if not isinstance(self._fmt, ticker.ScalarFormatter): 

260 self._fmt = ticker.ScalarFormatter() 

261 self._fmt.set_axis(axis) 

262 self._fmt.set_useOffset(False) # No additive offset. 

263 self._fmt.set_useMathText(True) # x sign before multiplicative offset. 

264 

265 ax.set_axis_off() 

266 ax.set_navigate(False) 

267 

268 self.connect_event("button_press_event", self._update) 

269 self.connect_event("button_release_event", self._update) 

270 if dragging: 

271 self.connect_event("motion_notify_event", self._update) 

272 self._observers = cbook.CallbackRegistry(signals=["changed"]) 

273 

274 def _stepped_value(self, val): 

275 """Return *val* coerced to closest number in the ``valstep`` grid.""" 

276 if isinstance(self.valstep, Number): 

277 val = (self.valmin 

278 + round((val - self.valmin) / self.valstep) * self.valstep) 

279 elif self.valstep is not None: 

280 valstep = np.asanyarray(self.valstep) 

281 if valstep.ndim != 1: 

282 raise ValueError( 

283 f"valstep must have 1 dimension but has {valstep.ndim}" 

284 ) 

285 val = valstep[np.argmin(np.abs(valstep - val))] 

286 return val 

287 

288 def disconnect(self, cid): 

289 """ 

290 Remove the observer with connection id *cid*. 

291 

292 Parameters 

293 ---------- 

294 cid : int 

295 Connection id of the observer to be removed. 

296 """ 

297 self._observers.disconnect(cid) 

298 

299 def reset(self): 

300 """Reset the slider to the initial value.""" 

301 if np.any(self.val != self.valinit): 

302 self.set_val(self.valinit) 

303 

304 

305class Slider(SliderBase): 

306 """ 

307 A slider representing a floating point range. 

308 

309 Create a slider from *valmin* to *valmax* in Axes *ax*. For the slider to 

310 remain responsive you must maintain a reference to it. Call 

311 :meth:`on_changed` to connect to the slider event. 

312 

313 Attributes 

314 ---------- 

315 val : float 

316 Slider value. 

317 """ 

318 

319 def __init__(self, ax, label, valmin, valmax, valinit=0.5, valfmt=None, 

320 closedmin=True, closedmax=True, slidermin=None, 

321 slidermax=None, dragging=True, valstep=None, 

322 orientation='horizontal', *, initcolor='r', 

323 track_color='lightgrey', handle_style=None, **kwargs): 

324 """ 

325 Parameters 

326 ---------- 

327 ax : Axes 

328 The Axes to put the slider in. 

329 

330 label : str 

331 Slider label. 

332 

333 valmin : float 

334 The minimum value of the slider. 

335 

336 valmax : float 

337 The maximum value of the slider. 

338 

339 valinit : float, default: 0.5 

340 The slider initial position. 

341 

342 valfmt : str, default: None 

343 %-format string used to format the slider value. If None, a 

344 `.ScalarFormatter` is used instead. 

345 

346 closedmin : bool, default: True 

347 Whether the slider interval is closed on the bottom. 

348 

349 closedmax : bool, default: True 

350 Whether the slider interval is closed on the top. 

351 

352 slidermin : Slider, default: None 

353 Do not allow the current slider to have a value less than 

354 the value of the Slider *slidermin*. 

355 

356 slidermax : Slider, default: None 

357 Do not allow the current slider to have a value greater than 

358 the value of the Slider *slidermax*. 

359 

360 dragging : bool, default: True 

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

362 

363 valstep : float or array-like, default: None 

364 If a float, the slider will snap to multiples of *valstep*. 

365 If an array the slider will snap to the values in the array. 

366 

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

368 The orientation of the slider. 

369 

370 initcolor : color, default: 'r' 

371 The color of the line at the *valinit* position. Set to ``'none'`` 

372 for no line. 

373 

374 track_color : color, default: 'lightgrey' 

375 The color of the background track. The track is accessible for 

376 further styling via the *track* attribute. 

377 

378 handle_style : dict 

379 Properties of the slider handle. Default values are 

380 

381 ========= ===== ======= ======================================== 

382 Key Value Default Description 

383 ========= ===== ======= ======================================== 

384 facecolor color 'white' The facecolor of the slider handle. 

385 edgecolor color '.75' The edgecolor of the slider handle. 

386 size int 10 The size of the slider handle in points. 

387 ========= ===== ======= ======================================== 

388 

389 Other values will be transformed as marker{foo} and passed to the 

390 `~.Line2D` constructor. e.g. ``handle_style = {'style'='x'}`` will 

391 result in ``markerstyle = 'x'``. 

392 

393 Notes 

394 ----- 

395 Additional kwargs are passed on to ``self.poly`` which is the 

396 `~matplotlib.patches.Polygon` that draws the slider knob. See the 

397 `.Polygon` documentation for valid property names (``facecolor``, 

398 ``edgecolor``, ``alpha``, etc.). 

399 """ 

400 super().__init__(ax, orientation, closedmin, closedmax, 

401 valmin, valmax, valfmt, dragging, valstep) 

402 

403 if slidermin is not None and not hasattr(slidermin, 'val'): 

404 raise ValueError( 

405 f"Argument slidermin ({type(slidermin)}) has no 'val'") 

406 if slidermax is not None and not hasattr(slidermax, 'val'): 

407 raise ValueError( 

408 f"Argument slidermax ({type(slidermax)}) has no 'val'") 

409 self.slidermin = slidermin 

410 self.slidermax = slidermax 

411 valinit = self._value_in_bounds(valinit) 

412 if valinit is None: 

413 valinit = valmin 

414 self.val = valinit 

415 self.valinit = valinit 

416 

417 defaults = {'facecolor': 'white', 'edgecolor': '.75', 'size': 10} 

418 handle_style = {} if handle_style is None else handle_style 

419 marker_props = { 

420 f'marker{k}': v for k, v in {**defaults, **handle_style}.items() 

421 } 

422 

423 if orientation == 'vertical': 

424 self.track = Rectangle( 

425 (.25, 0), .5, 1, 

426 transform=ax.transAxes, 

427 facecolor=track_color 

428 ) 

429 ax.add_patch(self.track) 

430 self.poly = ax.axhspan(valmin, valinit, .25, .75, **kwargs) 

431 # Drawing a longer line and clipping it to the track avoids 

432 # pixelation-related asymmetries. 

433 self.hline = ax.axhline(valinit, 0, 1, color=initcolor, lw=1, 

434 clip_path=TransformedPatchPath(self.track)) 

435 handleXY = [[0.5], [valinit]] 

436 else: 

437 self.track = Rectangle( 

438 (0, .25), 1, .5, 

439 transform=ax.transAxes, 

440 facecolor=track_color 

441 ) 

442 ax.add_patch(self.track) 

443 self.poly = ax.axvspan(valmin, valinit, .25, .75, **kwargs) 

444 self.vline = ax.axvline(valinit, 0, 1, color=initcolor, lw=1, 

445 clip_path=TransformedPatchPath(self.track)) 

446 handleXY = [[valinit], [0.5]] 

447 self._handle, = ax.plot( 

448 *handleXY, 

449 "o", 

450 **marker_props, 

451 clip_on=False 

452 ) 

453 

454 if orientation == 'vertical': 

455 self.label = ax.text(0.5, 1.02, label, transform=ax.transAxes, 

456 verticalalignment='bottom', 

457 horizontalalignment='center') 

458 

459 self.valtext = ax.text(0.5, -0.02, self._format(valinit), 

460 transform=ax.transAxes, 

461 verticalalignment='top', 

462 horizontalalignment='center') 

463 else: 

464 self.label = ax.text(-0.02, 0.5, label, transform=ax.transAxes, 

465 verticalalignment='center', 

466 horizontalalignment='right') 

467 

468 self.valtext = ax.text(1.02, 0.5, self._format(valinit), 

469 transform=ax.transAxes, 

470 verticalalignment='center', 

471 horizontalalignment='left') 

472 

473 self.set_val(valinit) 

474 

475 def _value_in_bounds(self, val): 

476 """Makes sure *val* is with given bounds.""" 

477 val = self._stepped_value(val) 

478 

479 if val <= self.valmin: 

480 if not self.closedmin: 

481 return 

482 val = self.valmin 

483 elif val >= self.valmax: 

484 if not self.closedmax: 

485 return 

486 val = self.valmax 

487 

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

489 if not self.closedmin: 

490 return 

491 val = self.slidermin.val 

492 

493 if self.slidermax is not None and val >= self.slidermax.val: 

494 if not self.closedmax: 

495 return 

496 val = self.slidermax.val 

497 return val 

498 

499 def _update(self, event): 

500 """Update the slider position.""" 

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

502 return 

503 

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

505 self.drag_active = True 

506 event.canvas.grab_mouse(self.ax) 

507 

508 if not self.drag_active: 

509 return 

510 

511 elif ((event.name == 'button_release_event') or 

512 (event.name == 'button_press_event' and 

513 event.inaxes != self.ax)): 

514 self.drag_active = False 

515 event.canvas.release_mouse(self.ax) 

516 return 

517 if self.orientation == 'vertical': 

518 val = self._value_in_bounds(event.ydata) 

519 else: 

520 val = self._value_in_bounds(event.xdata) 

521 if val not in [None, self.val]: 

522 self.set_val(val) 

523 

524 def _format(self, val): 

525 """Pretty-print *val*.""" 

526 if self.valfmt is not None: 

527 return self.valfmt % val 

528 else: 

529 _, s, _ = self._fmt.format_ticks([self.valmin, val, self.valmax]) 

530 # fmt.get_offset is actually the multiplicative factor, if any. 

531 return s + self._fmt.get_offset() 

532 

533 def set_val(self, val): 

534 """ 

535 Set slider value to *val*. 

536 

537 Parameters 

538 ---------- 

539 val : float 

540 """ 

541 xy = self.poly.xy 

542 if self.orientation == 'vertical': 

543 xy[1] = .25, val 

544 xy[2] = .75, val 

545 self._handle.set_ydata([val]) 

546 else: 

547 xy[2] = val, .75 

548 xy[3] = val, .25 

549 self._handle.set_xdata([val]) 

550 self.poly.xy = xy 

551 self.valtext.set_text(self._format(val)) 

552 if self.drawon: 

553 self.ax.figure.canvas.draw_idle() 

554 self.val = val 

555 if self.eventson: 

556 self._observers.process('changed', val) 

557 

558 def on_changed(self, func): 

559 """ 

560 Connect *func* as callback function to changes of the slider value. 

561 

562 Parameters 

563 ---------- 

564 func : callable 

565 Function to call when slider is changed. 

566 The function must accept a single float as its arguments. 

567 

568 Returns 

569 ------- 

570 int 

571 Connection id (which can be used to disconnect *func*). 

572 """ 

573 return self._observers.connect('changed', lambda val: func(val)) 

574 

575 

576class RangeSlider(SliderBase): 

577 """ 

578 A slider representing a range of floating point values. Defines the min and 

579 max of the range via the *val* attribute as a tuple of (min, max). 

580 

581 Create a slider that defines a range contained within [*valmin*, *valmax*] 

582 in Axes *ax*. For the slider to remain responsive you must maintain a 

583 reference to it. Call :meth:`on_changed` to connect to the slider event. 

584 

585 Attributes 

586 ---------- 

587 val : tuple of float 

588 Slider value. 

589 """ 

590 

591 def __init__( 

592 self, 

593 ax, 

594 label, 

595 valmin, 

596 valmax, 

597 valinit=None, 

598 valfmt=None, 

599 closedmin=True, 

600 closedmax=True, 

601 dragging=True, 

602 valstep=None, 

603 orientation="horizontal", 

604 track_color='lightgrey', 

605 handle_style=None, 

606 **kwargs, 

607 ): 

608 """ 

609 Parameters 

610 ---------- 

611 ax : Axes 

612 The Axes to put the slider in. 

613 

614 label : str 

615 Slider label. 

616 

617 valmin : float 

618 The minimum value of the slider. 

619 

620 valmax : float 

621 The maximum value of the slider. 

622 

623 valinit : tuple of float or None, default: None 

624 The initial positions of the slider. If None the initial positions 

625 will be at the 25th and 75th percentiles of the range. 

626 

627 valfmt : str, default: None 

628 %-format string used to format the slider values. If None, a 

629 `.ScalarFormatter` is used instead. 

630 

631 closedmin : bool, default: True 

632 Whether the slider interval is closed on the bottom. 

633 

634 closedmax : bool, default: True 

635 Whether the slider interval is closed on the top. 

636 

637 dragging : bool, default: True 

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

639 

640 valstep : float, default: None 

641 If given, the slider will snap to multiples of *valstep*. 

642 

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

644 The orientation of the slider. 

645 

646 track_color : color, default: 'lightgrey' 

647 The color of the background track. The track is accessible for 

648 further styling via the *track* attribute. 

649 

650 handle_style : dict 

651 Properties of the slider handles. Default values are 

652 

653 ========= ===== ======= ========================================= 

654 Key Value Default Description 

655 ========= ===== ======= ========================================= 

656 facecolor color 'white' The facecolor of the slider handles. 

657 edgecolor color '.75' The edgecolor of the slider handles. 

658 size int 10 The size of the slider handles in points. 

659 ========= ===== ======= ========================================= 

660 

661 Other values will be transformed as marker{foo} and passed to the 

662 `~.Line2D` constructor. e.g. ``handle_style = {'style'='x'}`` will 

663 result in ``markerstyle = 'x'``. 

664 

665 Notes 

666 ----- 

667 Additional kwargs are passed on to ``self.poly`` which is the 

668 `~matplotlib.patches.Polygon` that draws the slider knob. See the 

669 `.Polygon` documentation for valid property names (``facecolor``, 

670 ``edgecolor``, ``alpha``, etc.). 

671 """ 

672 super().__init__(ax, orientation, closedmin, closedmax, 

673 valmin, valmax, valfmt, dragging, valstep) 

674 

675 # Set a value to allow _value_in_bounds() to work. 

676 self.val = [valmin, valmax] 

677 if valinit is None: 

678 # Place at the 25th and 75th percentiles 

679 extent = valmax - valmin 

680 valinit = np.array([valmin + extent * 0.25, 

681 valmin + extent * 0.75]) 

682 else: 

683 valinit = self._value_in_bounds(valinit) 

684 self.val = valinit 

685 self.valinit = valinit 

686 

687 defaults = {'facecolor': 'white', 'edgecolor': '.75', 'size': 10} 

688 handle_style = {} if handle_style is None else handle_style 

689 marker_props = { 

690 f'marker{k}': v for k, v in {**defaults, **handle_style}.items() 

691 } 

692 

693 if orientation == "vertical": 

694 self.track = Rectangle( 

695 (.25, 0), .5, 2, 

696 transform=ax.transAxes, 

697 facecolor=track_color 

698 ) 

699 ax.add_patch(self.track) 

700 poly_transform = self.ax.get_yaxis_transform(which="grid") 

701 handleXY_1 = [.5, valinit[0]] 

702 handleXY_2 = [.5, valinit[1]] 

703 else: 

704 self.track = Rectangle( 

705 (0, .25), 1, .5, 

706 transform=ax.transAxes, 

707 facecolor=track_color 

708 ) 

709 ax.add_patch(self.track) 

710 poly_transform = self.ax.get_xaxis_transform(which="grid") 

711 handleXY_1 = [valinit[0], .5] 

712 handleXY_2 = [valinit[1], .5] 

713 self.poly = Polygon(np.zeros([5, 2]), **kwargs) 

714 self._update_selection_poly(*valinit) 

715 self.poly.set_transform(poly_transform) 

716 self.poly.get_path()._interpolation_steps = 100 

717 self.ax.add_patch(self.poly) 

718 self.ax._request_autoscale_view() 

719 self._handles = [ 

720 ax.plot( 

721 *handleXY_1, 

722 "o", 

723 **marker_props, 

724 clip_on=False 

725 )[0], 

726 ax.plot( 

727 *handleXY_2, 

728 "o", 

729 **marker_props, 

730 clip_on=False 

731 )[0] 

732 ] 

733 

734 if orientation == "vertical": 

735 self.label = ax.text( 

736 0.5, 

737 1.02, 

738 label, 

739 transform=ax.transAxes, 

740 verticalalignment="bottom", 

741 horizontalalignment="center", 

742 ) 

743 

744 self.valtext = ax.text( 

745 0.5, 

746 -0.02, 

747 self._format(valinit), 

748 transform=ax.transAxes, 

749 verticalalignment="top", 

750 horizontalalignment="center", 

751 ) 

752 else: 

753 self.label = ax.text( 

754 -0.02, 

755 0.5, 

756 label, 

757 transform=ax.transAxes, 

758 verticalalignment="center", 

759 horizontalalignment="right", 

760 ) 

761 

762 self.valtext = ax.text( 

763 1.02, 

764 0.5, 

765 self._format(valinit), 

766 transform=ax.transAxes, 

767 verticalalignment="center", 

768 horizontalalignment="left", 

769 ) 

770 

771 self._active_handle = None 

772 self.set_val(valinit) 

773 

774 def _update_selection_poly(self, vmin, vmax): 

775 """ 

776 Update the vertices of the *self.poly* slider in-place 

777 to cover the data range *vmin*, *vmax*. 

778 """ 

779 # The vertices are positioned 

780 # 1 ------ 2 

781 # | | 

782 # 0, 4 ---- 3 

783 verts = self.poly.xy 

784 if self.orientation == "vertical": 

785 verts[0] = verts[4] = .25, vmin 

786 verts[1] = .25, vmax 

787 verts[2] = .75, vmax 

788 verts[3] = .75, vmin 

789 else: 

790 verts[0] = verts[4] = vmin, .25 

791 verts[1] = vmin, .75 

792 verts[2] = vmax, .75 

793 verts[3] = vmax, .25 

794 

795 def _min_in_bounds(self, min): 

796 """Ensure the new min value is between valmin and self.val[1].""" 

797 if min <= self.valmin: 

798 if not self.closedmin: 

799 return self.val[0] 

800 min = self.valmin 

801 

802 if min > self.val[1]: 

803 min = self.val[1] 

804 return self._stepped_value(min) 

805 

806 def _max_in_bounds(self, max): 

807 """Ensure the new max value is between valmax and self.val[0].""" 

808 if max >= self.valmax: 

809 if not self.closedmax: 

810 return self.val[1] 

811 max = self.valmax 

812 

813 if max <= self.val[0]: 

814 max = self.val[0] 

815 return self._stepped_value(max) 

816 

817 def _value_in_bounds(self, vals): 

818 """Clip min, max values to the bounds.""" 

819 return (self._min_in_bounds(vals[0]), self._max_in_bounds(vals[1])) 

820 

821 def _update_val_from_pos(self, pos): 

822 """Update the slider value based on a given position.""" 

823 idx = np.argmin(np.abs(self.val - pos)) 

824 if idx == 0: 

825 val = self._min_in_bounds(pos) 

826 self.set_min(val) 

827 else: 

828 val = self._max_in_bounds(pos) 

829 self.set_max(val) 

830 if self._active_handle: 

831 if self.orientation == "vertical": 

832 self._active_handle.set_ydata([val]) 

833 else: 

834 self._active_handle.set_xdata([val]) 

835 

836 def _update(self, event): 

837 """Update the slider position.""" 

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

839 return 

840 

841 if event.name == "button_press_event" and event.inaxes == self.ax: 

842 self.drag_active = True 

843 event.canvas.grab_mouse(self.ax) 

844 

845 if not self.drag_active: 

846 return 

847 

848 elif (event.name == "button_release_event") or ( 

849 event.name == "button_press_event" and event.inaxes != self.ax 

850 ): 

851 self.drag_active = False 

852 event.canvas.release_mouse(self.ax) 

853 self._active_handle = None 

854 return 

855 

856 # determine which handle was grabbed 

857 if self.orientation == "vertical": 

858 handle_index = np.argmin( 

859 np.abs([h.get_ydata()[0] - event.ydata for h in self._handles]) 

860 ) 

861 else: 

862 handle_index = np.argmin( 

863 np.abs([h.get_xdata()[0] - event.xdata for h in self._handles]) 

864 ) 

865 handle = self._handles[handle_index] 

866 

867 # these checks ensure smooth behavior if the handles swap which one 

868 # has a higher value. i.e. if one is dragged over and past the other. 

869 if handle is not self._active_handle: 

870 self._active_handle = handle 

871 

872 if self.orientation == "vertical": 

873 self._update_val_from_pos(event.ydata) 

874 else: 

875 self._update_val_from_pos(event.xdata) 

876 

877 def _format(self, val): 

878 """Pretty-print *val*.""" 

879 if self.valfmt is not None: 

880 return f"({self.valfmt % val[0]}, {self.valfmt % val[1]})" 

881 else: 

882 _, s1, s2, _ = self._fmt.format_ticks( 

883 [self.valmin, *val, self.valmax] 

884 ) 

885 # fmt.get_offset is actually the multiplicative factor, if any. 

886 s1 += self._fmt.get_offset() 

887 s2 += self._fmt.get_offset() 

888 # Use f string to avoid issues with backslashes when cast to a str 

889 return f"({s1}, {s2})" 

890 

891 def set_min(self, min): 

892 """ 

893 Set the lower value of the slider to *min*. 

894 

895 Parameters 

896 ---------- 

897 min : float 

898 """ 

899 self.set_val((min, self.val[1])) 

900 

901 def set_max(self, max): 

902 """ 

903 Set the lower value of the slider to *max*. 

904 

905 Parameters 

906 ---------- 

907 max : float 

908 """ 

909 self.set_val((self.val[0], max)) 

910 

911 def set_val(self, val): 

912 """ 

913 Set slider value to *val*. 

914 

915 Parameters 

916 ---------- 

917 val : tuple or array-like of float 

918 """ 

919 val = np.sort(val) 

920 _api.check_shape((2,), val=val) 

921 vmin, vmax = val 

922 vmin = self._min_in_bounds(vmin) 

923 vmax = self._max_in_bounds(vmax) 

924 self._update_selection_poly(vmin, vmax) 

925 if self.orientation == "vertical": 

926 self._handles[0].set_ydata([vmin]) 

927 self._handles[1].set_ydata([vmax]) 

928 else: 

929 self._handles[0].set_xdata([vmin]) 

930 self._handles[1].set_xdata([vmax]) 

931 

932 self.valtext.set_text(self._format((vmin, vmax))) 

933 

934 if self.drawon: 

935 self.ax.figure.canvas.draw_idle() 

936 self.val = (vmin, vmax) 

937 if self.eventson: 

938 self._observers.process("changed", (vmin, vmax)) 

939 

940 def on_changed(self, func): 

941 """ 

942 Connect *func* as callback function to changes of the slider value. 

943 

944 Parameters 

945 ---------- 

946 func : callable 

947 Function to call when slider is changed. The function 

948 must accept a numpy array with shape (2,) as its argument. 

949 

950 Returns 

951 ------- 

952 int 

953 Connection id (which can be used to disconnect *func*). 

954 """ 

955 return self._observers.connect('changed', lambda val: func(val)) 

956 

957 

958class CheckButtons(AxesWidget): 

959 r""" 

960 A GUI neutral set of check buttons. 

961 

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

963 reference to this object. 

964 

965 Connect to the CheckButtons with the `.on_clicked` method. 

966 

967 Attributes 

968 ---------- 

969 ax : `~matplotlib.axes.Axes` 

970 The parent Axes for the widget. 

971 labels : list of `.Text` 

972 

973 rectangles : list of `.Rectangle` 

974 

975 lines : list of (`.Line2D`, `.Line2D`) pairs 

976 List of lines for the x's in the checkboxes. These lines exist for 

977 each box, but have ``set_visible(False)`` when its box is not checked. 

978 """ 

979 

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

981 """ 

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

983 

984 Parameters 

985 ---------- 

986 ax : `~matplotlib.axes.Axes` 

987 The parent Axes for the widget. 

988 

989 labels : list of str 

990 The labels of the check buttons. 

991 

992 actives : list of bool, optional 

993 The initial check states of the buttons. The list must have the 

994 same length as *labels*. If not given, all buttons are unchecked. 

995 """ 

996 super().__init__(ax) 

997 

998 ax.set_xticks([]) 

999 ax.set_yticks([]) 

1000 ax.set_navigate(False) 

1001 

1002 if actives is None: 

1003 actives = [False] * len(labels) 

1004 

1005 if len(labels) > 1: 

1006 dy = 1. / (len(labels) + 1) 

1007 ys = np.linspace(1 - dy, dy, len(labels)) 

1008 else: 

1009 dy = 0.25 

1010 ys = [0.5] 

1011 

1012 axcolor = ax.get_facecolor() 

1013 

1014 self.labels = [] 

1015 self.lines = [] 

1016 self.rectangles = [] 

1017 

1018 lineparams = {'color': 'k', 'linewidth': 1.25, 

1019 'transform': ax.transAxes, 'solid_capstyle': 'butt'} 

1020 for y, label, active in zip(ys, labels, actives): 

1021 t = ax.text(0.25, y, label, transform=ax.transAxes, 

1022 horizontalalignment='left', 

1023 verticalalignment='center') 

1024 

1025 w, h = dy / 2, dy / 2 

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

1027 

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

1029 facecolor=axcolor, transform=ax.transAxes) 

1030 

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

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

1033 

1034 l1.set_visible(active) 

1035 l2.set_visible(active) 

1036 self.labels.append(t) 

1037 self.rectangles.append(p) 

1038 self.lines.append((l1, l2)) 

1039 ax.add_patch(p) 

1040 ax.add_line(l1) 

1041 ax.add_line(l2) 

1042 

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

1044 

1045 self._observers = cbook.CallbackRegistry(signals=["clicked"]) 

1046 

1047 def _clicked(self, event): 

1048 if self.ignore(event) or event.button != 1 or event.inaxes != self.ax: 

1049 return 

1050 for i, (p, t) in enumerate(zip(self.rectangles, self.labels)): 

1051 if (t.get_window_extent().contains(event.x, event.y) or 

1052 p.get_window_extent().contains(event.x, event.y)): 

1053 self.set_active(i) 

1054 break 

1055 

1056 def set_active(self, index): 

1057 """ 

1058 Toggle (activate or deactivate) a check button by index. 

1059 

1060 Callbacks will be triggered if :attr:`eventson` is True. 

1061 

1062 Parameters 

1063 ---------- 

1064 index : int 

1065 Index of the check button to toggle. 

1066 

1067 Raises 

1068 ------ 

1069 ValueError 

1070 If *index* is invalid. 

1071 """ 

1072 if index not in range(len(self.labels)): 

1073 raise ValueError(f'Invalid CheckButton index: {index}') 

1074 

1075 l1, l2 = self.lines[index] 

1076 l1.set_visible(not l1.get_visible()) 

1077 l2.set_visible(not l2.get_visible()) 

1078 

1079 if self.drawon: 

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

1081 

1082 if self.eventson: 

1083 self._observers.process('clicked', self.labels[index].get_text()) 

1084 

1085 def get_status(self): 

1086 """ 

1087 Return a tuple of the status (True/False) of all of the check buttons. 

1088 """ 

1089 return [l1.get_visible() for (l1, l2) in self.lines] 

1090 

1091 def on_clicked(self, func): 

1092 """ 

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

1094 

1095 Returns a connection id, which can be used to disconnect the callback. 

1096 """ 

1097 return self._observers.connect('clicked', lambda text: func(text)) 

1098 

1099 def disconnect(self, cid): 

1100 """Remove the observer with connection id *cid*.""" 

1101 self._observers.disconnect(cid) 

1102 

1103 

1104class TextBox(AxesWidget): 

1105 """ 

1106 A GUI neutral text input box. 

1107 

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

1109 

1110 Call `.on_text_change` to be updated whenever the text changes. 

1111 

1112 Call `.on_submit` to be updated whenever the user hits enter or 

1113 leaves the text entry field. 

1114 

1115 Attributes 

1116 ---------- 

1117 ax : `~matplotlib.axes.Axes` 

1118 The parent Axes for the widget. 

1119 label : `.Text` 

1120 

1121 color : color 

1122 The color of the text box when not hovering. 

1123 hovercolor : color 

1124 The color of the text box when hovering. 

1125 """ 

1126 

1127 DIST_FROM_LEFT = _api.deprecate_privatize_attribute("3.5") 

1128 

1129 def __init__(self, ax, label, initial='', 

1130 color='.95', hovercolor='1', label_pad=.01, 

1131 textalignment="left"): 

1132 """ 

1133 Parameters 

1134 ---------- 

1135 ax : `~matplotlib.axes.Axes` 

1136 The `~.axes.Axes` instance the button will be placed into. 

1137 label : str 

1138 Label for this text box. 

1139 initial : str 

1140 Initial value in the text box. 

1141 color : color 

1142 The color of the box. 

1143 hovercolor : color 

1144 The color of the box when the mouse is over it. 

1145 label_pad : float 

1146 The distance between the label and the right side of the textbox. 

1147 textalignment : {'left', 'center', 'right'} 

1148 The horizontal location of the text. 

1149 """ 

1150 super().__init__(ax) 

1151 

1152 self._DIST_FROM_LEFT = .05 

1153 

1154 self._text_position = _api.check_getitem( 

1155 {"left": 0.05, "center": 0.5, "right": 0.95}, 

1156 textalignment=textalignment) 

1157 

1158 self.label = ax.text( 

1159 -label_pad, 0.5, label, transform=ax.transAxes, 

1160 verticalalignment='center', horizontalalignment='right') 

1161 

1162 # TextBox's text object should not parse mathtext at all. 

1163 self.text_disp = self.ax.text( 

1164 self._text_position, 0.5, initial, transform=self.ax.transAxes, 

1165 verticalalignment='center', horizontalalignment=textalignment, 

1166 parse_math=False) 

1167 

1168 self._observers = cbook.CallbackRegistry(signals=["change", "submit"]) 

1169 

1170 ax.set( 

1171 xlim=(0, 1), ylim=(0, 1), # s.t. cursor appears from first click. 

1172 navigate=False, facecolor=color, 

1173 xticks=[], yticks=[]) 

1174 

1175 self.cursor_index = 0 

1176 

1177 self.cursor = ax.vlines(0, 0, 0, visible=False, color="k", lw=1, 

1178 transform=mpl.transforms.IdentityTransform()) 

1179 

1180 self.connect_event('button_press_event', self._click) 

1181 self.connect_event('button_release_event', self._release) 

1182 self.connect_event('motion_notify_event', self._motion) 

1183 self.connect_event('key_press_event', self._keypress) 

1184 self.connect_event('resize_event', self._resize) 

1185 

1186 self.color = color 

1187 self.hovercolor = hovercolor 

1188 

1189 self.capturekeystrokes = False 

1190 

1191 @property 

1192 def text(self): 

1193 return self.text_disp.get_text() 

1194 

1195 def _rendercursor(self): 

1196 # this is a hack to figure out where the cursor should go. 

1197 # we draw the text up to where the cursor should go, measure 

1198 # and save its dimensions, draw the real text, then put the cursor 

1199 # at the saved dimensions 

1200 

1201 # This causes a single extra draw if the figure has never been rendered 

1202 # yet, which should be fine as we're going to repeatedly re-render the 

1203 # figure later anyways. 

1204 if self.ax.figure._get_renderer() is None: 

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

1206 

1207 text = self.text_disp.get_text() # Save value before overwriting it. 

1208 widthtext = text[:self.cursor_index] 

1209 

1210 bb_text = self.text_disp.get_window_extent() 

1211 self.text_disp.set_text(widthtext or ",") 

1212 bb_widthtext = self.text_disp.get_window_extent() 

1213 

1214 if bb_text.y0 == bb_text.y1: # Restoring the height if no text. 

1215 bb_text.y0 -= bb_widthtext.height / 2 

1216 bb_text.y1 += bb_widthtext.height / 2 

1217 elif not widthtext: # Keep width to 0. 

1218 bb_text.x1 = bb_text.x0 

1219 else: # Move the cursor using width of bb_widthtext. 

1220 bb_text.x1 = bb_text.x0 + bb_widthtext.width 

1221 

1222 self.cursor.set( 

1223 segments=[[(bb_text.x1, bb_text.y0), (bb_text.x1, bb_text.y1)]], 

1224 visible=True) 

1225 self.text_disp.set_text(text) 

1226 

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

1228 

1229 def _release(self, event): 

1230 if self.ignore(event): 

1231 return 

1232 if event.canvas.mouse_grabber != self.ax: 

1233 return 

1234 event.canvas.release_mouse(self.ax) 

1235 

1236 def _keypress(self, event): 

1237 if self.ignore(event): 

1238 return 

1239 if self.capturekeystrokes: 

1240 key = event.key 

1241 text = self.text 

1242 if len(key) == 1: 

1243 text = (text[:self.cursor_index] + key + 

1244 text[self.cursor_index:]) 

1245 self.cursor_index += 1 

1246 elif key == "right": 

1247 if self.cursor_index != len(text): 

1248 self.cursor_index += 1 

1249 elif key == "left": 

1250 if self.cursor_index != 0: 

1251 self.cursor_index -= 1 

1252 elif key == "home": 

1253 self.cursor_index = 0 

1254 elif key == "end": 

1255 self.cursor_index = len(text) 

1256 elif key == "backspace": 

1257 if self.cursor_index != 0: 

1258 text = (text[:self.cursor_index - 1] + 

1259 text[self.cursor_index:]) 

1260 self.cursor_index -= 1 

1261 elif key == "delete": 

1262 if self.cursor_index != len(self.text): 

1263 text = (text[:self.cursor_index] + 

1264 text[self.cursor_index + 1:]) 

1265 self.text_disp.set_text(text) 

1266 self._rendercursor() 

1267 if self.eventson: 

1268 self._observers.process('change', self.text) 

1269 if key in ["enter", "return"]: 

1270 self._observers.process('submit', self.text) 

1271 

1272 def set_val(self, val): 

1273 newval = str(val) 

1274 if self.text == newval: 

1275 return 

1276 self.text_disp.set_text(newval) 

1277 self._rendercursor() 

1278 if self.eventson: 

1279 self._observers.process('change', self.text) 

1280 self._observers.process('submit', self.text) 

1281 

1282 def begin_typing(self, x): 

1283 self.capturekeystrokes = True 

1284 # Disable keypress shortcuts, which may otherwise cause the figure to 

1285 # be saved, closed, etc., until the user stops typing. The way to 

1286 # achieve this depends on whether toolmanager is in use. 

1287 stack = ExitStack() # Register cleanup actions when user stops typing. 

1288 self._on_stop_typing = stack.close 

1289 toolmanager = getattr( 

1290 self.ax.figure.canvas.manager, "toolmanager", None) 

1291 if toolmanager is not None: 

1292 # If using toolmanager, lock keypresses, and plan to release the 

1293 # lock when typing stops. 

1294 toolmanager.keypresslock(self) 

1295 stack.callback(toolmanager.keypresslock.release, self) 

1296 else: 

1297 # If not using toolmanager, disable all keypress-related rcParams. 

1298 # Avoid spurious warnings if keymaps are getting deprecated. 

1299 with _api.suppress_matplotlib_deprecation_warning(): 

1300 stack.enter_context(mpl.rc_context( 

1301 {k: [] for k in mpl.rcParams if k.startswith("keymap.")})) 

1302 

1303 def stop_typing(self): 

1304 if self.capturekeystrokes: 

1305 self._on_stop_typing() 

1306 self._on_stop_typing = None 

1307 notifysubmit = True 

1308 else: 

1309 notifysubmit = False 

1310 self.capturekeystrokes = False 

1311 self.cursor.set_visible(False) 

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

1313 if notifysubmit and self.eventson: 

1314 # Because process() might throw an error in the user's code, only 

1315 # call it once we've already done our cleanup. 

1316 self._observers.process('submit', self.text) 

1317 

1318 def position_cursor(self, x): 

1319 # now, we have to figure out where the cursor goes. 

1320 # approximate it based on assuming all characters the same length 

1321 if len(self.text) == 0: 

1322 self.cursor_index = 0 

1323 else: 

1324 bb = self.text_disp.get_window_extent() 

1325 ratio = np.clip((x - bb.x0) / bb.width, 0, 1) 

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

1327 self._rendercursor() 

1328 

1329 def _click(self, event): 

1330 if self.ignore(event): 

1331 return 

1332 if event.inaxes != self.ax: 

1333 self.stop_typing() 

1334 return 

1335 if not self.eventson: 

1336 return 

1337 if event.canvas.mouse_grabber != self.ax: 

1338 event.canvas.grab_mouse(self.ax) 

1339 if not self.capturekeystrokes: 

1340 self.begin_typing(event.x) 

1341 self.position_cursor(event.x) 

1342 

1343 def _resize(self, event): 

1344 self.stop_typing() 

1345 

1346 def _motion(self, event): 

1347 if self.ignore(event): 

1348 return 

1349 c = self.hovercolor if event.inaxes == self.ax else self.color 

1350 if not colors.same_color(c, self.ax.get_facecolor()): 

1351 self.ax.set_facecolor(c) 

1352 if self.drawon: 

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

1354 

1355 def on_text_change(self, func): 

1356 """ 

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

1358 

1359 A connection id is returned which can be used to disconnect. 

1360 """ 

1361 return self._observers.connect('change', lambda text: func(text)) 

1362 

1363 def on_submit(self, func): 

1364 """ 

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

1366 *func* with event. 

1367 

1368 A connection id is returned which can be used to disconnect. 

1369 """ 

1370 return self._observers.connect('submit', lambda text: func(text)) 

1371 

1372 def disconnect(self, cid): 

1373 """Remove the observer with connection id *cid*.""" 

1374 self._observers.disconnect(cid) 

1375 

1376 

1377class RadioButtons(AxesWidget): 

1378 """ 

1379 A GUI neutral radio button. 

1380 

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

1382 object. 

1383 

1384 Connect to the RadioButtons with the `.on_clicked` method. 

1385 

1386 Attributes 

1387 ---------- 

1388 ax : `~matplotlib.axes.Axes` 

1389 The parent Axes for the widget. 

1390 activecolor : color 

1391 The color of the selected button. 

1392 labels : list of `.Text` 

1393 The button labels. 

1394 circles : list of `~.patches.Circle` 

1395 The buttons. 

1396 value_selected : str 

1397 The label text of the currently selected button. 

1398 """ 

1399 

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

1401 """ 

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

1403 

1404 Parameters 

1405 ---------- 

1406 ax : `~matplotlib.axes.Axes` 

1407 The Axes to add the buttons to. 

1408 labels : list of str 

1409 The button labels. 

1410 active : int 

1411 The index of the initially selected button. 

1412 activecolor : color 

1413 The color of the selected button. 

1414 """ 

1415 super().__init__(ax) 

1416 self.activecolor = activecolor 

1417 self.value_selected = None 

1418 

1419 ax.set_xticks([]) 

1420 ax.set_yticks([]) 

1421 ax.set_navigate(False) 

1422 dy = 1. / (len(labels) + 1) 

1423 ys = np.linspace(1 - dy, dy, len(labels)) 

1424 cnt = 0 

1425 axcolor = ax.get_facecolor() 

1426 

1427 # scale the radius of the circle with the spacing between each one 

1428 circle_radius = dy / 2 - 0.01 

1429 # default to hard-coded value if the radius becomes too large 

1430 circle_radius = min(circle_radius, 0.05) 

1431 

1432 self.labels = [] 

1433 self.circles = [] 

1434 for y, label in zip(ys, labels): 

1435 t = ax.text(0.25, y, label, transform=ax.transAxes, 

1436 horizontalalignment='left', 

1437 verticalalignment='center') 

1438 

1439 if cnt == active: 

1440 self.value_selected = label 

1441 facecolor = activecolor 

1442 else: 

1443 facecolor = axcolor 

1444 

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

1446 facecolor=facecolor, transform=ax.transAxes) 

1447 

1448 self.labels.append(t) 

1449 self.circles.append(p) 

1450 ax.add_patch(p) 

1451 cnt += 1 

1452 

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

1454 

1455 self._observers = cbook.CallbackRegistry(signals=["clicked"]) 

1456 

1457 def _clicked(self, event): 

1458 if self.ignore(event) or event.button != 1 or event.inaxes != self.ax: 

1459 return 

1460 pclicked = self.ax.transAxes.inverted().transform((event.x, event.y)) 

1461 distances = {} 

1462 for i, (p, t) in enumerate(zip(self.circles, self.labels)): 

1463 if (t.get_window_extent().contains(event.x, event.y) 

1464 or np.linalg.norm(pclicked - p.center) < p.radius): 

1465 distances[i] = np.linalg.norm(pclicked - p.center) 

1466 if len(distances) > 0: 

1467 closest = min(distances, key=distances.get) 

1468 self.set_active(closest) 

1469 

1470 def set_active(self, index): 

1471 """ 

1472 Select button with number *index*. 

1473 

1474 Callbacks will be triggered if :attr:`eventson` is True. 

1475 """ 

1476 if index not in range(len(self.labels)): 

1477 raise ValueError(f'Invalid RadioButton index: {index}') 

1478 

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

1480 

1481 for i, p in enumerate(self.circles): 

1482 if i == index: 

1483 color = self.activecolor 

1484 else: 

1485 color = self.ax.get_facecolor() 

1486 p.set_facecolor(color) 

1487 

1488 if self.drawon: 

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

1490 

1491 if self.eventson: 

1492 self._observers.process('clicked', self.labels[index].get_text()) 

1493 

1494 def on_clicked(self, func): 

1495 """ 

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

1497 

1498 Returns a connection id, which can be used to disconnect the callback. 

1499 """ 

1500 return self._observers.connect('clicked', func) 

1501 

1502 def disconnect(self, cid): 

1503 """Remove the observer with connection id *cid*.""" 

1504 self._observers.disconnect(cid) 

1505 

1506 

1507class SubplotTool(Widget): 

1508 """ 

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

1510 """ 

1511 

1512 def __init__(self, targetfig, toolfig): 

1513 """ 

1514 Parameters 

1515 ---------- 

1516 targetfig : `.Figure` 

1517 The figure instance to adjust. 

1518 toolfig : `.Figure` 

1519 The figure instance to embed the subplot tool into. 

1520 """ 

1521 

1522 self.figure = toolfig 

1523 self.targetfig = targetfig 

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

1525 toolfig.suptitle("Click on slider to adjust subplot param") 

1526 

1527 self._sliders = [] 

1528 names = ["left", "bottom", "right", "top", "wspace", "hspace"] 

1529 # The last subplot, removed below, keeps space for the "Reset" button. 

1530 for name, ax in zip(names, toolfig.subplots(len(names) + 1)): 

1531 ax.set_navigate(False) 

1532 slider = Slider(ax, name, 

1533 0, 1, getattr(targetfig.subplotpars, name)) 

1534 slider.on_changed(self._on_slider_changed) 

1535 self._sliders.append(slider) 

1536 toolfig.axes[-1].remove() 

1537 (self.sliderleft, self.sliderbottom, self.sliderright, self.slidertop, 

1538 self.sliderwspace, self.sliderhspace) = self._sliders 

1539 for slider in [self.sliderleft, self.sliderbottom, 

1540 self.sliderwspace, self.sliderhspace]: 

1541 slider.closedmax = False 

1542 for slider in [self.sliderright, self.slidertop]: 

1543 slider.closedmin = False 

1544 

1545 # constraints 

1546 self.sliderleft.slidermax = self.sliderright 

1547 self.sliderright.slidermin = self.sliderleft 

1548 self.sliderbottom.slidermax = self.slidertop 

1549 self.slidertop.slidermin = self.sliderbottom 

1550 

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

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

1553 self.buttonreset.on_clicked(self._on_reset) 

1554 

1555 def _on_slider_changed(self, _): 

1556 self.targetfig.subplots_adjust( 

1557 **{slider.label.get_text(): slider.val 

1558 for slider in self._sliders}) 

1559 if self.drawon: 

1560 self.targetfig.canvas.draw() 

1561 

1562 def _on_reset(self, event): 

1563 with ExitStack() as stack: 

1564 # Temporarily disable drawing on self and self's sliders, and 

1565 # disconnect slider events (as the subplotparams can be temporarily 

1566 # invalid, depending on the order in which they are restored). 

1567 stack.enter_context(cbook._setattr_cm(self, drawon=False)) 

1568 for slider in self._sliders: 

1569 stack.enter_context( 

1570 cbook._setattr_cm(slider, drawon=False, eventson=False)) 

1571 # Reset the slider to the initial position. 

1572 for slider in self._sliders: 

1573 slider.reset() 

1574 if self.drawon: 

1575 event.canvas.draw() # Redraw the subplottool canvas. 

1576 self._on_slider_changed(None) # Apply changes to the target window. 

1577 

1578 

1579class Cursor(AxesWidget): 

1580 """ 

1581 A crosshair cursor that spans the Axes and moves with mouse cursor. 

1582 

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

1584 

1585 Parameters 

1586 ---------- 

1587 ax : `matplotlib.axes.Axes` 

1588 The `~.axes.Axes` to attach the cursor to. 

1589 horizOn : bool, default: True 

1590 Whether to draw the horizontal line. 

1591 vertOn : bool, default: True 

1592 Whether to draw the vertical line. 

1593 useblit : bool, default: False 

1594 Use blitting for faster drawing if supported by the backend. 

1595 See the tutorial :doc:`/tutorials/advanced/blitting` for details. 

1596 

1597 Other Parameters 

1598 ---------------- 

1599 **lineprops 

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

1601 See also `~.Axes.axhline`. 

1602 

1603 Examples 

1604 -------- 

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

1606 """ 

1607 

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

1609 **lineprops): 

1610 super().__init__(ax) 

1611 

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

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

1614 

1615 self.visible = True 

1616 self.horizOn = horizOn 

1617 self.vertOn = vertOn 

1618 self.useblit = useblit and self.canvas.supports_blit 

1619 

1620 if self.useblit: 

1621 lineprops['animated'] = True 

1622 self.lineh = ax.axhline(ax.get_ybound()[0], visible=False, **lineprops) 

1623 self.linev = ax.axvline(ax.get_xbound()[0], visible=False, **lineprops) 

1624 

1625 self.background = None 

1626 self.needclear = False 

1627 

1628 def clear(self, event): 

1629 """Internal event handler to clear the cursor.""" 

1630 if self.ignore(event): 

1631 return 

1632 if self.useblit: 

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

1634 self.linev.set_visible(False) 

1635 self.lineh.set_visible(False) 

1636 

1637 def onmove(self, event): 

1638 """Internal event handler to draw the cursor when the mouse moves.""" 

1639 if self.ignore(event): 

1640 return 

1641 if not self.canvas.widgetlock.available(self): 

1642 return 

1643 if event.inaxes != self.ax: 

1644 self.linev.set_visible(False) 

1645 self.lineh.set_visible(False) 

1646 

1647 if self.needclear: 

1648 self.canvas.draw() 

1649 self.needclear = False 

1650 return 

1651 self.needclear = True 

1652 if not self.visible: 

1653 return 

1654 self.linev.set_xdata((event.xdata, event.xdata)) 

1655 

1656 self.lineh.set_ydata((event.ydata, event.ydata)) 

1657 self.linev.set_visible(self.visible and self.vertOn) 

1658 self.lineh.set_visible(self.visible and self.horizOn) 

1659 

1660 self._update() 

1661 

1662 def _update(self): 

1663 if self.useblit: 

1664 if self.background is not None: 

1665 self.canvas.restore_region(self.background) 

1666 self.ax.draw_artist(self.linev) 

1667 self.ax.draw_artist(self.lineh) 

1668 self.canvas.blit(self.ax.bbox) 

1669 else: 

1670 self.canvas.draw_idle() 

1671 return False 

1672 

1673 

1674class MultiCursor(Widget): 

1675 """ 

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

1677 multiple Axes. 

1678 

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

1680 

1681 Parameters 

1682 ---------- 

1683 canvas : object 

1684 This parameter is entirely unused and only kept for back-compatibility. 

1685 

1686 axes : list of `matplotlib.axes.Axes` 

1687 The `~.axes.Axes` to attach the cursor to. 

1688 

1689 useblit : bool, default: True 

1690 Use blitting for faster drawing if supported by the backend. 

1691 See the tutorial :doc:`/tutorials/advanced/blitting` 

1692 for details. 

1693 

1694 horizOn : bool, default: False 

1695 Whether to draw the horizontal line. 

1696 

1697 vertOn : bool, default: True 

1698 Whether to draw the vertical line. 

1699 

1700 Other Parameters 

1701 ---------------- 

1702 **lineprops 

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

1704 See also `~.Axes.axhline`. 

1705 

1706 Examples 

1707 -------- 

1708 See :doc:`/gallery/widgets/multicursor`. 

1709 """ 

1710 

1711 @_api.make_keyword_only("3.6", "useblit") 

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

1713 **lineprops): 

1714 # canvas is stored only to provide the deprecated .canvas attribute; 

1715 # once it goes away the unused argument won't need to be stored at all. 

1716 self._canvas = canvas 

1717 

1718 self.axes = axes 

1719 self.horizOn = horizOn 

1720 self.vertOn = vertOn 

1721 

1722 self._canvas_infos = { 

1723 ax.figure.canvas: {"cids": [], "background": None} for ax in axes} 

1724 

1725 xmin, xmax = axes[-1].get_xlim() 

1726 ymin, ymax = axes[-1].get_ylim() 

1727 xmid = 0.5 * (xmin + xmax) 

1728 ymid = 0.5 * (ymin + ymax) 

1729 

1730 self.visible = True 

1731 self.useblit = ( 

1732 useblit 

1733 and all(canvas.supports_blit for canvas in self._canvas_infos)) 

1734 self.needclear = False 

1735 

1736 if self.useblit: 

1737 lineprops['animated'] = True 

1738 

1739 if vertOn: 

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

1741 for ax in axes] 

1742 else: 

1743 self.vlines = [] 

1744 

1745 if horizOn: 

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

1747 for ax in axes] 

1748 else: 

1749 self.hlines = [] 

1750 

1751 self.connect() 

1752 

1753 canvas = _api.deprecate_privatize_attribute("3.6") 

1754 background = _api.deprecated("3.6")(lambda self: ( 

1755 self._backgrounds[self.axes[0].figure.canvas] if self.axes else None)) 

1756 

1757 def connect(self): 

1758 """Connect events.""" 

1759 for canvas, info in self._canvas_infos.items(): 

1760 info["cids"] = [ 

1761 canvas.mpl_connect('motion_notify_event', self.onmove), 

1762 canvas.mpl_connect('draw_event', self.clear), 

1763 ] 

1764 

1765 def disconnect(self): 

1766 """Disconnect events.""" 

1767 for canvas, info in self._canvas_infos.items(): 

1768 for cid in info["cids"]: 

1769 canvas.mpl_disconnect(cid) 

1770 info["cids"].clear() 

1771 

1772 def clear(self, event): 

1773 """Clear the cursor.""" 

1774 if self.ignore(event): 

1775 return 

1776 if self.useblit: 

1777 for canvas, info in self._canvas_infos.items(): 

1778 info["background"] = canvas.copy_from_bbox(canvas.figure.bbox) 

1779 for line in self.vlines + self.hlines: 

1780 line.set_visible(False) 

1781 

1782 def onmove(self, event): 

1783 if (self.ignore(event) 

1784 or event.inaxes not in self.axes 

1785 or not event.canvas.widgetlock.available(self)): 

1786 return 

1787 self.needclear = True 

1788 if not self.visible: 

1789 return 

1790 if self.vertOn: 

1791 for line in self.vlines: 

1792 line.set_xdata((event.xdata, event.xdata)) 

1793 line.set_visible(self.visible) 

1794 if self.horizOn: 

1795 for line in self.hlines: 

1796 line.set_ydata((event.ydata, event.ydata)) 

1797 line.set_visible(self.visible) 

1798 self._update() 

1799 

1800 def _update(self): 

1801 if self.useblit: 

1802 for canvas, info in self._canvas_infos.items(): 

1803 if info["background"]: 

1804 canvas.restore_region(info["background"]) 

1805 if self.vertOn: 

1806 for ax, line in zip(self.axes, self.vlines): 

1807 ax.draw_artist(line) 

1808 if self.horizOn: 

1809 for ax, line in zip(self.axes, self.hlines): 

1810 ax.draw_artist(line) 

1811 for canvas in self._canvas_infos: 

1812 canvas.blit() 

1813 else: 

1814 for canvas in self._canvas_infos: 

1815 canvas.draw_idle() 

1816 

1817 

1818class _SelectorWidget(AxesWidget): 

1819 

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

1821 state_modifier_keys=None, use_data_coordinates=False): 

1822 super().__init__(ax) 

1823 

1824 self._visible = True 

1825 self.onselect = onselect 

1826 self.useblit = useblit and self.canvas.supports_blit 

1827 self.connect_default_events() 

1828 

1829 self._state_modifier_keys = dict(move=' ', clear='escape', 

1830 square='shift', center='control', 

1831 rotate='r') 

1832 self._state_modifier_keys.update(state_modifier_keys or {}) 

1833 self._use_data_coordinates = use_data_coordinates 

1834 

1835 self.background = None 

1836 

1837 if isinstance(button, Integral): 

1838 self.validButtons = [button] 

1839 else: 

1840 self.validButtons = button 

1841 

1842 # Set to True when a selection is completed, otherwise is False 

1843 self._selection_completed = False 

1844 

1845 # will save the data (position at mouseclick) 

1846 self._eventpress = None 

1847 # will save the data (pos. at mouserelease) 

1848 self._eventrelease = None 

1849 self._prev_event = None 

1850 self._state = set() 

1851 

1852 eventpress = _api.deprecate_privatize_attribute("3.5") 

1853 eventrelease = _api.deprecate_privatize_attribute("3.5") 

1854 state = _api.deprecate_privatize_attribute("3.5") 

1855 state_modifier_keys = _api.deprecate_privatize_attribute("3.6") 

1856 

1857 def set_active(self, active): 

1858 super().set_active(active) 

1859 if active: 

1860 self.update_background(None) 

1861 

1862 def _get_animated_artists(self): 

1863 """ 

1864 Convenience method to get all animated artists of the figure containing 

1865 this widget, excluding those already present in self.artists. 

1866 The returned tuple is not sorted by 'z_order': z_order sorting is 

1867 valid only when considering all artists and not only a subset of all 

1868 artists. 

1869 """ 

1870 return tuple(a for ax_ in self.ax.get_figure().get_axes() 

1871 for a in ax_.get_children() 

1872 if a.get_animated() and a not in self.artists) 

1873 

1874 def update_background(self, event): 

1875 """Force an update of the background.""" 

1876 # If you add a call to `ignore` here, you'll want to check edge case: 

1877 # `release` can call a draw event even when `ignore` is True. 

1878 if not self.useblit: 

1879 return 

1880 # Make sure that widget artists don't get accidentally included in the 

1881 # background, by re-rendering the background if needed (and then 

1882 # re-re-rendering the canvas with the visible widget artists). 

1883 # We need to remove all artists which will be drawn when updating 

1884 # the selector: if we have animated artists in the figure, it is safer 

1885 # to redrawn by default, in case they have updated by the callback 

1886 # zorder needs to be respected when redrawing 

1887 artists = sorted(self.artists + self._get_animated_artists(), 

1888 key=lambda a: a.get_zorder()) 

1889 needs_redraw = any(artist.get_visible() for artist in artists) 

1890 with ExitStack() as stack: 

1891 if needs_redraw: 

1892 for artist in artists: 

1893 stack.enter_context(artist._cm_set(visible=False)) 

1894 self.canvas.draw() 

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

1896 if needs_redraw: 

1897 for artist in artists: 

1898 self.ax.draw_artist(artist) 

1899 

1900 def connect_default_events(self): 

1901 """Connect the major canvas events to methods.""" 

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

1903 self.connect_event('button_press_event', self.press) 

1904 self.connect_event('button_release_event', self.release) 

1905 self.connect_event('draw_event', self.update_background) 

1906 self.connect_event('key_press_event', self.on_key_press) 

1907 self.connect_event('key_release_event', self.on_key_release) 

1908 self.connect_event('scroll_event', self.on_scroll) 

1909 

1910 def ignore(self, event): 

1911 # docstring inherited 

1912 if not self.active or not self.ax.get_visible(): 

1913 return True 

1914 # If canvas was locked 

1915 if not self.canvas.widgetlock.available(self): 

1916 return True 

1917 if not hasattr(event, 'button'): 

1918 event.button = None 

1919 # Only do rectangle selection if event was triggered 

1920 # with a desired button 

1921 if (self.validButtons is not None 

1922 and event.button not in self.validButtons): 

1923 return True 

1924 # If no button was pressed yet ignore the event if it was out 

1925 # of the Axes 

1926 if self._eventpress is None: 

1927 return event.inaxes != self.ax 

1928 # If a button was pressed, check if the release-button is the same. 

1929 if event.button == self._eventpress.button: 

1930 return False 

1931 # If a button was pressed, check if the release-button is the same. 

1932 return (event.inaxes != self.ax or 

1933 event.button != self._eventpress.button) 

1934 

1935 def update(self): 

1936 """Draw using blit() or draw_idle(), depending on ``self.useblit``.""" 

1937 if (not self.ax.get_visible() or 

1938 self.ax.figure._get_renderer() is None): 

1939 return 

1940 if self.useblit: 

1941 if self.background is not None: 

1942 self.canvas.restore_region(self.background) 

1943 else: 

1944 self.update_background(None) 

1945 # We need to draw all artists, which are not included in the 

1946 # background, therefore we also draw self._get_animated_artists() 

1947 # and we make sure that we respect z_order 

1948 artists = sorted(self.artists + self._get_animated_artists(), 

1949 key=lambda a: a.get_zorder()) 

1950 for artist in artists: 

1951 self.ax.draw_artist(artist) 

1952 self.canvas.blit(self.ax.bbox) 

1953 else: 

1954 self.canvas.draw_idle() 

1955 

1956 def _get_data(self, event): 

1957 """Get the xdata and ydata for event, with limits.""" 

1958 if event.xdata is None: 

1959 return None, None 

1960 xdata = np.clip(event.xdata, *self.ax.get_xbound()) 

1961 ydata = np.clip(event.ydata, *self.ax.get_ybound()) 

1962 return xdata, ydata 

1963 

1964 def _clean_event(self, event): 

1965 """ 

1966 Preprocess an event: 

1967 

1968 - Replace *event* by the previous event if *event* has no ``xdata``. 

1969 - Clip ``xdata`` and ``ydata`` to the axes limits. 

1970 - Update the previous event. 

1971 """ 

1972 if event.xdata is None: 

1973 event = self._prev_event 

1974 else: 

1975 event = copy.copy(event) 

1976 event.xdata, event.ydata = self._get_data(event) 

1977 self._prev_event = event 

1978 return event 

1979 

1980 def press(self, event): 

1981 """Button press handler and validator.""" 

1982 if not self.ignore(event): 

1983 event = self._clean_event(event) 

1984 self._eventpress = event 

1985 self._prev_event = event 

1986 key = event.key or '' 

1987 key = key.replace('ctrl', 'control') 

1988 # move state is locked in on a button press 

1989 if key == self._state_modifier_keys['move']: 

1990 self._state.add('move') 

1991 self._press(event) 

1992 return True 

1993 return False 

1994 

1995 def _press(self, event): 

1996 """Button press event handler.""" 

1997 

1998 def release(self, event): 

1999 """Button release event handler and validator.""" 

2000 if not self.ignore(event) and self._eventpress: 

2001 event = self._clean_event(event) 

2002 self._eventrelease = event 

2003 self._release(event) 

2004 self._eventpress = None 

2005 self._eventrelease = None 

2006 self._state.discard('move') 

2007 return True 

2008 return False 

2009 

2010 def _release(self, event): 

2011 """Button release event handler.""" 

2012 

2013 def onmove(self, event): 

2014 """Cursor move event handler and validator.""" 

2015 if not self.ignore(event) and self._eventpress: 

2016 event = self._clean_event(event) 

2017 self._onmove(event) 

2018 return True 

2019 return False 

2020 

2021 def _onmove(self, event): 

2022 """Cursor move event handler.""" 

2023 

2024 def on_scroll(self, event): 

2025 """Mouse scroll event handler and validator.""" 

2026 if not self.ignore(event): 

2027 self._on_scroll(event) 

2028 

2029 def _on_scroll(self, event): 

2030 """Mouse scroll event handler.""" 

2031 

2032 def on_key_press(self, event): 

2033 """Key press event handler and validator for all selection widgets.""" 

2034 if self.active: 

2035 key = event.key or '' 

2036 key = key.replace('ctrl', 'control') 

2037 if key == self._state_modifier_keys['clear']: 

2038 self.clear() 

2039 return 

2040 for (state, modifier) in self._state_modifier_keys.items(): 

2041 if modifier in key.split('+'): 

2042 # 'rotate' is changing _state on press and is not removed 

2043 # from _state when releasing 

2044 if state == 'rotate': 

2045 if state in self._state: 

2046 self._state.discard(state) 

2047 else: 

2048 self._state.add(state) 

2049 else: 

2050 self._state.add(state) 

2051 self._on_key_press(event) 

2052 

2053 def _on_key_press(self, event): 

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

2055 

2056 def on_key_release(self, event): 

2057 """Key release event handler and validator.""" 

2058 if self.active: 

2059 key = event.key or '' 

2060 for (state, modifier) in self._state_modifier_keys.items(): 

2061 # 'rotate' is changing _state on press and is not removed 

2062 # from _state when releasing 

2063 if modifier in key.split('+') and state != 'rotate': 

2064 self._state.discard(state) 

2065 self._on_key_release(event) 

2066 

2067 def _on_key_release(self, event): 

2068 """Key release event handler.""" 

2069 

2070 def set_visible(self, visible): 

2071 """Set the visibility of the selector artists.""" 

2072 self._visible = visible 

2073 for artist in self.artists: 

2074 artist.set_visible(visible) 

2075 

2076 def get_visible(self): 

2077 """Get the visibility of the selector artists.""" 

2078 return self._visible 

2079 

2080 @property 

2081 def visible(self): 

2082 return self.get_visible() 

2083 

2084 @visible.setter 

2085 def visible(self, visible): 

2086 _api.warn_deprecated("3.6", alternative="set_visible") 

2087 self.set_visible(visible) 

2088 

2089 def clear(self): 

2090 """Clear the selection and set the selector ready to make a new one.""" 

2091 self._clear_without_update() 

2092 self.update() 

2093 

2094 def _clear_without_update(self): 

2095 self._selection_completed = False 

2096 self.set_visible(False) 

2097 

2098 @property 

2099 def artists(self): 

2100 """Tuple of the artists of the selector.""" 

2101 handles_artists = getattr(self, '_handles_artists', ()) 

2102 return (self._selection_artist,) + handles_artists 

2103 

2104 def set_props(self, **props): 

2105 """ 

2106 Set the properties of the selector artist. See the `props` argument 

2107 in the selector docstring to know which properties are supported. 

2108 """ 

2109 artist = self._selection_artist 

2110 props = cbook.normalize_kwargs(props, artist) 

2111 artist.set(**props) 

2112 if self.useblit: 

2113 self.update() 

2114 self._props.update(props) 

2115 

2116 def set_handle_props(self, **handle_props): 

2117 """ 

2118 Set the properties of the handles selector artist. See the 

2119 `handle_props` argument in the selector docstring to know which 

2120 properties are supported. 

2121 """ 

2122 if not hasattr(self, '_handles_artists'): 

2123 raise NotImplementedError("This selector doesn't have handles.") 

2124 

2125 artist = self._handles_artists[0] 

2126 handle_props = cbook.normalize_kwargs(handle_props, artist) 

2127 for handle in self._handles_artists: 

2128 handle.set(**handle_props) 

2129 if self.useblit: 

2130 self.update() 

2131 self._handle_props.update(handle_props) 

2132 

2133 def _validate_state(self, state): 

2134 supported_state = [ 

2135 key for key, value in self._state_modifier_keys.items() 

2136 if key != 'clear' and value != 'not-applicable' 

2137 ] 

2138 _api.check_in_list(supported_state, state=state) 

2139 

2140 def add_state(self, state): 

2141 """ 

2142 Add a state to define the widget's behavior. See the 

2143 `state_modifier_keys` parameters for details. 

2144 

2145 Parameters 

2146 ---------- 

2147 state : str 

2148 Must be a supported state of the selector. See the 

2149 `state_modifier_keys` parameters for details. 

2150 

2151 Raises 

2152 ------ 

2153 ValueError 

2154 When the state is not supported by the selector. 

2155 

2156 """ 

2157 self._validate_state(state) 

2158 self._state.add(state) 

2159 

2160 def remove_state(self, state): 

2161 """ 

2162 Remove a state to define the widget's behavior. See the 

2163 `state_modifier_keys` parameters for details. 

2164 

2165 Parameters 

2166 ---------- 

2167 value : str 

2168 Must be a supported state of the selector. See the 

2169 `state_modifier_keys` parameters for details. 

2170 

2171 Raises 

2172 ------ 

2173 ValueError 

2174 When the state is not supported by the selector. 

2175 

2176 """ 

2177 self._validate_state(state) 

2178 self._state.remove(state) 

2179 

2180 

2181class SpanSelector(_SelectorWidget): 

2182 """ 

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

2184 those values. 

2185 

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

2187 

2188 In order to turn off the SpanSelector, set ``span_selector.active`` to 

2189 False. To turn it back on, set it to True. 

2190 

2191 Press and release events triggered at the same coordinates outside the 

2192 selection will clear the selector, except when 

2193 ``ignore_event_outside=True``. 

2194 

2195 Parameters 

2196 ---------- 

2197 ax : `matplotlib.axes.Axes` 

2198 

2199 onselect : callable 

2200 A callback function that is called after a release event and the 

2201 selection is created, changed or removed. 

2202 It must have the signature:: 

2203 

2204 def on_select(min: float, max: float) -> Any 

2205 

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

2207 The direction along which to draw the span selector. 

2208 

2209 minspan : float, default: 0 

2210 If selection is less than or equal to *minspan*, the selection is 

2211 removed (when already existing) or cancelled. 

2212 

2213 useblit : bool, default: False 

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

2215 canvas updates. See the tutorial :doc:`/tutorials/advanced/blitting` 

2216 for details. 

2217 

2218 props : dict, optional 

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

2220 Default: 

2221 

2222 ``dict(facecolor='red', alpha=0.5)`` 

2223 

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

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

2226 

2227 span_stays : bool, default: False 

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

2229 Deprecated, use *interactive* instead. 

2230 

2231 interactive : bool, default: False 

2232 Whether to draw a set of handles that allow interaction with the 

2233 widget after it is drawn. 

2234 

2235 button : `.MouseButton` or list of `.MouseButton`, default: all buttons 

2236 The mouse buttons which activate the span selector. 

2237 

2238 handle_props : dict, default: None 

2239 Properties of the handle lines at the edges of the span. Only used 

2240 when *interactive* is True. See `matplotlib.lines.Line2D` for valid 

2241 properties. 

2242 

2243 grab_range : float, default: 10 

2244 Distance in pixels within which the interactive tool handles can be 

2245 activated. 

2246 

2247 state_modifier_keys : dict, optional 

2248 Keyboard modifiers which affect the widget's behavior. Values 

2249 amend the defaults, which are: 

2250 

2251 - "clear": Clear the current shape, default: "escape". 

2252 

2253 drag_from_anywhere : bool, default: False 

2254 If `True`, the widget can be moved by clicking anywhere within 

2255 its bounds. 

2256 

2257 ignore_event_outside : bool, default: False 

2258 If `True`, the event triggered outside the span selector will be 

2259 ignored. 

2260 

2261 snap_values : 1D array-like, optional 

2262 Snap the selector edges to the given values. 

2263 

2264 Examples 

2265 -------- 

2266 >>> import matplotlib.pyplot as plt 

2267 >>> import matplotlib.widgets as mwidgets 

2268 >>> fig, ax = plt.subplots() 

2269 >>> ax.plot([1, 2, 3], [10, 50, 100]) 

2270 >>> def onselect(vmin, vmax): 

2271 ... print(vmin, vmax) 

2272 >>> span = mwidgets.SpanSelector(ax, onselect, 'horizontal', 

2273 ... props=dict(facecolor='blue', alpha=0.5)) 

2274 >>> fig.show() 

2275 

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

2277 """ 

2278 

2279 @_api.rename_parameter("3.5", "rectprops", "props") 

2280 @_api.rename_parameter("3.5", "span_stays", "interactive") 

2281 def __init__(self, ax, onselect, direction, minspan=0, useblit=False, 

2282 props=None, onmove_callback=None, interactive=False, 

2283 button=None, handle_props=None, grab_range=10, 

2284 state_modifier_keys=None, drag_from_anywhere=False, 

2285 ignore_event_outside=False, snap_values=None): 

2286 

2287 if state_modifier_keys is None: 

2288 state_modifier_keys = dict(clear='escape', 

2289 square='not-applicable', 

2290 center='not-applicable', 

2291 rotate='not-applicable') 

2292 super().__init__(ax, onselect, useblit=useblit, button=button, 

2293 state_modifier_keys=state_modifier_keys) 

2294 

2295 if props is None: 

2296 props = dict(facecolor='red', alpha=0.5) 

2297 

2298 props['animated'] = self.useblit 

2299 

2300 self.direction = direction 

2301 self._extents_on_press = None 

2302 self.snap_values = snap_values 

2303 

2304 # self._pressv is deprecated and we don't use it internally anymore 

2305 # but we maintain it until it is removed 

2306 self._pressv = None 

2307 

2308 self._props = props 

2309 self.onmove_callback = onmove_callback 

2310 self.minspan = minspan 

2311 

2312 self.grab_range = grab_range 

2313 self._interactive = interactive 

2314 self._edge_handles = None 

2315 self.drag_from_anywhere = drag_from_anywhere 

2316 self.ignore_event_outside = ignore_event_outside 

2317 

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

2319 self.canvas = None 

2320 self.new_axes(ax) 

2321 

2322 # Setup handles 

2323 self._handle_props = { 

2324 'color': props.get('facecolor', 'r'), 

2325 **cbook.normalize_kwargs(handle_props, Line2D)} 

2326 

2327 if self._interactive: 

2328 self._edge_order = ['min', 'max'] 

2329 self._setup_edge_handles(self._handle_props) 

2330 

2331 self._active_handle = None 

2332 

2333 # prev attribute is deprecated but we still need to maintain it 

2334 self._prev = (0, 0) 

2335 

2336 rect = _api.deprecated("3.5")( 

2337 property(lambda self: self._selection_artist) 

2338 ) 

2339 

2340 rectprops = _api.deprecated("3.5")( 

2341 property(lambda self: self._props) 

2342 ) 

2343 

2344 active_handle = _api.deprecate_privatize_attribute("3.5") 

2345 

2346 pressv = _api.deprecate_privatize_attribute("3.5") 

2347 

2348 span_stays = _api.deprecated("3.5")( 

2349 property(lambda self: self._interactive) 

2350 ) 

2351 

2352 prev = _api.deprecate_privatize_attribute("3.5") 

2353 

2354 def new_axes(self, ax): 

2355 """Set SpanSelector to operate on a new Axes.""" 

2356 self.ax = ax 

2357 if self.canvas is not ax.figure.canvas: 

2358 if self.canvas is not None: 

2359 self.disconnect_events() 

2360 

2361 self.canvas = ax.figure.canvas 

2362 self.connect_default_events() 

2363 

2364 # Reset 

2365 self._selection_completed = False 

2366 

2367 if self.direction == 'horizontal': 

2368 trans = ax.get_xaxis_transform() 

2369 w, h = 0, 1 

2370 else: 

2371 trans = ax.get_yaxis_transform() 

2372 w, h = 1, 0 

2373 rect_artist = Rectangle((0, 0), w, h, 

2374 transform=trans, 

2375 visible=False, 

2376 **self._props) 

2377 

2378 self.ax.add_patch(rect_artist) 

2379 self._selection_artist = rect_artist 

2380 

2381 def _setup_edge_handles(self, props): 

2382 # Define initial position using the axis bounds to keep the same bounds 

2383 if self.direction == 'horizontal': 

2384 positions = self.ax.get_xbound() 

2385 else: 

2386 positions = self.ax.get_ybound() 

2387 self._edge_handles = ToolLineHandles(self.ax, positions, 

2388 direction=self.direction, 

2389 line_props=props, 

2390 useblit=self.useblit) 

2391 

2392 @property 

2393 def _handles_artists(self): 

2394 if self._edge_handles is not None: 

2395 return self._edge_handles.artists 

2396 else: 

2397 return () 

2398 

2399 def _set_cursor(self, enabled): 

2400 """Update the canvas cursor based on direction of the selector.""" 

2401 if enabled: 

2402 cursor = (backend_tools.Cursors.RESIZE_HORIZONTAL 

2403 if self.direction == 'horizontal' else 

2404 backend_tools.Cursors.RESIZE_VERTICAL) 

2405 else: 

2406 cursor = backend_tools.Cursors.POINTER 

2407 

2408 self.ax.figure.canvas.set_cursor(cursor) 

2409 

2410 def connect_default_events(self): 

2411 # docstring inherited 

2412 super().connect_default_events() 

2413 if getattr(self, '_interactive', False): 

2414 self.connect_event('motion_notify_event', self._hover) 

2415 

2416 def _press(self, event): 

2417 """Button press event handler.""" 

2418 self._set_cursor(True) 

2419 if self._interactive and self._selection_artist.get_visible(): 

2420 self._set_active_handle(event) 

2421 else: 

2422 self._active_handle = None 

2423 

2424 if self._active_handle is None or not self._interactive: 

2425 # Clear previous rectangle before drawing new rectangle. 

2426 self.update() 

2427 

2428 v = event.xdata if self.direction == 'horizontal' else event.ydata 

2429 # self._pressv and self._prev are deprecated but we still need to 

2430 # maintain them 

2431 self._pressv = v 

2432 self._prev = self._get_data(event) 

2433 

2434 if self._active_handle is None and not self.ignore_event_outside: 

2435 # when the press event outside the span, we initially set the 

2436 # visibility to False and extents to (v, v) 

2437 # update will be called when setting the extents 

2438 self._visible = False 

2439 self.extents = v, v 

2440 # We need to set the visibility back, so the span selector will be 

2441 # drawn when necessary (span width > 0) 

2442 self._visible = True 

2443 else: 

2444 self.set_visible(True) 

2445 

2446 return False 

2447 

2448 @property 

2449 def direction(self): 

2450 """Direction of the span selector: 'vertical' or 'horizontal'.""" 

2451 return self._direction 

2452 

2453 @direction.setter 

2454 def direction(self, direction): 

2455 """Set the direction of the span selector.""" 

2456 _api.check_in_list(['horizontal', 'vertical'], direction=direction) 

2457 if hasattr(self, '_direction') and direction != self._direction: 

2458 # remove previous artists 

2459 self._selection_artist.remove() 

2460 if self._interactive: 

2461 self._edge_handles.remove() 

2462 self._direction = direction 

2463 self.new_axes(self.ax) 

2464 if self._interactive: 

2465 self._setup_edge_handles(self._handle_props) 

2466 else: 

2467 self._direction = direction 

2468 

2469 def _release(self, event): 

2470 """Button release event handler.""" 

2471 self._set_cursor(False) 

2472 # self._pressv is deprecated but we still need to maintain it 

2473 self._pressv = None 

2474 

2475 if not self._interactive: 

2476 self._selection_artist.set_visible(False) 

2477 

2478 if (self._active_handle is None and self._selection_completed and 

2479 self.ignore_event_outside): 

2480 return 

2481 

2482 vmin, vmax = self.extents 

2483 span = vmax - vmin 

2484 

2485 if span <= self.minspan: 

2486 # Remove span and set self._selection_completed = False 

2487 self.set_visible(False) 

2488 if self._selection_completed: 

2489 # Call onselect, only when the span is already existing 

2490 self.onselect(vmin, vmax) 

2491 self._selection_completed = False 

2492 else: 

2493 self.onselect(vmin, vmax) 

2494 self._selection_completed = True 

2495 

2496 self.update() 

2497 

2498 self._active_handle = None 

2499 

2500 return False 

2501 

2502 def _hover(self, event): 

2503 """Update the canvas cursor if it's over a handle.""" 

2504 if self.ignore(event): 

2505 return 

2506 

2507 if self._active_handle is not None or not self._selection_completed: 

2508 # Do nothing if button is pressed and a handle is active, which may 

2509 # occur with drag_from_anywhere=True. 

2510 # Do nothing if selection is not completed, which occurs when 

2511 # a selector has been cleared 

2512 return 

2513 

2514 _, e_dist = self._edge_handles.closest(event.x, event.y) 

2515 self._set_cursor(e_dist <= self.grab_range) 

2516 

2517 def _onmove(self, event): 

2518 """Motion notify event handler.""" 

2519 

2520 # self._prev are deprecated but we still need to maintain it 

2521 self._prev = self._get_data(event) 

2522 

2523 v = event.xdata if self.direction == 'horizontal' else event.ydata 

2524 if self.direction == 'horizontal': 

2525 vpress = self._eventpress.xdata 

2526 else: 

2527 vpress = self._eventpress.ydata 

2528 

2529 # move existing span 

2530 # When "dragging from anywhere", `self._active_handle` is set to 'C' 

2531 # (match notation used in the RectangleSelector) 

2532 if self._active_handle == 'C' and self._extents_on_press is not None: 

2533 vmin, vmax = self._extents_on_press 

2534 dv = v - vpress 

2535 vmin += dv 

2536 vmax += dv 

2537 

2538 # resize an existing shape 

2539 elif self._active_handle and self._active_handle != 'C': 

2540 vmin, vmax = self._extents_on_press 

2541 if self._active_handle == 'min': 

2542 vmin = v 

2543 else: 

2544 vmax = v 

2545 # new shape 

2546 else: 

2547 # Don't create a new span if there is already one when 

2548 # ignore_event_outside=True 

2549 if self.ignore_event_outside and self._selection_completed: 

2550 return 

2551 vmin, vmax = vpress, v 

2552 if vmin > vmax: 

2553 vmin, vmax = vmax, vmin 

2554 

2555 self.extents = vmin, vmax 

2556 

2557 if self.onmove_callback is not None: 

2558 self.onmove_callback(vmin, vmax) 

2559 

2560 return False 

2561 

2562 def _draw_shape(self, vmin, vmax): 

2563 if vmin > vmax: 

2564 vmin, vmax = vmax, vmin 

2565 if self.direction == 'horizontal': 

2566 self._selection_artist.set_x(vmin) 

2567 self._selection_artist.set_width(vmax - vmin) 

2568 else: 

2569 self._selection_artist.set_y(vmin) 

2570 self._selection_artist.set_height(vmax - vmin) 

2571 

2572 def _set_active_handle(self, event): 

2573 """Set active handle based on the location of the mouse event.""" 

2574 # Note: event.xdata/ydata in data coordinates, event.x/y in pixels 

2575 e_idx, e_dist = self._edge_handles.closest(event.x, event.y) 

2576 

2577 # Prioritise center handle over other handles 

2578 # Use 'C' to match the notation used in the RectangleSelector 

2579 if 'move' in self._state: 

2580 self._active_handle = 'C' 

2581 elif e_dist > self.grab_range: 

2582 # Not close to any handles 

2583 self._active_handle = None 

2584 if self.drag_from_anywhere and self._contains(event): 

2585 # Check if we've clicked inside the region 

2586 self._active_handle = 'C' 

2587 self._extents_on_press = self.extents 

2588 else: 

2589 self._active_handle = None 

2590 return 

2591 else: 

2592 # Closest to an edge handle 

2593 self._active_handle = self._edge_order[e_idx] 

2594 

2595 # Save coordinates of rectangle at the start of handle movement. 

2596 self._extents_on_press = self.extents 

2597 

2598 def _contains(self, event): 

2599 """Return True if event is within the patch.""" 

2600 return self._selection_artist.contains(event, radius=0)[0] 

2601 

2602 @staticmethod 

2603 def _snap(values, snap_values): 

2604 """Snap values to a given array values (snap_values).""" 

2605 # take into account machine precision 

2606 eps = np.min(np.abs(np.diff(snap_values))) * 1e-12 

2607 return tuple( 

2608 snap_values[np.abs(snap_values - v + np.sign(v) * eps).argmin()] 

2609 for v in values) 

2610 

2611 @property 

2612 def extents(self): 

2613 """Return extents of the span selector.""" 

2614 if self.direction == 'horizontal': 

2615 vmin = self._selection_artist.get_x() 

2616 vmax = vmin + self._selection_artist.get_width() 

2617 else: 

2618 vmin = self._selection_artist.get_y() 

2619 vmax = vmin + self._selection_artist.get_height() 

2620 return vmin, vmax 

2621 

2622 @extents.setter 

2623 def extents(self, extents): 

2624 # Update displayed shape 

2625 if self.snap_values is not None: 

2626 extents = tuple(self._snap(extents, self.snap_values)) 

2627 self._draw_shape(*extents) 

2628 if self._interactive: 

2629 # Update displayed handles 

2630 self._edge_handles.set_data(self.extents) 

2631 self.set_visible(self._visible) 

2632 self.update() 

2633 

2634 

2635class ToolLineHandles: 

2636 """ 

2637 Control handles for canvas tools. 

2638 

2639 Parameters 

2640 ---------- 

2641 ax : `matplotlib.axes.Axes` 

2642 Matplotlib Axes where tool handles are displayed. 

2643 positions : 1D array 

2644 Positions of handles in data coordinates. 

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

2646 Direction of handles, either 'vertical' or 'horizontal' 

2647 line_props : dict, optional 

2648 Additional line properties. See `matplotlib.lines.Line2D`. 

2649 useblit : bool, default: True 

2650 Whether to use blitting for faster drawing (if supported by the 

2651 backend). See the tutorial :doc:`/tutorials/advanced/blitting` 

2652 for details. 

2653 """ 

2654 

2655 def __init__(self, ax, positions, direction, line_props=None, 

2656 useblit=True): 

2657 self.ax = ax 

2658 

2659 _api.check_in_list(['horizontal', 'vertical'], direction=direction) 

2660 self._direction = direction 

2661 

2662 if line_props is None: 

2663 line_props = {} 

2664 line_props.update({'visible': False, 'animated': useblit}) 

2665 

2666 line_fun = ax.axvline if self.direction == 'horizontal' else ax.axhline 

2667 

2668 self._artists = [line_fun(p, **line_props) for p in positions] 

2669 

2670 @property 

2671 def artists(self): 

2672 return tuple(self._artists) 

2673 

2674 @property 

2675 def positions(self): 

2676 """Positions of the handle in data coordinates.""" 

2677 method = 'get_xdata' if self.direction == 'horizontal' else 'get_ydata' 

2678 return [getattr(line, method)()[0] for line in self.artists] 

2679 

2680 @property 

2681 def direction(self): 

2682 """Direction of the handle: 'vertical' or 'horizontal'.""" 

2683 return self._direction 

2684 

2685 def set_data(self, positions): 

2686 """ 

2687 Set x- or y-positions of handles, depending on if the lines are 

2688 vertical or horizontal. 

2689 

2690 Parameters 

2691 ---------- 

2692 positions : tuple of length 2 

2693 Set the positions of the handle in data coordinates 

2694 """ 

2695 method = 'set_xdata' if self.direction == 'horizontal' else 'set_ydata' 

2696 for line, p in zip(self.artists, positions): 

2697 getattr(line, method)([p, p]) 

2698 

2699 def set_visible(self, value): 

2700 """Set the visibility state of the handles artist.""" 

2701 for artist in self.artists: 

2702 artist.set_visible(value) 

2703 

2704 def set_animated(self, value): 

2705 """Set the animated state of the handles artist.""" 

2706 for artist in self.artists: 

2707 artist.set_animated(value) 

2708 

2709 def remove(self): 

2710 """Remove the handles artist from the figure.""" 

2711 for artist in self._artists: 

2712 artist.remove() 

2713 

2714 def closest(self, x, y): 

2715 """ 

2716 Return index and pixel distance to closest handle. 

2717 

2718 Parameters 

2719 ---------- 

2720 x, y : float 

2721 x, y position from which the distance will be calculated to 

2722 determinate the closest handle 

2723 

2724 Returns 

2725 ------- 

2726 index, distance : index of the handle and its distance from 

2727 position x, y 

2728 """ 

2729 if self.direction == 'horizontal': 

2730 p_pts = np.array([ 

2731 self.ax.transData.transform((p, 0))[0] for p in self.positions 

2732 ]) 

2733 dist = abs(p_pts - x) 

2734 else: 

2735 p_pts = np.array([ 

2736 self.ax.transData.transform((0, p))[1] for p in self.positions 

2737 ]) 

2738 dist = abs(p_pts - y) 

2739 index = np.argmin(dist) 

2740 return index, dist[index] 

2741 

2742 

2743class ToolHandles: 

2744 """ 

2745 Control handles for canvas tools. 

2746 

2747 Parameters 

2748 ---------- 

2749 ax : `matplotlib.axes.Axes` 

2750 Matplotlib Axes where tool handles are displayed. 

2751 x, y : 1D arrays 

2752 Coordinates of control handles. 

2753 marker : str, default: 'o' 

2754 Shape of marker used to display handle. See `matplotlib.pyplot.plot`. 

2755 marker_props : dict, optional 

2756 Additional marker properties. See `matplotlib.lines.Line2D`. 

2757 useblit : bool, default: True 

2758 Whether to use blitting for faster drawing (if supported by the 

2759 backend). See the tutorial :doc:`/tutorials/advanced/blitting` 

2760 for details. 

2761 """ 

2762 

2763 def __init__(self, ax, x, y, marker='o', marker_props=None, useblit=True): 

2764 self.ax = ax 

2765 props = {'marker': marker, 'markersize': 7, 'markerfacecolor': 'w', 

2766 'linestyle': 'none', 'alpha': 0.5, 'visible': False, 

2767 'label': '_nolegend_', 

2768 **cbook.normalize_kwargs(marker_props, Line2D._alias_map)} 

2769 self._markers = Line2D(x, y, animated=useblit, **props) 

2770 self.ax.add_line(self._markers) 

2771 

2772 @property 

2773 def x(self): 

2774 return self._markers.get_xdata() 

2775 

2776 @property 

2777 def y(self): 

2778 return self._markers.get_ydata() 

2779 

2780 @property 

2781 def artists(self): 

2782 return (self._markers, ) 

2783 

2784 def set_data(self, pts, y=None): 

2785 """Set x and y positions of handles.""" 

2786 if y is not None: 

2787 x = pts 

2788 pts = np.array([x, y]) 

2789 self._markers.set_data(pts) 

2790 

2791 def set_visible(self, val): 

2792 self._markers.set_visible(val) 

2793 

2794 def set_animated(self, val): 

2795 self._markers.set_animated(val) 

2796 

2797 def closest(self, x, y): 

2798 """Return index and pixel distance to closest index.""" 

2799 pts = np.column_stack([self.x, self.y]) 

2800 # Transform data coordinates to pixel coordinates. 

2801 pts = self.ax.transData.transform(pts) 

2802 diff = pts - [x, y] 

2803 dist = np.hypot(*diff.T) 

2804 min_index = np.argmin(dist) 

2805 return min_index, dist[min_index] 

2806 

2807 

2808_RECTANGLESELECTOR_PARAMETERS_DOCSTRING = \ 

2809 r""" 

2810 Parameters 

2811 ---------- 

2812 ax : `~matplotlib.axes.Axes` 

2813 The parent axes for the widget. 

2814 

2815 onselect : function 

2816 A callback function that is called after a release event and the 

2817 selection is created, changed or removed. 

2818 It must have the signature:: 

2819 

2820 def onselect(eclick: MouseEvent, erelease: MouseEvent) 

2821 

2822 where *eclick* and *erelease* are the mouse click and release 

2823 `.MouseEvent`\s that start and complete the selection. 

2824 

2825 minspanx : float, default: 0 

2826 Selections with an x-span less than or equal to *minspanx* are removed 

2827 (when already existing) or cancelled. 

2828 

2829 minspany : float, default: 0 

2830 Selections with an y-span less than or equal to *minspanx* are removed 

2831 (when already existing) or cancelled. 

2832 

2833 useblit : bool, default: False 

2834 Whether to use blitting for faster drawing (if supported by the 

2835 backend). See the tutorial :doc:`/tutorials/advanced/blitting` 

2836 for details. 

2837 

2838 props : dict, optional 

2839 Properties with which the __ARTIST_NAME__ is drawn. See 

2840 `matplotlib.patches.Patch` for valid properties. 

2841 Default: 

2842 

2843 ``dict(facecolor='red', edgecolor='black', alpha=0.2, fill=True)`` 

2844 

2845 spancoords : {"data", "pixels"}, default: "data" 

2846 Whether to interpret *minspanx* and *minspany* in data or in pixel 

2847 coordinates. 

2848 

2849 button : `.MouseButton`, list of `.MouseButton`, default: all buttons 

2850 Button(s) that trigger rectangle selection. 

2851 

2852 grab_range : float, default: 10 

2853 Distance in pixels within which the interactive tool handles can be 

2854 activated. 

2855 

2856 handle_props : dict, optional 

2857 Properties with which the interactive handles (marker artists) are 

2858 drawn. See the marker arguments in `matplotlib.lines.Line2D` for valid 

2859 properties. Default values are defined in ``mpl.rcParams`` except for 

2860 the default value of ``markeredgecolor`` which will be the same as the 

2861 ``edgecolor`` property in *props*. 

2862 

2863 interactive : bool, default: False 

2864 Whether to draw a set of handles that allow interaction with the 

2865 widget after it is drawn. 

2866 

2867 state_modifier_keys : dict, optional 

2868 Keyboard modifiers which affect the widget's behavior. Values 

2869 amend the defaults, which are: 

2870 

2871 - "move": Move the existing shape, default: no modifier. 

2872 - "clear": Clear the current shape, default: "escape". 

2873 - "square": Make the shape square, default: "shift". 

2874 - "center": change the shape around its center, default: "ctrl". 

2875 - "rotate": Rotate the shape around its center between -45° and 45°, 

2876 default: "r". 

2877 

2878 "square" and "center" can be combined. The square shape can be defined 

2879 in data or display coordinates as determined by the 

2880 ``use_data_coordinates`` argument specified when creating the selector. 

2881 

2882 drag_from_anywhere : bool, default: False 

2883 If `True`, the widget can be moved by clicking anywhere within 

2884 its bounds. 

2885 

2886 ignore_event_outside : bool, default: False 

2887 If `True`, the event triggered outside the span selector will be 

2888 ignored. 

2889 

2890 use_data_coordinates : bool, default: False 

2891 If `True`, the "square" shape of the selector is defined in 

2892 data coordinates instead of display coordinates. 

2893 """ 

2894 

2895 

2896@_docstring.Substitution(_RECTANGLESELECTOR_PARAMETERS_DOCSTRING.replace( 

2897 '__ARTIST_NAME__', 'rectangle')) 

2898class RectangleSelector(_SelectorWidget): 

2899 """ 

2900 Select a rectangular region of an Axes. 

2901 

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

2903 

2904 Press and release events triggered at the same coordinates outside the 

2905 selection will clear the selector, except when 

2906 ``ignore_event_outside=True``. 

2907 

2908 %s 

2909 

2910 Examples 

2911 -------- 

2912 >>> import matplotlib.pyplot as plt 

2913 >>> import matplotlib.widgets as mwidgets 

2914 >>> fig, ax = plt.subplots() 

2915 >>> ax.plot([1, 2, 3], [10, 50, 100]) 

2916 >>> def onselect(eclick, erelease): 

2917 ... print(eclick.xdata, eclick.ydata) 

2918 ... print(erelease.xdata, erelease.ydata) 

2919 >>> props = dict(facecolor='blue', alpha=0.5) 

2920 >>> rect = mwidgets.RectangleSelector(ax, onselect, interactive=True, 

2921 ... props=props) 

2922 >>> fig.show() 

2923 >>> rect.add_state('square') 

2924 

2925 See also: :doc:`/gallery/widgets/rectangle_selector` 

2926 """ 

2927 

2928 @_api.rename_parameter("3.5", "maxdist", "grab_range") 

2929 @_api.rename_parameter("3.5", "marker_props", "handle_props") 

2930 @_api.rename_parameter("3.5", "rectprops", "props") 

2931 @_api.delete_parameter("3.5", "drawtype") 

2932 @_api.delete_parameter("3.5", "lineprops") 

2933 def __init__(self, ax, onselect, drawtype='box', 

2934 minspanx=0, minspany=0, useblit=False, 

2935 lineprops=None, props=None, spancoords='data', 

2936 button=None, grab_range=10, handle_props=None, 

2937 interactive=False, state_modifier_keys=None, 

2938 drag_from_anywhere=False, ignore_event_outside=False, 

2939 use_data_coordinates=False): 

2940 super().__init__(ax, onselect, useblit=useblit, button=button, 

2941 state_modifier_keys=state_modifier_keys, 

2942 use_data_coordinates=use_data_coordinates) 

2943 

2944 self._interactive = interactive 

2945 self.drag_from_anywhere = drag_from_anywhere 

2946 self.ignore_event_outside = ignore_event_outside 

2947 self._rotation = 0.0 

2948 self._aspect_ratio_correction = 1.0 

2949 

2950 # State to allow the option of an interactive selector that can't be 

2951 # interactively drawn. This is used in PolygonSelector as an 

2952 # interactive bounding box to allow the polygon to be easily resized 

2953 self._allow_creation = True 

2954 

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

2956 _api.warn_deprecated( 

2957 "3.5", message="Support for drawtype='none' is deprecated " 

2958 "since %(since)s and will be removed " 

2959 "%(removal)s." 

2960 "Use props=dict(visible=False) instead.") 

2961 drawtype = 'line' 

2962 self._visible = False 

2963 

2964 if drawtype == 'box': 

2965 if props is None: 

2966 props = dict(facecolor='red', edgecolor='black', 

2967 alpha=0.2, fill=True) 

2968 props['animated'] = self.useblit 

2969 self._visible = props.pop('visible', self._visible) 

2970 self._props = props 

2971 to_draw = self._init_shape(**self._props) 

2972 self.ax.add_patch(to_draw) 

2973 if drawtype == 'line': 

2974 _api.warn_deprecated( 

2975 "3.5", message="Support for drawtype='line' is deprecated " 

2976 "since %(since)s and will be removed " 

2977 "%(removal)s.") 

2978 if lineprops is None: 

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

2980 linewidth=2, alpha=0.5) 

2981 lineprops['animated'] = self.useblit 

2982 self._props = lineprops 

2983 to_draw = Line2D([0, 0], [0, 0], visible=False, **self._props) 

2984 self.ax.add_line(to_draw) 

2985 

2986 self._selection_artist = to_draw 

2987 self._set_aspect_ratio_correction() 

2988 

2989 self.minspanx = minspanx 

2990 self.minspany = minspany 

2991 

2992 _api.check_in_list(['data', 'pixels'], spancoords=spancoords) 

2993 self.spancoords = spancoords 

2994 self._drawtype = drawtype 

2995 

2996 self.grab_range = grab_range 

2997 

2998 if self._interactive: 

2999 self._handle_props = { 

3000 'markeredgecolor': (self._props or {}).get( 

3001 'edgecolor', 'black'), 

3002 **cbook.normalize_kwargs(handle_props, Line2D)} 

3003 

3004 self._corner_order = ['SW', 'SE', 'NE', 'NW'] 

3005 xc, yc = self.corners 

3006 self._corner_handles = ToolHandles(self.ax, xc, yc, 

3007 marker_props=self._handle_props, 

3008 useblit=self.useblit) 

3009 

3010 self._edge_order = ['W', 'S', 'E', 'N'] 

3011 xe, ye = self.edge_centers 

3012 self._edge_handles = ToolHandles(self.ax, xe, ye, marker='s', 

3013 marker_props=self._handle_props, 

3014 useblit=self.useblit) 

3015 

3016 xc, yc = self.center 

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

3018 marker_props=self._handle_props, 

3019 useblit=self.useblit) 

3020 

3021 self._active_handle = None 

3022 

3023 self._extents_on_press = None 

3024 

3025 to_draw = _api.deprecated("3.5")( 

3026 property(lambda self: self._selection_artist) 

3027 ) 

3028 

3029 drawtype = _api.deprecate_privatize_attribute("3.5") 

3030 

3031 active_handle = _api.deprecate_privatize_attribute("3.5") 

3032 

3033 interactive = _api.deprecate_privatize_attribute("3.5") 

3034 

3035 maxdist = _api.deprecated("3.5", name="maxdist", alternative="grab_range")( 

3036 property(lambda self: self.grab_range, 

3037 lambda self, value: setattr(self, "grab_range", value))) 

3038 

3039 @property 

3040 def _handles_artists(self): 

3041 return (*self._center_handle.artists, *self._corner_handles.artists, 

3042 *self._edge_handles.artists) 

3043 

3044 def _init_shape(self, **props): 

3045 return Rectangle((0, 0), 0, 1, visible=False, 

3046 rotation_point='center', **props) 

3047 

3048 def _press(self, event): 

3049 """Button press event handler.""" 

3050 # make the drawn box/line visible get the click-coordinates, 

3051 # button, ... 

3052 if self._interactive and self._selection_artist.get_visible(): 

3053 self._set_active_handle(event) 

3054 else: 

3055 self._active_handle = None 

3056 

3057 if ((self._active_handle is None or not self._interactive) and 

3058 self._allow_creation): 

3059 # Clear previous rectangle before drawing new rectangle. 

3060 self.update() 

3061 

3062 if (self._active_handle is None and not self.ignore_event_outside and 

3063 self._allow_creation): 

3064 x = event.xdata 

3065 y = event.ydata 

3066 self._visible = False 

3067 self.extents = x, x, y, y 

3068 self._visible = True 

3069 else: 

3070 self.set_visible(True) 

3071 

3072 self._extents_on_press = self.extents 

3073 self._rotation_on_press = self._rotation 

3074 self._set_aspect_ratio_correction() 

3075 

3076 return False 

3077 

3078 def _release(self, event): 

3079 """Button release event handler.""" 

3080 if not self._interactive: 

3081 self._selection_artist.set_visible(False) 

3082 

3083 if (self._active_handle is None and self._selection_completed and 

3084 self.ignore_event_outside): 

3085 return 

3086 

3087 # update the eventpress and eventrelease with the resulting extents 

3088 x0, x1, y0, y1 = self.extents 

3089 self._eventpress.xdata = x0 

3090 self._eventpress.ydata = y0 

3091 xy0 = self.ax.transData.transform([x0, y0]) 

3092 self._eventpress.x, self._eventpress.y = xy0 

3093 

3094 self._eventrelease.xdata = x1 

3095 self._eventrelease.ydata = y1 

3096 xy1 = self.ax.transData.transform([x1, y1]) 

3097 self._eventrelease.x, self._eventrelease.y = xy1 

3098 

3099 # calculate dimensions of box or line 

3100 if self.spancoords == 'data': 

3101 spanx = abs(self._eventpress.xdata - self._eventrelease.xdata) 

3102 spany = abs(self._eventpress.ydata - self._eventrelease.ydata) 

3103 elif self.spancoords == 'pixels': 

3104 spanx = abs(self._eventpress.x - self._eventrelease.x) 

3105 spany = abs(self._eventpress.y - self._eventrelease.y) 

3106 else: 

3107 _api.check_in_list(['data', 'pixels'], 

3108 spancoords=self.spancoords) 

3109 # check if drawn distance (if it exists) is not too small in 

3110 # either x or y-direction 

3111 minspanxy = (spanx <= self.minspanx or spany <= self.minspany) 

3112 if (self._drawtype != 'none' and minspanxy): 

3113 if self._selection_completed: 

3114 # Call onselect, only when the selection is already existing 

3115 self.onselect(self._eventpress, self._eventrelease) 

3116 self._clear_without_update() 

3117 else: 

3118 self.onselect(self._eventpress, self._eventrelease) 

3119 self._selection_completed = True 

3120 

3121 self.update() 

3122 self._active_handle = None 

3123 self._extents_on_press = None 

3124 

3125 return False 

3126 

3127 def _onmove(self, event): 

3128 """ 

3129 Motion notify event handler. 

3130 

3131 This can do one of four things: 

3132 - Translate 

3133 - Rotate 

3134 - Re-size 

3135 - Continue the creation of a new shape 

3136 """ 

3137 eventpress = self._eventpress 

3138 # The calculations are done for rotation at zero: we apply inverse 

3139 # transformation to events except when we rotate and move 

3140 state = self._state 

3141 rotate = ('rotate' in state and 

3142 self._active_handle in self._corner_order) 

3143 move = self._active_handle == 'C' 

3144 resize = self._active_handle and not move 

3145 

3146 if resize: 

3147 inv_tr = self._get_rotation_transform().inverted() 

3148 event.xdata, event.ydata = inv_tr.transform( 

3149 [event.xdata, event.ydata]) 

3150 eventpress.xdata, eventpress.ydata = inv_tr.transform( 

3151 [eventpress.xdata, eventpress.ydata] 

3152 ) 

3153 

3154 dx = event.xdata - eventpress.xdata 

3155 dy = event.ydata - eventpress.ydata 

3156 # refmax is used when moving the corner handle with the square state 

3157 # and is the maximum between refx and refy 

3158 refmax = None 

3159 if self._use_data_coordinates: 

3160 refx, refy = dx, dy 

3161 else: 

3162 # Get dx/dy in display coordinates 

3163 refx = event.x - eventpress.x 

3164 refy = event.y - eventpress.y 

3165 

3166 x0, x1, y0, y1 = self._extents_on_press 

3167 # rotate an existing shape 

3168 if rotate: 

3169 # calculate angle abc 

3170 a = np.array([eventpress.xdata, eventpress.ydata]) 

3171 b = np.array(self.center) 

3172 c = np.array([event.xdata, event.ydata]) 

3173 angle = (np.arctan2(c[1]-b[1], c[0]-b[0]) - 

3174 np.arctan2(a[1]-b[1], a[0]-b[0])) 

3175 self.rotation = np.rad2deg(self._rotation_on_press + angle) 

3176 

3177 elif resize: 

3178 size_on_press = [x1 - x0, y1 - y0] 

3179 center = [x0 + size_on_press[0] / 2, y0 + size_on_press[1] / 2] 

3180 

3181 # Keeping the center fixed 

3182 if 'center' in state: 

3183 # hh, hw are half-height and half-width 

3184 if 'square' in state: 

3185 # when using a corner, find which reference to use 

3186 if self._active_handle in self._corner_order: 

3187 refmax = max(refx, refy, key=abs) 

3188 if self._active_handle in ['E', 'W'] or refmax == refx: 

3189 hw = event.xdata - center[0] 

3190 hh = hw / self._aspect_ratio_correction 

3191 else: 

3192 hh = event.ydata - center[1] 

3193 hw = hh * self._aspect_ratio_correction 

3194 else: 

3195 hw = size_on_press[0] / 2 

3196 hh = size_on_press[1] / 2 

3197 # cancel changes in perpendicular direction 

3198 if self._active_handle in ['E', 'W'] + self._corner_order: 

3199 hw = abs(event.xdata - center[0]) 

3200 if self._active_handle in ['N', 'S'] + self._corner_order: 

3201 hh = abs(event.ydata - center[1]) 

3202 

3203 x0, x1, y0, y1 = (center[0] - hw, center[0] + hw, 

3204 center[1] - hh, center[1] + hh) 

3205 

3206 else: 

3207 # change sign of relative changes to simplify calculation 

3208 # Switch variables so that x1 and/or y1 are updated on move 

3209 if 'W' in self._active_handle: 

3210 x0 = x1 

3211 if 'S' in self._active_handle: 

3212 y0 = y1 

3213 if self._active_handle in ['E', 'W'] + self._corner_order: 

3214 x1 = event.xdata 

3215 if self._active_handle in ['N', 'S'] + self._corner_order: 

3216 y1 = event.ydata 

3217 if 'square' in state: 

3218 # when using a corner, find which reference to use 

3219 if self._active_handle in self._corner_order: 

3220 refmax = max(refx, refy, key=abs) 

3221 if self._active_handle in ['E', 'W'] or refmax == refx: 

3222 sign = np.sign(event.ydata - y0) 

3223 y1 = y0 + sign * abs(x1 - x0) / \ 

3224 self._aspect_ratio_correction 

3225 else: 

3226 sign = np.sign(event.xdata - x0) 

3227 x1 = x0 + sign * abs(y1 - y0) * \ 

3228 self._aspect_ratio_correction 

3229 

3230 elif move: 

3231 x0, x1, y0, y1 = self._extents_on_press 

3232 dx = event.xdata - eventpress.xdata 

3233 dy = event.ydata - eventpress.ydata 

3234 x0 += dx 

3235 x1 += dx 

3236 y0 += dy 

3237 y1 += dy 

3238 

3239 else: 

3240 # Create a new shape 

3241 self._rotation = 0 

3242 # Don't create a new rectangle if there is already one when 

3243 # ignore_event_outside=True 

3244 if ((self.ignore_event_outside and self._selection_completed) or 

3245 not self._allow_creation): 

3246 return 

3247 center = [eventpress.xdata, eventpress.ydata] 

3248 dx = (event.xdata - center[0]) / 2. 

3249 dy = (event.ydata - center[1]) / 2. 

3250 

3251 # square shape 

3252 if 'square' in state: 

3253 refmax = max(refx, refy, key=abs) 

3254 if refmax == refx: 

3255 dy = np.sign(dy) * abs(dx) / self._aspect_ratio_correction 

3256 else: 

3257 dx = np.sign(dx) * abs(dy) * self._aspect_ratio_correction 

3258 

3259 # from center 

3260 if 'center' in state: 

3261 dx *= 2 

3262 dy *= 2 

3263 

3264 # from corner 

3265 else: 

3266 center[0] += dx 

3267 center[1] += dy 

3268 

3269 x0, x1, y0, y1 = (center[0] - dx, center[0] + dx, 

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

3271 

3272 self.extents = x0, x1, y0, y1 

3273 

3274 @property 

3275 def _rect_bbox(self): 

3276 if self._drawtype == 'box': 

3277 return self._selection_artist.get_bbox().bounds 

3278 else: 

3279 x, y = self._selection_artist.get_data() 

3280 x0, x1 = min(x), max(x) 

3281 y0, y1 = min(y), max(y) 

3282 return x0, y0, x1 - x0, y1 - y0 

3283 

3284 def _set_aspect_ratio_correction(self): 

3285 aspect_ratio = self.ax._get_aspect_ratio() 

3286 if not hasattr(self._selection_artist, '_aspect_ratio_correction'): 

3287 # Aspect ratio correction is not supported with deprecated 

3288 # drawtype='line'. Remove this block in matplotlib 3.7 

3289 self._aspect_ratio_correction = 1 

3290 return 

3291 

3292 self._selection_artist._aspect_ratio_correction = aspect_ratio 

3293 if self._use_data_coordinates: 

3294 self._aspect_ratio_correction = 1 

3295 else: 

3296 self._aspect_ratio_correction = aspect_ratio 

3297 

3298 def _get_rotation_transform(self): 

3299 aspect_ratio = self.ax._get_aspect_ratio() 

3300 return Affine2D().translate(-self.center[0], -self.center[1]) \ 

3301 .scale(1, aspect_ratio) \ 

3302 .rotate(self._rotation) \ 

3303 .scale(1, 1 / aspect_ratio) \ 

3304 .translate(*self.center) 

3305 

3306 @property 

3307 def corners(self): 

3308 """ 

3309 Corners of rectangle in data coordinates from lower left, 

3310 moving clockwise. 

3311 """ 

3312 x0, y0, width, height = self._rect_bbox 

3313 xc = x0, x0 + width, x0 + width, x0 

3314 yc = y0, y0, y0 + height, y0 + height 

3315 transform = self._get_rotation_transform() 

3316 coords = transform.transform(np.array([xc, yc]).T).T 

3317 return coords[0], coords[1] 

3318 

3319 @property 

3320 def edge_centers(self): 

3321 """ 

3322 Midpoint of rectangle edges in data coordinates from left, 

3323 moving anti-clockwise. 

3324 """ 

3325 x0, y0, width, height = self._rect_bbox 

3326 w = width / 2. 

3327 h = height / 2. 

3328 xe = x0, x0 + w, x0 + width, x0 + w 

3329 ye = y0 + h, y0, y0 + h, y0 + height 

3330 transform = self._get_rotation_transform() 

3331 coords = transform.transform(np.array([xe, ye]).T).T 

3332 return coords[0], coords[1] 

3333 

3334 @property 

3335 def center(self): 

3336 """Center of rectangle in data coordinates.""" 

3337 x0, y0, width, height = self._rect_bbox 

3338 return x0 + width / 2., y0 + height / 2. 

3339 

3340 @property 

3341 def extents(self): 

3342 """ 

3343 Return (xmin, xmax, ymin, ymax) in data coordinates as defined by the 

3344 bounding box before rotation. 

3345 """ 

3346 x0, y0, width, height = self._rect_bbox 

3347 xmin, xmax = sorted([x0, x0 + width]) 

3348 ymin, ymax = sorted([y0, y0 + height]) 

3349 return xmin, xmax, ymin, ymax 

3350 

3351 @extents.setter 

3352 def extents(self, extents): 

3353 # Update displayed shape 

3354 self._draw_shape(extents) 

3355 if self._interactive: 

3356 # Update displayed handles 

3357 self._corner_handles.set_data(*self.corners) 

3358 self._edge_handles.set_data(*self.edge_centers) 

3359 self._center_handle.set_data(*self.center) 

3360 self.set_visible(self._visible) 

3361 self.update() 

3362 

3363 @property 

3364 def rotation(self): 

3365 """ 

3366 Rotation in degree in interval [-45°, 45°]. The rotation is limited in 

3367 range to keep the implementation simple. 

3368 """ 

3369 return np.rad2deg(self._rotation) 

3370 

3371 @rotation.setter 

3372 def rotation(self, value): 

3373 # Restrict to a limited range of rotation [-45°, 45°] to avoid changing 

3374 # order of handles 

3375 if -45 <= value and value <= 45: 

3376 self._rotation = np.deg2rad(value) 

3377 # call extents setter to draw shape and update handles positions 

3378 self.extents = self.extents 

3379 

3380 draw_shape = _api.deprecate_privatize_attribute('3.5') 

3381 

3382 def _draw_shape(self, extents): 

3383 x0, x1, y0, y1 = extents 

3384 xmin, xmax = sorted([x0, x1]) 

3385 ymin, ymax = sorted([y0, y1]) 

3386 xlim = sorted(self.ax.get_xlim()) 

3387 ylim = sorted(self.ax.get_ylim()) 

3388 

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

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

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

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

3393 

3394 if self._drawtype == 'box': 

3395 self._selection_artist.set_x(xmin) 

3396 self._selection_artist.set_y(ymin) 

3397 self._selection_artist.set_width(xmax - xmin) 

3398 self._selection_artist.set_height(ymax - ymin) 

3399 self._selection_artist.set_angle(self.rotation) 

3400 

3401 elif self._drawtype == 'line': 

3402 self._selection_artist.set_data([xmin, xmax], [ymin, ymax]) 

3403 

3404 def _set_active_handle(self, event): 

3405 """Set active handle based on the location of the mouse event.""" 

3406 # Note: event.xdata/ydata in data coordinates, event.x/y in pixels 

3407 c_idx, c_dist = self._corner_handles.closest(event.x, event.y) 

3408 e_idx, e_dist = self._edge_handles.closest(event.x, event.y) 

3409 m_idx, m_dist = self._center_handle.closest(event.x, event.y) 

3410 

3411 if 'move' in self._state: 

3412 self._active_handle = 'C' 

3413 # Set active handle as closest handle, if mouse click is close enough. 

3414 elif m_dist < self.grab_range * 2: 

3415 # Prioritise center handle over other handles 

3416 self._active_handle = 'C' 

3417 elif c_dist > self.grab_range and e_dist > self.grab_range: 

3418 # Not close to any handles 

3419 if self.drag_from_anywhere and self._contains(event): 

3420 # Check if we've clicked inside the region 

3421 self._active_handle = 'C' 

3422 else: 

3423 self._active_handle = None 

3424 return 

3425 elif c_dist < e_dist: 

3426 # Closest to a corner handle 

3427 self._active_handle = self._corner_order[c_idx] 

3428 else: 

3429 # Closest to an edge handle 

3430 self._active_handle = self._edge_order[e_idx] 

3431 

3432 def _contains(self, event): 

3433 """Return True if event is within the patch.""" 

3434 return self._selection_artist.contains(event, radius=0)[0] 

3435 

3436 @property 

3437 def geometry(self): 

3438 """ 

3439 Return an array of shape (2, 5) containing the 

3440 x (``RectangleSelector.geometry[1, :]``) and 

3441 y (``RectangleSelector.geometry[0, :]``) data coordinates of the four 

3442 corners of the rectangle starting and ending in the top left corner. 

3443 """ 

3444 if hasattr(self._selection_artist, 'get_verts'): 

3445 xfm = self.ax.transData.inverted() 

3446 y, x = xfm.transform(self._selection_artist.get_verts()).T 

3447 return np.array([x, y]) 

3448 else: 

3449 return np.array(self._selection_artist.get_data()) 

3450 

3451 

3452@_docstring.Substitution(_RECTANGLESELECTOR_PARAMETERS_DOCSTRING.replace( 

3453 '__ARTIST_NAME__', 'ellipse')) 

3454class EllipseSelector(RectangleSelector): 

3455 """ 

3456 Select an elliptical region of an Axes. 

3457 

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

3459 

3460 Press and release events triggered at the same coordinates outside the 

3461 selection will clear the selector, except when 

3462 ``ignore_event_outside=True``. 

3463 

3464 %s 

3465 

3466 Examples 

3467 -------- 

3468 :doc:`/gallery/widgets/rectangle_selector` 

3469 """ 

3470 

3471 draw_shape = _api.deprecate_privatize_attribute('3.5') 

3472 

3473 def _init_shape(self, **props): 

3474 return Ellipse((0, 0), 0, 1, visible=False, **props) 

3475 

3476 def _draw_shape(self, extents): 

3477 x0, x1, y0, y1 = extents 

3478 xmin, xmax = sorted([x0, x1]) 

3479 ymin, ymax = sorted([y0, y1]) 

3480 center = [x0 + (x1 - x0) / 2., y0 + (y1 - y0) / 2.] 

3481 a = (xmax - xmin) / 2. 

3482 b = (ymax - ymin) / 2. 

3483 

3484 if self._drawtype == 'box': 

3485 self._selection_artist.center = center 

3486 self._selection_artist.width = 2 * a 

3487 self._selection_artist.height = 2 * b 

3488 self._selection_artist.angle = self.rotation 

3489 else: 

3490 rad = np.deg2rad(np.arange(31) * 12) 

3491 x = a * np.cos(rad) + center[0] 

3492 y = b * np.sin(rad) + center[1] 

3493 self._selection_artist.set_data(x, y) 

3494 

3495 @property 

3496 def _rect_bbox(self): 

3497 if self._drawtype == 'box': 

3498 x, y = self._selection_artist.center 

3499 width = self._selection_artist.width 

3500 height = self._selection_artist.height 

3501 return x - width / 2., y - height / 2., width, height 

3502 else: 

3503 x, y = self._selection_artist.get_data() 

3504 x0, x1 = min(x), max(x) 

3505 y0, y1 = min(y), max(y) 

3506 return x0, y0, x1 - x0, y1 - y0 

3507 

3508 

3509class LassoSelector(_SelectorWidget): 

3510 """ 

3511 Selection curve of an arbitrary shape. 

3512 

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

3514 

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

3516 to select data points from an image. 

3517 

3518 In contrast to `Lasso`, `LassoSelector` is written with an interface 

3519 similar to `RectangleSelector` and `SpanSelector`, and will continue to 

3520 interact with the Axes until disconnected. 

3521 

3522 Example usage:: 

3523 

3524 ax = plt.subplot() 

3525 ax.plot(x, y) 

3526 

3527 def onselect(verts): 

3528 print(verts) 

3529 lasso = LassoSelector(ax, onselect) 

3530 

3531 Parameters 

3532 ---------- 

3533 ax : `~matplotlib.axes.Axes` 

3534 The parent Axes for the widget. 

3535 onselect : function 

3536 Whenever the lasso is released, the *onselect* function is called and 

3537 passed the vertices of the selected path. 

3538 useblit : bool, default: True 

3539 Whether to use blitting for faster drawing (if supported by the 

3540 backend). See the tutorial :doc:`/tutorials/advanced/blitting` 

3541 for details. 

3542 props : dict, optional 

3543 Properties with which the line is drawn, see `matplotlib.lines.Line2D` 

3544 for valid properties. Default values are defined in ``mpl.rcParams``. 

3545 button : `.MouseButton` or list of `.MouseButton`, optional 

3546 The mouse buttons used for rectangle selection. Default is ``None``, 

3547 which corresponds to all buttons. 

3548 """ 

3549 

3550 @_api.rename_parameter("3.5", "lineprops", "props") 

3551 def __init__(self, ax, onselect=None, useblit=True, props=None, 

3552 button=None): 

3553 super().__init__(ax, onselect, useblit=useblit, button=button) 

3554 self.verts = None 

3555 if props is None: 

3556 props = dict() 

3557 # self.useblit may be != useblit, if the canvas doesn't support blit. 

3558 props.update(animated=self.useblit, visible=False) 

3559 line = Line2D([], [], **props) 

3560 self.ax.add_line(line) 

3561 self._selection_artist = line 

3562 

3563 @_api.deprecated("3.5", alternative="press") 

3564 def onpress(self, event): 

3565 self.press(event) 

3566 

3567 def _press(self, event): 

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

3569 self._selection_artist.set_visible(True) 

3570 

3571 @_api.deprecated("3.5", alternative="release") 

3572 def onrelease(self, event): 

3573 self.release(event) 

3574 

3575 def _release(self, event): 

3576 if self.verts is not None: 

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

3578 self.onselect(self.verts) 

3579 self._selection_artist.set_data([[], []]) 

3580 self._selection_artist.set_visible(False) 

3581 self.verts = None 

3582 

3583 def _onmove(self, event): 

3584 if self.verts is None: 

3585 return 

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

3587 self._selection_artist.set_data(list(zip(*self.verts))) 

3588 

3589 self.update() 

3590 

3591 

3592class PolygonSelector(_SelectorWidget): 

3593 """ 

3594 Select a polygon region of an Axes. 

3595 

3596 Place vertices with each mouse click, and make the selection by completing 

3597 the polygon (clicking on the first vertex). Once drawn individual vertices 

3598 can be moved by clicking and dragging with the left mouse button, or 

3599 removed by clicking the right mouse button. 

3600 

3601 In addition, the following modifier keys can be used: 

3602 

3603 - Hold *ctrl* and click and drag a vertex to reposition it before the 

3604 polygon has been completed. 

3605 - Hold the *shift* key and click and drag anywhere in the Axes to move 

3606 all vertices. 

3607 - Press the *esc* key to start a new polygon. 

3608 

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

3610 

3611 Parameters 

3612 ---------- 

3613 ax : `~matplotlib.axes.Axes` 

3614 The parent Axes for the widget. 

3615 

3616 onselect : function 

3617 When a polygon is completed or modified after completion, 

3618 the *onselect* function is called and passed a list of the vertices as 

3619 ``(xdata, ydata)`` tuples. 

3620 

3621 useblit : bool, default: False 

3622 Whether to use blitting for faster drawing (if supported by the 

3623 backend). See the tutorial :doc:`/tutorials/advanced/blitting` 

3624 for details. 

3625 

3626 props : dict, optional 

3627 Properties with which the line is drawn, see `matplotlib.lines.Line2D` 

3628 for valid properties. 

3629 Default: 

3630 

3631 ``dict(color='k', linestyle='-', linewidth=2, alpha=0.5)`` 

3632 

3633 handle_props : dict, optional 

3634 Artist properties for the markers drawn at the vertices of the polygon. 

3635 See the marker arguments in `matplotlib.lines.Line2D` for valid 

3636 properties. Default values are defined in ``mpl.rcParams`` except for 

3637 the default value of ``markeredgecolor`` which will be the same as the 

3638 ``color`` property in *props*. 

3639 

3640 grab_range : float, default: 10 

3641 A vertex is selected (to complete the polygon or to move a vertex) if 

3642 the mouse click is within *grab_range* pixels of the vertex. 

3643 

3644 draw_bounding_box : bool, optional 

3645 If `True`, a bounding box will be drawn around the polygon selector 

3646 once it is complete. This box can be used to move and resize the 

3647 selector. 

3648 

3649 box_handle_props : dict, optional 

3650 Properties to set for the box handles. See the documentation for the 

3651 *handle_props* argument to `RectangleSelector` for more info. 

3652 

3653 box_props : dict, optional 

3654 Properties to set for the box. See the documentation for the *props* 

3655 argument to `RectangleSelector` for more info. 

3656 

3657 Examples 

3658 -------- 

3659 :doc:`/gallery/widgets/polygon_selector_simple` 

3660 :doc:`/gallery/widgets/polygon_selector_demo` 

3661 

3662 Notes 

3663 ----- 

3664 If only one point remains after removing points, the selector reverts to an 

3665 incomplete state and you can start drawing a new polygon from the existing 

3666 point. 

3667 """ 

3668 

3669 @_api.rename_parameter("3.5", "lineprops", "props") 

3670 @_api.rename_parameter("3.5", "markerprops", "handle_props") 

3671 @_api.rename_parameter("3.5", "vertex_select_radius", "grab_range") 

3672 def __init__(self, ax, onselect, useblit=False, 

3673 props=None, handle_props=None, grab_range=10, *, 

3674 draw_bounding_box=False, box_handle_props=None, 

3675 box_props=None): 

3676 # The state modifiers 'move', 'square', and 'center' are expected by 

3677 # _SelectorWidget but are not supported by PolygonSelector 

3678 # Note: could not use the existing 'move' state modifier in-place of 

3679 # 'move_all' because _SelectorWidget automatically discards 'move' 

3680 # from the state on button release. 

3681 state_modifier_keys = dict(clear='escape', move_vertex='control', 

3682 move_all='shift', move='not-applicable', 

3683 square='not-applicable', 

3684 center='not-applicable', 

3685 rotate='not-applicable') 

3686 super().__init__(ax, onselect, useblit=useblit, 

3687 state_modifier_keys=state_modifier_keys) 

3688 

3689 self._xys = [(0, 0)] 

3690 

3691 if props is None: 

3692 props = dict(color='k', linestyle='-', linewidth=2, alpha=0.5) 

3693 props['animated'] = self.useblit 

3694 self._props = props 

3695 self._selection_artist = line = Line2D([], [], **self._props) 

3696 self.ax.add_line(line) 

3697 

3698 if handle_props is None: 

3699 handle_props = dict(markeredgecolor='k', 

3700 markerfacecolor=self._props.get('color', 'k')) 

3701 self._handle_props = handle_props 

3702 self._polygon_handles = ToolHandles(self.ax, [], [], 

3703 useblit=self.useblit, 

3704 marker_props=self._handle_props) 

3705 

3706 self._active_handle_idx = -1 

3707 self.grab_range = grab_range 

3708 

3709 self.set_visible(True) 

3710 self._draw_box = draw_bounding_box 

3711 self._box = None 

3712 

3713 if box_handle_props is None: 

3714 box_handle_props = {} 

3715 self._box_handle_props = self._handle_props.update(box_handle_props) 

3716 self._box_props = box_props 

3717 

3718 def _get_bbox(self): 

3719 return self._selection_artist.get_bbox() 

3720 

3721 def _add_box(self): 

3722 self._box = RectangleSelector(self.ax, 

3723 onselect=lambda *args, **kwargs: None, 

3724 useblit=self.useblit, 

3725 grab_range=self.grab_range, 

3726 handle_props=self._box_handle_props, 

3727 props=self._box_props, 

3728 interactive=True) 

3729 self._box._state_modifier_keys.pop('rotate') 

3730 self._box.connect_event('motion_notify_event', self._scale_polygon) 

3731 self._update_box() 

3732 # Set state that prevents the RectangleSelector from being created 

3733 # by the user 

3734 self._box._allow_creation = False 

3735 self._box._selection_completed = True 

3736 self._draw_polygon() 

3737 

3738 def _remove_box(self): 

3739 if self._box is not None: 

3740 self._box.set_visible(False) 

3741 self._box = None 

3742 

3743 def _update_box(self): 

3744 # Update selection box extents to the extents of the polygon 

3745 if self._box is not None: 

3746 bbox = self._get_bbox() 

3747 self._box.extents = [bbox.x0, bbox.x1, bbox.y0, bbox.y1] 

3748 # Save a copy 

3749 self._old_box_extents = self._box.extents 

3750 

3751 def _scale_polygon(self, event): 

3752 """ 

3753 Scale the polygon selector points when the bounding box is moved or 

3754 scaled. 

3755 

3756 This is set as a callback on the bounding box RectangleSelector. 

3757 """ 

3758 if not self._selection_completed: 

3759 return 

3760 

3761 if self._old_box_extents == self._box.extents: 

3762 return 

3763 

3764 # Create transform from old box to new box 

3765 x1, y1, w1, h1 = self._box._rect_bbox 

3766 old_bbox = self._get_bbox() 

3767 t = (transforms.Affine2D() 

3768 .translate(-old_bbox.x0, -old_bbox.y0) 

3769 .scale(1 / old_bbox.width, 1 / old_bbox.height) 

3770 .scale(w1, h1) 

3771 .translate(x1, y1)) 

3772 

3773 # Update polygon verts. Must be a list of tuples for consistency. 

3774 new_verts = [(x, y) for x, y in t.transform(np.array(self.verts))] 

3775 self._xys = [*new_verts, new_verts[0]] 

3776 self._draw_polygon() 

3777 self._old_box_extents = self._box.extents 

3778 

3779 line = _api.deprecated("3.5")( 

3780 property(lambda self: self._selection_artist) 

3781 ) 

3782 

3783 vertex_select_radius = _api.deprecated("3.5", name="vertex_select_radius", 

3784 alternative="grab_range")( 

3785 property(lambda self: self.grab_range, 

3786 lambda self, value: setattr(self, "grab_range", value)) 

3787 ) 

3788 

3789 @property 

3790 def _handles_artists(self): 

3791 return self._polygon_handles.artists 

3792 

3793 def _remove_vertex(self, i): 

3794 """Remove vertex with index i.""" 

3795 if (len(self._xys) > 2 and 

3796 self._selection_completed and 

3797 i in (0, len(self._xys) - 1)): 

3798 # If selecting the first or final vertex, remove both first and 

3799 # last vertex as they are the same for a closed polygon 

3800 self._xys.pop(0) 

3801 self._xys.pop(-1) 

3802 # Close the polygon again by appending the new first vertex to the 

3803 # end 

3804 self._xys.append(self._xys[0]) 

3805 else: 

3806 self._xys.pop(i) 

3807 if len(self._xys) <= 2: 

3808 # If only one point left, return to incomplete state to let user 

3809 # start drawing again 

3810 self._selection_completed = False 

3811 self._remove_box() 

3812 

3813 def _press(self, event): 

3814 """Button press event handler.""" 

3815 # Check for selection of a tool handle. 

3816 if ((self._selection_completed or 'move_vertex' in self._state) 

3817 and len(self._xys) > 0): 

3818 h_idx, h_dist = self._polygon_handles.closest(event.x, event.y) 

3819 if h_dist < self.grab_range: 

3820 self._active_handle_idx = h_idx 

3821 # Save the vertex positions at the time of the press event (needed to 

3822 # support the 'move_all' state modifier). 

3823 self._xys_at_press = self._xys.copy() 

3824 

3825 def _release(self, event): 

3826 """Button release event handler.""" 

3827 # Release active tool handle. 

3828 if self._active_handle_idx >= 0: 

3829 if event.button == 3: 

3830 self._remove_vertex(self._active_handle_idx) 

3831 self._draw_polygon() 

3832 self._active_handle_idx = -1 

3833 

3834 # Complete the polygon. 

3835 elif len(self._xys) > 3 and self._xys[-1] == self._xys[0]: 

3836 self._selection_completed = True 

3837 if self._draw_box and self._box is None: 

3838 self._add_box() 

3839 

3840 # Place new vertex. 

3841 elif (not self._selection_completed 

3842 and 'move_all' not in self._state 

3843 and 'move_vertex' not in self._state): 

3844 self._xys.insert(-1, (event.xdata, event.ydata)) 

3845 

3846 if self._selection_completed: 

3847 self.onselect(self.verts) 

3848 

3849 def onmove(self, event): 

3850 """Cursor move event handler and validator.""" 

3851 # Method overrides _SelectorWidget.onmove because the polygon selector 

3852 # needs to process the move callback even if there is no button press. 

3853 # _SelectorWidget.onmove include logic to ignore move event if 

3854 # _eventpress is None. 

3855 if not self.ignore(event): 

3856 event = self._clean_event(event) 

3857 self._onmove(event) 

3858 return True 

3859 return False 

3860 

3861 def _onmove(self, event): 

3862 """Cursor move event handler.""" 

3863 # Move the active vertex (ToolHandle). 

3864 if self._active_handle_idx >= 0: 

3865 idx = self._active_handle_idx 

3866 self._xys[idx] = event.xdata, event.ydata 

3867 # Also update the end of the polygon line if the first vertex is 

3868 # the active handle and the polygon is completed. 

3869 if idx == 0 and self._selection_completed: 

3870 self._xys[-1] = event.xdata, event.ydata 

3871 

3872 # Move all vertices. 

3873 elif 'move_all' in self._state and self._eventpress: 

3874 dx = event.xdata - self._eventpress.xdata 

3875 dy = event.ydata - self._eventpress.ydata 

3876 for k in range(len(self._xys)): 

3877 x_at_press, y_at_press = self._xys_at_press[k] 

3878 self._xys[k] = x_at_press + dx, y_at_press + dy 

3879 

3880 # Do nothing if completed or waiting for a move. 

3881 elif (self._selection_completed 

3882 or 'move_vertex' in self._state or 'move_all' in self._state): 

3883 return 

3884 

3885 # Position pending vertex. 

3886 else: 

3887 # Calculate distance to the start vertex. 

3888 x0, y0 = \ 

3889 self._selection_artist.get_transform().transform(self._xys[0]) 

3890 v0_dist = np.hypot(x0 - event.x, y0 - event.y) 

3891 # Lock on to the start vertex if near it and ready to complete. 

3892 if len(self._xys) > 3 and v0_dist < self.grab_range: 

3893 self._xys[-1] = self._xys[0] 

3894 else: 

3895 self._xys[-1] = event.xdata, event.ydata 

3896 

3897 self._draw_polygon() 

3898 

3899 def _on_key_press(self, event): 

3900 """Key press event handler.""" 

3901 # Remove the pending vertex if entering the 'move_vertex' or 

3902 # 'move_all' mode 

3903 if (not self._selection_completed 

3904 and ('move_vertex' in self._state or 

3905 'move_all' in self._state)): 

3906 self._xys.pop() 

3907 self._draw_polygon() 

3908 

3909 def _on_key_release(self, event): 

3910 """Key release event handler.""" 

3911 # Add back the pending vertex if leaving the 'move_vertex' or 

3912 # 'move_all' mode (by checking the released key) 

3913 if (not self._selection_completed 

3914 and 

3915 (event.key == self._state_modifier_keys.get('move_vertex') 

3916 or event.key == self._state_modifier_keys.get('move_all'))): 

3917 self._xys.append((event.xdata, event.ydata)) 

3918 self._draw_polygon() 

3919 # Reset the polygon if the released key is the 'clear' key. 

3920 elif event.key == self._state_modifier_keys.get('clear'): 

3921 event = self._clean_event(event) 

3922 self._xys = [(event.xdata, event.ydata)] 

3923 self._selection_completed = False 

3924 self._remove_box() 

3925 self.set_visible(True) 

3926 

3927 def _draw_polygon(self): 

3928 """Redraw the polygon based on the new vertex positions.""" 

3929 xs, ys = zip(*self._xys) if self._xys else ([], []) 

3930 self._selection_artist.set_data(xs, ys) 

3931 self._update_box() 

3932 # Only show one tool handle at the start and end vertex of the polygon 

3933 # if the polygon is completed or the user is locked on to the start 

3934 # vertex. 

3935 if (self._selection_completed 

3936 or (len(self._xys) > 3 

3937 and self._xys[-1] == self._xys[0])): 

3938 self._polygon_handles.set_data(xs[:-1], ys[:-1]) 

3939 else: 

3940 self._polygon_handles.set_data(xs, ys) 

3941 self.update() 

3942 

3943 @property 

3944 def verts(self): 

3945 """The polygon vertices, as a list of ``(x, y)`` pairs.""" 

3946 return self._xys[:-1] 

3947 

3948 @verts.setter 

3949 def verts(self, xys): 

3950 """ 

3951 Set the polygon vertices. 

3952 

3953 This will remove any preexisting vertices, creating a complete polygon 

3954 with the new vertices. 

3955 """ 

3956 self._xys = [*xys, xys[0]] 

3957 self._selection_completed = True 

3958 self.set_visible(True) 

3959 if self._draw_box and self._box is None: 

3960 self._add_box() 

3961 self._draw_polygon() 

3962 

3963 

3964class Lasso(AxesWidget): 

3965 """ 

3966 Selection curve of an arbitrary shape. 

3967 

3968 The selected path can be used in conjunction with 

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

3970 

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

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

3973 

3974 Parameters 

3975 ---------- 

3976 ax : `~matplotlib.axes.Axes` 

3977 The parent Axes for the widget. 

3978 xy : (float, float) 

3979 Coordinates of the start of the lasso. 

3980 useblit : bool, default: True 

3981 Whether to use blitting for faster drawing (if supported by the 

3982 backend). See the tutorial :doc:`/tutorials/advanced/blitting` 

3983 for details. 

3984 callback : callable 

3985 Whenever the lasso is released, the *callback* function is called and 

3986 passed the vertices of the selected path. 

3987 """ 

3988 

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

3990 super().__init__(ax) 

3991 

3992 self.useblit = useblit and self.canvas.supports_blit 

3993 if self.useblit: 

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

3995 

3996 x, y = xy 

3997 self.verts = [(x, y)] 

3998 self.line = Line2D([x], [y], linestyle='-', color='black', lw=2) 

3999 self.ax.add_line(self.line) 

4000 self.callback = callback 

4001 self.connect_event('button_release_event', self.onrelease) 

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

4003 

4004 def onrelease(self, event): 

4005 if self.ignore(event): 

4006 return 

4007 if self.verts is not None: 

4008 self.verts.append((event.xdata, event.ydata)) 

4009 if len(self.verts) > 2: 

4010 self.callback(self.verts) 

4011 self.ax.lines.remove(self.line) 

4012 self.verts = None 

4013 self.disconnect_events() 

4014 

4015 def onmove(self, event): 

4016 if self.ignore(event): 

4017 return 

4018 if self.verts is None: 

4019 return 

4020 if event.inaxes != self.ax: 

4021 return 

4022 if event.button != 1: 

4023 return 

4024 self.verts.append((event.xdata, event.ydata)) 

4025 

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

4027 

4028 if self.useblit: 

4029 self.canvas.restore_region(self.background) 

4030 self.ax.draw_artist(self.line) 

4031 self.canvas.blit(self.ax.bbox) 

4032 else: 

4033 self.canvas.draw_idle()