4725 lines
177 KiB
Python
4725 lines
177 KiB
Python
from collections.abc import Iterable, Sequence
|
||
from contextlib import ExitStack
|
||
import functools
|
||
import inspect
|
||
import logging
|
||
from numbers import Real
|
||
from operator import attrgetter
|
||
import re
|
||
import types
|
||
|
||
import numpy as np
|
||
|
||
import matplotlib as mpl
|
||
from matplotlib import _api, cbook, _docstring, offsetbox
|
||
import matplotlib.artist as martist
|
||
import matplotlib.axis as maxis
|
||
from matplotlib.cbook import _OrderedSet, _check_1d, index_of
|
||
import matplotlib.collections as mcoll
|
||
import matplotlib.colors as mcolors
|
||
import matplotlib.font_manager as font_manager
|
||
from matplotlib.gridspec import SubplotSpec
|
||
import matplotlib.image as mimage
|
||
import matplotlib.lines as mlines
|
||
import matplotlib.patches as mpatches
|
||
from matplotlib.rcsetup import cycler, validate_axisbelow
|
||
import matplotlib.spines as mspines
|
||
import matplotlib.table as mtable
|
||
import matplotlib.text as mtext
|
||
import matplotlib.ticker as mticker
|
||
import matplotlib.transforms as mtransforms
|
||
|
||
_log = logging.getLogger(__name__)
|
||
|
||
|
||
class _axis_method_wrapper:
|
||
"""
|
||
Helper to generate Axes methods wrapping Axis methods.
|
||
|
||
After ::
|
||
|
||
get_foo = _axis_method_wrapper("xaxis", "get_bar")
|
||
|
||
(in the body of a class) ``get_foo`` is a method that forwards it arguments
|
||
to the ``get_bar`` method of the ``xaxis`` attribute, and gets its
|
||
signature and docstring from ``Axis.get_bar``.
|
||
|
||
The docstring of ``get_foo`` is built by replacing "this Axis" by "the
|
||
{attr_name}" (i.e., "the xaxis", "the yaxis") in the wrapped method's
|
||
dedented docstring; additional replacements can be given in *doc_sub*.
|
||
"""
|
||
|
||
def __init__(self, attr_name, method_name, *, doc_sub=None):
|
||
self.attr_name = attr_name
|
||
self.method_name = method_name
|
||
# Immediately put the docstring in ``self.__doc__`` so that docstring
|
||
# manipulations within the class body work as expected.
|
||
doc = inspect.getdoc(getattr(maxis.Axis, method_name))
|
||
self._missing_subs = []
|
||
if doc:
|
||
doc_sub = {"this Axis": f"the {self.attr_name}", **(doc_sub or {})}
|
||
for k, v in doc_sub.items():
|
||
if k not in doc: # Delay raising error until we know qualname.
|
||
self._missing_subs.append(k)
|
||
doc = doc.replace(k, v)
|
||
self.__doc__ = doc
|
||
|
||
def __set_name__(self, owner, name):
|
||
# This is called at the end of the class body as
|
||
# ``self.__set_name__(cls, name_under_which_self_is_assigned)``; we
|
||
# rely on that to give the wrapper the correct __name__/__qualname__.
|
||
get_method = attrgetter(f"{self.attr_name}.{self.method_name}")
|
||
|
||
def wrapper(self, *args, **kwargs):
|
||
return get_method(self)(*args, **kwargs)
|
||
|
||
wrapper.__module__ = owner.__module__
|
||
wrapper.__name__ = name
|
||
wrapper.__qualname__ = f"{owner.__qualname__}.{name}"
|
||
wrapper.__doc__ = self.__doc__
|
||
# Manually copy the signature instead of using functools.wraps because
|
||
# displaying the Axis method source when asking for the Axes method
|
||
# source would be confusing.
|
||
wrapper.__signature__ = inspect.signature(
|
||
getattr(maxis.Axis, self.method_name))
|
||
|
||
if self._missing_subs:
|
||
raise ValueError(
|
||
"The definition of {} expected that the docstring of Axis.{} "
|
||
"contains {!r} as substrings".format(
|
||
wrapper.__qualname__, self.method_name,
|
||
", ".join(map(repr, self._missing_subs))))
|
||
|
||
setattr(owner, name, wrapper)
|
||
|
||
|
||
class _TransformedBoundsLocator:
|
||
"""
|
||
Axes locator for `.Axes.inset_axes` and similarly positioned Axes.
|
||
|
||
The locator is a callable object used in `.Axes.set_aspect` to compute the
|
||
Axes location depending on the renderer.
|
||
"""
|
||
|
||
def __init__(self, bounds, transform):
|
||
"""
|
||
*bounds* (a ``[l, b, w, h]`` rectangle) and *transform* together
|
||
specify the position of the inset Axes.
|
||
"""
|
||
self._bounds = bounds
|
||
self._transform = transform
|
||
|
||
def __call__(self, ax, renderer):
|
||
# Subtracting transSubfigure will typically rely on inverted(),
|
||
# freezing the transform; thus, this needs to be delayed until draw
|
||
# time as transSubfigure may otherwise change after this is evaluated.
|
||
return mtransforms.TransformedBbox(
|
||
mtransforms.Bbox.from_bounds(*self._bounds),
|
||
self._transform - ax.figure.transSubfigure)
|
||
|
||
|
||
def _process_plot_format(fmt, *, ambiguous_fmt_datakey=False):
|
||
"""
|
||
Convert a MATLAB style color/line style format string to a (*linestyle*,
|
||
*marker*, *color*) tuple.
|
||
|
||
Example format strings include:
|
||
|
||
* 'ko': black circles
|
||
* '.b': blue dots
|
||
* 'r--': red dashed lines
|
||
* 'C2--': the third color in the color cycle, dashed lines
|
||
|
||
The format is absolute in the sense that if a linestyle or marker is not
|
||
defined in *fmt*, there is no line or marker. This is expressed by
|
||
returning 'None' for the respective quantity.
|
||
|
||
See Also
|
||
--------
|
||
matplotlib.Line2D.lineStyles, matplotlib.colors.cnames
|
||
All possible styles and color format strings.
|
||
"""
|
||
|
||
linestyle = None
|
||
marker = None
|
||
color = None
|
||
|
||
# First check whether fmt is just a colorspec, but specifically exclude the
|
||
# grayscale string "1" (not "1.0"), which is interpreted as the tri_down
|
||
# marker "1". The grayscale string "0" could be unambiguously understood
|
||
# as a color (black) but also excluded for consistency.
|
||
if fmt not in ["0", "1"]:
|
||
try:
|
||
color = mcolors.to_rgba(fmt)
|
||
return linestyle, marker, color
|
||
except ValueError:
|
||
pass
|
||
|
||
errfmt = ("{!r} is neither a data key nor a valid format string ({})"
|
||
if ambiguous_fmt_datakey else
|
||
"{!r} is not a valid format string ({})")
|
||
|
||
i = 0
|
||
while i < len(fmt):
|
||
c = fmt[i]
|
||
if fmt[i:i+2] in mlines.lineStyles: # First, the two-char styles.
|
||
if linestyle is not None:
|
||
raise ValueError(errfmt.format(fmt, "two linestyle symbols"))
|
||
linestyle = fmt[i:i+2]
|
||
i += 2
|
||
elif c in mlines.lineStyles:
|
||
if linestyle is not None:
|
||
raise ValueError(errfmt.format(fmt, "two linestyle symbols"))
|
||
linestyle = c
|
||
i += 1
|
||
elif c in mlines.lineMarkers:
|
||
if marker is not None:
|
||
raise ValueError(errfmt.format(fmt, "two marker symbols"))
|
||
marker = c
|
||
i += 1
|
||
elif c in mcolors.get_named_colors_mapping():
|
||
if color is not None:
|
||
raise ValueError(errfmt.format(fmt, "two color symbols"))
|
||
color = c
|
||
i += 1
|
||
elif c == "C":
|
||
cn_color = re.match(r"C\d+", fmt[i:])
|
||
if not cn_color:
|
||
raise ValueError(errfmt.format(fmt, "'C' must be followed by a number"))
|
||
color = mcolors.to_rgba(cn_color[0])
|
||
i += len(cn_color[0])
|
||
else:
|
||
raise ValueError(errfmt.format(fmt, f"unrecognized character {c!r}"))
|
||
|
||
if linestyle is None and marker is None:
|
||
linestyle = mpl.rcParams['lines.linestyle']
|
||
if linestyle is None:
|
||
linestyle = 'None'
|
||
if marker is None:
|
||
marker = 'None'
|
||
|
||
return linestyle, marker, color
|
||
|
||
|
||
class _process_plot_var_args:
|
||
"""
|
||
Process variable length arguments to `~.Axes.plot`, to support ::
|
||
|
||
plot(t, s)
|
||
plot(t1, s1, t2, s2)
|
||
plot(t1, s1, 'ko', t2, s2)
|
||
plot(t1, s1, 'ko', t2, s2, 'r--', t3, e3)
|
||
|
||
an arbitrary number of *x*, *y*, *fmt* are allowed
|
||
"""
|
||
|
||
def __init__(self, command='plot'):
|
||
self.command = command
|
||
self.set_prop_cycle(None)
|
||
|
||
def set_prop_cycle(self, cycler):
|
||
if cycler is None:
|
||
cycler = mpl.rcParams['axes.prop_cycle']
|
||
self._idx = 0
|
||
self._cycler_items = [*cycler]
|
||
|
||
def __call__(self, axes, *args, data=None, **kwargs):
|
||
axes._process_unit_info(kwargs=kwargs)
|
||
|
||
for pos_only in "xy":
|
||
if pos_only in kwargs:
|
||
raise _api.kwarg_error(self.command, pos_only)
|
||
|
||
if not args:
|
||
return
|
||
|
||
if data is None: # Process dict views
|
||
args = [cbook.sanitize_sequence(a) for a in args]
|
||
else: # Process the 'data' kwarg.
|
||
replaced = [mpl._replacer(data, arg) for arg in args]
|
||
if len(args) == 1:
|
||
label_namer_idx = 0
|
||
elif len(args) == 2: # Can be x, y or y, c.
|
||
# Figure out what the second argument is.
|
||
# 1) If the second argument cannot be a format shorthand, the
|
||
# second argument is the label_namer.
|
||
# 2) Otherwise (it could have been a format shorthand),
|
||
# a) if we did perform a substitution, emit a warning, and
|
||
# use it as label_namer.
|
||
# b) otherwise, it is indeed a format shorthand; use the
|
||
# first argument as label_namer.
|
||
try:
|
||
_process_plot_format(args[1])
|
||
except ValueError: # case 1)
|
||
label_namer_idx = 1
|
||
else:
|
||
if replaced[1] is not args[1]: # case 2a)
|
||
_api.warn_external(
|
||
f"Second argument {args[1]!r} is ambiguous: could "
|
||
f"be a format string but is in 'data'; using as "
|
||
f"data. If it was intended as data, set the "
|
||
f"format string to an empty string to suppress "
|
||
f"this warning. If it was intended as a format "
|
||
f"string, explicitly pass the x-values as well. "
|
||
f"Alternatively, rename the entry in 'data'.",
|
||
RuntimeWarning)
|
||
label_namer_idx = 1
|
||
else: # case 2b)
|
||
label_namer_idx = 0
|
||
elif len(args) == 3:
|
||
label_namer_idx = 1
|
||
else:
|
||
raise ValueError(
|
||
"Using arbitrary long args with data is not supported due "
|
||
"to ambiguity of arguments; use multiple plotting calls "
|
||
"instead")
|
||
if kwargs.get("label") is None:
|
||
kwargs["label"] = mpl._label_from_arg(
|
||
replaced[label_namer_idx], args[label_namer_idx])
|
||
args = replaced
|
||
ambiguous_fmt_datakey = data is not None and len(args) == 2
|
||
|
||
if len(args) >= 4 and not cbook.is_scalar_or_string(
|
||
kwargs.get("label")):
|
||
raise ValueError("plot() with multiple groups of data (i.e., "
|
||
"pairs of x and y) does not support multiple "
|
||
"labels")
|
||
|
||
# Repeatedly grab (x, y) or (x, y, format) from the front of args and
|
||
# massage them into arguments to plot() or fill().
|
||
|
||
while args:
|
||
this, args = args[:2], args[2:]
|
||
if args and isinstance(args[0], str):
|
||
this += args[0],
|
||
args = args[1:]
|
||
yield from self._plot_args(
|
||
axes, this, kwargs, ambiguous_fmt_datakey=ambiguous_fmt_datakey)
|
||
|
||
def get_next_color(self):
|
||
"""Return the next color in the cycle."""
|
||
entry = self._cycler_items[self._idx]
|
||
if "color" in entry:
|
||
self._idx = (self._idx + 1) % len(self._cycler_items) # Advance cycler.
|
||
return entry["color"]
|
||
else:
|
||
return "k"
|
||
|
||
def _getdefaults(self, kw, ignore=frozenset()):
|
||
"""
|
||
If some keys in the property cycle (excluding those in the set
|
||
*ignore*) are absent or set to None in the dict *kw*, return a copy
|
||
of the next entry in the property cycle, excluding keys in *ignore*.
|
||
Otherwise, don't advance the property cycle, and return an empty dict.
|
||
"""
|
||
defaults = self._cycler_items[self._idx]
|
||
if any(kw.get(k, None) is None for k in {*defaults} - ignore):
|
||
self._idx = (self._idx + 1) % len(self._cycler_items) # Advance cycler.
|
||
# Return a new dict to avoid exposing _cycler_items entries to mutation.
|
||
return {k: v for k, v in defaults.items() if k not in ignore}
|
||
else:
|
||
return {}
|
||
|
||
def _setdefaults(self, defaults, kw):
|
||
"""
|
||
Add to the dict *kw* the entries in the dict *default* that are absent
|
||
or set to None in *kw*.
|
||
"""
|
||
for k in defaults:
|
||
if kw.get(k, None) is None:
|
||
kw[k] = defaults[k]
|
||
|
||
def _makeline(self, axes, x, y, kw, kwargs):
|
||
kw = {**kw, **kwargs} # Don't modify the original kw.
|
||
self._setdefaults(self._getdefaults(kw), kw)
|
||
seg = mlines.Line2D(x, y, **kw)
|
||
return seg, kw
|
||
|
||
def _makefill(self, axes, x, y, kw, kwargs):
|
||
# Polygon doesn't directly support unitized inputs.
|
||
x = axes.convert_xunits(x)
|
||
y = axes.convert_yunits(y)
|
||
|
||
kw = kw.copy() # Don't modify the original kw.
|
||
kwargs = kwargs.copy()
|
||
|
||
# Ignore 'marker'-related properties as they aren't Polygon
|
||
# properties, but they are Line2D properties, and so they are
|
||
# likely to appear in the default cycler construction.
|
||
# This is done here to the defaults dictionary as opposed to the
|
||
# other two dictionaries because we do want to capture when a
|
||
# *user* explicitly specifies a marker which should be an error.
|
||
# We also want to prevent advancing the cycler if there are no
|
||
# defaults needed after ignoring the given properties.
|
||
ignores = ({'marker', 'markersize', 'markeredgecolor',
|
||
'markerfacecolor', 'markeredgewidth'}
|
||
# Also ignore anything provided by *kwargs*.
|
||
| {k for k, v in kwargs.items() if v is not None})
|
||
|
||
# Only using the first dictionary to use as basis
|
||
# for getting defaults for back-compat reasons.
|
||
# Doing it with both seems to mess things up in
|
||
# various places (probably due to logic bugs elsewhere).
|
||
default_dict = self._getdefaults(kw, ignores)
|
||
self._setdefaults(default_dict, kw)
|
||
|
||
# Looks like we don't want "color" to be interpreted to
|
||
# mean both facecolor and edgecolor for some reason.
|
||
# So the "kw" dictionary is thrown out, and only its
|
||
# 'color' value is kept and translated as a 'facecolor'.
|
||
# This design should probably be revisited as it increases
|
||
# complexity.
|
||
facecolor = kw.get('color', None)
|
||
|
||
# Throw out 'color' as it is now handled as a facecolor
|
||
default_dict.pop('color', None)
|
||
|
||
# To get other properties set from the cycler
|
||
# modify the kwargs dictionary.
|
||
self._setdefaults(default_dict, kwargs)
|
||
|
||
seg = mpatches.Polygon(np.column_stack((x, y)),
|
||
facecolor=facecolor,
|
||
fill=kwargs.get('fill', True),
|
||
closed=kw['closed'])
|
||
seg.set(**kwargs)
|
||
return seg, kwargs
|
||
|
||
def _plot_args(self, axes, tup, kwargs, *,
|
||
return_kwargs=False, ambiguous_fmt_datakey=False):
|
||
"""
|
||
Process the arguments of ``plot([x], y, [fmt], **kwargs)`` calls.
|
||
|
||
This processes a single set of ([x], y, [fmt]) parameters; i.e. for
|
||
``plot(x, y, x2, y2)`` it will be called twice. Once for (x, y) and
|
||
once for (x2, y2).
|
||
|
||
x and y may be 2D and thus can still represent multiple datasets.
|
||
|
||
For multiple datasets, if the keyword argument *label* is a list, this
|
||
will unpack the list and assign the individual labels to the datasets.
|
||
|
||
Parameters
|
||
----------
|
||
tup : tuple
|
||
A tuple of the positional parameters. This can be one of
|
||
|
||
- (y,)
|
||
- (x, y)
|
||
- (y, fmt)
|
||
- (x, y, fmt)
|
||
|
||
kwargs : dict
|
||
The keyword arguments passed to ``plot()``.
|
||
|
||
return_kwargs : bool
|
||
Whether to also return the effective keyword arguments after label
|
||
unpacking as well.
|
||
|
||
ambiguous_fmt_datakey : bool
|
||
Whether the format string in *tup* could also have been a
|
||
misspelled data key.
|
||
|
||
Returns
|
||
-------
|
||
result
|
||
If *return_kwargs* is false, a list of Artists representing the
|
||
dataset(s).
|
||
If *return_kwargs* is true, a list of (Artist, effective_kwargs)
|
||
representing the dataset(s). See *return_kwargs*.
|
||
The Artist is either `.Line2D` (if called from ``plot()``) or
|
||
`.Polygon` otherwise.
|
||
"""
|
||
if len(tup) > 1 and isinstance(tup[-1], str):
|
||
# xy is tup with fmt stripped (could still be (y,) only)
|
||
*xy, fmt = tup
|
||
linestyle, marker, color = _process_plot_format(
|
||
fmt, ambiguous_fmt_datakey=ambiguous_fmt_datakey)
|
||
elif len(tup) == 3:
|
||
raise ValueError('third arg must be a format string')
|
||
else:
|
||
xy = tup
|
||
linestyle, marker, color = None, None, None
|
||
|
||
# Don't allow any None value; these would be up-converted to one
|
||
# element array of None which causes problems downstream.
|
||
if any(v is None for v in tup):
|
||
raise ValueError("x, y, and format string must not be None")
|
||
|
||
kw = {}
|
||
for prop_name, val in zip(('linestyle', 'marker', 'color'),
|
||
(linestyle, marker, color)):
|
||
if val is not None:
|
||
# check for conflicts between fmt and kwargs
|
||
if (fmt.lower() != 'none'
|
||
and prop_name in kwargs
|
||
and val != 'None'):
|
||
# Technically ``plot(x, y, 'o', ls='--')`` is a conflict
|
||
# because 'o' implicitly unsets the linestyle
|
||
# (linestyle='None').
|
||
# We'll gracefully not warn in this case because an
|
||
# explicit set via kwargs can be seen as intention to
|
||
# override an implicit unset.
|
||
# Note: We don't val.lower() != 'none' because val is not
|
||
# necessarily a string (can be a tuple for colors). This
|
||
# is safe, because *val* comes from _process_plot_format()
|
||
# which only returns 'None'.
|
||
_api.warn_external(
|
||
f"{prop_name} is redundantly defined by the "
|
||
f"'{prop_name}' keyword argument and the fmt string "
|
||
f'"{fmt}" (-> {prop_name}={val!r}). The keyword '
|
||
f"argument will take precedence.")
|
||
kw[prop_name] = val
|
||
|
||
if len(xy) == 2:
|
||
x = _check_1d(xy[0])
|
||
y = _check_1d(xy[1])
|
||
else:
|
||
x, y = index_of(xy[-1])
|
||
|
||
if axes.xaxis is not None:
|
||
axes.xaxis.update_units(x)
|
||
if axes.yaxis is not None:
|
||
axes.yaxis.update_units(y)
|
||
|
||
if x.shape[0] != y.shape[0]:
|
||
raise ValueError(f"x and y must have same first dimension, but "
|
||
f"have shapes {x.shape} and {y.shape}")
|
||
if x.ndim > 2 or y.ndim > 2:
|
||
raise ValueError(f"x and y can be no greater than 2D, but have "
|
||
f"shapes {x.shape} and {y.shape}")
|
||
if x.ndim == 1:
|
||
x = x[:, np.newaxis]
|
||
if y.ndim == 1:
|
||
y = y[:, np.newaxis]
|
||
|
||
if self.command == 'plot':
|
||
make_artist = self._makeline
|
||
else:
|
||
kw['closed'] = kwargs.get('closed', True)
|
||
make_artist = self._makefill
|
||
|
||
ncx, ncy = x.shape[1], y.shape[1]
|
||
if ncx > 1 and ncy > 1 and ncx != ncy:
|
||
raise ValueError(f"x has {ncx} columns but y has {ncy} columns")
|
||
if ncx == 0 or ncy == 0:
|
||
return []
|
||
|
||
label = kwargs.get('label')
|
||
n_datasets = max(ncx, ncy)
|
||
|
||
if cbook.is_scalar_or_string(label):
|
||
labels = [label] * n_datasets
|
||
elif len(label) == n_datasets:
|
||
labels = label
|
||
elif n_datasets == 1:
|
||
msg = (f'Passing label as a length {len(label)} sequence when '
|
||
'plotting a single dataset is deprecated in Matplotlib 3.9 '
|
||
'and will error in 3.11. To keep the current behavior, '
|
||
'cast the sequence to string before passing.')
|
||
_api.warn_deprecated('3.9', message=msg)
|
||
labels = [label]
|
||
else:
|
||
raise ValueError(
|
||
f"label must be scalar or have the same length as the input "
|
||
f"data, but found {len(label)} for {n_datasets} datasets.")
|
||
|
||
result = (make_artist(axes, x[:, j % ncx], y[:, j % ncy], kw,
|
||
{**kwargs, 'label': label})
|
||
for j, label in enumerate(labels))
|
||
|
||
if return_kwargs:
|
||
return list(result)
|
||
else:
|
||
return [l[0] for l in result]
|
||
|
||
|
||
@_api.define_aliases({"facecolor": ["fc"]})
|
||
class _AxesBase(martist.Artist):
|
||
name = "rectilinear"
|
||
|
||
# axis names are the prefixes for the attributes that contain the
|
||
# respective axis; e.g. 'x' <-> self.xaxis, containing an XAxis.
|
||
# Note that PolarAxes uses these attributes as well, so that we have
|
||
# 'x' <-> self.xaxis, containing a ThetaAxis. In particular we do not
|
||
# have 'theta' in _axis_names.
|
||
# In practice, this is ('x', 'y') for all 2D Axes and ('x', 'y', 'z')
|
||
# for Axes3D.
|
||
_axis_names = ("x", "y")
|
||
_shared_axes = {name: cbook.Grouper() for name in _axis_names}
|
||
_twinned_axes = cbook.Grouper()
|
||
|
||
_subclass_uses_cla = False
|
||
|
||
@property
|
||
def _axis_map(self):
|
||
"""A mapping of axis names, e.g. 'x', to `Axis` instances."""
|
||
return {name: getattr(self, f"{name}axis")
|
||
for name in self._axis_names}
|
||
|
||
def __str__(self):
|
||
return "{0}({1[0]:g},{1[1]:g};{1[2]:g}x{1[3]:g})".format(
|
||
type(self).__name__, self._position.bounds)
|
||
|
||
def __init__(self, fig,
|
||
*args,
|
||
facecolor=None, # defaults to rc axes.facecolor
|
||
frameon=True,
|
||
sharex=None, # use Axes instance's xaxis info
|
||
sharey=None, # use Axes instance's yaxis info
|
||
label='',
|
||
xscale=None,
|
||
yscale=None,
|
||
box_aspect=None,
|
||
forward_navigation_events="auto",
|
||
**kwargs
|
||
):
|
||
"""
|
||
Build an Axes in a figure.
|
||
|
||
Parameters
|
||
----------
|
||
fig : `~matplotlib.figure.Figure`
|
||
The Axes is built in the `.Figure` *fig*.
|
||
|
||
*args
|
||
``*args`` can be a single ``(left, bottom, width, height)``
|
||
rectangle or a single `.Bbox`. This specifies the rectangle (in
|
||
figure coordinates) where the Axes is positioned.
|
||
|
||
``*args`` can also consist of three numbers or a single three-digit
|
||
number; in the latter case, the digits are considered as
|
||
independent numbers. The numbers are interpreted as ``(nrows,
|
||
ncols, index)``: ``(nrows, ncols)`` specifies the size of an array
|
||
of subplots, and ``index`` is the 1-based index of the subplot
|
||
being created. Finally, ``*args`` can also directly be a
|
||
`.SubplotSpec` instance.
|
||
|
||
sharex, sharey : `~matplotlib.axes.Axes`, optional
|
||
The x- or y-`~.matplotlib.axis` is shared with the x- or y-axis in
|
||
the input `~.axes.Axes`.
|
||
|
||
frameon : bool, default: True
|
||
Whether the Axes frame is visible.
|
||
|
||
box_aspect : float, optional
|
||
Set a fixed aspect for the Axes box, i.e. the ratio of height to
|
||
width. See `~.axes.Axes.set_box_aspect` for details.
|
||
|
||
forward_navigation_events : bool or "auto", default: "auto"
|
||
Control whether pan/zoom events are passed through to Axes below
|
||
this one. "auto" is *True* for axes with an invisible patch and
|
||
*False* otherwise.
|
||
|
||
**kwargs
|
||
Other optional keyword arguments:
|
||
|
||
%(Axes:kwdoc)s
|
||
|
||
Returns
|
||
-------
|
||
`~.axes.Axes`
|
||
The new `~.axes.Axes` object.
|
||
"""
|
||
|
||
super().__init__()
|
||
if "rect" in kwargs:
|
||
if args:
|
||
raise TypeError(
|
||
"'rect' cannot be used together with positional arguments")
|
||
rect = kwargs.pop("rect")
|
||
_api.check_isinstance((mtransforms.Bbox, Iterable), rect=rect)
|
||
args = (rect,)
|
||
subplotspec = None
|
||
if len(args) == 1 and isinstance(args[0], mtransforms.Bbox):
|
||
self._position = args[0]
|
||
elif len(args) == 1 and np.iterable(args[0]):
|
||
self._position = mtransforms.Bbox.from_bounds(*args[0])
|
||
else:
|
||
self._position = self._originalPosition = mtransforms.Bbox.unit()
|
||
subplotspec = SubplotSpec._from_subplot_args(fig, args)
|
||
if self._position.width < 0 or self._position.height < 0:
|
||
raise ValueError('Width and height specified must be non-negative')
|
||
self._originalPosition = self._position.frozen()
|
||
self.axes = self
|
||
self._aspect = 'auto'
|
||
self._adjustable = 'box'
|
||
self._anchor = 'C'
|
||
self._stale_viewlims = {name: False for name in self._axis_names}
|
||
self._forward_navigation_events = forward_navigation_events
|
||
self._sharex = sharex
|
||
self._sharey = sharey
|
||
self.set_label(label)
|
||
self.set_figure(fig)
|
||
# The subplotspec needs to be set after the figure (so that
|
||
# figure-level subplotpars are taken into account), but the figure
|
||
# needs to be set after self._position is initialized.
|
||
if subplotspec:
|
||
self.set_subplotspec(subplotspec)
|
||
else:
|
||
self._subplotspec = None
|
||
self.set_box_aspect(box_aspect)
|
||
self._axes_locator = None # Optionally set via update(kwargs).
|
||
|
||
self._children = []
|
||
|
||
# placeholder for any colorbars added that use this Axes.
|
||
# (see colorbar.py):
|
||
self._colorbars = []
|
||
self.spines = mspines.Spines.from_dict(self._gen_axes_spines())
|
||
|
||
# this call may differ for non-sep axes, e.g., polar
|
||
self._init_axis()
|
||
if facecolor is None:
|
||
facecolor = mpl.rcParams['axes.facecolor']
|
||
self._facecolor = facecolor
|
||
self._frameon = frameon
|
||
self.set_axisbelow(mpl.rcParams['axes.axisbelow'])
|
||
|
||
self._rasterization_zorder = None
|
||
self.clear()
|
||
|
||
# funcs used to format x and y - fall back on major formatters
|
||
self.fmt_xdata = None
|
||
self.fmt_ydata = None
|
||
|
||
self.set_navigate(True)
|
||
self.set_navigate_mode(None)
|
||
|
||
if xscale:
|
||
self.set_xscale(xscale)
|
||
if yscale:
|
||
self.set_yscale(yscale)
|
||
|
||
self._internal_update(kwargs)
|
||
|
||
for name, axis in self._axis_map.items():
|
||
axis.callbacks._connect_picklable(
|
||
'units', self._unit_change_handler(name))
|
||
|
||
rcParams = mpl.rcParams
|
||
self.tick_params(
|
||
top=rcParams['xtick.top'] and rcParams['xtick.minor.top'],
|
||
bottom=rcParams['xtick.bottom'] and rcParams['xtick.minor.bottom'],
|
||
labeltop=(rcParams['xtick.labeltop'] and
|
||
rcParams['xtick.minor.top']),
|
||
labelbottom=(rcParams['xtick.labelbottom'] and
|
||
rcParams['xtick.minor.bottom']),
|
||
left=rcParams['ytick.left'] and rcParams['ytick.minor.left'],
|
||
right=rcParams['ytick.right'] and rcParams['ytick.minor.right'],
|
||
labelleft=(rcParams['ytick.labelleft'] and
|
||
rcParams['ytick.minor.left']),
|
||
labelright=(rcParams['ytick.labelright'] and
|
||
rcParams['ytick.minor.right']),
|
||
which='minor')
|
||
|
||
self.tick_params(
|
||
top=rcParams['xtick.top'] and rcParams['xtick.major.top'],
|
||
bottom=rcParams['xtick.bottom'] and rcParams['xtick.major.bottom'],
|
||
labeltop=(rcParams['xtick.labeltop'] and
|
||
rcParams['xtick.major.top']),
|
||
labelbottom=(rcParams['xtick.labelbottom'] and
|
||
rcParams['xtick.major.bottom']),
|
||
left=rcParams['ytick.left'] and rcParams['ytick.major.left'],
|
||
right=rcParams['ytick.right'] and rcParams['ytick.major.right'],
|
||
labelleft=(rcParams['ytick.labelleft'] and
|
||
rcParams['ytick.major.left']),
|
||
labelright=(rcParams['ytick.labelright'] and
|
||
rcParams['ytick.major.right']),
|
||
which='major')
|
||
|
||
def __init_subclass__(cls, **kwargs):
|
||
parent_uses_cla = super(cls, cls)._subclass_uses_cla
|
||
if 'cla' in cls.__dict__:
|
||
_api.warn_deprecated(
|
||
'3.6',
|
||
pending=True,
|
||
message=f'Overriding `Axes.cla` in {cls.__qualname__} is '
|
||
'pending deprecation in %(since)s and will be fully '
|
||
'deprecated in favor of `Axes.clear` in the future. '
|
||
'Please report '
|
||
f'this to the {cls.__module__!r} author.')
|
||
cls._subclass_uses_cla = 'cla' in cls.__dict__ or parent_uses_cla
|
||
super().__init_subclass__(**kwargs)
|
||
|
||
def __getstate__(self):
|
||
state = super().__getstate__()
|
||
# Prune the sharing & twinning info to only contain the current group.
|
||
state["_shared_axes"] = {
|
||
name: self._shared_axes[name].get_siblings(self)
|
||
for name in self._axis_names if self in self._shared_axes[name]}
|
||
state["_twinned_axes"] = (self._twinned_axes.get_siblings(self)
|
||
if self in self._twinned_axes else None)
|
||
return state
|
||
|
||
def __setstate__(self, state):
|
||
# Merge the grouping info back into the global groupers.
|
||
shared_axes = state.pop("_shared_axes")
|
||
for name, shared_siblings in shared_axes.items():
|
||
self._shared_axes[name].join(*shared_siblings)
|
||
twinned_siblings = state.pop("_twinned_axes")
|
||
if twinned_siblings:
|
||
self._twinned_axes.join(*twinned_siblings)
|
||
self.__dict__ = state
|
||
self._stale = True
|
||
|
||
def __repr__(self):
|
||
fields = []
|
||
if self.get_label():
|
||
fields += [f"label={self.get_label()!r}"]
|
||
if hasattr(self, "get_title"):
|
||
titles = {}
|
||
for k in ["left", "center", "right"]:
|
||
title = self.get_title(loc=k)
|
||
if title:
|
||
titles[k] = title
|
||
if titles:
|
||
fields += [f"title={titles}"]
|
||
for name, axis in self._axis_map.items():
|
||
if axis.get_label() and axis.get_label().get_text():
|
||
fields += [f"{name}label={axis.get_label().get_text()!r}"]
|
||
return f"<{self.__class__.__name__}: " + ", ".join(fields) + ">"
|
||
|
||
def get_subplotspec(self):
|
||
"""Return the `.SubplotSpec` associated with the subplot, or None."""
|
||
return self._subplotspec
|
||
|
||
def set_subplotspec(self, subplotspec):
|
||
"""Set the `.SubplotSpec`. associated with the subplot."""
|
||
self._subplotspec = subplotspec
|
||
self._set_position(subplotspec.get_position(self.figure))
|
||
|
||
def get_gridspec(self):
|
||
"""Return the `.GridSpec` associated with the subplot, or None."""
|
||
return self._subplotspec.get_gridspec() if self._subplotspec else None
|
||
|
||
def get_window_extent(self, renderer=None):
|
||
"""
|
||
Return the Axes bounding box in display space.
|
||
|
||
This bounding box does not include the spines, ticks, ticklabels,
|
||
or other labels. For a bounding box including these elements use
|
||
`~matplotlib.axes.Axes.get_tightbbox`.
|
||
|
||
See Also
|
||
--------
|
||
matplotlib.axes.Axes.get_tightbbox
|
||
matplotlib.axis.Axis.get_tightbbox
|
||
matplotlib.spines.Spine.get_window_extent
|
||
"""
|
||
return self.bbox
|
||
|
||
def _init_axis(self):
|
||
# This is moved out of __init__ because non-separable axes don't use it
|
||
self.xaxis = maxis.XAxis(self, clear=False)
|
||
self.spines.bottom.register_axis(self.xaxis)
|
||
self.spines.top.register_axis(self.xaxis)
|
||
self.yaxis = maxis.YAxis(self, clear=False)
|
||
self.spines.left.register_axis(self.yaxis)
|
||
self.spines.right.register_axis(self.yaxis)
|
||
|
||
def set_figure(self, fig):
|
||
# docstring inherited
|
||
super().set_figure(fig)
|
||
|
||
self.bbox = mtransforms.TransformedBbox(self._position,
|
||
fig.transSubfigure)
|
||
# these will be updated later as data is added
|
||
self.dataLim = mtransforms.Bbox.null()
|
||
self._viewLim = mtransforms.Bbox.unit()
|
||
self.transScale = mtransforms.TransformWrapper(
|
||
mtransforms.IdentityTransform())
|
||
|
||
self._set_lim_and_transforms()
|
||
|
||
def _unstale_viewLim(self):
|
||
# We should arrange to store this information once per share-group
|
||
# instead of on every axis.
|
||
need_scale = {
|
||
name: any(ax._stale_viewlims[name]
|
||
for ax in self._shared_axes[name].get_siblings(self))
|
||
for name in self._axis_names}
|
||
if any(need_scale.values()):
|
||
for name in need_scale:
|
||
for ax in self._shared_axes[name].get_siblings(self):
|
||
ax._stale_viewlims[name] = False
|
||
self.autoscale_view(**{f"scale{name}": scale
|
||
for name, scale in need_scale.items()})
|
||
|
||
@property
|
||
def viewLim(self):
|
||
self._unstale_viewLim()
|
||
return self._viewLim
|
||
|
||
def _request_autoscale_view(self, axis="all", tight=None):
|
||
"""
|
||
Mark a single axis, or all of them, as stale wrt. autoscaling.
|
||
|
||
No computation is performed until the next autoscaling; thus, separate
|
||
calls to control individual axises incur negligible performance cost.
|
||
|
||
Parameters
|
||
----------
|
||
axis : str, default: "all"
|
||
Either an element of ``self._axis_names``, or "all".
|
||
tight : bool or None, default: None
|
||
"""
|
||
axis_names = _api.check_getitem(
|
||
{**{k: [k] for k in self._axis_names}, "all": self._axis_names},
|
||
axis=axis)
|
||
for name in axis_names:
|
||
self._stale_viewlims[name] = True
|
||
if tight is not None:
|
||
self._tight = tight
|
||
|
||
def _set_lim_and_transforms(self):
|
||
"""
|
||
Set the *_xaxis_transform*, *_yaxis_transform*, *transScale*,
|
||
*transData*, *transLimits* and *transAxes* transformations.
|
||
|
||
.. note::
|
||
|
||
This method is primarily used by rectilinear projections of the
|
||
`~matplotlib.axes.Axes` class, and is meant to be overridden by
|
||
new kinds of projection Axes that need different transformations
|
||
and limits. (See `~matplotlib.projections.polar.PolarAxes` for an
|
||
example.)
|
||
"""
|
||
self.transAxes = mtransforms.BboxTransformTo(self.bbox)
|
||
|
||
# Transforms the x and y axis separately by a scale factor.
|
||
# It is assumed that this part will have non-linear components
|
||
# (e.g., for a log scale).
|
||
self.transScale = mtransforms.TransformWrapper(
|
||
mtransforms.IdentityTransform())
|
||
|
||
# An affine transformation on the data, generally to limit the
|
||
# range of the axes
|
||
self.transLimits = mtransforms.BboxTransformFrom(
|
||
mtransforms.TransformedBbox(self._viewLim, self.transScale))
|
||
|
||
# The parentheses are important for efficiency here -- they
|
||
# group the last two (which are usually affines) separately
|
||
# from the first (which, with log-scaling can be non-affine).
|
||
self.transData = self.transScale + (self.transLimits + self.transAxes)
|
||
|
||
self._xaxis_transform = mtransforms.blended_transform_factory(
|
||
self.transData, self.transAxes)
|
||
self._yaxis_transform = mtransforms.blended_transform_factory(
|
||
self.transAxes, self.transData)
|
||
|
||
def get_xaxis_transform(self, which='grid'):
|
||
"""
|
||
Get the transformation used for drawing x-axis labels, ticks
|
||
and gridlines. The x-direction is in data coordinates and the
|
||
y-direction is in axis coordinates.
|
||
|
||
.. note::
|
||
|
||
This transformation is primarily used by the
|
||
`~matplotlib.axis.Axis` class, and is meant to be
|
||
overridden by new kinds of projections that may need to
|
||
place axis elements in different locations.
|
||
|
||
Parameters
|
||
----------
|
||
which : {'grid', 'tick1', 'tick2'}
|
||
"""
|
||
if which == 'grid':
|
||
return self._xaxis_transform
|
||
elif which == 'tick1':
|
||
# for cartesian projection, this is bottom spine
|
||
return self.spines.bottom.get_spine_transform()
|
||
elif which == 'tick2':
|
||
# for cartesian projection, this is top spine
|
||
return self.spines.top.get_spine_transform()
|
||
else:
|
||
raise ValueError(f'unknown value for which: {which!r}')
|
||
|
||
def get_xaxis_text1_transform(self, pad_points):
|
||
"""
|
||
Returns
|
||
-------
|
||
transform : Transform
|
||
The transform used for drawing x-axis labels, which will add
|
||
*pad_points* of padding (in points) between the axis and the label.
|
||
The x-direction is in data coordinates and the y-direction is in
|
||
axis coordinates
|
||
valign : {'center', 'top', 'bottom', 'baseline', 'center_baseline'}
|
||
The text vertical alignment.
|
||
halign : {'center', 'left', 'right'}
|
||
The text horizontal alignment.
|
||
|
||
Notes
|
||
-----
|
||
This transformation is primarily used by the `~matplotlib.axis.Axis`
|
||
class, and is meant to be overridden by new kinds of projections that
|
||
may need to place axis elements in different locations.
|
||
"""
|
||
labels_align = mpl.rcParams["xtick.alignment"]
|
||
return (self.get_xaxis_transform(which='tick1') +
|
||
mtransforms.ScaledTranslation(0, -1 * pad_points / 72,
|
||
self.figure.dpi_scale_trans),
|
||
"top", labels_align)
|
||
|
||
def get_xaxis_text2_transform(self, pad_points):
|
||
"""
|
||
Returns
|
||
-------
|
||
transform : Transform
|
||
The transform used for drawing secondary x-axis labels, which will
|
||
add *pad_points* of padding (in points) between the axis and the
|
||
label. The x-direction is in data coordinates and the y-direction
|
||
is in axis coordinates
|
||
valign : {'center', 'top', 'bottom', 'baseline', 'center_baseline'}
|
||
The text vertical alignment.
|
||
halign : {'center', 'left', 'right'}
|
||
The text horizontal alignment.
|
||
|
||
Notes
|
||
-----
|
||
This transformation is primarily used by the `~matplotlib.axis.Axis`
|
||
class, and is meant to be overridden by new kinds of projections that
|
||
may need to place axis elements in different locations.
|
||
"""
|
||
labels_align = mpl.rcParams["xtick.alignment"]
|
||
return (self.get_xaxis_transform(which='tick2') +
|
||
mtransforms.ScaledTranslation(0, pad_points / 72,
|
||
self.figure.dpi_scale_trans),
|
||
"bottom", labels_align)
|
||
|
||
def get_yaxis_transform(self, which='grid'):
|
||
"""
|
||
Get the transformation used for drawing y-axis labels, ticks
|
||
and gridlines. The x-direction is in axis coordinates and the
|
||
y-direction is in data coordinates.
|
||
|
||
.. note::
|
||
|
||
This transformation is primarily used by the
|
||
`~matplotlib.axis.Axis` class, and is meant to be
|
||
overridden by new kinds of projections that may need to
|
||
place axis elements in different locations.
|
||
|
||
Parameters
|
||
----------
|
||
which : {'grid', 'tick1', 'tick2'}
|
||
"""
|
||
if which == 'grid':
|
||
return self._yaxis_transform
|
||
elif which == 'tick1':
|
||
# for cartesian projection, this is bottom spine
|
||
return self.spines.left.get_spine_transform()
|
||
elif which == 'tick2':
|
||
# for cartesian projection, this is top spine
|
||
return self.spines.right.get_spine_transform()
|
||
else:
|
||
raise ValueError(f'unknown value for which: {which!r}')
|
||
|
||
def get_yaxis_text1_transform(self, pad_points):
|
||
"""
|
||
Returns
|
||
-------
|
||
transform : Transform
|
||
The transform used for drawing y-axis labels, which will add
|
||
*pad_points* of padding (in points) between the axis and the label.
|
||
The x-direction is in axis coordinates and the y-direction is in
|
||
data coordinates
|
||
valign : {'center', 'top', 'bottom', 'baseline', 'center_baseline'}
|
||
The text vertical alignment.
|
||
halign : {'center', 'left', 'right'}
|
||
The text horizontal alignment.
|
||
|
||
Notes
|
||
-----
|
||
This transformation is primarily used by the `~matplotlib.axis.Axis`
|
||
class, and is meant to be overridden by new kinds of projections that
|
||
may need to place axis elements in different locations.
|
||
"""
|
||
labels_align = mpl.rcParams["ytick.alignment"]
|
||
return (self.get_yaxis_transform(which='tick1') +
|
||
mtransforms.ScaledTranslation(-1 * pad_points / 72, 0,
|
||
self.figure.dpi_scale_trans),
|
||
labels_align, "right")
|
||
|
||
def get_yaxis_text2_transform(self, pad_points):
|
||
"""
|
||
Returns
|
||
-------
|
||
transform : Transform
|
||
The transform used for drawing secondart y-axis labels, which will
|
||
add *pad_points* of padding (in points) between the axis and the
|
||
label. The x-direction is in axis coordinates and the y-direction
|
||
is in data coordinates
|
||
valign : {'center', 'top', 'bottom', 'baseline', 'center_baseline'}
|
||
The text vertical alignment.
|
||
halign : {'center', 'left', 'right'}
|
||
The text horizontal alignment.
|
||
|
||
Notes
|
||
-----
|
||
This transformation is primarily used by the `~matplotlib.axis.Axis`
|
||
class, and is meant to be overridden by new kinds of projections that
|
||
may need to place axis elements in different locations.
|
||
"""
|
||
labels_align = mpl.rcParams["ytick.alignment"]
|
||
return (self.get_yaxis_transform(which='tick2') +
|
||
mtransforms.ScaledTranslation(pad_points / 72, 0,
|
||
self.figure.dpi_scale_trans),
|
||
labels_align, "left")
|
||
|
||
def _update_transScale(self):
|
||
self.transScale.set(
|
||
mtransforms.blended_transform_factory(
|
||
self.xaxis.get_transform(), self.yaxis.get_transform()))
|
||
|
||
def get_position(self, original=False):
|
||
"""
|
||
Return the position of the Axes within the figure as a `.Bbox`.
|
||
|
||
Parameters
|
||
----------
|
||
original : bool
|
||
If ``True``, return the original position. Otherwise, return the
|
||
active position. For an explanation of the positions see
|
||
`.set_position`.
|
||
|
||
Returns
|
||
-------
|
||
`.Bbox`
|
||
|
||
"""
|
||
if original:
|
||
return self._originalPosition.frozen()
|
||
else:
|
||
locator = self.get_axes_locator()
|
||
if not locator:
|
||
self.apply_aspect()
|
||
return self._position.frozen()
|
||
|
||
def set_position(self, pos, which='both'):
|
||
"""
|
||
Set the Axes position.
|
||
|
||
Axes have two position attributes. The 'original' position is the
|
||
position allocated for the Axes. The 'active' position is the
|
||
position the Axes is actually drawn at. These positions are usually
|
||
the same unless a fixed aspect is set to the Axes. See
|
||
`.Axes.set_aspect` for details.
|
||
|
||
Parameters
|
||
----------
|
||
pos : [left, bottom, width, height] or `~matplotlib.transforms.Bbox`
|
||
The new position of the Axes in `.Figure` coordinates.
|
||
|
||
which : {'both', 'active', 'original'}, default: 'both'
|
||
Determines which position variables to change.
|
||
|
||
See Also
|
||
--------
|
||
matplotlib.transforms.Bbox.from_bounds
|
||
matplotlib.transforms.Bbox.from_extents
|
||
"""
|
||
self._set_position(pos, which=which)
|
||
# because this is being called externally to the library we
|
||
# don't let it be in the layout.
|
||
self.set_in_layout(False)
|
||
|
||
def _set_position(self, pos, which='both'):
|
||
"""
|
||
Private version of set_position.
|
||
|
||
Call this internally to get the same functionality of `set_position`,
|
||
but not to take the axis out of the constrained_layout hierarchy.
|
||
"""
|
||
if not isinstance(pos, mtransforms.BboxBase):
|
||
pos = mtransforms.Bbox.from_bounds(*pos)
|
||
for ax in self._twinned_axes.get_siblings(self):
|
||
if which in ('both', 'active'):
|
||
ax._position.set(pos)
|
||
if which in ('both', 'original'):
|
||
ax._originalPosition.set(pos)
|
||
self.stale = True
|
||
|
||
def reset_position(self):
|
||
"""
|
||
Reset the active position to the original position.
|
||
|
||
This undoes changes to the active position (as defined in
|
||
`.set_position`) which may have been performed to satisfy fixed-aspect
|
||
constraints.
|
||
"""
|
||
for ax in self._twinned_axes.get_siblings(self):
|
||
pos = ax.get_position(original=True)
|
||
ax.set_position(pos, which='active')
|
||
|
||
def set_axes_locator(self, locator):
|
||
"""
|
||
Set the Axes locator.
|
||
|
||
Parameters
|
||
----------
|
||
locator : Callable[[Axes, Renderer], Bbox]
|
||
"""
|
||
self._axes_locator = locator
|
||
self.stale = True
|
||
|
||
def get_axes_locator(self):
|
||
"""
|
||
Return the axes_locator.
|
||
"""
|
||
return self._axes_locator
|
||
|
||
def _set_artist_props(self, a):
|
||
"""Set the boilerplate props for artists added to Axes."""
|
||
a.set_figure(self.figure)
|
||
if not a.is_transform_set():
|
||
a.set_transform(self.transData)
|
||
|
||
a.axes = self
|
||
if a.get_mouseover():
|
||
self._mouseover_set.add(a)
|
||
|
||
def _gen_axes_patch(self):
|
||
"""
|
||
Returns
|
||
-------
|
||
Patch
|
||
The patch used to draw the background of the Axes. It is also used
|
||
as the clipping path for any data elements on the Axes.
|
||
|
||
In the standard Axes, this is a rectangle, but in other projections
|
||
it may not be.
|
||
|
||
Notes
|
||
-----
|
||
Intended to be overridden by new projection types.
|
||
"""
|
||
return mpatches.Rectangle((0.0, 0.0), 1.0, 1.0)
|
||
|
||
def _gen_axes_spines(self, locations=None, offset=0.0, units='inches'):
|
||
"""
|
||
Returns
|
||
-------
|
||
dict
|
||
Mapping of spine names to `.Line2D` or `.Patch` instances that are
|
||
used to draw Axes spines.
|
||
|
||
In the standard Axes, spines are single line segments, but in other
|
||
projections they may not be.
|
||
|
||
Notes
|
||
-----
|
||
Intended to be overridden by new projection types.
|
||
"""
|
||
return {side: mspines.Spine.linear_spine(self, side)
|
||
for side in ['left', 'right', 'bottom', 'top']}
|
||
|
||
def sharex(self, other):
|
||
"""
|
||
Share the x-axis with *other*.
|
||
|
||
This is equivalent to passing ``sharex=other`` when constructing the
|
||
Axes, and cannot be used if the x-axis is already being shared with
|
||
another Axes.
|
||
"""
|
||
_api.check_isinstance(_AxesBase, other=other)
|
||
if self._sharex is not None and other is not self._sharex:
|
||
raise ValueError("x-axis is already shared")
|
||
self._shared_axes["x"].join(self, other)
|
||
self._sharex = other
|
||
self.xaxis.major = other.xaxis.major # Ticker instances holding
|
||
self.xaxis.minor = other.xaxis.minor # locator and formatter.
|
||
x0, x1 = other.get_xlim()
|
||
self.set_xlim(x0, x1, emit=False, auto=other.get_autoscalex_on())
|
||
self.xaxis._scale = other.xaxis._scale
|
||
|
||
def sharey(self, other):
|
||
"""
|
||
Share the y-axis with *other*.
|
||
|
||
This is equivalent to passing ``sharey=other`` when constructing the
|
||
Axes, and cannot be used if the y-axis is already being shared with
|
||
another Axes.
|
||
"""
|
||
_api.check_isinstance(_AxesBase, other=other)
|
||
if self._sharey is not None and other is not self._sharey:
|
||
raise ValueError("y-axis is already shared")
|
||
self._shared_axes["y"].join(self, other)
|
||
self._sharey = other
|
||
self.yaxis.major = other.yaxis.major # Ticker instances holding
|
||
self.yaxis.minor = other.yaxis.minor # locator and formatter.
|
||
y0, y1 = other.get_ylim()
|
||
self.set_ylim(y0, y1, emit=False, auto=other.get_autoscaley_on())
|
||
self.yaxis._scale = other.yaxis._scale
|
||
|
||
def __clear(self):
|
||
"""Clear the Axes."""
|
||
# The actual implementation of clear() as long as clear() has to be
|
||
# an adapter delegating to the correct implementation.
|
||
# The implementation can move back into clear() when the
|
||
# deprecation on cla() subclassing expires.
|
||
|
||
# stash the current visibility state
|
||
if hasattr(self, 'patch'):
|
||
patch_visible = self.patch.get_visible()
|
||
else:
|
||
patch_visible = True
|
||
|
||
xaxis_visible = self.xaxis.get_visible()
|
||
yaxis_visible = self.yaxis.get_visible()
|
||
|
||
for axis in self._axis_map.values():
|
||
axis.clear() # Also resets the scale to linear.
|
||
for spine in self.spines.values():
|
||
spine._clear() # Use _clear to not clear Axis again
|
||
|
||
self.ignore_existing_data_limits = True
|
||
self.callbacks = cbook.CallbackRegistry(
|
||
signals=["xlim_changed", "ylim_changed", "zlim_changed"])
|
||
|
||
# update the minor locator for x and y axis based on rcParams
|
||
if mpl.rcParams['xtick.minor.visible']:
|
||
self.xaxis.set_minor_locator(mticker.AutoMinorLocator())
|
||
if mpl.rcParams['ytick.minor.visible']:
|
||
self.yaxis.set_minor_locator(mticker.AutoMinorLocator())
|
||
|
||
self._xmargin = mpl.rcParams['axes.xmargin']
|
||
self._ymargin = mpl.rcParams['axes.ymargin']
|
||
self._tight = None
|
||
self._use_sticky_edges = True
|
||
|
||
self._get_lines = _process_plot_var_args()
|
||
self._get_patches_for_fill = _process_plot_var_args('fill')
|
||
|
||
self._gridOn = mpl.rcParams['axes.grid']
|
||
old_children, self._children = self._children, []
|
||
for chld in old_children:
|
||
chld.axes = chld.figure = None
|
||
self._mouseover_set = _OrderedSet()
|
||
self.child_axes = []
|
||
self._current_image = None # strictly for pyplot via _sci, _gci
|
||
self._projection_init = None # strictly for pyplot.subplot
|
||
self.legend_ = None
|
||
self.containers = []
|
||
|
||
self.grid(False) # Disable grid on init to use rcParameter
|
||
self.grid(self._gridOn, which=mpl.rcParams['axes.grid.which'],
|
||
axis=mpl.rcParams['axes.grid.axis'])
|
||
props = font_manager.FontProperties(
|
||
size=mpl.rcParams['axes.titlesize'],
|
||
weight=mpl.rcParams['axes.titleweight'])
|
||
|
||
y = mpl.rcParams['axes.titley']
|
||
if y is None:
|
||
y = 1.0
|
||
self._autotitlepos = True
|
||
else:
|
||
self._autotitlepos = False
|
||
|
||
self.title = mtext.Text(
|
||
x=0.5, y=y, text='',
|
||
fontproperties=props,
|
||
verticalalignment='baseline',
|
||
horizontalalignment='center',
|
||
)
|
||
self._left_title = mtext.Text(
|
||
x=0.0, y=y, text='',
|
||
fontproperties=props.copy(),
|
||
verticalalignment='baseline',
|
||
horizontalalignment='left', )
|
||
self._right_title = mtext.Text(
|
||
x=1.0, y=y, text='',
|
||
fontproperties=props.copy(),
|
||
verticalalignment='baseline',
|
||
horizontalalignment='right',
|
||
)
|
||
title_offset_points = mpl.rcParams['axes.titlepad']
|
||
# refactor this out so it can be called in ax.set_title if
|
||
# pad argument used...
|
||
self._set_title_offset_trans(title_offset_points)
|
||
|
||
for _title in (self.title, self._left_title, self._right_title):
|
||
self._set_artist_props(_title)
|
||
|
||
# The patch draws the background of the Axes. We want this to be below
|
||
# the other artists. We use the frame to draw the edges so we are
|
||
# setting the edgecolor to None.
|
||
self.patch = self._gen_axes_patch()
|
||
self.patch.set_figure(self.figure)
|
||
self.patch.set_facecolor(self._facecolor)
|
||
self.patch.set_edgecolor('none')
|
||
self.patch.set_linewidth(0)
|
||
self.patch.set_transform(self.transAxes)
|
||
|
||
self.set_axis_on()
|
||
|
||
self.xaxis.set_clip_path(self.patch)
|
||
self.yaxis.set_clip_path(self.patch)
|
||
|
||
if self._sharex is not None:
|
||
self.xaxis.set_visible(xaxis_visible)
|
||
self.patch.set_visible(patch_visible)
|
||
if self._sharey is not None:
|
||
self.yaxis.set_visible(yaxis_visible)
|
||
self.patch.set_visible(patch_visible)
|
||
|
||
# This comes last, as the call to _set_lim may trigger an autoscale (in
|
||
# case of shared axes), requiring children to be already set up.
|
||
for name, axis in self._axis_map.items():
|
||
share = getattr(self, f"_share{name}")
|
||
if share is not None:
|
||
getattr(self, f"share{name}")(share)
|
||
else:
|
||
# Although the scale was set to linear as part of clear,
|
||
# polar requires that _set_scale is called again
|
||
if self.name == "polar":
|
||
axis._set_scale("linear")
|
||
axis._set_lim(0, 1, auto=True)
|
||
self._update_transScale()
|
||
|
||
self.stale = True
|
||
|
||
def clear(self):
|
||
"""Clear the Axes."""
|
||
# Act as an alias, or as the superclass implementation depending on the
|
||
# subclass implementation.
|
||
if self._subclass_uses_cla:
|
||
self.cla()
|
||
else:
|
||
self.__clear()
|
||
|
||
def cla(self):
|
||
"""Clear the Axes."""
|
||
# Act as an alias, or as the superclass implementation depending on the
|
||
# subclass implementation.
|
||
if self._subclass_uses_cla:
|
||
self.__clear()
|
||
else:
|
||
self.clear()
|
||
|
||
class ArtistList(Sequence):
|
||
"""
|
||
A sublist of Axes children based on their type.
|
||
|
||
The type-specific children sublists were made immutable in Matplotlib
|
||
3.7. In the future these artist lists may be replaced by tuples. Use
|
||
as if this is a tuple already.
|
||
"""
|
||
def __init__(self, axes, prop_name,
|
||
valid_types=None, invalid_types=None):
|
||
"""
|
||
Parameters
|
||
----------
|
||
axes : `~matplotlib.axes.Axes`
|
||
The Axes from which this sublist will pull the children
|
||
Artists.
|
||
prop_name : str
|
||
The property name used to access this sublist from the Axes;
|
||
used to generate deprecation warnings.
|
||
valid_types : list of type, optional
|
||
A list of types that determine which children will be returned
|
||
by this sublist. If specified, then the Artists in the sublist
|
||
must be instances of any of these types. If unspecified, then
|
||
any type of Artist is valid (unless limited by
|
||
*invalid_types*.)
|
||
invalid_types : tuple, optional
|
||
A list of types that determine which children will *not* be
|
||
returned by this sublist. If specified, then Artists in the
|
||
sublist will never be an instance of these types. Otherwise, no
|
||
types will be excluded.
|
||
"""
|
||
self._axes = axes
|
||
self._prop_name = prop_name
|
||
self._type_check = lambda artist: (
|
||
(not valid_types or isinstance(artist, valid_types)) and
|
||
(not invalid_types or not isinstance(artist, invalid_types))
|
||
)
|
||
|
||
def __repr__(self):
|
||
return f'<Axes.ArtistList of {len(self)} {self._prop_name}>'
|
||
|
||
def __len__(self):
|
||
return sum(self._type_check(artist)
|
||
for artist in self._axes._children)
|
||
|
||
def __iter__(self):
|
||
for artist in list(self._axes._children):
|
||
if self._type_check(artist):
|
||
yield artist
|
||
|
||
def __getitem__(self, key):
|
||
return [artist
|
||
for artist in self._axes._children
|
||
if self._type_check(artist)][key]
|
||
|
||
def __add__(self, other):
|
||
if isinstance(other, (list, _AxesBase.ArtistList)):
|
||
return [*self, *other]
|
||
if isinstance(other, (tuple, _AxesBase.ArtistList)):
|
||
return (*self, *other)
|
||
return NotImplemented
|
||
|
||
def __radd__(self, other):
|
||
if isinstance(other, list):
|
||
return other + list(self)
|
||
if isinstance(other, tuple):
|
||
return other + tuple(self)
|
||
return NotImplemented
|
||
|
||
@property
|
||
def artists(self):
|
||
return self.ArtistList(self, 'artists', invalid_types=(
|
||
mcoll.Collection, mimage.AxesImage, mlines.Line2D, mpatches.Patch,
|
||
mtable.Table, mtext.Text))
|
||
|
||
@property
|
||
def collections(self):
|
||
return self.ArtistList(self, 'collections',
|
||
valid_types=mcoll.Collection)
|
||
|
||
@property
|
||
def images(self):
|
||
return self.ArtistList(self, 'images', valid_types=mimage.AxesImage)
|
||
|
||
@property
|
||
def lines(self):
|
||
return self.ArtistList(self, 'lines', valid_types=mlines.Line2D)
|
||
|
||
@property
|
||
def patches(self):
|
||
return self.ArtistList(self, 'patches', valid_types=mpatches.Patch)
|
||
|
||
@property
|
||
def tables(self):
|
||
return self.ArtistList(self, 'tables', valid_types=mtable.Table)
|
||
|
||
@property
|
||
def texts(self):
|
||
return self.ArtistList(self, 'texts', valid_types=mtext.Text)
|
||
|
||
def get_facecolor(self):
|
||
"""Get the facecolor of the Axes."""
|
||
return self.patch.get_facecolor()
|
||
|
||
def set_facecolor(self, color):
|
||
"""
|
||
Set the facecolor of the Axes.
|
||
|
||
Parameters
|
||
----------
|
||
color : :mpltype:`color`
|
||
"""
|
||
self._facecolor = color
|
||
self.stale = True
|
||
return self.patch.set_facecolor(color)
|
||
|
||
def _set_title_offset_trans(self, title_offset_points):
|
||
"""
|
||
Set the offset for the title either from :rc:`axes.titlepad`
|
||
or from set_title kwarg ``pad``.
|
||
"""
|
||
self.titleOffsetTrans = mtransforms.ScaledTranslation(
|
||
0.0, title_offset_points / 72,
|
||
self.figure.dpi_scale_trans)
|
||
for _title in (self.title, self._left_title, self._right_title):
|
||
_title.set_transform(self.transAxes + self.titleOffsetTrans)
|
||
_title.set_clip_box(None)
|
||
|
||
def set_prop_cycle(self, *args, **kwargs):
|
||
"""
|
||
Set the property cycle of the Axes.
|
||
|
||
The property cycle controls the style properties such as color,
|
||
marker and linestyle of future plot commands. The style properties
|
||
of data already added to the Axes are not modified.
|
||
|
||
Call signatures::
|
||
|
||
set_prop_cycle(cycler)
|
||
set_prop_cycle(label=values[, label2=values2[, ...]])
|
||
set_prop_cycle(label, values)
|
||
|
||
Form 1 sets given `~cycler.Cycler` object.
|
||
|
||
Form 2 creates a `~cycler.Cycler` which cycles over one or more
|
||
properties simultaneously and set it as the property cycle of the
|
||
Axes. If multiple properties are given, their value lists must have
|
||
the same length. This is just a shortcut for explicitly creating a
|
||
cycler and passing it to the function, i.e. it's short for
|
||
``set_prop_cycle(cycler(label=values label2=values2, ...))``.
|
||
|
||
Form 3 creates a `~cycler.Cycler` for a single property and set it
|
||
as the property cycle of the Axes. This form exists for compatibility
|
||
with the original `cycler.cycler` interface. Its use is discouraged
|
||
in favor of the kwarg form, i.e. ``set_prop_cycle(label=values)``.
|
||
|
||
Parameters
|
||
----------
|
||
cycler : `~cycler.Cycler`
|
||
Set the given Cycler. *None* resets to the cycle defined by the
|
||
current style.
|
||
|
||
.. ACCEPTS: `~cycler.Cycler`
|
||
|
||
label : str
|
||
The property key. Must be a valid `.Artist` property.
|
||
For example, 'color' or 'linestyle'. Aliases are allowed,
|
||
such as 'c' for 'color' and 'lw' for 'linewidth'.
|
||
|
||
values : iterable
|
||
Finite-length iterable of the property values. These values
|
||
are validated and will raise a ValueError if invalid.
|
||
|
||
See Also
|
||
--------
|
||
matplotlib.rcsetup.cycler
|
||
Convenience function for creating validated cyclers for properties.
|
||
cycler.cycler
|
||
The original function for creating unvalidated cyclers.
|
||
|
||
Examples
|
||
--------
|
||
Setting the property cycle for a single property:
|
||
|
||
>>> ax.set_prop_cycle(color=['red', 'green', 'blue'])
|
||
|
||
Setting the property cycle for simultaneously cycling over multiple
|
||
properties (e.g. red circle, green plus, blue cross):
|
||
|
||
>>> ax.set_prop_cycle(color=['red', 'green', 'blue'],
|
||
... marker=['o', '+', 'x'])
|
||
|
||
"""
|
||
if args and kwargs:
|
||
raise TypeError("Cannot supply both positional and keyword "
|
||
"arguments to this method.")
|
||
# Can't do `args == (None,)` as that crashes cycler.
|
||
if len(args) == 1 and args[0] is None:
|
||
prop_cycle = None
|
||
else:
|
||
prop_cycle = cycler(*args, **kwargs)
|
||
self._get_lines.set_prop_cycle(prop_cycle)
|
||
self._get_patches_for_fill.set_prop_cycle(prop_cycle)
|
||
|
||
def get_aspect(self):
|
||
"""
|
||
Return the aspect ratio of the Axes scaling.
|
||
|
||
This is either "auto" or a float giving the ratio of y/x-scale.
|
||
"""
|
||
return self._aspect
|
||
|
||
def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
|
||
"""
|
||
Set the aspect ratio of the Axes scaling, i.e. y/x-scale.
|
||
|
||
Parameters
|
||
----------
|
||
aspect : {'auto', 'equal'} or float
|
||
Possible values:
|
||
|
||
- 'auto': fill the position rectangle with data.
|
||
- 'equal': same as ``aspect=1``, i.e. same scaling for x and y.
|
||
- *float*: The displayed size of 1 unit in y-data coordinates will
|
||
be *aspect* times the displayed size of 1 unit in x-data
|
||
coordinates; e.g. for ``aspect=2`` a square in data coordinates
|
||
will be rendered with a height of twice its width.
|
||
|
||
adjustable : None or {'box', 'datalim'}, optional
|
||
If not ``None``, this defines which parameter will be adjusted to
|
||
meet the required aspect. See `.set_adjustable` for further
|
||
details.
|
||
|
||
anchor : None or str or (float, float), optional
|
||
If not ``None``, this defines where the Axes will be drawn if there
|
||
is extra space due to aspect constraints. The most common way
|
||
to specify the anchor are abbreviations of cardinal directions:
|
||
|
||
===== =====================
|
||
value description
|
||
===== =====================
|
||
'C' centered
|
||
'SW' lower left corner
|
||
'S' middle of bottom edge
|
||
'SE' lower right corner
|
||
etc.
|
||
===== =====================
|
||
|
||
See `~.Axes.set_anchor` for further details.
|
||
|
||
share : bool, default: False
|
||
If ``True``, apply the settings to all shared Axes.
|
||
|
||
See Also
|
||
--------
|
||
matplotlib.axes.Axes.set_adjustable
|
||
Set how the Axes adjusts to achieve the required aspect ratio.
|
||
matplotlib.axes.Axes.set_anchor
|
||
Set the position in case of extra space.
|
||
"""
|
||
if cbook._str_equal(aspect, 'equal'):
|
||
aspect = 1
|
||
if not cbook._str_equal(aspect, 'auto'):
|
||
aspect = float(aspect) # raise ValueError if necessary
|
||
if aspect <= 0 or not np.isfinite(aspect):
|
||
raise ValueError("aspect must be finite and positive ")
|
||
|
||
if share:
|
||
axes = {sibling for name in self._axis_names
|
||
for sibling in self._shared_axes[name].get_siblings(self)}
|
||
else:
|
||
axes = [self]
|
||
|
||
for ax in axes:
|
||
ax._aspect = aspect
|
||
|
||
if adjustable is None:
|
||
adjustable = self._adjustable
|
||
self.set_adjustable(adjustable, share=share) # Handle sharing.
|
||
|
||
if anchor is not None:
|
||
self.set_anchor(anchor, share=share)
|
||
self.stale = True
|
||
|
||
def get_adjustable(self):
|
||
"""
|
||
Return whether the Axes will adjust its physical dimension ('box') or
|
||
its data limits ('datalim') to achieve the desired aspect ratio.
|
||
|
||
See Also
|
||
--------
|
||
matplotlib.axes.Axes.set_adjustable
|
||
Set how the Axes adjusts to achieve the required aspect ratio.
|
||
matplotlib.axes.Axes.set_aspect
|
||
For a description of aspect handling.
|
||
"""
|
||
return self._adjustable
|
||
|
||
def set_adjustable(self, adjustable, share=False):
|
||
"""
|
||
Set how the Axes adjusts to achieve the required aspect ratio.
|
||
|
||
Parameters
|
||
----------
|
||
adjustable : {'box', 'datalim'}
|
||
If 'box', change the physical dimensions of the Axes.
|
||
If 'datalim', change the ``x`` or ``y`` data limits.
|
||
|
||
share : bool, default: False
|
||
If ``True``, apply the settings to all shared Axes.
|
||
|
||
See Also
|
||
--------
|
||
matplotlib.axes.Axes.set_aspect
|
||
For a description of aspect handling.
|
||
|
||
Notes
|
||
-----
|
||
Shared Axes (of which twinned Axes are a special case)
|
||
impose restrictions on how aspect ratios can be imposed.
|
||
For twinned Axes, use 'datalim'. For Axes that share both
|
||
x and y, use 'box'. Otherwise, either 'datalim' or 'box'
|
||
may be used. These limitations are partly a requirement
|
||
to avoid over-specification, and partly a result of the
|
||
particular implementation we are currently using, in
|
||
which the adjustments for aspect ratios are done sequentially
|
||
and independently on each Axes as it is drawn.
|
||
"""
|
||
_api.check_in_list(["box", "datalim"], adjustable=adjustable)
|
||
if share:
|
||
axs = {sibling for name in self._axis_names
|
||
for sibling in self._shared_axes[name].get_siblings(self)}
|
||
else:
|
||
axs = [self]
|
||
if (adjustable == "datalim"
|
||
and any(getattr(ax.get_data_ratio, "__func__", None)
|
||
!= _AxesBase.get_data_ratio
|
||
for ax in axs)):
|
||
# Limits adjustment by apply_aspect assumes that the axes' aspect
|
||
# ratio can be computed from the data limits and scales.
|
||
raise ValueError("Cannot set Axes adjustable to 'datalim' for "
|
||
"Axes which override 'get_data_ratio'")
|
||
for ax in axs:
|
||
ax._adjustable = adjustable
|
||
self.stale = True
|
||
|
||
def get_box_aspect(self):
|
||
"""
|
||
Return the Axes box aspect, i.e. the ratio of height to width.
|
||
|
||
The box aspect is ``None`` (i.e. chosen depending on the available
|
||
figure space) unless explicitly specified.
|
||
|
||
See Also
|
||
--------
|
||
matplotlib.axes.Axes.set_box_aspect
|
||
for a description of box aspect.
|
||
matplotlib.axes.Axes.set_aspect
|
||
for a description of aspect handling.
|
||
"""
|
||
return self._box_aspect
|
||
|
||
def set_box_aspect(self, aspect=None):
|
||
"""
|
||
Set the Axes box aspect, i.e. the ratio of height to width.
|
||
|
||
This defines the aspect of the Axes in figure space and is not to be
|
||
confused with the data aspect (see `~.Axes.set_aspect`).
|
||
|
||
Parameters
|
||
----------
|
||
aspect : float or None
|
||
Changes the physical dimensions of the Axes, such that the ratio
|
||
of the Axes height to the Axes width in physical units is equal to
|
||
*aspect*. Defining a box aspect will change the *adjustable*
|
||
property to 'datalim' (see `~.Axes.set_adjustable`).
|
||
|
||
*None* will disable a fixed box aspect so that height and width
|
||
of the Axes are chosen independently.
|
||
|
||
See Also
|
||
--------
|
||
matplotlib.axes.Axes.set_aspect
|
||
for a description of aspect handling.
|
||
"""
|
||
axs = {*self._twinned_axes.get_siblings(self),
|
||
*self._twinned_axes.get_siblings(self)}
|
||
|
||
if aspect is not None:
|
||
aspect = float(aspect)
|
||
# when box_aspect is set to other than ´None`,
|
||
# adjustable must be "datalim"
|
||
for ax in axs:
|
||
ax.set_adjustable("datalim")
|
||
|
||
for ax in axs:
|
||
ax._box_aspect = aspect
|
||
ax.stale = True
|
||
|
||
def get_anchor(self):
|
||
"""
|
||
Get the anchor location.
|
||
|
||
See Also
|
||
--------
|
||
matplotlib.axes.Axes.set_anchor
|
||
for a description of the anchor.
|
||
matplotlib.axes.Axes.set_aspect
|
||
for a description of aspect handling.
|
||
"""
|
||
return self._anchor
|
||
|
||
def set_anchor(self, anchor, share=False):
|
||
"""
|
||
Define the anchor location.
|
||
|
||
The actual drawing area (active position) of the Axes may be smaller
|
||
than the Bbox (original position) when a fixed aspect is required. The
|
||
anchor defines where the drawing area will be located within the
|
||
available space.
|
||
|
||
Parameters
|
||
----------
|
||
anchor : (float, float) or {'C', 'SW', 'S', 'SE', 'E', 'NE', ...}
|
||
Either an (*x*, *y*) pair of relative coordinates (0 is left or
|
||
bottom, 1 is right or top), 'C' (center), or a cardinal direction
|
||
('SW', southwest, is bottom left, etc.). str inputs are shorthands
|
||
for (*x*, *y*) coordinates, as shown in the following diagram::
|
||
|
||
┌─────────────────┬─────────────────┬─────────────────┐
|
||
│ 'NW' (0.0, 1.0) │ 'N' (0.5, 1.0) │ 'NE' (1.0, 1.0) │
|
||
├─────────────────┼─────────────────┼─────────────────┤
|
||
│ 'W' (0.0, 0.5) │ 'C' (0.5, 0.5) │ 'E' (1.0, 0.5) │
|
||
├─────────────────┼─────────────────┼─────────────────┤
|
||
│ 'SW' (0.0, 0.0) │ 'S' (0.5, 0.0) │ 'SE' (1.0, 0.0) │
|
||
└─────────────────┴─────────────────┴─────────────────┘
|
||
|
||
share : bool, default: False
|
||
If ``True``, apply the settings to all shared Axes.
|
||
|
||
See Also
|
||
--------
|
||
matplotlib.axes.Axes.set_aspect
|
||
for a description of aspect handling.
|
||
"""
|
||
if not (anchor in mtransforms.Bbox.coefs or len(anchor) == 2):
|
||
raise ValueError('argument must be among %s' %
|
||
', '.join(mtransforms.Bbox.coefs))
|
||
if share:
|
||
axes = {sibling for name in self._axis_names
|
||
for sibling in self._shared_axes[name].get_siblings(self)}
|
||
else:
|
||
axes = [self]
|
||
for ax in axes:
|
||
ax._anchor = anchor
|
||
|
||
self.stale = True
|
||
|
||
def get_data_ratio(self):
|
||
"""
|
||
Return the aspect ratio of the scaled data.
|
||
|
||
Notes
|
||
-----
|
||
This method is intended to be overridden by new projection types.
|
||
"""
|
||
txmin, txmax = self.xaxis.get_transform().transform(self.get_xbound())
|
||
tymin, tymax = self.yaxis.get_transform().transform(self.get_ybound())
|
||
xsize = max(abs(txmax - txmin), 1e-30)
|
||
ysize = max(abs(tymax - tymin), 1e-30)
|
||
return ysize / xsize
|
||
|
||
def apply_aspect(self, position=None):
|
||
"""
|
||
Adjust the Axes for a specified data aspect ratio.
|
||
|
||
Depending on `.get_adjustable` this will modify either the
|
||
Axes box (position) or the view limits. In the former case,
|
||
`~matplotlib.axes.Axes.get_anchor` will affect the position.
|
||
|
||
Parameters
|
||
----------
|
||
position : None or .Bbox
|
||
If not ``None``, this defines the position of the
|
||
Axes within the figure as a Bbox. See `~.Axes.get_position`
|
||
for further details.
|
||
|
||
Notes
|
||
-----
|
||
This is called automatically when each Axes is drawn. You may need
|
||
to call it yourself if you need to update the Axes position and/or
|
||
view limits before the Figure is drawn.
|
||
|
||
See Also
|
||
--------
|
||
matplotlib.axes.Axes.set_aspect
|
||
For a description of aspect ratio handling.
|
||
matplotlib.axes.Axes.set_adjustable
|
||
Set how the Axes adjusts to achieve the required aspect ratio.
|
||
matplotlib.axes.Axes.set_anchor
|
||
Set the position in case of extra space.
|
||
"""
|
||
if position is None:
|
||
position = self.get_position(original=True)
|
||
|
||
aspect = self.get_aspect()
|
||
|
||
if aspect == 'auto' and self._box_aspect is None:
|
||
self._set_position(position, which='active')
|
||
return
|
||
|
||
trans = self.get_figure().transSubfigure
|
||
bb = mtransforms.Bbox.unit().transformed(trans)
|
||
# this is the physical aspect of the panel (or figure):
|
||
fig_aspect = bb.height / bb.width
|
||
|
||
if self._adjustable == 'box':
|
||
if self in self._twinned_axes:
|
||
raise RuntimeError("Adjustable 'box' is not allowed in a "
|
||
"twinned Axes; use 'datalim' instead")
|
||
box_aspect = aspect * self.get_data_ratio()
|
||
pb = position.frozen()
|
||
pb1 = pb.shrunk_to_aspect(box_aspect, pb, fig_aspect)
|
||
self._set_position(pb1.anchored(self.get_anchor(), pb), 'active')
|
||
return
|
||
|
||
# The following is only seen if self._adjustable == 'datalim'
|
||
if self._box_aspect is not None:
|
||
pb = position.frozen()
|
||
pb1 = pb.shrunk_to_aspect(self._box_aspect, pb, fig_aspect)
|
||
self._set_position(pb1.anchored(self.get_anchor(), pb), 'active')
|
||
if aspect == "auto":
|
||
return
|
||
|
||
# reset active to original in case it had been changed by prior use
|
||
# of 'box'
|
||
if self._box_aspect is None:
|
||
self._set_position(position, which='active')
|
||
else:
|
||
position = pb1.anchored(self.get_anchor(), pb)
|
||
|
||
x_trf = self.xaxis.get_transform()
|
||
y_trf = self.yaxis.get_transform()
|
||
xmin, xmax = x_trf.transform(self.get_xbound())
|
||
ymin, ymax = y_trf.transform(self.get_ybound())
|
||
xsize = max(abs(xmax - xmin), 1e-30)
|
||
ysize = max(abs(ymax - ymin), 1e-30)
|
||
|
||
box_aspect = fig_aspect * (position.height / position.width)
|
||
data_ratio = box_aspect / aspect
|
||
|
||
y_expander = data_ratio * xsize / ysize - 1
|
||
# If y_expander > 0, the dy/dx viewLim ratio needs to increase
|
||
if abs(y_expander) < 0.005:
|
||
return
|
||
|
||
dL = self.dataLim
|
||
x0, x1 = x_trf.transform(dL.intervalx)
|
||
y0, y1 = y_trf.transform(dL.intervaly)
|
||
xr = 1.05 * (x1 - x0)
|
||
yr = 1.05 * (y1 - y0)
|
||
|
||
xmarg = xsize - xr
|
||
ymarg = ysize - yr
|
||
Ysize = data_ratio * xsize
|
||
Xsize = ysize / data_ratio
|
||
Xmarg = Xsize - xr
|
||
Ymarg = Ysize - yr
|
||
# Setting these targets to, e.g., 0.05*xr does not seem to help.
|
||
xm = 0
|
||
ym = 0
|
||
|
||
shared_x = self in self._shared_axes["x"]
|
||
shared_y = self in self._shared_axes["y"]
|
||
|
||
if shared_x and shared_y:
|
||
raise RuntimeError("set_aspect(..., adjustable='datalim') or "
|
||
"axis('equal') are not allowed when both axes "
|
||
"are shared. Try set_aspect(..., "
|
||
"adjustable='box').")
|
||
|
||
# If y is shared, then we are only allowed to change x, etc.
|
||
if shared_y:
|
||
adjust_y = False
|
||
else:
|
||
if xmarg > xm and ymarg > ym:
|
||
adjy = ((Ymarg > 0 and y_expander < 0) or
|
||
(Xmarg < 0 and y_expander > 0))
|
||
else:
|
||
adjy = y_expander > 0
|
||
adjust_y = shared_x or adjy # (Ymarg > xmarg)
|
||
|
||
if adjust_y:
|
||
yc = 0.5 * (ymin + ymax)
|
||
y0 = yc - Ysize / 2.0
|
||
y1 = yc + Ysize / 2.0
|
||
self.set_ybound(y_trf.inverted().transform([y0, y1]))
|
||
else:
|
||
xc = 0.5 * (xmin + xmax)
|
||
x0 = xc - Xsize / 2.0
|
||
x1 = xc + Xsize / 2.0
|
||
self.set_xbound(x_trf.inverted().transform([x0, x1]))
|
||
|
||
def axis(self, arg=None, /, *, emit=True, **kwargs):
|
||
"""
|
||
Convenience method to get or set some axis properties.
|
||
|
||
Call signatures::
|
||
|
||
xmin, xmax, ymin, ymax = axis()
|
||
xmin, xmax, ymin, ymax = axis([xmin, xmax, ymin, ymax])
|
||
xmin, xmax, ymin, ymax = axis(option)
|
||
xmin, xmax, ymin, ymax = axis(**kwargs)
|
||
|
||
Parameters
|
||
----------
|
||
xmin, xmax, ymin, ymax : float, optional
|
||
The axis limits to be set. This can also be achieved using ::
|
||
|
||
ax.set(xlim=(xmin, xmax), ylim=(ymin, ymax))
|
||
|
||
option : bool or str
|
||
If a bool, turns axis lines and labels on or off. If a string,
|
||
possible values are:
|
||
|
||
================ ===========================================================
|
||
Value Description
|
||
================ ===========================================================
|
||
'off' or `False` Hide all axis decorations, i.e. axis labels, spines,
|
||
tick marks, tick labels, and grid lines.
|
||
This is the same as `~.Axes.set_axis_off()`.
|
||
'on' or `True` Do not hide all axis decorations, i.e. axis labels, spines,
|
||
tick marks, tick labels, and grid lines.
|
||
This is the same as `~.Axes.set_axis_on()`.
|
||
'equal' Set equal scaling (i.e., make circles circular) by
|
||
changing the axis limits. This is the same as
|
||
``ax.set_aspect('equal', adjustable='datalim')``.
|
||
Explicit data limits may not be respected in this case.
|
||
'scaled' Set equal scaling (i.e., make circles circular) by
|
||
changing dimensions of the plot box. This is the same as
|
||
``ax.set_aspect('equal', adjustable='box', anchor='C')``.
|
||
Additionally, further autoscaling will be disabled.
|
||
'tight' Set limits just large enough to show all data, then
|
||
disable further autoscaling.
|
||
'auto' Automatic scaling (fill plot box with data).
|
||
'image' 'scaled' with axis limits equal to data limits.
|
||
'square' Square plot; similar to 'scaled', but initially forcing
|
||
``xmax-xmin == ymax-ymin``.
|
||
================ ===========================================================
|
||
|
||
emit : bool, default: True
|
||
Whether observers are notified of the axis limit change.
|
||
This option is passed on to `~.Axes.set_xlim` and
|
||
`~.Axes.set_ylim`.
|
||
|
||
Returns
|
||
-------
|
||
xmin, xmax, ymin, ymax : float
|
||
The axis limits.
|
||
|
||
See Also
|
||
--------
|
||
matplotlib.axes.Axes.set_xlim
|
||
matplotlib.axes.Axes.set_ylim
|
||
|
||
Notes
|
||
-----
|
||
For 3D Axes, this method additionally takes *zmin*, *zmax* as
|
||
parameters and likewise returns them.
|
||
"""
|
||
if isinstance(arg, (str, bool)):
|
||
if arg is True:
|
||
arg = 'on'
|
||
if arg is False:
|
||
arg = 'off'
|
||
arg = arg.lower()
|
||
if arg == 'on':
|
||
self.set_axis_on()
|
||
elif arg == 'off':
|
||
self.set_axis_off()
|
||
elif arg in [
|
||
'equal', 'tight', 'scaled', 'auto', 'image', 'square']:
|
||
self.set_autoscale_on(True)
|
||
self.set_aspect('auto')
|
||
self.autoscale_view(tight=False)
|
||
if arg == 'equal':
|
||
self.set_aspect('equal', adjustable='datalim')
|
||
elif arg == 'scaled':
|
||
self.set_aspect('equal', adjustable='box', anchor='C')
|
||
self.set_autoscale_on(False) # Req. by Mark Bakker
|
||
elif arg == 'tight':
|
||
self.autoscale_view(tight=True)
|
||
self.set_autoscale_on(False)
|
||
elif arg == 'image':
|
||
self.autoscale_view(tight=True)
|
||
self.set_autoscale_on(False)
|
||
self.set_aspect('equal', adjustable='box', anchor='C')
|
||
elif arg == 'square':
|
||
self.set_aspect('equal', adjustable='box', anchor='C')
|
||
self.set_autoscale_on(False)
|
||
xlim = self.get_xlim()
|
||
ylim = self.get_ylim()
|
||
edge_size = max(np.diff(xlim), np.diff(ylim))[0]
|
||
self.set_xlim([xlim[0], xlim[0] + edge_size],
|
||
emit=emit, auto=False)
|
||
self.set_ylim([ylim[0], ylim[0] + edge_size],
|
||
emit=emit, auto=False)
|
||
else:
|
||
raise ValueError(f"Unrecognized string {arg!r} to axis; "
|
||
"try 'on' or 'off'")
|
||
else:
|
||
if arg is not None:
|
||
if len(arg) != 2*len(self._axis_names):
|
||
raise TypeError(
|
||
"The first argument to axis() must be an iterable of the form "
|
||
"[{}]".format(", ".join(
|
||
f"{name}min, {name}max" for name in self._axis_names)))
|
||
limits = {
|
||
name: arg[2*i:2*(i+1)]
|
||
for i, name in enumerate(self._axis_names)
|
||
}
|
||
else:
|
||
limits = {}
|
||
for name in self._axis_names:
|
||
ax_min = kwargs.pop(f'{name}min', None)
|
||
ax_max = kwargs.pop(f'{name}max', None)
|
||
limits[name] = (ax_min, ax_max)
|
||
for name, (ax_min, ax_max) in limits.items():
|
||
ax_auto = (None # Keep autoscale state as is.
|
||
if ax_min is None and ax_max is None
|
||
else False) # Turn off autoscale.
|
||
set_ax_lim = getattr(self, f'set_{name}lim')
|
||
set_ax_lim(ax_min, ax_max, emit=emit, auto=ax_auto)
|
||
if kwargs:
|
||
raise _api.kwarg_error("axis", kwargs)
|
||
lims = ()
|
||
for name in self._axis_names:
|
||
get_ax_lim = getattr(self, f'get_{name}lim')
|
||
lims += get_ax_lim()
|
||
return lims
|
||
|
||
def get_legend(self):
|
||
"""Return the `.Legend` instance, or None if no legend is defined."""
|
||
return self.legend_
|
||
|
||
def get_images(self):
|
||
r"""Return a list of `.AxesImage`\s contained by the Axes."""
|
||
return cbook.silent_list('AxesImage', self.images)
|
||
|
||
def get_lines(self):
|
||
"""Return a list of lines contained by the Axes."""
|
||
return cbook.silent_list('Line2D', self.lines)
|
||
|
||
def get_xaxis(self):
|
||
"""
|
||
[*Discouraged*] Return the XAxis instance.
|
||
|
||
.. admonition:: Discouraged
|
||
|
||
The use of this function is discouraged. You should instead
|
||
directly access the attribute ``ax.xaxis``.
|
||
"""
|
||
return self.xaxis
|
||
|
||
def get_yaxis(self):
|
||
"""
|
||
[*Discouraged*] Return the YAxis instance.
|
||
|
||
.. admonition:: Discouraged
|
||
|
||
The use of this function is discouraged. You should instead
|
||
directly access the attribute ``ax.yaxis``.
|
||
"""
|
||
return self.yaxis
|
||
|
||
get_xgridlines = _axis_method_wrapper("xaxis", "get_gridlines")
|
||
get_xticklines = _axis_method_wrapper("xaxis", "get_ticklines")
|
||
get_ygridlines = _axis_method_wrapper("yaxis", "get_gridlines")
|
||
get_yticklines = _axis_method_wrapper("yaxis", "get_ticklines")
|
||
|
||
# Adding and tracking artists
|
||
|
||
def _sci(self, im):
|
||
"""
|
||
Set the current image.
|
||
|
||
This image will be the target of colormap functions like
|
||
``pyplot.viridis``, and other functions such as `~.pyplot.clim`. The
|
||
current image is an attribute of the current Axes.
|
||
"""
|
||
_api.check_isinstance((mcoll.Collection, mimage.AxesImage), im=im)
|
||
if im not in self._children:
|
||
raise ValueError("Argument must be an image or collection in this Axes")
|
||
self._current_image = im
|
||
|
||
def _gci(self):
|
||
"""Helper for `~matplotlib.pyplot.gci`; do not use elsewhere."""
|
||
return self._current_image
|
||
|
||
def has_data(self):
|
||
"""
|
||
Return whether any artists have been added to the Axes.
|
||
|
||
This should not be used to determine whether the *dataLim*
|
||
need to be updated, and may not actually be useful for
|
||
anything.
|
||
"""
|
||
return any(isinstance(a, (mcoll.Collection, mimage.AxesImage,
|
||
mlines.Line2D, mpatches.Patch))
|
||
for a in self._children)
|
||
|
||
def add_artist(self, a):
|
||
"""
|
||
Add an `.Artist` to the Axes; return the artist.
|
||
|
||
Use `add_artist` only for artists for which there is no dedicated
|
||
"add" method; and if necessary, use a method such as `update_datalim`
|
||
to manually update the dataLim if the artist is to be included in
|
||
autoscaling.
|
||
|
||
If no ``transform`` has been specified when creating the artist (e.g.
|
||
``artist.get_transform() == None``) then the transform is set to
|
||
``ax.transData``.
|
||
"""
|
||
a.axes = self
|
||
self._children.append(a)
|
||
a._remove_method = self._children.remove
|
||
self._set_artist_props(a)
|
||
if a.get_clip_path() is None:
|
||
a.set_clip_path(self.patch)
|
||
self.stale = True
|
||
return a
|
||
|
||
def add_child_axes(self, ax):
|
||
"""
|
||
Add an `.AxesBase` to the Axes' children; return the child Axes.
|
||
|
||
This is the lowlevel version. See `.axes.Axes.inset_axes`.
|
||
"""
|
||
|
||
# normally Axes have themselves as the Axes, but these need to have
|
||
# their parent...
|
||
# Need to bypass the getter...
|
||
ax._axes = self
|
||
ax.stale_callback = martist._stale_axes_callback
|
||
|
||
self.child_axes.append(ax)
|
||
ax._remove_method = functools.partial(
|
||
self.figure._remove_axes, owners=[self.child_axes])
|
||
self.stale = True
|
||
return ax
|
||
|
||
def add_collection(self, collection, autolim=True):
|
||
"""
|
||
Add a `.Collection` to the Axes; return the collection.
|
||
"""
|
||
_api.check_isinstance(mcoll.Collection, collection=collection)
|
||
if not collection.get_label():
|
||
collection.set_label(f'_child{len(self._children)}')
|
||
self._children.append(collection)
|
||
collection._remove_method = self._children.remove
|
||
self._set_artist_props(collection)
|
||
|
||
if collection.get_clip_path() is None:
|
||
collection.set_clip_path(self.patch)
|
||
|
||
if autolim:
|
||
# Make sure viewLim is not stale (mostly to match
|
||
# pre-lazy-autoscale behavior, which is not really better).
|
||
self._unstale_viewLim()
|
||
datalim = collection.get_datalim(self.transData)
|
||
points = datalim.get_points()
|
||
if not np.isinf(datalim.minpos).all():
|
||
# By definition, if minpos (minimum positive value) is set
|
||
# (i.e., non-inf), then min(points) <= minpos <= max(points),
|
||
# and minpos would be superfluous. However, we add minpos to
|
||
# the call so that self.dataLim will update its own minpos.
|
||
# This ensures that log scales see the correct minimum.
|
||
points = np.concatenate([points, [datalim.minpos]])
|
||
self.update_datalim(points)
|
||
|
||
self.stale = True
|
||
return collection
|
||
|
||
def add_image(self, image):
|
||
"""
|
||
Add an `.AxesImage` to the Axes; return the image.
|
||
"""
|
||
_api.check_isinstance(mimage.AxesImage, image=image)
|
||
self._set_artist_props(image)
|
||
if not image.get_label():
|
||
image.set_label(f'_child{len(self._children)}')
|
||
self._children.append(image)
|
||
image._remove_method = self._children.remove
|
||
self.stale = True
|
||
return image
|
||
|
||
def _update_image_limits(self, image):
|
||
xmin, xmax, ymin, ymax = image.get_extent()
|
||
self.axes.update_datalim(((xmin, ymin), (xmax, ymax)))
|
||
|
||
def add_line(self, line):
|
||
"""
|
||
Add a `.Line2D` to the Axes; return the line.
|
||
"""
|
||
_api.check_isinstance(mlines.Line2D, line=line)
|
||
self._set_artist_props(line)
|
||
if line.get_clip_path() is None:
|
||
line.set_clip_path(self.patch)
|
||
|
||
self._update_line_limits(line)
|
||
if not line.get_label():
|
||
line.set_label(f'_child{len(self._children)}')
|
||
self._children.append(line)
|
||
line._remove_method = self._children.remove
|
||
self.stale = True
|
||
return line
|
||
|
||
def _add_text(self, txt):
|
||
"""
|
||
Add a `.Text` to the Axes; return the text.
|
||
"""
|
||
_api.check_isinstance(mtext.Text, txt=txt)
|
||
self._set_artist_props(txt)
|
||
self._children.append(txt)
|
||
txt._remove_method = self._children.remove
|
||
self.stale = True
|
||
return txt
|
||
|
||
def _update_line_limits(self, line):
|
||
"""
|
||
Figures out the data limit of the given line, updating self.dataLim.
|
||
"""
|
||
path = line.get_path()
|
||
if path.vertices.size == 0:
|
||
return
|
||
|
||
line_trf = line.get_transform()
|
||
|
||
if line_trf == self.transData:
|
||
data_path = path
|
||
elif any(line_trf.contains_branch_seperately(self.transData)):
|
||
# Compute the transform from line coordinates to data coordinates.
|
||
trf_to_data = line_trf - self.transData
|
||
# If transData is affine we can use the cached non-affine component
|
||
# of line's path (since the non-affine part of line_trf is
|
||
# entirely encapsulated in trf_to_data).
|
||
if self.transData.is_affine:
|
||
line_trans_path = line._get_transformed_path()
|
||
na_path, _ = line_trans_path.get_transformed_path_and_affine()
|
||
data_path = trf_to_data.transform_path_affine(na_path)
|
||
else:
|
||
data_path = trf_to_data.transform_path(path)
|
||
else:
|
||
# For backwards compatibility we update the dataLim with the
|
||
# coordinate range of the given path, even though the coordinate
|
||
# systems are completely different. This may occur in situations
|
||
# such as when ax.transAxes is passed through for absolute
|
||
# positioning.
|
||
data_path = path
|
||
|
||
if not data_path.vertices.size:
|
||
return
|
||
|
||
updatex, updatey = line_trf.contains_branch_seperately(self.transData)
|
||
if self.name != "rectilinear":
|
||
# This block is mostly intended to handle axvline in polar plots,
|
||
# for which updatey would otherwise be True.
|
||
if updatex and line_trf == self.get_yaxis_transform():
|
||
updatex = False
|
||
if updatey and line_trf == self.get_xaxis_transform():
|
||
updatey = False
|
||
self.dataLim.update_from_path(data_path,
|
||
self.ignore_existing_data_limits,
|
||
updatex=updatex, updatey=updatey)
|
||
self.ignore_existing_data_limits = False
|
||
|
||
def add_patch(self, p):
|
||
"""
|
||
Add a `.Patch` to the Axes; return the patch.
|
||
"""
|
||
_api.check_isinstance(mpatches.Patch, p=p)
|
||
self._set_artist_props(p)
|
||
if p.get_clip_path() is None:
|
||
p.set_clip_path(self.patch)
|
||
self._update_patch_limits(p)
|
||
self._children.append(p)
|
||
p._remove_method = self._children.remove
|
||
return p
|
||
|
||
def _update_patch_limits(self, patch):
|
||
"""Update the data limits for the given patch."""
|
||
# hist can add zero height Rectangles, which is useful to keep
|
||
# the bins, counts and patches lined up, but it throws off log
|
||
# scaling. We'll ignore rects with zero height or width in
|
||
# the auto-scaling
|
||
|
||
# cannot check for '==0' since unitized data may not compare to zero
|
||
# issue #2150 - we update the limits if patch has non zero width
|
||
# or height.
|
||
if (isinstance(patch, mpatches.Rectangle) and
|
||
((not patch.get_width()) and (not patch.get_height()))):
|
||
return
|
||
p = patch.get_path()
|
||
# Get all vertices on the path
|
||
# Loop through each segment to get extrema for Bezier curve sections
|
||
vertices = []
|
||
for curve, code in p.iter_bezier(simplify=False):
|
||
# Get distance along the curve of any extrema
|
||
_, dzeros = curve.axis_aligned_extrema()
|
||
# Calculate vertices of start, end and any extrema in between
|
||
vertices.append(curve([0, *dzeros, 1]))
|
||
|
||
if len(vertices):
|
||
vertices = np.vstack(vertices)
|
||
|
||
patch_trf = patch.get_transform()
|
||
updatex, updatey = patch_trf.contains_branch_seperately(self.transData)
|
||
if not (updatex or updatey):
|
||
return
|
||
if self.name != "rectilinear":
|
||
# As in _update_line_limits, but for axvspan.
|
||
if updatex and patch_trf == self.get_yaxis_transform():
|
||
updatex = False
|
||
if updatey and patch_trf == self.get_xaxis_transform():
|
||
updatey = False
|
||
trf_to_data = patch_trf - self.transData
|
||
xys = trf_to_data.transform(vertices)
|
||
self.update_datalim(xys, updatex=updatex, updatey=updatey)
|
||
|
||
def add_table(self, tab):
|
||
"""
|
||
Add a `.Table` to the Axes; return the table.
|
||
"""
|
||
_api.check_isinstance(mtable.Table, tab=tab)
|
||
self._set_artist_props(tab)
|
||
self._children.append(tab)
|
||
if tab.get_clip_path() is None:
|
||
tab.set_clip_path(self.patch)
|
||
tab._remove_method = self._children.remove
|
||
return tab
|
||
|
||
def add_container(self, container):
|
||
"""
|
||
Add a `.Container` to the Axes' containers; return the container.
|
||
"""
|
||
label = container.get_label()
|
||
if not label:
|
||
container.set_label('_container%d' % len(self.containers))
|
||
self.containers.append(container)
|
||
container._remove_method = self.containers.remove
|
||
return container
|
||
|
||
def _unit_change_handler(self, axis_name, event=None):
|
||
"""
|
||
Process axis units changes: requests updates to data and view limits.
|
||
"""
|
||
if event is None: # Allow connecting `self._unit_change_handler(name)`
|
||
return functools.partial(
|
||
self._unit_change_handler, axis_name, event=object())
|
||
_api.check_in_list(self._axis_map, axis_name=axis_name)
|
||
for line in self.lines:
|
||
line.recache_always()
|
||
self.relim()
|
||
self._request_autoscale_view(axis_name)
|
||
|
||
def relim(self, visible_only=False):
|
||
"""
|
||
Recompute the data limits based on current artists.
|
||
|
||
At present, `.Collection` instances are not supported.
|
||
|
||
Parameters
|
||
----------
|
||
visible_only : bool, default: False
|
||
Whether to exclude invisible artists.
|
||
"""
|
||
# Collections are deliberately not supported (yet); see
|
||
# the TODO note in artists.py.
|
||
self.dataLim.ignore(True)
|
||
self.dataLim.set_points(mtransforms.Bbox.null().get_points())
|
||
self.ignore_existing_data_limits = True
|
||
|
||
for artist in self._children:
|
||
if not visible_only or artist.get_visible():
|
||
if isinstance(artist, mlines.Line2D):
|
||
self._update_line_limits(artist)
|
||
elif isinstance(artist, mpatches.Patch):
|
||
self._update_patch_limits(artist)
|
||
elif isinstance(artist, mimage.AxesImage):
|
||
self._update_image_limits(artist)
|
||
|
||
def update_datalim(self, xys, updatex=True, updatey=True):
|
||
"""
|
||
Extend the `~.Axes.dataLim` Bbox to include the given points.
|
||
|
||
If no data is set currently, the Bbox will ignore its limits and set
|
||
the bound to be the bounds of the xydata (*xys*). Otherwise, it will
|
||
compute the bounds of the union of its current data and the data in
|
||
*xys*.
|
||
|
||
Parameters
|
||
----------
|
||
xys : 2D array-like
|
||
The points to include in the data limits Bbox. This can be either
|
||
a list of (x, y) tuples or a (N, 2) array.
|
||
|
||
updatex, updatey : bool, default: True
|
||
Whether to update the x/y limits.
|
||
"""
|
||
xys = np.asarray(xys)
|
||
if not np.any(np.isfinite(xys)):
|
||
return
|
||
self.dataLim.update_from_data_xy(xys, self.ignore_existing_data_limits,
|
||
updatex=updatex, updatey=updatey)
|
||
self.ignore_existing_data_limits = False
|
||
|
||
def _process_unit_info(self, datasets=None, kwargs=None, *, convert=True):
|
||
"""
|
||
Set axis units based on *datasets* and *kwargs*, and optionally apply
|
||
unit conversions to *datasets*.
|
||
|
||
Parameters
|
||
----------
|
||
datasets : list
|
||
List of (axis_name, dataset) pairs (where the axis name is defined
|
||
as in `._axis_map`). Individual datasets can also be None
|
||
(which gets passed through).
|
||
kwargs : dict
|
||
Other parameters from which unit info (i.e., the *xunits*,
|
||
*yunits*, *zunits* (for 3D Axes), *runits* and *thetaunits* (for
|
||
polar) entries) is popped, if present. Note that this dict is
|
||
mutated in-place!
|
||
convert : bool, default: True
|
||
Whether to return the original datasets or the converted ones.
|
||
|
||
Returns
|
||
-------
|
||
list
|
||
Either the original datasets if *convert* is False, or the
|
||
converted ones if *convert* is True (the default).
|
||
"""
|
||
# The API makes datasets a list of pairs rather than an axis_name to
|
||
# dataset mapping because it is sometimes necessary to process multiple
|
||
# datasets for a single axis, and concatenating them may be tricky
|
||
# (e.g. if some are scalars, etc.).
|
||
datasets = datasets or []
|
||
kwargs = kwargs or {}
|
||
axis_map = self._axis_map
|
||
for axis_name, data in datasets:
|
||
try:
|
||
axis = axis_map[axis_name]
|
||
except KeyError:
|
||
raise ValueError(f"Invalid axis name: {axis_name!r}") from None
|
||
# Update from data if axis is already set but no unit is set yet.
|
||
if axis is not None and data is not None and not axis.have_units():
|
||
axis.update_units(data)
|
||
for axis_name, axis in axis_map.items():
|
||
# Return if no axis is set.
|
||
if axis is None:
|
||
continue
|
||
# Check for units in the kwargs, and if present update axis.
|
||
units = kwargs.pop(f"{axis_name}units", axis.units)
|
||
if self.name == "polar":
|
||
# Special case: polar supports "thetaunits"/"runits".
|
||
polar_units = {"x": "thetaunits", "y": "runits"}
|
||
units = kwargs.pop(polar_units[axis_name], units)
|
||
if units != axis.units and units is not None:
|
||
axis.set_units(units)
|
||
# If the units being set imply a different converter,
|
||
# we need to update again.
|
||
for dataset_axis_name, data in datasets:
|
||
if dataset_axis_name == axis_name and data is not None:
|
||
axis.update_units(data)
|
||
return [axis_map[axis_name].convert_units(data)
|
||
if convert and data is not None else data
|
||
for axis_name, data in datasets]
|
||
|
||
def in_axes(self, mouseevent):
|
||
"""
|
||
Return whether the given event (in display coords) is in the Axes.
|
||
"""
|
||
return self.patch.contains(mouseevent)[0]
|
||
|
||
get_autoscalex_on = _axis_method_wrapper("xaxis", "_get_autoscale_on")
|
||
get_autoscaley_on = _axis_method_wrapper("yaxis", "_get_autoscale_on")
|
||
set_autoscalex_on = _axis_method_wrapper("xaxis", "_set_autoscale_on")
|
||
set_autoscaley_on = _axis_method_wrapper("yaxis", "_set_autoscale_on")
|
||
|
||
def get_autoscale_on(self):
|
||
"""Return True if each axis is autoscaled, False otherwise."""
|
||
return all(axis._get_autoscale_on()
|
||
for axis in self._axis_map.values())
|
||
|
||
def set_autoscale_on(self, b):
|
||
"""
|
||
Set whether autoscaling is applied to each axis on the next draw or
|
||
call to `.Axes.autoscale_view`.
|
||
|
||
Parameters
|
||
----------
|
||
b : bool
|
||
"""
|
||
for axis in self._axis_map.values():
|
||
axis._set_autoscale_on(b)
|
||
|
||
@property
|
||
def use_sticky_edges(self):
|
||
"""
|
||
When autoscaling, whether to obey all `Artist.sticky_edges`.
|
||
|
||
Default is ``True``.
|
||
|
||
Setting this to ``False`` ensures that the specified margins
|
||
will be applied, even if the plot includes an image, for
|
||
example, which would otherwise force a view limit to coincide
|
||
with its data limit.
|
||
|
||
The changing this property does not change the plot until
|
||
`autoscale` or `autoscale_view` is called.
|
||
"""
|
||
return self._use_sticky_edges
|
||
|
||
@use_sticky_edges.setter
|
||
def use_sticky_edges(self, b):
|
||
self._use_sticky_edges = bool(b)
|
||
# No effect until next autoscaling, which will mark the Axes as stale.
|
||
|
||
def get_xmargin(self):
|
||
"""
|
||
Retrieve autoscaling margin of the x-axis.
|
||
|
||
.. versionadded:: 3.9
|
||
|
||
Returns
|
||
-------
|
||
xmargin : float
|
||
|
||
See Also
|
||
--------
|
||
matplotlib.axes.Axes.set_xmargin
|
||
"""
|
||
return self._xmargin
|
||
|
||
def get_ymargin(self):
|
||
"""
|
||
Retrieve autoscaling margin of the y-axis.
|
||
|
||
.. versionadded:: 3.9
|
||
|
||
Returns
|
||
-------
|
||
ymargin : float
|
||
|
||
See Also
|
||
--------
|
||
matplotlib.axes.Axes.set_ymargin
|
||
"""
|
||
return self._ymargin
|
||
|
||
def set_xmargin(self, m):
|
||
"""
|
||
Set padding of X data limits prior to autoscaling.
|
||
|
||
*m* times the data interval will be added to each end of that interval
|
||
before it is used in autoscaling. If *m* is negative, this will clip
|
||
the data range instead of expanding it.
|
||
|
||
For example, if your data is in the range [0, 2], a margin of 0.1 will
|
||
result in a range [-0.2, 2.2]; a margin of -0.1 will result in a range
|
||
of [0.2, 1.8].
|
||
|
||
Parameters
|
||
----------
|
||
m : float greater than -0.5
|
||
"""
|
||
if m <= -0.5:
|
||
raise ValueError("margin must be greater than -0.5")
|
||
self._xmargin = m
|
||
self._request_autoscale_view("x")
|
||
self.stale = True
|
||
|
||
def set_ymargin(self, m):
|
||
"""
|
||
Set padding of Y data limits prior to autoscaling.
|
||
|
||
*m* times the data interval will be added to each end of that interval
|
||
before it is used in autoscaling. If *m* is negative, this will clip
|
||
the data range instead of expanding it.
|
||
|
||
For example, if your data is in the range [0, 2], a margin of 0.1 will
|
||
result in a range [-0.2, 2.2]; a margin of -0.1 will result in a range
|
||
of [0.2, 1.8].
|
||
|
||
Parameters
|
||
----------
|
||
m : float greater than -0.5
|
||
"""
|
||
if m <= -0.5:
|
||
raise ValueError("margin must be greater than -0.5")
|
||
self._ymargin = m
|
||
self._request_autoscale_view("y")
|
||
self.stale = True
|
||
|
||
def margins(self, *margins, x=None, y=None, tight=True):
|
||
"""
|
||
Set or retrieve autoscaling margins.
|
||
|
||
The padding added to each limit of the Axes is the *margin*
|
||
times the data interval. All input parameters must be floats
|
||
greater than -0.5. Passing both positional and keyword
|
||
arguments is invalid and will raise a TypeError. If no
|
||
arguments (positional or otherwise) are provided, the current
|
||
margins will remain unchanged and simply be returned.
|
||
|
||
Specifying any margin changes only the autoscaling; for example,
|
||
if *xmargin* is not None, then *xmargin* times the X data
|
||
interval will be added to each end of that interval before
|
||
it is used in autoscaling.
|
||
|
||
Parameters
|
||
----------
|
||
*margins : float, optional
|
||
If a single positional argument is provided, it specifies
|
||
both margins of the x-axis and y-axis limits. If two
|
||
positional arguments are provided, they will be interpreted
|
||
as *xmargin*, *ymargin*. If setting the margin on a single
|
||
axis is desired, use the keyword arguments described below.
|
||
|
||
x, y : float, optional
|
||
Specific margin values for the x-axis and y-axis,
|
||
respectively. These cannot be used with positional
|
||
arguments, but can be used individually to alter on e.g.,
|
||
only the y-axis.
|
||
|
||
tight : bool or None, default: True
|
||
The *tight* parameter is passed to `~.axes.Axes.autoscale_view`,
|
||
which is executed after a margin is changed; the default
|
||
here is *True*, on the assumption that when margins are
|
||
specified, no additional padding to match tick marks is
|
||
usually desired. Setting *tight* to *None* preserves
|
||
the previous setting.
|
||
|
||
Returns
|
||
-------
|
||
xmargin, ymargin : float
|
||
|
||
Notes
|
||
-----
|
||
If a previously used Axes method such as :meth:`pcolor` has set
|
||
:attr:`use_sticky_edges` to `True`, only the limits not set by
|
||
the "sticky artists" will be modified. To force all of the
|
||
margins to be set, set :attr:`use_sticky_edges` to `False`
|
||
before calling :meth:`margins`.
|
||
"""
|
||
|
||
if margins and (x is not None or y is not None):
|
||
raise TypeError('Cannot pass both positional and keyword '
|
||
'arguments for x and/or y.')
|
||
elif len(margins) == 1:
|
||
x = y = margins[0]
|
||
elif len(margins) == 2:
|
||
x, y = margins
|
||
elif margins:
|
||
raise TypeError('Must pass a single positional argument for all '
|
||
'margins, or one for each margin (x, y).')
|
||
|
||
if x is None and y is None:
|
||
if tight is not True:
|
||
_api.warn_external(f'ignoring tight={tight!r} in get mode')
|
||
return self._xmargin, self._ymargin
|
||
|
||
if tight is not None:
|
||
self._tight = tight
|
||
if x is not None:
|
||
self.set_xmargin(x)
|
||
if y is not None:
|
||
self.set_ymargin(y)
|
||
|
||
def set_rasterization_zorder(self, z):
|
||
"""
|
||
Set the zorder threshold for rasterization for vector graphics output.
|
||
|
||
All artists with a zorder below the given value will be rasterized if
|
||
they support rasterization.
|
||
|
||
This setting is ignored for pixel-based output.
|
||
|
||
See also :doc:`/gallery/misc/rasterization_demo`.
|
||
|
||
Parameters
|
||
----------
|
||
z : float or None
|
||
The zorder below which artists are rasterized.
|
||
If ``None`` rasterization based on zorder is deactivated.
|
||
"""
|
||
self._rasterization_zorder = z
|
||
self.stale = True
|
||
|
||
def get_rasterization_zorder(self):
|
||
"""Return the zorder value below which artists will be rasterized."""
|
||
return self._rasterization_zorder
|
||
|
||
def autoscale(self, enable=True, axis='both', tight=None):
|
||
"""
|
||
Autoscale the axis view to the data (toggle).
|
||
|
||
Convenience method for simple axis view autoscaling.
|
||
It turns autoscaling on or off, and then,
|
||
if autoscaling for either axis is on, it performs
|
||
the autoscaling on the specified axis or Axes.
|
||
|
||
Parameters
|
||
----------
|
||
enable : bool or None, default: True
|
||
True turns autoscaling on, False turns it off.
|
||
None leaves the autoscaling state unchanged.
|
||
axis : {'both', 'x', 'y'}, default: 'both'
|
||
The axis on which to operate. (For 3D Axes, *axis* can also be set
|
||
to 'z', and 'both' refers to all three Axes.)
|
||
tight : bool or None, default: None
|
||
If True, first set the margins to zero. Then, this argument is
|
||
forwarded to `~.axes.Axes.autoscale_view` (regardless of
|
||
its value); see the description of its behavior there.
|
||
"""
|
||
if enable is None:
|
||
scalex = True
|
||
scaley = True
|
||
else:
|
||
if axis in ['x', 'both']:
|
||
self.set_autoscalex_on(bool(enable))
|
||
scalex = self.get_autoscalex_on()
|
||
else:
|
||
scalex = False
|
||
if axis in ['y', 'both']:
|
||
self.set_autoscaley_on(bool(enable))
|
||
scaley = self.get_autoscaley_on()
|
||
else:
|
||
scaley = False
|
||
if tight and scalex:
|
||
self._xmargin = 0
|
||
if tight and scaley:
|
||
self._ymargin = 0
|
||
if scalex:
|
||
self._request_autoscale_view("x", tight=tight)
|
||
if scaley:
|
||
self._request_autoscale_view("y", tight=tight)
|
||
|
||
def autoscale_view(self, tight=None, scalex=True, scaley=True):
|
||
"""
|
||
Autoscale the view limits using the data limits.
|
||
|
||
Parameters
|
||
----------
|
||
tight : bool or None
|
||
If *True*, only expand the axis limits using the margins. Note
|
||
that unlike for `autoscale`, ``tight=True`` does *not* set the
|
||
margins to zero.
|
||
|
||
If *False* and :rc:`axes.autolimit_mode` is 'round_numbers', then
|
||
after expansion by the margins, further expand the axis limits
|
||
using the axis major locator.
|
||
|
||
If None (the default), reuse the value set in the previous call to
|
||
`autoscale_view` (the initial value is False, but the default style
|
||
sets :rc:`axes.autolimit_mode` to 'data', in which case this
|
||
behaves like True).
|
||
|
||
scalex : bool, default: True
|
||
Whether to autoscale the x-axis.
|
||
|
||
scaley : bool, default: True
|
||
Whether to autoscale the y-axis.
|
||
|
||
Notes
|
||
-----
|
||
The autoscaling preserves any preexisting axis direction reversal.
|
||
|
||
The data limits are not updated automatically when artist data are
|
||
changed after the artist has been added to an Axes instance. In that
|
||
case, use :meth:`matplotlib.axes.Axes.relim` prior to calling
|
||
autoscale_view.
|
||
|
||
If the views of the Axes are fixed, e.g. via `set_xlim`, they will
|
||
not be changed by autoscale_view().
|
||
See :meth:`matplotlib.axes.Axes.autoscale` for an alternative.
|
||
"""
|
||
if tight is not None:
|
||
self._tight = bool(tight)
|
||
|
||
x_stickies = y_stickies = np.array([])
|
||
if self.use_sticky_edges:
|
||
if self._xmargin and scalex and self.get_autoscalex_on():
|
||
x_stickies = np.sort(np.concatenate([
|
||
artist.sticky_edges.x
|
||
for ax in self._shared_axes["x"].get_siblings(self)
|
||
for artist in ax.get_children()]))
|
||
if self._ymargin and scaley and self.get_autoscaley_on():
|
||
y_stickies = np.sort(np.concatenate([
|
||
artist.sticky_edges.y
|
||
for ax in self._shared_axes["y"].get_siblings(self)
|
||
for artist in ax.get_children()]))
|
||
if self.get_xscale() == 'log':
|
||
x_stickies = x_stickies[x_stickies > 0]
|
||
if self.get_yscale() == 'log':
|
||
y_stickies = y_stickies[y_stickies > 0]
|
||
|
||
def handle_single_axis(
|
||
scale, shared_axes, name, axis, margin, stickies, set_bound):
|
||
|
||
if not (scale and axis._get_autoscale_on()):
|
||
return # nothing to do...
|
||
|
||
shared = shared_axes.get_siblings(self)
|
||
# Base autoscaling on finite data limits when there is at least one
|
||
# finite data limit among all the shared_axes and intervals.
|
||
values = [val for ax in shared
|
||
for val in getattr(ax.dataLim, f"interval{name}")
|
||
if np.isfinite(val)]
|
||
if values:
|
||
x0, x1 = (min(values), max(values))
|
||
elif getattr(self._viewLim, f"mutated{name}")():
|
||
# No data, but explicit viewLims already set:
|
||
# in mutatedx or mutatedy.
|
||
return
|
||
else:
|
||
x0, x1 = (-np.inf, np.inf)
|
||
# If x0 and x1 are nonfinite, get default limits from the locator.
|
||
locator = axis.get_major_locator()
|
||
x0, x1 = locator.nonsingular(x0, x1)
|
||
# Find the minimum minpos for use in the margin calculation.
|
||
minimum_minpos = min(
|
||
getattr(ax.dataLim, f"minpos{name}") for ax in shared)
|
||
|
||
# Prevent margin addition from crossing a sticky value. A small
|
||
# tolerance must be added due to floating point issues with
|
||
# streamplot; it is defined relative to x0, x1, x1-x0 but has
|
||
# no absolute term (e.g. "+1e-8") to avoid issues when working with
|
||
# datasets where all values are tiny (less than 1e-8).
|
||
tol = 1e-5 * max(abs(x0), abs(x1), abs(x1 - x0))
|
||
# Index of largest element < x0 + tol, if any.
|
||
i0 = stickies.searchsorted(x0 + tol) - 1
|
||
x0bound = stickies[i0] if i0 != -1 else None
|
||
# Index of smallest element > x1 - tol, if any.
|
||
i1 = stickies.searchsorted(x1 - tol)
|
||
x1bound = stickies[i1] if i1 != len(stickies) else None
|
||
|
||
# Add the margin in figure space and then transform back, to handle
|
||
# non-linear scales.
|
||
transform = axis.get_transform()
|
||
inverse_trans = transform.inverted()
|
||
x0, x1 = axis._scale.limit_range_for_scale(x0, x1, minimum_minpos)
|
||
x0t, x1t = transform.transform([x0, x1])
|
||
delta = (x1t - x0t) * margin
|
||
if not np.isfinite(delta):
|
||
delta = 0 # If a bound isn't finite, set margin to zero.
|
||
x0, x1 = inverse_trans.transform([x0t - delta, x1t + delta])
|
||
|
||
# Apply sticky bounds.
|
||
if x0bound is not None:
|
||
x0 = max(x0, x0bound)
|
||
if x1bound is not None:
|
||
x1 = min(x1, x1bound)
|
||
|
||
if not self._tight:
|
||
x0, x1 = locator.view_limits(x0, x1)
|
||
set_bound(x0, x1)
|
||
# End of definition of internal function 'handle_single_axis'.
|
||
|
||
handle_single_axis(
|
||
scalex, self._shared_axes["x"], 'x', self.xaxis, self._xmargin,
|
||
x_stickies, self.set_xbound)
|
||
handle_single_axis(
|
||
scaley, self._shared_axes["y"], 'y', self.yaxis, self._ymargin,
|
||
y_stickies, self.set_ybound)
|
||
|
||
def _update_title_position(self, renderer):
|
||
"""
|
||
Update the title position based on the bounding box enclosing
|
||
all the ticklabels and x-axis spine and xlabel...
|
||
"""
|
||
if self._autotitlepos is not None and not self._autotitlepos:
|
||
_log.debug('title position was updated manually, not adjusting')
|
||
return
|
||
|
||
titles = (self.title, self._left_title, self._right_title)
|
||
|
||
# Need to check all our twins too, aligned axes, and all the children
|
||
# as well.
|
||
axs = set()
|
||
axs.update(self.child_axes)
|
||
axs.update(self._twinned_axes.get_siblings(self))
|
||
axs.update(self.figure._align_label_groups['title'].get_siblings(self))
|
||
|
||
for ax in self.child_axes: # Child positions must be updated first.
|
||
locator = ax.get_axes_locator()
|
||
ax.apply_aspect(locator(self, renderer) if locator else None)
|
||
|
||
for title in titles:
|
||
x, _ = title.get_position()
|
||
# need to start again in case of window resizing
|
||
title.set_position((x, 1.0))
|
||
top = -np.inf
|
||
for ax in axs:
|
||
bb = None
|
||
if (ax.xaxis.get_ticks_position() in ['top', 'unknown']
|
||
or ax.xaxis.get_label_position() == 'top'):
|
||
bb = ax.xaxis.get_tightbbox(renderer)
|
||
if bb is None:
|
||
if 'outline' in ax.spines:
|
||
# Special case for colorbars:
|
||
bb = ax.spines['outline'].get_window_extent()
|
||
else:
|
||
bb = ax.get_window_extent(renderer)
|
||
top = max(top, bb.ymax)
|
||
if title.get_text():
|
||
ax.yaxis.get_tightbbox(renderer) # update offsetText
|
||
if ax.yaxis.offsetText.get_text():
|
||
bb = ax.yaxis.offsetText.get_tightbbox(renderer)
|
||
if bb.intersection(title.get_tightbbox(renderer), bb):
|
||
top = bb.ymax
|
||
if top < 0:
|
||
# the top of Axes is not even on the figure, so don't try and
|
||
# automatically place it.
|
||
_log.debug('top of Axes not in the figure, so title not moved')
|
||
return
|
||
if title.get_window_extent(renderer).ymin < top:
|
||
_, y = self.transAxes.inverted().transform((0, top))
|
||
title.set_position((x, y))
|
||
# empirically, this doesn't always get the min to top,
|
||
# so we need to adjust again.
|
||
if title.get_window_extent(renderer).ymin < top:
|
||
_, y = self.transAxes.inverted().transform(
|
||
(0., 2 * top - title.get_window_extent(renderer).ymin))
|
||
title.set_position((x, y))
|
||
|
||
ymax = max(title.get_position()[1] for title in titles)
|
||
for title in titles:
|
||
# now line up all the titles at the highest baseline.
|
||
x, _ = title.get_position()
|
||
title.set_position((x, ymax))
|
||
|
||
# Drawing
|
||
@martist.allow_rasterization
|
||
def draw(self, renderer):
|
||
# docstring inherited
|
||
if renderer is None:
|
||
raise RuntimeError('No renderer defined')
|
||
if not self.get_visible():
|
||
return
|
||
self._unstale_viewLim()
|
||
|
||
renderer.open_group('axes', gid=self.get_gid())
|
||
|
||
# prevent triggering call backs during the draw process
|
||
self._stale = True
|
||
|
||
# loop over self and child Axes...
|
||
locator = self.get_axes_locator()
|
||
self.apply_aspect(locator(self, renderer) if locator else None)
|
||
|
||
artists = self.get_children()
|
||
artists.remove(self.patch)
|
||
|
||
# the frame draws the edges around the Axes patch -- we
|
||
# decouple these so the patch can be in the background and the
|
||
# frame in the foreground. Do this before drawing the axis
|
||
# objects so that the spine has the opportunity to update them.
|
||
if not (self.axison and self._frameon):
|
||
for spine in self.spines.values():
|
||
artists.remove(spine)
|
||
|
||
self._update_title_position(renderer)
|
||
|
||
if not self.axison:
|
||
for _axis in self._axis_map.values():
|
||
artists.remove(_axis)
|
||
|
||
if not self.figure.canvas.is_saving():
|
||
artists = [
|
||
a for a in artists
|
||
if not a.get_animated() or isinstance(a, mimage.AxesImage)]
|
||
artists = sorted(artists, key=attrgetter('zorder'))
|
||
|
||
# rasterize artists with negative zorder
|
||
# if the minimum zorder is negative, start rasterization
|
||
rasterization_zorder = self._rasterization_zorder
|
||
|
||
if (rasterization_zorder is not None and
|
||
artists and artists[0].zorder < rasterization_zorder):
|
||
split_index = np.searchsorted(
|
||
[art.zorder for art in artists],
|
||
rasterization_zorder, side='right'
|
||
)
|
||
artists_rasterized = artists[:split_index]
|
||
artists = artists[split_index:]
|
||
else:
|
||
artists_rasterized = []
|
||
|
||
if self.axison and self._frameon:
|
||
if artists_rasterized:
|
||
artists_rasterized = [self.patch] + artists_rasterized
|
||
else:
|
||
artists = [self.patch] + artists
|
||
|
||
if artists_rasterized:
|
||
_draw_rasterized(self.figure, artists_rasterized, renderer)
|
||
|
||
mimage._draw_list_compositing_images(
|
||
renderer, self, artists, self.figure.suppressComposite)
|
||
|
||
renderer.close_group('axes')
|
||
self.stale = False
|
||
|
||
def draw_artist(self, a):
|
||
"""
|
||
Efficiently redraw a single artist.
|
||
"""
|
||
a.draw(self.figure.canvas.get_renderer())
|
||
|
||
def redraw_in_frame(self):
|
||
"""
|
||
Efficiently redraw Axes data, but not axis ticks, labels, etc.
|
||
"""
|
||
with ExitStack() as stack:
|
||
for artist in [*self._axis_map.values(),
|
||
self.title, self._left_title, self._right_title]:
|
||
stack.enter_context(artist._cm_set(visible=False))
|
||
self.draw(self.figure.canvas.get_renderer())
|
||
|
||
# Axes rectangle characteristics
|
||
|
||
def get_frame_on(self):
|
||
"""Get whether the Axes rectangle patch is drawn."""
|
||
return self._frameon
|
||
|
||
def set_frame_on(self, b):
|
||
"""
|
||
Set whether the Axes rectangle patch is drawn.
|
||
|
||
Parameters
|
||
----------
|
||
b : bool
|
||
"""
|
||
self._frameon = b
|
||
self.stale = True
|
||
|
||
def get_axisbelow(self):
|
||
"""
|
||
Get whether axis ticks and gridlines are above or below most artists.
|
||
|
||
Returns
|
||
-------
|
||
bool or 'line'
|
||
|
||
See Also
|
||
--------
|
||
set_axisbelow
|
||
"""
|
||
return self._axisbelow
|
||
|
||
def set_axisbelow(self, b):
|
||
"""
|
||
Set whether axis ticks and gridlines are above or below most artists.
|
||
|
||
This controls the zorder of the ticks and gridlines. For more
|
||
information on the zorder see :doc:`/gallery/misc/zorder_demo`.
|
||
|
||
Parameters
|
||
----------
|
||
b : bool or 'line'
|
||
Possible values:
|
||
|
||
- *True* (zorder = 0.5): Ticks and gridlines are below patches and
|
||
lines, though still above images.
|
||
- 'line' (zorder = 1.5): Ticks and gridlines are above patches
|
||
(e.g. rectangles, with default zorder = 1) but still below lines
|
||
and markers (with their default zorder = 2).
|
||
- *False* (zorder = 2.5): Ticks and gridlines are above patches
|
||
and lines / markers.
|
||
|
||
Notes
|
||
-----
|
||
For more control, call the `~.Artist.set_zorder` method of each axis.
|
||
|
||
See Also
|
||
--------
|
||
get_axisbelow
|
||
"""
|
||
# Check that b is True, False or 'line'
|
||
self._axisbelow = axisbelow = validate_axisbelow(b)
|
||
zorder = {
|
||
True: 0.5,
|
||
'line': 1.5,
|
||
False: 2.5,
|
||
}[axisbelow]
|
||
for axis in self._axis_map.values():
|
||
axis.set_zorder(zorder)
|
||
self.stale = True
|
||
|
||
@_docstring.dedent_interpd
|
||
def grid(self, visible=None, which='major', axis='both', **kwargs):
|
||
"""
|
||
Configure the grid lines.
|
||
|
||
Parameters
|
||
----------
|
||
visible : bool or None, optional
|
||
Whether to show the grid lines. If any *kwargs* are supplied, it
|
||
is assumed you want the grid on and *visible* will be set to True.
|
||
|
||
If *visible* is *None* and there are no *kwargs*, this toggles the
|
||
visibility of the lines.
|
||
|
||
which : {'major', 'minor', 'both'}, optional
|
||
The grid lines to apply the changes on.
|
||
|
||
axis : {'both', 'x', 'y'}, optional
|
||
The axis to apply the changes on.
|
||
|
||
**kwargs : `~matplotlib.lines.Line2D` properties
|
||
Define the line properties of the grid, e.g.::
|
||
|
||
grid(color='r', linestyle='-', linewidth=2)
|
||
|
||
Valid keyword arguments are:
|
||
|
||
%(Line2D:kwdoc)s
|
||
|
||
Notes
|
||
-----
|
||
The axis is drawn as a unit, so the effective zorder for drawing the
|
||
grid is determined by the zorder of each axis, not by the zorder of the
|
||
`.Line2D` objects comprising the grid. Therefore, to set grid zorder,
|
||
use `.set_axisbelow` or, for more control, call the
|
||
`~.Artist.set_zorder` method of each axis.
|
||
"""
|
||
_api.check_in_list(['x', 'y', 'both'], axis=axis)
|
||
if axis in ['x', 'both']:
|
||
self.xaxis.grid(visible, which=which, **kwargs)
|
||
if axis in ['y', 'both']:
|
||
self.yaxis.grid(visible, which=which, **kwargs)
|
||
|
||
def ticklabel_format(self, *, axis='both', style=None, scilimits=None,
|
||
useOffset=None, useLocale=None, useMathText=None):
|
||
r"""
|
||
Configure the `.ScalarFormatter` used by default for linear Axes.
|
||
|
||
If a parameter is not set, the corresponding property of the formatter
|
||
is left unchanged.
|
||
|
||
Parameters
|
||
----------
|
||
axis : {'x', 'y', 'both'}, default: 'both'
|
||
The axis to configure. Only major ticks are affected.
|
||
|
||
style : {'sci', 'scientific', 'plain'}
|
||
Whether to use scientific notation.
|
||
The formatter default is to use scientific notation.
|
||
'sci' is equivalent to 'scientific'.
|
||
|
||
scilimits : pair of ints (m, n)
|
||
Scientific notation is used only for numbers outside the range
|
||
10\ :sup:`m` to 10\ :sup:`n` (and only if the formatter is
|
||
configured to use scientific notation at all). Use (0, 0) to
|
||
include all numbers. Use (m, m) where m != 0 to fix the order of
|
||
magnitude to 10\ :sup:`m`.
|
||
The formatter default is :rc:`axes.formatter.limits`.
|
||
|
||
useOffset : bool or float
|
||
If True, the offset is calculated as needed.
|
||
If False, no offset is used.
|
||
If a numeric value, it sets the offset.
|
||
The formatter default is :rc:`axes.formatter.useoffset`.
|
||
|
||
useLocale : bool
|
||
Whether to format the number using the current locale or using the
|
||
C (English) locale. This affects e.g. the decimal separator. The
|
||
formatter default is :rc:`axes.formatter.use_locale`.
|
||
|
||
useMathText : bool
|
||
Render the offset and scientific notation in mathtext.
|
||
The formatter default is :rc:`axes.formatter.use_mathtext`.
|
||
|
||
Raises
|
||
------
|
||
AttributeError
|
||
If the current formatter is not a `.ScalarFormatter`.
|
||
"""
|
||
if isinstance(style, str):
|
||
style = style.lower()
|
||
axis = axis.lower()
|
||
if scilimits is not None:
|
||
try:
|
||
m, n = scilimits
|
||
m + n + 1 # check that both are numbers
|
||
except (ValueError, TypeError) as err:
|
||
raise ValueError("scilimits must be a sequence of 2 integers"
|
||
) from err
|
||
STYLES = {'sci': True, 'scientific': True, 'plain': False, '': None, None: None}
|
||
# The '' option is included for backwards-compatibility.
|
||
is_sci_style = _api.check_getitem(STYLES, style=style)
|
||
axis_map = {**{k: [v] for k, v in self._axis_map.items()},
|
||
'both': list(self._axis_map.values())}
|
||
axises = _api.check_getitem(axis_map, axis=axis)
|
||
try:
|
||
for axis in axises:
|
||
if is_sci_style is not None:
|
||
axis.major.formatter.set_scientific(is_sci_style)
|
||
if scilimits is not None:
|
||
axis.major.formatter.set_powerlimits(scilimits)
|
||
if useOffset is not None:
|
||
axis.major.formatter.set_useOffset(useOffset)
|
||
if useLocale is not None:
|
||
axis.major.formatter.set_useLocale(useLocale)
|
||
if useMathText is not None:
|
||
axis.major.formatter.set_useMathText(useMathText)
|
||
except AttributeError as err:
|
||
raise AttributeError(
|
||
"This method only works with the ScalarFormatter") from err
|
||
|
||
def locator_params(self, axis='both', tight=None, **kwargs):
|
||
"""
|
||
Control behavior of major tick locators.
|
||
|
||
Because the locator is involved in autoscaling, `~.Axes.autoscale_view`
|
||
is called automatically after the parameters are changed.
|
||
|
||
Parameters
|
||
----------
|
||
axis : {'both', 'x', 'y'}, default: 'both'
|
||
The axis on which to operate. (For 3D Axes, *axis* can also be
|
||
set to 'z', and 'both' refers to all three axes.)
|
||
tight : bool or None, optional
|
||
Parameter passed to `~.Axes.autoscale_view`.
|
||
Default is None, for no change.
|
||
|
||
Other Parameters
|
||
----------------
|
||
**kwargs
|
||
Remaining keyword arguments are passed to directly to the
|
||
``set_params()`` method of the locator. Supported keywords depend
|
||
on the type of the locator. See for example
|
||
`~.ticker.MaxNLocator.set_params` for the `.ticker.MaxNLocator`
|
||
used by default for linear.
|
||
|
||
Examples
|
||
--------
|
||
When plotting small subplots, one might want to reduce the maximum
|
||
number of ticks and use tight bounds, for example::
|
||
|
||
ax.locator_params(tight=True, nbins=4)
|
||
|
||
"""
|
||
_api.check_in_list([*self._axis_names, "both"], axis=axis)
|
||
for name in self._axis_names:
|
||
if axis in [name, "both"]:
|
||
loc = self._axis_map[name].get_major_locator()
|
||
loc.set_params(**kwargs)
|
||
self._request_autoscale_view(name, tight=tight)
|
||
self.stale = True
|
||
|
||
def tick_params(self, axis='both', **kwargs):
|
||
"""
|
||
Change the appearance of ticks, tick labels, and gridlines.
|
||
|
||
Tick properties that are not explicitly set using the keyword
|
||
arguments remain unchanged unless *reset* is True. For the current
|
||
style settings, see `.Axis.get_tick_params`.
|
||
|
||
Parameters
|
||
----------
|
||
axis : {'x', 'y', 'both'}, default: 'both'
|
||
The axis to which the parameters are applied.
|
||
which : {'major', 'minor', 'both'}, default: 'major'
|
||
The group of ticks to which the parameters are applied.
|
||
reset : bool, default: False
|
||
Whether to reset the ticks to defaults before updating them.
|
||
|
||
Other Parameters
|
||
----------------
|
||
direction : {'in', 'out', 'inout'}
|
||
Puts ticks inside the Axes, outside the Axes, or both.
|
||
length : float
|
||
Tick length in points.
|
||
width : float
|
||
Tick width in points.
|
||
color : :mpltype:`color`
|
||
Tick color.
|
||
pad : float
|
||
Distance in points between tick and label.
|
||
labelsize : float or str
|
||
Tick label font size in points or as a string (e.g., 'large').
|
||
labelcolor : :mpltype:`color`
|
||
Tick label color.
|
||
labelfontfamily : str
|
||
Tick label font.
|
||
colors : :mpltype:`color`
|
||
Tick color and label color.
|
||
zorder : float
|
||
Tick and label zorder.
|
||
bottom, top, left, right : bool
|
||
Whether to draw the respective ticks.
|
||
labelbottom, labeltop, labelleft, labelright : bool
|
||
Whether to draw the respective tick labels.
|
||
labelrotation : float
|
||
Tick label rotation
|
||
grid_color : :mpltype:`color`
|
||
Gridline color.
|
||
grid_alpha : float
|
||
Transparency of gridlines: 0 (transparent) to 1 (opaque).
|
||
grid_linewidth : float
|
||
Width of gridlines in points.
|
||
grid_linestyle : str
|
||
Any valid `.Line2D` line style spec.
|
||
|
||
Examples
|
||
--------
|
||
::
|
||
|
||
ax.tick_params(direction='out', length=6, width=2, colors='r',
|
||
grid_color='r', grid_alpha=0.5)
|
||
|
||
This will make all major ticks be red, pointing out of the box,
|
||
and with dimensions 6 points by 2 points. Tick labels will
|
||
also be red. Gridlines will be red and translucent.
|
||
|
||
"""
|
||
_api.check_in_list(['x', 'y', 'both'], axis=axis)
|
||
if axis in ['x', 'both']:
|
||
xkw = dict(kwargs)
|
||
xkw.pop('left', None)
|
||
xkw.pop('right', None)
|
||
xkw.pop('labelleft', None)
|
||
xkw.pop('labelright', None)
|
||
self.xaxis.set_tick_params(**xkw)
|
||
if axis in ['y', 'both']:
|
||
ykw = dict(kwargs)
|
||
ykw.pop('top', None)
|
||
ykw.pop('bottom', None)
|
||
ykw.pop('labeltop', None)
|
||
ykw.pop('labelbottom', None)
|
||
self.yaxis.set_tick_params(**ykw)
|
||
|
||
def set_axis_off(self):
|
||
"""
|
||
Hide all visual components of the x- and y-axis.
|
||
|
||
This sets a flag to suppress drawing of all axis decorations, i.e.
|
||
axis labels, axis spines, and the axis tick component (tick markers,
|
||
tick labels, and grid lines). Individual visibility settings of these
|
||
components are ignored as long as `set_axis_off()` is in effect.
|
||
"""
|
||
self.axison = False
|
||
self.stale = True
|
||
|
||
def set_axis_on(self):
|
||
"""
|
||
Do not hide all visual components of the x- and y-axis.
|
||
|
||
This reverts the effect of a prior `.set_axis_off()` call. Whether the
|
||
individual axis decorations are drawn is controlled by their respective
|
||
visibility settings.
|
||
|
||
This is on by default.
|
||
"""
|
||
self.axison = True
|
||
self.stale = True
|
||
|
||
# data limits, ticks, tick labels, and formatting
|
||
|
||
def get_xlabel(self):
|
||
"""
|
||
Get the xlabel text string.
|
||
"""
|
||
label = self.xaxis.get_label()
|
||
return label.get_text()
|
||
|
||
def set_xlabel(self, xlabel, fontdict=None, labelpad=None, *,
|
||
loc=None, **kwargs):
|
||
"""
|
||
Set the label for the x-axis.
|
||
|
||
Parameters
|
||
----------
|
||
xlabel : str
|
||
The label text.
|
||
|
||
labelpad : float, default: :rc:`axes.labelpad`
|
||
Spacing in points from the Axes bounding box including ticks
|
||
and tick labels. If None, the previous value is left as is.
|
||
|
||
loc : {'left', 'center', 'right'}, default: :rc:`xaxis.labellocation`
|
||
The label position. This is a high-level alternative for passing
|
||
parameters *x* and *horizontalalignment*.
|
||
|
||
Other Parameters
|
||
----------------
|
||
**kwargs : `~matplotlib.text.Text` properties
|
||
`.Text` properties control the appearance of the label.
|
||
|
||
See Also
|
||
--------
|
||
text : Documents the properties supported by `.Text`.
|
||
"""
|
||
if labelpad is not None:
|
||
self.xaxis.labelpad = labelpad
|
||
protected_kw = ['x', 'horizontalalignment', 'ha']
|
||
if {*kwargs} & {*protected_kw}:
|
||
if loc is not None:
|
||
raise TypeError(f"Specifying 'loc' is disallowed when any of "
|
||
f"its corresponding low level keyword "
|
||
f"arguments ({protected_kw}) are also "
|
||
f"supplied")
|
||
|
||
else:
|
||
loc = (loc if loc is not None
|
||
else mpl.rcParams['xaxis.labellocation'])
|
||
_api.check_in_list(('left', 'center', 'right'), loc=loc)
|
||
|
||
x = {
|
||
'left': 0,
|
||
'center': 0.5,
|
||
'right': 1,
|
||
}[loc]
|
||
kwargs.update(x=x, horizontalalignment=loc)
|
||
|
||
return self.xaxis.set_label_text(xlabel, fontdict, **kwargs)
|
||
|
||
def invert_xaxis(self):
|
||
"""
|
||
Invert the x-axis.
|
||
|
||
See Also
|
||
--------
|
||
xaxis_inverted
|
||
get_xlim, set_xlim
|
||
get_xbound, set_xbound
|
||
"""
|
||
self.xaxis.set_inverted(not self.xaxis.get_inverted())
|
||
|
||
xaxis_inverted = _axis_method_wrapper("xaxis", "get_inverted")
|
||
|
||
def get_xbound(self):
|
||
"""
|
||
Return the lower and upper x-axis bounds, in increasing order.
|
||
|
||
See Also
|
||
--------
|
||
set_xbound
|
||
get_xlim, set_xlim
|
||
invert_xaxis, xaxis_inverted
|
||
"""
|
||
left, right = self.get_xlim()
|
||
if left < right:
|
||
return left, right
|
||
else:
|
||
return right, left
|
||
|
||
def set_xbound(self, lower=None, upper=None):
|
||
"""
|
||
Set the lower and upper numerical bounds of the x-axis.
|
||
|
||
This method will honor axis inversion regardless of parameter order.
|
||
It will not change the autoscaling setting (`.get_autoscalex_on()`).
|
||
|
||
Parameters
|
||
----------
|
||
lower, upper : float or None
|
||
The lower and upper bounds. If *None*, the respective axis bound
|
||
is not modified.
|
||
|
||
.. ACCEPTS: (lower: float, upper: float)
|
||
|
||
See Also
|
||
--------
|
||
get_xbound
|
||
get_xlim, set_xlim
|
||
invert_xaxis, xaxis_inverted
|
||
"""
|
||
if upper is None and np.iterable(lower):
|
||
lower, upper = lower
|
||
|
||
old_lower, old_upper = self.get_xbound()
|
||
if lower is None:
|
||
lower = old_lower
|
||
if upper is None:
|
||
upper = old_upper
|
||
|
||
self.set_xlim(sorted((lower, upper),
|
||
reverse=bool(self.xaxis_inverted())),
|
||
auto=None)
|
||
|
||
def get_xlim(self):
|
||
"""
|
||
Return the x-axis view limits.
|
||
|
||
Returns
|
||
-------
|
||
left, right : (float, float)
|
||
The current x-axis limits in data coordinates.
|
||
|
||
See Also
|
||
--------
|
||
.Axes.set_xlim
|
||
.Axes.set_xbound, .Axes.get_xbound
|
||
.Axes.invert_xaxis, .Axes.xaxis_inverted
|
||
|
||
Notes
|
||
-----
|
||
The x-axis may be inverted, in which case the *left* value will
|
||
be greater than the *right* value.
|
||
"""
|
||
return tuple(self.viewLim.intervalx)
|
||
|
||
def _validate_converted_limits(self, limit, convert):
|
||
"""
|
||
Raise ValueError if converted limits are non-finite.
|
||
|
||
Note that this function also accepts None as a limit argument.
|
||
|
||
Returns
|
||
-------
|
||
The limit value after call to convert(), or None if limit is None.
|
||
"""
|
||
if limit is not None:
|
||
converted_limit = convert(limit)
|
||
if isinstance(converted_limit, np.ndarray):
|
||
converted_limit = converted_limit.squeeze()
|
||
if (isinstance(converted_limit, Real)
|
||
and not np.isfinite(converted_limit)):
|
||
raise ValueError("Axis limits cannot be NaN or Inf")
|
||
return converted_limit
|
||
|
||
def set_xlim(self, left=None, right=None, *, emit=True, auto=False,
|
||
xmin=None, xmax=None):
|
||
"""
|
||
Set the x-axis view limits.
|
||
|
||
Parameters
|
||
----------
|
||
left : float, optional
|
||
The left xlim in data coordinates. Passing *None* leaves the
|
||
limit unchanged.
|
||
|
||
The left and right xlims may also be passed as the tuple
|
||
(*left*, *right*) as the first positional argument (or as
|
||
the *left* keyword argument).
|
||
|
||
.. ACCEPTS: (left: float, right: float)
|
||
|
||
right : float, optional
|
||
The right xlim in data coordinates. Passing *None* leaves the
|
||
limit unchanged.
|
||
|
||
emit : bool, default: True
|
||
Whether to notify observers of limit change.
|
||
|
||
auto : bool or None, default: False
|
||
Whether to turn on autoscaling of the x-axis. True turns on,
|
||
False turns off, None leaves unchanged.
|
||
|
||
xmin, xmax : float, optional
|
||
They are equivalent to left and right respectively, and it is an
|
||
error to pass both *xmin* and *left* or *xmax* and *right*.
|
||
|
||
Returns
|
||
-------
|
||
left, right : (float, float)
|
||
The new x-axis limits in data coordinates.
|
||
|
||
See Also
|
||
--------
|
||
get_xlim
|
||
set_xbound, get_xbound
|
||
invert_xaxis, xaxis_inverted
|
||
|
||
Notes
|
||
-----
|
||
The *left* value may be greater than the *right* value, in which
|
||
case the x-axis values will decrease from left to right.
|
||
|
||
Examples
|
||
--------
|
||
>>> set_xlim(left, right)
|
||
>>> set_xlim((left, right))
|
||
>>> left, right = set_xlim(left, right)
|
||
|
||
One limit may be left unchanged.
|
||
|
||
>>> set_xlim(right=right_lim)
|
||
|
||
Limits may be passed in reverse order to flip the direction of
|
||
the x-axis. For example, suppose *x* represents the number of
|
||
years before present. The x-axis limits might be set like the
|
||
following so 5000 years ago is on the left of the plot and the
|
||
present is on the right.
|
||
|
||
>>> set_xlim(5000, 0)
|
||
"""
|
||
if right is None and np.iterable(left):
|
||
left, right = left
|
||
if xmin is not None:
|
||
if left is not None:
|
||
raise TypeError("Cannot pass both 'left' and 'xmin'")
|
||
left = xmin
|
||
if xmax is not None:
|
||
if right is not None:
|
||
raise TypeError("Cannot pass both 'right' and 'xmax'")
|
||
right = xmax
|
||
return self.xaxis._set_lim(left, right, emit=emit, auto=auto)
|
||
|
||
get_xscale = _axis_method_wrapper("xaxis", "get_scale")
|
||
set_xscale = _axis_method_wrapper("xaxis", "_set_axes_scale")
|
||
get_xticks = _axis_method_wrapper("xaxis", "get_ticklocs")
|
||
set_xticks = _axis_method_wrapper("xaxis", "set_ticks",
|
||
doc_sub={'set_ticks': 'set_xticks'})
|
||
get_xmajorticklabels = _axis_method_wrapper("xaxis", "get_majorticklabels")
|
||
get_xminorticklabels = _axis_method_wrapper("xaxis", "get_minorticklabels")
|
||
get_xticklabels = _axis_method_wrapper("xaxis", "get_ticklabels")
|
||
set_xticklabels = _axis_method_wrapper(
|
||
"xaxis", "set_ticklabels",
|
||
doc_sub={"Axis.set_ticks": "Axes.set_xticks"})
|
||
|
||
def get_ylabel(self):
|
||
"""
|
||
Get the ylabel text string.
|
||
"""
|
||
label = self.yaxis.get_label()
|
||
return label.get_text()
|
||
|
||
def set_ylabel(self, ylabel, fontdict=None, labelpad=None, *,
|
||
loc=None, **kwargs):
|
||
"""
|
||
Set the label for the y-axis.
|
||
|
||
Parameters
|
||
----------
|
||
ylabel : str
|
||
The label text.
|
||
|
||
labelpad : float, default: :rc:`axes.labelpad`
|
||
Spacing in points from the Axes bounding box including ticks
|
||
and tick labels. If None, the previous value is left as is.
|
||
|
||
loc : {'bottom', 'center', 'top'}, default: :rc:`yaxis.labellocation`
|
||
The label position. This is a high-level alternative for passing
|
||
parameters *y* and *horizontalalignment*.
|
||
|
||
Other Parameters
|
||
----------------
|
||
**kwargs : `~matplotlib.text.Text` properties
|
||
`.Text` properties control the appearance of the label.
|
||
|
||
See Also
|
||
--------
|
||
text : Documents the properties supported by `.Text`.
|
||
"""
|
||
if labelpad is not None:
|
||
self.yaxis.labelpad = labelpad
|
||
protected_kw = ['y', 'horizontalalignment', 'ha']
|
||
if {*kwargs} & {*protected_kw}:
|
||
if loc is not None:
|
||
raise TypeError(f"Specifying 'loc' is disallowed when any of "
|
||
f"its corresponding low level keyword "
|
||
f"arguments ({protected_kw}) are also "
|
||
f"supplied")
|
||
|
||
else:
|
||
loc = (loc if loc is not None
|
||
else mpl.rcParams['yaxis.labellocation'])
|
||
_api.check_in_list(('bottom', 'center', 'top'), loc=loc)
|
||
|
||
y, ha = {
|
||
'bottom': (0, 'left'),
|
||
'center': (0.5, 'center'),
|
||
'top': (1, 'right')
|
||
}[loc]
|
||
kwargs.update(y=y, horizontalalignment=ha)
|
||
|
||
return self.yaxis.set_label_text(ylabel, fontdict, **kwargs)
|
||
|
||
def invert_yaxis(self):
|
||
"""
|
||
Invert the y-axis.
|
||
|
||
See Also
|
||
--------
|
||
yaxis_inverted
|
||
get_ylim, set_ylim
|
||
get_ybound, set_ybound
|
||
"""
|
||
self.yaxis.set_inverted(not self.yaxis.get_inverted())
|
||
|
||
yaxis_inverted = _axis_method_wrapper("yaxis", "get_inverted")
|
||
|
||
def get_ybound(self):
|
||
"""
|
||
Return the lower and upper y-axis bounds, in increasing order.
|
||
|
||
See Also
|
||
--------
|
||
set_ybound
|
||
get_ylim, set_ylim
|
||
invert_yaxis, yaxis_inverted
|
||
"""
|
||
bottom, top = self.get_ylim()
|
||
if bottom < top:
|
||
return bottom, top
|
||
else:
|
||
return top, bottom
|
||
|
||
def set_ybound(self, lower=None, upper=None):
|
||
"""
|
||
Set the lower and upper numerical bounds of the y-axis.
|
||
|
||
This method will honor axis inversion regardless of parameter order.
|
||
It will not change the autoscaling setting (`.get_autoscaley_on()`).
|
||
|
||
Parameters
|
||
----------
|
||
lower, upper : float or None
|
||
The lower and upper bounds. If *None*, the respective axis bound
|
||
is not modified.
|
||
|
||
.. ACCEPTS: (lower: float, upper: float)
|
||
|
||
See Also
|
||
--------
|
||
get_ybound
|
||
get_ylim, set_ylim
|
||
invert_yaxis, yaxis_inverted
|
||
"""
|
||
if upper is None and np.iterable(lower):
|
||
lower, upper = lower
|
||
|
||
old_lower, old_upper = self.get_ybound()
|
||
if lower is None:
|
||
lower = old_lower
|
||
if upper is None:
|
||
upper = old_upper
|
||
|
||
self.set_ylim(sorted((lower, upper),
|
||
reverse=bool(self.yaxis_inverted())),
|
||
auto=None)
|
||
|
||
def get_ylim(self):
|
||
"""
|
||
Return the y-axis view limits.
|
||
|
||
Returns
|
||
-------
|
||
bottom, top : (float, float)
|
||
The current y-axis limits in data coordinates.
|
||
|
||
See Also
|
||
--------
|
||
.Axes.set_ylim
|
||
.Axes.set_ybound, .Axes.get_ybound
|
||
.Axes.invert_yaxis, .Axes.yaxis_inverted
|
||
|
||
Notes
|
||
-----
|
||
The y-axis may be inverted, in which case the *bottom* value
|
||
will be greater than the *top* value.
|
||
"""
|
||
return tuple(self.viewLim.intervaly)
|
||
|
||
def set_ylim(self, bottom=None, top=None, *, emit=True, auto=False,
|
||
ymin=None, ymax=None):
|
||
"""
|
||
Set the y-axis view limits.
|
||
|
||
Parameters
|
||
----------
|
||
bottom : float, optional
|
||
The bottom ylim in data coordinates. Passing *None* leaves the
|
||
limit unchanged.
|
||
|
||
The bottom and top ylims may also be passed as the tuple
|
||
(*bottom*, *top*) as the first positional argument (or as
|
||
the *bottom* keyword argument).
|
||
|
||
.. ACCEPTS: (bottom: float, top: float)
|
||
|
||
top : float, optional
|
||
The top ylim in data coordinates. Passing *None* leaves the
|
||
limit unchanged.
|
||
|
||
emit : bool, default: True
|
||
Whether to notify observers of limit change.
|
||
|
||
auto : bool or None, default: False
|
||
Whether to turn on autoscaling of the y-axis. *True* turns on,
|
||
*False* turns off, *None* leaves unchanged.
|
||
|
||
ymin, ymax : float, optional
|
||
They are equivalent to bottom and top respectively, and it is an
|
||
error to pass both *ymin* and *bottom* or *ymax* and *top*.
|
||
|
||
Returns
|
||
-------
|
||
bottom, top : (float, float)
|
||
The new y-axis limits in data coordinates.
|
||
|
||
See Also
|
||
--------
|
||
get_ylim
|
||
set_ybound, get_ybound
|
||
invert_yaxis, yaxis_inverted
|
||
|
||
Notes
|
||
-----
|
||
The *bottom* value may be greater than the *top* value, in which
|
||
case the y-axis values will decrease from *bottom* to *top*.
|
||
|
||
Examples
|
||
--------
|
||
>>> set_ylim(bottom, top)
|
||
>>> set_ylim((bottom, top))
|
||
>>> bottom, top = set_ylim(bottom, top)
|
||
|
||
One limit may be left unchanged.
|
||
|
||
>>> set_ylim(top=top_lim)
|
||
|
||
Limits may be passed in reverse order to flip the direction of
|
||
the y-axis. For example, suppose ``y`` represents depth of the
|
||
ocean in m. The y-axis limits might be set like the following
|
||
so 5000 m depth is at the bottom of the plot and the surface,
|
||
0 m, is at the top.
|
||
|
||
>>> set_ylim(5000, 0)
|
||
"""
|
||
if top is None and np.iterable(bottom):
|
||
bottom, top = bottom
|
||
if ymin is not None:
|
||
if bottom is not None:
|
||
raise TypeError("Cannot pass both 'bottom' and 'ymin'")
|
||
bottom = ymin
|
||
if ymax is not None:
|
||
if top is not None:
|
||
raise TypeError("Cannot pass both 'top' and 'ymax'")
|
||
top = ymax
|
||
return self.yaxis._set_lim(bottom, top, emit=emit, auto=auto)
|
||
|
||
get_yscale = _axis_method_wrapper("yaxis", "get_scale")
|
||
set_yscale = _axis_method_wrapper("yaxis", "_set_axes_scale")
|
||
get_yticks = _axis_method_wrapper("yaxis", "get_ticklocs")
|
||
set_yticks = _axis_method_wrapper("yaxis", "set_ticks",
|
||
doc_sub={'set_ticks': 'set_yticks'})
|
||
get_ymajorticklabels = _axis_method_wrapper("yaxis", "get_majorticklabels")
|
||
get_yminorticklabels = _axis_method_wrapper("yaxis", "get_minorticklabels")
|
||
get_yticklabels = _axis_method_wrapper("yaxis", "get_ticklabels")
|
||
set_yticklabels = _axis_method_wrapper(
|
||
"yaxis", "set_ticklabels",
|
||
doc_sub={"Axis.set_ticks": "Axes.set_yticks"})
|
||
|
||
xaxis_date = _axis_method_wrapper("xaxis", "axis_date")
|
||
yaxis_date = _axis_method_wrapper("yaxis", "axis_date")
|
||
|
||
def format_xdata(self, x):
|
||
"""
|
||
Return *x* formatted as an x-value.
|
||
|
||
This function will use the `.fmt_xdata` attribute if it is not None,
|
||
else will fall back on the xaxis major formatter.
|
||
"""
|
||
return (self.fmt_xdata if self.fmt_xdata is not None
|
||
else self.xaxis.get_major_formatter().format_data_short)(x)
|
||
|
||
def format_ydata(self, y):
|
||
"""
|
||
Return *y* formatted as a y-value.
|
||
|
||
This function will use the `.fmt_ydata` attribute if it is not None,
|
||
else will fall back on the yaxis major formatter.
|
||
"""
|
||
return (self.fmt_ydata if self.fmt_ydata is not None
|
||
else self.yaxis.get_major_formatter().format_data_short)(y)
|
||
|
||
def format_coord(self, x, y):
|
||
"""Return a format string formatting the *x*, *y* coordinates."""
|
||
twins = self._twinned_axes.get_siblings(self)
|
||
if len(twins) == 1:
|
||
return "(x, y) = ({}, {})".format(
|
||
"???" if x is None else self.format_xdata(x),
|
||
"???" if y is None else self.format_ydata(y))
|
||
screen_xy = self.transData.transform((x, y))
|
||
xy_strs = []
|
||
# Retrieve twins in the order of self.figure.axes to sort tied zorders (which is
|
||
# the common case) by the order in which they are added to the figure.
|
||
for ax in sorted(twins, key=attrgetter("zorder")):
|
||
data_x, data_y = ax.transData.inverted().transform(screen_xy)
|
||
xy_strs.append(
|
||
"({}, {})".format(ax.format_xdata(data_x), ax.format_ydata(data_y)))
|
||
return "(x, y) = {}".format(" | ".join(xy_strs))
|
||
|
||
def minorticks_on(self):
|
||
"""
|
||
Display minor ticks on the Axes.
|
||
|
||
Displaying minor ticks may reduce performance; you may turn them off
|
||
using `minorticks_off()` if drawing speed is a problem.
|
||
"""
|
||
self.xaxis.minorticks_on()
|
||
self.yaxis.minorticks_on()
|
||
|
||
def minorticks_off(self):
|
||
"""Remove minor ticks from the Axes."""
|
||
self.xaxis.minorticks_off()
|
||
self.yaxis.minorticks_off()
|
||
|
||
# Interactive manipulation
|
||
|
||
def can_zoom(self):
|
||
"""
|
||
Return whether this Axes supports the zoom box button functionality.
|
||
"""
|
||
return True
|
||
|
||
def can_pan(self):
|
||
"""
|
||
Return whether this Axes supports any pan/zoom button functionality.
|
||
"""
|
||
return True
|
||
|
||
def get_navigate(self):
|
||
"""
|
||
Get whether the Axes responds to navigation commands.
|
||
"""
|
||
return self._navigate
|
||
|
||
def set_navigate(self, b):
|
||
"""
|
||
Set whether the Axes responds to navigation toolbar commands.
|
||
|
||
Parameters
|
||
----------
|
||
b : bool
|
||
|
||
See Also
|
||
--------
|
||
matplotlib.axes.Axes.set_forward_navigation_events
|
||
|
||
"""
|
||
self._navigate = b
|
||
|
||
def get_navigate_mode(self):
|
||
"""
|
||
Get the navigation toolbar button status: 'PAN', 'ZOOM', or None.
|
||
"""
|
||
return self._navigate_mode
|
||
|
||
def set_navigate_mode(self, b):
|
||
"""
|
||
Set the navigation toolbar button status.
|
||
|
||
.. warning::
|
||
This is not a user-API function.
|
||
|
||
"""
|
||
self._navigate_mode = b
|
||
|
||
def _get_view(self):
|
||
"""
|
||
Save information required to reproduce the current view.
|
||
|
||
This method is called before a view is changed, such as during a pan or zoom
|
||
initiated by the user. It returns an opaque object that describes the current
|
||
view, in a format compatible with :meth:`_set_view`.
|
||
|
||
The default implementation saves the view limits and autoscaling state.
|
||
Subclasses may override this as needed, as long as :meth:`_set_view` is also
|
||
adjusted accordingly.
|
||
"""
|
||
return {
|
||
"xlim": self.get_xlim(), "autoscalex_on": self.get_autoscalex_on(),
|
||
"ylim": self.get_ylim(), "autoscaley_on": self.get_autoscaley_on(),
|
||
}
|
||
|
||
def _set_view(self, view):
|
||
"""
|
||
Apply a previously saved view.
|
||
|
||
This method is called when restoring a view (with the return value of
|
||
:meth:`_get_view` as argument), such as with the navigation buttons.
|
||
|
||
Subclasses that override :meth:`_get_view` also need to override this method
|
||
accordingly.
|
||
"""
|
||
self.set(**view)
|
||
|
||
def _prepare_view_from_bbox(self, bbox, direction='in',
|
||
mode=None, twinx=False, twiny=False):
|
||
"""
|
||
Helper function to prepare the new bounds from a bbox.
|
||
|
||
This helper function returns the new x and y bounds from the zoom
|
||
bbox. This a convenience method to abstract the bbox logic
|
||
out of the base setter.
|
||
"""
|
||
if len(bbox) == 3:
|
||
xp, yp, scl = bbox # Zooming code
|
||
if scl == 0: # Should not happen
|
||
scl = 1.
|
||
if scl > 1:
|
||
direction = 'in'
|
||
else:
|
||
direction = 'out'
|
||
scl = 1/scl
|
||
# get the limits of the axes
|
||
(xmin, ymin), (xmax, ymax) = self.transData.transform(
|
||
np.transpose([self.get_xlim(), self.get_ylim()]))
|
||
# set the range
|
||
xwidth = xmax - xmin
|
||
ywidth = ymax - ymin
|
||
xcen = (xmax + xmin)*.5
|
||
ycen = (ymax + ymin)*.5
|
||
xzc = (xp*(scl - 1) + xcen)/scl
|
||
yzc = (yp*(scl - 1) + ycen)/scl
|
||
bbox = [xzc - xwidth/2./scl, yzc - ywidth/2./scl,
|
||
xzc + xwidth/2./scl, yzc + ywidth/2./scl]
|
||
elif len(bbox) != 4:
|
||
# should be len 3 or 4 but nothing else
|
||
_api.warn_external(
|
||
"Warning in _set_view_from_bbox: bounding box is not a tuple "
|
||
"of length 3 or 4. Ignoring the view change.")
|
||
return
|
||
|
||
# Original limits.
|
||
xmin0, xmax0 = self.get_xbound()
|
||
ymin0, ymax0 = self.get_ybound()
|
||
# The zoom box in screen coords.
|
||
startx, starty, stopx, stopy = bbox
|
||
# Convert to data coords.
|
||
(startx, starty), (stopx, stopy) = self.transData.inverted().transform(
|
||
[(startx, starty), (stopx, stopy)])
|
||
# Clip to axes limits.
|
||
xmin, xmax = np.clip(sorted([startx, stopx]), xmin0, xmax0)
|
||
ymin, ymax = np.clip(sorted([starty, stopy]), ymin0, ymax0)
|
||
# Don't double-zoom twinned axes or if zooming only the other axis.
|
||
if twinx or mode == "y":
|
||
xmin, xmax = xmin0, xmax0
|
||
if twiny or mode == "x":
|
||
ymin, ymax = ymin0, ymax0
|
||
|
||
if direction == "in":
|
||
new_xbound = xmin, xmax
|
||
new_ybound = ymin, ymax
|
||
|
||
elif direction == "out":
|
||
x_trf = self.xaxis.get_transform()
|
||
sxmin0, sxmax0, sxmin, sxmax = x_trf.transform(
|
||
[xmin0, xmax0, xmin, xmax]) # To screen space.
|
||
factor = (sxmax0 - sxmin0) / (sxmax - sxmin) # Unzoom factor.
|
||
# Move original bounds away by
|
||
# (factor) x (distance between unzoom box and Axes bbox).
|
||
sxmin1 = sxmin0 - factor * (sxmin - sxmin0)
|
||
sxmax1 = sxmax0 + factor * (sxmax0 - sxmax)
|
||
# And back to data space.
|
||
new_xbound = x_trf.inverted().transform([sxmin1, sxmax1])
|
||
|
||
y_trf = self.yaxis.get_transform()
|
||
symin0, symax0, symin, symax = y_trf.transform(
|
||
[ymin0, ymax0, ymin, ymax])
|
||
factor = (symax0 - symin0) / (symax - symin)
|
||
symin1 = symin0 - factor * (symin - symin0)
|
||
symax1 = symax0 + factor * (symax0 - symax)
|
||
new_ybound = y_trf.inverted().transform([symin1, symax1])
|
||
|
||
return new_xbound, new_ybound
|
||
|
||
def _set_view_from_bbox(self, bbox, direction='in',
|
||
mode=None, twinx=False, twiny=False):
|
||
"""
|
||
Update view from a selection bbox.
|
||
|
||
.. note::
|
||
|
||
Intended to be overridden by new projection types, but if not, the
|
||
default implementation sets the view limits to the bbox directly.
|
||
|
||
Parameters
|
||
----------
|
||
bbox : 4-tuple or 3 tuple
|
||
* If bbox is a 4 tuple, it is the selected bounding box limits,
|
||
in *display* coordinates.
|
||
* If bbox is a 3 tuple, it is an (xp, yp, scl) triple, where
|
||
(xp, yp) is the center of zooming and scl the scale factor to
|
||
zoom by.
|
||
|
||
direction : str
|
||
The direction to apply the bounding box.
|
||
* `'in'` - The bounding box describes the view directly, i.e.,
|
||
it zooms in.
|
||
* `'out'` - The bounding box describes the size to make the
|
||
existing view, i.e., it zooms out.
|
||
|
||
mode : str or None
|
||
The selection mode, whether to apply the bounding box in only the
|
||
`'x'` direction, `'y'` direction or both (`None`).
|
||
|
||
twinx : bool
|
||
Whether this axis is twinned in the *x*-direction.
|
||
|
||
twiny : bool
|
||
Whether this axis is twinned in the *y*-direction.
|
||
"""
|
||
new_xbound, new_ybound = self._prepare_view_from_bbox(
|
||
bbox, direction=direction, mode=mode, twinx=twinx, twiny=twiny)
|
||
if not twinx and mode != "y":
|
||
self.set_xbound(new_xbound)
|
||
self.set_autoscalex_on(False)
|
||
if not twiny and mode != "x":
|
||
self.set_ybound(new_ybound)
|
||
self.set_autoscaley_on(False)
|
||
|
||
def start_pan(self, x, y, button):
|
||
"""
|
||
Called when a pan operation has started.
|
||
|
||
Parameters
|
||
----------
|
||
x, y : float
|
||
The mouse coordinates in display coords.
|
||
button : `.MouseButton`
|
||
The pressed mouse button.
|
||
|
||
Notes
|
||
-----
|
||
This is intended to be overridden by new projection types.
|
||
"""
|
||
self._pan_start = types.SimpleNamespace(
|
||
lim=self.viewLim.frozen(),
|
||
trans=self.transData.frozen(),
|
||
trans_inverse=self.transData.inverted().frozen(),
|
||
bbox=self.bbox.frozen(),
|
||
x=x,
|
||
y=y)
|
||
|
||
def end_pan(self):
|
||
"""
|
||
Called when a pan operation completes (when the mouse button is up.)
|
||
|
||
Notes
|
||
-----
|
||
This is intended to be overridden by new projection types.
|
||
"""
|
||
del self._pan_start
|
||
|
||
def _get_pan_points(self, button, key, x, y):
|
||
"""
|
||
Helper function to return the new points after a pan.
|
||
|
||
This helper function returns the points on the axis after a pan has
|
||
occurred. This is a convenience method to abstract the pan logic
|
||
out of the base setter.
|
||
"""
|
||
def format_deltas(key, dx, dy):
|
||
if key == 'control':
|
||
if abs(dx) > abs(dy):
|
||
dy = dx
|
||
else:
|
||
dx = dy
|
||
elif key == 'x':
|
||
dy = 0
|
||
elif key == 'y':
|
||
dx = 0
|
||
elif key == 'shift':
|
||
if 2 * abs(dx) < abs(dy):
|
||
dx = 0
|
||
elif 2 * abs(dy) < abs(dx):
|
||
dy = 0
|
||
elif abs(dx) > abs(dy):
|
||
dy = dy / abs(dy) * abs(dx)
|
||
else:
|
||
dx = dx / abs(dx) * abs(dy)
|
||
return dx, dy
|
||
|
||
p = self._pan_start
|
||
dx = x - p.x
|
||
dy = y - p.y
|
||
if dx == dy == 0:
|
||
return
|
||
if button == 1:
|
||
dx, dy = format_deltas(key, dx, dy)
|
||
result = p.bbox.translated(-dx, -dy).transformed(p.trans_inverse)
|
||
elif button == 3:
|
||
try:
|
||
dx = -dx / self.bbox.width
|
||
dy = -dy / self.bbox.height
|
||
dx, dy = format_deltas(key, dx, dy)
|
||
if self.get_aspect() != 'auto':
|
||
dx = dy = 0.5 * (dx + dy)
|
||
alpha = np.power(10.0, (dx, dy))
|
||
start = np.array([p.x, p.y])
|
||
oldpoints = p.lim.transformed(p.trans)
|
||
newpoints = start + alpha * (oldpoints - start)
|
||
result = (mtransforms.Bbox(newpoints)
|
||
.transformed(p.trans_inverse))
|
||
except OverflowError:
|
||
_api.warn_external('Overflow while panning')
|
||
return
|
||
else:
|
||
return
|
||
|
||
valid = np.isfinite(result.transformed(p.trans))
|
||
points = result.get_points().astype(object)
|
||
# Just ignore invalid limits (typically, underflow in log-scale).
|
||
points[~valid] = None
|
||
return points
|
||
|
||
def drag_pan(self, button, key, x, y):
|
||
"""
|
||
Called when the mouse moves during a pan operation.
|
||
|
||
Parameters
|
||
----------
|
||
button : `.MouseButton`
|
||
The pressed mouse button.
|
||
key : str or None
|
||
The pressed key, if any.
|
||
x, y : float
|
||
The mouse coordinates in display coords.
|
||
|
||
Notes
|
||
-----
|
||
This is intended to be overridden by new projection types.
|
||
"""
|
||
points = self._get_pan_points(button, key, x, y)
|
||
if points is not None:
|
||
self.set_xlim(points[:, 0])
|
||
self.set_ylim(points[:, 1])
|
||
|
||
def get_children(self):
|
||
# docstring inherited.
|
||
return [
|
||
*self._children,
|
||
*self.spines.values(),
|
||
*self._axis_map.values(),
|
||
self.title, self._left_title, self._right_title,
|
||
*self.child_axes,
|
||
*([self.legend_] if self.legend_ is not None else []),
|
||
self.patch,
|
||
]
|
||
|
||
def contains(self, mouseevent):
|
||
# docstring inherited.
|
||
return self.patch.contains(mouseevent)
|
||
|
||
def contains_point(self, point):
|
||
"""
|
||
Return whether *point* (pair of pixel coordinates) is inside the Axes
|
||
patch.
|
||
"""
|
||
return self.patch.contains_point(point, radius=1.0)
|
||
|
||
def get_default_bbox_extra_artists(self):
|
||
"""
|
||
Return a default list of artists that are used for the bounding box
|
||
calculation.
|
||
|
||
Artists are excluded either by not being visible or
|
||
``artist.set_in_layout(False)``.
|
||
"""
|
||
|
||
artists = self.get_children()
|
||
|
||
for axis in self._axis_map.values():
|
||
# axis tight bboxes are calculated separately inside
|
||
# Axes.get_tightbbox() using for_layout_only=True
|
||
artists.remove(axis)
|
||
if not (self.axison and self._frameon):
|
||
# don't do bbox on spines if frame not on.
|
||
for spine in self.spines.values():
|
||
artists.remove(spine)
|
||
|
||
artists.remove(self.title)
|
||
artists.remove(self._left_title)
|
||
artists.remove(self._right_title)
|
||
|
||
# always include types that do not internally implement clipping
|
||
# to Axes. may have clip_on set to True and clip_box equivalent
|
||
# to ax.bbox but then ignore these properties during draws.
|
||
noclip = (_AxesBase, maxis.Axis,
|
||
offsetbox.AnnotationBbox, offsetbox.OffsetBox)
|
||
return [a for a in artists if a.get_visible() and a.get_in_layout()
|
||
and (isinstance(a, noclip) or not a._fully_clipped_to_axes())]
|
||
|
||
@_api.make_keyword_only("3.8", "call_axes_locator")
|
||
def get_tightbbox(self, renderer=None, call_axes_locator=True,
|
||
bbox_extra_artists=None, *, for_layout_only=False):
|
||
"""
|
||
Return the tight bounding box of the Axes, including axis and their
|
||
decorators (xlabel, title, etc).
|
||
|
||
Artists that have ``artist.set_in_layout(False)`` are not included
|
||
in the bbox.
|
||
|
||
Parameters
|
||
----------
|
||
renderer : `.RendererBase` subclass
|
||
renderer that will be used to draw the figures (i.e.
|
||
``fig.canvas.get_renderer()``)
|
||
|
||
bbox_extra_artists : list of `.Artist` or ``None``
|
||
List of artists to include in the tight bounding box. If
|
||
``None`` (default), then all artist children of the Axes are
|
||
included in the tight bounding box.
|
||
|
||
call_axes_locator : bool, default: True
|
||
If *call_axes_locator* is ``False``, it does not call the
|
||
``_axes_locator`` attribute, which is necessary to get the correct
|
||
bounding box. ``call_axes_locator=False`` can be used if the
|
||
caller is only interested in the relative size of the tightbbox
|
||
compared to the Axes bbox.
|
||
|
||
for_layout_only : default: False
|
||
The bounding box will *not* include the x-extent of the title and
|
||
the xlabel, or the y-extent of the ylabel.
|
||
|
||
Returns
|
||
-------
|
||
`.BboxBase`
|
||
Bounding box in figure pixel coordinates.
|
||
|
||
See Also
|
||
--------
|
||
matplotlib.axes.Axes.get_window_extent
|
||
matplotlib.axis.Axis.get_tightbbox
|
||
matplotlib.spines.Spine.get_window_extent
|
||
"""
|
||
|
||
bb = []
|
||
if renderer is None:
|
||
renderer = self.figure._get_renderer()
|
||
|
||
if not self.get_visible():
|
||
return None
|
||
|
||
locator = self.get_axes_locator()
|
||
self.apply_aspect(
|
||
locator(self, renderer) if locator and call_axes_locator else None)
|
||
|
||
for axis in self._axis_map.values():
|
||
if self.axison and axis.get_visible():
|
||
ba = martist._get_tightbbox_for_layout_only(axis, renderer)
|
||
if ba:
|
||
bb.append(ba)
|
||
self._update_title_position(renderer)
|
||
axbbox = self.get_window_extent(renderer)
|
||
bb.append(axbbox)
|
||
|
||
for title in [self.title, self._left_title, self._right_title]:
|
||
if title.get_visible():
|
||
bt = title.get_window_extent(renderer)
|
||
if for_layout_only and bt.width > 0:
|
||
# make the title bbox 1 pixel wide so its width
|
||
# is not accounted for in bbox calculations in
|
||
# tight/constrained_layout
|
||
bt.x0 = (bt.x0 + bt.x1) / 2 - 0.5
|
||
bt.x1 = bt.x0 + 1.0
|
||
bb.append(bt)
|
||
|
||
bbox_artists = bbox_extra_artists
|
||
if bbox_artists is None:
|
||
bbox_artists = self.get_default_bbox_extra_artists()
|
||
|
||
for a in bbox_artists:
|
||
bbox = a.get_tightbbox(renderer)
|
||
if (bbox is not None
|
||
and 0 < bbox.width < np.inf
|
||
and 0 < bbox.height < np.inf):
|
||
bb.append(bbox)
|
||
return mtransforms.Bbox.union(
|
||
[b for b in bb if b.width != 0 or b.height != 0])
|
||
|
||
def _make_twin_axes(self, *args, **kwargs):
|
||
"""Make a twinx Axes of self. This is used for twinx and twiny."""
|
||
if 'sharex' in kwargs and 'sharey' in kwargs:
|
||
# The following line is added in v2.2 to avoid breaking Seaborn,
|
||
# which currently uses this internal API.
|
||
if kwargs["sharex"] is not self and kwargs["sharey"] is not self:
|
||
raise ValueError("Twinned Axes may share only one axis")
|
||
ss = self.get_subplotspec()
|
||
if ss:
|
||
twin = self.figure.add_subplot(ss, *args, **kwargs)
|
||
else:
|
||
twin = self.figure.add_axes(
|
||
self.get_position(True), *args, **kwargs,
|
||
axes_locator=_TransformedBoundsLocator(
|
||
[0, 0, 1, 1], self.transAxes))
|
||
self.set_adjustable('datalim')
|
||
twin.set_adjustable('datalim')
|
||
twin.set_zorder(self.zorder)
|
||
|
||
self._twinned_axes.join(self, twin)
|
||
return twin
|
||
|
||
def twinx(self):
|
||
"""
|
||
Create a twin Axes sharing the xaxis.
|
||
|
||
Create a new Axes with an invisible x-axis and an independent
|
||
y-axis positioned opposite to the original one (i.e. at right). The
|
||
x-axis autoscale setting will be inherited from the original
|
||
Axes. To ensure that the tick marks of both y-axes align, see
|
||
`~matplotlib.ticker.LinearLocator`.
|
||
|
||
Returns
|
||
-------
|
||
Axes
|
||
The newly created Axes instance
|
||
|
||
Notes
|
||
-----
|
||
For those who are 'picking' artists while using twinx, pick
|
||
events are only called for the artists in the top-most Axes.
|
||
"""
|
||
ax2 = self._make_twin_axes(sharex=self)
|
||
ax2.yaxis.tick_right()
|
||
ax2.yaxis.set_label_position('right')
|
||
ax2.yaxis.set_offset_position('right')
|
||
ax2.set_autoscalex_on(self.get_autoscalex_on())
|
||
self.yaxis.tick_left()
|
||
ax2.xaxis.set_visible(False)
|
||
ax2.patch.set_visible(False)
|
||
ax2.xaxis.units = self.xaxis.units
|
||
return ax2
|
||
|
||
def twiny(self):
|
||
"""
|
||
Create a twin Axes sharing the yaxis.
|
||
|
||
Create a new Axes with an invisible y-axis and an independent
|
||
x-axis positioned opposite to the original one (i.e. at top). The
|
||
y-axis autoscale setting will be inherited from the original Axes.
|
||
To ensure that the tick marks of both x-axes align, see
|
||
`~matplotlib.ticker.LinearLocator`.
|
||
|
||
Returns
|
||
-------
|
||
Axes
|
||
The newly created Axes instance
|
||
|
||
Notes
|
||
-----
|
||
For those who are 'picking' artists while using twiny, pick
|
||
events are only called for the artists in the top-most Axes.
|
||
"""
|
||
ax2 = self._make_twin_axes(sharey=self)
|
||
ax2.xaxis.tick_top()
|
||
ax2.xaxis.set_label_position('top')
|
||
ax2.set_autoscaley_on(self.get_autoscaley_on())
|
||
self.xaxis.tick_bottom()
|
||
ax2.yaxis.set_visible(False)
|
||
ax2.patch.set_visible(False)
|
||
ax2.yaxis.units = self.yaxis.units
|
||
return ax2
|
||
|
||
def get_shared_x_axes(self):
|
||
"""Return an immutable view on the shared x-axes Grouper."""
|
||
return cbook.GrouperView(self._shared_axes["x"])
|
||
|
||
def get_shared_y_axes(self):
|
||
"""Return an immutable view on the shared y-axes Grouper."""
|
||
return cbook.GrouperView(self._shared_axes["y"])
|
||
|
||
def label_outer(self, remove_inner_ticks=False):
|
||
"""
|
||
Only show "outer" labels and tick labels.
|
||
|
||
x-labels are only kept for subplots on the last row (or first row, if
|
||
labels are on the top side); y-labels only for subplots on the first
|
||
column (or last column, if labels are on the right side).
|
||
|
||
Parameters
|
||
----------
|
||
remove_inner_ticks : bool, default: False
|
||
If True, remove the inner ticks as well (not only tick labels).
|
||
|
||
.. versionadded:: 3.8
|
||
"""
|
||
self._label_outer_xaxis(skip_non_rectangular_axes=False,
|
||
remove_inner_ticks=remove_inner_ticks)
|
||
self._label_outer_yaxis(skip_non_rectangular_axes=False,
|
||
remove_inner_ticks=remove_inner_ticks)
|
||
|
||
def _label_outer_xaxis(self, *, skip_non_rectangular_axes,
|
||
remove_inner_ticks=False):
|
||
# see documentation in label_outer.
|
||
if skip_non_rectangular_axes and not isinstance(self.patch,
|
||
mpl.patches.Rectangle):
|
||
return
|
||
ss = self.get_subplotspec()
|
||
if not ss:
|
||
return
|
||
label_position = self.xaxis.get_label_position()
|
||
if not ss.is_first_row(): # Remove top label/ticklabels/offsettext.
|
||
if label_position == "top":
|
||
self.set_xlabel("")
|
||
top_kw = {'top': False} if remove_inner_ticks else {}
|
||
self.xaxis.set_tick_params(
|
||
which="both", labeltop=False, **top_kw)
|
||
if self.xaxis.offsetText.get_position()[1] == 1:
|
||
self.xaxis.offsetText.set_visible(False)
|
||
if not ss.is_last_row(): # Remove bottom label/ticklabels/offsettext.
|
||
if label_position == "bottom":
|
||
self.set_xlabel("")
|
||
bottom_kw = {'bottom': False} if remove_inner_ticks else {}
|
||
self.xaxis.set_tick_params(
|
||
which="both", labelbottom=False, **bottom_kw)
|
||
if self.xaxis.offsetText.get_position()[1] == 0:
|
||
self.xaxis.offsetText.set_visible(False)
|
||
|
||
def _label_outer_yaxis(self, *, skip_non_rectangular_axes,
|
||
remove_inner_ticks=False):
|
||
# see documentation in label_outer.
|
||
if skip_non_rectangular_axes and not isinstance(self.patch,
|
||
mpl.patches.Rectangle):
|
||
return
|
||
ss = self.get_subplotspec()
|
||
if not ss:
|
||
return
|
||
label_position = self.yaxis.get_label_position()
|
||
if not ss.is_first_col(): # Remove left label/ticklabels/offsettext.
|
||
if label_position == "left":
|
||
self.set_ylabel("")
|
||
left_kw = {'left': False} if remove_inner_ticks else {}
|
||
self.yaxis.set_tick_params(
|
||
which="both", labelleft=False, **left_kw)
|
||
if self.yaxis.offsetText.get_position()[0] == 0:
|
||
self.yaxis.offsetText.set_visible(False)
|
||
if not ss.is_last_col(): # Remove right label/ticklabels/offsettext.
|
||
if label_position == "right":
|
||
self.set_ylabel("")
|
||
right_kw = {'right': False} if remove_inner_ticks else {}
|
||
self.yaxis.set_tick_params(
|
||
which="both", labelright=False, **right_kw)
|
||
if self.yaxis.offsetText.get_position()[0] == 1:
|
||
self.yaxis.offsetText.set_visible(False)
|
||
|
||
def set_forward_navigation_events(self, forward):
|
||
"""
|
||
Set how pan/zoom events are forwarded to Axes below this one.
|
||
|
||
Parameters
|
||
----------
|
||
forward : bool or "auto"
|
||
Possible values:
|
||
|
||
- True: Forward events to other axes with lower or equal zorder.
|
||
- False: Events are only executed on this axes.
|
||
- "auto": Default behaviour (*True* for axes with an invisible
|
||
patch and *False* otherwise)
|
||
|
||
See Also
|
||
--------
|
||
matplotlib.axes.Axes.set_navigate
|
||
|
||
"""
|
||
self._forward_navigation_events = forward
|
||
|
||
def get_forward_navigation_events(self):
|
||
"""Get how pan/zoom events are forwarded to Axes below this one."""
|
||
return self._forward_navigation_events
|
||
|
||
|
||
def _draw_rasterized(figure, artists, renderer):
|
||
"""
|
||
A helper function for rasterizing the list of artists.
|
||
|
||
The bookkeeping to track if we are or are not in rasterizing mode
|
||
with the mixed-mode backends is relatively complicated and is now
|
||
handled in the matplotlib.artist.allow_rasterization decorator.
|
||
|
||
This helper defines the absolute minimum methods and attributes on a
|
||
shim class to be compatible with that decorator and then uses it to
|
||
rasterize the list of artists.
|
||
|
||
This is maybe too-clever, but allows us to reuse the same code that is
|
||
used on normal artists to participate in the "are we rasterizing"
|
||
accounting.
|
||
|
||
Please do not use this outside of the "rasterize below a given zorder"
|
||
functionality of Axes.
|
||
|
||
Parameters
|
||
----------
|
||
figure : matplotlib.figure.Figure
|
||
The figure all of the artists belong to (not checked). We need this
|
||
because we can at the figure level suppress composition and insert each
|
||
rasterized artist as its own image.
|
||
|
||
artists : List[matplotlib.artist.Artist]
|
||
The list of Artists to be rasterized. These are assumed to all
|
||
be in the same Figure.
|
||
|
||
renderer : matplotlib.backendbases.RendererBase
|
||
The currently active renderer
|
||
|
||
Returns
|
||
-------
|
||
None
|
||
|
||
"""
|
||
class _MinimalArtist:
|
||
def get_rasterized(self):
|
||
return True
|
||
|
||
def get_agg_filter(self):
|
||
return None
|
||
|
||
def __init__(self, figure, artists):
|
||
self.figure = figure
|
||
self.artists = artists
|
||
|
||
@martist.allow_rasterization
|
||
def draw(self, renderer):
|
||
for a in self.artists:
|
||
a.draw(renderer)
|
||
|
||
return _MinimalArtist(figure, artists).draw(renderer)
|