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