Coverage for /usr/lib/python3/dist-packages/matplotlib/backend_bases.py: 47%

1278 statements  

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

1""" 

2Abstract base classes define the primitives that renderers and 

3graphics contexts must implement to serve as a Matplotlib backend. 

4 

5`RendererBase` 

6 An abstract base class to handle drawing/rendering operations. 

7 

8`FigureCanvasBase` 

9 The abstraction layer that separates the `.Figure` from the backend 

10 specific details like a user interface drawing area. 

11 

12`GraphicsContextBase` 

13 An abstract base class that provides color, line styles, etc. 

14 

15`Event` 

16 The base class for all of the Matplotlib event handling. Derived classes 

17 such as `KeyEvent` and `MouseEvent` store the meta data like keys and 

18 buttons pressed, x and y locations in pixel and `~.axes.Axes` coordinates. 

19 

20`ShowBase` 

21 The base class for the ``Show`` class of each interactive backend; the 

22 'show' callable is then set to ``Show.__call__``. 

23 

24`ToolContainerBase` 

25 The base class for the Toolbar class of each interactive backend. 

26""" 

27 

28from collections import namedtuple 

29from contextlib import ExitStack, contextmanager, nullcontext 

30from enum import Enum, IntEnum 

31import functools 

32import importlib 

33import inspect 

34import io 

35import itertools 

36import logging 

37import os 

38import sys 

39import time 

40from weakref import WeakKeyDictionary 

41 

42import numpy as np 

43 

44import matplotlib as mpl 

45from matplotlib import ( 

46 _api, backend_tools as tools, cbook, colors, _docstring, textpath, 

47 _tight_bbox, transforms, widgets, get_backend, is_interactive, rcParams) 

48from matplotlib._pylab_helpers import Gcf 

49from matplotlib.backend_managers import ToolManager 

50from matplotlib.cbook import _setattr_cm 

51from matplotlib.path import Path 

52from matplotlib.texmanager import TexManager 

53from matplotlib.transforms import Affine2D 

54from matplotlib._enums import JoinStyle, CapStyle 

55 

56 

57_log = logging.getLogger(__name__) 

58_default_filetypes = { 

59 'eps': 'Encapsulated Postscript', 

60 'jpg': 'Joint Photographic Experts Group', 

61 'jpeg': 'Joint Photographic Experts Group', 

62 'pdf': 'Portable Document Format', 

63 'pgf': 'PGF code for LaTeX', 

64 'png': 'Portable Network Graphics', 

65 'ps': 'Postscript', 

66 'raw': 'Raw RGBA bitmap', 

67 'rgba': 'Raw RGBA bitmap', 

68 'svg': 'Scalable Vector Graphics', 

69 'svgz': 'Scalable Vector Graphics', 

70 'tif': 'Tagged Image File Format', 

71 'tiff': 'Tagged Image File Format', 

72 'webp': 'WebP Image Format', 

73} 

74_default_backends = { 

75 'eps': 'matplotlib.backends.backend_ps', 

76 'jpg': 'matplotlib.backends.backend_agg', 

77 'jpeg': 'matplotlib.backends.backend_agg', 

78 'pdf': 'matplotlib.backends.backend_pdf', 

79 'pgf': 'matplotlib.backends.backend_pgf', 

80 'png': 'matplotlib.backends.backend_agg', 

81 'ps': 'matplotlib.backends.backend_ps', 

82 'raw': 'matplotlib.backends.backend_agg', 

83 'rgba': 'matplotlib.backends.backend_agg', 

84 'svg': 'matplotlib.backends.backend_svg', 

85 'svgz': 'matplotlib.backends.backend_svg', 

86 'tif': 'matplotlib.backends.backend_agg', 

87 'tiff': 'matplotlib.backends.backend_agg', 

88 'webp': 'matplotlib.backends.backend_agg', 

89} 

90 

91 

92def _safe_pyplot_import(): 

93 """ 

94 Import and return ``pyplot``, correctly setting the backend if one is 

95 already forced. 

96 """ 

97 try: 

98 import matplotlib.pyplot as plt 

99 except ImportError: # Likely due to a framework mismatch. 

100 current_framework = cbook._get_running_interactive_framework() 

101 if current_framework is None: 

102 raise # No, something else went wrong, likely with the install... 

103 backend_mapping = { 

104 'qt': 'qtagg', 

105 'gtk3': 'gtk3agg', 

106 'gtk4': 'gtk4agg', 

107 'wx': 'wxagg', 

108 'tk': 'tkagg', 

109 'macosx': 'macosx', 

110 'headless': 'agg', 

111 } 

112 backend = backend_mapping[current_framework] 

113 rcParams["backend"] = mpl.rcParamsOrig["backend"] = backend 

114 import matplotlib.pyplot as plt # Now this should succeed. 

115 return plt 

116 

117 

118def register_backend(format, backend, description=None): 

119 """ 

120 Register a backend for saving to a given file format. 

121 

122 Parameters 

123 ---------- 

124 format : str 

125 File extension 

126 backend : module string or canvas class 

127 Backend for handling file output 

128 description : str, default: "" 

129 Description of the file type. 

130 """ 

131 if description is None: 

132 description = '' 

133 _default_backends[format] = backend 

134 _default_filetypes[format] = description 

135 

136 

137def get_registered_canvas_class(format): 

138 """ 

139 Return the registered default canvas for given file format. 

140 Handles deferred import of required backend. 

141 """ 

142 if format not in _default_backends: 

143 return None 

144 backend_class = _default_backends[format] 

145 if isinstance(backend_class, str): 

146 backend_class = importlib.import_module(backend_class).FigureCanvas 

147 _default_backends[format] = backend_class 

148 return backend_class 

149 

150 

151class RendererBase: 

152 """ 

153 An abstract base class to handle drawing/rendering operations. 

154 

155 The following methods must be implemented in the backend for full 

156 functionality (though just implementing `draw_path` alone would give a 

157 highly capable backend): 

158 

159 * `draw_path` 

160 * `draw_image` 

161 * `draw_gouraud_triangle` 

162 

163 The following methods *should* be implemented in the backend for 

164 optimization reasons: 

165 

166 * `draw_text` 

167 * `draw_markers` 

168 * `draw_path_collection` 

169 * `draw_quad_mesh` 

170 """ 

171 

172 def __init__(self): 

173 super().__init__() 

174 self._texmanager = None 

175 self._text2path = textpath.TextToPath() 

176 self._raster_depth = 0 

177 self._rasterizing = False 

178 

179 def open_group(self, s, gid=None): 

180 """ 

181 Open a grouping element with label *s* and *gid* (if set) as id. 

182 

183 Only used by the SVG renderer. 

184 """ 

185 

186 def close_group(self, s): 

187 """ 

188 Close a grouping element with label *s*. 

189 

190 Only used by the SVG renderer. 

191 """ 

192 

193 def draw_path(self, gc, path, transform, rgbFace=None): 

194 """Draw a `~.path.Path` instance using the given affine transform.""" 

195 raise NotImplementedError 

196 

197 def draw_markers(self, gc, marker_path, marker_trans, path, 

198 trans, rgbFace=None): 

199 """ 

200 Draw a marker at each of *path*'s vertices (excluding control points). 

201 

202 The base (fallback) implementation makes multiple calls to `draw_path`. 

203 Backends may want to override this method in order to draw the marker 

204 only once and reuse it multiple times. 

205 

206 Parameters 

207 ---------- 

208 gc : `.GraphicsContextBase` 

209 The graphics context. 

210 marker_trans : `matplotlib.transforms.Transform` 

211 An affine transform applied to the marker. 

212 trans : `matplotlib.transforms.Transform` 

213 An affine transform applied to the path. 

214 """ 

215 for vertices, codes in path.iter_segments(trans, simplify=False): 

216 if len(vertices): 

217 x, y = vertices[-2:] 

218 self.draw_path(gc, marker_path, 

219 marker_trans + 

220 transforms.Affine2D().translate(x, y), 

221 rgbFace) 

222 

223 def draw_path_collection(self, gc, master_transform, paths, all_transforms, 

224 offsets, offset_trans, facecolors, edgecolors, 

225 linewidths, linestyles, antialiaseds, urls, 

226 offset_position): 

227 """ 

228 Draw a collection of *paths*. 

229 

230 Each path is first transformed by the corresponding entry 

231 in *all_transforms* (a list of (3, 3) matrices) and then by 

232 *master_transform*. They are then translated by the corresponding 

233 entry in *offsets*, which has been first transformed by *offset_trans*. 

234 

235 *facecolors*, *edgecolors*, *linewidths*, *linestyles*, and 

236 *antialiased* are lists that set the corresponding properties. 

237 

238 *offset_position* is unused now, but the argument is kept for 

239 backwards compatibility. 

240 

241 The base (fallback) implementation makes multiple calls to `draw_path`. 

242 Backends may want to override this in order to render each set of 

243 path data only once, and then reference that path multiple times with 

244 the different offsets, colors, styles etc. The generator methods 

245 `_iter_collection_raw_paths` and `_iter_collection` are provided to 

246 help with (and standardize) the implementation across backends. It 

247 is highly recommended to use those generators, so that changes to the 

248 behavior of `draw_path_collection` can be made globally. 

249 """ 

250 path_ids = self._iter_collection_raw_paths(master_transform, 

251 paths, all_transforms) 

252 

253 for xo, yo, path_id, gc0, rgbFace in self._iter_collection( 

254 gc, list(path_ids), offsets, offset_trans, 

255 facecolors, edgecolors, linewidths, linestyles, 

256 antialiaseds, urls, offset_position): 

257 path, transform = path_id 

258 # Only apply another translation if we have an offset, else we 

259 # reuse the initial transform. 

260 if xo != 0 or yo != 0: 

261 # The transformation can be used by multiple paths. Since 

262 # translate is a inplace operation, we need to copy the 

263 # transformation by .frozen() before applying the translation. 

264 transform = transform.frozen() 

265 transform.translate(xo, yo) 

266 self.draw_path(gc0, path, transform, rgbFace) 

267 

268 def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight, 

269 coordinates, offsets, offsetTrans, facecolors, 

270 antialiased, edgecolors): 

271 """ 

272 Draw a quadmesh. 

273 

274 The base (fallback) implementation converts the quadmesh to paths and 

275 then calls `draw_path_collection`. 

276 """ 

277 

278 from matplotlib.collections import QuadMesh 

279 paths = QuadMesh._convert_mesh_to_paths(coordinates) 

280 

281 if edgecolors is None: 

282 edgecolors = facecolors 

283 linewidths = np.array([gc.get_linewidth()], float) 

284 

285 return self.draw_path_collection( 

286 gc, master_transform, paths, [], offsets, offsetTrans, facecolors, 

287 edgecolors, linewidths, [], [antialiased], [None], 'screen') 

288 

289 def draw_gouraud_triangle(self, gc, points, colors, transform): 

290 """ 

291 Draw a Gouraud-shaded triangle. 

292 

293 Parameters 

294 ---------- 

295 gc : `.GraphicsContextBase` 

296 The graphics context. 

297 points : (3, 2) array-like 

298 Array of (x, y) points for the triangle. 

299 colors : (3, 4) array-like 

300 RGBA colors for each point of the triangle. 

301 transform : `matplotlib.transforms.Transform` 

302 An affine transform to apply to the points. 

303 """ 

304 raise NotImplementedError 

305 

306 def draw_gouraud_triangles(self, gc, triangles_array, colors_array, 

307 transform): 

308 """ 

309 Draw a series of Gouraud triangles. 

310 

311 Parameters 

312 ---------- 

313 points : (N, 3, 2) array-like 

314 Array of *N* (x, y) points for the triangles. 

315 colors : (N, 3, 4) array-like 

316 Array of *N* RGBA colors for each point of the triangles. 

317 transform : `matplotlib.transforms.Transform` 

318 An affine transform to apply to the points. 

319 """ 

320 transform = transform.frozen() 

321 for tri, col in zip(triangles_array, colors_array): 

322 self.draw_gouraud_triangle(gc, tri, col, transform) 

323 

324 def _iter_collection_raw_paths(self, master_transform, paths, 

325 all_transforms): 

326 """ 

327 Helper method (along with `_iter_collection`) to implement 

328 `draw_path_collection` in a memory-efficient manner. 

329 

330 This method yields all of the base path/transform combinations, given a 

331 master transform, a list of paths and list of transforms. 

332 

333 The arguments should be exactly what is passed in to 

334 `draw_path_collection`. 

335 

336 The backend should take each yielded path and transform and create an 

337 object that can be referenced (reused) later. 

338 """ 

339 Npaths = len(paths) 

340 Ntransforms = len(all_transforms) 

341 N = max(Npaths, Ntransforms) 

342 

343 if Npaths == 0: 

344 return 

345 

346 transform = transforms.IdentityTransform() 

347 for i in range(N): 

348 path = paths[i % Npaths] 

349 if Ntransforms: 

350 transform = Affine2D(all_transforms[i % Ntransforms]) 

351 yield path, transform + master_transform 

352 

353 def _iter_collection_uses_per_path(self, paths, all_transforms, 

354 offsets, facecolors, edgecolors): 

355 """ 

356 Compute how many times each raw path object returned by 

357 `_iter_collection_raw_paths` would be used when calling 

358 `_iter_collection`. This is intended for the backend to decide 

359 on the tradeoff between using the paths in-line and storing 

360 them once and reusing. Rounds up in case the number of uses 

361 is not the same for every path. 

362 """ 

363 Npaths = len(paths) 

364 if Npaths == 0 or len(facecolors) == len(edgecolors) == 0: 

365 return 0 

366 Npath_ids = max(Npaths, len(all_transforms)) 

367 N = max(Npath_ids, len(offsets)) 

368 return (N + Npath_ids - 1) // Npath_ids 

369 

370 def _iter_collection(self, gc, path_ids, offsets, offset_trans, facecolors, 

371 edgecolors, linewidths, linestyles, 

372 antialiaseds, urls, offset_position): 

373 """ 

374 Helper method (along with `_iter_collection_raw_paths`) to implement 

375 `draw_path_collection` in a memory-efficient manner. 

376 

377 This method yields all of the path, offset and graphics context 

378 combinations to draw the path collection. The caller should already 

379 have looped over the results of `_iter_collection_raw_paths` to draw 

380 this collection. 

381 

382 The arguments should be the same as that passed into 

383 `draw_path_collection`, with the exception of *path_ids*, which is a 

384 list of arbitrary objects that the backend will use to reference one of 

385 the paths created in the `_iter_collection_raw_paths` stage. 

386 

387 Each yielded result is of the form:: 

388 

389 xo, yo, path_id, gc, rgbFace 

390 

391 where *xo*, *yo* is an offset; *path_id* is one of the elements of 

392 *path_ids*; *gc* is a graphics context and *rgbFace* is a color to 

393 use for filling the path. 

394 """ 

395 Npaths = len(path_ids) 

396 Noffsets = len(offsets) 

397 N = max(Npaths, Noffsets) 

398 Nfacecolors = len(facecolors) 

399 Nedgecolors = len(edgecolors) 

400 Nlinewidths = len(linewidths) 

401 Nlinestyles = len(linestyles) 

402 Nurls = len(urls) 

403 

404 if (Nfacecolors == 0 and Nedgecolors == 0) or Npaths == 0: 

405 return 

406 

407 gc0 = self.new_gc() 

408 gc0.copy_properties(gc) 

409 

410 def cycle_or_default(seq, default=None): 

411 # Cycle over *seq* if it is not empty; else always yield *default*. 

412 return (itertools.cycle(seq) if len(seq) 

413 else itertools.repeat(default)) 

414 

415 pathids = cycle_or_default(path_ids) 

416 toffsets = cycle_or_default(offset_trans.transform(offsets), (0, 0)) 

417 fcs = cycle_or_default(facecolors) 

418 ecs = cycle_or_default(edgecolors) 

419 lws = cycle_or_default(linewidths) 

420 lss = cycle_or_default(linestyles) 

421 aas = cycle_or_default(antialiaseds) 

422 urls = cycle_or_default(urls) 

423 

424 if Nedgecolors == 0: 

425 gc0.set_linewidth(0.0) 

426 

427 for pathid, (xo, yo), fc, ec, lw, ls, aa, url in itertools.islice( 

428 zip(pathids, toffsets, fcs, ecs, lws, lss, aas, urls), N): 

429 if not (np.isfinite(xo) and np.isfinite(yo)): 

430 continue 

431 if Nedgecolors: 

432 if Nlinewidths: 

433 gc0.set_linewidth(lw) 

434 if Nlinestyles: 

435 gc0.set_dashes(*ls) 

436 if len(ec) == 4 and ec[3] == 0.0: 

437 gc0.set_linewidth(0) 

438 else: 

439 gc0.set_foreground(ec) 

440 if fc is not None and len(fc) == 4 and fc[3] == 0: 

441 fc = None 

442 gc0.set_antialiased(aa) 

443 if Nurls: 

444 gc0.set_url(url) 

445 yield xo, yo, pathid, gc0, fc 

446 gc0.restore() 

447 

448 def get_image_magnification(self): 

449 """ 

450 Get the factor by which to magnify images passed to `draw_image`. 

451 Allows a backend to have images at a different resolution to other 

452 artists. 

453 """ 

454 return 1.0 

455 

456 def draw_image(self, gc, x, y, im, transform=None): 

457 """ 

458 Draw an RGBA image. 

459 

460 Parameters 

461 ---------- 

462 gc : `.GraphicsContextBase` 

463 A graphics context with clipping information. 

464 

465 x : scalar 

466 The distance in physical units (i.e., dots or pixels) from the left 

467 hand side of the canvas. 

468 

469 y : scalar 

470 The distance in physical units (i.e., dots or pixels) from the 

471 bottom side of the canvas. 

472 

473 im : (N, M, 4) array-like of np.uint8 

474 An array of RGBA pixels. 

475 

476 transform : `matplotlib.transforms.Affine2DBase` 

477 If and only if the concrete backend is written such that 

478 `option_scale_image` returns ``True``, an affine transformation 

479 (i.e., an `.Affine2DBase`) *may* be passed to `draw_image`. The 

480 translation vector of the transformation is given in physical units 

481 (i.e., dots or pixels). Note that the transformation does not 

482 override *x* and *y*, and has to be applied *before* translating 

483 the result by *x* and *y* (this can be accomplished by adding *x* 

484 and *y* to the translation vector defined by *transform*). 

485 """ 

486 raise NotImplementedError 

487 

488 def option_image_nocomposite(self): 

489 """ 

490 Return whether image composition by Matplotlib should be skipped. 

491 

492 Raster backends should usually return False (letting the C-level 

493 rasterizer take care of image composition); vector backends should 

494 usually return ``not rcParams["image.composite_image"]``. 

495 """ 

496 return False 

497 

498 def option_scale_image(self): 

499 """ 

500 Return whether arbitrary affine transformations in `draw_image` are 

501 supported (True for most vector backends). 

502 """ 

503 return False 

504 

505 def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): 

506 """ 

507 """ 

508 self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX") 

509 

510 def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): 

511 """ 

512 Draw a text instance. 

513 

514 Parameters 

515 ---------- 

516 gc : `.GraphicsContextBase` 

517 The graphics context. 

518 x : float 

519 The x location of the text in display coords. 

520 y : float 

521 The y location of the text baseline in display coords. 

522 s : str 

523 The text string. 

524 prop : `matplotlib.font_manager.FontProperties` 

525 The font properties. 

526 angle : float 

527 The rotation angle in degrees anti-clockwise. 

528 mtext : `matplotlib.text.Text` 

529 The original text object to be rendered. 

530 

531 Notes 

532 ----- 

533 **Note for backend implementers:** 

534 

535 When you are trying to determine if you have gotten your bounding box 

536 right (which is what enables the text layout/alignment to work 

537 properly), it helps to change the line in text.py:: 

538 

539 if 0: bbox_artist(self, renderer) 

540 

541 to if 1, and then the actual bounding box will be plotted along with 

542 your text. 

543 """ 

544 

545 self._draw_text_as_path(gc, x, y, s, prop, angle, ismath) 

546 

547 def _get_text_path_transform(self, x, y, s, prop, angle, ismath): 

548 """ 

549 Return the text path and transform. 

550 

551 Parameters 

552 ---------- 

553 prop : `matplotlib.font_manager.FontProperties` 

554 The font property. 

555 s : str 

556 The text to be converted. 

557 ismath : bool or "TeX" 

558 If True, use mathtext parser. If "TeX", use *usetex* mode. 

559 """ 

560 

561 text2path = self._text2path 

562 fontsize = self.points_to_pixels(prop.get_size_in_points()) 

563 verts, codes = text2path.get_text_path(prop, s, ismath=ismath) 

564 

565 path = Path(verts, codes) 

566 angle = np.deg2rad(angle) 

567 if self.flipy(): 

568 width, height = self.get_canvas_width_height() 

569 transform = (Affine2D() 

570 .scale(fontsize / text2path.FONT_SCALE) 

571 .rotate(angle) 

572 .translate(x, height - y)) 

573 else: 

574 transform = (Affine2D() 

575 .scale(fontsize / text2path.FONT_SCALE) 

576 .rotate(angle) 

577 .translate(x, y)) 

578 

579 return path, transform 

580 

581 def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath): 

582 """ 

583 Draw the text by converting them to paths using textpath module. 

584 

585 Parameters 

586 ---------- 

587 prop : `matplotlib.font_manager.FontProperties` 

588 The font property. 

589 s : str 

590 The text to be converted. 

591 usetex : bool 

592 Whether to use usetex mode. 

593 ismath : bool or "TeX" 

594 If True, use mathtext parser. If "TeX", use *usetex* mode. 

595 """ 

596 path, transform = self._get_text_path_transform( 

597 x, y, s, prop, angle, ismath) 

598 color = gc.get_rgb() 

599 gc.set_linewidth(0.0) 

600 self.draw_path(gc, path, transform, rgbFace=color) 

601 

602 def get_text_width_height_descent(self, s, prop, ismath): 

603 """ 

604 Get the width, height, and descent (offset from the bottom 

605 to the baseline), in display coords, of the string *s* with 

606 `.FontProperties` *prop*. 

607 """ 

608 fontsize = prop.get_size_in_points() 

609 

610 if ismath == 'TeX': 

611 # todo: handle props 

612 return TexManager().get_text_width_height_descent( 

613 s, fontsize, renderer=self) 

614 

615 dpi = self.points_to_pixels(72) 

616 if ismath: 

617 dims = self._text2path.mathtext_parser.parse(s, dpi, prop) 

618 return dims[0:3] # return width, height, descent 

619 

620 flags = self._text2path._get_hinting_flag() 

621 font = self._text2path._get_font(prop) 

622 font.set_size(fontsize, dpi) 

623 # the width and height of unrotated string 

624 font.set_text(s, 0.0, flags=flags) 

625 w, h = font.get_width_height() 

626 d = font.get_descent() 

627 w /= 64.0 # convert from subpixels 

628 h /= 64.0 

629 d /= 64.0 

630 return w, h, d 

631 

632 def flipy(self): 

633 """ 

634 Return whether y values increase from top to bottom. 

635 

636 Note that this only affects drawing of texts. 

637 """ 

638 return True 

639 

640 def get_canvas_width_height(self): 

641 """Return the canvas width and height in display coords.""" 

642 return 1, 1 

643 

644 def get_texmanager(self): 

645 """Return the `.TexManager` instance.""" 

646 if self._texmanager is None: 

647 self._texmanager = TexManager() 

648 return self._texmanager 

649 

650 def new_gc(self): 

651 """Return an instance of a `.GraphicsContextBase`.""" 

652 return GraphicsContextBase() 

653 

654 def points_to_pixels(self, points): 

655 """ 

656 Convert points to display units. 

657 

658 You need to override this function (unless your backend 

659 doesn't have a dpi, e.g., postscript or svg). Some imaging 

660 systems assume some value for pixels per inch:: 

661 

662 points to pixels = points * pixels_per_inch/72 * dpi/72 

663 

664 Parameters 

665 ---------- 

666 points : float or array-like 

667 a float or a numpy array of float 

668 

669 Returns 

670 ------- 

671 Points converted to pixels 

672 """ 

673 return points 

674 

675 def start_rasterizing(self): 

676 """ 

677 Switch to the raster renderer. 

678 

679 Used by `.MixedModeRenderer`. 

680 """ 

681 

682 def stop_rasterizing(self): 

683 """ 

684 Switch back to the vector renderer and draw the contents of the raster 

685 renderer as an image on the vector renderer. 

686 

687 Used by `.MixedModeRenderer`. 

688 """ 

689 

690 def start_filter(self): 

691 """ 

692 Switch to a temporary renderer for image filtering effects. 

693 

694 Currently only supported by the agg renderer. 

695 """ 

696 

697 def stop_filter(self, filter_func): 

698 """ 

699 Switch back to the original renderer. The contents of the temporary 

700 renderer is processed with the *filter_func* and is drawn on the 

701 original renderer as an image. 

702 

703 Currently only supported by the agg renderer. 

704 """ 

705 

706 def _draw_disabled(self): 

707 """ 

708 Context manager to temporary disable drawing. 

709 

710 This is used for getting the drawn size of Artists. This lets us 

711 run the draw process to update any Python state but does not pay the 

712 cost of the draw_XYZ calls on the canvas. 

713 """ 

714 no_ops = { 

715 meth_name: lambda *args, **kwargs: None 

716 for meth_name in dir(RendererBase) 

717 if (meth_name.startswith("draw_") 

718 or meth_name in ["open_group", "close_group"]) 

719 } 

720 

721 return _setattr_cm(self, **no_ops) 

722 

723 

724class GraphicsContextBase: 

725 """An abstract base class that provides color, line styles, etc.""" 

726 

727 def __init__(self): 

728 self._alpha = 1.0 

729 self._forced_alpha = False # if True, _alpha overrides A from RGBA 

730 self._antialiased = 1 # use 0, 1 not True, False for extension code 

731 self._capstyle = CapStyle('butt') 

732 self._cliprect = None 

733 self._clippath = None 

734 self._dashes = 0, None 

735 self._joinstyle = JoinStyle('round') 

736 self._linestyle = 'solid' 

737 self._linewidth = 1 

738 self._rgb = (0.0, 0.0, 0.0, 1.0) 

739 self._hatch = None 

740 self._hatch_color = colors.to_rgba(rcParams['hatch.color']) 

741 self._hatch_linewidth = rcParams['hatch.linewidth'] 

742 self._url = None 

743 self._gid = None 

744 self._snap = None 

745 self._sketch = None 

746 

747 def copy_properties(self, gc): 

748 """Copy properties from *gc* to self.""" 

749 self._alpha = gc._alpha 

750 self._forced_alpha = gc._forced_alpha 

751 self._antialiased = gc._antialiased 

752 self._capstyle = gc._capstyle 

753 self._cliprect = gc._cliprect 

754 self._clippath = gc._clippath 

755 self._dashes = gc._dashes 

756 self._joinstyle = gc._joinstyle 

757 self._linestyle = gc._linestyle 

758 self._linewidth = gc._linewidth 

759 self._rgb = gc._rgb 

760 self._hatch = gc._hatch 

761 self._hatch_color = gc._hatch_color 

762 self._hatch_linewidth = gc._hatch_linewidth 

763 self._url = gc._url 

764 self._gid = gc._gid 

765 self._snap = gc._snap 

766 self._sketch = gc._sketch 

767 

768 def restore(self): 

769 """ 

770 Restore the graphics context from the stack - needed only 

771 for backends that save graphics contexts on a stack. 

772 """ 

773 

774 def get_alpha(self): 

775 """ 

776 Return the alpha value used for blending - not supported on all 

777 backends. 

778 """ 

779 return self._alpha 

780 

781 def get_antialiased(self): 

782 """Return whether the object should try to do antialiased rendering.""" 

783 return self._antialiased 

784 

785 def get_capstyle(self): 

786 """Return the `.CapStyle`.""" 

787 return self._capstyle.name 

788 

789 def get_clip_rectangle(self): 

790 """ 

791 Return the clip rectangle as a `~matplotlib.transforms.Bbox` instance. 

792 """ 

793 return self._cliprect 

794 

795 def get_clip_path(self): 

796 """ 

797 Return the clip path in the form (path, transform), where path 

798 is a `~.path.Path` instance, and transform is 

799 an affine transform to apply to the path before clipping. 

800 """ 

801 if self._clippath is not None: 

802 tpath, tr = self._clippath.get_transformed_path_and_affine() 

803 if np.all(np.isfinite(tpath.vertices)): 

804 return tpath, tr 

805 else: 

806 _log.warning("Ill-defined clip_path detected. Returning None.") 

807 return None, None 

808 return None, None 

809 

810 def get_dashes(self): 

811 """ 

812 Return the dash style as an (offset, dash-list) pair. 

813 

814 See `.set_dashes` for details. 

815 

816 Default value is (None, None). 

817 """ 

818 return self._dashes 

819 

820 def get_forced_alpha(self): 

821 """ 

822 Return whether the value given by get_alpha() should be used to 

823 override any other alpha-channel values. 

824 """ 

825 return self._forced_alpha 

826 

827 def get_joinstyle(self): 

828 """Return the `.JoinStyle`.""" 

829 return self._joinstyle.name 

830 

831 def get_linewidth(self): 

832 """Return the line width in points.""" 

833 return self._linewidth 

834 

835 def get_rgb(self): 

836 """Return a tuple of three or four floats from 0-1.""" 

837 return self._rgb 

838 

839 def get_url(self): 

840 """Return a url if one is set, None otherwise.""" 

841 return self._url 

842 

843 def get_gid(self): 

844 """Return the object identifier if one is set, None otherwise.""" 

845 return self._gid 

846 

847 def get_snap(self): 

848 """ 

849 Return the snap setting, which can be: 

850 

851 * True: snap vertices to the nearest pixel center 

852 * False: leave vertices as-is 

853 * None: (auto) If the path contains only rectilinear line segments, 

854 round to the nearest pixel center 

855 """ 

856 return self._snap 

857 

858 def set_alpha(self, alpha): 

859 """ 

860 Set the alpha value used for blending - not supported on all backends. 

861 

862 If ``alpha=None`` (the default), the alpha components of the 

863 foreground and fill colors will be used to set their respective 

864 transparencies (where applicable); otherwise, ``alpha`` will override 

865 them. 

866 """ 

867 if alpha is not None: 

868 self._alpha = alpha 

869 self._forced_alpha = True 

870 else: 

871 self._alpha = 1.0 

872 self._forced_alpha = False 

873 self.set_foreground(self._rgb, isRGBA=True) 

874 

875 def set_antialiased(self, b): 

876 """Set whether object should be drawn with antialiased rendering.""" 

877 # Use ints to make life easier on extension code trying to read the gc. 

878 self._antialiased = int(bool(b)) 

879 

880 @_docstring.interpd 

881 def set_capstyle(self, cs): 

882 """ 

883 Set how to draw endpoints of lines. 

884 

885 Parameters 

886 ---------- 

887 cs : `.CapStyle` or %(CapStyle)s 

888 """ 

889 self._capstyle = CapStyle(cs) 

890 

891 def set_clip_rectangle(self, rectangle): 

892 """Set the clip rectangle to a `.Bbox` or None.""" 

893 self._cliprect = rectangle 

894 

895 def set_clip_path(self, path): 

896 """Set the clip path to a `.TransformedPath` or None.""" 

897 _api.check_isinstance((transforms.TransformedPath, None), path=path) 

898 self._clippath = path 

899 

900 def set_dashes(self, dash_offset, dash_list): 

901 """ 

902 Set the dash style for the gc. 

903 

904 Parameters 

905 ---------- 

906 dash_offset : float 

907 Distance, in points, into the dash pattern at which to 

908 start the pattern. It is usually set to 0. 

909 dash_list : array-like or None 

910 The on-off sequence as points. None specifies a solid line. All 

911 values must otherwise be non-negative (:math:`\\ge 0`). 

912 

913 Notes 

914 ----- 

915 See p. 666 of the PostScript 

916 `Language Reference 

917 <https://www.adobe.com/jp/print/postscript/pdfs/PLRM.pdf>`_ 

918 for more info. 

919 """ 

920 if dash_list is not None: 

921 dl = np.asarray(dash_list) 

922 if np.any(dl < 0.0): 

923 raise ValueError( 

924 "All values in the dash list must be non-negative") 

925 if dl.size and not np.any(dl > 0.0): 

926 raise ValueError( 

927 'At least one value in the dash list must be positive') 

928 self._dashes = dash_offset, dash_list 

929 

930 def set_foreground(self, fg, isRGBA=False): 

931 """ 

932 Set the foreground color. 

933 

934 Parameters 

935 ---------- 

936 fg : color 

937 isRGBA : bool 

938 If *fg* is known to be an ``(r, g, b, a)`` tuple, *isRGBA* can be 

939 set to True to improve performance. 

940 """ 

941 if self._forced_alpha and isRGBA: 

942 self._rgb = fg[:3] + (self._alpha,) 

943 elif self._forced_alpha: 

944 self._rgb = colors.to_rgba(fg, self._alpha) 

945 elif isRGBA: 

946 self._rgb = fg 

947 else: 

948 self._rgb = colors.to_rgba(fg) 

949 

950 @_docstring.interpd 

951 def set_joinstyle(self, js): 

952 """ 

953 Set how to draw connections between line segments. 

954 

955 Parameters 

956 ---------- 

957 js : `.JoinStyle` or %(JoinStyle)s 

958 """ 

959 self._joinstyle = JoinStyle(js) 

960 

961 def set_linewidth(self, w): 

962 """Set the linewidth in points.""" 

963 self._linewidth = float(w) 

964 

965 def set_url(self, url): 

966 """Set the url for links in compatible backends.""" 

967 self._url = url 

968 

969 def set_gid(self, id): 

970 """Set the id.""" 

971 self._gid = id 

972 

973 def set_snap(self, snap): 

974 """ 

975 Set the snap setting which may be: 

976 

977 * True: snap vertices to the nearest pixel center 

978 * False: leave vertices as-is 

979 * None: (auto) If the path contains only rectilinear line segments, 

980 round to the nearest pixel center 

981 """ 

982 self._snap = snap 

983 

984 def set_hatch(self, hatch): 

985 """Set the hatch style (for fills).""" 

986 self._hatch = hatch 

987 

988 def get_hatch(self): 

989 """Get the current hatch style.""" 

990 return self._hatch 

991 

992 def get_hatch_path(self, density=6.0): 

993 """Return a `.Path` for the current hatch.""" 

994 hatch = self.get_hatch() 

995 if hatch is None: 

996 return None 

997 return Path.hatch(hatch, density) 

998 

999 def get_hatch_color(self): 

1000 """Get the hatch color.""" 

1001 return self._hatch_color 

1002 

1003 def set_hatch_color(self, hatch_color): 

1004 """Set the hatch color.""" 

1005 self._hatch_color = hatch_color 

1006 

1007 def get_hatch_linewidth(self): 

1008 """Get the hatch linewidth.""" 

1009 return self._hatch_linewidth 

1010 

1011 def get_sketch_params(self): 

1012 """ 

1013 Return the sketch parameters for the artist. 

1014 

1015 Returns 

1016 ------- 

1017 tuple or `None` 

1018 

1019 A 3-tuple with the following elements: 

1020 

1021 * ``scale``: The amplitude of the wiggle perpendicular to the 

1022 source line. 

1023 * ``length``: The length of the wiggle along the line. 

1024 * ``randomness``: The scale factor by which the length is 

1025 shrunken or expanded. 

1026 

1027 May return `None` if no sketch parameters were set. 

1028 """ 

1029 return self._sketch 

1030 

1031 def set_sketch_params(self, scale=None, length=None, randomness=None): 

1032 """ 

1033 Set the sketch parameters. 

1034 

1035 Parameters 

1036 ---------- 

1037 scale : float, optional 

1038 The amplitude of the wiggle perpendicular to the source line, in 

1039 pixels. If scale is `None`, or not provided, no sketch filter will 

1040 be provided. 

1041 length : float, default: 128 

1042 The length of the wiggle along the line, in pixels. 

1043 randomness : float, default: 16 

1044 The scale factor by which the length is shrunken or expanded. 

1045 """ 

1046 self._sketch = ( 

1047 None if scale is None 

1048 else (scale, length or 128., randomness or 16.)) 

1049 

1050 

1051class TimerBase: 

1052 """ 

1053 A base class for providing timer events, useful for things animations. 

1054 Backends need to implement a few specific methods in order to use their 

1055 own timing mechanisms so that the timer events are integrated into their 

1056 event loops. 

1057 

1058 Subclasses must override the following methods: 

1059 

1060 - ``_timer_start``: Backend-specific code for starting the timer. 

1061 - ``_timer_stop``: Backend-specific code for stopping the timer. 

1062 

1063 Subclasses may additionally override the following methods: 

1064 

1065 - ``_timer_set_single_shot``: Code for setting the timer to single shot 

1066 operating mode, if supported by the timer object. If not, the `Timer` 

1067 class itself will store the flag and the ``_on_timer`` method should be 

1068 overridden to support such behavior. 

1069 

1070 - ``_timer_set_interval``: Code for setting the interval on the timer, if 

1071 there is a method for doing so on the timer object. 

1072 

1073 - ``_on_timer``: The internal function that any timer object should call, 

1074 which will handle the task of running all callbacks that have been set. 

1075 """ 

1076 

1077 def __init__(self, interval=None, callbacks=None): 

1078 """ 

1079 Parameters 

1080 ---------- 

1081 interval : int, default: 1000ms 

1082 The time between timer events in milliseconds. Will be stored as 

1083 ``timer.interval``. 

1084 callbacks : list[tuple[callable, tuple, dict]] 

1085 List of (func, args, kwargs) tuples that will be called upon 

1086 timer events. This list is accessible as ``timer.callbacks`` and 

1087 can be manipulated directly, or the functions `add_callback` and 

1088 `remove_callback` can be used. 

1089 """ 

1090 self.callbacks = [] if callbacks is None else callbacks.copy() 

1091 # Set .interval and not ._interval to go through the property setter. 

1092 self.interval = 1000 if interval is None else interval 

1093 self.single_shot = False 

1094 

1095 def __del__(self): 

1096 """Need to stop timer and possibly disconnect timer.""" 

1097 self._timer_stop() 

1098 

1099 def start(self, interval=None): 

1100 """ 

1101 Start the timer object. 

1102 

1103 Parameters 

1104 ---------- 

1105 interval : int, optional 

1106 Timer interval in milliseconds; overrides a previously set interval 

1107 if provided. 

1108 """ 

1109 if interval is not None: 

1110 self.interval = interval 

1111 self._timer_start() 

1112 

1113 def stop(self): 

1114 """Stop the timer.""" 

1115 self._timer_stop() 

1116 

1117 def _timer_start(self): 

1118 pass 

1119 

1120 def _timer_stop(self): 

1121 pass 

1122 

1123 @property 

1124 def interval(self): 

1125 """The time between timer events, in milliseconds.""" 

1126 return self._interval 

1127 

1128 @interval.setter 

1129 def interval(self, interval): 

1130 # Force to int since none of the backends actually support fractional 

1131 # milliseconds, and some error or give warnings. 

1132 interval = int(interval) 

1133 self._interval = interval 

1134 self._timer_set_interval() 

1135 

1136 @property 

1137 def single_shot(self): 

1138 """Whether this timer should stop after a single run.""" 

1139 return self._single 

1140 

1141 @single_shot.setter 

1142 def single_shot(self, ss): 

1143 self._single = ss 

1144 self._timer_set_single_shot() 

1145 

1146 def add_callback(self, func, *args, **kwargs): 

1147 """ 

1148 Register *func* to be called by timer when the event fires. Any 

1149 additional arguments provided will be passed to *func*. 

1150 

1151 This function returns *func*, which makes it possible to use it as a 

1152 decorator. 

1153 """ 

1154 self.callbacks.append((func, args, kwargs)) 

1155 return func 

1156 

1157 def remove_callback(self, func, *args, **kwargs): 

1158 """ 

1159 Remove *func* from list of callbacks. 

1160 

1161 *args* and *kwargs* are optional and used to distinguish between copies 

1162 of the same function registered to be called with different arguments. 

1163 This behavior is deprecated. In the future, ``*args, **kwargs`` won't 

1164 be considered anymore; to keep a specific callback removable by itself, 

1165 pass it to `add_callback` as a `functools.partial` object. 

1166 """ 

1167 if args or kwargs: 

1168 _api.warn_deprecated( 

1169 "3.1", message="In a future version, Timer.remove_callback " 

1170 "will not take *args, **kwargs anymore, but remove all " 

1171 "callbacks where the callable matches; to keep a specific " 

1172 "callback removable by itself, pass it to add_callback as a " 

1173 "functools.partial object.") 

1174 self.callbacks.remove((func, args, kwargs)) 

1175 else: 

1176 funcs = [c[0] for c in self.callbacks] 

1177 if func in funcs: 

1178 self.callbacks.pop(funcs.index(func)) 

1179 

1180 def _timer_set_interval(self): 

1181 """Used to set interval on underlying timer object.""" 

1182 

1183 def _timer_set_single_shot(self): 

1184 """Used to set single shot on underlying timer object.""" 

1185 

1186 def _on_timer(self): 

1187 """ 

1188 Runs all function that have been registered as callbacks. Functions 

1189 can return False (or 0) if they should not be called any more. If there 

1190 are no callbacks, the timer is automatically stopped. 

1191 """ 

1192 for func, args, kwargs in self.callbacks: 

1193 ret = func(*args, **kwargs) 

1194 # docstring above explains why we use `if ret == 0` here, 

1195 # instead of `if not ret`. 

1196 # This will also catch `ret == False` as `False == 0` 

1197 # but does not annoy the linters 

1198 # https://docs.python.org/3/library/stdtypes.html#boolean-values 

1199 if ret == 0: 

1200 self.callbacks.remove((func, args, kwargs)) 

1201 

1202 if len(self.callbacks) == 0: 

1203 self.stop() 

1204 

1205 

1206class Event: 

1207 """ 

1208 A Matplotlib event. 

1209 

1210 The following attributes are defined and shown with their default values. 

1211 Subclasses may define additional attributes. 

1212 

1213 Attributes 

1214 ---------- 

1215 name : str 

1216 The event name. 

1217 canvas : `FigureCanvasBase` 

1218 The backend-specific canvas instance generating the event. 

1219 guiEvent 

1220 The GUI event that triggered the Matplotlib event. 

1221 """ 

1222 

1223 def __init__(self, name, canvas, guiEvent=None): 

1224 self.name = name 

1225 self.canvas = canvas 

1226 self.guiEvent = guiEvent 

1227 

1228 def _process(self): 

1229 """Generate an event with name ``self.name`` on ``self.canvas``.""" 

1230 self.canvas.callbacks.process(self.name, self) 

1231 

1232 

1233class DrawEvent(Event): 

1234 """ 

1235 An event triggered by a draw operation on the canvas. 

1236 

1237 In most backends, callbacks subscribed to this event will be fired after 

1238 the rendering is complete but before the screen is updated. Any extra 

1239 artists drawn to the canvas's renderer will be reflected without an 

1240 explicit call to ``blit``. 

1241 

1242 .. warning:: 

1243 

1244 Calling ``canvas.draw`` and ``canvas.blit`` in these callbacks may 

1245 not be safe with all backends and may cause infinite recursion. 

1246 

1247 A DrawEvent has a number of special attributes in addition to those defined 

1248 by the parent `Event` class. 

1249 

1250 Attributes 

1251 ---------- 

1252 renderer : `RendererBase` 

1253 The renderer for the draw event. 

1254 """ 

1255 def __init__(self, name, canvas, renderer): 

1256 super().__init__(name, canvas) 

1257 self.renderer = renderer 

1258 

1259 

1260class ResizeEvent(Event): 

1261 """ 

1262 An event triggered by a canvas resize. 

1263 

1264 A ResizeEvent has a number of special attributes in addition to those 

1265 defined by the parent `Event` class. 

1266 

1267 Attributes 

1268 ---------- 

1269 width : int 

1270 Width of the canvas in pixels. 

1271 height : int 

1272 Height of the canvas in pixels. 

1273 """ 

1274 

1275 def __init__(self, name, canvas): 

1276 super().__init__(name, canvas) 

1277 self.width, self.height = canvas.get_width_height() 

1278 

1279 

1280class CloseEvent(Event): 

1281 """An event triggered by a figure being closed.""" 

1282 

1283 

1284class LocationEvent(Event): 

1285 """ 

1286 An event that has a screen location. 

1287 

1288 A LocationEvent has a number of special attributes in addition to those 

1289 defined by the parent `Event` class. 

1290 

1291 Attributes 

1292 ---------- 

1293 x, y : int or None 

1294 Event location in pixels from bottom left of canvas. 

1295 inaxes : `~.axes.Axes` or None 

1296 The `~.axes.Axes` instance over which the mouse is, if any. 

1297 xdata, ydata : float or None 

1298 Data coordinates of the mouse within *inaxes*, or *None* if the mouse 

1299 is not over an Axes. 

1300 """ 

1301 

1302 lastevent = None # The last event processed so far. 

1303 

1304 def __init__(self, name, canvas, x, y, guiEvent=None): 

1305 super().__init__(name, canvas, guiEvent=guiEvent) 

1306 # x position - pixels from left of canvas 

1307 self.x = int(x) if x is not None else x 

1308 # y position - pixels from right of canvas 

1309 self.y = int(y) if y is not None else y 

1310 self.inaxes = None # the Axes instance the mouse is over 

1311 self.xdata = None # x coord of mouse in data coords 

1312 self.ydata = None # y coord of mouse in data coords 

1313 

1314 if x is None or y is None: 

1315 # cannot check if event was in Axes if no (x, y) info 

1316 return 

1317 

1318 if self.canvas.mouse_grabber is None: 

1319 self.inaxes = self.canvas.inaxes((x, y)) 

1320 else: 

1321 self.inaxes = self.canvas.mouse_grabber 

1322 

1323 if self.inaxes is not None: 

1324 try: 

1325 trans = self.inaxes.transData.inverted() 

1326 xdata, ydata = trans.transform((x, y)) 

1327 except ValueError: 

1328 pass 

1329 else: 

1330 self.xdata = xdata 

1331 self.ydata = ydata 

1332 

1333 

1334class MouseButton(IntEnum): 

1335 LEFT = 1 

1336 MIDDLE = 2 

1337 RIGHT = 3 

1338 BACK = 8 

1339 FORWARD = 9 

1340 

1341 

1342class MouseEvent(LocationEvent): 

1343 """ 

1344 A mouse event ('button_press_event', 'button_release_event', \ 

1345'scroll_event', 'motion_notify_event'). 

1346 

1347 A MouseEvent has a number of special attributes in addition to those 

1348 defined by the parent `Event` and `LocationEvent` classes. 

1349 

1350 Attributes 

1351 ---------- 

1352 button : None or `MouseButton` or {'up', 'down'} 

1353 The button pressed. 'up' and 'down' are used for scroll events. 

1354 

1355 Note that LEFT and RIGHT actually refer to the "primary" and 

1356 "secondary" buttons, i.e. if the user inverts their left and right 

1357 buttons ("left-handed setting") then the LEFT button will be the one 

1358 physically on the right. 

1359 

1360 If this is unset, *name* is "scroll_event", and *step* is nonzero, then 

1361 this will be set to "up" or "down" depending on the sign of *step*. 

1362 

1363 key : None or str 

1364 The key pressed when the mouse event triggered, e.g. 'shift'. 

1365 See `KeyEvent`. 

1366 

1367 .. warning:: 

1368 This key is currently obtained from the last 'key_press_event' or 

1369 'key_release_event' that occurred within the canvas. Thus, if the 

1370 last change of keyboard state occurred while the canvas did not have 

1371 focus, this attribute will be wrong. 

1372 

1373 step : float 

1374 The number of scroll steps (positive for 'up', negative for 'down'). 

1375 This applies only to 'scroll_event' and defaults to 0 otherwise. 

1376 

1377 dblclick : bool 

1378 Whether the event is a double-click. This applies only to 

1379 'button_press_event' and is False otherwise. In particular, it's 

1380 not used in 'button_release_event'. 

1381 

1382 Examples 

1383 -------- 

1384 :: 

1385 

1386 def on_press(event): 

1387 print('you pressed', event.button, event.xdata, event.ydata) 

1388 

1389 cid = fig.canvas.mpl_connect('button_press_event', on_press) 

1390 """ 

1391 

1392 def __init__(self, name, canvas, x, y, button=None, key=None, 

1393 step=0, dblclick=False, guiEvent=None): 

1394 super().__init__(name, canvas, x, y, guiEvent=guiEvent) 

1395 if button in MouseButton.__members__.values(): 

1396 button = MouseButton(button) 

1397 if name == "scroll_event" and button is None: 

1398 if step > 0: 

1399 button = "up" 

1400 elif step < 0: 

1401 button = "down" 

1402 self.button = button 

1403 self.key = key 

1404 self.step = step 

1405 self.dblclick = dblclick 

1406 

1407 def __str__(self): 

1408 return (f"{self.name}: " 

1409 f"xy=({self.x}, {self.y}) xydata=({self.xdata}, {self.ydata}) " 

1410 f"button={self.button} dblclick={self.dblclick} " 

1411 f"inaxes={self.inaxes}") 

1412 

1413 

1414class PickEvent(Event): 

1415 """ 

1416 A pick event. 

1417 

1418 This event is fired when the user picks a location on the canvas 

1419 sufficiently close to an artist that has been made pickable with 

1420 `.Artist.set_picker`. 

1421 

1422 A PickEvent has a number of special attributes in addition to those defined 

1423 by the parent `Event` class. 

1424 

1425 Attributes 

1426 ---------- 

1427 mouseevent : `MouseEvent` 

1428 The mouse event that generated the pick. 

1429 artist : `matplotlib.artist.Artist` 

1430 The picked artist. Note that artists are not pickable by default 

1431 (see `.Artist.set_picker`). 

1432 other 

1433 Additional attributes may be present depending on the type of the 

1434 picked object; e.g., a `.Line2D` pick may define different extra 

1435 attributes than a `.PatchCollection` pick. 

1436 

1437 Examples 

1438 -------- 

1439 Bind a function ``on_pick()`` to pick events, that prints the coordinates 

1440 of the picked data point:: 

1441 

1442 ax.plot(np.rand(100), 'o', picker=5) # 5 points tolerance 

1443 

1444 def on_pick(event): 

1445 line = event.artist 

1446 xdata, ydata = line.get_data() 

1447 ind = event.ind 

1448 print('on pick line:', np.array([xdata[ind], ydata[ind]]).T) 

1449 

1450 cid = fig.canvas.mpl_connect('pick_event', on_pick) 

1451 """ 

1452 

1453 def __init__(self, name, canvas, mouseevent, artist, 

1454 guiEvent=None, **kwargs): 

1455 if guiEvent is None: 

1456 guiEvent = mouseevent.guiEvent 

1457 super().__init__(name, canvas, guiEvent) 

1458 self.mouseevent = mouseevent 

1459 self.artist = artist 

1460 self.__dict__.update(kwargs) 

1461 

1462 

1463class KeyEvent(LocationEvent): 

1464 """ 

1465 A key event (key press, key release). 

1466 

1467 A KeyEvent has a number of special attributes in addition to those defined 

1468 by the parent `Event` and `LocationEvent` classes. 

1469 

1470 Attributes 

1471 ---------- 

1472 key : None or str 

1473 The key(s) pressed. Could be *None*, a single case sensitive Unicode 

1474 character ("g", "G", "#", etc.), a special key ("control", "shift", 

1475 "f1", "up", etc.) or a combination of the above (e.g., "ctrl+alt+g", 

1476 "ctrl+alt+G"). 

1477 

1478 Notes 

1479 ----- 

1480 Modifier keys will be prefixed to the pressed key and will be in the order 

1481 "ctrl", "alt", "super". The exception to this rule is when the pressed key 

1482 is itself a modifier key, therefore "ctrl+alt" and "alt+control" can both 

1483 be valid key values. 

1484 

1485 Examples 

1486 -------- 

1487 :: 

1488 

1489 def on_key(event): 

1490 print('you pressed', event.key, event.xdata, event.ydata) 

1491 

1492 cid = fig.canvas.mpl_connect('key_press_event', on_key) 

1493 """ 

1494 

1495 def __init__(self, name, canvas, key, x=0, y=0, guiEvent=None): 

1496 super().__init__(name, canvas, x, y, guiEvent=guiEvent) 

1497 self.key = key 

1498 

1499 

1500# Default callback for key events. 

1501def _key_handler(event): 

1502 # Dead reckoning of key. 

1503 if event.name == "key_press_event": 

1504 event.canvas._key = event.key 

1505 elif event.name == "key_release_event": 

1506 event.canvas._key = None 

1507 

1508 

1509# Default callback for mouse events. 

1510def _mouse_handler(event): 

1511 # Dead-reckoning of button and key. 

1512 if event.name == "button_press_event": 

1513 event.canvas._button = event.button 

1514 elif event.name == "button_release_event": 

1515 event.canvas._button = None 

1516 elif event.name == "motion_notify_event" and event.button is None: 

1517 event.button = event.canvas._button 

1518 if event.key is None: 

1519 event.key = event.canvas._key 

1520 # Emit axes_enter/axes_leave. 

1521 if event.name == "motion_notify_event": 

1522 last = LocationEvent.lastevent 

1523 last_axes = last.inaxes if last is not None else None 

1524 if last_axes != event.inaxes: 

1525 if last_axes is not None: 

1526 try: 

1527 last.canvas.callbacks.process("axes_leave_event", last) 

1528 except Exception: 

1529 pass # The last canvas may already have been torn down. 

1530 if event.inaxes is not None: 

1531 event.canvas.callbacks.process("axes_enter_event", event) 

1532 LocationEvent.lastevent = ( 

1533 None if event.name == "figure_leave_event" else event) 

1534 

1535 

1536def _get_renderer(figure, print_method=None): 

1537 """ 

1538 Get the renderer that would be used to save a `.Figure`. 

1539 

1540 If you need a renderer without any active draw methods use 

1541 renderer._draw_disabled to temporary patch them out at your call site. 

1542 """ 

1543 # This is implemented by triggering a draw, then immediately jumping out of 

1544 # Figure.draw() by raising an exception. 

1545 

1546 class Done(Exception): 

1547 pass 

1548 

1549 def _draw(renderer): raise Done(renderer) 

1550 

1551 with cbook._setattr_cm(figure, draw=_draw), ExitStack() as stack: 

1552 if print_method is None: 

1553 fmt = figure.canvas.get_default_filetype() 

1554 # Even for a canvas' default output type, a canvas switch may be 

1555 # needed, e.g. for FigureCanvasBase. 

1556 print_method = stack.enter_context( 

1557 figure.canvas._switch_canvas_and_return_print_method(fmt)) 

1558 try: 

1559 print_method(io.BytesIO()) 

1560 except Done as exc: 

1561 renderer, = exc.args 

1562 return renderer 

1563 else: 

1564 raise RuntimeError(f"{print_method} did not call Figure.draw, so " 

1565 f"no renderer is available") 

1566 

1567 

1568def _no_output_draw(figure): 

1569 # _no_output_draw was promoted to the figure level, but 

1570 # keep this here in case someone was calling it... 

1571 figure.draw_without_rendering() 

1572 

1573 

1574def _is_non_interactive_terminal_ipython(ip): 

1575 """ 

1576 Return whether we are in a terminal IPython, but non interactive. 

1577 

1578 When in _terminal_ IPython, ip.parent will have and `interact` attribute, 

1579 if this attribute is False we do not setup eventloop integration as the 

1580 user will _not_ interact with IPython. In all other case (ZMQKernel, or is 

1581 interactive), we do. 

1582 """ 

1583 return (hasattr(ip, 'parent') 

1584 and (ip.parent is not None) 

1585 and getattr(ip.parent, 'interact', None) is False) 

1586 

1587 

1588class FigureCanvasBase: 

1589 """ 

1590 The canvas the figure renders into. 

1591 

1592 Attributes 

1593 ---------- 

1594 figure : `matplotlib.figure.Figure` 

1595 A high-level figure instance. 

1596 """ 

1597 

1598 # Set to one of {"qt", "gtk3", "gtk4", "wx", "tk", "macosx"} if an 

1599 # interactive framework is required, or None otherwise. 

1600 required_interactive_framework = None 

1601 

1602 # The manager class instantiated by new_manager. 

1603 # (This is defined as a classproperty because the manager class is 

1604 # currently defined *after* the canvas class, but one could also assign 

1605 # ``FigureCanvasBase.manager_class = FigureManagerBase`` 

1606 # after defining both classes.) 

1607 manager_class = _api.classproperty(lambda cls: FigureManagerBase) 

1608 

1609 events = [ 

1610 'resize_event', 

1611 'draw_event', 

1612 'key_press_event', 

1613 'key_release_event', 

1614 'button_press_event', 

1615 'button_release_event', 

1616 'scroll_event', 

1617 'motion_notify_event', 

1618 'pick_event', 

1619 'figure_enter_event', 

1620 'figure_leave_event', 

1621 'axes_enter_event', 

1622 'axes_leave_event', 

1623 'close_event' 

1624 ] 

1625 

1626 fixed_dpi = None 

1627 

1628 filetypes = _default_filetypes 

1629 

1630 @_api.classproperty 

1631 def supports_blit(cls): 

1632 """If this Canvas sub-class supports blitting.""" 

1633 return (hasattr(cls, "copy_from_bbox") 

1634 and hasattr(cls, "restore_region")) 

1635 

1636 def __init__(self, figure=None): 

1637 from matplotlib.figure import Figure 

1638 self._fix_ipython_backend2gui() 

1639 self._is_idle_drawing = True 

1640 self._is_saving = False 

1641 if figure is None: 

1642 figure = Figure() 

1643 figure.set_canvas(self) 

1644 self.figure = figure 

1645 self.manager = None 

1646 self.widgetlock = widgets.LockDraw() 

1647 self._button = None # the button pressed 

1648 self._key = None # the key pressed 

1649 self._lastx, self._lasty = None, None 

1650 self.mouse_grabber = None # the Axes currently grabbing mouse 

1651 self.toolbar = None # NavigationToolbar2 will set me 

1652 self._is_idle_drawing = False 

1653 # We don't want to scale up the figure DPI more than once. 

1654 figure._original_dpi = figure.dpi 

1655 self._device_pixel_ratio = 1 

1656 super().__init__() # Typically the GUI widget init (if any). 

1657 

1658 callbacks = property(lambda self: self.figure._canvas_callbacks) 

1659 button_pick_id = property(lambda self: self.figure._button_pick_id) 

1660 scroll_pick_id = property(lambda self: self.figure._scroll_pick_id) 

1661 

1662 @classmethod 

1663 @functools.lru_cache() 

1664 def _fix_ipython_backend2gui(cls): 

1665 # Fix hard-coded module -> toolkit mapping in IPython (used for 

1666 # `ipython --auto`). This cannot be done at import time due to 

1667 # ordering issues, so we do it when creating a canvas, and should only 

1668 # be done once per class (hence the `lru_cache(1)`). 

1669 if sys.modules.get("IPython") is None: 

1670 return 

1671 import IPython 

1672 ip = IPython.get_ipython() 

1673 if not ip: 

1674 return 

1675 from IPython.core import pylabtools as pt 

1676 if (not hasattr(pt, "backend2gui") 

1677 or not hasattr(ip, "enable_matplotlib")): 

1678 # In case we ever move the patch to IPython and remove these APIs, 

1679 # don't break on our side. 

1680 return 

1681 backend2gui_rif = { 

1682 "qt": "qt", 

1683 "gtk3": "gtk3", 

1684 "gtk4": "gtk4", 

1685 "wx": "wx", 

1686 "macosx": "osx", 

1687 }.get(cls.required_interactive_framework) 

1688 if backend2gui_rif: 

1689 if _is_non_interactive_terminal_ipython(ip): 

1690 ip.enable_gui(backend2gui_rif) 

1691 

1692 @classmethod 

1693 def new_manager(cls, figure, num): 

1694 """ 

1695 Create a new figure manager for *figure*, using this canvas class. 

1696 

1697 Notes 

1698 ----- 

1699 This method should not be reimplemented in subclasses. If 

1700 custom manager creation logic is needed, please reimplement 

1701 ``FigureManager.create_with_canvas``. 

1702 """ 

1703 return cls.manager_class.create_with_canvas(cls, figure, num) 

1704 

1705 @contextmanager 

1706 def _idle_draw_cntx(self): 

1707 self._is_idle_drawing = True 

1708 try: 

1709 yield 

1710 finally: 

1711 self._is_idle_drawing = False 

1712 

1713 def is_saving(self): 

1714 """ 

1715 Return whether the renderer is in the process of saving 

1716 to a file, rather than rendering for an on-screen buffer. 

1717 """ 

1718 return self._is_saving 

1719 

1720 @_api.deprecated("3.6", alternative="canvas.figure.pick") 

1721 def pick(self, mouseevent): 

1722 if not self.widgetlock.locked(): 

1723 self.figure.pick(mouseevent) 

1724 

1725 def blit(self, bbox=None): 

1726 """Blit the canvas in bbox (default entire canvas).""" 

1727 

1728 def resize(self, w, h): 

1729 """ 

1730 UNUSED: Set the canvas size in pixels. 

1731 

1732 Certain backends may implement a similar method internally, but this is 

1733 not a requirement of, nor is it used by, Matplotlib itself. 

1734 """ 

1735 # The entire method is actually deprecated, but we allow pass-through 

1736 # to a parent class to support e.g. QWidget.resize. 

1737 if hasattr(super(), "resize"): 

1738 return super().resize(w, h) 

1739 else: 

1740 _api.warn_deprecated("3.6", name="resize", obj_type="method", 

1741 alternative="FigureManagerBase.resize") 

1742 

1743 @_api.deprecated("3.6", alternative=( 

1744 "callbacks.process('draw_event', DrawEvent(...))")) 

1745 def draw_event(self, renderer): 

1746 """Pass a `DrawEvent` to all functions connected to ``draw_event``.""" 

1747 s = 'draw_event' 

1748 event = DrawEvent(s, self, renderer) 

1749 self.callbacks.process(s, event) 

1750 

1751 @_api.deprecated("3.6", alternative=( 

1752 "callbacks.process('resize_event', ResizeEvent(...))")) 

1753 def resize_event(self): 

1754 """ 

1755 Pass a `ResizeEvent` to all functions connected to ``resize_event``. 

1756 """ 

1757 s = 'resize_event' 

1758 event = ResizeEvent(s, self) 

1759 self.callbacks.process(s, event) 

1760 self.draw_idle() 

1761 

1762 @_api.deprecated("3.6", alternative=( 

1763 "callbacks.process('close_event', CloseEvent(...))")) 

1764 def close_event(self, guiEvent=None): 

1765 """ 

1766 Pass a `CloseEvent` to all functions connected to ``close_event``. 

1767 """ 

1768 s = 'close_event' 

1769 try: 

1770 event = CloseEvent(s, self, guiEvent=guiEvent) 

1771 self.callbacks.process(s, event) 

1772 except (TypeError, AttributeError): 

1773 pass 

1774 # Suppress the TypeError when the python session is being killed. 

1775 # It may be that a better solution would be a mechanism to 

1776 # disconnect all callbacks upon shutdown. 

1777 # AttributeError occurs on OSX with qt4agg upon exiting 

1778 # with an open window; 'callbacks' attribute no longer exists. 

1779 

1780 @_api.deprecated("3.6", alternative=( 

1781 "callbacks.process('key_press_event', KeyEvent(...))")) 

1782 def key_press_event(self, key, guiEvent=None): 

1783 """ 

1784 Pass a `KeyEvent` to all functions connected to ``key_press_event``. 

1785 """ 

1786 self._key = key 

1787 s = 'key_press_event' 

1788 event = KeyEvent( 

1789 s, self, key, self._lastx, self._lasty, guiEvent=guiEvent) 

1790 self.callbacks.process(s, event) 

1791 

1792 @_api.deprecated("3.6", alternative=( 

1793 "callbacks.process('key_release_event', KeyEvent(...))")) 

1794 def key_release_event(self, key, guiEvent=None): 

1795 """ 

1796 Pass a `KeyEvent` to all functions connected to ``key_release_event``. 

1797 """ 

1798 s = 'key_release_event' 

1799 event = KeyEvent( 

1800 s, self, key, self._lastx, self._lasty, guiEvent=guiEvent) 

1801 self.callbacks.process(s, event) 

1802 self._key = None 

1803 

1804 @_api.deprecated("3.6", alternative=( 

1805 "callbacks.process('pick_event', PickEvent(...))")) 

1806 def pick_event(self, mouseevent, artist, **kwargs): 

1807 """ 

1808 Callback processing for pick events. 

1809 

1810 This method will be called by artists who are picked and will 

1811 fire off `PickEvent` callbacks registered listeners. 

1812 

1813 Note that artists are not pickable by default (see 

1814 `.Artist.set_picker`). 

1815 """ 

1816 s = 'pick_event' 

1817 event = PickEvent(s, self, mouseevent, artist, 

1818 guiEvent=mouseevent.guiEvent, 

1819 **kwargs) 

1820 self.callbacks.process(s, event) 

1821 

1822 @_api.deprecated("3.6", alternative=( 

1823 "callbacks.process('scroll_event', MouseEvent(...))")) 

1824 def scroll_event(self, x, y, step, guiEvent=None): 

1825 """ 

1826 Callback processing for scroll events. 

1827 

1828 Backend derived classes should call this function on any 

1829 scroll wheel event. (*x*, *y*) are the canvas coords ((0, 0) is lower 

1830 left). button and key are as defined in `MouseEvent`. 

1831 

1832 This method will call all functions connected to the 'scroll_event' 

1833 with a `MouseEvent` instance. 

1834 """ 

1835 if step >= 0: 

1836 self._button = 'up' 

1837 else: 

1838 self._button = 'down' 

1839 s = 'scroll_event' 

1840 mouseevent = MouseEvent(s, self, x, y, self._button, self._key, 

1841 step=step, guiEvent=guiEvent) 

1842 self.callbacks.process(s, mouseevent) 

1843 

1844 @_api.deprecated("3.6", alternative=( 

1845 "callbacks.process('button_press_event', MouseEvent(...))")) 

1846 def button_press_event(self, x, y, button, dblclick=False, guiEvent=None): 

1847 """ 

1848 Callback processing for mouse button press events. 

1849 

1850 Backend derived classes should call this function on any mouse 

1851 button press. (*x*, *y*) are the canvas coords ((0, 0) is lower left). 

1852 button and key are as defined in `MouseEvent`. 

1853 

1854 This method will call all functions connected to the 

1855 'button_press_event' with a `MouseEvent` instance. 

1856 """ 

1857 self._button = button 

1858 s = 'button_press_event' 

1859 mouseevent = MouseEvent(s, self, x, y, button, self._key, 

1860 dblclick=dblclick, guiEvent=guiEvent) 

1861 self.callbacks.process(s, mouseevent) 

1862 

1863 @_api.deprecated("3.6", alternative=( 

1864 "callbacks.process('button_release_event', MouseEvent(...))")) 

1865 def button_release_event(self, x, y, button, guiEvent=None): 

1866 """ 

1867 Callback processing for mouse button release events. 

1868 

1869 Backend derived classes should call this function on any mouse 

1870 button release. 

1871 

1872 This method will call all functions connected to the 

1873 'button_release_event' with a `MouseEvent` instance. 

1874 

1875 Parameters 

1876 ---------- 

1877 x : float 

1878 The canvas coordinates where 0=left. 

1879 y : float 

1880 The canvas coordinates where 0=bottom. 

1881 guiEvent 

1882 The native UI event that generated the Matplotlib event. 

1883 """ 

1884 s = 'button_release_event' 

1885 event = MouseEvent(s, self, x, y, button, self._key, guiEvent=guiEvent) 

1886 self.callbacks.process(s, event) 

1887 self._button = None 

1888 

1889 # Also remove _lastx, _lasty when this goes away. 

1890 @_api.deprecated("3.6", alternative=( 

1891 "callbacks.process('motion_notify_event', MouseEvent(...))")) 

1892 def motion_notify_event(self, x, y, guiEvent=None): 

1893 """ 

1894 Callback processing for mouse movement events. 

1895 

1896 Backend derived classes should call this function on any 

1897 motion-notify-event. 

1898 

1899 This method will call all functions connected to the 

1900 'motion_notify_event' with a `MouseEvent` instance. 

1901 

1902 Parameters 

1903 ---------- 

1904 x : float 

1905 The canvas coordinates where 0=left. 

1906 y : float 

1907 The canvas coordinates where 0=bottom. 

1908 guiEvent 

1909 The native UI event that generated the Matplotlib event. 

1910 """ 

1911 self._lastx, self._lasty = x, y 

1912 s = 'motion_notify_event' 

1913 event = MouseEvent(s, self, x, y, self._button, self._key, 

1914 guiEvent=guiEvent) 

1915 self.callbacks.process(s, event) 

1916 

1917 @_api.deprecated("3.6", alternative=( 

1918 "callbacks.process('leave_notify_event', LocationEvent(...))")) 

1919 def leave_notify_event(self, guiEvent=None): 

1920 """ 

1921 Callback processing for the mouse cursor leaving the canvas. 

1922 

1923 Backend derived classes should call this function when leaving 

1924 canvas. 

1925 

1926 Parameters 

1927 ---------- 

1928 guiEvent 

1929 The native UI event that generated the Matplotlib event. 

1930 """ 

1931 self.callbacks.process('figure_leave_event', LocationEvent.lastevent) 

1932 LocationEvent.lastevent = None 

1933 self._lastx, self._lasty = None, None 

1934 

1935 @_api.deprecated("3.6", alternative=( 

1936 "callbacks.process('enter_notify_event', LocationEvent(...))")) 

1937 def enter_notify_event(self, guiEvent=None, xy=None): 

1938 """ 

1939 Callback processing for the mouse cursor entering the canvas. 

1940 

1941 Backend derived classes should call this function when entering 

1942 canvas. 

1943 

1944 Parameters 

1945 ---------- 

1946 guiEvent 

1947 The native UI event that generated the Matplotlib event. 

1948 xy : (float, float) 

1949 The coordinate location of the pointer when the canvas is entered. 

1950 """ 

1951 if xy is not None: 

1952 x, y = xy 

1953 self._lastx, self._lasty = x, y 

1954 else: 

1955 x = None 

1956 y = None 

1957 _api.warn_deprecated( 

1958 '3.0', removal='3.5', name='enter_notify_event', 

1959 message='Since %(since)s, %(name)s expects a location but ' 

1960 'your backend did not pass one. This will become an error ' 

1961 '%(removal)s.') 

1962 

1963 event = LocationEvent('figure_enter_event', self, x, y, guiEvent) 

1964 self.callbacks.process('figure_enter_event', event) 

1965 

1966 def inaxes(self, xy): 

1967 """ 

1968 Return the topmost visible `~.axes.Axes` containing the point *xy*. 

1969 

1970 Parameters 

1971 ---------- 

1972 xy : (float, float) 

1973 (x, y) pixel positions from left/bottom of the canvas. 

1974 

1975 Returns 

1976 ------- 

1977 `~matplotlib.axes.Axes` or None 

1978 The topmost visible Axes containing the point, or None if there 

1979 is no Axes at the point. 

1980 """ 

1981 axes_list = [a for a in self.figure.get_axes() 

1982 if a.patch.contains_point(xy) and a.get_visible()] 

1983 if axes_list: 

1984 axes = cbook._topmost_artist(axes_list) 

1985 else: 

1986 axes = None 

1987 

1988 return axes 

1989 

1990 def grab_mouse(self, ax): 

1991 """ 

1992 Set the child `~.axes.Axes` which is grabbing the mouse events. 

1993 

1994 Usually called by the widgets themselves. It is an error to call this 

1995 if the mouse is already grabbed by another Axes. 

1996 """ 

1997 if self.mouse_grabber not in (None, ax): 

1998 raise RuntimeError("Another Axes already grabs mouse input") 

1999 self.mouse_grabber = ax 

2000 

2001 def release_mouse(self, ax): 

2002 """ 

2003 Release the mouse grab held by the `~.axes.Axes` *ax*. 

2004 

2005 Usually called by the widgets. It is ok to call this even if *ax* 

2006 doesn't have the mouse grab currently. 

2007 """ 

2008 if self.mouse_grabber is ax: 

2009 self.mouse_grabber = None 

2010 

2011 def set_cursor(self, cursor): 

2012 """ 

2013 Set the current cursor. 

2014 

2015 This may have no effect if the backend does not display anything. 

2016 

2017 If required by the backend, this method should trigger an update in 

2018 the backend event loop after the cursor is set, as this method may be 

2019 called e.g. before a long-running task during which the GUI is not 

2020 updated. 

2021 

2022 Parameters 

2023 ---------- 

2024 cursor : `.Cursors` 

2025 The cursor to display over the canvas. Note: some backends may 

2026 change the cursor for the entire window. 

2027 """ 

2028 

2029 def draw(self, *args, **kwargs): 

2030 """ 

2031 Render the `.Figure`. 

2032 

2033 This method must walk the artist tree, even if no output is produced, 

2034 because it triggers deferred work that users may want to access 

2035 before saving output to disk. For example computing limits, 

2036 auto-limits, and tick values. 

2037 """ 

2038 

2039 def draw_idle(self, *args, **kwargs): 

2040 """ 

2041 Request a widget redraw once control returns to the GUI event loop. 

2042 

2043 Even if multiple calls to `draw_idle` occur before control returns 

2044 to the GUI event loop, the figure will only be rendered once. 

2045 

2046 Notes 

2047 ----- 

2048 Backends may choose to override the method and implement their own 

2049 strategy to prevent multiple renderings. 

2050 

2051 """ 

2052 if not self._is_idle_drawing: 

2053 with self._idle_draw_cntx(): 

2054 self.draw(*args, **kwargs) 

2055 

2056 @property 

2057 def device_pixel_ratio(self): 

2058 """ 

2059 The ratio of physical to logical pixels used for the canvas on screen. 

2060 

2061 By default, this is 1, meaning physical and logical pixels are the same 

2062 size. Subclasses that support High DPI screens may set this property to 

2063 indicate that said ratio is different. All Matplotlib interaction, 

2064 unless working directly with the canvas, remains in logical pixels. 

2065 

2066 """ 

2067 return self._device_pixel_ratio 

2068 

2069 def _set_device_pixel_ratio(self, ratio): 

2070 """ 

2071 Set the ratio of physical to logical pixels used for the canvas. 

2072 

2073 Subclasses that support High DPI screens can set this property to 

2074 indicate that said ratio is different. The canvas itself will be 

2075 created at the physical size, while the client side will use the 

2076 logical size. Thus the DPI of the Figure will change to be scaled by 

2077 this ratio. Implementations that support High DPI screens should use 

2078 physical pixels for events so that transforms back to Axes space are 

2079 correct. 

2080 

2081 By default, this is 1, meaning physical and logical pixels are the same 

2082 size. 

2083 

2084 Parameters 

2085 ---------- 

2086 ratio : float 

2087 The ratio of logical to physical pixels used for the canvas. 

2088 

2089 Returns 

2090 ------- 

2091 bool 

2092 Whether the ratio has changed. Backends may interpret this as a 

2093 signal to resize the window, repaint the canvas, or change any 

2094 other relevant properties. 

2095 """ 

2096 if self._device_pixel_ratio == ratio: 

2097 return False 

2098 # In cases with mixed resolution displays, we need to be careful if the 

2099 # device pixel ratio changes - in this case we need to resize the 

2100 # canvas accordingly. Some backends provide events that indicate a 

2101 # change in DPI, but those that don't will update this before drawing. 

2102 dpi = ratio * self.figure._original_dpi 

2103 self.figure._set_dpi(dpi, forward=False) 

2104 self._device_pixel_ratio = ratio 

2105 return True 

2106 

2107 def get_width_height(self, *, physical=False): 

2108 """ 

2109 Return the figure width and height in integral points or pixels. 

2110 

2111 When the figure is used on High DPI screens (and the backend supports 

2112 it), the truncation to integers occurs after scaling by the device 

2113 pixel ratio. 

2114 

2115 Parameters 

2116 ---------- 

2117 physical : bool, default: False 

2118 Whether to return true physical pixels or logical pixels. Physical 

2119 pixels may be used by backends that support HiDPI, but still 

2120 configure the canvas using its actual size. 

2121 

2122 Returns 

2123 ------- 

2124 width, height : int 

2125 The size of the figure, in points or pixels, depending on the 

2126 backend. 

2127 """ 

2128 return tuple(int(size / (1 if physical else self.device_pixel_ratio)) 

2129 for size in self.figure.bbox.max) 

2130 

2131 @classmethod 

2132 def get_supported_filetypes(cls): 

2133 """Return dict of savefig file formats supported by this backend.""" 

2134 return cls.filetypes 

2135 

2136 @classmethod 

2137 def get_supported_filetypes_grouped(cls): 

2138 """ 

2139 Return a dict of savefig file formats supported by this backend, 

2140 where the keys are a file type name, such as 'Joint Photographic 

2141 Experts Group', and the values are a list of filename extensions used 

2142 for that filetype, such as ['jpg', 'jpeg']. 

2143 """ 

2144 groupings = {} 

2145 for ext, name in cls.filetypes.items(): 

2146 groupings.setdefault(name, []).append(ext) 

2147 groupings[name].sort() 

2148 return groupings 

2149 

2150 @contextmanager 

2151 def _switch_canvas_and_return_print_method(self, fmt, backend=None): 

2152 """ 

2153 Context manager temporarily setting the canvas for saving the figure:: 

2154 

2155 with canvas._switch_canvas_and_return_print_method(fmt, backend) \\ 

2156 as print_method: 

2157 # ``print_method`` is a suitable ``print_{fmt}`` method, and 

2158 # the figure's canvas is temporarily switched to the method's 

2159 # canvas within the with... block. ``print_method`` is also 

2160 # wrapped to suppress extra kwargs passed by ``print_figure``. 

2161 

2162 Parameters 

2163 ---------- 

2164 fmt : str 

2165 If *backend* is None, then determine a suitable canvas class for 

2166 saving to format *fmt* -- either the current canvas class, if it 

2167 supports *fmt*, or whatever `get_registered_canvas_class` returns; 

2168 switch the figure canvas to that canvas class. 

2169 backend : str or None, default: None 

2170 If not None, switch the figure canvas to the ``FigureCanvas`` class 

2171 of the given backend. 

2172 """ 

2173 canvas = None 

2174 if backend is not None: 

2175 # Return a specific canvas class, if requested. 

2176 canvas_class = ( 

2177 importlib.import_module(cbook._backend_module_name(backend)) 

2178 .FigureCanvas) 

2179 if not hasattr(canvas_class, f"print_{fmt}"): 

2180 raise ValueError( 

2181 f"The {backend!r} backend does not support {fmt} output") 

2182 elif hasattr(self, f"print_{fmt}"): 

2183 # Return the current canvas if it supports the requested format. 

2184 canvas = self 

2185 canvas_class = None # Skip call to switch_backends. 

2186 else: 

2187 # Return a default canvas for the requested format, if it exists. 

2188 canvas_class = get_registered_canvas_class(fmt) 

2189 if canvas_class: 

2190 canvas = self.switch_backends(canvas_class) 

2191 if canvas is None: 

2192 raise ValueError( 

2193 "Format {!r} is not supported (supported formats: {})".format( 

2194 fmt, ", ".join(sorted(self.get_supported_filetypes())))) 

2195 meth = getattr(canvas, f"print_{fmt}") 

2196 mod = (meth.func.__module__ 

2197 if hasattr(meth, "func") # partialmethod, e.g. backend_wx. 

2198 else meth.__module__) 

2199 if mod.startswith(("matplotlib.", "mpl_toolkits.")): 

2200 optional_kws = { # Passed by print_figure for other renderers. 

2201 "dpi", "facecolor", "edgecolor", "orientation", 

2202 "bbox_inches_restore"} 

2203 skip = optional_kws - {*inspect.signature(meth).parameters} 

2204 print_method = functools.wraps(meth)(lambda *args, **kwargs: meth( 

2205 *args, **{k: v for k, v in kwargs.items() if k not in skip})) 

2206 else: # Let third-parties do as they see fit. 

2207 print_method = meth 

2208 try: 

2209 yield print_method 

2210 finally: 

2211 self.figure.canvas = self 

2212 

2213 def print_figure( 

2214 self, filename, dpi=None, facecolor=None, edgecolor=None, 

2215 orientation='portrait', format=None, *, 

2216 bbox_inches=None, pad_inches=None, bbox_extra_artists=None, 

2217 backend=None, **kwargs): 

2218 """ 

2219 Render the figure to hardcopy. Set the figure patch face and edge 

2220 colors. This is useful because some of the GUIs have a gray figure 

2221 face color background and you'll probably want to override this on 

2222 hardcopy. 

2223 

2224 Parameters 

2225 ---------- 

2226 filename : str or path-like or file-like 

2227 The file where the figure is saved. 

2228 

2229 dpi : float, default: :rc:`savefig.dpi` 

2230 The dots per inch to save the figure in. 

2231 

2232 facecolor : color or 'auto', default: :rc:`savefig.facecolor` 

2233 The facecolor of the figure. If 'auto', use the current figure 

2234 facecolor. 

2235 

2236 edgecolor : color or 'auto', default: :rc:`savefig.edgecolor` 

2237 The edgecolor of the figure. If 'auto', use the current figure 

2238 edgecolor. 

2239 

2240 orientation : {'landscape', 'portrait'}, default: 'portrait' 

2241 Only currently applies to PostScript printing. 

2242 

2243 format : str, optional 

2244 Force a specific file format. If not given, the format is inferred 

2245 from the *filename* extension, and if that fails from 

2246 :rc:`savefig.format`. 

2247 

2248 bbox_inches : 'tight' or `.Bbox`, default: :rc:`savefig.bbox` 

2249 Bounding box in inches: only the given portion of the figure is 

2250 saved. If 'tight', try to figure out the tight bbox of the figure. 

2251 

2252 pad_inches : float, default: :rc:`savefig.pad_inches` 

2253 Amount of padding around the figure when *bbox_inches* is 'tight'. 

2254 

2255 bbox_extra_artists : list of `~matplotlib.artist.Artist`, optional 

2256 A list of extra artists that will be considered when the 

2257 tight bbox is calculated. 

2258 

2259 backend : str, optional 

2260 Use a non-default backend to render the file, e.g. to render a 

2261 png file with the "cairo" backend rather than the default "agg", 

2262 or a pdf file with the "pgf" backend rather than the default 

2263 "pdf". Note that the default backend is normally sufficient. See 

2264 :ref:`the-builtin-backends` for a list of valid backends for each 

2265 file format. Custom backends can be referenced as "module://...". 

2266 """ 

2267 if format is None: 

2268 # get format from filename, or from backend's default filetype 

2269 if isinstance(filename, os.PathLike): 

2270 filename = os.fspath(filename) 

2271 if isinstance(filename, str): 

2272 format = os.path.splitext(filename)[1][1:] 

2273 if format is None or format == '': 

2274 format = self.get_default_filetype() 

2275 if isinstance(filename, str): 

2276 filename = filename.rstrip('.') + '.' + format 

2277 format = format.lower() 

2278 

2279 if dpi is None: 

2280 dpi = rcParams['savefig.dpi'] 

2281 if dpi == 'figure': 

2282 dpi = getattr(self.figure, '_original_dpi', self.figure.dpi) 

2283 

2284 # Remove the figure manager, if any, to avoid resizing the GUI widget. 

2285 with cbook._setattr_cm(self, manager=None), \ 

2286 self._switch_canvas_and_return_print_method(format, backend) \ 

2287 as print_method, \ 

2288 cbook._setattr_cm(self.figure, dpi=dpi), \ 

2289 cbook._setattr_cm(self.figure.canvas, _device_pixel_ratio=1), \ 

2290 cbook._setattr_cm(self.figure.canvas, _is_saving=True), \ 

2291 ExitStack() as stack: 

2292 

2293 for prop in ["facecolor", "edgecolor"]: 

2294 color = locals()[prop] 

2295 if color is None: 

2296 color = rcParams[f"savefig.{prop}"] 

2297 if not cbook._str_equal(color, "auto"): 

2298 stack.enter_context(self.figure._cm_set(**{prop: color})) 

2299 

2300 if bbox_inches is None: 

2301 bbox_inches = rcParams['savefig.bbox'] 

2302 

2303 if (self.figure.get_layout_engine() is not None or 

2304 bbox_inches == "tight"): 

2305 # we need to trigger a draw before printing to make sure 

2306 # CL works. "tight" also needs a draw to get the right 

2307 # locations: 

2308 renderer = _get_renderer( 

2309 self.figure, 

2310 functools.partial( 

2311 print_method, orientation=orientation) 

2312 ) 

2313 with getattr(renderer, "_draw_disabled", nullcontext)(): 

2314 self.figure.draw(renderer) 

2315 

2316 if bbox_inches: 

2317 if bbox_inches == "tight": 

2318 bbox_inches = self.figure.get_tightbbox( 

2319 renderer, bbox_extra_artists=bbox_extra_artists) 

2320 if pad_inches is None: 

2321 pad_inches = rcParams['savefig.pad_inches'] 

2322 bbox_inches = bbox_inches.padded(pad_inches) 

2323 

2324 # call adjust_bbox to save only the given area 

2325 restore_bbox = _tight_bbox.adjust_bbox( 

2326 self.figure, bbox_inches, self.figure.canvas.fixed_dpi) 

2327 

2328 _bbox_inches_restore = (bbox_inches, restore_bbox) 

2329 else: 

2330 _bbox_inches_restore = None 

2331 

2332 # we have already done layout above, so turn it off: 

2333 stack.enter_context(self.figure._cm_set(layout_engine='none')) 

2334 try: 

2335 # _get_renderer may change the figure dpi (as vector formats 

2336 # force the figure dpi to 72), so we need to set it again here. 

2337 with cbook._setattr_cm(self.figure, dpi=dpi): 

2338 result = print_method( 

2339 filename, 

2340 facecolor=facecolor, 

2341 edgecolor=edgecolor, 

2342 orientation=orientation, 

2343 bbox_inches_restore=_bbox_inches_restore, 

2344 **kwargs) 

2345 finally: 

2346 if bbox_inches and restore_bbox: 

2347 restore_bbox() 

2348 

2349 return result 

2350 

2351 @classmethod 

2352 def get_default_filetype(cls): 

2353 """ 

2354 Return the default savefig file format as specified in 

2355 :rc:`savefig.format`. 

2356 

2357 The returned string does not include a period. This method is 

2358 overridden in backends that only support a single file type. 

2359 """ 

2360 return rcParams['savefig.format'] 

2361 

2362 def get_default_filename(self): 

2363 """ 

2364 Return a string, which includes extension, suitable for use as 

2365 a default filename. 

2366 """ 

2367 basename = (self.manager.get_window_title() if self.manager is not None 

2368 else '') 

2369 basename = (basename or 'image').replace(' ', '_') 

2370 filetype = self.get_default_filetype() 

2371 filename = basename + '.' + filetype 

2372 return filename 

2373 

2374 def switch_backends(self, FigureCanvasClass): 

2375 """ 

2376 Instantiate an instance of FigureCanvasClass 

2377 

2378 This is used for backend switching, e.g., to instantiate a 

2379 FigureCanvasPS from a FigureCanvasGTK. Note, deep copying is 

2380 not done, so any changes to one of the instances (e.g., setting 

2381 figure size or line props), will be reflected in the other 

2382 """ 

2383 newCanvas = FigureCanvasClass(self.figure) 

2384 newCanvas._is_saving = self._is_saving 

2385 return newCanvas 

2386 

2387 def mpl_connect(self, s, func): 

2388 """ 

2389 Bind function *func* to event *s*. 

2390 

2391 Parameters 

2392 ---------- 

2393 s : str 

2394 One of the following events ids: 

2395 

2396 - 'button_press_event' 

2397 - 'button_release_event' 

2398 - 'draw_event' 

2399 - 'key_press_event' 

2400 - 'key_release_event' 

2401 - 'motion_notify_event' 

2402 - 'pick_event' 

2403 - 'resize_event' 

2404 - 'scroll_event' 

2405 - 'figure_enter_event', 

2406 - 'figure_leave_event', 

2407 - 'axes_enter_event', 

2408 - 'axes_leave_event' 

2409 - 'close_event'. 

2410 

2411 func : callable 

2412 The callback function to be executed, which must have the 

2413 signature:: 

2414 

2415 def func(event: Event) -> Any 

2416 

2417 For the location events (button and key press/release), if the 

2418 mouse is over the Axes, the ``inaxes`` attribute of the event will 

2419 be set to the `~matplotlib.axes.Axes` the event occurs is over, and 

2420 additionally, the variables ``xdata`` and ``ydata`` attributes will 

2421 be set to the mouse location in data coordinates. See `.KeyEvent` 

2422 and `.MouseEvent` for more info. 

2423 

2424 .. note:: 

2425 

2426 If func is a method, this only stores a weak reference to the 

2427 method. Thus, the figure does not influence the lifetime of 

2428 the associated object. Usually, you want to make sure that the 

2429 object is kept alive throughout the lifetime of the figure by 

2430 holding a reference to it. 

2431 

2432 Returns 

2433 ------- 

2434 cid 

2435 A connection id that can be used with 

2436 `.FigureCanvasBase.mpl_disconnect`. 

2437 

2438 Examples 

2439 -------- 

2440 :: 

2441 

2442 def on_press(event): 

2443 print('you pressed', event.button, event.xdata, event.ydata) 

2444 

2445 cid = canvas.mpl_connect('button_press_event', on_press) 

2446 """ 

2447 

2448 return self.callbacks.connect(s, func) 

2449 

2450 def mpl_disconnect(self, cid): 

2451 """ 

2452 Disconnect the callback with id *cid*. 

2453 

2454 Examples 

2455 -------- 

2456 :: 

2457 

2458 cid = canvas.mpl_connect('button_press_event', on_press) 

2459 # ... later 

2460 canvas.mpl_disconnect(cid) 

2461 """ 

2462 return self.callbacks.disconnect(cid) 

2463 

2464 # Internal subclasses can override _timer_cls instead of new_timer, though 

2465 # this is not a public API for third-party subclasses. 

2466 _timer_cls = TimerBase 

2467 

2468 def new_timer(self, interval=None, callbacks=None): 

2469 """ 

2470 Create a new backend-specific subclass of `.Timer`. 

2471 

2472 This is useful for getting periodic events through the backend's native 

2473 event loop. Implemented only for backends with GUIs. 

2474 

2475 Parameters 

2476 ---------- 

2477 interval : int 

2478 Timer interval in milliseconds. 

2479 

2480 callbacks : list[tuple[callable, tuple, dict]] 

2481 Sequence of (func, args, kwargs) where ``func(*args, **kwargs)`` 

2482 will be executed by the timer every *interval*. 

2483 

2484 Callbacks which return ``False`` or ``0`` will be removed from the 

2485 timer. 

2486 

2487 Examples 

2488 -------- 

2489 >>> timer = fig.canvas.new_timer(callbacks=[(f1, (1,), {'a': 3})]) 

2490 """ 

2491 return self._timer_cls(interval=interval, callbacks=callbacks) 

2492 

2493 def flush_events(self): 

2494 """ 

2495 Flush the GUI events for the figure. 

2496 

2497 Interactive backends need to reimplement this method. 

2498 """ 

2499 

2500 def start_event_loop(self, timeout=0): 

2501 """ 

2502 Start a blocking event loop. 

2503 

2504 Such an event loop is used by interactive functions, such as 

2505 `~.Figure.ginput` and `~.Figure.waitforbuttonpress`, to wait for 

2506 events. 

2507 

2508 The event loop blocks until a callback function triggers 

2509 `stop_event_loop`, or *timeout* is reached. 

2510 

2511 If *timeout* is 0 or negative, never timeout. 

2512 

2513 Only interactive backends need to reimplement this method and it relies 

2514 on `flush_events` being properly implemented. 

2515 

2516 Interactive backends should implement this in a more native way. 

2517 """ 

2518 if timeout <= 0: 

2519 timeout = np.inf 

2520 timestep = 0.01 

2521 counter = 0 

2522 self._looping = True 

2523 while self._looping and counter * timestep < timeout: 

2524 self.flush_events() 

2525 time.sleep(timestep) 

2526 counter += 1 

2527 

2528 def stop_event_loop(self): 

2529 """ 

2530 Stop the current blocking event loop. 

2531 

2532 Interactive backends need to reimplement this to match 

2533 `start_event_loop` 

2534 """ 

2535 self._looping = False 

2536 

2537 

2538def key_press_handler(event, canvas=None, toolbar=None): 

2539 """ 

2540 Implement the default Matplotlib key bindings for the canvas and toolbar 

2541 described at :ref:`key-event-handling`. 

2542 

2543 Parameters 

2544 ---------- 

2545 event : `KeyEvent` 

2546 A key press/release event. 

2547 canvas : `FigureCanvasBase`, default: ``event.canvas`` 

2548 The backend-specific canvas instance. This parameter is kept for 

2549 back-compatibility, but, if set, should always be equal to 

2550 ``event.canvas``. 

2551 toolbar : `NavigationToolbar2`, default: ``event.canvas.toolbar`` 

2552 The navigation cursor toolbar. This parameter is kept for 

2553 back-compatibility, but, if set, should always be equal to 

2554 ``event.canvas.toolbar``. 

2555 """ 

2556 # these bindings happen whether you are over an Axes or not 

2557 

2558 if event.key is None: 

2559 return 

2560 if canvas is None: 

2561 canvas = event.canvas 

2562 if toolbar is None: 

2563 toolbar = canvas.toolbar 

2564 

2565 # Load key-mappings from rcParams. 

2566 fullscreen_keys = rcParams['keymap.fullscreen'] 

2567 home_keys = rcParams['keymap.home'] 

2568 back_keys = rcParams['keymap.back'] 

2569 forward_keys = rcParams['keymap.forward'] 

2570 pan_keys = rcParams['keymap.pan'] 

2571 zoom_keys = rcParams['keymap.zoom'] 

2572 save_keys = rcParams['keymap.save'] 

2573 quit_keys = rcParams['keymap.quit'] 

2574 quit_all_keys = rcParams['keymap.quit_all'] 

2575 grid_keys = rcParams['keymap.grid'] 

2576 grid_minor_keys = rcParams['keymap.grid_minor'] 

2577 toggle_yscale_keys = rcParams['keymap.yscale'] 

2578 toggle_xscale_keys = rcParams['keymap.xscale'] 

2579 

2580 # toggle fullscreen mode ('f', 'ctrl + f') 

2581 if event.key in fullscreen_keys: 

2582 try: 

2583 canvas.manager.full_screen_toggle() 

2584 except AttributeError: 

2585 pass 

2586 

2587 # quit the figure (default key 'ctrl+w') 

2588 if event.key in quit_keys: 

2589 Gcf.destroy_fig(canvas.figure) 

2590 if event.key in quit_all_keys: 

2591 Gcf.destroy_all() 

2592 

2593 if toolbar is not None: 

2594 # home or reset mnemonic (default key 'h', 'home' and 'r') 

2595 if event.key in home_keys: 

2596 toolbar.home() 

2597 # forward / backward keys to enable left handed quick navigation 

2598 # (default key for backward: 'left', 'backspace' and 'c') 

2599 elif event.key in back_keys: 

2600 toolbar.back() 

2601 # (default key for forward: 'right' and 'v') 

2602 elif event.key in forward_keys: 

2603 toolbar.forward() 

2604 # pan mnemonic (default key 'p') 

2605 elif event.key in pan_keys: 

2606 toolbar.pan() 

2607 toolbar._update_cursor(event) 

2608 # zoom mnemonic (default key 'o') 

2609 elif event.key in zoom_keys: 

2610 toolbar.zoom() 

2611 toolbar._update_cursor(event) 

2612 # saving current figure (default key 's') 

2613 elif event.key in save_keys: 

2614 toolbar.save_figure() 

2615 

2616 if event.inaxes is None: 

2617 return 

2618 

2619 # these bindings require the mouse to be over an Axes to trigger 

2620 def _get_uniform_gridstate(ticks): 

2621 # Return True/False if all grid lines are on or off, None if they are 

2622 # not all in the same state. 

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

2624 return True 

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

2626 return False 

2627 else: 

2628 return None 

2629 

2630 ax = event.inaxes 

2631 # toggle major grids in current Axes (default key 'g') 

2632 # Both here and below (for 'G'), we do nothing if *any* grid (major or 

2633 # minor, x or y) is not in a uniform state, to avoid messing up user 

2634 # customization. 

2635 if (event.key in grid_keys 

2636 # Exclude minor grids not in a uniform state. 

2637 and None not in [_get_uniform_gridstate(ax.xaxis.minorTicks), 

2638 _get_uniform_gridstate(ax.yaxis.minorTicks)]): 

2639 x_state = _get_uniform_gridstate(ax.xaxis.majorTicks) 

2640 y_state = _get_uniform_gridstate(ax.yaxis.majorTicks) 

2641 cycle = [(False, False), (True, False), (True, True), (False, True)] 

2642 try: 

2643 x_state, y_state = ( 

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

2645 except ValueError: 

2646 # Exclude major grids not in a uniform state. 

2647 pass 

2648 else: 

2649 # If turning major grids off, also turn minor grids off. 

2650 ax.grid(x_state, which="major" if x_state else "both", axis="x") 

2651 ax.grid(y_state, which="major" if y_state else "both", axis="y") 

2652 canvas.draw_idle() 

2653 # toggle major and minor grids in current Axes (default key 'G') 

2654 if (event.key in grid_minor_keys 

2655 # Exclude major grids not in a uniform state. 

2656 and None not in [_get_uniform_gridstate(ax.xaxis.majorTicks), 

2657 _get_uniform_gridstate(ax.yaxis.majorTicks)]): 

2658 x_state = _get_uniform_gridstate(ax.xaxis.minorTicks) 

2659 y_state = _get_uniform_gridstate(ax.yaxis.minorTicks) 

2660 cycle = [(False, False), (True, False), (True, True), (False, True)] 

2661 try: 

2662 x_state, y_state = ( 

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

2664 except ValueError: 

2665 # Exclude minor grids not in a uniform state. 

2666 pass 

2667 else: 

2668 ax.grid(x_state, which="both", axis="x") 

2669 ax.grid(y_state, which="both", axis="y") 

2670 canvas.draw_idle() 

2671 # toggle scaling of y-axes between 'log and 'linear' (default key 'l') 

2672 elif event.key in toggle_yscale_keys: 

2673 scale = ax.get_yscale() 

2674 if scale == 'log': 

2675 ax.set_yscale('linear') 

2676 ax.figure.canvas.draw_idle() 

2677 elif scale == 'linear': 

2678 try: 

2679 ax.set_yscale('log') 

2680 except ValueError as exc: 

2681 _log.warning(str(exc)) 

2682 ax.set_yscale('linear') 

2683 ax.figure.canvas.draw_idle() 

2684 # toggle scaling of x-axes between 'log and 'linear' (default key 'k') 

2685 elif event.key in toggle_xscale_keys: 

2686 scalex = ax.get_xscale() 

2687 if scalex == 'log': 

2688 ax.set_xscale('linear') 

2689 ax.figure.canvas.draw_idle() 

2690 elif scalex == 'linear': 

2691 try: 

2692 ax.set_xscale('log') 

2693 except ValueError as exc: 

2694 _log.warning(str(exc)) 

2695 ax.set_xscale('linear') 

2696 ax.figure.canvas.draw_idle() 

2697 

2698 

2699def button_press_handler(event, canvas=None, toolbar=None): 

2700 """ 

2701 The default Matplotlib button actions for extra mouse buttons. 

2702 

2703 Parameters are as for `key_press_handler`, except that *event* is a 

2704 `MouseEvent`. 

2705 """ 

2706 if canvas is None: 

2707 canvas = event.canvas 

2708 if toolbar is None: 

2709 toolbar = canvas.toolbar 

2710 if toolbar is not None: 

2711 button_name = str(MouseButton(event.button)) 

2712 if button_name in rcParams['keymap.back']: 

2713 toolbar.back() 

2714 elif button_name in rcParams['keymap.forward']: 

2715 toolbar.forward() 

2716 

2717 

2718class NonGuiException(Exception): 

2719 """Raised when trying show a figure in a non-GUI backend.""" 

2720 pass 

2721 

2722 

2723class FigureManagerBase: 

2724 """ 

2725 A backend-independent abstraction of a figure container and controller. 

2726 

2727 The figure manager is used by pyplot to interact with the window in a 

2728 backend-independent way. It's an adapter for the real (GUI) framework that 

2729 represents the visual figure on screen. 

2730 

2731 GUI backends define from this class to translate common operations such 

2732 as *show* or *resize* to the GUI-specific code. Non-GUI backends do not 

2733 support these operations an can just use the base class. 

2734 

2735 This following basic operations are accessible: 

2736 

2737 **Window operations** 

2738 

2739 - `~.FigureManagerBase.show` 

2740 - `~.FigureManagerBase.destroy` 

2741 - `~.FigureManagerBase.full_screen_toggle` 

2742 - `~.FigureManagerBase.resize` 

2743 - `~.FigureManagerBase.get_window_title` 

2744 - `~.FigureManagerBase.set_window_title` 

2745 

2746 **Key and mouse button press handling** 

2747 

2748 The figure manager sets up default key and mouse button press handling by 

2749 hooking up the `.key_press_handler` to the matplotlib event system. This 

2750 ensures the same shortcuts and mouse actions across backends. 

2751 

2752 **Other operations** 

2753 

2754 Subclasses will have additional attributes and functions to access 

2755 additional functionality. This is of course backend-specific. For example, 

2756 most GUI backends have ``window`` and ``toolbar`` attributes that give 

2757 access to the native GUI widgets of the respective framework. 

2758 

2759 Attributes 

2760 ---------- 

2761 canvas : `FigureCanvasBase` 

2762 The backend-specific canvas instance. 

2763 

2764 num : int or str 

2765 The figure number. 

2766 

2767 key_press_handler_id : int 

2768 The default key handler cid, when using the toolmanager. 

2769 To disable the default key press handling use:: 

2770 

2771 figure.canvas.mpl_disconnect( 

2772 figure.canvas.manager.key_press_handler_id) 

2773 

2774 button_press_handler_id : int 

2775 The default mouse button handler cid, when using the toolmanager. 

2776 To disable the default button press handling use:: 

2777 

2778 figure.canvas.mpl_disconnect( 

2779 figure.canvas.manager.button_press_handler_id) 

2780 """ 

2781 

2782 _toolbar2_class = None 

2783 _toolmanager_toolbar_class = None 

2784 

2785 def __init__(self, canvas, num): 

2786 self.canvas = canvas 

2787 canvas.manager = self # store a pointer to parent 

2788 self.num = num 

2789 self.set_window_title(f"Figure {num:d}") 

2790 

2791 self.key_press_handler_id = None 

2792 self.button_press_handler_id = None 

2793 if rcParams['toolbar'] != 'toolmanager': 

2794 self.key_press_handler_id = self.canvas.mpl_connect( 

2795 'key_press_event', key_press_handler) 

2796 self.button_press_handler_id = self.canvas.mpl_connect( 

2797 'button_press_event', button_press_handler) 

2798 

2799 self.toolmanager = (ToolManager(canvas.figure) 

2800 if mpl.rcParams['toolbar'] == 'toolmanager' 

2801 else None) 

2802 if (mpl.rcParams["toolbar"] == "toolbar2" 

2803 and self._toolbar2_class): 

2804 self.toolbar = self._toolbar2_class(self.canvas) 

2805 elif (mpl.rcParams["toolbar"] == "toolmanager" 

2806 and self._toolmanager_toolbar_class): 

2807 self.toolbar = self._toolmanager_toolbar_class(self.toolmanager) 

2808 else: 

2809 self.toolbar = None 

2810 

2811 if self.toolmanager: 

2812 tools.add_tools_to_manager(self.toolmanager) 

2813 if self.toolbar: 

2814 tools.add_tools_to_container(self.toolbar) 

2815 

2816 @self.canvas.figure.add_axobserver 

2817 def notify_axes_change(fig): 

2818 # Called whenever the current Axes is changed. 

2819 if self.toolmanager is None and self.toolbar is not None: 

2820 self.toolbar.update() 

2821 

2822 @classmethod 

2823 def create_with_canvas(cls, canvas_class, figure, num): 

2824 """ 

2825 Create a manager for a given *figure* using a specific *canvas_class*. 

2826 

2827 Backends should override this method if they have specific needs for 

2828 setting up the canvas or the manager. 

2829 """ 

2830 return cls(canvas_class(figure), num) 

2831 

2832 def show(self): 

2833 """ 

2834 For GUI backends, show the figure window and redraw. 

2835 For non-GUI backends, raise an exception, unless running headless (i.e. 

2836 on Linux with an unset DISPLAY); this exception is converted to a 

2837 warning in `.Figure.show`. 

2838 """ 

2839 # This should be overridden in GUI backends. 

2840 if sys.platform == "linux" and not os.environ.get("DISPLAY"): 

2841 # We cannot check _get_running_interactive_framework() == 

2842 # "headless" because that would also suppress the warning when 

2843 # $DISPLAY exists but is invalid, which is more likely an error and 

2844 # thus warrants a warning. 

2845 return 

2846 raise NonGuiException( 

2847 f"Matplotlib is currently using {get_backend()}, which is a " 

2848 f"non-GUI backend, so cannot show the figure.") 

2849 

2850 def destroy(self): 

2851 pass 

2852 

2853 def full_screen_toggle(self): 

2854 pass 

2855 

2856 def resize(self, w, h): 

2857 """For GUI backends, resize the window (in physical pixels).""" 

2858 

2859 def get_window_title(self): 

2860 """ 

2861 Return the title text of the window containing the figure, or None 

2862 if there is no window (e.g., a PS backend). 

2863 """ 

2864 return 'image' 

2865 

2866 def set_window_title(self, title): 

2867 """ 

2868 Set the title text of the window containing the figure. 

2869 

2870 This has no effect for non-GUI (e.g., PS) backends. 

2871 """ 

2872 

2873 

2874cursors = tools.cursors 

2875 

2876 

2877class _Mode(str, Enum): 

2878 NONE = "" 

2879 PAN = "pan/zoom" 

2880 ZOOM = "zoom rect" 

2881 

2882 def __str__(self): 

2883 return self.value 

2884 

2885 @property 

2886 def _navigate_mode(self): 

2887 return self.name if self is not _Mode.NONE else None 

2888 

2889 

2890class NavigationToolbar2: 

2891 """ 

2892 Base class for the navigation cursor, version 2. 

2893 

2894 Backends must implement a canvas that handles connections for 

2895 'button_press_event' and 'button_release_event'. See 

2896 :meth:`FigureCanvasBase.mpl_connect` for more information. 

2897 

2898 They must also define 

2899 

2900 :meth:`save_figure` 

2901 save the current figure 

2902 

2903 :meth:`draw_rubberband` (optional) 

2904 draw the zoom to rect "rubberband" rectangle 

2905 

2906 :meth:`set_message` (optional) 

2907 display message 

2908 

2909 :meth:`set_history_buttons` (optional) 

2910 you can change the history back / forward buttons to 

2911 indicate disabled / enabled state. 

2912 

2913 and override ``__init__`` to set up the toolbar -- without forgetting to 

2914 call the base-class init. Typically, ``__init__`` needs to set up toolbar 

2915 buttons connected to the `home`, `back`, `forward`, `pan`, `zoom`, and 

2916 `save_figure` methods and using standard icons in the "images" subdirectory 

2917 of the data path. 

2918 

2919 That's it, we'll do the rest! 

2920 """ 

2921 

2922 # list of toolitems to add to the toolbar, format is: 

2923 # ( 

2924 # text, # the text of the button (often not visible to users) 

2925 # tooltip_text, # the tooltip shown on hover (where possible) 

2926 # image_file, # name of the image for the button (without the extension) 

2927 # name_of_method, # name of the method in NavigationToolbar2 to call 

2928 # ) 

2929 toolitems = ( 

2930 ('Home', 'Reset original view', 'home', 'home'), 

2931 ('Back', 'Back to previous view', 'back', 'back'), 

2932 ('Forward', 'Forward to next view', 'forward', 'forward'), 

2933 (None, None, None, None), 

2934 ('Pan', 

2935 'Left button pans, Right button zooms\n' 

2936 'x/y fixes axis, CTRL fixes aspect', 

2937 'move', 'pan'), 

2938 ('Zoom', 'Zoom to rectangle\nx/y fixes axis', 'zoom_to_rect', 'zoom'), 

2939 ('Subplots', 'Configure subplots', 'subplots', 'configure_subplots'), 

2940 (None, None, None, None), 

2941 ('Save', 'Save the figure', 'filesave', 'save_figure'), 

2942 ) 

2943 

2944 def __init__(self, canvas): 

2945 self.canvas = canvas 

2946 canvas.toolbar = self 

2947 self._nav_stack = cbook.Stack() 

2948 # This cursor will be set after the initial draw. 

2949 self._last_cursor = tools.Cursors.POINTER 

2950 

2951 self._id_press = self.canvas.mpl_connect( 

2952 'button_press_event', self._zoom_pan_handler) 

2953 self._id_release = self.canvas.mpl_connect( 

2954 'button_release_event', self._zoom_pan_handler) 

2955 self._id_drag = self.canvas.mpl_connect( 

2956 'motion_notify_event', self.mouse_move) 

2957 self._pan_info = None 

2958 self._zoom_info = None 

2959 

2960 self.mode = _Mode.NONE # a mode string for the status bar 

2961 self.set_history_buttons() 

2962 

2963 def set_message(self, s): 

2964 """Display a message on toolbar or in status bar.""" 

2965 

2966 def draw_rubberband(self, event, x0, y0, x1, y1): 

2967 """ 

2968 Draw a rectangle rubberband to indicate zoom limits. 

2969 

2970 Note that it is not guaranteed that ``x0 <= x1`` and ``y0 <= y1``. 

2971 """ 

2972 

2973 def remove_rubberband(self): 

2974 """Remove the rubberband.""" 

2975 

2976 def home(self, *args): 

2977 """ 

2978 Restore the original view. 

2979 

2980 For convenience of being directly connected as a GUI callback, which 

2981 often get passed additional parameters, this method accepts arbitrary 

2982 parameters, but does not use them. 

2983 """ 

2984 self._nav_stack.home() 

2985 self.set_history_buttons() 

2986 self._update_view() 

2987 

2988 def back(self, *args): 

2989 """ 

2990 Move back up the view lim stack. 

2991 

2992 For convenience of being directly connected as a GUI callback, which 

2993 often get passed additional parameters, this method accepts arbitrary 

2994 parameters, but does not use them. 

2995 """ 

2996 self._nav_stack.back() 

2997 self.set_history_buttons() 

2998 self._update_view() 

2999 

3000 def forward(self, *args): 

3001 """ 

3002 Move forward in the view lim stack. 

3003 

3004 For convenience of being directly connected as a GUI callback, which 

3005 often get passed additional parameters, this method accepts arbitrary 

3006 parameters, but does not use them. 

3007 """ 

3008 self._nav_stack.forward() 

3009 self.set_history_buttons() 

3010 self._update_view() 

3011 

3012 def _update_cursor(self, event): 

3013 """ 

3014 Update the cursor after a mouse move event or a tool (de)activation. 

3015 """ 

3016 if self.mode and event.inaxes and event.inaxes.get_navigate(): 

3017 if (self.mode == _Mode.ZOOM 

3018 and self._last_cursor != tools.Cursors.SELECT_REGION): 

3019 self.canvas.set_cursor(tools.Cursors.SELECT_REGION) 

3020 self._last_cursor = tools.Cursors.SELECT_REGION 

3021 elif (self.mode == _Mode.PAN 

3022 and self._last_cursor != tools.Cursors.MOVE): 

3023 self.canvas.set_cursor(tools.Cursors.MOVE) 

3024 self._last_cursor = tools.Cursors.MOVE 

3025 elif self._last_cursor != tools.Cursors.POINTER: 

3026 self.canvas.set_cursor(tools.Cursors.POINTER) 

3027 self._last_cursor = tools.Cursors.POINTER 

3028 

3029 @contextmanager 

3030 def _wait_cursor_for_draw_cm(self): 

3031 """ 

3032 Set the cursor to a wait cursor when drawing the canvas. 

3033 

3034 In order to avoid constantly changing the cursor when the canvas 

3035 changes frequently, do nothing if this context was triggered during the 

3036 last second. (Optimally we'd prefer only setting the wait cursor if 

3037 the *current* draw takes too long, but the current draw blocks the GUI 

3038 thread). 

3039 """ 

3040 self._draw_time, last_draw_time = ( 

3041 time.time(), getattr(self, "_draw_time", -np.inf)) 

3042 if self._draw_time - last_draw_time > 1: 

3043 try: 

3044 self.canvas.set_cursor(tools.Cursors.WAIT) 

3045 yield 

3046 finally: 

3047 self.canvas.set_cursor(self._last_cursor) 

3048 else: 

3049 yield 

3050 

3051 @staticmethod 

3052 def _mouse_event_to_message(event): 

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

3054 try: 

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

3056 except (ValueError, OverflowError): 

3057 pass 

3058 else: 

3059 s = s.rstrip() 

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

3061 if a.contains(event)[0] and a.get_visible()] 

3062 if artists: 

3063 a = cbook._topmost_artist(artists) 

3064 if a is not event.inaxes.patch: 

3065 data = a.get_cursor_data(event) 

3066 if data is not None: 

3067 data_str = a.format_cursor_data(data).rstrip() 

3068 if data_str: 

3069 s = s + '\n' + data_str 

3070 return s 

3071 

3072 def mouse_move(self, event): 

3073 self._update_cursor(event) 

3074 

3075 s = self._mouse_event_to_message(event) 

3076 if s is not None: 

3077 self.set_message(s) 

3078 else: 

3079 self.set_message(self.mode) 

3080 

3081 def _zoom_pan_handler(self, event): 

3082 if self.mode == _Mode.PAN: 

3083 if event.name == "button_press_event": 

3084 self.press_pan(event) 

3085 elif event.name == "button_release_event": 

3086 self.release_pan(event) 

3087 if self.mode == _Mode.ZOOM: 

3088 if event.name == "button_press_event": 

3089 self.press_zoom(event) 

3090 elif event.name == "button_release_event": 

3091 self.release_zoom(event) 

3092 

3093 def pan(self, *args): 

3094 """ 

3095 Toggle the pan/zoom tool. 

3096 

3097 Pan with left button, zoom with right. 

3098 """ 

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

3100 self.set_message("pan unavailable") 

3101 return 

3102 if self.mode == _Mode.PAN: 

3103 self.mode = _Mode.NONE 

3104 self.canvas.widgetlock.release(self) 

3105 else: 

3106 self.mode = _Mode.PAN 

3107 self.canvas.widgetlock(self) 

3108 for a in self.canvas.figure.get_axes(): 

3109 a.set_navigate_mode(self.mode._navigate_mode) 

3110 self.set_message(self.mode) 

3111 

3112 _PanInfo = namedtuple("_PanInfo", "button axes cid") 

3113 

3114 def press_pan(self, event): 

3115 """Callback for mouse button press in pan/zoom mode.""" 

3116 if (event.button not in [MouseButton.LEFT, MouseButton.RIGHT] 

3117 or event.x is None or event.y is None): 

3118 return 

3119 axes = [a for a in self.canvas.figure.get_axes() 

3120 if a.in_axes(event) and a.get_navigate() and a.can_pan()] 

3121 if not axes: 

3122 return 

3123 if self._nav_stack() is None: 

3124 self.push_current() # set the home button to this view 

3125 for ax in axes: 

3126 ax.start_pan(event.x, event.y, event.button) 

3127 self.canvas.mpl_disconnect(self._id_drag) 

3128 id_drag = self.canvas.mpl_connect("motion_notify_event", self.drag_pan) 

3129 self._pan_info = self._PanInfo( 

3130 button=event.button, axes=axes, cid=id_drag) 

3131 

3132 def drag_pan(self, event): 

3133 """Callback for dragging in pan/zoom mode.""" 

3134 for ax in self._pan_info.axes: 

3135 # Using the recorded button at the press is safer than the current 

3136 # button, as multiple buttons can get pressed during motion. 

3137 ax.drag_pan(self._pan_info.button, event.key, event.x, event.y) 

3138 self.canvas.draw_idle() 

3139 

3140 def release_pan(self, event): 

3141 """Callback for mouse button release in pan/zoom mode.""" 

3142 if self._pan_info is None: 

3143 return 

3144 self.canvas.mpl_disconnect(self._pan_info.cid) 

3145 self._id_drag = self.canvas.mpl_connect( 

3146 'motion_notify_event', self.mouse_move) 

3147 for ax in self._pan_info.axes: 

3148 ax.end_pan() 

3149 self.canvas.draw_idle() 

3150 self._pan_info = None 

3151 self.push_current() 

3152 

3153 def zoom(self, *args): 

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

3155 self.set_message("zoom unavailable") 

3156 return 

3157 """Toggle zoom to rect mode.""" 

3158 if self.mode == _Mode.ZOOM: 

3159 self.mode = _Mode.NONE 

3160 self.canvas.widgetlock.release(self) 

3161 else: 

3162 self.mode = _Mode.ZOOM 

3163 self.canvas.widgetlock(self) 

3164 for a in self.canvas.figure.get_axes(): 

3165 a.set_navigate_mode(self.mode._navigate_mode) 

3166 self.set_message(self.mode) 

3167 

3168 _ZoomInfo = namedtuple("_ZoomInfo", "direction start_xy axes cid cbar") 

3169 

3170 def press_zoom(self, event): 

3171 """Callback for mouse button press in zoom to rect mode.""" 

3172 if (event.button not in [MouseButton.LEFT, MouseButton.RIGHT] 

3173 or event.x is None or event.y is None): 

3174 return 

3175 axes = [a for a in self.canvas.figure.get_axes() 

3176 if a.in_axes(event) and a.get_navigate() and a.can_zoom()] 

3177 if not axes: 

3178 return 

3179 if self._nav_stack() is None: 

3180 self.push_current() # set the home button to this view 

3181 id_zoom = self.canvas.mpl_connect( 

3182 "motion_notify_event", self.drag_zoom) 

3183 # A colorbar is one-dimensional, so we extend the zoom rectangle out 

3184 # to the edge of the Axes bbox in the other dimension. To do that we 

3185 # store the orientation of the colorbar for later. 

3186 if hasattr(axes[0], "_colorbar"): 

3187 cbar = axes[0]._colorbar.orientation 

3188 else: 

3189 cbar = None 

3190 self._zoom_info = self._ZoomInfo( 

3191 direction="in" if event.button == 1 else "out", 

3192 start_xy=(event.x, event.y), axes=axes, cid=id_zoom, cbar=cbar) 

3193 

3194 def drag_zoom(self, event): 

3195 """Callback for dragging in zoom mode.""" 

3196 start_xy = self._zoom_info.start_xy 

3197 ax = self._zoom_info.axes[0] 

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

3199 [start_xy, [event.x, event.y]], ax.bbox.min, ax.bbox.max) 

3200 key = event.key 

3201 # Force the key on colorbars to extend the short-axis bbox 

3202 if self._zoom_info.cbar == "horizontal": 

3203 key = "x" 

3204 elif self._zoom_info.cbar == "vertical": 

3205 key = "y" 

3206 if key == "x": 

3207 y1, y2 = ax.bbox.intervaly 

3208 elif key == "y": 

3209 x1, x2 = ax.bbox.intervalx 

3210 

3211 self.draw_rubberband(event, x1, y1, x2, y2) 

3212 

3213 def release_zoom(self, event): 

3214 """Callback for mouse button release in zoom to rect mode.""" 

3215 if self._zoom_info is None: 

3216 return 

3217 

3218 # We don't check the event button here, so that zooms can be cancelled 

3219 # by (pressing and) releasing another mouse button. 

3220 self.canvas.mpl_disconnect(self._zoom_info.cid) 

3221 self.remove_rubberband() 

3222 

3223 start_x, start_y = self._zoom_info.start_xy 

3224 key = event.key 

3225 # Force the key on colorbars to ignore the zoom-cancel on the 

3226 # short-axis side 

3227 if self._zoom_info.cbar == "horizontal": 

3228 key = "x" 

3229 elif self._zoom_info.cbar == "vertical": 

3230 key = "y" 

3231 # Ignore single clicks: 5 pixels is a threshold that allows the user to 

3232 # "cancel" a zoom action by zooming by less than 5 pixels. 

3233 if ((abs(event.x - start_x) < 5 and key != "y") or 

3234 (abs(event.y - start_y) < 5 and key != "x")): 

3235 self.canvas.draw_idle() 

3236 self._zoom_info = None 

3237 return 

3238 

3239 for i, ax in enumerate(self._zoom_info.axes): 

3240 # Detect whether this Axes is twinned with an earlier Axes in the 

3241 # list of zoomed Axes, to avoid double zooming. 

3242 twinx = any(ax.get_shared_x_axes().joined(ax, prev) 

3243 for prev in self._zoom_info.axes[:i]) 

3244 twiny = any(ax.get_shared_y_axes().joined(ax, prev) 

3245 for prev in self._zoom_info.axes[:i]) 

3246 ax._set_view_from_bbox( 

3247 (start_x, start_y, event.x, event.y), 

3248 self._zoom_info.direction, key, twinx, twiny) 

3249 

3250 self.canvas.draw_idle() 

3251 self._zoom_info = None 

3252 self.push_current() 

3253 

3254 def push_current(self): 

3255 """Push the current view limits and position onto the stack.""" 

3256 self._nav_stack.push( 

3257 WeakKeyDictionary( 

3258 {ax: (ax._get_view(), 

3259 # Store both the original and modified positions. 

3260 (ax.get_position(True).frozen(), 

3261 ax.get_position().frozen())) 

3262 for ax in self.canvas.figure.axes})) 

3263 self.set_history_buttons() 

3264 

3265 def _update_view(self): 

3266 """ 

3267 Update the viewlim and position from the view and position stack for 

3268 each Axes. 

3269 """ 

3270 nav_info = self._nav_stack() 

3271 if nav_info is None: 

3272 return 

3273 # Retrieve all items at once to avoid any risk of GC deleting an Axes 

3274 # while in the middle of the loop below. 

3275 items = list(nav_info.items()) 

3276 for ax, (view, (pos_orig, pos_active)) in items: 

3277 ax._set_view(view) 

3278 # Restore both the original and modified positions 

3279 ax._set_position(pos_orig, 'original') 

3280 ax._set_position(pos_active, 'active') 

3281 self.canvas.draw_idle() 

3282 

3283 def configure_subplots(self, *args): 

3284 if hasattr(self, "subplot_tool"): 

3285 self.subplot_tool.figure.canvas.manager.show() 

3286 return 

3287 # This import needs to happen here due to circular imports. 

3288 from matplotlib.figure import Figure 

3289 with mpl.rc_context({"toolbar": "none"}): # No navbar for the toolfig. 

3290 manager = type(self.canvas).new_manager(Figure(figsize=(6, 3)), -1) 

3291 manager.set_window_title("Subplot configuration tool") 

3292 tool_fig = manager.canvas.figure 

3293 tool_fig.subplots_adjust(top=0.9) 

3294 self.subplot_tool = widgets.SubplotTool(self.canvas.figure, tool_fig) 

3295 tool_fig.canvas.mpl_connect( 

3296 "close_event", lambda e: delattr(self, "subplot_tool")) 

3297 self.canvas.mpl_connect( 

3298 "close_event", lambda e: manager.destroy()) 

3299 manager.show() 

3300 return self.subplot_tool 

3301 

3302 def save_figure(self, *args): 

3303 """Save the current figure.""" 

3304 raise NotImplementedError 

3305 

3306 @_api.deprecated("3.5", alternative="`.FigureCanvasBase.set_cursor`") 

3307 def set_cursor(self, cursor): 

3308 """ 

3309 Set the current cursor to one of the :class:`Cursors` enums values. 

3310 

3311 If required by the backend, this method should trigger an update in 

3312 the backend event loop after the cursor is set, as this method may be 

3313 called e.g. before a long-running task during which the GUI is not 

3314 updated. 

3315 """ 

3316 self.canvas.set_cursor(cursor) 

3317 

3318 def update(self): 

3319 """Reset the Axes stack.""" 

3320 self._nav_stack.clear() 

3321 self.set_history_buttons() 

3322 

3323 def set_history_buttons(self): 

3324 """Enable or disable the back/forward button.""" 

3325 

3326 

3327class ToolContainerBase: 

3328 """ 

3329 Base class for all tool containers, e.g. toolbars. 

3330 

3331 Attributes 

3332 ---------- 

3333 toolmanager : `.ToolManager` 

3334 The tools with which this `ToolContainer` wants to communicate. 

3335 """ 

3336 

3337 _icon_extension = '.png' 

3338 """ 

3339 Toolcontainer button icon image format extension 

3340 

3341 **String**: Image extension 

3342 """ 

3343 

3344 def __init__(self, toolmanager): 

3345 self.toolmanager = toolmanager 

3346 toolmanager.toolmanager_connect( 

3347 'tool_message_event', 

3348 lambda event: self.set_message(event.message)) 

3349 toolmanager.toolmanager_connect( 

3350 'tool_removed_event', 

3351 lambda event: self.remove_toolitem(event.tool.name)) 

3352 

3353 def _tool_toggled_cbk(self, event): 

3354 """ 

3355 Capture the 'tool_trigger_[name]' 

3356 

3357 This only gets used for toggled tools. 

3358 """ 

3359 self.toggle_toolitem(event.tool.name, event.tool.toggled) 

3360 

3361 def add_tool(self, tool, group, position=-1): 

3362 """ 

3363 Add a tool to this container. 

3364 

3365 Parameters 

3366 ---------- 

3367 tool : tool_like 

3368 The tool to add, see `.ToolManager.get_tool`. 

3369 group : str 

3370 The name of the group to add this tool to. 

3371 position : int, default: -1 

3372 The position within the group to place this tool. 

3373 """ 

3374 tool = self.toolmanager.get_tool(tool) 

3375 image = self._get_image_filename(tool.image) 

3376 toggle = getattr(tool, 'toggled', None) is not None 

3377 self.add_toolitem(tool.name, group, position, 

3378 image, tool.description, toggle) 

3379 if toggle: 

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

3381 self._tool_toggled_cbk) 

3382 # If initially toggled 

3383 if tool.toggled: 

3384 self.toggle_toolitem(tool.name, True) 

3385 

3386 def _get_image_filename(self, image): 

3387 """Find the image based on its name.""" 

3388 if not image: 

3389 return None 

3390 

3391 basedir = cbook._get_data_path("images") 

3392 for fname in [ 

3393 image, 

3394 image + self._icon_extension, 

3395 str(basedir / image), 

3396 str(basedir / (image + self._icon_extension)), 

3397 ]: 

3398 if os.path.isfile(fname): 

3399 return fname 

3400 

3401 def trigger_tool(self, name): 

3402 """ 

3403 Trigger the tool. 

3404 

3405 Parameters 

3406 ---------- 

3407 name : str 

3408 Name (id) of the tool triggered from within the container. 

3409 """ 

3410 self.toolmanager.trigger_tool(name, sender=self) 

3411 

3412 def add_toolitem(self, name, group, position, image, description, toggle): 

3413 """ 

3414 Add a toolitem to the container. 

3415 

3416 This method must be implemented per backend. 

3417 

3418 The callback associated with the button click event, 

3419 must be *exactly* ``self.trigger_tool(name)``. 

3420 

3421 Parameters 

3422 ---------- 

3423 name : str 

3424 Name of the tool to add, this gets used as the tool's ID and as the 

3425 default label of the buttons. 

3426 group : str 

3427 Name of the group that this tool belongs to. 

3428 position : int 

3429 Position of the tool within its group, if -1 it goes at the end. 

3430 image : str 

3431 Filename of the image for the button or `None`. 

3432 description : str 

3433 Description of the tool, used for the tooltips. 

3434 toggle : bool 

3435 * `True` : The button is a toggle (change the pressed/unpressed 

3436 state between consecutive clicks). 

3437 * `False` : The button is a normal button (returns to unpressed 

3438 state after release). 

3439 """ 

3440 raise NotImplementedError 

3441 

3442 def toggle_toolitem(self, name, toggled): 

3443 """ 

3444 Toggle the toolitem without firing event. 

3445 

3446 Parameters 

3447 ---------- 

3448 name : str 

3449 Id of the tool to toggle. 

3450 toggled : bool 

3451 Whether to set this tool as toggled or not. 

3452 """ 

3453 raise NotImplementedError 

3454 

3455 def remove_toolitem(self, name): 

3456 """ 

3457 Remove a toolitem from the `ToolContainer`. 

3458 

3459 This method must get implemented per backend. 

3460 

3461 Called when `.ToolManager` emits a `tool_removed_event`. 

3462 

3463 Parameters 

3464 ---------- 

3465 name : str 

3466 Name of the tool to remove. 

3467 """ 

3468 raise NotImplementedError 

3469 

3470 def set_message(self, s): 

3471 """ 

3472 Display a message on the toolbar. 

3473 

3474 Parameters 

3475 ---------- 

3476 s : str 

3477 Message text. 

3478 """ 

3479 raise NotImplementedError 

3480 

3481 

3482class _Backend: 

3483 # A backend can be defined by using the following pattern: 

3484 # 

3485 # @_Backend.export 

3486 # class FooBackend(_Backend): 

3487 # # override the attributes and methods documented below. 

3488 

3489 # `backend_version` may be overridden by the subclass. 

3490 backend_version = "unknown" 

3491 

3492 # The `FigureCanvas` class must be defined. 

3493 FigureCanvas = None 

3494 

3495 # For interactive backends, the `FigureManager` class must be overridden. 

3496 FigureManager = FigureManagerBase 

3497 

3498 # For interactive backends, `mainloop` should be a function taking no 

3499 # argument and starting the backend main loop. It should be left as None 

3500 # for non-interactive backends. 

3501 mainloop = None 

3502 

3503 # The following methods will be automatically defined and exported, but 

3504 # can be overridden. 

3505 

3506 @classmethod 

3507 def new_figure_manager(cls, num, *args, **kwargs): 

3508 """Create a new figure manager instance.""" 

3509 # This import needs to happen here due to circular imports. 

3510 from matplotlib.figure import Figure 

3511 fig_cls = kwargs.pop('FigureClass', Figure) 

3512 fig = fig_cls(*args, **kwargs) 

3513 return cls.new_figure_manager_given_figure(num, fig) 

3514 

3515 @classmethod 

3516 def new_figure_manager_given_figure(cls, num, figure): 

3517 """Create a new figure manager instance for the given figure.""" 

3518 return cls.FigureCanvas.new_manager(figure, num) 

3519 

3520 @classmethod 

3521 def draw_if_interactive(cls): 

3522 if cls.mainloop is not None and is_interactive(): 

3523 manager = Gcf.get_active() 

3524 if manager: 

3525 manager.canvas.draw_idle() 

3526 

3527 @classmethod 

3528 def show(cls, *, block=None): 

3529 """ 

3530 Show all figures. 

3531 

3532 `show` blocks by calling `mainloop` if *block* is ``True``, or if it 

3533 is ``None`` and we are neither in IPython's ``%pylab`` mode, nor in 

3534 `interactive` mode. 

3535 """ 

3536 managers = Gcf.get_all_fig_managers() 

3537 if not managers: 

3538 return 

3539 for manager in managers: 

3540 try: 

3541 manager.show() # Emits a warning for non-interactive backend. 

3542 except NonGuiException as exc: 

3543 _api.warn_external(str(exc)) 

3544 if cls.mainloop is None: 

3545 return 

3546 if block is None: 

3547 # Hack: Are we in IPython's %pylab mode? In pylab mode, IPython 

3548 # (>= 0.10) tacks a _needmain attribute onto pyplot.show (always 

3549 # set to False). 

3550 from matplotlib import pyplot 

3551 ipython_pylab = hasattr(pyplot.show, "_needmain") 

3552 block = not ipython_pylab and not is_interactive() 

3553 if block: 

3554 cls.mainloop() 

3555 

3556 # This method is the one actually exporting the required methods. 

3557 

3558 @staticmethod 

3559 def export(cls): 

3560 for name in [ 

3561 "backend_version", 

3562 "FigureCanvas", 

3563 "FigureManager", 

3564 "new_figure_manager", 

3565 "new_figure_manager_given_figure", 

3566 "draw_if_interactive", 

3567 "show", 

3568 ]: 

3569 setattr(sys.modules[cls.__module__], name, getattr(cls, name)) 

3570 

3571 # For back-compatibility, generate a shim `Show` class. 

3572 

3573 class Show(ShowBase): 

3574 def mainloop(self): 

3575 return cls.mainloop() 

3576 

3577 setattr(sys.modules[cls.__module__], "Show", Show) 

3578 return cls 

3579 

3580 

3581class ShowBase(_Backend): 

3582 """ 

3583 Simple base class to generate a ``show()`` function in backends. 

3584 

3585 Subclass must override ``mainloop()`` method. 

3586 """ 

3587 

3588 def __call__(self, block=None): 

3589 return self.show(block=block)