4448 lines
156 KiB
Python
4448 lines
156 KiB
Python
from collections import OrderedDict
|
|
import itertools
|
|
import logging
|
|
import math
|
|
from numbers import Real
|
|
from operator import attrgetter
|
|
import types
|
|
|
|
import numpy as np
|
|
|
|
import matplotlib as mpl
|
|
from matplotlib import cbook, rcParams
|
|
from matplotlib.cbook import _OrderedSet, _check_1d, index_of
|
|
from matplotlib import docstring
|
|
import matplotlib.colors as mcolors
|
|
import matplotlib.lines as mlines
|
|
import matplotlib.patches as mpatches
|
|
import matplotlib.artist as martist
|
|
import matplotlib.transforms as mtransforms
|
|
import matplotlib.ticker as mticker
|
|
import matplotlib.axis as maxis
|
|
import matplotlib.spines as mspines
|
|
import matplotlib.font_manager as font_manager
|
|
import matplotlib.text as mtext
|
|
import matplotlib.image as mimage
|
|
from matplotlib.rcsetup import cycler, validate_axisbelow
|
|
|
|
_log = logging.getLogger(__name__)
|
|
|
|
|
|
def _process_plot_format(fmt):
|
|
"""
|
|
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
|
|
|
|
See Also
|
|
--------
|
|
matplotlib.Line2D.lineStyles, matplotlib.colors.cnames
|
|
All possible styles and color format strings.
|
|
"""
|
|
|
|
linestyle = None
|
|
marker = None
|
|
color = None
|
|
|
|
# Is fmt just a colorspec?
|
|
try:
|
|
color = mcolors.to_rgba(fmt)
|
|
|
|
# We need to differentiate grayscale '1.0' from tri_down marker '1'
|
|
try:
|
|
fmtint = str(int(fmt))
|
|
except ValueError:
|
|
return linestyle, marker, color # Yes
|
|
else:
|
|
if fmt != fmtint:
|
|
# user definitely doesn't want tri_down marker
|
|
return linestyle, marker, color # Yes
|
|
else:
|
|
# ignore converted color
|
|
color = None
|
|
except ValueError:
|
|
pass # No, not just a color.
|
|
|
|
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(
|
|
'Illegal format string "%s"; two linestyle symbols' % fmt)
|
|
linestyle = fmt[i:i+2]
|
|
i += 2
|
|
elif c in mlines.lineStyles:
|
|
if linestyle is not None:
|
|
raise ValueError(
|
|
'Illegal format string "%s"; two linestyle symbols' % fmt)
|
|
linestyle = c
|
|
i += 1
|
|
elif c in mlines.lineMarkers:
|
|
if marker is not None:
|
|
raise ValueError(
|
|
'Illegal format string "%s"; two marker symbols' % fmt)
|
|
marker = c
|
|
i += 1
|
|
elif c in mcolors.get_named_colors_mapping():
|
|
if color is not None:
|
|
raise ValueError(
|
|
'Illegal format string "%s"; two color symbols' % fmt)
|
|
color = c
|
|
i += 1
|
|
elif c == 'C' and i < len(fmt) - 1:
|
|
color_cycle_number = int(fmt[i + 1])
|
|
color = mcolors.to_rgba("C{}".format(color_cycle_number))
|
|
i += 2
|
|
else:
|
|
raise ValueError(
|
|
'Unrecognized character %c in format string' % c)
|
|
|
|
if linestyle is None and marker is None:
|
|
linestyle = 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 the plot command, so that
|
|
plot commands like the following are supported::
|
|
|
|
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, axes, command='plot'):
|
|
self.axes = axes
|
|
self.command = command
|
|
self.set_prop_cycle()
|
|
|
|
def __getstate__(self):
|
|
# note: it is not possible to pickle a generator (and thus a cycler).
|
|
return {'axes': self.axes, 'command': self.command}
|
|
|
|
def __setstate__(self, state):
|
|
self.__dict__ = state.copy()
|
|
self.set_prop_cycle()
|
|
|
|
def set_prop_cycle(self, *args, **kwargs):
|
|
# Can't do `args == (None,)` as that crashes cycler.
|
|
if not (args or kwargs) or (len(args) == 1 and args[0] is None):
|
|
prop_cycler = rcParams['axes.prop_cycle']
|
|
else:
|
|
prop_cycler = cycler(*args, **kwargs)
|
|
|
|
self.prop_cycler = itertools.cycle(prop_cycler)
|
|
# This should make a copy
|
|
self._prop_keys = prop_cycler.keys
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
self.axes._process_unit_info(kwargs=kwargs)
|
|
|
|
for pos_only in "xy":
|
|
if pos_only in kwargs:
|
|
raise TypeError("{} got an unexpected keyword argument {!r}"
|
|
.format(self.command, pos_only))
|
|
|
|
if not args:
|
|
return
|
|
|
|
# Process the 'data' kwarg.
|
|
data = kwargs.pop("data", None)
|
|
if data is not None:
|
|
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)
|
|
cbook._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
|
|
|
|
# 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(this, kwargs)
|
|
|
|
def get_next_color(self):
|
|
"""Return the next color in the cycle."""
|
|
if 'color' not in self._prop_keys:
|
|
return 'k'
|
|
return next(self.prop_cycler)['color']
|
|
|
|
def _getdefaults(self, ignore, kw):
|
|
"""
|
|
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.
|
|
"""
|
|
prop_keys = self._prop_keys - ignore
|
|
if any(kw.get(k, None) is None for k in prop_keys):
|
|
# Need to copy this dictionary or else the next time around
|
|
# in the cycle, the dictionary could be missing entries.
|
|
default_dict = next(self.prop_cycler).copy()
|
|
for p in ignore:
|
|
default_dict.pop(p, None)
|
|
else:
|
|
default_dict = {}
|
|
return default_dict
|
|
|
|
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, x, y, kw, kwargs):
|
|
kw = {**kw, **kwargs} # Don't modify the original kw.
|
|
default_dict = self._getdefaults(set(), kw)
|
|
self._setdefaults(default_dict, kw)
|
|
seg = mlines.Line2D(x, y, **kw)
|
|
return seg
|
|
|
|
def _makefill(self, x, y, kw, kwargs):
|
|
# Polygon doesn't directly support unitized inputs.
|
|
x = self.axes.convert_xunits(x)
|
|
y = self.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*.
|
|
for k, v in kwargs.items():
|
|
if v is not None:
|
|
ignores.add(k)
|
|
|
|
# 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(ignores, kw)
|
|
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
|
|
|
|
def _plot_args(self, tup, kwargs):
|
|
if len(tup) > 1 and isinstance(tup[-1], str):
|
|
linestyle, marker, color = _process_plot_format(tup[-1])
|
|
tup = tup[:-1]
|
|
elif len(tup) == 3:
|
|
raise ValueError('third arg must be a format string')
|
|
else:
|
|
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 k, v in zip(('linestyle', 'marker', 'color'),
|
|
(linestyle, marker, color)):
|
|
if v is not None:
|
|
kw[k] = v
|
|
|
|
if len(tup) == 2:
|
|
x = _check_1d(tup[0])
|
|
y = _check_1d(tup[-1])
|
|
else:
|
|
x, y = index_of(tup[-1])
|
|
|
|
if self.axes.xaxis is not None:
|
|
self.axes.xaxis.update_units(x)
|
|
if self.axes.yaxis is not None:
|
|
self.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 2-D, 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':
|
|
func = self._makeline
|
|
else:
|
|
kw['closed'] = kwargs.get('closed', True)
|
|
func = self._makefill
|
|
|
|
ncx, ncy = x.shape[1], y.shape[1]
|
|
if ncx > 1 and ncy > 1 and ncx != ncy:
|
|
cbook.warn_deprecated(
|
|
"2.2", message="cycling among columns of inputs with "
|
|
"non-matching shapes is deprecated.")
|
|
return [func(x[:, j % ncx], y[:, j % ncy], kw, kwargs)
|
|
for j in range(max(ncx, ncy))]
|
|
|
|
|
|
class _AxesBase(martist.Artist):
|
|
name = "rectilinear"
|
|
|
|
_shared_x_axes = cbook.Grouper()
|
|
_shared_y_axes = cbook.Grouper()
|
|
_twinned_axes = cbook.Grouper()
|
|
|
|
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, rect,
|
|
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,
|
|
**kwargs
|
|
):
|
|
"""
|
|
Build an axes in a figure.
|
|
|
|
Parameters
|
|
----------
|
|
fig : `~matplotlib.figure.Figure`
|
|
The axes is build in the `.Figure` *fig*.
|
|
|
|
rect : [left, bottom, width, height]
|
|
The axes is build in the rectangle *rect*. *rect* is in
|
|
`.Figure` coordinates.
|
|
|
|
sharex, sharey : `~.axes.Axes`, optional
|
|
The x or y `~.matplotlib.axis` is shared with the x or
|
|
y axis in the input `~.axes.Axes`.
|
|
|
|
frameon : bool, optional
|
|
True means that the axes frame is visible.
|
|
|
|
**kwargs
|
|
Other optional keyword arguments:
|
|
|
|
%(Axes)s
|
|
|
|
Returns
|
|
-------
|
|
axes : `~.axes.Axes`
|
|
The new `~.axes.Axes` object.
|
|
"""
|
|
|
|
martist.Artist.__init__(self)
|
|
if isinstance(rect, mtransforms.Bbox):
|
|
self._position = rect
|
|
else:
|
|
self._position = mtransforms.Bbox.from_bounds(*rect)
|
|
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_viewlim_x = False
|
|
self._stale_viewlim_y = False
|
|
self._sharex = sharex
|
|
self._sharey = sharey
|
|
if sharex is not None:
|
|
self._shared_x_axes.join(self, sharex)
|
|
if sharey is not None:
|
|
self._shared_y_axes.join(self, sharey)
|
|
self.set_label(label)
|
|
self.set_figure(fig)
|
|
|
|
self.set_axes_locator(kwargs.get("axes_locator", None))
|
|
|
|
self.spines = self._gen_axes_spines()
|
|
|
|
# this call may differ for non-sep axes, e.g., polar
|
|
self._init_axis()
|
|
if facecolor is None:
|
|
facecolor = rcParams['axes.facecolor']
|
|
self._facecolor = facecolor
|
|
self._frameon = frameon
|
|
self.set_axisbelow(rcParams['axes.axisbelow'])
|
|
|
|
self._rasterization_zorder = None
|
|
self.cla()
|
|
|
|
# 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.update(kwargs)
|
|
|
|
if self.xaxis is not None:
|
|
self._xcid = self.xaxis.callbacks.connect(
|
|
'units finalize', lambda: self._on_units_changed(scalex=True))
|
|
|
|
if self.yaxis is not None:
|
|
self._ycid = self.yaxis.callbacks.connect(
|
|
'units finalize', lambda: self._on_units_changed(scaley=True))
|
|
|
|
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')
|
|
|
|
self._layoutbox = None
|
|
self._poslayoutbox = None
|
|
|
|
def __getstate__(self):
|
|
# The renderer should be re-created by the figure, and then cached at
|
|
# that point.
|
|
state = super().__getstate__()
|
|
for key in ['_layoutbox', '_poslayoutbox']:
|
|
state[key] = None
|
|
# Prune the sharing & twinning info to only contain the current group.
|
|
for grouper_name in [
|
|
'_shared_x_axes', '_shared_y_axes', '_twinned_axes']:
|
|
grouper = getattr(self, grouper_name)
|
|
state[grouper_name] = (grouper.get_siblings(self)
|
|
if self in grouper else None)
|
|
return state
|
|
|
|
def __setstate__(self, state):
|
|
# Merge the grouping info back into the global groupers.
|
|
for grouper_name in [
|
|
'_shared_x_axes', '_shared_y_axes', '_twinned_axes']:
|
|
siblings = state.pop(grouper_name)
|
|
if siblings:
|
|
getattr(self, grouper_name).join(*siblings)
|
|
self.__dict__ = state
|
|
self._stale = True
|
|
|
|
def get_window_extent(self, *args, **kwargs):
|
|
"""
|
|
Return the axes bounding box in display space; *args* and *kwargs*
|
|
are empty.
|
|
|
|
This bounding box does not include the spines, ticks, ticklables,
|
|
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.get_window_extent
|
|
|
|
"""
|
|
return self.bbox
|
|
|
|
def _init_axis(self):
|
|
"move this out of __init__ because non-separable axes don't use it"
|
|
self.xaxis = maxis.XAxis(self)
|
|
self.spines['bottom'].register_axis(self.xaxis)
|
|
self.spines['top'].register_axis(self.xaxis)
|
|
self.yaxis = maxis.YAxis(self)
|
|
self.spines['left'].register_axis(self.yaxis)
|
|
self.spines['right'].register_axis(self.yaxis)
|
|
self._update_transScale()
|
|
|
|
def set_figure(self, fig):
|
|
"""
|
|
Set the `.Figure` for this `.Axes`.
|
|
|
|
Parameters
|
|
----------
|
|
fig : `.Figure`
|
|
"""
|
|
martist.Artist.set_figure(self, fig)
|
|
|
|
self.bbox = mtransforms.TransformedBbox(self._position,
|
|
fig.transFigure)
|
|
# 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.
|
|
scalex = any(ax._stale_viewlim_x
|
|
for ax in self._shared_x_axes.get_siblings(self))
|
|
scaley = any(ax._stale_viewlim_y
|
|
for ax in self._shared_y_axes.get_siblings(self))
|
|
if scalex or scaley:
|
|
for ax in self._shared_x_axes.get_siblings(self):
|
|
ax._stale_viewlim_x = False
|
|
for ax in self._shared_y_axes.get_siblings(self):
|
|
ax._stale_viewlim_y = False
|
|
self.autoscale_view(scalex=scalex, scaley=scaley)
|
|
|
|
@property
|
|
def viewLim(self):
|
|
self._unstale_viewLim()
|
|
return self._viewLim
|
|
|
|
# API could be better, right now this is just to match the old calls to
|
|
# autoscale_view() after each plotting method.
|
|
def _request_autoscale_view(self, tight=None, scalex=True, scaley=True):
|
|
if tight is not None:
|
|
self._tight = tight
|
|
if scalex:
|
|
self._stale_viewlim_x = True # Else keep old state.
|
|
if scaley:
|
|
self._stale_viewlim_y = True
|
|
|
|
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.
|
|
"""
|
|
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('unknown value for which')
|
|
|
|
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 axes and the label.
|
|
The x-direction is in data coordinates and the y-direction is in
|
|
axis corrdinates
|
|
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 = 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 axes and the
|
|
label. The x-direction is in data coordinates and the y-direction
|
|
is in axis corrdinates
|
|
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 = 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.
|
|
"""
|
|
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('unknown value for which')
|
|
|
|
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 axes and the label.
|
|
The x-direction is in axis coordinates and the y-direction is in
|
|
data corrdinates
|
|
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 = 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 axes and the
|
|
label. The x-direction is in axis coordinates and the y-direction
|
|
is in data corrdinates
|
|
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 = 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()))
|
|
for line in getattr(self, "lines", []): # Not set during init.
|
|
try:
|
|
line._transformed_path.invalidate()
|
|
except AttributeError:
|
|
pass
|
|
|
|
def get_position(self, original=False):
|
|
"""
|
|
Get a copy of the axes rectangle 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
|
|
-------
|
|
pos : `.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 `.set_aspect`
|
|
for details.
|
|
|
|
Parameters
|
|
----------
|
|
pos : [left, bottom, width, height] or `~matplotlib.transforms.Bbox`
|
|
The new position of the in `.Figure` coordinates.
|
|
|
|
which : {'both', 'active', 'original'}, optional
|
|
Determines which position variables to change.
|
|
|
|
"""
|
|
self._set_position(pos, which=which)
|
|
# because this is being called externally to the library we
|
|
# zero the constrained layout parts.
|
|
self._layoutbox = None
|
|
self._poslayoutbox = None
|
|
|
|
def _set_position(self, pos, which='both'):
|
|
"""
|
|
private version of set_position. Call this internally
|
|
to get the same functionality of `get_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 resets the a possible position change due to aspect constraints.
|
|
For an explanation of the positions see `.set_position`.
|
|
"""
|
|
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.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 OrderedDict((side, mspines.Spine.linear_spine(self, side))
|
|
for side in ['left', 'right', 'bottom', 'top'])
|
|
|
|
def cla(self):
|
|
"""Clear the current axes."""
|
|
# Note: this is called by Axes.__init__()
|
|
|
|
# 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()
|
|
|
|
self.xaxis.cla()
|
|
self.yaxis.cla()
|
|
|
|
for name, spine in self.spines.items():
|
|
spine.cla()
|
|
|
|
self.ignore_existing_data_limits = True
|
|
self.callbacks = cbook.CallbackRegistry()
|
|
|
|
if self._sharex is not None:
|
|
# major and minor are axis.Ticker class instances with
|
|
# locator and formatter attributes
|
|
self.xaxis.major = self._sharex.xaxis.major
|
|
self.xaxis.minor = self._sharex.xaxis.minor
|
|
x0, x1 = self._sharex.get_xlim()
|
|
self.set_xlim(x0, x1, emit=False,
|
|
auto=self._sharex.get_autoscalex_on())
|
|
self.xaxis._scale = self._sharex.xaxis._scale
|
|
else:
|
|
self.xaxis._set_scale('linear')
|
|
try:
|
|
self.set_xlim(0, 1)
|
|
except TypeError:
|
|
pass
|
|
|
|
if self._sharey is not None:
|
|
self.yaxis.major = self._sharey.yaxis.major
|
|
self.yaxis.minor = self._sharey.yaxis.minor
|
|
y0, y1 = self._sharey.get_ylim()
|
|
self.set_ylim(y0, y1, emit=False,
|
|
auto=self._sharey.get_autoscaley_on())
|
|
self.yaxis._scale = self._sharey.yaxis._scale
|
|
else:
|
|
self.yaxis._set_scale('linear')
|
|
try:
|
|
self.set_ylim(0, 1)
|
|
except TypeError:
|
|
pass
|
|
# update the minor locator for x and y axis based on rcParams
|
|
if rcParams['xtick.minor.visible']:
|
|
self.xaxis.set_minor_locator(mticker.AutoMinorLocator())
|
|
|
|
if rcParams['ytick.minor.visible']:
|
|
self.yaxis.set_minor_locator(mticker.AutoMinorLocator())
|
|
|
|
if self._sharex is None:
|
|
self._autoscaleXon = True
|
|
if self._sharey is None:
|
|
self._autoscaleYon = True
|
|
self._xmargin = rcParams['axes.xmargin']
|
|
self._ymargin = rcParams['axes.ymargin']
|
|
self._tight = None
|
|
self._use_sticky_edges = True
|
|
self._update_transScale() # needed?
|
|
|
|
self._get_lines = _process_plot_var_args(self)
|
|
self._get_patches_for_fill = _process_plot_var_args(self, 'fill')
|
|
|
|
self._gridOn = rcParams['axes.grid']
|
|
self.lines = []
|
|
self.patches = []
|
|
self.texts = []
|
|
self.tables = []
|
|
self.artists = []
|
|
self.images = []
|
|
self._mouseover_set = _OrderedSet()
|
|
self.child_axes = []
|
|
self._current_image = None # strictly for pyplot via _sci, _gci
|
|
self.legend_ = None
|
|
self.collections = [] # collection.Collection instances
|
|
self.containers = []
|
|
|
|
self.grid(False) # Disable grid on init to use rcParameter
|
|
self.grid(self._gridOn, which=rcParams['axes.grid.which'],
|
|
axis=rcParams['axes.grid.axis'])
|
|
props = font_manager.FontProperties(
|
|
size=rcParams['axes.titlesize'],
|
|
weight=rcParams['axes.titleweight'])
|
|
|
|
self.title = mtext.Text(
|
|
x=0.5, y=1.0, text='',
|
|
fontproperties=props,
|
|
verticalalignment='baseline',
|
|
horizontalalignment='center',
|
|
)
|
|
self._left_title = mtext.Text(
|
|
x=0.0, y=1.0, text='',
|
|
fontproperties=props.copy(),
|
|
verticalalignment='baseline',
|
|
horizontalalignment='left', )
|
|
self._right_title = mtext.Text(
|
|
x=1.0, y=1.0, text='',
|
|
fontproperties=props.copy(),
|
|
verticalalignment='baseline',
|
|
horizontalalignment='right',
|
|
)
|
|
title_offset_points = 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)
|
|
# determine if the title position has been set manually:
|
|
self._autotitlepos = None
|
|
|
|
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)
|
|
|
|
self._shared_x_axes.clean()
|
|
self._shared_y_axes.clean()
|
|
if self._sharex:
|
|
self.xaxis.set_visible(xaxis_visible)
|
|
self.patch.set_visible(patch_visible)
|
|
|
|
if self._sharey:
|
|
self.yaxis.set_visible(yaxis_visible)
|
|
self.patch.set_visible(patch_visible)
|
|
|
|
self.stale = True
|
|
|
|
def clear(self):
|
|
"""Clear the axes."""
|
|
self.cla()
|
|
|
|
def get_facecolor(self):
|
|
"""Get the facecolor of the Axes."""
|
|
return self.patch.get_facecolor()
|
|
get_fc = get_facecolor
|
|
|
|
def set_facecolor(self, color):
|
|
"""
|
|
Set the facecolor of the Axes.
|
|
|
|
Parameters
|
|
----------
|
|
color : color
|
|
"""
|
|
self._facecolor = color
|
|
self.stale = True
|
|
return self.patch.set_facecolor(color)
|
|
set_fc = set_facecolor
|
|
|
|
def _set_title_offset_trans(self, title_offset_points):
|
|
"""
|
|
Set the offset for the title either from rcParams['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
|
|
Set the given Cycler. *None* resets to the cycle defined by the
|
|
current style.
|
|
|
|
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.
|
|
|
|
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'])
|
|
|
|
See Also
|
|
--------
|
|
matplotlib.rcsetup.cycler
|
|
Convenience function for creating validated cyclers for properties.
|
|
cycler.cycler
|
|
The original function for creating unvalidated cyclers.
|
|
|
|
"""
|
|
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 self._aspect
|
|
|
|
def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
|
|
"""
|
|
Set the aspect of the axis scaling, i.e. the ratio of y-unit to x-unit.
|
|
|
|
Parameters
|
|
----------
|
|
aspect : {'auto', 'equal'} or num
|
|
Possible values:
|
|
|
|
======== ================================================
|
|
value description
|
|
======== ================================================
|
|
'auto' automatic; fill the position rectangle with data
|
|
'equal' same scaling from data to plot units for x and y
|
|
num a circle will be stretched such that the height
|
|
is num times the width. aspect=1 is the same as
|
|
aspect='equal'.
|
|
======== ================================================
|
|
|
|
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 2-tuple of 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
|
|
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 `.set_anchor` for further details.
|
|
|
|
share : bool, optional
|
|
If ``True``, apply the settings to all shared Axes.
|
|
Default is ``False``.
|
|
|
|
See Also
|
|
--------
|
|
matplotlib.axes.Axes.set_adjustable
|
|
defining the parameter to adjust in order to meet the required
|
|
aspect.
|
|
matplotlib.axes.Axes.set_anchor
|
|
defining the position in case of extra space.
|
|
"""
|
|
if not (cbook._str_equal(aspect, 'equal')
|
|
or cbook._str_equal(aspect, 'auto')):
|
|
aspect = float(aspect) # raise ValueError if necessary
|
|
|
|
if (not cbook._str_equal(aspect, 'auto')) and self.name == '3d':
|
|
raise NotImplementedError(
|
|
'It is not currently possible to manually set the aspect '
|
|
'on 3D axes')
|
|
|
|
if share:
|
|
axes = {*self._shared_x_axes.get_siblings(self),
|
|
*self._shared_y_axes.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 self._adjustable
|
|
|
|
def set_adjustable(self, adjustable, share=False):
|
|
"""
|
|
Define which parameter the Axes will change to achieve a given aspect.
|
|
|
|
Parameters
|
|
----------
|
|
adjustable : {'box', 'datalim'}
|
|
If 'box', change the physical dimensions of the Axes.
|
|
If 'datalim', change the ``x`` or ``y`` data limits.
|
|
|
|
share : bool, optional
|
|
If ``True``, apply the settings to all shared Axes.
|
|
Default is ``False``.
|
|
|
|
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.
|
|
"""
|
|
cbook._check_in_list(["box", "datalim"], adjustable=adjustable)
|
|
if share:
|
|
axs = {*self._shared_x_axes.get_siblings(self),
|
|
*self._shared_y_axes.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_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 : 2-tuple of floats or {'C', 'SW', 'S', 'SE', ...}
|
|
The anchor position may be either:
|
|
|
|
- a sequence (*cx*, *cy*). *cx* and *cy* may range from 0
|
|
to 1, where 0 is left or bottom and 1 is right or top.
|
|
|
|
- a string using cardinal directions as abbreviation:
|
|
|
|
- 'C' for centered
|
|
- 'S' (south) for bottom-center
|
|
- 'SW' (south west) for bottom-left
|
|
- etc.
|
|
|
|
Here is an overview of the possible positions:
|
|
|
|
+------+------+------+
|
|
| 'NW' | 'N' | 'NE' |
|
|
+------+------+------+
|
|
| 'W' | 'C' | 'E' |
|
|
+------+------+------+
|
|
| 'SW' | 'S' | 'SE' |
|
|
+------+------+------+
|
|
|
|
share : bool, optional
|
|
If ``True``, apply the settings to all shared Axes.
|
|
Default is ``False``.
|
|
|
|
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 = {*self._shared_x_axes.get_siblings(self),
|
|
*self._shared_y_axes.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
|
|
|
|
@cbook.deprecated("3.2")
|
|
def get_data_ratio_log(self):
|
|
"""
|
|
Return the aspect ratio of the raw data in log scale.
|
|
|
|
Notes
|
|
-----
|
|
Will be used when both axis are in log scale.
|
|
"""
|
|
xmin, xmax = self.get_xbound()
|
|
ymin, ymax = self.get_ybound()
|
|
|
|
xsize = max(abs(math.log10(xmax) - math.log10(xmin)), 1e-30)
|
|
ysize = max(abs(math.log10(ymax) - math.log10(ymin)), 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, `.get_anchor`
|
|
will affect the position.
|
|
|
|
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
|
|
defining the parameter to adjust in order to meet the required
|
|
aspect.
|
|
matplotlib.axes.Axes.set_anchor
|
|
defining the position in case of extra space.
|
|
"""
|
|
if position is None:
|
|
position = self.get_position(original=True)
|
|
|
|
aspect = self.get_aspect()
|
|
|
|
if aspect == 'auto':
|
|
self._set_position(position, which='active')
|
|
return
|
|
|
|
if aspect == 'equal':
|
|
aspect = 1
|
|
|
|
fig_width, fig_height = self.get_figure().get_size_inches()
|
|
fig_aspect = fig_height / fig_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
|
|
|
|
# self._adjustable == 'datalim'
|
|
|
|
# reset active to original in case it had been changed by prior use
|
|
# of 'box'
|
|
self._set_position(position, which='active')
|
|
|
|
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)
|
|
|
|
l, b, w, h = position.bounds
|
|
box_aspect = fig_aspect * (h / w)
|
|
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_x_axes
|
|
shared_y = self in self._shared_y_axes
|
|
# Not sure whether we need this check:
|
|
if shared_x and shared_y:
|
|
raise RuntimeError("adjustable='datalim' is not allowed when both "
|
|
"axes are shared")
|
|
|
|
# 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, *args, 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
|
|
======== ==========================================================
|
|
'on' Turn on axis lines and labels. Same as ``True``.
|
|
'off' Turn off axis lines and labels. Same as ``False``.
|
|
'equal' Set equal scaling (i.e., make circles circular) by
|
|
changing 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).
|
|
'normal' Same as 'auto'; deprecated.
|
|
'image' 'scaled' with axis limits equal to data limits.
|
|
'square' Square plot; similar to 'scaled', but initially forcing
|
|
``xmax-xmin == ymax-ymin``.
|
|
======== ==========================================================
|
|
|
|
emit : bool, optional, 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
|
|
"""
|
|
if len(args) == 1 and isinstance(args[0], (str, bool)):
|
|
s = args[0]
|
|
if s is True:
|
|
s = 'on'
|
|
if s is False:
|
|
s = 'off'
|
|
s = s.lower()
|
|
if s == 'on':
|
|
self.set_axis_on()
|
|
elif s == 'off':
|
|
self.set_axis_off()
|
|
elif s in ('equal', 'tight', 'scaled', 'normal',
|
|
'auto', 'image', 'square'):
|
|
if s == 'normal':
|
|
cbook.warn_deprecated(
|
|
"3.1", message="Passing 'normal' to axis() is "
|
|
"deprecated since %(since)s; use 'auto' instead.")
|
|
self.set_autoscale_on(True)
|
|
self.set_aspect('auto')
|
|
self.autoscale_view(tight=False)
|
|
# self.apply_aspect()
|
|
if s == 'equal':
|
|
self.set_aspect('equal', adjustable='datalim')
|
|
elif s == 'scaled':
|
|
self.set_aspect('equal', adjustable='box', anchor='C')
|
|
self.set_autoscale_on(False) # Req. by Mark Bakker
|
|
elif s == 'tight':
|
|
self.autoscale_view(tight=True)
|
|
self.set_autoscale_on(False)
|
|
elif s == 'image':
|
|
self.autoscale_view(tight=True)
|
|
self.set_autoscale_on(False)
|
|
self.set_aspect('equal', adjustable='box', anchor='C')
|
|
elif s == '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('Unrecognized string %s to axis; '
|
|
'try on or off' % s)
|
|
else:
|
|
if len(args) >= 1:
|
|
if len(args) != 1:
|
|
cbook.warn_deprecated(
|
|
"3.2", message="Passing more than one positional "
|
|
"argument to axis() is deprecated and will raise a "
|
|
"TypeError %(removal)s.")
|
|
limits = args[0]
|
|
try:
|
|
xmin, xmax, ymin, ymax = limits
|
|
except (TypeError, ValueError):
|
|
raise TypeError('the first argument to axis() must be an '
|
|
'interable of the form '
|
|
'[xmin, xmax, ymin, ymax]')
|
|
else:
|
|
xmin = kwargs.pop('xmin', None)
|
|
xmax = kwargs.pop('xmax', None)
|
|
ymin = kwargs.pop('ymin', None)
|
|
ymax = kwargs.pop('ymax', None)
|
|
xauto = (None # Keep autoscale state as is.
|
|
if xmin is None and xmax is None
|
|
else False) # Turn off autoscale.
|
|
yauto = (None
|
|
if ymin is None and ymax is None
|
|
else False)
|
|
self.set_xlim(xmin, xmax, emit=emit, auto=xauto)
|
|
self.set_ylim(ymin, ymax, emit=emit, auto=yauto)
|
|
if kwargs:
|
|
cbook.warn_deprecated(
|
|
"3.1", message="Passing unsupported keyword arguments to "
|
|
"axis() will raise a TypeError %(removal)s.")
|
|
return (*self.get_xlim(), *self.get_ylim())
|
|
|
|
def get_legend(self):
|
|
"""Return the `Legend` instance, or None if no legend is defined."""
|
|
return self.legend_
|
|
|
|
def get_images(self):
|
|
"""return a list of Axes images 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):
|
|
"""Return the XAxis instance."""
|
|
return self.xaxis
|
|
|
|
def get_xgridlines(self):
|
|
"""Get the x grid lines as a list of `Line2D` instances."""
|
|
return self.xaxis.get_gridlines()
|
|
|
|
def get_xticklines(self):
|
|
"""Get the x tick lines as a list of `Line2D` instances."""
|
|
return self.xaxis.get_ticklines()
|
|
|
|
def get_yaxis(self):
|
|
"""Return the YAxis instance."""
|
|
return self.yaxis
|
|
|
|
def get_ygridlines(self):
|
|
"""Get the y grid lines as a list of `Line2D` instances."""
|
|
return self.yaxis.get_gridlines()
|
|
|
|
def get_yticklines(self):
|
|
"""Get the y tick lines as a list of `Line2D` instances."""
|
|
return self.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.
|
|
"""
|
|
if isinstance(im, mpl.contour.ContourSet):
|
|
if im.collections[0] not in self.collections:
|
|
raise ValueError("ContourSet must be in current Axes")
|
|
elif im not in self.images and im not in self.collections:
|
|
raise ValueError("Argument must be an image, collection, or "
|
|
"ContourSet in this Axes")
|
|
self._current_image = im
|
|
|
|
def _gci(self):
|
|
"""
|
|
Helper for :func:`~matplotlib.pyplot.gci`;
|
|
do not use elsewhere.
|
|
"""
|
|
return self._current_image
|
|
|
|
def has_data(self):
|
|
"""
|
|
Return *True* if any artists have been added to axes.
|
|
|
|
This should not be used to determine whether the *dataLim*
|
|
need to be updated, and may not actually be useful for
|
|
anything.
|
|
"""
|
|
return (
|
|
len(self.collections) +
|
|
len(self.images) +
|
|
len(self.lines) +
|
|
len(self.patches)) > 0
|
|
|
|
def add_artist(self, a):
|
|
"""
|
|
Add an `~.Artist` to the axes, and 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.artists.append(a)
|
|
a._remove_method = self.artists.remove
|
|
self._set_artist_props(a)
|
|
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 = self.child_axes.remove
|
|
self.stale = True
|
|
return ax
|
|
|
|
def add_collection(self, collection, autolim=True):
|
|
"""
|
|
Add a `~.Collection` to the axes' collections; return the collection.
|
|
"""
|
|
label = collection.get_label()
|
|
if not label:
|
|
collection.set_label('_collection%d' % len(self.collections))
|
|
self.collections.append(collection)
|
|
collection._remove_method = self.collections.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()
|
|
self.update_datalim(collection.get_datalim(self.transData))
|
|
|
|
self.stale = True
|
|
return collection
|
|
|
|
def add_image(self, image):
|
|
"""
|
|
Add an `~.AxesImage` to the axes' images; return the image.
|
|
"""
|
|
self._set_artist_props(image)
|
|
if not image.get_label():
|
|
image.set_label('_image%d' % len(self.images))
|
|
self.images.append(image)
|
|
image._remove_method = self.images.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' lines; return the 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('_line%d' % len(self.lines))
|
|
self.lines.append(line)
|
|
line._remove_method = self.lines.remove
|
|
self.stale = True
|
|
return line
|
|
|
|
def _add_text(self, txt):
|
|
"""
|
|
Add a `~.Text` to the axes' texts; return the text.
|
|
"""
|
|
self._set_artist_props(txt)
|
|
self.texts.append(txt)
|
|
txt._remove_method = self.texts.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_trans = line.get_transform()
|
|
|
|
if line_trans == self.transData:
|
|
data_path = path
|
|
|
|
elif any(line_trans.contains_branch_seperately(self.transData)):
|
|
# identify the transform to go from line's coordinates
|
|
# to data coordinates
|
|
trans_to_data = line_trans - 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_trans is
|
|
# entirely encapsulated in trans_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 = trans_to_data.transform_path_affine(na_path)
|
|
else:
|
|
data_path = trans_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 data_path.vertices.size > 0:
|
|
updatex, updatey = line_trans.contains_branch_seperately(
|
|
self.transData)
|
|
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' patches; return the patch.
|
|
"""
|
|
self._set_artist_props(p)
|
|
if p.get_clip_path() is None:
|
|
p.set_clip_path(self.patch)
|
|
self._update_patch_limits(p)
|
|
self.patches.append(p)
|
|
p._remove_method = self.patches.remove
|
|
return p
|
|
|
|
def _update_patch_limits(self, patch):
|
|
"""update the data limits for patch *p*"""
|
|
# 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
|
|
vertices = patch.get_path().vertices
|
|
if vertices.size > 0:
|
|
xys = patch.get_patch_transform().transform(vertices)
|
|
if patch.get_data_transform() != self.transData:
|
|
patch_to_data = (patch.get_data_transform() -
|
|
self.transData)
|
|
xys = patch_to_data.transform(xys)
|
|
|
|
updatex, updatey = patch.get_transform().\
|
|
contains_branch_seperately(self.transData)
|
|
self.update_datalim(xys, updatex=updatex,
|
|
updatey=updatey)
|
|
|
|
def add_table(self, tab):
|
|
"""
|
|
Add a `~.Table` to the axes' tables; return the table.
|
|
"""
|
|
self._set_artist_props(tab)
|
|
self.tables.append(tab)
|
|
tab.set_clip_path(self.patch)
|
|
tab._remove_method = self.tables.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 _on_units_changed(self, scalex=False, scaley=False):
|
|
"""
|
|
Callback for processing changes to axis units.
|
|
|
|
Currently requests updates of data limits and view limits.
|
|
"""
|
|
self.relim()
|
|
self._request_autoscale_view(scalex=scalex, scaley=scaley)
|
|
|
|
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
|
|
Whether to exclude invisible artists. Defaults to False.
|
|
"""
|
|
# 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 line in self.lines:
|
|
if not visible_only or line.get_visible():
|
|
self._update_line_limits(line)
|
|
|
|
for p in self.patches:
|
|
if not visible_only or p.get_visible():
|
|
self._update_patch_limits(p)
|
|
|
|
for image in self.images:
|
|
if not visible_only or image.get_visible():
|
|
self._update_image_limits(image)
|
|
|
|
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 Nx2 array.
|
|
|
|
updatex, updatey : bool, optional, default *True*
|
|
Whether to update the x/y limits.
|
|
"""
|
|
xys = np.asarray(xys)
|
|
if not len(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 update_datalim_bounds(self, bounds):
|
|
"""
|
|
Extend the `~.Axes.datalim` Bbox to include the given
|
|
`~matplotlib.transforms.Bbox`.
|
|
|
|
Parameters
|
|
----------
|
|
bounds : `~matplotlib.transforms.Bbox`
|
|
"""
|
|
self.dataLim.set(mtransforms.Bbox.union([self.dataLim, bounds]))
|
|
|
|
def _process_unit_info(self, xdata=None, ydata=None, kwargs=None):
|
|
"""Look for unit *kwargs* and update the axis instances as necessary"""
|
|
|
|
def _process_single_axis(data, axis, unit_name, kwargs):
|
|
# Return if there's no axis set
|
|
if axis is None:
|
|
return kwargs
|
|
|
|
if data is not None:
|
|
# We only need to update if there is nothing set yet.
|
|
if not axis.have_units():
|
|
axis.update_units(data)
|
|
|
|
# Check for units in the kwargs, and if present update axis
|
|
if kwargs is not None:
|
|
units = kwargs.pop(unit_name, axis.units)
|
|
if self.name == 'polar':
|
|
polar_units = {'xunits': 'thetaunits', 'yunits': 'runits'}
|
|
units = kwargs.pop(polar_units[unit_name], units)
|
|
|
|
if units != axis.units:
|
|
axis.set_units(units)
|
|
# If the units being set imply a different converter,
|
|
# we need to update.
|
|
if data is not None:
|
|
axis.update_units(data)
|
|
return kwargs
|
|
|
|
kwargs = _process_single_axis(xdata, self.xaxis, 'xunits', kwargs)
|
|
kwargs = _process_single_axis(ydata, self.yaxis, 'yunits', kwargs)
|
|
return kwargs
|
|
|
|
def in_axes(self, mouseevent):
|
|
"""
|
|
Return *True* if the given *mouseevent* (in display coords)
|
|
is in the Axes
|
|
"""
|
|
return self.patch.contains(mouseevent)[0]
|
|
|
|
def get_autoscale_on(self):
|
|
"""
|
|
Get whether autoscaling is applied for both axes on plot commands
|
|
"""
|
|
return self._autoscaleXon and self._autoscaleYon
|
|
|
|
def get_autoscalex_on(self):
|
|
"""
|
|
Get whether autoscaling for the x-axis is applied on plot commands
|
|
"""
|
|
return self._autoscaleXon
|
|
|
|
def get_autoscaley_on(self):
|
|
"""
|
|
Get whether autoscaling for the y-axis is applied on plot commands
|
|
"""
|
|
return self._autoscaleYon
|
|
|
|
def set_autoscale_on(self, b):
|
|
"""
|
|
Set whether autoscaling is applied on plot commands
|
|
|
|
Parameters
|
|
----------
|
|
b : bool
|
|
"""
|
|
self._autoscaleXon = b
|
|
self._autoscaleYon = b
|
|
|
|
def set_autoscalex_on(self, b):
|
|
"""
|
|
Set whether autoscaling for the x-axis is applied on plot commands
|
|
|
|
Parameters
|
|
----------
|
|
b : bool
|
|
"""
|
|
self._autoscaleXon = b
|
|
|
|
def set_autoscaley_on(self, b):
|
|
"""
|
|
Set whether autoscaling for the y-axis is applied on plot commands
|
|
|
|
Parameters
|
|
----------
|
|
b : bool
|
|
"""
|
|
self._autoscaleYon = 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 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.
|
|
For example, if your data is in the range [0, 2], a factor of
|
|
``m = 0.1`` will result in a range [-0.2, 2.2].
|
|
|
|
Negative values -0.5 < m < 0 will result in clipping of the data range.
|
|
I.e. for a data range [0, 2], a factor of ``m = -0.1`` will result in
|
|
a range [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.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.
|
|
For example, if your data is in the range [0, 2], a factor of
|
|
``m = 0.1`` will result in a range [-0.2, 2.2].
|
|
|
|
Negative values -0.5 < m < 0 will result in clipping of the data range.
|
|
I.e. for a data range [0, 2], a factor of ``m = -0.1`` will result in
|
|
a range [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.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
|
|
within the range [0, 1]. 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 in place 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 is True
|
|
The *tight* parameter is passed to :meth:`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. Set *tight* to *None* will preserve
|
|
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 and 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:
|
|
cbook._warn_external(f'ignoring tight={tight!r} in get mode')
|
|
return self._xmargin, self._ymargin
|
|
|
|
if x is not None:
|
|
self.set_xmargin(x)
|
|
if y is not None:
|
|
self.set_ymargin(y)
|
|
|
|
self._request_autoscale_view(
|
|
tight=tight, scalex=(x is not None), scaley=(y is not None)
|
|
)
|
|
|
|
def set_rasterization_zorder(self, z):
|
|
"""
|
|
Parameters
|
|
----------
|
|
z : float or None
|
|
zorder below which artists are rasterized. ``None`` means that
|
|
artists do not get rasterized based on zorder.
|
|
"""
|
|
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, optional
|
|
True (default) turns autoscaling on, False turns it off.
|
|
None leaves the autoscaling state unchanged.
|
|
|
|
axis : {'both', 'x', 'y'}, optional
|
|
Which axis to operate on; default is 'both'.
|
|
|
|
tight : bool or None, optional
|
|
If True, first set the margins to zero. Then, this argument is
|
|
forwarded to `autoscale_view` (regardless of its value); see the
|
|
description of its behavior there.
|
|
"""
|
|
if enable is None:
|
|
scalex = True
|
|
scaley = True
|
|
else:
|
|
scalex = False
|
|
scaley = False
|
|
if axis in ['x', 'both']:
|
|
self._autoscaleXon = bool(enable)
|
|
scalex = self._autoscaleXon
|
|
if axis in ['y', 'both']:
|
|
self._autoscaleYon = bool(enable)
|
|
scaley = self._autoscaleYon
|
|
if tight and scalex:
|
|
self._xmargin = 0
|
|
if tight and scaley:
|
|
self._ymargin = 0
|
|
self._request_autoscale_view(tight=tight, scalex=scalex, scaley=scaley)
|
|
|
|
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
|
|
Whether to autoscale the x axis (default is True).
|
|
|
|
scaley : bool
|
|
Whether to autoscale the x axis (default is True).
|
|
|
|
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:
|
|
# Only iterate over axes and artists if needed. The check for
|
|
# ``hasattr(ax, "lines")`` is necessary because this can be called
|
|
# very early in the axes init process (e.g., for twin axes) when
|
|
# these attributes don't even exist yet, in which case
|
|
# `get_children` would raise an AttributeError.
|
|
if self._xmargin and scalex and self._autoscaleXon:
|
|
x_stickies = np.sort(np.concatenate([
|
|
artist.sticky_edges.x
|
|
for ax in self._shared_x_axes.get_siblings(self)
|
|
if hasattr(ax, "lines")
|
|
for artist in ax.get_children()]))
|
|
if self._ymargin and scaley and self._autoscaleYon:
|
|
y_stickies = np.sort(np.concatenate([
|
|
artist.sticky_edges.y
|
|
for ax in self._shared_y_axes.get_siblings(self)
|
|
if hasattr(ax, "lines")
|
|
for artist in ax.get_children()]))
|
|
if self.get_xscale().lower() == 'log':
|
|
x_stickies = x_stickies[x_stickies > 0]
|
|
if self.get_yscale().lower() == 'log':
|
|
y_stickies = y_stickies[y_stickies > 0]
|
|
|
|
def handle_single_axis(scale, autoscaleon, shared_axes, interval,
|
|
minpos, axis, margin, stickies, set_bound):
|
|
|
|
if not (scale and autoscaleon):
|
|
return # nothing to do...
|
|
|
|
shared = shared_axes.get_siblings(self)
|
|
dl = [ax.dataLim for ax in shared]
|
|
# ignore non-finite data limits if good limits exist
|
|
finite_dl = [d for d in dl if np.isfinite(d).all()]
|
|
if len(finite_dl):
|
|
# if finite limits exist for atleast one axis (and the
|
|
# other is infinite), restore the finite limits
|
|
x_finite = [d for d in dl
|
|
if (np.isfinite(d.intervalx).all() and
|
|
(d not in finite_dl))]
|
|
y_finite = [d for d in dl
|
|
if (np.isfinite(d.intervaly).all() and
|
|
(d not in finite_dl))]
|
|
|
|
dl = finite_dl
|
|
dl.extend(x_finite)
|
|
dl.extend(y_finite)
|
|
|
|
bb = mtransforms.BboxBase.union(dl)
|
|
x0, x1 = getattr(bb, interval)
|
|
# If x0 and x1 are non finite, use the locator to figure out
|
|
# default limits.
|
|
locator = axis.get_major_locator()
|
|
x0, x1 = locator.nonsingular(x0, x1)
|
|
|
|
# Prevent margin addition from crossing a sticky value. Small
|
|
# tolerances (whose values come from isclose()) must be used due to
|
|
# floating point issues with streamplot.
|
|
def tol(x): return 1e-5 * abs(x) + 1e-8
|
|
# Index of largest element < x0 + tol, if any.
|
|
i0 = stickies.searchsorted(x0 + tol(x0)) - 1
|
|
x0bound = stickies[i0] if i0 != -1 else None
|
|
# Index of smallest element > x1 - tol, if any.
|
|
i1 = stickies.searchsorted(x1 - tol(x1))
|
|
x1bound = stickies[i1] if i1 != len(stickies) else None
|
|
|
|
# Add the margin in figure space and then transform back, to handle
|
|
# non-linear scales.
|
|
minpos = getattr(bb, minpos)
|
|
transform = axis.get_transform()
|
|
inverse_trans = transform.inverted()
|
|
x0, x1 = axis._scale.limit_range_for_scale(x0, x1, 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._autoscaleXon, self._shared_x_axes, 'intervalx',
|
|
'minposx', self.xaxis, self._xmargin, x_stickies, self.set_xbound)
|
|
handle_single_axis(
|
|
scaley, self._autoscaleYon, self._shared_y_axes, 'intervaly',
|
|
'minposy', self.yaxis, self._ymargin, y_stickies, self.set_ybound)
|
|
|
|
def _get_axis_list(self):
|
|
return (self.xaxis, self.yaxis)
|
|
|
|
def _get_axis_map(self):
|
|
"""
|
|
Return a mapping of `Axis` "names" to `Axis` instances.
|
|
|
|
The `Axis` name is derived from the attribute under which the instance
|
|
is stored, so e.g. for polar axes, the theta-axis is still named "x"
|
|
and the r-axis is still named "y" (for back-compatibility).
|
|
|
|
In practice, this means that the entries are typically "x" and "y", and
|
|
additionally "z" for 3D axes.
|
|
"""
|
|
d = {}
|
|
axis_list = self._get_axis_list()
|
|
for k, v in vars(self).items():
|
|
if k.endswith("axis") and v in axis_list:
|
|
d[k[:-len("axis")]] = v
|
|
return d
|
|
|
|
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)
|
|
|
|
if self._autotitlepos is None:
|
|
for title in titles:
|
|
x, y = title.get_position()
|
|
if not np.isclose(y, 1.0):
|
|
self._autotitlepos = False
|
|
_log.debug('not adjusting title pos because a title was'
|
|
' already placed manually: %f', y)
|
|
return
|
|
self._autotitlepos = True
|
|
|
|
for title in titles:
|
|
x, _ = title.get_position()
|
|
# need to start again in case of window resizing
|
|
title.set_position((x, 1.0))
|
|
# need to check all our twins too...
|
|
axs = self._twinned_axes.get_siblings(self)
|
|
# and all the children
|
|
for ax in self.child_axes:
|
|
if ax is not None:
|
|
locator = ax.get_axes_locator()
|
|
if locator:
|
|
pos = locator(self, renderer)
|
|
ax.apply_aspect(pos)
|
|
else:
|
|
ax.apply_aspect()
|
|
axs = axs + [ax]
|
|
top = 0
|
|
for ax in axs:
|
|
if (ax.xaxis.get_ticks_position() in ['top', 'unknown']
|
|
or ax.xaxis.get_label_position() == 'top'):
|
|
bb = ax.xaxis.get_tightbbox(renderer)
|
|
else:
|
|
bb = ax.get_window_extent(renderer)
|
|
if bb is not None:
|
|
top = max(top, bb.ymax)
|
|
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=None, inframe=False):
|
|
"""Draw everything (plot lines, axes, labels)"""
|
|
if renderer is None:
|
|
renderer = self.figure._cachedRenderer
|
|
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()
|
|
if locator:
|
|
pos = locator(self, renderer)
|
|
self.apply_aspect(pos)
|
|
else:
|
|
self.apply_aspect()
|
|
|
|
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 or inframe:
|
|
for _axis in self._get_axis_list():
|
|
artists.remove(_axis)
|
|
|
|
if inframe:
|
|
artists.remove(self.title)
|
|
artists.remove(self._left_title)
|
|
artists.remove(self._right_title)
|
|
|
|
if not self.figure.canvas.is_saving():
|
|
artists = [a for a in artists
|
|
if not a.get_animated() or a in self.images]
|
|
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):
|
|
renderer.start_rasterizing()
|
|
artists_rasterized = [a for a in artists
|
|
if a.zorder < rasterization_zorder]
|
|
artists = [a for a in artists
|
|
if a.zorder >= rasterization_zorder]
|
|
else:
|
|
artists_rasterized = []
|
|
|
|
# the patch draws the background rectangle -- the frame below
|
|
# will draw the edges
|
|
if self.axison and self._frameon:
|
|
self.patch.draw(renderer)
|
|
|
|
if artists_rasterized:
|
|
for a in artists_rasterized:
|
|
a.draw(renderer)
|
|
renderer.stop_rasterizing()
|
|
|
|
mimage._draw_list_compositing_images(renderer, self, artists)
|
|
|
|
renderer.close_group('axes')
|
|
self.stale = False
|
|
|
|
def draw_artist(self, a):
|
|
"""
|
|
This method can only be used after an initial draw which
|
|
caches the renderer. It is used to efficiently update Axes
|
|
data (axis ticks, labels, etc are not updated)
|
|
"""
|
|
if self.figure._cachedRenderer is None:
|
|
raise AttributeError("draw_artist can only be used after an "
|
|
"initial draw which caches the renderer")
|
|
a.draw(self.figure._cachedRenderer)
|
|
|
|
def redraw_in_frame(self):
|
|
"""
|
|
This method can only be used after an initial draw which
|
|
caches the renderer. It is used to efficiently update Axes
|
|
data (axis ticks, labels, etc are not updated)
|
|
"""
|
|
if self.figure._cachedRenderer is None:
|
|
raise AttributeError("redraw_in_frame can only be used after an "
|
|
"initial draw which caches the renderer")
|
|
self.draw(self.figure._cachedRenderer, inframe=True)
|
|
|
|
def get_renderer_cache(self):
|
|
return self.figure._cachedRenderer
|
|
|
|
# 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
|
|
-------
|
|
axisbelow : 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 all Artists.
|
|
- '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.
|
|
|
|
See Also
|
|
--------
|
|
get_axisbelow
|
|
"""
|
|
self._axisbelow = axisbelow = validate_axisbelow(b)
|
|
if axisbelow is True:
|
|
zorder = 0.5
|
|
elif axisbelow is False:
|
|
zorder = 2.5
|
|
elif axisbelow == "line":
|
|
zorder = 1.5
|
|
else:
|
|
raise ValueError("Unexpected axisbelow value")
|
|
for axis in self._get_axis_list():
|
|
axis.set_zorder(zorder)
|
|
self.stale = True
|
|
|
|
@docstring.dedent_interpd
|
|
def grid(self, b=None, which='major', axis='both', **kwargs):
|
|
"""
|
|
Configure the grid lines.
|
|
|
|
Parameters
|
|
----------
|
|
b : bool or None, optional
|
|
Whether to show the grid lines. If any *kwargs* are supplied,
|
|
it is assumed you want the grid on and *b* will be set to True.
|
|
|
|
If *b* 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 : `.Line2D` properties
|
|
Define the line properties of the grid, e.g.::
|
|
|
|
grid(color='r', linestyle='-', linewidth=2)
|
|
|
|
Valid keyword arguments are:
|
|
|
|
%(_Line2D_docstr)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
|
|
`~matplotlib.axis.Axis.set_zorder` method of each axis.
|
|
"""
|
|
if len(kwargs):
|
|
b = True
|
|
cbook._check_in_list(['x', 'y', 'both'], axis=axis)
|
|
if axis in ['x', 'both']:
|
|
self.xaxis.grid(b, which=which, **kwargs)
|
|
if axis in ['y', 'both']:
|
|
self.yaxis.grid(b, which=which, **kwargs)
|
|
|
|
def ticklabel_format(self, *, axis='both', style='', scilimits=None,
|
|
useOffset=None, useLocale=None, useMathText=None):
|
|
r"""
|
|
Change the `~matplotlib.ticker.ScalarFormatter` used by
|
|
default for linear axes.
|
|
|
|
Optional keyword arguments:
|
|
|
|
============== =========================================
|
|
Keyword Description
|
|
============== =========================================
|
|
*axis* {'x', 'y', 'both'}
|
|
*style* {'sci' (or 'scientific'), 'plain'}
|
|
plain turns off scientific notation
|
|
*scilimits* (m, n), pair of integers; if *style*
|
|
is 'sci', scientific notation will
|
|
be used for numbers outside the range
|
|
10\ :sup:`m` to 10\ :sup:`n`.
|
|
Use (0, 0) to include all numbers.
|
|
Use (m, m) where m != 0 to fix the order
|
|
of magnitude to 10\ :sup:`m`.
|
|
*useOffset* bool or float
|
|
If True, the offset will be calculated as
|
|
needed; if False, no offset will be used;
|
|
if a numeric offset is specified, it will
|
|
be used.
|
|
*useLocale* If True, format the number according to
|
|
the current locale. This affects things
|
|
such as the character used for the
|
|
decimal separator. If False, use
|
|
C-style (English) formatting. The
|
|
default setting is controlled by the
|
|
axes.formatter.use_locale rcparam.
|
|
*useMathText* If True, render the offset and scientific
|
|
notation in mathtext
|
|
============== =========================================
|
|
|
|
Only the major ticks are affected.
|
|
If the method is called when the `~matplotlib.ticker.ScalarFormatter`
|
|
is not the `~matplotlib.ticker.Formatter` being used, an
|
|
`AttributeError` will be raised.
|
|
"""
|
|
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):
|
|
raise ValueError("scilimits must be a sequence of 2 integers")
|
|
STYLES = {'sci': True, 'scientific': True, 'plain': False, '': None}
|
|
is_sci_style = cbook._check_getitem(STYLES, style=style)
|
|
axis_map = {**{k: [v] for k, v in self._get_axis_map().items()},
|
|
'both': self._get_axis_list()}
|
|
axises = cbook._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:
|
|
raise AttributeError(
|
|
"This method only works with the ScalarFormatter")
|
|
|
|
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'}, optional
|
|
The axis on which to operate.
|
|
|
|
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 axes.
|
|
|
|
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)
|
|
|
|
"""
|
|
_x = axis in ['x', 'both']
|
|
_y = axis in ['y', 'both']
|
|
if _x:
|
|
self.xaxis.get_major_locator().set_params(**kwargs)
|
|
if _y:
|
|
self.yaxis.get_major_locator().set_params(**kwargs)
|
|
self._request_autoscale_view(tight=tight, scalex=_x, scaley=_y)
|
|
|
|
def tick_params(self, axis='both', **kwargs):
|
|
"""Change the appearance of ticks, tick labels, and gridlines.
|
|
|
|
Parameters
|
|
----------
|
|
axis : {'x', 'y', 'both'}, optional
|
|
Which axis to apply the parameters to.
|
|
|
|
Other Parameters
|
|
----------------
|
|
axis : {'x', 'y', 'both'}
|
|
Axis on which to operate; default is 'both'.
|
|
reset : bool, default: False
|
|
If *True*, set all parameters to defaults before processing other
|
|
keyword arguments.
|
|
which : {'major', 'minor', 'both'}
|
|
Default is 'major'; apply arguments to *which* ticks.
|
|
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 : 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 : color
|
|
Tick label color.
|
|
colors : 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 : 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
|
|
--------
|
|
Usage ::
|
|
|
|
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.
|
|
|
|
"""
|
|
cbook._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):
|
|
"""
|
|
Turn the x- and y-axis off.
|
|
|
|
This affects the axis lines, ticks, ticklabels, grid and axis labels.
|
|
"""
|
|
self.axison = False
|
|
self.stale = True
|
|
|
|
def set_axis_on(self):
|
|
"""
|
|
Turn the x- and y-axis on.
|
|
|
|
This affects the axis lines, ticks, ticklabels, grid and axis labels.
|
|
"""
|
|
self.axison = True
|
|
self.stale = True
|
|
|
|
# data limits, ticks, tick labels, and formatting
|
|
|
|
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())
|
|
|
|
def xaxis_inverted(self):
|
|
"""
|
|
Return whether the x-axis is inverted.
|
|
|
|
The axis is inverted if the left value is larger than the right value.
|
|
|
|
See Also
|
|
--------
|
|
invert_xaxis
|
|
get_xlim, set_xlim
|
|
get_xbound, set_xbound
|
|
"""
|
|
return self.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 axes inversion regardless of parameter order.
|
|
It will not change the autoscaling setting (``Axes._autoscaleXon``).
|
|
|
|
Parameters
|
|
----------
|
|
lower, upper : float or None
|
|
The lower and upper bounds. If *None*, the respective axis bound
|
|
is not modified.
|
|
|
|
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
|
|
|
|
if self.xaxis_inverted():
|
|
if lower < upper:
|
|
self.set_xlim(upper, lower, auto=None)
|
|
else:
|
|
self.set_xlim(lower, upper, auto=None)
|
|
else:
|
|
if lower < upper:
|
|
self.set_xlim(lower, upper, auto=None)
|
|
else:
|
|
self.set_xlim(upper, lower, 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
|
|
--------
|
|
set_xlim
|
|
set_xbound, get_xbound
|
|
invert_xaxis, 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, 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: (bottom: float, top: float)
|
|
|
|
right : float, optional
|
|
The right xlim in data coordinates. Passing *None* leaves the
|
|
limit unchanged.
|
|
|
|
emit : bool, optional
|
|
Whether to notify observers of limit change (default: True).
|
|
|
|
auto : bool or None, optional
|
|
Whether to turn on autoscaling of the x-axis. True turns on,
|
|
False turns off (default action), None leaves unchanged.
|
|
|
|
xmin, xmax : scalar, 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 `xmin` and `left`')
|
|
left = xmin
|
|
if xmax is not None:
|
|
if right is not None:
|
|
raise TypeError('Cannot pass both `xmax` and `right`')
|
|
right = xmax
|
|
|
|
self._process_unit_info(xdata=(left, right))
|
|
left = self._validate_converted_limits(left, self.convert_xunits)
|
|
right = self._validate_converted_limits(right, self.convert_xunits)
|
|
|
|
if left is None or right is None:
|
|
# Axes init calls set_xlim(0, 1) before get_xlim() can be called,
|
|
# so only grab the limits if we really need them.
|
|
old_left, old_right = self.get_xlim()
|
|
if left is None:
|
|
left = old_left
|
|
if right is None:
|
|
right = old_right
|
|
|
|
if self.get_xscale() == 'log' and (left <= 0 or right <= 0):
|
|
# Axes init calls set_xlim(0, 1) before get_xlim() can be called,
|
|
# so only grab the limits if we really need them.
|
|
old_left, old_right = self.get_xlim()
|
|
if left <= 0:
|
|
cbook._warn_external(
|
|
'Attempted to set non-positive left xlim on a '
|
|
'log-scaled axis.\n'
|
|
'Invalid limit will be ignored.')
|
|
left = old_left
|
|
if right <= 0:
|
|
cbook._warn_external(
|
|
'Attempted to set non-positive right xlim on a '
|
|
'log-scaled axis.\n'
|
|
'Invalid limit will be ignored.')
|
|
right = old_right
|
|
if left == right:
|
|
cbook._warn_external(
|
|
f"Attempting to set identical left == right == {left} results "
|
|
f"in singular transformations; automatically expanding.")
|
|
reverse = left > right
|
|
left, right = self.xaxis.get_major_locator().nonsingular(left, right)
|
|
left, right = self.xaxis.limit_range_for_scale(left, right)
|
|
# cast to bool to avoid bad interaction between python 3.8 and np.bool_
|
|
left, right = sorted([left, right], reverse=bool(reverse))
|
|
|
|
self._viewLim.intervalx = (left, right)
|
|
# Mark viewlims as no longer stale without triggering an autoscale.
|
|
for ax in self._shared_x_axes.get_siblings(self):
|
|
ax._stale_viewlim_x = False
|
|
if auto is not None:
|
|
self._autoscaleXon = bool(auto)
|
|
|
|
if emit:
|
|
self.callbacks.process('xlim_changed', self)
|
|
# Call all of the other x-axes that are shared with this one
|
|
for other in self._shared_x_axes.get_siblings(self):
|
|
if other is not self:
|
|
other.set_xlim(self.viewLim.intervalx,
|
|
emit=False, auto=auto)
|
|
if other.figure != self.figure:
|
|
other.figure.canvas.draw_idle()
|
|
self.stale = True
|
|
return left, right
|
|
|
|
def get_xscale(self):
|
|
"""
|
|
Return the x-axis scale as string.
|
|
|
|
See Also
|
|
--------
|
|
set_xscale
|
|
"""
|
|
return self.xaxis.get_scale()
|
|
|
|
def set_xscale(self, value, **kwargs):
|
|
"""
|
|
Set the x-axis scale.
|
|
|
|
Parameters
|
|
----------
|
|
value : {"linear", "log", "symlog", "logit", ...}
|
|
The axis scale type to apply.
|
|
|
|
**kwargs
|
|
Different keyword arguments are accepted, depending on the scale.
|
|
See the respective class keyword arguments:
|
|
|
|
- `matplotlib.scale.LinearScale`
|
|
- `matplotlib.scale.LogScale`
|
|
- `matplotlib.scale.SymmetricalLogScale`
|
|
- `matplotlib.scale.LogitScale`
|
|
|
|
Notes
|
|
-----
|
|
By default, Matplotlib supports the above mentioned scales.
|
|
Additionally, custom scales may be registered using
|
|
`matplotlib.scale.register_scale`. These scales can then also
|
|
be used here.
|
|
"""
|
|
old_default_lims = (self.xaxis.get_major_locator()
|
|
.nonsingular(-np.inf, np.inf))
|
|
g = self.get_shared_x_axes()
|
|
for ax in g.get_siblings(self):
|
|
ax.xaxis._set_scale(value, **kwargs)
|
|
ax._update_transScale()
|
|
ax.stale = True
|
|
new_default_lims = (self.xaxis.get_major_locator()
|
|
.nonsingular(-np.inf, np.inf))
|
|
if old_default_lims != new_default_lims:
|
|
# Force autoscaling now, to take advantage of the scale locator's
|
|
# nonsingular() before it possibly gets swapped out by the user.
|
|
self.autoscale_view(scaley=False)
|
|
|
|
@cbook._make_keyword_only("3.2", "minor")
|
|
def get_xticks(self, minor=False):
|
|
"""Return the x ticks as a list of locations"""
|
|
return self.xaxis.get_ticklocs(minor=minor)
|
|
|
|
@cbook._make_keyword_only("3.2", "minor")
|
|
def set_xticks(self, ticks, minor=False):
|
|
"""
|
|
Set the x ticks with list of *ticks*
|
|
|
|
Parameters
|
|
----------
|
|
ticks : list
|
|
List of x-axis tick locations.
|
|
|
|
minor : bool, optional
|
|
If ``False`` sets major ticks, if ``True`` sets minor ticks.
|
|
Default is ``False``.
|
|
"""
|
|
ret = self.xaxis.set_ticks(ticks, minor=minor)
|
|
self.stale = True
|
|
return ret
|
|
|
|
def get_xmajorticklabels(self):
|
|
"""
|
|
Get the major x tick labels.
|
|
|
|
Returns
|
|
-------
|
|
labels : list
|
|
List of `~matplotlib.text.Text` instances
|
|
"""
|
|
return self.xaxis.get_majorticklabels()
|
|
|
|
def get_xminorticklabels(self):
|
|
"""
|
|
Get the minor x tick labels.
|
|
|
|
Returns
|
|
-------
|
|
labels : list
|
|
List of `~matplotlib.text.Text` instances
|
|
"""
|
|
return self.xaxis.get_minorticklabels()
|
|
|
|
def get_xticklabels(self, minor=False, which=None):
|
|
"""
|
|
Get the x tick labels as a list of `~matplotlib.text.Text` instances.
|
|
|
|
Parameters
|
|
----------
|
|
minor : bool, optional
|
|
If True return the minor ticklabels,
|
|
else return the major ticklabels.
|
|
|
|
which : None, ('minor', 'major', 'both')
|
|
Overrides *minor*.
|
|
|
|
Selects which ticklabels to return
|
|
|
|
Returns
|
|
-------
|
|
ret : list
|
|
List of `~matplotlib.text.Text` instances.
|
|
"""
|
|
return self.xaxis.get_ticklabels(minor=minor, which=which)
|
|
|
|
def set_xticklabels(self, labels, fontdict=None, minor=False, **kwargs):
|
|
"""
|
|
Set the x-tick labels with list of string labels.
|
|
|
|
Parameters
|
|
----------
|
|
labels : List[str]
|
|
List of string labels.
|
|
|
|
fontdict : dict, optional
|
|
A dictionary controlling the appearance of the ticklabels.
|
|
The default *fontdict* is::
|
|
|
|
{'fontsize': rcParams['axes.titlesize'],
|
|
'fontweight': rcParams['axes.titleweight'],
|
|
'verticalalignment': 'baseline',
|
|
'horizontalalignment': loc}
|
|
|
|
minor : bool, optional
|
|
Whether to set the minor ticklabels rather than the major ones.
|
|
|
|
Returns
|
|
-------
|
|
A list of `~.text.Text` instances.
|
|
|
|
Other Parameters
|
|
-----------------
|
|
**kwargs : `~.text.Text` properties.
|
|
"""
|
|
if fontdict is not None:
|
|
kwargs.update(fontdict)
|
|
ret = self.xaxis.set_ticklabels(labels,
|
|
minor=minor, **kwargs)
|
|
self.stale = True
|
|
return ret
|
|
|
|
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())
|
|
|
|
def yaxis_inverted(self):
|
|
"""
|
|
Return whether the y-axis is inverted.
|
|
|
|
The axis is inverted if the bottom value is larger than the top value.
|
|
|
|
See Also
|
|
--------
|
|
invert_yaxis
|
|
get_ylim, set_ylim
|
|
get_ybound, set_ybound
|
|
"""
|
|
return self.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 axes inversion regardless of parameter order.
|
|
It will not change the autoscaling setting (``Axes._autoscaleYon``).
|
|
|
|
Parameters
|
|
----------
|
|
lower, upper : float or None
|
|
The lower and upper bounds. If *None*, the respective axis bound
|
|
is not modified.
|
|
|
|
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
|
|
|
|
if self.yaxis_inverted():
|
|
if lower < upper:
|
|
self.set_ylim(upper, lower, auto=None)
|
|
else:
|
|
self.set_ylim(lower, upper, auto=None)
|
|
else:
|
|
if lower < upper:
|
|
self.set_ylim(lower, upper, auto=None)
|
|
else:
|
|
self.set_ylim(upper, lower, 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
|
|
--------
|
|
set_ylim
|
|
set_ybound, get_ybound
|
|
invert_yaxis, 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, optional
|
|
Whether to notify observers of limit change (default: ``True``).
|
|
|
|
auto : bool or None, optional
|
|
Whether to turn on autoscaling of the y-axis. *True* turns on,
|
|
*False* turns off (default action), *None* leaves unchanged.
|
|
|
|
ymin, ymax : scalar, 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 `ymin` and `bottom`')
|
|
bottom = ymin
|
|
if ymax is not None:
|
|
if top is not None:
|
|
raise TypeError('Cannot pass both `ymax` and `top`')
|
|
top = ymax
|
|
|
|
self._process_unit_info(ydata=(bottom, top))
|
|
bottom = self._validate_converted_limits(bottom, self.convert_yunits)
|
|
top = self._validate_converted_limits(top, self.convert_yunits)
|
|
|
|
if bottom is None or top is None:
|
|
# Axes init calls set_ylim(0, 1) before get_ylim() can be called,
|
|
# so only grab the limits if we really need them.
|
|
old_bottom, old_top = self.get_ylim()
|
|
if bottom is None:
|
|
bottom = old_bottom
|
|
if top is None:
|
|
top = old_top
|
|
|
|
if self.get_yscale() == 'log' and (bottom <= 0 or top <= 0):
|
|
# Axes init calls set_xlim(0, 1) before get_xlim() can be called,
|
|
# so only grab the limits if we really need them.
|
|
old_bottom, old_top = self.get_ylim()
|
|
if bottom <= 0:
|
|
cbook._warn_external(
|
|
'Attempted to set non-positive bottom ylim on a '
|
|
'log-scaled axis.\n'
|
|
'Invalid limit will be ignored.')
|
|
bottom = old_bottom
|
|
if top <= 0:
|
|
cbook._warn_external(
|
|
'Attempted to set non-positive top ylim on a '
|
|
'log-scaled axis.\n'
|
|
'Invalid limit will be ignored.')
|
|
top = old_top
|
|
if bottom == top:
|
|
cbook._warn_external(
|
|
f"Attempting to set identical bottom == top == {bottom} "
|
|
f"results in singular transformations; automatically "
|
|
f"expanding.")
|
|
reverse = bottom > top
|
|
bottom, top = self.yaxis.get_major_locator().nonsingular(bottom, top)
|
|
bottom, top = self.yaxis.limit_range_for_scale(bottom, top)
|
|
# cast to bool to avoid bad interaction between python 3.8 and np.bool_
|
|
bottom, top = sorted([bottom, top], reverse=bool(reverse))
|
|
|
|
self._viewLim.intervaly = (bottom, top)
|
|
# Mark viewlims as no longer stale without triggering an autoscale.
|
|
for ax in self._shared_y_axes.get_siblings(self):
|
|
ax._stale_viewlim_y = False
|
|
if auto is not None:
|
|
self._autoscaleYon = bool(auto)
|
|
|
|
if emit:
|
|
self.callbacks.process('ylim_changed', self)
|
|
# Call all of the other y-axes that are shared with this one
|
|
for other in self._shared_y_axes.get_siblings(self):
|
|
if other is not self:
|
|
other.set_ylim(self.viewLim.intervaly,
|
|
emit=False, auto=auto)
|
|
if other.figure != self.figure:
|
|
other.figure.canvas.draw_idle()
|
|
self.stale = True
|
|
return bottom, top
|
|
|
|
def get_yscale(self):
|
|
"""
|
|
Return the y-axis scale as string.
|
|
|
|
See Also
|
|
--------
|
|
set_yscale
|
|
"""
|
|
return self.yaxis.get_scale()
|
|
|
|
def set_yscale(self, value, **kwargs):
|
|
"""
|
|
Set the y-axis scale.
|
|
|
|
Parameters
|
|
----------
|
|
value : {"linear", "log", "symlog", "logit", ...}
|
|
The axis scale type to apply.
|
|
|
|
**kwargs
|
|
Different keyword arguments are accepted, depending on the scale.
|
|
See the respective class keyword arguments:
|
|
|
|
- `matplotlib.scale.LinearScale`
|
|
- `matplotlib.scale.LogScale`
|
|
- `matplotlib.scale.SymmetricalLogScale`
|
|
- `matplotlib.scale.LogitScale`
|
|
|
|
Notes
|
|
-----
|
|
By default, Matplotlib supports the above mentioned scales.
|
|
Additionally, custom scales may be registered using
|
|
`matplotlib.scale.register_scale`. These scales can then also
|
|
be used here.
|
|
"""
|
|
old_default_lims = (self.yaxis.get_major_locator()
|
|
.nonsingular(-np.inf, np.inf))
|
|
g = self.get_shared_y_axes()
|
|
for ax in g.get_siblings(self):
|
|
ax.yaxis._set_scale(value, **kwargs)
|
|
ax._update_transScale()
|
|
ax.stale = True
|
|
new_default_lims = (self.yaxis.get_major_locator()
|
|
.nonsingular(-np.inf, np.inf))
|
|
if old_default_lims != new_default_lims:
|
|
# Force autoscaling now, to take advantage of the scale locator's
|
|
# nonsingular() before it possibly gets swapped out by the user.
|
|
self.autoscale_view(scalex=False)
|
|
|
|
@cbook._make_keyword_only("3.2", "minor")
|
|
def get_yticks(self, minor=False):
|
|
"""Return the y ticks as a list of locations"""
|
|
return self.yaxis.get_ticklocs(minor=minor)
|
|
|
|
@cbook._make_keyword_only("3.2", "minor")
|
|
def set_yticks(self, ticks, minor=False):
|
|
"""
|
|
Set the y ticks with list of *ticks*
|
|
|
|
Parameters
|
|
----------
|
|
ticks : list
|
|
List of y-axis tick locations
|
|
|
|
minor : bool, optional
|
|
If ``False`` sets major ticks, if ``True`` sets minor ticks.
|
|
Default is ``False``.
|
|
"""
|
|
ret = self.yaxis.set_ticks(ticks, minor=minor)
|
|
return ret
|
|
|
|
def get_ymajorticklabels(self):
|
|
"""
|
|
Get the major y tick labels.
|
|
|
|
Returns
|
|
-------
|
|
labels : list
|
|
List of `~matplotlib.text.Text` instances
|
|
"""
|
|
return self.yaxis.get_majorticklabels()
|
|
|
|
def get_yminorticklabels(self):
|
|
"""
|
|
Get the minor y tick labels.
|
|
|
|
Returns
|
|
-------
|
|
labels : list
|
|
List of `~matplotlib.text.Text` instances
|
|
"""
|
|
return self.yaxis.get_minorticklabels()
|
|
|
|
def get_yticklabels(self, minor=False, which=None):
|
|
"""
|
|
Get the y tick labels as a list of `~matplotlib.text.Text` instances.
|
|
|
|
Parameters
|
|
----------
|
|
minor : bool
|
|
If True return the minor ticklabels,
|
|
else return the major ticklabels
|
|
|
|
which : None, ('minor', 'major', 'both')
|
|
Overrides *minor*.
|
|
|
|
Selects which ticklabels to return
|
|
|
|
Returns
|
|
-------
|
|
ret : list
|
|
List of `~matplotlib.text.Text` instances.
|
|
"""
|
|
return self.yaxis.get_ticklabels(minor=minor, which=which)
|
|
|
|
def set_yticklabels(self, labels, fontdict=None, minor=False, **kwargs):
|
|
"""
|
|
Set the y-tick labels with list of strings labels.
|
|
|
|
Parameters
|
|
----------
|
|
labels : List[str]
|
|
list of string labels
|
|
|
|
fontdict : dict, optional
|
|
A dictionary controlling the appearance of the ticklabels.
|
|
The default *fontdict* is::
|
|
|
|
{'fontsize': rcParams['axes.titlesize'],
|
|
'fontweight': rcParams['axes.titleweight'],
|
|
'verticalalignment': 'baseline',
|
|
'horizontalalignment': loc}
|
|
|
|
minor : bool, optional
|
|
Whether to set the minor ticklabels rather than the major ones.
|
|
|
|
Returns
|
|
-------
|
|
A list of `~.text.Text` instances.
|
|
|
|
Other Parameters
|
|
----------------
|
|
**kwargs : `~.text.Text` properties.
|
|
"""
|
|
if fontdict is not None:
|
|
kwargs.update(fontdict)
|
|
return self.yaxis.set_ticklabels(labels,
|
|
minor=minor, **kwargs)
|
|
|
|
def xaxis_date(self, tz=None):
|
|
"""
|
|
Sets up x-axis ticks and labels that treat the x data as dates.
|
|
|
|
Parameters
|
|
----------
|
|
tz : str or `tzinfo` instance, optional
|
|
Timezone. Defaults to :rc:`timezone`.
|
|
"""
|
|
# should be enough to inform the unit conversion interface
|
|
# dates are coming in
|
|
self.xaxis.axis_date(tz)
|
|
|
|
def yaxis_date(self, tz=None):
|
|
"""
|
|
Sets up y-axis ticks and labels that treat the y data as dates.
|
|
|
|
Parameters
|
|
----------
|
|
tz : str or `tzinfo` instance, optional
|
|
Timezone. Defaults to :rc:`timezone`.
|
|
"""
|
|
self.yaxis.axis_date(tz)
|
|
|
|
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 an 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."""
|
|
if x is None:
|
|
xs = '???'
|
|
else:
|
|
xs = self.format_xdata(x)
|
|
if y is None:
|
|
ys = '???'
|
|
else:
|
|
ys = self.format_ydata(y)
|
|
return 'x=%s y=%s' % (xs, ys)
|
|
|
|
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.
|
|
"""
|
|
for ax in (self.xaxis, self.yaxis):
|
|
scale = ax.get_scale()
|
|
if scale == 'log':
|
|
s = ax._scale
|
|
ax.set_minor_locator(mticker.LogLocator(s.base, s.subs))
|
|
elif scale == 'symlog':
|
|
s = ax._scale
|
|
ax.set_minor_locator(
|
|
mticker.SymmetricalLogLocator(s._transform, s.subs))
|
|
else:
|
|
ax.set_minor_locator(mticker.AutoMinorLocator())
|
|
|
|
def minorticks_off(self):
|
|
"""Remove minor ticks from the axes."""
|
|
self.xaxis.set_minor_locator(mticker.NullLocator())
|
|
self.yaxis.set_minor_locator(mticker.NullLocator())
|
|
|
|
# Interactive manipulation
|
|
|
|
def can_zoom(self):
|
|
"""
|
|
Return *True* if this axes supports the zoom box button functionality.
|
|
"""
|
|
return True
|
|
|
|
def can_pan(self):
|
|
"""
|
|
Return *True* if 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
|
|
"""
|
|
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.
|
|
|
|
Called before a view is changed, such as during a pan or zoom
|
|
initiated by the user. You may return any information you deem
|
|
necessary to describe the view.
|
|
|
|
.. note::
|
|
|
|
Intended to be overridden by new projection types, but if not, the
|
|
default implementation saves the view limits. You *must* implement
|
|
:meth:`_set_view` if you implement this method.
|
|
"""
|
|
xmin, xmax = self.get_xlim()
|
|
ymin, ymax = self.get_ylim()
|
|
return (xmin, xmax, ymin, ymax)
|
|
|
|
def _set_view(self, view):
|
|
"""
|
|
Apply a previously saved view.
|
|
|
|
Called when restoring a view, such as with the navigation buttons.
|
|
|
|
.. note::
|
|
|
|
Intended to be overridden by new projection types, but if not, the
|
|
default implementation restores the view limits. You *must*
|
|
implement :meth:`_get_view` if you implement this method.
|
|
"""
|
|
xmin, xmax, ymin, ymax = view
|
|
self.set_xlim((xmin, xmax))
|
|
self.set_ylim((ymin, ymax))
|
|
|
|
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.
|
|
"""
|
|
Xmin, Xmax = self.get_xlim()
|
|
Ymin, Ymax = self.get_ylim()
|
|
|
|
if len(bbox) == 3:
|
|
# Zooming code
|
|
xp, yp, scl = bbox
|
|
|
|
# Should not happen
|
|
if scl == 0:
|
|
scl = 1.
|
|
|
|
# direction = 'in'
|
|
if scl > 1:
|
|
direction = 'in'
|
|
else:
|
|
direction = 'out'
|
|
scl = 1/scl
|
|
|
|
# get the limits of the axes
|
|
tranD2C = self.transData.transform
|
|
xmin, ymin = tranD2C((Xmin, Ymin))
|
|
xmax, ymax = tranD2C((Xmax, Ymax))
|
|
|
|
# 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
|
|
cbook._warn_external(
|
|
"Warning in _set_view_from_bbox: bounding box is not a tuple "
|
|
"of length 3 or 4. Ignoring the view change.")
|
|
return
|
|
|
|
# Just grab bounding box
|
|
lastx, lasty, x, y = bbox
|
|
|
|
# zoom to rect
|
|
inverse = self.transData.inverted()
|
|
(lastx, lasty), (x, y) = inverse.transform([(lastx, lasty), (x, y)])
|
|
|
|
if twinx:
|
|
x0, x1 = Xmin, Xmax
|
|
else:
|
|
if Xmin < Xmax:
|
|
if x < lastx:
|
|
x0, x1 = x, lastx
|
|
else:
|
|
x0, x1 = lastx, x
|
|
if x0 < Xmin:
|
|
x0 = Xmin
|
|
if x1 > Xmax:
|
|
x1 = Xmax
|
|
else:
|
|
if x > lastx:
|
|
x0, x1 = x, lastx
|
|
else:
|
|
x0, x1 = lastx, x
|
|
if x0 > Xmin:
|
|
x0 = Xmin
|
|
if x1 < Xmax:
|
|
x1 = Xmax
|
|
|
|
if twiny:
|
|
y0, y1 = Ymin, Ymax
|
|
else:
|
|
if Ymin < Ymax:
|
|
if y < lasty:
|
|
y0, y1 = y, lasty
|
|
else:
|
|
y0, y1 = lasty, y
|
|
if y0 < Ymin:
|
|
y0 = Ymin
|
|
if y1 > Ymax:
|
|
y1 = Ymax
|
|
else:
|
|
if y > lasty:
|
|
y0, y1 = y, lasty
|
|
else:
|
|
y0, y1 = lasty, y
|
|
if y0 > Ymin:
|
|
y0 = Ymin
|
|
if y1 < Ymax:
|
|
y1 = Ymax
|
|
|
|
if direction == 'in':
|
|
if mode == 'x':
|
|
self.set_xlim((x0, x1))
|
|
elif mode == 'y':
|
|
self.set_ylim((y0, y1))
|
|
else:
|
|
self.set_xlim((x0, x1))
|
|
self.set_ylim((y0, y1))
|
|
elif direction == 'out':
|
|
if self.get_xscale() == 'log':
|
|
alpha = np.log(Xmax / Xmin) / np.log(x1 / x0)
|
|
rx1 = pow(Xmin / x0, alpha) * Xmin
|
|
rx2 = pow(Xmax / x0, alpha) * Xmin
|
|
else:
|
|
alpha = (Xmax - Xmin) / (x1 - x0)
|
|
rx1 = alpha * (Xmin - x0) + Xmin
|
|
rx2 = alpha * (Xmax - x0) + Xmin
|
|
if self.get_yscale() == 'log':
|
|
alpha = np.log(Ymax / Ymin) / np.log(y1 / y0)
|
|
ry1 = pow(Ymin / y0, alpha) * Ymin
|
|
ry2 = pow(Ymax / y0, alpha) * Ymin
|
|
else:
|
|
alpha = (Ymax - Ymin) / (y1 - y0)
|
|
ry1 = alpha * (Ymin - y0) + Ymin
|
|
ry2 = alpha * (Ymax - y0) + Ymin
|
|
|
|
if mode == 'x':
|
|
self.set_xlim((rx1, rx2))
|
|
elif mode == 'y':
|
|
self.set_ylim((ry1, ry2))
|
|
else:
|
|
self.set_xlim((rx1, rx2))
|
|
self.set_ylim((ry1, ry2))
|
|
|
|
def start_pan(self, x, y, button):
|
|
"""
|
|
Called when a pan operation has started.
|
|
|
|
*x*, *y* are the mouse coordinates in display coords.
|
|
button is the mouse button number:
|
|
|
|
* 1: LEFT
|
|
* 2: MIDDLE
|
|
* 3: RIGHT
|
|
|
|
.. note::
|
|
|
|
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.)
|
|
|
|
.. note::
|
|
|
|
Intended to be overridden by new projection types.
|
|
|
|
"""
|
|
del self._pan_start
|
|
|
|
def drag_pan(self, button, key, x, y):
|
|
"""
|
|
Called when the mouse moves during a pan operation.
|
|
|
|
*button* is the mouse button number:
|
|
|
|
* 1: LEFT
|
|
* 2: MIDDLE
|
|
* 3: RIGHT
|
|
|
|
*key* is a "shift" key
|
|
|
|
*x*, *y* are the mouse coordinates in display coords.
|
|
|
|
.. note::
|
|
|
|
Intended to be overridden by new projection types.
|
|
|
|
"""
|
|
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:
|
|
cbook._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
|
|
self.set_xlim(points[:, 0])
|
|
self.set_ylim(points[:, 1])
|
|
|
|
def get_children(self):
|
|
# docstring inherited.
|
|
return [
|
|
*self.collections,
|
|
*self.patches,
|
|
*self.lines,
|
|
*self.texts,
|
|
*self.artists,
|
|
*self.spines.values(),
|
|
*self._get_axis_list(),
|
|
self.title, self._left_title, self._right_title,
|
|
*self.tables,
|
|
*self.images,
|
|
*self.child_axes,
|
|
*([self.legend_] if self.legend_ is not None else []),
|
|
self.patch,
|
|
]
|
|
|
|
def contains(self, mouseevent):
|
|
# docstring inherited.
|
|
inside, info = self._default_contains(mouseevent)
|
|
if inside is not None:
|
|
return inside, info
|
|
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()
|
|
|
|
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)
|
|
|
|
if not self.axison:
|
|
for _axis in self._get_axis_list():
|
|
artists.remove(_axis)
|
|
|
|
return [artist for artist in artists
|
|
if (artist.get_visible() and artist.get_in_layout())]
|
|
|
|
def get_tightbbox(self, renderer, call_axes_locator=True,
|
|
bbox_extra_artists=None):
|
|
"""
|
|
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` instance
|
|
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 : boolean (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.
|
|
|
|
Returns
|
|
-------
|
|
bbox : `.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 not self.get_visible():
|
|
return None
|
|
|
|
locator = self.get_axes_locator()
|
|
if locator and call_axes_locator:
|
|
pos = locator(self, renderer)
|
|
self.apply_aspect(pos)
|
|
else:
|
|
self.apply_aspect()
|
|
|
|
if self.axison:
|
|
bb_xaxis = self.xaxis.get_tightbbox(renderer)
|
|
if bb_xaxis:
|
|
bb.append(bb_xaxis)
|
|
|
|
bb_yaxis = self.yaxis.get_tightbbox(renderer)
|
|
if bb_yaxis:
|
|
bb.append(bb_yaxis)
|
|
|
|
self._update_title_position(renderer)
|
|
axbbox = self.get_window_extent(renderer)
|
|
bb.append(axbbox)
|
|
|
|
self._update_title_position(renderer)
|
|
if self.title.get_visible():
|
|
bb.append(self.title.get_window_extent(renderer))
|
|
if self._left_title.get_visible():
|
|
bb.append(self._left_title.get_window_extent(renderer))
|
|
if self._right_title.get_visible():
|
|
bb.append(self._right_title.get_window_extent(renderer))
|
|
|
|
bb.append(self.get_window_extent(renderer))
|
|
|
|
bbox_artists = bbox_extra_artists
|
|
if bbox_artists is None:
|
|
bbox_artists = self.get_default_bbox_extra_artists()
|
|
|
|
for a in bbox_artists:
|
|
# Extra check here to quickly see if clipping is on and
|
|
# contained in the axes. If it is, don't get the tightbbox for
|
|
# this artist because this can be expensive:
|
|
clip_extent = a._get_clipping_extent_bbox()
|
|
if clip_extent is not None:
|
|
clip_extent = mtransforms.Bbox.intersection(clip_extent,
|
|
axbbox)
|
|
if np.all(clip_extent.extents == axbbox.extents):
|
|
# clip extent is inside the axes bbox so don't check
|
|
# this artist
|
|
continue
|
|
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)
|
|
_bbox = mtransforms.Bbox.union(
|
|
[b for b in bb if b.width != 0 or b.height != 0])
|
|
|
|
return _bbox
|
|
|
|
def _make_twin_axes(self, *args, **kwargs):
|
|
"""Make a twinx axes of self. This is used for twinx and twiny."""
|
|
# Typically, SubplotBase._make_twin_axes is called instead of this.
|
|
if 'sharex' in kwargs and 'sharey' in kwargs:
|
|
raise ValueError("Twinned Axes may share only one axis")
|
|
ax2 = self.figure.add_axes(self.get_position(True), *args, **kwargs)
|
|
self.set_adjustable('datalim')
|
|
ax2.set_adjustable('datalim')
|
|
self._twinned_axes.join(self, ax2)
|
|
return ax2
|
|
|
|
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
|
|
-------
|
|
ax_twin : 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)
|
|
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
|
|
-------
|
|
ax_twin : 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)
|
|
return ax2
|
|
|
|
def get_shared_x_axes(self):
|
|
"""Return a reference to the shared axes Grouper object for x axes."""
|
|
return self._shared_x_axes
|
|
|
|
def get_shared_y_axes(self):
|
|
"""Return a reference to the shared axes Grouper object for y axes."""
|
|
return self._shared_y_axes
|