Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2Abstract base classes define the primitives for Tools. 

3These tools are used by `matplotlib.backend_managers.ToolManager` 

4 

5:class:`ToolBase` 

6 Simple stateless tool 

7 

8:class:`ToolToggleBase` 

9 Tool that has two states, only one Toggle tool can be 

10 active at any given time for the same 

11 `matplotlib.backend_managers.ToolManager` 

12""" 

13 

14from enum import IntEnum 

15import logging 

16import re 

17import time 

18from types import SimpleNamespace 

19from weakref import WeakKeyDictionary 

20 

21import numpy as np 

22 

23from matplotlib import rcParams 

24from matplotlib._pylab_helpers import Gcf 

25import matplotlib.cbook as cbook 

26 

27_log = logging.getLogger(__name__) 

28 

29 

30class Cursors(IntEnum): # Must subclass int for the macOS backend. 

31 """Backend-independent cursor types.""" 

32 HAND, POINTER, SELECT_REGION, MOVE, WAIT = range(5) 

33cursors = Cursors # Backcompat. 

34 

35# Views positions tool 

36_views_positions = 'viewpos' 

37 

38 

39class ToolBase: 

40 """ 

41 Base tool class 

42 

43 A base tool, only implements `trigger` method or not method at all. 

44 The tool is instantiated by `matplotlib.backend_managers.ToolManager` 

45 

46 Attributes 

47 ---------- 

48 toolmanager : `matplotlib.backend_managers.ToolManager` 

49 ToolManager that controls this Tool 

50 figure : `FigureCanvas` 

51 Figure instance that is affected by this Tool 

52 name : str 

53 Used as **Id** of the tool, has to be unique among tools of the same 

54 ToolManager 

55 """ 

56 

57 default_keymap = None 

58 """ 

59 Keymap to associate with this tool 

60 

61 **String**: List of comma separated keys that will be used to call this 

62 tool when the keypress event of *self.figure.canvas* is emitted 

63 """ 

64 

65 description = None 

66 """ 

67 Description of the Tool 

68 

69 **String**: If the Tool is included in the Toolbar this text is used 

70 as a Tooltip 

71 """ 

72 

73 image = None 

74 """ 

75 Filename of the image 

76 

77 **String**: Filename of the image to use in the toolbar. If None, the 

78 *name* is used as a label in the toolbar button 

79 """ 

80 

81 def __init__(self, toolmanager, name): 

82 cbook._warn_external( 

83 'The new Tool classes introduced in v1.5 are experimental; their ' 

84 'API (including names) will likely change in future versions.') 

85 self._name = name 

86 self._toolmanager = toolmanager 

87 self._figure = None 

88 

89 @property 

90 def figure(self): 

91 return self._figure 

92 

93 @figure.setter 

94 def figure(self, figure): 

95 self.set_figure(figure) 

96 

97 @property 

98 def canvas(self): 

99 if not self._figure: 

100 return None 

101 return self._figure.canvas 

102 

103 @property 

104 def toolmanager(self): 

105 return self._toolmanager 

106 

107 def _make_classic_style_pseudo_toolbar(self): 

108 """ 

109 Return a placeholder object with a single `canvas` attribute. 

110 

111 This is useful to reuse the implementations of tools already provided 

112 by the classic Toolbars. 

113 """ 

114 return SimpleNamespace(canvas=self.canvas) 

115 

116 def set_figure(self, figure): 

117 """ 

118 Assign a figure to the tool 

119 

120 Parameters 

121 ---------- 

122 figure : `Figure` 

123 """ 

124 self._figure = figure 

125 

126 def trigger(self, sender, event, data=None): 

127 """ 

128 Called when this tool gets used 

129 

130 This method is called by 

131 `matplotlib.backend_managers.ToolManager.trigger_tool` 

132 

133 Parameters 

134 ---------- 

135 event : `Event` 

136 The Canvas event that caused this tool to be called 

137 sender : object 

138 Object that requested the tool to be triggered 

139 data : object 

140 Extra data 

141 """ 

142 

143 pass 

144 

145 @property 

146 def name(self): 

147 """Tool Id""" 

148 return self._name 

149 

150 def destroy(self): 

151 """ 

152 Destroy the tool 

153 

154 This method is called when the tool is removed by 

155 `matplotlib.backend_managers.ToolManager.remove_tool` 

156 """ 

157 pass 

158 

159 

160class ToolToggleBase(ToolBase): 

161 """ 

162 Toggleable tool 

163 

164 Every time it is triggered, it switches between enable and disable 

165 

166 Parameters 

167 ---------- 

168 ``*args`` 

169 Variable length argument to be used by the Tool 

170 ``**kwargs`` 

171 `toggled` if present and True, sets the initial state of the Tool 

172 Arbitrary keyword arguments to be consumed by the Tool 

173 """ 

174 

175 radio_group = None 

176 """Attribute to group 'radio' like tools (mutually exclusive) 

177 

178 **String** that identifies the group or **None** if not belonging to a 

179 group 

180 """ 

181 

182 cursor = None 

183 """Cursor to use when the tool is active""" 

184 

185 default_toggled = False 

186 """Default of toggled state""" 

187 

188 def __init__(self, *args, **kwargs): 

189 self._toggled = kwargs.pop('toggled', self.default_toggled) 

190 ToolBase.__init__(self, *args, **kwargs) 

191 

192 def trigger(self, sender, event, data=None): 

193 """Calls `enable` or `disable` based on `toggled` value""" 

194 if self._toggled: 

195 self.disable(event) 

196 else: 

197 self.enable(event) 

198 self._toggled = not self._toggled 

199 

200 def enable(self, event=None): 

201 """ 

202 Enable the toggle tool 

203 

204 `trigger` calls this method when `toggled` is False 

205 """ 

206 pass 

207 

208 def disable(self, event=None): 

209 """ 

210 Disable the toggle tool 

211 

212 `trigger` call this method when `toggled` is True. 

213 

214 This can happen in different circumstances 

215 

216 * Click on the toolbar tool button 

217 * Call to `matplotlib.backend_managers.ToolManager.trigger_tool` 

218 * Another `ToolToggleBase` derived tool is triggered 

219 (from the same `ToolManager`) 

220 """ 

221 pass 

222 

223 @property 

224 def toggled(self): 

225 """State of the toggled tool""" 

226 

227 return self._toggled 

228 

229 def set_figure(self, figure): 

230 toggled = self.toggled 

231 if toggled: 

232 if self.figure: 

233 self.trigger(self, None) 

234 else: 

235 # if no figure the internal state is not changed 

236 # we change it here so next call to trigger will change it back 

237 self._toggled = False 

238 ToolBase.set_figure(self, figure) 

239 if toggled: 

240 if figure: 

241 self.trigger(self, None) 

242 else: 

243 # if there is no figure, trigger won't change the internal 

244 # state we change it back 

245 self._toggled = True 

246 

247 

248class SetCursorBase(ToolBase): 

249 """ 

250 Change to the current cursor while inaxes 

251 

252 This tool, keeps track of all `ToolToggleBase` derived tools, and calls 

253 set_cursor when a tool gets triggered 

254 """ 

255 def __init__(self, *args, **kwargs): 

256 ToolBase.__init__(self, *args, **kwargs) 

257 self._idDrag = None 

258 self._cursor = None 

259 self._default_cursor = cursors.POINTER 

260 self._last_cursor = self._default_cursor 

261 self.toolmanager.toolmanager_connect('tool_added_event', 

262 self._add_tool_cbk) 

263 

264 # process current tools 

265 for tool in self.toolmanager.tools.values(): 

266 self._add_tool(tool) 

267 

268 def set_figure(self, figure): 

269 if self._idDrag: 

270 self.canvas.mpl_disconnect(self._idDrag) 

271 ToolBase.set_figure(self, figure) 

272 if figure: 

273 self._idDrag = self.canvas.mpl_connect( 

274 'motion_notify_event', self._set_cursor_cbk) 

275 

276 def _tool_trigger_cbk(self, event): 

277 if event.tool.toggled: 

278 self._cursor = event.tool.cursor 

279 else: 

280 self._cursor = None 

281 

282 self._set_cursor_cbk(event.canvasevent) 

283 

284 def _add_tool(self, tool): 

285 """Set the cursor when the tool is triggered.""" 

286 if getattr(tool, 'cursor', None) is not None: 

287 self.toolmanager.toolmanager_connect('tool_trigger_%s' % tool.name, 

288 self._tool_trigger_cbk) 

289 

290 def _add_tool_cbk(self, event): 

291 """Process every newly added tool.""" 

292 if event.tool is self: 

293 return 

294 self._add_tool(event.tool) 

295 

296 def _set_cursor_cbk(self, event): 

297 if not event: 

298 return 

299 

300 if not getattr(event, 'inaxes', False) or not self._cursor: 

301 if self._last_cursor != self._default_cursor: 

302 self.set_cursor(self._default_cursor) 

303 self._last_cursor = self._default_cursor 

304 elif self._cursor: 

305 cursor = self._cursor 

306 if cursor and self._last_cursor != cursor: 

307 self.set_cursor(cursor) 

308 self._last_cursor = cursor 

309 

310 def set_cursor(self, cursor): 

311 """ 

312 Set the cursor 

313 

314 This method has to be implemented per backend 

315 """ 

316 raise NotImplementedError 

317 

318 

319class ToolCursorPosition(ToolBase): 

320 """ 

321 Send message with the current pointer position 

322 

323 This tool runs in the background reporting the position of the cursor 

324 """ 

325 def __init__(self, *args, **kwargs): 

326 self._idDrag = None 

327 ToolBase.__init__(self, *args, **kwargs) 

328 

329 def set_figure(self, figure): 

330 if self._idDrag: 

331 self.canvas.mpl_disconnect(self._idDrag) 

332 ToolBase.set_figure(self, figure) 

333 if figure: 

334 self._idDrag = self.canvas.mpl_connect( 

335 'motion_notify_event', self.send_message) 

336 

337 def send_message(self, event): 

338 """Call `matplotlib.backend_managers.ToolManager.message_event`""" 

339 if self.toolmanager.messagelock.locked(): 

340 return 

341 

342 message = ' ' 

343 

344 if event.inaxes and event.inaxes.get_navigate(): 

345 try: 

346 s = event.inaxes.format_coord(event.xdata, event.ydata) 

347 except (ValueError, OverflowError): 

348 pass 

349 else: 

350 artists = [a for a in event.inaxes._mouseover_set 

351 if a.contains(event) and a.get_visible()] 

352 

353 if artists: 

354 a = cbook._topmost_artist(artists) 

355 if a is not event.inaxes.patch: 

356 data = a.get_cursor_data(event) 

357 if data is not None: 

358 data_str = a.format_cursor_data(data) 

359 if data_str is not None: 

360 s = s + ' ' + data_str 

361 

362 message = s 

363 self.toolmanager.message_event(message, self) 

364 

365 

366class RubberbandBase(ToolBase): 

367 """Draw and remove rubberband""" 

368 def trigger(self, sender, event, data): 

369 """Call `draw_rubberband` or `remove_rubberband` based on data""" 

370 if not self.figure.canvas.widgetlock.available(sender): 

371 return 

372 if data is not None: 

373 self.draw_rubberband(*data) 

374 else: 

375 self.remove_rubberband() 

376 

377 def draw_rubberband(self, *data): 

378 """ 

379 Draw rubberband 

380 

381 This method must get implemented per backend 

382 """ 

383 raise NotImplementedError 

384 

385 def remove_rubberband(self): 

386 """ 

387 Remove rubberband 

388 

389 This method should get implemented per backend 

390 """ 

391 pass 

392 

393 

394class ToolQuit(ToolBase): 

395 """Tool to call the figure manager destroy method""" 

396 

397 description = 'Quit the figure' 

398 default_keymap = rcParams['keymap.quit'] 

399 

400 def trigger(self, sender, event, data=None): 

401 Gcf.destroy_fig(self.figure) 

402 

403 

404class ToolQuitAll(ToolBase): 

405 """Tool to call the figure manager destroy method""" 

406 

407 description = 'Quit all figures' 

408 default_keymap = rcParams['keymap.quit_all'] 

409 

410 def trigger(self, sender, event, data=None): 

411 Gcf.destroy_all() 

412 

413 

414class ToolEnableAllNavigation(ToolBase): 

415 """Tool to enable all axes for toolmanager interaction""" 

416 

417 description = 'Enable all axes toolmanager' 

418 default_keymap = rcParams['keymap.all_axes'] 

419 

420 def trigger(self, sender, event, data=None): 

421 if event.inaxes is None: 

422 return 

423 

424 for a in self.figure.get_axes(): 

425 if (event.x is not None and event.y is not None 

426 and a.in_axes(event)): 

427 a.set_navigate(True) 

428 

429 

430class ToolEnableNavigation(ToolBase): 

431 """Tool to enable a specific axes for toolmanager interaction""" 

432 

433 description = 'Enable one axes toolmanager' 

434 default_keymap = (1, 2, 3, 4, 5, 6, 7, 8, 9) 

435 

436 def trigger(self, sender, event, data=None): 

437 if event.inaxes is None: 

438 return 

439 

440 n = int(event.key) - 1 

441 if n < len(self.figure.get_axes()): 

442 for i, a in enumerate(self.figure.get_axes()): 

443 if (event.x is not None and event.y is not None 

444 and a.in_axes(event)): 

445 a.set_navigate(i == n) 

446 

447 

448class _ToolGridBase(ToolBase): 

449 """Common functionality between ToolGrid and ToolMinorGrid.""" 

450 

451 _cycle = [(False, False), (True, False), (True, True), (False, True)] 

452 

453 def trigger(self, sender, event, data=None): 

454 ax = event.inaxes 

455 if ax is None: 

456 return 

457 try: 

458 x_state, x_which, y_state, y_which = self._get_next_grid_states(ax) 

459 except ValueError: 

460 pass 

461 else: 

462 ax.grid(x_state, which=x_which, axis="x") 

463 ax.grid(y_state, which=y_which, axis="y") 

464 ax.figure.canvas.draw_idle() 

465 

466 @staticmethod 

467 def _get_uniform_grid_state(ticks): 

468 """ 

469 Check whether all grid lines are in the same visibility state. 

470 

471 Returns True/False if all grid lines are on or off, None if they are 

472 not all in the same state. 

473 """ 

474 if all(tick.gridline.get_visible() for tick in ticks): 

475 return True 

476 elif not any(tick.gridline.get_visible() for tick in ticks): 

477 return False 

478 else: 

479 return None 

480 

481 

482class ToolGrid(_ToolGridBase): 

483 """Tool to toggle the major grids of the figure""" 

484 

485 description = 'Toggle major grids' 

486 default_keymap = rcParams['keymap.grid'] 

487 

488 def _get_next_grid_states(self, ax): 

489 if None in map(self._get_uniform_grid_state, 

490 [ax.xaxis.minorTicks, ax.yaxis.minorTicks]): 

491 # Bail out if minor grids are not in a uniform state. 

492 raise ValueError 

493 x_state, y_state = map(self._get_uniform_grid_state, 

494 [ax.xaxis.majorTicks, ax.yaxis.majorTicks]) 

495 cycle = self._cycle 

496 # Bail out (via ValueError) if major grids are not in a uniform state. 

497 x_state, y_state = ( 

498 cycle[(cycle.index((x_state, y_state)) + 1) % len(cycle)]) 

499 return (x_state, "major" if x_state else "both", 

500 y_state, "major" if y_state else "both") 

501 

502 

503class ToolMinorGrid(_ToolGridBase): 

504 """Tool to toggle the major and minor grids of the figure""" 

505 

506 description = 'Toggle major and minor grids' 

507 default_keymap = rcParams['keymap.grid_minor'] 

508 

509 def _get_next_grid_states(self, ax): 

510 if None in map(self._get_uniform_grid_state, 

511 [ax.xaxis.majorTicks, ax.yaxis.majorTicks]): 

512 # Bail out if major grids are not in a uniform state. 

513 raise ValueError 

514 x_state, y_state = map(self._get_uniform_grid_state, 

515 [ax.xaxis.minorTicks, ax.yaxis.minorTicks]) 

516 cycle = self._cycle 

517 # Bail out (via ValueError) if minor grids are not in a uniform state. 

518 x_state, y_state = ( 

519 cycle[(cycle.index((x_state, y_state)) + 1) % len(cycle)]) 

520 return x_state, "both", y_state, "both" 

521 

522 

523class ToolFullScreen(ToolToggleBase): 

524 """Tool to toggle full screen""" 

525 

526 description = 'Toggle fullscreen mode' 

527 default_keymap = rcParams['keymap.fullscreen'] 

528 

529 def enable(self, event): 

530 self.figure.canvas.manager.full_screen_toggle() 

531 

532 def disable(self, event): 

533 self.figure.canvas.manager.full_screen_toggle() 

534 

535 

536class AxisScaleBase(ToolToggleBase): 

537 """Base Tool to toggle between linear and logarithmic""" 

538 

539 def trigger(self, sender, event, data=None): 

540 if event.inaxes is None: 

541 return 

542 ToolToggleBase.trigger(self, sender, event, data) 

543 

544 def enable(self, event): 

545 self.set_scale(event.inaxes, 'log') 

546 self.figure.canvas.draw_idle() 

547 

548 def disable(self, event): 

549 self.set_scale(event.inaxes, 'linear') 

550 self.figure.canvas.draw_idle() 

551 

552 

553class ToolYScale(AxisScaleBase): 

554 """Tool to toggle between linear and logarithmic scales on the Y axis""" 

555 

556 description = 'Toggle scale Y axis' 

557 default_keymap = rcParams['keymap.yscale'] 

558 

559 def set_scale(self, ax, scale): 

560 ax.set_yscale(scale) 

561 

562 

563class ToolXScale(AxisScaleBase): 

564 """Tool to toggle between linear and logarithmic scales on the X axis""" 

565 

566 description = 'Toggle scale X axis' 

567 default_keymap = rcParams['keymap.xscale'] 

568 

569 def set_scale(self, ax, scale): 

570 ax.set_xscale(scale) 

571 

572 

573class ToolViewsPositions(ToolBase): 

574 """ 

575 Auxiliary Tool to handle changes in views and positions 

576 

577 Runs in the background and should get used by all the tools that 

578 need to access the figure's history of views and positions, e.g. 

579 

580 * `ToolZoom` 

581 * `ToolPan` 

582 * `ToolHome` 

583 * `ToolBack` 

584 * `ToolForward` 

585 """ 

586 

587 def __init__(self, *args, **kwargs): 

588 self.views = WeakKeyDictionary() 

589 self.positions = WeakKeyDictionary() 

590 self.home_views = WeakKeyDictionary() 

591 ToolBase.__init__(self, *args, **kwargs) 

592 

593 def add_figure(self, figure): 

594 """Add the current figure to the stack of views and positions""" 

595 

596 if figure not in self.views: 

597 self.views[figure] = cbook.Stack() 

598 self.positions[figure] = cbook.Stack() 

599 self.home_views[figure] = WeakKeyDictionary() 

600 # Define Home 

601 self.push_current(figure) 

602 # Make sure we add a home view for new axes as they're added 

603 figure.add_axobserver(lambda fig: self.update_home_views(fig)) 

604 

605 def clear(self, figure): 

606 """Reset the axes stack""" 

607 if figure in self.views: 

608 self.views[figure].clear() 

609 self.positions[figure].clear() 

610 self.home_views[figure].clear() 

611 self.update_home_views() 

612 

613 def update_view(self): 

614 """ 

615 Update the view limits and position for each axes from the current 

616 stack position. If any axes are present in the figure that aren't in 

617 the current stack position, use the home view limits for those axes and 

618 don't update *any* positions. 

619 """ 

620 

621 views = self.views[self.figure]() 

622 if views is None: 

623 return 

624 pos = self.positions[self.figure]() 

625 if pos is None: 

626 return 

627 home_views = self.home_views[self.figure] 

628 all_axes = self.figure.get_axes() 

629 for a in all_axes: 

630 if a in views: 

631 cur_view = views[a] 

632 else: 

633 cur_view = home_views[a] 

634 a._set_view(cur_view) 

635 

636 if set(all_axes).issubset(pos): 

637 for a in all_axes: 

638 # Restore both the original and modified positions 

639 a._set_position(pos[a][0], 'original') 

640 a._set_position(pos[a][1], 'active') 

641 

642 self.figure.canvas.draw_idle() 

643 

644 def push_current(self, figure=None): 

645 """ 

646 Push the current view limits and position onto their respective stacks 

647 """ 

648 if not figure: 

649 figure = self.figure 

650 views = WeakKeyDictionary() 

651 pos = WeakKeyDictionary() 

652 for a in figure.get_axes(): 

653 views[a] = a._get_view() 

654 pos[a] = self._axes_pos(a) 

655 self.views[figure].push(views) 

656 self.positions[figure].push(pos) 

657 

658 def _axes_pos(self, ax): 

659 """ 

660 Return the original and modified positions for the specified axes 

661 

662 Parameters 

663 ---------- 

664 ax : (matplotlib.axes.AxesSubplot) 

665 The axes to get the positions for 

666 

667 Returns 

668 ------- 

669 limits : (tuple) 

670 A tuple of the original and modified positions 

671 """ 

672 

673 return (ax.get_position(True).frozen(), 

674 ax.get_position().frozen()) 

675 

676 def update_home_views(self, figure=None): 

677 """ 

678 Make sure that self.home_views has an entry for all axes present in the 

679 figure 

680 """ 

681 

682 if not figure: 

683 figure = self.figure 

684 for a in figure.get_axes(): 

685 if a not in self.home_views[figure]: 

686 self.home_views[figure][a] = a._get_view() 

687 

688 def refresh_locators(self): 

689 """Redraw the canvases, update the locators""" 

690 for a in self.figure.get_axes(): 

691 xaxis = getattr(a, 'xaxis', None) 

692 yaxis = getattr(a, 'yaxis', None) 

693 zaxis = getattr(a, 'zaxis', None) 

694 locators = [] 

695 if xaxis is not None: 

696 locators.append(xaxis.get_major_locator()) 

697 locators.append(xaxis.get_minor_locator()) 

698 if yaxis is not None: 

699 locators.append(yaxis.get_major_locator()) 

700 locators.append(yaxis.get_minor_locator()) 

701 if zaxis is not None: 

702 locators.append(zaxis.get_major_locator()) 

703 locators.append(zaxis.get_minor_locator()) 

704 

705 for loc in locators: 

706 loc.refresh() 

707 self.figure.canvas.draw_idle() 

708 

709 def home(self): 

710 """Recall the first view and position from the stack""" 

711 self.views[self.figure].home() 

712 self.positions[self.figure].home() 

713 

714 def back(self): 

715 """Back one step in the stack of views and positions""" 

716 self.views[self.figure].back() 

717 self.positions[self.figure].back() 

718 

719 def forward(self): 

720 """Forward one step in the stack of views and positions""" 

721 self.views[self.figure].forward() 

722 self.positions[self.figure].forward() 

723 

724 

725class ViewsPositionsBase(ToolBase): 

726 """Base class for `ToolHome`, `ToolBack` and `ToolForward`""" 

727 

728 _on_trigger = None 

729 

730 def trigger(self, sender, event, data=None): 

731 self.toolmanager.get_tool(_views_positions).add_figure(self.figure) 

732 getattr(self.toolmanager.get_tool(_views_positions), 

733 self._on_trigger)() 

734 self.toolmanager.get_tool(_views_positions).update_view() 

735 

736 

737class ToolHome(ViewsPositionsBase): 

738 """Restore the original view lim""" 

739 

740 description = 'Reset original view' 

741 image = 'home' 

742 default_keymap = rcParams['keymap.home'] 

743 _on_trigger = 'home' 

744 

745 

746class ToolBack(ViewsPositionsBase): 

747 """Move back up the view lim stack""" 

748 

749 description = 'Back to previous view' 

750 image = 'back' 

751 default_keymap = rcParams['keymap.back'] 

752 _on_trigger = 'back' 

753 

754 

755class ToolForward(ViewsPositionsBase): 

756 """Move forward in the view lim stack""" 

757 

758 description = 'Forward to next view' 

759 image = 'forward' 

760 default_keymap = rcParams['keymap.forward'] 

761 _on_trigger = 'forward' 

762 

763 

764class ConfigureSubplotsBase(ToolBase): 

765 """Base tool for the configuration of subplots""" 

766 

767 description = 'Configure subplots' 

768 image = 'subplots' 

769 

770 

771class SaveFigureBase(ToolBase): 

772 """Base tool for figure saving""" 

773 

774 description = 'Save the figure' 

775 image = 'filesave' 

776 default_keymap = rcParams['keymap.save'] 

777 

778 

779class ZoomPanBase(ToolToggleBase): 

780 """Base class for `ToolZoom` and `ToolPan`""" 

781 def __init__(self, *args): 

782 ToolToggleBase.__init__(self, *args) 

783 self._button_pressed = None 

784 self._xypress = None 

785 self._idPress = None 

786 self._idRelease = None 

787 self._idScroll = None 

788 self.base_scale = 2. 

789 self.scrollthresh = .5 # .5 second scroll threshold 

790 self.lastscroll = time.time()-self.scrollthresh 

791 

792 def enable(self, event): 

793 """Connect press/release events and lock the canvas""" 

794 self.figure.canvas.widgetlock(self) 

795 self._idPress = self.figure.canvas.mpl_connect( 

796 'button_press_event', self._press) 

797 self._idRelease = self.figure.canvas.mpl_connect( 

798 'button_release_event', self._release) 

799 self._idScroll = self.figure.canvas.mpl_connect( 

800 'scroll_event', self.scroll_zoom) 

801 

802 def disable(self, event): 

803 """Release the canvas and disconnect press/release events""" 

804 self._cancel_action() 

805 self.figure.canvas.widgetlock.release(self) 

806 self.figure.canvas.mpl_disconnect(self._idPress) 

807 self.figure.canvas.mpl_disconnect(self._idRelease) 

808 self.figure.canvas.mpl_disconnect(self._idScroll) 

809 

810 def trigger(self, sender, event, data=None): 

811 self.toolmanager.get_tool(_views_positions).add_figure(self.figure) 

812 ToolToggleBase.trigger(self, sender, event, data) 

813 

814 def scroll_zoom(self, event): 

815 # https://gist.github.com/tacaswell/3144287 

816 if event.inaxes is None: 

817 return 

818 

819 if event.button == 'up': 

820 # deal with zoom in 

821 scl = self.base_scale 

822 elif event.button == 'down': 

823 # deal with zoom out 

824 scl = 1/self.base_scale 

825 else: 

826 # deal with something that should never happen 

827 scl = 1 

828 

829 ax = event.inaxes 

830 ax._set_view_from_bbox([event.x, event.y, scl]) 

831 

832 # If last scroll was done within the timing threshold, delete the 

833 # previous view 

834 if (time.time()-self.lastscroll) < self.scrollthresh: 

835 self.toolmanager.get_tool(_views_positions).back() 

836 

837 self.figure.canvas.draw_idle() # force re-draw 

838 

839 self.lastscroll = time.time() 

840 self.toolmanager.get_tool(_views_positions).push_current() 

841 

842 

843class ToolZoom(ZoomPanBase): 

844 """Zoom to rectangle""" 

845 

846 description = 'Zoom to rectangle' 

847 image = 'zoom_to_rect' 

848 default_keymap = rcParams['keymap.zoom'] 

849 cursor = cursors.SELECT_REGION 

850 radio_group = 'default' 

851 

852 def __init__(self, *args): 

853 ZoomPanBase.__init__(self, *args) 

854 self._ids_zoom = [] 

855 

856 def _cancel_action(self): 

857 for zoom_id in self._ids_zoom: 

858 self.figure.canvas.mpl_disconnect(zoom_id) 

859 self.toolmanager.trigger_tool('rubberband', self) 

860 self.toolmanager.get_tool(_views_positions).refresh_locators() 

861 self._xypress = None 

862 self._button_pressed = None 

863 self._ids_zoom = [] 

864 return 

865 

866 def _press(self, event): 

867 """Callback for mouse button presses in zoom-to-rectangle mode.""" 

868 

869 # If we're already in the middle of a zoom, pressing another 

870 # button works to "cancel" 

871 if self._ids_zoom != []: 

872 self._cancel_action() 

873 

874 if event.button == 1: 

875 self._button_pressed = 1 

876 elif event.button == 3: 

877 self._button_pressed = 3 

878 else: 

879 self._cancel_action() 

880 return 

881 

882 x, y = event.x, event.y 

883 

884 self._xypress = [] 

885 for i, a in enumerate(self.figure.get_axes()): 

886 if (x is not None and y is not None and a.in_axes(event) and 

887 a.get_navigate() and a.can_zoom()): 

888 self._xypress.append((x, y, a, i, a._get_view())) 

889 

890 id1 = self.figure.canvas.mpl_connect( 

891 'motion_notify_event', self._mouse_move) 

892 id2 = self.figure.canvas.mpl_connect( 

893 'key_press_event', self._switch_on_zoom_mode) 

894 id3 = self.figure.canvas.mpl_connect( 

895 'key_release_event', self._switch_off_zoom_mode) 

896 

897 self._ids_zoom = id1, id2, id3 

898 self._zoom_mode = event.key 

899 

900 def _switch_on_zoom_mode(self, event): 

901 self._zoom_mode = event.key 

902 self._mouse_move(event) 

903 

904 def _switch_off_zoom_mode(self, event): 

905 self._zoom_mode = None 

906 self._mouse_move(event) 

907 

908 def _mouse_move(self, event): 

909 """Callback for mouse moves in zoom-to-rectangle mode.""" 

910 

911 if self._xypress: 

912 x, y = event.x, event.y 

913 lastx, lasty, a, ind, view = self._xypress[0] 

914 (x1, y1), (x2, y2) = np.clip( 

915 [[lastx, lasty], [x, y]], a.bbox.min, a.bbox.max) 

916 if self._zoom_mode == "x": 

917 y1, y2 = a.bbox.intervaly 

918 elif self._zoom_mode == "y": 

919 x1, x2 = a.bbox.intervalx 

920 self.toolmanager.trigger_tool( 

921 'rubberband', self, data=(x1, y1, x2, y2)) 

922 

923 def _release(self, event): 

924 """Callback for mouse button releases in zoom-to-rectangle mode.""" 

925 

926 for zoom_id in self._ids_zoom: 

927 self.figure.canvas.mpl_disconnect(zoom_id) 

928 self._ids_zoom = [] 

929 

930 if not self._xypress: 

931 self._cancel_action() 

932 return 

933 

934 last_a = [] 

935 

936 for cur_xypress in self._xypress: 

937 x, y = event.x, event.y 

938 lastx, lasty, a, _ind, view = cur_xypress 

939 # ignore singular clicks - 5 pixels is a threshold 

940 if abs(x - lastx) < 5 or abs(y - lasty) < 5: 

941 self._cancel_action() 

942 return 

943 

944 # detect twinx, twiny axes and avoid double zooming 

945 twinx, twiny = False, False 

946 if last_a: 

947 for la in last_a: 

948 if a.get_shared_x_axes().joined(a, la): 

949 twinx = True 

950 if a.get_shared_y_axes().joined(a, la): 

951 twiny = True 

952 last_a.append(a) 

953 

954 if self._button_pressed == 1: 

955 direction = 'in' 

956 elif self._button_pressed == 3: 

957 direction = 'out' 

958 else: 

959 continue 

960 

961 a._set_view_from_bbox((lastx, lasty, x, y), direction, 

962 self._zoom_mode, twinx, twiny) 

963 

964 self._zoom_mode = None 

965 self.toolmanager.get_tool(_views_positions).push_current() 

966 self._cancel_action() 

967 

968 

969class ToolPan(ZoomPanBase): 

970 """Pan axes with left mouse, zoom with right""" 

971 

972 default_keymap = rcParams['keymap.pan'] 

973 description = 'Pan axes with left mouse, zoom with right' 

974 image = 'move' 

975 cursor = cursors.MOVE 

976 radio_group = 'default' 

977 

978 def __init__(self, *args): 

979 ZoomPanBase.__init__(self, *args) 

980 self._idDrag = None 

981 

982 def _cancel_action(self): 

983 self._button_pressed = None 

984 self._xypress = [] 

985 self.figure.canvas.mpl_disconnect(self._idDrag) 

986 self.toolmanager.messagelock.release(self) 

987 self.toolmanager.get_tool(_views_positions).refresh_locators() 

988 

989 def _press(self, event): 

990 if event.button == 1: 

991 self._button_pressed = 1 

992 elif event.button == 3: 

993 self._button_pressed = 3 

994 else: 

995 self._cancel_action() 

996 return 

997 

998 x, y = event.x, event.y 

999 

1000 self._xypress = [] 

1001 for i, a in enumerate(self.figure.get_axes()): 

1002 if (x is not None and y is not None and a.in_axes(event) and 

1003 a.get_navigate() and a.can_pan()): 

1004 a.start_pan(x, y, event.button) 

1005 self._xypress.append((a, i)) 

1006 self.toolmanager.messagelock(self) 

1007 self._idDrag = self.figure.canvas.mpl_connect( 

1008 'motion_notify_event', self._mouse_move) 

1009 

1010 def _release(self, event): 

1011 if self._button_pressed is None: 

1012 self._cancel_action() 

1013 return 

1014 

1015 self.figure.canvas.mpl_disconnect(self._idDrag) 

1016 self.toolmanager.messagelock.release(self) 

1017 

1018 for a, _ind in self._xypress: 

1019 a.end_pan() 

1020 if not self._xypress: 

1021 self._cancel_action() 

1022 return 

1023 

1024 self.toolmanager.get_tool(_views_positions).push_current() 

1025 self._cancel_action() 

1026 

1027 def _mouse_move(self, event): 

1028 for a, _ind in self._xypress: 

1029 # safer to use the recorded button at the _press than current 

1030 # button: # multiple button can get pressed during motion... 

1031 a.drag_pan(self._button_pressed, event.key, event.x, event.y) 

1032 self.toolmanager.canvas.draw_idle() 

1033 

1034 

1035class ToolHelpBase(ToolBase): 

1036 description = 'Print tool list, shortcuts and description' 

1037 default_keymap = rcParams['keymap.help'] 

1038 image = 'help.png' 

1039 

1040 @staticmethod 

1041 def format_shortcut(key_sequence): 

1042 """ 

1043 Converts a shortcut string from the notation used in rc config to the 

1044 standard notation for displaying shortcuts, e.g. 'ctrl+a' -> 'Ctrl+A'. 

1045 """ 

1046 return (key_sequence if len(key_sequence) == 1 else 

1047 re.sub(r"\+[A-Z]", r"+Shift\g<0>", key_sequence).title()) 

1048 

1049 def _format_tool_keymap(self, name): 

1050 keymaps = self.toolmanager.get_tool_keymap(name) 

1051 return ", ".join(self.format_shortcut(keymap) for keymap in keymaps) 

1052 

1053 def _get_help_entries(self): 

1054 return [(name, self._format_tool_keymap(name), tool.description) 

1055 for name, tool in sorted(self.toolmanager.tools.items()) 

1056 if tool.description] 

1057 

1058 def _get_help_text(self): 

1059 entries = self._get_help_entries() 

1060 entries = ["{}: {}\n\t{}".format(*entry) for entry in entries] 

1061 return "\n".join(entries) 

1062 

1063 def _get_help_html(self): 

1064 fmt = "<tr><td>{}</td><td>{}</td><td>{}</td></tr>" 

1065 rows = [fmt.format( 

1066 "<b>Action</b>", "<b>Shortcuts</b>", "<b>Description</b>")] 

1067 rows += [fmt.format(*row) for row in self._get_help_entries()] 

1068 return ("<style>td {padding: 0px 4px}</style>" 

1069 "<table><thead>" + rows[0] + "</thead>" 

1070 "<tbody>".join(rows[1:]) + "</tbody></table>") 

1071 

1072 

1073class ToolCopyToClipboardBase(ToolBase): 

1074 """Tool to copy the figure to the clipboard""" 

1075 

1076 description = 'Copy the canvas figure to clipboard' 

1077 default_keymap = rcParams['keymap.copy'] 

1078 

1079 def trigger(self, *args, **kwargs): 

1080 message = "Copy tool is not available" 

1081 self.toolmanager.message_event(message, self) 

1082 

1083 

1084default_tools = {'home': ToolHome, 'back': ToolBack, 'forward': ToolForward, 

1085 'zoom': ToolZoom, 'pan': ToolPan, 

1086 'subplots': 'ToolConfigureSubplots', 

1087 'save': 'ToolSaveFigure', 

1088 'grid': ToolGrid, 

1089 'grid_minor': ToolMinorGrid, 

1090 'fullscreen': ToolFullScreen, 

1091 'quit': ToolQuit, 

1092 'quit_all': ToolQuitAll, 

1093 'allnav': ToolEnableAllNavigation, 

1094 'nav': ToolEnableNavigation, 

1095 'xscale': ToolXScale, 

1096 'yscale': ToolYScale, 

1097 'position': ToolCursorPosition, 

1098 _views_positions: ToolViewsPositions, 

1099 'cursor': 'ToolSetCursor', 

1100 'rubberband': 'ToolRubberband', 

1101 'help': 'ToolHelp', 

1102 'copy': 'ToolCopyToClipboard', 

1103 } 

1104"""Default tools""" 

1105 

1106default_toolbar_tools = [['navigation', ['home', 'back', 'forward']], 

1107 ['zoompan', ['pan', 'zoom', 'subplots']], 

1108 ['io', ['save', 'help']]] 

1109"""Default tools in the toolbar""" 

1110 

1111 

1112def add_tools_to_manager(toolmanager, tools=default_tools): 

1113 """ 

1114 Add multiple tools to `ToolManager` 

1115 

1116 Parameters 

1117 ---------- 

1118 toolmanager : ToolManager 

1119 `backend_managers.ToolManager` object that will get the tools added 

1120 tools : {str: class_like}, optional 

1121 The tools to add in a {name: tool} dict, see `add_tool` for more 

1122 info. 

1123 """ 

1124 

1125 for name, tool in tools.items(): 

1126 toolmanager.add_tool(name, tool) 

1127 

1128 

1129def add_tools_to_container(container, tools=default_toolbar_tools): 

1130 """ 

1131 Add multiple tools to the container. 

1132 

1133 Parameters 

1134 ---------- 

1135 container : Container 

1136 `backend_bases.ToolContainerBase` object that will get the tools added 

1137 tools : list, optional 

1138 List in the form 

1139 [[group1, [tool1, tool2 ...]], [group2, [...]]] 

1140 Where the tools given by tool1, and tool2 will display in group1. 

1141 See `add_tool` for details. 

1142 """ 

1143 

1144 for group, grouptools in tools: 

1145 for position, tool in enumerate(grouptools): 

1146 container.add_tool(tool, group, position)