|
""" |
|
axes3d.py, original mplot3d version by John Porter |
|
Created: 23 Sep 2005 |
|
|
|
Parts fixed by Reinier Heeres <[email protected]> |
|
Minor additions by Ben Axelrod <[email protected]> |
|
Significant updates and revisions by Ben Root <[email protected]> |
|
|
|
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() |
|
|
|
self.zz_dataLim = Bbox.unit() |
|
|
|
|
|
|
|
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 |
|
) |
|
|
|
super().set_axis_off() |
|
|
|
self.set_axis_on() |
|
self.M = None |
|
self.invM = None |
|
|
|
|
|
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) |
|
|
|
pseudo_bbox = self.transLimits.inverted().transform([(0, 0), (1, 1)]) |
|
self._pseudo_w, self._pseudo_h = pseudo_bbox[1] - pseudo_bbox[0] |
|
|
|
|
|
|
|
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): |
|
|
|
|
|
xdwl = 0.95 / self._dist |
|
xdw = 0.9 / self._dist |
|
ydwl = 0.95 / self._dist |
|
ydw = 0.9 / self._dist |
|
|
|
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_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 = [] |
|
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) |
|
|
|
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) |
|
|
|
|
|
|
|
|
|
trans = self.get_figure().transSubfigure |
|
bb = mtransforms.Bbox.unit().transformed(trans) |
|
|
|
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() |
|
|
|
|
|
self.patch.draw(renderer) |
|
self._frameon = False |
|
|
|
|
|
|
|
|
|
|
|
|
|
locator = self.get_axes_locator() |
|
self.apply_aspect(locator(self, renderer) if locator else None) |
|
|
|
|
|
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: |
|
|
|
|
|
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: |
|
|
|
for axis in self._axis_map.values(): |
|
axis.draw_pane(renderer) |
|
|
|
for axis in self._axis_map.values(): |
|
axis.draw_grid(renderer) |
|
|
|
for axis in self._axis_map.values(): |
|
axis.draw(renderer) |
|
|
|
|
|
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): |
|
|
|
|
|
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) |
|
|
|
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. |
|
""" |
|
|
|
|
|
if tight is None: |
|
_tight = self._tight |
|
if not _tight: |
|
|
|
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 |
|
|
|
|
|
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): |
|
|
|
return tuple(self.xy_viewLim.intervalx) |
|
|
|
def get_ylim(self): |
|
|
|
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") |
|
|
|
|
|
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 |
|
|
|
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: |
|
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.""" |
|
|
|
|
|
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, |
|
) |
|
|
|
|
|
R = 0.5 * box_aspect |
|
|
|
|
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
ps = self._roll_to_vertical([p0, p1, p2]) |
|
|
|
|
|
|
|
eye = R + self._dist * ps |
|
|
|
|
|
vvec = R - eye |
|
self._eye = eye |
|
self._vvec = vvec / np.linalg.norm(vvec) |
|
|
|
|
|
u, v, w = self._calc_view_axes(eye) |
|
self._view_u = u |
|
self._view_v = v |
|
self._view_w = w |
|
|
|
|
|
if self._focal_length == np.inf: |
|
|
|
viewM = proj3d._view_transformation_uvw(u, v, w, eye) |
|
projM = proj3d._ortho_transformation(-self._dist, self._dist) |
|
else: |
|
|
|
|
|
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) |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
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): |
|
|
|
return True |
|
|
|
def can_pan(self): |
|
|
|
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 |
|
self.zaxis.minor = other.zaxis.minor |
|
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): |
|
|
|
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 |
|
|
|
|
|
if toolbar and self.get_navigate_mode() is None: |
|
toolbar.push_current() |
|
|
|
def _get_view(self): |
|
|
|
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): |
|
|
|
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: |
|
|
|
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: |
|
focal_length = 1e9 |
|
else: |
|
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: |
|
zv = 1 |
|
else: |
|
zv = -1 / self._focal_length |
|
|
|
|
|
p1 = np.array(proj3d.inv_transform(xv, yv, zv, self.invM)).ravel() |
|
|
|
|
|
vec = self._get_camera_loc() - p1 |
|
|
|
|
|
pane_locs = [] |
|
for axis in self._axis_map.values(): |
|
xys, loc = axis.active_pane(renderer) |
|
pane_locs.append(loc) |
|
|
|
|
|
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] |
|
|
|
|
|
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: |
|
|
|
|
|
return |
|
|
|
if self.M is None: |
|
return |
|
|
|
x, y = event.xdata, event.ydata |
|
|
|
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 |
|
|
|
|
|
if self.button_pressed in self._rotate_btn: |
|
|
|
|
|
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 |
|
|
|
|
|
elif self.button_pressed in self._pan_btn: |
|
|
|
px, py = self.transData.transform([self._sx, self._sy]) |
|
self.start_pan(px, py, 2) |
|
|
|
self.drag_pan(2, None, event.x, event.y) |
|
self.end_pan() |
|
|
|
|
|
elif self.button_pressed in self._zoom_btn: |
|
|
|
scale = h/(h - dy) |
|
self._scale_axis_limits(scale, scale, scale) |
|
|
|
|
|
self._sx, self._sy = x, y |
|
|
|
self.figure.canvas.draw_idle() |
|
|
|
def drag_pan(self, button, key, x, y): |
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
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]) |
|
|
|
|
|
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] |
|
|
|
|
|
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)) |
|
|
|
|
|
R = 0.5 * self._roll_to_vertical(self._box_aspect) |
|
|
|
|
|
|
|
|
|
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] |
|
|
|
|
|
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]) |
|
|
|
|
|
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() |
|
|
|
|
|
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]) |
|
|
|
|
|
scale = max(scale_u, scale_v) |
|
|
|
|
|
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]) |
|
|
|
|
|
if not np.allclose(scale, scale_u): |
|
|
|
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) |
|
|
|
|
|
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. |
|
""" |
|
|
|
cx, cy, cz, dx, dy, dz = self._get_w_centers_ranges() |
|
|
|
|
|
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.""" |
|
|
|
minx, maxx, miny, maxy, minz, maxz = self.get_w_lims() |
|
cx = (maxx + minx)/2 |
|
cy = (maxy + miny)/2 |
|
cz = (maxz + minz)/2 |
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
|
|
|
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. |
|
""" |
|
|
|
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) |
|
|
|
|
|
|
|
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() |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
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']: |
|
|
|
|
|
|
|
compute_strides = has_count |
|
else: |
|
|
|
|
|
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 = [] |
|
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: |
|
|
|
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 = [ |
|
|
|
cbook._array_perimeter(a[rs:rs_next+1, cs:cs_next+1]) |
|
for a in (X, Y, Z) |
|
] |
|
|
|
ps = np.array(ps).T |
|
polys.append(ps) |
|
|
|
if fcolors is not None: |
|
colset.append(fcolors[rs][cs]) |
|
|
|
|
|
|
|
|
|
if not isinstance(polys, np.ndarray) or not np.isfinite(polys).all(): |
|
new_polys = [] |
|
new_colset = [] |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
|
|
polys = new_polys |
|
if fcolors is not None: |
|
colset = new_colset |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
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.") |
|
|
|
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']: |
|
|
|
|
|
|
|
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 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 |
|
|
|
|
|
|
|
|
|
tX, tY, tZ = np.transpose(X), np.transpose(Y), np.transpose(Z) |
|
|
|
if rstride: |
|
rii = list(range(0, rows, rstride)) |
|
|
|
if rows > 0 and rii[-1] != (rows - 1): |
|
rii += [rows-1] |
|
else: |
|
rii = [] |
|
if cstride: |
|
cii = list(range(0, cols, cstride)) |
|
|
|
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 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() |
|
|
|
|
|
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: |
|
|
|
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) |
|
|
|
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), |
|
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 |
|
|
|
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: |
|
|
|
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): |
|
|
|
|
|
dim_vals = {'x': X, 'y': Y, 'z': Z, zdir: levels} |
|
|
|
|
|
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: |
|
|
|
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) |
|
|
|
|
|
|
|
|
|
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) |
|
|
|
xs, ys, zs, s, c, color = cbook.delete_masked_points( |
|
xs, ys, zs, s, c, kwargs.get('color', None) |
|
) |
|
if kwargs.get("color") is not None: |
|
kwargs['color'] = color |
|
|
|
|
|
if np.may_share_memory(zs_orig, zs): |
|
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: |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
|
|
|
|
cuboid = np.array([ |
|
|
|
( |
|
(0, 0, 0), |
|
(0, 1, 0), |
|
(1, 1, 0), |
|
(1, 0, 0), |
|
), |
|
|
|
( |
|
(0, 0, 1), |
|
(1, 0, 1), |
|
(1, 1, 1), |
|
(0, 1, 1), |
|
), |
|
|
|
( |
|
(0, 0, 0), |
|
(1, 0, 0), |
|
(1, 0, 1), |
|
(0, 0, 1), |
|
), |
|
|
|
( |
|
(0, 1, 0), |
|
(0, 1, 1), |
|
(1, 1, 1), |
|
(1, 1, 0), |
|
), |
|
|
|
( |
|
(0, 0, 0), |
|
(0, 0, 1), |
|
(0, 1, 1), |
|
(0, 1, 0), |
|
), |
|
|
|
( |
|
(1, 0, 0), |
|
(1, 1, 0), |
|
(1, 1, 1), |
|
(1, 0, 1), |
|
), |
|
]) |
|
|
|
|
|
polys = np.empty(x.shape + cuboid.shape) |
|
|
|
|
|
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] |
|
|
|
|
|
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): |
|
|
|
for c in color: |
|
facecolors.extend([c] * 6) |
|
else: |
|
|
|
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): |
|
|
|
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): |
|
|
|
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)) |
|
|
|
rangle = math.radians(15) |
|
c = math.cos(rangle) |
|
s = math.sin(rangle) |
|
|
|
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)]]) |
|
|
|
Rneg = Rpos.copy() |
|
Rneg[[0, 1, 2, 2], [2, 2, 0, 1]] *= -1 |
|
|
|
Rpos_vecs = np.einsum("ij...,...j->...i", Rpos, UVW) |
|
Rneg_vecs = np.einsum("ij...,...j->...i", Rneg, UVW) |
|
|
|
return np.stack([Rpos_vecs, Rneg_vecs], axis=1) |
|
|
|
had_data = self.has_data() |
|
|
|
input_args = [X, Y, Z, U, V, W] |
|
|
|
|
|
masks = [k.mask for k in input_args |
|
if isinstance(k, np.ma.MaskedArray)] |
|
|
|
bcast = np.broadcast_arrays(*input_args, *masks) |
|
input_args = bcast[:6] |
|
masks = bcast[6:] |
|
if masks: |
|
|
|
mask = functools.reduce(np.logical_or, masks) |
|
|
|
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): |
|
|
|
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) |
|
|
|
|
|
norm = np.linalg.norm(UVW, axis=1) |
|
|
|
|
|
mask = norm > 0 |
|
XYZ = XYZ[mask] |
|
if normalize: |
|
UVW = UVW[mask] / norm[mask].reshape((-1, 1)) |
|
else: |
|
UVW = UVW[mask] |
|
|
|
if len(XYZ) > 0: |
|
|
|
shafts = (XYZ - np.multiply.outer(shaft_dt, UVW)).swapaxes(0, 1) |
|
|
|
head_dirs = calc_arrows(UVW) |
|
|
|
heads = shafts[:, :1] - np.multiply.outer(arrow_dt, head_dirs) |
|
|
|
heads = heads.reshape((len(arrow_dt), -1, 3)) |
|
|
|
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 |
|
""" |
|
|
|
|
|
|
|
if len(args) >= 3: |
|
|
|
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) |
|
|
|
|
|
if filled.ndim != 3: |
|
raise ValueError("Argument filled must be 3-dimensional") |
|
size = np.array(filled.shape, dtype=np.intp) |
|
|
|
|
|
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): |
|
|
|
return np.broadcast_to(color, filled.shape + np.shape(color)) |
|
elif np.ndim(color) in (3, 4): |
|
|
|
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") |
|
|
|
|
|
if facecolors is None: |
|
facecolors = self._get_patches_for_fill.get_next_color() |
|
facecolors = _broadcast_color_arg(facecolors, 'facecolors') |
|
|
|
|
|
edgecolors = _broadcast_color_arg(edgecolors, 'edgecolors') |
|
|
|
|
|
self.auto_scale_xyz(x, y, z) |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
for permute in permutation_matrices(3): |
|
|
|
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] |
|
|
|
|
|
for p in pinds: |
|
for q in qinds: |
|
|
|
|
|
|
|
|
|
|
|
p0 = permute.dot([p, q, 0]) |
|
i0 = tuple(p0) |
|
if filled[i0]: |
|
voxel_faces[i0].append(p0 + square_rot_neg) |
|
|
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
polygons = {} |
|
for coord, faces_inds in voxel_faces.items(): |
|
|
|
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) |
|
|
|
|
|
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) |
|
|
|
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) |
|
|
|
|
|
|
|
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_' |
|
|
|
|
|
|
|
|
|
|
|
(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) |
|
|
|
|
|
if barsabove: |
|
data_line.set_zorder(kwargs['zorder'] - .1) |
|
else: |
|
data_line.set_zorder(kwargs['zorder'] + .1) |
|
|
|
|
|
if fmt.lower() != 'none': |
|
self.add_line(data_line) |
|
else: |
|
data_line = None |
|
|
|
base_style.pop('color') |
|
|
|
if 'color' not in base_style: |
|
base_style['color'] = 'C0' |
|
if ecolor is None: |
|
ecolor = base_style['color'] |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
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] |
|
|
|
|
|
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 [[*itertools.compress(array, mask)] for array in arrays] |
|
|
|
def _extract_errs(err, data, lomask, himask): |
|
|
|
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 |
|
|
|
|
|
errlines, caplines, limmarks = [], [], [] |
|
|
|
|
|
coorderrs = [] |
|
|
|
|
|
|
|
capmarker = {0: '|', 1: '|', 2: '_'} |
|
i_xyz = {'x': 0, 'y': 1, 'z': 2} |
|
|
|
|
|
|
|
|
|
|
|
|
|
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)) |
|
|
|
|
|
|
|
with cbook._setattr_cm(self, elev=0, azim=0, roll=0): |
|
invM = np.linalg.inv(self.get_proj()) |
|
|
|
|
|
quiversize = np.dot(invM, [quiversize, 0, 0, 0])[1] |
|
|
|
|
|
|
|
quiversize *= 1.8660254037844388 |
|
eb_quiver_style = {**eb_cap_style, |
|
'length': quiversize, 'arrow_length_ratio': 1} |
|
eb_quiver_style.pop('markersize', None) |
|
|
|
|
|
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) |
|
|
|
|
|
lolims = np.broadcast_to(lolims, len(data)).astype(bool) |
|
uplims = np.broadcast_to(uplims, len(data)).astype(bool) |
|
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
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) |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
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)) |
|
|
|
|
|
|
|
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)] |
|
|
|
|
|
linestyle, linemarker, linecolor = _process_plot_format(linefmt) |
|
if linestyle is None: |
|
linestyle = mpl.rcParams['lines.linestyle'] |
|
|
|
|
|
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 |
|
|