aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/matplotlib/py3/mpl_toolkits
diff options
context:
space:
mode:
authorshumkovnd <shumkovnd@yandex-team.com>2023-11-10 14:39:34 +0300
committershumkovnd <shumkovnd@yandex-team.com>2023-11-10 16:42:24 +0300
commit77eb2d3fdcec5c978c64e025ced2764c57c00285 (patch)
treec51edb0748ca8d4a08d7c7323312c27ba1a8b79a /contrib/python/matplotlib/py3/mpl_toolkits
parentdd6d20cadb65582270ac23f4b3b14ae189704b9d (diff)
downloadydb-77eb2d3fdcec5c978c64e025ced2764c57c00285.tar.gz
KIKIMR-19287: add task_stats_drawing script
Diffstat (limited to 'contrib/python/matplotlib/py3/mpl_toolkits')
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/__init__.py10
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/anchored_artists.py462
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_divider.py694
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_grid.py550
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_rgb.py157
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/axes_size.py248
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/inset_locator.py561
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/mpl_axes.py128
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axes_grid1/parasite_axes.py257
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/__init__.py13
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/angle_helper.py394
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_divider.py2
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_grid.py23
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_rgb.py18
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axis_artist.py1115
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axisline_style.py193
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axislines.py531
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/floating_axes.py298
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/grid_finder.py335
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/grid_helper_curvelinear.py336
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/axisartist/parasite_axes.py7
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/__init__.py3
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/art3d.py1252
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/axes3d.py3448
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/axis3d.py753
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/proj3d.py259
26 files changed, 12047 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
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/__init__.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/__init__.py
new file mode 100644
index 0000000000..47242cf7f0
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/__init__.py
@@ -0,0 +1,13 @@
+from .axislines import (
+ Axes, AxesZero, AxisArtistHelper, AxisArtistHelperRectlinear,
+ GridHelperBase, GridHelperRectlinear, Subplot, SubplotZero)
+from .axis_artist import AxisArtist, GridlinesCollection
+from .grid_helper_curvelinear import GridHelperCurveLinear
+from .floating_axes import FloatingAxes, FloatingSubplot
+from mpl_toolkits.axes_grid1.parasite_axes import (
+ host_axes_class_factory, parasite_axes_class_factory)
+
+
+ParasiteAxes = parasite_axes_class_factory(Axes)
+HostAxes = host_axes_class_factory(Axes)
+SubplotHost = HostAxes
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/angle_helper.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/angle_helper.py
new file mode 100644
index 0000000000..1786cd70bc
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/angle_helper.py
@@ -0,0 +1,394 @@
+import numpy as np
+import math
+
+from mpl_toolkits.axisartist.grid_finder import ExtremeFinderSimple
+
+
+def select_step_degree(dv):
+
+ degree_limits_ = [1.5, 3, 7, 13, 20, 40, 70, 120, 270, 520]
+ degree_steps_ = [1, 2, 5, 10, 15, 30, 45, 90, 180, 360]
+ degree_factors = [1.] * len(degree_steps_)
+
+ minsec_limits_ = [1.5, 2.5, 3.5, 8, 11, 18, 25, 45]
+ minsec_steps_ = [1, 2, 3, 5, 10, 15, 20, 30]
+
+ minute_limits_ = np.array(minsec_limits_) / 60
+ minute_factors = [60.] * len(minute_limits_)
+
+ second_limits_ = np.array(minsec_limits_) / 3600
+ second_factors = [3600.] * len(second_limits_)
+
+ degree_limits = [*second_limits_, *minute_limits_, *degree_limits_]
+ degree_steps = [*minsec_steps_, *minsec_steps_, *degree_steps_]
+ degree_factors = [*second_factors, *minute_factors, *degree_factors]
+
+ n = np.searchsorted(degree_limits, dv)
+ step = degree_steps[n]
+ factor = degree_factors[n]
+
+ return step, factor
+
+
+def select_step_hour(dv):
+
+ hour_limits_ = [1.5, 2.5, 3.5, 5, 7, 10, 15, 21, 36]
+ hour_steps_ = [1, 2, 3, 4, 6, 8, 12, 18, 24]
+ hour_factors = [1.] * len(hour_steps_)
+
+ minsec_limits_ = [1.5, 2.5, 3.5, 4.5, 5.5, 8, 11, 14, 18, 25, 45]
+ minsec_steps_ = [1, 2, 3, 4, 5, 6, 10, 12, 15, 20, 30]
+
+ minute_limits_ = np.array(minsec_limits_) / 60
+ minute_factors = [60.] * len(minute_limits_)
+
+ second_limits_ = np.array(minsec_limits_) / 3600
+ second_factors = [3600.] * len(second_limits_)
+
+ hour_limits = [*second_limits_, *minute_limits_, *hour_limits_]
+ hour_steps = [*minsec_steps_, *minsec_steps_, *hour_steps_]
+ hour_factors = [*second_factors, *minute_factors, *hour_factors]
+
+ n = np.searchsorted(hour_limits, dv)
+ step = hour_steps[n]
+ factor = hour_factors[n]
+
+ return step, factor
+
+
+def select_step_sub(dv):
+
+ # subarcsec or degree
+ tmp = 10.**(int(math.log10(dv))-1.)
+
+ factor = 1./tmp
+
+ if 1.5*tmp >= dv:
+ step = 1
+ elif 3.*tmp >= dv:
+ step = 2
+ elif 7.*tmp >= dv:
+ step = 5
+ else:
+ step = 1
+ factor = 0.1*factor
+
+ return step, factor
+
+
+def select_step(v1, v2, nv, hour=False, include_last=True,
+ threshold_factor=3600.):
+
+ if v1 > v2:
+ v1, v2 = v2, v1
+
+ dv = (v2 - v1) / nv
+
+ if hour:
+ _select_step = select_step_hour
+ cycle = 24.
+ else:
+ _select_step = select_step_degree
+ cycle = 360.
+
+ # for degree
+ if dv > 1 / threshold_factor:
+ step, factor = _select_step(dv)
+ else:
+ step, factor = select_step_sub(dv*threshold_factor)
+
+ factor = factor * threshold_factor
+
+ levs = np.arange(np.floor(v1 * factor / step),
+ np.ceil(v2 * factor / step) + 0.5,
+ dtype=int) * step
+
+ # n : number of valid levels. If there is a cycle, e.g., [0, 90, 180,
+ # 270, 360], the grid line needs to be extended from 0 to 360, so
+ # we need to return the whole array. However, the last level (360)
+ # needs to be ignored often. In this case, so we return n=4.
+
+ n = len(levs)
+
+ # we need to check the range of values
+ # for example, -90 to 90, 0 to 360,
+
+ if factor == 1. and levs[-1] >= levs[0] + cycle: # check for cycle
+ nv = int(cycle / step)
+ if include_last:
+ levs = levs[0] + np.arange(0, nv+1, 1) * step
+ else:
+ levs = levs[0] + np.arange(0, nv, 1) * step
+
+ n = len(levs)
+
+ return np.array(levs), n, factor
+
+
+def select_step24(v1, v2, nv, include_last=True, threshold_factor=3600):
+ v1, v2 = v1 / 15, v2 / 15
+ levs, n, factor = select_step(v1, v2, nv, hour=True,
+ include_last=include_last,
+ threshold_factor=threshold_factor)
+ return levs * 15, n, factor
+
+
+def select_step360(v1, v2, nv, include_last=True, threshold_factor=3600):
+ return select_step(v1, v2, nv, hour=False,
+ include_last=include_last,
+ threshold_factor=threshold_factor)
+
+
+class LocatorBase:
+ def __init__(self, nbins, include_last=True):
+ self.nbins = nbins
+ self._include_last = include_last
+
+ def set_params(self, nbins=None):
+ if nbins is not None:
+ self.nbins = int(nbins)
+
+
+class LocatorHMS(LocatorBase):
+ def __call__(self, v1, v2):
+ return select_step24(v1, v2, self.nbins, self._include_last)
+
+
+class LocatorHM(LocatorBase):
+ def __call__(self, v1, v2):
+ return select_step24(v1, v2, self.nbins, self._include_last,
+ threshold_factor=60)
+
+
+class LocatorH(LocatorBase):
+ def __call__(self, v1, v2):
+ return select_step24(v1, v2, self.nbins, self._include_last,
+ threshold_factor=1)
+
+
+class LocatorDMS(LocatorBase):
+ def __call__(self, v1, v2):
+ return select_step360(v1, v2, self.nbins, self._include_last)
+
+
+class LocatorDM(LocatorBase):
+ def __call__(self, v1, v2):
+ return select_step360(v1, v2, self.nbins, self._include_last,
+ threshold_factor=60)
+
+
+class LocatorD(LocatorBase):
+ def __call__(self, v1, v2):
+ return select_step360(v1, v2, self.nbins, self._include_last,
+ threshold_factor=1)
+
+
+class FormatterDMS:
+ deg_mark = r"^{\circ}"
+ min_mark = r"^{\prime}"
+ sec_mark = r"^{\prime\prime}"
+
+ fmt_d = "$%d" + deg_mark + "$"
+ fmt_ds = r"$%d.%s" + deg_mark + "$"
+
+ # %s for sign
+ fmt_d_m = r"$%s%d" + deg_mark + r"\,%02d" + min_mark + "$"
+ fmt_d_ms = r"$%s%d" + deg_mark + r"\,%02d.%s" + min_mark + "$"
+
+ fmt_d_m_partial = "$%s%d" + deg_mark + r"\,%02d" + min_mark + r"\,"
+ fmt_s_partial = "%02d" + sec_mark + "$"
+ fmt_ss_partial = "%02d.%s" + sec_mark + "$"
+
+ def _get_number_fraction(self, factor):
+ ## check for fractional numbers
+ number_fraction = None
+ # check for 60
+
+ for threshold in [1, 60, 3600]:
+ if factor <= threshold:
+ break
+
+ d = factor // threshold
+ int_log_d = int(np.floor(np.log10(d)))
+ if 10**int_log_d == d and d != 1:
+ number_fraction = int_log_d
+ factor = factor // 10**int_log_d
+ return factor, number_fraction
+
+ return factor, number_fraction
+
+ def __call__(self, direction, factor, values):
+ if len(values) == 0:
+ return []
+
+ ss = np.sign(values)
+ signs = ["-" if v < 0 else "" for v in values]
+
+ factor, number_fraction = self._get_number_fraction(factor)
+
+ values = np.abs(values)
+
+ if number_fraction is not None:
+ values, frac_part = divmod(values, 10 ** number_fraction)
+ frac_fmt = "%%0%dd" % (number_fraction,)
+ frac_str = [frac_fmt % (f1,) for f1 in frac_part]
+
+ if factor == 1:
+ if number_fraction is None:
+ return [self.fmt_d % (s * int(v),) for s, v in zip(ss, values)]
+ else:
+ return [self.fmt_ds % (s * int(v), f1)
+ for s, v, f1 in zip(ss, values, frac_str)]
+ elif factor == 60:
+ deg_part, min_part = divmod(values, 60)
+ if number_fraction is None:
+ return [self.fmt_d_m % (s1, d1, m1)
+ for s1, d1, m1 in zip(signs, deg_part, min_part)]
+ else:
+ return [self.fmt_d_ms % (s, d1, m1, f1)
+ for s, d1, m1, f1
+ in zip(signs, deg_part, min_part, frac_str)]
+
+ elif factor == 3600:
+ if ss[-1] == -1:
+ inverse_order = True
+ values = values[::-1]
+ signs = signs[::-1]
+ else:
+ inverse_order = False
+
+ l_hm_old = ""
+ r = []
+
+ deg_part, min_part_ = divmod(values, 3600)
+ min_part, sec_part = divmod(min_part_, 60)
+
+ if number_fraction is None:
+ sec_str = [self.fmt_s_partial % (s1,) for s1 in sec_part]
+ else:
+ sec_str = [self.fmt_ss_partial % (s1, f1)
+ for s1, f1 in zip(sec_part, frac_str)]
+
+ for s, d1, m1, s1 in zip(signs, deg_part, min_part, sec_str):
+ l_hm = self.fmt_d_m_partial % (s, d1, m1)
+ if l_hm != l_hm_old:
+ l_hm_old = l_hm
+ l = l_hm + s1
+ else:
+ l = "$" + s + s1
+ r.append(l)
+
+ if inverse_order:
+ return r[::-1]
+ else:
+ return r
+
+ else: # factor > 3600.
+ return [r"$%s^{\circ}$" % v for v in ss*values]
+
+
+class FormatterHMS(FormatterDMS):
+ deg_mark = r"^\mathrm{h}"
+ min_mark = r"^\mathrm{m}"
+ sec_mark = r"^\mathrm{s}"
+
+ fmt_d = "$%d" + deg_mark + "$"
+ fmt_ds = r"$%d.%s" + deg_mark + "$"
+
+ # %s for sign
+ fmt_d_m = r"$%s%d" + deg_mark + r"\,%02d" + min_mark+"$"
+ fmt_d_ms = r"$%s%d" + deg_mark + r"\,%02d.%s" + min_mark+"$"
+
+ fmt_d_m_partial = "$%s%d" + deg_mark + r"\,%02d" + min_mark + r"\,"
+ fmt_s_partial = "%02d" + sec_mark + "$"
+ fmt_ss_partial = "%02d.%s" + sec_mark + "$"
+
+ def __call__(self, direction, factor, values): # hour
+ return super().__call__(direction, factor, np.asarray(values) / 15)
+
+
+class ExtremeFinderCycle(ExtremeFinderSimple):
+ # docstring inherited
+
+ def __init__(self, nx, ny,
+ lon_cycle=360., lat_cycle=None,
+ lon_minmax=None, lat_minmax=(-90, 90)):
+ """
+ This subclass handles the case where one or both coordinates should be
+ taken modulo 360, or be restricted to not exceed a specific range.
+
+ Parameters
+ ----------
+ nx, ny : int
+ The number of samples in each direction.
+
+ lon_cycle, lat_cycle : 360 or None
+ If not None, values in the corresponding direction are taken modulo
+ *lon_cycle* or *lat_cycle*; in theory this can be any number but
+ the implementation actually assumes that it is 360 (if not None);
+ other values give nonsensical results.
+
+ This is done by "unwrapping" the transformed grid coordinates so
+ that jumps are less than a half-cycle; then normalizing the span to
+ no more than a full cycle.
+
+ For example, if values are in the union of the [0, 2] and
+ [358, 360] intervals (typically, angles measured modulo 360), the
+ values in the second interval are normalized to [-2, 0] instead so
+ that the values now cover [-2, 2]. If values are in a range of
+ [5, 1000], this gets normalized to [5, 365].
+
+ lon_minmax, lat_minmax : (float, float) or None
+ If not None, the computed bounding box is clipped to the given
+ range in the corresponding direction.
+ """
+ self.nx, self.ny = nx, ny
+ self.lon_cycle, self.lat_cycle = lon_cycle, lat_cycle
+ self.lon_minmax = lon_minmax
+ self.lat_minmax = lat_minmax
+
+ def __call__(self, transform_xy, x1, y1, x2, y2):
+ # docstring inherited
+ x, y = np.meshgrid(
+ np.linspace(x1, x2, self.nx), np.linspace(y1, y2, self.ny))
+ lon, lat = transform_xy(np.ravel(x), np.ravel(y))
+
+ # iron out jumps, but algorithm should be improved.
+ # This is just naive way of doing and my fail for some cases.
+ # Consider replacing this with numpy.unwrap
+ # We are ignoring invalid warnings. They are triggered when
+ # comparing arrays with NaNs using > We are already handling
+ # that correctly using np.nanmin and np.nanmax
+ with np.errstate(invalid='ignore'):
+ if self.lon_cycle is not None:
+ lon0 = np.nanmin(lon)
+ lon -= 360. * ((lon - lon0) > 180.)
+ if self.lat_cycle is not None:
+ lat0 = np.nanmin(lat)
+ lat -= 360. * ((lat - lat0) > 180.)
+
+ lon_min, lon_max = np.nanmin(lon), np.nanmax(lon)
+ lat_min, lat_max = np.nanmin(lat), np.nanmax(lat)
+
+ lon_min, lon_max, lat_min, lat_max = \
+ self._add_pad(lon_min, lon_max, lat_min, lat_max)
+
+ # check cycle
+ if self.lon_cycle:
+ lon_max = min(lon_max, lon_min + self.lon_cycle)
+ if self.lat_cycle:
+ lat_max = min(lat_max, lat_min + self.lat_cycle)
+
+ if self.lon_minmax is not None:
+ min0 = self.lon_minmax[0]
+ lon_min = max(min0, lon_min)
+ max0 = self.lon_minmax[1]
+ lon_max = min(max0, lon_max)
+
+ if self.lat_minmax is not None:
+ min0 = self.lat_minmax[0]
+ lat_min = max(min0, lat_min)
+ max0 = self.lat_minmax[1]
+ lat_max = min(max0, lat_max)
+
+ return lon_min, lon_max, lat_min, lat_max
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_divider.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_divider.py
new file mode 100644
index 0000000000..a01d4e27df
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_divider.py
@@ -0,0 +1,2 @@
+from mpl_toolkits.axes_grid1.axes_divider import ( # noqa
+ Divider, AxesLocator, SubplotDivider, AxesDivider, make_axes_locatable)
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_grid.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_grid.py
new file mode 100644
index 0000000000..ecb3e9d92c
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_grid.py
@@ -0,0 +1,23 @@
+from matplotlib import _api
+
+import mpl_toolkits.axes_grid1.axes_grid as axes_grid_orig
+from .axislines import Axes
+
+
+_api.warn_deprecated(
+ "3.8", name=__name__, obj_type="module", alternative="axes_grid1.axes_grid")
+
+
+@_api.deprecated("3.8", alternative=(
+ "axes_grid1.axes_grid.Grid(..., axes_class=axislines.Axes"))
+class Grid(axes_grid_orig.Grid):
+ _defaultAxesClass = Axes
+
+
+@_api.deprecated("3.8", alternative=(
+ "axes_grid1.axes_grid.ImageGrid(..., axes_class=axislines.Axes"))
+class ImageGrid(axes_grid_orig.ImageGrid):
+ _defaultAxesClass = Axes
+
+
+AxesGrid = ImageGrid
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_rgb.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_rgb.py
new file mode 100644
index 0000000000..2195747469
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axes_rgb.py
@@ -0,0 +1,18 @@
+from matplotlib import _api
+from mpl_toolkits.axes_grid1.axes_rgb import ( # noqa
+ make_rgb_axes, RGBAxes as _RGBAxes)
+from .axislines import Axes
+
+
+_api.warn_deprecated(
+ "3.8", name=__name__, obj_type="module", alternative="axes_grid1.axes_rgb")
+
+
+@_api.deprecated("3.8", alternative=(
+ "axes_grid1.axes_rgb.RGBAxes(..., axes_class=axislines.Axes"))
+class RGBAxes(_RGBAxes):
+ """
+ Subclass of `~.axes_grid1.axes_rgb.RGBAxes` with
+ ``_defaultAxesClass`` = `.axislines.Axes`.
+ """
+ _defaultAxesClass = Axes
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axis_artist.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axis_artist.py
new file mode 100644
index 0000000000..407ad07a3d
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axis_artist.py
@@ -0,0 +1,1115 @@
+"""
+The :mod:`.axis_artist` module implements custom artists to draw axis elements
+(axis lines and labels, tick lines and labels, grid lines).
+
+Axis lines and labels and tick lines and labels are managed by the `AxisArtist`
+class; grid lines are managed by the `GridlinesCollection` class.
+
+There is one `AxisArtist` per Axis; it can be accessed through
+the ``axis`` dictionary of the parent Axes (which should be a
+`mpl_toolkits.axislines.Axes`), e.g. ``ax.axis["bottom"]``.
+
+Children of the AxisArtist are accessed as attributes: ``.line`` and ``.label``
+for the axis line and label, ``.major_ticks``, ``.major_ticklabels``,
+``.minor_ticks``, ``.minor_ticklabels`` for the tick lines and labels (e.g.
+``ax.axis["bottom"].line``).
+
+Children properties (colors, fonts, line widths, etc.) can be set using
+setters, e.g. ::
+
+ # Make the major ticks of the bottom axis red.
+ ax.axis["bottom"].major_ticks.set_color("red")
+
+However, things like the locations of ticks, and their ticklabels need to be
+changed from the side of the grid_helper.
+
+axis_direction
+--------------
+
+`AxisArtist`, `AxisLabel`, `TickLabels` have an *axis_direction* attribute,
+which adjusts the location, angle, etc. The *axis_direction* must be one of
+"left", "right", "bottom", "top", and follows the Matplotlib convention for
+rectangular axis.
+
+For example, for the *bottom* axis (the left and right is relative to the
+direction of the increasing coordinate),
+
+* ticklabels and axislabel are on the right
+* ticklabels and axislabel have text angle of 0
+* ticklabels are baseline, center-aligned
+* axislabel is top, center-aligned
+
+The text angles are actually relative to (90 + angle of the direction to the
+ticklabel), which gives 0 for bottom axis.
+
+=================== ====== ======== ====== ========
+Property left bottom right top
+=================== ====== ======== ====== ========
+ticklabel location left right right left
+axislabel location left right right left
+ticklabel angle 90 0 -90 180
+axislabel angle 180 0 0 180
+ticklabel va center baseline center baseline
+axislabel va center top center bottom
+ticklabel ha right center right center
+axislabel ha right center right center
+=================== ====== ======== ====== ========
+
+Ticks are by default direct opposite side of the ticklabels. To make ticks to
+the same side of the ticklabels, ::
+
+ ax.axis["bottom"].major_ticks.set_tick_out(True)
+
+The following attributes can be customized (use the ``set_xxx`` methods):
+
+* `Ticks`: ticksize, tick_out
+* `TickLabels`: pad
+* `AxisLabel`: pad
+"""
+
+# FIXME :
+# angles are given in data coordinate - need to convert it to canvas coordinate
+
+
+from operator import methodcaller
+
+import numpy as np
+
+import matplotlib as mpl
+from matplotlib import _api, cbook
+import matplotlib.artist as martist
+import matplotlib.colors as mcolors
+import matplotlib.text as mtext
+from matplotlib.collections import LineCollection
+from matplotlib.lines import Line2D
+from matplotlib.patches import PathPatch
+from matplotlib.path import Path
+from matplotlib.transforms import (
+ Affine2D, Bbox, IdentityTransform, ScaledTranslation)
+
+from .axisline_style import AxislineStyle
+
+
+class AttributeCopier:
+ def get_ref_artist(self):
+ """
+ Return the underlying artist that actually defines some properties
+ (e.g., color) of this artist.
+ """
+ raise RuntimeError("get_ref_artist must overridden")
+
+ def get_attribute_from_ref_artist(self, attr_name):
+ getter = methodcaller("get_" + attr_name)
+ prop = getter(super())
+ return getter(self.get_ref_artist()) if prop == "auto" else prop
+
+
+class Ticks(AttributeCopier, Line2D):
+ """
+ Ticks are derived from `.Line2D`, and note that ticks themselves
+ are markers. Thus, you should use set_mec, set_mew, etc.
+
+ To change the tick size (length), you need to use
+ `set_ticksize`. To change the direction of the ticks (ticks are
+ in opposite direction of ticklabels by default), use
+ ``set_tick_out(False)``
+ """
+
+ def __init__(self, ticksize, tick_out=False, *, axis=None, **kwargs):
+ self._ticksize = ticksize
+ self.locs_angles_labels = []
+
+ self.set_tick_out(tick_out)
+
+ self._axis = axis
+ if self._axis is not None:
+ if "color" not in kwargs:
+ kwargs["color"] = "auto"
+ if "mew" not in kwargs and "markeredgewidth" not in kwargs:
+ kwargs["markeredgewidth"] = "auto"
+
+ Line2D.__init__(self, [0.], [0.], **kwargs)
+ self.set_snap(True)
+
+ def get_ref_artist(self):
+ # docstring inherited
+ return self._axis.majorTicks[0].tick1line
+
+ def set_color(self, color):
+ # docstring inherited
+ # Unlike the base Line2D.set_color, this also supports "auto".
+ if not cbook._str_equal(color, "auto"):
+ mcolors._check_color_like(color=color)
+ self._color = color
+ self.stale = True
+
+ def get_color(self):
+ return self.get_attribute_from_ref_artist("color")
+
+ def get_markeredgecolor(self):
+ return self.get_attribute_from_ref_artist("markeredgecolor")
+
+ def get_markeredgewidth(self):
+ return self.get_attribute_from_ref_artist("markeredgewidth")
+
+ def set_tick_out(self, b):
+ """Set whether ticks are drawn inside or outside the axes."""
+ self._tick_out = b
+
+ def get_tick_out(self):
+ """Return whether ticks are drawn inside or outside the axes."""
+ return self._tick_out
+
+ def set_ticksize(self, ticksize):
+ """Set length of the ticks in points."""
+ self._ticksize = ticksize
+
+ def get_ticksize(self):
+ """Return length of the ticks in points."""
+ return self._ticksize
+
+ def set_locs_angles(self, locs_angles):
+ self.locs_angles = locs_angles
+
+ _tickvert_path = Path([[0., 0.], [1., 0.]])
+
+ def draw(self, renderer):
+ if not self.get_visible():
+ return
+
+ gc = renderer.new_gc()
+ gc.set_foreground(self.get_markeredgecolor())
+ gc.set_linewidth(self.get_markeredgewidth())
+ gc.set_alpha(self._alpha)
+
+ path_trans = self.get_transform()
+ marker_transform = (Affine2D()
+ .scale(renderer.points_to_pixels(self._ticksize)))
+ if self.get_tick_out():
+ marker_transform.rotate_deg(180)
+
+ for loc, angle in self.locs_angles:
+ locs = path_trans.transform_non_affine(np.array([loc]))
+ if self.axes and not self.axes.viewLim.contains(*locs[0]):
+ continue
+ renderer.draw_markers(
+ gc, self._tickvert_path,
+ marker_transform + Affine2D().rotate_deg(angle),
+ Path(locs), path_trans.get_affine())
+
+ gc.restore()
+
+
+class LabelBase(mtext.Text):
+ """
+ A base class for `.AxisLabel` and `.TickLabels`. The position and
+ angle of the text are calculated by the offset_ref_angle,
+ text_ref_angle, and offset_radius attributes.
+ """
+
+ def __init__(self, *args, **kwargs):
+ self.locs_angles_labels = []
+ self._ref_angle = 0
+ self._offset_radius = 0.
+
+ super().__init__(*args, **kwargs)
+
+ self.set_rotation_mode("anchor")
+ self._text_follow_ref_angle = True
+
+ @property
+ def _text_ref_angle(self):
+ if self._text_follow_ref_angle:
+ return self._ref_angle + 90
+ else:
+ return 0
+
+ @property
+ def _offset_ref_angle(self):
+ return self._ref_angle
+
+ _get_opposite_direction = {"left": "right",
+ "right": "left",
+ "top": "bottom",
+ "bottom": "top"}.__getitem__
+
+ def draw(self, renderer):
+ if not self.get_visible():
+ return
+
+ # save original and adjust some properties
+ tr = self.get_transform()
+ angle_orig = self.get_rotation()
+ theta = np.deg2rad(self._offset_ref_angle)
+ dd = self._offset_radius
+ dx, dy = dd * np.cos(theta), dd * np.sin(theta)
+
+ self.set_transform(tr + Affine2D().translate(dx, dy))
+ self.set_rotation(self._text_ref_angle + angle_orig)
+ super().draw(renderer)
+ # restore original properties
+ self.set_transform(tr)
+ self.set_rotation(angle_orig)
+
+ def get_window_extent(self, renderer=None):
+ if renderer is None:
+ renderer = self.figure._get_renderer()
+
+ # save original and adjust some properties
+ tr = self.get_transform()
+ angle_orig = self.get_rotation()
+ theta = np.deg2rad(self._offset_ref_angle)
+ dd = self._offset_radius
+ dx, dy = dd * np.cos(theta), dd * np.sin(theta)
+
+ self.set_transform(tr + Affine2D().translate(dx, dy))
+ self.set_rotation(self._text_ref_angle + angle_orig)
+ bbox = super().get_window_extent(renderer).frozen()
+ # restore original properties
+ self.set_transform(tr)
+ self.set_rotation(angle_orig)
+
+ return bbox
+
+
+class AxisLabel(AttributeCopier, LabelBase):
+ """
+ Axis label. Derived from `.Text`. The position of the text is updated
+ in the fly, so changing text position has no effect. Otherwise, the
+ properties can be changed as a normal `.Text`.
+
+ To change the pad between tick labels and axis label, use `set_pad`.
+ """
+
+ def __init__(self, *args, axis_direction="bottom", axis=None, **kwargs):
+ self._axis = axis
+ self._pad = 5
+ self._external_pad = 0 # in pixels
+ LabelBase.__init__(self, *args, **kwargs)
+ self.set_axis_direction(axis_direction)
+
+ def set_pad(self, pad):
+ """
+ Set the internal pad in points.
+
+ The actual pad will be the sum of the internal pad and the
+ external pad (the latter is set automatically by the `.AxisArtist`).
+
+ Parameters
+ ----------
+ pad : float
+ The internal pad in points.
+ """
+ self._pad = pad
+
+ def get_pad(self):
+ """
+ Return the internal pad in points.
+
+ See `.set_pad` for more details.
+ """
+ return self._pad
+
+ def get_ref_artist(self):
+ # docstring inherited
+ return self._axis.get_label()
+
+ def get_text(self):
+ # docstring inherited
+ t = super().get_text()
+ if t == "__from_axes__":
+ return self._axis.get_label().get_text()
+ return self._text
+
+ _default_alignments = dict(left=("bottom", "center"),
+ right=("top", "center"),
+ bottom=("top", "center"),
+ top=("bottom", "center"))
+
+ def set_default_alignment(self, d):
+ """
+ Set the default alignment. See `set_axis_direction` for details.
+
+ Parameters
+ ----------
+ d : {"left", "bottom", "right", "top"}
+ """
+ va, ha = _api.check_getitem(self._default_alignments, d=d)
+ self.set_va(va)
+ self.set_ha(ha)
+
+ _default_angles = dict(left=180,
+ right=0,
+ bottom=0,
+ top=180)
+
+ def set_default_angle(self, d):
+ """
+ Set the default angle. See `set_axis_direction` for details.
+
+ Parameters
+ ----------
+ d : {"left", "bottom", "right", "top"}
+ """
+ self.set_rotation(_api.check_getitem(self._default_angles, d=d))
+
+ def set_axis_direction(self, d):
+ """
+ Adjust the text angle and text alignment of axis label
+ according to the matplotlib convention.
+
+ ===================== ========== ========= ========== ==========
+ Property left bottom right top
+ ===================== ========== ========= ========== ==========
+ axislabel angle 180 0 0 180
+ axislabel va center top center bottom
+ axislabel ha right center right center
+ ===================== ========== ========= ========== ==========
+
+ Note that the text angles are actually relative to (90 + angle
+ of the direction to the ticklabel), which gives 0 for bottom
+ axis.
+
+ Parameters
+ ----------
+ d : {"left", "bottom", "right", "top"}
+ """
+ self.set_default_alignment(d)
+ self.set_default_angle(d)
+
+ def get_color(self):
+ return self.get_attribute_from_ref_artist("color")
+
+ def draw(self, renderer):
+ if not self.get_visible():
+ return
+
+ self._offset_radius = \
+ self._external_pad + renderer.points_to_pixels(self.get_pad())
+
+ super().draw(renderer)
+
+ def get_window_extent(self, renderer=None):
+ if renderer is None:
+ renderer = self.figure._get_renderer()
+ if not self.get_visible():
+ return
+
+ r = self._external_pad + renderer.points_to_pixels(self.get_pad())
+ self._offset_radius = r
+
+ bb = super().get_window_extent(renderer)
+
+ return bb
+
+
+class TickLabels(AxisLabel): # mtext.Text
+ """
+ Tick labels. While derived from `.Text`, this single artist draws all
+ ticklabels. As in `.AxisLabel`, the position of the text is updated
+ in the fly, so changing text position has no effect. Otherwise,
+ the properties can be changed as a normal `.Text`. Unlike the
+ ticklabels of the mainline Matplotlib, properties of a single
+ ticklabel alone cannot be modified.
+
+ To change the pad between ticks and ticklabels, use `~.AxisLabel.set_pad`.
+ """
+
+ def __init__(self, *, axis_direction="bottom", **kwargs):
+ super().__init__(**kwargs)
+ self.set_axis_direction(axis_direction)
+ self._axislabel_pad = 0
+
+ def get_ref_artist(self):
+ # docstring inherited
+ return self._axis.get_ticklabels()[0]
+
+ def set_axis_direction(self, label_direction):
+ """
+ Adjust the text angle and text alignment of ticklabels
+ according to the Matplotlib convention.
+
+ The *label_direction* must be one of [left, right, bottom, top].
+
+ ===================== ========== ========= ========== ==========
+ Property left bottom right top
+ ===================== ========== ========= ========== ==========
+ ticklabel angle 90 0 -90 180
+ ticklabel va center baseline center baseline
+ ticklabel ha right center right center
+ ===================== ========== ========= ========== ==========
+
+ Note that the text angles are actually relative to (90 + angle
+ of the direction to the ticklabel), which gives 0 for bottom
+ axis.
+
+ Parameters
+ ----------
+ label_direction : {"left", "bottom", "right", "top"}
+
+ """
+ self.set_default_alignment(label_direction)
+ self.set_default_angle(label_direction)
+ self._axis_direction = label_direction
+
+ def invert_axis_direction(self):
+ label_direction = self._get_opposite_direction(self._axis_direction)
+ self.set_axis_direction(label_direction)
+
+ def _get_ticklabels_offsets(self, renderer, label_direction):
+ """
+ Calculate the ticklabel offsets from the tick and their total heights.
+
+ The offset only takes account the offset due to the vertical alignment
+ of the ticklabels: if axis direction is bottom and va is 'top', it will
+ return 0; if va is 'baseline', it will return (height-descent).
+ """
+ whd_list = self.get_texts_widths_heights_descents(renderer)
+
+ if not whd_list:
+ return 0, 0
+
+ r = 0
+ va, ha = self.get_va(), self.get_ha()
+
+ if label_direction == "left":
+ pad = max(w for w, h, d in whd_list)
+ if ha == "left":
+ r = pad
+ elif ha == "center":
+ r = .5 * pad
+ elif label_direction == "right":
+ pad = max(w for w, h, d in whd_list)
+ if ha == "right":
+ r = pad
+ elif ha == "center":
+ r = .5 * pad
+ elif label_direction == "bottom":
+ pad = max(h for w, h, d in whd_list)
+ if va == "bottom":
+ r = pad
+ elif va == "center":
+ r = .5 * pad
+ elif va == "baseline":
+ max_ascent = max(h - d for w, h, d in whd_list)
+ max_descent = max(d for w, h, d in whd_list)
+ r = max_ascent
+ pad = max_ascent + max_descent
+ elif label_direction == "top":
+ pad = max(h for w, h, d in whd_list)
+ if va == "top":
+ r = pad
+ elif va == "center":
+ r = .5 * pad
+ elif va == "baseline":
+ max_ascent = max(h - d for w, h, d in whd_list)
+ max_descent = max(d for w, h, d in whd_list)
+ r = max_descent
+ pad = max_ascent + max_descent
+
+ # r : offset
+ # pad : total height of the ticklabels. This will be used to
+ # calculate the pad for the axislabel.
+ return r, pad
+
+ _default_alignments = dict(left=("center", "right"),
+ right=("center", "left"),
+ bottom=("baseline", "center"),
+ top=("baseline", "center"))
+
+ _default_angles = dict(left=90,
+ right=-90,
+ bottom=0,
+ top=180)
+
+ def draw(self, renderer):
+ if not self.get_visible():
+ self._axislabel_pad = self._external_pad
+ return
+
+ r, total_width = self._get_ticklabels_offsets(renderer,
+ self._axis_direction)
+
+ pad = self._external_pad + renderer.points_to_pixels(self.get_pad())
+ self._offset_radius = r + pad
+
+ for (x, y), a, l in self._locs_angles_labels:
+ if not l.strip():
+ continue
+ self._ref_angle = a
+ self.set_x(x)
+ self.set_y(y)
+ self.set_text(l)
+ LabelBase.draw(self, renderer)
+
+ # the value saved will be used to draw axislabel.
+ self._axislabel_pad = total_width + pad
+
+ def set_locs_angles_labels(self, locs_angles_labels):
+ self._locs_angles_labels = locs_angles_labels
+
+ def get_window_extents(self, renderer=None):
+ if renderer is None:
+ renderer = self.figure._get_renderer()
+
+ if not self.get_visible():
+ self._axislabel_pad = self._external_pad
+ return []
+
+ bboxes = []
+
+ r, total_width = self._get_ticklabels_offsets(renderer,
+ self._axis_direction)
+
+ pad = self._external_pad + renderer.points_to_pixels(self.get_pad())
+ self._offset_radius = r + pad
+
+ for (x, y), a, l in self._locs_angles_labels:
+ self._ref_angle = a
+ self.set_x(x)
+ self.set_y(y)
+ self.set_text(l)
+ bb = LabelBase.get_window_extent(self, renderer)
+ bboxes.append(bb)
+
+ # the value saved will be used to draw axislabel.
+ self._axislabel_pad = total_width + pad
+
+ return bboxes
+
+ def get_texts_widths_heights_descents(self, renderer):
+ """
+ Return a list of ``(width, height, descent)`` tuples for ticklabels.
+
+ Empty labels are left out.
+ """
+ whd_list = []
+ for _loc, _angle, label in self._locs_angles_labels:
+ if not label.strip():
+ continue
+ clean_line, ismath = self._preprocess_math(label)
+ whd = renderer.get_text_width_height_descent(
+ clean_line, self._fontproperties, ismath=ismath)
+ whd_list.append(whd)
+ return whd_list
+
+
+class GridlinesCollection(LineCollection):
+ def __init__(self, *args, which="major", axis="both", **kwargs):
+ """
+ Collection of grid lines.
+
+ Parameters
+ ----------
+ which : {"major", "minor"}
+ Which grid to consider.
+ axis : {"both", "x", "y"}
+ Which axis to consider.
+ *args, **kwargs
+ Passed to `.LineCollection`.
+ """
+ self._which = which
+ self._axis = axis
+ super().__init__(*args, **kwargs)
+ self.set_grid_helper(None)
+
+ def set_which(self, which):
+ """
+ Select major or minor grid lines.
+
+ Parameters
+ ----------
+ which : {"major", "minor"}
+ """
+ self._which = which
+
+ def set_axis(self, axis):
+ """
+ Select axis.
+
+ Parameters
+ ----------
+ axis : {"both", "x", "y"}
+ """
+ self._axis = axis
+
+ def set_grid_helper(self, grid_helper):
+ """
+ Set grid helper.
+
+ Parameters
+ ----------
+ grid_helper : `.GridHelperBase` subclass
+ """
+ self._grid_helper = grid_helper
+
+ def draw(self, renderer):
+ if self._grid_helper is not None:
+ self._grid_helper.update_lim(self.axes)
+ gl = self._grid_helper.get_gridlines(self._which, self._axis)
+ self.set_segments([np.transpose(l) for l in gl])
+ super().draw(renderer)
+
+
+class AxisArtist(martist.Artist):
+ """
+ An artist which draws axis (a line along which the n-th axes coord
+ is constant) line, ticks, tick labels, and axis label.
+ """
+
+ zorder = 2.5
+
+ @property
+ def LABELPAD(self):
+ return self.label.get_pad()
+
+ @LABELPAD.setter
+ def LABELPAD(self, v):
+ self.label.set_pad(v)
+
+ def __init__(self, axes,
+ helper,
+ offset=None,
+ axis_direction="bottom",
+ **kwargs):
+ """
+ Parameters
+ ----------
+ axes : `mpl_toolkits.axisartist.axislines.Axes`
+ helper : `~mpl_toolkits.axisartist.axislines.AxisArtistHelper`
+ """
+ # axes is also used to follow the axis attribute (tick color, etc).
+
+ super().__init__(**kwargs)
+
+ self.axes = axes
+
+ self._axis_artist_helper = helper
+
+ if offset is None:
+ offset = (0, 0)
+ self.offset_transform = ScaledTranslation(
+ *offset,
+ Affine2D().scale(1 / 72) # points to inches.
+ + self.axes.figure.dpi_scale_trans)
+
+ if axis_direction in ["left", "right"]:
+ self.axis = axes.yaxis
+ else:
+ self.axis = axes.xaxis
+
+ self._axisline_style = None
+ self._axis_direction = axis_direction
+
+ self._init_line()
+ self._init_ticks(**kwargs)
+ self._init_offsetText(axis_direction)
+ self._init_label()
+
+ # axis direction
+ self._ticklabel_add_angle = 0.
+ self._axislabel_add_angle = 0.
+ self.set_axis_direction(axis_direction)
+
+ # axis direction
+
+ def set_axis_direction(self, axis_direction):
+ """
+ Adjust the direction, text angle, and text alignment of tick labels
+ and axis labels following the Matplotlib convention for the rectangle
+ axes.
+
+ The *axis_direction* must be one of [left, right, bottom, top].
+
+ ===================== ========== ========= ========== ==========
+ Property left bottom right top
+ ===================== ========== ========= ========== ==========
+ ticklabel direction "-" "+" "+" "-"
+ axislabel direction "-" "+" "+" "-"
+ ticklabel angle 90 0 -90 180
+ ticklabel va center baseline center baseline
+ ticklabel ha right center right center
+ axislabel angle 180 0 0 180
+ axislabel va center top center bottom
+ axislabel ha right center right center
+ ===================== ========== ========= ========== ==========
+
+ Note that the direction "+" and "-" are relative to the direction of
+ the increasing coordinate. Also, the text angles are actually
+ relative to (90 + angle of the direction to the ticklabel),
+ which gives 0 for bottom axis.
+
+ Parameters
+ ----------
+ axis_direction : {"left", "bottom", "right", "top"}
+ """
+ self.major_ticklabels.set_axis_direction(axis_direction)
+ self.label.set_axis_direction(axis_direction)
+ self._axis_direction = axis_direction
+ if axis_direction in ["left", "top"]:
+ self.set_ticklabel_direction("-")
+ self.set_axislabel_direction("-")
+ else:
+ self.set_ticklabel_direction("+")
+ self.set_axislabel_direction("+")
+
+ def set_ticklabel_direction(self, tick_direction):
+ r"""
+ Adjust the direction of the tick labels.
+
+ Note that the *tick_direction*\s '+' and '-' are relative to the
+ direction of the increasing coordinate.
+
+ Parameters
+ ----------
+ tick_direction : {"+", "-"}
+ """
+ self._ticklabel_add_angle = _api.check_getitem(
+ {"+": 0, "-": 180}, tick_direction=tick_direction)
+
+ def invert_ticklabel_direction(self):
+ self._ticklabel_add_angle = (self._ticklabel_add_angle + 180) % 360
+ self.major_ticklabels.invert_axis_direction()
+ self.minor_ticklabels.invert_axis_direction()
+
+ def set_axislabel_direction(self, label_direction):
+ r"""
+ Adjust the direction of the axis label.
+
+ Note that the *label_direction*\s '+' and '-' are relative to the
+ direction of the increasing coordinate.
+
+ Parameters
+ ----------
+ label_direction : {"+", "-"}
+ """
+ self._axislabel_add_angle = _api.check_getitem(
+ {"+": 0, "-": 180}, label_direction=label_direction)
+
+ def get_transform(self):
+ return self.axes.transAxes + self.offset_transform
+
+ def get_helper(self):
+ """
+ Return axis artist helper instance.
+ """
+ return self._axis_artist_helper
+
+ def set_axisline_style(self, axisline_style=None, **kwargs):
+ """
+ Set the axisline style.
+
+ The new style is completely defined by the passed attributes. Existing
+ style attributes are forgotten.
+
+ Parameters
+ ----------
+ axisline_style : str or None
+ The line style, e.g. '->', optionally followed by a comma-separated
+ list of attributes. Alternatively, the attributes can be provided
+ as keywords.
+
+ If *None* this returns a string containing the available styles.
+
+ Examples
+ --------
+ The following two commands are equal:
+
+ >>> set_axisline_style("->,size=1.5")
+ >>> set_axisline_style("->", size=1.5)
+ """
+ if axisline_style is None:
+ return AxislineStyle.pprint_styles()
+
+ if isinstance(axisline_style, AxislineStyle._Base):
+ self._axisline_style = axisline_style
+ else:
+ self._axisline_style = AxislineStyle(axisline_style, **kwargs)
+
+ self._init_line()
+
+ def get_axisline_style(self):
+ """Return the current axisline style."""
+ return self._axisline_style
+
+ def _init_line(self):
+ """
+ Initialize the *line* artist that is responsible to draw the axis line.
+ """
+ tran = (self._axis_artist_helper.get_line_transform(self.axes)
+ + self.offset_transform)
+
+ axisline_style = self.get_axisline_style()
+ if axisline_style is None:
+ self.line = PathPatch(
+ self._axis_artist_helper.get_line(self.axes),
+ color=mpl.rcParams['axes.edgecolor'],
+ fill=False,
+ linewidth=mpl.rcParams['axes.linewidth'],
+ capstyle=mpl.rcParams['lines.solid_capstyle'],
+ joinstyle=mpl.rcParams['lines.solid_joinstyle'],
+ transform=tran)
+ else:
+ self.line = axisline_style(self, transform=tran)
+
+ def _draw_line(self, renderer):
+ self.line.set_path(self._axis_artist_helper.get_line(self.axes))
+ if self.get_axisline_style() is not None:
+ self.line.set_line_mutation_scale(self.major_ticklabels.get_size())
+ self.line.draw(renderer)
+
+ def _init_ticks(self, **kwargs):
+ axis_name = self.axis.axis_name
+
+ trans = (self._axis_artist_helper.get_tick_transform(self.axes)
+ + self.offset_transform)
+
+ self.major_ticks = Ticks(
+ kwargs.get(
+ "major_tick_size",
+ mpl.rcParams[f"{axis_name}tick.major.size"]),
+ axis=self.axis, transform=trans)
+ self.minor_ticks = Ticks(
+ kwargs.get(
+ "minor_tick_size",
+ mpl.rcParams[f"{axis_name}tick.minor.size"]),
+ axis=self.axis, transform=trans)
+
+ size = mpl.rcParams[f"{axis_name}tick.labelsize"]
+ self.major_ticklabels = TickLabels(
+ axis=self.axis,
+ axis_direction=self._axis_direction,
+ figure=self.axes.figure,
+ transform=trans,
+ fontsize=size,
+ pad=kwargs.get(
+ "major_tick_pad", mpl.rcParams[f"{axis_name}tick.major.pad"]),
+ )
+ self.minor_ticklabels = TickLabels(
+ axis=self.axis,
+ axis_direction=self._axis_direction,
+ figure=self.axes.figure,
+ transform=trans,
+ fontsize=size,
+ pad=kwargs.get(
+ "minor_tick_pad", mpl.rcParams[f"{axis_name}tick.minor.pad"]),
+ )
+
+ def _get_tick_info(self, tick_iter):
+ """
+ Return a pair of:
+
+ - list of locs and angles for ticks
+ - list of locs, angles and labels for ticklabels.
+ """
+ ticks_loc_angle = []
+ ticklabels_loc_angle_label = []
+
+ ticklabel_add_angle = self._ticklabel_add_angle
+
+ for loc, angle_normal, angle_tangent, label in tick_iter:
+ angle_label = angle_tangent - 90 + ticklabel_add_angle
+ angle_tick = (angle_normal
+ if 90 <= (angle_label - angle_normal) % 360 <= 270
+ else angle_normal + 180)
+ ticks_loc_angle.append([loc, angle_tick])
+ ticklabels_loc_angle_label.append([loc, angle_label, label])
+
+ return ticks_loc_angle, ticklabels_loc_angle_label
+
+ def _update_ticks(self, renderer=None):
+ # set extra pad for major and minor ticklabels: use ticksize of
+ # majorticks even for minor ticks. not clear what is best.
+
+ if renderer is None:
+ renderer = self.figure._get_renderer()
+
+ dpi_cor = renderer.points_to_pixels(1.)
+ if self.major_ticks.get_visible() and self.major_ticks.get_tick_out():
+ ticklabel_pad = self.major_ticks._ticksize * dpi_cor
+ self.major_ticklabels._external_pad = ticklabel_pad
+ self.minor_ticklabels._external_pad = ticklabel_pad
+ else:
+ self.major_ticklabels._external_pad = 0
+ self.minor_ticklabels._external_pad = 0
+
+ majortick_iter, minortick_iter = \
+ self._axis_artist_helper.get_tick_iterators(self.axes)
+
+ tick_loc_angle, ticklabel_loc_angle_label = \
+ self._get_tick_info(majortick_iter)
+ self.major_ticks.set_locs_angles(tick_loc_angle)
+ self.major_ticklabels.set_locs_angles_labels(ticklabel_loc_angle_label)
+
+ tick_loc_angle, ticklabel_loc_angle_label = \
+ self._get_tick_info(minortick_iter)
+ self.minor_ticks.set_locs_angles(tick_loc_angle)
+ self.minor_ticklabels.set_locs_angles_labels(ticklabel_loc_angle_label)
+
+ def _draw_ticks(self, renderer):
+ self._update_ticks(renderer)
+ self.major_ticks.draw(renderer)
+ self.major_ticklabels.draw(renderer)
+ self.minor_ticks.draw(renderer)
+ self.minor_ticklabels.draw(renderer)
+ if (self.major_ticklabels.get_visible()
+ or self.minor_ticklabels.get_visible()):
+ self._draw_offsetText(renderer)
+
+ _offsetText_pos = dict(left=(0, 1, "bottom", "right"),
+ right=(1, 1, "bottom", "left"),
+ bottom=(1, 0, "top", "right"),
+ top=(1, 1, "bottom", "right"))
+
+ def _init_offsetText(self, direction):
+ x, y, va, ha = self._offsetText_pos[direction]
+ self.offsetText = mtext.Annotation(
+ "",
+ xy=(x, y), xycoords="axes fraction",
+ xytext=(0, 0), textcoords="offset points",
+ color=mpl.rcParams['xtick.color'],
+ horizontalalignment=ha, verticalalignment=va,
+ )
+ self.offsetText.set_transform(IdentityTransform())
+ self.axes._set_artist_props(self.offsetText)
+
+ def _update_offsetText(self):
+ self.offsetText.set_text(self.axis.major.formatter.get_offset())
+ self.offsetText.set_size(self.major_ticklabels.get_size())
+ offset = (self.major_ticklabels.get_pad()
+ + self.major_ticklabels.get_size()
+ + 2)
+ self.offsetText.xyann = (0, offset)
+
+ def _draw_offsetText(self, renderer):
+ self._update_offsetText()
+ self.offsetText.draw(renderer)
+
+ def _init_label(self, **kwargs):
+ tr = (self._axis_artist_helper.get_axislabel_transform(self.axes)
+ + self.offset_transform)
+ self.label = AxisLabel(
+ 0, 0, "__from_axes__",
+ color="auto",
+ fontsize=kwargs.get("labelsize", mpl.rcParams['axes.labelsize']),
+ fontweight=mpl.rcParams['axes.labelweight'],
+ axis=self.axis,
+ transform=tr,
+ axis_direction=self._axis_direction,
+ )
+ self.label.set_figure(self.axes.figure)
+ labelpad = kwargs.get("labelpad", 5)
+ self.label.set_pad(labelpad)
+
+ def _update_label(self, renderer):
+ if not self.label.get_visible():
+ return
+
+ if self._ticklabel_add_angle != self._axislabel_add_angle:
+ if ((self.major_ticks.get_visible()
+ and not self.major_ticks.get_tick_out())
+ or (self.minor_ticks.get_visible()
+ and not self.major_ticks.get_tick_out())):
+ axislabel_pad = self.major_ticks._ticksize
+ else:
+ axislabel_pad = 0
+ else:
+ axislabel_pad = max(self.major_ticklabels._axislabel_pad,
+ self.minor_ticklabels._axislabel_pad)
+
+ self.label._external_pad = axislabel_pad
+
+ xy, angle_tangent = \
+ self._axis_artist_helper.get_axislabel_pos_angle(self.axes)
+ if xy is None:
+ return
+
+ angle_label = angle_tangent - 90
+
+ x, y = xy
+ self.label._ref_angle = angle_label + self._axislabel_add_angle
+ self.label.set(x=x, y=y)
+
+ def _draw_label(self, renderer):
+ self._update_label(renderer)
+ self.label.draw(renderer)
+
+ def set_label(self, s):
+ # docstring inherited
+ self.label.set_text(s)
+
+ def get_tightbbox(self, renderer=None):
+ if not self.get_visible():
+ return
+ self._axis_artist_helper.update_lim(self.axes)
+ self._update_ticks(renderer)
+ self._update_label(renderer)
+
+ self.line.set_path(self._axis_artist_helper.get_line(self.axes))
+ if self.get_axisline_style() is not None:
+ self.line.set_line_mutation_scale(self.major_ticklabels.get_size())
+
+ bb = [
+ *self.major_ticklabels.get_window_extents(renderer),
+ *self.minor_ticklabels.get_window_extents(renderer),
+ self.label.get_window_extent(renderer),
+ self.offsetText.get_window_extent(renderer),
+ self.line.get_window_extent(renderer),
+ ]
+ bb = [b for b in bb if b and (b.width != 0 or b.height != 0)]
+ if bb:
+ _bbox = Bbox.union(bb)
+ return _bbox
+ else:
+ return None
+
+ @martist.allow_rasterization
+ def draw(self, renderer):
+ # docstring inherited
+ if not self.get_visible():
+ return
+ renderer.open_group(__name__, gid=self.get_gid())
+ self._axis_artist_helper.update_lim(self.axes)
+ self._draw_ticks(renderer)
+ self._draw_line(renderer)
+ self._draw_label(renderer)
+ renderer.close_group(__name__)
+
+ def toggle(self, all=None, ticks=None, ticklabels=None, label=None):
+ """
+ Toggle visibility of ticks, ticklabels, and (axis) label.
+ To turn all off, ::
+
+ axis.toggle(all=False)
+
+ To turn all off but ticks on ::
+
+ axis.toggle(all=False, ticks=True)
+
+ To turn all on but (axis) label off ::
+
+ axis.toggle(all=True, label=False)
+
+ """
+ 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:
+ self.major_ticks.set_visible(_ticks)
+ self.minor_ticks.set_visible(_ticks)
+ if _ticklabels is not None:
+ self.major_ticklabels.set_visible(_ticklabels)
+ self.minor_ticklabels.set_visible(_ticklabels)
+ if _label is not None:
+ self.label.set_visible(_label)
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axisline_style.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axisline_style.py
new file mode 100644
index 0000000000..5ae188021b
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axisline_style.py
@@ -0,0 +1,193 @@
+"""
+Provides classes to style the axis lines.
+"""
+import math
+
+import numpy as np
+
+import matplotlib as mpl
+from matplotlib.patches import _Style, FancyArrowPatch
+from matplotlib.path import Path
+from matplotlib.transforms import IdentityTransform
+
+
+class _FancyAxislineStyle:
+ class SimpleArrow(FancyArrowPatch):
+ """The artist class that will be returned for SimpleArrow style."""
+ _ARROW_STYLE = "->"
+
+ def __init__(self, axis_artist, line_path, transform,
+ line_mutation_scale):
+ self._axis_artist = axis_artist
+ self._line_transform = transform
+ self._line_path = line_path
+ self._line_mutation_scale = line_mutation_scale
+
+ FancyArrowPatch.__init__(self,
+ path=self._line_path,
+ arrowstyle=self._ARROW_STYLE,
+ patchA=None,
+ patchB=None,
+ shrinkA=0.,
+ shrinkB=0.,
+ mutation_scale=line_mutation_scale,
+ mutation_aspect=None,
+ transform=IdentityTransform(),
+ )
+
+ def set_line_mutation_scale(self, scale):
+ self.set_mutation_scale(scale*self._line_mutation_scale)
+
+ def _extend_path(self, path, mutation_size=10):
+ """
+ Extend the path to make a room for drawing arrow.
+ """
+ (x0, y0), (x1, y1) = path.vertices[-2:]
+ theta = math.atan2(y1 - y0, x1 - x0)
+ x2 = x1 + math.cos(theta) * mutation_size
+ y2 = y1 + math.sin(theta) * mutation_size
+ if path.codes is None:
+ return Path(np.concatenate([path.vertices, [[x2, y2]]]))
+ else:
+ return Path(np.concatenate([path.vertices, [[x2, y2]]]),
+ np.concatenate([path.codes, [Path.LINETO]]))
+
+ def set_path(self, path):
+ self._line_path = path
+
+ def draw(self, renderer):
+ """
+ Draw the axis line.
+ 1) Transform the path to the display coordinate.
+ 2) Extend the path to make a room for arrow.
+ 3) Update the path of the FancyArrowPatch.
+ 4) Draw.
+ """
+ path_in_disp = self._line_transform.transform_path(self._line_path)
+ mutation_size = self.get_mutation_scale() # line_mutation_scale()
+ extended_path = self._extend_path(path_in_disp,
+ mutation_size=mutation_size)
+ self._path_original = extended_path
+ FancyArrowPatch.draw(self, renderer)
+
+ def get_window_extent(self, renderer=None):
+
+ path_in_disp = self._line_transform.transform_path(self._line_path)
+ mutation_size = self.get_mutation_scale() # line_mutation_scale()
+ extended_path = self._extend_path(path_in_disp,
+ mutation_size=mutation_size)
+ self._path_original = extended_path
+ return FancyArrowPatch.get_window_extent(self, renderer)
+
+ class FilledArrow(SimpleArrow):
+ """The artist class that will be returned for FilledArrow style."""
+ _ARROW_STYLE = "-|>"
+
+ def __init__(self, axis_artist, line_path, transform,
+ line_mutation_scale, facecolor):
+ super().__init__(axis_artist, line_path, transform,
+ line_mutation_scale)
+ self.set_facecolor(facecolor)
+
+
+class AxislineStyle(_Style):
+ """
+ A container class which defines style classes for AxisArtists.
+
+ An instance of any axisline style class is a callable object,
+ whose call signature is ::
+
+ __call__(self, axis_artist, path, transform)
+
+ When called, this should return an `.Artist` with the following methods::
+
+ def set_path(self, path):
+ # set the path for axisline.
+
+ def set_line_mutation_scale(self, scale):
+ # set the scale
+
+ def draw(self, renderer):
+ # draw
+ """
+
+ _style_list = {}
+
+ class _Base:
+ # The derived classes are required to be able to be initialized
+ # w/o arguments, i.e., all its argument (except self) must have
+ # the default values.
+
+ def __init__(self):
+ """
+ initialization.
+ """
+ super().__init__()
+
+ def __call__(self, axis_artist, transform):
+ """
+ Given the AxisArtist instance, and transform for the path (set_path
+ method), return the Matplotlib artist for drawing the axis line.
+ """
+ return self.new_line(axis_artist, transform)
+
+ class SimpleArrow(_Base):
+ """
+ A simple arrow.
+ """
+
+ ArrowAxisClass = _FancyAxislineStyle.SimpleArrow
+
+ def __init__(self, size=1):
+ """
+ Parameters
+ ----------
+ size : float
+ Size of the arrow as a fraction of the ticklabel size.
+ """
+
+ self.size = size
+ super().__init__()
+
+ def new_line(self, axis_artist, transform):
+
+ linepath = Path([(0, 0), (0, 1)])
+ axisline = self.ArrowAxisClass(axis_artist, linepath, transform,
+ line_mutation_scale=self.size)
+ return axisline
+
+ _style_list["->"] = SimpleArrow
+
+ class FilledArrow(SimpleArrow):
+ """
+ An arrow with a filled head.
+ """
+
+ ArrowAxisClass = _FancyAxislineStyle.FilledArrow
+
+ def __init__(self, size=1, facecolor=None):
+ """
+ Parameters
+ ----------
+ size : float
+ Size of the arrow as a fraction of the ticklabel size.
+ facecolor : color, default: :rc:`axes.edgecolor`
+ Fill color.
+
+ .. versionadded:: 3.7
+ """
+
+ if facecolor is None:
+ facecolor = mpl.rcParams['axes.edgecolor']
+ self.size = size
+ self._facecolor = facecolor
+ super().__init__(size=size)
+
+ def new_line(self, axis_artist, transform):
+ linepath = Path([(0, 0), (0, 1)])
+ axisline = self.ArrowAxisClass(axis_artist, linepath, transform,
+ line_mutation_scale=self.size,
+ facecolor=self._facecolor)
+ return axisline
+
+ _style_list["-|>"] = FilledArrow
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axislines.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axislines.py
new file mode 100644
index 0000000000..35717da8ea
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/axislines.py
@@ -0,0 +1,531 @@
+"""
+Axislines includes modified implementation of the Axes class. The
+biggest difference is that the artists responsible for drawing the axis spine,
+ticks, ticklabels and axis labels are separated out from Matplotlib's Axis
+class. Originally, this change was motivated to support curvilinear
+grid. Here are a few reasons that I came up with a new axes class:
+
+* "top" and "bottom" x-axis (or "left" and "right" y-axis) can have
+ different ticks (tick locations and labels). This is not possible
+ with the current Matplotlib, although some twin axes trick can help.
+
+* Curvilinear grid.
+
+* angled ticks.
+
+In the new axes class, xaxis and yaxis is set to not visible by
+default, and new set of artist (AxisArtist) are defined to draw axis
+line, ticks, ticklabels and axis label. Axes.axis attribute serves as
+a dictionary of these artists, i.e., ax.axis["left"] is a AxisArtist
+instance responsible to draw left y-axis. The default Axes.axis contains
+"bottom", "left", "top" and "right".
+
+AxisArtist can be considered as a container artist and has the following
+children artists which will draw ticks, labels, etc.
+
+* line
+* major_ticks, major_ticklabels
+* minor_ticks, minor_ticklabels
+* offsetText
+* label
+
+Note that these are separate artists from `matplotlib.axis.Axis`, thus most
+tick-related functions in Matplotlib won't work. For example, color and
+markerwidth of the ``ax.axis["bottom"].major_ticks`` will follow those of
+Axes.xaxis unless explicitly specified.
+
+In addition to AxisArtist, the Axes will have *gridlines* attribute,
+which obviously draws grid lines. The gridlines needs to be separated
+from the axis as some gridlines can never pass any axis.
+"""
+
+import numpy as np
+
+import matplotlib as mpl
+from matplotlib import _api
+import matplotlib.axes as maxes
+from matplotlib.path import Path
+from mpl_toolkits.axes_grid1 import mpl_axes
+from .axisline_style import AxislineStyle # noqa
+from .axis_artist import AxisArtist, GridlinesCollection
+
+
+class _AxisArtistHelperBase:
+ """
+ Base class for axis helper.
+
+ Subclasses should define the methods listed below. The *axes*
+ argument will be the ``.axes`` attribute of the caller artist. ::
+
+ # Construct the spine.
+
+ def get_line_transform(self, axes):
+ return transform
+
+ def get_line(self, axes):
+ return path
+
+ # Construct the label.
+
+ def get_axislabel_transform(self, axes):
+ return transform
+
+ def get_axislabel_pos_angle(self, axes):
+ return (x, y), angle
+
+ # Construct the ticks.
+
+ def get_tick_transform(self, axes):
+ return transform
+
+ def get_tick_iterators(self, axes):
+ # A pair of iterables (one for major ticks, one for minor ticks)
+ # that yield (tick_position, tick_angle, tick_label).
+ return iter_major, iter_minor
+ """
+
+ def update_lim(self, axes):
+ pass
+
+ def _to_xy(self, values, const):
+ """
+ Create a (*values.shape, 2)-shape array representing (x, y) pairs.
+
+ The other coordinate is filled with the constant *const*.
+
+ Example::
+
+ >>> self.nth_coord = 0
+ >>> self._to_xy([1, 2, 3], const=0)
+ array([[1, 0],
+ [2, 0],
+ [3, 0]])
+ """
+ if self.nth_coord == 0:
+ return np.stack(np.broadcast_arrays(values, const), axis=-1)
+ elif self.nth_coord == 1:
+ return np.stack(np.broadcast_arrays(const, values), axis=-1)
+ else:
+ raise ValueError("Unexpected nth_coord")
+
+
+class _FixedAxisArtistHelperBase(_AxisArtistHelperBase):
+ """Helper class for a fixed (in the axes coordinate) axis."""
+
+ passthru_pt = _api.deprecated("3.7")(property(
+ lambda self: {"left": (0, 0), "right": (1, 0),
+ "bottom": (0, 0), "top": (0, 1)}[self._loc]))
+
+ def __init__(self, loc, nth_coord=None):
+ """``nth_coord = 0``: x-axis; ``nth_coord = 1``: y-axis."""
+ self.nth_coord = (
+ nth_coord if nth_coord is not None else
+ _api.check_getitem(
+ {"bottom": 0, "top": 0, "left": 1, "right": 1}, loc=loc))
+ if (nth_coord == 0 and loc not in ["left", "right"]
+ or nth_coord == 1 and loc not in ["bottom", "top"]):
+ _api.warn_deprecated(
+ "3.7", message=f"{loc=!r} is incompatible with "
+ "{nth_coord=}; support is deprecated since %(since)s")
+ self._loc = loc
+ self._pos = {"bottom": 0, "top": 1, "left": 0, "right": 1}[loc]
+ super().__init__()
+ # axis line in transAxes
+ self._path = Path(self._to_xy((0, 1), const=self._pos))
+
+ def get_nth_coord(self):
+ return self.nth_coord
+
+ # LINE
+
+ def get_line(self, axes):
+ return self._path
+
+ def get_line_transform(self, axes):
+ return axes.transAxes
+
+ # LABEL
+
+ def get_axislabel_transform(self, axes):
+ return axes.transAxes
+
+ def get_axislabel_pos_angle(self, axes):
+ """
+ Return the label reference position in transAxes.
+
+ get_label_transform() returns a transform of (transAxes+offset)
+ """
+ return dict(left=((0., 0.5), 90), # (position, angle_tangent)
+ right=((1., 0.5), 90),
+ bottom=((0.5, 0.), 0),
+ top=((0.5, 1.), 0))[self._loc]
+
+ # TICK
+
+ def get_tick_transform(self, axes):
+ return [axes.get_xaxis_transform(),
+ axes.get_yaxis_transform()][self.nth_coord]
+
+
+class _FloatingAxisArtistHelperBase(_AxisArtistHelperBase):
+
+ def __init__(self, nth_coord, value):
+ self.nth_coord = nth_coord
+ self._value = value
+ super().__init__()
+
+ def get_nth_coord(self):
+ return self.nth_coord
+
+ def get_line(self, axes):
+ raise RuntimeError(
+ "get_line method should be defined by the derived class")
+
+
+class FixedAxisArtistHelperRectilinear(_FixedAxisArtistHelperBase):
+
+ def __init__(self, axes, loc, nth_coord=None):
+ """
+ nth_coord = along which coordinate value varies
+ in 2D, nth_coord = 0 -> x axis, nth_coord = 1 -> y axis
+ """
+ super().__init__(loc, nth_coord)
+ self.axis = [axes.xaxis, axes.yaxis][self.nth_coord]
+
+ # TICK
+
+ def get_tick_iterators(self, axes):
+ """tick_loc, tick_angle, tick_label"""
+ if self._loc in ["bottom", "top"]:
+ angle_normal, angle_tangent = 90, 0
+ else: # "left", "right"
+ angle_normal, angle_tangent = 0, 90
+
+ major = self.axis.major
+ major_locs = major.locator()
+ major_labels = major.formatter.format_ticks(major_locs)
+
+ minor = self.axis.minor
+ minor_locs = minor.locator()
+ minor_labels = minor.formatter.format_ticks(minor_locs)
+
+ tick_to_axes = self.get_tick_transform(axes) - axes.transAxes
+
+ def _f(locs, labels):
+ for loc, label in zip(locs, labels):
+ c = self._to_xy(loc, const=self._pos)
+ # check if the tick point is inside axes
+ c2 = tick_to_axes.transform(c)
+ if mpl.transforms._interval_contains_close(
+ (0, 1), c2[self.nth_coord]):
+ yield c, angle_normal, angle_tangent, label
+
+ return _f(major_locs, major_labels), _f(minor_locs, minor_labels)
+
+
+class FloatingAxisArtistHelperRectilinear(_FloatingAxisArtistHelperBase):
+
+ def __init__(self, axes, nth_coord,
+ passingthrough_point, axis_direction="bottom"):
+ super().__init__(nth_coord, passingthrough_point)
+ self._axis_direction = axis_direction
+ self.axis = [axes.xaxis, axes.yaxis][self.nth_coord]
+
+ def get_line(self, axes):
+ fixed_coord = 1 - self.nth_coord
+ data_to_axes = axes.transData - axes.transAxes
+ p = data_to_axes.transform([self._value, self._value])
+ return Path(self._to_xy((0, 1), const=p[fixed_coord]))
+
+ def get_line_transform(self, axes):
+ return axes.transAxes
+
+ def get_axislabel_transform(self, axes):
+ return axes.transAxes
+
+ def get_axislabel_pos_angle(self, axes):
+ """
+ Return the label reference position in transAxes.
+
+ get_label_transform() returns a transform of (transAxes+offset)
+ """
+ angle = [0, 90][self.nth_coord]
+ fixed_coord = 1 - self.nth_coord
+ data_to_axes = axes.transData - axes.transAxes
+ p = data_to_axes.transform([self._value, self._value])
+ verts = self._to_xy(0.5, const=p[fixed_coord])
+ if 0 <= verts[fixed_coord] <= 1:
+ return verts, angle
+ else:
+ return None, None
+
+ def get_tick_transform(self, axes):
+ return axes.transData
+
+ def get_tick_iterators(self, axes):
+ """tick_loc, tick_angle, tick_label"""
+ if self.nth_coord == 0:
+ angle_normal, angle_tangent = 90, 0
+ else:
+ angle_normal, angle_tangent = 0, 90
+
+ major = self.axis.major
+ major_locs = major.locator()
+ major_labels = major.formatter.format_ticks(major_locs)
+
+ minor = self.axis.minor
+ minor_locs = minor.locator()
+ minor_labels = minor.formatter.format_ticks(minor_locs)
+
+ data_to_axes = axes.transData - axes.transAxes
+
+ def _f(locs, labels):
+ for loc, label in zip(locs, labels):
+ c = self._to_xy(loc, const=self._value)
+ c1, c2 = data_to_axes.transform(c)
+ if 0 <= c1 <= 1 and 0 <= c2 <= 1:
+ yield c, angle_normal, angle_tangent, label
+
+ return _f(major_locs, major_labels), _f(minor_locs, minor_labels)
+
+
+class AxisArtistHelper: # Backcompat.
+ Fixed = _FixedAxisArtistHelperBase
+ Floating = _FloatingAxisArtistHelperBase
+
+
+class AxisArtistHelperRectlinear: # Backcompat.
+ Fixed = FixedAxisArtistHelperRectilinear
+ Floating = FloatingAxisArtistHelperRectilinear
+
+
+class GridHelperBase:
+
+ def __init__(self):
+ self._old_limits = None
+ super().__init__()
+
+ def update_lim(self, axes):
+ x1, x2 = axes.get_xlim()
+ y1, y2 = axes.get_ylim()
+ if self._old_limits != (x1, x2, y1, y2):
+ self._update_grid(x1, y1, x2, y2)
+ self._old_limits = (x1, x2, y1, y2)
+
+ def _update_grid(self, x1, y1, x2, y2):
+ """Cache relevant computations when the axes limits have changed."""
+
+ def get_gridlines(self, which, axis):
+ """
+ Return list of grid lines as a list of paths (list of points).
+
+ Parameters
+ ----------
+ which : {"both", "major", "minor"}
+ axis : {"both", "x", "y"}
+ """
+ return []
+
+
+class GridHelperRectlinear(GridHelperBase):
+
+ def __init__(self, axes):
+ super().__init__()
+ self.axes = axes
+
+ def new_fixed_axis(self, loc,
+ nth_coord=None,
+ axis_direction=None,
+ offset=None,
+ axes=None,
+ ):
+ if axes is None:
+ _api.warn_external(
+ "'new_fixed_axis' explicitly requires the axes keyword.")
+ axes = self.axes
+ if axis_direction is None:
+ axis_direction = loc
+
+ helper = FixedAxisArtistHelperRectilinear(axes, loc, nth_coord)
+ axisline = AxisArtist(axes, helper, offset=offset,
+ axis_direction=axis_direction)
+ return axisline
+
+ def new_floating_axis(self, nth_coord, value,
+ axis_direction="bottom",
+ axes=None,
+ ):
+ if axes is None:
+ _api.warn_external(
+ "'new_floating_axis' explicitly requires the axes keyword.")
+ axes = self.axes
+
+ helper = FloatingAxisArtistHelperRectilinear(
+ axes, nth_coord, value, axis_direction)
+ axisline = AxisArtist(axes, helper, axis_direction=axis_direction)
+ axisline.line.set_clip_on(True)
+ axisline.line.set_clip_box(axisline.axes.bbox)
+ return axisline
+
+ def get_gridlines(self, which="major", axis="both"):
+ """
+ Return list of gridline coordinates in data coordinates.
+
+ Parameters
+ ----------
+ which : {"both", "major", "minor"}
+ axis : {"both", "x", "y"}
+ """
+ _api.check_in_list(["both", "major", "minor"], which=which)
+ _api.check_in_list(["both", "x", "y"], axis=axis)
+ gridlines = []
+
+ if axis in ("both", "x"):
+ locs = []
+ y1, y2 = self.axes.get_ylim()
+ if which in ("both", "major"):
+ locs.extend(self.axes.xaxis.major.locator())
+ if which in ("both", "minor"):
+ locs.extend(self.axes.xaxis.minor.locator())
+
+ for x in locs:
+ gridlines.append([[x, x], [y1, y2]])
+
+ if axis in ("both", "y"):
+ x1, x2 = self.axes.get_xlim()
+ locs = []
+ if self.axes.yaxis._major_tick_kw["gridOn"]:
+ locs.extend(self.axes.yaxis.major.locator())
+ if self.axes.yaxis._minor_tick_kw["gridOn"]:
+ locs.extend(self.axes.yaxis.minor.locator())
+
+ for y in locs:
+ gridlines.append([[x1, x2], [y, y]])
+
+ return gridlines
+
+
+class Axes(maxes.Axes):
+
+ @_api.deprecated("3.8", alternative="ax.axis")
+ def __call__(self, *args, **kwargs):
+ return maxes.Axes.axis(self.axes, *args, **kwargs)
+
+ def __init__(self, *args, grid_helper=None, **kwargs):
+ self._axisline_on = True
+ self._grid_helper = (grid_helper if grid_helper
+ else GridHelperRectlinear(self))
+ super().__init__(*args, **kwargs)
+ self.toggle_axisline(True)
+
+ def toggle_axisline(self, b=None):
+ if b is None:
+ b = not self._axisline_on
+ if b:
+ self._axisline_on = True
+ self.spines[:].set_visible(False)
+ self.xaxis.set_visible(False)
+ self.yaxis.set_visible(False)
+ else:
+ self._axisline_on = False
+ self.spines[:].set_visible(True)
+ self.xaxis.set_visible(True)
+ self.yaxis.set_visible(True)
+
+ @property
+ def axis(self):
+ return self._axislines
+
+ def clear(self):
+ # docstring inherited
+
+ # Init gridlines before clear() as clear() calls grid().
+ self.gridlines = gridlines = GridlinesCollection(
+ [],
+ colors=mpl.rcParams['grid.color'],
+ linestyles=mpl.rcParams['grid.linestyle'],
+ linewidths=mpl.rcParams['grid.linewidth'])
+ self._set_artist_props(gridlines)
+ gridlines.set_grid_helper(self.get_grid_helper())
+
+ super().clear()
+
+ # clip_path is set after Axes.clear(): that's when a patch is created.
+ gridlines.set_clip_path(self.axes.patch)
+
+ # Init axis artists.
+ self._axislines = mpl_axes.Axes.AxisDict(self)
+ new_fixed_axis = self.get_grid_helper().new_fixed_axis
+ self._axislines.update({
+ loc: new_fixed_axis(loc=loc, axes=self, axis_direction=loc)
+ for loc in ["bottom", "top", "left", "right"]})
+ for axisline in [self._axislines["top"], self._axislines["right"]]:
+ axisline.label.set_visible(False)
+ axisline.major_ticklabels.set_visible(False)
+ axisline.minor_ticklabels.set_visible(False)
+
+ def get_grid_helper(self):
+ return self._grid_helper
+
+ def grid(self, visible=None, which='major', axis="both", **kwargs):
+ """
+ Toggle the gridlines, and optionally set the properties of the lines.
+ """
+ # There are some discrepancies in the behavior of grid() between
+ # axes_grid and Matplotlib, because axes_grid explicitly sets the
+ # visibility of the gridlines.
+ super().grid(visible, which=which, axis=axis, **kwargs)
+ if not self._axisline_on:
+ return
+ if visible is None:
+ visible = (self.axes.xaxis._minor_tick_kw["gridOn"]
+ or self.axes.xaxis._major_tick_kw["gridOn"]
+ or self.axes.yaxis._minor_tick_kw["gridOn"]
+ or self.axes.yaxis._major_tick_kw["gridOn"])
+ self.gridlines.set(which=which, axis=axis, visible=visible)
+ self.gridlines.set(**kwargs)
+
+ def get_children(self):
+ if self._axisline_on:
+ children = [*self._axislines.values(), self.gridlines]
+ else:
+ children = []
+ children.extend(super().get_children())
+ return children
+
+ def new_fixed_axis(self, loc, offset=None):
+ gh = self.get_grid_helper()
+ axis = gh.new_fixed_axis(loc,
+ nth_coord=None,
+ axis_direction=None,
+ offset=offset,
+ axes=self,
+ )
+ return axis
+
+ def new_floating_axis(self, nth_coord, value, axis_direction="bottom"):
+ gh = self.get_grid_helper()
+ axis = gh.new_floating_axis(nth_coord, value,
+ axis_direction=axis_direction,
+ axes=self)
+ return axis
+
+
+class AxesZero(Axes):
+
+ def clear(self):
+ super().clear()
+ new_floating_axis = self.get_grid_helper().new_floating_axis
+ self._axislines.update(
+ xzero=new_floating_axis(
+ nth_coord=0, value=0., axis_direction="bottom", axes=self),
+ yzero=new_floating_axis(
+ nth_coord=1, value=0., axis_direction="left", axes=self),
+ )
+ for k in ["xzero", "yzero"]:
+ self._axislines[k].line.set_clip_path(self.patch)
+ self._axislines[k].set_visible(False)
+
+
+Subplot = Axes
+SubplotZero = AxesZero
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/floating_axes.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/floating_axes.py
new file mode 100644
index 0000000000..97dafe98c6
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/floating_axes.py
@@ -0,0 +1,298 @@
+"""
+An experimental support for curvilinear grid.
+"""
+
+# TODO :
+# see if tick_iterator method can be simplified by reusing the parent method.
+
+import functools
+
+import numpy as np
+
+import matplotlib as mpl
+from matplotlib import _api, cbook
+import matplotlib.patches as mpatches
+from matplotlib.path import Path
+
+from mpl_toolkits.axes_grid1.parasite_axes import host_axes_class_factory
+
+from . import axislines, grid_helper_curvelinear
+from .axis_artist import AxisArtist
+from .grid_finder import ExtremeFinderSimple
+
+
+class FloatingAxisArtistHelper(
+ grid_helper_curvelinear.FloatingAxisArtistHelper):
+ pass
+
+
+class FixedAxisArtistHelper(grid_helper_curvelinear.FloatingAxisArtistHelper):
+
+ def __init__(self, grid_helper, side, nth_coord_ticks=None):
+ """
+ nth_coord = along which coordinate value varies.
+ nth_coord = 0 -> x axis, nth_coord = 1 -> y axis
+ """
+ lon1, lon2, lat1, lat2 = grid_helper.grid_finder.extreme_finder(*[None] * 5)
+ value, nth_coord = _api.check_getitem(
+ dict(left=(lon1, 0), right=(lon2, 0), bottom=(lat1, 1), top=(lat2, 1)),
+ side=side)
+ super().__init__(grid_helper, nth_coord, value, axis_direction=side)
+ if nth_coord_ticks is None:
+ nth_coord_ticks = nth_coord
+ self.nth_coord_ticks = nth_coord_ticks
+
+ self.value = value
+ self.grid_helper = grid_helper
+ self._side = side
+
+ def update_lim(self, axes):
+ self.grid_helper.update_lim(axes)
+ self._grid_info = self.grid_helper._grid_info
+
+ def get_tick_iterators(self, axes):
+ """tick_loc, tick_angle, tick_label, (optionally) tick_label"""
+
+ grid_finder = self.grid_helper.grid_finder
+
+ lat_levs, lat_n, lat_factor = self._grid_info["lat_info"]
+ yy0 = lat_levs / lat_factor
+
+ lon_levs, lon_n, lon_factor = self._grid_info["lon_info"]
+ xx0 = lon_levs / lon_factor
+
+ extremes = self.grid_helper.grid_finder.extreme_finder(*[None] * 5)
+ xmin, xmax = sorted(extremes[:2])
+ ymin, ymax = sorted(extremes[2:])
+
+ def trf_xy(x, y):
+ trf = grid_finder.get_transform() + axes.transData
+ return trf.transform(np.column_stack(np.broadcast_arrays(x, y))).T
+
+ if self.nth_coord == 0:
+ mask = (ymin <= yy0) & (yy0 <= ymax)
+ (xx1, yy1), (dxx1, dyy1), (dxx2, dyy2) = \
+ grid_helper_curvelinear._value_and_jacobian(
+ trf_xy, self.value, yy0[mask], (xmin, xmax), (ymin, ymax))
+ labels = self._grid_info["lat_labels"]
+
+ elif self.nth_coord == 1:
+ mask = (xmin <= xx0) & (xx0 <= xmax)
+ (xx1, yy1), (dxx2, dyy2), (dxx1, dyy1) = \
+ grid_helper_curvelinear._value_and_jacobian(
+ trf_xy, xx0[mask], self.value, (xmin, xmax), (ymin, ymax))
+ labels = self._grid_info["lon_labels"]
+
+ labels = [l for l, m in zip(labels, mask) if m]
+
+ angle_normal = np.arctan2(dyy1, dxx1)
+ angle_tangent = np.arctan2(dyy2, dxx2)
+ mm = (dyy1 == 0) & (dxx1 == 0) # points with degenerate normal
+ angle_normal[mm] = angle_tangent[mm] + np.pi / 2
+
+ tick_to_axes = self.get_tick_transform(axes) - axes.transAxes
+ in_01 = functools.partial(
+ mpl.transforms._interval_contains_close, (0, 1))
+
+ def f1():
+ for x, y, normal, tangent, lab \
+ in zip(xx1, yy1, angle_normal, angle_tangent, labels):
+ c2 = tick_to_axes.transform((x, y))
+ if in_01(c2[0]) and in_01(c2[1]):
+ yield [x, y], *np.rad2deg([normal, tangent]), lab
+
+ return f1(), iter([])
+
+ def get_line(self, axes):
+ self.update_lim(axes)
+ k, v = dict(left=("lon_lines0", 0),
+ right=("lon_lines0", 1),
+ bottom=("lat_lines0", 0),
+ top=("lat_lines0", 1))[self._side]
+ xx, yy = self._grid_info[k][v]
+ return Path(np.column_stack([xx, yy]))
+
+
+class ExtremeFinderFixed(ExtremeFinderSimple):
+ # docstring inherited
+
+ def __init__(self, extremes):
+ """
+ This subclass always returns the same bounding box.
+
+ Parameters
+ ----------
+ extremes : (float, float, float, float)
+ The bounding box that this helper always returns.
+ """
+ self._extremes = extremes
+
+ def __call__(self, transform_xy, x1, y1, x2, y2):
+ # docstring inherited
+ return self._extremes
+
+
+class GridHelperCurveLinear(grid_helper_curvelinear.GridHelperCurveLinear):
+
+ def __init__(self, aux_trans, extremes,
+ grid_locator1=None,
+ grid_locator2=None,
+ tick_formatter1=None,
+ tick_formatter2=None):
+ # docstring inherited
+ super().__init__(aux_trans,
+ extreme_finder=ExtremeFinderFixed(extremes),
+ grid_locator1=grid_locator1,
+ grid_locator2=grid_locator2,
+ tick_formatter1=tick_formatter1,
+ tick_formatter2=tick_formatter2)
+
+ @_api.deprecated("3.8")
+ def get_data_boundary(self, side):
+ """
+ Return v=0, nth=1.
+ """
+ lon1, lon2, lat1, lat2 = self.grid_finder.extreme_finder(*[None] * 5)
+ return dict(left=(lon1, 0),
+ right=(lon2, 0),
+ bottom=(lat1, 1),
+ top=(lat2, 1))[side]
+
+ def new_fixed_axis(self, loc,
+ nth_coord=None,
+ axis_direction=None,
+ offset=None,
+ axes=None):
+ if axes is None:
+ axes = self.axes
+ if axis_direction is None:
+ axis_direction = loc
+ # This is not the same as the FixedAxisArtistHelper class used by
+ # grid_helper_curvelinear.GridHelperCurveLinear.new_fixed_axis!
+ helper = FixedAxisArtistHelper(
+ self, loc, nth_coord_ticks=nth_coord)
+ axisline = AxisArtist(axes, helper, axis_direction=axis_direction)
+ # Perhaps should be moved to the base class?
+ axisline.line.set_clip_on(True)
+ axisline.line.set_clip_box(axisline.axes.bbox)
+ return axisline
+
+ # new_floating_axis will inherit the grid_helper's extremes.
+
+ # def new_floating_axis(self, nth_coord,
+ # value,
+ # axes=None,
+ # axis_direction="bottom"
+ # ):
+
+ # axis = super(GridHelperCurveLinear,
+ # self).new_floating_axis(nth_coord,
+ # value, axes=axes,
+ # axis_direction=axis_direction)
+
+ # # set extreme values of the axis helper
+ # if nth_coord == 1:
+ # axis.get_helper().set_extremes(*self._extremes[:2])
+ # elif nth_coord == 0:
+ # axis.get_helper().set_extremes(*self._extremes[2:])
+
+ # return axis
+
+ def _update_grid(self, x1, y1, x2, y2):
+ if self._grid_info is None:
+ self._grid_info = dict()
+
+ grid_info = self._grid_info
+
+ grid_finder = self.grid_finder
+ extremes = grid_finder.extreme_finder(grid_finder.inv_transform_xy,
+ x1, y1, x2, y2)
+
+ lon_min, lon_max = sorted(extremes[:2])
+ lat_min, lat_max = sorted(extremes[2:])
+ grid_info["extremes"] = lon_min, lon_max, lat_min, lat_max # extremes
+
+ lon_levs, lon_n, lon_factor = \
+ grid_finder.grid_locator1(lon_min, lon_max)
+ lon_levs = np.asarray(lon_levs)
+ lat_levs, lat_n, lat_factor = \
+ grid_finder.grid_locator2(lat_min, lat_max)
+ lat_levs = np.asarray(lat_levs)
+
+ grid_info["lon_info"] = lon_levs, lon_n, lon_factor
+ grid_info["lat_info"] = lat_levs, lat_n, lat_factor
+
+ grid_info["lon_labels"] = grid_finder.tick_formatter1(
+ "bottom", lon_factor, lon_levs)
+ grid_info["lat_labels"] = grid_finder.tick_formatter2(
+ "bottom", lat_factor, lat_levs)
+
+ lon_values = lon_levs[:lon_n] / lon_factor
+ lat_values = lat_levs[:lat_n] / lat_factor
+
+ lon_lines, lat_lines = grid_finder._get_raw_grid_lines(
+ lon_values[(lon_min < lon_values) & (lon_values < lon_max)],
+ lat_values[(lat_min < lat_values) & (lat_values < lat_max)],
+ lon_min, lon_max, lat_min, lat_max)
+
+ grid_info["lon_lines"] = lon_lines
+ grid_info["lat_lines"] = lat_lines
+
+ lon_lines, lat_lines = grid_finder._get_raw_grid_lines(
+ # lon_min, lon_max, lat_min, lat_max)
+ extremes[:2], extremes[2:], *extremes)
+
+ grid_info["lon_lines0"] = lon_lines
+ grid_info["lat_lines0"] = lat_lines
+
+ def get_gridlines(self, which="major", axis="both"):
+ grid_lines = []
+ if axis in ["both", "x"]:
+ grid_lines.extend(self._grid_info["lon_lines"])
+ if axis in ["both", "y"]:
+ grid_lines.extend(self._grid_info["lat_lines"])
+ return grid_lines
+
+
+class FloatingAxesBase:
+
+ def __init__(self, *args, grid_helper, **kwargs):
+ _api.check_isinstance(GridHelperCurveLinear, grid_helper=grid_helper)
+ super().__init__(*args, grid_helper=grid_helper, **kwargs)
+ self.set_aspect(1.)
+
+ def _gen_axes_patch(self):
+ # docstring inherited
+ x0, x1, y0, y1 = self.get_grid_helper().grid_finder.extreme_finder(*[None] * 5)
+ patch = mpatches.Polygon([(x0, y0), (x1, y0), (x1, y1), (x0, y1)])
+ patch.get_path()._interpolation_steps = 100
+ return patch
+
+ def clear(self):
+ super().clear()
+ self.patch.set_transform(
+ self.get_grid_helper().grid_finder.get_transform()
+ + self.transData)
+ # The original patch is not in the draw tree; it is only used for
+ # clipping purposes.
+ orig_patch = super()._gen_axes_patch()
+ orig_patch.set_figure(self.figure)
+ orig_patch.set_transform(self.transAxes)
+ self.patch.set_clip_path(orig_patch)
+ self.gridlines.set_clip_path(orig_patch)
+ self.adjust_axes_lim()
+
+ def adjust_axes_lim(self):
+ bbox = self.patch.get_path().get_extents(
+ # First transform to pixel coords, then to parent data coords.
+ self.patch.get_transform() - self.transData)
+ bbox = bbox.expanded(1.02, 1.02)
+ self.set_xlim(bbox.xmin, bbox.xmax)
+ self.set_ylim(bbox.ymin, bbox.ymax)
+
+
+floatingaxes_class_factory = cbook._make_class_factory(
+ FloatingAxesBase, "Floating{}")
+FloatingAxes = floatingaxes_class_factory(
+ host_axes_class_factory(axislines.Axes))
+FloatingSubplot = FloatingAxes
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/grid_finder.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/grid_finder.py
new file mode 100644
index 0000000000..f969b011c4
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/grid_finder.py
@@ -0,0 +1,335 @@
+import numpy as np
+
+from matplotlib import ticker as mticker
+from matplotlib.transforms import Bbox, Transform
+
+
+def _find_line_box_crossings(xys, bbox):
+ """
+ Find the points where a polyline crosses a bbox, and the crossing angles.
+
+ Parameters
+ ----------
+ xys : (N, 2) array
+ The polyline coordinates.
+ bbox : `.Bbox`
+ The bounding box.
+
+ Returns
+ -------
+ list of ((float, float), float)
+ Four separate lists of crossings, for the left, right, bottom, and top
+ sides of the bbox, respectively. For each list, the entries are the
+ ``((x, y), ccw_angle_in_degrees)`` of the crossing, where an angle of 0
+ means that the polyline is moving to the right at the crossing point.
+
+ The entries are computed by linearly interpolating at each crossing
+ between the nearest points on either side of the bbox edges.
+ """
+ crossings = []
+ dxys = xys[1:] - xys[:-1]
+ for sl in [slice(None), slice(None, None, -1)]:
+ us, vs = xys.T[sl] # "this" coord, "other" coord
+ dus, dvs = dxys.T[sl]
+ umin, vmin = bbox.min[sl]
+ umax, vmax = bbox.max[sl]
+ for u0, inside in [(umin, us > umin), (umax, us < umax)]:
+ crossings.append([])
+ idxs, = (inside[:-1] ^ inside[1:]).nonzero()
+ for idx in idxs:
+ v = vs[idx] + (u0 - us[idx]) * dvs[idx] / dus[idx]
+ if not vmin <= v <= vmax:
+ continue
+ crossing = (u0, v)[sl]
+ theta = np.degrees(np.arctan2(*dxys[idx][::-1]))
+ crossings[-1].append((crossing, theta))
+ return crossings
+
+
+class ExtremeFinderSimple:
+ """
+ A helper class to figure out the range of grid lines that need to be drawn.
+ """
+
+ def __init__(self, nx, ny):
+ """
+ Parameters
+ ----------
+ nx, ny : int
+ The number of samples in each direction.
+ """
+ self.nx = nx
+ self.ny = ny
+
+ def __call__(self, transform_xy, x1, y1, x2, y2):
+ """
+ Compute an approximation of the bounding box obtained by applying
+ *transform_xy* to the box delimited by ``(x1, y1, x2, y2)``.
+
+ The intended use is to have ``(x1, y1, x2, y2)`` in axes coordinates,
+ and have *transform_xy* be the transform from axes coordinates to data
+ coordinates; this method then returns the range of data coordinates
+ that span the actual axes.
+
+ The computation is done by sampling ``nx * ny`` equispaced points in
+ the ``(x1, y1, x2, y2)`` box and finding the resulting points with
+ extremal coordinates; then adding some padding to take into account the
+ finite sampling.
+
+ As each sampling step covers a relative range of *1/nx* or *1/ny*,
+ the padding is computed by expanding the span covered by the extremal
+ coordinates by these fractions.
+ """
+ x, y = np.meshgrid(
+ np.linspace(x1, x2, self.nx), np.linspace(y1, y2, self.ny))
+ xt, yt = transform_xy(np.ravel(x), np.ravel(y))
+ return self._add_pad(xt.min(), xt.max(), yt.min(), yt.max())
+
+ def _add_pad(self, x_min, x_max, y_min, y_max):
+ """Perform the padding mentioned in `__call__`."""
+ dx = (x_max - x_min) / self.nx
+ dy = (y_max - y_min) / self.ny
+ return x_min - dx, x_max + dx, y_min - dy, y_max + dy
+
+
+class _User2DTransform(Transform):
+ """A transform defined by two user-set functions."""
+
+ input_dims = output_dims = 2
+
+ def __init__(self, forward, backward):
+ """
+ Parameters
+ ----------
+ forward, backward : callable
+ The forward and backward transforms, taking ``x`` and ``y`` as
+ separate arguments and returning ``(tr_x, tr_y)``.
+ """
+ # The normal Matplotlib convention would be to take and return an
+ # (N, 2) array but axisartist uses the transposed version.
+ super().__init__()
+ self._forward = forward
+ self._backward = backward
+
+ def transform_non_affine(self, values):
+ # docstring inherited
+ return np.transpose(self._forward(*np.transpose(values)))
+
+ def inverted(self):
+ # docstring inherited
+ return type(self)(self._backward, self._forward)
+
+
+class GridFinder:
+ """
+ Internal helper for `~.grid_helper_curvelinear.GridHelperCurveLinear`, with
+ the same constructor parameters; should not be directly instantiated.
+ """
+
+ def __init__(self,
+ transform,
+ extreme_finder=None,
+ grid_locator1=None,
+ grid_locator2=None,
+ tick_formatter1=None,
+ tick_formatter2=None):
+ if extreme_finder is None:
+ extreme_finder = ExtremeFinderSimple(20, 20)
+ if grid_locator1 is None:
+ grid_locator1 = MaxNLocator()
+ if grid_locator2 is None:
+ grid_locator2 = MaxNLocator()
+ if tick_formatter1 is None:
+ tick_formatter1 = FormatterPrettyPrint()
+ if tick_formatter2 is None:
+ tick_formatter2 = FormatterPrettyPrint()
+ self.extreme_finder = extreme_finder
+ self.grid_locator1 = grid_locator1
+ self.grid_locator2 = grid_locator2
+ self.tick_formatter1 = tick_formatter1
+ self.tick_formatter2 = tick_formatter2
+ self.set_transform(transform)
+
+ def get_grid_info(self, x1, y1, x2, y2):
+ """
+ lon_values, lat_values : list of grid values. if integer is given,
+ rough number of grids in each direction.
+ """
+
+ extremes = self.extreme_finder(self.inv_transform_xy, x1, y1, x2, y2)
+
+ # min & max rage of lat (or lon) for each grid line will be drawn.
+ # i.e., gridline of lon=0 will be drawn from lat_min to lat_max.
+
+ lon_min, lon_max, lat_min, lat_max = extremes
+ lon_levs, lon_n, lon_factor = self.grid_locator1(lon_min, lon_max)
+ lon_levs = np.asarray(lon_levs)
+ lat_levs, lat_n, lat_factor = self.grid_locator2(lat_min, lat_max)
+ lat_levs = np.asarray(lat_levs)
+
+ lon_values = lon_levs[:lon_n] / lon_factor
+ lat_values = lat_levs[:lat_n] / lat_factor
+
+ lon_lines, lat_lines = self._get_raw_grid_lines(lon_values,
+ lat_values,
+ lon_min, lon_max,
+ lat_min, lat_max)
+
+ ddx = (x2-x1)*1.e-10
+ ddy = (y2-y1)*1.e-10
+ bb = Bbox.from_extents(x1-ddx, y1-ddy, x2+ddx, y2+ddy)
+
+ grid_info = {
+ "extremes": extremes,
+ "lon_lines": lon_lines,
+ "lat_lines": lat_lines,
+ "lon": self._clip_grid_lines_and_find_ticks(
+ lon_lines, lon_values, lon_levs, bb),
+ "lat": self._clip_grid_lines_and_find_ticks(
+ lat_lines, lat_values, lat_levs, bb),
+ }
+
+ tck_labels = grid_info["lon"]["tick_labels"] = {}
+ for direction in ["left", "bottom", "right", "top"]:
+ levs = grid_info["lon"]["tick_levels"][direction]
+ tck_labels[direction] = self.tick_formatter1(
+ direction, lon_factor, levs)
+
+ tck_labels = grid_info["lat"]["tick_labels"] = {}
+ for direction in ["left", "bottom", "right", "top"]:
+ levs = grid_info["lat"]["tick_levels"][direction]
+ tck_labels[direction] = self.tick_formatter2(
+ direction, lat_factor, levs)
+
+ return grid_info
+
+ def _get_raw_grid_lines(self,
+ lon_values, lat_values,
+ lon_min, lon_max, lat_min, lat_max):
+
+ lons_i = np.linspace(lon_min, lon_max, 100) # for interpolation
+ lats_i = np.linspace(lat_min, lat_max, 100)
+
+ lon_lines = [self.transform_xy(np.full_like(lats_i, lon), lats_i)
+ for lon in lon_values]
+ lat_lines = [self.transform_xy(lons_i, np.full_like(lons_i, lat))
+ for lat in lat_values]
+
+ return lon_lines, lat_lines
+
+ def _clip_grid_lines_and_find_ticks(self, lines, values, levs, bb):
+ gi = {
+ "values": [],
+ "levels": [],
+ "tick_levels": dict(left=[], bottom=[], right=[], top=[]),
+ "tick_locs": dict(left=[], bottom=[], right=[], top=[]),
+ "lines": [],
+ }
+
+ tck_levels = gi["tick_levels"]
+ tck_locs = gi["tick_locs"]
+ for (lx, ly), v, lev in zip(lines, values, levs):
+ tcks = _find_line_box_crossings(np.column_stack([lx, ly]), bb)
+ gi["levels"].append(v)
+ gi["lines"].append([(lx, ly)])
+
+ for tck, direction in zip(tcks,
+ ["left", "right", "bottom", "top"]):
+ for t in tck:
+ tck_levels[direction].append(lev)
+ tck_locs[direction].append(t)
+
+ return gi
+
+ def set_transform(self, aux_trans):
+ if isinstance(aux_trans, Transform):
+ self._aux_transform = aux_trans
+ elif len(aux_trans) == 2 and all(map(callable, aux_trans)):
+ self._aux_transform = _User2DTransform(*aux_trans)
+ else:
+ raise TypeError("'aux_trans' must be either a Transform "
+ "instance or a pair of callables")
+
+ def get_transform(self):
+ return self._aux_transform
+
+ update_transform = set_transform # backcompat alias.
+
+ def transform_xy(self, x, y):
+ return self._aux_transform.transform(np.column_stack([x, y])).T
+
+ def inv_transform_xy(self, x, y):
+ return self._aux_transform.inverted().transform(
+ np.column_stack([x, y])).T
+
+ def update(self, **kwargs):
+ for k, v in kwargs.items():
+ if k in ["extreme_finder",
+ "grid_locator1",
+ "grid_locator2",
+ "tick_formatter1",
+ "tick_formatter2"]:
+ setattr(self, k, v)
+ else:
+ raise ValueError(f"Unknown update property {k!r}")
+
+
+class MaxNLocator(mticker.MaxNLocator):
+ def __init__(self, nbins=10, steps=None,
+ trim=True,
+ integer=False,
+ symmetric=False,
+ prune=None):
+ # trim argument has no effect. It has been left for API compatibility
+ super().__init__(nbins, steps=steps, integer=integer,
+ symmetric=symmetric, prune=prune)
+ self.create_dummy_axis()
+
+ def __call__(self, v1, v2):
+ locs = super().tick_values(v1, v2)
+ return np.array(locs), len(locs), 1 # 1: factor (see angle_helper)
+
+
+class FixedLocator:
+ def __init__(self, locs):
+ self._locs = locs
+
+ def __call__(self, v1, v2):
+ v1, v2 = sorted([v1, v2])
+ locs = np.array([l for l in self._locs if v1 <= l <= v2])
+ return locs, len(locs), 1 # 1: factor (see angle_helper)
+
+
+# Tick Formatter
+
+class FormatterPrettyPrint:
+ def __init__(self, useMathText=True):
+ self._fmt = mticker.ScalarFormatter(
+ useMathText=useMathText, useOffset=False)
+ self._fmt.create_dummy_axis()
+
+ def __call__(self, direction, factor, values):
+ return self._fmt.format_ticks(values)
+
+
+class DictFormatter:
+ def __init__(self, format_dict, formatter=None):
+ """
+ format_dict : dictionary for format strings to be used.
+ formatter : fall-back formatter
+ """
+ super().__init__()
+ self._format_dict = format_dict
+ self._fallback_formatter = formatter
+
+ def __call__(self, direction, factor, values):
+ """
+ factor is ignored if value is found in the dictionary
+ """
+ if self._fallback_formatter:
+ fallback_strings = self._fallback_formatter(
+ direction, factor, values)
+ else:
+ fallback_strings = [""] * len(values)
+ return [self._format_dict.get(k, v)
+ for k, v in zip(values, fallback_strings)]
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/grid_helper_curvelinear.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/grid_helper_curvelinear.py
new file mode 100644
index 0000000000..ae17452b6c
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/grid_helper_curvelinear.py
@@ -0,0 +1,336 @@
+"""
+An experimental support for curvilinear grid.
+"""
+
+import functools
+from itertools import chain
+
+import numpy as np
+
+import matplotlib as mpl
+from matplotlib.path import Path
+from matplotlib.transforms import Affine2D, IdentityTransform
+from .axislines import (
+ _FixedAxisArtistHelperBase, _FloatingAxisArtistHelperBase, GridHelperBase)
+from .axis_artist import AxisArtist
+from .grid_finder import GridFinder
+
+
+def _value_and_jacobian(func, xs, ys, xlims, ylims):
+ """
+ Compute *func* and its derivatives along x and y at positions *xs*, *ys*,
+ while ensuring that finite difference calculations don't try to evaluate
+ values outside of *xlims*, *ylims*.
+ """
+ eps = np.finfo(float).eps ** (1/2) # see e.g. scipy.optimize.approx_fprime
+ val = func(xs, ys)
+ # Take the finite difference step in the direction where the bound is the
+ # furthest; the step size is min of epsilon and distance to that bound.
+ xlo, xhi = sorted(xlims)
+ dxlo = xs - xlo
+ dxhi = xhi - xs
+ xeps = (np.take([-1, 1], dxhi >= dxlo)
+ * np.minimum(eps, np.maximum(dxlo, dxhi)))
+ val_dx = func(xs + xeps, ys)
+ ylo, yhi = sorted(ylims)
+ dylo = ys - ylo
+ dyhi = yhi - ys
+ yeps = (np.take([-1, 1], dyhi >= dylo)
+ * np.minimum(eps, np.maximum(dylo, dyhi)))
+ val_dy = func(xs, ys + yeps)
+ return (val, (val_dx - val) / xeps, (val_dy - val) / yeps)
+
+
+class FixedAxisArtistHelper(_FixedAxisArtistHelperBase):
+ """
+ Helper class for a fixed axis.
+ """
+
+ def __init__(self, grid_helper, side, nth_coord_ticks=None):
+ """
+ nth_coord = along which coordinate value varies.
+ nth_coord = 0 -> x axis, nth_coord = 1 -> y axis
+ """
+
+ super().__init__(loc=side)
+
+ self.grid_helper = grid_helper
+ if nth_coord_ticks is None:
+ nth_coord_ticks = self.nth_coord
+ self.nth_coord_ticks = nth_coord_ticks
+
+ self.side = side
+
+ def update_lim(self, axes):
+ self.grid_helper.update_lim(axes)
+
+ def get_tick_transform(self, axes):
+ return axes.transData
+
+ def get_tick_iterators(self, axes):
+ """tick_loc, tick_angle, tick_label"""
+ v1, v2 = axes.get_ylim() if self.nth_coord == 0 else axes.get_xlim()
+ if v1 > v2: # Inverted limits.
+ side = {"left": "right", "right": "left",
+ "top": "bottom", "bottom": "top"}[self.side]
+ else:
+ side = self.side
+ g = self.grid_helper
+ ti1 = g.get_tick_iterator(self.nth_coord_ticks, side)
+ ti2 = g.get_tick_iterator(1-self.nth_coord_ticks, side, minor=True)
+ return chain(ti1, ti2), iter([])
+
+
+class FloatingAxisArtistHelper(_FloatingAxisArtistHelperBase):
+
+ def __init__(self, grid_helper, nth_coord, value, axis_direction=None):
+ """
+ nth_coord = along which coordinate value varies.
+ nth_coord = 0 -> x axis, nth_coord = 1 -> y axis
+ """
+ super().__init__(nth_coord, value)
+ self.value = value
+ self.grid_helper = grid_helper
+ self._extremes = -np.inf, np.inf
+ self._line_num_points = 100 # number of points to create a line
+
+ def set_extremes(self, e1, e2):
+ if e1 is None:
+ e1 = -np.inf
+ if e2 is None:
+ e2 = np.inf
+ self._extremes = e1, e2
+
+ def update_lim(self, axes):
+ self.grid_helper.update_lim(axes)
+
+ x1, x2 = axes.get_xlim()
+ y1, y2 = axes.get_ylim()
+ grid_finder = self.grid_helper.grid_finder
+ extremes = grid_finder.extreme_finder(grid_finder.inv_transform_xy,
+ x1, y1, x2, y2)
+
+ lon_min, lon_max, lat_min, lat_max = extremes
+ e_min, e_max = self._extremes # ranges of other coordinates
+ if self.nth_coord == 0:
+ lat_min = max(e_min, lat_min)
+ lat_max = min(e_max, lat_max)
+ elif self.nth_coord == 1:
+ lon_min = max(e_min, lon_min)
+ lon_max = min(e_max, lon_max)
+
+ lon_levs, lon_n, lon_factor = \
+ grid_finder.grid_locator1(lon_min, lon_max)
+ lat_levs, lat_n, lat_factor = \
+ grid_finder.grid_locator2(lat_min, lat_max)
+
+ if self.nth_coord == 0:
+ xx0 = np.full(self._line_num_points, self.value)
+ yy0 = np.linspace(lat_min, lat_max, self._line_num_points)
+ xx, yy = grid_finder.transform_xy(xx0, yy0)
+ elif self.nth_coord == 1:
+ xx0 = np.linspace(lon_min, lon_max, self._line_num_points)
+ yy0 = np.full(self._line_num_points, self.value)
+ xx, yy = grid_finder.transform_xy(xx0, yy0)
+
+ self._grid_info = {
+ "extremes": (lon_min, lon_max, lat_min, lat_max),
+ "lon_info": (lon_levs, lon_n, np.asarray(lon_factor)),
+ "lat_info": (lat_levs, lat_n, np.asarray(lat_factor)),
+ "lon_labels": grid_finder.tick_formatter1(
+ "bottom", lon_factor, lon_levs),
+ "lat_labels": grid_finder.tick_formatter2(
+ "bottom", lat_factor, lat_levs),
+ "line_xy": (xx, yy),
+ }
+
+ def get_axislabel_transform(self, axes):
+ return Affine2D() # axes.transData
+
+ def get_axislabel_pos_angle(self, axes):
+ def trf_xy(x, y):
+ trf = self.grid_helper.grid_finder.get_transform() + axes.transData
+ return trf.transform([x, y]).T
+
+ xmin, xmax, ymin, ymax = self._grid_info["extremes"]
+ if self.nth_coord == 0:
+ xx0 = self.value
+ yy0 = (ymin + ymax) / 2
+ elif self.nth_coord == 1:
+ xx0 = (xmin + xmax) / 2
+ yy0 = self.value
+ xy1, dxy1_dx, dxy1_dy = _value_and_jacobian(
+ trf_xy, xx0, yy0, (xmin, xmax), (ymin, ymax))
+ p = axes.transAxes.inverted().transform(xy1)
+ if 0 <= p[0] <= 1 and 0 <= p[1] <= 1:
+ d = [dxy1_dy, dxy1_dx][self.nth_coord]
+ return xy1, np.rad2deg(np.arctan2(*d[::-1]))
+ else:
+ return None, None
+
+ def get_tick_transform(self, axes):
+ return IdentityTransform() # axes.transData
+
+ def get_tick_iterators(self, axes):
+ """tick_loc, tick_angle, tick_label, (optionally) tick_label"""
+
+ lat_levs, lat_n, lat_factor = self._grid_info["lat_info"]
+ yy0 = lat_levs / lat_factor
+
+ lon_levs, lon_n, lon_factor = self._grid_info["lon_info"]
+ xx0 = lon_levs / lon_factor
+
+ e0, e1 = self._extremes
+
+ def trf_xy(x, y):
+ trf = self.grid_helper.grid_finder.get_transform() + axes.transData
+ return trf.transform(np.column_stack(np.broadcast_arrays(x, y))).T
+
+ # find angles
+ if self.nth_coord == 0:
+ mask = (e0 <= yy0) & (yy0 <= e1)
+ (xx1, yy1), (dxx1, dyy1), (dxx2, dyy2) = _value_and_jacobian(
+ trf_xy, self.value, yy0[mask], (-np.inf, np.inf), (e0, e1))
+ labels = self._grid_info["lat_labels"]
+
+ elif self.nth_coord == 1:
+ mask = (e0 <= xx0) & (xx0 <= e1)
+ (xx1, yy1), (dxx2, dyy2), (dxx1, dyy1) = _value_and_jacobian(
+ trf_xy, xx0[mask], self.value, (-np.inf, np.inf), (e0, e1))
+ labels = self._grid_info["lon_labels"]
+
+ labels = [l for l, m in zip(labels, mask) if m]
+
+ angle_normal = np.arctan2(dyy1, dxx1)
+ angle_tangent = np.arctan2(dyy2, dxx2)
+ mm = (dyy1 == 0) & (dxx1 == 0) # points with degenerate normal
+ angle_normal[mm] = angle_tangent[mm] + np.pi / 2
+
+ tick_to_axes = self.get_tick_transform(axes) - axes.transAxes
+ in_01 = functools.partial(
+ mpl.transforms._interval_contains_close, (0, 1))
+
+ def f1():
+ for x, y, normal, tangent, lab \
+ in zip(xx1, yy1, angle_normal, angle_tangent, labels):
+ c2 = tick_to_axes.transform((x, y))
+ if in_01(c2[0]) and in_01(c2[1]):
+ yield [x, y], *np.rad2deg([normal, tangent]), lab
+
+ return f1(), iter([])
+
+ def get_line_transform(self, axes):
+ return axes.transData
+
+ def get_line(self, axes):
+ self.update_lim(axes)
+ x, y = self._grid_info["line_xy"]
+ return Path(np.column_stack([x, y]))
+
+
+class GridHelperCurveLinear(GridHelperBase):
+ def __init__(self, aux_trans,
+ extreme_finder=None,
+ grid_locator1=None,
+ grid_locator2=None,
+ tick_formatter1=None,
+ tick_formatter2=None):
+ """
+ Parameters
+ ----------
+ aux_trans : `.Transform` or tuple[Callable, Callable]
+ The transform from curved coordinates to rectilinear coordinate:
+ either a `.Transform` instance (which provides also its inverse),
+ or a pair of callables ``(trans, inv_trans)`` that define the
+ transform and its inverse. The callables should have signature::
+
+ x_rect, y_rect = trans(x_curved, y_curved)
+ x_curved, y_curved = inv_trans(x_rect, y_rect)
+
+ extreme_finder
+
+ grid_locator1, grid_locator2
+ Grid locators for each axis.
+
+ tick_formatter1, tick_formatter2
+ Tick formatters for each axis.
+ """
+ super().__init__()
+ self._grid_info = None
+ self.grid_finder = GridFinder(aux_trans,
+ extreme_finder,
+ grid_locator1,
+ grid_locator2,
+ tick_formatter1,
+ tick_formatter2)
+
+ def update_grid_finder(self, aux_trans=None, **kwargs):
+ if aux_trans is not None:
+ self.grid_finder.update_transform(aux_trans)
+ self.grid_finder.update(**kwargs)
+ self._old_limits = None # Force revalidation.
+
+ def new_fixed_axis(self, loc,
+ nth_coord=None,
+ axis_direction=None,
+ offset=None,
+ axes=None):
+ if axes is None:
+ axes = self.axes
+ if axis_direction is None:
+ axis_direction = loc
+ helper = FixedAxisArtistHelper(self, loc, nth_coord_ticks=nth_coord)
+ axisline = AxisArtist(axes, helper, axis_direction=axis_direction)
+ # Why is clip not set on axisline, unlike in new_floating_axis or in
+ # the floating_axig.GridHelperCurveLinear subclass?
+ return axisline
+
+ def new_floating_axis(self, nth_coord,
+ value,
+ axes=None,
+ axis_direction="bottom"
+ ):
+ if axes is None:
+ axes = self.axes
+ helper = FloatingAxisArtistHelper(
+ self, nth_coord, value, axis_direction)
+ axisline = AxisArtist(axes, helper)
+ axisline.line.set_clip_on(True)
+ axisline.line.set_clip_box(axisline.axes.bbox)
+ # axisline.major_ticklabels.set_visible(True)
+ # axisline.minor_ticklabels.set_visible(False)
+ return axisline
+
+ def _update_grid(self, x1, y1, x2, y2):
+ self._grid_info = self.grid_finder.get_grid_info(x1, y1, x2, y2)
+
+ def get_gridlines(self, which="major", axis="both"):
+ grid_lines = []
+ if axis in ["both", "x"]:
+ for gl in self._grid_info["lon"]["lines"]:
+ grid_lines.extend(gl)
+ if axis in ["both", "y"]:
+ for gl in self._grid_info["lat"]["lines"]:
+ grid_lines.extend(gl)
+ return grid_lines
+
+ def get_tick_iterator(self, nth_coord, axis_side, minor=False):
+
+ # axisnr = dict(left=0, bottom=1, right=2, top=3)[axis_side]
+ angle_tangent = dict(left=90, right=90, bottom=0, top=0)[axis_side]
+ # angle = [0, 90, 180, 270][axisnr]
+ lon_or_lat = ["lon", "lat"][nth_coord]
+ if not minor: # major ticks
+ for (xy, a), l in zip(
+ self._grid_info[lon_or_lat]["tick_locs"][axis_side],
+ self._grid_info[lon_or_lat]["tick_labels"][axis_side]):
+ angle_normal = a
+ yield xy, angle_normal, angle_tangent, l
+ else:
+ for (xy, a), l in zip(
+ self._grid_info[lon_or_lat]["tick_locs"][axis_side],
+ self._grid_info[lon_or_lat]["tick_labels"][axis_side]):
+ angle_normal = a
+ yield xy, angle_normal, angle_tangent, ""
+ # for xy, a, l in self._grid_info[lon_or_lat]["ticks"][axis_side]:
+ # yield xy, a, ""
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/parasite_axes.py b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/parasite_axes.py
new file mode 100644
index 0000000000..4ebd6acc03
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/axisartist/parasite_axes.py
@@ -0,0 +1,7 @@
+from mpl_toolkits.axes_grid1.parasite_axes import (
+ host_axes_class_factory, parasite_axes_class_factory)
+from .axislines import Axes
+
+
+ParasiteAxes = parasite_axes_class_factory(Axes)
+HostAxes = SubplotHost = host_axes_class_factory(Axes)
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/__init__.py b/contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/__init__.py
new file mode 100644
index 0000000000..a089fbd6b7
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/__init__.py
@@ -0,0 +1,3 @@
+from .axes3d import Axes3D
+
+__all__ = ['Axes3D']
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/art3d.py b/contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/art3d.py
new file mode 100644
index 0000000000..4aff115b0c
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/art3d.py
@@ -0,0 +1,1252 @@
+# art3d.py, original mplot3d version by John Porter
+# Parts rewritten by Reinier Heeres <reinier@heeres.eu>
+# Minor additions by Ben Axelrod <baxelrod@coroware.com>
+
+"""
+Module containing 3D artist code and functions to convert 2D
+artists into 3D versions which can be added to an Axes3D.
+"""
+
+import math
+
+import numpy as np
+
+from contextlib import contextmanager
+
+from matplotlib import (
+ artist, cbook, colors as mcolors, lines, text as mtext,
+ path as mpath)
+from matplotlib.collections import (
+ Collection, LineCollection, PolyCollection, PatchCollection, PathCollection)
+from matplotlib.colors import Normalize
+from matplotlib.patches import Patch
+from . import proj3d
+
+
+def _norm_angle(a):
+ """Return the given angle normalized to -180 < *a* <= 180 degrees."""
+ a = (a + 360) % 360
+ if a > 180:
+ a = a - 360
+ return a
+
+
+def _norm_text_angle(a):
+ """Return the given angle normalized to -90 < *a* <= 90 degrees."""
+ a = (a + 180) % 180
+ if a > 90:
+ a = a - 180
+ return a
+
+
+def get_dir_vector(zdir):
+ """
+ Return a direction vector.
+
+ Parameters
+ ----------
+ zdir : {'x', 'y', 'z', None, 3-tuple}
+ The direction. Possible values are:
+
+ - 'x': equivalent to (1, 0, 0)
+ - 'y': equivalent to (0, 1, 0)
+ - 'z': equivalent to (0, 0, 1)
+ - *None*: equivalent to (0, 0, 0)
+ - an iterable (x, y, z) is converted to an array
+
+ Returns
+ -------
+ x, y, z : array
+ The direction vector.
+ """
+ if zdir == 'x':
+ return np.array((1, 0, 0))
+ elif zdir == 'y':
+ return np.array((0, 1, 0))
+ elif zdir == 'z':
+ return np.array((0, 0, 1))
+ elif zdir is None:
+ return np.array((0, 0, 0))
+ elif np.iterable(zdir) and len(zdir) == 3:
+ return np.array(zdir)
+ else:
+ raise ValueError("'x', 'y', 'z', None or vector of length 3 expected")
+
+
+class Text3D(mtext.Text):
+ """
+ Text object with 3D position and direction.
+
+ Parameters
+ ----------
+ x, y, z : float
+ The position of the text.
+ text : str
+ The text string to display.
+ zdir : {'x', 'y', 'z', None, 3-tuple}
+ The direction of the text. See `.get_dir_vector` for a description of
+ the values.
+
+ Other Parameters
+ ----------------
+ **kwargs
+ All other parameters are passed on to `~matplotlib.text.Text`.
+ """
+
+ def __init__(self, x=0, y=0, z=0, text='', zdir='z', **kwargs):
+ mtext.Text.__init__(self, x, y, text, **kwargs)
+ self.set_3d_properties(z, zdir)
+
+ def get_position_3d(self):
+ """Return the (x, y, z) position of the text."""
+ return self._x, self._y, self._z
+
+ def set_position_3d(self, xyz, zdir=None):
+ """
+ Set the (*x*, *y*, *z*) position of the text.
+
+ Parameters
+ ----------
+ xyz : (float, float, float)
+ The position in 3D space.
+ zdir : {'x', 'y', 'z', None, 3-tuple}
+ The direction of the text. If unspecified, the *zdir* will not be
+ changed. See `.get_dir_vector` for a description of the values.
+ """
+ super().set_position(xyz[:2])
+ self.set_z(xyz[2])
+ if zdir is not None:
+ self._dir_vec = get_dir_vector(zdir)
+
+ def set_z(self, z):
+ """
+ Set the *z* position of the text.
+
+ Parameters
+ ----------
+ z : float
+ """
+ self._z = z
+ self.stale = True
+
+ def set_3d_properties(self, z=0, zdir='z'):
+ """
+ Set the *z* position and direction of the text.
+
+ Parameters
+ ----------
+ z : float
+ The z-position in 3D space.
+ zdir : {'x', 'y', 'z', 3-tuple}
+ The direction of the text. Default: 'z'.
+ See `.get_dir_vector` for a description of the values.
+ """
+ self._z = z
+ self._dir_vec = get_dir_vector(zdir)
+ self.stale = True
+
+ @artist.allow_rasterization
+ def draw(self, renderer):
+ position3d = np.array((self._x, self._y, self._z))
+ proj = proj3d._proj_trans_points(
+ [position3d, position3d + self._dir_vec], self.axes.M)
+ dx = proj[0][1] - proj[0][0]
+ dy = proj[1][1] - proj[1][0]
+ angle = math.degrees(math.atan2(dy, dx))
+ with cbook._setattr_cm(self, _x=proj[0][0], _y=proj[1][0],
+ _rotation=_norm_text_angle(angle)):
+ mtext.Text.draw(self, renderer)
+ self.stale = False
+
+ def get_tightbbox(self, renderer=None):
+ # Overwriting the 2d Text behavior which is not valid for 3d.
+ # For now, just return None to exclude from layout calculation.
+ return None
+
+
+def text_2d_to_3d(obj, z=0, zdir='z'):
+ """
+ Convert a `.Text` to a `.Text3D` object.
+
+ Parameters
+ ----------
+ z : float
+ The z-position in 3D space.
+ zdir : {'x', 'y', 'z', 3-tuple}
+ The direction of the text. Default: 'z'.
+ See `.get_dir_vector` for a description of the values.
+ """
+ obj.__class__ = Text3D
+ obj.set_3d_properties(z, zdir)
+
+
+class Line3D(lines.Line2D):
+ """
+ 3D line object.
+
+ .. note:: Use `get_data_3d` to obtain the data associated with the line.
+ `~.Line2D.get_data`, `~.Line2D.get_xdata`, and `~.Line2D.get_ydata` return
+ the x- and y-coordinates of the projected 2D-line, not the x- and y-data of
+ the 3D-line. Similarly, use `set_data_3d` to set the data, not
+ `~.Line2D.set_data`, `~.Line2D.set_xdata`, and `~.Line2D.set_ydata`.
+ """
+
+ def __init__(self, xs, ys, zs, *args, **kwargs):
+ """
+
+ Parameters
+ ----------
+ xs : array-like
+ The x-data to be plotted.
+ ys : array-like
+ The y-data to be plotted.
+ zs : array-like
+ The z-data to be plotted.
+ *args, **kwargs
+ Additional arguments are passed to `~matplotlib.lines.Line2D`.
+ """
+ super().__init__([], [], *args, **kwargs)
+ self.set_data_3d(xs, ys, zs)
+
+ def set_3d_properties(self, zs=0, zdir='z'):
+ """
+ Set the *z* position and direction of the line.
+
+ Parameters
+ ----------
+ zs : float or array of floats
+ The location along the *zdir* axis in 3D space to position the
+ line.
+ zdir : {'x', 'y', 'z'}
+ Plane to plot line orthogonal to. Default: 'z'.
+ See `.get_dir_vector` for a description of the values.
+ """
+ xs = self.get_xdata()
+ ys = self.get_ydata()
+ zs = cbook._to_unmasked_float_array(zs).ravel()
+ zs = np.broadcast_to(zs, len(xs))
+ self._verts3d = juggle_axes(xs, ys, zs, zdir)
+ self.stale = True
+
+ def set_data_3d(self, *args):
+ """
+ Set the x, y and z data
+
+ Parameters
+ ----------
+ x : array-like
+ The x-data to be plotted.
+ y : array-like
+ The y-data to be plotted.
+ z : array-like
+ The z-data to be plotted.
+
+ Notes
+ -----
+ Accepts x, y, z arguments or a single array-like (x, y, z)
+ """
+ if len(args) == 1:
+ args = args[0]
+ for name, xyz in zip('xyz', args):
+ if not np.iterable(xyz):
+ raise RuntimeError(f'{name} must be a sequence')
+ self._verts3d = args
+ self.stale = True
+
+ def get_data_3d(self):
+ """
+ Get the current data
+
+ Returns
+ -------
+ verts3d : length-3 tuple or array-like
+ The current data as a tuple or array-like.
+ """
+ return self._verts3d
+
+ @artist.allow_rasterization
+ def draw(self, renderer):
+ xs3d, ys3d, zs3d = self._verts3d
+ xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, self.axes.M)
+ self.set_data(xs, ys)
+ super().draw(renderer)
+ self.stale = False
+
+
+def line_2d_to_3d(line, zs=0, zdir='z'):
+ """
+ Convert a `.Line2D` to a `.Line3D` object.
+
+ Parameters
+ ----------
+ zs : float
+ The location along the *zdir* axis in 3D space to position the line.
+ zdir : {'x', 'y', 'z'}
+ Plane to plot line orthogonal to. Default: 'z'.
+ See `.get_dir_vector` for a description of the values.
+ """
+
+ line.__class__ = Line3D
+ line.set_3d_properties(zs, zdir)
+
+
+def _path_to_3d_segment(path, zs=0, zdir='z'):
+ """Convert a path to a 3D segment."""
+
+ zs = np.broadcast_to(zs, len(path))
+ pathsegs = path.iter_segments(simplify=False, curves=False)
+ seg = [(x, y, z) for (((x, y), code), z) in zip(pathsegs, zs)]
+ seg3d = [juggle_axes(x, y, z, zdir) for (x, y, z) in seg]
+ return seg3d
+
+
+def _paths_to_3d_segments(paths, zs=0, zdir='z'):
+ """Convert paths from a collection object to 3D segments."""
+
+ if not np.iterable(zs):
+ zs = np.broadcast_to(zs, len(paths))
+ else:
+ if len(zs) != len(paths):
+ raise ValueError('Number of z-coordinates does not match paths.')
+
+ segs = [_path_to_3d_segment(path, pathz, zdir)
+ for path, pathz in zip(paths, zs)]
+ return segs
+
+
+def _path_to_3d_segment_with_codes(path, zs=0, zdir='z'):
+ """Convert a path to a 3D segment with path codes."""
+
+ zs = np.broadcast_to(zs, len(path))
+ pathsegs = path.iter_segments(simplify=False, curves=False)
+ seg_codes = [((x, y, z), code) for ((x, y), code), z in zip(pathsegs, zs)]
+ if seg_codes:
+ seg, codes = zip(*seg_codes)
+ seg3d = [juggle_axes(x, y, z, zdir) for (x, y, z) in seg]
+ else:
+ seg3d = []
+ codes = []
+ return seg3d, list(codes)
+
+
+def _paths_to_3d_segments_with_codes(paths, zs=0, zdir='z'):
+ """
+ Convert paths from a collection object to 3D segments with path codes.
+ """
+
+ zs = np.broadcast_to(zs, len(paths))
+ segments_codes = [_path_to_3d_segment_with_codes(path, pathz, zdir)
+ for path, pathz in zip(paths, zs)]
+ if segments_codes:
+ segments, codes = zip(*segments_codes)
+ else:
+ segments, codes = [], []
+ return list(segments), list(codes)
+
+
+class Collection3D(Collection):
+ """A collection of 3D paths."""
+
+ def do_3d_projection(self):
+ """Project the points according to renderer matrix."""
+ xyzs_list = [proj3d.proj_transform(*vs.T, self.axes.M)
+ for vs, _ in self._3dverts_codes]
+ self._paths = [mpath.Path(np.column_stack([xs, ys]), cs)
+ for (xs, ys, _), (_, cs) in zip(xyzs_list, self._3dverts_codes)]
+ zs = np.concatenate([zs for _, _, zs in xyzs_list])
+ return zs.min() if len(zs) else 1e9
+
+
+def collection_2d_to_3d(col, zs=0, zdir='z'):
+ """Convert a `.Collection` to a `.Collection3D` object."""
+ zs = np.broadcast_to(zs, len(col.get_paths()))
+ col._3dverts_codes = [
+ (np.column_stack(juggle_axes(
+ *np.column_stack([p.vertices, np.broadcast_to(z, len(p.vertices))]).T,
+ zdir)),
+ p.codes)
+ for p, z in zip(col.get_paths(), zs)]
+ col.__class__ = cbook._make_class_factory(Collection3D, "{}3D")(type(col))
+
+
+class Line3DCollection(LineCollection):
+ """
+ A collection of 3D lines.
+ """
+
+ def set_sort_zpos(self, val):
+ """Set the position to use for z-sorting."""
+ self._sort_zpos = val
+ self.stale = True
+
+ def set_segments(self, segments):
+ """
+ Set 3D segments.
+ """
+ self._segments3d = segments
+ super().set_segments([])
+
+ def do_3d_projection(self):
+ """
+ Project the points according to renderer matrix.
+ """
+ xyslist = [proj3d._proj_trans_points(points, self.axes.M)
+ for points in self._segments3d]
+ segments_2d = [np.column_stack([xs, ys]) for xs, ys, zs in xyslist]
+ LineCollection.set_segments(self, segments_2d)
+
+ # FIXME
+ minz = 1e9
+ for xs, ys, zs in xyslist:
+ minz = min(minz, min(zs))
+ return minz
+
+
+def line_collection_2d_to_3d(col, zs=0, zdir='z'):
+ """Convert a `.LineCollection` to a `.Line3DCollection` object."""
+ segments3d = _paths_to_3d_segments(col.get_paths(), zs, zdir)
+ col.__class__ = Line3DCollection
+ col.set_segments(segments3d)
+
+
+class Patch3D(Patch):
+ """
+ 3D patch object.
+ """
+
+ def __init__(self, *args, zs=(), zdir='z', **kwargs):
+ """
+ Parameters
+ ----------
+ verts :
+ zs : float
+ The location along the *zdir* axis in 3D space to position the
+ patch.
+ zdir : {'x', 'y', 'z'}
+ Plane to plot patch orthogonal to. Default: 'z'.
+ See `.get_dir_vector` for a description of the values.
+ """
+ super().__init__(*args, **kwargs)
+ self.set_3d_properties(zs, zdir)
+
+ def set_3d_properties(self, verts, zs=0, zdir='z'):
+ """
+ Set the *z* position and direction of the patch.
+
+ Parameters
+ ----------
+ verts :
+ zs : float
+ The location along the *zdir* axis in 3D space to position the
+ patch.
+ zdir : {'x', 'y', 'z'}
+ Plane to plot patch orthogonal to. Default: 'z'.
+ See `.get_dir_vector` for a description of the values.
+ """
+ zs = np.broadcast_to(zs, len(verts))
+ self._segment3d = [juggle_axes(x, y, z, zdir)
+ for ((x, y), z) in zip(verts, zs)]
+
+ def get_path(self):
+ return self._path2d
+
+ def do_3d_projection(self):
+ s = self._segment3d
+ xs, ys, zs = zip(*s)
+ vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs,
+ self.axes.M)
+ self._path2d = mpath.Path(np.column_stack([vxs, vys]))
+ return min(vzs)
+
+
+class PathPatch3D(Patch3D):
+ """
+ 3D PathPatch object.
+ """
+
+ def __init__(self, path, *, zs=(), zdir='z', **kwargs):
+ """
+ Parameters
+ ----------
+ path :
+ zs : float
+ The location along the *zdir* axis in 3D space to position the
+ path patch.
+ zdir : {'x', 'y', 'z', 3-tuple}
+ Plane to plot path patch orthogonal to. Default: 'z'.
+ See `.get_dir_vector` for a description of the values.
+ """
+ # Not super().__init__!
+ Patch.__init__(self, **kwargs)
+ self.set_3d_properties(path, zs, zdir)
+
+ def set_3d_properties(self, path, zs=0, zdir='z'):
+ """
+ Set the *z* position and direction of the path patch.
+
+ Parameters
+ ----------
+ path :
+ zs : float
+ The location along the *zdir* axis in 3D space to position the
+ path patch.
+ zdir : {'x', 'y', 'z', 3-tuple}
+ Plane to plot path patch orthogonal to. Default: 'z'.
+ See `.get_dir_vector` for a description of the values.
+ """
+ Patch3D.set_3d_properties(self, path.vertices, zs=zs, zdir=zdir)
+ self._code3d = path.codes
+
+ def do_3d_projection(self):
+ s = self._segment3d
+ xs, ys, zs = zip(*s)
+ vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs,
+ self.axes.M)
+ self._path2d = mpath.Path(np.column_stack([vxs, vys]), self._code3d)
+ return min(vzs)
+
+
+def _get_patch_verts(patch):
+ """Return a list of vertices for the path of a patch."""
+ trans = patch.get_patch_transform()
+ path = patch.get_path()
+ polygons = path.to_polygons(trans)
+ return polygons[0] if len(polygons) else np.array([])
+
+
+def patch_2d_to_3d(patch, z=0, zdir='z'):
+ """Convert a `.Patch` to a `.Patch3D` object."""
+ verts = _get_patch_verts(patch)
+ patch.__class__ = Patch3D
+ patch.set_3d_properties(verts, z, zdir)
+
+
+def pathpatch_2d_to_3d(pathpatch, z=0, zdir='z'):
+ """Convert a `.PathPatch` to a `.PathPatch3D` object."""
+ path = pathpatch.get_path()
+ trans = pathpatch.get_patch_transform()
+
+ mpath = trans.transform_path(path)
+ pathpatch.__class__ = PathPatch3D
+ pathpatch.set_3d_properties(mpath, z, zdir)
+
+
+class Patch3DCollection(PatchCollection):
+ """
+ A collection of 3D patches.
+ """
+
+ def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs):
+ """
+ Create a collection of flat 3D patches with its normal vector
+ pointed in *zdir* direction, and located at *zs* on the *zdir*
+ axis. 'zs' can be a scalar or an array-like of the same length as
+ the number of patches in the collection.
+
+ Constructor arguments are the same as for
+ :class:`~matplotlib.collections.PatchCollection`. In addition,
+ keywords *zs=0* and *zdir='z'* are available.
+
+ Also, the keyword argument *depthshade* is available to indicate
+ whether to shade the patches in order to give the appearance of depth
+ (default is *True*). This is typically desired in scatter plots.
+ """
+ self._depthshade = depthshade
+ super().__init__(*args, **kwargs)
+ self.set_3d_properties(zs, zdir)
+
+ def get_depthshade(self):
+ return self._depthshade
+
+ def set_depthshade(self, depthshade):
+ """
+ Set whether depth shading is performed on collection members.
+
+ Parameters
+ ----------
+ depthshade : bool
+ Whether to shade the patches in order to give the appearance of
+ depth.
+ """
+ self._depthshade = depthshade
+ self.stale = True
+
+ def set_sort_zpos(self, val):
+ """Set the position to use for z-sorting."""
+ self._sort_zpos = val
+ self.stale = True
+
+ def set_3d_properties(self, zs, zdir):
+ """
+ Set the *z* positions and direction of the patches.
+
+ Parameters
+ ----------
+ zs : float or array of floats
+ The location or locations to place the patches in the collection
+ along the *zdir* axis.
+ zdir : {'x', 'y', 'z'}
+ Plane to plot patches orthogonal to.
+ All patches must have the same direction.
+ See `.get_dir_vector` for a description of the values.
+ """
+ # Force the collection to initialize the face and edgecolors
+ # just in case it is a scalarmappable with a colormap.
+ self.update_scalarmappable()
+ offsets = self.get_offsets()
+ if len(offsets) > 0:
+ xs, ys = offsets.T
+ else:
+ xs = []
+ ys = []
+ self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir)
+ self._z_markers_idx = slice(-1)
+ self._vzs = None
+ self.stale = True
+
+ def do_3d_projection(self):
+ xs, ys, zs = self._offsets3d
+ vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs,
+ self.axes.M)
+ self._vzs = vzs
+ super().set_offsets(np.column_stack([vxs, vys]))
+
+ if vzs.size > 0:
+ return min(vzs)
+ else:
+ return np.nan
+
+ def _maybe_depth_shade_and_sort_colors(self, color_array):
+ color_array = (
+ _zalpha(color_array, self._vzs)
+ if self._vzs is not None and self._depthshade
+ else color_array
+ )
+ if len(color_array) > 1:
+ color_array = color_array[self._z_markers_idx]
+ return mcolors.to_rgba_array(color_array, self._alpha)
+
+ def get_facecolor(self):
+ return self._maybe_depth_shade_and_sort_colors(super().get_facecolor())
+
+ def get_edgecolor(self):
+ # We need this check here to make sure we do not double-apply the depth
+ # based alpha shading when the edge color is "face" which means the
+ # edge colour should be identical to the face colour.
+ if cbook._str_equal(self._edgecolors, 'face'):
+ return self.get_facecolor()
+ return self._maybe_depth_shade_and_sort_colors(super().get_edgecolor())
+
+
+class Path3DCollection(PathCollection):
+ """
+ A collection of 3D paths.
+ """
+
+ def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs):
+ """
+ Create a collection of flat 3D paths with its normal vector
+ pointed in *zdir* direction, and located at *zs* on the *zdir*
+ axis. 'zs' can be a scalar or an array-like of the same length as
+ the number of paths in the collection.
+
+ Constructor arguments are the same as for
+ :class:`~matplotlib.collections.PathCollection`. In addition,
+ keywords *zs=0* and *zdir='z'* are available.
+
+ Also, the keyword argument *depthshade* is available to indicate
+ whether to shade the patches in order to give the appearance of depth
+ (default is *True*). This is typically desired in scatter plots.
+ """
+ self._depthshade = depthshade
+ self._in_draw = False
+ super().__init__(*args, **kwargs)
+ self.set_3d_properties(zs, zdir)
+ self._offset_zordered = None
+
+ def draw(self, renderer):
+ with self._use_zordered_offset():
+ with cbook._setattr_cm(self, _in_draw=True):
+ super().draw(renderer)
+
+ def set_sort_zpos(self, val):
+ """Set the position to use for z-sorting."""
+ self._sort_zpos = val
+ self.stale = True
+
+ def set_3d_properties(self, zs, zdir):
+ """
+ Set the *z* positions and direction of the paths.
+
+ Parameters
+ ----------
+ zs : float or array of floats
+ The location or locations to place the paths in the collection
+ along the *zdir* axis.
+ zdir : {'x', 'y', 'z'}
+ Plane to plot paths orthogonal to.
+ All paths must have the same direction.
+ See `.get_dir_vector` for a description of the values.
+ """
+ # Force the collection to initialize the face and edgecolors
+ # just in case it is a scalarmappable with a colormap.
+ self.update_scalarmappable()
+ offsets = self.get_offsets()
+ if len(offsets) > 0:
+ xs, ys = offsets.T
+ else:
+ xs = []
+ ys = []
+ self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir)
+ # In the base draw methods we access the attributes directly which
+ # means we cannot resolve the shuffling in the getter methods like
+ # we do for the edge and face colors.
+ #
+ # This means we need to carry around a cache of the unsorted sizes and
+ # widths (postfixed with 3d) and in `do_3d_projection` set the
+ # depth-sorted version of that data into the private state used by the
+ # base collection class in its draw method.
+ #
+ # Grab the current sizes and linewidths to preserve them.
+ self._sizes3d = self._sizes
+ self._linewidths3d = np.array(self._linewidths)
+ xs, ys, zs = self._offsets3d
+
+ # Sort the points based on z coordinates
+ # Performance optimization: Create a sorted index array and reorder
+ # points and point properties according to the index array
+ self._z_markers_idx = slice(-1)
+ self._vzs = None
+ self.stale = True
+
+ def set_sizes(self, sizes, dpi=72.0):
+ super().set_sizes(sizes, dpi)
+ if not self._in_draw:
+ self._sizes3d = sizes
+
+ def set_linewidth(self, lw):
+ super().set_linewidth(lw)
+ if not self._in_draw:
+ self._linewidths3d = np.array(self._linewidths)
+
+ def get_depthshade(self):
+ return self._depthshade
+
+ def set_depthshade(self, depthshade):
+ """
+ Set whether depth shading is performed on collection members.
+
+ Parameters
+ ----------
+ depthshade : bool
+ Whether to shade the patches in order to give the appearance of
+ depth.
+ """
+ self._depthshade = depthshade
+ self.stale = True
+
+ def do_3d_projection(self):
+ xs, ys, zs = self._offsets3d
+ vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs,
+ self.axes.M)
+ # Sort the points based on z coordinates
+ # Performance optimization: Create a sorted index array and reorder
+ # points and point properties according to the index array
+ z_markers_idx = self._z_markers_idx = np.argsort(vzs)[::-1]
+ self._vzs = vzs
+
+ # we have to special case the sizes because of code in collections.py
+ # as the draw method does
+ # self.set_sizes(self._sizes, self.figure.dpi)
+ # so we cannot rely on doing the sorting on the way out via get_*
+
+ if len(self._sizes3d) > 1:
+ self._sizes = self._sizes3d[z_markers_idx]
+
+ if len(self._linewidths3d) > 1:
+ self._linewidths = self._linewidths3d[z_markers_idx]
+
+ PathCollection.set_offsets(self, np.column_stack((vxs, vys)))
+
+ # Re-order items
+ vzs = vzs[z_markers_idx]
+ vxs = vxs[z_markers_idx]
+ vys = vys[z_markers_idx]
+
+ # Store ordered offset for drawing purpose
+ self._offset_zordered = np.column_stack((vxs, vys))
+
+ return np.min(vzs) if vzs.size else np.nan
+
+ @contextmanager
+ def _use_zordered_offset(self):
+ if self._offset_zordered is None:
+ # Do nothing
+ yield
+ else:
+ # Swap offset with z-ordered offset
+ old_offset = self._offsets
+ super().set_offsets(self._offset_zordered)
+ try:
+ yield
+ finally:
+ self._offsets = old_offset
+
+ def _maybe_depth_shade_and_sort_colors(self, color_array):
+ color_array = (
+ _zalpha(color_array, self._vzs)
+ if self._vzs is not None and self._depthshade
+ else color_array
+ )
+ if len(color_array) > 1:
+ color_array = color_array[self._z_markers_idx]
+ return mcolors.to_rgba_array(color_array, self._alpha)
+
+ def get_facecolor(self):
+ return self._maybe_depth_shade_and_sort_colors(super().get_facecolor())
+
+ def get_edgecolor(self):
+ # We need this check here to make sure we do not double-apply the depth
+ # based alpha shading when the edge color is "face" which means the
+ # edge colour should be identical to the face colour.
+ if cbook._str_equal(self._edgecolors, 'face'):
+ return self.get_facecolor()
+ return self._maybe_depth_shade_and_sort_colors(super().get_edgecolor())
+
+
+def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True):
+ """
+ Convert a `.PatchCollection` into a `.Patch3DCollection` object
+ (or a `.PathCollection` into a `.Path3DCollection` object).
+
+ Parameters
+ ----------
+ zs : float or array of floats
+ The location or locations to place the patches in the collection along
+ the *zdir* axis. Default: 0.
+ zdir : {'x', 'y', 'z'}
+ The axis in which to place the patches. Default: "z".
+ See `.get_dir_vector` for a description of the values.
+ depthshade
+ Whether to shade the patches to give a sense of depth. Default: *True*.
+
+ """
+ if isinstance(col, PathCollection):
+ col.__class__ = Path3DCollection
+ col._offset_zordered = None
+ elif isinstance(col, PatchCollection):
+ col.__class__ = Patch3DCollection
+ col._depthshade = depthshade
+ col._in_draw = False
+ col.set_3d_properties(zs, zdir)
+
+
+class Poly3DCollection(PolyCollection):
+ """
+ A collection of 3D polygons.
+
+ .. note::
+ **Filling of 3D polygons**
+
+ There is no simple definition of the enclosed surface of a 3D polygon
+ unless the polygon is planar.
+
+ In practice, Matplotlib fills the 2D projection of the polygon. This
+ gives a correct filling appearance only for planar polygons. For all
+ other polygons, you'll find orientations in which the edges of the
+ polygon intersect in the projection. This will lead to an incorrect
+ visualization of the 3D area.
+
+ If you need filled areas, it is recommended to create them via
+ `~mpl_toolkits.mplot3d.axes3d.Axes3D.plot_trisurf`, which creates a
+ triangulation and thus generates consistent surfaces.
+ """
+
+ def __init__(self, verts, *args, zsort='average', shade=False,
+ lightsource=None, **kwargs):
+ """
+ Parameters
+ ----------
+ verts : list of (N, 3) array-like
+ The sequence of polygons [*verts0*, *verts1*, ...] where each
+ element *verts_i* defines the vertices of polygon *i* as a 2D
+ array-like of shape (N, 3).
+ zsort : {'average', 'min', 'max'}, default: 'average'
+ The calculation method for the z-order.
+ See `~.Poly3DCollection.set_zsort` for details.
+ shade : bool, default: False
+ Whether to shade *facecolors* and *edgecolors*. When activating
+ *shade*, *facecolors* and/or *edgecolors* must be provided.
+
+ .. versionadded:: 3.7
+
+ lightsource : `~matplotlib.colors.LightSource`, optional
+ The lightsource to use when *shade* is True.
+
+ .. versionadded:: 3.7
+
+ *args, **kwargs
+ All other parameters are forwarded to `.PolyCollection`.
+
+ Notes
+ -----
+ Note that this class does a bit of magic with the _facecolors
+ and _edgecolors properties.
+ """
+ if shade:
+ normals = _generate_normals(verts)
+ facecolors = kwargs.get('facecolors', None)
+ if facecolors is not None:
+ kwargs['facecolors'] = _shade_colors(
+ facecolors, normals, lightsource
+ )
+
+ edgecolors = kwargs.get('edgecolors', None)
+ if edgecolors is not None:
+ kwargs['edgecolors'] = _shade_colors(
+ edgecolors, normals, lightsource
+ )
+ if facecolors is None and edgecolors is None:
+ raise ValueError(
+ "You must provide facecolors, edgecolors, or both for "
+ "shade to work.")
+ super().__init__(verts, *args, **kwargs)
+ if isinstance(verts, np.ndarray):
+ if verts.ndim != 3:
+ raise ValueError('verts must be a list of (N, 3) array-like')
+ else:
+ if any(len(np.shape(vert)) != 2 for vert in verts):
+ raise ValueError('verts must be a list of (N, 3) array-like')
+ self.set_zsort(zsort)
+ self._codes3d = None
+
+ _zsort_functions = {
+ 'average': np.average,
+ 'min': np.min,
+ 'max': np.max,
+ }
+
+ def set_zsort(self, zsort):
+ """
+ Set the calculation method for the z-order.
+
+ Parameters
+ ----------
+ zsort : {'average', 'min', 'max'}
+ The function applied on the z-coordinates of the vertices in the
+ viewer's coordinate system, to determine the z-order.
+ """
+ self._zsortfunc = self._zsort_functions[zsort]
+ self._sort_zpos = None
+ self.stale = True
+
+ def get_vector(self, segments3d):
+ """Optimize points for projection."""
+ if len(segments3d):
+ xs, ys, zs = np.vstack(segments3d).T
+ else: # vstack can't stack zero arrays.
+ xs, ys, zs = [], [], []
+ ones = np.ones(len(xs))
+ self._vec = np.array([xs, ys, zs, ones])
+
+ indices = [0, *np.cumsum([len(segment) for segment in segments3d])]
+ self._segslices = [*map(slice, indices[:-1], indices[1:])]
+
+ def set_verts(self, verts, closed=True):
+ """
+ Set 3D vertices.
+
+ Parameters
+ ----------
+ verts : list of (N, 3) array-like
+ The sequence of polygons [*verts0*, *verts1*, ...] where each
+ element *verts_i* defines the vertices of polygon *i* as a 2D
+ array-like of shape (N, 3).
+ closed : bool, default: True
+ Whether the polygon should be closed by adding a CLOSEPOLY
+ connection at the end.
+ """
+ self.get_vector(verts)
+ # 2D verts will be updated at draw time
+ super().set_verts([], False)
+ self._closed = closed
+
+ def set_verts_and_codes(self, verts, codes):
+ """Set 3D vertices with path codes."""
+ # set vertices with closed=False to prevent PolyCollection from
+ # setting path codes
+ self.set_verts(verts, closed=False)
+ # and set our own codes instead.
+ self._codes3d = codes
+
+ def set_3d_properties(self):
+ # Force the collection to initialize the face and edgecolors
+ # just in case it is a scalarmappable with a colormap.
+ self.update_scalarmappable()
+ self._sort_zpos = None
+ self.set_zsort('average')
+ self._facecolor3d = PolyCollection.get_facecolor(self)
+ self._edgecolor3d = PolyCollection.get_edgecolor(self)
+ self._alpha3d = PolyCollection.get_alpha(self)
+ self.stale = True
+
+ def set_sort_zpos(self, val):
+ """Set the position to use for z-sorting."""
+ self._sort_zpos = val
+ self.stale = True
+
+ def do_3d_projection(self):
+ """
+ Perform the 3D projection for this object.
+ """
+ if self._A is not None:
+ # force update of color mapping because we re-order them
+ # below. If we do not do this here, the 2D draw will call
+ # this, but we will never port the color mapped values back
+ # to the 3D versions.
+ #
+ # We hold the 3D versions in a fixed order (the order the user
+ # passed in) and sort the 2D version by view depth.
+ self.update_scalarmappable()
+ if self._face_is_mapped:
+ self._facecolor3d = self._facecolors
+ if self._edge_is_mapped:
+ self._edgecolor3d = self._edgecolors
+ txs, tys, tzs = proj3d._proj_transform_vec(self._vec, self.axes.M)
+ xyzlist = [(txs[sl], tys[sl], tzs[sl]) for sl in self._segslices]
+
+ # This extra fuss is to re-order face / edge colors
+ cface = self._facecolor3d
+ cedge = self._edgecolor3d
+ if len(cface) != len(xyzlist):
+ cface = cface.repeat(len(xyzlist), axis=0)
+ if len(cedge) != len(xyzlist):
+ if len(cedge) == 0:
+ cedge = cface
+ else:
+ cedge = cedge.repeat(len(xyzlist), axis=0)
+
+ if xyzlist:
+ # sort by depth (furthest drawn first)
+ z_segments_2d = sorted(
+ ((self._zsortfunc(zs), np.column_stack([xs, ys]), fc, ec, idx)
+ for idx, ((xs, ys, zs), fc, ec)
+ in enumerate(zip(xyzlist, cface, cedge))),
+ key=lambda x: x[0], reverse=True)
+
+ _, segments_2d, self._facecolors2d, self._edgecolors2d, idxs = \
+ zip(*z_segments_2d)
+ else:
+ segments_2d = []
+ self._facecolors2d = np.empty((0, 4))
+ self._edgecolors2d = np.empty((0, 4))
+ idxs = []
+
+ if self._codes3d is not None:
+ codes = [self._codes3d[idx] for idx in idxs]
+ PolyCollection.set_verts_and_codes(self, segments_2d, codes)
+ else:
+ PolyCollection.set_verts(self, segments_2d, self._closed)
+
+ if len(self._edgecolor3d) != len(cface):
+ self._edgecolors2d = self._edgecolor3d
+
+ # Return zorder value
+ if self._sort_zpos is not None:
+ zvec = np.array([[0], [0], [self._sort_zpos], [1]])
+ ztrans = proj3d._proj_transform_vec(zvec, self.axes.M)
+ return ztrans[2][0]
+ elif tzs.size > 0:
+ # FIXME: Some results still don't look quite right.
+ # In particular, examine contourf3d_demo2.py
+ # with az = -54 and elev = -45.
+ return np.min(tzs)
+ else:
+ return np.nan
+
+ def set_facecolor(self, colors):
+ # docstring inherited
+ super().set_facecolor(colors)
+ self._facecolor3d = PolyCollection.get_facecolor(self)
+
+ def set_edgecolor(self, colors):
+ # docstring inherited
+ super().set_edgecolor(colors)
+ self._edgecolor3d = PolyCollection.get_edgecolor(self)
+
+ def set_alpha(self, alpha):
+ # docstring inherited
+ artist.Artist.set_alpha(self, alpha)
+ try:
+ self._facecolor3d = mcolors.to_rgba_array(
+ self._facecolor3d, self._alpha)
+ except (AttributeError, TypeError, IndexError):
+ pass
+ try:
+ self._edgecolors = mcolors.to_rgba_array(
+ self._edgecolor3d, self._alpha)
+ except (AttributeError, TypeError, IndexError):
+ pass
+ self.stale = True
+
+ def get_facecolor(self):
+ # docstring inherited
+ # self._facecolors2d is not initialized until do_3d_projection
+ if not hasattr(self, '_facecolors2d'):
+ self.axes.M = self.axes.get_proj()
+ self.do_3d_projection()
+ return np.asarray(self._facecolors2d)
+
+ def get_edgecolor(self):
+ # docstring inherited
+ # self._edgecolors2d is not initialized until do_3d_projection
+ if not hasattr(self, '_edgecolors2d'):
+ self.axes.M = self.axes.get_proj()
+ self.do_3d_projection()
+ return np.asarray(self._edgecolors2d)
+
+
+def poly_collection_2d_to_3d(col, zs=0, zdir='z'):
+ """
+ Convert a `.PolyCollection` into a `.Poly3DCollection` object.
+
+ Parameters
+ ----------
+ zs : float or array of floats
+ The location or locations to place the polygons in the collection along
+ the *zdir* axis. Default: 0.
+ zdir : {'x', 'y', 'z'}
+ The axis in which to place the patches. Default: 'z'.
+ See `.get_dir_vector` for a description of the values.
+ """
+ segments_3d, codes = _paths_to_3d_segments_with_codes(
+ col.get_paths(), zs, zdir)
+ col.__class__ = Poly3DCollection
+ col.set_verts_and_codes(segments_3d, codes)
+ col.set_3d_properties()
+
+
+def juggle_axes(xs, ys, zs, zdir):
+ """
+ Reorder coordinates so that 2D *xs*, *ys* can be plotted in the plane
+ orthogonal to *zdir*. *zdir* is normally 'x', 'y' or 'z'. However, if
+ *zdir* starts with a '-' it is interpreted as a compensation for
+ `rotate_axes`.
+ """
+ if zdir == 'x':
+ return zs, xs, ys
+ elif zdir == 'y':
+ return xs, zs, ys
+ elif zdir[0] == '-':
+ return rotate_axes(xs, ys, zs, zdir)
+ else:
+ return xs, ys, zs
+
+
+def rotate_axes(xs, ys, zs, zdir):
+ """
+ Reorder coordinates so that the axes are rotated with *zdir* along
+ the original z axis. Prepending the axis with a '-' does the
+ inverse transform, so *zdir* can be 'x', '-x', 'y', '-y', 'z' or '-z'.
+ """
+ if zdir in ('x', '-y'):
+ return ys, zs, xs
+ elif zdir in ('-x', 'y'):
+ return zs, xs, ys
+ else:
+ return xs, ys, zs
+
+
+def _zalpha(colors, zs):
+ """Modify the alphas of the color list according to depth."""
+ # FIXME: This only works well if the points for *zs* are well-spaced
+ # in all three dimensions. Otherwise, at certain orientations,
+ # the min and max zs are very close together.
+ # Should really normalize against the viewing depth.
+ if len(colors) == 0 or len(zs) == 0:
+ return np.zeros((0, 4))
+ norm = Normalize(min(zs), max(zs))
+ sats = 1 - norm(zs) * 0.7
+ rgba = np.broadcast_to(mcolors.to_rgba_array(colors), (len(zs), 4))
+ return np.column_stack([rgba[:, :3], rgba[:, 3] * sats])
+
+
+def _generate_normals(polygons):
+ """
+ Compute the normals of a list of polygons, one normal per polygon.
+
+ Normals point towards the viewer for a face with its vertices in
+ counterclockwise order, following the right hand rule.
+
+ Uses three points equally spaced around the polygon. This method assumes
+ that the points are in a plane. Otherwise, more than one shade is required,
+ which is not supported.
+
+ Parameters
+ ----------
+ polygons : list of (M_i, 3) array-like, or (..., M, 3) array-like
+ A sequence of polygons to compute normals for, which can have
+ varying numbers of vertices. If the polygons all have the same
+ number of vertices and array is passed, then the operation will
+ be vectorized.
+
+ Returns
+ -------
+ normals : (..., 3) array
+ A normal vector estimated for the polygon.
+ """
+ if isinstance(polygons, np.ndarray):
+ # optimization: polygons all have the same number of points, so can
+ # vectorize
+ n = polygons.shape[-2]
+ i1, i2, i3 = 0, n//3, 2*n//3
+ v1 = polygons[..., i1, :] - polygons[..., i2, :]
+ v2 = polygons[..., i2, :] - polygons[..., i3, :]
+ else:
+ # The subtraction doesn't vectorize because polygons is jagged.
+ v1 = np.empty((len(polygons), 3))
+ v2 = np.empty((len(polygons), 3))
+ for poly_i, ps in enumerate(polygons):
+ n = len(ps)
+ i1, i2, i3 = 0, n//3, 2*n//3
+ v1[poly_i, :] = ps[i1, :] - ps[i2, :]
+ v2[poly_i, :] = ps[i2, :] - ps[i3, :]
+ return np.cross(v1, v2)
+
+
+def _shade_colors(color, normals, lightsource=None):
+ """
+ Shade *color* using normal vectors given by *normals*,
+ assuming a *lightsource* (using default position if not given).
+ *color* can also be an array of the same length as *normals*.
+ """
+ if lightsource is None:
+ # chosen for backwards-compatibility
+ lightsource = mcolors.LightSource(azdeg=225, altdeg=19.4712)
+
+ with np.errstate(invalid="ignore"):
+ shade = ((normals / np.linalg.norm(normals, axis=1, keepdims=True))
+ @ lightsource.direction)
+ mask = ~np.isnan(shade)
+
+ if mask.any():
+ # convert dot product to allowed shading fractions
+ in_norm = mcolors.Normalize(-1, 1)
+ out_norm = mcolors.Normalize(0.3, 1).inverse
+
+ def norm(x):
+ return out_norm(in_norm(x))
+
+ shade[~mask] = 0
+
+ color = mcolors.to_rgba_array(color)
+ # shape of color should be (M, 4) (where M is number of faces)
+ # shape of shade should be (M,)
+ # colors should have final shape of (M, 4)
+ alpha = color[:, 3]
+ colors = norm(shade)[:, np.newaxis] * color
+ colors[:, 3] = alpha
+ else:
+ colors = np.asanyarray(color).copy()
+
+ return colors
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/axes3d.py b/contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/axes3d.py
new file mode 100644
index 0000000000..aeb6a66d2c
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/axes3d.py
@@ -0,0 +1,3448 @@
+"""
+axes3d.py, original mplot3d version by John Porter
+Created: 23 Sep 2005
+
+Parts fixed by Reinier Heeres <reinier@heeres.eu>
+Minor additions by Ben Axelrod <baxelrod@coroware.com>
+Significant updates and revisions by Ben Root <ben.v.root@gmail.com>
+
+Module containing Axes3D, an object which can plot 3D objects on a
+2D matplotlib figure.
+"""
+
+from collections import defaultdict
+import functools
+import itertools
+import math
+import textwrap
+
+import numpy as np
+
+import matplotlib as mpl
+from matplotlib import _api, cbook, _docstring, _preprocess_data
+import matplotlib.artist as martist
+import matplotlib.axes as maxes
+import matplotlib.collections as mcoll
+import matplotlib.colors as mcolors
+import matplotlib.image as mimage
+import matplotlib.lines as mlines
+import matplotlib.patches as mpatches
+import matplotlib.container as mcontainer
+import matplotlib.transforms as mtransforms
+from matplotlib.axes import Axes
+from matplotlib.axes._base import _axis_method_wrapper, _process_plot_format
+from matplotlib.transforms import Bbox
+from matplotlib.tri._triangulation import Triangulation
+
+from . import art3d
+from . import proj3d
+from . import axis3d
+
+
+@_docstring.interpd
+@_api.define_aliases({
+ "xlim": ["xlim3d"], "ylim": ["ylim3d"], "zlim": ["zlim3d"]})
+class Axes3D(Axes):
+ """
+ 3D Axes object.
+
+ .. note::
+
+ As a user, you do not instantiate Axes directly, but use Axes creation
+ methods instead; e.g. from `.pyplot` or `.Figure`:
+ `~.pyplot.subplots`, `~.pyplot.subplot_mosaic` or `.Figure.add_axes`.
+ """
+ name = '3d'
+
+ _axis_names = ("x", "y", "z")
+ Axes._shared_axes["z"] = cbook.Grouper()
+ Axes._shared_axes["view"] = cbook.Grouper()
+
+ vvec = _api.deprecate_privatize_attribute("3.7")
+ eye = _api.deprecate_privatize_attribute("3.7")
+ sx = _api.deprecate_privatize_attribute("3.7")
+ sy = _api.deprecate_privatize_attribute("3.7")
+
+ def __init__(
+ self, fig, rect=None, *args,
+ elev=30, azim=-60, roll=0, sharez=None, proj_type='persp',
+ box_aspect=None, computed_zorder=True, focal_length=None,
+ shareview=None,
+ **kwargs):
+ """
+ Parameters
+ ----------
+ fig : Figure
+ The parent figure.
+ rect : tuple (left, bottom, width, height), default: None.
+ The ``(left, bottom, width, height)`` axes position.
+ elev : float, default: 30
+ The elevation angle in degrees rotates the camera above and below
+ the x-y plane, with a positive angle corresponding to a location
+ above the plane.
+ azim : float, default: -60
+ The azimuthal angle in degrees rotates the camera about the z axis,
+ with a positive angle corresponding to a right-handed rotation. In
+ other words, a positive azimuth rotates the camera about the origin
+ from its location along the +x axis towards the +y axis.
+ roll : float, default: 0
+ The roll angle in degrees rotates the camera about the viewing
+ axis. A positive angle spins the camera clockwise, causing the
+ scene to rotate counter-clockwise.
+ sharez : Axes3D, optional
+ Other Axes to share z-limits with.
+ proj_type : {'persp', 'ortho'}
+ The projection type, default 'persp'.
+ box_aspect : 3-tuple of floats, default: None
+ Changes the physical dimensions of the Axes3D, such that the ratio
+ of the axis lengths in display units is x:y:z.
+ If None, defaults to 4:4:3
+ computed_zorder : bool, default: True
+ If True, the draw order is computed based on the average position
+ of the `.Artist`\\s along the view direction.
+ Set to False if you want to manually control the order in which
+ Artists are drawn on top of each other using their *zorder*
+ attribute. This can be used for fine-tuning if the automatic order
+ does not produce the desired result. Note however, that a manual
+ zorder will only be correct for a limited view angle. If the figure
+ is rotated by the user, it will look wrong from certain angles.
+ focal_length : float, default: None
+ For a projection type of 'persp', the focal length of the virtual
+ camera. Must be > 0. If None, defaults to 1.
+ For a projection type of 'ortho', must be set to either None
+ or infinity (numpy.inf). If None, defaults to infinity.
+ The focal length can be computed from a desired Field Of View via
+ the equation: focal_length = 1/tan(FOV/2)
+ shareview : Axes3D, optional
+ Other Axes to share view angles with.
+
+ **kwargs
+ Other optional keyword arguments:
+
+ %(Axes3D:kwdoc)s
+ """
+
+ if rect is None:
+ rect = [0.0, 0.0, 1.0, 1.0]
+
+ self.initial_azim = azim
+ self.initial_elev = elev
+ self.initial_roll = roll
+ self.set_proj_type(proj_type, focal_length)
+ self.computed_zorder = computed_zorder
+
+ self.xy_viewLim = Bbox.unit()
+ self.zz_viewLim = Bbox.unit()
+ self.xy_dataLim = Bbox.unit()
+ # z-limits are encoded in the x-component of the Bbox, y is un-used
+ self.zz_dataLim = Bbox.unit()
+
+ # inhibit autoscale_view until the axes are defined
+ # they can't be defined until Axes.__init__ has been called
+ self.view_init(self.initial_elev, self.initial_azim, self.initial_roll)
+
+ self._sharez = sharez
+ if sharez is not None:
+ self._shared_axes["z"].join(self, sharez)
+ self._adjustable = 'datalim'
+
+ self._shareview = shareview
+ if shareview is not None:
+ self._shared_axes["view"].join(self, shareview)
+
+ if kwargs.pop('auto_add_to_figure', False):
+ raise AttributeError(
+ 'auto_add_to_figure is no longer supported for Axes3D. '
+ 'Use fig.add_axes(ax) instead.'
+ )
+
+ super().__init__(
+ fig, rect, frameon=True, box_aspect=box_aspect, *args, **kwargs
+ )
+ # Disable drawing of axes by base class
+ super().set_axis_off()
+ # Enable drawing of axes by Axes3D class
+ self.set_axis_on()
+ self.M = None
+ self.invM = None
+
+ # func used to format z -- fall back on major formatters
+ self.fmt_zdata = None
+
+ self.mouse_init()
+ self.figure.canvas.callbacks._connect_picklable(
+ 'motion_notify_event', self._on_move)
+ self.figure.canvas.callbacks._connect_picklable(
+ 'button_press_event', self._button_press)
+ self.figure.canvas.callbacks._connect_picklable(
+ 'button_release_event', self._button_release)
+ self.set_top_view()
+
+ self.patch.set_linewidth(0)
+ # Calculate the pseudo-data width and height
+ pseudo_bbox = self.transLimits.inverted().transform([(0, 0), (1, 1)])
+ self._pseudo_w, self._pseudo_h = pseudo_bbox[1] - pseudo_bbox[0]
+
+ # mplot3d currently manages its own spines and needs these turned off
+ # for bounding box calculations
+ self.spines[:].set_visible(False)
+
+ def set_axis_off(self):
+ self._axis3don = False
+ self.stale = True
+
+ def set_axis_on(self):
+ self._axis3don = True
+ self.stale = True
+
+ def convert_zunits(self, z):
+ """
+ For artists in an Axes, if the zaxis has units support,
+ convert *z* using zaxis unit type
+ """
+ return self.zaxis.convert_units(z)
+
+ def set_top_view(self):
+ # this happens to be the right view for the viewing coordinates
+ # moved up and to the left slightly to fit labels and axes
+ xdwl = 0.95 / self._dist
+ xdw = 0.9 / self._dist
+ ydwl = 0.95 / self._dist
+ ydw = 0.9 / self._dist
+ # Set the viewing pane.
+ self.viewLim.intervalx = (-xdwl, xdw)
+ self.viewLim.intervaly = (-ydwl, ydw)
+ self.stale = True
+
+ def _init_axis(self):
+ """Init 3D axes; overrides creation of regular X/Y axes."""
+ self.xaxis = axis3d.XAxis(self)
+ self.yaxis = axis3d.YAxis(self)
+ self.zaxis = axis3d.ZAxis(self)
+
+ def get_zaxis(self):
+ """Return the ``ZAxis`` (`~.axis3d.Axis`) instance."""
+ return self.zaxis
+
+ get_zgridlines = _axis_method_wrapper("zaxis", "get_gridlines")
+ get_zticklines = _axis_method_wrapper("zaxis", "get_ticklines")
+
+ @_api.deprecated("3.7")
+ def unit_cube(self, vals=None):
+ return self._unit_cube(vals)
+
+ def _unit_cube(self, vals=None):
+ minx, maxx, miny, maxy, minz, maxz = vals or self.get_w_lims()
+ return [(minx, miny, minz),
+ (maxx, miny, minz),
+ (maxx, maxy, minz),
+ (minx, maxy, minz),
+ (minx, miny, maxz),
+ (maxx, miny, maxz),
+ (maxx, maxy, maxz),
+ (minx, maxy, maxz)]
+
+ @_api.deprecated("3.7")
+ def tunit_cube(self, vals=None, M=None):
+ return self._tunit_cube(vals, M)
+
+ def _tunit_cube(self, vals=None, M=None):
+ if M is None:
+ M = self.M
+ xyzs = self._unit_cube(vals)
+ tcube = proj3d._proj_points(xyzs, M)
+ return tcube
+
+ @_api.deprecated("3.7")
+ def tunit_edges(self, vals=None, M=None):
+ return self._tunit_edges(vals, M)
+
+ def _tunit_edges(self, vals=None, M=None):
+ tc = self._tunit_cube(vals, M)
+ edges = [(tc[0], tc[1]),
+ (tc[1], tc[2]),
+ (tc[2], tc[3]),
+ (tc[3], tc[0]),
+
+ (tc[0], tc[4]),
+ (tc[1], tc[5]),
+ (tc[2], tc[6]),
+ (tc[3], tc[7]),
+
+ (tc[4], tc[5]),
+ (tc[5], tc[6]),
+ (tc[6], tc[7]),
+ (tc[7], tc[4])]
+ return edges
+
+ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
+ """
+ Set the aspect ratios.
+
+ Parameters
+ ----------
+ aspect : {'auto', 'equal', 'equalxy', 'equalxz', 'equalyz'}
+ Possible values:
+
+ ========= ==================================================
+ value description
+ ========= ==================================================
+ 'auto' automatic; fill the position rectangle with data.
+ 'equal' adapt all the axes to have equal aspect ratios.
+ 'equalxy' adapt the x and y axes to have equal aspect ratios.
+ 'equalxz' adapt the x and z axes to have equal aspect ratios.
+ 'equalyz' adapt the y and z axes to have equal aspect ratios.
+ ========= ==================================================
+
+ adjustable : None or {'box', 'datalim'}, optional
+ If not *None*, this defines which parameter will be adjusted to
+ meet the required aspect. See `.set_adjustable` for further
+ details.
+
+ anchor : None or str or 2-tuple of float, optional
+ If not *None*, this defines where the Axes will be drawn if there
+ is extra space due to aspect constraints. The most common way to
+ specify the anchor are abbreviations of cardinal directions:
+
+ ===== =====================
+ value description
+ ===== =====================
+ 'C' centered
+ 'SW' lower left corner
+ 'S' middle of bottom edge
+ 'SE' lower right corner
+ etc.
+ ===== =====================
+
+ See `~.Axes.set_anchor` for further details.
+
+ share : bool, default: False
+ If ``True``, apply the settings to all shared Axes.
+
+ See Also
+ --------
+ mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect
+ """
+ _api.check_in_list(('auto', 'equal', 'equalxy', 'equalyz', 'equalxz'),
+ aspect=aspect)
+ super().set_aspect(
+ aspect='auto', adjustable=adjustable, anchor=anchor, share=share)
+ self._aspect = aspect
+
+ if aspect in ('equal', 'equalxy', 'equalxz', 'equalyz'):
+ ax_indices = self._equal_aspect_axis_indices(aspect)
+
+ view_intervals = np.array([self.xaxis.get_view_interval(),
+ self.yaxis.get_view_interval(),
+ self.zaxis.get_view_interval()])
+ ptp = np.ptp(view_intervals, axis=1)
+ if self._adjustable == 'datalim':
+ mean = np.mean(view_intervals, axis=1)
+ scale = max(ptp[ax_indices] / self._box_aspect[ax_indices])
+ deltas = scale * self._box_aspect
+
+ for i, set_lim in enumerate((self.set_xlim3d,
+ self.set_ylim3d,
+ self.set_zlim3d)):
+ if i in ax_indices:
+ set_lim(mean[i] - deltas[i]/2., mean[i] + deltas[i]/2.)
+ else: # 'box'
+ # Change the box aspect such that the ratio of the length of
+ # the unmodified axis to the length of the diagonal
+ # perpendicular to it remains unchanged.
+ box_aspect = np.array(self._box_aspect)
+ box_aspect[ax_indices] = ptp[ax_indices]
+ remaining_ax_indices = {0, 1, 2}.difference(ax_indices)
+ if remaining_ax_indices:
+ remaining = remaining_ax_indices.pop()
+ old_diag = np.linalg.norm(self._box_aspect[ax_indices])
+ new_diag = np.linalg.norm(box_aspect[ax_indices])
+ box_aspect[remaining] *= new_diag / old_diag
+ self.set_box_aspect(box_aspect)
+
+ def _equal_aspect_axis_indices(self, aspect):
+ """
+ Get the indices for which of the x, y, z axes are constrained to have
+ equal aspect ratios.
+
+ Parameters
+ ----------
+ aspect : {'auto', 'equal', 'equalxy', 'equalxz', 'equalyz'}
+ See descriptions in docstring for `.set_aspect()`.
+ """
+ ax_indices = [] # aspect == 'auto'
+ if aspect == 'equal':
+ ax_indices = [0, 1, 2]
+ elif aspect == 'equalxy':
+ ax_indices = [0, 1]
+ elif aspect == 'equalxz':
+ ax_indices = [0, 2]
+ elif aspect == 'equalyz':
+ ax_indices = [1, 2]
+ return ax_indices
+
+ def set_box_aspect(self, aspect, *, zoom=1):
+ """
+ Set the Axes box aspect.
+
+ The box aspect is the ratio of height to width in display
+ units for each face of the box when viewed perpendicular to
+ that face. This is not to be confused with the data aspect (see
+ `~.Axes3D.set_aspect`). The default ratios are 4:4:3 (x:y:z).
+
+ To simulate having equal aspect in data space, set the box
+ aspect to match your data range in each dimension.
+
+ *zoom* controls the overall size of the Axes3D in the figure.
+
+ Parameters
+ ----------
+ aspect : 3-tuple of floats or None
+ Changes the physical dimensions of the Axes3D, such that the ratio
+ of the axis lengths in display units is x:y:z.
+ If None, defaults to (4, 4, 3).
+
+ zoom : float, default: 1
+ Control overall size of the Axes3D in the figure. Must be > 0.
+ """
+ if zoom <= 0:
+ raise ValueError(f'Argument zoom = {zoom} must be > 0')
+
+ if aspect is None:
+ aspect = np.asarray((4, 4, 3), dtype=float)
+ else:
+ aspect = np.asarray(aspect, dtype=float)
+ _api.check_shape((3,), aspect=aspect)
+ # default scale tuned to match the mpl32 appearance.
+ aspect *= 1.8294640721620434 * zoom / np.linalg.norm(aspect)
+
+ self._box_aspect = aspect
+ self.stale = True
+
+ def apply_aspect(self, position=None):
+ if position is None:
+ position = self.get_position(original=True)
+
+ # in the superclass, we would go through and actually deal with axis
+ # scales and box/datalim. Those are all irrelevant - all we need to do
+ # is make sure our coordinate system is square.
+ trans = self.get_figure().transSubfigure
+ bb = mtransforms.Bbox.unit().transformed(trans)
+ # this is the physical aspect of the panel (or figure):
+ fig_aspect = bb.height / bb.width
+
+ box_aspect = 1
+ pb = position.frozen()
+ pb1 = pb.shrunk_to_aspect(box_aspect, pb, fig_aspect)
+ self._set_position(pb1.anchored(self.get_anchor(), pb), 'active')
+
+ @martist.allow_rasterization
+ def draw(self, renderer):
+ if not self.get_visible():
+ return
+ self._unstale_viewLim()
+
+ # draw the background patch
+ self.patch.draw(renderer)
+ self._frameon = False
+
+ # first, set the aspect
+ # this is duplicated from `axes._base._AxesBase.draw`
+ # but must be called before any of the artist are drawn as
+ # it adjusts the view limits and the size of the bounding box
+ # of the Axes
+ locator = self.get_axes_locator()
+ self.apply_aspect(locator(self, renderer) if locator else None)
+
+ # add the projection matrix to the renderer
+ self.M = self.get_proj()
+ self.invM = np.linalg.inv(self.M)
+
+ collections_and_patches = (
+ artist for artist in self._children
+ if isinstance(artist, (mcoll.Collection, mpatches.Patch))
+ and artist.get_visible())
+ if self.computed_zorder:
+ # Calculate projection of collections and patches and zorder
+ # them. Make sure they are drawn above the grids.
+ zorder_offset = max(axis.get_zorder()
+ for axis in self._axis_map.values()) + 1
+ collection_zorder = patch_zorder = zorder_offset
+
+ for artist in sorted(collections_and_patches,
+ key=lambda artist: artist.do_3d_projection(),
+ reverse=True):
+ if isinstance(artist, mcoll.Collection):
+ artist.zorder = collection_zorder
+ collection_zorder += 1
+ elif isinstance(artist, mpatches.Patch):
+ artist.zorder = patch_zorder
+ patch_zorder += 1
+ else:
+ for artist in collections_and_patches:
+ artist.do_3d_projection()
+
+ if self._axis3don:
+ # Draw panes first
+ for axis in self._axis_map.values():
+ axis.draw_pane(renderer)
+ # Then gridlines
+ for axis in self._axis_map.values():
+ axis.draw_grid(renderer)
+ # Then axes, labels, text, and ticks
+ for axis in self._axis_map.values():
+ axis.draw(renderer)
+
+ # Then rest
+ super().draw(renderer)
+
+ def get_axis_position(self):
+ vals = self.get_w_lims()
+ tc = self._tunit_cube(vals, self.M)
+ xhigh = tc[1][2] > tc[2][2]
+ yhigh = tc[3][2] > tc[2][2]
+ zhigh = tc[0][2] > tc[2][2]
+ return xhigh, yhigh, zhigh
+
+ def update_datalim(self, xys, **kwargs):
+ """
+ Not implemented in `~mpl_toolkits.mplot3d.axes3d.Axes3D`.
+ """
+ pass
+
+ get_autoscalez_on = _axis_method_wrapper("zaxis", "_get_autoscale_on")
+ set_autoscalez_on = _axis_method_wrapper("zaxis", "_set_autoscale_on")
+
+ def set_zmargin(self, m):
+ """
+ Set padding of Z data limits prior to autoscaling.
+
+ *m* times the data interval will be added to each end of that interval
+ before it is used in autoscaling. If *m* is negative, this will clip
+ the data range instead of expanding it.
+
+ For example, if your data is in the range [0, 2], a margin of 0.1 will
+ result in a range [-0.2, 2.2]; a margin of -0.1 will result in a range
+ of [0.2, 1.8].
+
+ Parameters
+ ----------
+ m : float greater than -0.5
+ """
+ if m <= -0.5:
+ raise ValueError("margin must be greater than -0.5")
+ self._zmargin = m
+ self._request_autoscale_view("z")
+ self.stale = True
+
+ def margins(self, *margins, x=None, y=None, z=None, tight=True):
+ """
+ Set or retrieve autoscaling margins.
+
+ See `.Axes.margins` for full documentation. Because this function
+ applies to 3D Axes, it also takes a *z* argument, and returns
+ ``(xmargin, ymargin, zmargin)``.
+ """
+ if margins and (x is not None or y is not None or z is not None):
+ raise TypeError('Cannot pass both positional and keyword '
+ 'arguments for x, y, and/or z.')
+ elif len(margins) == 1:
+ x = y = z = margins[0]
+ elif len(margins) == 3:
+ x, y, z = margins
+ elif margins:
+ raise TypeError('Must pass a single positional argument for all '
+ 'margins, or one for each margin (x, y, z).')
+
+ if x is None and y is None and z is None:
+ if tight is not True:
+ _api.warn_external(f'ignoring tight={tight!r} in get mode')
+ return self._xmargin, self._ymargin, self._zmargin
+
+ if x is not None:
+ self.set_xmargin(x)
+ if y is not None:
+ self.set_ymargin(y)
+ if z is not None:
+ self.set_zmargin(z)
+
+ self.autoscale_view(
+ tight=tight, scalex=(x is not None), scaley=(y is not None),
+ scalez=(z is not None)
+ )
+
+ def autoscale(self, enable=True, axis='both', tight=None):
+ """
+ Convenience method for simple axis view autoscaling.
+
+ See `.Axes.autoscale` for full documentation. Because this function
+ applies to 3D Axes, *axis* can also be set to 'z', and setting *axis*
+ to 'both' autoscales all three axes.
+ """
+ if enable is None:
+ scalex = True
+ scaley = True
+ scalez = True
+ else:
+ if axis in ['x', 'both']:
+ self.set_autoscalex_on(bool(enable))
+ scalex = self.get_autoscalex_on()
+ else:
+ scalex = False
+ if axis in ['y', 'both']:
+ self.set_autoscaley_on(bool(enable))
+ scaley = self.get_autoscaley_on()
+ else:
+ scaley = False
+ if axis in ['z', 'both']:
+ self.set_autoscalez_on(bool(enable))
+ scalez = self.get_autoscalez_on()
+ else:
+ scalez = False
+ if scalex:
+ self._request_autoscale_view("x", tight=tight)
+ if scaley:
+ self._request_autoscale_view("y", tight=tight)
+ if scalez:
+ self._request_autoscale_view("z", tight=tight)
+
+ def auto_scale_xyz(self, X, Y, Z=None, had_data=None):
+ # This updates the bounding boxes as to keep a record as to what the
+ # minimum sized rectangular volume holds the data.
+ if np.shape(X) == np.shape(Y):
+ self.xy_dataLim.update_from_data_xy(
+ np.column_stack([np.ravel(X), np.ravel(Y)]), not had_data)
+ else:
+ self.xy_dataLim.update_from_data_x(X, not had_data)
+ self.xy_dataLim.update_from_data_y(Y, not had_data)
+ if Z is not None:
+ self.zz_dataLim.update_from_data_x(Z, not had_data)
+ # Let autoscale_view figure out how to use this data.
+ self.autoscale_view()
+
+ def autoscale_view(self, tight=None, scalex=True, scaley=True,
+ scalez=True):
+ """
+ Autoscale the view limits using the data limits.
+
+ See `.Axes.autoscale_view` for full documentation. Because this
+ function applies to 3D Axes, it also takes a *scalez* argument.
+ """
+ # This method looks at the rectangular volume (see above)
+ # of data and decides how to scale the view portal to fit it.
+ if tight is None:
+ _tight = self._tight
+ if not _tight:
+ # if image data only just use the datalim
+ for artist in self._children:
+ if isinstance(artist, mimage.AxesImage):
+ _tight = True
+ elif isinstance(artist, (mlines.Line2D, mpatches.Patch)):
+ _tight = False
+ break
+ else:
+ _tight = self._tight = bool(tight)
+
+ if scalex and self.get_autoscalex_on():
+ x0, x1 = self.xy_dataLim.intervalx
+ xlocator = self.xaxis.get_major_locator()
+ x0, x1 = xlocator.nonsingular(x0, x1)
+ if self._xmargin > 0:
+ delta = (x1 - x0) * self._xmargin
+ x0 -= delta
+ x1 += delta
+ if not _tight:
+ x0, x1 = xlocator.view_limits(x0, x1)
+ self.set_xbound(x0, x1)
+
+ if scaley and self.get_autoscaley_on():
+ y0, y1 = self.xy_dataLim.intervaly
+ ylocator = self.yaxis.get_major_locator()
+ y0, y1 = ylocator.nonsingular(y0, y1)
+ if self._ymargin > 0:
+ delta = (y1 - y0) * self._ymargin
+ y0 -= delta
+ y1 += delta
+ if not _tight:
+ y0, y1 = ylocator.view_limits(y0, y1)
+ self.set_ybound(y0, y1)
+
+ if scalez and self.get_autoscalez_on():
+ z0, z1 = self.zz_dataLim.intervalx
+ zlocator = self.zaxis.get_major_locator()
+ z0, z1 = zlocator.nonsingular(z0, z1)
+ if self._zmargin > 0:
+ delta = (z1 - z0) * self._zmargin
+ z0 -= delta
+ z1 += delta
+ if not _tight:
+ z0, z1 = zlocator.view_limits(z0, z1)
+ self.set_zbound(z0, z1)
+
+ def get_w_lims(self):
+ """Get 3D world limits."""
+ minx, maxx = self.get_xlim3d()
+ miny, maxy = self.get_ylim3d()
+ minz, maxz = self.get_zlim3d()
+ return minx, maxx, miny, maxy, minz, maxz
+
+ # set_xlim, set_ylim are directly inherited from base Axes.
+ def set_zlim(self, bottom=None, top=None, *, emit=True, auto=False,
+ zmin=None, zmax=None):
+ """
+ Set 3D z limits.
+
+ See `.Axes.set_ylim` for full documentation
+ """
+ if top is None and np.iterable(bottom):
+ bottom, top = bottom
+ if zmin is not None:
+ if bottom is not None:
+ raise TypeError("Cannot pass both 'bottom' and 'zmin'")
+ bottom = zmin
+ if zmax is not None:
+ if top is not None:
+ raise TypeError("Cannot pass both 'top' and 'zmax'")
+ top = zmax
+ return self.zaxis._set_lim(bottom, top, emit=emit, auto=auto)
+
+ set_xlim3d = maxes.Axes.set_xlim
+ set_ylim3d = maxes.Axes.set_ylim
+ set_zlim3d = set_zlim
+
+ def get_xlim(self):
+ # docstring inherited
+ return tuple(self.xy_viewLim.intervalx)
+
+ def get_ylim(self):
+ # docstring inherited
+ return tuple(self.xy_viewLim.intervaly)
+
+ def get_zlim(self):
+ """
+ Return the 3D z-axis view limits.
+
+ Returns
+ -------
+ left, right : (float, float)
+ The current z-axis limits in data coordinates.
+
+ See Also
+ --------
+ set_zlim
+ set_zbound, get_zbound
+ invert_zaxis, zaxis_inverted
+
+ Notes
+ -----
+ The z-axis may be inverted, in which case the *left* value will
+ be greater than the *right* value.
+ """
+ return tuple(self.zz_viewLim.intervalx)
+
+ get_zscale = _axis_method_wrapper("zaxis", "get_scale")
+
+ # Redefine all three methods to overwrite their docstrings.
+ set_xscale = _axis_method_wrapper("xaxis", "_set_axes_scale")
+ set_yscale = _axis_method_wrapper("yaxis", "_set_axes_scale")
+ set_zscale = _axis_method_wrapper("zaxis", "_set_axes_scale")
+ set_xscale.__doc__, set_yscale.__doc__, set_zscale.__doc__ = map(
+ """
+ Set the {}-axis scale.
+
+ Parameters
+ ----------
+ value : {{"linear"}}
+ The axis scale type to apply. 3D axes currently only support
+ linear scales; other scales yield nonsensical results.
+
+ **kwargs
+ Keyword arguments are nominally forwarded to the scale class, but
+ none of them is applicable for linear scales.
+ """.format,
+ ["x", "y", "z"])
+
+ get_zticks = _axis_method_wrapper("zaxis", "get_ticklocs")
+ set_zticks = _axis_method_wrapper("zaxis", "set_ticks")
+ get_zmajorticklabels = _axis_method_wrapper("zaxis", "get_majorticklabels")
+ get_zminorticklabels = _axis_method_wrapper("zaxis", "get_minorticklabels")
+ get_zticklabels = _axis_method_wrapper("zaxis", "get_ticklabels")
+ set_zticklabels = _axis_method_wrapper(
+ "zaxis", "set_ticklabels",
+ doc_sub={"Axis.set_ticks": "Axes3D.set_zticks"})
+
+ zaxis_date = _axis_method_wrapper("zaxis", "axis_date")
+ if zaxis_date.__doc__:
+ zaxis_date.__doc__ += textwrap.dedent("""
+
+ Notes
+ -----
+ This function is merely provided for completeness, but 3D axes do not
+ support dates for ticks, and so this may not work as expected.
+ """)
+
+ def clabel(self, *args, **kwargs):
+ """Currently not implemented for 3D axes, and returns *None*."""
+ return None
+
+ def view_init(self, elev=None, azim=None, roll=None, vertical_axis="z",
+ share=False):
+ """
+ Set the elevation and azimuth of the axes in degrees (not radians).
+
+ This can be used to rotate the axes programmatically.
+
+ To look normal to the primary planes, the following elevation and
+ azimuth angles can be used. A roll angle of 0, 90, 180, or 270 deg
+ will rotate these views while keeping the axes at right angles.
+
+ ========== ==== ====
+ view plane elev azim
+ ========== ==== ====
+ XY 90 -90
+ XZ 0 -90
+ YZ 0 0
+ -XY -90 90
+ -XZ 0 90
+ -YZ 0 180
+ ========== ==== ====
+
+ Parameters
+ ----------
+ elev : float, default: None
+ The elevation angle in degrees rotates the camera above the plane
+ pierced by the vertical axis, with a positive angle corresponding
+ to a location above that plane. For example, with the default
+ vertical axis of 'z', the elevation defines the angle of the camera
+ location above the x-y plane.
+ If None, then the initial value as specified in the `Axes3D`
+ constructor is used.
+ azim : float, default: None
+ The azimuthal angle in degrees rotates the camera about the
+ vertical axis, with a positive angle corresponding to a
+ right-handed rotation. For example, with the default vertical axis
+ of 'z', a positive azimuth rotates the camera about the origin from
+ its location along the +x axis towards the +y axis.
+ If None, then the initial value as specified in the `Axes3D`
+ constructor is used.
+ roll : float, default: None
+ The roll angle in degrees rotates the camera about the viewing
+ axis. A positive angle spins the camera clockwise, causing the
+ scene to rotate counter-clockwise.
+ If None, then the initial value as specified in the `Axes3D`
+ constructor is used.
+ vertical_axis : {"z", "x", "y"}, default: "z"
+ The axis to align vertically. *azim* rotates about this axis.
+ share : bool, default: False
+ If ``True``, apply the settings to all Axes with shared views.
+ """
+
+ self._dist = 10 # The camera distance from origin. Behaves like zoom
+
+ if elev is None:
+ elev = self.initial_elev
+ if azim is None:
+ azim = self.initial_azim
+ if roll is None:
+ roll = self.initial_roll
+ vertical_axis = _api.check_getitem(
+ dict(x=0, y=1, z=2), vertical_axis=vertical_axis
+ )
+
+ if share:
+ axes = {sibling for sibling
+ in self._shared_axes['view'].get_siblings(self)}
+ else:
+ axes = [self]
+
+ for ax in axes:
+ ax.elev = elev
+ ax.azim = azim
+ ax.roll = roll
+ ax._vertical_axis = vertical_axis
+
+ def set_proj_type(self, proj_type, focal_length=None):
+ """
+ Set the projection type.
+
+ Parameters
+ ----------
+ proj_type : {'persp', 'ortho'}
+ The projection type.
+ focal_length : float, default: None
+ For a projection type of 'persp', the focal length of the virtual
+ camera. Must be > 0. If None, defaults to 1.
+ The focal length can be computed from a desired Field Of View via
+ the equation: focal_length = 1/tan(FOV/2)
+ """
+ _api.check_in_list(['persp', 'ortho'], proj_type=proj_type)
+ if proj_type == 'persp':
+ if focal_length is None:
+ focal_length = 1
+ elif focal_length <= 0:
+ raise ValueError(f"focal_length = {focal_length} must be "
+ "greater than 0")
+ self._focal_length = focal_length
+ else: # 'ortho':
+ if focal_length not in (None, np.inf):
+ raise ValueError(f"focal_length = {focal_length} must be "
+ f"None for proj_type = {proj_type}")
+ self._focal_length = np.inf
+
+ def _roll_to_vertical(self, arr):
+ """Roll arrays to match the different vertical axis."""
+ return np.roll(arr, self._vertical_axis - 2)
+
+ def get_proj(self):
+ """Create the projection matrix from the current viewing position."""
+
+ # Transform to uniform world coordinates 0-1, 0-1, 0-1
+ box_aspect = self._roll_to_vertical(self._box_aspect)
+ worldM = proj3d.world_transformation(
+ *self.get_xlim3d(),
+ *self.get_ylim3d(),
+ *self.get_zlim3d(),
+ pb_aspect=box_aspect,
+ )
+
+ # Look into the middle of the world coordinates:
+ R = 0.5 * box_aspect
+
+ # elev: elevation angle in the z plane.
+ # azim: azimuth angle in the xy plane.
+ # Coordinates for a point that rotates around the box of data.
+ # p0, p1 corresponds to rotating the box only around the vertical axis.
+ # p2 corresponds to rotating the box only around the horizontal axis.
+ elev_rad = np.deg2rad(self.elev)
+ azim_rad = np.deg2rad(self.azim)
+ p0 = np.cos(elev_rad) * np.cos(azim_rad)
+ p1 = np.cos(elev_rad) * np.sin(azim_rad)
+ p2 = np.sin(elev_rad)
+
+ # When changing vertical axis the coordinates changes as well.
+ # Roll the values to get the same behaviour as the default:
+ ps = self._roll_to_vertical([p0, p1, p2])
+
+ # The coordinates for the eye viewing point. The eye is looking
+ # towards the middle of the box of data from a distance:
+ eye = R + self._dist * ps
+
+ # vvec, self._vvec and self._eye are unused, remove when deprecated
+ vvec = R - eye
+ self._eye = eye
+ self._vvec = vvec / np.linalg.norm(vvec)
+
+ # Calculate the viewing axes for the eye position
+ u, v, w = self._calc_view_axes(eye)
+ self._view_u = u # _view_u is towards the right of the screen
+ self._view_v = v # _view_v is towards the top of the screen
+ self._view_w = w # _view_w is out of the screen
+
+ # Generate the view and projection transformation matrices
+ if self._focal_length == np.inf:
+ # Orthographic projection
+ viewM = proj3d._view_transformation_uvw(u, v, w, eye)
+ projM = proj3d._ortho_transformation(-self._dist, self._dist)
+ else:
+ # Perspective projection
+ # Scale the eye dist to compensate for the focal length zoom effect
+ eye_focal = R + self._dist * ps * self._focal_length
+ viewM = proj3d._view_transformation_uvw(u, v, w, eye_focal)
+ projM = proj3d._persp_transformation(-self._dist,
+ self._dist,
+ self._focal_length)
+
+ # Combine all the transformation matrices to get the final projection
+ M0 = np.dot(viewM, worldM)
+ M = np.dot(projM, M0)
+ return M
+
+ def mouse_init(self, rotate_btn=1, pan_btn=2, zoom_btn=3):
+ """
+ Set the mouse buttons for 3D rotation and zooming.
+
+ Parameters
+ ----------
+ rotate_btn : int or list of int, default: 1
+ The mouse button or buttons to use for 3D rotation of the axes.
+ pan_btn : int or list of int, default: 2
+ The mouse button or buttons to use to pan the 3D axes.
+ zoom_btn : int or list of int, default: 3
+ The mouse button or buttons to use to zoom the 3D axes.
+ """
+ self.button_pressed = None
+ # coerce scalars into array-like, then convert into
+ # a regular list to avoid comparisons against None
+ # which breaks in recent versions of numpy.
+ self._rotate_btn = np.atleast_1d(rotate_btn).tolist()
+ self._pan_btn = np.atleast_1d(pan_btn).tolist()
+ self._zoom_btn = np.atleast_1d(zoom_btn).tolist()
+
+ def disable_mouse_rotation(self):
+ """Disable mouse buttons for 3D rotation, panning, and zooming."""
+ self.mouse_init(rotate_btn=[], pan_btn=[], zoom_btn=[])
+
+ def can_zoom(self):
+ # doc-string inherited
+ return True
+
+ def can_pan(self):
+ # doc-string inherited
+ return True
+
+ def sharez(self, other):
+ """
+ Share the z-axis with *other*.
+
+ This is equivalent to passing ``sharez=other`` when constructing the
+ Axes, and cannot be used if the z-axis is already being shared with
+ another Axes.
+ """
+ _api.check_isinstance(Axes3D, other=other)
+ if self._sharez is not None and other is not self._sharez:
+ raise ValueError("z-axis is already shared")
+ self._shared_axes["z"].join(self, other)
+ self._sharez = other
+ self.zaxis.major = other.zaxis.major # Ticker instances holding
+ self.zaxis.minor = other.zaxis.minor # locator and formatter.
+ z0, z1 = other.get_zlim()
+ self.set_zlim(z0, z1, emit=False, auto=other.get_autoscalez_on())
+ self.zaxis._scale = other.zaxis._scale
+
+ def shareview(self, other):
+ """
+ Share the view angles with *other*.
+
+ This is equivalent to passing ``shareview=other`` when
+ constructing the Axes, and cannot be used if the view angles are
+ already being shared with another Axes.
+ """
+ _api.check_isinstance(Axes3D, other=other)
+ if self._shareview is not None and other is not self._shareview:
+ raise ValueError("view angles are already shared")
+ self._shared_axes["view"].join(self, other)
+ self._shareview = other
+ vertical_axis = {0: "x", 1: "y", 2: "z"}[other._vertical_axis]
+ self.view_init(elev=other.elev, azim=other.azim, roll=other.roll,
+ vertical_axis=vertical_axis, share=True)
+
+ def clear(self):
+ # docstring inherited.
+ super().clear()
+ if self._focal_length == np.inf:
+ self._zmargin = mpl.rcParams['axes.zmargin']
+ else:
+ self._zmargin = 0.
+ self.grid(mpl.rcParams['axes3d.grid'])
+
+ def _button_press(self, event):
+ if event.inaxes == self:
+ self.button_pressed = event.button
+ self._sx, self._sy = event.xdata, event.ydata
+ toolbar = self.figure.canvas.toolbar
+ if toolbar and toolbar._nav_stack() is None:
+ toolbar.push_current()
+
+ def _button_release(self, event):
+ self.button_pressed = None
+ toolbar = self.figure.canvas.toolbar
+ # backend_bases.release_zoom and backend_bases.release_pan call
+ # push_current, so check the navigation mode so we don't call it twice
+ if toolbar and self.get_navigate_mode() is None:
+ toolbar.push_current()
+
+ def _get_view(self):
+ # docstring inherited
+ return {
+ "xlim": self.get_xlim(), "autoscalex_on": self.get_autoscalex_on(),
+ "ylim": self.get_ylim(), "autoscaley_on": self.get_autoscaley_on(),
+ "zlim": self.get_zlim(), "autoscalez_on": self.get_autoscalez_on(),
+ }, (self.elev, self.azim, self.roll)
+
+ def _set_view(self, view):
+ # docstring inherited
+ props, (elev, azim, roll) = view
+ self.set(**props)
+ self.elev = elev
+ self.azim = azim
+ self.roll = roll
+
+ def format_zdata(self, z):
+ """
+ Return *z* string formatted. This function will use the
+ :attr:`fmt_zdata` attribute if it is callable, else will fall
+ back on the zaxis major formatter
+ """
+ try:
+ return self.fmt_zdata(z)
+ except (AttributeError, TypeError):
+ func = self.zaxis.get_major_formatter().format_data_short
+ val = func(z)
+ return val
+
+ def format_coord(self, xv, yv, renderer=None):
+ """
+ Return a string giving the current view rotation angles, or the x, y, z
+ coordinates of the point on the nearest axis pane underneath the mouse
+ cursor, depending on the mouse button pressed.
+ """
+ coords = ''
+
+ if self.button_pressed in self._rotate_btn:
+ # ignore xv and yv and display angles instead
+ coords = self._rotation_coords()
+
+ elif self.M is not None:
+ coords = self._location_coords(xv, yv, renderer)
+
+ return coords
+
+ def _rotation_coords(self):
+ """
+ Return the rotation angles as a string.
+ """
+ norm_elev = art3d._norm_angle(self.elev)
+ norm_azim = art3d._norm_angle(self.azim)
+ norm_roll = art3d._norm_angle(self.roll)
+ coords = (f"elevation={norm_elev:.0f}\N{DEGREE SIGN}, "
+ f"azimuth={norm_azim:.0f}\N{DEGREE SIGN}, "
+ f"roll={norm_roll:.0f}\N{DEGREE SIGN}"
+ ).replace("-", "\N{MINUS SIGN}")
+ return coords
+
+ def _location_coords(self, xv, yv, renderer):
+ """
+ Return the location on the axis pane underneath the cursor as a string.
+ """
+ p1, pane_idx = self._calc_coord(xv, yv, renderer)
+ xs = self.format_xdata(p1[0])
+ ys = self.format_ydata(p1[1])
+ zs = self.format_zdata(p1[2])
+ if pane_idx == 0:
+ coords = f'x pane={xs}, y={ys}, z={zs}'
+ elif pane_idx == 1:
+ coords = f'x={xs}, y pane={ys}, z={zs}'
+ elif pane_idx == 2:
+ coords = f'x={xs}, y={ys}, z pane={zs}'
+ return coords
+
+ def _get_camera_loc(self):
+ """
+ Returns the current camera location in data coordinates.
+ """
+ cx, cy, cz, dx, dy, dz = self._get_w_centers_ranges()
+ c = np.array([cx, cy, cz])
+ r = np.array([dx, dy, dz])
+
+ if self._focal_length == np.inf: # orthographic projection
+ focal_length = 1e9 # large enough to be effectively infinite
+ else: # perspective projection
+ focal_length = self._focal_length
+ eye = c + self._view_w * self._dist * r / self._box_aspect * focal_length
+ return eye
+
+ def _calc_coord(self, xv, yv, renderer=None):
+ """
+ Given the 2D view coordinates, find the point on the nearest axis pane
+ that lies directly below those coordinates. Returns a 3D point in data
+ coordinates.
+ """
+ if self._focal_length == np.inf: # orthographic projection
+ zv = 1
+ else: # perspective projection
+ zv = -1 / self._focal_length
+
+ # Convert point on view plane to data coordinates
+ p1 = np.array(proj3d.inv_transform(xv, yv, zv, self.invM)).ravel()
+
+ # Get the vector from the camera to the point on the view plane
+ vec = self._get_camera_loc() - p1
+
+ # Get the pane locations for each of the axes
+ pane_locs = []
+ for axis in self._axis_map.values():
+ xys, loc = axis.active_pane(renderer)
+ pane_locs.append(loc)
+
+ # Find the distance to the nearest pane by projecting the view vector
+ scales = np.zeros(3)
+ for i in range(3):
+ if vec[i] == 0:
+ scales[i] = np.inf
+ else:
+ scales[i] = (p1[i] - pane_locs[i]) / vec[i]
+ pane_idx = np.argmin(abs(scales))
+ scale = scales[pane_idx]
+
+ # Calculate the point on the closest pane
+ p2 = p1 - scale*vec
+ return p2, pane_idx
+
+ def _on_move(self, event):
+ """
+ Mouse moving.
+
+ By default, button-1 rotates, button-2 pans, and button-3 zooms;
+ these buttons can be modified via `mouse_init`.
+ """
+
+ if not self.button_pressed:
+ return
+
+ if self.get_navigate_mode() is not None:
+ # we don't want to rotate if we are zooming/panning
+ # from the toolbar
+ return
+
+ if self.M is None:
+ return
+
+ x, y = event.xdata, event.ydata
+ # In case the mouse is out of bounds.
+ if x is None or event.inaxes != self:
+ return
+
+ dx, dy = x - self._sx, y - self._sy
+ w = self._pseudo_w
+ h = self._pseudo_h
+
+ # Rotation
+ if self.button_pressed in self._rotate_btn:
+ # rotate viewing point
+ # get the x and y pixel coords
+ if dx == 0 and dy == 0:
+ return
+
+ roll = np.deg2rad(self.roll)
+ delev = -(dy/h)*180*np.cos(roll) + (dx/w)*180*np.sin(roll)
+ dazim = -(dy/h)*180*np.sin(roll) - (dx/w)*180*np.cos(roll)
+ elev = self.elev + delev
+ azim = self.azim + dazim
+ self.view_init(elev=elev, azim=azim, roll=roll, share=True)
+ self.stale = True
+
+ # Pan
+ elif self.button_pressed in self._pan_btn:
+ # Start the pan event with pixel coordinates
+ px, py = self.transData.transform([self._sx, self._sy])
+ self.start_pan(px, py, 2)
+ # pan view (takes pixel coordinate input)
+ self.drag_pan(2, None, event.x, event.y)
+ self.end_pan()
+
+ # Zoom
+ elif self.button_pressed in self._zoom_btn:
+ # zoom view (dragging down zooms in)
+ scale = h/(h - dy)
+ self._scale_axis_limits(scale, scale, scale)
+
+ # Store the event coordinates for the next time through.
+ self._sx, self._sy = x, y
+ # Always request a draw update at the end of interaction
+ self.figure.canvas.draw_idle()
+
+ def drag_pan(self, button, key, x, y):
+ # docstring inherited
+
+ # Get the coordinates from the move event
+ p = self._pan_start
+ (xdata, ydata), (xdata_start, ydata_start) = p.trans_inverse.transform(
+ [(x, y), (p.x, p.y)])
+ self._sx, self._sy = xdata, ydata
+ # Calling start_pan() to set the x/y of this event as the starting
+ # move location for the next event
+ self.start_pan(x, y, button)
+ du, dv = xdata - xdata_start, ydata - ydata_start
+ dw = 0
+ if key == 'x':
+ dv = 0
+ elif key == 'y':
+ du = 0
+ if du == 0 and dv == 0:
+ return
+
+ # Transform the pan from the view axes to the data axes
+ R = np.array([self._view_u, self._view_v, self._view_w])
+ R = -R / self._box_aspect * self._dist
+ duvw_projected = R.T @ np.array([du, dv, dw])
+
+ # Calculate pan distance
+ minx, maxx, miny, maxy, minz, maxz = self.get_w_lims()
+ dx = (maxx - minx) * duvw_projected[0]
+ dy = (maxy - miny) * duvw_projected[1]
+ dz = (maxz - minz) * duvw_projected[2]
+
+ # Set the new axis limits
+ self.set_xlim3d(minx + dx, maxx + dx)
+ self.set_ylim3d(miny + dy, maxy + dy)
+ self.set_zlim3d(minz + dz, maxz + dz)
+
+ def _calc_view_axes(self, eye):
+ """
+ Get the unit vectors for the viewing axes in data coordinates.
+ `u` is towards the right of the screen
+ `v` is towards the top of the screen
+ `w` is out of the screen
+ """
+ elev_rad = np.deg2rad(art3d._norm_angle(self.elev))
+ roll_rad = np.deg2rad(art3d._norm_angle(self.roll))
+
+ # Look into the middle of the world coordinates
+ R = 0.5 * self._roll_to_vertical(self._box_aspect)
+
+ # Define which axis should be vertical. A negative value
+ # indicates the plot is upside down and therefore the values
+ # have been reversed:
+ V = np.zeros(3)
+ V[self._vertical_axis] = -1 if abs(elev_rad) > np.pi/2 else 1
+
+ u, v, w = proj3d._view_axes(eye, R, V, roll_rad)
+ return u, v, w
+
+ def _set_view_from_bbox(self, bbox, direction='in',
+ mode=None, twinx=False, twiny=False):
+ """
+ Zoom in or out of the bounding box.
+
+ Will center the view in the center of the bounding box, and zoom by
+ the ratio of the size of the bounding box to the size of the Axes3D.
+ """
+ (start_x, start_y, stop_x, stop_y) = bbox
+ if mode == 'x':
+ start_y = self.bbox.min[1]
+ stop_y = self.bbox.max[1]
+ elif mode == 'y':
+ start_x = self.bbox.min[0]
+ stop_x = self.bbox.max[0]
+
+ # Clip to bounding box limits
+ start_x, stop_x = np.clip(sorted([start_x, stop_x]),
+ self.bbox.min[0], self.bbox.max[0])
+ start_y, stop_y = np.clip(sorted([start_y, stop_y]),
+ self.bbox.min[1], self.bbox.max[1])
+
+ # Move the center of the view to the center of the bbox
+ zoom_center_x = (start_x + stop_x)/2
+ zoom_center_y = (start_y + stop_y)/2
+
+ ax_center_x = (self.bbox.max[0] + self.bbox.min[0])/2
+ ax_center_y = (self.bbox.max[1] + self.bbox.min[1])/2
+
+ self.start_pan(zoom_center_x, zoom_center_y, 2)
+ self.drag_pan(2, None, ax_center_x, ax_center_y)
+ self.end_pan()
+
+ # Calculate zoom level
+ dx = abs(start_x - stop_x)
+ dy = abs(start_y - stop_y)
+ scale_u = dx / (self.bbox.max[0] - self.bbox.min[0])
+ scale_v = dy / (self.bbox.max[1] - self.bbox.min[1])
+
+ # Keep aspect ratios equal
+ scale = max(scale_u, scale_v)
+
+ # Zoom out
+ if direction == 'out':
+ scale = 1 / scale
+
+ self._zoom_data_limits(scale, scale, scale)
+
+ def _zoom_data_limits(self, scale_u, scale_v, scale_w):
+ """
+ Zoom in or out of a 3D plot.
+
+ Will scale the data limits by the scale factors. These will be
+ transformed to the x, y, z data axes based on the current view angles.
+ A scale factor > 1 zooms out and a scale factor < 1 zooms in.
+
+ For an axes that has had its aspect ratio set to 'equal', 'equalxy',
+ 'equalyz', or 'equalxz', the relevant axes are constrained to zoom
+ equally.
+
+ Parameters
+ ----------
+ scale_u : float
+ Scale factor for the u view axis (view screen horizontal).
+ scale_v : float
+ Scale factor for the v view axis (view screen vertical).
+ scale_w : float
+ Scale factor for the w view axis (view screen depth).
+ """
+ scale = np.array([scale_u, scale_v, scale_w])
+
+ # Only perform frame conversion if unequal scale factors
+ if not np.allclose(scale, scale_u):
+ # Convert the scale factors from the view frame to the data frame
+ R = np.array([self._view_u, self._view_v, self._view_w])
+ S = scale * np.eye(3)
+ scale = np.linalg.norm(R.T @ S, axis=1)
+
+ # Set the constrained scale factors to the factor closest to 1
+ if self._aspect in ('equal', 'equalxy', 'equalxz', 'equalyz'):
+ ax_idxs = self._equal_aspect_axis_indices(self._aspect)
+ min_ax_idxs = np.argmin(np.abs(scale[ax_idxs] - 1))
+ scale[ax_idxs] = scale[ax_idxs][min_ax_idxs]
+
+ self._scale_axis_limits(scale[0], scale[1], scale[2])
+
+ def _scale_axis_limits(self, scale_x, scale_y, scale_z):
+ """
+ Keeping the center of the x, y, and z data axes fixed, scale their
+ limits by scale factors. A scale factor > 1 zooms out and a scale
+ factor < 1 zooms in.
+
+ Parameters
+ ----------
+ scale_x : float
+ Scale factor for the x data axis.
+ scale_y : float
+ Scale factor for the y data axis.
+ scale_z : float
+ Scale factor for the z data axis.
+ """
+ # Get the axis centers and ranges
+ cx, cy, cz, dx, dy, dz = self._get_w_centers_ranges()
+
+ # Set the scaled axis limits
+ self.set_xlim3d(cx - dx*scale_x/2, cx + dx*scale_x/2)
+ self.set_ylim3d(cy - dy*scale_y/2, cy + dy*scale_y/2)
+ self.set_zlim3d(cz - dz*scale_z/2, cz + dz*scale_z/2)
+
+ def _get_w_centers_ranges(self):
+ """Get 3D world centers and axis ranges."""
+ # Calculate center of axis limits
+ minx, maxx, miny, maxy, minz, maxz = self.get_w_lims()
+ cx = (maxx + minx)/2
+ cy = (maxy + miny)/2
+ cz = (maxz + minz)/2
+
+ # Calculate range of axis limits
+ dx = (maxx - minx)
+ dy = (maxy - miny)
+ dz = (maxz - minz)
+ return cx, cy, cz, dx, dy, dz
+
+ def set_zlabel(self, zlabel, fontdict=None, labelpad=None, **kwargs):
+ """
+ Set zlabel. See doc for `.set_ylabel` for description.
+ """
+ if labelpad is not None:
+ self.zaxis.labelpad = labelpad
+ return self.zaxis.set_label_text(zlabel, fontdict, **kwargs)
+
+ def get_zlabel(self):
+ """
+ Get the z-label text string.
+ """
+ label = self.zaxis.get_label()
+ return label.get_text()
+
+ # Axes rectangle characteristics
+
+ # The frame_on methods are not available for 3D axes.
+ # Python will raise a TypeError if they are called.
+ get_frame_on = None
+ set_frame_on = None
+
+ def grid(self, visible=True, **kwargs):
+ """
+ Set / unset 3D grid.
+
+ .. note::
+
+ Currently, this function does not behave the same as
+ `.axes.Axes.grid`, but it is intended to eventually support that
+ behavior.
+ """
+ # TODO: Operate on each axes separately
+ if len(kwargs):
+ visible = True
+ self._draw_grid = visible
+ self.stale = True
+
+ def tick_params(self, axis='both', **kwargs):
+ """
+ Convenience method for changing the appearance of ticks and
+ tick labels.
+
+ See `.Axes.tick_params` for full documentation. Because this function
+ applies to 3D Axes, *axis* can also be set to 'z', and setting *axis*
+ to 'both' autoscales all three axes.
+
+ Also, because of how Axes3D objects are drawn very differently
+ from regular 2D axes, some of these settings may have
+ ambiguous meaning. For simplicity, the 'z' axis will
+ accept settings as if it was like the 'y' axis.
+
+ .. note::
+ Axes3D currently ignores some of these settings.
+ """
+ _api.check_in_list(['x', 'y', 'z', 'both'], axis=axis)
+ if axis in ['x', 'y', 'both']:
+ super().tick_params(axis, **kwargs)
+ if axis in ['z', 'both']:
+ zkw = dict(kwargs)
+ zkw.pop('top', None)
+ zkw.pop('bottom', None)
+ zkw.pop('labeltop', None)
+ zkw.pop('labelbottom', None)
+ self.zaxis.set_tick_params(**zkw)
+
+ # data limits, ticks, tick labels, and formatting
+
+ def invert_zaxis(self):
+ """
+ Invert the z-axis.
+
+ See Also
+ --------
+ zaxis_inverted
+ get_zlim, set_zlim
+ get_zbound, set_zbound
+ """
+ bottom, top = self.get_zlim()
+ self.set_zlim(top, bottom, auto=None)
+
+ zaxis_inverted = _axis_method_wrapper("zaxis", "get_inverted")
+
+ def get_zbound(self):
+ """
+ Return the lower and upper z-axis bounds, in increasing order.
+
+ See Also
+ --------
+ set_zbound
+ get_zlim, set_zlim
+ invert_zaxis, zaxis_inverted
+ """
+ bottom, top = self.get_zlim()
+ if bottom < top:
+ return bottom, top
+ else:
+ return top, bottom
+
+ def set_zbound(self, lower=None, upper=None):
+ """
+ Set the lower and upper numerical bounds of the z-axis.
+
+ This method will honor axes inversion regardless of parameter order.
+ It will not change the autoscaling setting (`.get_autoscalez_on()`).
+
+ Parameters
+ ----------
+ lower, upper : float or None
+ The lower and upper bounds. If *None*, the respective axis bound
+ is not modified.
+
+ See Also
+ --------
+ get_zbound
+ get_zlim, set_zlim
+ invert_zaxis, zaxis_inverted
+ """
+ if upper is None and np.iterable(lower):
+ lower, upper = lower
+
+ old_lower, old_upper = self.get_zbound()
+ if lower is None:
+ lower = old_lower
+ if upper is None:
+ upper = old_upper
+
+ self.set_zlim(sorted((lower, upper),
+ reverse=bool(self.zaxis_inverted())),
+ auto=None)
+
+ def text(self, x, y, z, s, zdir=None, **kwargs):
+ """
+ Add the text *s* to the 3D Axes at location *x*, *y*, *z* in data coordinates.
+
+ Parameters
+ ----------
+ x, y, z : float
+ The position to place the text.
+ s : str
+ The text.
+ zdir : {'x', 'y', 'z', 3-tuple}, optional
+ The direction to be used as the z-direction. Default: 'z'.
+ See `.get_dir_vector` for a description of the values.
+ **kwargs
+ Other arguments are forwarded to `matplotlib.axes.Axes.text`.
+
+ Returns
+ -------
+ `.Text3D`
+ The created `.Text3D` instance.
+ """
+ text = super().text(x, y, s, **kwargs)
+ art3d.text_2d_to_3d(text, z, zdir)
+ return text
+
+ text3D = text
+ text2D = Axes.text
+
+ def plot(self, xs, ys, *args, zdir='z', **kwargs):
+ """
+ Plot 2D or 3D data.
+
+ Parameters
+ ----------
+ xs : 1D array-like
+ x coordinates of vertices.
+ ys : 1D array-like
+ y coordinates of vertices.
+ zs : float or 1D array-like
+ z coordinates of vertices; either one for all points or one for
+ each point.
+ zdir : {'x', 'y', 'z'}, default: 'z'
+ When plotting 2D data, the direction to use as z.
+ **kwargs
+ Other arguments are forwarded to `matplotlib.axes.Axes.plot`.
+ """
+ had_data = self.has_data()
+
+ # `zs` can be passed positionally or as keyword; checking whether
+ # args[0] is a string matches the behavior of 2D `plot` (via
+ # `_process_plot_var_args`).
+ if args and not isinstance(args[0], str):
+ zs, *args = args
+ if 'zs' in kwargs:
+ raise TypeError("plot() for multiple values for argument 'z'")
+ else:
+ zs = kwargs.pop('zs', 0)
+
+ # Match length
+ zs = np.broadcast_to(zs, np.shape(xs))
+
+ lines = super().plot(xs, ys, *args, **kwargs)
+ for line in lines:
+ art3d.line_2d_to_3d(line, zs=zs, zdir=zdir)
+
+ xs, ys, zs = art3d.juggle_axes(xs, ys, zs, zdir)
+ self.auto_scale_xyz(xs, ys, zs, had_data)
+ return lines
+
+ plot3D = plot
+
+ def plot_surface(self, X, Y, Z, *, norm=None, vmin=None,
+ vmax=None, lightsource=None, **kwargs):
+ """
+ Create a surface plot.
+
+ By default, it will be colored in shades of a solid color, but it also
+ supports colormapping by supplying the *cmap* argument.
+
+ .. note::
+
+ The *rcount* and *ccount* kwargs, which both default to 50,
+ determine the maximum number of samples used in each direction. If
+ the input data is larger, it will be downsampled (by slicing) to
+ these numbers of points.
+
+ .. note::
+
+ To maximize rendering speed consider setting *rstride* and *cstride*
+ to divisors of the number of rows minus 1 and columns minus 1
+ respectively. For example, given 51 rows rstride can be any of the
+ divisors of 50.
+
+ Similarly, a setting of *rstride* and *cstride* equal to 1 (or
+ *rcount* and *ccount* equal the number of rows and columns) can use
+ the optimized path.
+
+ Parameters
+ ----------
+ X, Y, Z : 2D arrays
+ Data values.
+
+ rcount, ccount : int
+ Maximum number of samples used in each direction. If the input
+ data is larger, it will be downsampled (by slicing) to these
+ numbers of points. Defaults to 50.
+
+ rstride, cstride : int
+ Downsampling stride in each direction. These arguments are
+ mutually exclusive with *rcount* and *ccount*. If only one of
+ *rstride* or *cstride* is set, the other defaults to 10.
+
+ 'classic' mode uses a default of ``rstride = cstride = 10`` instead
+ of the new default of ``rcount = ccount = 50``.
+
+ color : color-like
+ Color of the surface patches.
+
+ cmap : Colormap
+ Colormap of the surface patches.
+
+ facecolors : array-like of colors.
+ Colors of each individual patch.
+
+ norm : Normalize
+ Normalization for the colormap.
+
+ vmin, vmax : float
+ Bounds for the normalization.
+
+ shade : bool, default: True
+ Whether to shade the facecolors. Shading is always disabled when
+ *cmap* is specified.
+
+ lightsource : `~matplotlib.colors.LightSource`
+ The lightsource to use when *shade* is True.
+
+ **kwargs
+ Other keyword arguments are forwarded to `.Poly3DCollection`.
+ """
+
+ had_data = self.has_data()
+
+ if Z.ndim != 2:
+ raise ValueError("Argument Z must be 2-dimensional.")
+
+ Z = cbook._to_unmasked_float_array(Z)
+ X, Y, Z = np.broadcast_arrays(X, Y, Z)
+ rows, cols = Z.shape
+
+ has_stride = 'rstride' in kwargs or 'cstride' in kwargs
+ has_count = 'rcount' in kwargs or 'ccount' in kwargs
+
+ if has_stride and has_count:
+ raise ValueError("Cannot specify both stride and count arguments")
+
+ rstride = kwargs.pop('rstride', 10)
+ cstride = kwargs.pop('cstride', 10)
+ rcount = kwargs.pop('rcount', 50)
+ ccount = kwargs.pop('ccount', 50)
+
+ if mpl.rcParams['_internal.classic_mode']:
+ # Strides have priority over counts in classic mode.
+ # So, only compute strides from counts
+ # if counts were explicitly given
+ compute_strides = has_count
+ else:
+ # If the strides are provided then it has priority.
+ # Otherwise, compute the strides from the counts.
+ compute_strides = not has_stride
+
+ if compute_strides:
+ rstride = int(max(np.ceil(rows / rcount), 1))
+ cstride = int(max(np.ceil(cols / ccount), 1))
+
+ fcolors = kwargs.pop('facecolors', None)
+
+ cmap = kwargs.get('cmap', None)
+ shade = kwargs.pop('shade', cmap is None)
+ if shade is None:
+ raise ValueError("shade cannot be None.")
+
+ colset = [] # the sampled facecolor
+ if (rows - 1) % rstride == 0 and \
+ (cols - 1) % cstride == 0 and \
+ fcolors is None:
+ polys = np.stack(
+ [cbook._array_patch_perimeters(a, rstride, cstride)
+ for a in (X, Y, Z)],
+ axis=-1)
+ else:
+ # evenly spaced, and including both endpoints
+ row_inds = list(range(0, rows-1, rstride)) + [rows-1]
+ col_inds = list(range(0, cols-1, cstride)) + [cols-1]
+
+ polys = []
+ for rs, rs_next in zip(row_inds[:-1], row_inds[1:]):
+ for cs, cs_next in zip(col_inds[:-1], col_inds[1:]):
+ ps = [
+ # +1 ensures we share edges between polygons
+ cbook._array_perimeter(a[rs:rs_next+1, cs:cs_next+1])
+ for a in (X, Y, Z)
+ ]
+ # ps = np.stack(ps, axis=-1)
+ ps = np.array(ps).T
+ polys.append(ps)
+
+ if fcolors is not None:
+ colset.append(fcolors[rs][cs])
+
+ # In cases where there are non-finite values in the data (possibly NaNs from
+ # masked arrays), artifacts can be introduced. Here check whether such values
+ # are present and remove them.
+ if not isinstance(polys, np.ndarray) or not np.isfinite(polys).all():
+ new_polys = []
+ new_colset = []
+
+ # Depending on fcolors, colset is either an empty list or has as
+ # many elements as polys. In the former case new_colset results in
+ # a list with None entries, that is discarded later.
+ for p, col in itertools.zip_longest(polys, colset):
+ new_poly = np.array(p)[np.isfinite(p).all(axis=1)]
+ if len(new_poly):
+ new_polys.append(new_poly)
+ new_colset.append(col)
+
+ # Replace previous polys and, if fcolors is not None, colset
+ polys = new_polys
+ if fcolors is not None:
+ colset = new_colset
+
+ # note that the striding causes some polygons to have more coordinates
+ # than others
+
+ if fcolors is not None:
+ polyc = art3d.Poly3DCollection(
+ polys, edgecolors=colset, facecolors=colset, shade=shade,
+ lightsource=lightsource, **kwargs)
+ elif cmap:
+ polyc = art3d.Poly3DCollection(polys, **kwargs)
+ # can't always vectorize, because polys might be jagged
+ if isinstance(polys, np.ndarray):
+ avg_z = polys[..., 2].mean(axis=-1)
+ else:
+ avg_z = np.array([ps[:, 2].mean() for ps in polys])
+ polyc.set_array(avg_z)
+ if vmin is not None or vmax is not None:
+ polyc.set_clim(vmin, vmax)
+ if norm is not None:
+ polyc.set_norm(norm)
+ else:
+ color = kwargs.pop('color', None)
+ if color is None:
+ color = self._get_lines.get_next_color()
+ color = np.array(mcolors.to_rgba(color))
+
+ polyc = art3d.Poly3DCollection(
+ polys, facecolors=color, shade=shade,
+ lightsource=lightsource, **kwargs)
+
+ self.add_collection(polyc)
+ self.auto_scale_xyz(X, Y, Z, had_data)
+
+ return polyc
+
+ def plot_wireframe(self, X, Y, Z, **kwargs):
+ """
+ Plot a 3D wireframe.
+
+ .. note::
+
+ The *rcount* and *ccount* kwargs, which both default to 50,
+ determine the maximum number of samples used in each direction. If
+ the input data is larger, it will be downsampled (by slicing) to
+ these numbers of points.
+
+ Parameters
+ ----------
+ X, Y, Z : 2D arrays
+ Data values.
+
+ rcount, ccount : int
+ Maximum number of samples used in each direction. If the input
+ data is larger, it will be downsampled (by slicing) to these
+ numbers of points. Setting a count to zero causes the data to be
+ not sampled in the corresponding direction, producing a 3D line
+ plot rather than a wireframe plot. Defaults to 50.
+
+ rstride, cstride : int
+ Downsampling stride in each direction. These arguments are
+ mutually exclusive with *rcount* and *ccount*. If only one of
+ *rstride* or *cstride* is set, the other defaults to 1. Setting a
+ stride to zero causes the data to be not sampled in the
+ corresponding direction, producing a 3D line plot rather than a
+ wireframe plot.
+
+ 'classic' mode uses a default of ``rstride = cstride = 1`` instead
+ of the new default of ``rcount = ccount = 50``.
+
+ **kwargs
+ Other keyword arguments are forwarded to `.Line3DCollection`.
+ """
+
+ had_data = self.has_data()
+ if Z.ndim != 2:
+ raise ValueError("Argument Z must be 2-dimensional.")
+ # FIXME: Support masked arrays
+ X, Y, Z = np.broadcast_arrays(X, Y, Z)
+ rows, cols = Z.shape
+
+ has_stride = 'rstride' in kwargs or 'cstride' in kwargs
+ has_count = 'rcount' in kwargs or 'ccount' in kwargs
+
+ if has_stride and has_count:
+ raise ValueError("Cannot specify both stride and count arguments")
+
+ rstride = kwargs.pop('rstride', 1)
+ cstride = kwargs.pop('cstride', 1)
+ rcount = kwargs.pop('rcount', 50)
+ ccount = kwargs.pop('ccount', 50)
+
+ if mpl.rcParams['_internal.classic_mode']:
+ # Strides have priority over counts in classic mode.
+ # So, only compute strides from counts
+ # if counts were explicitly given
+ if has_count:
+ rstride = int(max(np.ceil(rows / rcount), 1)) if rcount else 0
+ cstride = int(max(np.ceil(cols / ccount), 1)) if ccount else 0
+ else:
+ # If the strides are provided then it has priority.
+ # Otherwise, compute the strides from the counts.
+ if not has_stride:
+ rstride = int(max(np.ceil(rows / rcount), 1)) if rcount else 0
+ cstride = int(max(np.ceil(cols / ccount), 1)) if ccount else 0
+
+ # We want two sets of lines, one running along the "rows" of
+ # Z and another set of lines running along the "columns" of Z.
+ # This transpose will make it easy to obtain the columns.
+ tX, tY, tZ = np.transpose(X), np.transpose(Y), np.transpose(Z)
+
+ if rstride:
+ rii = list(range(0, rows, rstride))
+ # Add the last index only if needed
+ if rows > 0 and rii[-1] != (rows - 1):
+ rii += [rows-1]
+ else:
+ rii = []
+ if cstride:
+ cii = list(range(0, cols, cstride))
+ # Add the last index only if needed
+ if cols > 0 and cii[-1] != (cols - 1):
+ cii += [cols-1]
+ else:
+ cii = []
+
+ if rstride == 0 and cstride == 0:
+ raise ValueError("Either rstride or cstride must be non zero")
+
+ # If the inputs were empty, then just
+ # reset everything.
+ if Z.size == 0:
+ rii = []
+ cii = []
+
+ xlines = [X[i] for i in rii]
+ ylines = [Y[i] for i in rii]
+ zlines = [Z[i] for i in rii]
+
+ txlines = [tX[i] for i in cii]
+ tylines = [tY[i] for i in cii]
+ tzlines = [tZ[i] for i in cii]
+
+ lines = ([list(zip(xl, yl, zl))
+ for xl, yl, zl in zip(xlines, ylines, zlines)]
+ + [list(zip(xl, yl, zl))
+ for xl, yl, zl in zip(txlines, tylines, tzlines)])
+
+ linec = art3d.Line3DCollection(lines, **kwargs)
+ self.add_collection(linec)
+ self.auto_scale_xyz(X, Y, Z, had_data)
+
+ return linec
+
+ def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None,
+ lightsource=None, **kwargs):
+ """
+ Plot a triangulated surface.
+
+ The (optional) triangulation can be specified in one of two ways;
+ either::
+
+ plot_trisurf(triangulation, ...)
+
+ where triangulation is a `~matplotlib.tri.Triangulation` object, or::
+
+ plot_trisurf(X, Y, ...)
+ plot_trisurf(X, Y, triangles, ...)
+ plot_trisurf(X, Y, triangles=triangles, ...)
+
+ in which case a Triangulation object will be created. See
+ `.Triangulation` for an explanation of these possibilities.
+
+ The remaining arguments are::
+
+ plot_trisurf(..., Z)
+
+ where *Z* is the array of values to contour, one per point
+ in the triangulation.
+
+ Parameters
+ ----------
+ X, Y, Z : array-like
+ Data values as 1D arrays.
+ color
+ Color of the surface patches.
+ cmap
+ A colormap for the surface patches.
+ norm : Normalize
+ An instance of Normalize to map values to colors.
+ vmin, vmax : float, default: None
+ Minimum and maximum value to map.
+ shade : bool, default: True
+ Whether to shade the facecolors. Shading is always disabled when
+ *cmap* is specified.
+ lightsource : `~matplotlib.colors.LightSource`
+ The lightsource to use when *shade* is True.
+ **kwargs
+ All other keyword arguments are passed on to
+ :class:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection`
+
+ Examples
+ --------
+ .. plot:: gallery/mplot3d/trisurf3d.py
+ .. plot:: gallery/mplot3d/trisurf3d_2.py
+ """
+
+ had_data = self.has_data()
+
+ # TODO: Support custom face colours
+ if color is None:
+ color = self._get_lines.get_next_color()
+ color = np.array(mcolors.to_rgba(color))
+
+ cmap = kwargs.get('cmap', None)
+ shade = kwargs.pop('shade', cmap is None)
+
+ tri, args, kwargs = \
+ Triangulation.get_from_args_and_kwargs(*args, **kwargs)
+ try:
+ z = kwargs.pop('Z')
+ except KeyError:
+ # We do this so Z doesn't get passed as an arg to PolyCollection
+ z, *args = args
+ z = np.asarray(z)
+
+ triangles = tri.get_masked_triangles()
+ xt = tri.x[triangles]
+ yt = tri.y[triangles]
+ zt = z[triangles]
+ verts = np.stack((xt, yt, zt), axis=-1)
+
+ if cmap:
+ polyc = art3d.Poly3DCollection(verts, *args, **kwargs)
+ # average over the three points of each triangle
+ avg_z = verts[:, :, 2].mean(axis=1)
+ polyc.set_array(avg_z)
+ if vmin is not None or vmax is not None:
+ polyc.set_clim(vmin, vmax)
+ if norm is not None:
+ polyc.set_norm(norm)
+ else:
+ polyc = art3d.Poly3DCollection(
+ verts, *args, shade=shade, lightsource=lightsource,
+ facecolors=color, **kwargs)
+
+ self.add_collection(polyc)
+ self.auto_scale_xyz(tri.x, tri.y, z, had_data)
+
+ return polyc
+
+ def _3d_extend_contour(self, cset, stride=5):
+ """
+ Extend a contour in 3D by creating
+ """
+
+ dz = (cset.levels[1] - cset.levels[0]) / 2
+ polyverts = []
+ colors = []
+ for idx, level in enumerate(cset.levels):
+ path = cset.get_paths()[idx]
+ subpaths = [*path._iter_connected_components()]
+ color = cset.get_edgecolor()[idx]
+ top = art3d._paths_to_3d_segments(subpaths, level - dz)
+ bot = art3d._paths_to_3d_segments(subpaths, level + dz)
+ if not len(top[0]):
+ continue
+ nsteps = max(round(len(top[0]) / stride), 2)
+ stepsize = (len(top[0]) - 1) / (nsteps - 1)
+ polyverts.extend([
+ (top[0][round(i * stepsize)], top[0][round((i + 1) * stepsize)],
+ bot[0][round((i + 1) * stepsize)], bot[0][round(i * stepsize)])
+ for i in range(round(nsteps) - 1)])
+ colors.extend([color] * (round(nsteps) - 1))
+ self.add_collection3d(art3d.Poly3DCollection(
+ np.array(polyverts), # All polygons have 4 vertices, so vectorize.
+ facecolors=colors, edgecolors=colors, shade=True))
+ cset.remove()
+
+ def add_contour_set(
+ self, cset, extend3d=False, stride=5, zdir='z', offset=None):
+ zdir = '-' + zdir
+ if extend3d:
+ self._3d_extend_contour(cset, stride)
+ else:
+ art3d.collection_2d_to_3d(
+ cset, zs=offset if offset is not None else cset.levels, zdir=zdir)
+
+ def add_contourf_set(self, cset, zdir='z', offset=None):
+ self._add_contourf_set(cset, zdir=zdir, offset=offset)
+
+ def _add_contourf_set(self, cset, zdir='z', offset=None):
+ """
+ Returns
+ -------
+ levels : `numpy.ndarray`
+ Levels at which the filled contours are added.
+ """
+ zdir = '-' + zdir
+
+ midpoints = cset.levels[:-1] + np.diff(cset.levels) / 2
+ # Linearly interpolate to get levels for any extensions
+ if cset._extend_min:
+ min_level = cset.levels[0] - np.diff(cset.levels[:2]) / 2
+ midpoints = np.insert(midpoints, 0, min_level)
+ if cset._extend_max:
+ max_level = cset.levels[-1] + np.diff(cset.levels[-2:]) / 2
+ midpoints = np.append(midpoints, max_level)
+
+ art3d.collection_2d_to_3d(
+ cset, zs=offset if offset is not None else midpoints, zdir=zdir)
+ return midpoints
+
+ @_preprocess_data()
+ def contour(self, X, Y, Z, *args,
+ extend3d=False, stride=5, zdir='z', offset=None, **kwargs):
+ """
+ Create a 3D contour plot.
+
+ Parameters
+ ----------
+ X, Y, Z : array-like,
+ Input data. See `.Axes.contour` for supported data shapes.
+ extend3d : bool, default: False
+ Whether to extend contour in 3D.
+ stride : int
+ Step size for extending contour.
+ zdir : {'x', 'y', 'z'}, default: 'z'
+ The direction to use.
+ offset : float, optional
+ If specified, plot a projection of the contour lines at this
+ position in a plane normal to *zdir*.
+ data : indexable object, optional
+ DATA_PARAMETER_PLACEHOLDER
+
+ *args, **kwargs
+ Other arguments are forwarded to `matplotlib.axes.Axes.contour`.
+
+ Returns
+ -------
+ matplotlib.contour.QuadContourSet
+ """
+ had_data = self.has_data()
+
+ jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
+ cset = super().contour(jX, jY, jZ, *args, **kwargs)
+ self.add_contour_set(cset, extend3d, stride, zdir, offset)
+
+ self.auto_scale_xyz(X, Y, Z, had_data)
+ return cset
+
+ contour3D = contour
+
+ @_preprocess_data()
+ def tricontour(self, *args,
+ extend3d=False, stride=5, zdir='z', offset=None, **kwargs):
+ """
+ Create a 3D contour plot.
+
+ .. note::
+ This method currently produces incorrect output due to a
+ longstanding bug in 3D PolyCollection rendering.
+
+ Parameters
+ ----------
+ X, Y, Z : array-like
+ Input data. See `.Axes.tricontour` for supported data shapes.
+ extend3d : bool, default: False
+ Whether to extend contour in 3D.
+ stride : int
+ Step size for extending contour.
+ zdir : {'x', 'y', 'z'}, default: 'z'
+ The direction to use.
+ offset : float, optional
+ If specified, plot a projection of the contour lines at this
+ position in a plane normal to *zdir*.
+ data : indexable object, optional
+ DATA_PARAMETER_PLACEHOLDER
+ *args, **kwargs
+ Other arguments are forwarded to `matplotlib.axes.Axes.tricontour`.
+
+ Returns
+ -------
+ matplotlib.tri._tricontour.TriContourSet
+ """
+ had_data = self.has_data()
+
+ tri, args, kwargs = Triangulation.get_from_args_and_kwargs(
+ *args, **kwargs)
+ X = tri.x
+ Y = tri.y
+ if 'Z' in kwargs:
+ Z = kwargs.pop('Z')
+ else:
+ # We do this so Z doesn't get passed as an arg to Axes.tricontour
+ Z, *args = args
+
+ jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
+ tri = Triangulation(jX, jY, tri.triangles, tri.mask)
+
+ cset = super().tricontour(tri, jZ, *args, **kwargs)
+ self.add_contour_set(cset, extend3d, stride, zdir, offset)
+
+ self.auto_scale_xyz(X, Y, Z, had_data)
+ return cset
+
+ def _auto_scale_contourf(self, X, Y, Z, zdir, levels, had_data):
+ # Autoscale in the zdir based on the levels added, which are
+ # different from data range if any contour extensions are present
+ dim_vals = {'x': X, 'y': Y, 'z': Z, zdir: levels}
+ # Input data and levels have different sizes, but auto_scale_xyz
+ # expected same-size input, so manually take min/max limits
+ limits = [(np.nanmin(dim_vals[dim]), np.nanmax(dim_vals[dim]))
+ for dim in ['x', 'y', 'z']]
+ self.auto_scale_xyz(*limits, had_data)
+
+ @_preprocess_data()
+ def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs):
+ """
+ Create a 3D filled contour plot.
+
+ Parameters
+ ----------
+ X, Y, Z : array-like
+ Input data. See `.Axes.contourf` for supported data shapes.
+ zdir : {'x', 'y', 'z'}, default: 'z'
+ The direction to use.
+ offset : float, optional
+ If specified, plot a projection of the contour lines at this
+ position in a plane normal to *zdir*.
+ data : indexable object, optional
+ DATA_PARAMETER_PLACEHOLDER
+ *args, **kwargs
+ Other arguments are forwarded to `matplotlib.axes.Axes.contourf`.
+
+ Returns
+ -------
+ matplotlib.contour.QuadContourSet
+ """
+ had_data = self.has_data()
+
+ jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
+ cset = super().contourf(jX, jY, jZ, *args, **kwargs)
+ levels = self._add_contourf_set(cset, zdir, offset)
+
+ self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data)
+ return cset
+
+ contourf3D = contourf
+
+ @_preprocess_data()
+ def tricontourf(self, *args, zdir='z', offset=None, **kwargs):
+ """
+ Create a 3D filled contour plot.
+
+ .. note::
+ This method currently produces incorrect output due to a
+ longstanding bug in 3D PolyCollection rendering.
+
+ Parameters
+ ----------
+ X, Y, Z : array-like
+ Input data. See `.Axes.tricontourf` for supported data shapes.
+ zdir : {'x', 'y', 'z'}, default: 'z'
+ The direction to use.
+ offset : float, optional
+ If specified, plot a projection of the contour lines at this
+ position in a plane normal to zdir.
+ data : indexable object, optional
+ DATA_PARAMETER_PLACEHOLDER
+ *args, **kwargs
+ Other arguments are forwarded to
+ `matplotlib.axes.Axes.tricontourf`.
+
+ Returns
+ -------
+ matplotlib.tri._tricontour.TriContourSet
+ """
+ had_data = self.has_data()
+
+ tri, args, kwargs = Triangulation.get_from_args_and_kwargs(
+ *args, **kwargs)
+ X = tri.x
+ Y = tri.y
+ if 'Z' in kwargs:
+ Z = kwargs.pop('Z')
+ else:
+ # We do this so Z doesn't get passed as an arg to Axes.tricontourf
+ Z, *args = args
+
+ jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
+ tri = Triangulation(jX, jY, tri.triangles, tri.mask)
+
+ cset = super().tricontourf(tri, jZ, *args, **kwargs)
+ levels = self._add_contourf_set(cset, zdir, offset)
+
+ self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data)
+ return cset
+
+ def add_collection3d(self, col, zs=0, zdir='z'):
+ """
+ Add a 3D collection object to the plot.
+
+ 2D collection types are converted to a 3D version by
+ modifying the object and adding z coordinate information.
+
+ Supported are:
+
+ - PolyCollection
+ - LineCollection
+ - PatchCollection
+ """
+ zvals = np.atleast_1d(zs)
+ zsortval = (np.min(zvals) if zvals.size
+ else 0) # FIXME: arbitrary default
+
+ # FIXME: use issubclass() (although, then a 3D collection
+ # object would also pass.) Maybe have a collection3d
+ # abstract class to test for and exclude?
+ if type(col) is mcoll.PolyCollection:
+ art3d.poly_collection_2d_to_3d(col, zs=zs, zdir=zdir)
+ col.set_sort_zpos(zsortval)
+ elif type(col) is mcoll.LineCollection:
+ art3d.line_collection_2d_to_3d(col, zs=zs, zdir=zdir)
+ col.set_sort_zpos(zsortval)
+ elif type(col) is mcoll.PatchCollection:
+ art3d.patch_collection_2d_to_3d(col, zs=zs, zdir=zdir)
+ col.set_sort_zpos(zsortval)
+
+ collection = super().add_collection(col)
+ return collection
+
+ @_preprocess_data(replace_names=["xs", "ys", "zs", "s",
+ "edgecolors", "c", "facecolor",
+ "facecolors", "color"])
+ def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True,
+ *args, **kwargs):
+ """
+ Create a scatter plot.
+
+ Parameters
+ ----------
+ xs, ys : array-like
+ The data positions.
+ zs : float or array-like, default: 0
+ The z-positions. Either an array of the same length as *xs* and
+ *ys* or a single value to place all points in the same plane.
+ zdir : {'x', 'y', 'z', '-x', '-y', '-z'}, default: 'z'
+ The axis direction for the *zs*. This is useful when plotting 2D
+ data on a 3D Axes. The data must be passed as *xs*, *ys*. Setting
+ *zdir* to 'y' then plots the data to the x-z-plane.
+
+ See also :doc:`/gallery/mplot3d/2dcollections3d`.
+
+ s : float or array-like, default: 20
+ The marker size in points**2. Either an array of the same length
+ as *xs* and *ys* or a single value to make all markers the same
+ size.
+ c : color, sequence, or sequence of colors, optional
+ The marker color. Possible values:
+
+ - A single color format string.
+ - A sequence of colors of length n.
+ - A sequence of n numbers to be mapped to colors using *cmap* and
+ *norm*.
+ - A 2D array in which the rows are RGB or RGBA.
+
+ For more details see the *c* argument of `~.axes.Axes.scatter`.
+ depthshade : bool, default: True
+ Whether to shade the scatter markers to give the appearance of
+ depth. Each call to ``scatter()`` will perform its depthshading
+ independently.
+ data : indexable object, optional
+ DATA_PARAMETER_PLACEHOLDER
+ **kwargs
+ All other keyword arguments are passed on to `~.axes.Axes.scatter`.
+
+ Returns
+ -------
+ paths : `~matplotlib.collections.PathCollection`
+ """
+
+ had_data = self.has_data()
+ zs_orig = zs
+
+ xs, ys, zs = np.broadcast_arrays(
+ *[np.ravel(np.ma.filled(t, np.nan)) for t in [xs, ys, zs]])
+ s = np.ma.ravel(s) # This doesn't have to match x, y in size.
+
+ xs, ys, zs, s, c, color = cbook.delete_masked_points(
+ xs, ys, zs, s, c, kwargs.get('color', None)
+ )
+ if kwargs.get('color', None):
+ kwargs['color'] = color
+
+ # For xs and ys, 2D scatter() will do the copying.
+ if np.may_share_memory(zs_orig, zs): # Avoid unnecessary copies.
+ zs = zs.copy()
+
+ patches = super().scatter(xs, ys, s=s, c=c, *args, **kwargs)
+ art3d.patch_collection_2d_to_3d(patches, zs=zs, zdir=zdir,
+ depthshade=depthshade)
+
+ if self._zmargin < 0.05 and xs.size > 0:
+ self.set_zmargin(0.05)
+
+ self.auto_scale_xyz(xs, ys, zs, had_data)
+
+ return patches
+
+ scatter3D = scatter
+
+ @_preprocess_data()
+ def bar(self, left, height, zs=0, zdir='z', *args, **kwargs):
+ """
+ Add 2D bar(s).
+
+ Parameters
+ ----------
+ left : 1D array-like
+ The x coordinates of the left sides of the bars.
+ height : 1D array-like
+ The height of the bars.
+ zs : float or 1D array-like
+ Z coordinate of bars; if a single value is specified, it will be
+ used for all bars.
+ zdir : {'x', 'y', 'z'}, default: 'z'
+ When plotting 2D data, the direction to use as z ('x', 'y' or 'z').
+ data : indexable object, optional
+ DATA_PARAMETER_PLACEHOLDER
+ **kwargs
+ Other keyword arguments are forwarded to
+ `matplotlib.axes.Axes.bar`.
+
+ Returns
+ -------
+ mpl_toolkits.mplot3d.art3d.Patch3DCollection
+ """
+ had_data = self.has_data()
+
+ patches = super().bar(left, height, *args, **kwargs)
+
+ zs = np.broadcast_to(zs, len(left))
+
+ verts = []
+ verts_zs = []
+ for p, z in zip(patches, zs):
+ vs = art3d._get_patch_verts(p)
+ verts += vs.tolist()
+ verts_zs += [z] * len(vs)
+ art3d.patch_2d_to_3d(p, z, zdir)
+ if 'alpha' in kwargs:
+ p.set_alpha(kwargs['alpha'])
+
+ if len(verts) > 0:
+ # the following has to be skipped if verts is empty
+ # NOTE: Bugs could still occur if len(verts) > 0,
+ # but the "2nd dimension" is empty.
+ xs, ys = zip(*verts)
+ else:
+ xs, ys = [], []
+
+ xs, ys, verts_zs = art3d.juggle_axes(xs, ys, verts_zs, zdir)
+ self.auto_scale_xyz(xs, ys, verts_zs, had_data)
+
+ return patches
+
+ @_preprocess_data()
+ def bar3d(self, x, y, z, dx, dy, dz, color=None,
+ zsort='average', shade=True, lightsource=None, *args, **kwargs):
+ """
+ Generate a 3D barplot.
+
+ This method creates three-dimensional barplot where the width,
+ depth, height, and color of the bars can all be uniquely set.
+
+ Parameters
+ ----------
+ x, y, z : array-like
+ The coordinates of the anchor point of the bars.
+
+ dx, dy, dz : float or array-like
+ The width, depth, and height of the bars, respectively.
+
+ color : sequence of colors, optional
+ The color of the bars can be specified globally or
+ individually. This parameter can be:
+
+ - A single color, to color all bars the same color.
+ - An array of colors of length N bars, to color each bar
+ independently.
+ - An array of colors of length 6, to color the faces of the
+ bars similarly.
+ - An array of colors of length 6 * N bars, to color each face
+ independently.
+
+ When coloring the faces of the boxes specifically, this is
+ the order of the coloring:
+
+ 1. -Z (bottom of box)
+ 2. +Z (top of box)
+ 3. -Y
+ 4. +Y
+ 5. -X
+ 6. +X
+
+ zsort : str, optional
+ The z-axis sorting scheme passed onto `~.art3d.Poly3DCollection`
+
+ shade : bool, default: True
+ When true, this shades the dark sides of the bars (relative
+ to the plot's source of light).
+
+ lightsource : `~matplotlib.colors.LightSource`
+ The lightsource to use when *shade* is True.
+
+ data : indexable object, optional
+ DATA_PARAMETER_PLACEHOLDER
+
+ **kwargs
+ Any additional keyword arguments are passed onto
+ `~.art3d.Poly3DCollection`.
+
+ Returns
+ -------
+ collection : `~.art3d.Poly3DCollection`
+ A collection of three-dimensional polygons representing the bars.
+ """
+
+ had_data = self.has_data()
+
+ x, y, z, dx, dy, dz = np.broadcast_arrays(
+ np.atleast_1d(x), y, z, dx, dy, dz)
+ minx = np.min(x)
+ maxx = np.max(x + dx)
+ miny = np.min(y)
+ maxy = np.max(y + dy)
+ minz = np.min(z)
+ maxz = np.max(z + dz)
+
+ # shape (6, 4, 3)
+ # All faces are oriented facing outwards - when viewed from the
+ # outside, their vertices are in a counterclockwise ordering.
+ cuboid = np.array([
+ # -z
+ (
+ (0, 0, 0),
+ (0, 1, 0),
+ (1, 1, 0),
+ (1, 0, 0),
+ ),
+ # +z
+ (
+ (0, 0, 1),
+ (1, 0, 1),
+ (1, 1, 1),
+ (0, 1, 1),
+ ),
+ # -y
+ (
+ (0, 0, 0),
+ (1, 0, 0),
+ (1, 0, 1),
+ (0, 0, 1),
+ ),
+ # +y
+ (
+ (0, 1, 0),
+ (0, 1, 1),
+ (1, 1, 1),
+ (1, 1, 0),
+ ),
+ # -x
+ (
+ (0, 0, 0),
+ (0, 0, 1),
+ (0, 1, 1),
+ (0, 1, 0),
+ ),
+ # +x
+ (
+ (1, 0, 0),
+ (1, 1, 0),
+ (1, 1, 1),
+ (1, 0, 1),
+ ),
+ ])
+
+ # indexed by [bar, face, vertex, coord]
+ polys = np.empty(x.shape + cuboid.shape)
+
+ # handle each coordinate separately
+ for i, p, dp in [(0, x, dx), (1, y, dy), (2, z, dz)]:
+ p = p[..., np.newaxis, np.newaxis]
+ dp = dp[..., np.newaxis, np.newaxis]
+ polys[..., i] = p + dp * cuboid[..., i]
+
+ # collapse the first two axes
+ polys = polys.reshape((-1,) + polys.shape[2:])
+
+ facecolors = []
+ if color is None:
+ color = [self._get_patches_for_fill.get_next_color()]
+
+ color = list(mcolors.to_rgba_array(color))
+
+ if len(color) == len(x):
+ # bar colors specified, need to expand to number of faces
+ for c in color:
+ facecolors.extend([c] * 6)
+ else:
+ # a single color specified, or face colors specified explicitly
+ facecolors = color
+ if len(facecolors) < len(x):
+ facecolors *= (6 * len(x))
+
+ col = art3d.Poly3DCollection(polys,
+ zsort=zsort,
+ facecolors=facecolors,
+ shade=shade,
+ lightsource=lightsource,
+ *args, **kwargs)
+ self.add_collection(col)
+
+ self.auto_scale_xyz((minx, maxx), (miny, maxy), (minz, maxz), had_data)
+
+ return col
+
+ def set_title(self, label, fontdict=None, loc='center', **kwargs):
+ # docstring inherited
+ ret = super().set_title(label, fontdict=fontdict, loc=loc, **kwargs)
+ (x, y) = self.title.get_position()
+ self.title.set_y(0.92 * y)
+ return ret
+
+ @_preprocess_data()
+ def quiver(self, X, Y, Z, U, V, W, *,
+ length=1, arrow_length_ratio=.3, pivot='tail', normalize=False,
+ **kwargs):
+ """
+ Plot a 3D field of arrows.
+
+ The arguments can be array-like or scalars, so long as they can be
+ broadcast together. The arguments can also be masked arrays. If an
+ element in any of argument is masked, then that corresponding quiver
+ element will not be plotted.
+
+ Parameters
+ ----------
+ X, Y, Z : array-like
+ The x, y and z coordinates of the arrow locations (default is
+ tail of arrow; see *pivot* kwarg).
+
+ U, V, W : array-like
+ The x, y and z components of the arrow vectors.
+
+ length : float, default: 1
+ The length of each quiver.
+
+ arrow_length_ratio : float, default: 0.3
+ The ratio of the arrow head with respect to the quiver.
+
+ pivot : {'tail', 'middle', 'tip'}, default: 'tail'
+ The part of the arrow that is at the grid point; the arrow
+ rotates about this point, hence the name *pivot*.
+
+ normalize : bool, default: False
+ Whether all arrows are normalized to have the same length, or keep
+ the lengths defined by *u*, *v*, and *w*.
+
+ data : indexable object, optional
+ DATA_PARAMETER_PLACEHOLDER
+
+ **kwargs
+ Any additional keyword arguments are delegated to
+ :class:`.Line3DCollection`
+ """
+
+ def calc_arrows(UVW):
+ # get unit direction vector perpendicular to (u, v, w)
+ x = UVW[:, 0]
+ y = UVW[:, 1]
+ norm = np.linalg.norm(UVW[:, :2], axis=1)
+ x_p = np.divide(y, norm, where=norm != 0, out=np.zeros_like(x))
+ y_p = np.divide(-x, norm, where=norm != 0, out=np.ones_like(x))
+ # compute the two arrowhead direction unit vectors
+ rangle = math.radians(15)
+ c = math.cos(rangle)
+ s = math.sin(rangle)
+ # construct the rotation matrices of shape (3, 3, n)
+ r13 = y_p * s
+ r32 = x_p * s
+ r12 = x_p * y_p * (1 - c)
+ Rpos = np.array(
+ [[c + (x_p ** 2) * (1 - c), r12, r13],
+ [r12, c + (y_p ** 2) * (1 - c), -r32],
+ [-r13, r32, np.full_like(x_p, c)]])
+ # opposite rotation negates all the sin terms
+ Rneg = Rpos.copy()
+ Rneg[[0, 1, 2, 2], [2, 2, 0, 1]] *= -1
+ # Batch n (3, 3) x (3) matrix multiplications ((3, 3, n) x (n, 3)).
+ Rpos_vecs = np.einsum("ij...,...j->...i", Rpos, UVW)
+ Rneg_vecs = np.einsum("ij...,...j->...i", Rneg, UVW)
+ # Stack into (n, 2, 3) result.
+ return np.stack([Rpos_vecs, Rneg_vecs], axis=1)
+
+ had_data = self.has_data()
+
+ input_args = [X, Y, Z, U, V, W]
+
+ # extract the masks, if any
+ masks = [k.mask for k in input_args
+ if isinstance(k, np.ma.MaskedArray)]
+ # broadcast to match the shape
+ bcast = np.broadcast_arrays(*input_args, *masks)
+ input_args = bcast[:6]
+ masks = bcast[6:]
+ if masks:
+ # combine the masks into one
+ mask = functools.reduce(np.logical_or, masks)
+ # put mask on and compress
+ input_args = [np.ma.array(k, mask=mask).compressed()
+ for k in input_args]
+ else:
+ input_args = [np.ravel(k) for k in input_args]
+
+ if any(len(v) == 0 for v in input_args):
+ # No quivers, so just make an empty collection and return early
+ linec = art3d.Line3DCollection([], **kwargs)
+ self.add_collection(linec)
+ return linec
+
+ shaft_dt = np.array([0., length], dtype=float)
+ arrow_dt = shaft_dt * arrow_length_ratio
+
+ _api.check_in_list(['tail', 'middle', 'tip'], pivot=pivot)
+ if pivot == 'tail':
+ shaft_dt -= length
+ elif pivot == 'middle':
+ shaft_dt -= length / 2
+
+ XYZ = np.column_stack(input_args[:3])
+ UVW = np.column_stack(input_args[3:]).astype(float)
+
+ # Normalize rows of UVW
+ norm = np.linalg.norm(UVW, axis=1)
+
+ # If any row of UVW is all zeros, don't make a quiver for it
+ mask = norm > 0
+ XYZ = XYZ[mask]
+ if normalize:
+ UVW = UVW[mask] / norm[mask].reshape((-1, 1))
+ else:
+ UVW = UVW[mask]
+
+ if len(XYZ) > 0:
+ # compute the shaft lines all at once with an outer product
+ shafts = (XYZ - np.multiply.outer(shaft_dt, UVW)).swapaxes(0, 1)
+ # compute head direction vectors, n heads x 2 sides x 3 dimensions
+ head_dirs = calc_arrows(UVW)
+ # compute all head lines at once, starting from the shaft ends
+ heads = shafts[:, :1] - np.multiply.outer(arrow_dt, head_dirs)
+ # stack left and right head lines together
+ heads = heads.reshape((len(arrow_dt), -1, 3))
+ # transpose to get a list of lines
+ heads = heads.swapaxes(0, 1)
+
+ lines = [*shafts, *heads]
+ else:
+ lines = []
+
+ linec = art3d.Line3DCollection(lines, **kwargs)
+ self.add_collection(linec)
+
+ self.auto_scale_xyz(XYZ[:, 0], XYZ[:, 1], XYZ[:, 2], had_data)
+
+ return linec
+
+ quiver3D = quiver
+
+ def voxels(self, *args, facecolors=None, edgecolors=None, shade=True,
+ lightsource=None, **kwargs):
+ """
+ ax.voxels([x, y, z,] /, filled, facecolors=None, edgecolors=None, \
+**kwargs)
+
+ Plot a set of filled voxels
+
+ All voxels are plotted as 1x1x1 cubes on the axis, with
+ ``filled[0, 0, 0]`` placed with its lower corner at the origin.
+ Occluded faces are not plotted.
+
+ Parameters
+ ----------
+ filled : 3D np.array of bool
+ A 3D array of values, with truthy values indicating which voxels
+ to fill
+
+ x, y, z : 3D np.array, optional
+ The coordinates of the corners of the voxels. This should broadcast
+ to a shape one larger in every dimension than the shape of
+ *filled*. These can be used to plot non-cubic voxels.
+
+ If not specified, defaults to increasing integers along each axis,
+ like those returned by :func:`~numpy.indices`.
+ As indicated by the ``/`` in the function signature, these
+ arguments can only be passed positionally.
+
+ facecolors, edgecolors : array-like, optional
+ The color to draw the faces and edges of the voxels. Can only be
+ passed as keyword arguments.
+ These parameters can be:
+
+ - A single color value, to color all voxels the same color. This
+ can be either a string, or a 1D RGB/RGBA array
+ - ``None``, the default, to use a single color for the faces, and
+ the style default for the edges.
+ - A 3D `~numpy.ndarray` of color names, with each item the color
+ for the corresponding voxel. The size must match the voxels.
+ - A 4D `~numpy.ndarray` of RGB/RGBA data, with the components
+ along the last axis.
+
+ shade : bool, default: True
+ Whether to shade the facecolors.
+
+ lightsource : `~matplotlib.colors.LightSource`
+ The lightsource to use when *shade* is True.
+
+ **kwargs
+ Additional keyword arguments to pass onto
+ `~mpl_toolkits.mplot3d.art3d.Poly3DCollection`.
+
+ Returns
+ -------
+ faces : dict
+ A dictionary indexed by coordinate, where ``faces[i, j, k]`` is a
+ `.Poly3DCollection` of the faces drawn for the voxel
+ ``filled[i, j, k]``. If no faces were drawn for a given voxel,
+ either because it was not asked to be drawn, or it is fully
+ occluded, then ``(i, j, k) not in faces``.
+
+ Examples
+ --------
+ .. plot:: gallery/mplot3d/voxels.py
+ .. plot:: gallery/mplot3d/voxels_rgb.py
+ .. plot:: gallery/mplot3d/voxels_torus.py
+ .. plot:: gallery/mplot3d/voxels_numpy_logo.py
+ """
+
+ # work out which signature we should be using, and use it to parse
+ # the arguments. Name must be voxels for the correct error message
+ if len(args) >= 3:
+ # underscores indicate position only
+ def voxels(__x, __y, __z, filled, **kwargs):
+ return (__x, __y, __z), filled, kwargs
+ else:
+ def voxels(filled, **kwargs):
+ return None, filled, kwargs
+
+ xyz, filled, kwargs = voxels(*args, **kwargs)
+
+ # check dimensions
+ if filled.ndim != 3:
+ raise ValueError("Argument filled must be 3-dimensional")
+ size = np.array(filled.shape, dtype=np.intp)
+
+ # check xyz coordinates, which are one larger than the filled shape
+ coord_shape = tuple(size + 1)
+ if xyz is None:
+ x, y, z = np.indices(coord_shape)
+ else:
+ x, y, z = (np.broadcast_to(c, coord_shape) for c in xyz)
+
+ def _broadcast_color_arg(color, name):
+ if np.ndim(color) in (0, 1):
+ # single color, like "red" or [1, 0, 0]
+ return np.broadcast_to(color, filled.shape + np.shape(color))
+ elif np.ndim(color) in (3, 4):
+ # 3D array of strings, or 4D array with last axis rgb
+ if np.shape(color)[:3] != filled.shape:
+ raise ValueError(
+ f"When multidimensional, {name} must match the shape "
+ "of filled")
+ return color
+ else:
+ raise ValueError(f"Invalid {name} argument")
+
+ # broadcast and default on facecolors
+ if facecolors is None:
+ facecolors = self._get_patches_for_fill.get_next_color()
+ facecolors = _broadcast_color_arg(facecolors, 'facecolors')
+
+ # broadcast but no default on edgecolors
+ edgecolors = _broadcast_color_arg(edgecolors, 'edgecolors')
+
+ # scale to the full array, even if the data is only in the center
+ self.auto_scale_xyz(x, y, z)
+
+ # points lying on corners of a square
+ square = np.array([
+ [0, 0, 0],
+ [1, 0, 0],
+ [1, 1, 0],
+ [0, 1, 0],
+ ], dtype=np.intp)
+
+ voxel_faces = defaultdict(list)
+
+ def permutation_matrices(n):
+ """Generate cyclic permutation matrices."""
+ mat = np.eye(n, dtype=np.intp)
+ for i in range(n):
+ yield mat
+ mat = np.roll(mat, 1, axis=0)
+
+ # iterate over each of the YZ, ZX, and XY orientations, finding faces
+ # to render
+ for permute in permutation_matrices(3):
+ # find the set of ranges to iterate over
+ pc, qc, rc = permute.T.dot(size)
+ pinds = np.arange(pc)
+ qinds = np.arange(qc)
+ rinds = np.arange(rc)
+
+ square_rot_pos = square.dot(permute.T)
+ square_rot_neg = square_rot_pos[::-1]
+
+ # iterate within the current plane
+ for p in pinds:
+ for q in qinds:
+ # iterate perpendicularly to the current plane, handling
+ # boundaries. We only draw faces between a voxel and an
+ # empty space, to avoid drawing internal faces.
+
+ # draw lower faces
+ p0 = permute.dot([p, q, 0])
+ i0 = tuple(p0)
+ if filled[i0]:
+ voxel_faces[i0].append(p0 + square_rot_neg)
+
+ # draw middle faces
+ for r1, r2 in zip(rinds[:-1], rinds[1:]):
+ p1 = permute.dot([p, q, r1])
+ p2 = permute.dot([p, q, r2])
+
+ i1 = tuple(p1)
+ i2 = tuple(p2)
+
+ if filled[i1] and not filled[i2]:
+ voxel_faces[i1].append(p2 + square_rot_pos)
+ elif not filled[i1] and filled[i2]:
+ voxel_faces[i2].append(p2 + square_rot_neg)
+
+ # draw upper faces
+ pk = permute.dot([p, q, rc-1])
+ pk2 = permute.dot([p, q, rc])
+ ik = tuple(pk)
+ if filled[ik]:
+ voxel_faces[ik].append(pk2 + square_rot_pos)
+
+ # iterate over the faces, and generate a Poly3DCollection for each
+ # voxel
+ polygons = {}
+ for coord, faces_inds in voxel_faces.items():
+ # convert indices into 3D positions
+ if xyz is None:
+ faces = faces_inds
+ else:
+ faces = []
+ for face_inds in faces_inds:
+ ind = face_inds[:, 0], face_inds[:, 1], face_inds[:, 2]
+ face = np.empty(face_inds.shape)
+ face[:, 0] = x[ind]
+ face[:, 1] = y[ind]
+ face[:, 2] = z[ind]
+ faces.append(face)
+
+ # shade the faces
+ facecolor = facecolors[coord]
+ edgecolor = edgecolors[coord]
+
+ poly = art3d.Poly3DCollection(
+ faces, facecolors=facecolor, edgecolors=edgecolor,
+ shade=shade, lightsource=lightsource, **kwargs)
+ self.add_collection3d(poly)
+ polygons[coord] = poly
+
+ return polygons
+
+ @_preprocess_data(replace_names=["x", "y", "z", "xerr", "yerr", "zerr"])
+ def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='',
+ barsabove=False, errorevery=1, ecolor=None, elinewidth=None,
+ capsize=None, capthick=None, xlolims=False, xuplims=False,
+ ylolims=False, yuplims=False, zlolims=False, zuplims=False,
+ **kwargs):
+ """
+ Plot lines and/or markers with errorbars around them.
+
+ *x*/*y*/*z* define the data locations, and *xerr*/*yerr*/*zerr* define
+ the errorbar sizes. By default, this draws the data markers/lines as
+ well the errorbars. Use fmt='none' to draw errorbars only.
+
+ Parameters
+ ----------
+ x, y, z : float or array-like
+ The data positions.
+
+ xerr, yerr, zerr : float or array-like, shape (N,) or (2, N), optional
+ The errorbar sizes:
+
+ - scalar: Symmetric +/- values for all data points.
+ - shape(N,): Symmetric +/-values for each data point.
+ - shape(2, N): Separate - and + values for each bar. First row
+ contains the lower errors, the second row contains the upper
+ errors.
+ - *None*: No errorbar.
+
+ Note that all error arrays should have *positive* values.
+
+ fmt : str, default: ''
+ The format for the data points / data lines. See `.plot` for
+ details.
+
+ Use 'none' (case-insensitive) to plot errorbars without any data
+ markers.
+
+ ecolor : color, default: None
+ The color of the errorbar lines. If None, use the color of the
+ line connecting the markers.
+
+ elinewidth : float, default: None
+ The linewidth of the errorbar lines. If None, the linewidth of
+ the current style is used.
+
+ capsize : float, default: :rc:`errorbar.capsize`
+ The length of the error bar caps in points.
+
+ capthick : float, default: None
+ An alias to the keyword argument *markeredgewidth* (a.k.a. *mew*).
+ This setting is a more sensible name for the property that
+ controls the thickness of the error bar cap in points. For
+ backwards compatibility, if *mew* or *markeredgewidth* are given,
+ then they will over-ride *capthick*. This may change in future
+ releases.
+
+ barsabove : bool, default: False
+ If True, will plot the errorbars above the plot
+ symbols. Default is below.
+
+ xlolims, ylolims, zlolims : bool, default: False
+ These arguments can be used to indicate that a value gives only
+ lower limits. In that case a caret symbol is used to indicate
+ this. *lims*-arguments may be scalars, or array-likes of the same
+ length as the errors. To use limits with inverted axes,
+ `~.Axes.set_xlim` or `~.Axes.set_ylim` must be called before
+ `errorbar`. Note the tricky parameter names: setting e.g.
+ *ylolims* to True means that the y-value is a *lower* limit of the
+ True value, so, only an *upward*-pointing arrow will be drawn!
+
+ xuplims, yuplims, zuplims : bool, default: False
+ Same as above, but for controlling the upper limits.
+
+ errorevery : int or (int, int), default: 1
+ draws error bars on a subset of the data. *errorevery* =N draws
+ error bars on the points (x[::N], y[::N], z[::N]).
+ *errorevery* =(start, N) draws error bars on the points
+ (x[start::N], y[start::N], z[start::N]). e.g. *errorevery* =(6, 3)
+ adds error bars to the data at (x[6], x[9], x[12], x[15], ...).
+ Used to avoid overlapping error bars when two series share x-axis
+ values.
+
+ Returns
+ -------
+ errlines : list
+ List of `~mpl_toolkits.mplot3d.art3d.Line3DCollection` instances
+ each containing an errorbar line.
+ caplines : list
+ List of `~mpl_toolkits.mplot3d.art3d.Line3D` instances each
+ containing a capline object.
+ limmarks : list
+ List of `~mpl_toolkits.mplot3d.art3d.Line3D` instances each
+ containing a marker with an upper or lower limit.
+
+ Other Parameters
+ ----------------
+ data : indexable object, optional
+ DATA_PARAMETER_PLACEHOLDER
+
+ **kwargs
+ All other keyword arguments for styling errorbar lines are passed
+ `~mpl_toolkits.mplot3d.art3d.Line3DCollection`.
+
+ Examples
+ --------
+ .. plot:: gallery/mplot3d/errorbar3d.py
+ """
+ had_data = self.has_data()
+
+ kwargs = cbook.normalize_kwargs(kwargs, mlines.Line2D)
+ # Drop anything that comes in as None to use the default instead.
+ kwargs = {k: v for k, v in kwargs.items() if v is not None}
+ kwargs.setdefault('zorder', 2)
+
+ self._process_unit_info([("x", x), ("y", y), ("z", z)], kwargs,
+ convert=False)
+
+ # make sure all the args are iterable; use lists not arrays to
+ # preserve units
+ x = x if np.iterable(x) else [x]
+ y = y if np.iterable(y) else [y]
+ z = z if np.iterable(z) else [z]
+
+ if not len(x) == len(y) == len(z):
+ raise ValueError("'x', 'y', and 'z' must have the same size")
+
+ everymask = self._errorevery_to_mask(x, errorevery)
+
+ label = kwargs.pop("label", None)
+ kwargs['label'] = '_nolegend_'
+
+ # Create the main line and determine overall kwargs for child artists.
+ # We avoid calling self.plot() directly, or self._get_lines(), because
+ # that would call self._process_unit_info again, and do other indirect
+ # data processing.
+ (data_line, base_style), = self._get_lines._plot_args(
+ self, (x, y) if fmt == '' else (x, y, fmt), kwargs, return_kwargs=True)
+ art3d.line_2d_to_3d(data_line, zs=z)
+
+ # Do this after creating `data_line` to avoid modifying `base_style`.
+ if barsabove:
+ data_line.set_zorder(kwargs['zorder'] - .1)
+ else:
+ data_line.set_zorder(kwargs['zorder'] + .1)
+
+ # Add line to plot, or throw it away and use it to determine kwargs.
+ if fmt.lower() != 'none':
+ self.add_line(data_line)
+ else:
+ data_line = None
+ # Remove alpha=0 color that _process_plot_format returns.
+ base_style.pop('color')
+
+ if 'color' not in base_style:
+ base_style['color'] = 'C0'
+ if ecolor is None:
+ ecolor = base_style['color']
+
+ # Eject any line-specific information from format string, as it's not
+ # needed for bars or caps.
+ for key in ['marker', 'markersize', 'markerfacecolor',
+ 'markeredgewidth', 'markeredgecolor', 'markevery',
+ 'linestyle', 'fillstyle', 'drawstyle', 'dash_capstyle',
+ 'dash_joinstyle', 'solid_capstyle', 'solid_joinstyle']:
+ base_style.pop(key, None)
+
+ # Make the style dict for the line collections (the bars).
+ eb_lines_style = {**base_style, 'color': ecolor}
+
+ if elinewidth:
+ eb_lines_style['linewidth'] = elinewidth
+ elif 'linewidth' in kwargs:
+ eb_lines_style['linewidth'] = kwargs['linewidth']
+
+ for key in ('transform', 'alpha', 'zorder', 'rasterized'):
+ if key in kwargs:
+ eb_lines_style[key] = kwargs[key]
+
+ # Make the style dict for caps (the "hats").
+ eb_cap_style = {**base_style, 'linestyle': 'None'}
+ if capsize is None:
+ capsize = mpl.rcParams["errorbar.capsize"]
+ if capsize > 0:
+ eb_cap_style['markersize'] = 2. * capsize
+ if capthick is not None:
+ eb_cap_style['markeredgewidth'] = capthick
+ eb_cap_style['color'] = ecolor
+
+ def _apply_mask(arrays, mask):
+ # Return, for each array in *arrays*, the elements for which *mask*
+ # is True, without using fancy indexing.
+ return [[*itertools.compress(array, mask)] for array in arrays]
+
+ def _extract_errs(err, data, lomask, himask):
+ # For separate +/- error values we need to unpack err
+ if len(err.shape) == 2:
+ low_err, high_err = err
+ else:
+ low_err, high_err = err, err
+
+ lows = np.where(lomask | ~everymask, data, data - low_err)
+ highs = np.where(himask | ~everymask, data, data + high_err)
+
+ return lows, highs
+
+ # collect drawn items while looping over the three coordinates
+ errlines, caplines, limmarks = [], [], []
+
+ # list of endpoint coordinates, used for auto-scaling
+ coorderrs = []
+
+ # define the markers used for errorbar caps and limits below
+ # the dictionary key is mapped by the `i_xyz` helper dictionary
+ capmarker = {0: '|', 1: '|', 2: '_'}
+ i_xyz = {'x': 0, 'y': 1, 'z': 2}
+
+ # Calculate marker size from points to quiver length. Because these are
+ # not markers, and 3D Axes do not use the normal transform stack, this
+ # is a bit involved. Since the quiver arrows will change size as the
+ # scene is rotated, they are given a standard size based on viewing
+ # them directly in planar form.
+ quiversize = eb_cap_style.get('markersize',
+ mpl.rcParams['lines.markersize']) ** 2
+ quiversize *= self.figure.dpi / 72
+ quiversize = self.transAxes.inverted().transform([
+ (0, 0), (quiversize, quiversize)])
+ quiversize = np.mean(np.diff(quiversize, axis=0))
+ # quiversize is now in Axes coordinates, and to convert back to data
+ # coordinates, we need to run it through the inverse 3D transform. For
+ # consistency, this uses a fixed elevation, azimuth, and roll.
+ with cbook._setattr_cm(self, elev=0, azim=0, roll=0):
+ invM = np.linalg.inv(self.get_proj())
+ # elev=azim=roll=0 produces the Y-Z plane, so quiversize in 2D 'x' is
+ # 'y' in 3D, hence the 1 index.
+ quiversize = np.dot(invM, [quiversize, 0, 0, 0])[1]
+ # Quivers use a fixed 15-degree arrow head, so scale up the length so
+ # that the size corresponds to the base. In other words, this constant
+ # corresponds to the equation tan(15) = (base / 2) / (arrow length).
+ quiversize *= 1.8660254037844388
+ eb_quiver_style = {**eb_cap_style,
+ 'length': quiversize, 'arrow_length_ratio': 1}
+ eb_quiver_style.pop('markersize', None)
+
+ # loop over x-, y-, and z-direction and draw relevant elements
+ for zdir, data, err, lolims, uplims in zip(
+ ['x', 'y', 'z'], [x, y, z], [xerr, yerr, zerr],
+ [xlolims, ylolims, zlolims], [xuplims, yuplims, zuplims]):
+
+ dir_vector = art3d.get_dir_vector(zdir)
+ i_zdir = i_xyz[zdir]
+
+ if err is None:
+ continue
+
+ if not np.iterable(err):
+ err = [err] * len(data)
+
+ err = np.atleast_1d(err)
+
+ # arrays fine here, they are booleans and hence not units
+ lolims = np.broadcast_to(lolims, len(data)).astype(bool)
+ uplims = np.broadcast_to(uplims, len(data)).astype(bool)
+
+ # a nested list structure that expands to (xl,xh),(yl,yh),(zl,zh),
+ # where x/y/z and l/h correspond to dimensions and low/high
+ # positions of errorbars in a dimension we're looping over
+ coorderr = [
+ _extract_errs(err * dir_vector[i], coord, lolims, uplims)
+ for i, coord in enumerate([x, y, z])]
+ (xl, xh), (yl, yh), (zl, zh) = coorderr
+
+ # draws capmarkers - flat caps orthogonal to the error bars
+ nolims = ~(lolims | uplims)
+ if nolims.any() and capsize > 0:
+ lo_caps_xyz = _apply_mask([xl, yl, zl], nolims & everymask)
+ hi_caps_xyz = _apply_mask([xh, yh, zh], nolims & everymask)
+
+ # setting '_' for z-caps and '|' for x- and y-caps;
+ # these markers will rotate as the viewing angle changes
+ cap_lo = art3d.Line3D(*lo_caps_xyz, ls='',
+ marker=capmarker[i_zdir],
+ **eb_cap_style)
+ cap_hi = art3d.Line3D(*hi_caps_xyz, ls='',
+ marker=capmarker[i_zdir],
+ **eb_cap_style)
+ self.add_line(cap_lo)
+ self.add_line(cap_hi)
+ caplines.append(cap_lo)
+ caplines.append(cap_hi)
+
+ if lolims.any():
+ xh0, yh0, zh0 = _apply_mask([xh, yh, zh], lolims & everymask)
+ self.quiver(xh0, yh0, zh0, *dir_vector, **eb_quiver_style)
+ if uplims.any():
+ xl0, yl0, zl0 = _apply_mask([xl, yl, zl], uplims & everymask)
+ self.quiver(xl0, yl0, zl0, *-dir_vector, **eb_quiver_style)
+
+ errline = art3d.Line3DCollection(np.array(coorderr).T,
+ **eb_lines_style)
+ self.add_collection(errline)
+ errlines.append(errline)
+ coorderrs.append(coorderr)
+
+ coorderrs = np.array(coorderrs)
+
+ def _digout_minmax(err_arr, coord_label):
+ return (np.nanmin(err_arr[:, i_xyz[coord_label], :, :]),
+ np.nanmax(err_arr[:, i_xyz[coord_label], :, :]))
+
+ minx, maxx = _digout_minmax(coorderrs, 'x')
+ miny, maxy = _digout_minmax(coorderrs, 'y')
+ minz, maxz = _digout_minmax(coorderrs, 'z')
+ self.auto_scale_xyz((minx, maxx), (miny, maxy), (minz, maxz), had_data)
+
+ # Adapting errorbar containers for 3d case, assuming z-axis points "up"
+ errorbar_container = mcontainer.ErrorbarContainer(
+ (data_line, tuple(caplines), tuple(errlines)),
+ has_xerr=(xerr is not None or yerr is not None),
+ has_yerr=(zerr is not None),
+ label=label)
+ self.containers.append(errorbar_container)
+
+ return errlines, caplines, limmarks
+
+ @_api.make_keyword_only("3.8", "call_axes_locator")
+ def get_tightbbox(self, renderer=None, call_axes_locator=True,
+ bbox_extra_artists=None, *, for_layout_only=False):
+ ret = super().get_tightbbox(renderer,
+ call_axes_locator=call_axes_locator,
+ bbox_extra_artists=bbox_extra_artists,
+ for_layout_only=for_layout_only)
+ batch = [ret]
+ if self._axis3don:
+ for axis in self._axis_map.values():
+ if axis.get_visible():
+ axis_bb = martist._get_tightbbox_for_layout_only(
+ axis, renderer)
+ if axis_bb:
+ batch.append(axis_bb)
+ return mtransforms.Bbox.union(batch)
+
+ @_preprocess_data()
+ def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-',
+ bottom=0, label=None, orientation='z'):
+ """
+ Create a 3D stem plot.
+
+ A stem plot draws lines perpendicular to a baseline, and places markers
+ at the heads. By default, the baseline is defined by *x* and *y*, and
+ stems are drawn vertically from *bottom* to *z*.
+
+ Parameters
+ ----------
+ x, y, z : array-like
+ The positions of the heads of the stems. The stems are drawn along
+ the *orientation*-direction from the baseline at *bottom* (in the
+ *orientation*-coordinate) to the heads. By default, the *x* and *y*
+ positions are used for the baseline and *z* for the head position,
+ but this can be changed by *orientation*.
+
+ linefmt : str, default: 'C0-'
+ A string defining the properties of the vertical lines. Usually,
+ this will be a color or a color and a linestyle:
+
+ ========= =============
+ Character Line Style
+ ========= =============
+ ``'-'`` solid line
+ ``'--'`` dashed line
+ ``'-.'`` dash-dot line
+ ``':'`` dotted line
+ ========= =============
+
+ Note: While it is technically possible to specify valid formats
+ other than color or color and linestyle (e.g. 'rx' or '-.'), this
+ is beyond the intention of the method and will most likely not
+ result in a reasonable plot.
+
+ markerfmt : str, default: 'C0o'
+ A string defining the properties of the markers at the stem heads.
+
+ basefmt : str, default: 'C3-'
+ A format string defining the properties of the baseline.
+
+ bottom : float, default: 0
+ The position of the baseline, in *orientation*-coordinates.
+
+ label : str, default: None
+ The label to use for the stems in legends.
+
+ orientation : {'x', 'y', 'z'}, default: 'z'
+ The direction along which stems are drawn.
+
+ data : indexable object, optional
+ DATA_PARAMETER_PLACEHOLDER
+
+ Returns
+ -------
+ `.StemContainer`
+ The container may be treated like a tuple
+ (*markerline*, *stemlines*, *baseline*)
+
+ Examples
+ --------
+ .. plot:: gallery/mplot3d/stem3d_demo.py
+ """
+
+ from matplotlib.container import StemContainer
+
+ had_data = self.has_data()
+
+ _api.check_in_list(['x', 'y', 'z'], orientation=orientation)
+
+ xlim = (np.min(x), np.max(x))
+ ylim = (np.min(y), np.max(y))
+ zlim = (np.min(z), np.max(z))
+
+ # Determine the appropriate plane for the baseline and the direction of
+ # stemlines based on the value of orientation.
+ if orientation == 'x':
+ basex, basexlim = y, ylim
+ basey, baseylim = z, zlim
+ lines = [[(bottom, thisy, thisz), (thisx, thisy, thisz)]
+ for thisx, thisy, thisz in zip(x, y, z)]
+ elif orientation == 'y':
+ basex, basexlim = x, xlim
+ basey, baseylim = z, zlim
+ lines = [[(thisx, bottom, thisz), (thisx, thisy, thisz)]
+ for thisx, thisy, thisz in zip(x, y, z)]
+ else:
+ basex, basexlim = x, xlim
+ basey, baseylim = y, ylim
+ lines = [[(thisx, thisy, bottom), (thisx, thisy, thisz)]
+ for thisx, thisy, thisz in zip(x, y, z)]
+
+ # Determine style for stem lines.
+ linestyle, linemarker, linecolor = _process_plot_format(linefmt)
+ if linestyle is None:
+ linestyle = mpl.rcParams['lines.linestyle']
+
+ # Plot everything in required order.
+ baseline, = self.plot(basex, basey, basefmt, zs=bottom,
+ zdir=orientation, label='_nolegend_')
+ stemlines = art3d.Line3DCollection(
+ lines, linestyles=linestyle, colors=linecolor, label='_nolegend_')
+ self.add_collection(stemlines)
+ markerline, = self.plot(x, y, z, markerfmt, label='_nolegend_')
+
+ stem_container = StemContainer((markerline, stemlines, baseline),
+ label=label)
+ self.add_container(stem_container)
+
+ jx, jy, jz = art3d.juggle_axes(basexlim, baseylim, [bottom, bottom],
+ orientation)
+ self.auto_scale_xyz([*jx, *xlim], [*jy, *ylim], [*jz, *zlim], had_data)
+
+ return stem_container
+
+ stem3D = stem
+
+
+def get_test_data(delta=0.05):
+ """Return a tuple X, Y, Z with a test data set."""
+ x = y = np.arange(-3.0, 3.0, delta)
+ X, Y = np.meshgrid(x, y)
+
+ Z1 = np.exp(-(X**2 + Y**2) / 2) / (2 * np.pi)
+ Z2 = (np.exp(-(((X - 1) / 1.5)**2 + ((Y - 1) / 0.5)**2) / 2) /
+ (2 * np.pi * 0.5 * 1.5))
+ Z = Z2 - Z1
+
+ X = X * 10
+ Y = Y * 10
+ Z = Z * 500
+ return X, Y, Z
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/axis3d.py b/contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/axis3d.py
new file mode 100644
index 0000000000..4c5fa8a9c9
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/axis3d.py
@@ -0,0 +1,753 @@
+# axis3d.py, original mplot3d version by John Porter
+# Created: 23 Sep 2005
+# Parts rewritten by Reinier Heeres <reinier@heeres.eu>
+
+import inspect
+
+import numpy as np
+
+import matplotlib as mpl
+from matplotlib import (
+ _api, artist, lines as mlines, axis as maxis, patches as mpatches,
+ transforms as mtransforms, colors as mcolors)
+from . import art3d, proj3d
+
+
+def _move_from_center(coord, centers, deltas, axmask=(True, True, True)):
+ """
+ For each coordinate where *axmask* is True, move *coord* away from
+ *centers* by *deltas*.
+ """
+ coord = np.asarray(coord)
+ return coord + axmask * np.copysign(1, coord - centers) * deltas
+
+
+def _tick_update_position(tick, tickxs, tickys, labelpos):
+ """Update tick line and label position and style."""
+
+ tick.label1.set_position(labelpos)
+ tick.label2.set_position(labelpos)
+ tick.tick1line.set_visible(True)
+ tick.tick2line.set_visible(False)
+ tick.tick1line.set_linestyle('-')
+ tick.tick1line.set_marker('')
+ tick.tick1line.set_data(tickxs, tickys)
+ tick.gridline.set_data([0], [0])
+
+
+class Axis(maxis.XAxis):
+ """An Axis class for the 3D plots."""
+ # These points from the unit cube make up the x, y and z-planes
+ _PLANES = (
+ (0, 3, 7, 4), (1, 2, 6, 5), # yz planes
+ (0, 1, 5, 4), (3, 2, 6, 7), # xz planes
+ (0, 1, 2, 3), (4, 5, 6, 7), # xy planes
+ )
+
+ # Some properties for the axes
+ _AXINFO = {
+ 'x': {'i': 0, 'tickdir': 1, 'juggled': (1, 0, 2)},
+ 'y': {'i': 1, 'tickdir': 0, 'juggled': (0, 1, 2)},
+ 'z': {'i': 2, 'tickdir': 0, 'juggled': (0, 2, 1)},
+ }
+
+ def _old_init(self, adir, v_intervalx, d_intervalx, axes, *args,
+ rotate_label=None, **kwargs):
+ return locals()
+
+ def _new_init(self, axes, *, rotate_label=None, **kwargs):
+ return locals()
+
+ def __init__(self, *args, **kwargs):
+ params = _api.select_matching_signature(
+ [self._old_init, self._new_init], *args, **kwargs)
+ if "adir" in params:
+ _api.warn_deprecated(
+ "3.6", message=f"The signature of 3D Axis constructors has "
+ f"changed in %(since)s; the new signature is "
+ f"{inspect.signature(type(self).__init__)}", pending=True)
+ if params["adir"] != self.axis_name:
+ raise ValueError(f"Cannot instantiate {type(self).__name__} "
+ f"with adir={params['adir']!r}")
+ axes = params["axes"]
+ rotate_label = params["rotate_label"]
+ args = params.get("args", ())
+ kwargs = params["kwargs"]
+
+ name = self.axis_name
+
+ self._label_position = 'default'
+ self._tick_position = 'default'
+
+ # This is a temporary member variable.
+ # Do not depend on this existing in future releases!
+ self._axinfo = self._AXINFO[name].copy()
+ # Common parts
+ self._axinfo.update({
+ 'label': {'va': 'center', 'ha': 'center',
+ 'rotation_mode': 'anchor'},
+ 'color': mpl.rcParams[f'axes3d.{name}axis.panecolor'],
+ 'tick': {
+ 'inward_factor': 0.2,
+ 'outward_factor': 0.1,
+ },
+ })
+
+ if mpl.rcParams['_internal.classic_mode']:
+ self._axinfo.update({
+ 'axisline': {'linewidth': 0.75, 'color': (0, 0, 0, 1)},
+ 'grid': {
+ 'color': (0.9, 0.9, 0.9, 1),
+ 'linewidth': 1.0,
+ 'linestyle': '-',
+ },
+ })
+ self._axinfo['tick'].update({
+ 'linewidth': {
+ True: mpl.rcParams['lines.linewidth'], # major
+ False: mpl.rcParams['lines.linewidth'], # minor
+ }
+ })
+ else:
+ self._axinfo.update({
+ 'axisline': {
+ 'linewidth': mpl.rcParams['axes.linewidth'],
+ 'color': mpl.rcParams['axes.edgecolor'],
+ },
+ 'grid': {
+ 'color': mpl.rcParams['grid.color'],
+ 'linewidth': mpl.rcParams['grid.linewidth'],
+ 'linestyle': mpl.rcParams['grid.linestyle'],
+ },
+ })
+ self._axinfo['tick'].update({
+ 'linewidth': {
+ True: ( # major
+ mpl.rcParams['xtick.major.width'] if name in 'xz'
+ else mpl.rcParams['ytick.major.width']),
+ False: ( # minor
+ mpl.rcParams['xtick.minor.width'] if name in 'xz'
+ else mpl.rcParams['ytick.minor.width']),
+ }
+ })
+
+ super().__init__(axes, *args, **kwargs)
+
+ # data and viewing intervals for this direction
+ if "d_intervalx" in params:
+ self.set_data_interval(*params["d_intervalx"])
+ if "v_intervalx" in params:
+ self.set_view_interval(*params["v_intervalx"])
+ self.set_rotate_label(rotate_label)
+ self._init3d() # Inline after init3d deprecation elapses.
+
+ __init__.__signature__ = inspect.signature(_new_init)
+ adir = _api.deprecated("3.6", pending=True)(
+ property(lambda self: self.axis_name))
+
+ def _init3d(self):
+ self.line = mlines.Line2D(
+ xdata=(0, 0), ydata=(0, 0),
+ linewidth=self._axinfo['axisline']['linewidth'],
+ color=self._axinfo['axisline']['color'],
+ antialiased=True)
+
+ # Store dummy data in Polygon object
+ self.pane = mpatches.Polygon([[0, 0], [0, 1]], closed=False)
+ self.set_pane_color(self._axinfo['color'])
+
+ self.axes._set_artist_props(self.line)
+ self.axes._set_artist_props(self.pane)
+ self.gridlines = art3d.Line3DCollection([])
+ self.axes._set_artist_props(self.gridlines)
+ self.axes._set_artist_props(self.label)
+ self.axes._set_artist_props(self.offsetText)
+ # Need to be able to place the label at the correct location
+ self.label._transform = self.axes.transData
+ self.offsetText._transform = self.axes.transData
+
+ @_api.deprecated("3.6", pending=True)
+ def init3d(self): # After deprecation elapses, inline _init3d to __init__.
+ self._init3d()
+
+ def get_major_ticks(self, numticks=None):
+ ticks = super().get_major_ticks(numticks)
+ for t in ticks:
+ for obj in [
+ t.tick1line, t.tick2line, t.gridline, t.label1, t.label2]:
+ obj.set_transform(self.axes.transData)
+ return ticks
+
+ def get_minor_ticks(self, numticks=None):
+ ticks = super().get_minor_ticks(numticks)
+ for t in ticks:
+ for obj in [
+ t.tick1line, t.tick2line, t.gridline, t.label1, t.label2]:
+ obj.set_transform(self.axes.transData)
+ return ticks
+
+ def set_ticks_position(self, position):
+ """
+ Set the ticks position.
+
+ Parameters
+ ----------
+ position : {'lower', 'upper', 'both', 'default', 'none'}
+ The position of the bolded axis lines, ticks, and tick labels.
+ """
+ if position in ['top', 'bottom']:
+ _api.warn_deprecated('3.8', name=f'{position=}',
+ obj_type='argument value',
+ alternative="'upper' or 'lower'")
+ return
+ _api.check_in_list(['lower', 'upper', 'both', 'default', 'none'],
+ position=position)
+ self._tick_position = position
+
+ def get_ticks_position(self):
+ """
+ Get the ticks position.
+
+ Returns
+ -------
+ str : {'lower', 'upper', 'both', 'default', 'none'}
+ The position of the bolded axis lines, ticks, and tick labels.
+ """
+ return self._tick_position
+
+ def set_label_position(self, position):
+ """
+ Set the label position.
+
+ Parameters
+ ----------
+ position : {'lower', 'upper', 'both', 'default', 'none'}
+ The position of the axis label.
+ """
+ if position in ['top', 'bottom']:
+ _api.warn_deprecated('3.8', name=f'{position=}',
+ obj_type='argument value',
+ alternative="'upper' or 'lower'")
+ return
+ _api.check_in_list(['lower', 'upper', 'both', 'default', 'none'],
+ position=position)
+ self._label_position = position
+
+ def get_label_position(self):
+ """
+ Get the label position.
+
+ Returns
+ -------
+ str : {'lower', 'upper', 'both', 'default', 'none'}
+ The position of the axis label.
+ """
+ return self._label_position
+
+ def set_pane_color(self, color, alpha=None):
+ """
+ Set pane color.
+
+ Parameters
+ ----------
+ color : color
+ Color for axis pane.
+ alpha : float, optional
+ Alpha value for axis pane. If None, base it on *color*.
+ """
+ color = mcolors.to_rgba(color, alpha)
+ self._axinfo['color'] = color
+ self.pane.set_edgecolor(color)
+ self.pane.set_facecolor(color)
+ self.pane.set_alpha(color[-1])
+ self.stale = True
+
+ def set_rotate_label(self, val):
+ """
+ Whether to rotate the axis label: True, False or None.
+ If set to None the label will be rotated if longer than 4 chars.
+ """
+ self._rotate_label = val
+ self.stale = True
+
+ def get_rotate_label(self, text):
+ if self._rotate_label is not None:
+ return self._rotate_label
+ else:
+ return len(text) > 4
+
+ def _get_coord_info(self, renderer):
+ mins, maxs = np.array([
+ self.axes.get_xbound(),
+ self.axes.get_ybound(),
+ self.axes.get_zbound(),
+ ]).T
+
+ # Get the mean value for each bound:
+ centers = 0.5 * (maxs + mins)
+
+ # Add a small offset between min/max point and the edge of the plot:
+ deltas = (maxs - mins) / 12
+ mins -= 0.25 * deltas
+ maxs += 0.25 * deltas
+
+ # Project the bounds along the current position of the cube:
+ bounds = mins[0], maxs[0], mins[1], maxs[1], mins[2], maxs[2]
+ bounds_proj = self.axes._tunit_cube(bounds, self.axes.M)
+
+ # Determine which one of the parallel planes are higher up:
+ means_z0 = np.zeros(3)
+ means_z1 = np.zeros(3)
+ for i in range(3):
+ means_z0[i] = np.mean(bounds_proj[self._PLANES[2 * i], 2])
+ means_z1[i] = np.mean(bounds_proj[self._PLANES[2 * i + 1], 2])
+ highs = means_z0 < means_z1
+
+ # Special handling for edge-on views
+ equals = np.abs(means_z0 - means_z1) <= np.finfo(float).eps
+ if np.sum(equals) == 2:
+ vertical = np.where(~equals)[0][0]
+ if vertical == 2: # looking at XY plane
+ highs = np.array([True, True, highs[2]])
+ elif vertical == 1: # looking at XZ plane
+ highs = np.array([True, highs[1], False])
+ elif vertical == 0: # looking at YZ plane
+ highs = np.array([highs[0], False, False])
+
+ return mins, maxs, centers, deltas, bounds_proj, highs
+
+ def _get_axis_line_edge_points(self, minmax, maxmin, position=None):
+ """Get the edge points for the black bolded axis line."""
+ # When changing vertical axis some of the axes has to be
+ # moved to the other plane so it looks the same as if the z-axis
+ # was the vertical axis.
+ mb = [minmax, maxmin] # line from origin to nearest corner to camera
+ mb_rev = mb[::-1]
+ mm = [[mb, mb_rev, mb_rev], [mb_rev, mb_rev, mb], [mb, mb, mb]]
+ mm = mm[self.axes._vertical_axis][self._axinfo["i"]]
+
+ juggled = self._axinfo["juggled"]
+ edge_point_0 = mm[0].copy() # origin point
+
+ if ((position == 'lower' and mm[1][juggled[-1]] < mm[0][juggled[-1]]) or
+ (position == 'upper' and mm[1][juggled[-1]] > mm[0][juggled[-1]])):
+ edge_point_0[juggled[-1]] = mm[1][juggled[-1]]
+ else:
+ edge_point_0[juggled[0]] = mm[1][juggled[0]]
+
+ edge_point_1 = edge_point_0.copy()
+ edge_point_1[juggled[1]] = mm[1][juggled[1]]
+
+ return edge_point_0, edge_point_1
+
+ def _get_all_axis_line_edge_points(self, minmax, maxmin, axis_position=None):
+ # Determine edge points for the axis lines
+ edgep1s = []
+ edgep2s = []
+ position = []
+ if axis_position in (None, 'default'):
+ edgep1, edgep2 = self._get_axis_line_edge_points(minmax, maxmin)
+ edgep1s = [edgep1]
+ edgep2s = [edgep2]
+ position = ['default']
+ else:
+ edgep1_l, edgep2_l = self._get_axis_line_edge_points(minmax, maxmin,
+ position='lower')
+ edgep1_u, edgep2_u = self._get_axis_line_edge_points(minmax, maxmin,
+ position='upper')
+ if axis_position in ('lower', 'both'):
+ edgep1s.append(edgep1_l)
+ edgep2s.append(edgep2_l)
+ position.append('lower')
+ if axis_position in ('upper', 'both'):
+ edgep1s.append(edgep1_u)
+ edgep2s.append(edgep2_u)
+ position.append('upper')
+ return edgep1s, edgep2s, position
+
+ def _get_tickdir(self, position):
+ """
+ Get the direction of the tick.
+
+ Parameters
+ ----------
+ position : str, optional : {'upper', 'lower', 'default'}
+ The position of the axis.
+
+ Returns
+ -------
+ tickdir : int
+ Index which indicates which coordinate the tick line will
+ align with.
+ """
+ _api.check_in_list(('upper', 'lower', 'default'), position=position)
+
+ # TODO: Move somewhere else where it's triggered less:
+ tickdirs_base = [v["tickdir"] for v in self._AXINFO.values()] # default
+ elev_mod = np.mod(self.axes.elev + 180, 360) - 180
+ azim_mod = np.mod(self.axes.azim, 360)
+ if position == 'upper':
+ if elev_mod >= 0:
+ tickdirs_base = [2, 2, 0]
+ else:
+ tickdirs_base = [1, 0, 0]
+ if 0 <= azim_mod < 180:
+ tickdirs_base[2] = 1
+ elif position == 'lower':
+ if elev_mod >= 0:
+ tickdirs_base = [1, 0, 1]
+ else:
+ tickdirs_base = [2, 2, 1]
+ if 0 <= azim_mod < 180:
+ tickdirs_base[2] = 0
+ info_i = [v["i"] for v in self._AXINFO.values()]
+
+ i = self._axinfo["i"]
+ vert_ax = self.axes._vertical_axis
+ j = vert_ax - 2
+ # default: tickdir = [[1, 2, 1], [2, 2, 0], [1, 0, 0]][vert_ax][i]
+ tickdir = np.roll(info_i, -j)[np.roll(tickdirs_base, j)][i]
+ return tickdir
+
+ def active_pane(self, renderer):
+ mins, maxs, centers, deltas, tc, highs = self._get_coord_info(renderer)
+ info = self._axinfo
+ index = info['i']
+ if not highs[index]:
+ loc = mins[index]
+ plane = self._PLANES[2 * index]
+ else:
+ loc = maxs[index]
+ plane = self._PLANES[2 * index + 1]
+ xys = np.array([tc[p] for p in plane])
+ return xys, loc
+
+ def draw_pane(self, renderer):
+ """
+ Draw pane.
+
+ Parameters
+ ----------
+ renderer : `~matplotlib.backend_bases.RendererBase` subclass
+ """
+ renderer.open_group('pane3d', gid=self.get_gid())
+ xys, loc = self.active_pane(renderer)
+ self.pane.xy = xys[:, :2]
+ self.pane.draw(renderer)
+ renderer.close_group('pane3d')
+
+ def _axmask(self):
+ axmask = [True, True, True]
+ axmask[self._axinfo["i"]] = False
+ return axmask
+
+ def _draw_ticks(self, renderer, edgep1, centers, deltas, highs,
+ deltas_per_point, pos):
+ ticks = self._update_ticks()
+ info = self._axinfo
+ index = info["i"]
+
+ # Draw ticks:
+ tickdir = self._get_tickdir(pos)
+ tickdelta = deltas[tickdir] if highs[tickdir] else -deltas[tickdir]
+
+ tick_info = info['tick']
+ tick_out = tick_info['outward_factor'] * tickdelta
+ tick_in = tick_info['inward_factor'] * tickdelta
+ tick_lw = tick_info['linewidth']
+ edgep1_tickdir = edgep1[tickdir]
+ out_tickdir = edgep1_tickdir + tick_out
+ in_tickdir = edgep1_tickdir - tick_in
+
+ default_label_offset = 8. # A rough estimate
+ points = deltas_per_point * deltas
+ for tick in ticks:
+ # Get tick line positions
+ pos = edgep1.copy()
+ pos[index] = tick.get_loc()
+ pos[tickdir] = out_tickdir
+ x1, y1, z1 = proj3d.proj_transform(*pos, self.axes.M)
+ pos[tickdir] = in_tickdir
+ x2, y2, z2 = proj3d.proj_transform(*pos, self.axes.M)
+
+ # Get position of label
+ labeldeltas = (tick.get_pad() + default_label_offset) * points
+
+ pos[tickdir] = edgep1_tickdir
+ pos = _move_from_center(pos, centers, labeldeltas, self._axmask())
+ lx, ly, lz = proj3d.proj_transform(*pos, self.axes.M)
+
+ _tick_update_position(tick, (x1, x2), (y1, y2), (lx, ly))
+ tick.tick1line.set_linewidth(tick_lw[tick._major])
+ tick.draw(renderer)
+
+ def _draw_offset_text(self, renderer, edgep1, edgep2, labeldeltas, centers,
+ highs, pep, dx, dy):
+ # Get general axis information:
+ info = self._axinfo
+ index = info["i"]
+ juggled = info["juggled"]
+ tickdir = info["tickdir"]
+
+ # Which of the two edge points do we want to
+ # use for locating the offset text?
+ if juggled[2] == 2:
+ outeredgep = edgep1
+ outerindex = 0
+ else:
+ outeredgep = edgep2
+ outerindex = 1
+
+ pos = _move_from_center(outeredgep, centers, labeldeltas,
+ self._axmask())
+ olx, oly, olz = proj3d.proj_transform(*pos, self.axes.M)
+ self.offsetText.set_text(self.major.formatter.get_offset())
+ self.offsetText.set_position((olx, oly))
+ angle = art3d._norm_text_angle(np.rad2deg(np.arctan2(dy, dx)))
+ self.offsetText.set_rotation(angle)
+ # Must set rotation mode to "anchor" so that
+ # the alignment point is used as the "fulcrum" for rotation.
+ self.offsetText.set_rotation_mode('anchor')
+
+ # ----------------------------------------------------------------------
+ # Note: the following statement for determining the proper alignment of
+ # the offset text. This was determined entirely by trial-and-error
+ # and should not be in any way considered as "the way". There are
+ # still some edge cases where alignment is not quite right, but this
+ # seems to be more of a geometry issue (in other words, I might be
+ # using the wrong reference points).
+ #
+ # (TT, FF, TF, FT) are the shorthand for the tuple of
+ # (centpt[tickdir] <= pep[tickdir, outerindex],
+ # centpt[index] <= pep[index, outerindex])
+ #
+ # Three-letters (e.g., TFT, FTT) are short-hand for the array of bools
+ # from the variable 'highs'.
+ # ---------------------------------------------------------------------
+ centpt = proj3d.proj_transform(*centers, self.axes.M)
+ if centpt[tickdir] > pep[tickdir, outerindex]:
+ # if FT and if highs has an even number of Trues
+ if (centpt[index] <= pep[index, outerindex]
+ and np.count_nonzero(highs) % 2 == 0):
+ # Usually, this means align right, except for the FTT case,
+ # in which offset for axis 1 and 2 are aligned left.
+ if highs.tolist() == [False, True, True] and index in (1, 2):
+ align = 'left'
+ else:
+ align = 'right'
+ else:
+ # The FF case
+ align = 'left'
+ else:
+ # if TF and if highs has an even number of Trues
+ if (centpt[index] > pep[index, outerindex]
+ and np.count_nonzero(highs) % 2 == 0):
+ # Usually mean align left, except if it is axis 2
+ align = 'right' if index == 2 else 'left'
+ else:
+ # The TT case
+ align = 'right'
+
+ self.offsetText.set_va('center')
+ self.offsetText.set_ha(align)
+ self.offsetText.draw(renderer)
+
+ def _draw_labels(self, renderer, edgep1, edgep2, labeldeltas, centers, dx, dy):
+ label = self._axinfo["label"]
+
+ # Draw labels
+ lxyz = 0.5 * (edgep1 + edgep2)
+ lxyz = _move_from_center(lxyz, centers, labeldeltas, self._axmask())
+ tlx, tly, tlz = proj3d.proj_transform(*lxyz, self.axes.M)
+ self.label.set_position((tlx, tly))
+ if self.get_rotate_label(self.label.get_text()):
+ angle = art3d._norm_text_angle(np.rad2deg(np.arctan2(dy, dx)))
+ self.label.set_rotation(angle)
+ self.label.set_va(label['va'])
+ self.label.set_ha(label['ha'])
+ self.label.set_rotation_mode(label['rotation_mode'])
+ self.label.draw(renderer)
+
+ @artist.allow_rasterization
+ def draw(self, renderer):
+ self.label._transform = self.axes.transData
+ self.offsetText._transform = self.axes.transData
+ renderer.open_group("axis3d", gid=self.get_gid())
+
+ # Get general axis information:
+ mins, maxs, centers, deltas, tc, highs = self._get_coord_info(renderer)
+
+ # Calculate offset distances
+ # A rough estimate; points are ambiguous since 3D plots rotate
+ reltoinches = self.figure.dpi_scale_trans.inverted()
+ ax_inches = reltoinches.transform(self.axes.bbox.size)
+ ax_points_estimate = sum(72. * ax_inches)
+ deltas_per_point = 48 / ax_points_estimate
+ default_offset = 21.
+ labeldeltas = (self.labelpad + default_offset) * deltas_per_point * deltas
+
+ # Determine edge points for the axis lines
+ minmax = np.where(highs, maxs, mins) # "origin" point
+ maxmin = np.where(~highs, maxs, mins) # "opposite" corner near camera
+
+ for edgep1, edgep2, pos in zip(*self._get_all_axis_line_edge_points(
+ minmax, maxmin, self._tick_position)):
+ # Project the edge points along the current position
+ pep = proj3d._proj_trans_points([edgep1, edgep2], self.axes.M)
+ pep = np.asarray(pep)
+
+ # The transAxes transform is used because the Text object
+ # rotates the text relative to the display coordinate system.
+ # Therefore, if we want the labels to remain parallel to the
+ # axis regardless of the aspect ratio, we need to convert the
+ # edge points of the plane to display coordinates and calculate
+ # an angle from that.
+ # TODO: Maybe Text objects should handle this themselves?
+ dx, dy = (self.axes.transAxes.transform([pep[0:2, 1]]) -
+ self.axes.transAxes.transform([pep[0:2, 0]]))[0]
+
+ # Draw the lines
+ self.line.set_data(pep[0], pep[1])
+ self.line.draw(renderer)
+
+ # Draw ticks
+ self._draw_ticks(renderer, edgep1, centers, deltas, highs,
+ deltas_per_point, pos)
+
+ # Draw Offset text
+ self._draw_offset_text(renderer, edgep1, edgep2, labeldeltas,
+ centers, highs, pep, dx, dy)
+
+ for edgep1, edgep2, pos in zip(*self._get_all_axis_line_edge_points(
+ minmax, maxmin, self._label_position)):
+ # See comments above
+ pep = proj3d._proj_trans_points([edgep1, edgep2], self.axes.M)
+ pep = np.asarray(pep)
+ dx, dy = (self.axes.transAxes.transform([pep[0:2, 1]]) -
+ self.axes.transAxes.transform([pep[0:2, 0]]))[0]
+
+ # Draw labels
+ self._draw_labels(renderer, edgep1, edgep2, labeldeltas, centers, dx, dy)
+
+ renderer.close_group('axis3d')
+ self.stale = False
+
+ @artist.allow_rasterization
+ def draw_grid(self, renderer):
+ if not self.axes._draw_grid:
+ return
+
+ renderer.open_group("grid3d", gid=self.get_gid())
+
+ ticks = self._update_ticks()
+ if len(ticks):
+ # Get general axis information:
+ info = self._axinfo
+ index = info["i"]
+
+ mins, maxs, _, _, _, highs = self._get_coord_info(renderer)
+
+ minmax = np.where(highs, maxs, mins)
+ maxmin = np.where(~highs, maxs, mins)
+
+ # Grid points where the planes meet
+ xyz0 = np.tile(minmax, (len(ticks), 1))
+ xyz0[:, index] = [tick.get_loc() for tick in ticks]
+
+ # Grid lines go from the end of one plane through the plane
+ # intersection (at xyz0) to the end of the other plane. The first
+ # point (0) differs along dimension index-2 and the last (2) along
+ # dimension index-1.
+ lines = np.stack([xyz0, xyz0, xyz0], axis=1)
+ lines[:, 0, index - 2] = maxmin[index - 2]
+ lines[:, 2, index - 1] = maxmin[index - 1]
+ self.gridlines.set_segments(lines)
+ gridinfo = info['grid']
+ self.gridlines.set_color(gridinfo['color'])
+ self.gridlines.set_linewidth(gridinfo['linewidth'])
+ self.gridlines.set_linestyle(gridinfo['linestyle'])
+ self.gridlines.do_3d_projection()
+ self.gridlines.draw(renderer)
+
+ renderer.close_group('grid3d')
+
+ # TODO: Get this to work (more) properly when mplot3d supports the
+ # transforms framework.
+ def get_tightbbox(self, renderer=None, *, for_layout_only=False):
+ # docstring inherited
+ if not self.get_visible():
+ return
+ # We have to directly access the internal data structures
+ # (and hope they are up to date) because at draw time we
+ # shift the ticks and their labels around in (x, y) space
+ # based on the projection, the current view port, and their
+ # position in 3D space. If we extend the transforms framework
+ # into 3D we would not need to do this different book keeping
+ # than we do in the normal axis
+ major_locs = self.get_majorticklocs()
+ minor_locs = self.get_minorticklocs()
+
+ ticks = [*self.get_minor_ticks(len(minor_locs)),
+ *self.get_major_ticks(len(major_locs))]
+ view_low, view_high = self.get_view_interval()
+ if view_low > view_high:
+ view_low, view_high = view_high, view_low
+ interval_t = self.get_transform().transform([view_low, view_high])
+
+ ticks_to_draw = []
+ for tick in ticks:
+ try:
+ loc_t = self.get_transform().transform(tick.get_loc())
+ except AssertionError:
+ # Transform.transform doesn't allow masked values but
+ # some scales might make them, so we need this try/except.
+ pass
+ else:
+ if mtransforms._interval_contains_close(interval_t, loc_t):
+ ticks_to_draw.append(tick)
+
+ ticks = ticks_to_draw
+
+ bb_1, bb_2 = self._get_ticklabel_bboxes(ticks, renderer)
+ other = []
+
+ if self.line.get_visible():
+ other.append(self.line.get_window_extent(renderer))
+ if (self.label.get_visible() and not for_layout_only and
+ self.label.get_text()):
+ other.append(self.label.get_window_extent(renderer))
+
+ return mtransforms.Bbox.union([*bb_1, *bb_2, *other])
+
+ d_interval = _api.deprecated(
+ "3.6", alternative="get_data_interval", pending=True)(
+ property(lambda self: self.get_data_interval(),
+ lambda self, minmax: self.set_data_interval(*minmax)))
+ v_interval = _api.deprecated(
+ "3.6", alternative="get_view_interval", pending=True)(
+ property(lambda self: self.get_view_interval(),
+ lambda self, minmax: self.set_view_interval(*minmax)))
+
+
+class XAxis(Axis):
+ axis_name = "x"
+ get_view_interval, set_view_interval = maxis._make_getset_interval(
+ "view", "xy_viewLim", "intervalx")
+ get_data_interval, set_data_interval = maxis._make_getset_interval(
+ "data", "xy_dataLim", "intervalx")
+
+
+class YAxis(Axis):
+ axis_name = "y"
+ get_view_interval, set_view_interval = maxis._make_getset_interval(
+ "view", "xy_viewLim", "intervaly")
+ get_data_interval, set_data_interval = maxis._make_getset_interval(
+ "data", "xy_dataLim", "intervaly")
+
+
+class ZAxis(Axis):
+ axis_name = "z"
+ get_view_interval, set_view_interval = maxis._make_getset_interval(
+ "view", "zz_viewLim", "intervalx")
+ get_data_interval, set_data_interval = maxis._make_getset_interval(
+ "data", "zz_dataLim", "intervalx")
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/proj3d.py b/contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/proj3d.py
new file mode 100644
index 0000000000..098a7b6f66
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/proj3d.py
@@ -0,0 +1,259 @@
+"""
+Various transforms used for by the 3D code
+"""
+
+import numpy as np
+
+from matplotlib import _api
+
+
+def world_transformation(xmin, xmax,
+ ymin, ymax,
+ zmin, zmax, pb_aspect=None):
+ """
+ Produce a matrix that scales homogeneous coords in the specified ranges
+ to [0, 1], or [0, pb_aspect[i]] if the plotbox aspect ratio is specified.
+ """
+ dx = xmax - xmin
+ dy = ymax - ymin
+ dz = zmax - zmin
+ if pb_aspect is not None:
+ ax, ay, az = pb_aspect
+ dx /= ax
+ dy /= ay
+ dz /= az
+
+ return np.array([[1/dx, 0, 0, -xmin/dx],
+ [0, 1/dy, 0, -ymin/dy],
+ [0, 0, 1/dz, -zmin/dz],
+ [0, 0, 0, 1]])
+
+
+@_api.deprecated("3.8")
+def rotation_about_vector(v, angle):
+ """
+ Produce a rotation matrix for an angle in radians about a vector.
+ """
+ return _rotation_about_vector(v, angle)
+
+
+def _rotation_about_vector(v, angle):
+ """
+ Produce a rotation matrix for an angle in radians about a vector.
+ """
+ vx, vy, vz = v / np.linalg.norm(v)
+ s = np.sin(angle)
+ c = np.cos(angle)
+ t = 2*np.sin(angle/2)**2 # more numerically stable than t = 1-c
+
+ R = np.array([
+ [t*vx*vx + c, t*vx*vy - vz*s, t*vx*vz + vy*s],
+ [t*vy*vx + vz*s, t*vy*vy + c, t*vy*vz - vx*s],
+ [t*vz*vx - vy*s, t*vz*vy + vx*s, t*vz*vz + c]])
+
+ return R
+
+
+def _view_axes(E, R, V, roll):
+ """
+ Get the unit viewing axes in data coordinates.
+
+ Parameters
+ ----------
+ E : 3-element numpy array
+ The coordinates of the eye/camera.
+ R : 3-element numpy array
+ The coordinates of the center of the view box.
+ V : 3-element numpy array
+ Unit vector in the direction of the vertical axis.
+ roll : float
+ The roll angle in radians.
+
+ Returns
+ -------
+ u : 3-element numpy array
+ Unit vector pointing towards the right of the screen.
+ v : 3-element numpy array
+ Unit vector pointing towards the top of the screen.
+ w : 3-element numpy array
+ Unit vector pointing out of the screen.
+ """
+ w = (E - R)
+ w = w/np.linalg.norm(w)
+ u = np.cross(V, w)
+ u = u/np.linalg.norm(u)
+ v = np.cross(w, u) # Will be a unit vector
+
+ # Save some computation for the default roll=0
+ if roll != 0:
+ # A positive rotation of the camera is a negative rotation of the world
+ Rroll = _rotation_about_vector(w, -roll)
+ u = np.dot(Rroll, u)
+ v = np.dot(Rroll, v)
+ return u, v, w
+
+
+def _view_transformation_uvw(u, v, w, E):
+ """
+ Return the view transformation matrix.
+
+ Parameters
+ ----------
+ u : 3-element numpy array
+ Unit vector pointing towards the right of the screen.
+ v : 3-element numpy array
+ Unit vector pointing towards the top of the screen.
+ w : 3-element numpy array
+ Unit vector pointing out of the screen.
+ E : 3-element numpy array
+ The coordinates of the eye/camera.
+ """
+ Mr = np.eye(4)
+ Mt = np.eye(4)
+ Mr[:3, :3] = [u, v, w]
+ Mt[:3, -1] = -E
+ M = np.dot(Mr, Mt)
+ return M
+
+
+@_api.deprecated("3.8")
+def view_transformation(E, R, V, roll):
+ """
+ Return the view transformation matrix.
+
+ Parameters
+ ----------
+ E : 3-element numpy array
+ The coordinates of the eye/camera.
+ R : 3-element numpy array
+ The coordinates of the center of the view box.
+ V : 3-element numpy array
+ Unit vector in the direction of the vertical axis.
+ roll : float
+ The roll angle in radians.
+ """
+ u, v, w = _view_axes(E, R, V, roll)
+ M = _view_transformation_uvw(u, v, w, E)
+ return M
+
+
+@_api.deprecated("3.8")
+def persp_transformation(zfront, zback, focal_length):
+ return _persp_transformation(zfront, zback, focal_length)
+
+
+def _persp_transformation(zfront, zback, focal_length):
+ e = focal_length
+ a = 1 # aspect ratio
+ b = (zfront+zback)/(zfront-zback)
+ c = -2*(zfront*zback)/(zfront-zback)
+ proj_matrix = np.array([[e, 0, 0, 0],
+ [0, e/a, 0, 0],
+ [0, 0, b, c],
+ [0, 0, -1, 0]])
+ return proj_matrix
+
+
+@_api.deprecated("3.8")
+def ortho_transformation(zfront, zback):
+ return _ortho_transformation(zfront, zback)
+
+
+def _ortho_transformation(zfront, zback):
+ # note: w component in the resulting vector will be (zback-zfront), not 1
+ a = -(zfront + zback)
+ b = -(zfront - zback)
+ proj_matrix = np.array([[2, 0, 0, 0],
+ [0, 2, 0, 0],
+ [0, 0, -2, 0],
+ [0, 0, a, b]])
+ return proj_matrix
+
+
+def _proj_transform_vec(vec, M):
+ vecw = np.dot(M, vec)
+ w = vecw[3]
+ # clip here..
+ txs, tys, tzs = vecw[0]/w, vecw[1]/w, vecw[2]/w
+ return txs, tys, tzs
+
+
+def _proj_transform_vec_clip(vec, M):
+ vecw = np.dot(M, vec)
+ w = vecw[3]
+ # clip here.
+ txs, tys, tzs = vecw[0] / w, vecw[1] / w, vecw[2] / w
+ tis = (0 <= vecw[0]) & (vecw[0] <= 1) & (0 <= vecw[1]) & (vecw[1] <= 1)
+ if np.any(tis):
+ tis = vecw[1] < 1
+ return txs, tys, tzs, tis
+
+
+def inv_transform(xs, ys, zs, invM):
+ """
+ Transform the points by the inverse of the projection matrix, *invM*.
+ """
+ vec = _vec_pad_ones(xs, ys, zs)
+ vecr = np.dot(invM, vec)
+ if vecr.shape == (4,):
+ vecr = vecr.reshape((4, 1))
+ for i in range(vecr.shape[1]):
+ if vecr[3][i] != 0:
+ vecr[:, i] = vecr[:, i] / vecr[3][i]
+ return vecr[0], vecr[1], vecr[2]
+
+
+def _vec_pad_ones(xs, ys, zs):
+ return np.array([xs, ys, zs, np.ones_like(xs)])
+
+
+def proj_transform(xs, ys, zs, M):
+ """
+ Transform the points by the projection matrix *M*.
+ """
+ vec = _vec_pad_ones(xs, ys, zs)
+ return _proj_transform_vec(vec, M)
+
+
+transform = _api.deprecated(
+ "3.8", obj_type="function", name="transform",
+ alternative="proj_transform")(proj_transform)
+
+
+def proj_transform_clip(xs, ys, zs, M):
+ """
+ Transform the points by the projection matrix
+ and return the clipping result
+ returns txs, tys, tzs, tis
+ """
+ vec = _vec_pad_ones(xs, ys, zs)
+ return _proj_transform_vec_clip(vec, M)
+
+
+@_api.deprecated("3.8")
+def proj_points(points, M):
+ return _proj_points(points, M)
+
+
+def _proj_points(points, M):
+ return np.column_stack(_proj_trans_points(points, M))
+
+
+@_api.deprecated("3.8")
+def proj_trans_points(points, M):
+ return _proj_trans_points(points, M)
+
+
+def _proj_trans_points(points, M):
+ xs, ys, zs = zip(*points)
+ return proj_transform(xs, ys, zs, M)
+
+
+@_api.deprecated("3.8")
+def rot_x(V, alpha):
+ cosa, sina = np.cos(alpha), np.sin(alpha)
+ M1 = np.array([[1, 0, 0, 0],
+ [0, cosa, -sina, 0],
+ [0, sina, cosa, 0],
+ [0, 0, 0, 1]])
+ return np.dot(M1, V)