diff options
author | shumkovnd <shumkovnd@yandex-team.com> | 2023-11-10 14:39:34 +0300 |
---|---|---|
committer | shumkovnd <shumkovnd@yandex-team.com> | 2023-11-10 16:42:24 +0300 |
commit | 77eb2d3fdcec5c978c64e025ced2764c57c00285 (patch) | |
tree | c51edb0748ca8d4a08d7c7323312c27ba1a8b79a /contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1 | |
parent | dd6d20cadb65582270ac23f4b3b14ae189704b9d (diff) | |
download | ydb-77eb2d3fdcec5c978c64e025ced2764c57c00285.tar.gz |
KIKIMR-19287: add task_stats_drawing script
Diffstat (limited to 'contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1')
9 files changed, 3067 insertions, 0 deletions
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/__init__.py b/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/__init__.py new file mode 100644 index 0000000000..c55302485e --- /dev/null +++ b/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/__init__.py @@ -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"] diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/anchored_artists.py b/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/anchored_artists.py new file mode 100644 index 0000000000..1238310b46 --- /dev/null +++ b/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/anchored_artists.py @@ -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) diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_divider.py b/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_divider.py new file mode 100644 index 0000000000..f6c38f35db --- /dev/null +++ b/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_divider.py @@ -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) diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_grid.py b/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_grid.py new file mode 100644 index 0000000000..720d985414 --- /dev/null +++ b/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_grid.py @@ -0,0 +1,550 @@ +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. + """ + + _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: None + 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. + """ + 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: + # Use _api.check_in_list at the top of the method when deprecation + # period expires + if mode != 'keep': + _api.warn_deprecated( + '3.7', name="Grid label_mode", + message='Passing an undefined label_mode is deprecated ' + 'since %(since)s and will become an error ' + '%(removal)s. To silence this warning, pass ' + '"keep", which gives the same behaviour.') + 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 diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_rgb.py b/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_rgb.py new file mode 100644 index 0000000000..52fd707e87 --- /dev/null +++ b/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_rgb.py @@ -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 diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_size.py b/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_size.py new file mode 100644 index 0000000000..d251472077 --- /dev/null +++ b/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_size.py @@ -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): + self._get_size = _api.check_getitem( + self._get_size_map, direction=direction) + self._ax_list = [ax] if isinstance(ax, Axes) else ax + + def get_size(self, renderer): + sz = max([ + self._get_size(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 diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/inset_locator.py b/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/inset_locator.py new file mode 100644 index 0000000000..6d591a4531 --- /dev/null +++ b/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/inset_locator.py @@ -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 diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/mpl_axes.py b/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/mpl_axes.py new file mode 100644 index 0000000000..51c8748758 --- /dev/null +++ b/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/mpl_axes.py @@ -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) diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/parasite_axes.py b/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/parasite_axes.py new file mode 100644 index 0000000000..2a2b5957e8 --- /dev/null +++ b/contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/parasite_axes.py @@ -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 |