ai-content-maker/.venv/Lib/site-packages/mpl_toolkits/axes_grid1/axes_divider.py

695 lines
24 KiB
Python
Raw Normal View History

2024-05-03 04:18:51 +03:00
"""
Helper classes to adjust the positions of multiple axes at drawing time.
"""
import functools
import numpy as np
import matplotlib as mpl
from matplotlib import _api
from matplotlib.gridspec import SubplotSpec
import matplotlib.transforms as mtransforms
from . import axes_size as Size
class Divider:
"""
An Axes positioning class.
The divider is initialized with lists of horizontal and vertical sizes
(:mod:`mpl_toolkits.axes_grid1.axes_size`) based on which a given
rectangular area will be divided.
The `new_locator` method then creates a callable object
that can be used as the *axes_locator* of the axes.
"""
def __init__(self, fig, pos, horizontal, vertical,
aspect=None, anchor="C"):
"""
Parameters
----------
fig : Figure
pos : tuple of 4 floats
Position of the rectangle that will be divided.
horizontal : list of :mod:`~mpl_toolkits.axes_grid1.axes_size`
Sizes for horizontal division.
vertical : list of :mod:`~mpl_toolkits.axes_grid1.axes_size`
Sizes for vertical division.
aspect : bool, optional
Whether overall rectangular area is reduced so that the relative
part of the horizontal and vertical scales have the same scale.
anchor : (float, float) or {'C', 'SW', 'S', 'SE', 'E', 'NE', 'N', \
'NW', 'W'}, default: 'C'
Placement of the reduced rectangle, when *aspect* is True.
"""
self._fig = fig
self._pos = pos
self._horizontal = horizontal
self._vertical = vertical
self._anchor = anchor
self.set_anchor(anchor)
self._aspect = aspect
self._xrefindex = 0
self._yrefindex = 0
self._locator = None
def get_horizontal_sizes(self, renderer):
return np.array([s.get_size(renderer) for s in self.get_horizontal()])
def get_vertical_sizes(self, renderer):
return np.array([s.get_size(renderer) for s in self.get_vertical()])
def set_position(self, pos):
"""
Set the position of the rectangle.
Parameters
----------
pos : tuple of 4 floats
position of the rectangle that will be divided
"""
self._pos = pos
def get_position(self):
"""Return the position of the rectangle."""
return self._pos
def set_anchor(self, anchor):
"""
Parameters
----------
anchor : (float, float) or {'C', 'SW', 'S', 'SE', 'E', 'NE', 'N', \
'NW', 'W'}
Either an (*x*, *y*) pair of relative coordinates (0 is left or
bottom, 1 is right or top), 'C' (center), or a cardinal direction
('SW', southwest, is bottom left, etc.).
See Also
--------
.Axes.set_anchor
"""
if isinstance(anchor, str):
_api.check_in_list(mtransforms.Bbox.coefs, anchor=anchor)
elif not isinstance(anchor, (tuple, list)) or len(anchor) != 2:
raise TypeError("anchor must be str or 2-tuple")
self._anchor = anchor
def get_anchor(self):
"""Return the anchor."""
return self._anchor
def get_subplotspec(self):
return None
def set_horizontal(self, h):
"""
Parameters
----------
h : list of :mod:`~mpl_toolkits.axes_grid1.axes_size`
sizes for horizontal division
"""
self._horizontal = h
def get_horizontal(self):
"""Return horizontal sizes."""
return self._horizontal
def set_vertical(self, v):
"""
Parameters
----------
v : list of :mod:`~mpl_toolkits.axes_grid1.axes_size`
sizes for vertical division
"""
self._vertical = v
def get_vertical(self):
"""Return vertical sizes."""
return self._vertical
def set_aspect(self, aspect=False):
"""
Parameters
----------
aspect : bool
"""
self._aspect = aspect
def get_aspect(self):
"""Return aspect."""
return self._aspect
def set_locator(self, _locator):
self._locator = _locator
def get_locator(self):
return self._locator
def get_position_runtime(self, ax, renderer):
if self._locator is None:
return self.get_position()
else:
return self._locator(ax, renderer).bounds
@staticmethod
def _calc_k(sizes, total):
# sizes is a (n, 2) array of (rel_size, abs_size); this method finds
# the k factor such that sum(rel_size * k + abs_size) == total.
rel_sum, abs_sum = sizes.sum(0)
return (total - abs_sum) / rel_sum if rel_sum else 0
@staticmethod
def _calc_offsets(sizes, k):
# Apply k factors to (n, 2) sizes array of (rel_size, abs_size); return
# the resulting cumulative offset positions.
return np.cumsum([0, *(sizes @ [k, 1])])
def new_locator(self, nx, ny, nx1=None, ny1=None):
"""
Return an axes locator callable for the specified cell.
Parameters
----------
nx, nx1 : int
Integers specifying the column-position of the
cell. When *nx1* is None, a single *nx*-th column is
specified. Otherwise, location of columns spanning between *nx*
to *nx1* (but excluding *nx1*-th column) is specified.
ny, ny1 : int
Same as *nx* and *nx1*, but for row positions.
"""
if nx1 is None:
nx1 = nx + 1
if ny1 is None:
ny1 = ny + 1
# append_size("left") adds a new size at the beginning of the
# horizontal size lists; this shift transforms e.g.
# new_locator(nx=2, ...) into effectively new_locator(nx=3, ...). To
# take that into account, instead of recording nx, we record
# nx-self._xrefindex, where _xrefindex is shifted by 1 by each
# append_size("left"), and re-add self._xrefindex back to nx in
# _locate, when the actual axes position is computed. Ditto for y.
xref = self._xrefindex
yref = self._yrefindex
locator = functools.partial(
self._locate, nx - xref, ny - yref, nx1 - xref, ny1 - yref)
locator.get_subplotspec = self.get_subplotspec
return locator
@_api.deprecated(
"3.8", alternative="divider.new_locator(...)(ax, renderer)")
def locate(self, nx, ny, nx1=None, ny1=None, axes=None, renderer=None):
"""
Implementation of ``divider.new_locator().__call__``.
Parameters
----------
nx, nx1 : int
Integers specifying the column-position of the cell. When *nx1* is
None, a single *nx*-th column is specified. Otherwise, the
location of columns spanning between *nx* to *nx1* (but excluding
*nx1*-th column) is specified.
ny, ny1 : int
Same as *nx* and *nx1*, but for row positions.
axes
renderer
"""
xref = self._xrefindex
yref = self._yrefindex
return self._locate(
nx - xref, (nx + 1 if nx1 is None else nx1) - xref,
ny - yref, (ny + 1 if ny1 is None else ny1) - yref,
axes, renderer)
def _locate(self, nx, ny, nx1, ny1, axes, renderer):
"""
Implementation of ``divider.new_locator().__call__``.
The axes locator callable returned by ``new_locator()`` is created as
a `functools.partial` of this method with *nx*, *ny*, *nx1*, and *ny1*
specifying the requested cell.
"""
nx += self._xrefindex
nx1 += self._xrefindex
ny += self._yrefindex
ny1 += self._yrefindex
fig_w, fig_h = self._fig.bbox.size / self._fig.dpi
x, y, w, h = self.get_position_runtime(axes, renderer)
hsizes = self.get_horizontal_sizes(renderer)
vsizes = self.get_vertical_sizes(renderer)
k_h = self._calc_k(hsizes, fig_w * w)
k_v = self._calc_k(vsizes, fig_h * h)
if self.get_aspect():
k = min(k_h, k_v)
ox = self._calc_offsets(hsizes, k)
oy = self._calc_offsets(vsizes, k)
ww = (ox[-1] - ox[0]) / fig_w
hh = (oy[-1] - oy[0]) / fig_h
pb = mtransforms.Bbox.from_bounds(x, y, w, h)
pb1 = mtransforms.Bbox.from_bounds(x, y, ww, hh)
x0, y0 = pb1.anchored(self.get_anchor(), pb).p0
else:
ox = self._calc_offsets(hsizes, k_h)
oy = self._calc_offsets(vsizes, k_v)
x0, y0 = x, y
if nx1 is None:
nx1 = -1
if ny1 is None:
ny1 = -1
x1, w1 = x0 + ox[nx] / fig_w, (ox[nx1] - ox[nx]) / fig_w
y1, h1 = y0 + oy[ny] / fig_h, (oy[ny1] - oy[ny]) / fig_h
return mtransforms.Bbox.from_bounds(x1, y1, w1, h1)
def append_size(self, position, size):
_api.check_in_list(["left", "right", "bottom", "top"],
position=position)
if position == "left":
self._horizontal.insert(0, size)
self._xrefindex += 1
elif position == "right":
self._horizontal.append(size)
elif position == "bottom":
self._vertical.insert(0, size)
self._yrefindex += 1
else: # 'top'
self._vertical.append(size)
def add_auto_adjustable_area(self, use_axes, pad=0.1, adjust_dirs=None):
"""
Add auto-adjustable padding around *use_axes* to take their decorations
(title, labels, ticks, ticklabels) into account during layout.
Parameters
----------
use_axes : `~matplotlib.axes.Axes` or list of `~matplotlib.axes.Axes`
The Axes whose decorations are taken into account.
pad : float, default: 0.1
Additional padding in inches.
adjust_dirs : list of {"left", "right", "bottom", "top"}, optional
The sides where padding is added; defaults to all four sides.
"""
if adjust_dirs is None:
adjust_dirs = ["left", "right", "bottom", "top"]
for d in adjust_dirs:
self.append_size(d, Size._AxesDecorationsSize(use_axes, d) + pad)
@_api.deprecated("3.8")
class AxesLocator:
"""
A callable object which returns the position and size of a given
`.AxesDivider` cell.
"""
def __init__(self, axes_divider, nx, ny, nx1=None, ny1=None):
"""
Parameters
----------
axes_divider : `~mpl_toolkits.axes_grid1.axes_divider.AxesDivider`
nx, nx1 : int
Integers specifying the column-position of the
cell. When *nx1* is None, a single *nx*-th column is
specified. Otherwise, location of columns spanning between *nx*
to *nx1* (but excluding *nx1*-th column) is specified.
ny, ny1 : int
Same as *nx* and *nx1*, but for row positions.
"""
self._axes_divider = axes_divider
_xrefindex = axes_divider._xrefindex
_yrefindex = axes_divider._yrefindex
self._nx, self._ny = nx - _xrefindex, ny - _yrefindex
if nx1 is None:
nx1 = len(self._axes_divider)
if ny1 is None:
ny1 = len(self._axes_divider[0])
self._nx1 = nx1 - _xrefindex
self._ny1 = ny1 - _yrefindex
def __call__(self, axes, renderer):
_xrefindex = self._axes_divider._xrefindex
_yrefindex = self._axes_divider._yrefindex
return self._axes_divider.locate(self._nx + _xrefindex,
self._ny + _yrefindex,
self._nx1 + _xrefindex,
self._ny1 + _yrefindex,
axes,
renderer)
def get_subplotspec(self):
return self._axes_divider.get_subplotspec()
class SubplotDivider(Divider):
"""
The Divider class whose rectangle area is specified as a subplot geometry.
"""
def __init__(self, fig, *args, horizontal=None, vertical=None,
aspect=None, anchor='C'):
"""
Parameters
----------
fig : `~matplotlib.figure.Figure`
*args : tuple (*nrows*, *ncols*, *index*) or int
The array of subplots in the figure has dimensions ``(nrows,
ncols)``, and *index* is the index of the subplot being created.
*index* starts at 1 in the upper left corner and increases to the
right.
If *nrows*, *ncols*, and *index* are all single digit numbers, then
*args* can be passed as a single 3-digit number (e.g. 234 for
(2, 3, 4)).
horizontal : list of :mod:`~mpl_toolkits.axes_grid1.axes_size`, optional
Sizes for horizontal division.
vertical : list of :mod:`~mpl_toolkits.axes_grid1.axes_size`, optional
Sizes for vertical division.
aspect : bool, optional
Whether overall rectangular area is reduced so that the relative
part of the horizontal and vertical scales have the same scale.
anchor : (float, float) or {'C', 'SW', 'S', 'SE', 'E', 'NE', 'N', \
'NW', 'W'}, default: 'C'
Placement of the reduced rectangle, when *aspect* is True.
"""
self.figure = fig
super().__init__(fig, [0, 0, 1, 1],
horizontal=horizontal or [], vertical=vertical or [],
aspect=aspect, anchor=anchor)
self.set_subplotspec(SubplotSpec._from_subplot_args(fig, args))
def get_position(self):
"""Return the bounds of the subplot box."""
return self.get_subplotspec().get_position(self.figure).bounds
def get_subplotspec(self):
"""Get the SubplotSpec instance."""
return self._subplotspec
def set_subplotspec(self, subplotspec):
"""Set the SubplotSpec instance."""
self._subplotspec = subplotspec
self.set_position(subplotspec.get_position(self.figure))
class AxesDivider(Divider):
"""
Divider based on the preexisting axes.
"""
def __init__(self, axes, xref=None, yref=None):
"""
Parameters
----------
axes : :class:`~matplotlib.axes.Axes`
xref
yref
"""
self._axes = axes
if xref is None:
self._xref = Size.AxesX(axes)
else:
self._xref = xref
if yref is None:
self._yref = Size.AxesY(axes)
else:
self._yref = yref
super().__init__(fig=axes.get_figure(), pos=None,
horizontal=[self._xref], vertical=[self._yref],
aspect=None, anchor="C")
def _get_new_axes(self, *, axes_class=None, **kwargs):
axes = self._axes
if axes_class is None:
axes_class = type(axes)
return axes_class(axes.get_figure(), axes.get_position(original=True),
**kwargs)
def new_horizontal(self, size, pad=None, pack_start=False, **kwargs):
"""
Helper method for ``append_axes("left")`` and ``append_axes("right")``.
See the documentation of `append_axes` for more details.
:meta private:
"""
if pad is None:
pad = mpl.rcParams["figure.subplot.wspace"] * self._xref
pos = "left" if pack_start else "right"
if pad:
if not isinstance(pad, Size._Base):
pad = Size.from_any(pad, fraction_ref=self._xref)
self.append_size(pos, pad)
if not isinstance(size, Size._Base):
size = Size.from_any(size, fraction_ref=self._xref)
self.append_size(pos, size)
locator = self.new_locator(
nx=0 if pack_start else len(self._horizontal) - 1,
ny=self._yrefindex)
ax = self._get_new_axes(**kwargs)
ax.set_axes_locator(locator)
return ax
def new_vertical(self, size, pad=None, pack_start=False, **kwargs):
"""
Helper method for ``append_axes("top")`` and ``append_axes("bottom")``.
See the documentation of `append_axes` for more details.
:meta private:
"""
if pad is None:
pad = mpl.rcParams["figure.subplot.hspace"] * self._yref
pos = "bottom" if pack_start else "top"
if pad:
if not isinstance(pad, Size._Base):
pad = Size.from_any(pad, fraction_ref=self._yref)
self.append_size(pos, pad)
if not isinstance(size, Size._Base):
size = Size.from_any(size, fraction_ref=self._yref)
self.append_size(pos, size)
locator = self.new_locator(
nx=self._xrefindex,
ny=0 if pack_start else len(self._vertical) - 1)
ax = self._get_new_axes(**kwargs)
ax.set_axes_locator(locator)
return ax
def append_axes(self, position, size, pad=None, *, axes_class=None,
**kwargs):
"""
Add a new axes on a given side of the main axes.
Parameters
----------
position : {"left", "right", "bottom", "top"}
Where the new axes is positioned relative to the main axes.
size : :mod:`~mpl_toolkits.axes_grid1.axes_size` or float or str
The axes width or height. float or str arguments are interpreted
as ``axes_size.from_any(size, AxesX(<main_axes>))`` for left or
right axes, and likewise with ``AxesY`` for bottom or top axes.
pad : :mod:`~mpl_toolkits.axes_grid1.axes_size` or float or str
Padding between the axes. float or str arguments are interpreted
as for *size*. Defaults to :rc:`figure.subplot.wspace` times the
main Axes width (left or right axes) or :rc:`figure.subplot.hspace`
times the main Axes height (bottom or top axes).
axes_class : subclass type of `~.axes.Axes`, optional
The type of the new axes. Defaults to the type of the main axes.
**kwargs
All extra keywords arguments are passed to the created axes.
"""
create_axes, pack_start = _api.check_getitem({
"left": (self.new_horizontal, True),
"right": (self.new_horizontal, False),
"bottom": (self.new_vertical, True),
"top": (self.new_vertical, False),
}, position=position)
ax = create_axes(
size, pad, pack_start=pack_start, axes_class=axes_class, **kwargs)
self._fig.add_axes(ax)
return ax
def get_aspect(self):
if self._aspect is None:
aspect = self._axes.get_aspect()
if aspect == "auto":
return False
else:
return True
else:
return self._aspect
def get_position(self):
if self._pos is None:
bbox = self._axes.get_position(original=True)
return bbox.bounds
else:
return self._pos
def get_anchor(self):
if self._anchor is None:
return self._axes.get_anchor()
else:
return self._anchor
def get_subplotspec(self):
return self._axes.get_subplotspec()
# Helper for HBoxDivider/VBoxDivider.
# The variable names are written for a horizontal layout, but the calculations
# work identically for vertical layouts.
def _locate(x, y, w, h, summed_widths, equal_heights, fig_w, fig_h, anchor):
total_width = fig_w * w
max_height = fig_h * h
# Determine the k factors.
n = len(equal_heights)
eq_rels, eq_abss = equal_heights.T
sm_rels, sm_abss = summed_widths.T
A = np.diag([*eq_rels, 0])
A[:n, -1] = -1
A[-1, :-1] = sm_rels
B = [*(-eq_abss), total_width - sm_abss.sum()]
# A @ K = B: This finds factors {k_0, ..., k_{N-1}, H} so that
# eq_rel_i * k_i + eq_abs_i = H for all i: all axes have the same height
# sum(sm_rel_i * k_i + sm_abs_i) = total_width: fixed total width
# (foo_rel_i * k_i + foo_abs_i will end up being the size of foo.)
*karray, height = np.linalg.solve(A, B)
if height > max_height: # Additionally, upper-bound the height.
karray = (max_height - eq_abss) / eq_rels
# Compute the offsets corresponding to these factors.
ox = np.cumsum([0, *(sm_rels * karray + sm_abss)])
ww = (ox[-1] - ox[0]) / fig_w
h0_rel, h0_abs = equal_heights[0]
hh = (karray[0]*h0_rel + h0_abs) / fig_h
pb = mtransforms.Bbox.from_bounds(x, y, w, h)
pb1 = mtransforms.Bbox.from_bounds(x, y, ww, hh)
x0, y0 = pb1.anchored(anchor, pb).p0
return x0, y0, ox, hh
class HBoxDivider(SubplotDivider):
"""
A `.SubplotDivider` for laying out axes horizontally, while ensuring that
they have equal heights.
Examples
--------
.. plot:: gallery/axes_grid1/demo_axes_hbox_divider.py
"""
def new_locator(self, nx, nx1=None):
"""
Create an axes locator callable for the specified cell.
Parameters
----------
nx, nx1 : int
Integers specifying the column-position of the
cell. When *nx1* is None, a single *nx*-th column is
specified. Otherwise, location of columns spanning between *nx*
to *nx1* (but excluding *nx1*-th column) is specified.
"""
return super().new_locator(nx, 0, nx1, 0)
def _locate(self, nx, ny, nx1, ny1, axes, renderer):
# docstring inherited
nx += self._xrefindex
nx1 += self._xrefindex
fig_w, fig_h = self._fig.bbox.size / self._fig.dpi
x, y, w, h = self.get_position_runtime(axes, renderer)
summed_ws = self.get_horizontal_sizes(renderer)
equal_hs = self.get_vertical_sizes(renderer)
x0, y0, ox, hh = _locate(
x, y, w, h, summed_ws, equal_hs, fig_w, fig_h, self.get_anchor())
if nx1 is None:
nx1 = -1
x1, w1 = x0 + ox[nx] / fig_w, (ox[nx1] - ox[nx]) / fig_w
y1, h1 = y0, hh
return mtransforms.Bbox.from_bounds(x1, y1, w1, h1)
class VBoxDivider(SubplotDivider):
"""
A `.SubplotDivider` for laying out axes vertically, while ensuring that
they have equal widths.
"""
def new_locator(self, ny, ny1=None):
"""
Create an axes locator callable for the specified cell.
Parameters
----------
ny, ny1 : int
Integers specifying the row-position of the
cell. When *ny1* is None, a single *ny*-th row is
specified. Otherwise, location of rows spanning between *ny*
to *ny1* (but excluding *ny1*-th row) is specified.
"""
return super().new_locator(0, ny, 0, ny1)
def _locate(self, nx, ny, nx1, ny1, axes, renderer):
# docstring inherited
ny += self._yrefindex
ny1 += self._yrefindex
fig_w, fig_h = self._fig.bbox.size / self._fig.dpi
x, y, w, h = self.get_position_runtime(axes, renderer)
summed_hs = self.get_vertical_sizes(renderer)
equal_ws = self.get_horizontal_sizes(renderer)
y0, x0, oy, ww = _locate(
y, x, h, w, summed_hs, equal_ws, fig_h, fig_w, self.get_anchor())
if ny1 is None:
ny1 = -1
x1, w1 = x0, ww
y1, h1 = y0 + oy[ny] / fig_h, (oy[ny1] - oy[ny]) / fig_h
return mtransforms.Bbox.from_bounds(x1, y1, w1, h1)
def make_axes_locatable(axes):
divider = AxesDivider(axes)
locator = divider.new_locator(nx=0, ny=0)
axes.set_axes_locator(locator)
return divider
def make_axes_area_auto_adjustable(
ax, use_axes=None, pad=0.1, adjust_dirs=None):
"""
Add auto-adjustable padding around *ax* to take its decorations (title,
labels, ticks, ticklabels) into account during layout, using
`.Divider.add_auto_adjustable_area`.
By default, padding is determined from the decorations of *ax*.
Pass *use_axes* to consider the decorations of other Axes instead.
"""
if adjust_dirs is None:
adjust_dirs = ["left", "right", "bottom", "top"]
divider = make_axes_locatable(ax)
if use_axes is None:
use_axes = ax
divider.add_auto_adjustable_area(use_axes=use_axes, pad=pad,
adjust_dirs=adjust_dirs)