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 pathlib
39import signal
40import socket
41import sys
42import time
43import weakref
44from weakref import WeakKeyDictionary
45
46import numpy as np
47
48import matplotlib as mpl
49from matplotlib import (
50 _api, backend_tools as tools, cbook, colors, _docstring, text,
51 _tight_bbox, transforms, widgets, is_interactive, rcParams)
52from matplotlib._pylab_helpers import Gcf
53from matplotlib.backend_managers import ToolManager
54from matplotlib.cbook import _setattr_cm
55from matplotlib.layout_engine import ConstrainedLayoutEngine
56from matplotlib.path import Path
57from matplotlib.texmanager import TexManager
58from matplotlib.transforms import Affine2D
59from matplotlib._enums import JoinStyle, CapStyle
60
61
62_log = logging.getLogger(__name__)
63_default_filetypes = {
64 'eps': 'Encapsulated Postscript',
65 'jpg': 'Joint Photographic Experts Group',
66 'jpeg': 'Joint Photographic Experts Group',
67 'pdf': 'Portable Document Format',
68 'pgf': 'PGF code for LaTeX',
69 'png': 'Portable Network Graphics',
70 'ps': 'Postscript',
71 'raw': 'Raw RGBA bitmap',
72 'rgba': 'Raw RGBA bitmap',
73 'svg': 'Scalable Vector Graphics',
74 'svgz': 'Scalable Vector Graphics',
75 'tif': 'Tagged Image File Format',
76 'tiff': 'Tagged Image File Format',
77 'webp': 'WebP Image Format',
78}
79_default_backends = {
80 'eps': 'matplotlib.backends.backend_ps',
81 'jpg': 'matplotlib.backends.backend_agg',
82 'jpeg': 'matplotlib.backends.backend_agg',
83 'pdf': 'matplotlib.backends.backend_pdf',
84 'pgf': 'matplotlib.backends.backend_pgf',
85 'png': 'matplotlib.backends.backend_agg',
86 'ps': 'matplotlib.backends.backend_ps',
87 'raw': 'matplotlib.backends.backend_agg',
88 'rgba': 'matplotlib.backends.backend_agg',
89 'svg': 'matplotlib.backends.backend_svg',
90 'svgz': 'matplotlib.backends.backend_svg',
91 'tif': 'matplotlib.backends.backend_agg',
92 'tiff': 'matplotlib.backends.backend_agg',
93 'webp': 'matplotlib.backends.backend_agg',
94}
95
96
97def register_backend(format, backend, description=None):
98 """
99 Register a backend for saving to a given file format.
100
101 Parameters
102 ----------
103 format : str
104 File extension
105 backend : module string or canvas class
106 Backend for handling file output
107 description : str, default: ""
108 Description of the file type.
109 """
110 if description is None:
111 description = ''
112 _default_backends[format] = backend
113 _default_filetypes[format] = description
114
115
116def get_registered_canvas_class(format):
117 """
118 Return the registered default canvas for given file format.
119 Handles deferred import of required backend.
120 """
121 if format not in _default_backends:
122 return None
123 backend_class = _default_backends[format]
124 if isinstance(backend_class, str):
125 backend_class = importlib.import_module(backend_class).FigureCanvas
126 _default_backends[format] = backend_class
127 return backend_class
128
129
130class RendererBase:
131 """
132 An abstract base class to handle drawing/rendering operations.
133
134 The following methods must be implemented in the backend for full
135 functionality (though just implementing `draw_path` alone would give a
136 highly capable backend):
137
138 * `draw_path`
139 * `draw_image`
140 * `draw_gouraud_triangles`
141
142 The following methods *should* be implemented in the backend for
143 optimization reasons:
144
145 * `draw_text`
146 * `draw_markers`
147 * `draw_path_collection`
148 * `draw_quad_mesh`
149 """
150 def __init__(self):
151 super().__init__()
152 self._texmanager = None
153 self._text2path = text.TextToPath()
154 self._raster_depth = 0
155 self._rasterizing = False
156
157 def open_group(self, s, gid=None):
158 """
159 Open a grouping element with label *s* and *gid* (if set) as id.
160
161 Only used by the SVG renderer.
162 """
163
164 def close_group(self, s):
165 """
166 Close a grouping element with label *s*.
167
168 Only used by the SVG renderer.
169 """
170
171 def draw_path(self, gc, path, transform, rgbFace=None):
172 """Draw a `~.path.Path` instance using the given affine transform."""
173 raise NotImplementedError
174
175 def draw_markers(self, gc, marker_path, marker_trans, path,
176 trans, rgbFace=None):
177 """
178 Draw a marker at each of *path*'s vertices (excluding control points).
179
180 The base (fallback) implementation makes multiple calls to `draw_path`.
181 Backends may want to override this method in order to draw the marker
182 only once and reuse it multiple times.
183
184 Parameters
185 ----------
186 gc : `.GraphicsContextBase`
187 The graphics context.
188 marker_path : `~matplotlib.path.Path`
189 The path for the marker.
190 marker_trans : `~matplotlib.transforms.Transform`
191 An affine transform applied to the marker.
192 path : `~matplotlib.path.Path`
193 The locations to draw the markers.
194 trans : `~matplotlib.transforms.Transform`
195 An affine transform applied to the path.
196 rgbFace : :mpltype:`color`, optional
197 """
198 for vertices, codes in path.iter_segments(trans, simplify=False):
199 if len(vertices):
200 x, y = vertices[-2:]
201 self.draw_path(gc, marker_path,
202 marker_trans +
203 transforms.Affine2D().translate(x, y),
204 rgbFace)
205
206 def draw_path_collection(self, gc, master_transform, paths, all_transforms,
207 offsets, offset_trans, facecolors, edgecolors,
208 linewidths, linestyles, antialiaseds, urls,
209 offset_position):
210 """
211 Draw a collection of *paths*.
212
213 Each path is first transformed by the corresponding entry
214 in *all_transforms* (a list of (3, 3) matrices) and then by
215 *master_transform*. They are then translated by the corresponding
216 entry in *offsets*, which has been first transformed by *offset_trans*.
217
218 *facecolors*, *edgecolors*, *linewidths*, *linestyles*, and
219 *antialiased* are lists that set the corresponding properties.
220
221 *offset_position* is unused now, but the argument is kept for
222 backwards compatibility.
223
224 The base (fallback) implementation makes multiple calls to `draw_path`.
225 Backends may want to override this in order to render each set of
226 path data only once, and then reference that path multiple times with
227 the different offsets, colors, styles etc. The generator methods
228 `_iter_collection_raw_paths` and `_iter_collection` are provided to
229 help with (and standardize) the implementation across backends. It
230 is highly recommended to use those generators, so that changes to the
231 behavior of `draw_path_collection` can be made globally.
232 """
233 path_ids = self._iter_collection_raw_paths(master_transform,
234 paths, all_transforms)
235
236 for xo, yo, path_id, gc0, rgbFace in self._iter_collection(
237 gc, list(path_ids), offsets, offset_trans,
238 facecolors, edgecolors, linewidths, linestyles,
239 antialiaseds, urls, offset_position):
240 path, transform = path_id
241 # Only apply another translation if we have an offset, else we
242 # reuse the initial transform.
243 if xo != 0 or yo != 0:
244 # The transformation can be used by multiple paths. Since
245 # translate is a inplace operation, we need to copy the
246 # transformation by .frozen() before applying the translation.
247 transform = transform.frozen()
248 transform.translate(xo, yo)
249 self.draw_path(gc0, path, transform, rgbFace)
250
251 def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight,
252 coordinates, offsets, offsetTrans, facecolors,
253 antialiased, edgecolors):
254 """
255 Draw a quadmesh.
256
257 The base (fallback) implementation converts the quadmesh to paths and
258 then calls `draw_path_collection`.
259 """
260
261 from matplotlib.collections import QuadMesh
262 paths = QuadMesh._convert_mesh_to_paths(coordinates)
263
264 if edgecolors is None:
265 edgecolors = facecolors
266 linewidths = np.array([gc.get_linewidth()], float)
267
268 return self.draw_path_collection(
269 gc, master_transform, paths, [], offsets, offsetTrans, facecolors,
270 edgecolors, linewidths, [], [antialiased], [None], 'screen')
271
272 def draw_gouraud_triangles(self, gc, triangles_array, colors_array,
273 transform):
274 """
275 Draw a series of Gouraud triangles.
276
277 Parameters
278 ----------
279 gc : `.GraphicsContextBase`
280 The graphics context.
281 triangles_array : (N, 3, 2) array-like
282 Array of *N* (x, y) points for the triangles.
283 colors_array : (N, 3, 4) array-like
284 Array of *N* RGBA colors for each point of the triangles.
285 transform : `~matplotlib.transforms.Transform`
286 An affine transform to apply to the points.
287 """
288 raise NotImplementedError
289
290 def _iter_collection_raw_paths(self, master_transform, paths,
291 all_transforms):
292 """
293 Helper method (along with `_iter_collection`) to implement
294 `draw_path_collection` in a memory-efficient manner.
295
296 This method yields all of the base path/transform combinations, given a
297 master transform, a list of paths and list of transforms.
298
299 The arguments should be exactly what is passed in to
300 `draw_path_collection`.
301
302 The backend should take each yielded path and transform and create an
303 object that can be referenced (reused) later.
304 """
305 Npaths = len(paths)
306 Ntransforms = len(all_transforms)
307 N = max(Npaths, Ntransforms)
308
309 if Npaths == 0:
310 return
311
312 transform = transforms.IdentityTransform()
313 for i in range(N):
314 path = paths[i % Npaths]
315 if Ntransforms:
316 transform = Affine2D(all_transforms[i % Ntransforms])
317 yield path, transform + master_transform
318
319 def _iter_collection_uses_per_path(self, paths, all_transforms,
320 offsets, facecolors, edgecolors):
321 """
322 Compute how many times each raw path object returned by
323 `_iter_collection_raw_paths` would be used when calling
324 `_iter_collection`. This is intended for the backend to decide
325 on the tradeoff between using the paths in-line and storing
326 them once and reusing. Rounds up in case the number of uses
327 is not the same for every path.
328 """
329 Npaths = len(paths)
330 if Npaths == 0 or len(facecolors) == len(edgecolors) == 0:
331 return 0
332 Npath_ids = max(Npaths, len(all_transforms))
333 N = max(Npath_ids, len(offsets))
334 return (N + Npath_ids - 1) // Npath_ids
335
336 def _iter_collection(self, gc, path_ids, offsets, offset_trans, facecolors,
337 edgecolors, linewidths, linestyles,
338 antialiaseds, urls, offset_position):
339 """
340 Helper method (along with `_iter_collection_raw_paths`) to implement
341 `draw_path_collection` in a memory-efficient manner.
342
343 This method yields all of the path, offset and graphics context
344 combinations to draw the path collection. The caller should already
345 have looped over the results of `_iter_collection_raw_paths` to draw
346 this collection.
347
348 The arguments should be the same as that passed into
349 `draw_path_collection`, with the exception of *path_ids*, which is a
350 list of arbitrary objects that the backend will use to reference one of
351 the paths created in the `_iter_collection_raw_paths` stage.
352
353 Each yielded result is of the form::
354
355 xo, yo, path_id, gc, rgbFace
356
357 where *xo*, *yo* is an offset; *path_id* is one of the elements of
358 *path_ids*; *gc* is a graphics context and *rgbFace* is a color to
359 use for filling the path.
360 """
361 Npaths = len(path_ids)
362 Noffsets = len(offsets)
363 N = max(Npaths, Noffsets)
364 Nfacecolors = len(facecolors)
365 Nedgecolors = len(edgecolors)
366 Nlinewidths = len(linewidths)
367 Nlinestyles = len(linestyles)
368 Nurls = len(urls)
369
370 if (Nfacecolors == 0 and Nedgecolors == 0) or Npaths == 0:
371 return
372
373 gc0 = self.new_gc()
374 gc0.copy_properties(gc)
375
376 def cycle_or_default(seq, default=None):
377 # Cycle over *seq* if it is not empty; else always yield *default*.
378 return (itertools.cycle(seq) if len(seq)
379 else itertools.repeat(default))
380
381 pathids = cycle_or_default(path_ids)
382 toffsets = cycle_or_default(offset_trans.transform(offsets), (0, 0))
383 fcs = cycle_or_default(facecolors)
384 ecs = cycle_or_default(edgecolors)
385 lws = cycle_or_default(linewidths)
386 lss = cycle_or_default(linestyles)
387 aas = cycle_or_default(antialiaseds)
388 urls = cycle_or_default(urls)
389
390 if Nedgecolors == 0:
391 gc0.set_linewidth(0.0)
392
393 for pathid, (xo, yo), fc, ec, lw, ls, aa, url in itertools.islice(
394 zip(pathids, toffsets, fcs, ecs, lws, lss, aas, urls), N):
395 if not (np.isfinite(xo) and np.isfinite(yo)):
396 continue
397 if Nedgecolors:
398 if Nlinewidths:
399 gc0.set_linewidth(lw)
400 if Nlinestyles:
401 gc0.set_dashes(*ls)
402 if len(ec) == 4 and ec[3] == 0.0:
403 gc0.set_linewidth(0)
404 else:
405 gc0.set_foreground(ec)
406 if fc is not None and len(fc) == 4 and fc[3] == 0:
407 fc = None
408 gc0.set_antialiased(aa)
409 if Nurls:
410 gc0.set_url(url)
411 yield xo, yo, pathid, gc0, fc
412 gc0.restore()
413
414 def get_image_magnification(self):
415 """
416 Get the factor by which to magnify images passed to `draw_image`.
417 Allows a backend to have images at a different resolution to other
418 artists.
419 """
420 return 1.0
421
422 def draw_image(self, gc, x, y, im, transform=None):
423 """
424 Draw an RGBA image.
425
426 Parameters
427 ----------
428 gc : `.GraphicsContextBase`
429 A graphics context with clipping information.
430
431 x : scalar
432 The distance in physical units (i.e., dots or pixels) from the left
433 hand side of the canvas.
434
435 y : scalar
436 The distance in physical units (i.e., dots or pixels) from the
437 bottom side of the canvas.
438
439 im : (N, M, 4) array of `numpy.uint8`
440 An array of RGBA pixels.
441
442 transform : `~matplotlib.transforms.Affine2DBase`
443 If and only if the concrete backend is written such that
444 `option_scale_image` returns ``True``, an affine transformation
445 (i.e., an `.Affine2DBase`) *may* be passed to `draw_image`. The
446 translation vector of the transformation is given in physical units
447 (i.e., dots or pixels). Note that the transformation does not
448 override *x* and *y*, and has to be applied *before* translating
449 the result by *x* and *y* (this can be accomplished by adding *x*
450 and *y* to the translation vector defined by *transform*).
451 """
452 raise NotImplementedError
453
454 def option_image_nocomposite(self):
455 """
456 Return whether image composition by Matplotlib should be skipped.
457
458 Raster backends should usually return False (letting the C-level
459 rasterizer take care of image composition); vector backends should
460 usually return ``not rcParams["image.composite_image"]``.
461 """
462 return False
463
464 def option_scale_image(self):
465 """
466 Return whether arbitrary affine transformations in `draw_image` are
467 supported (True for most vector backends).
468 """
469 return False
470
471 def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None):
472 """
473 Draw a TeX instance.
474
475 Parameters
476 ----------
477 gc : `.GraphicsContextBase`
478 The graphics context.
479 x : float
480 The x location of the text in display coords.
481 y : float
482 The y location of the text baseline in display coords.
483 s : str
484 The TeX text string.
485 prop : `~matplotlib.font_manager.FontProperties`
486 The font properties.
487 angle : float
488 The rotation angle in degrees anti-clockwise.
489 mtext : `~matplotlib.text.Text`
490 The original text object to be rendered.
491 """
492 self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX")
493
494 def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None):
495 """
496 Draw a text instance.
497
498 Parameters
499 ----------
500 gc : `.GraphicsContextBase`
501 The graphics context.
502 x : float
503 The x location of the text in display coords.
504 y : float
505 The y location of the text baseline in display coords.
506 s : str
507 The text string.
508 prop : `~matplotlib.font_manager.FontProperties`
509 The font properties.
510 angle : float
511 The rotation angle in degrees anti-clockwise.
512 ismath : bool or "TeX"
513 If True, use mathtext parser. If "TeX", use tex for rendering.
514 mtext : `~matplotlib.text.Text`
515 The original text object to be rendered.
516 """
517 self._draw_text_as_path(gc, x, y, s, prop, angle, ismath)
518
519 def _get_text_path_transform(self, x, y, s, prop, angle, ismath):
520 """
521 Return the text path and transform.
522
523 Parameters
524 ----------
525 x : float
526 The x location of the text in display coords.
527 y : float
528 The y location of the text baseline in display coords.
529 s : str
530 The text to be converted.
531 prop : `~matplotlib.font_manager.FontProperties`
532 The font property.
533 angle : float
534 Angle in degrees to render the text at.
535 ismath : bool or "TeX"
536 If True, use mathtext parser. If "TeX", use tex for rendering.
537 """
538
539 text2path = self._text2path
540 fontsize = self.points_to_pixels(prop.get_size_in_points())
541 verts, codes = text2path.get_text_path(prop, s, ismath=ismath)
542
543 path = Path(verts, codes)
544 angle = np.deg2rad(angle)
545 if self.flipy():
546 width, height = self.get_canvas_width_height()
547 transform = (Affine2D()
548 .scale(fontsize / text2path.FONT_SCALE)
549 .rotate(angle)
550 .translate(x, height - y))
551 else:
552 transform = (Affine2D()
553 .scale(fontsize / text2path.FONT_SCALE)
554 .rotate(angle)
555 .translate(x, y))
556
557 return path, transform
558
559 def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath):
560 """
561 Draw the text by converting them to paths using `.TextToPath`.
562
563 Parameters
564 ----------
565 gc : `.GraphicsContextBase`
566 The graphics context.
567 x : float
568 The x location of the text in display coords.
569 y : float
570 The y location of the text baseline in display coords.
571 s : str
572 The text to be converted.
573 prop : `~matplotlib.font_manager.FontProperties`
574 The font property.
575 angle : float
576 Angle in degrees to render the text at.
577 ismath : bool or "TeX"
578 If True, use mathtext parser. If "TeX", use tex for rendering.
579 """
580 path, transform = self._get_text_path_transform(
581 x, y, s, prop, angle, ismath)
582 color = gc.get_rgb()
583 gc.set_linewidth(0.0)
584 self.draw_path(gc, path, transform, rgbFace=color)
585
586 def get_text_width_height_descent(self, s, prop, ismath):
587 """
588 Get the width, height, and descent (offset from the bottom to the baseline), in
589 display coords, of the string *s* with `.FontProperties` *prop*.
590
591 Whitespace at the start and the end of *s* is included in the reported width.
592 """
593 fontsize = prop.get_size_in_points()
594
595 if ismath == 'TeX':
596 # todo: handle properties
597 return self.get_texmanager().get_text_width_height_descent(
598 s, fontsize, renderer=self)
599
600 dpi = self.points_to_pixels(72)
601 if ismath:
602 dims = self._text2path.mathtext_parser.parse(s, dpi, prop)
603 return dims[0:3] # return width, height, descent
604
605 flags = self._text2path._get_hinting_flag()
606 font = self._text2path._get_font(prop)
607 font.set_size(fontsize, dpi)
608 # the width and height of unrotated string
609 font.set_text(s, 0.0, flags=flags)
610 w, h = font.get_width_height()
611 d = font.get_descent()
612 w /= 64.0 # convert from subpixels
613 h /= 64.0
614 d /= 64.0
615 return w, h, d
616
617 def flipy(self):
618 """
619 Return whether y values increase from top to bottom.
620
621 Note that this only affects drawing of texts.
622 """
623 return True
624
625 def get_canvas_width_height(self):
626 """Return the canvas width and height in display coords."""
627 return 1, 1
628
629 def get_texmanager(self):
630 """Return the `.TexManager` instance."""
631 if self._texmanager is None:
632 self._texmanager = TexManager()
633 return self._texmanager
634
635 def new_gc(self):
636 """Return an instance of a `.GraphicsContextBase`."""
637 return GraphicsContextBase()
638
639 def points_to_pixels(self, points):
640 """
641 Convert points to display units.
642
643 You need to override this function (unless your backend
644 doesn't have a dpi, e.g., postscript or svg). Some imaging
645 systems assume some value for pixels per inch::
646
647 points to pixels = points * pixels_per_inch/72 * dpi/72
648
649 Parameters
650 ----------
651 points : float or array-like
652
653 Returns
654 -------
655 Points converted to pixels
656 """
657 return points
658
659 def start_rasterizing(self):
660 """
661 Switch to the raster renderer.
662
663 Used by `.MixedModeRenderer`.
664 """
665
666 def stop_rasterizing(self):
667 """
668 Switch back to the vector renderer and draw the contents of the raster
669 renderer as an image on the vector renderer.
670
671 Used by `.MixedModeRenderer`.
672 """
673
674 def start_filter(self):
675 """
676 Switch to a temporary renderer for image filtering effects.
677
678 Currently only supported by the agg renderer.
679 """
680
681 def stop_filter(self, filter_func):
682 """
683 Switch back to the original renderer. The contents of the temporary
684 renderer is processed with the *filter_func* and is drawn on the
685 original renderer as an image.
686
687 Currently only supported by the agg renderer.
688 """
689
690 def _draw_disabled(self):
691 """
692 Context manager to temporary disable drawing.
693
694 This is used for getting the drawn size of Artists. This lets us
695 run the draw process to update any Python state but does not pay the
696 cost of the draw_XYZ calls on the canvas.
697 """
698 no_ops = {
699 meth_name: lambda *args, **kwargs: None
700 for meth_name in dir(RendererBase)
701 if (meth_name.startswith("draw_")
702 or meth_name in ["open_group", "close_group"])
703 }
704
705 return _setattr_cm(self, **no_ops)
706
707
708class GraphicsContextBase:
709 """An abstract base class that provides color, line styles, etc."""
710
711 def __init__(self):
712 self._alpha = 1.0
713 self._forced_alpha = False # if True, _alpha overrides A from RGBA
714 self._antialiased = 1 # use 0, 1 not True, False for extension code
715 self._capstyle = CapStyle('butt')
716 self._cliprect = None
717 self._clippath = None
718 self._dashes = 0, None
719 self._joinstyle = JoinStyle('round')
720 self._linestyle = 'solid'
721 self._linewidth = 1
722 self._rgb = (0.0, 0.0, 0.0, 1.0)
723 self._hatch = None
724 self._hatch_color = colors.to_rgba(rcParams['hatch.color'])
725 self._hatch_linewidth = rcParams['hatch.linewidth']
726 self._url = None
727 self._gid = None
728 self._snap = None
729 self._sketch = None
730
731 def copy_properties(self, gc):
732 """Copy properties from *gc* to self."""
733 self._alpha = gc._alpha
734 self._forced_alpha = gc._forced_alpha
735 self._antialiased = gc._antialiased
736 self._capstyle = gc._capstyle
737 self._cliprect = gc._cliprect
738 self._clippath = gc._clippath
739 self._dashes = gc._dashes
740 self._joinstyle = gc._joinstyle
741 self._linestyle = gc._linestyle
742 self._linewidth = gc._linewidth
743 self._rgb = gc._rgb
744 self._hatch = gc._hatch
745 self._hatch_color = gc._hatch_color
746 self._hatch_linewidth = gc._hatch_linewidth
747 self._url = gc._url
748 self._gid = gc._gid
749 self._snap = gc._snap
750 self._sketch = gc._sketch
751
752 def restore(self):
753 """
754 Restore the graphics context from the stack - needed only
755 for backends that save graphics contexts on a stack.
756 """
757
758 def get_alpha(self):
759 """
760 Return the alpha value used for blending - not supported on all
761 backends.
762 """
763 return self._alpha
764
765 def get_antialiased(self):
766 """Return whether the object should try to do antialiased rendering."""
767 return self._antialiased
768
769 def get_capstyle(self):
770 """Return the `.CapStyle`."""
771 return self._capstyle.name
772
773 def get_clip_rectangle(self):
774 """
775 Return the clip rectangle as a `~matplotlib.transforms.Bbox` instance.
776 """
777 return self._cliprect
778
779 def get_clip_path(self):
780 """
781 Return the clip path in the form (path, transform), where path
782 is a `~.path.Path` instance, and transform is
783 an affine transform to apply to the path before clipping.
784 """
785 if self._clippath is not None:
786 tpath, tr = self._clippath.get_transformed_path_and_affine()
787 if np.all(np.isfinite(tpath.vertices)):
788 return tpath, tr
789 else:
790 _log.warning("Ill-defined clip_path detected. Returning None.")
791 return None, None
792 return None, None
793
794 def get_dashes(self):
795 """
796 Return the dash style as an (offset, dash-list) pair.
797
798 See `.set_dashes` for details.
799
800 Default value is (None, None).
801 """
802 return self._dashes
803
804 def get_forced_alpha(self):
805 """
806 Return whether the value given by get_alpha() should be used to
807 override any other alpha-channel values.
808 """
809 return self._forced_alpha
810
811 def get_joinstyle(self):
812 """Return the `.JoinStyle`."""
813 return self._joinstyle.name
814
815 def get_linewidth(self):
816 """Return the line width in points."""
817 return self._linewidth
818
819 def get_rgb(self):
820 """Return a tuple of three or four floats from 0-1."""
821 return self._rgb
822
823 def get_url(self):
824 """Return a url if one is set, None otherwise."""
825 return self._url
826
827 def get_gid(self):
828 """Return the object identifier if one is set, None otherwise."""
829 return self._gid
830
831 def get_snap(self):
832 """
833 Return the snap setting, which can be:
834
835 * True: snap vertices to the nearest pixel center
836 * False: leave vertices as-is
837 * None: (auto) If the path contains only rectilinear line segments,
838 round to the nearest pixel center
839 """
840 return self._snap
841
842 def set_alpha(self, alpha):
843 """
844 Set the alpha value used for blending - not supported on all backends.
845
846 If ``alpha=None`` (the default), the alpha components of the
847 foreground and fill colors will be used to set their respective
848 transparencies (where applicable); otherwise, ``alpha`` will override
849 them.
850 """
851 if alpha is not None:
852 self._alpha = alpha
853 self._forced_alpha = True
854 else:
855 self._alpha = 1.0
856 self._forced_alpha = False
857 self.set_foreground(self._rgb, isRGBA=True)
858
859 def set_antialiased(self, b):
860 """Set whether object should be drawn with antialiased rendering."""
861 # Use ints to make life easier on extension code trying to read the gc.
862 self._antialiased = int(bool(b))
863
864 @_docstring.interpd
865 def set_capstyle(self, cs):
866 """
867 Set how to draw endpoints of lines.
868
869 Parameters
870 ----------
871 cs : `.CapStyle` or %(CapStyle)s
872 """
873 self._capstyle = CapStyle(cs)
874
875 def set_clip_rectangle(self, rectangle):
876 """Set the clip rectangle to a `.Bbox` or None."""
877 self._cliprect = rectangle
878
879 def set_clip_path(self, path):
880 """Set the clip path to a `.TransformedPath` or None."""
881 _api.check_isinstance((transforms.TransformedPath, None), path=path)
882 self._clippath = path
883
884 def set_dashes(self, dash_offset, dash_list):
885 """
886 Set the dash style for the gc.
887
888 Parameters
889 ----------
890 dash_offset : float
891 Distance, in points, into the dash pattern at which to
892 start the pattern. It is usually set to 0.
893 dash_list : array-like or None
894 The on-off sequence as points. None specifies a solid line. All
895 values must otherwise be non-negative (:math:`\\ge 0`).
896
897 Notes
898 -----
899 See p. 666 of the PostScript
900 `Language Reference
901 <https://www.adobe.com/jp/print/postscript/pdfs/PLRM.pdf>`_
902 for more info.
903 """
904 if dash_list is not None:
905 dl = np.asarray(dash_list)
906 if np.any(dl < 0.0):
907 raise ValueError(
908 "All values in the dash list must be non-negative")
909 if dl.size and not np.any(dl > 0.0):
910 raise ValueError(
911 'At least one value in the dash list must be positive')
912 self._dashes = dash_offset, dash_list
913
914 def set_foreground(self, fg, isRGBA=False):
915 """
916 Set the foreground color.
917
918 Parameters
919 ----------
920 fg : :mpltype:`color`
921 isRGBA : bool
922 If *fg* is known to be an ``(r, g, b, a)`` tuple, *isRGBA* can be
923 set to True to improve performance.
924 """
925 if self._forced_alpha and isRGBA:
926 self._rgb = fg[:3] + (self._alpha,)
927 elif self._forced_alpha:
928 self._rgb = colors.to_rgba(fg, self._alpha)
929 elif isRGBA:
930 self._rgb = fg
931 else:
932 self._rgb = colors.to_rgba(fg)
933
934 @_docstring.interpd
935 def set_joinstyle(self, js):
936 """
937 Set how to draw connections between line segments.
938
939 Parameters
940 ----------
941 js : `.JoinStyle` or %(JoinStyle)s
942 """
943 self._joinstyle = JoinStyle(js)
944
945 def set_linewidth(self, w):
946 """Set the linewidth in points."""
947 self._linewidth = float(w)
948
949 def set_url(self, url):
950 """Set the url for links in compatible backends."""
951 self._url = url
952
953 def set_gid(self, id):
954 """Set the id."""
955 self._gid = id
956
957 def set_snap(self, snap):
958 """
959 Set the snap setting which may be:
960
961 * True: snap vertices to the nearest pixel center
962 * False: leave vertices as-is
963 * None: (auto) If the path contains only rectilinear line segments,
964 round to the nearest pixel center
965 """
966 self._snap = snap
967
968 def set_hatch(self, hatch):
969 """Set the hatch style (for fills)."""
970 self._hatch = hatch
971
972 def get_hatch(self):
973 """Get the current hatch style."""
974 return self._hatch
975
976 def get_hatch_path(self, density=6.0):
977 """Return a `.Path` for the current hatch."""
978 hatch = self.get_hatch()
979 if hatch is None:
980 return None
981 return Path.hatch(hatch, density)
982
983 def get_hatch_color(self):
984 """Get the hatch color."""
985 return self._hatch_color
986
987 def set_hatch_color(self, hatch_color):
988 """Set the hatch color."""
989 self._hatch_color = hatch_color
990
991 def get_hatch_linewidth(self):
992 """Get the hatch linewidth."""
993 return self._hatch_linewidth
994
995 def get_sketch_params(self):
996 """
997 Return the sketch parameters for the artist.
998
999 Returns
1000 -------
1001 tuple or `None`
1002
1003 A 3-tuple with the following elements:
1004
1005 * ``scale``: The amplitude of the wiggle perpendicular to the
1006 source line.
1007 * ``length``: The length of the wiggle along the line.
1008 * ``randomness``: The scale factor by which the length is
1009 shrunken or expanded.
1010
1011 May return `None` if no sketch parameters were set.
1012 """
1013 return self._sketch
1014
1015 def set_sketch_params(self, scale=None, length=None, randomness=None):
1016 """
1017 Set the sketch parameters.
1018
1019 Parameters
1020 ----------
1021 scale : float, optional
1022 The amplitude of the wiggle perpendicular to the source line, in
1023 pixels. If scale is `None`, or not provided, no sketch filter will
1024 be provided.
1025 length : float, default: 128
1026 The length of the wiggle along the line, in pixels.
1027 randomness : float, default: 16
1028 The scale factor by which the length is shrunken or expanded.
1029 """
1030 self._sketch = (
1031 None if scale is None
1032 else (scale, length or 128., randomness or 16.))
1033
1034
1035class TimerBase:
1036 """
1037 A base class for providing timer events, useful for things animations.
1038 Backends need to implement a few specific methods in order to use their
1039 own timing mechanisms so that the timer events are integrated into their
1040 event loops.
1041
1042 Subclasses must override the following methods:
1043
1044 - ``_timer_start``: Backend-specific code for starting the timer.
1045 - ``_timer_stop``: Backend-specific code for stopping the timer.
1046
1047 Subclasses may additionally override the following methods:
1048
1049 - ``_timer_set_single_shot``: Code for setting the timer to single shot
1050 operating mode, if supported by the timer object. If not, the `Timer`
1051 class itself will store the flag and the ``_on_timer`` method should be
1052 overridden to support such behavior.
1053
1054 - ``_timer_set_interval``: Code for setting the interval on the timer, if
1055 there is a method for doing so on the timer object.
1056
1057 - ``_on_timer``: The internal function that any timer object should call,
1058 which will handle the task of running all callbacks that have been set.
1059 """
1060
1061 def __init__(self, interval=None, callbacks=None):
1062 """
1063 Parameters
1064 ----------
1065 interval : int, default: 1000ms
1066 The time between timer events in milliseconds. Will be stored as
1067 ``timer.interval``.
1068 callbacks : list[tuple[callable, tuple, dict]]
1069 List of (func, args, kwargs) tuples that will be called upon timer
1070 events. This list is accessible as ``timer.callbacks`` and can be
1071 manipulated directly, or the functions `~.TimerBase.add_callback`
1072 and `~.TimerBase.remove_callback` can be used.
1073 """
1074 self.callbacks = [] if callbacks is None else callbacks.copy()
1075 # Set .interval and not ._interval to go through the property setter.
1076 self.interval = 1000 if interval is None else interval
1077 self.single_shot = False
1078
1079 def __del__(self):
1080 """Need to stop timer and possibly disconnect timer."""
1081 self._timer_stop()
1082
1083 @_api.delete_parameter("3.9", "interval", alternative="timer.interval")
1084 def start(self, interval=None):
1085 """
1086 Start the timer object.
1087
1088 Parameters
1089 ----------
1090 interval : int, optional
1091 Timer interval in milliseconds; overrides a previously set interval
1092 if provided.
1093 """
1094 if interval is not None:
1095 self.interval = interval
1096 self._timer_start()
1097
1098 def stop(self):
1099 """Stop the timer."""
1100 self._timer_stop()
1101
1102 def _timer_start(self):
1103 pass
1104
1105 def _timer_stop(self):
1106 pass
1107
1108 @property
1109 def interval(self):
1110 """The time between timer events, in milliseconds."""
1111 return self._interval
1112
1113 @interval.setter
1114 def interval(self, interval):
1115 # Force to int since none of the backends actually support fractional
1116 # milliseconds, and some error or give warnings.
1117 # Some backends also fail when interval == 0, so ensure >= 1 msec
1118 interval = max(int(interval), 1)
1119 self._interval = interval
1120 self._timer_set_interval()
1121
1122 @property
1123 def single_shot(self):
1124 """Whether this timer should stop after a single run."""
1125 return self._single
1126
1127 @single_shot.setter
1128 def single_shot(self, ss):
1129 self._single = ss
1130 self._timer_set_single_shot()
1131
1132 def add_callback(self, func, *args, **kwargs):
1133 """
1134 Register *func* to be called by timer when the event fires. Any
1135 additional arguments provided will be passed to *func*.
1136
1137 This function returns *func*, which makes it possible to use it as a
1138 decorator.
1139 """
1140 self.callbacks.append((func, args, kwargs))
1141 return func
1142
1143 def remove_callback(self, func, *args, **kwargs):
1144 """
1145 Remove *func* from list of callbacks.
1146
1147 *args* and *kwargs* are optional and used to distinguish between copies
1148 of the same function registered to be called with different arguments.
1149 This behavior is deprecated. In the future, ``*args, **kwargs`` won't
1150 be considered anymore; to keep a specific callback removable by itself,
1151 pass it to `add_callback` as a `functools.partial` object.
1152 """
1153 if args or kwargs:
1154 _api.warn_deprecated(
1155 "3.1", message="In a future version, Timer.remove_callback "
1156 "will not take *args, **kwargs anymore, but remove all "
1157 "callbacks where the callable matches; to keep a specific "
1158 "callback removable by itself, pass it to add_callback as a "
1159 "functools.partial object.")
1160 self.callbacks.remove((func, args, kwargs))
1161 else:
1162 funcs = [c[0] for c in self.callbacks]
1163 if func in funcs:
1164 self.callbacks.pop(funcs.index(func))
1165
1166 def _timer_set_interval(self):
1167 """Used to set interval on underlying timer object."""
1168
1169 def _timer_set_single_shot(self):
1170 """Used to set single shot on underlying timer object."""
1171
1172 def _on_timer(self):
1173 """
1174 Runs all function that have been registered as callbacks. Functions
1175 can return False (or 0) if they should not be called any more. If there
1176 are no callbacks, the timer is automatically stopped.
1177 """
1178 for func, args, kwargs in self.callbacks:
1179 ret = func(*args, **kwargs)
1180 # docstring above explains why we use `if ret == 0` here,
1181 # instead of `if not ret`.
1182 # This will also catch `ret == False` as `False == 0`
1183 # but does not annoy the linters
1184 # https://docs.python.org/3/library/stdtypes.html#boolean-values
1185 if ret == 0:
1186 self.callbacks.remove((func, args, kwargs))
1187
1188 if len(self.callbacks) == 0:
1189 self.stop()
1190
1191
1192class Event:
1193 """
1194 A Matplotlib event.
1195
1196 The following attributes are defined and shown with their default values.
1197 Subclasses may define additional attributes.
1198
1199 Attributes
1200 ----------
1201 name : str
1202 The event name.
1203 canvas : `FigureCanvasBase`
1204 The backend-specific canvas instance generating the event.
1205 guiEvent
1206 The GUI event that triggered the Matplotlib event.
1207 """
1208
1209 def __init__(self, name, canvas, guiEvent=None):
1210 self.name = name
1211 self.canvas = canvas
1212 self._guiEvent = guiEvent
1213 self._guiEvent_deleted = False
1214
1215 def _process(self):
1216 """Process this event on ``self.canvas``, then unset ``guiEvent``."""
1217 self.canvas.callbacks.process(self.name, self)
1218 self._guiEvent_deleted = True
1219
1220 @property
1221 def guiEvent(self):
1222 # After deprecation elapses: remove _guiEvent_deleted; make guiEvent a plain
1223 # attribute set to None by _process.
1224 if self._guiEvent_deleted:
1225 _api.warn_deprecated(
1226 "3.8", message="Accessing guiEvent outside of the original GUI event "
1227 "handler is unsafe and deprecated since %(since)s; in the future, the "
1228 "attribute will be set to None after quitting the event handler. You "
1229 "may separately record the value of the guiEvent attribute at your own "
1230 "risk.")
1231 return self._guiEvent
1232
1233
1234class DrawEvent(Event):
1235 """
1236 An event triggered by a draw operation on the canvas.
1237
1238 In most backends, callbacks subscribed to this event will be fired after
1239 the rendering is complete but before the screen is updated. Any extra
1240 artists drawn to the canvas's renderer will be reflected without an
1241 explicit call to ``blit``.
1242
1243 .. warning::
1244
1245 Calling ``canvas.draw`` and ``canvas.blit`` in these callbacks may
1246 not be safe with all backends and may cause infinite recursion.
1247
1248 A DrawEvent has a number of special attributes in addition to those defined
1249 by the parent `Event` class.
1250
1251 Attributes
1252 ----------
1253 renderer : `RendererBase`
1254 The renderer for the draw event.
1255 """
1256 def __init__(self, name, canvas, renderer):
1257 super().__init__(name, canvas)
1258 self.renderer = renderer
1259
1260
1261class ResizeEvent(Event):
1262 """
1263 An event triggered by a canvas resize.
1264
1265 A ResizeEvent has a number of special attributes in addition to those
1266 defined by the parent `Event` class.
1267
1268 Attributes
1269 ----------
1270 width : int
1271 Width of the canvas in pixels.
1272 height : int
1273 Height of the canvas in pixels.
1274 """
1275
1276 def __init__(self, name, canvas):
1277 super().__init__(name, canvas)
1278 self.width, self.height = canvas.get_width_height()
1279
1280
1281class CloseEvent(Event):
1282 """An event triggered by a figure being closed."""
1283
1284
1285class LocationEvent(Event):
1286 """
1287 An event that has a screen location.
1288
1289 A LocationEvent has a number of special attributes in addition to those
1290 defined by the parent `Event` class.
1291
1292 Attributes
1293 ----------
1294 x, y : int or None
1295 Event location in pixels from bottom left of canvas.
1296 inaxes : `~matplotlib.axes.Axes` or None
1297 The `~.axes.Axes` instance over which the mouse is, if any.
1298 xdata, ydata : float or None
1299 Data coordinates of the mouse within *inaxes*, or *None* if the mouse
1300 is not over an Axes.
1301 modifiers : frozenset
1302 The keyboard modifiers currently being pressed (except for KeyEvent).
1303 """
1304
1305 # Fully delete all occurrences of lastevent after deprecation elapses.
1306 _lastevent = None
1307 lastevent = _api.deprecated("3.8")(
1308 _api.classproperty(lambda cls: cls._lastevent))
1309 _last_axes_ref = None
1310
1311 def __init__(self, name, canvas, x, y, guiEvent=None, *, modifiers=None):
1312 super().__init__(name, canvas, guiEvent=guiEvent)
1313 # x position - pixels from left of canvas
1314 self.x = int(x) if x is not None else x
1315 # y position - pixels from right of canvas
1316 self.y = int(y) if y is not None else y
1317 self.inaxes = None # the Axes instance the mouse is over
1318 self.xdata = None # x coord of mouse in data coords
1319 self.ydata = None # y coord of mouse in data coords
1320 self.modifiers = frozenset(modifiers if modifiers is not None else [])
1321
1322 if x is None or y is None:
1323 # cannot check if event was in Axes if no (x, y) info
1324 return
1325
1326 self._set_inaxes(self.canvas.inaxes((x, y))
1327 if self.canvas.mouse_grabber is None else
1328 self.canvas.mouse_grabber,
1329 (x, y))
1330
1331 # Splitting _set_inaxes out is useful for the axes_leave_event handler: it
1332 # needs to generate synthetic LocationEvents with manually-set inaxes. In
1333 # that latter case, xy has already been cast to int so it can directly be
1334 # read from self.x, self.y; in the normal case, however, it is more
1335 # accurate to pass the untruncated float x, y values passed to the ctor.
1336
1337 def _set_inaxes(self, inaxes, xy=None):
1338 self.inaxes = inaxes
1339 if inaxes is not None:
1340 try:
1341 self.xdata, self.ydata = inaxes.transData.inverted().transform(
1342 xy if xy is not None else (self.x, self.y))
1343 except ValueError:
1344 pass
1345
1346
1347class MouseButton(IntEnum):
1348 LEFT = 1
1349 MIDDLE = 2
1350 RIGHT = 3
1351 BACK = 8
1352 FORWARD = 9
1353
1354
1355class MouseEvent(LocationEvent):
1356 """
1357 A mouse event ('button_press_event', 'button_release_event', \
1358'scroll_event', 'motion_notify_event').
1359
1360 A MouseEvent has a number of special attributes in addition to those
1361 defined by the parent `Event` and `LocationEvent` classes.
1362
1363 Attributes
1364 ----------
1365 button : None or `MouseButton` or {'up', 'down'}
1366 The button pressed. 'up' and 'down' are used for scroll events.
1367
1368 Note that LEFT and RIGHT actually refer to the "primary" and
1369 "secondary" buttons, i.e. if the user inverts their left and right
1370 buttons ("left-handed setting") then the LEFT button will be the one
1371 physically on the right.
1372
1373 If this is unset, *name* is "scroll_event", and *step* is nonzero, then
1374 this will be set to "up" or "down" depending on the sign of *step*.
1375
1376 key : None or str
1377 The key pressed when the mouse event triggered, e.g. 'shift'.
1378 See `KeyEvent`.
1379
1380 .. warning::
1381 This key is currently obtained from the last 'key_press_event' or
1382 'key_release_event' that occurred within the canvas. Thus, if the
1383 last change of keyboard state occurred while the canvas did not have
1384 focus, this attribute will be wrong. On the other hand, the
1385 ``modifiers`` attribute should always be correct, but it can only
1386 report on modifier keys.
1387
1388 step : float
1389 The number of scroll steps (positive for 'up', negative for 'down').
1390 This applies only to 'scroll_event' and defaults to 0 otherwise.
1391
1392 dblclick : bool
1393 Whether the event is a double-click. This applies only to
1394 'button_press_event' and is False otherwise. In particular, it's
1395 not used in 'button_release_event'.
1396
1397 Examples
1398 --------
1399 ::
1400
1401 def on_press(event):
1402 print('you pressed', event.button, event.xdata, event.ydata)
1403
1404 cid = fig.canvas.mpl_connect('button_press_event', on_press)
1405 """
1406
1407 def __init__(self, name, canvas, x, y, button=None, key=None,
1408 step=0, dblclick=False, guiEvent=None, *, modifiers=None):
1409 super().__init__(
1410 name, canvas, x, y, guiEvent=guiEvent, modifiers=modifiers)
1411 if button in MouseButton.__members__.values():
1412 button = MouseButton(button)
1413 if name == "scroll_event" and button is None:
1414 if step > 0:
1415 button = "up"
1416 elif step < 0:
1417 button = "down"
1418 self.button = button
1419 self.key = key
1420 self.step = step
1421 self.dblclick = dblclick
1422
1423 def __str__(self):
1424 return (f"{self.name}: "
1425 f"xy=({self.x}, {self.y}) xydata=({self.xdata}, {self.ydata}) "
1426 f"button={self.button} dblclick={self.dblclick} "
1427 f"inaxes={self.inaxes}")
1428
1429
1430class PickEvent(Event):
1431 """
1432 A pick event.
1433
1434 This event is fired when the user picks a location on the canvas
1435 sufficiently close to an artist that has been made pickable with
1436 `.Artist.set_picker`.
1437
1438 A PickEvent has a number of special attributes in addition to those defined
1439 by the parent `Event` class.
1440
1441 Attributes
1442 ----------
1443 mouseevent : `MouseEvent`
1444 The mouse event that generated the pick.
1445 artist : `~matplotlib.artist.Artist`
1446 The picked artist. Note that artists are not pickable by default
1447 (see `.Artist.set_picker`).
1448 other
1449 Additional attributes may be present depending on the type of the
1450 picked object; e.g., a `.Line2D` pick may define different extra
1451 attributes than a `.PatchCollection` pick.
1452
1453 Examples
1454 --------
1455 Bind a function ``on_pick()`` to pick events, that prints the coordinates
1456 of the picked data point::
1457
1458 ax.plot(np.rand(100), 'o', picker=5) # 5 points tolerance
1459
1460 def on_pick(event):
1461 line = event.artist
1462 xdata, ydata = line.get_data()
1463 ind = event.ind
1464 print(f'on pick line: {xdata[ind]:.3f}, {ydata[ind]:.3f}')
1465
1466 cid = fig.canvas.mpl_connect('pick_event', on_pick)
1467 """
1468
1469 def __init__(self, name, canvas, mouseevent, artist,
1470 guiEvent=None, **kwargs):
1471 if guiEvent is None:
1472 guiEvent = mouseevent.guiEvent
1473 super().__init__(name, canvas, guiEvent)
1474 self.mouseevent = mouseevent
1475 self.artist = artist
1476 self.__dict__.update(kwargs)
1477
1478
1479class KeyEvent(LocationEvent):
1480 """
1481 A key event (key press, key release).
1482
1483 A KeyEvent has a number of special attributes in addition to those defined
1484 by the parent `Event` and `LocationEvent` classes.
1485
1486 Attributes
1487 ----------
1488 key : None or str
1489 The key(s) pressed. Could be *None*, a single case sensitive Unicode
1490 character ("g", "G", "#", etc.), a special key ("control", "shift",
1491 "f1", "up", etc.) or a combination of the above (e.g., "ctrl+alt+g",
1492 "ctrl+alt+G").
1493
1494 Notes
1495 -----
1496 Modifier keys will be prefixed to the pressed key and will be in the order
1497 "ctrl", "alt", "super". The exception to this rule is when the pressed key
1498 is itself a modifier key, therefore "ctrl+alt" and "alt+control" can both
1499 be valid key values.
1500
1501 Examples
1502 --------
1503 ::
1504
1505 def on_key(event):
1506 print('you pressed', event.key, event.xdata, event.ydata)
1507
1508 cid = fig.canvas.mpl_connect('key_press_event', on_key)
1509 """
1510
1511 def __init__(self, name, canvas, key, x=0, y=0, guiEvent=None):
1512 super().__init__(name, canvas, x, y, guiEvent=guiEvent)
1513 self.key = key
1514
1515
1516# Default callback for key events.
1517def _key_handler(event):
1518 # Dead reckoning of key.
1519 if event.name == "key_press_event":
1520 event.canvas._key = event.key
1521 elif event.name == "key_release_event":
1522 event.canvas._key = None
1523
1524
1525# Default callback for mouse events.
1526def _mouse_handler(event):
1527 # Dead-reckoning of button and key.
1528 if event.name == "button_press_event":
1529 event.canvas._button = event.button
1530 elif event.name == "button_release_event":
1531 event.canvas._button = None
1532 elif event.name == "motion_notify_event" and event.button is None:
1533 event.button = event.canvas._button
1534 if event.key is None:
1535 event.key = event.canvas._key
1536 # Emit axes_enter/axes_leave.
1537 if event.name == "motion_notify_event":
1538 last_ref = LocationEvent._last_axes_ref
1539 last_axes = last_ref() if last_ref else None
1540 if last_axes != event.inaxes:
1541 if last_axes is not None:
1542 # Create a synthetic LocationEvent for the axes_leave_event.
1543 # Its inaxes attribute needs to be manually set (because the
1544 # cursor is actually *out* of that Axes at that point); this is
1545 # done with the internal _set_inaxes method which ensures that
1546 # the xdata and ydata attributes are also correct.
1547 try:
1548 leave_event = LocationEvent(
1549 "axes_leave_event", last_axes.figure.canvas,
1550 event.x, event.y, event.guiEvent,
1551 modifiers=event.modifiers)
1552 leave_event._set_inaxes(last_axes)
1553 last_axes.figure.canvas.callbacks.process(
1554 "axes_leave_event", leave_event)
1555 except Exception:
1556 pass # The last canvas may already have been torn down.
1557 if event.inaxes is not None:
1558 event.canvas.callbacks.process("axes_enter_event", event)
1559 LocationEvent._last_axes_ref = (
1560 weakref.ref(event.inaxes) if event.inaxes else None)
1561 LocationEvent._lastevent = (
1562 None if event.name == "figure_leave_event" else event)
1563
1564
1565def _get_renderer(figure, print_method=None):
1566 """
1567 Get the renderer that would be used to save a `.Figure`.
1568
1569 If you need a renderer without any active draw methods use
1570 renderer._draw_disabled to temporary patch them out at your call site.
1571 """
1572 # This is implemented by triggering a draw, then immediately jumping out of
1573 # Figure.draw() by raising an exception.
1574
1575 class Done(Exception):
1576 pass
1577
1578 def _draw(renderer): raise Done(renderer)
1579
1580 with cbook._setattr_cm(figure, draw=_draw), ExitStack() as stack:
1581 if print_method is None:
1582 fmt = figure.canvas.get_default_filetype()
1583 # Even for a canvas' default output type, a canvas switch may be
1584 # needed, e.g. for FigureCanvasBase.
1585 print_method = stack.enter_context(
1586 figure.canvas._switch_canvas_and_return_print_method(fmt))
1587 try:
1588 print_method(io.BytesIO())
1589 except Done as exc:
1590 renderer, = exc.args
1591 return renderer
1592 else:
1593 raise RuntimeError(f"{print_method} did not call Figure.draw, so "
1594 f"no renderer is available")
1595
1596
1597def _no_output_draw(figure):
1598 # _no_output_draw was promoted to the figure level, but
1599 # keep this here in case someone was calling it...
1600 figure.draw_without_rendering()
1601
1602
1603def _is_non_interactive_terminal_ipython(ip):
1604 """
1605 Return whether we are in a terminal IPython, but non interactive.
1606
1607 When in _terminal_ IPython, ip.parent will have and `interact` attribute,
1608 if this attribute is False we do not setup eventloop integration as the
1609 user will _not_ interact with IPython. In all other case (ZMQKernel, or is
1610 interactive), we do.
1611 """
1612 return (hasattr(ip, 'parent')
1613 and (ip.parent is not None)
1614 and getattr(ip.parent, 'interact', None) is False)
1615
1616
1617@contextmanager
1618def _allow_interrupt(prepare_notifier, handle_sigint):
1619 """
1620 A context manager that allows terminating a plot by sending a SIGINT. It
1621 is necessary because the running backend prevents the Python interpreter
1622 from running and processing signals (i.e., to raise a KeyboardInterrupt).
1623 To solve this, one needs to somehow wake up the interpreter and make it
1624 close the plot window. We do this by using the signal.set_wakeup_fd()
1625 function which organizes a write of the signal number into a socketpair.
1626 A backend-specific function, *prepare_notifier*, arranges to listen to
1627 the pair's read socket while the event loop is running. (If it returns a
1628 notifier object, that object is kept alive while the context manager runs.)
1629
1630 If SIGINT was indeed caught, after exiting the on_signal() function the
1631 interpreter reacts to the signal according to the handler function which
1632 had been set up by a signal.signal() call; here, we arrange to call the
1633 backend-specific *handle_sigint* function. Finally, we call the old SIGINT
1634 handler with the same arguments that were given to our custom handler.
1635
1636 We do this only if the old handler for SIGINT was not None, which means
1637 that a non-python handler was installed, i.e. in Julia, and not SIG_IGN
1638 which means we should ignore the interrupts.
1639
1640 Parameters
1641 ----------
1642 prepare_notifier : Callable[[socket.socket], object]
1643 handle_sigint : Callable[[], object]
1644 """
1645
1646 old_sigint_handler = signal.getsignal(signal.SIGINT)
1647 if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL):
1648 yield
1649 return
1650
1651 handler_args = None
1652 wsock, rsock = socket.socketpair()
1653 wsock.setblocking(False)
1654 rsock.setblocking(False)
1655 old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno())
1656 notifier = prepare_notifier(rsock)
1657
1658 def save_args_and_handle_sigint(*args):
1659 nonlocal handler_args
1660 handler_args = args
1661 handle_sigint()
1662
1663 signal.signal(signal.SIGINT, save_args_and_handle_sigint)
1664 try:
1665 yield
1666 finally:
1667 wsock.close()
1668 rsock.close()
1669 signal.set_wakeup_fd(old_wakeup_fd)
1670 signal.signal(signal.SIGINT, old_sigint_handler)
1671 if handler_args is not None:
1672 old_sigint_handler(*handler_args)
1673
1674
1675class FigureCanvasBase:
1676 """
1677 The canvas the figure renders into.
1678
1679 Attributes
1680 ----------
1681 figure : `~matplotlib.figure.Figure`
1682 A high-level figure instance.
1683 """
1684
1685 # Set to one of {"qt", "gtk3", "gtk4", "wx", "tk", "macosx"} if an
1686 # interactive framework is required, or None otherwise.
1687 required_interactive_framework = None
1688
1689 # The manager class instantiated by new_manager.
1690 # (This is defined as a classproperty because the manager class is
1691 # currently defined *after* the canvas class, but one could also assign
1692 # ``FigureCanvasBase.manager_class = FigureManagerBase``
1693 # after defining both classes.)
1694 manager_class = _api.classproperty(lambda cls: FigureManagerBase)
1695
1696 events = [
1697 'resize_event',
1698 'draw_event',
1699 'key_press_event',
1700 'key_release_event',
1701 'button_press_event',
1702 'button_release_event',
1703 'scroll_event',
1704 'motion_notify_event',
1705 'pick_event',
1706 'figure_enter_event',
1707 'figure_leave_event',
1708 'axes_enter_event',
1709 'axes_leave_event',
1710 'close_event'
1711 ]
1712
1713 fixed_dpi = None
1714
1715 filetypes = _default_filetypes
1716
1717 @_api.classproperty
1718 def supports_blit(cls):
1719 """If this Canvas sub-class supports blitting."""
1720 return (hasattr(cls, "copy_from_bbox")
1721 and hasattr(cls, "restore_region"))
1722
1723 def __init__(self, figure=None):
1724 from matplotlib.figure import Figure
1725 self._fix_ipython_backend2gui()
1726 self._is_idle_drawing = True
1727 self._is_saving = False
1728 if figure is None:
1729 figure = Figure()
1730 figure.set_canvas(self)
1731 self.figure = figure
1732 self.manager = None
1733 self.widgetlock = widgets.LockDraw()
1734 self._button = None # the button pressed
1735 self._key = None # the key pressed
1736 self.mouse_grabber = None # the Axes currently grabbing mouse
1737 self.toolbar = None # NavigationToolbar2 will set me
1738 self._is_idle_drawing = False
1739 # We don't want to scale up the figure DPI more than once.
1740 figure._original_dpi = figure.dpi
1741 self._device_pixel_ratio = 1
1742 super().__init__() # Typically the GUI widget init (if any).
1743
1744 callbacks = property(lambda self: self.figure._canvas_callbacks)
1745 button_pick_id = property(lambda self: self.figure._button_pick_id)
1746 scroll_pick_id = property(lambda self: self.figure._scroll_pick_id)
1747
1748 @classmethod
1749 @functools.cache
1750 def _fix_ipython_backend2gui(cls):
1751 # Fix hard-coded module -> toolkit mapping in IPython (used for
1752 # `ipython --auto`). This cannot be done at import time due to
1753 # ordering issues, so we do it when creating a canvas, and should only
1754 # be done once per class (hence the `cache`).
1755
1756 # This function will not be needed when Python 3.12, the latest version
1757 # supported by IPython < 8.24, reaches end-of-life in late 2028.
1758 # At that time this function can be made a no-op and deprecated.
1759 mod_ipython = sys.modules.get("IPython")
1760 if mod_ipython is None or mod_ipython.version_info[:2] >= (8, 24):
1761 # Use of backend2gui is not needed for IPython >= 8.24 as the
1762 # functionality has been moved to Matplotlib.
1763 return
1764
1765 import IPython
1766 ip = IPython.get_ipython()
1767 if not ip:
1768 return
1769 from IPython.core import pylabtools as pt
1770 if (not hasattr(pt, "backend2gui")
1771 or not hasattr(ip, "enable_matplotlib")):
1772 # In case we ever move the patch to IPython and remove these APIs,
1773 # don't break on our side.
1774 return
1775 backend2gui_rif = {
1776 "qt": "qt",
1777 "gtk3": "gtk3",
1778 "gtk4": "gtk4",
1779 "wx": "wx",
1780 "macosx": "osx",
1781 }.get(cls.required_interactive_framework)
1782 if backend2gui_rif:
1783 if _is_non_interactive_terminal_ipython(ip):
1784 ip.enable_gui(backend2gui_rif)
1785
1786 @classmethod
1787 def new_manager(cls, figure, num):
1788 """
1789 Create a new figure manager for *figure*, using this canvas class.
1790
1791 Notes
1792 -----
1793 This method should not be reimplemented in subclasses. If
1794 custom manager creation logic is needed, please reimplement
1795 ``FigureManager.create_with_canvas``.
1796 """
1797 return cls.manager_class.create_with_canvas(cls, figure, num)
1798
1799 @contextmanager
1800 def _idle_draw_cntx(self):
1801 self._is_idle_drawing = True
1802 try:
1803 yield
1804 finally:
1805 self._is_idle_drawing = False
1806
1807 def is_saving(self):
1808 """
1809 Return whether the renderer is in the process of saving
1810 to a file, rather than rendering for an on-screen buffer.
1811 """
1812 return self._is_saving
1813
1814 def blit(self, bbox=None):
1815 """Blit the canvas in bbox (default entire canvas)."""
1816
1817 def inaxes(self, xy):
1818 """
1819 Return the topmost visible `~.axes.Axes` containing the point *xy*.
1820
1821 Parameters
1822 ----------
1823 xy : (float, float)
1824 (x, y) pixel positions from left/bottom of the canvas.
1825
1826 Returns
1827 -------
1828 `~matplotlib.axes.Axes` or None
1829 The topmost visible Axes containing the point, or None if there
1830 is no Axes at the point.
1831 """
1832 axes_list = [a for a in self.figure.get_axes()
1833 if a.patch.contains_point(xy) and a.get_visible()]
1834 if axes_list:
1835 axes = cbook._topmost_artist(axes_list)
1836 else:
1837 axes = None
1838
1839 return axes
1840
1841 def grab_mouse(self, ax):
1842 """
1843 Set the child `~.axes.Axes` which is grabbing the mouse events.
1844
1845 Usually called by the widgets themselves. It is an error to call this
1846 if the mouse is already grabbed by another Axes.
1847 """
1848 if self.mouse_grabber not in (None, ax):
1849 raise RuntimeError("Another Axes already grabs mouse input")
1850 self.mouse_grabber = ax
1851
1852 def release_mouse(self, ax):
1853 """
1854 Release the mouse grab held by the `~.axes.Axes` *ax*.
1855
1856 Usually called by the widgets. It is ok to call this even if *ax*
1857 doesn't have the mouse grab currently.
1858 """
1859 if self.mouse_grabber is ax:
1860 self.mouse_grabber = None
1861
1862 def set_cursor(self, cursor):
1863 """
1864 Set the current cursor.
1865
1866 This may have no effect if the backend does not display anything.
1867
1868 If required by the backend, this method should trigger an update in
1869 the backend event loop after the cursor is set, as this method may be
1870 called e.g. before a long-running task during which the GUI is not
1871 updated.
1872
1873 Parameters
1874 ----------
1875 cursor : `.Cursors`
1876 The cursor to display over the canvas. Note: some backends may
1877 change the cursor for the entire window.
1878 """
1879
1880 def draw(self, *args, **kwargs):
1881 """
1882 Render the `.Figure`.
1883
1884 This method must walk the artist tree, even if no output is produced,
1885 because it triggers deferred work that users may want to access
1886 before saving output to disk. For example computing limits,
1887 auto-limits, and tick values.
1888 """
1889
1890 def draw_idle(self, *args, **kwargs):
1891 """
1892 Request a widget redraw once control returns to the GUI event loop.
1893
1894 Even if multiple calls to `draw_idle` occur before control returns
1895 to the GUI event loop, the figure will only be rendered once.
1896
1897 Notes
1898 -----
1899 Backends may choose to override the method and implement their own
1900 strategy to prevent multiple renderings.
1901
1902 """
1903 if not self._is_idle_drawing:
1904 with self._idle_draw_cntx():
1905 self.draw(*args, **kwargs)
1906
1907 @property
1908 def device_pixel_ratio(self):
1909 """
1910 The ratio of physical to logical pixels used for the canvas on screen.
1911
1912 By default, this is 1, meaning physical and logical pixels are the same
1913 size. Subclasses that support High DPI screens may set this property to
1914 indicate that said ratio is different. All Matplotlib interaction,
1915 unless working directly with the canvas, remains in logical pixels.
1916
1917 """
1918 return self._device_pixel_ratio
1919
1920 def _set_device_pixel_ratio(self, ratio):
1921 """
1922 Set the ratio of physical to logical pixels used for the canvas.
1923
1924 Subclasses that support High DPI screens can set this property to
1925 indicate that said ratio is different. The canvas itself will be
1926 created at the physical size, while the client side will use the
1927 logical size. Thus the DPI of the Figure will change to be scaled by
1928 this ratio. Implementations that support High DPI screens should use
1929 physical pixels for events so that transforms back to Axes space are
1930 correct.
1931
1932 By default, this is 1, meaning physical and logical pixels are the same
1933 size.
1934
1935 Parameters
1936 ----------
1937 ratio : float
1938 The ratio of logical to physical pixels used for the canvas.
1939
1940 Returns
1941 -------
1942 bool
1943 Whether the ratio has changed. Backends may interpret this as a
1944 signal to resize the window, repaint the canvas, or change any
1945 other relevant properties.
1946 """
1947 if self._device_pixel_ratio == ratio:
1948 return False
1949 # In cases with mixed resolution displays, we need to be careful if the
1950 # device pixel ratio changes - in this case we need to resize the
1951 # canvas accordingly. Some backends provide events that indicate a
1952 # change in DPI, but those that don't will update this before drawing.
1953 dpi = ratio * self.figure._original_dpi
1954 self.figure._set_dpi(dpi, forward=False)
1955 self._device_pixel_ratio = ratio
1956 return True
1957
1958 def get_width_height(self, *, physical=False):
1959 """
1960 Return the figure width and height in integral points or pixels.
1961
1962 When the figure is used on High DPI screens (and the backend supports
1963 it), the truncation to integers occurs after scaling by the device
1964 pixel ratio.
1965
1966 Parameters
1967 ----------
1968 physical : bool, default: False
1969 Whether to return true physical pixels or logical pixels. Physical
1970 pixels may be used by backends that support HiDPI, but still
1971 configure the canvas using its actual size.
1972
1973 Returns
1974 -------
1975 width, height : int
1976 The size of the figure, in points or pixels, depending on the
1977 backend.
1978 """
1979 return tuple(int(size / (1 if physical else self.device_pixel_ratio))
1980 for size in self.figure.bbox.max)
1981
1982 @classmethod
1983 def get_supported_filetypes(cls):
1984 """Return dict of savefig file formats supported by this backend."""
1985 return cls.filetypes
1986
1987 @classmethod
1988 def get_supported_filetypes_grouped(cls):
1989 """
1990 Return a dict of savefig file formats supported by this backend,
1991 where the keys are a file type name, such as 'Joint Photographic
1992 Experts Group', and the values are a list of filename extensions used
1993 for that filetype, such as ['jpg', 'jpeg'].
1994 """
1995 groupings = {}
1996 for ext, name in cls.filetypes.items():
1997 groupings.setdefault(name, []).append(ext)
1998 groupings[name].sort()
1999 return groupings
2000
2001 @contextmanager
2002 def _switch_canvas_and_return_print_method(self, fmt, backend=None):
2003 """
2004 Context manager temporarily setting the canvas for saving the figure::
2005
2006 with canvas._switch_canvas_and_return_print_method(fmt, backend) \\
2007 as print_method:
2008 # ``print_method`` is a suitable ``print_{fmt}`` method, and
2009 # the figure's canvas is temporarily switched to the method's
2010 # canvas within the with... block. ``print_method`` is also
2011 # wrapped to suppress extra kwargs passed by ``print_figure``.
2012
2013 Parameters
2014 ----------
2015 fmt : str
2016 If *backend* is None, then determine a suitable canvas class for
2017 saving to format *fmt* -- either the current canvas class, if it
2018 supports *fmt*, or whatever `get_registered_canvas_class` returns;
2019 switch the figure canvas to that canvas class.
2020 backend : str or None, default: None
2021 If not None, switch the figure canvas to the ``FigureCanvas`` class
2022 of the given backend.
2023 """
2024 canvas = None
2025 if backend is not None:
2026 # Return a specific canvas class, if requested.
2027 from .backends.registry import backend_registry
2028 canvas_class = backend_registry.load_backend_module(backend).FigureCanvas
2029 if not hasattr(canvas_class, f"print_{fmt}"):
2030 raise ValueError(
2031 f"The {backend!r} backend does not support {fmt} output")
2032 canvas = canvas_class(self.figure)
2033 elif hasattr(self, f"print_{fmt}"):
2034 # Return the current canvas if it supports the requested format.
2035 canvas = self
2036 else:
2037 # Return a default canvas for the requested format, if it exists.
2038 canvas_class = get_registered_canvas_class(fmt)
2039 if canvas_class is None:
2040 raise ValueError(
2041 "Format {!r} is not supported (supported formats: {})".format(
2042 fmt, ", ".join(sorted(self.get_supported_filetypes()))))
2043 canvas = canvas_class(self.figure)
2044 canvas._is_saving = self._is_saving
2045 meth = getattr(canvas, f"print_{fmt}")
2046 mod = (meth.func.__module__
2047 if hasattr(meth, "func") # partialmethod, e.g. backend_wx.
2048 else meth.__module__)
2049 if mod.startswith(("matplotlib.", "mpl_toolkits.")):
2050 optional_kws = { # Passed by print_figure for other renderers.
2051 "dpi", "facecolor", "edgecolor", "orientation",
2052 "bbox_inches_restore"}
2053 skip = optional_kws - {*inspect.signature(meth).parameters}
2054 print_method = functools.wraps(meth)(lambda *args, **kwargs: meth(
2055 *args, **{k: v for k, v in kwargs.items() if k not in skip}))
2056 else: # Let third-parties do as they see fit.
2057 print_method = meth
2058 try:
2059 yield print_method
2060 finally:
2061 self.figure.canvas = self
2062
2063 def print_figure(
2064 self, filename, dpi=None, facecolor=None, edgecolor=None,
2065 orientation='portrait', format=None, *,
2066 bbox_inches=None, pad_inches=None, bbox_extra_artists=None,
2067 backend=None, **kwargs):
2068 """
2069 Render the figure to hardcopy. Set the figure patch face and edge
2070 colors. This is useful because some of the GUIs have a gray figure
2071 face color background and you'll probably want to override this on
2072 hardcopy.
2073
2074 Parameters
2075 ----------
2076 filename : str or path-like or file-like
2077 The file where the figure is saved.
2078
2079 dpi : float, default: :rc:`savefig.dpi`
2080 The dots per inch to save the figure in.
2081
2082 facecolor : :mpltype:`color` or 'auto', default: :rc:`savefig.facecolor`
2083 The facecolor of the figure. If 'auto', use the current figure
2084 facecolor.
2085
2086 edgecolor : :mpltype:`color` or 'auto', default: :rc:`savefig.edgecolor`
2087 The edgecolor of the figure. If 'auto', use the current figure
2088 edgecolor.
2089
2090 orientation : {'landscape', 'portrait'}, default: 'portrait'
2091 Only currently applies to PostScript printing.
2092
2093 format : str, optional
2094 Force a specific file format. If not given, the format is inferred
2095 from the *filename* extension, and if that fails from
2096 :rc:`savefig.format`.
2097
2098 bbox_inches : 'tight' or `.Bbox`, default: :rc:`savefig.bbox`
2099 Bounding box in inches: only the given portion of the figure is
2100 saved. If 'tight', try to figure out the tight bbox of the figure.
2101
2102 pad_inches : float or 'layout', default: :rc:`savefig.pad_inches`
2103 Amount of padding in inches around the figure when bbox_inches is
2104 'tight'. If 'layout' use the padding from the constrained or
2105 compressed layout engine; ignored if one of those engines is not in
2106 use.
2107
2108 bbox_extra_artists : list of `~matplotlib.artist.Artist`, optional
2109 A list of extra artists that will be considered when the
2110 tight bbox is calculated.
2111
2112 backend : str, optional
2113 Use a non-default backend to render the file, e.g. to render a
2114 png file with the "cairo" backend rather than the default "agg",
2115 or a pdf file with the "pgf" backend rather than the default
2116 "pdf". Note that the default backend is normally sufficient. See
2117 :ref:`the-builtin-backends` for a list of valid backends for each
2118 file format. Custom backends can be referenced as "module://...".
2119 """
2120 if format is None:
2121 # get format from filename, or from backend's default filetype
2122 if isinstance(filename, os.PathLike):
2123 filename = os.fspath(filename)
2124 if isinstance(filename, str):
2125 format = os.path.splitext(filename)[1][1:]
2126 if format is None or format == '':
2127 format = self.get_default_filetype()
2128 if isinstance(filename, str):
2129 filename = filename.rstrip('.') + '.' + format
2130 format = format.lower()
2131
2132 if dpi is None:
2133 dpi = rcParams['savefig.dpi']
2134 if dpi == 'figure':
2135 dpi = getattr(self.figure, '_original_dpi', self.figure.dpi)
2136
2137 if kwargs.get("papertype") == 'auto':
2138 # When deprecation elapses, remove backend_ps._get_papertype & its callers.
2139 _api.warn_deprecated(
2140 "3.8", name="papertype='auto'", addendum="Pass an explicit paper type, "
2141 "'figure', or omit the *papertype* argument entirely.")
2142
2143 # Remove the figure manager, if any, to avoid resizing the GUI widget.
2144 with cbook._setattr_cm(self, manager=None), \
2145 self._switch_canvas_and_return_print_method(format, backend) \
2146 as print_method, \
2147 cbook._setattr_cm(self.figure, dpi=dpi), \
2148 cbook._setattr_cm(self.figure.canvas, _device_pixel_ratio=1), \
2149 cbook._setattr_cm(self.figure.canvas, _is_saving=True), \
2150 ExitStack() as stack:
2151
2152 for prop in ["facecolor", "edgecolor"]:
2153 color = locals()[prop]
2154 if color is None:
2155 color = rcParams[f"savefig.{prop}"]
2156 if not cbook._str_equal(color, "auto"):
2157 stack.enter_context(self.figure._cm_set(**{prop: color}))
2158
2159 if bbox_inches is None:
2160 bbox_inches = rcParams['savefig.bbox']
2161
2162 layout_engine = self.figure.get_layout_engine()
2163 if layout_engine is not None or bbox_inches == "tight":
2164 # we need to trigger a draw before printing to make sure
2165 # CL works. "tight" also needs a draw to get the right
2166 # locations:
2167 renderer = _get_renderer(
2168 self.figure,
2169 functools.partial(
2170 print_method, orientation=orientation)
2171 )
2172 # we do this instead of `self.figure.draw_without_rendering`
2173 # so that we can inject the orientation
2174 with getattr(renderer, "_draw_disabled", nullcontext)():
2175 self.figure.draw(renderer)
2176 if bbox_inches:
2177 if bbox_inches == "tight":
2178 bbox_inches = self.figure.get_tightbbox(
2179 renderer, bbox_extra_artists=bbox_extra_artists)
2180 if (isinstance(layout_engine, ConstrainedLayoutEngine) and
2181 pad_inches == "layout"):
2182 h_pad = layout_engine.get()["h_pad"]
2183 w_pad = layout_engine.get()["w_pad"]
2184 else:
2185 if pad_inches in [None, "layout"]:
2186 pad_inches = rcParams['savefig.pad_inches']
2187 h_pad = w_pad = pad_inches
2188 bbox_inches = bbox_inches.padded(w_pad, h_pad)
2189
2190 # call adjust_bbox to save only the given area
2191 restore_bbox = _tight_bbox.adjust_bbox(
2192 self.figure, bbox_inches, self.figure.canvas.fixed_dpi)
2193
2194 _bbox_inches_restore = (bbox_inches, restore_bbox)
2195 else:
2196 _bbox_inches_restore = None
2197
2198 # we have already done layout above, so turn it off:
2199 stack.enter_context(self.figure._cm_set(layout_engine='none'))
2200 try:
2201 # _get_renderer may change the figure dpi (as vector formats
2202 # force the figure dpi to 72), so we need to set it again here.
2203 with cbook._setattr_cm(self.figure, dpi=dpi):
2204 result = print_method(
2205 filename,
2206 facecolor=facecolor,
2207 edgecolor=edgecolor,
2208 orientation=orientation,
2209 bbox_inches_restore=_bbox_inches_restore,
2210 **kwargs)
2211 finally:
2212 if bbox_inches and restore_bbox:
2213 restore_bbox()
2214
2215 return result
2216
2217 @classmethod
2218 def get_default_filetype(cls):
2219 """
2220 Return the default savefig file format as specified in
2221 :rc:`savefig.format`.
2222
2223 The returned string does not include a period. This method is
2224 overridden in backends that only support a single file type.
2225 """
2226 return rcParams['savefig.format']
2227
2228 def get_default_filename(self):
2229 """
2230 Return a string, which includes extension, suitable for use as
2231 a default filename.
2232 """
2233 basename = (self.manager.get_window_title() if self.manager is not None
2234 else '')
2235 basename = (basename or 'image').replace(' ', '_')
2236 filetype = self.get_default_filetype()
2237 filename = basename + '.' + filetype
2238 return filename
2239
2240 @_api.deprecated("3.8")
2241 def switch_backends(self, FigureCanvasClass):
2242 """
2243 Instantiate an instance of FigureCanvasClass
2244
2245 This is used for backend switching, e.g., to instantiate a
2246 FigureCanvasPS from a FigureCanvasGTK. Note, deep copying is
2247 not done, so any changes to one of the instances (e.g., setting
2248 figure size or line props), will be reflected in the other
2249 """
2250 newCanvas = FigureCanvasClass(self.figure)
2251 newCanvas._is_saving = self._is_saving
2252 return newCanvas
2253
2254 def mpl_connect(self, s, func):
2255 """
2256 Bind function *func* to event *s*.
2257
2258 Parameters
2259 ----------
2260 s : str
2261 One of the following events ids:
2262
2263 - 'button_press_event'
2264 - 'button_release_event'
2265 - 'draw_event'
2266 - 'key_press_event'
2267 - 'key_release_event'
2268 - 'motion_notify_event'
2269 - 'pick_event'
2270 - 'resize_event'
2271 - 'scroll_event'
2272 - 'figure_enter_event',
2273 - 'figure_leave_event',
2274 - 'axes_enter_event',
2275 - 'axes_leave_event'
2276 - 'close_event'.
2277
2278 func : callable
2279 The callback function to be executed, which must have the
2280 signature::
2281
2282 def func(event: Event) -> Any
2283
2284 For the location events (button and key press/release), if the
2285 mouse is over the Axes, the ``inaxes`` attribute of the event will
2286 be set to the `~matplotlib.axes.Axes` the event occurs is over, and
2287 additionally, the variables ``xdata`` and ``ydata`` attributes will
2288 be set to the mouse location in data coordinates. See `.KeyEvent`
2289 and `.MouseEvent` for more info.
2290
2291 .. note::
2292
2293 If func is a method, this only stores a weak reference to the
2294 method. Thus, the figure does not influence the lifetime of
2295 the associated object. Usually, you want to make sure that the
2296 object is kept alive throughout the lifetime of the figure by
2297 holding a reference to it.
2298
2299 Returns
2300 -------
2301 cid
2302 A connection id that can be used with
2303 `.FigureCanvasBase.mpl_disconnect`.
2304
2305 Examples
2306 --------
2307 ::
2308
2309 def on_press(event):
2310 print('you pressed', event.button, event.xdata, event.ydata)
2311
2312 cid = canvas.mpl_connect('button_press_event', on_press)
2313 """
2314
2315 return self.callbacks.connect(s, func)
2316
2317 def mpl_disconnect(self, cid):
2318 """
2319 Disconnect the callback with id *cid*.
2320
2321 Examples
2322 --------
2323 ::
2324
2325 cid = canvas.mpl_connect('button_press_event', on_press)
2326 # ... later
2327 canvas.mpl_disconnect(cid)
2328 """
2329 self.callbacks.disconnect(cid)
2330
2331 # Internal subclasses can override _timer_cls instead of new_timer, though
2332 # this is not a public API for third-party subclasses.
2333 _timer_cls = TimerBase
2334
2335 def new_timer(self, interval=None, callbacks=None):
2336 """
2337 Create a new backend-specific subclass of `.Timer`.
2338
2339 This is useful for getting periodic events through the backend's native
2340 event loop. Implemented only for backends with GUIs.
2341
2342 Parameters
2343 ----------
2344 interval : int
2345 Timer interval in milliseconds.
2346
2347 callbacks : list[tuple[callable, tuple, dict]]
2348 Sequence of (func, args, kwargs) where ``func(*args, **kwargs)``
2349 will be executed by the timer every *interval*.
2350
2351 Callbacks which return ``False`` or ``0`` will be removed from the
2352 timer.
2353
2354 Examples
2355 --------
2356 >>> timer = fig.canvas.new_timer(callbacks=[(f1, (1,), {'a': 3})])
2357 """
2358 return self._timer_cls(interval=interval, callbacks=callbacks)
2359
2360 def flush_events(self):
2361 """
2362 Flush the GUI events for the figure.
2363
2364 Interactive backends need to reimplement this method.
2365 """
2366
2367 def start_event_loop(self, timeout=0):
2368 """
2369 Start a blocking event loop.
2370
2371 Such an event loop is used by interactive functions, such as
2372 `~.Figure.ginput` and `~.Figure.waitforbuttonpress`, to wait for
2373 events.
2374
2375 The event loop blocks until a callback function triggers
2376 `stop_event_loop`, or *timeout* is reached.
2377
2378 If *timeout* is 0 or negative, never timeout.
2379
2380 Only interactive backends need to reimplement this method and it relies
2381 on `flush_events` being properly implemented.
2382
2383 Interactive backends should implement this in a more native way.
2384 """
2385 if timeout <= 0:
2386 timeout = np.inf
2387 timestep = 0.01
2388 counter = 0
2389 self._looping = True
2390 while self._looping and counter * timestep < timeout:
2391 self.flush_events()
2392 time.sleep(timestep)
2393 counter += 1
2394
2395 def stop_event_loop(self):
2396 """
2397 Stop the current blocking event loop.
2398
2399 Interactive backends need to reimplement this to match
2400 `start_event_loop`
2401 """
2402 self._looping = False
2403
2404
2405def key_press_handler(event, canvas=None, toolbar=None):
2406 """
2407 Implement the default Matplotlib key bindings for the canvas and toolbar
2408 described at :ref:`key-event-handling`.
2409
2410 Parameters
2411 ----------
2412 event : `KeyEvent`
2413 A key press/release event.
2414 canvas : `FigureCanvasBase`, default: ``event.canvas``
2415 The backend-specific canvas instance. This parameter is kept for
2416 back-compatibility, but, if set, should always be equal to
2417 ``event.canvas``.
2418 toolbar : `NavigationToolbar2`, default: ``event.canvas.toolbar``
2419 The navigation cursor toolbar. This parameter is kept for
2420 back-compatibility, but, if set, should always be equal to
2421 ``event.canvas.toolbar``.
2422 """
2423 if event.key is None:
2424 return
2425 if canvas is None:
2426 canvas = event.canvas
2427 if toolbar is None:
2428 toolbar = canvas.toolbar
2429
2430 # toggle fullscreen mode (default key 'f', 'ctrl + f')
2431 if event.key in rcParams['keymap.fullscreen']:
2432 try:
2433 canvas.manager.full_screen_toggle()
2434 except AttributeError:
2435 pass
2436
2437 # quit the figure (default key 'ctrl+w')
2438 if event.key in rcParams['keymap.quit']:
2439 Gcf.destroy_fig(canvas.figure)
2440 if event.key in rcParams['keymap.quit_all']:
2441 Gcf.destroy_all()
2442
2443 if toolbar is not None:
2444 # home or reset mnemonic (default key 'h', 'home' and 'r')
2445 if event.key in rcParams['keymap.home']:
2446 toolbar.home()
2447 # forward / backward keys to enable left handed quick navigation
2448 # (default key for backward: 'left', 'backspace' and 'c')
2449 elif event.key in rcParams['keymap.back']:
2450 toolbar.back()
2451 # (default key for forward: 'right' and 'v')
2452 elif event.key in rcParams['keymap.forward']:
2453 toolbar.forward()
2454 # pan mnemonic (default key 'p')
2455 elif event.key in rcParams['keymap.pan']:
2456 toolbar.pan()
2457 toolbar._update_cursor(event)
2458 # zoom mnemonic (default key 'o')
2459 elif event.key in rcParams['keymap.zoom']:
2460 toolbar.zoom()
2461 toolbar._update_cursor(event)
2462 # saving current figure (default key 's')
2463 elif event.key in rcParams['keymap.save']:
2464 toolbar.save_figure()
2465
2466 if event.inaxes is None:
2467 return
2468
2469 # these bindings require the mouse to be over an Axes to trigger
2470 def _get_uniform_gridstate(ticks):
2471 # Return True/False if all grid lines are on or off, None if they are
2472 # not all in the same state.
2473 return (True if all(tick.gridline.get_visible() for tick in ticks) else
2474 False if not any(tick.gridline.get_visible() for tick in ticks) else
2475 None)
2476
2477 ax = event.inaxes
2478 # toggle major grids in current Axes (default key 'g')
2479 # Both here and below (for 'G'), we do nothing if *any* grid (major or
2480 # minor, x or y) is not in a uniform state, to avoid messing up user
2481 # customization.
2482 if (event.key in rcParams['keymap.grid']
2483 # Exclude minor grids not in a uniform state.
2484 and None not in [_get_uniform_gridstate(ax.xaxis.minorTicks),
2485 _get_uniform_gridstate(ax.yaxis.minorTicks)]):
2486 x_state = _get_uniform_gridstate(ax.xaxis.majorTicks)
2487 y_state = _get_uniform_gridstate(ax.yaxis.majorTicks)
2488 cycle = [(False, False), (True, False), (True, True), (False, True)]
2489 try:
2490 x_state, y_state = (
2491 cycle[(cycle.index((x_state, y_state)) + 1) % len(cycle)])
2492 except ValueError:
2493 # Exclude major grids not in a uniform state.
2494 pass
2495 else:
2496 # If turning major grids off, also turn minor grids off.
2497 ax.grid(x_state, which="major" if x_state else "both", axis="x")
2498 ax.grid(y_state, which="major" if y_state else "both", axis="y")
2499 canvas.draw_idle()
2500 # toggle major and minor grids in current Axes (default key 'G')
2501 if (event.key in rcParams['keymap.grid_minor']
2502 # Exclude major grids not in a uniform state.
2503 and None not in [_get_uniform_gridstate(ax.xaxis.majorTicks),
2504 _get_uniform_gridstate(ax.yaxis.majorTicks)]):
2505 x_state = _get_uniform_gridstate(ax.xaxis.minorTicks)
2506 y_state = _get_uniform_gridstate(ax.yaxis.minorTicks)
2507 cycle = [(False, False), (True, False), (True, True), (False, True)]
2508 try:
2509 x_state, y_state = (
2510 cycle[(cycle.index((x_state, y_state)) + 1) % len(cycle)])
2511 except ValueError:
2512 # Exclude minor grids not in a uniform state.
2513 pass
2514 else:
2515 ax.grid(x_state, which="both", axis="x")
2516 ax.grid(y_state, which="both", axis="y")
2517 canvas.draw_idle()
2518 # toggle scaling of y-axes between 'log and 'linear' (default key 'l')
2519 elif event.key in rcParams['keymap.yscale']:
2520 scale = ax.get_yscale()
2521 if scale == 'log':
2522 ax.set_yscale('linear')
2523 ax.figure.canvas.draw_idle()
2524 elif scale == 'linear':
2525 try:
2526 ax.set_yscale('log')
2527 except ValueError as exc:
2528 _log.warning(str(exc))
2529 ax.set_yscale('linear')
2530 ax.figure.canvas.draw_idle()
2531 # toggle scaling of x-axes between 'log and 'linear' (default key 'k')
2532 elif event.key in rcParams['keymap.xscale']:
2533 scalex = ax.get_xscale()
2534 if scalex == 'log':
2535 ax.set_xscale('linear')
2536 ax.figure.canvas.draw_idle()
2537 elif scalex == 'linear':
2538 try:
2539 ax.set_xscale('log')
2540 except ValueError as exc:
2541 _log.warning(str(exc))
2542 ax.set_xscale('linear')
2543 ax.figure.canvas.draw_idle()
2544
2545
2546def button_press_handler(event, canvas=None, toolbar=None):
2547 """
2548 The default Matplotlib button actions for extra mouse buttons.
2549
2550 Parameters are as for `key_press_handler`, except that *event* is a
2551 `MouseEvent`.
2552 """
2553 if canvas is None:
2554 canvas = event.canvas
2555 if toolbar is None:
2556 toolbar = canvas.toolbar
2557 if toolbar is not None:
2558 button_name = str(MouseButton(event.button))
2559 if button_name in rcParams['keymap.back']:
2560 toolbar.back()
2561 elif button_name in rcParams['keymap.forward']:
2562 toolbar.forward()
2563
2564
2565class NonGuiException(Exception):
2566 """Raised when trying show a figure in a non-GUI backend."""
2567 pass
2568
2569
2570class FigureManagerBase:
2571 """
2572 A backend-independent abstraction of a figure container and controller.
2573
2574 The figure manager is used by pyplot to interact with the window in a
2575 backend-independent way. It's an adapter for the real (GUI) framework that
2576 represents the visual figure on screen.
2577
2578 The figure manager is connected to a specific canvas instance, which in turn
2579 is connected to a specific figure instance. To access a figure manager for
2580 a given figure in user code, you typically use ``fig.canvas.manager``.
2581
2582 GUI backends derive from this class to translate common operations such
2583 as *show* or *resize* to the GUI-specific code. Non-GUI backends do not
2584 support these operations and can just use the base class.
2585
2586 This following basic operations are accessible:
2587
2588 **Window operations**
2589
2590 - `~.FigureManagerBase.show`
2591 - `~.FigureManagerBase.destroy`
2592 - `~.FigureManagerBase.full_screen_toggle`
2593 - `~.FigureManagerBase.resize`
2594 - `~.FigureManagerBase.get_window_title`
2595 - `~.FigureManagerBase.set_window_title`
2596
2597 **Key and mouse button press handling**
2598
2599 The figure manager sets up default key and mouse button press handling by
2600 hooking up the `.key_press_handler` to the matplotlib event system. This
2601 ensures the same shortcuts and mouse actions across backends.
2602
2603 **Other operations**
2604
2605 Subclasses will have additional attributes and functions to access
2606 additional functionality. This is of course backend-specific. For example,
2607 most GUI backends have ``window`` and ``toolbar`` attributes that give
2608 access to the native GUI widgets of the respective framework.
2609
2610 Attributes
2611 ----------
2612 canvas : `FigureCanvasBase`
2613 The backend-specific canvas instance.
2614
2615 num : int or str
2616 The figure number.
2617
2618 key_press_handler_id : int
2619 The default key handler cid, when using the toolmanager.
2620 To disable the default key press handling use::
2621
2622 figure.canvas.mpl_disconnect(
2623 figure.canvas.manager.key_press_handler_id)
2624
2625 button_press_handler_id : int
2626 The default mouse button handler cid, when using the toolmanager.
2627 To disable the default button press handling use::
2628
2629 figure.canvas.mpl_disconnect(
2630 figure.canvas.manager.button_press_handler_id)
2631 """
2632
2633 _toolbar2_class = None
2634 _toolmanager_toolbar_class = None
2635
2636 def __init__(self, canvas, num):
2637 self.canvas = canvas
2638 canvas.manager = self # store a pointer to parent
2639 self.num = num
2640 self.set_window_title(f"Figure {num:d}")
2641
2642 self.key_press_handler_id = None
2643 self.button_press_handler_id = None
2644 if rcParams['toolbar'] != 'toolmanager':
2645 self.key_press_handler_id = self.canvas.mpl_connect(
2646 'key_press_event', key_press_handler)
2647 self.button_press_handler_id = self.canvas.mpl_connect(
2648 'button_press_event', button_press_handler)
2649
2650 self.toolmanager = (ToolManager(canvas.figure)
2651 if mpl.rcParams['toolbar'] == 'toolmanager'
2652 else None)
2653 if (mpl.rcParams["toolbar"] == "toolbar2"
2654 and self._toolbar2_class):
2655 self.toolbar = self._toolbar2_class(self.canvas)
2656 elif (mpl.rcParams["toolbar"] == "toolmanager"
2657 and self._toolmanager_toolbar_class):
2658 self.toolbar = self._toolmanager_toolbar_class(self.toolmanager)
2659 else:
2660 self.toolbar = None
2661
2662 if self.toolmanager:
2663 tools.add_tools_to_manager(self.toolmanager)
2664 if self.toolbar:
2665 tools.add_tools_to_container(self.toolbar)
2666
2667 @self.canvas.figure.add_axobserver
2668 def notify_axes_change(fig):
2669 # Called whenever the current Axes is changed.
2670 if self.toolmanager is None and self.toolbar is not None:
2671 self.toolbar.update()
2672
2673 @classmethod
2674 def create_with_canvas(cls, canvas_class, figure, num):
2675 """
2676 Create a manager for a given *figure* using a specific *canvas_class*.
2677
2678 Backends should override this method if they have specific needs for
2679 setting up the canvas or the manager.
2680 """
2681 return cls(canvas_class(figure), num)
2682
2683 @classmethod
2684 def start_main_loop(cls):
2685 """
2686 Start the main event loop.
2687
2688 This method is called by `.FigureManagerBase.pyplot_show`, which is the
2689 implementation of `.pyplot.show`. To customize the behavior of
2690 `.pyplot.show`, interactive backends should usually override
2691 `~.FigureManagerBase.start_main_loop`; if more customized logic is
2692 necessary, `~.FigureManagerBase.pyplot_show` can also be overridden.
2693 """
2694
2695 @classmethod
2696 def pyplot_show(cls, *, block=None):
2697 """
2698 Show all figures. This method is the implementation of `.pyplot.show`.
2699
2700 To customize the behavior of `.pyplot.show`, interactive backends
2701 should usually override `~.FigureManagerBase.start_main_loop`; if more
2702 customized logic is necessary, `~.FigureManagerBase.pyplot_show` can
2703 also be overridden.
2704
2705 Parameters
2706 ----------
2707 block : bool, optional
2708 Whether to block by calling ``start_main_loop``. The default,
2709 None, means to block if we are neither in IPython's ``%pylab`` mode
2710 nor in ``interactive`` mode.
2711 """
2712 managers = Gcf.get_all_fig_managers()
2713 if not managers:
2714 return
2715 for manager in managers:
2716 try:
2717 manager.show() # Emits a warning for non-interactive backend.
2718 except NonGuiException as exc:
2719 _api.warn_external(str(exc))
2720 if block is None:
2721 # Hack: Are we in IPython's %pylab mode? In pylab mode, IPython
2722 # (>= 0.10) tacks a _needmain attribute onto pyplot.show (always
2723 # set to False).
2724 pyplot_show = getattr(sys.modules.get("matplotlib.pyplot"), "show", None)
2725 ipython_pylab = hasattr(pyplot_show, "_needmain")
2726 block = not ipython_pylab and not is_interactive()
2727 if block:
2728 cls.start_main_loop()
2729
2730 def show(self):
2731 """
2732 For GUI backends, show the figure window and redraw.
2733 For non-GUI backends, raise an exception, unless running headless (i.e.
2734 on Linux with an unset DISPLAY); this exception is converted to a
2735 warning in `.Figure.show`.
2736 """
2737 # This should be overridden in GUI backends.
2738 if sys.platform == "linux" and not os.environ.get("DISPLAY"):
2739 # We cannot check _get_running_interactive_framework() ==
2740 # "headless" because that would also suppress the warning when
2741 # $DISPLAY exists but is invalid, which is more likely an error and
2742 # thus warrants a warning.
2743 return
2744 raise NonGuiException(
2745 f"{type(self.canvas).__name__} is non-interactive, and thus cannot be "
2746 f"shown")
2747
2748 def destroy(self):
2749 pass
2750
2751 def full_screen_toggle(self):
2752 pass
2753
2754 def resize(self, w, h):
2755 """For GUI backends, resize the window (in physical pixels)."""
2756
2757 def get_window_title(self):
2758 """
2759 Return the title text of the window containing the figure, or None
2760 if there is no window (e.g., a PS backend).
2761 """
2762 return 'image'
2763
2764 def set_window_title(self, title):
2765 """
2766 Set the title text of the window containing the figure.
2767
2768 This has no effect for non-GUI (e.g., PS) backends.
2769
2770 Examples
2771 --------
2772 >>> fig = plt.figure()
2773 >>> fig.canvas.manager.set_window_title('My figure')
2774 """
2775
2776
2777cursors = tools.cursors
2778
2779
2780class _Mode(str, Enum):
2781 NONE = ""
2782 PAN = "pan/zoom"
2783 ZOOM = "zoom rect"
2784
2785 def __str__(self):
2786 return self.value
2787
2788 @property
2789 def _navigate_mode(self):
2790 return self.name if self is not _Mode.NONE else None
2791
2792
2793class NavigationToolbar2:
2794 """
2795 Base class for the navigation cursor, version 2.
2796
2797 Backends must implement a canvas that handles connections for
2798 'button_press_event' and 'button_release_event'. See
2799 :meth:`FigureCanvasBase.mpl_connect` for more information.
2800
2801 They must also define
2802
2803 :meth:`save_figure`
2804 Save the current figure.
2805
2806 :meth:`draw_rubberband` (optional)
2807 Draw the zoom to rect "rubberband" rectangle.
2808
2809 :meth:`set_message` (optional)
2810 Display message.
2811
2812 :meth:`set_history_buttons` (optional)
2813 You can change the history back / forward buttons to indicate disabled / enabled
2814 state.
2815
2816 and override ``__init__`` to set up the toolbar -- without forgetting to
2817 call the base-class init. Typically, ``__init__`` needs to set up toolbar
2818 buttons connected to the `home`, `back`, `forward`, `pan`, `zoom`, and
2819 `save_figure` methods and using standard icons in the "images" subdirectory
2820 of the data path.
2821
2822 That's it, we'll do the rest!
2823 """
2824
2825 # list of toolitems to add to the toolbar, format is:
2826 # (
2827 # text, # the text of the button (often not visible to users)
2828 # tooltip_text, # the tooltip shown on hover (where possible)
2829 # image_file, # name of the image for the button (without the extension)
2830 # name_of_method, # name of the method in NavigationToolbar2 to call
2831 # )
2832 toolitems = (
2833 ('Home', 'Reset original view', 'home', 'home'),
2834 ('Back', 'Back to previous view', 'back', 'back'),
2835 ('Forward', 'Forward to next view', 'forward', 'forward'),
2836 (None, None, None, None),
2837 ('Pan',
2838 'Left button pans, Right button zooms\n'
2839 'x/y fixes axis, CTRL fixes aspect',
2840 'move', 'pan'),
2841 ('Zoom', 'Zoom to rectangle\nx/y fixes axis', 'zoom_to_rect', 'zoom'),
2842 ('Subplots', 'Configure subplots', 'subplots', 'configure_subplots'),
2843 (None, None, None, None),
2844 ('Save', 'Save the figure', 'filesave', 'save_figure'),
2845 )
2846
2847 def __init__(self, canvas):
2848 self.canvas = canvas
2849 canvas.toolbar = self
2850 self._nav_stack = cbook._Stack()
2851 # This cursor will be set after the initial draw.
2852 self._last_cursor = tools.Cursors.POINTER
2853
2854 self._id_press = self.canvas.mpl_connect(
2855 'button_press_event', self._zoom_pan_handler)
2856 self._id_release = self.canvas.mpl_connect(
2857 'button_release_event', self._zoom_pan_handler)
2858 self._id_drag = self.canvas.mpl_connect(
2859 'motion_notify_event', self.mouse_move)
2860 self._pan_info = None
2861 self._zoom_info = None
2862
2863 self.mode = _Mode.NONE # a mode string for the status bar
2864 self.set_history_buttons()
2865
2866 def set_message(self, s):
2867 """Display a message on toolbar or in status bar."""
2868
2869 def draw_rubberband(self, event, x0, y0, x1, y1):
2870 """
2871 Draw a rectangle rubberband to indicate zoom limits.
2872
2873 Note that it is not guaranteed that ``x0 <= x1`` and ``y0 <= y1``.
2874 """
2875
2876 def remove_rubberband(self):
2877 """Remove the rubberband."""
2878
2879 def home(self, *args):
2880 """
2881 Restore the original view.
2882
2883 For convenience of being directly connected as a GUI callback, which
2884 often get passed additional parameters, this method accepts arbitrary
2885 parameters, but does not use them.
2886 """
2887 self._nav_stack.home()
2888 self.set_history_buttons()
2889 self._update_view()
2890
2891 def back(self, *args):
2892 """
2893 Move back up the view lim stack.
2894
2895 For convenience of being directly connected as a GUI callback, which
2896 often get passed additional parameters, this method accepts arbitrary
2897 parameters, but does not use them.
2898 """
2899 self._nav_stack.back()
2900 self.set_history_buttons()
2901 self._update_view()
2902
2903 def forward(self, *args):
2904 """
2905 Move forward in the view lim stack.
2906
2907 For convenience of being directly connected as a GUI callback, which
2908 often get passed additional parameters, this method accepts arbitrary
2909 parameters, but does not use them.
2910 """
2911 self._nav_stack.forward()
2912 self.set_history_buttons()
2913 self._update_view()
2914
2915 def _update_cursor(self, event):
2916 """
2917 Update the cursor after a mouse move event or a tool (de)activation.
2918 """
2919 if self.mode and event.inaxes and event.inaxes.get_navigate():
2920 if (self.mode == _Mode.ZOOM
2921 and self._last_cursor != tools.Cursors.SELECT_REGION):
2922 self.canvas.set_cursor(tools.Cursors.SELECT_REGION)
2923 self._last_cursor = tools.Cursors.SELECT_REGION
2924 elif (self.mode == _Mode.PAN
2925 and self._last_cursor != tools.Cursors.MOVE):
2926 self.canvas.set_cursor(tools.Cursors.MOVE)
2927 self._last_cursor = tools.Cursors.MOVE
2928 elif self._last_cursor != tools.Cursors.POINTER:
2929 self.canvas.set_cursor(tools.Cursors.POINTER)
2930 self._last_cursor = tools.Cursors.POINTER
2931
2932 @contextmanager
2933 def _wait_cursor_for_draw_cm(self):
2934 """
2935 Set the cursor to a wait cursor when drawing the canvas.
2936
2937 In order to avoid constantly changing the cursor when the canvas
2938 changes frequently, do nothing if this context was triggered during the
2939 last second. (Optimally we'd prefer only setting the wait cursor if
2940 the *current* draw takes too long, but the current draw blocks the GUI
2941 thread).
2942 """
2943 self._draw_time, last_draw_time = (
2944 time.time(), getattr(self, "_draw_time", -np.inf))
2945 if self._draw_time - last_draw_time > 1:
2946 try:
2947 self.canvas.set_cursor(tools.Cursors.WAIT)
2948 yield
2949 finally:
2950 self.canvas.set_cursor(self._last_cursor)
2951 else:
2952 yield
2953
2954 @staticmethod
2955 def _mouse_event_to_message(event):
2956 if event.inaxes and event.inaxes.get_navigate():
2957 try:
2958 s = event.inaxes.format_coord(event.xdata, event.ydata)
2959 except (ValueError, OverflowError):
2960 pass
2961 else:
2962 s = s.rstrip()
2963 artists = [a for a in event.inaxes._mouseover_set
2964 if a.contains(event)[0] and a.get_visible()]
2965 if artists:
2966 a = cbook._topmost_artist(artists)
2967 if a is not event.inaxes.patch:
2968 data = a.get_cursor_data(event)
2969 if data is not None:
2970 data_str = a.format_cursor_data(data).rstrip()
2971 if data_str:
2972 s = s + '\n' + data_str
2973 return s
2974 return ""
2975
2976 def mouse_move(self, event):
2977 self._update_cursor(event)
2978 self.set_message(self._mouse_event_to_message(event))
2979
2980 def _zoom_pan_handler(self, event):
2981 if self.mode == _Mode.PAN:
2982 if event.name == "button_press_event":
2983 self.press_pan(event)
2984 elif event.name == "button_release_event":
2985 self.release_pan(event)
2986 if self.mode == _Mode.ZOOM:
2987 if event.name == "button_press_event":
2988 self.press_zoom(event)
2989 elif event.name == "button_release_event":
2990 self.release_zoom(event)
2991
2992 def _start_event_axes_interaction(self, event, *, method):
2993
2994 def _ax_filter(ax):
2995 return (ax.in_axes(event) and
2996 ax.get_navigate() and
2997 getattr(ax, f"can_{method}")()
2998 )
2999
3000 def _capture_events(ax):
3001 f = ax.get_forward_navigation_events()
3002 if f == "auto": # (capture = patch visibility)
3003 f = not ax.patch.get_visible()
3004 return not f
3005
3006 # get all relevant axes for the event
3007 axes = list(filter(_ax_filter, self.canvas.figure.get_axes()))
3008
3009 if len(axes) == 0:
3010 return []
3011
3012 if self._nav_stack() is None:
3013 self.push_current() # Set the home button to this view.
3014
3015 # group axes by zorder (reverse to trigger later axes first)
3016 grps = dict()
3017 for ax in reversed(axes):
3018 grps.setdefault(ax.get_zorder(), []).append(ax)
3019
3020 axes_to_trigger = []
3021 # go through zorders in reverse until we hit a capturing axes
3022 for zorder in sorted(grps, reverse=True):
3023 for ax in grps[zorder]:
3024 axes_to_trigger.append(ax)
3025 # NOTE: shared axes are automatically triggered, but twin-axes not!
3026 axes_to_trigger.extend(ax._twinned_axes.get_siblings(ax))
3027
3028 if _capture_events(ax):
3029 break # break if we hit a capturing axes
3030 else:
3031 # If the inner loop finished without an explicit break,
3032 # (e.g. no capturing axes was found) continue the
3033 # outer loop to the next zorder.
3034 continue
3035
3036 # If the inner loop was terminated with an explicit break,
3037 # terminate the outer loop as well.
3038 break
3039
3040 # avoid duplicated triggers (but keep order of list)
3041 axes_to_trigger = list(dict.fromkeys(axes_to_trigger))
3042
3043 return axes_to_trigger
3044
3045 def pan(self, *args):
3046 """
3047 Toggle the pan/zoom tool.
3048
3049 Pan with left button, zoom with right.
3050 """
3051 if not self.canvas.widgetlock.available(self):
3052 self.set_message("pan unavailable")
3053 return
3054 if self.mode == _Mode.PAN:
3055 self.mode = _Mode.NONE
3056 self.canvas.widgetlock.release(self)
3057 else:
3058 self.mode = _Mode.PAN
3059 self.canvas.widgetlock(self)
3060 for a in self.canvas.figure.get_axes():
3061 a.set_navigate_mode(self.mode._navigate_mode)
3062
3063 _PanInfo = namedtuple("_PanInfo", "button axes cid")
3064
3065 def press_pan(self, event):
3066 """Callback for mouse button press in pan/zoom mode."""
3067 if (event.button not in [MouseButton.LEFT, MouseButton.RIGHT]
3068 or event.x is None or event.y is None):
3069 return
3070
3071 axes = self._start_event_axes_interaction(event, method="pan")
3072 if not axes:
3073 return
3074
3075 # call "ax.start_pan(..)" on all relevant axes of an event
3076 for ax in axes:
3077 ax.start_pan(event.x, event.y, event.button)
3078
3079 self.canvas.mpl_disconnect(self._id_drag)
3080 id_drag = self.canvas.mpl_connect("motion_notify_event", self.drag_pan)
3081
3082 self._pan_info = self._PanInfo(
3083 button=event.button, axes=axes, cid=id_drag)
3084
3085 def drag_pan(self, event):
3086 """Callback for dragging in pan/zoom mode."""
3087 for ax in self._pan_info.axes:
3088 # Using the recorded button at the press is safer than the current
3089 # button, as multiple buttons can get pressed during motion.
3090 ax.drag_pan(self._pan_info.button, event.key, event.x, event.y)
3091 self.canvas.draw_idle()
3092
3093 def release_pan(self, event):
3094 """Callback for mouse button release in pan/zoom mode."""
3095 if self._pan_info is None:
3096 return
3097 self.canvas.mpl_disconnect(self._pan_info.cid)
3098 self._id_drag = self.canvas.mpl_connect(
3099 'motion_notify_event', self.mouse_move)
3100 for ax in self._pan_info.axes:
3101 ax.end_pan()
3102 self.canvas.draw_idle()
3103 self._pan_info = None
3104 self.push_current()
3105
3106 def zoom(self, *args):
3107 if not self.canvas.widgetlock.available(self):
3108 self.set_message("zoom unavailable")
3109 return
3110 """Toggle zoom to rect mode."""
3111 if self.mode == _Mode.ZOOM:
3112 self.mode = _Mode.NONE
3113 self.canvas.widgetlock.release(self)
3114 else:
3115 self.mode = _Mode.ZOOM
3116 self.canvas.widgetlock(self)
3117 for a in self.canvas.figure.get_axes():
3118 a.set_navigate_mode(self.mode._navigate_mode)
3119
3120 _ZoomInfo = namedtuple("_ZoomInfo", "direction start_xy axes cid cbar")
3121
3122 def press_zoom(self, event):
3123 """Callback for mouse button press in zoom to rect mode."""
3124 if (event.button not in [MouseButton.LEFT, MouseButton.RIGHT]
3125 or event.x is None or event.y is None):
3126 return
3127
3128 axes = self._start_event_axes_interaction(event, method="zoom")
3129 if not axes:
3130 return
3131
3132 id_zoom = self.canvas.mpl_connect(
3133 "motion_notify_event", self.drag_zoom)
3134
3135 # A colorbar is one-dimensional, so we extend the zoom rectangle out
3136 # to the edge of the Axes bbox in the other dimension. To do that we
3137 # store the orientation of the colorbar for later.
3138 parent_ax = axes[0]
3139 if hasattr(parent_ax, "_colorbar"):
3140 cbar = parent_ax._colorbar.orientation
3141 else:
3142 cbar = None
3143
3144 self._zoom_info = self._ZoomInfo(
3145 direction="in" if event.button == 1 else "out",
3146 start_xy=(event.x, event.y), axes=axes, cid=id_zoom, cbar=cbar)
3147
3148 def drag_zoom(self, event):
3149 """Callback for dragging in zoom mode."""
3150 start_xy = self._zoom_info.start_xy
3151 ax = self._zoom_info.axes[0]
3152 (x1, y1), (x2, y2) = np.clip(
3153 [start_xy, [event.x, event.y]], ax.bbox.min, ax.bbox.max)
3154 key = event.key
3155 # Force the key on colorbars to extend the short-axis bbox
3156 if self._zoom_info.cbar == "horizontal":
3157 key = "x"
3158 elif self._zoom_info.cbar == "vertical":
3159 key = "y"
3160 if key == "x":
3161 y1, y2 = ax.bbox.intervaly
3162 elif key == "y":
3163 x1, x2 = ax.bbox.intervalx
3164
3165 self.draw_rubberband(event, x1, y1, x2, y2)
3166
3167 def release_zoom(self, event):
3168 """Callback for mouse button release in zoom to rect mode."""
3169 if self._zoom_info is None:
3170 return
3171
3172 # We don't check the event button here, so that zooms can be cancelled
3173 # by (pressing and) releasing another mouse button.
3174 self.canvas.mpl_disconnect(self._zoom_info.cid)
3175 self.remove_rubberband()
3176
3177 start_x, start_y = self._zoom_info.start_xy
3178 key = event.key
3179 # Force the key on colorbars to ignore the zoom-cancel on the
3180 # short-axis side
3181 if self._zoom_info.cbar == "horizontal":
3182 key = "x"
3183 elif self._zoom_info.cbar == "vertical":
3184 key = "y"
3185 # Ignore single clicks: 5 pixels is a threshold that allows the user to
3186 # "cancel" a zoom action by zooming by less than 5 pixels.
3187 if ((abs(event.x - start_x) < 5 and key != "y") or
3188 (abs(event.y - start_y) < 5 and key != "x")):
3189 self.canvas.draw_idle()
3190 self._zoom_info = None
3191 return
3192
3193 for i, ax in enumerate(self._zoom_info.axes):
3194 # Detect whether this Axes is twinned with an earlier Axes in the
3195 # list of zoomed Axes, to avoid double zooming.
3196 twinx = any(ax.get_shared_x_axes().joined(ax, prev)
3197 for prev in self._zoom_info.axes[:i])
3198 twiny = any(ax.get_shared_y_axes().joined(ax, prev)
3199 for prev in self._zoom_info.axes[:i])
3200 ax._set_view_from_bbox(
3201 (start_x, start_y, event.x, event.y),
3202 self._zoom_info.direction, key, twinx, twiny)
3203
3204 self.canvas.draw_idle()
3205 self._zoom_info = None
3206 self.push_current()
3207
3208 def push_current(self):
3209 """Push the current view limits and position onto the stack."""
3210 self._nav_stack.push(
3211 WeakKeyDictionary(
3212 {ax: (ax._get_view(),
3213 # Store both the original and modified positions.
3214 (ax.get_position(True).frozen(),
3215 ax.get_position().frozen()))
3216 for ax in self.canvas.figure.axes}))
3217 self.set_history_buttons()
3218
3219 def _update_view(self):
3220 """
3221 Update the viewlim and position from the view and position stack for
3222 each Axes.
3223 """
3224 nav_info = self._nav_stack()
3225 if nav_info is None:
3226 return
3227 # Retrieve all items at once to avoid any risk of GC deleting an Axes
3228 # while in the middle of the loop below.
3229 items = list(nav_info.items())
3230 for ax, (view, (pos_orig, pos_active)) in items:
3231 ax._set_view(view)
3232 # Restore both the original and modified positions
3233 ax._set_position(pos_orig, 'original')
3234 ax._set_position(pos_active, 'active')
3235 self.canvas.draw_idle()
3236
3237 def configure_subplots(self, *args):
3238 if hasattr(self, "subplot_tool"):
3239 self.subplot_tool.figure.canvas.manager.show()
3240 return
3241 # This import needs to happen here due to circular imports.
3242 from matplotlib.figure import Figure
3243 with mpl.rc_context({"toolbar": "none"}): # No navbar for the toolfig.
3244 manager = type(self.canvas).new_manager(Figure(figsize=(6, 3)), -1)
3245 manager.set_window_title("Subplot configuration tool")
3246 tool_fig = manager.canvas.figure
3247 tool_fig.subplots_adjust(top=0.9)
3248 self.subplot_tool = widgets.SubplotTool(self.canvas.figure, tool_fig)
3249 cid = self.canvas.mpl_connect(
3250 "close_event", lambda e: manager.destroy())
3251
3252 def on_tool_fig_close(e):
3253 self.canvas.mpl_disconnect(cid)
3254 del self.subplot_tool
3255
3256 tool_fig.canvas.mpl_connect("close_event", on_tool_fig_close)
3257 manager.show()
3258 return self.subplot_tool
3259
3260 def save_figure(self, *args):
3261 """Save the current figure."""
3262 raise NotImplementedError
3263
3264 def update(self):
3265 """Reset the Axes stack."""
3266 self._nav_stack.clear()
3267 self.set_history_buttons()
3268
3269 def set_history_buttons(self):
3270 """Enable or disable the back/forward button."""
3271
3272
3273class ToolContainerBase:
3274 """
3275 Base class for all tool containers, e.g. toolbars.
3276
3277 Attributes
3278 ----------
3279 toolmanager : `.ToolManager`
3280 The tools with which this `ToolContainer` wants to communicate.
3281 """
3282
3283 _icon_extension = '.png'
3284 """
3285 Toolcontainer button icon image format extension
3286
3287 **String**: Image extension
3288 """
3289
3290 def __init__(self, toolmanager):
3291 self.toolmanager = toolmanager
3292 toolmanager.toolmanager_connect(
3293 'tool_message_event',
3294 lambda event: self.set_message(event.message))
3295 toolmanager.toolmanager_connect(
3296 'tool_removed_event',
3297 lambda event: self.remove_toolitem(event.tool.name))
3298
3299 def _tool_toggled_cbk(self, event):
3300 """
3301 Capture the 'tool_trigger_[name]'
3302
3303 This only gets used for toggled tools.
3304 """
3305 self.toggle_toolitem(event.tool.name, event.tool.toggled)
3306
3307 def add_tool(self, tool, group, position=-1):
3308 """
3309 Add a tool to this container.
3310
3311 Parameters
3312 ----------
3313 tool : tool_like
3314 The tool to add, see `.ToolManager.get_tool`.
3315 group : str
3316 The name of the group to add this tool to.
3317 position : int, default: -1
3318 The position within the group to place this tool.
3319 """
3320 tool = self.toolmanager.get_tool(tool)
3321 image = self._get_image_filename(tool)
3322 toggle = getattr(tool, 'toggled', None) is not None
3323 self.add_toolitem(tool.name, group, position,
3324 image, tool.description, toggle)
3325 if toggle:
3326 self.toolmanager.toolmanager_connect('tool_trigger_%s' % tool.name,
3327 self._tool_toggled_cbk)
3328 # If initially toggled
3329 if tool.toggled:
3330 self.toggle_toolitem(tool.name, True)
3331
3332 def _get_image_filename(self, tool):
3333 """Resolve a tool icon's filename."""
3334 if not tool.image:
3335 return None
3336 if os.path.isabs(tool.image):
3337 filename = tool.image
3338 else:
3339 if "image" in getattr(tool, "__dict__", {}):
3340 raise ValueError("If 'tool.image' is an instance variable, "
3341 "it must be an absolute path")
3342 for cls in type(tool).__mro__:
3343 if "image" in vars(cls):
3344 try:
3345 src = inspect.getfile(cls)
3346 break
3347 except (OSError, TypeError):
3348 raise ValueError("Failed to locate source file "
3349 "where 'tool.image' is defined") from None
3350 else:
3351 raise ValueError("Failed to find parent class defining 'tool.image'")
3352 filename = str(pathlib.Path(src).parent / tool.image)
3353 for filename in [filename, filename + self._icon_extension]:
3354 if os.path.isfile(filename):
3355 return os.path.abspath(filename)
3356 for fname in [ # Fallback; once deprecation elapses.
3357 tool.image,
3358 tool.image + self._icon_extension,
3359 cbook._get_data_path("images", tool.image),
3360 cbook._get_data_path("images", tool.image + self._icon_extension),
3361 ]:
3362 if os.path.isfile(fname):
3363 _api.warn_deprecated(
3364 "3.9", message=f"Loading icon {tool.image!r} from the current "
3365 "directory or from Matplotlib's image directory. This behavior "
3366 "is deprecated since %(since)s and will be removed %(removal)s; "
3367 "Tool.image should be set to a path relative to the Tool's source "
3368 "file, or to an absolute path.")
3369 return os.path.abspath(fname)
3370
3371 def trigger_tool(self, name):
3372 """
3373 Trigger the tool.
3374
3375 Parameters
3376 ----------
3377 name : str
3378 Name (id) of the tool triggered from within the container.
3379 """
3380 self.toolmanager.trigger_tool(name, sender=self)
3381
3382 def add_toolitem(self, name, group, position, image, description, toggle):
3383 """
3384 A hook to add a toolitem to the container.
3385
3386 This hook must be implemented in each backend and contains the
3387 backend-specific code to add an element to the toolbar.
3388
3389 .. warning::
3390 This is part of the backend implementation and should
3391 not be called by end-users. They should instead call
3392 `.ToolContainerBase.add_tool`.
3393
3394 The callback associated with the button click event
3395 must be *exactly* ``self.trigger_tool(name)``.
3396
3397 Parameters
3398 ----------
3399 name : str
3400 Name of the tool to add, this gets used as the tool's ID and as the
3401 default label of the buttons.
3402 group : str
3403 Name of the group that this tool belongs to.
3404 position : int
3405 Position of the tool within its group, if -1 it goes at the end.
3406 image : str
3407 Filename of the image for the button or `None`.
3408 description : str
3409 Description of the tool, used for the tooltips.
3410 toggle : bool
3411 * `True` : The button is a toggle (change the pressed/unpressed
3412 state between consecutive clicks).
3413 * `False` : The button is a normal button (returns to unpressed
3414 state after release).
3415 """
3416 raise NotImplementedError
3417
3418 def toggle_toolitem(self, name, toggled):
3419 """
3420 A hook to toggle a toolitem without firing an event.
3421
3422 This hook must be implemented in each backend and contains the
3423 backend-specific code to silently toggle a toolbar element.
3424
3425 .. warning::
3426 This is part of the backend implementation and should
3427 not be called by end-users. They should instead call
3428 `.ToolManager.trigger_tool` or `.ToolContainerBase.trigger_tool`
3429 (which are equivalent).
3430
3431 Parameters
3432 ----------
3433 name : str
3434 Id of the tool to toggle.
3435 toggled : bool
3436 Whether to set this tool as toggled or not.
3437 """
3438 raise NotImplementedError
3439
3440 def remove_toolitem(self, name):
3441 """
3442 A hook to remove a toolitem from the container.
3443
3444 This hook must be implemented in each backend and contains the
3445 backend-specific code to remove an element from the toolbar; it is
3446 called when `.ToolManager` emits a `tool_removed_event`.
3447
3448 Because some tools are present only on the `.ToolManager` but not on
3449 the `ToolContainer`, this method must be a no-op when called on a tool
3450 absent from the container.
3451
3452 .. warning::
3453 This is part of the backend implementation and should
3454 not be called by end-users. They should instead call
3455 `.ToolManager.remove_tool`.
3456
3457 Parameters
3458 ----------
3459 name : str
3460 Name of the tool to remove.
3461 """
3462 raise NotImplementedError
3463
3464 def set_message(self, s):
3465 """
3466 Display a message on the toolbar.
3467
3468 Parameters
3469 ----------
3470 s : str
3471 Message text.
3472 """
3473 raise NotImplementedError
3474
3475
3476class _Backend:
3477 # A backend can be defined by using the following pattern:
3478 #
3479 # @_Backend.export
3480 # class FooBackend(_Backend):
3481 # # override the attributes and methods documented below.
3482
3483 # `backend_version` may be overridden by the subclass.
3484 backend_version = "unknown"
3485
3486 # The `FigureCanvas` class must be defined.
3487 FigureCanvas = None
3488
3489 # For interactive backends, the `FigureManager` class must be overridden.
3490 FigureManager = FigureManagerBase
3491
3492 # For interactive backends, `mainloop` should be a function taking no
3493 # argument and starting the backend main loop. It should be left as None
3494 # for non-interactive backends.
3495 mainloop = None
3496
3497 # The following methods will be automatically defined and exported, but
3498 # can be overridden.
3499
3500 @classmethod
3501 def new_figure_manager(cls, num, *args, **kwargs):
3502 """Create a new figure manager instance."""
3503 # This import needs to happen here due to circular imports.
3504 from matplotlib.figure import Figure
3505 fig_cls = kwargs.pop('FigureClass', Figure)
3506 fig = fig_cls(*args, **kwargs)
3507 return cls.new_figure_manager_given_figure(num, fig)
3508
3509 @classmethod
3510 def new_figure_manager_given_figure(cls, num, figure):
3511 """Create a new figure manager instance for the given figure."""
3512 return cls.FigureCanvas.new_manager(figure, num)
3513
3514 @classmethod
3515 def draw_if_interactive(cls):
3516 manager_class = cls.FigureCanvas.manager_class
3517 # Interactive backends reimplement start_main_loop or pyplot_show.
3518 backend_is_interactive = (
3519 manager_class.start_main_loop != FigureManagerBase.start_main_loop
3520 or manager_class.pyplot_show != FigureManagerBase.pyplot_show)
3521 if backend_is_interactive and is_interactive():
3522 manager = Gcf.get_active()
3523 if manager:
3524 manager.canvas.draw_idle()
3525
3526 @classmethod
3527 def show(cls, *, block=None):
3528 """
3529 Show all figures.
3530
3531 `show` blocks by calling `mainloop` if *block* is ``True``, or if it is
3532 ``None`` and we are not in `interactive` mode and if IPython's
3533 ``%matplotlib`` integration has not been activated.
3534 """
3535 managers = Gcf.get_all_fig_managers()
3536 if not managers:
3537 return
3538 for manager in managers:
3539 try:
3540 manager.show() # Emits a warning for non-interactive backend.
3541 except NonGuiException as exc:
3542 _api.warn_external(str(exc))
3543 if cls.mainloop is None:
3544 return
3545 if block is None:
3546 # Hack: Is IPython's %matplotlib integration activated? If so,
3547 # IPython's activate_matplotlib (>= 0.10) tacks a _needmain
3548 # attribute onto pyplot.show (always set to False).
3549 pyplot_show = getattr(sys.modules.get("matplotlib.pyplot"), "show", None)
3550 ipython_pylab = hasattr(pyplot_show, "_needmain")
3551 block = not ipython_pylab and not is_interactive()
3552 if block:
3553 cls.mainloop()
3554
3555 # This method is the one actually exporting the required methods.
3556
3557 @staticmethod
3558 def export(cls):
3559 for name in [
3560 "backend_version",
3561 "FigureCanvas",
3562 "FigureManager",
3563 "new_figure_manager",
3564 "new_figure_manager_given_figure",
3565 "draw_if_interactive",
3566 "show",
3567 ]:
3568 setattr(sys.modules[cls.__module__], name, getattr(cls, name))
3569
3570 # For back-compatibility, generate a shim `Show` class.
3571
3572 class Show(ShowBase):
3573 def mainloop(self):
3574 return cls.mainloop()
3575
3576 setattr(sys.modules[cls.__module__], "Show", Show)
3577 return cls
3578
3579
3580class ShowBase(_Backend):
3581 """
3582 Simple base class to generate a ``show()`` function in backends.
3583
3584 Subclass must override ``mainloop()`` method.
3585 """
3586
3587 def __call__(self, block=None):
3588 return self.show(block=block)