Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/matplotlib/widgets.py: 16%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1794 statements  

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

14import itertools 

15from numbers import Integral, Number 

16 

17from cycler import cycler 

18import numpy as np 

19 

20import matplotlib as mpl 

21from . import (_api, _docstring, backend_tools, cbook, collections, colors, 

22 text as mtext, ticker, transforms) 

23from .lines import Line2D 

24from .patches import Rectangle, Ellipse, Polygon 

25from .transforms import TransformedPatchPath, Affine2D 

26 

27 

28class LockDraw: 

29 """ 

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

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

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

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

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

35 they also try to acquire the lock first). 

36 """ 

37 

38 def __init__(self): 

39 self._owner = None 

40 

41 def __call__(self, o): 

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

43 if not self.available(o): 

44 raise ValueError('already locked') 

45 self._owner = o 

46 

47 def release(self, o): 

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

49 if not self.available(o): 

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

51 self._owner = None 

52 

53 def available(self, o): 

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

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

56 

57 def isowner(self, o): 

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

59 return self._owner is o 

60 

61 def locked(self): 

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

63 return self._owner is not None 

64 

65 

66class Widget: 

67 """ 

68 Abstract base class for GUI neutral widgets. 

69 """ 

70 drawon = True 

71 eventson = True 

72 _active = True 

73 

74 def set_active(self, active): 

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

76 self._active = active 

77 

78 def get_active(self): 

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

80 return self._active 

81 

82 # set_active is overridden by SelectorWidgets. 

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

84 

85 def ignore(self, event): 

86 """ 

87 Return whether *event* should be ignored. 

88 

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

90 """ 

91 return not self.active 

92 

93 

94class AxesWidget(Widget): 

95 """ 

96 Widget connected to a single `~matplotlib.axes.Axes`. 

97 

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

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

100 

101 This is necessary because the callback registry 

102 maintains only weak-refs to the functions, which are member 

103 functions of the widget. If there are no references to the widget 

104 object it may be garbage collected which will disconnect the callbacks. 

105 

106 Attributes 

107 ---------- 

108 ax : `~matplotlib.axes.Axes` 

109 The parent Axes for the widget. 

110 canvas : `~matplotlib.backend_bases.FigureCanvasBase` 

111 The parent figure canvas for the widget. 

112 active : bool 

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

114 """ 

115 

116 def __init__(self, ax): 

117 self.ax = ax 

118 self._cids = [] 

119 

120 canvas = property(lambda self: self.ax.figure.canvas) 

121 

122 def connect_event(self, event, callback): 

123 """ 

124 Connect a callback function with an event. 

125 

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

127 function stores callback ids for later clean up. 

128 """ 

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

130 self._cids.append(cid) 

131 

132 def disconnect_events(self): 

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

134 for c in self._cids: 

135 self.canvas.mpl_disconnect(c) 

136 

137 def _get_data_coords(self, event): 

138 """Return *event*'s data coordinates in this widget's Axes.""" 

139 # This method handles the possibility that event.inaxes != self.ax (which may 

140 # occur if multiple Axes are overlaid), in which case event.xdata/.ydata will 

141 # be wrong. Note that we still special-case the common case where 

142 # event.inaxes == self.ax and avoid re-running the inverse data transform, 

143 # because that can introduce floating point errors for synthetic events. 

144 return ((event.xdata, event.ydata) if event.inaxes is self.ax 

145 else self.ax.transData.inverted().transform((event.x, event.y))) 

146 

147 

148class Button(AxesWidget): 

149 """ 

150 A GUI neutral button. 

151 

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

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

154 

155 Attributes 

156 ---------- 

157 ax 

158 The `~.axes.Axes` the button renders into. 

159 label 

160 A `.Text` instance. 

161 color 

162 The color of the button when not hovering. 

163 hovercolor 

164 The color of the button when hovering. 

165 """ 

166 

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

168 color='0.85', hovercolor='0.95', *, useblit=True): 

169 """ 

170 Parameters 

171 ---------- 

172 ax : `~matplotlib.axes.Axes` 

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

174 label : str 

175 The button text. 

176 image : array-like or PIL Image 

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

178 directly forwarded to `~.axes.Axes.imshow`. 

179 color : :mpltype:`color` 

180 The color of the button when not activated. 

181 hovercolor : :mpltype:`color` 

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

183 useblit : bool, default: True 

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

185 See the tutorial :ref:`blitting` for details. 

186 

187 .. versionadded:: 3.7 

188 """ 

189 super().__init__(ax) 

190 

191 if image is not None: 

192 ax.imshow(image) 

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

194 verticalalignment='center', 

195 horizontalalignment='center', 

196 transform=ax.transAxes) 

197 

198 self._useblit = useblit and self.canvas.supports_blit 

199 

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

201 

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

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

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

205 ax.set_navigate(False) 

206 ax.set_facecolor(color) 

207 ax.set_xticks([]) 

208 ax.set_yticks([]) 

209 self.color = color 

210 self.hovercolor = hovercolor 

211 

212 def _click(self, event): 

213 if not self.eventson or self.ignore(event) or not self.ax.contains(event)[0]: 

214 return 

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

216 event.canvas.grab_mouse(self.ax) 

217 

218 def _release(self, event): 

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

220 return 

221 event.canvas.release_mouse(self.ax) 

222 if self.eventson and self.ax.contains(event)[0]: 

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

224 

225 def _motion(self, event): 

226 if self.ignore(event): 

227 return 

228 c = self.hovercolor if self.ax.contains(event)[0] else self.color 

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

230 self.ax.set_facecolor(c) 

231 if self.drawon: 

232 if self._useblit: 

233 self.ax.draw_artist(self.ax) 

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

235 else: 

236 self.canvas.draw() 

237 

238 def on_clicked(self, func): 

239 """ 

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

241 

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

243 """ 

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

245 

246 def disconnect(self, cid): 

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

248 self._observers.disconnect(cid) 

249 

250 

251class SliderBase(AxesWidget): 

252 """ 

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

254 usage. 

255 

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

257 """ 

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

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

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

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

262 

263 super().__init__(ax) 

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

265 

266 self.orientation = orientation 

267 self.closedmin = closedmin 

268 self.closedmax = closedmax 

269 self.valmin = valmin 

270 self.valmax = valmax 

271 self.valstep = valstep 

272 self.drag_active = False 

273 self.valfmt = valfmt 

274 

275 if orientation == "vertical": 

276 ax.set_ylim((valmin, valmax)) 

277 axis = ax.yaxis 

278 else: 

279 ax.set_xlim((valmin, valmax)) 

280 axis = ax.xaxis 

281 

282 self._fmt = axis.get_major_formatter() 

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

284 self._fmt = ticker.ScalarFormatter() 

285 self._fmt.set_axis(axis) 

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

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

288 

289 ax.set_axis_off() 

290 ax.set_navigate(False) 

291 

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

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

294 if dragging: 

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

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

297 

298 def _stepped_value(self, val): 

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

300 if isinstance(self.valstep, Number): 

301 val = (self.valmin 

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

303 elif self.valstep is not None: 

304 valstep = np.asanyarray(self.valstep) 

305 if valstep.ndim != 1: 

306 raise ValueError( 

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

308 ) 

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

310 return val 

311 

312 def disconnect(self, cid): 

313 """ 

314 Remove the observer with connection id *cid*. 

315 

316 Parameters 

317 ---------- 

318 cid : int 

319 Connection id of the observer to be removed. 

320 """ 

321 self._observers.disconnect(cid) 

322 

323 def reset(self): 

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

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

326 self.set_val(self.valinit) 

327 

328 

329class Slider(SliderBase): 

330 """ 

331 A slider representing a floating point range. 

332 

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

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

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

336 

337 Attributes 

338 ---------- 

339 val : float 

340 Slider value. 

341 """ 

342 

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

344 closedmin=True, closedmax=True, slidermin=None, 

345 slidermax=None, dragging=True, valstep=None, 

346 orientation='horizontal', initcolor='r', 

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

348 """ 

349 Parameters 

350 ---------- 

351 ax : Axes 

352 The Axes to put the slider in. 

353 

354 label : str 

355 Slider label. 

356 

357 valmin : float 

358 The minimum value of the slider. 

359 

360 valmax : float 

361 The maximum value of the slider. 

362 

363 valinit : float, default: 0.5 

364 The slider initial position. 

365 

366 valfmt : str, default: None 

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

368 `.ScalarFormatter` is used instead. 

369 

370 closedmin : bool, default: True 

371 Whether the slider interval is closed on the bottom. 

372 

373 closedmax : bool, default: True 

374 Whether the slider interval is closed on the top. 

375 

376 slidermin : Slider, default: None 

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

378 the value of the Slider *slidermin*. 

379 

380 slidermax : Slider, default: None 

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

382 the value of the Slider *slidermax*. 

383 

384 dragging : bool, default: True 

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

386 

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

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

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

390 

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

392 The orientation of the slider. 

393 

394 initcolor : :mpltype:`color`, default: 'r' 

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

396 for no line. 

397 

398 track_color : :mpltype:`color`, default: 'lightgrey' 

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

400 further styling via the *track* attribute. 

401 

402 handle_style : dict 

403 Properties of the slider handle. Default values are 

404 

405 ========= ===== ======= ======================================== 

406 Key Value Default Description 

407 ========= ===== ======= ======================================== 

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

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

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

411 ========= ===== ======= ======================================== 

412 

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

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

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

416 

417 Notes 

418 ----- 

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

420 `~matplotlib.patches.Rectangle` that draws the slider knob. See the 

421 `.Rectangle` documentation for valid property names (``facecolor``, 

422 ``edgecolor``, ``alpha``, etc.). 

423 """ 

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

425 valmin, valmax, valfmt, dragging, valstep) 

426 

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

428 raise ValueError( 

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

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

431 raise ValueError( 

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

433 self.slidermin = slidermin 

434 self.slidermax = slidermax 

435 valinit = self._value_in_bounds(valinit) 

436 if valinit is None: 

437 valinit = valmin 

438 self.val = valinit 

439 self.valinit = valinit 

440 

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

442 handle_style = {} if handle_style is None else handle_style 

443 marker_props = { 

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

445 } 

446 

447 if orientation == 'vertical': 

448 self.track = Rectangle( 

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

450 transform=ax.transAxes, 

451 facecolor=track_color 

452 ) 

453 ax.add_patch(self.track) 

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

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

456 # pixelation-related asymmetries. 

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

458 clip_path=TransformedPatchPath(self.track)) 

459 handleXY = [[0.5], [valinit]] 

460 else: 

461 self.track = Rectangle( 

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

463 transform=ax.transAxes, 

464 facecolor=track_color 

465 ) 

466 ax.add_patch(self.track) 

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

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

469 clip_path=TransformedPatchPath(self.track)) 

470 handleXY = [[valinit], [0.5]] 

471 self._handle, = ax.plot( 

472 *handleXY, 

473 "o", 

474 **marker_props, 

475 clip_on=False 

476 ) 

477 

478 if orientation == 'vertical': 

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

480 verticalalignment='bottom', 

481 horizontalalignment='center') 

482 

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

484 transform=ax.transAxes, 

485 verticalalignment='top', 

486 horizontalalignment='center') 

487 else: 

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

489 verticalalignment='center', 

490 horizontalalignment='right') 

491 

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

493 transform=ax.transAxes, 

494 verticalalignment='center', 

495 horizontalalignment='left') 

496 

497 self.set_val(valinit) 

498 

499 def _value_in_bounds(self, val): 

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

501 val = self._stepped_value(val) 

502 

503 if val <= self.valmin: 

504 if not self.closedmin: 

505 return 

506 val = self.valmin 

507 elif val >= self.valmax: 

508 if not self.closedmax: 

509 return 

510 val = self.valmax 

511 

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

513 if not self.closedmin: 

514 return 

515 val = self.slidermin.val 

516 

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

518 if not self.closedmax: 

519 return 

520 val = self.slidermax.val 

521 return val 

522 

523 def _update(self, event): 

524 """Update the slider position.""" 

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

526 return 

527 

528 if event.name == 'button_press_event' and self.ax.contains(event)[0]: 

529 self.drag_active = True 

530 event.canvas.grab_mouse(self.ax) 

531 

532 if not self.drag_active: 

533 return 

534 

535 if (event.name == 'button_release_event' 

536 or event.name == 'button_press_event' and not self.ax.contains(event)[0]): 

537 self.drag_active = False 

538 event.canvas.release_mouse(self.ax) 

539 return 

540 

541 xdata, ydata = self._get_data_coords(event) 

542 val = self._value_in_bounds( 

543 xdata if self.orientation == 'horizontal' else ydata) 

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

545 self.set_val(val) 

546 

547 def _format(self, val): 

548 """Pretty-print *val*.""" 

549 if self.valfmt is not None: 

550 return self.valfmt % val 

551 else: 

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

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

554 return s + self._fmt.get_offset() 

555 

556 def set_val(self, val): 

557 """ 

558 Set slider value to *val*. 

559 

560 Parameters 

561 ---------- 

562 val : float 

563 """ 

564 if self.orientation == 'vertical': 

565 self.poly.set_height(val - self.poly.get_y()) 

566 self._handle.set_ydata([val]) 

567 else: 

568 self.poly.set_width(val - self.poly.get_x()) 

569 self._handle.set_xdata([val]) 

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

571 if self.drawon: 

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

573 self.val = val 

574 if self.eventson: 

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

576 

577 def on_changed(self, func): 

578 """ 

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

580 

581 Parameters 

582 ---------- 

583 func : callable 

584 Function to call when slider is changed. 

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

586 

587 Returns 

588 ------- 

589 int 

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

591 """ 

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

593 

594 

595class RangeSlider(SliderBase): 

596 """ 

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

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

599 

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

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

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

603 

604 Attributes 

605 ---------- 

606 val : tuple of float 

607 Slider value. 

608 """ 

609 

610 def __init__( 

611 self, 

612 ax, 

613 label, 

614 valmin, 

615 valmax, 

616 *, 

617 valinit=None, 

618 valfmt=None, 

619 closedmin=True, 

620 closedmax=True, 

621 dragging=True, 

622 valstep=None, 

623 orientation="horizontal", 

624 track_color='lightgrey', 

625 handle_style=None, 

626 **kwargs, 

627 ): 

628 """ 

629 Parameters 

630 ---------- 

631 ax : Axes 

632 The Axes to put the slider in. 

633 

634 label : str 

635 Slider label. 

636 

637 valmin : float 

638 The minimum value of the slider. 

639 

640 valmax : float 

641 The maximum value of the slider. 

642 

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

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

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

646 

647 valfmt : str, default: None 

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

649 `.ScalarFormatter` is used instead. 

650 

651 closedmin : bool, default: True 

652 Whether the slider interval is closed on the bottom. 

653 

654 closedmax : bool, default: True 

655 Whether the slider interval is closed on the top. 

656 

657 dragging : bool, default: True 

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

659 

660 valstep : float, default: None 

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

662 

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

664 The orientation of the slider. 

665 

666 track_color : :mpltype:`color`, default: 'lightgrey' 

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

668 further styling via the *track* attribute. 

669 

670 handle_style : dict 

671 Properties of the slider handles. Default values are 

672 

673 ========= ===== ======= ========================================= 

674 Key Value Default Description 

675 ========= ===== ======= ========================================= 

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

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

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

679 ========= ===== ======= ========================================= 

680 

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

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

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

684 

685 Notes 

686 ----- 

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

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

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

690 ``edgecolor``, ``alpha``, etc.). 

691 """ 

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

693 valmin, valmax, valfmt, dragging, valstep) 

694 

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

696 self.val = (valmin, valmax) 

697 if valinit is None: 

698 # Place at the 25th and 75th percentiles 

699 extent = valmax - valmin 

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

701 valmin + extent * 0.75]) 

702 else: 

703 valinit = self._value_in_bounds(valinit) 

704 self.val = valinit 

705 self.valinit = valinit 

706 

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

708 handle_style = {} if handle_style is None else handle_style 

709 marker_props = { 

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

711 } 

712 

713 if orientation == "vertical": 

714 self.track = Rectangle( 

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

716 transform=ax.transAxes, 

717 facecolor=track_color 

718 ) 

719 ax.add_patch(self.track) 

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

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

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

723 else: 

724 self.track = Rectangle( 

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

726 transform=ax.transAxes, 

727 facecolor=track_color 

728 ) 

729 ax.add_patch(self.track) 

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

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

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

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

734 self._update_selection_poly(*valinit) 

735 self.poly.set_transform(poly_transform) 

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

737 self.ax.add_patch(self.poly) 

738 self.ax._request_autoscale_view() 

739 self._handles = [ 

740 ax.plot( 

741 *handleXY_1, 

742 "o", 

743 **marker_props, 

744 clip_on=False 

745 )[0], 

746 ax.plot( 

747 *handleXY_2, 

748 "o", 

749 **marker_props, 

750 clip_on=False 

751 )[0] 

752 ] 

753 

754 if orientation == "vertical": 

755 self.label = ax.text( 

756 0.5, 

757 1.02, 

758 label, 

759 transform=ax.transAxes, 

760 verticalalignment="bottom", 

761 horizontalalignment="center", 

762 ) 

763 

764 self.valtext = ax.text( 

765 0.5, 

766 -0.02, 

767 self._format(valinit), 

768 transform=ax.transAxes, 

769 verticalalignment="top", 

770 horizontalalignment="center", 

771 ) 

772 else: 

773 self.label = ax.text( 

774 -0.02, 

775 0.5, 

776 label, 

777 transform=ax.transAxes, 

778 verticalalignment="center", 

779 horizontalalignment="right", 

780 ) 

781 

782 self.valtext = ax.text( 

783 1.02, 

784 0.5, 

785 self._format(valinit), 

786 transform=ax.transAxes, 

787 verticalalignment="center", 

788 horizontalalignment="left", 

789 ) 

790 

791 self._active_handle = None 

792 self.set_val(valinit) 

793 

794 def _update_selection_poly(self, vmin, vmax): 

795 """ 

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

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

798 """ 

799 # The vertices are positioned 

800 # 1 ------ 2 

801 # | | 

802 # 0, 4 ---- 3 

803 verts = self.poly.xy 

804 if self.orientation == "vertical": 

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

806 verts[1] = .25, vmax 

807 verts[2] = .75, vmax 

808 verts[3] = .75, vmin 

809 else: 

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

811 verts[1] = vmin, .75 

812 verts[2] = vmax, .75 

813 verts[3] = vmax, .25 

814 

815 def _min_in_bounds(self, min): 

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

817 if min <= self.valmin: 

818 if not self.closedmin: 

819 return self.val[0] 

820 min = self.valmin 

821 

822 if min > self.val[1]: 

823 min = self.val[1] 

824 return self._stepped_value(min) 

825 

826 def _max_in_bounds(self, max): 

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

828 if max >= self.valmax: 

829 if not self.closedmax: 

830 return self.val[1] 

831 max = self.valmax 

832 

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

834 max = self.val[0] 

835 return self._stepped_value(max) 

836 

837 def _value_in_bounds(self, vals): 

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

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

840 

841 def _update_val_from_pos(self, pos): 

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

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

844 if idx == 0: 

845 val = self._min_in_bounds(pos) 

846 self.set_min(val) 

847 else: 

848 val = self._max_in_bounds(pos) 

849 self.set_max(val) 

850 if self._active_handle: 

851 if self.orientation == "vertical": 

852 self._active_handle.set_ydata([val]) 

853 else: 

854 self._active_handle.set_xdata([val]) 

855 

856 def _update(self, event): 

857 """Update the slider position.""" 

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

859 return 

860 

861 if event.name == "button_press_event" and self.ax.contains(event)[0]: 

862 self.drag_active = True 

863 event.canvas.grab_mouse(self.ax) 

864 

865 if not self.drag_active: 

866 return 

867 

868 if (event.name == "button_release_event" 

869 or event.name == "button_press_event" and not self.ax.contains(event)[0]): 

870 self.drag_active = False 

871 event.canvas.release_mouse(self.ax) 

872 self._active_handle = None 

873 return 

874 

875 # determine which handle was grabbed 

876 xdata, ydata = self._get_data_coords(event) 

877 handle_index = np.argmin(np.abs( 

878 [h.get_xdata()[0] - xdata for h in self._handles] 

879 if self.orientation == "horizontal" else 

880 [h.get_ydata()[0] - ydata for h in self._handles])) 

881 handle = self._handles[handle_index] 

882 

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

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

885 if handle is not self._active_handle: 

886 self._active_handle = handle 

887 

888 self._update_val_from_pos(xdata if self.orientation == "horizontal" else ydata) 

889 

890 def _format(self, val): 

891 """Pretty-print *val*.""" 

892 if self.valfmt is not None: 

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

894 else: 

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

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

897 ) 

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

899 s1 += self._fmt.get_offset() 

900 s2 += self._fmt.get_offset() 

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

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

903 

904 def set_min(self, min): 

905 """ 

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

907 

908 Parameters 

909 ---------- 

910 min : float 

911 """ 

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

913 

914 def set_max(self, max): 

915 """ 

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

917 

918 Parameters 

919 ---------- 

920 max : float 

921 """ 

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

923 

924 def set_val(self, val): 

925 """ 

926 Set slider value to *val*. 

927 

928 Parameters 

929 ---------- 

930 val : tuple or array-like of float 

931 """ 

932 val = np.sort(val) 

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

934 # Reset value to allow _value_in_bounds() to work. 

935 self.val = (self.valmin, self.valmax) 

936 vmin, vmax = self._value_in_bounds(val) 

937 self._update_selection_poly(vmin, vmax) 

938 if self.orientation == "vertical": 

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

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

941 else: 

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

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

944 

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

946 

947 if self.drawon: 

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

949 self.val = (vmin, vmax) 

950 if self.eventson: 

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

952 

953 def on_changed(self, func): 

954 """ 

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

956 

957 Parameters 

958 ---------- 

959 func : callable 

960 Function to call when slider is changed. The function 

961 must accept a 2-tuple of floats as its argument. 

962 

963 Returns 

964 ------- 

965 int 

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

967 """ 

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

969 

970 

971def _expand_text_props(props): 

972 props = cbook.normalize_kwargs(props, mtext.Text) 

973 return cycler(**props)() if props else itertools.repeat({}) 

974 

975 

976class CheckButtons(AxesWidget): 

977 r""" 

978 A GUI neutral set of check buttons. 

979 

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

981 reference to this object. 

982 

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

984 

985 Attributes 

986 ---------- 

987 ax : `~matplotlib.axes.Axes` 

988 The parent Axes for the widget. 

989 labels : list of `~matplotlib.text.Text` 

990 The text label objects of the check buttons. 

991 """ 

992 

993 def __init__(self, ax, labels, actives=None, *, useblit=True, 

994 label_props=None, frame_props=None, check_props=None): 

995 """ 

996 Add check buttons to `~.axes.Axes` instance *ax*. 

997 

998 Parameters 

999 ---------- 

1000 ax : `~matplotlib.axes.Axes` 

1001 The parent Axes for the widget. 

1002 labels : list of str 

1003 The labels of the check buttons. 

1004 actives : list of bool, optional 

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

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

1007 useblit : bool, default: True 

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

1009 See the tutorial :ref:`blitting` for details. 

1010 

1011 .. versionadded:: 3.7 

1012 

1013 label_props : dict, optional 

1014 Dictionary of `.Text` properties to be used for the labels. 

1015 

1016 .. versionadded:: 3.7 

1017 frame_props : dict, optional 

1018 Dictionary of scatter `.Collection` properties to be used for the 

1019 check button frame. Defaults (label font size / 2)**2 size, black 

1020 edgecolor, no facecolor, and 1.0 linewidth. 

1021 

1022 .. versionadded:: 3.7 

1023 check_props : dict, optional 

1024 Dictionary of scatter `.Collection` properties to be used for the 

1025 check button check. Defaults to (label font size / 2)**2 size, 

1026 black color, and 1.0 linewidth. 

1027 

1028 .. versionadded:: 3.7 

1029 """ 

1030 super().__init__(ax) 

1031 

1032 _api.check_isinstance((dict, None), label_props=label_props, 

1033 frame_props=frame_props, check_props=check_props) 

1034 

1035 ax.set_xticks([]) 

1036 ax.set_yticks([]) 

1037 ax.set_navigate(False) 

1038 

1039 if actives is None: 

1040 actives = [False] * len(labels) 

1041 

1042 self._useblit = useblit and self.canvas.supports_blit 

1043 self._background = None 

1044 

1045 ys = np.linspace(1, 0, len(labels)+2)[1:-1] 

1046 

1047 label_props = _expand_text_props(label_props) 

1048 self.labels = [ 

1049 ax.text(0.25, y, label, transform=ax.transAxes, 

1050 horizontalalignment="left", verticalalignment="center", 

1051 **props) 

1052 for y, label, props in zip(ys, labels, label_props)] 

1053 text_size = np.array([text.get_fontsize() for text in self.labels]) / 2 

1054 

1055 frame_props = { 

1056 's': text_size**2, 

1057 'linewidth': 1, 

1058 **cbook.normalize_kwargs(frame_props, collections.PathCollection), 

1059 'marker': 's', 

1060 'transform': ax.transAxes, 

1061 } 

1062 frame_props.setdefault('facecolor', frame_props.get('color', 'none')) 

1063 frame_props.setdefault('edgecolor', frame_props.pop('color', 'black')) 

1064 self._frames = ax.scatter([0.15] * len(ys), ys, **frame_props) 

1065 check_props = { 

1066 'linewidth': 1, 

1067 's': text_size**2, 

1068 **cbook.normalize_kwargs(check_props, collections.PathCollection), 

1069 'marker': 'x', 

1070 'transform': ax.transAxes, 

1071 'animated': self._useblit, 

1072 } 

1073 check_props.setdefault('facecolor', check_props.pop('color', 'black')) 

1074 self._checks = ax.scatter([0.15] * len(ys), ys, **check_props) 

1075 # The user may have passed custom colours in check_props, so we need to 

1076 # create the checks (above), and modify the visibility after getting 

1077 # whatever the user set. 

1078 self._init_status(actives) 

1079 

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

1081 if self._useblit: 

1082 self.connect_event('draw_event', self._clear) 

1083 

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

1085 

1086 def _clear(self, event): 

1087 """Internal event handler to clear the buttons.""" 

1088 if self.ignore(event) or self.canvas.is_saving(): 

1089 return 

1090 self._background = self.canvas.copy_from_bbox(self.ax.bbox) 

1091 self.ax.draw_artist(self._checks) 

1092 

1093 def _clicked(self, event): 

1094 if self.ignore(event) or event.button != 1 or not self.ax.contains(event)[0]: 

1095 return 

1096 idxs = [ # Indices of frames and of texts that contain the event. 

1097 *self._frames.contains(event)[1]["ind"], 

1098 *[i for i, text in enumerate(self.labels) if text.contains(event)[0]]] 

1099 if idxs: 

1100 coords = self._frames.get_offset_transform().transform( 

1101 self._frames.get_offsets()) 

1102 self.set_active( # Closest index, only looking in idxs. 

1103 idxs[(((event.x, event.y) - coords[idxs]) ** 2).sum(-1).argmin()]) 

1104 

1105 def set_label_props(self, props): 

1106 """ 

1107 Set properties of the `.Text` labels. 

1108 

1109 .. versionadded:: 3.7 

1110 

1111 Parameters 

1112 ---------- 

1113 props : dict 

1114 Dictionary of `.Text` properties to be used for the labels. 

1115 """ 

1116 _api.check_isinstance(dict, props=props) 

1117 props = _expand_text_props(props) 

1118 for text, prop in zip(self.labels, props): 

1119 text.update(prop) 

1120 

1121 def set_frame_props(self, props): 

1122 """ 

1123 Set properties of the check button frames. 

1124 

1125 .. versionadded:: 3.7 

1126 

1127 Parameters 

1128 ---------- 

1129 props : dict 

1130 Dictionary of `.Collection` properties to be used for the check 

1131 button frames. 

1132 """ 

1133 _api.check_isinstance(dict, props=props) 

1134 if 's' in props: # Keep API consistent with constructor. 

1135 props['sizes'] = np.broadcast_to(props.pop('s'), len(self.labels)) 

1136 self._frames.update(props) 

1137 

1138 def set_check_props(self, props): 

1139 """ 

1140 Set properties of the check button checks. 

1141 

1142 .. versionadded:: 3.7 

1143 

1144 Parameters 

1145 ---------- 

1146 props : dict 

1147 Dictionary of `.Collection` properties to be used for the check 

1148 button check. 

1149 """ 

1150 _api.check_isinstance(dict, props=props) 

1151 if 's' in props: # Keep API consistent with constructor. 

1152 props['sizes'] = np.broadcast_to(props.pop('s'), len(self.labels)) 

1153 actives = self.get_status() 

1154 self._checks.update(props) 

1155 # If new colours are supplied, then we must re-apply the status. 

1156 self._init_status(actives) 

1157 

1158 def set_active(self, index, state=None): 

1159 """ 

1160 Modify the state of a check button by index. 

1161 

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

1163 

1164 Parameters 

1165 ---------- 

1166 index : int 

1167 Index of the check button to toggle. 

1168 

1169 state : bool, optional 

1170 If a boolean value, set the state explicitly. If no value is 

1171 provided, the state is toggled. 

1172 

1173 Raises 

1174 ------ 

1175 ValueError 

1176 If *index* is invalid. 

1177 TypeError 

1178 If *state* is not boolean. 

1179 """ 

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

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

1182 _api.check_isinstance((bool, None), state=state) 

1183 

1184 invisible = colors.to_rgba('none') 

1185 

1186 facecolors = self._checks.get_facecolor() 

1187 if state is None: 

1188 state = colors.same_color(facecolors[index], invisible) 

1189 facecolors[index] = self._active_check_colors[index] if state else invisible 

1190 self._checks.set_facecolor(facecolors) 

1191 

1192 if self.drawon: 

1193 if self._useblit: 

1194 if self._background is not None: 

1195 self.canvas.restore_region(self._background) 

1196 self.ax.draw_artist(self._checks) 

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

1198 else: 

1199 self.canvas.draw() 

1200 

1201 if self.eventson: 

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

1203 

1204 def _init_status(self, actives): 

1205 """ 

1206 Initialize properties to match active status. 

1207 

1208 The user may have passed custom colours in *check_props* to the 

1209 constructor, or to `.set_check_props`, so we need to modify the 

1210 visibility after getting whatever the user set. 

1211 """ 

1212 self._active_check_colors = self._checks.get_facecolor() 

1213 if len(self._active_check_colors) == 1: 

1214 self._active_check_colors = np.repeat(self._active_check_colors, 

1215 len(actives), axis=0) 

1216 self._checks.set_facecolor( 

1217 [ec if active else "none" 

1218 for ec, active in zip(self._active_check_colors, actives)]) 

1219 

1220 def clear(self): 

1221 """Uncheck all checkboxes.""" 

1222 

1223 self._checks.set_facecolor(['none'] * len(self._active_check_colors)) 

1224 

1225 if hasattr(self, '_lines'): 

1226 for l1, l2 in self._lines: 

1227 l1.set_visible(False) 

1228 l2.set_visible(False) 

1229 

1230 if self.drawon: 

1231 self.canvas.draw() 

1232 

1233 if self.eventson: 

1234 # Call with no label, as all checkboxes are being cleared. 

1235 self._observers.process('clicked', None) 

1236 

1237 def get_status(self): 

1238 """ 

1239 Return a list of the status (True/False) of all of the check buttons. 

1240 """ 

1241 return [not colors.same_color(color, colors.to_rgba("none")) 

1242 for color in self._checks.get_facecolors()] 

1243 

1244 def get_checked_labels(self): 

1245 """Return a list of labels currently checked by user.""" 

1246 

1247 return [l.get_text() for l, box_checked in 

1248 zip(self.labels, self.get_status()) 

1249 if box_checked] 

1250 

1251 def on_clicked(self, func): 

1252 """ 

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

1254 

1255 Parameters 

1256 ---------- 

1257 func : callable 

1258 When the button is clicked, call *func* with button label. 

1259 When all buttons are cleared, call *func* with None. 

1260 The callback func must have the signature:: 

1261 

1262 def func(label: str | None) -> Any 

1263 

1264 Return values may exist, but are ignored. 

1265 

1266 Returns 

1267 ------- 

1268 A connection id, which can be used to disconnect the callback. 

1269 """ 

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

1271 

1272 def disconnect(self, cid): 

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

1274 self._observers.disconnect(cid) 

1275 

1276 

1277class TextBox(AxesWidget): 

1278 """ 

1279 A GUI neutral text input box. 

1280 

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

1282 

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

1284 

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

1286 leaves the text entry field. 

1287 

1288 Attributes 

1289 ---------- 

1290 ax : `~matplotlib.axes.Axes` 

1291 The parent Axes for the widget. 

1292 label : `~matplotlib.text.Text` 

1293 

1294 color : :mpltype:`color` 

1295 The color of the text box when not hovering. 

1296 hovercolor : :mpltype:`color` 

1297 The color of the text box when hovering. 

1298 """ 

1299 

1300 def __init__(self, ax, label, initial='', *, 

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

1302 textalignment="left"): 

1303 """ 

1304 Parameters 

1305 ---------- 

1306 ax : `~matplotlib.axes.Axes` 

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

1308 label : str 

1309 Label for this text box. 

1310 initial : str 

1311 Initial value in the text box. 

1312 color : :mpltype:`color` 

1313 The color of the box. 

1314 hovercolor : :mpltype:`color` 

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

1316 label_pad : float 

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

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

1319 The horizontal location of the text. 

1320 """ 

1321 super().__init__(ax) 

1322 

1323 self._text_position = _api.check_getitem( 

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

1325 textalignment=textalignment) 

1326 

1327 self.label = ax.text( 

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

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

1330 

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

1332 self.text_disp = self.ax.text( 

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

1334 verticalalignment='center', horizontalalignment=textalignment, 

1335 parse_math=False) 

1336 

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

1338 

1339 ax.set( 

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

1341 navigate=False, facecolor=color, 

1342 xticks=[], yticks=[]) 

1343 

1344 self.cursor_index = 0 

1345 

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

1347 transform=mpl.transforms.IdentityTransform()) 

1348 

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

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

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

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

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

1354 

1355 self.color = color 

1356 self.hovercolor = hovercolor 

1357 

1358 self.capturekeystrokes = False 

1359 

1360 @property 

1361 def text(self): 

1362 return self.text_disp.get_text() 

1363 

1364 def _rendercursor(self): 

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

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

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

1368 # at the saved dimensions 

1369 

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

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

1372 # figure later anyways. 

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

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

1375 

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

1377 widthtext = text[:self.cursor_index] 

1378 

1379 bb_text = self.text_disp.get_window_extent() 

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

1381 bb_widthtext = self.text_disp.get_window_extent() 

1382 

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

1384 bb_text.y0 -= bb_widthtext.height / 2 

1385 bb_text.y1 += bb_widthtext.height / 2 

1386 elif not widthtext: # Keep width to 0. 

1387 bb_text.x1 = bb_text.x0 

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

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

1390 

1391 self.cursor.set( 

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

1393 visible=True) 

1394 self.text_disp.set_text(text) 

1395 

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

1397 

1398 def _release(self, event): 

1399 if self.ignore(event): 

1400 return 

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

1402 return 

1403 event.canvas.release_mouse(self.ax) 

1404 

1405 def _keypress(self, event): 

1406 if self.ignore(event): 

1407 return 

1408 if self.capturekeystrokes: 

1409 key = event.key 

1410 text = self.text 

1411 if len(key) == 1: 

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

1413 text[self.cursor_index:]) 

1414 self.cursor_index += 1 

1415 elif key == "right": 

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

1417 self.cursor_index += 1 

1418 elif key == "left": 

1419 if self.cursor_index != 0: 

1420 self.cursor_index -= 1 

1421 elif key == "home": 

1422 self.cursor_index = 0 

1423 elif key == "end": 

1424 self.cursor_index = len(text) 

1425 elif key == "backspace": 

1426 if self.cursor_index != 0: 

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

1428 text[self.cursor_index:]) 

1429 self.cursor_index -= 1 

1430 elif key == "delete": 

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

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

1433 text[self.cursor_index + 1:]) 

1434 self.text_disp.set_text(text) 

1435 self._rendercursor() 

1436 if self.eventson: 

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

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

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

1440 

1441 def set_val(self, val): 

1442 newval = str(val) 

1443 if self.text == newval: 

1444 return 

1445 self.text_disp.set_text(newval) 

1446 self._rendercursor() 

1447 if self.eventson: 

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

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

1450 

1451 def begin_typing(self): 

1452 self.capturekeystrokes = True 

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

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

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

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

1457 self._on_stop_typing = stack.close 

1458 toolmanager = getattr( 

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

1460 if toolmanager is not None: 

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

1462 # lock when typing stops. 

1463 toolmanager.keypresslock(self) 

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

1465 else: 

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

1467 # Avoid spurious warnings if keymaps are getting deprecated. 

1468 with _api.suppress_matplotlib_deprecation_warning(): 

1469 stack.enter_context(mpl.rc_context( 

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

1471 

1472 def stop_typing(self): 

1473 if self.capturekeystrokes: 

1474 self._on_stop_typing() 

1475 self._on_stop_typing = None 

1476 notifysubmit = True 

1477 else: 

1478 notifysubmit = False 

1479 self.capturekeystrokes = False 

1480 self.cursor.set_visible(False) 

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

1482 if notifysubmit and self.eventson: 

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

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

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

1486 

1487 def _click(self, event): 

1488 if self.ignore(event): 

1489 return 

1490 if not self.ax.contains(event)[0]: 

1491 self.stop_typing() 

1492 return 

1493 if not self.eventson: 

1494 return 

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

1496 event.canvas.grab_mouse(self.ax) 

1497 if not self.capturekeystrokes: 

1498 self.begin_typing() 

1499 self.cursor_index = self.text_disp._char_index_at(event.x) 

1500 self._rendercursor() 

1501 

1502 def _resize(self, event): 

1503 self.stop_typing() 

1504 

1505 def _motion(self, event): 

1506 if self.ignore(event): 

1507 return 

1508 c = self.hovercolor if self.ax.contains(event)[0] else self.color 

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

1510 self.ax.set_facecolor(c) 

1511 if self.drawon: 

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

1513 

1514 def on_text_change(self, func): 

1515 """ 

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

1517 

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

1519 """ 

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

1521 

1522 def on_submit(self, func): 

1523 """ 

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

1525 *func* with event. 

1526 

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

1528 """ 

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

1530 

1531 def disconnect(self, cid): 

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

1533 self._observers.disconnect(cid) 

1534 

1535 

1536class RadioButtons(AxesWidget): 

1537 """ 

1538 A GUI neutral radio button. 

1539 

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

1541 object. 

1542 

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

1544 

1545 Attributes 

1546 ---------- 

1547 ax : `~matplotlib.axes.Axes` 

1548 The parent Axes for the widget. 

1549 activecolor : :mpltype:`color` 

1550 The color of the selected button. 

1551 labels : list of `.Text` 

1552 The button labels. 

1553 value_selected : str 

1554 The label text of the currently selected button. 

1555 index_selected : int 

1556 The index of the selected button. 

1557 """ 

1558 

1559 def __init__(self, ax, labels, active=0, activecolor=None, *, 

1560 useblit=True, label_props=None, radio_props=None): 

1561 """ 

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

1563 

1564 Parameters 

1565 ---------- 

1566 ax : `~matplotlib.axes.Axes` 

1567 The Axes to add the buttons to. 

1568 labels : list of str 

1569 The button labels. 

1570 active : int 

1571 The index of the initially selected button. 

1572 activecolor : :mpltype:`color` 

1573 The color of the selected button. The default is ``'blue'`` if not 

1574 specified here or in *radio_props*. 

1575 useblit : bool, default: True 

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

1577 See the tutorial :ref:`blitting` for details. 

1578 

1579 .. versionadded:: 3.7 

1580 

1581 label_props : dict or list of dict, optional 

1582 Dictionary of `.Text` properties to be used for the labels. 

1583 

1584 .. versionadded:: 3.7 

1585 radio_props : dict, optional 

1586 Dictionary of scatter `.Collection` properties to be used for the 

1587 radio buttons. Defaults to (label font size / 2)**2 size, black 

1588 edgecolor, and *activecolor* facecolor (when active). 

1589 

1590 .. note:: 

1591 If a facecolor is supplied in *radio_props*, it will override 

1592 *activecolor*. This may be used to provide an active color per 

1593 button. 

1594 

1595 .. versionadded:: 3.7 

1596 """ 

1597 super().__init__(ax) 

1598 

1599 _api.check_isinstance((dict, None), label_props=label_props, 

1600 radio_props=radio_props) 

1601 

1602 radio_props = cbook.normalize_kwargs(radio_props, 

1603 collections.PathCollection) 

1604 if activecolor is not None: 

1605 if 'facecolor' in radio_props: 

1606 _api.warn_external( 

1607 'Both the *activecolor* parameter and the *facecolor* ' 

1608 'key in the *radio_props* parameter has been specified. ' 

1609 '*activecolor* will be ignored.') 

1610 else: 

1611 activecolor = 'blue' # Default. 

1612 

1613 self._activecolor = activecolor 

1614 self._initial_active = active 

1615 self.value_selected = labels[active] 

1616 self.index_selected = active 

1617 

1618 ax.set_xticks([]) 

1619 ax.set_yticks([]) 

1620 ax.set_navigate(False) 

1621 

1622 ys = np.linspace(1, 0, len(labels) + 2)[1:-1] 

1623 

1624 self._useblit = useblit and self.canvas.supports_blit 

1625 self._background = None 

1626 

1627 label_props = _expand_text_props(label_props) 

1628 self.labels = [ 

1629 ax.text(0.25, y, label, transform=ax.transAxes, 

1630 horizontalalignment="left", verticalalignment="center", 

1631 **props) 

1632 for y, label, props in zip(ys, labels, label_props)] 

1633 text_size = np.array([text.get_fontsize() for text in self.labels]) / 2 

1634 

1635 radio_props = { 

1636 's': text_size**2, 

1637 **radio_props, 

1638 'marker': 'o', 

1639 'transform': ax.transAxes, 

1640 'animated': self._useblit, 

1641 } 

1642 radio_props.setdefault('edgecolor', radio_props.get('color', 'black')) 

1643 radio_props.setdefault('facecolor', 

1644 radio_props.pop('color', activecolor)) 

1645 self._buttons = ax.scatter([.15] * len(ys), ys, **radio_props) 

1646 # The user may have passed custom colours in radio_props, so we need to 

1647 # create the radios, and modify the visibility after getting whatever 

1648 # the user set. 

1649 self._active_colors = self._buttons.get_facecolor() 

1650 if len(self._active_colors) == 1: 

1651 self._active_colors = np.repeat(self._active_colors, len(labels), 

1652 axis=0) 

1653 self._buttons.set_facecolor( 

1654 [activecolor if i == active else "none" 

1655 for i, activecolor in enumerate(self._active_colors)]) 

1656 

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

1658 if self._useblit: 

1659 self.connect_event('draw_event', self._clear) 

1660 

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

1662 

1663 def _clear(self, event): 

1664 """Internal event handler to clear the buttons.""" 

1665 if self.ignore(event) or self.canvas.is_saving(): 

1666 return 

1667 self._background = self.canvas.copy_from_bbox(self.ax.bbox) 

1668 self.ax.draw_artist(self._buttons) 

1669 

1670 def _clicked(self, event): 

1671 if self.ignore(event) or event.button != 1 or not self.ax.contains(event)[0]: 

1672 return 

1673 idxs = [ # Indices of buttons and of texts that contain the event. 

1674 *self._buttons.contains(event)[1]["ind"], 

1675 *[i for i, text in enumerate(self.labels) if text.contains(event)[0]]] 

1676 if idxs: 

1677 coords = self._buttons.get_offset_transform().transform( 

1678 self._buttons.get_offsets()) 

1679 self.set_active( # Closest index, only looking in idxs. 

1680 idxs[(((event.x, event.y) - coords[idxs]) ** 2).sum(-1).argmin()]) 

1681 

1682 def set_label_props(self, props): 

1683 """ 

1684 Set properties of the `.Text` labels. 

1685 

1686 .. versionadded:: 3.7 

1687 

1688 Parameters 

1689 ---------- 

1690 props : dict 

1691 Dictionary of `.Text` properties to be used for the labels. 

1692 """ 

1693 _api.check_isinstance(dict, props=props) 

1694 props = _expand_text_props(props) 

1695 for text, prop in zip(self.labels, props): 

1696 text.update(prop) 

1697 

1698 def set_radio_props(self, props): 

1699 """ 

1700 Set properties of the `.Text` labels. 

1701 

1702 .. versionadded:: 3.7 

1703 

1704 Parameters 

1705 ---------- 

1706 props : dict 

1707 Dictionary of `.Collection` properties to be used for the radio 

1708 buttons. 

1709 """ 

1710 _api.check_isinstance(dict, props=props) 

1711 if 's' in props: # Keep API consistent with constructor. 

1712 props['sizes'] = np.broadcast_to(props.pop('s'), len(self.labels)) 

1713 self._buttons.update(props) 

1714 self._active_colors = self._buttons.get_facecolor() 

1715 if len(self._active_colors) == 1: 

1716 self._active_colors = np.repeat(self._active_colors, 

1717 len(self.labels), axis=0) 

1718 self._buttons.set_facecolor( 

1719 [activecolor if text.get_text() == self.value_selected else "none" 

1720 for text, activecolor in zip(self.labels, self._active_colors)]) 

1721 

1722 @property 

1723 def activecolor(self): 

1724 return self._activecolor 

1725 

1726 @activecolor.setter 

1727 def activecolor(self, activecolor): 

1728 colors._check_color_like(activecolor=activecolor) 

1729 self._activecolor = activecolor 

1730 self.set_radio_props({'facecolor': activecolor}) 

1731 

1732 def set_active(self, index): 

1733 """ 

1734 Select button with number *index*. 

1735 

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

1737 

1738 Parameters 

1739 ---------- 

1740 index : int 

1741 The index of the button to activate. 

1742 

1743 Raises 

1744 ------ 

1745 ValueError 

1746 If the index is invalid. 

1747 """ 

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

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

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

1751 self.index_selected = index 

1752 button_facecolors = self._buttons.get_facecolor() 

1753 button_facecolors[:] = colors.to_rgba("none") 

1754 button_facecolors[index] = colors.to_rgba(self._active_colors[index]) 

1755 self._buttons.set_facecolor(button_facecolors) 

1756 

1757 if self.drawon: 

1758 if self._useblit: 

1759 if self._background is not None: 

1760 self.canvas.restore_region(self._background) 

1761 self.ax.draw_artist(self._buttons) 

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

1763 else: 

1764 self.canvas.draw() 

1765 

1766 if self.eventson: 

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

1768 

1769 def clear(self): 

1770 """Reset the active button to the initially active one.""" 

1771 self.set_active(self._initial_active) 

1772 

1773 def on_clicked(self, func): 

1774 """ 

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

1776 

1777 Parameters 

1778 ---------- 

1779 func : callable 

1780 When the button is clicked, call *func* with button label. 

1781 When all buttons are cleared, call *func* with None. 

1782 The callback func must have the signature:: 

1783 

1784 def func(label: str | None) -> Any 

1785 

1786 Return values may exist, but are ignored. 

1787 

1788 Returns 

1789 ------- 

1790 A connection id, which can be used to disconnect the callback. 

1791 """ 

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

1793 

1794 def disconnect(self, cid): 

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

1796 self._observers.disconnect(cid) 

1797 

1798 

1799class SubplotTool(Widget): 

1800 """ 

1801 A tool to adjust the subplot params of a `.Figure`. 

1802 """ 

1803 

1804 def __init__(self, targetfig, toolfig): 

1805 """ 

1806 Parameters 

1807 ---------- 

1808 targetfig : `~matplotlib.figure.Figure` 

1809 The figure instance to adjust. 

1810 toolfig : `~matplotlib.figure.Figure` 

1811 The figure instance to embed the subplot tool into. 

1812 """ 

1813 

1814 self.figure = toolfig 

1815 self.targetfig = targetfig 

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

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

1818 

1819 self._sliders = [] 

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

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

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

1823 ax.set_navigate(False) 

1824 slider = Slider(ax, name, 0, 1, 

1825 valinit=getattr(targetfig.subplotpars, name)) 

1826 slider.on_changed(self._on_slider_changed) 

1827 self._sliders.append(slider) 

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

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

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

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

1832 self.sliderwspace, self.sliderhspace]: 

1833 slider.closedmax = False 

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

1835 slider.closedmin = False 

1836 

1837 # constraints 

1838 self.sliderleft.slidermax = self.sliderright 

1839 self.sliderright.slidermin = self.sliderleft 

1840 self.sliderbottom.slidermax = self.slidertop 

1841 self.slidertop.slidermin = self.sliderbottom 

1842 

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

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

1845 self.buttonreset.on_clicked(self._on_reset) 

1846 

1847 def _on_slider_changed(self, _): 

1848 self.targetfig.subplots_adjust( 

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

1850 for slider in self._sliders}) 

1851 if self.drawon: 

1852 self.targetfig.canvas.draw() 

1853 

1854 def _on_reset(self, event): 

1855 with ExitStack() as stack: 

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

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

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

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

1860 for slider in self._sliders: 

1861 stack.enter_context( 

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

1863 # Reset the slider to the initial position. 

1864 for slider in self._sliders: 

1865 slider.reset() 

1866 if self.drawon: 

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

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

1869 

1870 

1871class Cursor(AxesWidget): 

1872 """ 

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

1874 

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

1876 

1877 Parameters 

1878 ---------- 

1879 ax : `~matplotlib.axes.Axes` 

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

1881 horizOn : bool, default: True 

1882 Whether to draw the horizontal line. 

1883 vertOn : bool, default: True 

1884 Whether to draw the vertical line. 

1885 useblit : bool, default: False 

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

1887 See the tutorial :ref:`blitting` for details. 

1888 

1889 Other Parameters 

1890 ---------------- 

1891 **lineprops 

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

1893 See also `~.Axes.axhline`. 

1894 

1895 Examples 

1896 -------- 

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

1898 """ 

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

1900 **lineprops): 

1901 super().__init__(ax) 

1902 

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

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

1905 

1906 self.visible = True 

1907 self.horizOn = horizOn 

1908 self.vertOn = vertOn 

1909 self.useblit = useblit and self.canvas.supports_blit 

1910 

1911 if self.useblit: 

1912 lineprops['animated'] = True 

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

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

1915 

1916 self.background = None 

1917 self.needclear = False 

1918 

1919 def clear(self, event): 

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

1921 if self.ignore(event) or self.canvas.is_saving(): 

1922 return 

1923 if self.useblit: 

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

1925 

1926 def onmove(self, event): 

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

1928 if self.ignore(event): 

1929 return 

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

1931 return 

1932 if not self.ax.contains(event)[0]: 

1933 self.linev.set_visible(False) 

1934 self.lineh.set_visible(False) 

1935 if self.needclear: 

1936 self.canvas.draw() 

1937 self.needclear = False 

1938 return 

1939 self.needclear = True 

1940 xdata, ydata = self._get_data_coords(event) 

1941 self.linev.set_xdata((xdata, xdata)) 

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

1943 self.lineh.set_ydata((ydata, ydata)) 

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

1945 if not (self.visible and (self.vertOn or self.horizOn)): 

1946 return 

1947 # Redraw. 

1948 if self.useblit: 

1949 if self.background is not None: 

1950 self.canvas.restore_region(self.background) 

1951 self.ax.draw_artist(self.linev) 

1952 self.ax.draw_artist(self.lineh) 

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

1954 else: 

1955 self.canvas.draw_idle() 

1956 

1957 

1958class MultiCursor(Widget): 

1959 """ 

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

1961 multiple Axes. 

1962 

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

1964 

1965 Parameters 

1966 ---------- 

1967 canvas : object 

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

1969 

1970 axes : list of `~matplotlib.axes.Axes` 

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

1972 

1973 useblit : bool, default: True 

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

1975 See the tutorial :ref:`blitting` 

1976 for details. 

1977 

1978 horizOn : bool, default: False 

1979 Whether to draw the horizontal line. 

1980 

1981 vertOn : bool, default: True 

1982 Whether to draw the vertical line. 

1983 

1984 Other Parameters 

1985 ---------------- 

1986 **lineprops 

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

1988 See also `~.Axes.axhline`. 

1989 

1990 Examples 

1991 -------- 

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

1993 """ 

1994 

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

1996 **lineprops): 

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

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

1999 self._canvas = canvas 

2000 

2001 self.axes = axes 

2002 self.horizOn = horizOn 

2003 self.vertOn = vertOn 

2004 

2005 self._canvas_infos = { 

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

2007 

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

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

2010 xmid = 0.5 * (xmin + xmax) 

2011 ymid = 0.5 * (ymin + ymax) 

2012 

2013 self.visible = True 

2014 self.useblit = ( 

2015 useblit 

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

2017 

2018 if self.useblit: 

2019 lineprops['animated'] = True 

2020 

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

2022 for ax in axes] 

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

2024 for ax in axes] 

2025 

2026 self.connect() 

2027 

2028 def connect(self): 

2029 """Connect events.""" 

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

2031 info["cids"] = [ 

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

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

2034 ] 

2035 

2036 def disconnect(self): 

2037 """Disconnect events.""" 

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

2039 for cid in info["cids"]: 

2040 canvas.mpl_disconnect(cid) 

2041 info["cids"].clear() 

2042 

2043 def clear(self, event): 

2044 """Clear the cursor.""" 

2045 if self.ignore(event): 

2046 return 

2047 if self.useblit: 

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

2049 # someone has switched the canvas on us! This happens if 

2050 # `savefig` needs to save to a format the previous backend did 

2051 # not support (e.g. saving a figure using an Agg based backend 

2052 # saved to a vector format). 

2053 if canvas is not canvas.figure.canvas: 

2054 continue 

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

2056 

2057 def onmove(self, event): 

2058 axs = [ax for ax in self.axes if ax.contains(event)[0]] 

2059 if self.ignore(event) or not axs or not event.canvas.widgetlock.available(self): 

2060 return 

2061 ax = cbook._topmost_artist(axs) 

2062 xdata, ydata = ((event.xdata, event.ydata) if event.inaxes is ax 

2063 else ax.transData.inverted().transform((event.x, event.y))) 

2064 for line in self.vlines: 

2065 line.set_xdata((xdata, xdata)) 

2066 line.set_visible(self.visible and self.vertOn) 

2067 for line in self.hlines: 

2068 line.set_ydata((ydata, ydata)) 

2069 line.set_visible(self.visible and self.horizOn) 

2070 if not (self.visible and (self.vertOn or self.horizOn)): 

2071 return 

2072 # Redraw. 

2073 if self.useblit: 

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

2075 if info["background"]: 

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

2077 if self.vertOn: 

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

2079 ax.draw_artist(line) 

2080 if self.horizOn: 

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

2082 ax.draw_artist(line) 

2083 for canvas in self._canvas_infos: 

2084 canvas.blit() 

2085 else: 

2086 for canvas in self._canvas_infos: 

2087 canvas.draw_idle() 

2088 

2089 

2090class _SelectorWidget(AxesWidget): 

2091 

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

2093 state_modifier_keys=None, use_data_coordinates=False): 

2094 super().__init__(ax) 

2095 

2096 self._visible = True 

2097 self.onselect = onselect 

2098 self.useblit = useblit and self.canvas.supports_blit 

2099 self.connect_default_events() 

2100 

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

2102 square='shift', center='control', 

2103 rotate='r') 

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

2105 self._use_data_coordinates = use_data_coordinates 

2106 

2107 self.background = None 

2108 

2109 if isinstance(button, Integral): 

2110 self.validButtons = [button] 

2111 else: 

2112 self.validButtons = button 

2113 

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

2115 self._selection_completed = False 

2116 

2117 # will save the data (position at mouseclick) 

2118 self._eventpress = None 

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

2120 self._eventrelease = None 

2121 self._prev_event = None 

2122 self._state = set() 

2123 

2124 def set_active(self, active): 

2125 super().set_active(active) 

2126 if active: 

2127 self.update_background(None) 

2128 

2129 def _get_animated_artists(self): 

2130 """ 

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

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

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

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

2135 artists. 

2136 """ 

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

2138 for a in ax_.get_children() 

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

2140 

2141 def update_background(self, event): 

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

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

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

2145 if not self.useblit: 

2146 return 

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

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

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

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

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

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

2153 # zorder needs to be respected when redrawing 

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

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

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

2157 with ExitStack() as stack: 

2158 if needs_redraw: 

2159 for artist in artists: 

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

2161 self.canvas.draw() 

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

2163 if needs_redraw: 

2164 for artist in artists: 

2165 self.ax.draw_artist(artist) 

2166 

2167 def connect_default_events(self): 

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

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

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

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

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

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

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

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

2176 

2177 def ignore(self, event): 

2178 # docstring inherited 

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

2180 return True 

2181 # If canvas was locked 

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

2183 return True 

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

2185 event.button = None 

2186 # Only do rectangle selection if event was triggered 

2187 # with a desired button 

2188 if (self.validButtons is not None 

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

2190 return True 

2191 # If no button was pressed yet ignore the event if it was out of the Axes. 

2192 if self._eventpress is None: 

2193 return not self.ax.contains(event)[0] 

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

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

2196 return False 

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

2198 return (not self.ax.contains(event)[0] or 

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

2200 

2201 def update(self): 

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

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

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

2205 return 

2206 if self.useblit: 

2207 if self.background is not None: 

2208 self.canvas.restore_region(self.background) 

2209 else: 

2210 self.update_background(None) 

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

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

2213 # and we make sure that we respect z_order 

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

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

2216 for artist in artists: 

2217 self.ax.draw_artist(artist) 

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

2219 else: 

2220 self.canvas.draw_idle() 

2221 

2222 def _get_data(self, event): 

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

2224 if event.xdata is None: 

2225 return None, None 

2226 xdata, ydata = self._get_data_coords(event) 

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

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

2229 return xdata, ydata 

2230 

2231 def _clean_event(self, event): 

2232 """ 

2233 Preprocess an event: 

2234 

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

2236 - Get ``xdata`` and ``ydata`` from this widget's Axes, and clip them to the axes 

2237 limits. 

2238 - Update the previous event. 

2239 """ 

2240 if event.xdata is None: 

2241 event = self._prev_event 

2242 else: 

2243 event = copy.copy(event) 

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

2245 self._prev_event = event 

2246 return event 

2247 

2248 def press(self, event): 

2249 """Button press handler and validator.""" 

2250 if not self.ignore(event): 

2251 event = self._clean_event(event) 

2252 self._eventpress = event 

2253 self._prev_event = event 

2254 key = event.key or '' 

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

2256 # move state is locked in on a button press 

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

2258 self._state.add('move') 

2259 self._press(event) 

2260 return True 

2261 return False 

2262 

2263 def _press(self, event): 

2264 """Button press event handler.""" 

2265 

2266 def release(self, event): 

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

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

2269 event = self._clean_event(event) 

2270 self._eventrelease = event 

2271 self._release(event) 

2272 self._eventpress = None 

2273 self._eventrelease = None 

2274 self._state.discard('move') 

2275 return True 

2276 return False 

2277 

2278 def _release(self, event): 

2279 """Button release event handler.""" 

2280 

2281 def onmove(self, event): 

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

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

2284 event = self._clean_event(event) 

2285 self._onmove(event) 

2286 return True 

2287 return False 

2288 

2289 def _onmove(self, event): 

2290 """Cursor move event handler.""" 

2291 

2292 def on_scroll(self, event): 

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

2294 if not self.ignore(event): 

2295 self._on_scroll(event) 

2296 

2297 def _on_scroll(self, event): 

2298 """Mouse scroll event handler.""" 

2299 

2300 def on_key_press(self, event): 

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

2302 if self.active: 

2303 key = event.key or '' 

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

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

2306 self.clear() 

2307 return 

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

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

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

2311 # from _state when releasing 

2312 if state == 'rotate': 

2313 if state in self._state: 

2314 self._state.discard(state) 

2315 else: 

2316 self._state.add(state) 

2317 else: 

2318 self._state.add(state) 

2319 self._on_key_press(event) 

2320 

2321 def _on_key_press(self, event): 

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

2323 

2324 def on_key_release(self, event): 

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

2326 if self.active: 

2327 key = event.key or '' 

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

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

2330 # from _state when releasing 

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

2332 self._state.discard(state) 

2333 self._on_key_release(event) 

2334 

2335 def _on_key_release(self, event): 

2336 """Key release event handler.""" 

2337 

2338 def set_visible(self, visible): 

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

2340 self._visible = visible 

2341 for artist in self.artists: 

2342 artist.set_visible(visible) 

2343 

2344 def get_visible(self): 

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

2346 return self._visible 

2347 

2348 @property 

2349 def visible(self): 

2350 _api.warn_deprecated("3.8", alternative="get_visible") 

2351 return self.get_visible() 

2352 

2353 def clear(self): 

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

2355 self._clear_without_update() 

2356 self.update() 

2357 

2358 def _clear_without_update(self): 

2359 self._selection_completed = False 

2360 self.set_visible(False) 

2361 

2362 @property 

2363 def artists(self): 

2364 """Tuple of the artists of the selector.""" 

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

2366 return (self._selection_artist,) + handles_artists 

2367 

2368 def set_props(self, **props): 

2369 """ 

2370 Set the properties of the selector artist. 

2371 

2372 See the *props* argument in the selector docstring to know which properties are 

2373 supported. 

2374 """ 

2375 artist = self._selection_artist 

2376 props = cbook.normalize_kwargs(props, artist) 

2377 artist.set(**props) 

2378 if self.useblit: 

2379 self.update() 

2380 

2381 def set_handle_props(self, **handle_props): 

2382 """ 

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

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

2385 properties are supported. 

2386 """ 

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

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

2389 

2390 artist = self._handles_artists[0] 

2391 handle_props = cbook.normalize_kwargs(handle_props, artist) 

2392 for handle in self._handles_artists: 

2393 handle.set(**handle_props) 

2394 if self.useblit: 

2395 self.update() 

2396 self._handle_props.update(handle_props) 

2397 

2398 def _validate_state(self, state): 

2399 supported_state = [ 

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

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

2402 ] 

2403 _api.check_in_list(supported_state, state=state) 

2404 

2405 def add_state(self, state): 

2406 """ 

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

2408 `state_modifier_keys` parameters for details. 

2409 

2410 Parameters 

2411 ---------- 

2412 state : str 

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

2414 `state_modifier_keys` parameters for details. 

2415 

2416 Raises 

2417 ------ 

2418 ValueError 

2419 When the state is not supported by the selector. 

2420 

2421 """ 

2422 self._validate_state(state) 

2423 self._state.add(state) 

2424 

2425 def remove_state(self, state): 

2426 """ 

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

2428 `state_modifier_keys` parameters for details. 

2429 

2430 Parameters 

2431 ---------- 

2432 state : str 

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

2434 `state_modifier_keys` parameters for details. 

2435 

2436 Raises 

2437 ------ 

2438 ValueError 

2439 When the state is not supported by the selector. 

2440 

2441 """ 

2442 self._validate_state(state) 

2443 self._state.remove(state) 

2444 

2445 

2446class SpanSelector(_SelectorWidget): 

2447 """ 

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

2449 those values. 

2450 

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

2452 

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

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

2455 

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

2457 selection will clear the selector, except when 

2458 ``ignore_event_outside=True``. 

2459 

2460 Parameters 

2461 ---------- 

2462 ax : `~matplotlib.axes.Axes` 

2463 

2464 onselect : callable with signature ``func(min: float, max: float)`` 

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

2466 selection is created, changed or removed. 

2467 

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

2469 The direction along which to draw the span selector. 

2470 

2471 minspan : float, default: 0 

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

2473 removed (when already existing) or cancelled. 

2474 

2475 useblit : bool, default: False 

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

2477 canvas updates. See the tutorial :ref:`blitting` for details. 

2478 

2479 props : dict, default: {'facecolor': 'red', 'alpha': 0.5} 

2480 Dictionary of `.Patch` properties. 

2481 

2482 onmove_callback : callable with signature ``func(min: float, max: float)``, optional 

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

2484 

2485 interactive : bool, default: False 

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

2487 widget after it is drawn. 

2488 

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

2490 The mouse buttons which activate the span selector. 

2491 

2492 handle_props : dict, default: None 

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

2494 when *interactive* is True. See `.Line2D` for valid properties. 

2495 

2496 grab_range : float, default: 10 

2497 Distance in pixels within which the interactive tool handles can be activated. 

2498 

2499 state_modifier_keys : dict, optional 

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

2501 amend the defaults, which are: 

2502 

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

2504 

2505 drag_from_anywhere : bool, default: False 

2506 If `True`, the widget can be moved by clicking anywhere within its bounds. 

2507 

2508 ignore_event_outside : bool, default: False 

2509 If `True`, the event triggered outside the span selector will be ignored. 

2510 

2511 snap_values : 1D array-like, optional 

2512 Snap the selector edges to the given values. 

2513 

2514 Examples 

2515 -------- 

2516 >>> import matplotlib.pyplot as plt 

2517 >>> import matplotlib.widgets as mwidgets 

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

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

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

2521 ... print(vmin, vmax) 

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

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

2524 >>> fig.show() 

2525 

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

2527 """ 

2528 

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

2530 props=None, onmove_callback=None, interactive=False, 

2531 button=None, handle_props=None, grab_range=10, 

2532 state_modifier_keys=None, drag_from_anywhere=False, 

2533 ignore_event_outside=False, snap_values=None): 

2534 

2535 if state_modifier_keys is None: 

2536 state_modifier_keys = dict(clear='escape', 

2537 square='not-applicable', 

2538 center='not-applicable', 

2539 rotate='not-applicable') 

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

2541 state_modifier_keys=state_modifier_keys) 

2542 

2543 if props is None: 

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

2545 

2546 props['animated'] = self.useblit 

2547 

2548 self.direction = direction 

2549 self._extents_on_press = None 

2550 self.snap_values = snap_values 

2551 

2552 self.onmove_callback = onmove_callback 

2553 self.minspan = minspan 

2554 

2555 self.grab_range = grab_range 

2556 self._interactive = interactive 

2557 self._edge_handles = None 

2558 self.drag_from_anywhere = drag_from_anywhere 

2559 self.ignore_event_outside = ignore_event_outside 

2560 

2561 self.new_axes(ax, _props=props, _init=True) 

2562 

2563 # Setup handles 

2564 self._handle_props = { 

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

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

2567 

2568 if self._interactive: 

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

2570 self._setup_edge_handles(self._handle_props) 

2571 

2572 self._active_handle = None 

2573 

2574 def new_axes(self, ax, *, _props=None, _init=False): 

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

2576 reconnect = False 

2577 if _init or self.canvas is not ax.figure.canvas: 

2578 if self.canvas is not None: 

2579 self.disconnect_events() 

2580 reconnect = True 

2581 self.ax = ax 

2582 if reconnect: 

2583 self.connect_default_events() 

2584 

2585 # Reset 

2586 self._selection_completed = False 

2587 

2588 if self.direction == 'horizontal': 

2589 trans = ax.get_xaxis_transform() 

2590 w, h = 0, 1 

2591 else: 

2592 trans = ax.get_yaxis_transform() 

2593 w, h = 1, 0 

2594 rect_artist = Rectangle((0, 0), w, h, transform=trans, visible=False) 

2595 if _props is not None: 

2596 rect_artist.update(_props) 

2597 elif self._selection_artist is not None: 

2598 rect_artist.update_from(self._selection_artist) 

2599 

2600 self.ax.add_patch(rect_artist) 

2601 self._selection_artist = rect_artist 

2602 

2603 def _setup_edge_handles(self, props): 

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

2605 if self.direction == 'horizontal': 

2606 positions = self.ax.get_xbound() 

2607 else: 

2608 positions = self.ax.get_ybound() 

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

2610 direction=self.direction, 

2611 line_props=props, 

2612 useblit=self.useblit) 

2613 

2614 @property 

2615 def _handles_artists(self): 

2616 if self._edge_handles is not None: 

2617 return self._edge_handles.artists 

2618 else: 

2619 return () 

2620 

2621 def _set_cursor(self, enabled): 

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

2623 if enabled: 

2624 cursor = (backend_tools.Cursors.RESIZE_HORIZONTAL 

2625 if self.direction == 'horizontal' else 

2626 backend_tools.Cursors.RESIZE_VERTICAL) 

2627 else: 

2628 cursor = backend_tools.Cursors.POINTER 

2629 

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

2631 

2632 def connect_default_events(self): 

2633 # docstring inherited 

2634 super().connect_default_events() 

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

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

2637 

2638 def _press(self, event): 

2639 """Button press event handler.""" 

2640 self._set_cursor(True) 

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

2642 self._set_active_handle(event) 

2643 else: 

2644 self._active_handle = None 

2645 

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

2647 # Clear previous rectangle before drawing new rectangle. 

2648 self.update() 

2649 

2650 xdata, ydata = self._get_data_coords(event) 

2651 v = xdata if self.direction == 'horizontal' else ydata 

2652 

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

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

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

2656 # update will be called when setting the extents 

2657 self._visible = False 

2658 self._set_extents((v, v)) 

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

2660 # drawn when necessary (span width > 0) 

2661 self._visible = True 

2662 else: 

2663 self.set_visible(True) 

2664 

2665 return False 

2666 

2667 @property 

2668 def direction(self): 

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

2670 return self._direction 

2671 

2672 @direction.setter 

2673 def direction(self, direction): 

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

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

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

2677 # remove previous artists 

2678 self._selection_artist.remove() 

2679 if self._interactive: 

2680 self._edge_handles.remove() 

2681 self._direction = direction 

2682 self.new_axes(self.ax) 

2683 if self._interactive: 

2684 self._setup_edge_handles(self._handle_props) 

2685 else: 

2686 self._direction = direction 

2687 

2688 def _release(self, event): 

2689 """Button release event handler.""" 

2690 self._set_cursor(False) 

2691 

2692 if not self._interactive: 

2693 self._selection_artist.set_visible(False) 

2694 

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

2696 self.ignore_event_outside): 

2697 return 

2698 

2699 vmin, vmax = self.extents 

2700 span = vmax - vmin 

2701 

2702 if span <= self.minspan: 

2703 # Remove span and set self._selection_completed = False 

2704 self.set_visible(False) 

2705 if self._selection_completed: 

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

2707 self.onselect(vmin, vmax) 

2708 self._selection_completed = False 

2709 else: 

2710 self.onselect(vmin, vmax) 

2711 self._selection_completed = True 

2712 

2713 self.update() 

2714 

2715 self._active_handle = None 

2716 

2717 return False 

2718 

2719 def _hover(self, event): 

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

2721 if self.ignore(event): 

2722 return 

2723 

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

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

2726 # occur with drag_from_anywhere=True. 

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

2728 # a selector has been cleared 

2729 return 

2730 

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

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

2733 

2734 def _onmove(self, event): 

2735 """Motion notify event handler.""" 

2736 

2737 xdata, ydata = self._get_data_coords(event) 

2738 if self.direction == 'horizontal': 

2739 v = xdata 

2740 vpress = self._eventpress.xdata 

2741 else: 

2742 v = ydata 

2743 vpress = self._eventpress.ydata 

2744 

2745 # move existing span 

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

2747 # (match notation used in the RectangleSelector) 

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

2749 vmin, vmax = self._extents_on_press 

2750 dv = v - vpress 

2751 vmin += dv 

2752 vmax += dv 

2753 

2754 # resize an existing shape 

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

2756 vmin, vmax = self._extents_on_press 

2757 if self._active_handle == 'min': 

2758 vmin = v 

2759 else: 

2760 vmax = v 

2761 # new shape 

2762 else: 

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

2764 # ignore_event_outside=True 

2765 if self.ignore_event_outside and self._selection_completed: 

2766 return 

2767 vmin, vmax = vpress, v 

2768 if vmin > vmax: 

2769 vmin, vmax = vmax, vmin 

2770 

2771 self._set_extents((vmin, vmax)) 

2772 

2773 if self.onmove_callback is not None: 

2774 self.onmove_callback(vmin, vmax) 

2775 

2776 return False 

2777 

2778 def _draw_shape(self, vmin, vmax): 

2779 if vmin > vmax: 

2780 vmin, vmax = vmax, vmin 

2781 if self.direction == 'horizontal': 

2782 self._selection_artist.set_x(vmin) 

2783 self._selection_artist.set_width(vmax - vmin) 

2784 else: 

2785 self._selection_artist.set_y(vmin) 

2786 self._selection_artist.set_height(vmax - vmin) 

2787 

2788 def _set_active_handle(self, event): 

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

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

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

2792 

2793 # Prioritise center handle over other handles 

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

2795 if 'move' in self._state: 

2796 self._active_handle = 'C' 

2797 elif e_dist > self.grab_range: 

2798 # Not close to any handles 

2799 self._active_handle = None 

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

2801 # Check if we've clicked inside the region 

2802 self._active_handle = 'C' 

2803 self._extents_on_press = self.extents 

2804 else: 

2805 self._active_handle = None 

2806 return 

2807 else: 

2808 # Closest to an edge handle 

2809 self._active_handle = self._edge_order[e_idx] 

2810 

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

2812 self._extents_on_press = self.extents 

2813 

2814 def _contains(self, event): 

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

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

2817 

2818 @staticmethod 

2819 def _snap(values, snap_values): 

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

2821 # take into account machine precision 

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

2823 return tuple( 

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

2825 for v in values) 

2826 

2827 @property 

2828 def extents(self): 

2829 """ 

2830 (float, float) 

2831 The values, in data coordinates, for the start and end points of the current 

2832 selection. If there is no selection then the start and end values will be 

2833 the same. 

2834 """ 

2835 if self.direction == 'horizontal': 

2836 vmin = self._selection_artist.get_x() 

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

2838 else: 

2839 vmin = self._selection_artist.get_y() 

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

2841 return vmin, vmax 

2842 

2843 @extents.setter 

2844 def extents(self, extents): 

2845 self._set_extents(extents) 

2846 self._selection_completed = True 

2847 

2848 def _set_extents(self, extents): 

2849 # Update displayed shape 

2850 if self.snap_values is not None: 

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

2852 self._draw_shape(*extents) 

2853 if self._interactive: 

2854 # Update displayed handles 

2855 self._edge_handles.set_data(self.extents) 

2856 self.set_visible(self._visible) 

2857 self.update() 

2858 

2859 

2860class ToolLineHandles: 

2861 """ 

2862 Control handles for canvas tools. 

2863 

2864 Parameters 

2865 ---------- 

2866 ax : `~matplotlib.axes.Axes` 

2867 Matplotlib Axes where tool handles are displayed. 

2868 positions : 1D array 

2869 Positions of handles in data coordinates. 

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

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

2872 line_props : dict, optional 

2873 Additional line properties. See `.Line2D`. 

2874 useblit : bool, default: True 

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

2876 backend). See the tutorial :ref:`blitting` 

2877 for details. 

2878 """ 

2879 

2880 def __init__(self, ax, positions, direction, *, line_props=None, 

2881 useblit=True): 

2882 self.ax = ax 

2883 

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

2885 self._direction = direction 

2886 

2887 line_props = { 

2888 **(line_props if line_props is not None else {}), 

2889 'visible': False, 

2890 'animated': useblit, 

2891 } 

2892 

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

2894 

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

2896 

2897 @property 

2898 def artists(self): 

2899 return tuple(self._artists) 

2900 

2901 @property 

2902 def positions(self): 

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

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

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

2906 

2907 @property 

2908 def direction(self): 

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

2910 return self._direction 

2911 

2912 def set_data(self, positions): 

2913 """ 

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

2915 vertical or horizontal. 

2916 

2917 Parameters 

2918 ---------- 

2919 positions : tuple of length 2 

2920 Set the positions of the handle in data coordinates 

2921 """ 

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

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

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

2925 

2926 def set_visible(self, value): 

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

2928 for artist in self.artists: 

2929 artist.set_visible(value) 

2930 

2931 def set_animated(self, value): 

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

2933 for artist in self.artists: 

2934 artist.set_animated(value) 

2935 

2936 def remove(self): 

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

2938 for artist in self._artists: 

2939 artist.remove() 

2940 

2941 def closest(self, x, y): 

2942 """ 

2943 Return index and pixel distance to closest handle. 

2944 

2945 Parameters 

2946 ---------- 

2947 x, y : float 

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

2949 determinate the closest handle 

2950 

2951 Returns 

2952 ------- 

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

2954 position x, y 

2955 """ 

2956 if self.direction == 'horizontal': 

2957 p_pts = np.array([ 

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

2959 ]) 

2960 dist = abs(p_pts - x) 

2961 else: 

2962 p_pts = np.array([ 

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

2964 ]) 

2965 dist = abs(p_pts - y) 

2966 index = np.argmin(dist) 

2967 return index, dist[index] 

2968 

2969 

2970class ToolHandles: 

2971 """ 

2972 Control handles for canvas tools. 

2973 

2974 Parameters 

2975 ---------- 

2976 ax : `~matplotlib.axes.Axes` 

2977 Matplotlib Axes where tool handles are displayed. 

2978 x, y : 1D arrays 

2979 Coordinates of control handles. 

2980 marker : str, default: 'o' 

2981 Shape of marker used to display handle. See `~.pyplot.plot`. 

2982 marker_props : dict, optional 

2983 Additional marker properties. See `.Line2D`. 

2984 useblit : bool, default: True 

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

2986 backend). See the tutorial :ref:`blitting` 

2987 for details. 

2988 """ 

2989 

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

2991 self.ax = ax 

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

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

2994 'label': '_nolegend_', 

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

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

2997 self.ax.add_line(self._markers) 

2998 

2999 @property 

3000 def x(self): 

3001 return self._markers.get_xdata() 

3002 

3003 @property 

3004 def y(self): 

3005 return self._markers.get_ydata() 

3006 

3007 @property 

3008 def artists(self): 

3009 return (self._markers, ) 

3010 

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

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

3013 if y is not None: 

3014 x = pts 

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

3016 self._markers.set_data(pts) 

3017 

3018 def set_visible(self, val): 

3019 self._markers.set_visible(val) 

3020 

3021 def set_animated(self, val): 

3022 self._markers.set_animated(val) 

3023 

3024 def closest(self, x, y): 

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

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

3027 # Transform data coordinates to pixel coordinates. 

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

3029 diff = pts - [x, y] 

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

3031 min_index = np.argmin(dist) 

3032 return min_index, dist[min_index] 

3033 

3034 

3035_RECTANGLESELECTOR_PARAMETERS_DOCSTRING = \ 

3036 r""" 

3037 Parameters 

3038 ---------- 

3039 ax : `~matplotlib.axes.Axes` 

3040 The parent Axes for the widget. 

3041 

3042 onselect : function 

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

3044 selection is created, changed or removed. 

3045 It must have the signature:: 

3046 

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

3048 

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

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

3051 

3052 minspanx : float, default: 0 

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

3054 (when already existing) or cancelled. 

3055 

3056 minspany : float, default: 0 

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

3058 (when already existing) or cancelled. 

3059 

3060 useblit : bool, default: False 

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

3062 backend). See the tutorial :ref:`blitting` 

3063 for details. 

3064 

3065 props : dict, optional 

3066 Properties with which the __ARTIST_NAME__ is drawn. See 

3067 `.Patch` for valid properties. 

3068 Default: 

3069 

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

3071 

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

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

3074 coordinates. 

3075 

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

3077 Button(s) that trigger rectangle selection. 

3078 

3079 grab_range : float, default: 10 

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

3081 activated. 

3082 

3083 handle_props : dict, optional 

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

3085 drawn. See the marker arguments in `.Line2D` for valid 

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

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

3088 ``edgecolor`` property in *props*. 

3089 

3090 interactive : bool, default: False 

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

3092 widget after it is drawn. 

3093 

3094 state_modifier_keys : dict, optional 

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

3096 amend the defaults, which are: 

3097 

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

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

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

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

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

3103 default: "r". 

3104 

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

3106 in data or display coordinates as determined by the 

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

3108 

3109 drag_from_anywhere : bool, default: False 

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

3111 its bounds. 

3112 

3113 ignore_event_outside : bool, default: False 

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

3115 ignored. 

3116 

3117 use_data_coordinates : bool, default: False 

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

3119 data coordinates instead of display coordinates. 

3120 """ 

3121 

3122 

3123@_docstring.Substitution(_RECTANGLESELECTOR_PARAMETERS_DOCSTRING.replace( 

3124 '__ARTIST_NAME__', 'rectangle')) 

3125class RectangleSelector(_SelectorWidget): 

3126 """ 

3127 Select a rectangular region of an Axes. 

3128 

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

3130 

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

3132 selection will clear the selector, except when 

3133 ``ignore_event_outside=True``. 

3134 

3135 %s 

3136 

3137 Examples 

3138 -------- 

3139 >>> import matplotlib.pyplot as plt 

3140 >>> import matplotlib.widgets as mwidgets 

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

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

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

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

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

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

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

3148 ... props=props) 

3149 >>> fig.show() 

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

3151 

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

3153 """ 

3154 

3155 def __init__(self, ax, onselect, *, minspanx=0, minspany=0, useblit=False, 

3156 props=None, spancoords='data', button=None, grab_range=10, 

3157 handle_props=None, interactive=False, 

3158 state_modifier_keys=None, drag_from_anywhere=False, 

3159 ignore_event_outside=False, use_data_coordinates=False): 

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

3161 state_modifier_keys=state_modifier_keys, 

3162 use_data_coordinates=use_data_coordinates) 

3163 

3164 self._interactive = interactive 

3165 self.drag_from_anywhere = drag_from_anywhere 

3166 self.ignore_event_outside = ignore_event_outside 

3167 self._rotation = 0.0 

3168 self._aspect_ratio_correction = 1.0 

3169 

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

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

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

3173 self._allow_creation = True 

3174 

3175 if props is None: 

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

3177 alpha=0.2, fill=True) 

3178 props = {**props, 'animated': self.useblit} 

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

3180 to_draw = self._init_shape(**props) 

3181 self.ax.add_patch(to_draw) 

3182 

3183 self._selection_artist = to_draw 

3184 self._set_aspect_ratio_correction() 

3185 

3186 self.minspanx = minspanx 

3187 self.minspany = minspany 

3188 

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

3190 self.spancoords = spancoords 

3191 

3192 self.grab_range = grab_range 

3193 

3194 if self._interactive: 

3195 self._handle_props = { 

3196 'markeredgecolor': (props or {}).get('edgecolor', 'black'), 

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

3198 

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

3200 xc, yc = self.corners 

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

3202 marker_props=self._handle_props, 

3203 useblit=self.useblit) 

3204 

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

3206 xe, ye = self.edge_centers 

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

3208 marker_props=self._handle_props, 

3209 useblit=self.useblit) 

3210 

3211 xc, yc = self.center 

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

3213 marker_props=self._handle_props, 

3214 useblit=self.useblit) 

3215 

3216 self._active_handle = None 

3217 

3218 self._extents_on_press = None 

3219 

3220 @property 

3221 def _handles_artists(self): 

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

3223 *self._edge_handles.artists) 

3224 

3225 def _init_shape(self, **props): 

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

3227 rotation_point='center', **props) 

3228 

3229 def _press(self, event): 

3230 """Button press event handler.""" 

3231 # make the drawn box/line visible get the click-coordinates, button, ... 

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

3233 self._set_active_handle(event) 

3234 else: 

3235 self._active_handle = None 

3236 

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

3238 self._allow_creation): 

3239 # Clear previous rectangle before drawing new rectangle. 

3240 self.update() 

3241 

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

3243 self._allow_creation): 

3244 x, y = self._get_data_coords(event) 

3245 self._visible = False 

3246 self.extents = x, x, y, y 

3247 self._visible = True 

3248 else: 

3249 self.set_visible(True) 

3250 

3251 self._extents_on_press = self.extents 

3252 self._rotation_on_press = self._rotation 

3253 self._set_aspect_ratio_correction() 

3254 

3255 return False 

3256 

3257 def _release(self, event): 

3258 """Button release event handler.""" 

3259 if not self._interactive: 

3260 self._selection_artist.set_visible(False) 

3261 

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

3263 self.ignore_event_outside): 

3264 return 

3265 

3266 # update the eventpress and eventrelease with the resulting extents 

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

3268 self._eventpress.xdata = x0 

3269 self._eventpress.ydata = y0 

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

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

3272 

3273 self._eventrelease.xdata = x1 

3274 self._eventrelease.ydata = y1 

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

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

3277 

3278 # calculate dimensions of box or line 

3279 if self.spancoords == 'data': 

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

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

3282 elif self.spancoords == 'pixels': 

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

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

3285 else: 

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

3287 spancoords=self.spancoords) 

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

3289 # either x or y-direction 

3290 if spanx <= self.minspanx or spany <= self.minspany: 

3291 if self._selection_completed: 

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

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

3294 self._clear_without_update() 

3295 else: 

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

3297 self._selection_completed = True 

3298 

3299 self.update() 

3300 self._active_handle = None 

3301 self._extents_on_press = None 

3302 

3303 return False 

3304 

3305 def _onmove(self, event): 

3306 """ 

3307 Motion notify event handler. 

3308 

3309 This can do one of four things: 

3310 - Translate 

3311 - Rotate 

3312 - Re-size 

3313 - Continue the creation of a new shape 

3314 """ 

3315 eventpress = self._eventpress 

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

3317 # transformation to events except when we rotate and move 

3318 state = self._state 

3319 rotate = 'rotate' in state and self._active_handle in self._corner_order 

3320 move = self._active_handle == 'C' 

3321 resize = self._active_handle and not move 

3322 

3323 xdata, ydata = self._get_data_coords(event) 

3324 if resize: 

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

3326 xdata, ydata = inv_tr.transform([xdata, ydata]) 

3327 eventpress.xdata, eventpress.ydata = inv_tr.transform( 

3328 (eventpress.xdata, eventpress.ydata)) 

3329 

3330 dx = xdata - eventpress.xdata 

3331 dy = ydata - eventpress.ydata 

3332 # refmax is used when moving the corner handle with the square state 

3333 # and is the maximum between refx and refy 

3334 refmax = None 

3335 if self._use_data_coordinates: 

3336 refx, refy = dx, dy 

3337 else: 

3338 # Get dx/dy in display coordinates 

3339 refx = event.x - eventpress.x 

3340 refy = event.y - eventpress.y 

3341 

3342 x0, x1, y0, y1 = self._extents_on_press 

3343 # rotate an existing shape 

3344 if rotate: 

3345 # calculate angle abc 

3346 a = (eventpress.xdata, eventpress.ydata) 

3347 b = self.center 

3348 c = (xdata, ydata) 

3349 angle = (np.arctan2(c[1]-b[1], c[0]-b[0]) - 

3350 np.arctan2(a[1]-b[1], a[0]-b[0])) 

3351 self.rotation = np.rad2deg(self._rotation_on_press + angle) 

3352 

3353 elif resize: 

3354 size_on_press = [x1 - x0, y1 - y0] 

3355 center = (x0 + size_on_press[0] / 2, y0 + size_on_press[1] / 2) 

3356 

3357 # Keeping the center fixed 

3358 if 'center' in state: 

3359 # hh, hw are half-height and half-width 

3360 if 'square' in state: 

3361 # when using a corner, find which reference to use 

3362 if self._active_handle in self._corner_order: 

3363 refmax = max(refx, refy, key=abs) 

3364 if self._active_handle in ['E', 'W'] or refmax == refx: 

3365 hw = xdata - center[0] 

3366 hh = hw / self._aspect_ratio_correction 

3367 else: 

3368 hh = ydata - center[1] 

3369 hw = hh * self._aspect_ratio_correction 

3370 else: 

3371 hw = size_on_press[0] / 2 

3372 hh = size_on_press[1] / 2 

3373 # cancel changes in perpendicular direction 

3374 if self._active_handle in ['E', 'W'] + self._corner_order: 

3375 hw = abs(xdata - center[0]) 

3376 if self._active_handle in ['N', 'S'] + self._corner_order: 

3377 hh = abs(ydata - center[1]) 

3378 

3379 x0, x1, y0, y1 = (center[0] - hw, center[0] + hw, 

3380 center[1] - hh, center[1] + hh) 

3381 

3382 else: 

3383 # change sign of relative changes to simplify calculation 

3384 # Switch variables so that x1 and/or y1 are updated on move 

3385 if 'W' in self._active_handle: 

3386 x0 = x1 

3387 if 'S' in self._active_handle: 

3388 y0 = y1 

3389 if self._active_handle in ['E', 'W'] + self._corner_order: 

3390 x1 = xdata 

3391 if self._active_handle in ['N', 'S'] + self._corner_order: 

3392 y1 = ydata 

3393 if 'square' in state: 

3394 # when using a corner, find which reference to use 

3395 if self._active_handle in self._corner_order: 

3396 refmax = max(refx, refy, key=abs) 

3397 if self._active_handle in ['E', 'W'] or refmax == refx: 

3398 sign = np.sign(ydata - y0) 

3399 y1 = y0 + sign * abs(x1 - x0) / self._aspect_ratio_correction 

3400 else: 

3401 sign = np.sign(xdata - x0) 

3402 x1 = x0 + sign * abs(y1 - y0) * self._aspect_ratio_correction 

3403 

3404 elif move: 

3405 x0, x1, y0, y1 = self._extents_on_press 

3406 dx = xdata - eventpress.xdata 

3407 dy = ydata - eventpress.ydata 

3408 x0 += dx 

3409 x1 += dx 

3410 y0 += dy 

3411 y1 += dy 

3412 

3413 else: 

3414 # Create a new shape 

3415 self._rotation = 0 

3416 # Don't create a new rectangle if there is already one when 

3417 # ignore_event_outside=True 

3418 if ((self.ignore_event_outside and self._selection_completed) or 

3419 not self._allow_creation): 

3420 return 

3421 center = [eventpress.xdata, eventpress.ydata] 

3422 dx = (xdata - center[0]) / 2 

3423 dy = (ydata - center[1]) / 2 

3424 

3425 # square shape 

3426 if 'square' in state: 

3427 refmax = max(refx, refy, key=abs) 

3428 if refmax == refx: 

3429 dy = np.sign(dy) * abs(dx) / self._aspect_ratio_correction 

3430 else: 

3431 dx = np.sign(dx) * abs(dy) * self._aspect_ratio_correction 

3432 

3433 # from center 

3434 if 'center' in state: 

3435 dx *= 2 

3436 dy *= 2 

3437 

3438 # from corner 

3439 else: 

3440 center[0] += dx 

3441 center[1] += dy 

3442 

3443 x0, x1, y0, y1 = (center[0] - dx, center[0] + dx, 

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

3445 

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

3447 

3448 @property 

3449 def _rect_bbox(self): 

3450 return self._selection_artist.get_bbox().bounds 

3451 

3452 def _set_aspect_ratio_correction(self): 

3453 aspect_ratio = self.ax._get_aspect_ratio() 

3454 self._selection_artist._aspect_ratio_correction = aspect_ratio 

3455 if self._use_data_coordinates: 

3456 self._aspect_ratio_correction = 1 

3457 else: 

3458 self._aspect_ratio_correction = aspect_ratio 

3459 

3460 def _get_rotation_transform(self): 

3461 aspect_ratio = self.ax._get_aspect_ratio() 

3462 return Affine2D().translate(-self.center[0], -self.center[1]) \ 

3463 .scale(1, aspect_ratio) \ 

3464 .rotate(self._rotation) \ 

3465 .scale(1, 1 / aspect_ratio) \ 

3466 .translate(*self.center) 

3467 

3468 @property 

3469 def corners(self): 

3470 """ 

3471 Corners of rectangle in data coordinates from lower left, 

3472 moving clockwise. 

3473 """ 

3474 x0, y0, width, height = self._rect_bbox 

3475 xc = x0, x0 + width, x0 + width, x0 

3476 yc = y0, y0, y0 + height, y0 + height 

3477 transform = self._get_rotation_transform() 

3478 coords = transform.transform(np.array([xc, yc]).T).T 

3479 return coords[0], coords[1] 

3480 

3481 @property 

3482 def edge_centers(self): 

3483 """ 

3484 Midpoint of rectangle edges in data coordinates from left, 

3485 moving anti-clockwise. 

3486 """ 

3487 x0, y0, width, height = self._rect_bbox 

3488 w = width / 2. 

3489 h = height / 2. 

3490 xe = x0, x0 + w, x0 + width, x0 + w 

3491 ye = y0 + h, y0, y0 + h, y0 + height 

3492 transform = self._get_rotation_transform() 

3493 coords = transform.transform(np.array([xe, ye]).T).T 

3494 return coords[0], coords[1] 

3495 

3496 @property 

3497 def center(self): 

3498 """Center of rectangle in data coordinates.""" 

3499 x0, y0, width, height = self._rect_bbox 

3500 return x0 + width / 2., y0 + height / 2. 

3501 

3502 @property 

3503 def extents(self): 

3504 """ 

3505 Return (xmin, xmax, ymin, ymax) in data coordinates as defined by the 

3506 bounding box before rotation. 

3507 """ 

3508 x0, y0, width, height = self._rect_bbox 

3509 xmin, xmax = sorted([x0, x0 + width]) 

3510 ymin, ymax = sorted([y0, y0 + height]) 

3511 return xmin, xmax, ymin, ymax 

3512 

3513 @extents.setter 

3514 def extents(self, extents): 

3515 # Update displayed shape 

3516 self._draw_shape(extents) 

3517 if self._interactive: 

3518 # Update displayed handles 

3519 self._corner_handles.set_data(*self.corners) 

3520 self._edge_handles.set_data(*self.edge_centers) 

3521 x, y = self.center 

3522 self._center_handle.set_data([x], [y]) 

3523 self.set_visible(self._visible) 

3524 self.update() 

3525 

3526 @property 

3527 def rotation(self): 

3528 """ 

3529 Rotation in degree in interval [-45°, 45°]. The rotation is limited in 

3530 range to keep the implementation simple. 

3531 """ 

3532 return np.rad2deg(self._rotation) 

3533 

3534 @rotation.setter 

3535 def rotation(self, value): 

3536 # Restrict to a limited range of rotation [-45°, 45°] to avoid changing 

3537 # order of handles 

3538 if -45 <= value and value <= 45: 

3539 self._rotation = np.deg2rad(value) 

3540 # call extents setter to draw shape and update handles positions 

3541 self.extents = self.extents 

3542 

3543 def _draw_shape(self, extents): 

3544 x0, x1, y0, y1 = extents 

3545 xmin, xmax = sorted([x0, x1]) 

3546 ymin, ymax = sorted([y0, y1]) 

3547 xlim = sorted(self.ax.get_xlim()) 

3548 ylim = sorted(self.ax.get_ylim()) 

3549 

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

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

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

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

3554 

3555 self._selection_artist.set_x(xmin) 

3556 self._selection_artist.set_y(ymin) 

3557 self._selection_artist.set_width(xmax - xmin) 

3558 self._selection_artist.set_height(ymax - ymin) 

3559 self._selection_artist.set_angle(self.rotation) 

3560 

3561 def _set_active_handle(self, event): 

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

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

3564 c_idx, c_dist = self._corner_handles.closest(event.x, event.y) 

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

3566 m_idx, m_dist = self._center_handle.closest(event.x, event.y) 

3567 

3568 if 'move' in self._state: 

3569 self._active_handle = 'C' 

3570 # Set active handle as closest handle, if mouse click is close enough. 

3571 elif m_dist < self.grab_range * 2: 

3572 # Prioritise center handle over other handles 

3573 self._active_handle = 'C' 

3574 elif c_dist > self.grab_range and e_dist > self.grab_range: 

3575 # Not close to any handles 

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

3577 # Check if we've clicked inside the region 

3578 self._active_handle = 'C' 

3579 else: 

3580 self._active_handle = None 

3581 return 

3582 elif c_dist < e_dist: 

3583 # Closest to a corner handle 

3584 self._active_handle = self._corner_order[c_idx] 

3585 else: 

3586 # Closest to an edge handle 

3587 self._active_handle = self._edge_order[e_idx] 

3588 

3589 def _contains(self, event): 

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

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

3592 

3593 @property 

3594 def geometry(self): 

3595 """ 

3596 Return an array of shape (2, 5) containing the 

3597 x (``RectangleSelector.geometry[1, :]``) and 

3598 y (``RectangleSelector.geometry[0, :]``) data coordinates of the four 

3599 corners of the rectangle starting and ending in the top left corner. 

3600 """ 

3601 if hasattr(self._selection_artist, 'get_verts'): 

3602 xfm = self.ax.transData.inverted() 

3603 y, x = xfm.transform(self._selection_artist.get_verts()).T 

3604 return np.array([x, y]) 

3605 else: 

3606 return np.array(self._selection_artist.get_data()) 

3607 

3608 

3609@_docstring.Substitution(_RECTANGLESELECTOR_PARAMETERS_DOCSTRING.replace( 

3610 '__ARTIST_NAME__', 'ellipse')) 

3611class EllipseSelector(RectangleSelector): 

3612 """ 

3613 Select an elliptical region of an Axes. 

3614 

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

3616 

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

3618 selection will clear the selector, except when 

3619 ``ignore_event_outside=True``. 

3620 

3621 %s 

3622 

3623 Examples 

3624 -------- 

3625 :doc:`/gallery/widgets/rectangle_selector` 

3626 """ 

3627 def _init_shape(self, **props): 

3628 return Ellipse((0, 0), 0, 1, visible=False, **props) 

3629 

3630 def _draw_shape(self, extents): 

3631 x0, x1, y0, y1 = extents 

3632 xmin, xmax = sorted([x0, x1]) 

3633 ymin, ymax = sorted([y0, y1]) 

3634 center = [x0 + (x1 - x0) / 2., y0 + (y1 - y0) / 2.] 

3635 a = (xmax - xmin) / 2. 

3636 b = (ymax - ymin) / 2. 

3637 

3638 self._selection_artist.center = center 

3639 self._selection_artist.width = 2 * a 

3640 self._selection_artist.height = 2 * b 

3641 self._selection_artist.angle = self.rotation 

3642 

3643 @property 

3644 def _rect_bbox(self): 

3645 x, y = self._selection_artist.center 

3646 width = self._selection_artist.width 

3647 height = self._selection_artist.height 

3648 return x - width / 2., y - height / 2., width, height 

3649 

3650 

3651class LassoSelector(_SelectorWidget): 

3652 """ 

3653 Selection curve of an arbitrary shape. 

3654 

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

3656 

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

3658 to select data points from an image. 

3659 

3660 In contrast to `Lasso`, `LassoSelector` is written with an interface 

3661 similar to `RectangleSelector` and `SpanSelector`, and will continue to 

3662 interact with the Axes until disconnected. 

3663 

3664 Example usage:: 

3665 

3666 ax = plt.subplot() 

3667 ax.plot(x, y) 

3668 

3669 def onselect(verts): 

3670 print(verts) 

3671 lasso = LassoSelector(ax, onselect) 

3672 

3673 Parameters 

3674 ---------- 

3675 ax : `~matplotlib.axes.Axes` 

3676 The parent Axes for the widget. 

3677 onselect : function 

3678 Whenever the lasso is released, the *onselect* function is called and 

3679 passed the vertices of the selected path. 

3680 useblit : bool, default: True 

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

3682 backend). See the tutorial :ref:`blitting` 

3683 for details. 

3684 props : dict, optional 

3685 Properties with which the line is drawn, see `.Line2D` 

3686 for valid properties. Default values are defined in ``mpl.rcParams``. 

3687 button : `.MouseButton` or list of `.MouseButton`, optional 

3688 The mouse buttons used for rectangle selection. Default is ``None``, 

3689 which corresponds to all buttons. 

3690 """ 

3691 

3692 def __init__(self, ax, onselect, *, useblit=True, props=None, button=None): 

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

3694 self.verts = None 

3695 props = { 

3696 **(props if props is not None else {}), 

3697 # Note that self.useblit may be != useblit, if the canvas doesn't 

3698 # support blitting. 

3699 'animated': self.useblit, 'visible': False, 

3700 } 

3701 line = Line2D([], [], **props) 

3702 self.ax.add_line(line) 

3703 self._selection_artist = line 

3704 

3705 def _press(self, event): 

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

3707 self._selection_artist.set_visible(True) 

3708 

3709 def _release(self, event): 

3710 if self.verts is not None: 

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

3712 self.onselect(self.verts) 

3713 self._selection_artist.set_data([[], []]) 

3714 self._selection_artist.set_visible(False) 

3715 self.verts = None 

3716 

3717 def _onmove(self, event): 

3718 if self.verts is None: 

3719 return 

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

3721 self._selection_artist.set_data(list(zip(*self.verts))) 

3722 

3723 self.update() 

3724 

3725 

3726class PolygonSelector(_SelectorWidget): 

3727 """ 

3728 Select a polygon region of an Axes. 

3729 

3730 Place vertices with each mouse click, and make the selection by completing 

3731 the polygon (clicking on the first vertex). Once drawn individual vertices 

3732 can be moved by clicking and dragging with the left mouse button, or 

3733 removed by clicking the right mouse button. 

3734 

3735 In addition, the following modifier keys can be used: 

3736 

3737 - Hold *ctrl* and click and drag a vertex to reposition it before the 

3738 polygon has been completed. 

3739 - Hold the *shift* key and click and drag anywhere in the Axes to move 

3740 all vertices. 

3741 - Press the *esc* key to start a new polygon. 

3742 

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

3744 

3745 Parameters 

3746 ---------- 

3747 ax : `~matplotlib.axes.Axes` 

3748 The parent Axes for the widget. 

3749 

3750 onselect : function 

3751 When a polygon is completed or modified after completion, 

3752 the *onselect* function is called and passed a list of the vertices as 

3753 ``(xdata, ydata)`` tuples. 

3754 

3755 useblit : bool, default: False 

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

3757 backend). See the tutorial :ref:`blitting` 

3758 for details. 

3759 

3760 props : dict, optional 

3761 Properties with which the line is drawn, see `.Line2D` for valid properties. 

3762 Default:: 

3763 

3764 dict(color='k', linestyle='-', linewidth=2, alpha=0.5) 

3765 

3766 handle_props : dict, optional 

3767 Artist properties for the markers drawn at the vertices of the polygon. 

3768 See the marker arguments in `.Line2D` for valid 

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

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

3771 ``color`` property in *props*. 

3772 

3773 grab_range : float, default: 10 

3774 A vertex is selected (to complete the polygon or to move a vertex) if 

3775 the mouse click is within *grab_range* pixels of the vertex. 

3776 

3777 draw_bounding_box : bool, optional 

3778 If `True`, a bounding box will be drawn around the polygon selector 

3779 once it is complete. This box can be used to move and resize the 

3780 selector. 

3781 

3782 box_handle_props : dict, optional 

3783 Properties to set for the box handles. See the documentation for the 

3784 *handle_props* argument to `RectangleSelector` for more info. 

3785 

3786 box_props : dict, optional 

3787 Properties to set for the box. See the documentation for the *props* 

3788 argument to `RectangleSelector` for more info. 

3789 

3790 Examples 

3791 -------- 

3792 :doc:`/gallery/widgets/polygon_selector_simple` 

3793 :doc:`/gallery/widgets/polygon_selector_demo` 

3794 

3795 Notes 

3796 ----- 

3797 If only one point remains after removing points, the selector reverts to an 

3798 incomplete state and you can start drawing a new polygon from the existing 

3799 point. 

3800 """ 

3801 

3802 def __init__(self, ax, onselect, *, useblit=False, 

3803 props=None, handle_props=None, grab_range=10, 

3804 draw_bounding_box=False, box_handle_props=None, 

3805 box_props=None): 

3806 # The state modifiers 'move', 'square', and 'center' are expected by 

3807 # _SelectorWidget but are not supported by PolygonSelector 

3808 # Note: could not use the existing 'move' state modifier in-place of 

3809 # 'move_all' because _SelectorWidget automatically discards 'move' 

3810 # from the state on button release. 

3811 state_modifier_keys = dict(clear='escape', move_vertex='control', 

3812 move_all='shift', move='not-applicable', 

3813 square='not-applicable', 

3814 center='not-applicable', 

3815 rotate='not-applicable') 

3816 super().__init__(ax, onselect, useblit=useblit, 

3817 state_modifier_keys=state_modifier_keys) 

3818 

3819 self._xys = [(0, 0)] 

3820 

3821 if props is None: 

3822 props = dict(color='k', linestyle='-', linewidth=2, alpha=0.5) 

3823 props = {**props, 'animated': self.useblit} 

3824 self._selection_artist = line = Line2D([], [], **props) 

3825 self.ax.add_line(line) 

3826 

3827 if handle_props is None: 

3828 handle_props = dict(markeredgecolor='k', 

3829 markerfacecolor=props.get('color', 'k')) 

3830 self._handle_props = handle_props 

3831 self._polygon_handles = ToolHandles(self.ax, [], [], 

3832 useblit=self.useblit, 

3833 marker_props=self._handle_props) 

3834 

3835 self._active_handle_idx = -1 

3836 self.grab_range = grab_range 

3837 

3838 self.set_visible(True) 

3839 self._draw_box = draw_bounding_box 

3840 self._box = None 

3841 

3842 if box_handle_props is None: 

3843 box_handle_props = {} 

3844 self._box_handle_props = self._handle_props.update(box_handle_props) 

3845 self._box_props = box_props 

3846 

3847 def _get_bbox(self): 

3848 return self._selection_artist.get_bbox() 

3849 

3850 def _add_box(self): 

3851 self._box = RectangleSelector(self.ax, 

3852 onselect=lambda *args, **kwargs: None, 

3853 useblit=self.useblit, 

3854 grab_range=self.grab_range, 

3855 handle_props=self._box_handle_props, 

3856 props=self._box_props, 

3857 interactive=True) 

3858 self._box._state_modifier_keys.pop('rotate') 

3859 self._box.connect_event('motion_notify_event', self._scale_polygon) 

3860 self._update_box() 

3861 # Set state that prevents the RectangleSelector from being created 

3862 # by the user 

3863 self._box._allow_creation = False 

3864 self._box._selection_completed = True 

3865 self._draw_polygon() 

3866 

3867 def _remove_box(self): 

3868 if self._box is not None: 

3869 self._box.set_visible(False) 

3870 self._box = None 

3871 

3872 def _update_box(self): 

3873 # Update selection box extents to the extents of the polygon 

3874 if self._box is not None: 

3875 bbox = self._get_bbox() 

3876 self._box.extents = [bbox.x0, bbox.x1, bbox.y0, bbox.y1] 

3877 # Save a copy 

3878 self._old_box_extents = self._box.extents 

3879 

3880 def _scale_polygon(self, event): 

3881 """ 

3882 Scale the polygon selector points when the bounding box is moved or 

3883 scaled. 

3884 

3885 This is set as a callback on the bounding box RectangleSelector. 

3886 """ 

3887 if not self._selection_completed: 

3888 return 

3889 

3890 if self._old_box_extents == self._box.extents: 

3891 return 

3892 

3893 # Create transform from old box to new box 

3894 x1, y1, w1, h1 = self._box._rect_bbox 

3895 old_bbox = self._get_bbox() 

3896 t = (transforms.Affine2D() 

3897 .translate(-old_bbox.x0, -old_bbox.y0) 

3898 .scale(1 / old_bbox.width, 1 / old_bbox.height) 

3899 .scale(w1, h1) 

3900 .translate(x1, y1)) 

3901 

3902 # Update polygon verts. Must be a list of tuples for consistency. 

3903 new_verts = [(x, y) for x, y in t.transform(np.array(self.verts))] 

3904 self._xys = [*new_verts, new_verts[0]] 

3905 self._draw_polygon() 

3906 self._old_box_extents = self._box.extents 

3907 

3908 @property 

3909 def _handles_artists(self): 

3910 return self._polygon_handles.artists 

3911 

3912 def _remove_vertex(self, i): 

3913 """Remove vertex with index i.""" 

3914 if (len(self._xys) > 2 and 

3915 self._selection_completed and 

3916 i in (0, len(self._xys) - 1)): 

3917 # If selecting the first or final vertex, remove both first and 

3918 # last vertex as they are the same for a closed polygon 

3919 self._xys.pop(0) 

3920 self._xys.pop(-1) 

3921 # Close the polygon again by appending the new first vertex to the 

3922 # end 

3923 self._xys.append(self._xys[0]) 

3924 else: 

3925 self._xys.pop(i) 

3926 if len(self._xys) <= 2: 

3927 # If only one point left, return to incomplete state to let user 

3928 # start drawing again 

3929 self._selection_completed = False 

3930 self._remove_box() 

3931 

3932 def _press(self, event): 

3933 """Button press event handler.""" 

3934 # Check for selection of a tool handle. 

3935 if ((self._selection_completed or 'move_vertex' in self._state) 

3936 and len(self._xys) > 0): 

3937 h_idx, h_dist = self._polygon_handles.closest(event.x, event.y) 

3938 if h_dist < self.grab_range: 

3939 self._active_handle_idx = h_idx 

3940 # Save the vertex positions at the time of the press event (needed to 

3941 # support the 'move_all' state modifier). 

3942 self._xys_at_press = self._xys.copy() 

3943 

3944 def _release(self, event): 

3945 """Button release event handler.""" 

3946 # Release active tool handle. 

3947 if self._active_handle_idx >= 0: 

3948 if event.button == 3: 

3949 self._remove_vertex(self._active_handle_idx) 

3950 self._draw_polygon() 

3951 self._active_handle_idx = -1 

3952 

3953 # Complete the polygon. 

3954 elif len(self._xys) > 3 and self._xys[-1] == self._xys[0]: 

3955 self._selection_completed = True 

3956 if self._draw_box and self._box is None: 

3957 self._add_box() 

3958 

3959 # Place new vertex. 

3960 elif (not self._selection_completed 

3961 and 'move_all' not in self._state 

3962 and 'move_vertex' not in self._state): 

3963 self._xys.insert(-1, self._get_data_coords(event)) 

3964 

3965 if self._selection_completed: 

3966 self.onselect(self.verts) 

3967 

3968 def onmove(self, event): 

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

3970 # Method overrides _SelectorWidget.onmove because the polygon selector 

3971 # needs to process the move callback even if there is no button press. 

3972 # _SelectorWidget.onmove include logic to ignore move event if 

3973 # _eventpress is None. 

3974 if not self.ignore(event): 

3975 event = self._clean_event(event) 

3976 self._onmove(event) 

3977 return True 

3978 return False 

3979 

3980 def _onmove(self, event): 

3981 """Cursor move event handler.""" 

3982 # Move the active vertex (ToolHandle). 

3983 if self._active_handle_idx >= 0: 

3984 idx = self._active_handle_idx 

3985 self._xys[idx] = self._get_data_coords(event) 

3986 # Also update the end of the polygon line if the first vertex is 

3987 # the active handle and the polygon is completed. 

3988 if idx == 0 and self._selection_completed: 

3989 self._xys[-1] = self._get_data_coords(event) 

3990 

3991 # Move all vertices. 

3992 elif 'move_all' in self._state and self._eventpress: 

3993 xdata, ydata = self._get_data_coords(event) 

3994 dx = xdata - self._eventpress.xdata 

3995 dy = ydata - self._eventpress.ydata 

3996 for k in range(len(self._xys)): 

3997 x_at_press, y_at_press = self._xys_at_press[k] 

3998 self._xys[k] = x_at_press + dx, y_at_press + dy 

3999 

4000 # Do nothing if completed or waiting for a move. 

4001 elif (self._selection_completed 

4002 or 'move_vertex' in self._state or 'move_all' in self._state): 

4003 return 

4004 

4005 # Position pending vertex. 

4006 else: 

4007 # Calculate distance to the start vertex. 

4008 x0, y0 = \ 

4009 self._selection_artist.get_transform().transform(self._xys[0]) 

4010 v0_dist = np.hypot(x0 - event.x, y0 - event.y) 

4011 # Lock on to the start vertex if near it and ready to complete. 

4012 if len(self._xys) > 3 and v0_dist < self.grab_range: 

4013 self._xys[-1] = self._xys[0] 

4014 else: 

4015 self._xys[-1] = self._get_data_coords(event) 

4016 

4017 self._draw_polygon() 

4018 

4019 def _on_key_press(self, event): 

4020 """Key press event handler.""" 

4021 # Remove the pending vertex if entering the 'move_vertex' or 

4022 # 'move_all' mode 

4023 if (not self._selection_completed 

4024 and ('move_vertex' in self._state or 

4025 'move_all' in self._state)): 

4026 self._xys.pop() 

4027 self._draw_polygon() 

4028 

4029 def _on_key_release(self, event): 

4030 """Key release event handler.""" 

4031 # Add back the pending vertex if leaving the 'move_vertex' or 

4032 # 'move_all' mode (by checking the released key) 

4033 if (not self._selection_completed 

4034 and 

4035 (event.key == self._state_modifier_keys.get('move_vertex') 

4036 or event.key == self._state_modifier_keys.get('move_all'))): 

4037 self._xys.append(self._get_data_coords(event)) 

4038 self._draw_polygon() 

4039 # Reset the polygon if the released key is the 'clear' key. 

4040 elif event.key == self._state_modifier_keys.get('clear'): 

4041 event = self._clean_event(event) 

4042 self._xys = [self._get_data_coords(event)] 

4043 self._selection_completed = False 

4044 self._remove_box() 

4045 self.set_visible(True) 

4046 

4047 def _draw_polygon_without_update(self): 

4048 """Redraw the polygon based on new vertex positions, no update().""" 

4049 xs, ys = zip(*self._xys) if self._xys else ([], []) 

4050 self._selection_artist.set_data(xs, ys) 

4051 self._update_box() 

4052 # Only show one tool handle at the start and end vertex of the polygon 

4053 # if the polygon is completed or the user is locked on to the start 

4054 # vertex. 

4055 if (self._selection_completed 

4056 or (len(self._xys) > 3 

4057 and self._xys[-1] == self._xys[0])): 

4058 self._polygon_handles.set_data(xs[:-1], ys[:-1]) 

4059 else: 

4060 self._polygon_handles.set_data(xs, ys) 

4061 

4062 def _draw_polygon(self): 

4063 """Redraw the polygon based on the new vertex positions.""" 

4064 self._draw_polygon_without_update() 

4065 self.update() 

4066 

4067 @property 

4068 def verts(self): 

4069 """The polygon vertices, as a list of ``(x, y)`` pairs.""" 

4070 return self._xys[:-1] 

4071 

4072 @verts.setter 

4073 def verts(self, xys): 

4074 """ 

4075 Set the polygon vertices. 

4076 

4077 This will remove any preexisting vertices, creating a complete polygon 

4078 with the new vertices. 

4079 """ 

4080 self._xys = [*xys, xys[0]] 

4081 self._selection_completed = True 

4082 self.set_visible(True) 

4083 if self._draw_box and self._box is None: 

4084 self._add_box() 

4085 self._draw_polygon() 

4086 

4087 def _clear_without_update(self): 

4088 self._selection_completed = False 

4089 self._xys = [(0, 0)] 

4090 self._draw_polygon_without_update() 

4091 

4092 

4093class Lasso(AxesWidget): 

4094 """ 

4095 Selection curve of an arbitrary shape. 

4096 

4097 The selected path can be used in conjunction with 

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

4099 

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

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

4102 

4103 Parameters 

4104 ---------- 

4105 ax : `~matplotlib.axes.Axes` 

4106 The parent Axes for the widget. 

4107 xy : (float, float) 

4108 Coordinates of the start of the lasso. 

4109 callback : callable 

4110 Whenever the lasso is released, the *callback* function is called and 

4111 passed the vertices of the selected path. 

4112 useblit : bool, default: True 

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

4114 backend). See the tutorial :ref:`blitting` 

4115 for details. 

4116 props: dict, optional 

4117 Lasso line properties. See `.Line2D` for valid properties. 

4118 Default *props* are:: 

4119 

4120 {'linestyle' : '-', 'color' : 'black', 'lw' : 2} 

4121 

4122 .. versionadded:: 3.9 

4123 """ 

4124 def __init__(self, ax, xy, callback, *, useblit=True, props=None): 

4125 super().__init__(ax) 

4126 

4127 self.useblit = useblit and self.canvas.supports_blit 

4128 if self.useblit: 

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

4130 

4131 style = {'linestyle': '-', 'color': 'black', 'lw': 2} 

4132 

4133 if props is not None: 

4134 style.update(props) 

4135 

4136 x, y = xy 

4137 self.verts = [(x, y)] 

4138 self.line = Line2D([x], [y], **style) 

4139 self.ax.add_line(self.line) 

4140 self.callback = callback 

4141 self.connect_event('button_release_event', self.onrelease) 

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

4143 

4144 def onrelease(self, event): 

4145 if self.ignore(event): 

4146 return 

4147 if self.verts is not None: 

4148 self.verts.append(self._get_data_coords(event)) 

4149 if len(self.verts) > 2: 

4150 self.callback(self.verts) 

4151 self.line.remove() 

4152 self.verts = None 

4153 self.disconnect_events() 

4154 

4155 def onmove(self, event): 

4156 if (self.ignore(event) 

4157 or self.verts is None 

4158 or event.button != 1 

4159 or not self.ax.contains(event)[0]): 

4160 return 

4161 self.verts.append(self._get_data_coords(event)) 

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

4163 

4164 if self.useblit: 

4165 self.canvas.restore_region(self.background) 

4166 self.ax.draw_artist(self.line) 

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

4168 else: 

4169 self.canvas.draw_idle()