diff --git "a/lib/python3.12/site-packages/matplotlib/backend_bases.py" "b/lib/python3.12/site-packages/matplotlib/backend_bases.py" new file mode 100644--- /dev/null +++ "b/lib/python3.12/site-packages/matplotlib/backend_bases.py" @@ -0,0 +1,3583 @@ +""" +Abstract base classes define the primitives that renderers and +graphics contexts must implement to serve as a Matplotlib backend. + +`RendererBase` + An abstract base class to handle drawing/rendering operations. + +`FigureCanvasBase` + The abstraction layer that separates the `.Figure` from the backend + specific details like a user interface drawing area. + +`GraphicsContextBase` + An abstract base class that provides color, line styles, etc. + +`Event` + The base class for all of the Matplotlib event handling. Derived classes + such as `KeyEvent` and `MouseEvent` store the meta data like keys and + buttons pressed, x and y locations in pixel and `~.axes.Axes` coordinates. + +`ShowBase` + The base class for the ``Show`` class of each interactive backend; the + 'show' callable is then set to ``Show.__call__``. + +`ToolContainerBase` + The base class for the Toolbar class of each interactive backend. +""" + +from collections import namedtuple +from contextlib import ExitStack, contextmanager, nullcontext +from enum import Enum, IntEnum +import functools +import importlib +import inspect +import io +import itertools +import logging +import os +import pathlib +import signal +import socket +import sys +import time +import weakref +from weakref import WeakKeyDictionary + +import numpy as np + +import matplotlib as mpl +from matplotlib import ( + _api, backend_tools as tools, cbook, colors, _docstring, text, + _tight_bbox, transforms, widgets, is_interactive, rcParams) +from matplotlib._pylab_helpers import Gcf +from matplotlib.backend_managers import ToolManager +from matplotlib.cbook import _setattr_cm +from matplotlib.layout_engine import ConstrainedLayoutEngine +from matplotlib.path import Path +from matplotlib.texmanager import TexManager +from matplotlib.transforms import Affine2D +from matplotlib._enums import JoinStyle, CapStyle + + +_log = logging.getLogger(__name__) +_default_filetypes = { + 'eps': 'Encapsulated Postscript', + 'jpg': 'Joint Photographic Experts Group', + 'jpeg': 'Joint Photographic Experts Group', + 'pdf': 'Portable Document Format', + 'pgf': 'PGF code for LaTeX', + 'png': 'Portable Network Graphics', + 'ps': 'Postscript', + 'raw': 'Raw RGBA bitmap', + 'rgba': 'Raw RGBA bitmap', + 'svg': 'Scalable Vector Graphics', + 'svgz': 'Scalable Vector Graphics', + 'tif': 'Tagged Image File Format', + 'tiff': 'Tagged Image File Format', + 'webp': 'WebP Image Format', +} +_default_backends = { + 'eps': 'matplotlib.backends.backend_ps', + 'jpg': 'matplotlib.backends.backend_agg', + 'jpeg': 'matplotlib.backends.backend_agg', + 'pdf': 'matplotlib.backends.backend_pdf', + 'pgf': 'matplotlib.backends.backend_pgf', + 'png': 'matplotlib.backends.backend_agg', + 'ps': 'matplotlib.backends.backend_ps', + 'raw': 'matplotlib.backends.backend_agg', + 'rgba': 'matplotlib.backends.backend_agg', + 'svg': 'matplotlib.backends.backend_svg', + 'svgz': 'matplotlib.backends.backend_svg', + 'tif': 'matplotlib.backends.backend_agg', + 'tiff': 'matplotlib.backends.backend_agg', + 'webp': 'matplotlib.backends.backend_agg', +} + + +def register_backend(format, backend, description=None): + """ + Register a backend for saving to a given file format. + + Parameters + ---------- + format : str + File extension + backend : module string or canvas class + Backend for handling file output + description : str, default: "" + Description of the file type. + """ + if description is None: + description = '' + _default_backends[format] = backend + _default_filetypes[format] = description + + +def get_registered_canvas_class(format): + """ + Return the registered default canvas for given file format. + Handles deferred import of required backend. + """ + if format not in _default_backends: + return None + backend_class = _default_backends[format] + if isinstance(backend_class, str): + backend_class = importlib.import_module(backend_class).FigureCanvas + _default_backends[format] = backend_class + return backend_class + + +class RendererBase: + """ + An abstract base class to handle drawing/rendering operations. + + The following methods must be implemented in the backend for full + functionality (though just implementing `draw_path` alone would give a + highly capable backend): + + * `draw_path` + * `draw_image` + * `draw_gouraud_triangles` + + The following methods *should* be implemented in the backend for + optimization reasons: + + * `draw_text` + * `draw_markers` + * `draw_path_collection` + * `draw_quad_mesh` + """ + def __init__(self): + super().__init__() + self._texmanager = None + self._text2path = text.TextToPath() + self._raster_depth = 0 + self._rasterizing = False + + def open_group(self, s, gid=None): + """ + Open a grouping element with label *s* and *gid* (if set) as id. + + Only used by the SVG renderer. + """ + + def close_group(self, s): + """ + Close a grouping element with label *s*. + + Only used by the SVG renderer. + """ + + def draw_path(self, gc, path, transform, rgbFace=None): + """Draw a `~.path.Path` instance using the given affine transform.""" + raise NotImplementedError + + def draw_markers(self, gc, marker_path, marker_trans, path, + trans, rgbFace=None): + """ + Draw a marker at each of *path*'s vertices (excluding control points). + + The base (fallback) implementation makes multiple calls to `draw_path`. + Backends may want to override this method in order to draw the marker + only once and reuse it multiple times. + + Parameters + ---------- + gc : `.GraphicsContextBase` + The graphics context. + marker_path : `~matplotlib.path.Path` + The path for the marker. + marker_trans : `~matplotlib.transforms.Transform` + An affine transform applied to the marker. + path : `~matplotlib.path.Path` + The locations to draw the markers. + trans : `~matplotlib.transforms.Transform` + An affine transform applied to the path. + rgbFace : :mpltype:`color`, optional + """ + for vertices, codes in path.iter_segments(trans, simplify=False): + if len(vertices): + x, y = vertices[-2:] + self.draw_path(gc, marker_path, + marker_trans + + transforms.Affine2D().translate(x, y), + rgbFace) + + def draw_path_collection(self, gc, master_transform, paths, all_transforms, + offsets, offset_trans, facecolors, edgecolors, + linewidths, linestyles, antialiaseds, urls, + offset_position): + """ + Draw a collection of *paths*. + + Each path is first transformed by the corresponding entry + in *all_transforms* (a list of (3, 3) matrices) and then by + *master_transform*. They are then translated by the corresponding + entry in *offsets*, which has been first transformed by *offset_trans*. + + *facecolors*, *edgecolors*, *linewidths*, *linestyles*, and + *antialiased* are lists that set the corresponding properties. + + *offset_position* is unused now, but the argument is kept for + backwards compatibility. + + The base (fallback) implementation makes multiple calls to `draw_path`. + Backends may want to override this in order to render each set of + path data only once, and then reference that path multiple times with + the different offsets, colors, styles etc. The generator methods + `_iter_collection_raw_paths` and `_iter_collection` are provided to + help with (and standardize) the implementation across backends. It + is highly recommended to use those generators, so that changes to the + behavior of `draw_path_collection` can be made globally. + """ + path_ids = self._iter_collection_raw_paths(master_transform, + paths, all_transforms) + + for xo, yo, path_id, gc0, rgbFace in self._iter_collection( + gc, list(path_ids), offsets, offset_trans, + facecolors, edgecolors, linewidths, linestyles, + antialiaseds, urls, offset_position): + path, transform = path_id + # Only apply another translation if we have an offset, else we + # reuse the initial transform. + if xo != 0 or yo != 0: + # The transformation can be used by multiple paths. Since + # translate is a inplace operation, we need to copy the + # transformation by .frozen() before applying the translation. + transform = transform.frozen() + transform.translate(xo, yo) + self.draw_path(gc0, path, transform, rgbFace) + + def draw_quad_mesh(self, gc, master_transform, meshWidth, meshHeight, + coordinates, offsets, offsetTrans, facecolors, + antialiased, edgecolors): + """ + Draw a quadmesh. + + The base (fallback) implementation converts the quadmesh to paths and + then calls `draw_path_collection`. + """ + + from matplotlib.collections import QuadMesh + paths = QuadMesh._convert_mesh_to_paths(coordinates) + + if edgecolors is None: + edgecolors = facecolors + linewidths = np.array([gc.get_linewidth()], float) + + return self.draw_path_collection( + gc, master_transform, paths, [], offsets, offsetTrans, facecolors, + edgecolors, linewidths, [], [antialiased], [None], 'screen') + + def draw_gouraud_triangles(self, gc, triangles_array, colors_array, + transform): + """ + Draw a series of Gouraud triangles. + + Parameters + ---------- + gc : `.GraphicsContextBase` + The graphics context. + triangles_array : (N, 3, 2) array-like + Array of *N* (x, y) points for the triangles. + colors_array : (N, 3, 4) array-like + Array of *N* RGBA colors for each point of the triangles. + transform : `~matplotlib.transforms.Transform` + An affine transform to apply to the points. + """ + raise NotImplementedError + + def _iter_collection_raw_paths(self, master_transform, paths, + all_transforms): + """ + Helper method (along with `_iter_collection`) to implement + `draw_path_collection` in a memory-efficient manner. + + This method yields all of the base path/transform combinations, given a + master transform, a list of paths and list of transforms. + + The arguments should be exactly what is passed in to + `draw_path_collection`. + + The backend should take each yielded path and transform and create an + object that can be referenced (reused) later. + """ + Npaths = len(paths) + Ntransforms = len(all_transforms) + N = max(Npaths, Ntransforms) + + if Npaths == 0: + return + + transform = transforms.IdentityTransform() + for i in range(N): + path = paths[i % Npaths] + if Ntransforms: + transform = Affine2D(all_transforms[i % Ntransforms]) + yield path, transform + master_transform + + def _iter_collection_uses_per_path(self, paths, all_transforms, + offsets, facecolors, edgecolors): + """ + Compute how many times each raw path object returned by + `_iter_collection_raw_paths` would be used when calling + `_iter_collection`. This is intended for the backend to decide + on the tradeoff between using the paths in-line and storing + them once and reusing. Rounds up in case the number of uses + is not the same for every path. + """ + Npaths = len(paths) + if Npaths == 0 or len(facecolors) == len(edgecolors) == 0: + return 0 + Npath_ids = max(Npaths, len(all_transforms)) + N = max(Npath_ids, len(offsets)) + return (N + Npath_ids - 1) // Npath_ids + + def _iter_collection(self, gc, path_ids, offsets, offset_trans, facecolors, + edgecolors, linewidths, linestyles, + antialiaseds, urls, offset_position): + """ + Helper method (along with `_iter_collection_raw_paths`) to implement + `draw_path_collection` in a memory-efficient manner. + + This method yields all of the path, offset and graphics context + combinations to draw the path collection. The caller should already + have looped over the results of `_iter_collection_raw_paths` to draw + this collection. + + The arguments should be the same as that passed into + `draw_path_collection`, with the exception of *path_ids*, which is a + list of arbitrary objects that the backend will use to reference one of + the paths created in the `_iter_collection_raw_paths` stage. + + Each yielded result is of the form:: + + xo, yo, path_id, gc, rgbFace + + where *xo*, *yo* is an offset; *path_id* is one of the elements of + *path_ids*; *gc* is a graphics context and *rgbFace* is a color to + use for filling the path. + """ + Npaths = len(path_ids) + Noffsets = len(offsets) + N = max(Npaths, Noffsets) + Nfacecolors = len(facecolors) + Nedgecolors = len(edgecolors) + Nlinewidths = len(linewidths) + Nlinestyles = len(linestyles) + Nurls = len(urls) + + if (Nfacecolors == 0 and Nedgecolors == 0) or Npaths == 0: + return + + gc0 = self.new_gc() + gc0.copy_properties(gc) + + def cycle_or_default(seq, default=None): + # Cycle over *seq* if it is not empty; else always yield *default*. + return (itertools.cycle(seq) if len(seq) + else itertools.repeat(default)) + + pathids = cycle_or_default(path_ids) + toffsets = cycle_or_default(offset_trans.transform(offsets), (0, 0)) + fcs = cycle_or_default(facecolors) + ecs = cycle_or_default(edgecolors) + lws = cycle_or_default(linewidths) + lss = cycle_or_default(linestyles) + aas = cycle_or_default(antialiaseds) + urls = cycle_or_default(urls) + + if Nedgecolors == 0: + gc0.set_linewidth(0.0) + + for pathid, (xo, yo), fc, ec, lw, ls, aa, url in itertools.islice( + zip(pathids, toffsets, fcs, ecs, lws, lss, aas, urls), N): + if not (np.isfinite(xo) and np.isfinite(yo)): + continue + if Nedgecolors: + if Nlinewidths: + gc0.set_linewidth(lw) + if Nlinestyles: + gc0.set_dashes(*ls) + if len(ec) == 4 and ec[3] == 0.0: + gc0.set_linewidth(0) + else: + gc0.set_foreground(ec) + if fc is not None and len(fc) == 4 and fc[3] == 0: + fc = None + gc0.set_antialiased(aa) + if Nurls: + gc0.set_url(url) + yield xo, yo, pathid, gc0, fc + gc0.restore() + + def get_image_magnification(self): + """ + Get the factor by which to magnify images passed to `draw_image`. + Allows a backend to have images at a different resolution to other + artists. + """ + return 1.0 + + def draw_image(self, gc, x, y, im, transform=None): + """ + Draw an RGBA image. + + Parameters + ---------- + gc : `.GraphicsContextBase` + A graphics context with clipping information. + + x : float + The distance in physical units (i.e., dots or pixels) from the left + hand side of the canvas. + + y : float + The distance in physical units (i.e., dots or pixels) from the + bottom side of the canvas. + + im : (N, M, 4) array of `numpy.uint8` + An array of RGBA pixels. + + transform : `~matplotlib.transforms.Affine2DBase` + If and only if the concrete backend is written such that + `option_scale_image` returns ``True``, an affine transformation + (i.e., an `.Affine2DBase`) *may* be passed to `draw_image`. The + translation vector of the transformation is given in physical units + (i.e., dots or pixels). Note that the transformation does not + override *x* and *y*, and has to be applied *before* translating + the result by *x* and *y* (this can be accomplished by adding *x* + and *y* to the translation vector defined by *transform*). + """ + raise NotImplementedError + + def option_image_nocomposite(self): + """ + Return whether image composition by Matplotlib should be skipped. + + Raster backends should usually return False (letting the C-level + rasterizer take care of image composition); vector backends should + usually return ``not rcParams["image.composite_image"]``. + """ + return False + + def option_scale_image(self): + """ + Return whether arbitrary affine transformations in `draw_image` are + supported (True for most vector backends). + """ + return False + + def draw_tex(self, gc, x, y, s, prop, angle, *, mtext=None): + """ + Draw a TeX instance. + + Parameters + ---------- + gc : `.GraphicsContextBase` + The graphics context. + x : float + The x location of the text in display coords. + y : float + The y location of the text baseline in display coords. + s : str + The TeX text string. + prop : `~matplotlib.font_manager.FontProperties` + The font properties. + angle : float + The rotation angle in degrees anti-clockwise. + mtext : `~matplotlib.text.Text` + The original text object to be rendered. + """ + self._draw_text_as_path(gc, x, y, s, prop, angle, ismath="TeX") + + def draw_text(self, gc, x, y, s, prop, angle, ismath=False, mtext=None): + """ + Draw a text instance. + + Parameters + ---------- + gc : `.GraphicsContextBase` + The graphics context. + x : float + The x location of the text in display coords. + y : float + The y location of the text baseline in display coords. + s : str + The text string. + prop : `~matplotlib.font_manager.FontProperties` + The font properties. + angle : float + The rotation angle in degrees anti-clockwise. + ismath : bool or "TeX" + If True, use mathtext parser. + mtext : `~matplotlib.text.Text` + The original text object to be rendered. + + Notes + ----- + **Notes for backend implementers:** + + `.RendererBase.draw_text` also supports passing "TeX" to the *ismath* + parameter to use TeX rendering, but this is not required for actual + rendering backends, and indeed many builtin backends do not support + this. Rather, TeX rendering is provided by `~.RendererBase.draw_tex`. + """ + self._draw_text_as_path(gc, x, y, s, prop, angle, ismath) + + def _draw_text_as_path(self, gc, x, y, s, prop, angle, ismath): + """ + Draw the text by converting them to paths using `.TextToPath`. + + This private helper supports the same parameters as + `~.RendererBase.draw_text`; setting *ismath* to "TeX" triggers TeX + rendering. + """ + text2path = self._text2path + fontsize = self.points_to_pixels(prop.get_size_in_points()) + verts, codes = text2path.get_text_path(prop, s, ismath=ismath) + path = Path(verts, codes) + if self.flipy(): + width, height = self.get_canvas_width_height() + transform = (Affine2D() + .scale(fontsize / text2path.FONT_SCALE) + .rotate_deg(angle) + .translate(x, height - y)) + else: + transform = (Affine2D() + .scale(fontsize / text2path.FONT_SCALE) + .rotate_deg(angle) + .translate(x, y)) + color = gc.get_rgb() + gc.set_linewidth(0.0) + self.draw_path(gc, path, transform, rgbFace=color) + + def get_text_width_height_descent(self, s, prop, ismath): + """ + Get the width, height, and descent (offset from the bottom to the baseline), in + display coords, of the string *s* with `.FontProperties` *prop*. + + Whitespace at the start and the end of *s* is included in the reported width. + """ + fontsize = prop.get_size_in_points() + + if ismath == 'TeX': + # todo: handle properties + return self.get_texmanager().get_text_width_height_descent( + s, fontsize, renderer=self) + + dpi = self.points_to_pixels(72) + if ismath: + dims = self._text2path.mathtext_parser.parse(s, dpi, prop) + return dims[0:3] # return width, height, descent + + flags = self._text2path._get_hinting_flag() + font = self._text2path._get_font(prop) + font.set_size(fontsize, dpi) + # the width and height of unrotated string + font.set_text(s, 0.0, flags=flags) + w, h = font.get_width_height() + d = font.get_descent() + w /= 64.0 # convert from subpixels + h /= 64.0 + d /= 64.0 + return w, h, d + + def flipy(self): + """ + Return whether y values increase from top to bottom. + + Note that this only affects drawing of texts. + """ + return True + + def get_canvas_width_height(self): + """Return the canvas width and height in display coords.""" + return 1, 1 + + def get_texmanager(self): + """Return the `.TexManager` instance.""" + if self._texmanager is None: + self._texmanager = TexManager() + return self._texmanager + + def new_gc(self): + """Return an instance of a `.GraphicsContextBase`.""" + return GraphicsContextBase() + + def points_to_pixels(self, points): + """ + Convert points to display units. + + You need to override this function (unless your backend + doesn't have a dpi, e.g., postscript or svg). Some imaging + systems assume some value for pixels per inch:: + + points to pixels = points * pixels_per_inch/72 * dpi/72 + + Parameters + ---------- + points : float or array-like + + Returns + ------- + Points converted to pixels + """ + return points + + def start_rasterizing(self): + """ + Switch to the raster renderer. + + Used by `.MixedModeRenderer`. + """ + + def stop_rasterizing(self): + """ + Switch back to the vector renderer and draw the contents of the raster + renderer as an image on the vector renderer. + + Used by `.MixedModeRenderer`. + """ + + def start_filter(self): + """ + Switch to a temporary renderer for image filtering effects. + + Currently only supported by the agg renderer. + """ + + def stop_filter(self, filter_func): + """ + Switch back to the original renderer. The contents of the temporary + renderer is processed with the *filter_func* and is drawn on the + original renderer as an image. + + Currently only supported by the agg renderer. + """ + + def _draw_disabled(self): + """ + Context manager to temporary disable drawing. + + This is used for getting the drawn size of Artists. This lets us + run the draw process to update any Python state but does not pay the + cost of the draw_XYZ calls on the canvas. + """ + no_ops = { + meth_name: lambda *args, **kwargs: None + for meth_name in dir(RendererBase) + if (meth_name.startswith("draw_") + or meth_name in ["open_group", "close_group"]) + } + + return _setattr_cm(self, **no_ops) + + +class GraphicsContextBase: + """An abstract base class that provides color, line styles, etc.""" + + def __init__(self): + self._alpha = 1.0 + self._forced_alpha = False # if True, _alpha overrides A from RGBA + self._antialiased = 1 # use 0, 1 not True, False for extension code + self._capstyle = CapStyle('butt') + self._cliprect = None + self._clippath = None + self._dashes = 0, None + self._joinstyle = JoinStyle('round') + self._linestyle = 'solid' + self._linewidth = 1 + self._rgb = (0.0, 0.0, 0.0, 1.0) + self._hatch = None + self._hatch_color = colors.to_rgba(rcParams['hatch.color']) + self._hatch_linewidth = rcParams['hatch.linewidth'] + self._url = None + self._gid = None + self._snap = None + self._sketch = None + + def copy_properties(self, gc): + """Copy properties from *gc* to self.""" + self._alpha = gc._alpha + self._forced_alpha = gc._forced_alpha + self._antialiased = gc._antialiased + self._capstyle = gc._capstyle + self._cliprect = gc._cliprect + self._clippath = gc._clippath + self._dashes = gc._dashes + self._joinstyle = gc._joinstyle + self._linestyle = gc._linestyle + self._linewidth = gc._linewidth + self._rgb = gc._rgb + self._hatch = gc._hatch + self._hatch_color = gc._hatch_color + self._hatch_linewidth = gc._hatch_linewidth + self._url = gc._url + self._gid = gc._gid + self._snap = gc._snap + self._sketch = gc._sketch + + def restore(self): + """ + Restore the graphics context from the stack - needed only + for backends that save graphics contexts on a stack. + """ + + def get_alpha(self): + """ + Return the alpha value used for blending - not supported on all + backends. + """ + return self._alpha + + def get_antialiased(self): + """Return whether the object should try to do antialiased rendering.""" + return self._antialiased + + def get_capstyle(self): + """Return the `.CapStyle`.""" + return self._capstyle.name + + def get_clip_rectangle(self): + """ + Return the clip rectangle as a `~matplotlib.transforms.Bbox` instance. + """ + return self._cliprect + + def get_clip_path(self): + """ + Return the clip path in the form (path, transform), where path + is a `~.path.Path` instance, and transform is + an affine transform to apply to the path before clipping. + """ + if self._clippath is not None: + tpath, tr = self._clippath.get_transformed_path_and_affine() + if np.all(np.isfinite(tpath.vertices)): + return tpath, tr + else: + _log.warning("Ill-defined clip_path detected. Returning None.") + return None, None + return None, None + + def get_dashes(self): + """ + Return the dash style as an (offset, dash-list) pair. + + See `.set_dashes` for details. + + Default value is (None, None). + """ + return self._dashes + + def get_forced_alpha(self): + """ + Return whether the value given by get_alpha() should be used to + override any other alpha-channel values. + """ + return self._forced_alpha + + def get_joinstyle(self): + """Return the `.JoinStyle`.""" + return self._joinstyle.name + + def get_linewidth(self): + """Return the line width in points.""" + return self._linewidth + + def get_rgb(self): + """Return a tuple of three or four floats from 0-1.""" + return self._rgb + + def get_url(self): + """Return a url if one is set, None otherwise.""" + return self._url + + def get_gid(self): + """Return the object identifier if one is set, None otherwise.""" + return self._gid + + def get_snap(self): + """ + Return the snap setting, which can be: + + * True: snap vertices to the nearest pixel center + * False: leave vertices as-is + * None: (auto) If the path contains only rectilinear line segments, + round to the nearest pixel center + """ + return self._snap + + def set_alpha(self, alpha): + """ + Set the alpha value used for blending - not supported on all backends. + + If ``alpha=None`` (the default), the alpha components of the + foreground and fill colors will be used to set their respective + transparencies (where applicable); otherwise, ``alpha`` will override + them. + """ + if alpha is not None: + self._alpha = alpha + self._forced_alpha = True + else: + self._alpha = 1.0 + self._forced_alpha = False + self.set_foreground(self._rgb, isRGBA=True) + + def set_antialiased(self, b): + """Set whether object should be drawn with antialiased rendering.""" + # Use ints to make life easier on extension code trying to read the gc. + self._antialiased = int(bool(b)) + + @_docstring.interpd + def set_capstyle(self, cs): + """ + Set how to draw endpoints of lines. + + Parameters + ---------- + cs : `.CapStyle` or %(CapStyle)s + """ + self._capstyle = CapStyle(cs) + + def set_clip_rectangle(self, rectangle): + """Set the clip rectangle to a `.Bbox` or None.""" + self._cliprect = rectangle + + def set_clip_path(self, path): + """Set the clip path to a `.TransformedPath` or None.""" + _api.check_isinstance((transforms.TransformedPath, None), path=path) + self._clippath = path + + def set_dashes(self, dash_offset, dash_list): + """ + Set the dash style for the gc. + + Parameters + ---------- + dash_offset : float + Distance, in points, into the dash pattern at which to + start the pattern. It is usually set to 0. + dash_list : array-like or None + The on-off sequence as points. None specifies a solid line. All + values must otherwise be non-negative (:math:`\\ge 0`). + + Notes + ----- + See p. 666 of the PostScript + `Language Reference + `_ + for more info. + """ + if dash_list is not None: + dl = np.asarray(dash_list) + if np.any(dl < 0.0): + raise ValueError( + "All values in the dash list must be non-negative") + if dl.size and not np.any(dl > 0.0): + raise ValueError( + 'At least one value in the dash list must be positive') + self._dashes = dash_offset, dash_list + + def set_foreground(self, fg, isRGBA=False): + """ + Set the foreground color. + + Parameters + ---------- + fg : :mpltype:`color` + isRGBA : bool + If *fg* is known to be an ``(r, g, b, a)`` tuple, *isRGBA* can be + set to True to improve performance. + """ + if self._forced_alpha and isRGBA: + self._rgb = fg[:3] + (self._alpha,) + elif self._forced_alpha: + self._rgb = colors.to_rgba(fg, self._alpha) + elif isRGBA: + self._rgb = fg + else: + self._rgb = colors.to_rgba(fg) + + @_docstring.interpd + def set_joinstyle(self, js): + """ + Set how to draw connections between line segments. + + Parameters + ---------- + js : `.JoinStyle` or %(JoinStyle)s + """ + self._joinstyle = JoinStyle(js) + + def set_linewidth(self, w): + """Set the linewidth in points.""" + self._linewidth = float(w) + + def set_url(self, url): + """Set the url for links in compatible backends.""" + self._url = url + + def set_gid(self, id): + """Set the id.""" + self._gid = id + + def set_snap(self, snap): + """ + Set the snap setting which may be: + + * True: snap vertices to the nearest pixel center + * False: leave vertices as-is + * None: (auto) If the path contains only rectilinear line segments, + round to the nearest pixel center + """ + self._snap = snap + + def set_hatch(self, hatch): + """Set the hatch style (for fills).""" + self._hatch = hatch + + def get_hatch(self): + """Get the current hatch style.""" + return self._hatch + + def get_hatch_path(self, density=6.0): + """Return a `.Path` for the current hatch.""" + hatch = self.get_hatch() + if hatch is None: + return None + return Path.hatch(hatch, density) + + def get_hatch_color(self): + """Get the hatch color.""" + return self._hatch_color + + def set_hatch_color(self, hatch_color): + """Set the hatch color.""" + self._hatch_color = hatch_color + + def get_hatch_linewidth(self): + """Get the hatch linewidth.""" + return self._hatch_linewidth + + def set_hatch_linewidth(self, hatch_linewidth): + """Set the hatch linewidth.""" + self._hatch_linewidth = hatch_linewidth + + def get_sketch_params(self): + """ + Return the sketch parameters for the artist. + + Returns + ------- + tuple or `None` + + A 3-tuple with the following elements: + + * ``scale``: The amplitude of the wiggle perpendicular to the + source line. + * ``length``: The length of the wiggle along the line. + * ``randomness``: The scale factor by which the length is + shrunken or expanded. + + May return `None` if no sketch parameters were set. + """ + return self._sketch + + def set_sketch_params(self, scale=None, length=None, randomness=None): + """ + Set the sketch parameters. + + Parameters + ---------- + scale : float, optional + The amplitude of the wiggle perpendicular to the source line, in + pixels. If scale is `None`, or not provided, no sketch filter will + be provided. + length : float, default: 128 + The length of the wiggle along the line, in pixels. + randomness : float, default: 16 + The scale factor by which the length is shrunken or expanded. + """ + self._sketch = ( + None if scale is None + else (scale, length or 128., randomness or 16.)) + + +class TimerBase: + """ + A base class for providing timer events, useful for things animations. + Backends need to implement a few specific methods in order to use their + own timing mechanisms so that the timer events are integrated into their + event loops. + + Subclasses must override the following methods: + + - ``_timer_start``: Backend-specific code for starting the timer. + - ``_timer_stop``: Backend-specific code for stopping the timer. + + Subclasses may additionally override the following methods: + + - ``_timer_set_single_shot``: Code for setting the timer to single shot + operating mode, if supported by the timer object. If not, the `Timer` + class itself will store the flag and the ``_on_timer`` method should be + overridden to support such behavior. + + - ``_timer_set_interval``: Code for setting the interval on the timer, if + there is a method for doing so on the timer object. + + - ``_on_timer``: The internal function that any timer object should call, + which will handle the task of running all callbacks that have been set. + """ + + def __init__(self, interval=None, callbacks=None): + """ + Parameters + ---------- + interval : int, default: 1000ms + The time between timer events in milliseconds. Will be stored as + ``timer.interval``. + callbacks : list[tuple[callable, tuple, dict]] + List of (func, args, kwargs) tuples that will be called upon timer + events. This list is accessible as ``timer.callbacks`` and can be + manipulated directly, or the functions `~.TimerBase.add_callback` + and `~.TimerBase.remove_callback` can be used. + """ + self.callbacks = [] if callbacks is None else callbacks.copy() + # Set .interval and not ._interval to go through the property setter. + self.interval = 1000 if interval is None else interval + self.single_shot = False + + def __del__(self): + """Need to stop timer and possibly disconnect timer.""" + self._timer_stop() + + @_api.delete_parameter("3.9", "interval", alternative="timer.interval") + def start(self, interval=None): + """ + Start the timer object. + + Parameters + ---------- + interval : int, optional + Timer interval in milliseconds; overrides a previously set interval + if provided. + """ + if interval is not None: + self.interval = interval + self._timer_start() + + def stop(self): + """Stop the timer.""" + self._timer_stop() + + def _timer_start(self): + pass + + def _timer_stop(self): + pass + + @property + def interval(self): + """The time between timer events, in milliseconds.""" + return self._interval + + @interval.setter + def interval(self, interval): + # Force to int since none of the backends actually support fractional + # milliseconds, and some error or give warnings. + # Some backends also fail when interval == 0, so ensure >= 1 msec + interval = max(int(interval), 1) + self._interval = interval + self._timer_set_interval() + + @property + def single_shot(self): + """Whether this timer should stop after a single run.""" + return self._single + + @single_shot.setter + def single_shot(self, ss): + self._single = ss + self._timer_set_single_shot() + + def add_callback(self, func, *args, **kwargs): + """ + Register *func* to be called by timer when the event fires. Any + additional arguments provided will be passed to *func*. + + This function returns *func*, which makes it possible to use it as a + decorator. + """ + self.callbacks.append((func, args, kwargs)) + return func + + def remove_callback(self, func, *args, **kwargs): + """ + Remove *func* from list of callbacks. + + *args* and *kwargs* are optional and used to distinguish between copies + of the same function registered to be called with different arguments. + This behavior is deprecated. In the future, ``*args, **kwargs`` won't + be considered anymore; to keep a specific callback removable by itself, + pass it to `add_callback` as a `functools.partial` object. + """ + if args or kwargs: + _api.warn_deprecated( + "3.1", message="In a future version, Timer.remove_callback " + "will not take *args, **kwargs anymore, but remove all " + "callbacks where the callable matches; to keep a specific " + "callback removable by itself, pass it to add_callback as a " + "functools.partial object.") + self.callbacks.remove((func, args, kwargs)) + else: + funcs = [c[0] for c in self.callbacks] + if func in funcs: + self.callbacks.pop(funcs.index(func)) + + def _timer_set_interval(self): + """Used to set interval on underlying timer object.""" + + def _timer_set_single_shot(self): + """Used to set single shot on underlying timer object.""" + + def _on_timer(self): + """ + Runs all function that have been registered as callbacks. Functions + can return False (or 0) if they should not be called any more. If there + are no callbacks, the timer is automatically stopped. + """ + for func, args, kwargs in self.callbacks: + ret = func(*args, **kwargs) + # docstring above explains why we use `if ret == 0` here, + # instead of `if not ret`. + # This will also catch `ret == False` as `False == 0` + # but does not annoy the linters + # https://docs.python.org/3/library/stdtypes.html#boolean-values + if ret == 0: + self.callbacks.remove((func, args, kwargs)) + + if len(self.callbacks) == 0: + self.stop() + + +class Event: + """ + A Matplotlib event. + + The following attributes are defined and shown with their default values. + Subclasses may define additional attributes. + + Attributes + ---------- + name : str + The event name. + canvas : `FigureCanvasBase` + The backend-specific canvas instance generating the event. + guiEvent + The GUI event that triggered the Matplotlib event. + """ + + def __init__(self, name, canvas, guiEvent=None): + self.name = name + self.canvas = canvas + self.guiEvent = guiEvent + + def _process(self): + """Process this event on ``self.canvas``, then unset ``guiEvent``.""" + self.canvas.callbacks.process(self.name, self) + self.guiEvent = None + + +class DrawEvent(Event): + """ + An event triggered by a draw operation on the canvas. + + In most backends, callbacks subscribed to this event will be fired after + the rendering is complete but before the screen is updated. Any extra + artists drawn to the canvas's renderer will be reflected without an + explicit call to ``blit``. + + .. warning:: + + Calling ``canvas.draw`` and ``canvas.blit`` in these callbacks may + not be safe with all backends and may cause infinite recursion. + + A DrawEvent has a number of special attributes in addition to those defined + by the parent `Event` class. + + Attributes + ---------- + renderer : `RendererBase` + The renderer for the draw event. + """ + def __init__(self, name, canvas, renderer): + super().__init__(name, canvas) + self.renderer = renderer + + +class ResizeEvent(Event): + """ + An event triggered by a canvas resize. + + A ResizeEvent has a number of special attributes in addition to those + defined by the parent `Event` class. + + Attributes + ---------- + width : int + Width of the canvas in pixels. + height : int + Height of the canvas in pixels. + """ + + def __init__(self, name, canvas): + super().__init__(name, canvas) + self.width, self.height = canvas.get_width_height() + + +class CloseEvent(Event): + """An event triggered by a figure being closed.""" + + +class LocationEvent(Event): + """ + An event that has a screen location. + + A LocationEvent has a number of special attributes in addition to those + defined by the parent `Event` class. + + Attributes + ---------- + x, y : int or None + Event location in pixels from bottom left of canvas. + inaxes : `~matplotlib.axes.Axes` or None + The `~.axes.Axes` instance over which the mouse is, if any. + xdata, ydata : float or None + Data coordinates of the mouse within *inaxes*, or *None* if the mouse + is not over an Axes. + modifiers : frozenset + The keyboard modifiers currently being pressed (except for KeyEvent). + """ + + _last_axes_ref = None + + def __init__(self, name, canvas, x, y, guiEvent=None, *, modifiers=None): + super().__init__(name, canvas, guiEvent=guiEvent) + # x position - pixels from left of canvas + self.x = int(x) if x is not None else x + # y position - pixels from right of canvas + self.y = int(y) if y is not None else y + self.inaxes = None # the Axes instance the mouse is over + self.xdata = None # x coord of mouse in data coords + self.ydata = None # y coord of mouse in data coords + self.modifiers = frozenset(modifiers if modifiers is not None else []) + + if x is None or y is None: + # cannot check if event was in Axes if no (x, y) info + return + + self._set_inaxes(self.canvas.inaxes((x, y)) + if self.canvas.mouse_grabber is None else + self.canvas.mouse_grabber, + (x, y)) + + # Splitting _set_inaxes out is useful for the axes_leave_event handler: it + # needs to generate synthetic LocationEvents with manually-set inaxes. In + # that latter case, xy has already been cast to int so it can directly be + # read from self.x, self.y; in the normal case, however, it is more + # accurate to pass the untruncated float x, y values passed to the ctor. + + def _set_inaxes(self, inaxes, xy=None): + self.inaxes = inaxes + if inaxes is not None: + try: + self.xdata, self.ydata = inaxes.transData.inverted().transform( + xy if xy is not None else (self.x, self.y)) + except ValueError: + pass + + +class MouseButton(IntEnum): + LEFT = 1 + MIDDLE = 2 + RIGHT = 3 + BACK = 8 + FORWARD = 9 + + +class MouseEvent(LocationEvent): + """ + A mouse event ('button_press_event', 'button_release_event', \ +'scroll_event', 'motion_notify_event'). + + A MouseEvent has a number of special attributes in addition to those + defined by the parent `Event` and `LocationEvent` classes. + + Attributes + ---------- + button : None or `MouseButton` or {'up', 'down'} + The button pressed. 'up' and 'down' are used for scroll events. + + Note that LEFT and RIGHT actually refer to the "primary" and + "secondary" buttons, i.e. if the user inverts their left and right + buttons ("left-handed setting") then the LEFT button will be the one + physically on the right. + + If this is unset, *name* is "scroll_event", and *step* is nonzero, then + this will be set to "up" or "down" depending on the sign of *step*. + + buttons : None or frozenset + For 'motion_notify_event', the mouse buttons currently being pressed + (a set of zero or more MouseButtons); + for other events, None. + + .. note:: + For 'motion_notify_event', this attribute is more accurate than + the ``button`` (singular) attribute, which is obtained from the last + 'button_press_event' or 'button_release_event' that occurred within + the canvas (and thus 1. be wrong if the last change in mouse state + occurred when the canvas did not have focus, and 2. cannot report + when multiple buttons are pressed). + + This attribute is not set for 'button_press_event' and + 'button_release_event' because GUI toolkits are inconsistent as to + whether they report the button state *before* or *after* the + press/release occurred. + + .. warning:: + On macOS, the Tk backends only report a single button even if + multiple buttons are pressed. + + key : None or str + The key pressed when the mouse event triggered, e.g. 'shift'. + See `KeyEvent`. + + .. warning:: + This key is currently obtained from the last 'key_press_event' or + 'key_release_event' that occurred within the canvas. Thus, if the + last change of keyboard state occurred while the canvas did not have + focus, this attribute will be wrong. On the other hand, the + ``modifiers`` attribute should always be correct, but it can only + report on modifier keys. + + step : float + The number of scroll steps (positive for 'up', negative for 'down'). + This applies only to 'scroll_event' and defaults to 0 otherwise. + + dblclick : bool + Whether the event is a double-click. This applies only to + 'button_press_event' and is False otherwise. In particular, it's + not used in 'button_release_event'. + + Examples + -------- + :: + + def on_press(event): + print('you pressed', event.button, event.xdata, event.ydata) + + cid = fig.canvas.mpl_connect('button_press_event', on_press) + """ + + def __init__(self, name, canvas, x, y, button=None, key=None, + step=0, dblclick=False, guiEvent=None, *, + buttons=None, modifiers=None): + super().__init__( + name, canvas, x, y, guiEvent=guiEvent, modifiers=modifiers) + if button in MouseButton.__members__.values(): + button = MouseButton(button) + if name == "scroll_event" and button is None: + if step > 0: + button = "up" + elif step < 0: + button = "down" + self.button = button + if name == "motion_notify_event": + self.buttons = frozenset(buttons if buttons is not None else []) + else: + # We don't support 'buttons' for button_press/release_event because + # toolkits are inconsistent as to whether they report the state + # before or after the event. + if buttons: + raise ValueError( + "'buttons' is only supported for 'motion_notify_event'") + self.buttons = None + self.key = key + self.step = step + self.dblclick = dblclick + + def __str__(self): + return (f"{self.name}: " + f"xy=({self.x}, {self.y}) xydata=({self.xdata}, {self.ydata}) " + f"button={self.button} dblclick={self.dblclick} " + f"inaxes={self.inaxes}") + + +class PickEvent(Event): + """ + A pick event. + + This event is fired when the user picks a location on the canvas + sufficiently close to an artist that has been made pickable with + `.Artist.set_picker`. + + A PickEvent has a number of special attributes in addition to those defined + by the parent `Event` class. + + Attributes + ---------- + mouseevent : `MouseEvent` + The mouse event that generated the pick. + artist : `~matplotlib.artist.Artist` + The picked artist. Note that artists are not pickable by default + (see `.Artist.set_picker`). + other + Additional attributes may be present depending on the type of the + picked object; e.g., a `.Line2D` pick may define different extra + attributes than a `.PatchCollection` pick. + + Examples + -------- + Bind a function ``on_pick()`` to pick events, that prints the coordinates + of the picked data point:: + + ax.plot(np.rand(100), 'o', picker=5) # 5 points tolerance + + def on_pick(event): + line = event.artist + xdata, ydata = line.get_data() + ind = event.ind + print(f'on pick line: {xdata[ind]:.3f}, {ydata[ind]:.3f}') + + cid = fig.canvas.mpl_connect('pick_event', on_pick) + """ + + def __init__(self, name, canvas, mouseevent, artist, + guiEvent=None, **kwargs): + if guiEvent is None: + guiEvent = mouseevent.guiEvent + super().__init__(name, canvas, guiEvent) + self.mouseevent = mouseevent + self.artist = artist + self.__dict__.update(kwargs) + + +class KeyEvent(LocationEvent): + """ + A key event (key press, key release). + + A KeyEvent has a number of special attributes in addition to those defined + by the parent `Event` and `LocationEvent` classes. + + Attributes + ---------- + key : None or str + The key(s) pressed. Could be *None*, a single case sensitive Unicode + character ("g", "G", "#", etc.), a special key ("control", "shift", + "f1", "up", etc.) or a combination of the above (e.g., "ctrl+alt+g", + "ctrl+alt+G"). + + Notes + ----- + Modifier keys will be prefixed to the pressed key and will be in the order + "ctrl", "alt", "super". The exception to this rule is when the pressed key + is itself a modifier key, therefore "ctrl+alt" and "alt+control" can both + be valid key values. + + Examples + -------- + :: + + def on_key(event): + print('you pressed', event.key, event.xdata, event.ydata) + + cid = fig.canvas.mpl_connect('key_press_event', on_key) + """ + + def __init__(self, name, canvas, key, x=0, y=0, guiEvent=None): + super().__init__(name, canvas, x, y, guiEvent=guiEvent) + self.key = key + + +# Default callback for key events. +def _key_handler(event): + # Dead reckoning of key. + if event.name == "key_press_event": + event.canvas._key = event.key + elif event.name == "key_release_event": + event.canvas._key = None + + +# Default callback for mouse events. +def _mouse_handler(event): + # Dead-reckoning of button and key. + if event.name == "button_press_event": + event.canvas._button = event.button + elif event.name == "button_release_event": + event.canvas._button = None + elif event.name == "motion_notify_event" and event.button is None: + event.button = event.canvas._button + if event.key is None: + event.key = event.canvas._key + # Emit axes_enter/axes_leave. + if event.name == "motion_notify_event": + last_ref = LocationEvent._last_axes_ref + last_axes = last_ref() if last_ref else None + if last_axes != event.inaxes: + if last_axes is not None: + # Create a synthetic LocationEvent for the axes_leave_event. + # Its inaxes attribute needs to be manually set (because the + # cursor is actually *out* of that Axes at that point); this is + # done with the internal _set_inaxes method which ensures that + # the xdata and ydata attributes are also correct. + try: + canvas = last_axes.get_figure(root=True).canvas + leave_event = LocationEvent( + "axes_leave_event", canvas, + event.x, event.y, event.guiEvent, + modifiers=event.modifiers) + leave_event._set_inaxes(last_axes) + canvas.callbacks.process("axes_leave_event", leave_event) + except Exception: + pass # The last canvas may already have been torn down. + if event.inaxes is not None: + event.canvas.callbacks.process("axes_enter_event", event) + LocationEvent._last_axes_ref = ( + weakref.ref(event.inaxes) if event.inaxes else None) + + +def _get_renderer(figure, print_method=None): + """ + Get the renderer that would be used to save a `.Figure`. + + If you need a renderer without any active draw methods use + renderer._draw_disabled to temporary patch them out at your call site. + """ + # This is implemented by triggering a draw, then immediately jumping out of + # Figure.draw() by raising an exception. + + class Done(Exception): + pass + + def _draw(renderer): raise Done(renderer) + + with cbook._setattr_cm(figure, draw=_draw), ExitStack() as stack: + if print_method is None: + fmt = figure.canvas.get_default_filetype() + # Even for a canvas' default output type, a canvas switch may be + # needed, e.g. for FigureCanvasBase. + print_method = stack.enter_context( + figure.canvas._switch_canvas_and_return_print_method(fmt)) + try: + print_method(io.BytesIO()) + except Done as exc: + renderer, = exc.args + return renderer + else: + raise RuntimeError(f"{print_method} did not call Figure.draw, so " + f"no renderer is available") + + +def _no_output_draw(figure): + # _no_output_draw was promoted to the figure level, but + # keep this here in case someone was calling it... + figure.draw_without_rendering() + + +def _is_non_interactive_terminal_ipython(ip): + """ + Return whether we are in a terminal IPython, but non interactive. + + When in _terminal_ IPython, ip.parent will have and `interact` attribute, + if this attribute is False we do not setup eventloop integration as the + user will _not_ interact with IPython. In all other case (ZMQKernel, or is + interactive), we do. + """ + return (hasattr(ip, 'parent') + and (ip.parent is not None) + and getattr(ip.parent, 'interact', None) is False) + + +@contextmanager +def _allow_interrupt(prepare_notifier, handle_sigint): + """ + A context manager that allows terminating a plot by sending a SIGINT. It + is necessary because the running backend prevents the Python interpreter + from running and processing signals (i.e., to raise a KeyboardInterrupt). + To solve this, one needs to somehow wake up the interpreter and make it + close the plot window. We do this by using the signal.set_wakeup_fd() + function which organizes a write of the signal number into a socketpair. + A backend-specific function, *prepare_notifier*, arranges to listen to + the pair's read socket while the event loop is running. (If it returns a + notifier object, that object is kept alive while the context manager runs.) + + If SIGINT was indeed caught, after exiting the on_signal() function the + interpreter reacts to the signal according to the handler function which + had been set up by a signal.signal() call; here, we arrange to call the + backend-specific *handle_sigint* function. Finally, we call the old SIGINT + handler with the same arguments that were given to our custom handler. + + We do this only if the old handler for SIGINT was not None, which means + that a non-python handler was installed, i.e. in Julia, and not SIG_IGN + which means we should ignore the interrupts. + + Parameters + ---------- + prepare_notifier : Callable[[socket.socket], object] + handle_sigint : Callable[[], object] + """ + + old_sigint_handler = signal.getsignal(signal.SIGINT) + if old_sigint_handler in (None, signal.SIG_IGN, signal.SIG_DFL): + yield + return + + handler_args = None + wsock, rsock = socket.socketpair() + wsock.setblocking(False) + rsock.setblocking(False) + old_wakeup_fd = signal.set_wakeup_fd(wsock.fileno()) + notifier = prepare_notifier(rsock) + + def save_args_and_handle_sigint(*args): + nonlocal handler_args + handler_args = args + handle_sigint() + + signal.signal(signal.SIGINT, save_args_and_handle_sigint) + try: + yield + finally: + wsock.close() + rsock.close() + signal.set_wakeup_fd(old_wakeup_fd) + signal.signal(signal.SIGINT, old_sigint_handler) + if handler_args is not None: + old_sigint_handler(*handler_args) + + +class FigureCanvasBase: + """ + The canvas the figure renders into. + + Attributes + ---------- + figure : `~matplotlib.figure.Figure` + A high-level figure instance. + """ + + # Set to one of {"qt", "gtk3", "gtk4", "wx", "tk", "macosx"} if an + # interactive framework is required, or None otherwise. + required_interactive_framework = None + + # The manager class instantiated by new_manager. + # (This is defined as a classproperty because the manager class is + # currently defined *after* the canvas class, but one could also assign + # ``FigureCanvasBase.manager_class = FigureManagerBase`` + # after defining both classes.) + manager_class = _api.classproperty(lambda cls: FigureManagerBase) + + events = [ + 'resize_event', + 'draw_event', + 'key_press_event', + 'key_release_event', + 'button_press_event', + 'button_release_event', + 'scroll_event', + 'motion_notify_event', + 'pick_event', + 'figure_enter_event', + 'figure_leave_event', + 'axes_enter_event', + 'axes_leave_event', + 'close_event' + ] + + fixed_dpi = None + + filetypes = _default_filetypes + + @_api.classproperty + def supports_blit(cls): + """If this Canvas sub-class supports blitting.""" + return (hasattr(cls, "copy_from_bbox") + and hasattr(cls, "restore_region")) + + def __init__(self, figure=None): + from matplotlib.figure import Figure + self._fix_ipython_backend2gui() + self._is_idle_drawing = True + self._is_saving = False + if figure is None: + figure = Figure() + figure.set_canvas(self) + self.figure = figure + self.manager = None + self.widgetlock = widgets.LockDraw() + self._button = None # the button pressed + self._key = None # the key pressed + self.mouse_grabber = None # the Axes currently grabbing mouse + self.toolbar = None # NavigationToolbar2 will set me + self._is_idle_drawing = False + # We don't want to scale up the figure DPI more than once. + figure._original_dpi = figure.dpi + self._device_pixel_ratio = 1 + super().__init__() # Typically the GUI widget init (if any). + + callbacks = property(lambda self: self.figure._canvas_callbacks) + button_pick_id = property(lambda self: self.figure._button_pick_id) + scroll_pick_id = property(lambda self: self.figure._scroll_pick_id) + + @classmethod + @functools.cache + def _fix_ipython_backend2gui(cls): + # Fix hard-coded module -> toolkit mapping in IPython (used for + # `ipython --auto`). This cannot be done at import time due to + # ordering issues, so we do it when creating a canvas, and should only + # be done once per class (hence the `cache`). + + # This function will not be needed when Python 3.12, the latest version + # supported by IPython < 8.24, reaches end-of-life in late 2028. + # At that time this function can be made a no-op and deprecated. + mod_ipython = sys.modules.get("IPython") + if mod_ipython is None or mod_ipython.version_info[:2] >= (8, 24): + # Use of backend2gui is not needed for IPython >= 8.24 as the + # functionality has been moved to Matplotlib. + return + + import IPython + ip = IPython.get_ipython() + if not ip: + return + from IPython.core import pylabtools as pt + if (not hasattr(pt, "backend2gui") + or not hasattr(ip, "enable_matplotlib")): + # In case we ever move the patch to IPython and remove these APIs, + # don't break on our side. + return + backend2gui_rif = { + "qt": "qt", + "gtk3": "gtk3", + "gtk4": "gtk4", + "wx": "wx", + "macosx": "osx", + }.get(cls.required_interactive_framework) + if backend2gui_rif: + if _is_non_interactive_terminal_ipython(ip): + ip.enable_gui(backend2gui_rif) + + @classmethod + def new_manager(cls, figure, num): + """ + Create a new figure manager for *figure*, using this canvas class. + + Notes + ----- + This method should not be reimplemented in subclasses. If + custom manager creation logic is needed, please reimplement + ``FigureManager.create_with_canvas``. + """ + return cls.manager_class.create_with_canvas(cls, figure, num) + + @contextmanager + def _idle_draw_cntx(self): + self._is_idle_drawing = True + try: + yield + finally: + self._is_idle_drawing = False + + def is_saving(self): + """ + Return whether the renderer is in the process of saving + to a file, rather than rendering for an on-screen buffer. + """ + return self._is_saving + + def blit(self, bbox=None): + """Blit the canvas in bbox (default entire canvas).""" + + def inaxes(self, xy): + """ + Return the topmost visible `~.axes.Axes` containing the point *xy*. + + Parameters + ---------- + xy : (float, float) + (x, y) pixel positions from left/bottom of the canvas. + + Returns + ------- + `~matplotlib.axes.Axes` or None + The topmost visible Axes containing the point, or None if there + is no Axes at the point. + """ + axes_list = [a for a in self.figure.get_axes() + if a.patch.contains_point(xy) and a.get_visible()] + if axes_list: + axes = cbook._topmost_artist(axes_list) + else: + axes = None + + return axes + + def grab_mouse(self, ax): + """ + Set the child `~.axes.Axes` which is grabbing the mouse events. + + Usually called by the widgets themselves. It is an error to call this + if the mouse is already grabbed by another Axes. + """ + if self.mouse_grabber not in (None, ax): + raise RuntimeError("Another Axes already grabs mouse input") + self.mouse_grabber = ax + + def release_mouse(self, ax): + """ + Release the mouse grab held by the `~.axes.Axes` *ax*. + + Usually called by the widgets. It is ok to call this even if *ax* + doesn't have the mouse grab currently. + """ + if self.mouse_grabber is ax: + self.mouse_grabber = None + + def set_cursor(self, cursor): + """ + Set the current cursor. + + This may have no effect if the backend does not display anything. + + If required by the backend, this method should trigger an update in + the backend event loop after the cursor is set, as this method may be + called e.g. before a long-running task during which the GUI is not + updated. + + Parameters + ---------- + cursor : `.Cursors` + The cursor to display over the canvas. Note: some backends may + change the cursor for the entire window. + """ + + def draw(self, *args, **kwargs): + """ + Render the `.Figure`. + + This method must walk the artist tree, even if no output is produced, + because it triggers deferred work that users may want to access + before saving output to disk. For example computing limits, + auto-limits, and tick values. + """ + + def draw_idle(self, *args, **kwargs): + """ + Request a widget redraw once control returns to the GUI event loop. + + Even if multiple calls to `draw_idle` occur before control returns + to the GUI event loop, the figure will only be rendered once. + + Notes + ----- + Backends may choose to override the method and implement their own + strategy to prevent multiple renderings. + + """ + if not self._is_idle_drawing: + with self._idle_draw_cntx(): + self.draw(*args, **kwargs) + + @property + def device_pixel_ratio(self): + """ + The ratio of physical to logical pixels used for the canvas on screen. + + By default, this is 1, meaning physical and logical pixels are the same + size. Subclasses that support High DPI screens may set this property to + indicate that said ratio is different. All Matplotlib interaction, + unless working directly with the canvas, remains in logical pixels. + + """ + return self._device_pixel_ratio + + def _set_device_pixel_ratio(self, ratio): + """ + Set the ratio of physical to logical pixels used for the canvas. + + Subclasses that support High DPI screens can set this property to + indicate that said ratio is different. The canvas itself will be + created at the physical size, while the client side will use the + logical size. Thus the DPI of the Figure will change to be scaled by + this ratio. Implementations that support High DPI screens should use + physical pixels for events so that transforms back to Axes space are + correct. + + By default, this is 1, meaning physical and logical pixels are the same + size. + + Parameters + ---------- + ratio : float + The ratio of logical to physical pixels used for the canvas. + + Returns + ------- + bool + Whether the ratio has changed. Backends may interpret this as a + signal to resize the window, repaint the canvas, or change any + other relevant properties. + """ + if self._device_pixel_ratio == ratio: + return False + # In cases with mixed resolution displays, we need to be careful if the + # device pixel ratio changes - in this case we need to resize the + # canvas accordingly. Some backends provide events that indicate a + # change in DPI, but those that don't will update this before drawing. + dpi = ratio * self.figure._original_dpi + self.figure._set_dpi(dpi, forward=False) + self._device_pixel_ratio = ratio + return True + + def get_width_height(self, *, physical=False): + """ + Return the figure width and height in integral points or pixels. + + When the figure is used on High DPI screens (and the backend supports + it), the truncation to integers occurs after scaling by the device + pixel ratio. + + Parameters + ---------- + physical : bool, default: False + Whether to return true physical pixels or logical pixels. Physical + pixels may be used by backends that support HiDPI, but still + configure the canvas using its actual size. + + Returns + ------- + width, height : int + The size of the figure, in points or pixels, depending on the + backend. + """ + return tuple(int(size / (1 if physical else self.device_pixel_ratio)) + for size in self.figure.bbox.max) + + @classmethod + def get_supported_filetypes(cls): + """Return dict of savefig file formats supported by this backend.""" + return cls.filetypes + + @classmethod + def get_supported_filetypes_grouped(cls): + """ + Return a dict of savefig file formats supported by this backend, + where the keys are a file type name, such as 'Joint Photographic + Experts Group', and the values are a list of filename extensions used + for that filetype, such as ['jpg', 'jpeg']. + """ + groupings = {} + for ext, name in cls.filetypes.items(): + groupings.setdefault(name, []).append(ext) + groupings[name].sort() + return groupings + + @contextmanager + def _switch_canvas_and_return_print_method(self, fmt, backend=None): + """ + Context manager temporarily setting the canvas for saving the figure:: + + with (canvas._switch_canvas_and_return_print_method(fmt, backend) + as print_method): + # ``print_method`` is a suitable ``print_{fmt}`` method, and + # the figure's canvas is temporarily switched to the method's + # canvas within the with... block. ``print_method`` is also + # wrapped to suppress extra kwargs passed by ``print_figure``. + + Parameters + ---------- + fmt : str + If *backend* is None, then determine a suitable canvas class for + saving to format *fmt* -- either the current canvas class, if it + supports *fmt*, or whatever `get_registered_canvas_class` returns; + switch the figure canvas to that canvas class. + backend : str or None, default: None + If not None, switch the figure canvas to the ``FigureCanvas`` class + of the given backend. + """ + canvas = None + if backend is not None: + # Return a specific canvas class, if requested. + from .backends.registry import backend_registry + canvas_class = backend_registry.load_backend_module(backend).FigureCanvas + if not hasattr(canvas_class, f"print_{fmt}"): + raise ValueError( + f"The {backend!r} backend does not support {fmt} output") + canvas = canvas_class(self.figure) + elif hasattr(self, f"print_{fmt}"): + # Return the current canvas if it supports the requested format. + canvas = self + else: + # Return a default canvas for the requested format, if it exists. + canvas_class = get_registered_canvas_class(fmt) + if canvas_class is None: + raise ValueError( + "Format {!r} is not supported (supported formats: {})".format( + fmt, ", ".join(sorted(self.get_supported_filetypes())))) + canvas = canvas_class(self.figure) + canvas._is_saving = self._is_saving + meth = getattr(canvas, f"print_{fmt}") + mod = (meth.func.__module__ + if hasattr(meth, "func") # partialmethod, e.g. backend_wx. + else meth.__module__) + if mod.startswith(("matplotlib.", "mpl_toolkits.")): + optional_kws = { # Passed by print_figure for other renderers. + "dpi", "facecolor", "edgecolor", "orientation", + "bbox_inches_restore"} + skip = optional_kws - {*inspect.signature(meth).parameters} + print_method = functools.wraps(meth)(lambda *args, **kwargs: meth( + *args, **{k: v for k, v in kwargs.items() if k not in skip})) + else: # Let third-parties do as they see fit. + print_method = meth + try: + yield print_method + finally: + self.figure.canvas = self + + def print_figure( + self, filename, dpi=None, facecolor=None, edgecolor=None, + orientation='portrait', format=None, *, + bbox_inches=None, pad_inches=None, bbox_extra_artists=None, + backend=None, **kwargs): + """ + Render the figure to hardcopy. Set the figure patch face and edge + colors. This is useful because some of the GUIs have a gray figure + face color background and you'll probably want to override this on + hardcopy. + + Parameters + ---------- + filename : str or path-like or file-like + The file where the figure is saved. + + dpi : float, default: :rc:`savefig.dpi` + The dots per inch to save the figure in. + + facecolor : :mpltype:`color` or 'auto', default: :rc:`savefig.facecolor` + The facecolor of the figure. If 'auto', use the current figure + facecolor. + + edgecolor : :mpltype:`color` or 'auto', default: :rc:`savefig.edgecolor` + The edgecolor of the figure. If 'auto', use the current figure + edgecolor. + + orientation : {'landscape', 'portrait'}, default: 'portrait' + Only currently applies to PostScript printing. + + format : str, optional + Force a specific file format. If not given, the format is inferred + from the *filename* extension, and if that fails from + :rc:`savefig.format`. + + bbox_inches : 'tight' or `.Bbox`, default: :rc:`savefig.bbox` + Bounding box in inches: only the given portion of the figure is + saved. If 'tight', try to figure out the tight bbox of the figure. + + pad_inches : float or 'layout', default: :rc:`savefig.pad_inches` + Amount of padding in inches around the figure when bbox_inches is + 'tight'. If 'layout' use the padding from the constrained or + compressed layout engine; ignored if one of those engines is not in + use. + + bbox_extra_artists : list of `~matplotlib.artist.Artist`, optional + A list of extra artists that will be considered when the + tight bbox is calculated. + + backend : str, optional + Use a non-default backend to render the file, e.g. to render a + png file with the "cairo" backend rather than the default "agg", + or a pdf file with the "pgf" backend rather than the default + "pdf". Note that the default backend is normally sufficient. See + :ref:`the-builtin-backends` for a list of valid backends for each + file format. Custom backends can be referenced as "module://...". + """ + if format is None: + # get format from filename, or from backend's default filetype + if isinstance(filename, os.PathLike): + filename = os.fspath(filename) + if isinstance(filename, str): + format = os.path.splitext(filename)[1][1:] + if format is None or format == '': + format = self.get_default_filetype() + if isinstance(filename, str): + filename = filename.rstrip('.') + '.' + format + format = format.lower() + + if dpi is None: + dpi = rcParams['savefig.dpi'] + if dpi == 'figure': + dpi = getattr(self.figure, '_original_dpi', self.figure.dpi) + + # Remove the figure manager, if any, to avoid resizing the GUI widget. + with (cbook._setattr_cm(self, manager=None), + self._switch_canvas_and_return_print_method(format, backend) + as print_method, + cbook._setattr_cm(self.figure, dpi=dpi), + cbook._setattr_cm(self.figure.canvas, _device_pixel_ratio=1), + cbook._setattr_cm(self.figure.canvas, _is_saving=True), + ExitStack() as stack): + + for prop in ["facecolor", "edgecolor"]: + color = locals()[prop] + if color is None: + color = rcParams[f"savefig.{prop}"] + if not cbook._str_equal(color, "auto"): + stack.enter_context(self.figure._cm_set(**{prop: color})) + + if bbox_inches is None: + bbox_inches = rcParams['savefig.bbox'] + + layout_engine = self.figure.get_layout_engine() + if layout_engine is not None or bbox_inches == "tight": + # we need to trigger a draw before printing to make sure + # CL works. "tight" also needs a draw to get the right + # locations: + renderer = _get_renderer( + self.figure, + functools.partial( + print_method, orientation=orientation) + ) + # we do this instead of `self.figure.draw_without_rendering` + # so that we can inject the orientation + with getattr(renderer, "_draw_disabled", nullcontext)(): + self.figure.draw(renderer) + if bbox_inches: + if bbox_inches == "tight": + bbox_inches = self.figure.get_tightbbox( + renderer, bbox_extra_artists=bbox_extra_artists) + if (isinstance(layout_engine, ConstrainedLayoutEngine) and + pad_inches == "layout"): + h_pad = layout_engine.get()["h_pad"] + w_pad = layout_engine.get()["w_pad"] + else: + if pad_inches in [None, "layout"]: + pad_inches = rcParams['savefig.pad_inches'] + h_pad = w_pad = pad_inches + bbox_inches = bbox_inches.padded(w_pad, h_pad) + + # call adjust_bbox to save only the given area + restore_bbox = _tight_bbox.adjust_bbox( + self.figure, bbox_inches, self.figure.canvas.fixed_dpi) + + _bbox_inches_restore = (bbox_inches, restore_bbox) + else: + _bbox_inches_restore = None + + # we have already done layout above, so turn it off: + stack.enter_context(self.figure._cm_set(layout_engine='none')) + try: + # _get_renderer may change the figure dpi (as vector formats + # force the figure dpi to 72), so we need to set it again here. + with cbook._setattr_cm(self.figure, dpi=dpi): + result = print_method( + filename, + facecolor=facecolor, + edgecolor=edgecolor, + orientation=orientation, + bbox_inches_restore=_bbox_inches_restore, + **kwargs) + finally: + if bbox_inches and restore_bbox: + restore_bbox() + + return result + + @classmethod + def get_default_filetype(cls): + """ + Return the default savefig file format as specified in + :rc:`savefig.format`. + + The returned string does not include a period. This method is + overridden in backends that only support a single file type. + """ + return rcParams['savefig.format'] + + def get_default_filename(self): + """ + Return a suitable default filename, including the extension. + """ + default_basename = ( + self.manager.get_window_title() + if self.manager is not None + else '' + ) + default_basename = default_basename or 'image' + # Characters to be avoided in a NT path: + # https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#naming_conventions + # plus ' ' + removed_chars = '<>:"/\\|?*\0 ' + default_basename = default_basename.translate( + {ord(c): "_" for c in removed_chars}) + default_filetype = self.get_default_filetype() + return f'{default_basename}.{default_filetype}' + + def mpl_connect(self, s, func): + """ + Bind function *func* to event *s*. + + Parameters + ---------- + s : str + One of the following events ids: + + - 'button_press_event' + - 'button_release_event' + - 'draw_event' + - 'key_press_event' + - 'key_release_event' + - 'motion_notify_event' + - 'pick_event' + - 'resize_event' + - 'scroll_event' + - 'figure_enter_event', + - 'figure_leave_event', + - 'axes_enter_event', + - 'axes_leave_event' + - 'close_event'. + + func : callable + The callback function to be executed, which must have the + signature:: + + def func(event: Event) -> Any + + For the location events (button and key press/release), if the + mouse is over the Axes, the ``inaxes`` attribute of the event will + be set to the `~matplotlib.axes.Axes` the event occurs is over, and + additionally, the variables ``xdata`` and ``ydata`` attributes will + be set to the mouse location in data coordinates. See `.KeyEvent` + and `.MouseEvent` for more info. + + .. note:: + + If func is a method, this only stores a weak reference to the + method. Thus, the figure does not influence the lifetime of + the associated object. Usually, you want to make sure that the + object is kept alive throughout the lifetime of the figure by + holding a reference to it. + + Returns + ------- + cid + A connection id that can be used with + `.FigureCanvasBase.mpl_disconnect`. + + Examples + -------- + :: + + def on_press(event): + print('you pressed', event.button, event.xdata, event.ydata) + + cid = canvas.mpl_connect('button_press_event', on_press) + """ + + return self.callbacks.connect(s, func) + + def mpl_disconnect(self, cid): + """ + Disconnect the callback with id *cid*. + + Examples + -------- + :: + + cid = canvas.mpl_connect('button_press_event', on_press) + # ... later + canvas.mpl_disconnect(cid) + """ + self.callbacks.disconnect(cid) + + # Internal subclasses can override _timer_cls instead of new_timer, though + # this is not a public API for third-party subclasses. + _timer_cls = TimerBase + + def new_timer(self, interval=None, callbacks=None): + """ + Create a new backend-specific subclass of `.Timer`. + + This is useful for getting periodic events through the backend's native + event loop. Implemented only for backends with GUIs. + + Parameters + ---------- + interval : int + Timer interval in milliseconds. + + callbacks : list[tuple[callable, tuple, dict]] + Sequence of (func, args, kwargs) where ``func(*args, **kwargs)`` + will be executed by the timer every *interval*. + + Callbacks which return ``False`` or ``0`` will be removed from the + timer. + + Examples + -------- + >>> timer = fig.canvas.new_timer(callbacks=[(f1, (1,), {'a': 3})]) + """ + return self._timer_cls(interval=interval, callbacks=callbacks) + + def flush_events(self): + """ + Flush the GUI events for the figure. + + Interactive backends need to reimplement this method. + """ + + def start_event_loop(self, timeout=0): + """ + Start a blocking event loop. + + Such an event loop is used by interactive functions, such as + `~.Figure.ginput` and `~.Figure.waitforbuttonpress`, to wait for + events. + + The event loop blocks until a callback function triggers + `stop_event_loop`, or *timeout* is reached. + + If *timeout* is 0 or negative, never timeout. + + Only interactive backends need to reimplement this method and it relies + on `flush_events` being properly implemented. + + Interactive backends should implement this in a more native way. + """ + if timeout <= 0: + timeout = np.inf + timestep = 0.01 + counter = 0 + self._looping = True + while self._looping and counter * timestep < timeout: + self.flush_events() + time.sleep(timestep) + counter += 1 + + def stop_event_loop(self): + """ + Stop the current blocking event loop. + + Interactive backends need to reimplement this to match + `start_event_loop` + """ + self._looping = False + + +def key_press_handler(event, canvas=None, toolbar=None): + """ + Implement the default Matplotlib key bindings for the canvas and toolbar + described at :ref:`key-event-handling`. + + Parameters + ---------- + event : `KeyEvent` + A key press/release event. + canvas : `FigureCanvasBase`, default: ``event.canvas`` + The backend-specific canvas instance. This parameter is kept for + back-compatibility, but, if set, should always be equal to + ``event.canvas``. + toolbar : `NavigationToolbar2`, default: ``event.canvas.toolbar`` + The navigation cursor toolbar. This parameter is kept for + back-compatibility, but, if set, should always be equal to + ``event.canvas.toolbar``. + """ + if event.key is None: + return + if canvas is None: + canvas = event.canvas + if toolbar is None: + toolbar = canvas.toolbar + + # toggle fullscreen mode (default key 'f', 'ctrl + f') + if event.key in rcParams['keymap.fullscreen']: + try: + canvas.manager.full_screen_toggle() + except AttributeError: + pass + + # quit the figure (default key 'ctrl+w') + if event.key in rcParams['keymap.quit']: + Gcf.destroy_fig(canvas.figure) + if event.key in rcParams['keymap.quit_all']: + Gcf.destroy_all() + + if toolbar is not None: + # home or reset mnemonic (default key 'h', 'home' and 'r') + if event.key in rcParams['keymap.home']: + toolbar.home() + # forward / backward keys to enable left handed quick navigation + # (default key for backward: 'left', 'backspace' and 'c') + elif event.key in rcParams['keymap.back']: + toolbar.back() + # (default key for forward: 'right' and 'v') + elif event.key in rcParams['keymap.forward']: + toolbar.forward() + # pan mnemonic (default key 'p') + elif event.key in rcParams['keymap.pan']: + toolbar.pan() + toolbar._update_cursor(event) + # zoom mnemonic (default key 'o') + elif event.key in rcParams['keymap.zoom']: + toolbar.zoom() + toolbar._update_cursor(event) + # saving current figure (default key 's') + elif event.key in rcParams['keymap.save']: + toolbar.save_figure() + + if event.inaxes is None: + return + + # these bindings require the mouse to be over an Axes to trigger + def _get_uniform_gridstate(ticks): + # Return True/False if all grid lines are on or off, None if they are + # not all in the same state. + return (True if all(tick.gridline.get_visible() for tick in ticks) else + False if not any(tick.gridline.get_visible() for tick in ticks) else + None) + + ax = event.inaxes + # toggle major grids in current Axes (default key 'g') + # Both here and below (for 'G'), we do nothing if *any* grid (major or + # minor, x or y) is not in a uniform state, to avoid messing up user + # customization. + if (event.key in rcParams['keymap.grid'] + # Exclude minor grids not in a uniform state. + and None not in [_get_uniform_gridstate(ax.xaxis.minorTicks), + _get_uniform_gridstate(ax.yaxis.minorTicks)]): + x_state = _get_uniform_gridstate(ax.xaxis.majorTicks) + y_state = _get_uniform_gridstate(ax.yaxis.majorTicks) + cycle = [(False, False), (True, False), (True, True), (False, True)] + try: + x_state, y_state = ( + cycle[(cycle.index((x_state, y_state)) + 1) % len(cycle)]) + except ValueError: + # Exclude major grids not in a uniform state. + pass + else: + # If turning major grids off, also turn minor grids off. + ax.grid(x_state, which="major" if x_state else "both", axis="x") + ax.grid(y_state, which="major" if y_state else "both", axis="y") + canvas.draw_idle() + # toggle major and minor grids in current Axes (default key 'G') + if (event.key in rcParams['keymap.grid_minor'] + # Exclude major grids not in a uniform state. + and None not in [_get_uniform_gridstate(ax.xaxis.majorTicks), + _get_uniform_gridstate(ax.yaxis.majorTicks)]): + x_state = _get_uniform_gridstate(ax.xaxis.minorTicks) + y_state = _get_uniform_gridstate(ax.yaxis.minorTicks) + cycle = [(False, False), (True, False), (True, True), (False, True)] + try: + x_state, y_state = ( + cycle[(cycle.index((x_state, y_state)) + 1) % len(cycle)]) + except ValueError: + # Exclude minor grids not in a uniform state. + pass + else: + ax.grid(x_state, which="both", axis="x") + ax.grid(y_state, which="both", axis="y") + canvas.draw_idle() + # toggle scaling of y-axes between 'log and 'linear' (default key 'l') + elif event.key in rcParams['keymap.yscale']: + scale = ax.get_yscale() + if scale == 'log': + ax.set_yscale('linear') + ax.get_figure(root=True).canvas.draw_idle() + elif scale == 'linear': + try: + ax.set_yscale('log') + except ValueError as exc: + _log.warning(str(exc)) + ax.set_yscale('linear') + ax.get_figure(root=True).canvas.draw_idle() + # toggle scaling of x-axes between 'log and 'linear' (default key 'k') + elif event.key in rcParams['keymap.xscale']: + scalex = ax.get_xscale() + if scalex == 'log': + ax.set_xscale('linear') + ax.get_figure(root=True).canvas.draw_idle() + elif scalex == 'linear': + try: + ax.set_xscale('log') + except ValueError as exc: + _log.warning(str(exc)) + ax.set_xscale('linear') + ax.get_figure(root=True).canvas.draw_idle() + + +def button_press_handler(event, canvas=None, toolbar=None): + """ + The default Matplotlib button actions for extra mouse buttons. + + Parameters are as for `key_press_handler`, except that *event* is a + `MouseEvent`. + """ + if canvas is None: + canvas = event.canvas + if toolbar is None: + toolbar = canvas.toolbar + if toolbar is not None: + button_name = str(MouseButton(event.button)) + if button_name in rcParams['keymap.back']: + toolbar.back() + elif button_name in rcParams['keymap.forward']: + toolbar.forward() + + +class NonGuiException(Exception): + """Raised when trying show a figure in a non-GUI backend.""" + pass + + +class FigureManagerBase: + """ + A backend-independent abstraction of a figure container and controller. + + The figure manager is used by pyplot to interact with the window in a + backend-independent way. It's an adapter for the real (GUI) framework that + represents the visual figure on screen. + + The figure manager is connected to a specific canvas instance, which in turn + is connected to a specific figure instance. To access a figure manager for + a given figure in user code, you typically use ``fig.canvas.manager``. + + GUI backends derive from this class to translate common operations such + as *show* or *resize* to the GUI-specific code. Non-GUI backends do not + support these operations and can just use the base class. + + This following basic operations are accessible: + + **Window operations** + + - `~.FigureManagerBase.show` + - `~.FigureManagerBase.destroy` + - `~.FigureManagerBase.full_screen_toggle` + - `~.FigureManagerBase.resize` + - `~.FigureManagerBase.get_window_title` + - `~.FigureManagerBase.set_window_title` + + **Key and mouse button press handling** + + The figure manager sets up default key and mouse button press handling by + hooking up the `.key_press_handler` to the matplotlib event system. This + ensures the same shortcuts and mouse actions across backends. + + **Other operations** + + Subclasses will have additional attributes and functions to access + additional functionality. This is of course backend-specific. For example, + most GUI backends have ``window`` and ``toolbar`` attributes that give + access to the native GUI widgets of the respective framework. + + Attributes + ---------- + canvas : `FigureCanvasBase` + The backend-specific canvas instance. + + num : int or str + The figure number. + + key_press_handler_id : int + The default key handler cid, when using the toolmanager. + To disable the default key press handling use:: + + figure.canvas.mpl_disconnect( + figure.canvas.manager.key_press_handler_id) + + button_press_handler_id : int + The default mouse button handler cid, when using the toolmanager. + To disable the default button press handling use:: + + figure.canvas.mpl_disconnect( + figure.canvas.manager.button_press_handler_id) + """ + + _toolbar2_class = None + _toolmanager_toolbar_class = None + + def __init__(self, canvas, num): + self.canvas = canvas + canvas.manager = self # store a pointer to parent + self.num = num + self.set_window_title(f"Figure {num:d}") + + self.key_press_handler_id = None + self.button_press_handler_id = None + if rcParams['toolbar'] != 'toolmanager': + self.key_press_handler_id = self.canvas.mpl_connect( + 'key_press_event', key_press_handler) + self.button_press_handler_id = self.canvas.mpl_connect( + 'button_press_event', button_press_handler) + + self.toolmanager = (ToolManager(canvas.figure) + if mpl.rcParams['toolbar'] == 'toolmanager' + else None) + if (mpl.rcParams["toolbar"] == "toolbar2" + and self._toolbar2_class): + self.toolbar = self._toolbar2_class(self.canvas) + elif (mpl.rcParams["toolbar"] == "toolmanager" + and self._toolmanager_toolbar_class): + self.toolbar = self._toolmanager_toolbar_class(self.toolmanager) + else: + self.toolbar = None + + if self.toolmanager: + tools.add_tools_to_manager(self.toolmanager) + if self.toolbar: + tools.add_tools_to_container(self.toolbar) + + @self.canvas.figure.add_axobserver + def notify_axes_change(fig): + # Called whenever the current Axes is changed. + if self.toolmanager is None and self.toolbar is not None: + self.toolbar.update() + + @classmethod + def create_with_canvas(cls, canvas_class, figure, num): + """ + Create a manager for a given *figure* using a specific *canvas_class*. + + Backends should override this method if they have specific needs for + setting up the canvas or the manager. + """ + return cls(canvas_class(figure), num) + + @classmethod + def start_main_loop(cls): + """ + Start the main event loop. + + This method is called by `.FigureManagerBase.pyplot_show`, which is the + implementation of `.pyplot.show`. To customize the behavior of + `.pyplot.show`, interactive backends should usually override + `~.FigureManagerBase.start_main_loop`; if more customized logic is + necessary, `~.FigureManagerBase.pyplot_show` can also be overridden. + """ + + @classmethod + def pyplot_show(cls, *, block=None): + """ + Show all figures. This method is the implementation of `.pyplot.show`. + + To customize the behavior of `.pyplot.show`, interactive backends + should usually override `~.FigureManagerBase.start_main_loop`; if more + customized logic is necessary, `~.FigureManagerBase.pyplot_show` can + also be overridden. + + Parameters + ---------- + block : bool, optional + Whether to block by calling ``start_main_loop``. The default, + None, means to block if we are neither in IPython's ``%pylab`` mode + nor in ``interactive`` mode. + """ + managers = Gcf.get_all_fig_managers() + if not managers: + return + for manager in managers: + try: + manager.show() # Emits a warning for non-interactive backend. + except NonGuiException as exc: + _api.warn_external(str(exc)) + if block is None: + # Hack: Are we in IPython's %pylab mode? In pylab mode, IPython + # (>= 0.10) tacks a _needmain attribute onto pyplot.show (always + # set to False). + pyplot_show = getattr(sys.modules.get("matplotlib.pyplot"), "show", None) + ipython_pylab = hasattr(pyplot_show, "_needmain") + block = not ipython_pylab and not is_interactive() + if block: + cls.start_main_loop() + + def show(self): + """ + For GUI backends, show the figure window and redraw. + For non-GUI backends, raise an exception, unless running headless (i.e. + on Linux with an unset DISPLAY); this exception is converted to a + warning in `.Figure.show`. + """ + # This should be overridden in GUI backends. + if sys.platform == "linux" and not os.environ.get("DISPLAY"): + # We cannot check _get_running_interactive_framework() == + # "headless" because that would also suppress the warning when + # $DISPLAY exists but is invalid, which is more likely an error and + # thus warrants a warning. + return + raise NonGuiException( + f"{type(self.canvas).__name__} is non-interactive, and thus cannot be " + f"shown") + + def destroy(self): + pass + + def full_screen_toggle(self): + pass + + def resize(self, w, h): + """For GUI backends, resize the window (in physical pixels).""" + + def get_window_title(self): + """Return the title text of the window containing the figure.""" + return self._window_title + + def set_window_title(self, title): + """ + Set the title text of the window containing the figure. + + Examples + -------- + >>> fig = plt.figure() + >>> fig.canvas.manager.set_window_title('My figure') + """ + # This attribute is not defined in __init__ (but __init__ calls this + # setter), as derived classes (real GUI managers) will store this + # information directly on the widget; only the base (non-GUI) manager + # class needs a specific attribute for it (so that filename escaping + # can be checked in the test suite). + self._window_title = title + + +cursors = tools.cursors + + +class _Mode(str, Enum): + NONE = "" + PAN = "pan/zoom" + ZOOM = "zoom rect" + + def __str__(self): + return self.value + + @property + def _navigate_mode(self): + return self.name if self is not _Mode.NONE else None + + +class NavigationToolbar2: + """ + Base class for the navigation cursor, version 2. + + Backends must implement a canvas that handles connections for + 'button_press_event' and 'button_release_event'. See + :meth:`FigureCanvasBase.mpl_connect` for more information. + + They must also define + + :meth:`save_figure` + Save the current figure. + + :meth:`draw_rubberband` (optional) + Draw the zoom to rect "rubberband" rectangle. + + :meth:`set_message` (optional) + Display message. + + :meth:`set_history_buttons` (optional) + You can change the history back / forward buttons to indicate disabled / enabled + state. + + and override ``__init__`` to set up the toolbar -- without forgetting to + call the base-class init. Typically, ``__init__`` needs to set up toolbar + buttons connected to the `home`, `back`, `forward`, `pan`, `zoom`, and + `save_figure` methods and using standard icons in the "images" subdirectory + of the data path. + + That's it, we'll do the rest! + """ + + # list of toolitems to add to the toolbar, format is: + # ( + # text, # the text of the button (often not visible to users) + # tooltip_text, # the tooltip shown on hover (where possible) + # image_file, # name of the image for the button (without the extension) + # name_of_method, # name of the method in NavigationToolbar2 to call + # ) + toolitems = ( + ('Home', 'Reset original view', 'home', 'home'), + ('Back', 'Back to previous view', 'back', 'back'), + ('Forward', 'Forward to next view', 'forward', 'forward'), + (None, None, None, None), + ('Pan', + 'Left button pans, Right button zooms\n' + 'x/y fixes axis, CTRL fixes aspect', + 'move', 'pan'), + ('Zoom', 'Zoom to rectangle\nx/y fixes axis', 'zoom_to_rect', 'zoom'), + ('Subplots', 'Configure subplots', 'subplots', 'configure_subplots'), + (None, None, None, None), + ('Save', 'Save the figure', 'filesave', 'save_figure'), + ) + + UNKNOWN_SAVED_STATUS = object() + + def __init__(self, canvas): + self.canvas = canvas + canvas.toolbar = self + self._nav_stack = cbook._Stack() + # This cursor will be set after the initial draw. + self._last_cursor = tools.Cursors.POINTER + + self._id_press = self.canvas.mpl_connect( + 'button_press_event', self._zoom_pan_handler) + self._id_release = self.canvas.mpl_connect( + 'button_release_event', self._zoom_pan_handler) + self._id_drag = self.canvas.mpl_connect( + 'motion_notify_event', self.mouse_move) + self._pan_info = None + self._zoom_info = None + + self.mode = _Mode.NONE # a mode string for the status bar + self.set_history_buttons() + + def set_message(self, s): + """Display a message on toolbar or in status bar.""" + + def draw_rubberband(self, event, x0, y0, x1, y1): + """ + Draw a rectangle rubberband to indicate zoom limits. + + Note that it is not guaranteed that ``x0 <= x1`` and ``y0 <= y1``. + """ + + def remove_rubberband(self): + """Remove the rubberband.""" + + def home(self, *args): + """ + Restore the original view. + + For convenience of being directly connected as a GUI callback, which + often get passed additional parameters, this method accepts arbitrary + parameters, but does not use them. + """ + self._nav_stack.home() + self.set_history_buttons() + self._update_view() + + def back(self, *args): + """ + Move back up the view lim stack. + + For convenience of being directly connected as a GUI callback, which + often get passed additional parameters, this method accepts arbitrary + parameters, but does not use them. + """ + self._nav_stack.back() + self.set_history_buttons() + self._update_view() + + def forward(self, *args): + """ + Move forward in the view lim stack. + + For convenience of being directly connected as a GUI callback, which + often get passed additional parameters, this method accepts arbitrary + parameters, but does not use them. + """ + self._nav_stack.forward() + self.set_history_buttons() + self._update_view() + + def _update_cursor(self, event): + """ + Update the cursor after a mouse move event or a tool (de)activation. + """ + if self.mode and event.inaxes and event.inaxes.get_navigate(): + if (self.mode == _Mode.ZOOM + and self._last_cursor != tools.Cursors.SELECT_REGION): + self.canvas.set_cursor(tools.Cursors.SELECT_REGION) + self._last_cursor = tools.Cursors.SELECT_REGION + elif (self.mode == _Mode.PAN + and self._last_cursor != tools.Cursors.MOVE): + self.canvas.set_cursor(tools.Cursors.MOVE) + self._last_cursor = tools.Cursors.MOVE + elif self._last_cursor != tools.Cursors.POINTER: + self.canvas.set_cursor(tools.Cursors.POINTER) + self._last_cursor = tools.Cursors.POINTER + + @contextmanager + def _wait_cursor_for_draw_cm(self): + """ + Set the cursor to a wait cursor when drawing the canvas. + + In order to avoid constantly changing the cursor when the canvas + changes frequently, do nothing if this context was triggered during the + last second. (Optimally we'd prefer only setting the wait cursor if + the *current* draw takes too long, but the current draw blocks the GUI + thread). + """ + self._draw_time, last_draw_time = ( + time.time(), getattr(self, "_draw_time", -np.inf)) + if self._draw_time - last_draw_time > 1: + try: + self.canvas.set_cursor(tools.Cursors.WAIT) + yield + finally: + self.canvas.set_cursor(self._last_cursor) + else: + yield + + @staticmethod + def _mouse_event_to_message(event): + if event.inaxes and event.inaxes.get_navigate(): + try: + s = event.inaxes.format_coord(event.xdata, event.ydata) + except (ValueError, OverflowError): + pass + else: + s = s.rstrip() + artists = [a for a in event.inaxes._mouseover_set + if a.contains(event)[0] and a.get_visible()] + if artists: + a = cbook._topmost_artist(artists) + if a is not event.inaxes.patch: + data = a.get_cursor_data(event) + if data is not None: + data_str = a.format_cursor_data(data).rstrip() + if data_str: + s = s + '\n' + data_str + return s + return "" + + def mouse_move(self, event): + self._update_cursor(event) + self.set_message(self._mouse_event_to_message(event)) + + def _zoom_pan_handler(self, event): + if self.mode == _Mode.PAN: + if event.name == "button_press_event": + self.press_pan(event) + elif event.name == "button_release_event": + self.release_pan(event) + if self.mode == _Mode.ZOOM: + if event.name == "button_press_event": + self.press_zoom(event) + elif event.name == "button_release_event": + self.release_zoom(event) + + def _start_event_axes_interaction(self, event, *, method): + + def _ax_filter(ax): + return (ax.in_axes(event) and + ax.get_navigate() and + getattr(ax, f"can_{method}")() + ) + + def _capture_events(ax): + f = ax.get_forward_navigation_events() + if f == "auto": # (capture = patch visibility) + f = not ax.patch.get_visible() + return not f + + # get all relevant axes for the event + axes = list(filter(_ax_filter, self.canvas.figure.get_axes())) + + if len(axes) == 0: + return [] + + if self._nav_stack() is None: + self.push_current() # Set the home button to this view. + + # group axes by zorder (reverse to trigger later axes first) + grps = dict() + for ax in reversed(axes): + grps.setdefault(ax.get_zorder(), []).append(ax) + + axes_to_trigger = [] + # go through zorders in reverse until we hit a capturing axes + for zorder in sorted(grps, reverse=True): + for ax in grps[zorder]: + axes_to_trigger.append(ax) + # NOTE: shared axes are automatically triggered, but twin-axes not! + axes_to_trigger.extend(ax._twinned_axes.get_siblings(ax)) + + if _capture_events(ax): + break # break if we hit a capturing axes + else: + # If the inner loop finished without an explicit break, + # (e.g. no capturing axes was found) continue the + # outer loop to the next zorder. + continue + + # If the inner loop was terminated with an explicit break, + # terminate the outer loop as well. + break + + # avoid duplicated triggers (but keep order of list) + axes_to_trigger = list(dict.fromkeys(axes_to_trigger)) + + return axes_to_trigger + + def pan(self, *args): + """ + Toggle the pan/zoom tool. + + Pan with left button, zoom with right. + """ + if not self.canvas.widgetlock.available(self): + self.set_message("pan unavailable") + return + if self.mode == _Mode.PAN: + self.mode = _Mode.NONE + self.canvas.widgetlock.release(self) + else: + self.mode = _Mode.PAN + self.canvas.widgetlock(self) + for a in self.canvas.figure.get_axes(): + a.set_navigate_mode(self.mode._navigate_mode) + + _PanInfo = namedtuple("_PanInfo", "button axes cid") + + def press_pan(self, event): + """Callback for mouse button press in pan/zoom mode.""" + if (event.button not in [MouseButton.LEFT, MouseButton.RIGHT] + or event.x is None or event.y is None): + return + + axes = self._start_event_axes_interaction(event, method="pan") + if not axes: + return + + # call "ax.start_pan(..)" on all relevant axes of an event + for ax in axes: + ax.start_pan(event.x, event.y, event.button) + + self.canvas.mpl_disconnect(self._id_drag) + id_drag = self.canvas.mpl_connect("motion_notify_event", self.drag_pan) + + self._pan_info = self._PanInfo( + button=event.button, axes=axes, cid=id_drag) + + def drag_pan(self, event): + """Callback for dragging in pan/zoom mode.""" + for ax in self._pan_info.axes: + # Using the recorded button at the press is safer than the current + # button, as multiple buttons can get pressed during motion. + ax.drag_pan(self._pan_info.button, event.key, event.x, event.y) + self.canvas.draw_idle() + + def release_pan(self, event): + """Callback for mouse button release in pan/zoom mode.""" + if self._pan_info is None: + return + self.canvas.mpl_disconnect(self._pan_info.cid) + self._id_drag = self.canvas.mpl_connect( + 'motion_notify_event', self.mouse_move) + for ax in self._pan_info.axes: + ax.end_pan() + self.canvas.draw_idle() + self._pan_info = None + self.push_current() + + def zoom(self, *args): + if not self.canvas.widgetlock.available(self): + self.set_message("zoom unavailable") + return + """Toggle zoom to rect mode.""" + if self.mode == _Mode.ZOOM: + self.mode = _Mode.NONE + self.canvas.widgetlock.release(self) + else: + self.mode = _Mode.ZOOM + self.canvas.widgetlock(self) + for a in self.canvas.figure.get_axes(): + a.set_navigate_mode(self.mode._navigate_mode) + + _ZoomInfo = namedtuple("_ZoomInfo", "direction start_xy axes cid cbar") + + def press_zoom(self, event): + """Callback for mouse button press in zoom to rect mode.""" + if (event.button not in [MouseButton.LEFT, MouseButton.RIGHT] + or event.x is None or event.y is None): + return + + axes = self._start_event_axes_interaction(event, method="zoom") + if not axes: + return + + id_zoom = self.canvas.mpl_connect( + "motion_notify_event", self.drag_zoom) + + # A colorbar is one-dimensional, so we extend the zoom rectangle out + # to the edge of the Axes bbox in the other dimension. To do that we + # store the orientation of the colorbar for later. + parent_ax = axes[0] + if hasattr(parent_ax, "_colorbar"): + cbar = parent_ax._colorbar.orientation + else: + cbar = None + + self._zoom_info = self._ZoomInfo( + direction="in" if event.button == 1 else "out", + start_xy=(event.x, event.y), axes=axes, cid=id_zoom, cbar=cbar) + + def drag_zoom(self, event): + """Callback for dragging in zoom mode.""" + start_xy = self._zoom_info.start_xy + ax = self._zoom_info.axes[0] + (x1, y1), (x2, y2) = np.clip( + [start_xy, [event.x, event.y]], ax.bbox.min, ax.bbox.max) + key = event.key + # Force the key on colorbars to extend the short-axis bbox + if self._zoom_info.cbar == "horizontal": + key = "x" + elif self._zoom_info.cbar == "vertical": + key = "y" + if key == "x": + y1, y2 = ax.bbox.intervaly + elif key == "y": + x1, x2 = ax.bbox.intervalx + + self.draw_rubberband(event, x1, y1, x2, y2) + + def release_zoom(self, event): + """Callback for mouse button release in zoom to rect mode.""" + if self._zoom_info is None: + return + + # We don't check the event button here, so that zooms can be cancelled + # by (pressing and) releasing another mouse button. + self.canvas.mpl_disconnect(self._zoom_info.cid) + self.remove_rubberband() + + start_x, start_y = self._zoom_info.start_xy + key = event.key + # Force the key on colorbars to ignore the zoom-cancel on the + # short-axis side + if self._zoom_info.cbar == "horizontal": + key = "x" + elif self._zoom_info.cbar == "vertical": + key = "y" + # Ignore single clicks: 5 pixels is a threshold that allows the user to + # "cancel" a zoom action by zooming by less than 5 pixels. + if ((abs(event.x - start_x) < 5 and key != "y") or + (abs(event.y - start_y) < 5 and key != "x")): + self.canvas.draw_idle() + self._zoom_info = None + return + + for i, ax in enumerate(self._zoom_info.axes): + # Detect whether this Axes is twinned with an earlier Axes in the + # list of zoomed Axes, to avoid double zooming. + twinx = any(ax.get_shared_x_axes().joined(ax, prev) + for prev in self._zoom_info.axes[:i]) + twiny = any(ax.get_shared_y_axes().joined(ax, prev) + for prev in self._zoom_info.axes[:i]) + ax._set_view_from_bbox( + (start_x, start_y, event.x, event.y), + self._zoom_info.direction, key, twinx, twiny) + + self.canvas.draw_idle() + self._zoom_info = None + self.push_current() + + def push_current(self): + """Push the current view limits and position onto the stack.""" + self._nav_stack.push( + WeakKeyDictionary( + {ax: (ax._get_view(), + # Store both the original and modified positions. + (ax.get_position(True).frozen(), + ax.get_position().frozen())) + for ax in self.canvas.figure.axes})) + self.set_history_buttons() + + def _update_view(self): + """ + Update the viewlim and position from the view and position stack for + each Axes. + """ + nav_info = self._nav_stack() + if nav_info is None: + return + # Retrieve all items at once to avoid any risk of GC deleting an Axes + # while in the middle of the loop below. + items = list(nav_info.items()) + for ax, (view, (pos_orig, pos_active)) in items: + ax._set_view(view) + # Restore both the original and modified positions + ax._set_position(pos_orig, 'original') + ax._set_position(pos_active, 'active') + self.canvas.draw_idle() + + def configure_subplots(self, *args): + if hasattr(self, "subplot_tool"): + self.subplot_tool.figure.canvas.manager.show() + return + # This import needs to happen here due to circular imports. + from matplotlib.figure import Figure + with mpl.rc_context({"toolbar": "none"}): # No navbar for the toolfig. + manager = type(self.canvas).new_manager(Figure(figsize=(6, 3)), -1) + manager.set_window_title("Subplot configuration tool") + tool_fig = manager.canvas.figure + tool_fig.subplots_adjust(top=0.9) + self.subplot_tool = widgets.SubplotTool(self.canvas.figure, tool_fig) + cid = self.canvas.mpl_connect( + "close_event", lambda e: manager.destroy()) + + def on_tool_fig_close(e): + self.canvas.mpl_disconnect(cid) + del self.subplot_tool + + tool_fig.canvas.mpl_connect("close_event", on_tool_fig_close) + manager.show() + return self.subplot_tool + + def save_figure(self, *args): + """ + Save the current figure. + + Backend implementations may choose to return + the absolute path of the saved file, if any, as + a string. + + If no file is created then `None` is returned. + + If the backend does not implement this functionality + then `NavigationToolbar2.UNKNOWN_SAVED_STATUS` is returned. + + Returns + ------- + str or `NavigationToolbar2.UNKNOWN_SAVED_STATUS` or `None` + The filepath of the saved figure. + Returns `None` if figure is not saved. + Returns `NavigationToolbar2.UNKNOWN_SAVED_STATUS` when + the backend does not provide the information. + """ + raise NotImplementedError + + def update(self): + """Reset the Axes stack.""" + self._nav_stack.clear() + self.set_history_buttons() + + def set_history_buttons(self): + """Enable or disable the back/forward button.""" + + +class ToolContainerBase: + """ + Base class for all tool containers, e.g. toolbars. + + Attributes + ---------- + toolmanager : `.ToolManager` + The tools with which this `ToolContainer` wants to communicate. + """ + + _icon_extension = '.png' + """ + Toolcontainer button icon image format extension + + **String**: Image extension + """ + + def __init__(self, toolmanager): + self.toolmanager = toolmanager + toolmanager.toolmanager_connect( + 'tool_message_event', + lambda event: self.set_message(event.message)) + toolmanager.toolmanager_connect( + 'tool_removed_event', + lambda event: self.remove_toolitem(event.tool.name)) + + def _tool_toggled_cbk(self, event): + """ + Capture the 'tool_trigger_[name]' + + This only gets used for toggled tools. + """ + self.toggle_toolitem(event.tool.name, event.tool.toggled) + + def add_tool(self, tool, group, position=-1): + """ + Add a tool to this container. + + Parameters + ---------- + tool : tool_like + The tool to add, see `.ToolManager.get_tool`. + group : str + The name of the group to add this tool to. + position : int, default: -1 + The position within the group to place this tool. + """ + tool = self.toolmanager.get_tool(tool) + image = self._get_image_filename(tool) + toggle = getattr(tool, 'toggled', None) is not None + self.add_toolitem(tool.name, group, position, + image, tool.description, toggle) + if toggle: + self.toolmanager.toolmanager_connect('tool_trigger_%s' % tool.name, + self._tool_toggled_cbk) + # If initially toggled + if tool.toggled: + self.toggle_toolitem(tool.name, True) + + def _get_image_filename(self, tool): + """Resolve a tool icon's filename.""" + if not tool.image: + return None + if os.path.isabs(tool.image): + filename = tool.image + else: + if "image" in getattr(tool, "__dict__", {}): + raise ValueError("If 'tool.image' is an instance variable, " + "it must be an absolute path") + for cls in type(tool).__mro__: + if "image" in vars(cls): + try: + src = inspect.getfile(cls) + break + except (OSError, TypeError): + raise ValueError("Failed to locate source file " + "where 'tool.image' is defined") from None + else: + raise ValueError("Failed to find parent class defining 'tool.image'") + filename = str(pathlib.Path(src).parent / tool.image) + for filename in [filename, filename + self._icon_extension]: + if os.path.isfile(filename): + return os.path.abspath(filename) + for fname in [ # Fallback; once deprecation elapses. + tool.image, + tool.image + self._icon_extension, + cbook._get_data_path("images", tool.image), + cbook._get_data_path("images", tool.image + self._icon_extension), + ]: + if os.path.isfile(fname): + _api.warn_deprecated( + "3.9", message=f"Loading icon {tool.image!r} from the current " + "directory or from Matplotlib's image directory. This behavior " + "is deprecated since %(since)s and will be removed in %(removal)s; " + "Tool.image should be set to a path relative to the Tool's source " + "file, or to an absolute path.") + return os.path.abspath(fname) + + def trigger_tool(self, name): + """ + Trigger the tool. + + Parameters + ---------- + name : str + Name (id) of the tool triggered from within the container. + """ + self.toolmanager.trigger_tool(name, sender=self) + + def add_toolitem(self, name, group, position, image, description, toggle): + """ + A hook to add a toolitem to the container. + + This hook must be implemented in each backend and contains the + backend-specific code to add an element to the toolbar. + + .. warning:: + This is part of the backend implementation and should + not be called by end-users. They should instead call + `.ToolContainerBase.add_tool`. + + The callback associated with the button click event + must be *exactly* ``self.trigger_tool(name)``. + + Parameters + ---------- + name : str + Name of the tool to add, this gets used as the tool's ID and as the + default label of the buttons. + group : str + Name of the group that this tool belongs to. + position : int + Position of the tool within its group, if -1 it goes at the end. + image : str + Filename of the image for the button or `None`. + description : str + Description of the tool, used for the tooltips. + toggle : bool + * `True` : The button is a toggle (change the pressed/unpressed + state between consecutive clicks). + * `False` : The button is a normal button (returns to unpressed + state after release). + """ + raise NotImplementedError + + def toggle_toolitem(self, name, toggled): + """ + A hook to toggle a toolitem without firing an event. + + This hook must be implemented in each backend and contains the + backend-specific code to silently toggle a toolbar element. + + .. warning:: + This is part of the backend implementation and should + not be called by end-users. They should instead call + `.ToolManager.trigger_tool` or `.ToolContainerBase.trigger_tool` + (which are equivalent). + + Parameters + ---------- + name : str + Id of the tool to toggle. + toggled : bool + Whether to set this tool as toggled or not. + """ + raise NotImplementedError + + def remove_toolitem(self, name): + """ + A hook to remove a toolitem from the container. + + This hook must be implemented in each backend and contains the + backend-specific code to remove an element from the toolbar; it is + called when `.ToolManager` emits a ``tool_removed_event``. + + Because some tools are present only on the `.ToolManager` but not on + the `ToolContainer`, this method must be a no-op when called on a tool + absent from the container. + + .. warning:: + This is part of the backend implementation and should + not be called by end-users. They should instead call + `.ToolManager.remove_tool`. + + Parameters + ---------- + name : str + Name of the tool to remove. + """ + raise NotImplementedError + + def set_message(self, s): + """ + Display a message on the toolbar. + + Parameters + ---------- + s : str + Message text. + """ + raise NotImplementedError + + +class _Backend: + # A backend can be defined by using the following pattern: + # + # @_Backend.export + # class FooBackend(_Backend): + # # override the attributes and methods documented below. + + # `backend_version` may be overridden by the subclass. + backend_version = "unknown" + + # The `FigureCanvas` class must be defined. + FigureCanvas = None + + # For interactive backends, the `FigureManager` class must be overridden. + FigureManager = FigureManagerBase + + # For interactive backends, `mainloop` should be a function taking no + # argument and starting the backend main loop. It should be left as None + # for non-interactive backends. + mainloop = None + + # The following methods will be automatically defined and exported, but + # can be overridden. + + @classmethod + def new_figure_manager(cls, num, *args, **kwargs): + """Create a new figure manager instance.""" + # This import needs to happen here due to circular imports. + from matplotlib.figure import Figure + fig_cls = kwargs.pop('FigureClass', Figure) + fig = fig_cls(*args, **kwargs) + return cls.new_figure_manager_given_figure(num, fig) + + @classmethod + def new_figure_manager_given_figure(cls, num, figure): + """Create a new figure manager instance for the given figure.""" + return cls.FigureCanvas.new_manager(figure, num) + + @classmethod + def draw_if_interactive(cls): + manager_class = cls.FigureCanvas.manager_class + # Interactive backends reimplement start_main_loop or pyplot_show. + backend_is_interactive = ( + manager_class.start_main_loop != FigureManagerBase.start_main_loop + or manager_class.pyplot_show != FigureManagerBase.pyplot_show) + if backend_is_interactive and is_interactive(): + manager = Gcf.get_active() + if manager: + manager.canvas.draw_idle() + + @classmethod + def show(cls, *, block=None): + """ + Show all figures. + + `show` blocks by calling `mainloop` if *block* is ``True``, or if it is + ``None`` and we are not in `interactive` mode and if IPython's + ``%matplotlib`` integration has not been activated. + """ + managers = Gcf.get_all_fig_managers() + if not managers: + return + for manager in managers: + try: + manager.show() # Emits a warning for non-interactive backend. + except NonGuiException as exc: + _api.warn_external(str(exc)) + if cls.mainloop is None: + return + if block is None: + # Hack: Is IPython's %matplotlib integration activated? If so, + # IPython's activate_matplotlib (>= 0.10) tacks a _needmain + # attribute onto pyplot.show (always set to False). + pyplot_show = getattr(sys.modules.get("matplotlib.pyplot"), "show", None) + ipython_pylab = hasattr(pyplot_show, "_needmain") + block = not ipython_pylab and not is_interactive() + if block: + cls.mainloop() + + # This method is the one actually exporting the required methods. + + @staticmethod + def export(cls): + for name in [ + "backend_version", + "FigureCanvas", + "FigureManager", + "new_figure_manager", + "new_figure_manager_given_figure", + "draw_if_interactive", + "show", + ]: + setattr(sys.modules[cls.__module__], name, getattr(cls, name)) + + # For back-compatibility, generate a shim `Show` class. + + class Show(ShowBase): + def mainloop(self): + return cls.mainloop() + + setattr(sys.modules[cls.__module__], "Show", Show) + return cls + + +class ShowBase(_Backend): + """ + Simple base class to generate a ``show()`` function in backends. + + Subclass must override ``mainloop()`` method. + """ + + def __call__(self, block=None): + return self.show(block=block)