I am done

This commit is contained in:
2024-10-30 22:14:35 +01:00
parent 720dc28c09
commit 40e2a747cf
36901 changed files with 5011519 additions and 0 deletions

View File

@ -0,0 +1,10 @@
from . import axes_size as Size
from .axes_divider import Divider, SubplotDivider, make_axes_locatable
from .axes_grid import AxesGrid, Grid, ImageGrid
from .parasite_axes import host_subplot, host_axes
__all__ = ["Size",
"Divider", "SubplotDivider", "make_axes_locatable",
"AxesGrid", "Grid", "ImageGrid",
"host_subplot", "host_axes"]

View File

@ -0,0 +1,462 @@
from matplotlib import _api, transforms
from matplotlib.offsetbox import (AnchoredOffsetbox, AuxTransformBox,
DrawingArea, TextArea, VPacker)
from matplotlib.patches import (Rectangle, Ellipse, ArrowStyle,
FancyArrowPatch, PathPatch)
from matplotlib.text import TextPath
__all__ = ['AnchoredDrawingArea', 'AnchoredAuxTransformBox',
'AnchoredEllipse', 'AnchoredSizeBar', 'AnchoredDirectionArrows']
class AnchoredDrawingArea(AnchoredOffsetbox):
def __init__(self, width, height, xdescent, ydescent,
loc, pad=0.4, borderpad=0.5, prop=None, frameon=True,
**kwargs):
"""
An anchored container with a fixed size and fillable `.DrawingArea`.
Artists added to the *drawing_area* will have their coordinates
interpreted as pixels. Any transformations set on the artists will be
overridden.
Parameters
----------
width, height : float
Width and height of the container, in pixels.
xdescent, ydescent : float
Descent of the container in the x- and y- direction, in pixels.
loc : str
Location of this artist. Valid locations are
'upper left', 'upper center', 'upper right',
'center left', 'center', 'center right',
'lower left', 'lower center', 'lower right'.
For backward compatibility, numeric values are accepted as well.
See the parameter *loc* of `.Legend` for details.
pad : float, default: 0.4
Padding around the child objects, in fraction of the font size.
borderpad : float, default: 0.5
Border padding, in fraction of the font size.
prop : `~matplotlib.font_manager.FontProperties`, optional
Font property used as a reference for paddings.
frameon : bool, default: True
If True, draw a box around this artist.
**kwargs
Keyword arguments forwarded to `.AnchoredOffsetbox`.
Attributes
----------
drawing_area : `~matplotlib.offsetbox.DrawingArea`
A container for artists to display.
Examples
--------
To display blue and red circles of different sizes in the upper right
of an Axes *ax*:
>>> ada = AnchoredDrawingArea(20, 20, 0, 0,
... loc='upper right', frameon=False)
>>> ada.drawing_area.add_artist(Circle((10, 10), 10, fc="b"))
>>> ada.drawing_area.add_artist(Circle((30, 10), 5, fc="r"))
>>> ax.add_artist(ada)
"""
self.da = DrawingArea(width, height, xdescent, ydescent)
self.drawing_area = self.da
super().__init__(
loc, pad=pad, borderpad=borderpad, child=self.da, prop=None,
frameon=frameon, **kwargs
)
class AnchoredAuxTransformBox(AnchoredOffsetbox):
def __init__(self, transform, loc,
pad=0.4, borderpad=0.5, prop=None, frameon=True, **kwargs):
"""
An anchored container with transformed coordinates.
Artists added to the *drawing_area* are scaled according to the
coordinates of the transformation used. The dimensions of this artist
will scale to contain the artists added.
Parameters
----------
transform : `~matplotlib.transforms.Transform`
The transformation object for the coordinate system in use, i.e.,
:attr:`matplotlib.axes.Axes.transData`.
loc : str
Location of this artist. Valid locations are
'upper left', 'upper center', 'upper right',
'center left', 'center', 'center right',
'lower left', 'lower center', 'lower right'.
For backward compatibility, numeric values are accepted as well.
See the parameter *loc* of `.Legend` for details.
pad : float, default: 0.4
Padding around the child objects, in fraction of the font size.
borderpad : float, default: 0.5
Border padding, in fraction of the font size.
prop : `~matplotlib.font_manager.FontProperties`, optional
Font property used as a reference for paddings.
frameon : bool, default: True
If True, draw a box around this artist.
**kwargs
Keyword arguments forwarded to `.AnchoredOffsetbox`.
Attributes
----------
drawing_area : `~matplotlib.offsetbox.AuxTransformBox`
A container for artists to display.
Examples
--------
To display an ellipse in the upper left, with a width of 0.1 and
height of 0.4 in data coordinates:
>>> box = AnchoredAuxTransformBox(ax.transData, loc='upper left')
>>> el = Ellipse((0, 0), width=0.1, height=0.4, angle=30)
>>> box.drawing_area.add_artist(el)
>>> ax.add_artist(box)
"""
self.drawing_area = AuxTransformBox(transform)
super().__init__(loc, pad=pad, borderpad=borderpad,
child=self.drawing_area, prop=prop, frameon=frameon,
**kwargs)
@_api.deprecated("3.8")
class AnchoredEllipse(AnchoredOffsetbox):
def __init__(self, transform, width, height, angle, loc,
pad=0.1, borderpad=0.1, prop=None, frameon=True, **kwargs):
"""
Draw an anchored ellipse of a given size.
Parameters
----------
transform : `~matplotlib.transforms.Transform`
The transformation object for the coordinate system in use, i.e.,
:attr:`matplotlib.axes.Axes.transData`.
width, height : float
Width and height of the ellipse, given in coordinates of
*transform*.
angle : float
Rotation of the ellipse, in degrees, anti-clockwise.
loc : str
Location of the ellipse. Valid locations are
'upper left', 'upper center', 'upper right',
'center left', 'center', 'center right',
'lower left', 'lower center', 'lower right'.
For backward compatibility, numeric values are accepted as well.
See the parameter *loc* of `.Legend` for details.
pad : float, default: 0.1
Padding around the ellipse, in fraction of the font size.
borderpad : float, default: 0.1
Border padding, in fraction of the font size.
frameon : bool, default: True
If True, draw a box around the ellipse.
prop : `~matplotlib.font_manager.FontProperties`, optional
Font property used as a reference for paddings.
**kwargs
Keyword arguments forwarded to `.AnchoredOffsetbox`.
Attributes
----------
ellipse : `~matplotlib.patches.Ellipse`
Ellipse patch drawn.
"""
self._box = AuxTransformBox(transform)
self.ellipse = Ellipse((0, 0), width, height, angle=angle)
self._box.add_artist(self.ellipse)
super().__init__(loc, pad=pad, borderpad=borderpad, child=self._box,
prop=prop, frameon=frameon, **kwargs)
class AnchoredSizeBar(AnchoredOffsetbox):
def __init__(self, transform, size, label, loc,
pad=0.1, borderpad=0.1, sep=2,
frameon=True, size_vertical=0, color='black',
label_top=False, fontproperties=None, fill_bar=None,
**kwargs):
"""
Draw a horizontal scale bar with a center-aligned label underneath.
Parameters
----------
transform : `~matplotlib.transforms.Transform`
The transformation object for the coordinate system in use, i.e.,
:attr:`matplotlib.axes.Axes.transData`.
size : float
Horizontal length of the size bar, given in coordinates of
*transform*.
label : str
Label to display.
loc : str
Location of the size bar. Valid locations are
'upper left', 'upper center', 'upper right',
'center left', 'center', 'center right',
'lower left', 'lower center', 'lower right'.
For backward compatibility, numeric values are accepted as well.
See the parameter *loc* of `.Legend` for details.
pad : float, default: 0.1
Padding around the label and size bar, in fraction of the font
size.
borderpad : float, default: 0.1
Border padding, in fraction of the font size.
sep : float, default: 2
Separation between the label and the size bar, in points.
frameon : bool, default: True
If True, draw a box around the horizontal bar and label.
size_vertical : float, default: 0
Vertical length of the size bar, given in coordinates of
*transform*.
color : str, default: 'black'
Color for the size bar and label.
label_top : bool, default: False
If True, the label will be over the size bar.
fontproperties : `~matplotlib.font_manager.FontProperties`, optional
Font properties for the label text.
fill_bar : bool, optional
If True and if *size_vertical* is nonzero, the size bar will
be filled in with the color specified by the size bar.
Defaults to True if *size_vertical* is greater than
zero and False otherwise.
**kwargs
Keyword arguments forwarded to `.AnchoredOffsetbox`.
Attributes
----------
size_bar : `~matplotlib.offsetbox.AuxTransformBox`
Container for the size bar.
txt_label : `~matplotlib.offsetbox.TextArea`
Container for the label of the size bar.
Notes
-----
If *prop* is passed as a keyword argument, but *fontproperties* is
not, then *prop* is assumed to be the intended *fontproperties*.
Using both *prop* and *fontproperties* is not supported.
Examples
--------
>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>> from mpl_toolkits.axes_grid1.anchored_artists import (
... AnchoredSizeBar)
>>> fig, ax = plt.subplots()
>>> ax.imshow(np.random.random((10, 10)))
>>> bar = AnchoredSizeBar(ax.transData, 3, '3 data units', 4)
>>> ax.add_artist(bar)
>>> fig.show()
Using all the optional parameters
>>> import matplotlib.font_manager as fm
>>> fontprops = fm.FontProperties(size=14, family='monospace')
>>> bar = AnchoredSizeBar(ax.transData, 3, '3 units', 4, pad=0.5,
... sep=5, borderpad=0.5, frameon=False,
... size_vertical=0.5, color='white',
... fontproperties=fontprops)
"""
if fill_bar is None:
fill_bar = size_vertical > 0
self.size_bar = AuxTransformBox(transform)
self.size_bar.add_artist(Rectangle((0, 0), size, size_vertical,
fill=fill_bar, facecolor=color,
edgecolor=color))
if fontproperties is None and 'prop' in kwargs:
fontproperties = kwargs.pop('prop')
if fontproperties is None:
textprops = {'color': color}
else:
textprops = {'color': color, 'fontproperties': fontproperties}
self.txt_label = TextArea(label, textprops=textprops)
if label_top:
_box_children = [self.txt_label, self.size_bar]
else:
_box_children = [self.size_bar, self.txt_label]
self._box = VPacker(children=_box_children,
align="center",
pad=0, sep=sep)
super().__init__(loc, pad=pad, borderpad=borderpad, child=self._box,
prop=fontproperties, frameon=frameon, **kwargs)
class AnchoredDirectionArrows(AnchoredOffsetbox):
def __init__(self, transform, label_x, label_y, length=0.15,
fontsize=0.08, loc='upper left', angle=0, aspect_ratio=1,
pad=0.4, borderpad=0.4, frameon=False, color='w', alpha=1,
sep_x=0.01, sep_y=0, fontproperties=None, back_length=0.15,
head_width=10, head_length=15, tail_width=2,
text_props=None, arrow_props=None,
**kwargs):
"""
Draw two perpendicular arrows to indicate directions.
Parameters
----------
transform : `~matplotlib.transforms.Transform`
The transformation object for the coordinate system in use, i.e.,
:attr:`matplotlib.axes.Axes.transAxes`.
label_x, label_y : str
Label text for the x and y arrows
length : float, default: 0.15
Length of the arrow, given in coordinates of *transform*.
fontsize : float, default: 0.08
Size of label strings, given in coordinates of *transform*.
loc : str, default: 'upper left'
Location of the arrow. Valid locations are
'upper left', 'upper center', 'upper right',
'center left', 'center', 'center right',
'lower left', 'lower center', 'lower right'.
For backward compatibility, numeric values are accepted as well.
See the parameter *loc* of `.Legend` for details.
angle : float, default: 0
The angle of the arrows in degrees.
aspect_ratio : float, default: 1
The ratio of the length of arrow_x and arrow_y.
Negative numbers can be used to change the direction.
pad : float, default: 0.4
Padding around the labels and arrows, in fraction of the font size.
borderpad : float, default: 0.4
Border padding, in fraction of the font size.
frameon : bool, default: False
If True, draw a box around the arrows and labels.
color : str, default: 'white'
Color for the arrows and labels.
alpha : float, default: 1
Alpha values of the arrows and labels
sep_x, sep_y : float, default: 0.01 and 0 respectively
Separation between the arrows and labels in coordinates of
*transform*.
fontproperties : `~matplotlib.font_manager.FontProperties`, optional
Font properties for the label text.
back_length : float, default: 0.15
Fraction of the arrow behind the arrow crossing.
head_width : float, default: 10
Width of arrow head, sent to `.ArrowStyle`.
head_length : float, default: 15
Length of arrow head, sent to `.ArrowStyle`.
tail_width : float, default: 2
Width of arrow tail, sent to `.ArrowStyle`.
text_props, arrow_props : dict
Properties of the text and arrows, passed to `.TextPath` and
`.FancyArrowPatch`.
**kwargs
Keyword arguments forwarded to `.AnchoredOffsetbox`.
Attributes
----------
arrow_x, arrow_y : `~matplotlib.patches.FancyArrowPatch`
Arrow x and y
text_path_x, text_path_y : `~matplotlib.text.TextPath`
Path for arrow labels
p_x, p_y : `~matplotlib.patches.PathPatch`
Patch for arrow labels
box : `~matplotlib.offsetbox.AuxTransformBox`
Container for the arrows and labels.
Notes
-----
If *prop* is passed as a keyword argument, but *fontproperties* is
not, then *prop* is assumed to be the intended *fontproperties*.
Using both *prop* and *fontproperties* is not supported.
Examples
--------
>>> import matplotlib.pyplot as plt
>>> import numpy as np
>>> from mpl_toolkits.axes_grid1.anchored_artists import (
... AnchoredDirectionArrows)
>>> fig, ax = plt.subplots()
>>> ax.imshow(np.random.random((10, 10)))
>>> arrows = AnchoredDirectionArrows(ax.transAxes, '111', '110')
>>> ax.add_artist(arrows)
>>> fig.show()
Using several of the optional parameters, creating downward pointing
arrow and high contrast text labels.
>>> import matplotlib.font_manager as fm
>>> fontprops = fm.FontProperties(family='monospace')
>>> arrows = AnchoredDirectionArrows(ax.transAxes, 'East', 'South',
... loc='lower left', color='k',
... aspect_ratio=-1, sep_x=0.02,
... sep_y=-0.01,
... text_props={'ec':'w', 'fc':'k'},
... fontproperties=fontprops)
"""
if arrow_props is None:
arrow_props = {}
if text_props is None:
text_props = {}
arrowstyle = ArrowStyle("Simple",
head_width=head_width,
head_length=head_length,
tail_width=tail_width)
if fontproperties is None and 'prop' in kwargs:
fontproperties = kwargs.pop('prop')
if 'color' not in arrow_props:
arrow_props['color'] = color
if 'alpha' not in arrow_props:
arrow_props['alpha'] = alpha
if 'color' not in text_props:
text_props['color'] = color
if 'alpha' not in text_props:
text_props['alpha'] = alpha
t_start = transform
t_end = t_start + transforms.Affine2D().rotate_deg(angle)
self.box = AuxTransformBox(t_end)
length_x = length
length_y = length*aspect_ratio
self.arrow_x = FancyArrowPatch(
(0, back_length*length_y),
(length_x, back_length*length_y),
arrowstyle=arrowstyle,
shrinkA=0.0,
shrinkB=0.0,
**arrow_props)
self.arrow_y = FancyArrowPatch(
(back_length*length_x, 0),
(back_length*length_x, length_y),
arrowstyle=arrowstyle,
shrinkA=0.0,
shrinkB=0.0,
**arrow_props)
self.box.add_artist(self.arrow_x)
self.box.add_artist(self.arrow_y)
text_path_x = TextPath((
length_x+sep_x, back_length*length_y+sep_y), label_x,
size=fontsize, prop=fontproperties)
self.p_x = PathPatch(text_path_x, transform=t_start, **text_props)
self.box.add_artist(self.p_x)
text_path_y = TextPath((
length_x*back_length+sep_x, length_y*(1-back_length)+sep_y),
label_y, size=fontsize, prop=fontproperties)
self.p_y = PathPatch(text_path_y, **text_props)
self.box.add_artist(self.p_y)
super().__init__(loc, pad=pad, borderpad=borderpad, child=self.box,
frameon=frameon, **kwargs)

View File

@ -0,0 +1,694 @@
"""
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)

View File

@ -0,0 +1,563 @@
from numbers import Number
import functools
from types import MethodType
import numpy as np
from matplotlib import _api, cbook
from matplotlib.gridspec import SubplotSpec
from .axes_divider import Size, SubplotDivider, Divider
from .mpl_axes import Axes, SimpleAxisArtist
class CbarAxesBase:
def __init__(self, *args, orientation, **kwargs):
self.orientation = orientation
super().__init__(*args, **kwargs)
def colorbar(self, mappable, **kwargs):
return self.figure.colorbar(
mappable, cax=self, location=self.orientation, **kwargs)
@_api.deprecated("3.8", alternative="ax.tick_params and colorbar.set_label")
def toggle_label(self, b):
axis = self.axis[self.orientation]
axis.toggle(ticklabels=b, label=b)
_cbaraxes_class_factory = cbook._make_class_factory(CbarAxesBase, "Cbar{}")
class Grid:
"""
A grid of Axes.
In Matplotlib, the Axes location (and size) is specified in normalized
figure coordinates. This may not be ideal for images that needs to be
displayed with a given aspect ratio; for example, it is difficult to
display multiple images of a same size with some fixed padding between
them. AxesGrid can be used in such case.
Attributes
----------
axes_all : list of Axes
A flat list of Axes. Note that you can also access this directly
from the grid. The following is equivalent ::
grid[i] == grid.axes_all[i]
len(grid) == len(grid.axes_all)
axes_column : list of list of Axes
A 2D list of Axes where the first index is the column. This results
in the usage pattern ``grid.axes_column[col][row]``.
axes_row : list of list of Axes
A 2D list of Axes where the first index is the row. This results
in the usage pattern ``grid.axes_row[row][col]``.
axes_llc : Axes
The Axes in the lower left corner.
ngrids : int
Number of Axes in the grid.
"""
_defaultAxesClass = Axes
def __init__(self, fig,
rect,
nrows_ncols,
ngrids=None,
direction="row",
axes_pad=0.02,
*,
share_all=False,
share_x=True,
share_y=True,
label_mode="L",
axes_class=None,
aspect=False,
):
"""
Parameters
----------
fig : `.Figure`
The parent figure.
rect : (float, float, float, float), (int, int, int), int, or \
`~.SubplotSpec`
The axes position, as a ``(left, bottom, width, height)`` tuple,
as a three-digit subplot position code (e.g., ``(1, 2, 1)`` or
``121``), or as a `~.SubplotSpec`.
nrows_ncols : (int, int)
Number of rows and columns in the grid.
ngrids : int or None, default: None
If not None, only the first *ngrids* axes in the grid are created.
direction : {"row", "column"}, default: "row"
Whether axes are created in row-major ("row by row") or
column-major order ("column by column"). This also affects the
order in which axes are accessed using indexing (``grid[index]``).
axes_pad : float or (float, float), default: 0.02
Padding or (horizontal padding, vertical padding) between axes, in
inches.
share_all : bool, default: False
Whether all axes share their x- and y-axis. Overrides *share_x*
and *share_y*.
share_x : bool, default: True
Whether all axes of a column share their x-axis.
share_y : bool, default: True
Whether all axes of a row share their y-axis.
label_mode : {"L", "1", "all", "keep"}, default: "L"
Determines which axes will get tick labels:
- "L": All axes on the left column get vertical tick labels;
all axes on the bottom row get horizontal tick labels.
- "1": Only the bottom left axes is labelled.
- "all": All axes are labelled.
- "keep": Do not do anything.
axes_class : subclass of `matplotlib.axes.Axes`, default: `.mpl_axes.Axes`
The type of Axes to create.
aspect : bool, default: False
Whether the axes aspect ratio follows the aspect ratio of the data
limits.
"""
self._nrows, self._ncols = nrows_ncols
if ngrids is None:
ngrids = self._nrows * self._ncols
else:
if not 0 < ngrids <= self._nrows * self._ncols:
raise ValueError(
"ngrids must be positive and not larger than nrows*ncols")
self.ngrids = ngrids
self._horiz_pad_size, self._vert_pad_size = map(
Size.Fixed, np.broadcast_to(axes_pad, 2))
_api.check_in_list(["column", "row"], direction=direction)
self._direction = direction
if axes_class is None:
axes_class = self._defaultAxesClass
elif isinstance(axes_class, (list, tuple)):
cls, kwargs = axes_class
axes_class = functools.partial(cls, **kwargs)
kw = dict(horizontal=[], vertical=[], aspect=aspect)
if isinstance(rect, (Number, SubplotSpec)):
self._divider = SubplotDivider(fig, rect, **kw)
elif len(rect) == 3:
self._divider = SubplotDivider(fig, *rect, **kw)
elif len(rect) == 4:
self._divider = Divider(fig, rect, **kw)
else:
raise TypeError("Incorrect rect format")
rect = self._divider.get_position()
axes_array = np.full((self._nrows, self._ncols), None, dtype=object)
for i in range(self.ngrids):
col, row = self._get_col_row(i)
if share_all:
sharex = sharey = axes_array[0, 0]
else:
sharex = axes_array[0, col] if share_x else None
sharey = axes_array[row, 0] if share_y else None
axes_array[row, col] = axes_class(
fig, rect, sharex=sharex, sharey=sharey)
self.axes_all = axes_array.ravel(
order="C" if self._direction == "row" else "F").tolist()
self.axes_column = axes_array.T.tolist()
self.axes_row = axes_array.tolist()
self.axes_llc = self.axes_column[0][-1]
self._init_locators()
for ax in self.axes_all:
fig.add_axes(ax)
self.set_label_mode(label_mode)
def _init_locators(self):
self._divider.set_horizontal(
[Size.Scaled(1), self._horiz_pad_size] * (self._ncols-1) + [Size.Scaled(1)])
self._divider.set_vertical(
[Size.Scaled(1), self._vert_pad_size] * (self._nrows-1) + [Size.Scaled(1)])
for i in range(self.ngrids):
col, row = self._get_col_row(i)
self.axes_all[i].set_axes_locator(
self._divider.new_locator(nx=2 * col, ny=2 * (self._nrows - 1 - row)))
def _get_col_row(self, n):
if self._direction == "column":
col, row = divmod(n, self._nrows)
else:
row, col = divmod(n, self._ncols)
return col, row
# Good to propagate __len__ if we have __getitem__
def __len__(self):
return len(self.axes_all)
def __getitem__(self, i):
return self.axes_all[i]
def get_geometry(self):
"""
Return the number of rows and columns of the grid as (nrows, ncols).
"""
return self._nrows, self._ncols
def set_axes_pad(self, axes_pad):
"""
Set the padding between the axes.
Parameters
----------
axes_pad : (float, float)
The padding (horizontal pad, vertical pad) in inches.
"""
self._horiz_pad_size.fixed_size = axes_pad[0]
self._vert_pad_size.fixed_size = axes_pad[1]
def get_axes_pad(self):
"""
Return the axes padding.
Returns
-------
hpad, vpad
Padding (horizontal pad, vertical pad) in inches.
"""
return (self._horiz_pad_size.fixed_size,
self._vert_pad_size.fixed_size)
def set_aspect(self, aspect):
"""Set the aspect of the SubplotDivider."""
self._divider.set_aspect(aspect)
def get_aspect(self):
"""Return the aspect of the SubplotDivider."""
return self._divider.get_aspect()
def set_label_mode(self, mode):
"""
Define which axes have tick labels.
Parameters
----------
mode : {"L", "1", "all", "keep"}
The label mode:
- "L": All axes on the left column get vertical tick labels;
all axes on the bottom row get horizontal tick labels.
- "1": Only the bottom left axes is labelled.
- "all": All axes are labelled.
- "keep": Do not do anything.
"""
_api.check_in_list(["all", "L", "1", "keep"], mode=mode)
is_last_row, is_first_col = (
np.mgrid[:self._nrows, :self._ncols] == [[[self._nrows - 1]], [[0]]])
if mode == "all":
bottom = left = np.full((self._nrows, self._ncols), True)
elif mode == "L":
bottom = is_last_row
left = is_first_col
elif mode == "1":
bottom = left = is_last_row & is_first_col
else:
return
for i in range(self._nrows):
for j in range(self._ncols):
ax = self.axes_row[i][j]
if isinstance(ax.axis, MethodType):
bottom_axis = SimpleAxisArtist(ax.xaxis, 1, ax.spines["bottom"])
left_axis = SimpleAxisArtist(ax.yaxis, 1, ax.spines["left"])
else:
bottom_axis = ax.axis["bottom"]
left_axis = ax.axis["left"]
bottom_axis.toggle(ticklabels=bottom[i, j], label=bottom[i, j])
left_axis.toggle(ticklabels=left[i, j], label=left[i, j])
def get_divider(self):
return self._divider
def set_axes_locator(self, locator):
self._divider.set_locator(locator)
def get_axes_locator(self):
return self._divider.get_locator()
class ImageGrid(Grid):
"""
A grid of Axes for Image display.
This class is a specialization of `~.axes_grid1.axes_grid.Grid` for displaying a
grid of images. In particular, it forces all axes in a column to share their x-axis
and all axes in a row to share their y-axis. It further provides helpers to add
colorbars to some or all axes.
"""
def __init__(self, fig,
rect,
nrows_ncols,
ngrids=None,
direction="row",
axes_pad=0.02,
*,
share_all=False,
aspect=True,
label_mode="L",
cbar_mode=None,
cbar_location="right",
cbar_pad=None,
cbar_size="5%",
cbar_set_cax=True,
axes_class=None,
):
"""
Parameters
----------
fig : `.Figure`
The parent figure.
rect : (float, float, float, float) or int
The axes position, as a ``(left, bottom, width, height)`` tuple or
as a three-digit subplot position code (e.g., "121").
nrows_ncols : (int, int)
Number of rows and columns in the grid.
ngrids : int or None, default: None
If not None, only the first *ngrids* axes in the grid are created.
direction : {"row", "column"}, default: "row"
Whether axes are created in row-major ("row by row") or
column-major order ("column by column"). This also affects the
order in which axes are accessed using indexing (``grid[index]``).
axes_pad : float or (float, float), default: 0.02in
Padding or (horizontal padding, vertical padding) between axes, in
inches.
share_all : bool, default: False
Whether all axes share their x- and y-axis. Note that in any case,
all axes in a column share their x-axis and all axes in a row share
their y-axis.
aspect : bool, default: True
Whether the axes aspect ratio follows the aspect ratio of the data
limits.
label_mode : {"L", "1", "all"}, default: "L"
Determines which axes will get tick labels:
- "L": All axes on the left column get vertical tick labels;
all axes on the bottom row get horizontal tick labels.
- "1": Only the bottom left axes is labelled.
- "all": all axes are labelled.
cbar_mode : {"each", "single", "edge", None}, default: None
Whether to create a colorbar for "each" axes, a "single" colorbar
for the entire grid, colorbars only for axes on the "edge"
determined by *cbar_location*, or no colorbars. The colorbars are
stored in the :attr:`cbar_axes` attribute.
cbar_location : {"left", "right", "bottom", "top"}, default: "right"
cbar_pad : float, default: None
Padding between the image axes and the colorbar axes.
cbar_size : size specification (see `.Size.from_any`), default: "5%"
Colorbar size.
cbar_set_cax : bool, default: True
If True, each axes in the grid has a *cax* attribute that is bound
to associated *cbar_axes*.
axes_class : subclass of `matplotlib.axes.Axes`, default: None
"""
_api.check_in_list(["each", "single", "edge", None],
cbar_mode=cbar_mode)
_api.check_in_list(["left", "right", "bottom", "top"],
cbar_location=cbar_location)
self._colorbar_mode = cbar_mode
self._colorbar_location = cbar_location
self._colorbar_pad = cbar_pad
self._colorbar_size = cbar_size
# The colorbar axes are created in _init_locators().
super().__init__(
fig, rect, nrows_ncols, ngrids,
direction=direction, axes_pad=axes_pad,
share_all=share_all, share_x=True, share_y=True, aspect=aspect,
label_mode=label_mode, axes_class=axes_class)
for ax in self.cbar_axes:
fig.add_axes(ax)
if cbar_set_cax:
if self._colorbar_mode == "single":
for ax in self.axes_all:
ax.cax = self.cbar_axes[0]
elif self._colorbar_mode == "edge":
for index, ax in enumerate(self.axes_all):
col, row = self._get_col_row(index)
if self._colorbar_location in ("left", "right"):
ax.cax = self.cbar_axes[row]
else:
ax.cax = self.cbar_axes[col]
else:
for ax, cax in zip(self.axes_all, self.cbar_axes):
ax.cax = cax
def _init_locators(self):
# Slightly abusing this method to inject colorbar creation into init.
if self._colorbar_pad is None:
# horizontal or vertical arrangement?
if self._colorbar_location in ("left", "right"):
self._colorbar_pad = self._horiz_pad_size.fixed_size
else:
self._colorbar_pad = self._vert_pad_size.fixed_size
self.cbar_axes = [
_cbaraxes_class_factory(self._defaultAxesClass)(
self.axes_all[0].figure, self._divider.get_position(),
orientation=self._colorbar_location)
for _ in range(self.ngrids)]
cb_mode = self._colorbar_mode
cb_location = self._colorbar_location
h = []
v = []
h_ax_pos = []
h_cb_pos = []
if cb_mode == "single" and cb_location in ("left", "bottom"):
if cb_location == "left":
sz = self._nrows * Size.AxesX(self.axes_llc)
h.append(Size.from_any(self._colorbar_size, sz))
h.append(Size.from_any(self._colorbar_pad, sz))
locator = self._divider.new_locator(nx=0, ny=0, ny1=-1)
elif cb_location == "bottom":
sz = self._ncols * Size.AxesY(self.axes_llc)
v.append(Size.from_any(self._colorbar_size, sz))
v.append(Size.from_any(self._colorbar_pad, sz))
locator = self._divider.new_locator(nx=0, nx1=-1, ny=0)
for i in range(self.ngrids):
self.cbar_axes[i].set_visible(False)
self.cbar_axes[0].set_axes_locator(locator)
self.cbar_axes[0].set_visible(True)
for col, ax in enumerate(self.axes_row[0]):
if h:
h.append(self._horiz_pad_size)
if ax:
sz = Size.AxesX(ax, aspect="axes", ref_ax=self.axes_all[0])
else:
sz = Size.AxesX(self.axes_all[0],
aspect="axes", ref_ax=self.axes_all[0])
if (cb_location == "left"
and (cb_mode == "each"
or (cb_mode == "edge" and col == 0))):
h_cb_pos.append(len(h))
h.append(Size.from_any(self._colorbar_size, sz))
h.append(Size.from_any(self._colorbar_pad, sz))
h_ax_pos.append(len(h))
h.append(sz)
if (cb_location == "right"
and (cb_mode == "each"
or (cb_mode == "edge" and col == self._ncols - 1))):
h.append(Size.from_any(self._colorbar_pad, sz))
h_cb_pos.append(len(h))
h.append(Size.from_any(self._colorbar_size, sz))
v_ax_pos = []
v_cb_pos = []
for row, ax in enumerate(self.axes_column[0][::-1]):
if v:
v.append(self._vert_pad_size)
if ax:
sz = Size.AxesY(ax, aspect="axes", ref_ax=self.axes_all[0])
else:
sz = Size.AxesY(self.axes_all[0],
aspect="axes", ref_ax=self.axes_all[0])
if (cb_location == "bottom"
and (cb_mode == "each"
or (cb_mode == "edge" and row == 0))):
v_cb_pos.append(len(v))
v.append(Size.from_any(self._colorbar_size, sz))
v.append(Size.from_any(self._colorbar_pad, sz))
v_ax_pos.append(len(v))
v.append(sz)
if (cb_location == "top"
and (cb_mode == "each"
or (cb_mode == "edge" and row == self._nrows - 1))):
v.append(Size.from_any(self._colorbar_pad, sz))
v_cb_pos.append(len(v))
v.append(Size.from_any(self._colorbar_size, sz))
for i in range(self.ngrids):
col, row = self._get_col_row(i)
locator = self._divider.new_locator(nx=h_ax_pos[col],
ny=v_ax_pos[self._nrows-1-row])
self.axes_all[i].set_axes_locator(locator)
if cb_mode == "each":
if cb_location in ("right", "left"):
locator = self._divider.new_locator(
nx=h_cb_pos[col], ny=v_ax_pos[self._nrows - 1 - row])
elif cb_location in ("top", "bottom"):
locator = self._divider.new_locator(
nx=h_ax_pos[col], ny=v_cb_pos[self._nrows - 1 - row])
self.cbar_axes[i].set_axes_locator(locator)
elif cb_mode == "edge":
if (cb_location == "left" and col == 0
or cb_location == "right" and col == self._ncols - 1):
locator = self._divider.new_locator(
nx=h_cb_pos[0], ny=v_ax_pos[self._nrows - 1 - row])
self.cbar_axes[row].set_axes_locator(locator)
elif (cb_location == "bottom" and row == self._nrows - 1
or cb_location == "top" and row == 0):
locator = self._divider.new_locator(nx=h_ax_pos[col],
ny=v_cb_pos[0])
self.cbar_axes[col].set_axes_locator(locator)
if cb_mode == "single":
if cb_location == "right":
sz = self._nrows * Size.AxesX(self.axes_llc)
h.append(Size.from_any(self._colorbar_pad, sz))
h.append(Size.from_any(self._colorbar_size, sz))
locator = self._divider.new_locator(nx=-2, ny=0, ny1=-1)
elif cb_location == "top":
sz = self._ncols * Size.AxesY(self.axes_llc)
v.append(Size.from_any(self._colorbar_pad, sz))
v.append(Size.from_any(self._colorbar_size, sz))
locator = self._divider.new_locator(nx=0, nx1=-1, ny=-2)
if cb_location in ("right", "top"):
for i in range(self.ngrids):
self.cbar_axes[i].set_visible(False)
self.cbar_axes[0].set_axes_locator(locator)
self.cbar_axes[0].set_visible(True)
elif cb_mode == "each":
for i in range(self.ngrids):
self.cbar_axes[i].set_visible(True)
elif cb_mode == "edge":
if cb_location in ("right", "left"):
count = self._nrows
else:
count = self._ncols
for i in range(count):
self.cbar_axes[i].set_visible(True)
for j in range(i + 1, self.ngrids):
self.cbar_axes[j].set_visible(False)
else:
for i in range(self.ngrids):
self.cbar_axes[i].set_visible(False)
self.cbar_axes[i].set_position([1., 1., 0.001, 0.001],
which="active")
self._divider.set_horizontal(h)
self._divider.set_vertical(v)
AxesGrid = ImageGrid

View File

@ -0,0 +1,157 @@
from types import MethodType
import numpy as np
from .axes_divider import make_axes_locatable, Size
from .mpl_axes import Axes, SimpleAxisArtist
def make_rgb_axes(ax, pad=0.01, axes_class=None, **kwargs):
"""
Parameters
----------
ax : `~matplotlib.axes.Axes`
Axes instance to create the RGB Axes in.
pad : float, optional
Fraction of the Axes height to pad.
axes_class : `matplotlib.axes.Axes` or None, optional
Axes class to use for the R, G, and B Axes. If None, use
the same class as *ax*.
**kwargs
Forwarded to *axes_class* init for the R, G, and B Axes.
"""
divider = make_axes_locatable(ax)
pad_size = pad * Size.AxesY(ax)
xsize = ((1-2*pad)/3) * Size.AxesX(ax)
ysize = ((1-2*pad)/3) * Size.AxesY(ax)
divider.set_horizontal([Size.AxesX(ax), pad_size, xsize])
divider.set_vertical([ysize, pad_size, ysize, pad_size, ysize])
ax.set_axes_locator(divider.new_locator(0, 0, ny1=-1))
ax_rgb = []
if axes_class is None:
axes_class = type(ax)
for ny in [4, 2, 0]:
ax1 = axes_class(ax.get_figure(), ax.get_position(original=True),
sharex=ax, sharey=ax, **kwargs)
locator = divider.new_locator(nx=2, ny=ny)
ax1.set_axes_locator(locator)
for t in ax1.yaxis.get_ticklabels() + ax1.xaxis.get_ticklabels():
t.set_visible(False)
try:
for axis in ax1.axis.values():
axis.major_ticklabels.set_visible(False)
except AttributeError:
pass
ax_rgb.append(ax1)
fig = ax.get_figure()
for ax1 in ax_rgb:
fig.add_axes(ax1)
return ax_rgb
class RGBAxes:
"""
4-panel `~.Axes.imshow` (RGB, R, G, B).
Layout::
┌───────────────┬─────┐
│ │ R │
│ ├─────┤
│ RGB │ G │
│ ├─────┤
│ │ B │
└───────────────┴─────┘
Subclasses can override the ``_defaultAxesClass`` attribute.
By default RGBAxes uses `.mpl_axes.Axes`.
Attributes
----------
RGB : ``_defaultAxesClass``
The Axes object for the three-channel `~.Axes.imshow`.
R : ``_defaultAxesClass``
The Axes object for the red channel `~.Axes.imshow`.
G : ``_defaultAxesClass``
The Axes object for the green channel `~.Axes.imshow`.
B : ``_defaultAxesClass``
The Axes object for the blue channel `~.Axes.imshow`.
"""
_defaultAxesClass = Axes
def __init__(self, *args, pad=0, **kwargs):
"""
Parameters
----------
pad : float, default: 0
Fraction of the Axes height to put as padding.
axes_class : `~matplotlib.axes.Axes`
Axes class to use. If not provided, ``_defaultAxesClass`` is used.
*args
Forwarded to *axes_class* init for the RGB Axes
**kwargs
Forwarded to *axes_class* init for the RGB, R, G, and B Axes
"""
axes_class = kwargs.pop("axes_class", self._defaultAxesClass)
self.RGB = ax = axes_class(*args, **kwargs)
ax.get_figure().add_axes(ax)
self.R, self.G, self.B = make_rgb_axes(
ax, pad=pad, axes_class=axes_class, **kwargs)
# Set the line color and ticks for the axes.
for ax1 in [self.RGB, self.R, self.G, self.B]:
if isinstance(ax1.axis, MethodType):
ad = Axes.AxisDict(self)
ad.update(
bottom=SimpleAxisArtist(ax1.xaxis, 1, ax1.spines["bottom"]),
top=SimpleAxisArtist(ax1.xaxis, 2, ax1.spines["top"]),
left=SimpleAxisArtist(ax1.yaxis, 1, ax1.spines["left"]),
right=SimpleAxisArtist(ax1.yaxis, 2, ax1.spines["right"]))
else:
ad = ax1.axis
ad[:].line.set_color("w")
ad[:].major_ticks.set_markeredgecolor("w")
def imshow_rgb(self, r, g, b, **kwargs):
"""
Create the four images {rgb, r, g, b}.
Parameters
----------
r, g, b : array-like
The red, green, and blue arrays.
**kwargs
Forwarded to `~.Axes.imshow` calls for the four images.
Returns
-------
rgb : `~matplotlib.image.AxesImage`
r : `~matplotlib.image.AxesImage`
g : `~matplotlib.image.AxesImage`
b : `~matplotlib.image.AxesImage`
"""
if not (r.shape == g.shape == b.shape):
raise ValueError(
f'Input shapes ({r.shape}, {g.shape}, {b.shape}) do not match')
RGB = np.dstack([r, g, b])
R = np.zeros_like(RGB)
R[:, :, 0] = r
G = np.zeros_like(RGB)
G[:, :, 1] = g
B = np.zeros_like(RGB)
B[:, :, 2] = b
im_rgb = self.RGB.imshow(RGB, **kwargs)
im_r = self.R.imshow(R, **kwargs)
im_g = self.G.imshow(G, **kwargs)
im_b = self.B.imshow(B, **kwargs)
return im_rgb, im_r, im_g, im_b

View File

@ -0,0 +1,248 @@
"""
Provides classes of simple units that will be used with `.AxesDivider`
class (or others) to determine the size of each Axes. The unit
classes define `get_size` method that returns a tuple of two floats,
meaning relative and absolute sizes, respectively.
Note that this class is nothing more than a simple tuple of two
floats. Take a look at the Divider class to see how these two
values are used.
"""
from numbers import Real
from matplotlib import _api
from matplotlib.axes import Axes
class _Base:
def __rmul__(self, other):
return Fraction(other, self)
def __add__(self, other):
if isinstance(other, _Base):
return Add(self, other)
else:
return Add(self, Fixed(other))
def get_size(self, renderer):
"""
Return two-float tuple with relative and absolute sizes.
"""
raise NotImplementedError("Subclasses must implement")
class Add(_Base):
"""
Sum of two sizes.
"""
def __init__(self, a, b):
self._a = a
self._b = b
def get_size(self, renderer):
a_rel_size, a_abs_size = self._a.get_size(renderer)
b_rel_size, b_abs_size = self._b.get_size(renderer)
return a_rel_size + b_rel_size, a_abs_size + b_abs_size
class Fixed(_Base):
"""
Simple fixed size with absolute part = *fixed_size* and relative part = 0.
"""
def __init__(self, fixed_size):
_api.check_isinstance(Real, fixed_size=fixed_size)
self.fixed_size = fixed_size
def get_size(self, renderer):
rel_size = 0.
abs_size = self.fixed_size
return rel_size, abs_size
class Scaled(_Base):
"""
Simple scaled(?) size with absolute part = 0 and
relative part = *scalable_size*.
"""
def __init__(self, scalable_size):
self._scalable_size = scalable_size
def get_size(self, renderer):
rel_size = self._scalable_size
abs_size = 0.
return rel_size, abs_size
Scalable = Scaled
def _get_axes_aspect(ax):
aspect = ax.get_aspect()
if aspect == "auto":
aspect = 1.
return aspect
class AxesX(_Base):
"""
Scaled size whose relative part corresponds to the data width
of the *axes* multiplied by the *aspect*.
"""
def __init__(self, axes, aspect=1., ref_ax=None):
self._axes = axes
self._aspect = aspect
if aspect == "axes" and ref_ax is None:
raise ValueError("ref_ax must be set when aspect='axes'")
self._ref_ax = ref_ax
def get_size(self, renderer):
l1, l2 = self._axes.get_xlim()
if self._aspect == "axes":
ref_aspect = _get_axes_aspect(self._ref_ax)
aspect = ref_aspect / _get_axes_aspect(self._axes)
else:
aspect = self._aspect
rel_size = abs(l2-l1)*aspect
abs_size = 0.
return rel_size, abs_size
class AxesY(_Base):
"""
Scaled size whose relative part corresponds to the data height
of the *axes* multiplied by the *aspect*.
"""
def __init__(self, axes, aspect=1., ref_ax=None):
self._axes = axes
self._aspect = aspect
if aspect == "axes" and ref_ax is None:
raise ValueError("ref_ax must be set when aspect='axes'")
self._ref_ax = ref_ax
def get_size(self, renderer):
l1, l2 = self._axes.get_ylim()
if self._aspect == "axes":
ref_aspect = _get_axes_aspect(self._ref_ax)
aspect = _get_axes_aspect(self._axes)
else:
aspect = self._aspect
rel_size = abs(l2-l1)*aspect
abs_size = 0.
return rel_size, abs_size
class MaxExtent(_Base):
"""
Size whose absolute part is either the largest width or the largest height
of the given *artist_list*.
"""
def __init__(self, artist_list, w_or_h):
self._artist_list = artist_list
_api.check_in_list(["width", "height"], w_or_h=w_or_h)
self._w_or_h = w_or_h
def add_artist(self, a):
self._artist_list.append(a)
def get_size(self, renderer):
rel_size = 0.
extent_list = [
getattr(a.get_window_extent(renderer), self._w_or_h) / a.figure.dpi
for a in self._artist_list]
abs_size = max(extent_list, default=0)
return rel_size, abs_size
class MaxWidth(MaxExtent):
"""
Size whose absolute part is the largest width of the given *artist_list*.
"""
def __init__(self, artist_list):
super().__init__(artist_list, "width")
class MaxHeight(MaxExtent):
"""
Size whose absolute part is the largest height of the given *artist_list*.
"""
def __init__(self, artist_list):
super().__init__(artist_list, "height")
class Fraction(_Base):
"""
An instance whose size is a *fraction* of the *ref_size*.
>>> s = Fraction(0.3, AxesX(ax))
"""
def __init__(self, fraction, ref_size):
_api.check_isinstance(Real, fraction=fraction)
self._fraction_ref = ref_size
self._fraction = fraction
def get_size(self, renderer):
if self._fraction_ref is None:
return self._fraction, 0.
else:
r, a = self._fraction_ref.get_size(renderer)
rel_size = r*self._fraction
abs_size = a*self._fraction
return rel_size, abs_size
def from_any(size, fraction_ref=None):
"""
Create a Fixed unit when the first argument is a float, or a
Fraction unit if that is a string that ends with %. The second
argument is only meaningful when Fraction unit is created.
>>> from mpl_toolkits.axes_grid1.axes_size import from_any
>>> a = from_any(1.2) # => Fixed(1.2)
>>> from_any("50%", a) # => Fraction(0.5, a)
"""
if isinstance(size, Real):
return Fixed(size)
elif isinstance(size, str):
if size[-1] == "%":
return Fraction(float(size[:-1]) / 100, fraction_ref)
raise ValueError("Unknown format")
class _AxesDecorationsSize(_Base):
"""
Fixed size, corresponding to the size of decorations on a given Axes side.
"""
_get_size_map = {
"left": lambda tight_bb, axes_bb: axes_bb.xmin - tight_bb.xmin,
"right": lambda tight_bb, axes_bb: tight_bb.xmax - axes_bb.xmax,
"bottom": lambda tight_bb, axes_bb: axes_bb.ymin - tight_bb.ymin,
"top": lambda tight_bb, axes_bb: tight_bb.ymax - axes_bb.ymax,
}
def __init__(self, ax, direction):
_api.check_in_list(self._get_size_map, direction=direction)
self._direction = direction
self._ax_list = [ax] if isinstance(ax, Axes) else ax
def get_size(self, renderer):
sz = max([
self._get_size_map[self._direction](
ax.get_tightbbox(renderer, call_axes_locator=False), ax.bbox)
for ax in self._ax_list])
dpi = renderer.points_to_pixels(72)
abs_size = sz / dpi
rel_size = 0
return rel_size, abs_size

View File

@ -0,0 +1,561 @@
"""
A collection of functions and objects for creating or placing inset axes.
"""
from matplotlib import _api, _docstring
from matplotlib.offsetbox import AnchoredOffsetbox
from matplotlib.patches import Patch, Rectangle
from matplotlib.path import Path
from matplotlib.transforms import Bbox, BboxTransformTo
from matplotlib.transforms import IdentityTransform, TransformedBbox
from . import axes_size as Size
from .parasite_axes import HostAxes
@_api.deprecated("3.8", alternative="Axes.inset_axes")
class InsetPosition:
@_docstring.dedent_interpd
def __init__(self, parent, lbwh):
"""
An object for positioning an inset axes.
This is created by specifying the normalized coordinates in the axes,
instead of the figure.
Parameters
----------
parent : `~matplotlib.axes.Axes`
Axes to use for normalizing coordinates.
lbwh : iterable of four floats
The left edge, bottom edge, width, and height of the inset axes, in
units of the normalized coordinate of the *parent* axes.
See Also
--------
:meth:`matplotlib.axes.Axes.set_axes_locator`
Examples
--------
The following bounds the inset axes to a box with 20%% of the parent
axes height and 40%% of the width. The size of the axes specified
([0, 0, 1, 1]) ensures that the axes completely fills the bounding box:
>>> parent_axes = plt.gca()
>>> ax_ins = plt.axes([0, 0, 1, 1])
>>> ip = InsetPosition(parent_axes, [0.5, 0.1, 0.4, 0.2])
>>> ax_ins.set_axes_locator(ip)
"""
self.parent = parent
self.lbwh = lbwh
def __call__(self, ax, renderer):
bbox_parent = self.parent.get_position(original=False)
trans = BboxTransformTo(bbox_parent)
bbox_inset = Bbox.from_bounds(*self.lbwh)
bb = TransformedBbox(bbox_inset, trans)
return bb
class AnchoredLocatorBase(AnchoredOffsetbox):
def __init__(self, bbox_to_anchor, offsetbox, loc,
borderpad=0.5, bbox_transform=None):
super().__init__(
loc, pad=0., child=None, borderpad=borderpad,
bbox_to_anchor=bbox_to_anchor, bbox_transform=bbox_transform
)
def draw(self, renderer):
raise RuntimeError("No draw method should be called")
def __call__(self, ax, renderer):
if renderer is None:
renderer = ax.figure._get_renderer()
self.axes = ax
bbox = self.get_window_extent(renderer)
px, py = self.get_offset(bbox.width, bbox.height, 0, 0, renderer)
bbox_canvas = Bbox.from_bounds(px, py, bbox.width, bbox.height)
tr = ax.figure.transSubfigure.inverted()
return TransformedBbox(bbox_canvas, tr)
class AnchoredSizeLocator(AnchoredLocatorBase):
def __init__(self, bbox_to_anchor, x_size, y_size, loc,
borderpad=0.5, bbox_transform=None):
super().__init__(
bbox_to_anchor, None, loc,
borderpad=borderpad, bbox_transform=bbox_transform
)
self.x_size = Size.from_any(x_size)
self.y_size = Size.from_any(y_size)
def get_bbox(self, renderer):
bbox = self.get_bbox_to_anchor()
dpi = renderer.points_to_pixels(72.)
r, a = self.x_size.get_size(renderer)
width = bbox.width * r + a * dpi
r, a = self.y_size.get_size(renderer)
height = bbox.height * r + a * dpi
fontsize = renderer.points_to_pixels(self.prop.get_size_in_points())
pad = self.pad * fontsize
return Bbox.from_bounds(0, 0, width, height).padded(pad)
class AnchoredZoomLocator(AnchoredLocatorBase):
def __init__(self, parent_axes, zoom, loc,
borderpad=0.5,
bbox_to_anchor=None,
bbox_transform=None):
self.parent_axes = parent_axes
self.zoom = zoom
if bbox_to_anchor is None:
bbox_to_anchor = parent_axes.bbox
super().__init__(
bbox_to_anchor, None, loc, borderpad=borderpad,
bbox_transform=bbox_transform)
def get_bbox(self, renderer):
bb = self.parent_axes.transData.transform_bbox(self.axes.viewLim)
fontsize = renderer.points_to_pixels(self.prop.get_size_in_points())
pad = self.pad * fontsize
return (
Bbox.from_bounds(
0, 0, abs(bb.width * self.zoom), abs(bb.height * self.zoom))
.padded(pad))
class BboxPatch(Patch):
@_docstring.dedent_interpd
def __init__(self, bbox, **kwargs):
"""
Patch showing the shape bounded by a Bbox.
Parameters
----------
bbox : `~matplotlib.transforms.Bbox`
Bbox to use for the extents of this patch.
**kwargs
Patch properties. Valid arguments include:
%(Patch:kwdoc)s
"""
if "transform" in kwargs:
raise ValueError("transform should not be set")
kwargs["transform"] = IdentityTransform()
super().__init__(**kwargs)
self.bbox = bbox
def get_path(self):
# docstring inherited
x0, y0, x1, y1 = self.bbox.extents
return Path._create_closed([(x0, y0), (x1, y0), (x1, y1), (x0, y1)])
class BboxConnector(Patch):
@staticmethod
def get_bbox_edge_pos(bbox, loc):
"""
Return the ``(x, y)`` coordinates of corner *loc* of *bbox*; parameters
behave as documented for the `.BboxConnector` constructor.
"""
x0, y0, x1, y1 = bbox.extents
if loc == 1:
return x1, y1
elif loc == 2:
return x0, y1
elif loc == 3:
return x0, y0
elif loc == 4:
return x1, y0
@staticmethod
def connect_bbox(bbox1, bbox2, loc1, loc2=None):
"""
Construct a `.Path` connecting corner *loc1* of *bbox1* to corner
*loc2* of *bbox2*, where parameters behave as documented as for the
`.BboxConnector` constructor.
"""
if isinstance(bbox1, Rectangle):
bbox1 = TransformedBbox(Bbox.unit(), bbox1.get_transform())
if isinstance(bbox2, Rectangle):
bbox2 = TransformedBbox(Bbox.unit(), bbox2.get_transform())
if loc2 is None:
loc2 = loc1
x1, y1 = BboxConnector.get_bbox_edge_pos(bbox1, loc1)
x2, y2 = BboxConnector.get_bbox_edge_pos(bbox2, loc2)
return Path([[x1, y1], [x2, y2]])
@_docstring.dedent_interpd
def __init__(self, bbox1, bbox2, loc1, loc2=None, **kwargs):
"""
Connect two bboxes with a straight line.
Parameters
----------
bbox1, bbox2 : `~matplotlib.transforms.Bbox`
Bounding boxes to connect.
loc1, loc2 : {1, 2, 3, 4}
Corner of *bbox1* and *bbox2* to draw the line. Valid values are::
'upper right' : 1,
'upper left' : 2,
'lower left' : 3,
'lower right' : 4
*loc2* is optional and defaults to *loc1*.
**kwargs
Patch properties for the line drawn. Valid arguments include:
%(Patch:kwdoc)s
"""
if "transform" in kwargs:
raise ValueError("transform should not be set")
kwargs["transform"] = IdentityTransform()
kwargs.setdefault(
"fill", bool({'fc', 'facecolor', 'color'}.intersection(kwargs)))
super().__init__(**kwargs)
self.bbox1 = bbox1
self.bbox2 = bbox2
self.loc1 = loc1
self.loc2 = loc2
def get_path(self):
# docstring inherited
return self.connect_bbox(self.bbox1, self.bbox2,
self.loc1, self.loc2)
class BboxConnectorPatch(BboxConnector):
@_docstring.dedent_interpd
def __init__(self, bbox1, bbox2, loc1a, loc2a, loc1b, loc2b, **kwargs):
"""
Connect two bboxes with a quadrilateral.
The quadrilateral is specified by two lines that start and end at
corners of the bboxes. The four sides of the quadrilateral are defined
by the two lines given, the line between the two corners specified in
*bbox1* and the line between the two corners specified in *bbox2*.
Parameters
----------
bbox1, bbox2 : `~matplotlib.transforms.Bbox`
Bounding boxes to connect.
loc1a, loc2a, loc1b, loc2b : {1, 2, 3, 4}
The first line connects corners *loc1a* of *bbox1* and *loc2a* of
*bbox2*; the second line connects corners *loc1b* of *bbox1* and
*loc2b* of *bbox2*. Valid values are::
'upper right' : 1,
'upper left' : 2,
'lower left' : 3,
'lower right' : 4
**kwargs
Patch properties for the line drawn:
%(Patch:kwdoc)s
"""
if "transform" in kwargs:
raise ValueError("transform should not be set")
super().__init__(bbox1, bbox2, loc1a, loc2a, **kwargs)
self.loc1b = loc1b
self.loc2b = loc2b
def get_path(self):
# docstring inherited
path1 = self.connect_bbox(self.bbox1, self.bbox2, self.loc1, self.loc2)
path2 = self.connect_bbox(self.bbox2, self.bbox1,
self.loc2b, self.loc1b)
path_merged = [*path1.vertices, *path2.vertices, path1.vertices[0]]
return Path(path_merged)
def _add_inset_axes(parent_axes, axes_class, axes_kwargs, axes_locator):
"""Helper function to add an inset axes and disable navigation in it."""
if axes_class is None:
axes_class = HostAxes
if axes_kwargs is None:
axes_kwargs = {}
inset_axes = axes_class(
parent_axes.figure, parent_axes.get_position(),
**{"navigate": False, **axes_kwargs, "axes_locator": axes_locator})
return parent_axes.figure.add_axes(inset_axes)
@_docstring.dedent_interpd
def inset_axes(parent_axes, width, height, loc='upper right',
bbox_to_anchor=None, bbox_transform=None,
axes_class=None, axes_kwargs=None,
borderpad=0.5):
"""
Create an inset axes with a given width and height.
Both sizes used can be specified either in inches or percentage.
For example,::
inset_axes(parent_axes, width='40%%', height='30%%', loc='lower left')
creates in inset axes in the lower left corner of *parent_axes* which spans
over 30%% in height and 40%% in width of the *parent_axes*. Since the usage
of `.inset_axes` may become slightly tricky when exceeding such standard
cases, it is recommended to read :doc:`the examples
</gallery/axes_grid1/inset_locator_demo>`.
Notes
-----
The meaning of *bbox_to_anchor* and *bbox_to_transform* is interpreted
differently from that of legend. The value of bbox_to_anchor
(or the return value of its get_points method; the default is
*parent_axes.bbox*) is transformed by the bbox_transform (the default
is Identity transform) and then interpreted as points in the pixel
coordinate (which is dpi dependent).
Thus, following three calls are identical and creates an inset axes
with respect to the *parent_axes*::
axins = inset_axes(parent_axes, "30%%", "40%%")
axins = inset_axes(parent_axes, "30%%", "40%%",
bbox_to_anchor=parent_axes.bbox)
axins = inset_axes(parent_axes, "30%%", "40%%",
bbox_to_anchor=(0, 0, 1, 1),
bbox_transform=parent_axes.transAxes)
Parameters
----------
parent_axes : `matplotlib.axes.Axes`
Axes to place the inset axes.
width, height : float or str
Size of the inset axes to create. If a float is provided, it is
the size in inches, e.g. *width=1.3*. If a string is provided, it is
the size in relative units, e.g. *width='40%%'*. By default, i.e. if
neither *bbox_to_anchor* nor *bbox_transform* are specified, those
are relative to the parent_axes. Otherwise, they are to be understood
relative to the bounding box provided via *bbox_to_anchor*.
loc : str, default: 'upper right'
Location to place the inset axes. Valid locations are
'upper left', 'upper center', 'upper right',
'center left', 'center', 'center right',
'lower left', 'lower center', 'lower right'.
For backward compatibility, numeric values are accepted as well.
See the parameter *loc* of `.Legend` for details.
bbox_to_anchor : tuple or `~matplotlib.transforms.BboxBase`, optional
Bbox that the inset axes will be anchored to. If None,
a tuple of (0, 0, 1, 1) is used if *bbox_transform* is set
to *parent_axes.transAxes* or *parent_axes.figure.transFigure*.
Otherwise, *parent_axes.bbox* is used. If a tuple, can be either
[left, bottom, width, height], or [left, bottom].
If the kwargs *width* and/or *height* are specified in relative units,
the 2-tuple [left, bottom] cannot be used. Note that,
unless *bbox_transform* is set, the units of the bounding box
are interpreted in the pixel coordinate. When using *bbox_to_anchor*
with tuple, it almost always makes sense to also specify
a *bbox_transform*. This might often be the axes transform
*parent_axes.transAxes*.
bbox_transform : `~matplotlib.transforms.Transform`, optional
Transformation for the bbox that contains the inset axes.
If None, a `.transforms.IdentityTransform` is used. The value
of *bbox_to_anchor* (or the return value of its get_points method)
is transformed by the *bbox_transform* and then interpreted
as points in the pixel coordinate (which is dpi dependent).
You may provide *bbox_to_anchor* in some normalized coordinate,
and give an appropriate transform (e.g., *parent_axes.transAxes*).
axes_class : `~matplotlib.axes.Axes` type, default: `.HostAxes`
The type of the newly created inset axes.
axes_kwargs : dict, optional
Keyword arguments to pass to the constructor of the inset axes.
Valid arguments include:
%(Axes:kwdoc)s
borderpad : float, default: 0.5
Padding between inset axes and the bbox_to_anchor.
The units are axes font size, i.e. for a default font size of 10 points
*borderpad = 0.5* is equivalent to a padding of 5 points.
Returns
-------
inset_axes : *axes_class*
Inset axes object created.
"""
if (bbox_transform in [parent_axes.transAxes, parent_axes.figure.transFigure]
and bbox_to_anchor is None):
_api.warn_external("Using the axes or figure transform requires a "
"bounding box in the respective coordinates. "
"Using bbox_to_anchor=(0, 0, 1, 1) now.")
bbox_to_anchor = (0, 0, 1, 1)
if bbox_to_anchor is None:
bbox_to_anchor = parent_axes.bbox
if (isinstance(bbox_to_anchor, tuple) and
(isinstance(width, str) or isinstance(height, str))):
if len(bbox_to_anchor) != 4:
raise ValueError("Using relative units for width or height "
"requires to provide a 4-tuple or a "
"`Bbox` instance to `bbox_to_anchor.")
return _add_inset_axes(
parent_axes, axes_class, axes_kwargs,
AnchoredSizeLocator(
bbox_to_anchor, width, height, loc=loc,
bbox_transform=bbox_transform, borderpad=borderpad))
@_docstring.dedent_interpd
def zoomed_inset_axes(parent_axes, zoom, loc='upper right',
bbox_to_anchor=None, bbox_transform=None,
axes_class=None, axes_kwargs=None,
borderpad=0.5):
"""
Create an anchored inset axes by scaling a parent axes. For usage, also see
:doc:`the examples </gallery/axes_grid1/inset_locator_demo2>`.
Parameters
----------
parent_axes : `~matplotlib.axes.Axes`
Axes to place the inset axes.
zoom : float
Scaling factor of the data axes. *zoom* > 1 will enlarge the
coordinates (i.e., "zoomed in"), while *zoom* < 1 will shrink the
coordinates (i.e., "zoomed out").
loc : str, default: 'upper right'
Location to place the inset axes. Valid locations are
'upper left', 'upper center', 'upper right',
'center left', 'center', 'center right',
'lower left', 'lower center', 'lower right'.
For backward compatibility, numeric values are accepted as well.
See the parameter *loc* of `.Legend` for details.
bbox_to_anchor : tuple or `~matplotlib.transforms.BboxBase`, optional
Bbox that the inset axes will be anchored to. If None,
*parent_axes.bbox* is used. If a tuple, can be either
[left, bottom, width, height], or [left, bottom].
If the kwargs *width* and/or *height* are specified in relative units,
the 2-tuple [left, bottom] cannot be used. Note that
the units of the bounding box are determined through the transform
in use. When using *bbox_to_anchor* it almost always makes sense to
also specify a *bbox_transform*. This might often be the axes transform
*parent_axes.transAxes*.
bbox_transform : `~matplotlib.transforms.Transform`, optional
Transformation for the bbox that contains the inset axes.
If None, a `.transforms.IdentityTransform` is used (i.e. pixel
coordinates). This is useful when not providing any argument to
*bbox_to_anchor*. When using *bbox_to_anchor* it almost always makes
sense to also specify a *bbox_transform*. This might often be the
axes transform *parent_axes.transAxes*. Inversely, when specifying
the axes- or figure-transform here, be aware that not specifying
*bbox_to_anchor* will use *parent_axes.bbox*, the units of which are
in display (pixel) coordinates.
axes_class : `~matplotlib.axes.Axes` type, default: `.HostAxes`
The type of the newly created inset axes.
axes_kwargs : dict, optional
Keyword arguments to pass to the constructor of the inset axes.
Valid arguments include:
%(Axes:kwdoc)s
borderpad : float, default: 0.5
Padding between inset axes and the bbox_to_anchor.
The units are axes font size, i.e. for a default font size of 10 points
*borderpad = 0.5* is equivalent to a padding of 5 points.
Returns
-------
inset_axes : *axes_class*
Inset axes object created.
"""
return _add_inset_axes(
parent_axes, axes_class, axes_kwargs,
AnchoredZoomLocator(
parent_axes, zoom=zoom, loc=loc,
bbox_to_anchor=bbox_to_anchor, bbox_transform=bbox_transform,
borderpad=borderpad))
class _TransformedBboxWithCallback(TransformedBbox):
"""
Variant of `.TransformBbox` which calls *callback* before returning points.
Used by `.mark_inset` to unstale the parent axes' viewlim as needed.
"""
def __init__(self, *args, callback, **kwargs):
super().__init__(*args, **kwargs)
self._callback = callback
def get_points(self):
self._callback()
return super().get_points()
@_docstring.dedent_interpd
def mark_inset(parent_axes, inset_axes, loc1, loc2, **kwargs):
"""
Draw a box to mark the location of an area represented by an inset axes.
This function draws a box in *parent_axes* at the bounding box of
*inset_axes*, and shows a connection with the inset axes by drawing lines
at the corners, giving a "zoomed in" effect.
Parameters
----------
parent_axes : `~matplotlib.axes.Axes`
Axes which contains the area of the inset axes.
inset_axes : `~matplotlib.axes.Axes`
The inset axes.
loc1, loc2 : {1, 2, 3, 4}
Corners to use for connecting the inset axes and the area in the
parent axes.
**kwargs
Patch properties for the lines and box drawn:
%(Patch:kwdoc)s
Returns
-------
pp : `~matplotlib.patches.Patch`
The patch drawn to represent the area of the inset axes.
p1, p2 : `~matplotlib.patches.Patch`
The patches connecting two corners of the inset axes and its area.
"""
rect = _TransformedBboxWithCallback(
inset_axes.viewLim, parent_axes.transData,
callback=parent_axes._unstale_viewLim)
kwargs.setdefault("fill", bool({'fc', 'facecolor', 'color'}.intersection(kwargs)))
pp = BboxPatch(rect, **kwargs)
parent_axes.add_patch(pp)
p1 = BboxConnector(inset_axes.bbox, rect, loc1=loc1, **kwargs)
inset_axes.add_patch(p1)
p1.set_clip_on(False)
p2 = BboxConnector(inset_axes.bbox, rect, loc1=loc2, **kwargs)
inset_axes.add_patch(p2)
p2.set_clip_on(False)
return pp, p1, p2

View File

@ -0,0 +1,128 @@
import matplotlib.axes as maxes
from matplotlib.artist import Artist
from matplotlib.axis import XAxis, YAxis
class SimpleChainedObjects:
def __init__(self, objects):
self._objects = objects
def __getattr__(self, k):
_a = SimpleChainedObjects([getattr(a, k) for a in self._objects])
return _a
def __call__(self, *args, **kwargs):
for m in self._objects:
m(*args, **kwargs)
class Axes(maxes.Axes):
class AxisDict(dict):
def __init__(self, axes):
self.axes = axes
super().__init__()
def __getitem__(self, k):
if isinstance(k, tuple):
r = SimpleChainedObjects(
# super() within a list comprehension needs explicit args.
[super(Axes.AxisDict, self).__getitem__(k1) for k1 in k])
return r
elif isinstance(k, slice):
if k.start is None and k.stop is None and k.step is None:
return SimpleChainedObjects(list(self.values()))
else:
raise ValueError("Unsupported slice")
else:
return dict.__getitem__(self, k)
def __call__(self, *v, **kwargs):
return maxes.Axes.axis(self.axes, *v, **kwargs)
@property
def axis(self):
return self._axislines
def clear(self):
# docstring inherited
super().clear()
# Init axis artists.
self._axislines = self.AxisDict(self)
self._axislines.update(
bottom=SimpleAxisArtist(self.xaxis, 1, self.spines["bottom"]),
top=SimpleAxisArtist(self.xaxis, 2, self.spines["top"]),
left=SimpleAxisArtist(self.yaxis, 1, self.spines["left"]),
right=SimpleAxisArtist(self.yaxis, 2, self.spines["right"]))
class SimpleAxisArtist(Artist):
def __init__(self, axis, axisnum, spine):
self._axis = axis
self._axisnum = axisnum
self.line = spine
if isinstance(axis, XAxis):
self._axis_direction = ["bottom", "top"][axisnum-1]
elif isinstance(axis, YAxis):
self._axis_direction = ["left", "right"][axisnum-1]
else:
raise ValueError(
f"axis must be instance of XAxis or YAxis, but got {axis}")
super().__init__()
@property
def major_ticks(self):
tickline = "tick%dline" % self._axisnum
return SimpleChainedObjects([getattr(tick, tickline)
for tick in self._axis.get_major_ticks()])
@property
def major_ticklabels(self):
label = "label%d" % self._axisnum
return SimpleChainedObjects([getattr(tick, label)
for tick in self._axis.get_major_ticks()])
@property
def label(self):
return self._axis.label
def set_visible(self, b):
self.toggle(all=b)
self.line.set_visible(b)
self._axis.set_visible(True)
super().set_visible(b)
def set_label(self, txt):
self._axis.set_label_text(txt)
def toggle(self, all=None, ticks=None, ticklabels=None, label=None):
if all:
_ticks, _ticklabels, _label = True, True, True
elif all is not None:
_ticks, _ticklabels, _label = False, False, False
else:
_ticks, _ticklabels, _label = None, None, None
if ticks is not None:
_ticks = ticks
if ticklabels is not None:
_ticklabels = ticklabels
if label is not None:
_label = label
if _ticks is not None:
tickparam = {f"tick{self._axisnum}On": _ticks}
self._axis.set_tick_params(**tickparam)
if _ticklabels is not None:
tickparam = {f"label{self._axisnum}On": _ticklabels}
self._axis.set_tick_params(**tickparam)
if _label is not None:
pos = self._axis.get_label_position()
if (pos == self._axis_direction) and not _label:
self._axis.label.set_visible(False)
elif _label:
self._axis.label.set_visible(True)
self._axis.set_label_position(self._axis_direction)

View File

@ -0,0 +1,257 @@
from matplotlib import _api, cbook
import matplotlib.artist as martist
import matplotlib.transforms as mtransforms
from matplotlib.transforms import Bbox
from .mpl_axes import Axes
class ParasiteAxesBase:
def __init__(self, parent_axes, aux_transform=None,
*, viewlim_mode=None, **kwargs):
self._parent_axes = parent_axes
self.transAux = aux_transform
self.set_viewlim_mode(viewlim_mode)
kwargs["frameon"] = False
super().__init__(parent_axes.figure, parent_axes._position, **kwargs)
def clear(self):
super().clear()
martist.setp(self.get_children(), visible=False)
self._get_lines = self._parent_axes._get_lines
self._parent_axes.callbacks._connect_picklable(
"xlim_changed", self._sync_lims)
self._parent_axes.callbacks._connect_picklable(
"ylim_changed", self._sync_lims)
def pick(self, mouseevent):
# This most likely goes to Artist.pick (depending on axes_class given
# to the factory), which only handles pick events registered on the
# axes associated with each child:
super().pick(mouseevent)
# But parasite axes are additionally given pick events from their host
# axes (cf. HostAxesBase.pick), which we handle here:
for a in self.get_children():
if (hasattr(mouseevent.inaxes, "parasites")
and self in mouseevent.inaxes.parasites):
a.pick(mouseevent)
# aux_transform support
def _set_lim_and_transforms(self):
if self.transAux is not None:
self.transAxes = self._parent_axes.transAxes
self.transData = self.transAux + self._parent_axes.transData
self._xaxis_transform = mtransforms.blended_transform_factory(
self.transData, self.transAxes)
self._yaxis_transform = mtransforms.blended_transform_factory(
self.transAxes, self.transData)
else:
super()._set_lim_and_transforms()
def set_viewlim_mode(self, mode):
_api.check_in_list([None, "equal", "transform"], mode=mode)
self._viewlim_mode = mode
def get_viewlim_mode(self):
return self._viewlim_mode
def _sync_lims(self, parent):
viewlim = parent.viewLim.frozen()
mode = self.get_viewlim_mode()
if mode is None:
pass
elif mode == "equal":
self.viewLim.set(viewlim)
elif mode == "transform":
self.viewLim.set(viewlim.transformed(self.transAux.inverted()))
else:
_api.check_in_list([None, "equal", "transform"], mode=mode)
# end of aux_transform support
parasite_axes_class_factory = cbook._make_class_factory(
ParasiteAxesBase, "{}Parasite")
ParasiteAxes = parasite_axes_class_factory(Axes)
class HostAxesBase:
def __init__(self, *args, **kwargs):
self.parasites = []
super().__init__(*args, **kwargs)
def get_aux_axes(
self, tr=None, viewlim_mode="equal", axes_class=None, **kwargs):
"""
Add a parasite axes to this host.
Despite this method's name, this should actually be thought of as an
``add_parasite_axes`` method.
.. versionchanged:: 3.7
Defaults to same base axes class as host axes.
Parameters
----------
tr : `~matplotlib.transforms.Transform` or None, default: None
If a `.Transform`, the following relation will hold:
``parasite.transData = tr + host.transData``.
If None, the parasite's and the host's ``transData`` are unrelated.
viewlim_mode : {"equal", "transform", None}, default: "equal"
How the parasite's view limits are set: directly equal to the
parent axes ("equal"), equal after application of *tr*
("transform"), or independently (None).
axes_class : subclass type of `~matplotlib.axes.Axes`, optional
The `~.axes.Axes` subclass that is instantiated. If None, the base
class of the host axes is used.
**kwargs
Other parameters are forwarded to the parasite axes constructor.
"""
if axes_class is None:
axes_class = self._base_axes_class
parasite_axes_class = parasite_axes_class_factory(axes_class)
ax2 = parasite_axes_class(
self, tr, viewlim_mode=viewlim_mode, **kwargs)
# note that ax2.transData == tr + ax1.transData
# Anything you draw in ax2 will match the ticks and grids of ax1.
self.parasites.append(ax2)
ax2._remove_method = self.parasites.remove
return ax2
def draw(self, renderer):
orig_children_len = len(self._children)
locator = self.get_axes_locator()
if locator:
pos = locator(self, renderer)
self.set_position(pos, which="active")
self.apply_aspect(pos)
else:
self.apply_aspect()
rect = self.get_position()
for ax in self.parasites:
ax.apply_aspect(rect)
self._children.extend(ax.get_children())
super().draw(renderer)
del self._children[orig_children_len:]
def clear(self):
super().clear()
for ax in self.parasites:
ax.clear()
def pick(self, mouseevent):
super().pick(mouseevent)
# Also pass pick events on to parasite axes and, in turn, their
# children (cf. ParasiteAxesBase.pick)
for a in self.parasites:
a.pick(mouseevent)
def twinx(self, axes_class=None):
"""
Create a twin of Axes with a shared x-axis but independent y-axis.
The y-axis of self will have ticks on the left and the returned axes
will have ticks on the right.
"""
ax = self._add_twin_axes(axes_class, sharex=self)
self.axis["right"].set_visible(False)
ax.axis["right"].set_visible(True)
ax.axis["left", "top", "bottom"].set_visible(False)
return ax
def twiny(self, axes_class=None):
"""
Create a twin of Axes with a shared y-axis but independent x-axis.
The x-axis of self will have ticks on the bottom and the returned axes
will have ticks on the top.
"""
ax = self._add_twin_axes(axes_class, sharey=self)
self.axis["top"].set_visible(False)
ax.axis["top"].set_visible(True)
ax.axis["left", "right", "bottom"].set_visible(False)
return ax
def twin(self, aux_trans=None, axes_class=None):
"""
Create a twin of Axes with no shared axis.
While self will have ticks on the left and bottom axis, the returned
axes will have ticks on the top and right axis.
"""
if aux_trans is None:
aux_trans = mtransforms.IdentityTransform()
ax = self._add_twin_axes(
axes_class, aux_transform=aux_trans, viewlim_mode="transform")
self.axis["top", "right"].set_visible(False)
ax.axis["top", "right"].set_visible(True)
ax.axis["left", "bottom"].set_visible(False)
return ax
def _add_twin_axes(self, axes_class, **kwargs):
"""
Helper for `.twinx`/`.twiny`/`.twin`.
*kwargs* are forwarded to the parasite axes constructor.
"""
if axes_class is None:
axes_class = self._base_axes_class
ax = parasite_axes_class_factory(axes_class)(self, **kwargs)
self.parasites.append(ax)
ax._remove_method = self._remove_any_twin
return ax
def _remove_any_twin(self, ax):
self.parasites.remove(ax)
restore = ["top", "right"]
if ax._sharex:
restore.remove("top")
if ax._sharey:
restore.remove("right")
self.axis[tuple(restore)].set_visible(True)
self.axis[tuple(restore)].toggle(ticklabels=False, label=False)
@_api.make_keyword_only("3.8", "call_axes_locator")
def get_tightbbox(self, renderer=None, call_axes_locator=True,
bbox_extra_artists=None):
bbs = [
*[ax.get_tightbbox(renderer, call_axes_locator=call_axes_locator)
for ax in self.parasites],
super().get_tightbbox(renderer,
call_axes_locator=call_axes_locator,
bbox_extra_artists=bbox_extra_artists)]
return Bbox.union([b for b in bbs if b.width != 0 or b.height != 0])
host_axes_class_factory = host_subplot_class_factory = \
cbook._make_class_factory(HostAxesBase, "{}HostAxes", "_base_axes_class")
HostAxes = SubplotHost = host_axes_class_factory(Axes)
def host_axes(*args, axes_class=Axes, figure=None, **kwargs):
"""
Create axes that can act as a hosts to parasitic axes.
Parameters
----------
figure : `~matplotlib.figure.Figure`
Figure to which the axes will be added. Defaults to the current figure
`.pyplot.gcf()`.
*args, **kwargs
Will be passed on to the underlying `~.axes.Axes` object creation.
"""
import matplotlib.pyplot as plt
host_axes_class = host_axes_class_factory(axes_class)
if figure is None:
figure = plt.gcf()
ax = host_axes_class(figure, *args, **kwargs)
figure.add_axes(ax)
return ax
host_subplot = host_axes

View File

@ -0,0 +1,10 @@
from pathlib import Path
# Check that the test directories exist
if not (Path(__file__).parent / "baseline_images").exists():
raise OSError(
'The baseline image directory does not exist. '
'This is most likely because the test data is not installed. '
'You may need to install matplotlib from source to get the '
'test data.')

View File

@ -0,0 +1,2 @@
from matplotlib.testing.conftest import (mpl_test_settings, # noqa
pytest_configure, pytest_unconfigure)

View File

@ -0,0 +1,792 @@
from itertools import product
import io
import platform
import matplotlib as mpl
import matplotlib.pyplot as plt
import matplotlib.ticker as mticker
from matplotlib import cbook
from matplotlib.backend_bases import MouseEvent
from matplotlib.colors import LogNorm
from matplotlib.patches import Circle, Ellipse
from matplotlib.transforms import Bbox, TransformedBbox
from matplotlib.testing.decorators import (
check_figures_equal, image_comparison, remove_ticks_and_titles)
from mpl_toolkits.axes_grid1 import (
axes_size as Size,
host_subplot, make_axes_locatable,
Grid, AxesGrid, ImageGrid)
from mpl_toolkits.axes_grid1.anchored_artists import (
AnchoredAuxTransformBox, AnchoredDrawingArea, AnchoredEllipse,
AnchoredDirectionArrows, AnchoredSizeBar)
from mpl_toolkits.axes_grid1.axes_divider import (
Divider, HBoxDivider, make_axes_area_auto_adjustable, SubplotDivider,
VBoxDivider)
from mpl_toolkits.axes_grid1.axes_rgb import RGBAxes
from mpl_toolkits.axes_grid1.inset_locator import (
zoomed_inset_axes, mark_inset, inset_axes, BboxConnectorPatch,
InsetPosition)
import mpl_toolkits.axes_grid1.mpl_axes
import pytest
import numpy as np
from numpy.testing import assert_array_equal, assert_array_almost_equal
def test_divider_append_axes():
fig, ax = plt.subplots()
divider = make_axes_locatable(ax)
axs = {
"main": ax,
"top": divider.append_axes("top", 1.2, pad=0.1, sharex=ax),
"bottom": divider.append_axes("bottom", 1.2, pad=0.1, sharex=ax),
"left": divider.append_axes("left", 1.2, pad=0.1, sharey=ax),
"right": divider.append_axes("right", 1.2, pad=0.1, sharey=ax),
}
fig.canvas.draw()
bboxes = {k: axs[k].get_window_extent() for k in axs}
dpi = fig.dpi
assert bboxes["top"].height == pytest.approx(1.2 * dpi)
assert bboxes["bottom"].height == pytest.approx(1.2 * dpi)
assert bboxes["left"].width == pytest.approx(1.2 * dpi)
assert bboxes["right"].width == pytest.approx(1.2 * dpi)
assert bboxes["top"].y0 - bboxes["main"].y1 == pytest.approx(0.1 * dpi)
assert bboxes["main"].y0 - bboxes["bottom"].y1 == pytest.approx(0.1 * dpi)
assert bboxes["main"].x0 - bboxes["left"].x1 == pytest.approx(0.1 * dpi)
assert bboxes["right"].x0 - bboxes["main"].x1 == pytest.approx(0.1 * dpi)
assert bboxes["left"].y0 == bboxes["main"].y0 == bboxes["right"].y0
assert bboxes["left"].y1 == bboxes["main"].y1 == bboxes["right"].y1
assert bboxes["top"].x0 == bboxes["main"].x0 == bboxes["bottom"].x0
assert bboxes["top"].x1 == bboxes["main"].x1 == bboxes["bottom"].x1
# Update style when regenerating the test image
@image_comparison(['twin_axes_empty_and_removed'], extensions=["png"], tol=1,
style=('classic', '_classic_test_patch'))
def test_twin_axes_empty_and_removed():
# Purely cosmetic font changes (avoid overlap)
mpl.rcParams.update(
{"font.size": 8, "xtick.labelsize": 8, "ytick.labelsize": 8})
generators = ["twinx", "twiny", "twin"]
modifiers = ["", "host invisible", "twin removed", "twin invisible",
"twin removed\nhost invisible"]
# Unmodified host subplot at the beginning for reference
h = host_subplot(len(modifiers)+1, len(generators), 2)
h.text(0.5, 0.5, "host_subplot",
horizontalalignment="center", verticalalignment="center")
# Host subplots with various modifications (twin*, visibility) applied
for i, (mod, gen) in enumerate(product(modifiers, generators),
len(generators) + 1):
h = host_subplot(len(modifiers)+1, len(generators), i)
t = getattr(h, gen)()
if "twin invisible" in mod:
t.axis[:].set_visible(False)
if "twin removed" in mod:
t.remove()
if "host invisible" in mod:
h.axis[:].set_visible(False)
h.text(0.5, 0.5, gen + ("\n" + mod if mod else ""),
horizontalalignment="center", verticalalignment="center")
plt.subplots_adjust(wspace=0.5, hspace=1)
def test_twin_axes_both_with_units():
host = host_subplot(111)
with pytest.warns(mpl.MatplotlibDeprecationWarning):
host.plot_date([0, 1, 2], [0, 1, 2], xdate=False, ydate=True)
twin = host.twinx()
twin.plot(["a", "b", "c"])
assert host.get_yticklabels()[0].get_text() == "00:00:00"
assert twin.get_yticklabels()[0].get_text() == "a"
def test_axesgrid_colorbar_log_smoketest():
fig = plt.figure()
grid = AxesGrid(fig, 111, # modified to be only subplot
nrows_ncols=(1, 1),
ngrids=1,
label_mode="L",
cbar_location="top",
cbar_mode="single",
)
Z = 10000 * np.random.rand(10, 10)
im = grid[0].imshow(Z, interpolation="nearest", norm=LogNorm())
grid.cbar_axes[0].colorbar(im)
def test_inset_colorbar_tight_layout_smoketest():
fig, ax = plt.subplots(1, 1)
pts = ax.scatter([0, 1], [0, 1], c=[1, 5])
cax = inset_axes(ax, width="3%", height="70%")
plt.colorbar(pts, cax=cax)
with pytest.warns(UserWarning, match="This figure includes Axes"):
# Will warn, but not raise an error
plt.tight_layout()
@image_comparison(['inset_locator.png'], style='default', remove_text=True)
def test_inset_locator():
fig, ax = plt.subplots(figsize=[5, 4])
# prepare the demo image
# Z is a 15x15 array
Z = cbook.get_sample_data("axes_grid/bivariate_normal.npy")
extent = (-3, 4, -4, 3)
Z2 = np.zeros((150, 150))
ny, nx = Z.shape
Z2[30:30+ny, 30:30+nx] = Z
ax.imshow(Z2, extent=extent, interpolation="nearest",
origin="lower")
axins = zoomed_inset_axes(ax, zoom=6, loc='upper right')
axins.imshow(Z2, extent=extent, interpolation="nearest",
origin="lower")
axins.yaxis.get_major_locator().set_params(nbins=7)
axins.xaxis.get_major_locator().set_params(nbins=7)
# sub region of the original image
x1, x2, y1, y2 = -1.5, -0.9, -2.5, -1.9
axins.set_xlim(x1, x2)
axins.set_ylim(y1, y2)
plt.xticks(visible=False)
plt.yticks(visible=False)
# draw a bbox of the region of the inset axes in the parent axes and
# connecting lines between the bbox and the inset axes area
mark_inset(ax, axins, loc1=2, loc2=4, fc="none", ec="0.5")
asb = AnchoredSizeBar(ax.transData,
0.5,
'0.5',
loc='lower center',
pad=0.1, borderpad=0.5, sep=5,
frameon=False)
ax.add_artist(asb)
@image_comparison(['inset_axes.png'], style='default', remove_text=True)
def test_inset_axes():
fig, ax = plt.subplots(figsize=[5, 4])
# prepare the demo image
# Z is a 15x15 array
Z = cbook.get_sample_data("axes_grid/bivariate_normal.npy")
extent = (-3, 4, -4, 3)
Z2 = np.zeros((150, 150))
ny, nx = Z.shape
Z2[30:30+ny, 30:30+nx] = Z
ax.imshow(Z2, extent=extent, interpolation="nearest",
origin="lower")
# creating our inset axes with a bbox_transform parameter
axins = inset_axes(ax, width=1., height=1., bbox_to_anchor=(1, 1),
bbox_transform=ax.transAxes)
axins.imshow(Z2, extent=extent, interpolation="nearest",
origin="lower")
axins.yaxis.get_major_locator().set_params(nbins=7)
axins.xaxis.get_major_locator().set_params(nbins=7)
# sub region of the original image
x1, x2, y1, y2 = -1.5, -0.9, -2.5, -1.9
axins.set_xlim(x1, x2)
axins.set_ylim(y1, y2)
plt.xticks(visible=False)
plt.yticks(visible=False)
# draw a bbox of the region of the inset axes in the parent axes and
# connecting lines between the bbox and the inset axes area
mark_inset(ax, axins, loc1=2, loc2=4, fc="none", ec="0.5")
asb = AnchoredSizeBar(ax.transData,
0.5,
'0.5',
loc='lower center',
pad=0.1, borderpad=0.5, sep=5,
frameon=False)
ax.add_artist(asb)
def test_inset_axes_complete():
dpi = 100
figsize = (6, 5)
fig, ax = plt.subplots(figsize=figsize, dpi=dpi)
fig.subplots_adjust(.1, .1, .9, .9)
ins = inset_axes(ax, width=2., height=2., borderpad=0)
fig.canvas.draw()
assert_array_almost_equal(
ins.get_position().extents,
[(0.9*figsize[0]-2.)/figsize[0], (0.9*figsize[1]-2.)/figsize[1],
0.9, 0.9])
ins = inset_axes(ax, width="40%", height="30%", borderpad=0)
fig.canvas.draw()
assert_array_almost_equal(
ins.get_position().extents, [.9-.8*.4, .9-.8*.3, 0.9, 0.9])
ins = inset_axes(ax, width=1., height=1.2, bbox_to_anchor=(200, 100),
loc=3, borderpad=0)
fig.canvas.draw()
assert_array_almost_equal(
ins.get_position().extents,
[200/dpi/figsize[0], 100/dpi/figsize[1],
(200/dpi+1)/figsize[0], (100/dpi+1.2)/figsize[1]])
ins1 = inset_axes(ax, width="35%", height="60%", loc=3, borderpad=1)
ins2 = inset_axes(ax, width="100%", height="100%",
bbox_to_anchor=(0, 0, .35, .60),
bbox_transform=ax.transAxes, loc=3, borderpad=1)
fig.canvas.draw()
assert_array_equal(ins1.get_position().extents,
ins2.get_position().extents)
with pytest.raises(ValueError):
ins = inset_axes(ax, width="40%", height="30%",
bbox_to_anchor=(0.4, 0.5))
with pytest.warns(UserWarning):
ins = inset_axes(ax, width="40%", height="30%",
bbox_transform=ax.transAxes)
def test_inset_axes_tight():
# gh-26287 found that inset_axes raised with bbox_inches=tight
fig, ax = plt.subplots()
inset_axes(ax, width=1.3, height=0.9)
f = io.BytesIO()
fig.savefig(f, bbox_inches="tight")
@image_comparison(['fill_facecolor.png'], remove_text=True, style='mpl20')
def test_fill_facecolor():
fig, ax = plt.subplots(1, 5)
fig.set_size_inches(5, 5)
for i in range(1, 4):
ax[i].yaxis.set_visible(False)
ax[4].yaxis.tick_right()
bbox = Bbox.from_extents(0, 0.4, 1, 0.6)
# fill with blue by setting 'fc' field
bbox1 = TransformedBbox(bbox, ax[0].transData)
bbox2 = TransformedBbox(bbox, ax[1].transData)
# set color to BboxConnectorPatch
p = BboxConnectorPatch(
bbox1, bbox2, loc1a=1, loc2a=2, loc1b=4, loc2b=3,
ec="r", fc="b")
p.set_clip_on(False)
ax[0].add_patch(p)
# set color to marked area
axins = zoomed_inset_axes(ax[0], 1, loc='upper right')
axins.set_xlim(0, 0.2)
axins.set_ylim(0, 0.2)
plt.gca().axes.xaxis.set_ticks([])
plt.gca().axes.yaxis.set_ticks([])
mark_inset(ax[0], axins, loc1=2, loc2=4, fc="b", ec="0.5")
# fill with yellow by setting 'facecolor' field
bbox3 = TransformedBbox(bbox, ax[1].transData)
bbox4 = TransformedBbox(bbox, ax[2].transData)
# set color to BboxConnectorPatch
p = BboxConnectorPatch(
bbox3, bbox4, loc1a=1, loc2a=2, loc1b=4, loc2b=3,
ec="r", facecolor="y")
p.set_clip_on(False)
ax[1].add_patch(p)
# set color to marked area
axins = zoomed_inset_axes(ax[1], 1, loc='upper right')
axins.set_xlim(0, 0.2)
axins.set_ylim(0, 0.2)
plt.gca().axes.xaxis.set_ticks([])
plt.gca().axes.yaxis.set_ticks([])
mark_inset(ax[1], axins, loc1=2, loc2=4, facecolor="y", ec="0.5")
# fill with green by setting 'color' field
bbox5 = TransformedBbox(bbox, ax[2].transData)
bbox6 = TransformedBbox(bbox, ax[3].transData)
# set color to BboxConnectorPatch
p = BboxConnectorPatch(
bbox5, bbox6, loc1a=1, loc2a=2, loc1b=4, loc2b=3,
ec="r", color="g")
p.set_clip_on(False)
ax[2].add_patch(p)
# set color to marked area
axins = zoomed_inset_axes(ax[2], 1, loc='upper right')
axins.set_xlim(0, 0.2)
axins.set_ylim(0, 0.2)
plt.gca().axes.xaxis.set_ticks([])
plt.gca().axes.yaxis.set_ticks([])
mark_inset(ax[2], axins, loc1=2, loc2=4, color="g", ec="0.5")
# fill with green but color won't show if set fill to False
bbox7 = TransformedBbox(bbox, ax[3].transData)
bbox8 = TransformedBbox(bbox, ax[4].transData)
# BboxConnectorPatch won't show green
p = BboxConnectorPatch(
bbox7, bbox8, loc1a=1, loc2a=2, loc1b=4, loc2b=3,
ec="r", fc="g", fill=False)
p.set_clip_on(False)
ax[3].add_patch(p)
# marked area won't show green
axins = zoomed_inset_axes(ax[3], 1, loc='upper right')
axins.set_xlim(0, 0.2)
axins.set_ylim(0, 0.2)
axins.xaxis.set_ticks([])
axins.yaxis.set_ticks([])
mark_inset(ax[3], axins, loc1=2, loc2=4, fc="g", ec="0.5", fill=False)
# Update style when regenerating the test image
@image_comparison(['zoomed_axes.png', 'inverted_zoomed_axes.png'],
style=('classic', '_classic_test_patch'),
tol=0.02 if platform.machine() == 'arm64' else 0)
def test_zooming_with_inverted_axes():
fig, ax = plt.subplots()
ax.plot([1, 2, 3], [1, 2, 3])
ax.axis([1, 3, 1, 3])
inset_ax = zoomed_inset_axes(ax, zoom=2.5, loc='lower right')
inset_ax.axis([1.1, 1.4, 1.1, 1.4])
fig, ax = plt.subplots()
ax.plot([1, 2, 3], [1, 2, 3])
ax.axis([3, 1, 3, 1])
inset_ax = zoomed_inset_axes(ax, zoom=2.5, loc='lower right')
inset_ax.axis([1.4, 1.1, 1.4, 1.1])
# Update style when regenerating the test image
@image_comparison(['anchored_direction_arrows.png'],
tol=0 if platform.machine() == 'x86_64' else 0.01,
style=('classic', '_classic_test_patch'))
def test_anchored_direction_arrows():
fig, ax = plt.subplots()
ax.imshow(np.zeros((10, 10)), interpolation='nearest')
simple_arrow = AnchoredDirectionArrows(ax.transAxes, 'X', 'Y')
ax.add_artist(simple_arrow)
# Update style when regenerating the test image
@image_comparison(['anchored_direction_arrows_many_args.png'],
style=('classic', '_classic_test_patch'))
def test_anchored_direction_arrows_many_args():
fig, ax = plt.subplots()
ax.imshow(np.ones((10, 10)))
direction_arrows = AnchoredDirectionArrows(
ax.transAxes, 'A', 'B', loc='upper right', color='red',
aspect_ratio=-0.5, pad=0.6, borderpad=2, frameon=True, alpha=0.7,
sep_x=-0.06, sep_y=-0.08, back_length=0.1, head_width=9,
head_length=10, tail_width=5)
ax.add_artist(direction_arrows)
def test_axes_locatable_position():
fig, ax = plt.subplots()
divider = make_axes_locatable(ax)
with mpl.rc_context({"figure.subplot.wspace": 0.02}):
cax = divider.append_axes('right', size='5%')
fig.canvas.draw()
assert np.isclose(cax.get_position(original=False).width,
0.03621495327102808)
@image_comparison(['image_grid_each_left_label_mode_all.png'], style='mpl20',
savefig_kwarg={'bbox_inches': 'tight'})
def test_image_grid_each_left_label_mode_all():
imdata = np.arange(100).reshape((10, 10))
fig = plt.figure(1, (3, 3))
grid = ImageGrid(fig, (1, 1, 1), nrows_ncols=(3, 2), axes_pad=(0.5, 0.3),
cbar_mode="each", cbar_location="left", cbar_size="15%",
label_mode="all")
# 3-tuple rect => SubplotDivider
assert isinstance(grid.get_divider(), SubplotDivider)
assert grid.get_axes_pad() == (0.5, 0.3)
assert grid.get_aspect() # True by default for ImageGrid
for ax, cax in zip(grid, grid.cbar_axes):
im = ax.imshow(imdata, interpolation='none')
cax.colorbar(im)
@image_comparison(['image_grid_single_bottom_label_mode_1.png'], style='mpl20',
savefig_kwarg={'bbox_inches': 'tight'})
def test_image_grid_single_bottom():
imdata = np.arange(100).reshape((10, 10))
fig = plt.figure(1, (2.5, 1.5))
grid = ImageGrid(fig, (0, 0, 1, 1), nrows_ncols=(1, 3),
axes_pad=(0.2, 0.15), cbar_mode="single",
cbar_location="bottom", cbar_size="10%", label_mode="1")
# 4-tuple rect => Divider, isinstance will give True for SubplotDivider
assert type(grid.get_divider()) is Divider
for i in range(3):
im = grid[i].imshow(imdata, interpolation='none')
grid.cbar_axes[0].colorbar(im)
def test_image_grid_label_mode_invalid():
fig = plt.figure()
with pytest.raises(ValueError, match="'foo' is not a valid value for mode"):
ImageGrid(fig, (0, 0, 1, 1), (2, 1), label_mode="foo")
@image_comparison(['image_grid.png'],
remove_text=True, style='mpl20',
savefig_kwarg={'bbox_inches': 'tight'})
def test_image_grid():
# test that image grid works with bbox_inches=tight.
im = np.arange(100).reshape((10, 10))
fig = plt.figure(1, (4, 4))
grid = ImageGrid(fig, 111, nrows_ncols=(2, 2), axes_pad=0.1)
assert grid.get_axes_pad() == (0.1, 0.1)
for i in range(4):
grid[i].imshow(im, interpolation='nearest')
def test_gettightbbox():
fig, ax = plt.subplots(figsize=(8, 6))
l, = ax.plot([1, 2, 3], [0, 1, 0])
ax_zoom = zoomed_inset_axes(ax, 4)
ax_zoom.plot([1, 2, 3], [0, 1, 0])
mark_inset(ax, ax_zoom, loc1=1, loc2=3, fc="none", ec='0.3')
remove_ticks_and_titles(fig)
bbox = fig.get_tightbbox(fig.canvas.get_renderer())
np.testing.assert_array_almost_equal(bbox.extents,
[-17.7, -13.9, 7.2, 5.4])
@pytest.mark.parametrize("click_on", ["big", "small"])
@pytest.mark.parametrize("big_on_axes,small_on_axes", [
("gca", "gca"),
("host", "host"),
("host", "parasite"),
("parasite", "host"),
("parasite", "parasite")
])
def test_picking_callbacks_overlap(big_on_axes, small_on_axes, click_on):
"""Test pick events on normal, host or parasite axes."""
# Two rectangles are drawn and "clicked on", a small one and a big one
# enclosing the small one. The axis on which they are drawn as well as the
# rectangle that is clicked on are varied.
# In each case we expect that both rectangles are picked if we click on the
# small one and only the big one is picked if we click on the big one.
# Also tests picking on normal axes ("gca") as a control.
big = plt.Rectangle((0.25, 0.25), 0.5, 0.5, picker=5)
small = plt.Rectangle((0.4, 0.4), 0.2, 0.2, facecolor="r", picker=5)
# Machinery for "receiving" events
received_events = []
def on_pick(event):
received_events.append(event)
plt.gcf().canvas.mpl_connect('pick_event', on_pick)
# Shortcut
rectangles_on_axes = (big_on_axes, small_on_axes)
# Axes setup
axes = {"gca": None, "host": None, "parasite": None}
if "gca" in rectangles_on_axes:
axes["gca"] = plt.gca()
if "host" in rectangles_on_axes or "parasite" in rectangles_on_axes:
axes["host"] = host_subplot(111)
axes["parasite"] = axes["host"].twin()
# Add rectangles to axes
axes[big_on_axes].add_patch(big)
axes[small_on_axes].add_patch(small)
# Simulate picking with click mouse event
if click_on == "big":
click_axes = axes[big_on_axes]
axes_coords = (0.3, 0.3)
else:
click_axes = axes[small_on_axes]
axes_coords = (0.5, 0.5)
# In reality mouse events never happen on parasite axes, only host axes
if click_axes is axes["parasite"]:
click_axes = axes["host"]
(x, y) = click_axes.transAxes.transform(axes_coords)
m = MouseEvent("button_press_event", click_axes.figure.canvas, x, y,
button=1)
click_axes.pick(m)
# Checks
expected_n_events = 2 if click_on == "small" else 1
assert len(received_events) == expected_n_events
event_rects = [event.artist for event in received_events]
assert big in event_rects
if click_on == "small":
assert small in event_rects
@image_comparison(['anchored_artists.png'], remove_text=True, style='mpl20')
def test_anchored_artists():
fig, ax = plt.subplots(figsize=(3, 3))
ada = AnchoredDrawingArea(40, 20, 0, 0, loc='upper right', pad=0.,
frameon=False)
p1 = Circle((10, 10), 10)
ada.drawing_area.add_artist(p1)
p2 = Circle((30, 10), 5, fc="r")
ada.drawing_area.add_artist(p2)
ax.add_artist(ada)
box = AnchoredAuxTransformBox(ax.transData, loc='upper left')
el = Ellipse((0, 0), width=0.1, height=0.4, angle=30, color='cyan')
box.drawing_area.add_artist(el)
ax.add_artist(box)
# Manually construct the ellipse instead, once the deprecation elapses.
with pytest.warns(mpl.MatplotlibDeprecationWarning):
ae = AnchoredEllipse(ax.transData, width=0.1, height=0.25, angle=-60,
loc='lower left', pad=0.5, borderpad=0.4,
frameon=True)
ax.add_artist(ae)
asb = AnchoredSizeBar(ax.transData, 0.2, r"0.2 units", loc='lower right',
pad=0.3, borderpad=0.4, sep=4, fill_bar=True,
frameon=False, label_top=True, prop={'size': 20},
size_vertical=0.05, color='green')
ax.add_artist(asb)
def test_hbox_divider():
arr1 = np.arange(20).reshape((4, 5))
arr2 = np.arange(20).reshape((5, 4))
fig, (ax1, ax2) = plt.subplots(1, 2)
ax1.imshow(arr1)
ax2.imshow(arr2)
pad = 0.5 # inches.
divider = HBoxDivider(
fig, 111, # Position of combined axes.
horizontal=[Size.AxesX(ax1), Size.Fixed(pad), Size.AxesX(ax2)],
vertical=[Size.AxesY(ax1), Size.Scaled(1), Size.AxesY(ax2)])
ax1.set_axes_locator(divider.new_locator(0))
ax2.set_axes_locator(divider.new_locator(2))
fig.canvas.draw()
p1 = ax1.get_position()
p2 = ax2.get_position()
assert p1.height == p2.height
assert p2.width / p1.width == pytest.approx((4 / 5) ** 2)
def test_vbox_divider():
arr1 = np.arange(20).reshape((4, 5))
arr2 = np.arange(20).reshape((5, 4))
fig, (ax1, ax2) = plt.subplots(1, 2)
ax1.imshow(arr1)
ax2.imshow(arr2)
pad = 0.5 # inches.
divider = VBoxDivider(
fig, 111, # Position of combined axes.
horizontal=[Size.AxesX(ax1), Size.Scaled(1), Size.AxesX(ax2)],
vertical=[Size.AxesY(ax1), Size.Fixed(pad), Size.AxesY(ax2)])
ax1.set_axes_locator(divider.new_locator(0))
ax2.set_axes_locator(divider.new_locator(2))
fig.canvas.draw()
p1 = ax1.get_position()
p2 = ax2.get_position()
assert p1.width == p2.width
assert p1.height / p2.height == pytest.approx((4 / 5) ** 2)
def test_axes_class_tuple():
fig = plt.figure()
axes_class = (mpl_toolkits.axes_grid1.mpl_axes.Axes, {})
gr = AxesGrid(fig, 111, nrows_ncols=(1, 1), axes_class=axes_class)
def test_grid_axes_lists():
"""Test Grid axes_all, axes_row and axes_column relationship."""
fig = plt.figure()
grid = Grid(fig, 111, (2, 3), direction="row")
assert_array_equal(grid, grid.axes_all)
assert_array_equal(grid.axes_row, np.transpose(grid.axes_column))
assert_array_equal(grid, np.ravel(grid.axes_row), "row")
assert grid.get_geometry() == (2, 3)
grid = Grid(fig, 111, (2, 3), direction="column")
assert_array_equal(grid, np.ravel(grid.axes_column), "column")
@pytest.mark.parametrize('direction', ('row', 'column'))
def test_grid_axes_position(direction):
"""Test positioning of the axes in Grid."""
fig = plt.figure()
grid = Grid(fig, 111, (2, 2), direction=direction)
loc = [ax.get_axes_locator() for ax in np.ravel(grid.axes_row)]
# Test nx.
assert loc[1].args[0] > loc[0].args[0]
assert loc[0].args[0] == loc[2].args[0]
assert loc[3].args[0] == loc[1].args[0]
# Test ny.
assert loc[2].args[1] < loc[0].args[1]
assert loc[0].args[1] == loc[1].args[1]
assert loc[3].args[1] == loc[2].args[1]
@pytest.mark.parametrize('rect, ngrids, error, message', (
((1, 1), None, TypeError, "Incorrect rect format"),
(111, -1, ValueError, "ngrids must be positive"),
(111, 7, ValueError, "ngrids must be positive"),
))
def test_grid_errors(rect, ngrids, error, message):
fig = plt.figure()
with pytest.raises(error, match=message):
Grid(fig, rect, (2, 3), ngrids=ngrids)
@pytest.mark.parametrize('anchor, error, message', (
(None, TypeError, "anchor must be str"),
("CC", ValueError, "'CC' is not a valid value for anchor"),
((1, 1, 1), TypeError, "anchor must be str"),
))
def test_divider_errors(anchor, error, message):
fig = plt.figure()
with pytest.raises(error, match=message):
Divider(fig, [0, 0, 1, 1], [Size.Fixed(1)], [Size.Fixed(1)],
anchor=anchor)
@check_figures_equal(extensions=["png"])
def test_mark_inset_unstales_viewlim(fig_test, fig_ref):
inset, full = fig_test.subplots(1, 2)
full.plot([0, 5], [0, 5])
inset.set(xlim=(1, 2), ylim=(1, 2))
# Check that mark_inset unstales full's viewLim before drawing the marks.
mark_inset(full, inset, 1, 4)
inset, full = fig_ref.subplots(1, 2)
full.plot([0, 5], [0, 5])
inset.set(xlim=(1, 2), ylim=(1, 2))
mark_inset(full, inset, 1, 4)
# Manually unstale the full's viewLim.
fig_ref.canvas.draw()
def test_auto_adjustable():
fig = plt.figure()
ax = fig.add_axes([0, 0, 1, 1])
pad = 0.1
make_axes_area_auto_adjustable(ax, pad=pad)
fig.canvas.draw()
tbb = ax.get_tightbbox()
assert tbb.x0 == pytest.approx(pad * fig.dpi)
assert tbb.x1 == pytest.approx(fig.bbox.width - pad * fig.dpi)
assert tbb.y0 == pytest.approx(pad * fig.dpi)
assert tbb.y1 == pytest.approx(fig.bbox.height - pad * fig.dpi)
# Update style when regenerating the test image
@image_comparison(['rgb_axes.png'], remove_text=True,
style=('classic', '_classic_test_patch'))
def test_rgb_axes():
fig = plt.figure()
ax = RGBAxes(fig, (0.1, 0.1, 0.8, 0.8), pad=0.1)
rng = np.random.default_rng(19680801)
r = rng.random((5, 5))
g = rng.random((5, 5))
b = rng.random((5, 5))
ax.imshow_rgb(r, g, b, interpolation='none')
# Update style when regenerating the test image
@image_comparison(['insetposition.png'], remove_text=True,
style=('classic', '_classic_test_patch'))
def test_insetposition():
fig, ax = plt.subplots(figsize=(2, 2))
ax_ins = plt.axes([0, 0, 1, 1])
with pytest.warns(mpl.MatplotlibDeprecationWarning):
ip = InsetPosition(ax, [0.2, 0.25, 0.5, 0.4])
ax_ins.set_axes_locator(ip)
# The original version of this test relied on mpl_toolkits's slightly different
# colorbar implementation; moving to matplotlib's own colorbar implementation
# caused the small image comparison error.
@image_comparison(['imagegrid_cbar_mode.png'],
remove_text=True, style='mpl20', tol=0.3)
def test_imagegrid_cbar_mode_edge():
arr = np.arange(16).reshape((4, 4))
fig = plt.figure(figsize=(18, 9))
positions = (241, 242, 243, 244, 245, 246, 247, 248)
directions = ['row']*4 + ['column']*4
cbar_locations = ['left', 'right', 'top', 'bottom']*2
for position, direction, location in zip(
positions, directions, cbar_locations):
grid = ImageGrid(fig, position,
nrows_ncols=(2, 2),
direction=direction,
cbar_location=location,
cbar_size='20%',
cbar_mode='edge')
ax1, ax2, ax3, ax4 = grid
ax1.imshow(arr, cmap='nipy_spectral')
ax2.imshow(arr.T, cmap='hot')
ax3.imshow(np.hypot(arr, arr.T), cmap='jet')
ax4.imshow(np.arctan2(arr, arr.T), cmap='hsv')
# In each row/column, the "first" colorbars must be overwritten by the
# "second" ones. To achieve this, clear out the axes first.
for ax in grid:
ax.cax.cla()
cb = ax.cax.colorbar(ax.images[0])
def test_imagegrid():
fig = plt.figure()
grid = ImageGrid(fig, 111, nrows_ncols=(1, 1))
ax = grid[0]
im = ax.imshow([[1, 2]], norm=mpl.colors.LogNorm())
cb = ax.cax.colorbar(im)
assert isinstance(cb.locator, mticker.LogLocator)
def test_removal():
import matplotlib.pyplot as plt
import mpl_toolkits.axisartist as AA
fig = plt.figure()
ax = host_subplot(111, axes_class=AA.Axes, figure=fig)
col = ax.fill_between(range(5), 0, range(5))
fig.canvas.draw()
col.remove()
fig.canvas.draw()
@image_comparison(['anchored_locator_base_call.png'], style="mpl20")
def test_anchored_locator_base_call():
fig = plt.figure(figsize=(3, 3))
fig1, fig2 = fig.subfigures(nrows=2, ncols=1)
ax = fig1.subplots()
ax.set(aspect=1, xlim=(-15, 15), ylim=(-20, 5))
ax.set(xticks=[], yticks=[])
Z = cbook.get_sample_data("axes_grid/bivariate_normal.npy")
extent = (-3, 4, -4, 3)
axins = zoomed_inset_axes(ax, zoom=2, loc="upper left")
axins.set(xticks=[], yticks=[])
axins.imshow(Z, extent=extent, origin="lower")
def test_grid_with_axes_class_not_overriding_axis():
Grid(plt.figure(), 111, (2, 2), axes_class=mpl.axes.Axes)
RGBAxes(plt.figure(), 111, axes_class=mpl.axes.Axes)