aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/axes3d.py
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/mplot3d/axes3d.py
parentdd6d20cadb65582270ac23f4b3b14ae189704b9d (diff)
downloadydb-77eb2d3fdcec5c978c64e025ced2764c57c00285.tar.gz
KIKIMR-19287: add task_stats_drawing script
Diffstat (limited to 'contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/axes3d.py')
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/axes3d.py3448
1 files changed, 3448 insertions, 0 deletions
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