diff --git a/pyproject.toml b/pyproject.toml index 4409cce6..98677da1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools == 75.1.0", # >= 65.0.0", +requires = ["setuptools == 75.1.0", "wheel", "versioningit",] build-backend = "setuptools.build_meta" @@ -22,7 +22,7 @@ classifiers = [ dependencies = [ "ipython", "numpy", - "matplotlib>=3.8.4", + "matplotlib>=3.9.2", "six", "qtawesome", "pre-commit", diff --git a/src/mslice/plotting/plot_window/cut_plot.py b/src/mslice/plotting/plot_window/cut_plot.py index 3eb16f08..7795784a 100644 --- a/src/mslice/plotting/plot_window/cut_plot.py +++ b/src/mslice/plotting/plot_window/cut_plot.py @@ -466,8 +466,8 @@ def _apply_offset(self, x, y): for line in line_containers.get_children(): line not in self._waterfall_cache and self._cache_line(line) if isinstance(line, Line2D): - line.set_xdata(self._waterfall_cache[line][0] + ind * x) - line.set_ydata(self._waterfall_cache[line][1] + ind * y) + line.set_xdata([self._waterfall_cache[line][0] + ind * x]) + line.set_ydata([self._waterfall_cache[line][1] + ind * y]) elif isinstance(line, LineCollection): for index, path in enumerate(line._paths): if not np.isnan(path.vertices).any(): diff --git a/src/mslice/plotting/pyplot.py b/src/mslice/plotting/pyplot.py index 4133e96e..f75d3c7d 100644 --- a/src/mslice/plotting/pyplot.py +++ b/src/mslice/plotting/pyplot.py @@ -7,6 +7,38 @@ Most functions are identical to matplotlib.pyplot. The differences come around due to the requirement that cuts and slices be treated differently, see globalfiguremanager.py. + +`matplotlib.pyplot` is a state-based interface to matplotlib. It provides +an implicit, MATLAB-like, way of plotting. It also opens figures on your +screen, and acts as the figure GUI manager. + +pyplot is mainly intended for interactive plots and simple cases of +programmatic plot generation:: + + import numpy as np + import matplotlib.pyplot as plt + + x = np.arange(0, 5, 0.1) + y = np.sin(x) + plt.plot(x, y) + +The explicit object-oriented API is recommended for complex plots, though +pyplot is still usually used to create the figure and often the Axes in the +figure. See `.pyplot.figure`, `.pyplot.subplots`, and +`.pyplot.subplot_mosaic` to create figures, and +:doc:`Axes API ` for the plotting methods on an Axes:: + + import numpy as np + import matplotlib.pyplot as plt + + x = np.arange(0, 5, 0.1) + y = np.sin(x) + fig, ax = plt.subplots() + ax.plot(x, y) + + +See :ref:`api_interfaces` for an explanation of the tradeoffs between the +implicit and explicit interfaces. """ # fmt: off @@ -19,20 +51,20 @@ import importlib import inspect import logging -import re import sys import threading import time -from typing import cast, overload +from typing import TYPE_CHECKING, cast, overload -from cycler import cycler +from cycler import cycler # noqa: F401 import matplotlib import matplotlib.colorbar import matplotlib.image from matplotlib import _api -from matplotlib import ( # Re-exported for typing. +from matplotlib import ( # noqa: F401 Re-exported for typing. cm as cm, get_backend as get_backend, rcParams as rcParams, style as style) -from matplotlib import _pylab_helpers, interactive +from matplotlib import _pylab_helpers +from matplotlib import interactive # noqa: F401 from matplotlib import cbook from matplotlib import _docstring from matplotlib.backend_bases import ( @@ -41,19 +73,18 @@ from matplotlib.gridspec import GridSpec, SubplotSpec from matplotlib import rcsetup, rcParamsDefault, rcParamsOrig from matplotlib.artist import Artist -from matplotlib.axes import Axes, Subplot # type: ignore -from matplotlib.projections import PolarAxes # type: ignore +from matplotlib.axes import Axes +from matplotlib.axes import Subplot # noqa: F401 +from matplotlib.backends import BackendFilter, backend_registry +from matplotlib.projections import PolarAxes from matplotlib import mlab # for detrend_none, window_hanning -from matplotlib.scale import get_scale_names +from matplotlib.scale import get_scale_names # noqa: F401 from matplotlib.cm import _colormaps -from matplotlib.cm import register_cmap # type: ignore -from matplotlib.colors import _color_sequences +from matplotlib.colors import _color_sequences, Colormap import numpy as np -from typing import TYPE_CHECKING, cast - if TYPE_CHECKING: from collections.abc import Callable, Hashable, Iterable, Sequence import datetime @@ -65,6 +96,9 @@ import PIL.Image from numpy.typing import ArrayLike + import matplotlib.axes + import matplotlib.artist + import matplotlib.backend_bases from matplotlib.axis import Tick from matplotlib.axes._base import _AxesBase from matplotlib.backend_bases import RendererBase, Event @@ -73,14 +107,12 @@ from matplotlib.collections import ( Collection, LineCollection, - BrokenBarHCollection, PolyCollection, PathCollection, EventCollection, QuadMesh, ) from matplotlib.colorbar import Colorbar - from matplotlib.colors import Colormap from matplotlib.container import ( BarContainer, ErrorbarContainer, @@ -106,10 +138,11 @@ from matplotlib.colors import Normalize from matplotlib.lines import Line2D, AxLine from matplotlib.text import Text, Annotation -from matplotlib.patches import Polygon, Rectangle, Circle, Arrow -from matplotlib.widgets import Button, Slider, Widget +from matplotlib.patches import Arrow, Circle, Rectangle # noqa: F401 +from matplotlib.patches import Polygon +from matplotlib.widgets import Button, Slider, Widget # noqa: F401 -from matplotlib.ticker import ( +from matplotlib.ticker import ( # noqa: F401 TickHelper, Formatter, FixedFormatter, NullFormatter, FuncFormatter, FormatStrFormatter, ScalarFormatter, LogFormatter, LogFormatterExponent, LogFormatterMathtext, Locator, IndexLocator, FixedLocator, NullLocator, @@ -162,12 +195,87 @@ def _copy_docstring_and_deprecators( method = method.__wrapped__ for decorator in decorators[::-1]: func = decorator(func) + _add_pyplot_note(func, method) return func +_NO_PYPLOT_NOTE = [ + 'FigureBase._gci', # wrapped_func is private + '_AxesBase._sci', # wrapped_func is private + 'Artist.findobj', # not a standard pyplot wrapper because it does not operate + # on the current Figure / Axes. Explanation of relation would + # be more complex and is not too important. +] + + +def _add_pyplot_note(func, wrapped_func): + """ + Add a note to the docstring of *func* that it is a pyplot wrapper. + + The note is added to the "Notes" section of the docstring. If that does + not exist, a "Notes" section is created. In numpydoc, the "Notes" + section is the third last possible section, only potentially followed by + "References" and "Examples". + """ + if not func.__doc__: + return # nothing to do + + qualname = wrapped_func.__qualname__ + if qualname in _NO_PYPLOT_NOTE: + return + + wrapped_func_is_method = True + if "." not in qualname: + # method qualnames are prefixed by the class and ".", e.g. "Axes.plot" + wrapped_func_is_method = False + link = f"{wrapped_func.__module__}.{qualname}" + elif qualname.startswith("Axes."): # e.g. "Axes.plot" + link = ".axes." + qualname + elif qualname.startswith("_AxesBase."): # e.g. "_AxesBase.set_xlabel" + link = ".axes.Axes" + qualname[9:] + elif qualname.startswith("Figure."): # e.g. "Figure.figimage" + link = "." + qualname + elif qualname.startswith("FigureBase."): # e.g. "FigureBase.gca" + link = ".Figure" + qualname[10:] + elif qualname.startswith("FigureCanvasBase."): # "FigureBaseCanvas.mpl_connect" + link = "." + qualname + else: + raise RuntimeError(f"Wrapped method from unexpected class: {qualname}") + + if wrapped_func_is_method: + message = f"This is the :ref:`pyplot wrapper ` for `{link}`." + else: + message = f"This is equivalent to `{link}`." + + # Find the correct insert position: + # - either we already have a "Notes" section into which we can insert + # - or we create one before the next present section. Note that in numpydoc, the + # "Notes" section is the third last possible section, only potentially followed + # by "References" and "Examples". + # - or we append a new "Notes" section at the end. + doc = inspect.cleandoc(func.__doc__) + if "\nNotes\n-----" in doc: + before, after = doc.split("\nNotes\n-----", 1) + elif (index := doc.find("\nReferences\n----------")) != -1: + before, after = doc[:index], doc[index:] + elif (index := doc.find("\nExamples\n--------")) != -1: + before, after = doc[:index], doc[index:] + else: + # No "Notes", "References", or "Examples" --> append to the end. + before = doc + "\n" + after = "" + + func.__doc__ = f"{before}\nNotes\n-----\n\n.. note::\n\n {message}\n{after}" + + ## Global ## +# The state controlled by {,un}install_repl_displayhook(). +_ReplDisplayHook = Enum("_ReplDisplayHook", ["NONE", "PLAIN", "IPYTHON"]) +_REPL_DISPLAYHOOK = _ReplDisplayHook.NONE + + def _draw_all_if_interactive() -> None: # We will always draw because mslice might be running without # matplotlib interactive @@ -180,11 +288,6 @@ def draw_all() -> None: fig.canvas.draw_idle() -# The state controlled by {,un}install_repl_displayhook(). -_ReplDisplayHook = Enum("_ReplDisplayHook", ["NONE", "PLAIN", "IPYTHON"]) -_REPL_DISPLAYHOOK = _ReplDisplayHook.NONE - - def install_repl_displayhook() -> None: """ Connect to the display hook of the current shell. @@ -215,10 +318,16 @@ def install_repl_displayhook() -> None: ip.events.register("post_execute", _draw_all_if_interactive) _REPL_DISPLAYHOOK = _ReplDisplayHook.IPYTHON - from IPython.core.pylabtools import backend2gui # type: ignore - + if mod_ipython.version_info[:2] < (8, 24): + # Use of backend2gui is not needed for IPython >= 8.24 as that functionality + # has been moved to Matplotlib. + # This code can be removed when Python 3.12, the latest version supported by + # IPython < 8.24, reaches end-of-life in late 2028. + from IPython.core.pylabtools import backend2gui + ipython_gui_name = backend2gui.get(get_backend()) + else: + _, ipython_gui_name = backend_registry.resolve_backend(get_backend()) # trigger IPython's eventloop integration, if available - ipython_gui_name = backend2gui.get(get_backend()) if ipython_gui_name: ip.enable_gui(ipython_gui_name) @@ -227,12 +336,12 @@ def uninstall_repl_displayhook() -> None: """Disconnect from the display hook of the current shell.""" global _REPL_DISPLAYHOOK if _REPL_DISPLAYHOOK is _ReplDisplayHook.IPYTHON: - from IPython import get_ipython # type: ignore - + from IPython import get_ipython ip = get_ipython() ip.events.unregister("post_execute", _draw_all_if_interactive) _REPL_DISPLAYHOOK = _ReplDisplayHook.NONE + # Ensure this appears in the pyplot docs. @_copy_docstring_and_deprecators(matplotlib.set_loglevel) def set_loglevel(*args, **kwargs) -> None: @@ -263,7 +372,7 @@ def _get_backend_mod() -> type[matplotlib.backend_bases._Backend]: # Use rcParams._get("backend") to avoid going through the fallback # logic (which will (re)import pyplot and then call switch_backend if # we need to resolve the auto sentinel) - switch_backend(rcParams._get("backend")) # type: ignore[attr-defined] + switch_backend(rcParams._get("backend")) return cast(type[matplotlib.backend_bases._Backend], _backend_mod) @@ -290,16 +399,11 @@ def switch_backend(newbackend: str) -> None: if newbackend is rcsetup._auto_backend_sentinel: current_framework = cbook._get_running_interactive_framework() - mapping = {'qt': 'qtagg', - 'gtk3': 'gtk3agg', - 'gtk4': 'gtk4agg', - 'wx': 'wxagg', - 'tk': 'tkagg', - 'macosx': 'macosx', - 'headless': 'agg'} - - if current_framework in mapping: - candidates = [mapping[current_framework]] + + if (current_framework and + (backend := backend_registry.backend_for_gui_framework( + current_framework))): + candidates = [backend] else: candidates = [] candidates += [ @@ -325,7 +429,7 @@ def switch_backend(newbackend: str) -> None: # have to escape the switch on access logic old_backend = dict.__getitem__(rcParams, 'backend') - module = importlib.import_module(cbook._backend_module_name(newbackend)) + module = backend_registry.load_backend_module(newbackend) canvas_class = module.FigureCanvas required_framework = canvas_class.required_interactive_framework @@ -400,6 +504,18 @@ def draw_if_interactive() -> None: _log.debug("Loaded backend %s version %s.", newbackend, backend_mod.backend_version) + if newbackend in ("ipympl", "widget"): + # ipympl < 0.9.4 expects rcParams["backend"] to be the fully-qualified backend + # name "module://ipympl.backend_nbagg" not short names "ipympl" or "widget". + import importlib.metadata as im + from matplotlib import _parse_to_version_info # type: ignore[attr-defined] + try: + module_version = im.version("ipympl") + if _parse_to_version_info(module_version) < (0, 9, 4): + newbackend = "module://ipympl.backend_nbagg" + except im.PackageNotFoundError: + pass + rcParams['backend'] = rcParamsDefault['backend'] = newbackend _backend_mod = backend_mod for func_name in ["new_figure_manager", "draw_if_interactive", "show"]: @@ -512,7 +628,6 @@ def show(*args, **kwargs) -> None: the end of every cell by default. Thus, you usually don't have to call it explicitly there. """ - #return gcf().show(*args, **kwargs) _warn_if_gui_out_of_main_thread() return _get_backend_mod().show(*args, **kwargs) @@ -547,7 +662,11 @@ def isinteractive() -> bool: return matplotlib.is_interactive() -def ioff() -> ExitStack: +# Note: The return type of ioff being AbstractContextManager +# instead of ExitStack is deliberate. +# See https://github.com/matplotlib/matplotlib/issues/27659 +# and https://github.com/matplotlib/matplotlib/pull/27667 for more info. +def ioff() -> AbstractContextManager: """ Disable interactive mode. @@ -577,7 +696,7 @@ def ioff() -> ExitStack: # ... To enable optional usage as a context manager, this function returns a - `~contextlib.ExitStack` object, which is not intended to be stored or + context manager object, which is not intended to be stored or accessed by the user. """ stack = ExitStack() @@ -587,7 +706,11 @@ def ioff() -> ExitStack: return stack -def ion() -> ExitStack: +# Note: The return type of ion being AbstractContextManager +# instead of ExitStack is deliberate. +# See https://github.com/matplotlib/matplotlib/issues/27659 +# and https://github.com/matplotlib/matplotlib/pull/27667 for more info. +def ion() -> AbstractContextManager: """ Enable interactive mode. @@ -617,7 +740,7 @@ def ion() -> ExitStack: # ... To enable optional usage as a context manager, this function returns a - `~contextlib.ExitStack` object, which is not intended to be stored or + context manager object, which is not intended to be stored or accessed by the user. """ stack = ExitStack() @@ -644,7 +767,7 @@ def pause(interval: float) -> None: matplotlib.animation : Proper animations show : Show all figures and optional block until all figures are closed. """ - manager = GlobalFigureManager.get_active() + manager = _pylab_helpers.Gcf.get_active() if manager is not None: canvas = manager.canvas if canvas.figure.stale: @@ -737,7 +860,7 @@ def xkcd( "xkcd mode is not compatible with text.usetex = True") stack = ExitStack() - stack.callback(dict.update, rcParams, rcParams.copy()) # type: ignore + stack.callback(dict.update, rcParams, rcParams.copy()) # type: ignore[arg-type] from matplotlib import patheffects rcParams.update({ @@ -764,7 +887,6 @@ def xkcd( ## Figures ## - def figure( # autoincrement if None, else integer from 1-N num: int | str | Figure | SubFigure | None = None, @@ -808,10 +930,10 @@ def figure( dpi : float, default: :rc:`figure.dpi` The resolution of the figure in dots-per-inch. - facecolor : color, default: :rc:`figure.facecolor` + facecolor : :mpltype:`color`, default: :rc:`figure.facecolor` The background color. - edgecolor : color, default: :rc:`figure.edgecolor` + edgecolor : :mpltype:`color`, default: :rc:`figure.edgecolor` The border color. frameon : bool, default: True @@ -830,8 +952,8 @@ def figure( overlapping Axes decorations (labels, ticks, etc). Note that layout managers can measurably slow down figure display. - - 'constrained': The constrained layout solver adjusts axes sizes - to avoid overlapping axes decorations. Can handle complex plot + - 'constrained': The constrained layout solver adjusts Axes sizes + to avoid overlapping Axes decorations. Can handle complex plot layouts and colorbars, and is thus recommended. See :ref:`constrainedlayout_guide` @@ -839,7 +961,7 @@ def figure( - 'compressed': uses the same algorithm as 'constrained', but removes extra space between fixed-aspect-ratio Axes. Best for - simple grids of axes. + simple grids of Axes. - 'tight': Use the tight layout mechanism. This is a relatively simple algorithm that adjusts the subplot parameters so that @@ -883,7 +1005,6 @@ def figure( `~matplotlib.rcParams` defines the default values, which can be modified in the matplotlibrc file. """ - return GlobalFigureManager.get_figure_number(num).figure @@ -1011,6 +1132,11 @@ def draw() -> None: This is equivalent to calling ``fig.canvas.draw_idle()``, where ``fig`` is the current figure. + + See Also + -------- + .FigureCanvasBase.draw_idle + .FigureCanvasBase.draw """ get_current_fig_manager().canvas.draw_idle() @@ -1039,7 +1165,6 @@ def figlegend(*args, **kwargs) -> Legend: ## Axes ## - @_docstring.dedent_interpd def axes( arg: None | tuple[float, float, float, float] = None, @@ -1085,7 +1210,7 @@ def axes( Returns ------- `~.axes.Axes`, or a subclass of `~.axes.Axes` - The returned axes class depends on the projection used. It is + The returned Axes class depends on the projection used. It is `~.axes.Axes` if rectilinear projection is used and `.projections.polar.PolarAxes` if polar projection is used. @@ -1132,7 +1257,7 @@ def axes( def delaxes(ax: matplotlib.axes.Axes | None = None) -> None: """ - Remove an `~.axes.Axes` (defaulting to the current axes) from its figure. + Remove an `~.axes.Axes` (defaulting to the current Axes) from its figure. """ if ax is None: ax = gca() @@ -1151,12 +1276,12 @@ def sca(ax: Axes) -> None: def cla() -> None: - """Clear the current axes.""" + """Clear the current Axes.""" # Not generated via boilerplate.py to allow a different docstring. return gca().cla() -## More ways of creating axes ## +## More ways of creating Axes ## @_docstring.dedent_interpd def subplot(*args, **kwargs) -> Axes: @@ -1203,10 +1328,10 @@ def subplot(*args, **kwargs) -> Axes: sharex, sharey : `~matplotlib.axes.Axes`, optional Share the x or y `~matplotlib.axis` with sharex and/or sharey. The axis will have the same limits, ticks, and scale as the axis of the - shared axes. + shared Axes. label : str - A label for the returned axes. + A label for the returned Axes. Returns ------- @@ -1219,7 +1344,7 @@ def subplot(*args, **kwargs) -> Axes: Other Parameters ---------------- **kwargs - This method also takes the keyword arguments for the returned axes + This method also takes the keyword arguments for the returned Axes base class; except for the *figure* argument. The keyword arguments for the rectilinear base class `~.axes.Axes` can be found in the following table but there might also be other keyword @@ -1237,7 +1362,7 @@ def subplot(*args, **kwargs) -> Axes: plt.plot([1, 2, 3]) # now create a subplot which represents the top plot of a grid # with 2 rows and 1 column. Since this subplot will overlap the - # first, the plot (and its axes) previously created, will be removed + # first, the plot (and its Axes) previously created, will be removed plt.subplot(211) If you do not want this behavior, use the `.Figure.add_subplot` method @@ -1288,7 +1413,7 @@ def subplot(*args, **kwargs) -> Axes: # add ax2 to the figure again plt.subplot(ax2) - # make the first axes "current" again + # make the first Axes "current" again plt.subplot(221) """ @@ -1330,8 +1455,8 @@ def subplot(*args, **kwargs) -> Axes: key = SubplotSpec._from_subplot_args(fig, args) for ax in fig.axes: - # If we found an Axes at the position, we can re-use it if the user passed no - # kwargs or if the axes class and kwargs are identical. + # If we found an Axes at the position, we can reuse it if the user passed no + # kwargs or if the Axes class and kwargs are identical. if (ax.get_subplotspec() == key and (kwargs == {} or (ax._projection_init @@ -1346,6 +1471,57 @@ def subplot(*args, **kwargs) -> Axes: return ax +@overload +def subplots( + nrows: Literal[1] = ..., + ncols: Literal[1] = ..., + *, + sharex: bool | Literal["none", "all", "row", "col"] = ..., + sharey: bool | Literal["none", "all", "row", "col"] = ..., + squeeze: Literal[True] = ..., + width_ratios: Sequence[float] | None = ..., + height_ratios: Sequence[float] | None = ..., + subplot_kw: dict[str, Any] | None = ..., + gridspec_kw: dict[str, Any] | None = ..., + **fig_kw +) -> tuple[Figure, Axes]: + ... + + +@overload +def subplots( + nrows: int = ..., + ncols: int = ..., + *, + sharex: bool | Literal["none", "all", "row", "col"] = ..., + sharey: bool | Literal["none", "all", "row", "col"] = ..., + squeeze: Literal[False], + width_ratios: Sequence[float] | None = ..., + height_ratios: Sequence[float] | None = ..., + subplot_kw: dict[str, Any] | None = ..., + gridspec_kw: dict[str, Any] | None = ..., + **fig_kw +) -> tuple[Figure, np.ndarray]: # TODO numpy/numpy#24738 + ... + + +@overload +def subplots( + nrows: int = ..., + ncols: int = ..., + *, + sharex: bool | Literal["none", "all", "row", "col"] = ..., + sharey: bool | Literal["none", "all", "row", "col"] = ..., + squeeze: bool = ..., + width_ratios: Sequence[float] | None = ..., + height_ratios: Sequence[float] | None = ..., + subplot_kw: dict[str, Any] | None = ..., + gridspec_kw: dict[str, Any] | None = ..., + **fig_kw +) -> tuple[Figure, Any]: + ... + + def subplots( nrows: int = 1, ncols: int = 1, *, sharex: bool | Literal["none", "all", "row", "col"] = False, @@ -1384,8 +1560,9 @@ def subplots( on, use `~matplotlib.axes.Axes.tick_params`. When subplots have a shared axis that has units, calling - `~matplotlib.axis.Axis.set_units` will update each axis with the - new units. + `.Axis.set_units` will update each axis with the new units. + + Note that it is not possible to unshare axes. squeeze : bool, default: True - If True, extra dimensions are squeezed out from the returned @@ -1478,7 +1655,7 @@ def subplots( ax1.set_title('Sharing Y axis') ax2.scatter(x, y) - # Create four polar axes and access them through the returned array + # Create four polar Axes and access them through the returned array fig, axs = plt.subplots(2, 2, subplot_kw=dict(projection="polar")) axs[0, 0].plot(x, y) axs[1, 1].scatter(x, y) @@ -1591,7 +1768,7 @@ def subplot_mosaic( x = [['A panel', 'A panel', 'edge'], ['C panel', '.', 'edge']] - produces 4 axes: + produces 4 Axes: - 'A panel' which is 1 row high and spans the first two columns - 'edge' which is 2 rows high and is on the right edge @@ -1669,7 +1846,7 @@ def subplot_mosaic( dict[label, Axes] A dictionary mapping the labels to the Axes objects. The order of - the axes is left-to-right and top-to-bottom of their position in the + the Axes is left-to-right and top-to-bottom of their position in the total layout. """ @@ -1739,8 +1916,8 @@ def subplot2grid( def twinx(ax: matplotlib.axes.Axes | None = None) -> _AxesBase: """ - Make and return a second axes that shares the *x*-axis. The new axes will - overlay *ax* (or the current axes if *ax* is *None*), and its ticks will be + Make and return a second Axes that shares the *x*-axis. The new Axes will + overlay *ax* (or the current Axes if *ax* is *None*), and its ticks will be on the right. Examples @@ -1755,8 +1932,8 @@ def twinx(ax: matplotlib.axes.Axes | None = None) -> _AxesBase: def twiny(ax: matplotlib.axes.Axes | None = None) -> _AxesBase: """ - Make and return a second axes that shares the *y*-axis. The new axes will - overlay *ax* (or the current axes if *ax* is *None*), and its ticks will be + Make and return a second Axes that shares the *y*-axis. The new Axes will + overlay *ax* (or the current Axes if *ax* is *None*), and its ticks will be on the top. Examples @@ -1800,7 +1977,7 @@ def subplot_tool(targetfig: Figure | None = None) -> SubplotTool | None: def box(on: bool | None = None) -> None: """ - Turn the axes box on or off on the current axes. + Turn the Axes box on or off on the current Axes. Parameters ---------- @@ -1818,13 +1995,12 @@ def box(on: bool | None = None) -> None: on = not ax.get_frame_on() ax.set_frame_on(on) - ## Axis ## def xlim(*args, **kwargs) -> tuple[float, float]: """ - Get or set the x limits of the current axes. + Get or set the x limits of the current Axes. Call signatures:: @@ -1848,9 +2024,9 @@ def xlim(*args, **kwargs) -> tuple[float, float]: Notes ----- Calling this function with no arguments (e.g. ``xlim()``) is the pyplot - equivalent of calling `~.Axes.get_xlim` on the current axes. + equivalent of calling `~.Axes.get_xlim` on the current Axes. Calling this function with arguments is the pyplot equivalent of calling - `~.Axes.set_xlim` on the current axes. All arguments are passed though. + `~.Axes.set_xlim` on the current Axes. All arguments are passed though. """ ax = gca() if not args and not kwargs: @@ -1861,7 +2037,7 @@ def xlim(*args, **kwargs) -> tuple[float, float]: def ylim(*args, **kwargs) -> tuple[float, float]: """ - Get or set the y-limits of the current axes. + Get or set the y-limits of the current Axes. Call signatures:: @@ -1885,9 +2061,9 @@ def ylim(*args, **kwargs) -> tuple[float, float]: Notes ----- Calling this function with no arguments (e.g. ``ylim()``) is the pyplot - equivalent of calling `~.Axes.get_ylim` on the current axes. + equivalent of calling `~.Axes.get_ylim` on the current Axes. Calling this function with arguments is the pyplot equivalent of calling - `~.Axes.set_ylim` on the current axes. All arguments are passed though. + `~.Axes.set_ylim` on the current Axes. All arguments are passed though. """ ax = gca() if not args and not kwargs: @@ -1921,6 +2097,21 @@ def xticks( **kwargs `.Text` properties can be used to control the appearance of the labels. + .. warning:: + + This only sets the properties of the current ticks, which is + only sufficient if you either pass *ticks*, resulting in a + fixed list of ticks, or if the plot is static. + + Ticks are not guaranteed to be persistent. Various operations + can create, delete and modify the Tick instances. There is an + imminent risk that these settings can get lost if you work on + the figure further (including also panning/zooming on a + displayed figure). + + Use `~.pyplot.tick_params` instead if possible. + + Returns ------- locs @@ -1932,9 +2123,9 @@ def xticks( ----- Calling this function with no arguments (e.g. ``xticks()``) is the pyplot equivalent of calling `~.Axes.get_xticks` and `~.Axes.get_xticklabels` on - the current axes. + the current Axes. Calling this function with arguments is the pyplot equivalent of calling - `~.Axes.set_xticks` and `~.Axes.set_xticklabels` on the current axes. + `~.Axes.set_xticks` and `~.Axes.set_xticklabels` on the current Axes. Examples -------- @@ -1947,23 +2138,24 @@ def xticks( """ ax = gca() + locs: list[Tick] | np.ndarray if ticks is None: locs = ax.get_xticks(minor=minor) if labels is not None: - raise TypeError( - "xticks(): Parameter 'labels' can't be set " "without setting 'ticks'" - ) + raise TypeError("xticks(): Parameter 'labels' can't be set " + "without setting 'ticks'") else: locs = ax.set_xticks(ticks, minor=minor) + labels_out: list[Text] = [] if labels is None: - labels = ax.get_xticklabels(minor=minor) - for l in labels: + labels_out = ax.get_xticklabels(minor=minor) + for l in labels_out: l._internal_update(kwargs) else: - labels = ax.set_xticklabels(labels, minor=minor, **kwargs) + labels_out = ax.set_xticklabels(labels, minor=minor, **kwargs) - return locs, labels + return locs, labels_out def yticks( @@ -1991,6 +2183,20 @@ def yticks( **kwargs `.Text` properties can be used to control the appearance of the labels. + .. warning:: + + This only sets the properties of the current ticks, which is + only sufficient if you either pass *ticks*, resulting in a + fixed list of ticks, or if the plot is static. + + Ticks are not guaranteed to be persistent. Various operations + can create, delete and modify the Tick instances. There is an + imminent risk that these settings can get lost if you work on + the figure further (including also panning/zooming on a + displayed figure). + + Use `~.pyplot.tick_params` instead if possible. + Returns ------- locs @@ -2002,9 +2208,9 @@ def yticks( ----- Calling this function with no arguments (e.g. ``yticks()``) is the pyplot equivalent of calling `~.Axes.get_yticks` and `~.Axes.get_yticklabels` on - the current axes. + the current Axes. Calling this function with arguments is the pyplot equivalent of calling - `~.Axes.set_yticks` and `~.Axes.set_yticklabels` on the current axes. + `~.Axes.set_yticks` and `~.Axes.set_yticklabels` on the current Axes. Examples -------- @@ -2017,23 +2223,24 @@ def yticks( """ ax = gca() + locs: list[Tick] | np.ndarray if ticks is None: locs = ax.get_yticks(minor=minor) if labels is not None: - raise TypeError( - "yticks(): Parameter 'labels' can't be set " "without setting 'ticks'" - ) + raise TypeError("yticks(): Parameter 'labels' can't be set " + "without setting 'ticks'") else: locs = ax.set_yticks(ticks, minor=minor) + labels_out: list[Text] = [] if labels is None: - labels = ax.get_yticklabels(minor=minor) - for l in labels: + labels_out = ax.get_yticklabels(minor=minor) + for l in labels_out: l._internal_update(kwargs) else: - labels = ax.set_yticklabels(labels, minor=minor, **kwargs) + labels_out = ax.set_yticklabels(labels, minor=minor, **kwargs) - return locs, labels + return locs, labels_out def rgrids( @@ -2103,15 +2310,16 @@ def rgrids( """ ax = gca() if not isinstance(ax, PolarAxes): - raise RuntimeError("rgrids only defined for polar axes") + raise RuntimeError('rgrids only defined for polar Axes') if all(p is None for p in [radii, labels, angle, fmt]) and not kwargs: - lines = ax.yaxis.get_gridlines() - labels = ax.yaxis.get_ticklabels() + lines_out: list[Line2D] = ax.yaxis.get_gridlines() + labels_out: list[Text] = ax.yaxis.get_ticklabels() + elif radii is None: + raise TypeError("'radii' cannot be None when other parameters are passed") else: - lines, labels = ax.set_rgrids( - radii, labels=labels, angle=angle, fmt=fmt, **kwargs - ) - return lines, labels + lines_out, labels_out = ax.set_rgrids( + radii, labels=labels, angle=angle, fmt=fmt, **kwargs) + return lines_out, labels_out def thetagrids( @@ -2177,13 +2385,17 @@ def thetagrids( """ ax = gca() if not isinstance(ax, PolarAxes): - raise RuntimeError("thetagrids only defined for polar axes") + raise RuntimeError('thetagrids only defined for polar Axes') if all(param is None for param in [angles, labels, fmt]) and not kwargs: - lines = ax.xaxis.get_ticklines() - labels = ax.xaxis.get_ticklabels() + lines_out: list[Line2D] = ax.xaxis.get_ticklines() + labels_out: list[Text] = ax.xaxis.get_ticklabels() + elif angles is None: + raise TypeError("'angles' cannot be None when other parameters are passed") else: - lines, labels = ax.set_thetagrids(angles, labels=labels, fmt=fmt, **kwargs) - return lines, labels + lines_out, labels_out = ax.set_thetagrids(angles, + labels=labels, fmt=fmt, + **kwargs) + return lines_out, labels_out @_api.deprecated("3.7", pending=True) @@ -2253,14 +2465,33 @@ def clim(vmin: float | None = None, vmax: float | None = None) -> None: im.set_clim(vmin, vmax) -# eventually this implementation should move here, use indirection for now to -# avoid having two copies of the code floating around. -def get_cmap( - name: Colormap | str | None = None, - lut: int | None = None -) -> Colormap: - return cm._get_cmap(name=name, lut=lut) # type: ignore -get_cmap.__doc__ = cm._get_cmap.__doc__ # type: ignore +def get_cmap(name: Colormap | str | None = None, lut: int | None = None) -> Colormap: + """ + Get a colormap instance, defaulting to rc values if *name* is None. + + Parameters + ---------- + name : `~matplotlib.colors.Colormap` or str or None, default: None + If a `.Colormap` instance, it will be returned. Otherwise, the name of + a colormap known to Matplotlib, which will be resampled by *lut*. The + default, None, means :rc:`image.cmap`. + lut : int or None, default: None + If *name* is not already a Colormap instance and *lut* is not None, the + colormap will be resampled to have *lut* entries in the lookup table. + + Returns + ------- + Colormap + """ + if name is None: + name = rcParams['image.cmap'] + if isinstance(name, Colormap): + return name + _api.check_in_list(sorted(_colormaps), name=name) + if lut is None: + return _colormaps[name] + else: + return _colormaps[name].resampled(lut) def set_cmap(cmap: Colormap | str) -> None: @@ -2275,8 +2506,7 @@ def set_cmap(cmap: Colormap | str) -> None: See Also -------- colormaps - matplotlib.cm.register_cmap - matplotlib.cm.get_cmap + get_cmap """ cmap = get_cmap(cmap) @@ -2303,12 +2533,20 @@ def imsave( def matshow(A: ArrayLike, fignum: None | int = None, **kwargs) -> AxesImage: """ - Display an array as a matrix in a new figure window. + Display a 2D array as a matrix in a new figure window. + + The origin is set at the upper left hand corner. + The indexing is ``(row, column)`` so that the first index runs vertically + and the second index runs horizontally in the figure: - The origin is set at the upper left hand corner and rows (first - dimension of the array) are displayed horizontally. The aspect - ratio of the figure window is that of the array, unless this would - make an excessively short or narrow figure. + .. code-block:: none + + A[0, 0] ⋯ A[0, M-1] + ⋮ ⋮ + A[N-1, 0] ⋯ A[N-1, M-1] + + The aspect ratio of the figure window is that of the array, + unless this would make an excessively short or narrow figure. Tick labels for the xaxis are placed on top. @@ -2376,14 +2614,14 @@ def polar(*args, **kwargs) -> list[Line2D]: # requested, ignore rcParams['backend'] and force selection of a backend that # is compatible with the current running interactive framework. if (rcParams["backend_fallback"] - and rcParams._get_backend_or_none() in ( # type: ignore - set(rcsetup.interactive_bk) - {'WebAgg', 'nbAgg'}) - and cbook._get_running_interactive_framework()): # type: ignore - rcParams._set("backend", rcsetup._auto_backend_sentinel) # type: ignore + and rcParams._get_backend_or_none() in ( # type: ignore[attr-defined] + set(backend_registry.list_builtin(BackendFilter.INTERACTIVE)) - + {'webagg', 'nbagg'}) + and cbook._get_running_interactive_framework()): + rcParams._set("backend", rcsetup._auto_backend_sentinel) # fmt: on - ################# REMAINING CONTENT GENERATED BY boilerplate.py ############## @@ -2599,7 +2837,7 @@ def axhline(y: float = 0, xmin: float = 0, xmax: float = 1, **kwargs) -> Line2D: @set_category(CATEGORY_CUT) def axhspan( ymin: float, ymax: float, xmin: float = 0, xmax: float = 1, **kwargs -) -> Polygon: +) -> Rectangle: return gca().axhspan(ymin, ymax, xmin=xmin, xmax=xmax, **kwargs) @@ -2641,7 +2879,7 @@ def axvline(x: float = 0, ymin: float = 0, ymax: float = 1, **kwargs) -> Line2D: @set_category(CATEGORY_CUT) def axvspan( xmin: float, xmax: float, ymin: float = 0, ymax: float = 1, **kwargs -) -> Polygon: +) -> Rectangle: return gca().axvspan(xmin, xmax, ymin=ymin, ymax=ymax, **kwargs) @@ -2743,7 +2981,7 @@ def boxplot( showbox: bool | None = None, showfliers: bool | None = None, boxprops: dict[str, Any] | None = None, - labels: Sequence[str] | None = None, + tick_labels: Sequence[str] | None = None, flierprops: dict[str, Any] | None = None, medianprops: dict[str, Any] | None = None, meanprops: dict[str, Any] | None = None, @@ -2753,6 +2991,7 @@ def boxplot( autorange: bool = False, zorder: float | None = None, capwidths: float | ArrayLike | None = None, + label: Sequence[str] | None = None, *, data=None, ) -> dict[str, Any]: @@ -2774,7 +3013,7 @@ def boxplot( showbox=showbox, showfliers=showfliers, boxprops=boxprops, - labels=labels, + tick_labels=tick_labels, flierprops=flierprops, medianprops=medianprops, meanprops=meanprops, @@ -2784,6 +3023,7 @@ def boxplot( autorange=autorange, zorder=zorder, capwidths=capwidths, + label=label, **({"data": data} if data is not None else {}), ) @@ -2797,7 +3037,7 @@ def broken_barh( *, data=None, **kwargs, -) -> BrokenBarHCollection: +) -> PolyCollection: return gca().broken_barh( xranges, yrange, **({"data": data} if data is not None else {}), **kwargs ) @@ -3732,12 +3972,15 @@ def specgram( # Autogenerated by boilerplate.py. Do not edit as changes will be lost. @_copy_docstring_and_deprecators(Axes.stackplot) @set_category(CATEGORY_CUT) -def stackplot(x, *args, labels=(), colors=None, baseline="zero", data=None, **kwargs): +def stackplot( + x, *args, labels=(), colors=None, hatch=None, baseline="zero", data=None, **kwargs +): return gca().stackplot( x, *args, labels=labels, colors=colors, + hatch=hatch, baseline=baseline, **({"data": data} if data is not None else {}), **kwargs, @@ -3899,7 +4142,7 @@ def tick_params(axis: Literal["both", "x", "y"] = "both", **kwargs) -> None: def ticklabel_format( *, axis: Literal["both", "x", "y"] = "both", - style: Literal["", "sci", "scientific", "plain"] = "", + style: Literal["", "sci", "scientific", "plain"] | None = None, scilimits: tuple[int, int] | None = None, useOffset: bool | float | None = None, useLocale: bool | None = None, @@ -3987,6 +4230,7 @@ def violinplot( bw_method: ( Literal["scott", "silverman"] | float | Callable[[GaussianKDE], float] | None ) = None, + side: Literal["both", "low", "high"] = "both", *, data=None, ) -> dict[str, Collection]: @@ -4001,6 +4245,7 @@ def violinplot( quantiles=quantiles, points=points, bw_method=bw_method, + side=side, **({"data": data} if data is not None else {}), ) @@ -4281,7 +4526,7 @@ def text( def ticklabel_format( *, axis: Literal["both", "x", "y"] = "both", - style: Literal["", "sci", "scientific", "plain"] = "", + style: Literal["", "sci", "scientific", "plain"] | None = None, scilimits: tuple[int, int] | None = None, useOffset: bool | float | None = None, useLocale: bool | None = None,