is_assignments/a2/code/.venv/lib/python3.10/site-packages/matplotlib/gridspec.py

750 lines
28 KiB
Python
Raw Normal View History

2022-12-19 10:09:00 +01:00
r"""
:mod:`~matplotlib.gridspec` contains classes that help to layout multiple
`~.axes.Axes` in a grid-like pattern within a figure.
The `GridSpec` specifies the overall grid structure. Individual cells within
the grid are referenced by `SubplotSpec`\s.
Often, users need not access this module directly, and can use higher-level
methods like `~.pyplot.subplots`, `~.pyplot.subplot_mosaic` and
`~.Figure.subfigures`. See the tutorial
:doc:`/tutorials/intermediate/arranging_axes` for a guide.
"""
import copy
import logging
from numbers import Integral
import numpy as np
import matplotlib as mpl
from matplotlib import _api, _pylab_helpers, _tight_layout
from matplotlib.transforms import Bbox
_log = logging.getLogger(__name__)
class GridSpecBase:
"""
A base class of GridSpec that specifies the geometry of the grid
that a subplot will be placed.
"""
def __init__(self, nrows, ncols, height_ratios=None, width_ratios=None):
"""
Parameters
----------
nrows, ncols : int
The number of rows and columns of the grid.
width_ratios : array-like of length *ncols*, optional
Defines the relative widths of the columns. Each column gets a
relative width of ``width_ratios[i] / sum(width_ratios)``.
If not given, all columns will have the same width.
height_ratios : array-like of length *nrows*, optional
Defines the relative heights of the rows. Each row gets a
relative height of ``height_ratios[i] / sum(height_ratios)``.
If not given, all rows will have the same height.
"""
if not isinstance(nrows, Integral) or nrows <= 0:
raise ValueError(
f"Number of rows must be a positive integer, not {nrows!r}")
if not isinstance(ncols, Integral) or ncols <= 0:
raise ValueError(
f"Number of columns must be a positive integer, not {ncols!r}")
self._nrows, self._ncols = nrows, ncols
self.set_height_ratios(height_ratios)
self.set_width_ratios(width_ratios)
def __repr__(self):
height_arg = (', height_ratios=%r' % (self._row_height_ratios,)
if len(set(self._row_height_ratios)) != 1 else '')
width_arg = (', width_ratios=%r' % (self._col_width_ratios,)
if len(set(self._col_width_ratios)) != 1 else '')
return '{clsname}({nrows}, {ncols}{optionals})'.format(
clsname=self.__class__.__name__,
nrows=self._nrows,
ncols=self._ncols,
optionals=height_arg + width_arg,
)
nrows = property(lambda self: self._nrows,
doc="The number of rows in the grid.")
ncols = property(lambda self: self._ncols,
doc="The number of columns in the grid.")
def get_geometry(self):
"""
Return a tuple containing the number of rows and columns in the grid.
"""
return self._nrows, self._ncols
def get_subplot_params(self, figure=None):
# Must be implemented in subclasses
pass
def new_subplotspec(self, loc, rowspan=1, colspan=1):
"""
Create and return a `.SubplotSpec` instance.
Parameters
----------
loc : (int, int)
The position of the subplot in the grid as
``(row_index, column_index)``.
rowspan, colspan : int, default: 1
The number of rows and columns the subplot should span in the grid.
"""
loc1, loc2 = loc
subplotspec = self[loc1:loc1+rowspan, loc2:loc2+colspan]
return subplotspec
def set_width_ratios(self, width_ratios):
"""
Set the relative widths of the columns.
*width_ratios* must be of length *ncols*. Each column gets a relative
width of ``width_ratios[i] / sum(width_ratios)``.
"""
if width_ratios is None:
width_ratios = [1] * self._ncols
elif len(width_ratios) != self._ncols:
raise ValueError('Expected the given number of width ratios to '
'match the number of columns of the grid')
self._col_width_ratios = width_ratios
def get_width_ratios(self):
"""
Return the width ratios.
This is *None* if no width ratios have been set explicitly.
"""
return self._col_width_ratios
def set_height_ratios(self, height_ratios):
"""
Set the relative heights of the rows.
*height_ratios* must be of length *nrows*. Each row gets a relative
height of ``height_ratios[i] / sum(height_ratios)``.
"""
if height_ratios is None:
height_ratios = [1] * self._nrows
elif len(height_ratios) != self._nrows:
raise ValueError('Expected the given number of height ratios to '
'match the number of rows of the grid')
self._row_height_ratios = height_ratios
def get_height_ratios(self):
"""
Return the height ratios.
This is *None* if no height ratios have been set explicitly.
"""
return self._row_height_ratios
@_api.delete_parameter("3.7", "raw")
def get_grid_positions(self, fig, raw=False):
"""
Return the positions of the grid cells in figure coordinates.
Parameters
----------
fig : `~matplotlib.figure.Figure`
The figure the grid should be applied to. The subplot parameters
(margins and spacing between subplots) are taken from *fig*.
raw : bool, default: False
If *True*, the subplot parameters of the figure are not taken
into account. The grid spans the range [0, 1] in both directions
without margins and there is no space between grid cells. This is
used for constrained_layout.
Returns
-------
bottoms, tops, lefts, rights : array
The bottom, top, left, right positions of the grid cells in
figure coordinates.
"""
nrows, ncols = self.get_geometry()
if raw:
left = 0.
right = 1.
bottom = 0.
top = 1.
wspace = 0.
hspace = 0.
else:
subplot_params = self.get_subplot_params(fig)
left = subplot_params.left
right = subplot_params.right
bottom = subplot_params.bottom
top = subplot_params.top
wspace = subplot_params.wspace
hspace = subplot_params.hspace
tot_width = right - left
tot_height = top - bottom
# calculate accumulated heights of columns
cell_h = tot_height / (nrows + hspace*(nrows-1))
sep_h = hspace * cell_h
norm = cell_h * nrows / sum(self._row_height_ratios)
cell_heights = [r * norm for r in self._row_height_ratios]
sep_heights = [0] + ([sep_h] * (nrows-1))
cell_hs = np.cumsum(np.column_stack([sep_heights, cell_heights]).flat)
# calculate accumulated widths of rows
cell_w = tot_width / (ncols + wspace*(ncols-1))
sep_w = wspace * cell_w
norm = cell_w * ncols / sum(self._col_width_ratios)
cell_widths = [r * norm for r in self._col_width_ratios]
sep_widths = [0] + ([sep_w] * (ncols-1))
cell_ws = np.cumsum(np.column_stack([sep_widths, cell_widths]).flat)
fig_tops, fig_bottoms = (top - cell_hs).reshape((-1, 2)).T
fig_lefts, fig_rights = (left + cell_ws).reshape((-1, 2)).T
return fig_bottoms, fig_tops, fig_lefts, fig_rights
@staticmethod
def _check_gridspec_exists(figure, nrows, ncols):
"""
Check if the figure already has a gridspec with these dimensions,
or create a new one
"""
for ax in figure.get_axes():
if hasattr(ax, 'get_subplotspec'):
gs = ax.get_subplotspec().get_gridspec()
if hasattr(gs, 'get_topmost_subplotspec'):
# This is needed for colorbar gridspec layouts.
# This is probably OK because this whole logic tree
# is for when the user is doing simple things with the
# add_subplot command. For complicated layouts
# like subgridspecs the proper gridspec is passed in...
gs = gs.get_topmost_subplotspec().get_gridspec()
if gs.get_geometry() == (nrows, ncols):
return gs
# else gridspec not found:
return GridSpec(nrows, ncols, figure=figure)
def __getitem__(self, key):
"""Create and return a `.SubplotSpec` instance."""
nrows, ncols = self.get_geometry()
def _normalize(key, size, axis): # Includes last index.
orig_key = key
if isinstance(key, slice):
start, stop, _ = key.indices(size)
if stop > start:
return start, stop - 1
raise IndexError("GridSpec slice would result in no space "
"allocated for subplot")
else:
if key < 0:
key = key + size
if 0 <= key < size:
return key, key
elif axis is not None:
raise IndexError(f"index {orig_key} is out of bounds for "
f"axis {axis} with size {size}")
else: # flat index
raise IndexError(f"index {orig_key} is out of bounds for "
f"GridSpec with size {size}")
if isinstance(key, tuple):
try:
k1, k2 = key
except ValueError as err:
raise ValueError("Unrecognized subplot spec") from err
num1, num2 = np.ravel_multi_index(
[_normalize(k1, nrows, 0), _normalize(k2, ncols, 1)],
(nrows, ncols))
else: # Single key
num1, num2 = _normalize(key, nrows * ncols, None)
return SubplotSpec(self, num1, num2)
def subplots(self, *, sharex=False, sharey=False, squeeze=True,
subplot_kw=None):
"""
Add all subplots specified by this `GridSpec` to its parent figure.
See `.Figure.subplots` for detailed documentation.
"""
figure = self.figure
if figure is None:
raise ValueError("GridSpec.subplots() only works for GridSpecs "
"created with a parent figure")
if isinstance(sharex, bool):
sharex = "all" if sharex else "none"
if isinstance(sharey, bool):
sharey = "all" if sharey else "none"
# This check was added because it is very easy to type
# `subplots(1, 2, 1)` when `subplot(1, 2, 1)` was intended.
# In most cases, no error will ever occur, but mysterious behavior
# will result because what was intended to be the subplot index is
# instead treated as a bool for sharex. This check should go away
# once sharex becomes kwonly.
if isinstance(sharex, Integral):
_api.warn_external(
"sharex argument to subplots() was an integer. Did you "
"intend to use subplot() (without 's')?")
_api.check_in_list(["all", "row", "col", "none"],
sharex=sharex, sharey=sharey)
if subplot_kw is None:
subplot_kw = {}
# don't mutate kwargs passed by user...
subplot_kw = subplot_kw.copy()
# Create array to hold all axes.
axarr = np.empty((self._nrows, self._ncols), dtype=object)
for row in range(self._nrows):
for col in range(self._ncols):
shared_with = {"none": None, "all": axarr[0, 0],
"row": axarr[row, 0], "col": axarr[0, col]}
subplot_kw["sharex"] = shared_with[sharex]
subplot_kw["sharey"] = shared_with[sharey]
axarr[row, col] = figure.add_subplot(
self[row, col], **subplot_kw)
# turn off redundant tick labeling
if sharex in ["col", "all"]:
for ax in axarr.flat:
ax._label_outer_xaxis(check_patch=True)
if sharey in ["row", "all"]:
for ax in axarr.flat:
ax._label_outer_yaxis(check_patch=True)
if squeeze:
# Discarding unneeded dimensions that equal 1. If we only have one
# subplot, just return it instead of a 1-element array.
return axarr.item() if axarr.size == 1 else axarr.squeeze()
else:
# Returned axis array will be always 2-d, even if nrows=ncols=1.
return axarr
class GridSpec(GridSpecBase):
"""
A grid layout to place subplots within a figure.
The location of the grid cells is determined in a similar way to
`~.figure.SubplotParams` using *left*, *right*, *top*, *bottom*, *wspace*
and *hspace*.
Indexing a GridSpec instance returns a `.SubplotSpec`.
"""
def __init__(self, nrows, ncols, figure=None,
left=None, bottom=None, right=None, top=None,
wspace=None, hspace=None,
width_ratios=None, height_ratios=None):
"""
Parameters
----------
nrows, ncols : int
The number of rows and columns of the grid.
figure : `.Figure`, optional
Only used for constrained layout to create a proper layoutgrid.
left, right, top, bottom : float, optional
Extent of the subplots as a fraction of figure width or height.
Left cannot be larger than right, and bottom cannot be larger than
top. If not given, the values will be inferred from a figure or
rcParams at draw time. See also `GridSpec.get_subplot_params`.
wspace : float, optional
The amount of width reserved for space between subplots,
expressed as a fraction of the average axis width.
If not given, the values will be inferred from a figure or
rcParams when necessary. See also `GridSpec.get_subplot_params`.
hspace : float, optional
The amount of height reserved for space between subplots,
expressed as a fraction of the average axis height.
If not given, the values will be inferred from a figure or
rcParams when necessary. See also `GridSpec.get_subplot_params`.
width_ratios : array-like of length *ncols*, optional
Defines the relative widths of the columns. Each column gets a
relative width of ``width_ratios[i] / sum(width_ratios)``.
If not given, all columns will have the same width.
height_ratios : array-like of length *nrows*, optional
Defines the relative heights of the rows. Each row gets a
relative height of ``height_ratios[i] / sum(height_ratios)``.
If not given, all rows will have the same height.
"""
self.left = left
self.bottom = bottom
self.right = right
self.top = top
self.wspace = wspace
self.hspace = hspace
self.figure = figure
super().__init__(nrows, ncols,
width_ratios=width_ratios,
height_ratios=height_ratios)
_AllowedKeys = ["left", "bottom", "right", "top", "wspace", "hspace"]
def update(self, **kwargs):
"""
Update the subplot parameters of the grid.
Parameters that are not explicitly given are not changed. Setting a
parameter to *None* resets it to :rc:`figure.subplot.*`.
Parameters
----------
left, right, top, bottom : float or None, optional
Extent of the subplots as a fraction of figure width or height.
wspace, hspace : float, optional
Spacing between the subplots as a fraction of the average subplot
width / height.
"""
for k, v in kwargs.items():
if k in self._AllowedKeys:
setattr(self, k, v)
else:
raise AttributeError(f"{k} is an unknown keyword")
for figmanager in _pylab_helpers.Gcf.figs.values():
for ax in figmanager.canvas.figure.axes:
if isinstance(ax, mpl.axes.SubplotBase):
ss = ax.get_subplotspec().get_topmost_subplotspec()
if ss.get_gridspec() == self:
ax._set_position(
ax.get_subplotspec().get_position(ax.figure))
def get_subplot_params(self, figure=None):
"""
Return the `.SubplotParams` for the GridSpec.
In order of precedence the values are taken from
- non-*None* attributes of the GridSpec
- the provided *figure*
- :rc:`figure.subplot.*`
"""
if figure is None:
kw = {k: mpl.rcParams["figure.subplot."+k]
for k in self._AllowedKeys}
subplotpars = mpl.figure.SubplotParams(**kw)
else:
subplotpars = copy.copy(figure.subplotpars)
subplotpars.update(**{k: getattr(self, k) for k in self._AllowedKeys})
return subplotpars
def locally_modified_subplot_params(self):
"""
Return a list of the names of the subplot parameters explicitly set
in the GridSpec.
This is a subset of the attributes of `.SubplotParams`.
"""
return [k for k in self._AllowedKeys if getattr(self, k)]
def tight_layout(self, figure, renderer=None,
pad=1.08, h_pad=None, w_pad=None, rect=None):
"""
Adjust subplot parameters to give specified padding.
Parameters
----------
pad : float
Padding between the figure edge and the edges of subplots, as a
fraction of the font-size.
h_pad, w_pad : float, optional
Padding (height/width) between edges of adjacent subplots.
Defaults to *pad*.
rect : tuple (left, bottom, right, top), default: None
(left, bottom, right, top) rectangle in normalized figure
coordinates that the whole subplots area (including labels) will
fit into. Default (None) is the whole figure.
"""
subplotspec_list = _tight_layout.get_subplotspec_list(
figure.axes, grid_spec=self)
if None in subplotspec_list:
_api.warn_external("This figure includes Axes that are not "
"compatible with tight_layout, so results "
"might be incorrect.")
if renderer is None:
renderer = figure._get_renderer()
kwargs = _tight_layout.get_tight_layout_figure(
figure, figure.axes, subplotspec_list, renderer,
pad=pad, h_pad=h_pad, w_pad=w_pad, rect=rect)
if kwargs:
self.update(**kwargs)
class GridSpecFromSubplotSpec(GridSpecBase):
"""
GridSpec whose subplot layout parameters are inherited from the
location specified by a given SubplotSpec.
"""
def __init__(self, nrows, ncols,
subplot_spec,
wspace=None, hspace=None,
height_ratios=None, width_ratios=None):
"""
Parameters
----------
nrows, ncols : int
Number of rows and number of columns of the grid.
subplot_spec : SubplotSpec
Spec from which the layout parameters are inherited.
wspace, hspace : float, optional
See `GridSpec` for more details. If not specified default values
(from the figure or rcParams) are used.
height_ratios : array-like of length *nrows*, optional
See `GridSpecBase` for details.
width_ratios : array-like of length *ncols*, optional
See `GridSpecBase` for details.
"""
self._wspace = wspace
self._hspace = hspace
self._subplot_spec = subplot_spec
self.figure = self._subplot_spec.get_gridspec().figure
super().__init__(nrows, ncols,
width_ratios=width_ratios,
height_ratios=height_ratios)
def get_subplot_params(self, figure=None):
"""Return a dictionary of subplot layout parameters."""
hspace = (self._hspace if self._hspace is not None
else figure.subplotpars.hspace if figure is not None
else mpl.rcParams["figure.subplot.hspace"])
wspace = (self._wspace if self._wspace is not None
else figure.subplotpars.wspace if figure is not None
else mpl.rcParams["figure.subplot.wspace"])
figbox = self._subplot_spec.get_position(figure)
left, bottom, right, top = figbox.extents
return mpl.figure.SubplotParams(left=left, right=right,
bottom=bottom, top=top,
wspace=wspace, hspace=hspace)
def get_topmost_subplotspec(self):
"""
Return the topmost `.SubplotSpec` instance associated with the subplot.
"""
return self._subplot_spec.get_topmost_subplotspec()
class SubplotSpec:
"""
The location of a subplot in a `GridSpec`.
.. note::
Likely, you'll never instantiate a `SubplotSpec` yourself. Instead you
will typically obtain one from a `GridSpec` using item-access.
Parameters
----------
gridspec : `~matplotlib.gridspec.GridSpec`
The GridSpec, which the subplot is referencing.
num1, num2 : int
The subplot will occupy the num1-th cell of the given
gridspec. If num2 is provided, the subplot will span between
num1-th cell and num2-th cell *inclusive*.
The index starts from 0.
"""
def __init__(self, gridspec, num1, num2=None):
self._gridspec = gridspec
self.num1 = num1
self.num2 = num2
def __repr__(self):
return (f"{self.get_gridspec()}["
f"{self.rowspan.start}:{self.rowspan.stop}, "
f"{self.colspan.start}:{self.colspan.stop}]")
@staticmethod
def _from_subplot_args(figure, args):
"""
Construct a `.SubplotSpec` from a parent `.Figure` and either
- a `.SubplotSpec` -- returned as is;
- one or three numbers -- a MATLAB-style subplot specifier.
"""
if len(args) == 1:
arg, = args
if isinstance(arg, SubplotSpec):
return arg
elif not isinstance(arg, Integral):
raise ValueError(
f"Single argument to subplot must be a three-digit "
f"integer, not {arg!r}")
try:
rows, cols, num = map(int, str(arg))
except ValueError:
raise ValueError(
f"Single argument to subplot must be a three-digit "
f"integer, not {arg!r}") from None
elif len(args) == 3:
rows, cols, num = args
else:
raise TypeError(f"subplot() takes 1 or 3 positional arguments but "
f"{len(args)} were given")
gs = GridSpec._check_gridspec_exists(figure, rows, cols)
if gs is None:
gs = GridSpec(rows, cols, figure=figure)
if isinstance(num, tuple) and len(num) == 2:
if not all(isinstance(n, Integral) for n in num):
raise ValueError(
f"Subplot specifier tuple must contain integers, not {num}"
)
i, j = num
else:
if not isinstance(num, Integral) or num < 1 or num > rows*cols:
raise ValueError(
f"num must be 1 <= num <= {rows*cols}, not {num!r}")
i = j = num
return gs[i-1:j]
# num2 is a property only to handle the case where it is None and someone
# mutates num1.
@property
def num2(self):
return self.num1 if self._num2 is None else self._num2
@num2.setter
def num2(self, value):
self._num2 = value
def get_gridspec(self):
return self._gridspec
def get_geometry(self):
"""
Return the subplot geometry as tuple ``(n_rows, n_cols, start, stop)``.
The indices *start* and *stop* define the range of the subplot within
the `GridSpec`. *stop* is inclusive (i.e. for a single cell
``start == stop``).
"""
rows, cols = self.get_gridspec().get_geometry()
return rows, cols, self.num1, self.num2
@property
def rowspan(self):
"""The rows spanned by this subplot, as a `range` object."""
ncols = self.get_gridspec().ncols
return range(self.num1 // ncols, self.num2 // ncols + 1)
@property
def colspan(self):
"""The columns spanned by this subplot, as a `range` object."""
ncols = self.get_gridspec().ncols
# We explicitly support num2 referring to a column on num1's *left*, so
# we must sort the column indices here so that the range makes sense.
c1, c2 = sorted([self.num1 % ncols, self.num2 % ncols])
return range(c1, c2 + 1)
def is_first_row(self):
return self.rowspan.start == 0
def is_last_row(self):
return self.rowspan.stop == self.get_gridspec().nrows
def is_first_col(self):
return self.colspan.start == 0
def is_last_col(self):
return self.colspan.stop == self.get_gridspec().ncols
def get_position(self, figure):
"""
Update the subplot position from ``figure.subplotpars``.
"""
gridspec = self.get_gridspec()
nrows, ncols = gridspec.get_geometry()
rows, cols = np.unravel_index([self.num1, self.num2], (nrows, ncols))
fig_bottoms, fig_tops, fig_lefts, fig_rights = \
gridspec.get_grid_positions(figure)
fig_bottom = fig_bottoms[rows].min()
fig_top = fig_tops[rows].max()
fig_left = fig_lefts[cols].min()
fig_right = fig_rights[cols].max()
return Bbox.from_extents(fig_left, fig_bottom, fig_right, fig_top)
def get_topmost_subplotspec(self):
"""
Return the topmost `SubplotSpec` instance associated with the subplot.
"""
gridspec = self.get_gridspec()
if hasattr(gridspec, "get_topmost_subplotspec"):
return gridspec.get_topmost_subplotspec()
else:
return self
def __eq__(self, other):
"""
Two SubplotSpecs are considered equal if they refer to the same
position(s) in the same `GridSpec`.
"""
# other may not even have the attributes we are checking.
return ((self._gridspec, self.num1, self.num2)
== (getattr(other, "_gridspec", object()),
getattr(other, "num1", object()),
getattr(other, "num2", object())))
def __hash__(self):
return hash((self._gridspec, self.num1, self.num2))
def subgridspec(self, nrows, ncols, **kwargs):
"""
Create a GridSpec within this subplot.
The created `.GridSpecFromSubplotSpec` will have this `SubplotSpec` as
a parent.
Parameters
----------
nrows : int
Number of rows in grid.
ncols : int
Number or columns in grid.
Returns
-------
`.GridSpecFromSubplotSpec`
Other Parameters
----------------
**kwargs
All other parameters are passed to `.GridSpecFromSubplotSpec`.
See Also
--------
matplotlib.pyplot.subplots
Examples
--------
Adding three subplots in the space occupied by a single subplot::
fig = plt.figure()
gs0 = fig.add_gridspec(3, 1)
ax1 = fig.add_subplot(gs0[0])
ax2 = fig.add_subplot(gs0[1])
gssub = gs0[2].subgridspec(1, 3)
for i in range(3):
fig.add_subplot(gssub[0, i])
"""
return GridSpecFromSubplotSpec(nrows, ncols, self, **kwargs)