Coverage for /usr/lib/python3/dist-packages/mpl_toolkits/mplot3d/axes3d.py: 31%
1278 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-14 15:55 +0200
1"""
2axes3d.py, original mplot3d version by John Porter
3Created: 23 Sep 2005
5Parts fixed by Reinier Heeres <reinier@heeres.eu>
6Minor additions by Ben Axelrod <baxelrod@coroware.com>
7Significant updates and revisions by Ben Root <ben.v.root@gmail.com>
9Module containing Axes3D, an object which can plot 3D objects on a
102D matplotlib figure.
11"""
13from collections import defaultdict
14import functools
15import itertools
16import math
17import textwrap
19import numpy as np
21import matplotlib as mpl
22from matplotlib import _api, cbook, _docstring, _preprocess_data
23import matplotlib.artist as martist
24import matplotlib.axes as maxes
25import matplotlib.collections as mcoll
26import matplotlib.colors as mcolors
27import matplotlib.image as mimage
28import matplotlib.lines as mlines
29import matplotlib.patches as mpatches
30import matplotlib.container as mcontainer
31import matplotlib.transforms as mtransforms
32from matplotlib.axes import Axes
33from matplotlib.axes._base import _axis_method_wrapper, _process_plot_format
34from matplotlib.transforms import Bbox
35from matplotlib.tri.triangulation import Triangulation
37from . import art3d
38from . import proj3d
39from . import axis3d
42@_docstring.interpd
43@_api.define_aliases({
44 "xlim": ["xlim3d"], "ylim": ["ylim3d"], "zlim": ["zlim3d"]})
45class Axes3D(Axes):
46 """
47 3D Axes object.
49 .. note::
51 As a user, you do not instantiate Axes directly, but use Axes creation
52 methods instead; e.g. from `.pyplot` or `.Figure`:
53 `~.pyplot.subplots`, `~.pyplot.subplot_mosaic` or `.Figure.add_axes`.
54 """
55 name = '3d'
57 _axis_names = ("x", "y", "z")
58 Axes._shared_axes["z"] = cbook.Grouper()
60 dist = _api.deprecate_privatize_attribute("3.6")
62 def __init__(
63 self, fig, rect=None, *args,
64 elev=30, azim=-60, roll=0, sharez=None, proj_type='persp',
65 box_aspect=None, computed_zorder=True, focal_length=None,
66 **kwargs):
67 """
68 Parameters
69 ----------
70 fig : Figure
71 The parent figure.
72 rect : tuple (left, bottom, width, height), default: None.
73 The ``(left, bottom, width, height)`` axes position.
74 elev : float, default: 30
75 The elevation angle in degrees rotates the camera above and below
76 the x-y plane, with a positive angle corresponding to a location
77 above the plane.
78 azim : float, default: -60
79 The azimuthal angle in degrees rotates the camera about the z axis,
80 with a positive angle corresponding to a right-handed rotation. In
81 other words, a positive azimuth rotates the camera about the origin
82 from its location along the +x axis towards the +y axis.
83 roll : float, default: 0
84 The roll angle in degrees rotates the camera about the viewing
85 axis. A positive angle spins the camera clockwise, causing the
86 scene to rotate counter-clockwise.
87 sharez : Axes3D, optional
88 Other Axes to share z-limits with.
89 proj_type : {'persp', 'ortho'}
90 The projection type, default 'persp'.
91 box_aspect : 3-tuple of floats, default: None
92 Changes the physical dimensions of the Axes3D, such that the ratio
93 of the axis lengths in display units is x:y:z.
94 If None, defaults to 4:4:3
95 computed_zorder : bool, default: True
96 If True, the draw order is computed based on the average position
97 of the `.Artist`\\s along the view direction.
98 Set to False if you want to manually control the order in which
99 Artists are drawn on top of each other using their *zorder*
100 attribute. This can be used for fine-tuning if the automatic order
101 does not produce the desired result. Note however, that a manual
102 zorder will only be correct for a limited view angle. If the figure
103 is rotated by the user, it will look wrong from certain angles.
104 auto_add_to_figure : bool, default: False
105 Prior to Matplotlib 3.4 Axes3D would add themselves
106 to their host Figure on init. Other Axes class do not
107 do this.
109 This behavior is deprecated in 3.4, the default is
110 changed to False in 3.6. The keyword will be undocumented
111 and a non-False value will be an error in 3.7.
112 focal_length : float, default: None
113 For a projection type of 'persp', the focal length of the virtual
114 camera. Must be > 0. If None, defaults to 1.
115 For a projection type of 'ortho', must be set to either None
116 or infinity (numpy.inf). If None, defaults to infinity.
117 The focal length can be computed from a desired Field Of View via
118 the equation: focal_length = 1/tan(FOV/2)
120 **kwargs
121 Other optional keyword arguments:
123 %(Axes3D:kwdoc)s
124 """
126 if rect is None:
127 rect = [0.0, 0.0, 1.0, 1.0]
129 self.initial_azim = azim
130 self.initial_elev = elev
131 self.initial_roll = roll
132 self.set_proj_type(proj_type, focal_length)
133 self.computed_zorder = computed_zorder
135 self.xy_viewLim = Bbox.unit()
136 self.zz_viewLim = Bbox.unit()
137 self.xy_dataLim = Bbox.unit()
138 # z-limits are encoded in the x-component of the Bbox, y is un-used
139 self.zz_dataLim = Bbox.unit()
141 # inhibit autoscale_view until the axes are defined
142 # they can't be defined until Axes.__init__ has been called
143 self.view_init(self.initial_elev, self.initial_azim, self.initial_roll)
145 self._sharez = sharez
146 if sharez is not None:
147 self._shared_axes["z"].join(self, sharez)
148 self._adjustable = 'datalim'
150 auto_add_to_figure = kwargs.pop('auto_add_to_figure', False)
152 super().__init__(
153 fig, rect, frameon=True, box_aspect=box_aspect, *args, **kwargs
154 )
155 # Disable drawing of axes by base class
156 super().set_axis_off()
157 # Enable drawing of axes by Axes3D class
158 self.set_axis_on()
159 self.M = None
161 # func used to format z -- fall back on major formatters
162 self.fmt_zdata = None
164 self.mouse_init()
165 self.figure.canvas.callbacks._connect_picklable(
166 'motion_notify_event', self._on_move)
167 self.figure.canvas.callbacks._connect_picklable(
168 'button_press_event', self._button_press)
169 self.figure.canvas.callbacks._connect_picklable(
170 'button_release_event', self._button_release)
171 self.set_top_view()
173 self.patch.set_linewidth(0)
174 # Calculate the pseudo-data width and height
175 pseudo_bbox = self.transLimits.inverted().transform([(0, 0), (1, 1)])
176 self._pseudo_w, self._pseudo_h = pseudo_bbox[1] - pseudo_bbox[0]
178 # mplot3d currently manages its own spines and needs these turned off
179 # for bounding box calculations
180 self.spines[:].set_visible(False)
182 if auto_add_to_figure:
183 _api.warn_deprecated(
184 "3.4", removal="3.7", message="Axes3D(fig) adding itself "
185 "to the figure is deprecated since %(since)s. "
186 "Pass the keyword argument auto_add_to_figure=False "
187 "and use fig.add_axes(ax) to suppress this warning. "
188 "The default value of auto_add_to_figure is changed to "
189 "False in mpl3.6 and True values will "
190 "no longer work %(removal)s. This is consistent with "
191 "other Axes classes.")
192 fig.add_axes(self)
194 def set_axis_off(self):
195 self._axis3don = False
196 self.stale = True
198 def set_axis_on(self):
199 self._axis3don = True
200 self.stale = True
202 def convert_zunits(self, z):
203 """
204 For artists in an Axes, if the zaxis has units support,
205 convert *z* using zaxis unit type
206 """
207 return self.zaxis.convert_units(z)
209 def set_top_view(self):
210 # this happens to be the right view for the viewing coordinates
211 # moved up and to the left slightly to fit labels and axes
212 xdwl = 0.95 / self._dist
213 xdw = 0.9 / self._dist
214 ydwl = 0.95 / self._dist
215 ydw = 0.9 / self._dist
216 # Set the viewing pane.
217 self.viewLim.intervalx = (-xdwl, xdw)
218 self.viewLim.intervaly = (-ydwl, ydw)
219 self.stale = True
221 def _init_axis(self):
222 """Init 3D axes; overrides creation of regular X/Y axes."""
223 self.xaxis = axis3d.XAxis(self)
224 self.yaxis = axis3d.YAxis(self)
225 self.zaxis = axis3d.ZAxis(self)
227 def get_zaxis(self):
228 """Return the ``ZAxis`` (`~.axis3d.Axis`) instance."""
229 return self.zaxis
231 get_zgridlines = _axis_method_wrapper("zaxis", "get_gridlines")
232 get_zticklines = _axis_method_wrapper("zaxis", "get_ticklines")
234 w_xaxis = _api.deprecated("3.1", alternative="xaxis", removal="3.8")(
235 property(lambda self: self.xaxis))
236 w_yaxis = _api.deprecated("3.1", alternative="yaxis", removal="3.8")(
237 property(lambda self: self.yaxis))
238 w_zaxis = _api.deprecated("3.1", alternative="zaxis", removal="3.8")(
239 property(lambda self: self.zaxis))
241 def unit_cube(self, vals=None):
242 minx, maxx, miny, maxy, minz, maxz = vals or self.get_w_lims()
243 return [(minx, miny, minz),
244 (maxx, miny, minz),
245 (maxx, maxy, minz),
246 (minx, maxy, minz),
247 (minx, miny, maxz),
248 (maxx, miny, maxz),
249 (maxx, maxy, maxz),
250 (minx, maxy, maxz)]
252 def tunit_cube(self, vals=None, M=None):
253 if M is None:
254 M = self.M
255 xyzs = self.unit_cube(vals)
256 tcube = proj3d.proj_points(xyzs, M)
257 return tcube
259 def tunit_edges(self, vals=None, M=None):
260 tc = self.tunit_cube(vals, M)
261 edges = [(tc[0], tc[1]),
262 (tc[1], tc[2]),
263 (tc[2], tc[3]),
264 (tc[3], tc[0]),
266 (tc[0], tc[4]),
267 (tc[1], tc[5]),
268 (tc[2], tc[6]),
269 (tc[3], tc[7]),
271 (tc[4], tc[5]),
272 (tc[5], tc[6]),
273 (tc[6], tc[7]),
274 (tc[7], tc[4])]
275 return edges
277 def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
278 """
279 Set the aspect ratios.
281 Parameters
282 ----------
283 aspect : {'auto', 'equal', 'equalxy', 'equalxz', 'equalyz'}
284 Possible values:
286 ========= ==================================================
287 value description
288 ========= ==================================================
289 'auto' automatic; fill the position rectangle with data.
290 'equal' adapt all the axes to have equal aspect ratios.
291 'equalxy' adapt the x and y axes to have equal aspect ratios.
292 'equalxz' adapt the x and z axes to have equal aspect ratios.
293 'equalyz' adapt the y and z axes to have equal aspect ratios.
294 ========= ==================================================
296 adjustable : None
297 Currently ignored by Axes3D
299 If not *None*, this defines which parameter will be adjusted to
300 meet the required aspect. See `.set_adjustable` for further
301 details.
303 anchor : None or str or 2-tuple of float, optional
304 If not *None*, this defines where the Axes will be drawn if there
305 is extra space due to aspect constraints. The most common way to
306 to specify the anchor are abbreviations of cardinal directions:
308 ===== =====================
309 value description
310 ===== =====================
311 'C' centered
312 'SW' lower left corner
313 'S' middle of bottom edge
314 'SE' lower right corner
315 etc.
316 ===== =====================
318 See `~.Axes.set_anchor` for further details.
320 share : bool, default: False
321 If ``True``, apply the settings to all shared Axes.
323 See Also
324 --------
325 mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect
326 """
327 _api.check_in_list(('auto', 'equal', 'equalxy', 'equalyz', 'equalxz'),
328 aspect=aspect)
329 super().set_aspect(
330 aspect='auto', adjustable=adjustable, anchor=anchor, share=share)
331 self._aspect = aspect
333 if aspect in ('equal', 'equalxy', 'equalxz', 'equalyz'):
334 if aspect == 'equal':
335 ax_indices = [0, 1, 2]
336 elif aspect == 'equalxy':
337 ax_indices = [0, 1]
338 elif aspect == 'equalxz':
339 ax_indices = [0, 2]
340 elif aspect == 'equalyz':
341 ax_indices = [1, 2]
343 view_intervals = np.array([self.xaxis.get_view_interval(),
344 self.yaxis.get_view_interval(),
345 self.zaxis.get_view_interval()])
346 mean = np.mean(view_intervals, axis=1)
347 ptp = np.ptp(view_intervals, axis=1)
348 delta = max(ptp[ax_indices])
349 scale = self._box_aspect[ptp == delta][0]
350 deltas = delta * self._box_aspect / scale
352 for i, set_lim in enumerate((self.set_xlim3d,
353 self.set_ylim3d,
354 self.set_zlim3d)):
355 if i in ax_indices:
356 set_lim(mean[i] - deltas[i]/2., mean[i] + deltas[i]/2.)
358 def set_box_aspect(self, aspect, *, zoom=1):
359 """
360 Set the Axes box aspect.
362 The box aspect is the ratio of height to width in display
363 units for each face of the box when viewed perpendicular to
364 that face. This is not to be confused with the data aspect (see
365 `~.Axes3D.set_aspect`). The default ratios are 4:4:3 (x:y:z).
367 To simulate having equal aspect in data space, set the box
368 aspect to match your data range in each dimension.
370 *zoom* controls the overall size of the Axes3D in the figure.
372 Parameters
373 ----------
374 aspect : 3-tuple of floats or None
375 Changes the physical dimensions of the Axes3D, such that the ratio
376 of the axis lengths in display units is x:y:z.
377 If None, defaults to (4,4,3).
379 zoom : float, default: 1
380 Control overall size of the Axes3D in the figure. Must be > 0.
381 """
382 if zoom <= 0:
383 raise ValueError(f'Argument zoom = {zoom} must be > 0')
385 if aspect is None:
386 aspect = np.asarray((4, 4, 3), dtype=float)
387 else:
388 aspect = np.asarray(aspect, dtype=float)
389 _api.check_shape((3,), aspect=aspect)
390 # default scale tuned to match the mpl32 appearance.
391 aspect *= 1.8294640721620434 * zoom / np.linalg.norm(aspect)
393 self._box_aspect = aspect
394 self.stale = True
396 def apply_aspect(self, position=None):
397 if position is None:
398 position = self.get_position(original=True)
400 # in the superclass, we would go through and actually deal with axis
401 # scales and box/datalim. Those are all irrelevant - all we need to do
402 # is make sure our coordinate system is square.
403 trans = self.get_figure().transSubfigure
404 bb = mtransforms.Bbox.unit().transformed(trans)
405 # this is the physical aspect of the panel (or figure):
406 fig_aspect = bb.height / bb.width
408 box_aspect = 1
409 pb = position.frozen()
410 pb1 = pb.shrunk_to_aspect(box_aspect, pb, fig_aspect)
411 self._set_position(pb1.anchored(self.get_anchor(), pb), 'active')
413 @martist.allow_rasterization
414 def draw(self, renderer):
415 if not self.get_visible():
416 return
417 self._unstale_viewLim()
419 # draw the background patch
420 self.patch.draw(renderer)
421 self._frameon = False
423 # first, set the aspect
424 # this is duplicated from `axes._base._AxesBase.draw`
425 # but must be called before any of the artist are drawn as
426 # it adjusts the view limits and the size of the bounding box
427 # of the Axes
428 locator = self.get_axes_locator()
429 self.apply_aspect(locator(self, renderer) if locator else None)
431 # add the projection matrix to the renderer
432 self.M = self.get_proj()
434 collections_and_patches = (
435 artist for artist in self._children
436 if isinstance(artist, (mcoll.Collection, mpatches.Patch))
437 and artist.get_visible())
438 if self.computed_zorder:
439 # Calculate projection of collections and patches and zorder
440 # them. Make sure they are drawn above the grids.
441 zorder_offset = max(axis.get_zorder()
442 for axis in self._axis_map.values()) + 1
443 collection_zorder = patch_zorder = zorder_offset
445 for artist in sorted(collections_and_patches,
446 key=lambda artist: artist.do_3d_projection(),
447 reverse=True):
448 if isinstance(artist, mcoll.Collection):
449 artist.zorder = collection_zorder
450 collection_zorder += 1
451 elif isinstance(artist, mpatches.Patch):
452 artist.zorder = patch_zorder
453 patch_zorder += 1
454 else:
455 for artist in collections_and_patches:
456 artist.do_3d_projection()
458 if self._axis3don:
459 # Draw panes first
460 for axis in self._axis_map.values():
461 axis.draw_pane(renderer)
462 # Then axes
463 for axis in self._axis_map.values():
464 axis.draw(renderer)
466 # Then rest
467 super().draw(renderer)
469 def get_axis_position(self):
470 vals = self.get_w_lims()
471 tc = self.tunit_cube(vals, self.M)
472 xhigh = tc[1][2] > tc[2][2]
473 yhigh = tc[3][2] > tc[2][2]
474 zhigh = tc[0][2] > tc[2][2]
475 return xhigh, yhigh, zhigh
477 def update_datalim(self, xys, **kwargs):
478 pass
480 get_autoscalez_on = _axis_method_wrapper("zaxis", "_get_autoscale_on")
481 set_autoscalez_on = _axis_method_wrapper("zaxis", "_set_autoscale_on")
483 def set_zmargin(self, m):
484 """
485 Set padding of Z data limits prior to autoscaling.
487 *m* times the data interval will be added to each end of that interval
488 before it is used in autoscaling. If *m* is negative, this will clip
489 the data range instead of expanding it.
491 For example, if your data is in the range [0, 2], a margin of 0.1 will
492 result in a range [-0.2, 2.2]; a margin of -0.1 will result in a range
493 of [0.2, 1.8].
495 Parameters
496 ----------
497 m : float greater than -0.5
498 """
499 if m <= -0.5:
500 raise ValueError("margin must be greater than -0.5")
501 self._zmargin = m
502 self._request_autoscale_view("z")
503 self.stale = True
505 def margins(self, *margins, x=None, y=None, z=None, tight=True):
506 """
507 Set or retrieve autoscaling margins.
509 See `.Axes.margins` for full documentation. Because this function
510 applies to 3D Axes, it also takes a *z* argument, and returns
511 ``(xmargin, ymargin, zmargin)``.
512 """
513 if margins and (x is not None or y is not None or z is not None):
514 raise TypeError('Cannot pass both positional and keyword '
515 'arguments for x, y, and/or z.')
516 elif len(margins) == 1:
517 x = y = z = margins[0]
518 elif len(margins) == 3:
519 x, y, z = margins
520 elif margins:
521 raise TypeError('Must pass a single positional argument for all '
522 'margins, or one for each margin (x, y, z).')
524 if x is None and y is None and z is None:
525 if tight is not True:
526 _api.warn_external(f'ignoring tight={tight!r} in get mode')
527 return self._xmargin, self._ymargin, self._zmargin
529 if x is not None:
530 self.set_xmargin(x)
531 if y is not None:
532 self.set_ymargin(y)
533 if z is not None:
534 self.set_zmargin(z)
536 self.autoscale_view(
537 tight=tight, scalex=(x is not None), scaley=(y is not None),
538 scalez=(z is not None)
539 )
541 def autoscale(self, enable=True, axis='both', tight=None):
542 """
543 Convenience method for simple axis view autoscaling.
545 See `.Axes.autoscale` for full documentation. Because this function
546 applies to 3D Axes, *axis* can also be set to 'z', and setting *axis*
547 to 'both' autoscales all three axes.
548 """
549 if enable is None:
550 scalex = True
551 scaley = True
552 scalez = True
553 else:
554 if axis in ['x', 'both']:
555 self.set_autoscalex_on(bool(enable))
556 scalex = self.get_autoscalex_on()
557 else:
558 scalex = False
559 if axis in ['y', 'both']:
560 self.set_autoscaley_on(bool(enable))
561 scaley = self.get_autoscaley_on()
562 else:
563 scaley = False
564 if axis in ['z', 'both']:
565 self.set_autoscalez_on(bool(enable))
566 scalez = self.get_autoscalez_on()
567 else:
568 scalez = False
569 if scalex:
570 self._request_autoscale_view("x", tight=tight)
571 if scaley:
572 self._request_autoscale_view("y", tight=tight)
573 if scalez:
574 self._request_autoscale_view("z", tight=tight)
576 def auto_scale_xyz(self, X, Y, Z=None, had_data=None):
577 # This updates the bounding boxes as to keep a record as to what the
578 # minimum sized rectangular volume holds the data.
579 if np.shape(X) == np.shape(Y):
580 self.xy_dataLim.update_from_data_xy(
581 np.column_stack([np.ravel(X), np.ravel(Y)]), not had_data)
582 else:
583 self.xy_dataLim.update_from_data_x(X, not had_data)
584 self.xy_dataLim.update_from_data_y(Y, not had_data)
585 if Z is not None:
586 self.zz_dataLim.update_from_data_x(Z, not had_data)
587 # Let autoscale_view figure out how to use this data.
588 self.autoscale_view()
590 def autoscale_view(self, tight=None, scalex=True, scaley=True,
591 scalez=True):
592 """
593 Autoscale the view limits using the data limits.
595 See `.Axes.autoscale_view` for full documentation. Because this
596 function applies to 3D Axes, it also takes a *scalez* argument.
597 """
598 # This method looks at the rectangular volume (see above)
599 # of data and decides how to scale the view portal to fit it.
600 if tight is None:
601 _tight = self._tight
602 if not _tight:
603 # if image data only just use the datalim
604 for artist in self._children:
605 if isinstance(artist, mimage.AxesImage):
606 _tight = True
607 elif isinstance(artist, (mlines.Line2D, mpatches.Patch)):
608 _tight = False
609 break
610 else:
611 _tight = self._tight = bool(tight)
613 if scalex and self.get_autoscalex_on():
614 self._shared_axes["x"].clean()
615 x0, x1 = self.xy_dataLim.intervalx
616 xlocator = self.xaxis.get_major_locator()
617 x0, x1 = xlocator.nonsingular(x0, x1)
618 if self._xmargin > 0:
619 delta = (x1 - x0) * self._xmargin
620 x0 -= delta
621 x1 += delta
622 if not _tight:
623 x0, x1 = xlocator.view_limits(x0, x1)
624 self.set_xbound(x0, x1)
626 if scaley and self.get_autoscaley_on():
627 self._shared_axes["y"].clean()
628 y0, y1 = self.xy_dataLim.intervaly
629 ylocator = self.yaxis.get_major_locator()
630 y0, y1 = ylocator.nonsingular(y0, y1)
631 if self._ymargin > 0:
632 delta = (y1 - y0) * self._ymargin
633 y0 -= delta
634 y1 += delta
635 if not _tight:
636 y0, y1 = ylocator.view_limits(y0, y1)
637 self.set_ybound(y0, y1)
639 if scalez and self.get_autoscalez_on():
640 self._shared_axes["z"].clean()
641 z0, z1 = self.zz_dataLim.intervalx
642 zlocator = self.zaxis.get_major_locator()
643 z0, z1 = zlocator.nonsingular(z0, z1)
644 if self._zmargin > 0:
645 delta = (z1 - z0) * self._zmargin
646 z0 -= delta
647 z1 += delta
648 if not _tight:
649 z0, z1 = zlocator.view_limits(z0, z1)
650 self.set_zbound(z0, z1)
652 def get_w_lims(self):
653 """Get 3D world limits."""
654 minx, maxx = self.get_xlim3d()
655 miny, maxy = self.get_ylim3d()
656 minz, maxz = self.get_zlim3d()
657 return minx, maxx, miny, maxy, minz, maxz
659 # set_xlim, set_ylim are directly inherited from base Axes.
660 @_api.make_keyword_only("3.6", "emit")
661 def set_zlim(self, bottom=None, top=None, emit=True, auto=False,
662 *, zmin=None, zmax=None):
663 """
664 Set 3D z limits.
666 See `.Axes.set_ylim` for full documentation
667 """
668 if top is None and np.iterable(bottom):
669 bottom, top = bottom
670 if zmin is not None:
671 if bottom is not None:
672 raise TypeError("Cannot pass both 'bottom' and 'zmin'")
673 bottom = zmin
674 if zmax is not None:
675 if top is not None:
676 raise TypeError("Cannot pass both 'top' and 'zmax'")
677 top = zmax
678 return self.zaxis._set_lim(bottom, top, emit=emit, auto=auto)
680 set_xlim3d = maxes.Axes.set_xlim
681 set_ylim3d = maxes.Axes.set_ylim
682 set_zlim3d = set_zlim
684 def get_xlim(self):
685 # docstring inherited
686 return tuple(self.xy_viewLim.intervalx)
688 def get_ylim(self):
689 # docstring inherited
690 return tuple(self.xy_viewLim.intervaly)
692 def get_zlim(self):
693 """Get 3D z limits."""
694 return tuple(self.zz_viewLim.intervalx)
696 get_zscale = _axis_method_wrapper("zaxis", "get_scale")
698 # Redefine all three methods to overwrite their docstrings.
699 set_xscale = _axis_method_wrapper("xaxis", "_set_axes_scale")
700 set_yscale = _axis_method_wrapper("yaxis", "_set_axes_scale")
701 set_zscale = _axis_method_wrapper("zaxis", "_set_axes_scale")
702 set_xscale.__doc__, set_yscale.__doc__, set_zscale.__doc__ = map(
703 """
704 Set the {}-axis scale.
706 Parameters
707 ----------
708 value : {{"linear"}}
709 The axis scale type to apply. 3D axes currently only support
710 linear scales; other scales yield nonsensical results.
712 **kwargs
713 Keyword arguments are nominally forwarded to the scale class, but
714 none of them is applicable for linear scales.
715 """.format,
716 ["x", "y", "z"])
718 get_zticks = _axis_method_wrapper("zaxis", "get_ticklocs")
719 set_zticks = _axis_method_wrapper("zaxis", "set_ticks")
720 get_zmajorticklabels = _axis_method_wrapper("zaxis", "get_majorticklabels")
721 get_zminorticklabels = _axis_method_wrapper("zaxis", "get_minorticklabels")
722 get_zticklabels = _axis_method_wrapper("zaxis", "get_ticklabels")
723 set_zticklabels = _axis_method_wrapper(
724 "zaxis", "_set_ticklabels",
725 doc_sub={"Axis.set_ticks": "Axes3D.set_zticks"})
727 zaxis_date = _axis_method_wrapper("zaxis", "axis_date")
728 if zaxis_date.__doc__:
729 zaxis_date.__doc__ += textwrap.dedent("""
731 Notes
732 -----
733 This function is merely provided for completeness, but 3D axes do not
734 support dates for ticks, and so this may not work as expected.
735 """)
737 def clabel(self, *args, **kwargs):
738 """Currently not implemented for 3D axes, and returns *None*."""
739 return None
741 def view_init(self, elev=None, azim=None, roll=None, vertical_axis="z"):
742 """
743 Set the elevation and azimuth of the axes in degrees (not radians).
745 This can be used to rotate the axes programmatically.
747 To look normal to the primary planes, the following elevation and
748 azimuth angles can be used. A roll angle of 0, 90, 180, or 270 deg
749 will rotate these views while keeping the axes at right angles.
751 ========== ==== ====
752 view plane elev azim
753 ========== ==== ====
754 XY 90 -90
755 XZ 0 -90
756 YZ 0 0
757 -XY -90 90
758 -XZ 0 90
759 -YZ 0 180
760 ========== ==== ====
762 Parameters
763 ----------
764 elev : float, default: None
765 The elevation angle in degrees rotates the camera above the plane
766 pierced by the vertical axis, with a positive angle corresponding
767 to a location above that plane. For example, with the default
768 vertical axis of 'z', the elevation defines the angle of the camera
769 location above the x-y plane.
770 If None, then the initial value as specified in the `Axes3D`
771 constructor is used.
772 azim : float, default: None
773 The azimuthal angle in degrees rotates the camera about the
774 vertical axis, with a positive angle corresponding to a
775 right-handed rotation. For example, with the default vertical axis
776 of 'z', a positive azimuth rotates the camera about the origin from
777 its location along the +x axis towards the +y axis.
778 If None, then the initial value as specified in the `Axes3D`
779 constructor is used.
780 roll : float, default: None
781 The roll angle in degrees rotates the camera about the viewing
782 axis. A positive angle spins the camera clockwise, causing the
783 scene to rotate counter-clockwise.
784 If None, then the initial value as specified in the `Axes3D`
785 constructor is used.
786 vertical_axis : {"z", "x", "y"}, default: "z"
787 The axis to align vertically. *azim* rotates about this axis.
788 """
790 self._dist = 10 # The camera distance from origin. Behaves like zoom
792 if elev is None:
793 self.elev = self.initial_elev
794 else:
795 self.elev = elev
797 if azim is None:
798 self.azim = self.initial_azim
799 else:
800 self.azim = azim
802 if roll is None:
803 self.roll = self.initial_roll
804 else:
805 self.roll = roll
807 self._vertical_axis = _api.check_getitem(
808 dict(x=0, y=1, z=2), vertical_axis=vertical_axis
809 )
811 def set_proj_type(self, proj_type, focal_length=None):
812 """
813 Set the projection type.
815 Parameters
816 ----------
817 proj_type : {'persp', 'ortho'}
818 The projection type.
819 focal_length : float, default: None
820 For a projection type of 'persp', the focal length of the virtual
821 camera. Must be > 0. If None, defaults to 1.
822 The focal length can be computed from a desired Field Of View via
823 the equation: focal_length = 1/tan(FOV/2)
824 """
825 _api.check_in_list(['persp', 'ortho'], proj_type=proj_type)
826 if proj_type == 'persp':
827 if focal_length is None:
828 focal_length = 1
829 elif focal_length <= 0:
830 raise ValueError(f"focal_length = {focal_length} must be "
831 "greater than 0")
832 self._focal_length = focal_length
833 else: # 'ortho':
834 if focal_length not in (None, np.inf):
835 raise ValueError(f"focal_length = {focal_length} must be "
836 f"None for proj_type = {proj_type}")
837 self._focal_length = np.inf
839 def _roll_to_vertical(self, arr):
840 """Roll arrays to match the different vertical axis."""
841 return np.roll(arr, self._vertical_axis - 2)
843 def get_proj(self):
844 """Create the projection matrix from the current viewing position."""
846 # Transform to uniform world coordinates 0-1, 0-1, 0-1
847 box_aspect = self._roll_to_vertical(self._box_aspect)
848 worldM = proj3d.world_transformation(
849 *self.get_xlim3d(),
850 *self.get_ylim3d(),
851 *self.get_zlim3d(),
852 pb_aspect=box_aspect,
853 )
855 # Look into the middle of the new coordinates:
856 R = 0.5 * box_aspect
858 # elev stores the elevation angle in the z plane
859 # azim stores the azimuth angle in the x,y plane
860 # roll stores the roll angle about the view axis
861 elev_rad = np.deg2rad(art3d._norm_angle(self.elev))
862 azim_rad = np.deg2rad(art3d._norm_angle(self.azim))
863 roll_rad = np.deg2rad(art3d._norm_angle(self.roll))
865 # Coordinates for a point that rotates around the box of data.
866 # p0, p1 corresponds to rotating the box only around the
867 # vertical axis.
868 # p2 corresponds to rotating the box only around the horizontal
869 # axis.
870 p0 = np.cos(elev_rad) * np.cos(azim_rad)
871 p1 = np.cos(elev_rad) * np.sin(azim_rad)
872 p2 = np.sin(elev_rad)
874 # When changing vertical axis the coordinates changes as well.
875 # Roll the values to get the same behaviour as the default:
876 ps = self._roll_to_vertical([p0, p1, p2])
878 # The coordinates for the eye viewing point. The eye is looking
879 # towards the middle of the box of data from a distance:
880 eye = R + self._dist * ps
882 # TODO: Is this being used somewhere? Can it be removed?
883 self.eye = eye
884 self.vvec = R - eye
885 self.vvec = self.vvec / np.linalg.norm(self.vvec)
887 # Define which axis should be vertical. A negative value
888 # indicates the plot is upside down and therefore the values
889 # have been reversed:
890 V = np.zeros(3)
891 V[self._vertical_axis] = -1 if abs(elev_rad) > 0.5 * np.pi else 1
893 # Generate the view and projection transformation matrices
894 if self._focal_length == np.inf:
895 # Orthographic projection
896 viewM = proj3d.view_transformation(eye, R, V, roll_rad)
897 projM = proj3d.ortho_transformation(-self._dist, self._dist)
898 else:
899 # Perspective projection
900 # Scale the eye dist to compensate for the focal length zoom effect
901 eye_focal = R + self._dist * ps * self._focal_length
902 viewM = proj3d.view_transformation(eye_focal, R, V, roll_rad)
903 projM = proj3d.persp_transformation(-self._dist,
904 self._dist,
905 self._focal_length)
907 # Combine all the transformation matrices to get the final projection
908 M0 = np.dot(viewM, worldM)
909 M = np.dot(projM, M0)
910 return M
912 def mouse_init(self, rotate_btn=1, zoom_btn=3):
913 """
914 Set the mouse buttons for 3D rotation and zooming.
916 Parameters
917 ----------
918 rotate_btn : int or list of int, default: 1
919 The mouse button or buttons to use for 3D rotation of the axes.
920 zoom_btn : int or list of int, default: 3
921 The mouse button or buttons to use to zoom the 3D axes.
922 """
923 self.button_pressed = None
924 # coerce scalars into array-like, then convert into
925 # a regular list to avoid comparisons against None
926 # which breaks in recent versions of numpy.
927 self._rotate_btn = np.atleast_1d(rotate_btn).tolist()
928 self._zoom_btn = np.atleast_1d(zoom_btn).tolist()
930 def disable_mouse_rotation(self):
931 """Disable mouse buttons for 3D rotation and zooming."""
932 self.mouse_init(rotate_btn=[], zoom_btn=[])
934 def can_zoom(self):
935 """
936 Return whether this Axes supports the zoom box button functionality.
938 Axes3D objects do not use the zoom box button.
939 """
940 return False
942 def can_pan(self):
943 """
944 Return whether this Axes supports the pan/zoom button functionality.
946 Axes3d objects do not use the pan/zoom button.
947 """
948 return False
950 def sharez(self, other):
951 """
952 Share the z-axis with *other*.
954 This is equivalent to passing ``sharex=other`` when constructing the
955 Axes, and cannot be used if the z-axis is already being shared with
956 another Axes.
957 """
958 _api.check_isinstance(maxes._base._AxesBase, other=other)
959 if self._sharez is not None and other is not self._sharez:
960 raise ValueError("z-axis is already shared")
961 self._shared_axes["z"].join(self, other)
962 self._sharez = other
963 self.zaxis.major = other.zaxis.major # Ticker instances holding
964 self.zaxis.minor = other.zaxis.minor # locator and formatter.
965 z0, z1 = other.get_zlim()
966 self.set_zlim(z0, z1, emit=False, auto=other.get_autoscalez_on())
967 self.zaxis._scale = other.zaxis._scale
969 def clear(self):
970 # docstring inherited.
971 super().clear()
972 if self._focal_length == np.inf:
973 self._zmargin = mpl.rcParams['axes.zmargin']
974 else:
975 self._zmargin = 0.
976 self.grid(mpl.rcParams['axes3d.grid'])
978 def _button_press(self, event):
979 if event.inaxes == self:
980 self.button_pressed = event.button
981 self.sx, self.sy = event.xdata, event.ydata
982 toolbar = getattr(self.figure.canvas, "toolbar")
983 if toolbar and toolbar._nav_stack() is None:
984 self.figure.canvas.toolbar.push_current()
986 def _button_release(self, event):
987 self.button_pressed = None
988 toolbar = getattr(self.figure.canvas, "toolbar")
989 if toolbar:
990 self.figure.canvas.toolbar.push_current()
992 def _get_view(self):
993 # docstring inherited
994 return (self.get_xlim(), self.get_ylim(), self.get_zlim(),
995 self.elev, self.azim, self.roll)
997 def _set_view(self, view):
998 # docstring inherited
999 xlim, ylim, zlim, elev, azim, roll = view
1000 self.set(xlim=xlim, ylim=ylim, zlim=zlim)
1001 self.elev = elev
1002 self.azim = azim
1003 self.roll = roll
1005 def format_zdata(self, z):
1006 """
1007 Return *z* string formatted. This function will use the
1008 :attr:`fmt_zdata` attribute if it is callable, else will fall
1009 back on the zaxis major formatter
1010 """
1011 try:
1012 return self.fmt_zdata(z)
1013 except (AttributeError, TypeError):
1014 func = self.zaxis.get_major_formatter().format_data_short
1015 val = func(z)
1016 return val
1018 def format_coord(self, xd, yd):
1019 """
1020 Given the 2D view coordinates attempt to guess a 3D coordinate.
1021 Looks for the nearest edge to the point and then assumes that
1022 the point is at the same z location as the nearest point on the edge.
1023 """
1025 if self.M is None:
1026 return ''
1028 if self.button_pressed in self._rotate_btn:
1029 # ignore xd and yd and display angles instead
1030 norm_elev = art3d._norm_angle(self.elev)
1031 norm_azim = art3d._norm_angle(self.azim)
1032 norm_roll = art3d._norm_angle(self.roll)
1033 return (f"elevation={norm_elev:.0f}\N{{DEGREE SIGN}}, "
1034 f"azimuth={norm_azim:.0f}\N{{DEGREE SIGN}}, "
1035 f"roll={norm_roll:.0f}\N{{DEGREE SIGN}}"
1036 ).replace("-", "\N{MINUS SIGN}")
1038 # nearest edge
1039 p0, p1 = min(self.tunit_edges(),
1040 key=lambda edge: proj3d._line2d_seg_dist(
1041 edge[0], edge[1], (xd, yd)))
1043 # scale the z value to match
1044 x0, y0, z0 = p0
1045 x1, y1, z1 = p1
1046 d0 = np.hypot(x0-xd, y0-yd)
1047 d1 = np.hypot(x1-xd, y1-yd)
1048 dt = d0+d1
1049 z = d1/dt * z0 + d0/dt * z1
1051 x, y, z = proj3d.inv_transform(xd, yd, z, self.M)
1053 xs = self.format_xdata(x)
1054 ys = self.format_ydata(y)
1055 zs = self.format_zdata(z)
1056 return 'x=%s, y=%s, z=%s' % (xs, ys, zs)
1058 def _on_move(self, event):
1059 """
1060 Mouse moving.
1062 By default, button-1 rotates and button-3 zooms; these buttons can be
1063 modified via `mouse_init`.
1064 """
1066 if not self.button_pressed:
1067 return
1069 if self.M is None:
1070 return
1072 x, y = event.xdata, event.ydata
1073 # In case the mouse is out of bounds.
1074 if x is None:
1075 return
1077 dx, dy = x - self.sx, y - self.sy
1078 w = self._pseudo_w
1079 h = self._pseudo_h
1080 self.sx, self.sy = x, y
1082 # Rotation
1083 if self.button_pressed in self._rotate_btn:
1084 # rotate viewing point
1085 # get the x and y pixel coords
1086 if dx == 0 and dy == 0:
1087 return
1089 roll = np.deg2rad(self.roll)
1090 delev = -(dy/h)*180*np.cos(roll) + (dx/w)*180*np.sin(roll)
1091 dazim = -(dy/h)*180*np.sin(roll) - (dx/w)*180*np.cos(roll)
1092 self.elev = self.elev + delev
1093 self.azim = self.azim + dazim
1094 self.get_proj()
1095 self.stale = True
1096 self.figure.canvas.draw_idle()
1098 elif self.button_pressed == 2:
1099 # pan view
1100 # get the x and y pixel coords
1101 if dx == 0 and dy == 0:
1102 return
1103 minx, maxx, miny, maxy, minz, maxz = self.get_w_lims()
1104 dx = 1-((w - dx)/w)
1105 dy = 1-((h - dy)/h)
1106 elev = np.deg2rad(self.elev)
1107 azim = np.deg2rad(self.azim)
1108 # project xv, yv, zv -> xw, yw, zw
1109 dxx = (maxx-minx)*(dy*np.sin(elev)*np.cos(azim) + dx*np.sin(azim))
1110 dyy = (maxy-miny)*(-dx*np.cos(azim) + dy*np.sin(elev)*np.sin(azim))
1111 dzz = (maxz-minz)*(-dy*np.cos(elev))
1112 # pan
1113 self.set_xlim3d(minx + dxx, maxx + dxx)
1114 self.set_ylim3d(miny + dyy, maxy + dyy)
1115 self.set_zlim3d(minz + dzz, maxz + dzz)
1116 self.get_proj()
1117 self.figure.canvas.draw_idle()
1119 # Zoom
1120 elif self.button_pressed in self._zoom_btn:
1121 # zoom view
1122 # hmmm..this needs some help from clipping....
1123 minx, maxx, miny, maxy, minz, maxz = self.get_w_lims()
1124 df = 1-((h - dy)/h)
1125 dx = (maxx-minx)*df
1126 dy = (maxy-miny)*df
1127 dz = (maxz-minz)*df
1128 self.set_xlim3d(minx - dx, maxx + dx)
1129 self.set_ylim3d(miny - dy, maxy + dy)
1130 self.set_zlim3d(minz - dz, maxz + dz)
1131 self.get_proj()
1132 self.figure.canvas.draw_idle()
1134 def set_zlabel(self, zlabel, fontdict=None, labelpad=None, **kwargs):
1135 """
1136 Set zlabel. See doc for `.set_ylabel` for description.
1137 """
1138 if labelpad is not None:
1139 self.zaxis.labelpad = labelpad
1140 return self.zaxis.set_label_text(zlabel, fontdict, **kwargs)
1142 def get_zlabel(self):
1143 """
1144 Get the z-label text string.
1145 """
1146 label = self.zaxis.get_label()
1147 return label.get_text()
1149 # Axes rectangle characteristics
1151 def get_frame_on(self):
1152 """Get whether the 3D axes panels are drawn."""
1153 return self._frameon
1155 def set_frame_on(self, b):
1156 """
1157 Set whether the 3D axes panels are drawn.
1159 Parameters
1160 ----------
1161 b : bool
1162 """
1163 self._frameon = bool(b)
1164 self.stale = True
1166 @_api.rename_parameter("3.5", "b", "visible")
1167 def grid(self, visible=True, **kwargs):
1168 """
1169 Set / unset 3D grid.
1171 .. note::
1173 Currently, this function does not behave the same as
1174 `.axes.Axes.grid`, but it is intended to eventually support that
1175 behavior.
1176 """
1177 # TODO: Operate on each axes separately
1178 if len(kwargs):
1179 visible = True
1180 self._draw_grid = visible
1181 self.stale = True
1183 def tick_params(self, axis='both', **kwargs):
1184 """
1185 Convenience method for changing the appearance of ticks and
1186 tick labels.
1188 See `.Axes.tick_params` for full documentation. Because this function
1189 applies to 3D Axes, *axis* can also be set to 'z', and setting *axis*
1190 to 'both' autoscales all three axes.
1192 Also, because of how Axes3D objects are drawn very differently
1193 from regular 2D axes, some of these settings may have
1194 ambiguous meaning. For simplicity, the 'z' axis will
1195 accept settings as if it was like the 'y' axis.
1197 .. note::
1198 Axes3D currently ignores some of these settings.
1199 """
1200 _api.check_in_list(['x', 'y', 'z', 'both'], axis=axis)
1201 if axis in ['x', 'y', 'both']:
1202 super().tick_params(axis, **kwargs)
1203 if axis in ['z', 'both']:
1204 zkw = dict(kwargs)
1205 zkw.pop('top', None)
1206 zkw.pop('bottom', None)
1207 zkw.pop('labeltop', None)
1208 zkw.pop('labelbottom', None)
1209 self.zaxis.set_tick_params(**zkw)
1211 # data limits, ticks, tick labels, and formatting
1213 def invert_zaxis(self):
1214 """
1215 Invert the z-axis.
1216 """
1217 bottom, top = self.get_zlim()
1218 self.set_zlim(top, bottom, auto=None)
1220 def zaxis_inverted(self):
1221 """
1222 Returns True if the z-axis is inverted.
1223 """
1224 bottom, top = self.get_zlim()
1225 return top < bottom
1227 def get_zbound(self):
1228 """
1229 Return the lower and upper z-axis bounds, in increasing order.
1230 """
1231 bottom, top = self.get_zlim()
1232 if bottom < top:
1233 return bottom, top
1234 else:
1235 return top, bottom
1237 def set_zbound(self, lower=None, upper=None):
1238 """
1239 Set the lower and upper numerical bounds of the z-axis.
1241 This method will honor axes inversion regardless of parameter order.
1242 It will not change the autoscaling setting (`.get_autoscalez_on()`).
1243 """
1244 if upper is None and np.iterable(lower):
1245 lower, upper = lower
1247 old_lower, old_upper = self.get_zbound()
1248 if lower is None:
1249 lower = old_lower
1250 if upper is None:
1251 upper = old_upper
1253 self.set_zlim(sorted((lower, upper),
1254 reverse=bool(self.zaxis_inverted())),
1255 auto=None)
1257 def text(self, x, y, z, s, zdir=None, **kwargs):
1258 """
1259 Add text to the plot. kwargs will be passed on to Axes.text,
1260 except for the *zdir* keyword, which sets the direction to be
1261 used as the z direction.
1262 """
1263 text = super().text(x, y, s, **kwargs)
1264 art3d.text_2d_to_3d(text, z, zdir)
1265 return text
1267 text3D = text
1268 text2D = Axes.text
1270 def plot(self, xs, ys, *args, zdir='z', **kwargs):
1271 """
1272 Plot 2D or 3D data.
1274 Parameters
1275 ----------
1276 xs : 1D array-like
1277 x coordinates of vertices.
1278 ys : 1D array-like
1279 y coordinates of vertices.
1280 zs : float or 1D array-like
1281 z coordinates of vertices; either one for all points or one for
1282 each point.
1283 zdir : {'x', 'y', 'z'}, default: 'z'
1284 When plotting 2D data, the direction to use as z ('x', 'y' or 'z').
1285 **kwargs
1286 Other arguments are forwarded to `matplotlib.axes.Axes.plot`.
1287 """
1288 had_data = self.has_data()
1290 # `zs` can be passed positionally or as keyword; checking whether
1291 # args[0] is a string matches the behavior of 2D `plot` (via
1292 # `_process_plot_var_args`).
1293 if args and not isinstance(args[0], str):
1294 zs, *args = args
1295 if 'zs' in kwargs:
1296 raise TypeError("plot() for multiple values for argument 'z'")
1297 else:
1298 zs = kwargs.pop('zs', 0)
1300 # Match length
1301 zs = np.broadcast_to(zs, np.shape(xs))
1303 lines = super().plot(xs, ys, *args, **kwargs)
1304 for line in lines:
1305 art3d.line_2d_to_3d(line, zs=zs, zdir=zdir)
1307 xs, ys, zs = art3d.juggle_axes(xs, ys, zs, zdir)
1308 self.auto_scale_xyz(xs, ys, zs, had_data)
1309 return lines
1311 plot3D = plot
1313 def plot_surface(self, X, Y, Z, *, norm=None, vmin=None,
1314 vmax=None, lightsource=None, **kwargs):
1315 """
1316 Create a surface plot.
1318 By default it will be colored in shades of a solid color, but it also
1319 supports colormapping by supplying the *cmap* argument.
1321 .. note::
1323 The *rcount* and *ccount* kwargs, which both default to 50,
1324 determine the maximum number of samples used in each direction. If
1325 the input data is larger, it will be downsampled (by slicing) to
1326 these numbers of points.
1328 .. note::
1330 To maximize rendering speed consider setting *rstride* and *cstride*
1331 to divisors of the number of rows minus 1 and columns minus 1
1332 respectively. For example, given 51 rows rstride can be any of the
1333 divisors of 50.
1335 Similarly, a setting of *rstride* and *cstride* equal to 1 (or
1336 *rcount* and *ccount* equal the number of rows and columns) can use
1337 the optimized path.
1339 Parameters
1340 ----------
1341 X, Y, Z : 2D arrays
1342 Data values.
1344 rcount, ccount : int
1345 Maximum number of samples used in each direction. If the input
1346 data is larger, it will be downsampled (by slicing) to these
1347 numbers of points. Defaults to 50.
1349 rstride, cstride : int
1350 Downsampling stride in each direction. These arguments are
1351 mutually exclusive with *rcount* and *ccount*. If only one of
1352 *rstride* or *cstride* is set, the other defaults to 10.
1354 'classic' mode uses a default of ``rstride = cstride = 10`` instead
1355 of the new default of ``rcount = ccount = 50``.
1357 color : color-like
1358 Color of the surface patches.
1360 cmap : Colormap
1361 Colormap of the surface patches.
1363 facecolors : array-like of colors.
1364 Colors of each individual patch.
1366 norm : Normalize
1367 Normalization for the colormap.
1369 vmin, vmax : float
1370 Bounds for the normalization.
1372 shade : bool, default: True
1373 Whether to shade the facecolors. Shading is always disabled when
1374 *cmap* is specified.
1376 lightsource : `~matplotlib.colors.LightSource`
1377 The lightsource to use when *shade* is True.
1379 **kwargs
1380 Other keyword arguments are forwarded to `.Poly3DCollection`.
1381 """
1383 had_data = self.has_data()
1385 if Z.ndim != 2:
1386 raise ValueError("Argument Z must be 2-dimensional.")
1388 Z = cbook._to_unmasked_float_array(Z)
1389 X, Y, Z = np.broadcast_arrays(X, Y, Z)
1390 rows, cols = Z.shape
1392 has_stride = 'rstride' in kwargs or 'cstride' in kwargs
1393 has_count = 'rcount' in kwargs or 'ccount' in kwargs
1395 if has_stride and has_count:
1396 raise ValueError("Cannot specify both stride and count arguments")
1398 rstride = kwargs.pop('rstride', 10)
1399 cstride = kwargs.pop('cstride', 10)
1400 rcount = kwargs.pop('rcount', 50)
1401 ccount = kwargs.pop('ccount', 50)
1403 if mpl.rcParams['_internal.classic_mode']:
1404 # Strides have priority over counts in classic mode.
1405 # So, only compute strides from counts
1406 # if counts were explicitly given
1407 compute_strides = has_count
1408 else:
1409 # If the strides are provided then it has priority.
1410 # Otherwise, compute the strides from the counts.
1411 compute_strides = not has_stride
1413 if compute_strides:
1414 rstride = int(max(np.ceil(rows / rcount), 1))
1415 cstride = int(max(np.ceil(cols / ccount), 1))
1417 if 'facecolors' in kwargs:
1418 fcolors = kwargs.pop('facecolors')
1419 else:
1420 color = kwargs.pop('color', None)
1421 if color is None:
1422 color = self._get_lines.get_next_color()
1423 color = np.array(mcolors.to_rgba(color))
1424 fcolors = None
1426 cmap = kwargs.get('cmap', None)
1427 shade = kwargs.pop('shade', cmap is None)
1428 if shade is None:
1429 raise ValueError("shade cannot be None.")
1431 colset = [] # the sampled facecolor
1432 if (rows - 1) % rstride == 0 and \
1433 (cols - 1) % cstride == 0 and \
1434 fcolors is None:
1435 polys = np.stack(
1436 [cbook._array_patch_perimeters(a, rstride, cstride)
1437 for a in (X, Y, Z)],
1438 axis=-1)
1439 else:
1440 # evenly spaced, and including both endpoints
1441 row_inds = list(range(0, rows-1, rstride)) + [rows-1]
1442 col_inds = list(range(0, cols-1, cstride)) + [cols-1]
1444 polys = []
1445 for rs, rs_next in zip(row_inds[:-1], row_inds[1:]):
1446 for cs, cs_next in zip(col_inds[:-1], col_inds[1:]):
1447 ps = [
1448 # +1 ensures we share edges between polygons
1449 cbook._array_perimeter(a[rs:rs_next+1, cs:cs_next+1])
1450 for a in (X, Y, Z)
1451 ]
1452 # ps = np.stack(ps, axis=-1)
1453 ps = np.array(ps).T
1454 polys.append(ps)
1456 if fcolors is not None:
1457 colset.append(fcolors[rs][cs])
1459 # In cases where there are NaNs in the data (possibly from masked
1460 # arrays), artifacts can be introduced. Here check whether NaNs exist
1461 # and remove the entries if so
1462 if not isinstance(polys, np.ndarray) or np.isnan(polys).any():
1463 new_polys = []
1464 new_colset = []
1466 # Depending on fcolors, colset is either an empty list or has as
1467 # many elements as polys. In the former case new_colset results in
1468 # a list with None entries, that is discarded later.
1469 for p, col in itertools.zip_longest(polys, colset):
1470 new_poly = np.array(p)[~np.isnan(p).any(axis=1)]
1471 if len(new_poly):
1472 new_polys.append(new_poly)
1473 new_colset.append(col)
1475 # Replace previous polys and, if fcolors is not None, colset
1476 polys = new_polys
1477 if fcolors is not None:
1478 colset = new_colset
1480 # note that the striding causes some polygons to have more coordinates
1481 # than others
1482 polyc = art3d.Poly3DCollection(polys, **kwargs)
1484 if fcolors is not None:
1485 if shade:
1486 colset = self._shade_colors(
1487 colset, self._generate_normals(polys), lightsource)
1488 polyc.set_facecolors(colset)
1489 polyc.set_edgecolors(colset)
1490 elif cmap:
1491 # can't always vectorize, because polys might be jagged
1492 if isinstance(polys, np.ndarray):
1493 avg_z = polys[..., 2].mean(axis=-1)
1494 else:
1495 avg_z = np.array([ps[:, 2].mean() for ps in polys])
1496 polyc.set_array(avg_z)
1497 if vmin is not None or vmax is not None:
1498 polyc.set_clim(vmin, vmax)
1499 if norm is not None:
1500 polyc.set_norm(norm)
1501 else:
1502 if shade:
1503 colset = self._shade_colors(
1504 color, self._generate_normals(polys), lightsource)
1505 else:
1506 colset = color
1507 polyc.set_facecolors(colset)
1509 self.add_collection(polyc)
1510 self.auto_scale_xyz(X, Y, Z, had_data)
1512 return polyc
1514 def _generate_normals(self, polygons):
1515 """
1516 Compute the normals of a list of polygons.
1518 Normals point towards the viewer for a face with its vertices in
1519 counterclockwise order, following the right hand rule.
1521 Uses three points equally spaced around the polygon.
1522 This normal of course might not make sense for polygons with more than
1523 three points not lying in a plane, but it's a plausible and fast
1524 approximation.
1526 Parameters
1527 ----------
1528 polygons : list of (M_i, 3) array-like, or (..., M, 3) array-like
1529 A sequence of polygons to compute normals for, which can have
1530 varying numbers of vertices. If the polygons all have the same
1531 number of vertices and array is passed, then the operation will
1532 be vectorized.
1534 Returns
1535 -------
1536 normals : (..., 3) array
1537 A normal vector estimated for the polygon.
1538 """
1539 if isinstance(polygons, np.ndarray):
1540 # optimization: polygons all have the same number of points, so can
1541 # vectorize
1542 n = polygons.shape[-2]
1543 i1, i2, i3 = 0, n//3, 2*n//3
1544 v1 = polygons[..., i1, :] - polygons[..., i2, :]
1545 v2 = polygons[..., i2, :] - polygons[..., i3, :]
1546 else:
1547 # The subtraction doesn't vectorize because polygons is jagged.
1548 v1 = np.empty((len(polygons), 3))
1549 v2 = np.empty((len(polygons), 3))
1550 for poly_i, ps in enumerate(polygons):
1551 n = len(ps)
1552 i1, i2, i3 = 0, n//3, 2*n//3
1553 v1[poly_i, :] = ps[i1, :] - ps[i2, :]
1554 v2[poly_i, :] = ps[i2, :] - ps[i3, :]
1555 return np.cross(v1, v2)
1557 def _shade_colors(self, color, normals, lightsource=None):
1558 """
1559 Shade *color* using normal vectors given by *normals*.
1560 *color* can also be an array of the same length as *normals*.
1561 """
1562 if lightsource is None:
1563 # chosen for backwards-compatibility
1564 lightsource = mcolors.LightSource(azdeg=225, altdeg=19.4712)
1566 with np.errstate(invalid="ignore"):
1567 shade = ((normals / np.linalg.norm(normals, axis=1, keepdims=True))
1568 @ lightsource.direction)
1569 mask = ~np.isnan(shade)
1571 if mask.any():
1572 # convert dot product to allowed shading fractions
1573 in_norm = mcolors.Normalize(-1, 1)
1574 out_norm = mcolors.Normalize(0.3, 1).inverse
1576 def norm(x):
1577 return out_norm(in_norm(x))
1579 shade[~mask] = 0
1581 color = mcolors.to_rgba_array(color)
1582 # shape of color should be (M, 4) (where M is number of faces)
1583 # shape of shade should be (M,)
1584 # colors should have final shape of (M, 4)
1585 alpha = color[:, 3]
1586 colors = norm(shade)[:, np.newaxis] * color
1587 colors[:, 3] = alpha
1588 else:
1589 colors = np.asanyarray(color).copy()
1591 return colors
1593 def plot_wireframe(self, X, Y, Z, **kwargs):
1594 """
1595 Plot a 3D wireframe.
1597 .. note::
1599 The *rcount* and *ccount* kwargs, which both default to 50,
1600 determine the maximum number of samples used in each direction. If
1601 the input data is larger, it will be downsampled (by slicing) to
1602 these numbers of points.
1604 Parameters
1605 ----------
1606 X, Y, Z : 2D arrays
1607 Data values.
1609 rcount, ccount : int
1610 Maximum number of samples used in each direction. If the input
1611 data is larger, it will be downsampled (by slicing) to these
1612 numbers of points. Setting a count to zero causes the data to be
1613 not sampled in the corresponding direction, producing a 3D line
1614 plot rather than a wireframe plot. Defaults to 50.
1616 rstride, cstride : int
1617 Downsampling stride in each direction. These arguments are
1618 mutually exclusive with *rcount* and *ccount*. If only one of
1619 *rstride* or *cstride* is set, the other defaults to 1. Setting a
1620 stride to zero causes the data to be not sampled in the
1621 corresponding direction, producing a 3D line plot rather than a
1622 wireframe plot.
1624 'classic' mode uses a default of ``rstride = cstride = 1`` instead
1625 of the new default of ``rcount = ccount = 50``.
1627 **kwargs
1628 Other keyword arguments are forwarded to `.Line3DCollection`.
1629 """
1631 had_data = self.has_data()
1632 if Z.ndim != 2:
1633 raise ValueError("Argument Z must be 2-dimensional.")
1634 # FIXME: Support masked arrays
1635 X, Y, Z = np.broadcast_arrays(X, Y, Z)
1636 rows, cols = Z.shape
1638 has_stride = 'rstride' in kwargs or 'cstride' in kwargs
1639 has_count = 'rcount' in kwargs or 'ccount' in kwargs
1641 if has_stride and has_count:
1642 raise ValueError("Cannot specify both stride and count arguments")
1644 rstride = kwargs.pop('rstride', 1)
1645 cstride = kwargs.pop('cstride', 1)
1646 rcount = kwargs.pop('rcount', 50)
1647 ccount = kwargs.pop('ccount', 50)
1649 if mpl.rcParams['_internal.classic_mode']:
1650 # Strides have priority over counts in classic mode.
1651 # So, only compute strides from counts
1652 # if counts were explicitly given
1653 if has_count:
1654 rstride = int(max(np.ceil(rows / rcount), 1)) if rcount else 0
1655 cstride = int(max(np.ceil(cols / ccount), 1)) if ccount else 0
1656 else:
1657 # If the strides are provided then it has priority.
1658 # Otherwise, compute the strides from the counts.
1659 if not has_stride:
1660 rstride = int(max(np.ceil(rows / rcount), 1)) if rcount else 0
1661 cstride = int(max(np.ceil(cols / ccount), 1)) if ccount else 0
1663 # We want two sets of lines, one running along the "rows" of
1664 # Z and another set of lines running along the "columns" of Z.
1665 # This transpose will make it easy to obtain the columns.
1666 tX, tY, tZ = np.transpose(X), np.transpose(Y), np.transpose(Z)
1668 if rstride:
1669 rii = list(range(0, rows, rstride))
1670 # Add the last index only if needed
1671 if rows > 0 and rii[-1] != (rows - 1):
1672 rii += [rows-1]
1673 else:
1674 rii = []
1675 if cstride:
1676 cii = list(range(0, cols, cstride))
1677 # Add the last index only if needed
1678 if cols > 0 and cii[-1] != (cols - 1):
1679 cii += [cols-1]
1680 else:
1681 cii = []
1683 if rstride == 0 and cstride == 0:
1684 raise ValueError("Either rstride or cstride must be non zero")
1686 # If the inputs were empty, then just
1687 # reset everything.
1688 if Z.size == 0:
1689 rii = []
1690 cii = []
1692 xlines = [X[i] for i in rii]
1693 ylines = [Y[i] for i in rii]
1694 zlines = [Z[i] for i in rii]
1696 txlines = [tX[i] for i in cii]
1697 tylines = [tY[i] for i in cii]
1698 tzlines = [tZ[i] for i in cii]
1700 lines = ([list(zip(xl, yl, zl))
1701 for xl, yl, zl in zip(xlines, ylines, zlines)]
1702 + [list(zip(xl, yl, zl))
1703 for xl, yl, zl in zip(txlines, tylines, tzlines)])
1705 linec = art3d.Line3DCollection(lines, **kwargs)
1706 self.add_collection(linec)
1707 self.auto_scale_xyz(X, Y, Z, had_data)
1709 return linec
1711 def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None,
1712 lightsource=None, **kwargs):
1713 """
1714 Plot a triangulated surface.
1716 The (optional) triangulation can be specified in one of two ways;
1717 either::
1719 plot_trisurf(triangulation, ...)
1721 where triangulation is a `~matplotlib.tri.Triangulation` object, or::
1723 plot_trisurf(X, Y, ...)
1724 plot_trisurf(X, Y, triangles, ...)
1725 plot_trisurf(X, Y, triangles=triangles, ...)
1727 in which case a Triangulation object will be created. See
1728 `.Triangulation` for a explanation of these possibilities.
1730 The remaining arguments are::
1732 plot_trisurf(..., Z)
1734 where *Z* is the array of values to contour, one per point
1735 in the triangulation.
1737 Parameters
1738 ----------
1739 X, Y, Z : array-like
1740 Data values as 1D arrays.
1741 color
1742 Color of the surface patches.
1743 cmap
1744 A colormap for the surface patches.
1745 norm : Normalize
1746 An instance of Normalize to map values to colors.
1747 vmin, vmax : float, default: None
1748 Minimum and maximum value to map.
1749 shade : bool, default: True
1750 Whether to shade the facecolors. Shading is always disabled when
1751 *cmap* is specified.
1752 lightsource : `~matplotlib.colors.LightSource`
1753 The lightsource to use when *shade* is True.
1754 **kwargs
1755 All other keyword arguments are passed on to
1756 :class:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection`
1758 Examples
1759 --------
1760 .. plot:: gallery/mplot3d/trisurf3d.py
1761 .. plot:: gallery/mplot3d/trisurf3d_2.py
1762 """
1764 had_data = self.has_data()
1766 # TODO: Support custom face colours
1767 if color is None:
1768 color = self._get_lines.get_next_color()
1769 color = np.array(mcolors.to_rgba(color))
1771 cmap = kwargs.get('cmap', None)
1772 shade = kwargs.pop('shade', cmap is None)
1774 tri, args, kwargs = \
1775 Triangulation.get_from_args_and_kwargs(*args, **kwargs)
1776 try:
1777 z = kwargs.pop('Z')
1778 except KeyError:
1779 # We do this so Z doesn't get passed as an arg to PolyCollection
1780 z, *args = args
1781 z = np.asarray(z)
1783 triangles = tri.get_masked_triangles()
1784 xt = tri.x[triangles]
1785 yt = tri.y[triangles]
1786 zt = z[triangles]
1787 verts = np.stack((xt, yt, zt), axis=-1)
1789 polyc = art3d.Poly3DCollection(verts, *args, **kwargs)
1791 if cmap:
1792 # average over the three points of each triangle
1793 avg_z = verts[:, :, 2].mean(axis=1)
1794 polyc.set_array(avg_z)
1795 if vmin is not None or vmax is not None:
1796 polyc.set_clim(vmin, vmax)
1797 if norm is not None:
1798 polyc.set_norm(norm)
1799 else:
1800 if shade:
1801 normals = self._generate_normals(verts)
1802 colset = self._shade_colors(color, normals, lightsource)
1803 else:
1804 colset = color
1805 polyc.set_facecolors(colset)
1807 self.add_collection(polyc)
1808 self.auto_scale_xyz(tri.x, tri.y, z, had_data)
1810 return polyc
1812 def _3d_extend_contour(self, cset, stride=5):
1813 """
1814 Extend a contour in 3D by creating
1815 """
1817 levels = cset.levels
1818 colls = cset.collections
1819 dz = (levels[1] - levels[0]) / 2
1821 for z, linec in zip(levels, colls):
1822 paths = linec.get_paths()
1823 if not paths:
1824 continue
1825 topverts = art3d._paths_to_3d_segments(paths, z - dz)
1826 botverts = art3d._paths_to_3d_segments(paths, z + dz)
1828 color = linec.get_edgecolor()[0]
1830 polyverts = []
1831 normals = []
1832 nsteps = round(len(topverts[0]) / stride)
1833 if nsteps <= 1:
1834 if len(topverts[0]) > 1:
1835 nsteps = 2
1836 else:
1837 continue
1839 stepsize = (len(topverts[0]) - 1) / (nsteps - 1)
1840 for i in range(int(round(nsteps)) - 1):
1841 i1 = int(round(i * stepsize))
1842 i2 = int(round((i + 1) * stepsize))
1843 polyverts.append([topverts[0][i1],
1844 topverts[0][i2],
1845 botverts[0][i2],
1846 botverts[0][i1]])
1848 # all polygons have 4 vertices, so vectorize
1849 polyverts = np.array(polyverts)
1850 normals = self._generate_normals(polyverts)
1852 colors = self._shade_colors(color, normals)
1853 colors2 = self._shade_colors(color, normals)
1854 polycol = art3d.Poly3DCollection(polyverts,
1855 facecolors=colors,
1856 edgecolors=colors2)
1857 polycol.set_sort_zpos(z)
1858 self.add_collection3d(polycol)
1860 for col in colls:
1861 col.remove()
1863 def add_contour_set(
1864 self, cset, extend3d=False, stride=5, zdir='z', offset=None):
1865 zdir = '-' + zdir
1866 if extend3d:
1867 self._3d_extend_contour(cset, stride)
1868 else:
1869 for z, linec in zip(cset.levels, cset.collections):
1870 if offset is not None:
1871 z = offset
1872 art3d.line_collection_2d_to_3d(linec, z, zdir=zdir)
1874 def add_contourf_set(self, cset, zdir='z', offset=None):
1875 self._add_contourf_set(cset, zdir=zdir, offset=offset)
1877 def _add_contourf_set(self, cset, zdir='z', offset=None):
1878 """
1879 Returns
1880 -------
1881 levels : numpy.ndarray
1882 Levels at which the filled contours are added.
1883 """
1884 zdir = '-' + zdir
1886 midpoints = cset.levels[:-1] + np.diff(cset.levels) / 2
1887 # Linearly interpolate to get levels for any extensions
1888 if cset._extend_min:
1889 min_level = cset.levels[0] - np.diff(cset.levels[:2]) / 2
1890 midpoints = np.insert(midpoints, 0, min_level)
1891 if cset._extend_max:
1892 max_level = cset.levels[-1] + np.diff(cset.levels[-2:]) / 2
1893 midpoints = np.append(midpoints, max_level)
1895 for z, linec in zip(midpoints, cset.collections):
1896 if offset is not None:
1897 z = offset
1898 art3d.poly_collection_2d_to_3d(linec, z, zdir=zdir)
1899 linec.set_sort_zpos(z)
1900 return midpoints
1902 @_preprocess_data()
1903 def contour(self, X, Y, Z, *args,
1904 extend3d=False, stride=5, zdir='z', offset=None, **kwargs):
1905 """
1906 Create a 3D contour plot.
1908 Parameters
1909 ----------
1910 X, Y, Z : array-like,
1911 Input data. See `.Axes.contour` for supported data shapes.
1912 extend3d : bool, default: False
1913 Whether to extend contour in 3D.
1914 stride : int
1915 Step size for extending contour.
1916 zdir : {'x', 'y', 'z'}, default: 'z'
1917 The direction to use.
1918 offset : float, optional
1919 If specified, plot a projection of the contour lines at this
1920 position in a plane normal to zdir.
1921 data : indexable object, optional
1922 DATA_PARAMETER_PLACEHOLDER
1924 *args, **kwargs
1925 Other arguments are forwarded to `matplotlib.axes.Axes.contour`.
1927 Returns
1928 -------
1929 matplotlib.contour.QuadContourSet
1930 """
1931 had_data = self.has_data()
1933 jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
1934 cset = super().contour(jX, jY, jZ, *args, **kwargs)
1935 self.add_contour_set(cset, extend3d, stride, zdir, offset)
1937 self.auto_scale_xyz(X, Y, Z, had_data)
1938 return cset
1940 contour3D = contour
1942 @_preprocess_data()
1943 def tricontour(self, *args,
1944 extend3d=False, stride=5, zdir='z', offset=None, **kwargs):
1945 """
1946 Create a 3D contour plot.
1948 .. note::
1949 This method currently produces incorrect output due to a
1950 longstanding bug in 3D PolyCollection rendering.
1952 Parameters
1953 ----------
1954 X, Y, Z : array-like
1955 Input data. See `.Axes.tricontour` for supported data shapes.
1956 extend3d : bool, default: False
1957 Whether to extend contour in 3D.
1958 stride : int
1959 Step size for extending contour.
1960 zdir : {'x', 'y', 'z'}, default: 'z'
1961 The direction to use.
1962 offset : float, optional
1963 If specified, plot a projection of the contour lines at this
1964 position in a plane normal to zdir.
1965 data : indexable object, optional
1966 DATA_PARAMETER_PLACEHOLDER
1967 *args, **kwargs
1968 Other arguments are forwarded to `matplotlib.axes.Axes.tricontour`.
1970 Returns
1971 -------
1972 matplotlib.tri.tricontour.TriContourSet
1973 """
1974 had_data = self.has_data()
1976 tri, args, kwargs = Triangulation.get_from_args_and_kwargs(
1977 *args, **kwargs)
1978 X = tri.x
1979 Y = tri.y
1980 if 'Z' in kwargs:
1981 Z = kwargs.pop('Z')
1982 else:
1983 # We do this so Z doesn't get passed as an arg to Axes.tricontour
1984 Z, *args = args
1986 jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
1987 tri = Triangulation(jX, jY, tri.triangles, tri.mask)
1989 cset = super().tricontour(tri, jZ, *args, **kwargs)
1990 self.add_contour_set(cset, extend3d, stride, zdir, offset)
1992 self.auto_scale_xyz(X, Y, Z, had_data)
1993 return cset
1995 def _auto_scale_contourf(self, X, Y, Z, zdir, levels, had_data):
1996 # Autoscale in the zdir based on the levels added, which are
1997 # different from data range if any contour extensions are present
1998 dim_vals = {'x': X, 'y': Y, 'z': Z, zdir: levels}
1999 # Input data and levels have different sizes, but auto_scale_xyz
2000 # expected same-size input, so manually take min/max limits
2001 limits = [(np.nanmin(dim_vals[dim]), np.nanmax(dim_vals[dim]))
2002 for dim in ['x', 'y', 'z']]
2003 self.auto_scale_xyz(*limits, had_data)
2005 @_preprocess_data()
2006 def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs):
2007 """
2008 Create a 3D filled contour plot.
2010 Parameters
2011 ----------
2012 X, Y, Z : array-like
2013 Input data. See `.Axes.contourf` for supported data shapes.
2014 zdir : {'x', 'y', 'z'}, default: 'z'
2015 The direction to use.
2016 offset : float, optional
2017 If specified, plot a projection of the contour lines at this
2018 position in a plane normal to zdir.
2019 data : indexable object, optional
2020 DATA_PARAMETER_PLACEHOLDER
2021 *args, **kwargs
2022 Other arguments are forwarded to `matplotlib.axes.Axes.contourf`.
2024 Returns
2025 -------
2026 matplotlib.contour.QuadContourSet
2027 """
2028 had_data = self.has_data()
2030 jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
2031 cset = super().contourf(jX, jY, jZ, *args, **kwargs)
2032 levels = self._add_contourf_set(cset, zdir, offset)
2034 self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data)
2035 return cset
2037 contourf3D = contourf
2039 @_preprocess_data()
2040 def tricontourf(self, *args, zdir='z', offset=None, **kwargs):
2041 """
2042 Create a 3D filled contour plot.
2044 .. note::
2045 This method currently produces incorrect output due to a
2046 longstanding bug in 3D PolyCollection rendering.
2048 Parameters
2049 ----------
2050 X, Y, Z : array-like
2051 Input data. See `.Axes.tricontourf` for supported data shapes.
2052 zdir : {'x', 'y', 'z'}, default: 'z'
2053 The direction to use.
2054 offset : float, optional
2055 If specified, plot a projection of the contour lines at this
2056 position in a plane normal to zdir.
2057 data : indexable object, optional
2058 DATA_PARAMETER_PLACEHOLDER
2059 *args, **kwargs
2060 Other arguments are forwarded to
2061 `matplotlib.axes.Axes.tricontourf`.
2063 Returns
2064 -------
2065 matplotlib.tri.tricontour.TriContourSet
2066 """
2067 had_data = self.has_data()
2069 tri, args, kwargs = Triangulation.get_from_args_and_kwargs(
2070 *args, **kwargs)
2071 X = tri.x
2072 Y = tri.y
2073 if 'Z' in kwargs:
2074 Z = kwargs.pop('Z')
2075 else:
2076 # We do this so Z doesn't get passed as an arg to Axes.tricontourf
2077 Z, *args = args
2079 jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
2080 tri = Triangulation(jX, jY, tri.triangles, tri.mask)
2082 cset = super().tricontourf(tri, jZ, *args, **kwargs)
2083 levels = self._add_contourf_set(cset, zdir, offset)
2085 self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data)
2086 return cset
2088 def add_collection3d(self, col, zs=0, zdir='z'):
2089 """
2090 Add a 3D collection object to the plot.
2092 2D collection types are converted to a 3D version by
2093 modifying the object and adding z coordinate information.
2095 Supported are:
2097 - PolyCollection
2098 - LineCollection
2099 - PatchCollection
2100 """
2101 zvals = np.atleast_1d(zs)
2102 zsortval = (np.min(zvals) if zvals.size
2103 else 0) # FIXME: arbitrary default
2105 # FIXME: use issubclass() (although, then a 3D collection
2106 # object would also pass.) Maybe have a collection3d
2107 # abstract class to test for and exclude?
2108 if type(col) is mcoll.PolyCollection:
2109 art3d.poly_collection_2d_to_3d(col, zs=zs, zdir=zdir)
2110 col.set_sort_zpos(zsortval)
2111 elif type(col) is mcoll.LineCollection:
2112 art3d.line_collection_2d_to_3d(col, zs=zs, zdir=zdir)
2113 col.set_sort_zpos(zsortval)
2114 elif type(col) is mcoll.PatchCollection:
2115 art3d.patch_collection_2d_to_3d(col, zs=zs, zdir=zdir)
2116 col.set_sort_zpos(zsortval)
2118 collection = super().add_collection(col)
2119 return collection
2121 @_preprocess_data(replace_names=["xs", "ys", "zs", "s",
2122 "edgecolors", "c", "facecolor",
2123 "facecolors", "color"])
2124 def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True,
2125 *args, **kwargs):
2126 """
2127 Create a scatter plot.
2129 Parameters
2130 ----------
2131 xs, ys : array-like
2132 The data positions.
2133 zs : float or array-like, default: 0
2134 The z-positions. Either an array of the same length as *xs* and
2135 *ys* or a single value to place all points in the same plane.
2136 zdir : {'x', 'y', 'z', '-x', '-y', '-z'}, default: 'z'
2137 The axis direction for the *zs*. This is useful when plotting 2D
2138 data on a 3D Axes. The data must be passed as *xs*, *ys*. Setting
2139 *zdir* to 'y' then plots the data to the x-z-plane.
2141 See also :doc:`/gallery/mplot3d/2dcollections3d`.
2143 s : float or array-like, default: 20
2144 The marker size in points**2. Either an array of the same length
2145 as *xs* and *ys* or a single value to make all markers the same
2146 size.
2147 c : color, sequence, or sequence of colors, optional
2148 The marker color. Possible values:
2150 - A single color format string.
2151 - A sequence of colors of length n.
2152 - A sequence of n numbers to be mapped to colors using *cmap* and
2153 *norm*.
2154 - A 2D array in which the rows are RGB or RGBA.
2156 For more details see the *c* argument of `~.axes.Axes.scatter`.
2157 depthshade : bool, default: True
2158 Whether to shade the scatter markers to give the appearance of
2159 depth. Each call to ``scatter()`` will perform its depthshading
2160 independently.
2161 data : indexable object, optional
2162 DATA_PARAMETER_PLACEHOLDER
2163 **kwargs
2164 All other keyword arguments are passed on to `~.axes.Axes.scatter`.
2166 Returns
2167 -------
2168 paths : `~matplotlib.collections.PathCollection`
2169 """
2171 had_data = self.has_data()
2172 zs_orig = zs
2174 xs, ys, zs = np.broadcast_arrays(
2175 *[np.ravel(np.ma.filled(t, np.nan)) for t in [xs, ys, zs]])
2176 s = np.ma.ravel(s) # This doesn't have to match x, y in size.
2178 xs, ys, zs, s, c = cbook.delete_masked_points(xs, ys, zs, s, c)
2180 # For xs and ys, 2D scatter() will do the copying.
2181 if np.may_share_memory(zs_orig, zs): # Avoid unnecessary copies.
2182 zs = zs.copy()
2184 patches = super().scatter(xs, ys, s=s, c=c, *args, **kwargs)
2185 art3d.patch_collection_2d_to_3d(patches, zs=zs, zdir=zdir,
2186 depthshade=depthshade)
2188 if self._zmargin < 0.05 and xs.size > 0:
2189 self.set_zmargin(0.05)
2191 self.auto_scale_xyz(xs, ys, zs, had_data)
2193 return patches
2195 scatter3D = scatter
2197 @_preprocess_data()
2198 def bar(self, left, height, zs=0, zdir='z', *args, **kwargs):
2199 """
2200 Add 2D bar(s).
2202 Parameters
2203 ----------
2204 left : 1D array-like
2205 The x coordinates of the left sides of the bars.
2206 height : 1D array-like
2207 The height of the bars.
2208 zs : float or 1D array-like
2209 Z coordinate of bars; if a single value is specified, it will be
2210 used for all bars.
2211 zdir : {'x', 'y', 'z'}, default: 'z'
2212 When plotting 2D data, the direction to use as z ('x', 'y' or 'z').
2213 data : indexable object, optional
2214 DATA_PARAMETER_PLACEHOLDER
2215 **kwargs
2216 Other keyword arguments are forwarded to
2217 `matplotlib.axes.Axes.bar`.
2219 Returns
2220 -------
2221 mpl_toolkits.mplot3d.art3d.Patch3DCollection
2222 """
2223 had_data = self.has_data()
2225 patches = super().bar(left, height, *args, **kwargs)
2227 zs = np.broadcast_to(zs, len(left))
2229 verts = []
2230 verts_zs = []
2231 for p, z in zip(patches, zs):
2232 vs = art3d._get_patch_verts(p)
2233 verts += vs.tolist()
2234 verts_zs += [z] * len(vs)
2235 art3d.patch_2d_to_3d(p, z, zdir)
2236 if 'alpha' in kwargs:
2237 p.set_alpha(kwargs['alpha'])
2239 if len(verts) > 0:
2240 # the following has to be skipped if verts is empty
2241 # NOTE: Bugs could still occur if len(verts) > 0,
2242 # but the "2nd dimension" is empty.
2243 xs, ys = zip(*verts)
2244 else:
2245 xs, ys = [], []
2247 xs, ys, verts_zs = art3d.juggle_axes(xs, ys, verts_zs, zdir)
2248 self.auto_scale_xyz(xs, ys, verts_zs, had_data)
2250 return patches
2252 @_preprocess_data()
2253 def bar3d(self, x, y, z, dx, dy, dz, color=None,
2254 zsort='average', shade=True, lightsource=None, *args, **kwargs):
2255 """
2256 Generate a 3D barplot.
2258 This method creates three-dimensional barplot where the width,
2259 depth, height, and color of the bars can all be uniquely set.
2261 Parameters
2262 ----------
2263 x, y, z : array-like
2264 The coordinates of the anchor point of the bars.
2266 dx, dy, dz : float or array-like
2267 The width, depth, and height of the bars, respectively.
2269 color : sequence of colors, optional
2270 The color of the bars can be specified globally or
2271 individually. This parameter can be:
2273 - A single color, to color all bars the same color.
2274 - An array of colors of length N bars, to color each bar
2275 independently.
2276 - An array of colors of length 6, to color the faces of the
2277 bars similarly.
2278 - An array of colors of length 6 * N bars, to color each face
2279 independently.
2281 When coloring the faces of the boxes specifically, this is
2282 the order of the coloring:
2284 1. -Z (bottom of box)
2285 2. +Z (top of box)
2286 3. -Y
2287 4. +Y
2288 5. -X
2289 6. +X
2291 zsort : str, optional
2292 The z-axis sorting scheme passed onto `~.art3d.Poly3DCollection`
2294 shade : bool, default: True
2295 When true, this shades the dark sides of the bars (relative
2296 to the plot's source of light).
2298 lightsource : `~matplotlib.colors.LightSource`
2299 The lightsource to use when *shade* is True.
2301 data : indexable object, optional
2302 DATA_PARAMETER_PLACEHOLDER
2304 **kwargs
2305 Any additional keyword arguments are passed onto
2306 `~.art3d.Poly3DCollection`.
2308 Returns
2309 -------
2310 collection : `~.art3d.Poly3DCollection`
2311 A collection of three-dimensional polygons representing the bars.
2312 """
2314 had_data = self.has_data()
2316 x, y, z, dx, dy, dz = np.broadcast_arrays(
2317 np.atleast_1d(x), y, z, dx, dy, dz)
2318 minx = np.min(x)
2319 maxx = np.max(x + dx)
2320 miny = np.min(y)
2321 maxy = np.max(y + dy)
2322 minz = np.min(z)
2323 maxz = np.max(z + dz)
2325 # shape (6, 4, 3)
2326 # All faces are oriented facing outwards - when viewed from the
2327 # outside, their vertices are in a counterclockwise ordering.
2328 cuboid = np.array([
2329 # -z
2330 (
2331 (0, 0, 0),
2332 (0, 1, 0),
2333 (1, 1, 0),
2334 (1, 0, 0),
2335 ),
2336 # +z
2337 (
2338 (0, 0, 1),
2339 (1, 0, 1),
2340 (1, 1, 1),
2341 (0, 1, 1),
2342 ),
2343 # -y
2344 (
2345 (0, 0, 0),
2346 (1, 0, 0),
2347 (1, 0, 1),
2348 (0, 0, 1),
2349 ),
2350 # +y
2351 (
2352 (0, 1, 0),
2353 (0, 1, 1),
2354 (1, 1, 1),
2355 (1, 1, 0),
2356 ),
2357 # -x
2358 (
2359 (0, 0, 0),
2360 (0, 0, 1),
2361 (0, 1, 1),
2362 (0, 1, 0),
2363 ),
2364 # +x
2365 (
2366 (1, 0, 0),
2367 (1, 1, 0),
2368 (1, 1, 1),
2369 (1, 0, 1),
2370 ),
2371 ])
2373 # indexed by [bar, face, vertex, coord]
2374 polys = np.empty(x.shape + cuboid.shape)
2376 # handle each coordinate separately
2377 for i, p, dp in [(0, x, dx), (1, y, dy), (2, z, dz)]:
2378 p = p[..., np.newaxis, np.newaxis]
2379 dp = dp[..., np.newaxis, np.newaxis]
2380 polys[..., i] = p + dp * cuboid[..., i]
2382 # collapse the first two axes
2383 polys = polys.reshape((-1,) + polys.shape[2:])
2385 facecolors = []
2386 if color is None:
2387 color = [self._get_patches_for_fill.get_next_color()]
2389 color = list(mcolors.to_rgba_array(color))
2391 if len(color) == len(x):
2392 # bar colors specified, need to expand to number of faces
2393 for c in color:
2394 facecolors.extend([c] * 6)
2395 else:
2396 # a single color specified, or face colors specified explicitly
2397 facecolors = color
2398 if len(facecolors) < len(x):
2399 facecolors *= (6 * len(x))
2401 if shade:
2402 normals = self._generate_normals(polys)
2403 sfacecolors = self._shade_colors(facecolors, normals, lightsource)
2404 else:
2405 sfacecolors = facecolors
2407 col = art3d.Poly3DCollection(polys,
2408 zsort=zsort,
2409 facecolor=sfacecolors,
2410 *args, **kwargs)
2411 self.add_collection(col)
2413 self.auto_scale_xyz((minx, maxx), (miny, maxy), (minz, maxz), had_data)
2415 return col
2417 def set_title(self, label, fontdict=None, loc='center', **kwargs):
2418 # docstring inherited
2419 ret = super().set_title(label, fontdict=fontdict, loc=loc, **kwargs)
2420 (x, y) = self.title.get_position()
2421 self.title.set_y(0.92 * y)
2422 return ret
2424 @_preprocess_data()
2425 def quiver(self, X, Y, Z, U, V, W, *,
2426 length=1, arrow_length_ratio=.3, pivot='tail', normalize=False,
2427 **kwargs):
2428 """
2429 Plot a 3D field of arrows.
2431 The arguments can be array-like or scalars, so long as they can be
2432 broadcast together. The arguments can also be masked arrays. If an
2433 element in any of argument is masked, then that corresponding quiver
2434 element will not be plotted.
2436 Parameters
2437 ----------
2438 X, Y, Z : array-like
2439 The x, y and z coordinates of the arrow locations (default is
2440 tail of arrow; see *pivot* kwarg).
2442 U, V, W : array-like
2443 The x, y and z components of the arrow vectors.
2445 length : float, default: 1
2446 The length of each quiver.
2448 arrow_length_ratio : float, default: 0.3
2449 The ratio of the arrow head with respect to the quiver.
2451 pivot : {'tail', 'middle', 'tip'}, default: 'tail'
2452 The part of the arrow that is at the grid point; the arrow
2453 rotates about this point, hence the name *pivot*.
2455 normalize : bool, default: False
2456 Whether all arrows are normalized to have the same length, or keep
2457 the lengths defined by *u*, *v*, and *w*.
2459 data : indexable object, optional
2460 DATA_PARAMETER_PLACEHOLDER
2462 **kwargs
2463 Any additional keyword arguments are delegated to
2464 :class:`.Line3DCollection`
2465 """
2467 def calc_arrows(UVW, angle=15):
2468 # get unit direction vector perpendicular to (u, v, w)
2469 x = UVW[:, 0]
2470 y = UVW[:, 1]
2471 norm = np.linalg.norm(UVW[:, :2], axis=1)
2472 x_p = np.divide(y, norm, where=norm != 0, out=np.zeros_like(x))
2473 y_p = np.divide(-x, norm, where=norm != 0, out=np.ones_like(x))
2474 # compute the two arrowhead direction unit vectors
2475 ra = math.radians(angle)
2476 c = math.cos(ra)
2477 s = math.sin(ra)
2478 # construct the rotation matrices of shape (3, 3, n)
2479 Rpos = np.array(
2480 [[c + (x_p ** 2) * (1 - c), x_p * y_p * (1 - c), y_p * s],
2481 [y_p * x_p * (1 - c), c + (y_p ** 2) * (1 - c), -x_p * s],
2482 [-y_p * s, x_p * s, np.full_like(x_p, c)]])
2483 # opposite rotation negates all the sin terms
2484 Rneg = Rpos.copy()
2485 Rneg[[0, 1, 2, 2], [2, 2, 0, 1]] *= -1
2486 # Batch n (3, 3) x (3) matrix multiplications ((3, 3, n) x (n, 3)).
2487 Rpos_vecs = np.einsum("ij...,...j->...i", Rpos, UVW)
2488 Rneg_vecs = np.einsum("ij...,...j->...i", Rneg, UVW)
2489 # Stack into (n, 2, 3) result.
2490 head_dirs = np.stack([Rpos_vecs, Rneg_vecs], axis=1)
2491 return head_dirs
2493 had_data = self.has_data()
2495 input_args = [X, Y, Z, U, V, W]
2497 # extract the masks, if any
2498 masks = [k.mask for k in input_args
2499 if isinstance(k, np.ma.MaskedArray)]
2500 # broadcast to match the shape
2501 bcast = np.broadcast_arrays(*input_args, *masks)
2502 input_args = bcast[:6]
2503 masks = bcast[6:]
2504 if masks:
2505 # combine the masks into one
2506 mask = functools.reduce(np.logical_or, masks)
2507 # put mask on and compress
2508 input_args = [np.ma.array(k, mask=mask).compressed()
2509 for k in input_args]
2510 else:
2511 input_args = [np.ravel(k) for k in input_args]
2513 if any(len(v) == 0 for v in input_args):
2514 # No quivers, so just make an empty collection and return early
2515 linec = art3d.Line3DCollection([], **kwargs)
2516 self.add_collection(linec)
2517 return linec
2519 shaft_dt = np.array([0., length], dtype=float)
2520 arrow_dt = shaft_dt * arrow_length_ratio
2522 _api.check_in_list(['tail', 'middle', 'tip'], pivot=pivot)
2523 if pivot == 'tail':
2524 shaft_dt -= length
2525 elif pivot == 'middle':
2526 shaft_dt -= length / 2
2528 XYZ = np.column_stack(input_args[:3])
2529 UVW = np.column_stack(input_args[3:]).astype(float)
2531 # Normalize rows of UVW
2532 norm = np.linalg.norm(UVW, axis=1)
2534 # If any row of UVW is all zeros, don't make a quiver for it
2535 mask = norm > 0
2536 XYZ = XYZ[mask]
2537 if normalize:
2538 UVW = UVW[mask] / norm[mask].reshape((-1, 1))
2539 else:
2540 UVW = UVW[mask]
2542 if len(XYZ) > 0:
2543 # compute the shaft lines all at once with an outer product
2544 shafts = (XYZ - np.multiply.outer(shaft_dt, UVW)).swapaxes(0, 1)
2545 # compute head direction vectors, n heads x 2 sides x 3 dimensions
2546 head_dirs = calc_arrows(UVW)
2547 # compute all head lines at once, starting from the shaft ends
2548 heads = shafts[:, :1] - np.multiply.outer(arrow_dt, head_dirs)
2549 # stack left and right head lines together
2550 heads = heads.reshape((len(arrow_dt), -1, 3))
2551 # transpose to get a list of lines
2552 heads = heads.swapaxes(0, 1)
2554 lines = [*shafts, *heads]
2555 else:
2556 lines = []
2558 linec = art3d.Line3DCollection(lines, **kwargs)
2559 self.add_collection(linec)
2561 self.auto_scale_xyz(XYZ[:, 0], XYZ[:, 1], XYZ[:, 2], had_data)
2563 return linec
2565 quiver3D = quiver
2567 def voxels(self, *args, facecolors=None, edgecolors=None, shade=True,
2568 lightsource=None, **kwargs):
2569 """
2570 ax.voxels([x, y, z,] /, filled, facecolors=None, edgecolors=None, \
2571**kwargs)
2573 Plot a set of filled voxels
2575 All voxels are plotted as 1x1x1 cubes on the axis, with
2576 ``filled[0, 0, 0]`` placed with its lower corner at the origin.
2577 Occluded faces are not plotted.
2579 Parameters
2580 ----------
2581 filled : 3D np.array of bool
2582 A 3D array of values, with truthy values indicating which voxels
2583 to fill
2585 x, y, z : 3D np.array, optional
2586 The coordinates of the corners of the voxels. This should broadcast
2587 to a shape one larger in every dimension than the shape of
2588 *filled*. These can be used to plot non-cubic voxels.
2590 If not specified, defaults to increasing integers along each axis,
2591 like those returned by :func:`~numpy.indices`.
2592 As indicated by the ``/`` in the function signature, these
2593 arguments can only be passed positionally.
2595 facecolors, edgecolors : array-like, optional
2596 The color to draw the faces and edges of the voxels. Can only be
2597 passed as keyword arguments.
2598 These parameters can be:
2600 - A single color value, to color all voxels the same color. This
2601 can be either a string, or a 1D rgb/rgba array
2602 - ``None``, the default, to use a single color for the faces, and
2603 the style default for the edges.
2604 - A 3D ndarray of color names, with each item the color for the
2605 corresponding voxel. The size must match the voxels.
2606 - A 4D ndarray of rgb/rgba data, with the components along the
2607 last axis.
2609 shade : bool, default: True
2610 Whether to shade the facecolors.
2612 lightsource : `~matplotlib.colors.LightSource`
2613 The lightsource to use when *shade* is True.
2615 **kwargs
2616 Additional keyword arguments to pass onto
2617 `~mpl_toolkits.mplot3d.art3d.Poly3DCollection`.
2619 Returns
2620 -------
2621 faces : dict
2622 A dictionary indexed by coordinate, where ``faces[i, j, k]`` is a
2623 `.Poly3DCollection` of the faces drawn for the voxel
2624 ``filled[i, j, k]``. If no faces were drawn for a given voxel,
2625 either because it was not asked to be drawn, or it is fully
2626 occluded, then ``(i, j, k) not in faces``.
2628 Examples
2629 --------
2630 .. plot:: gallery/mplot3d/voxels.py
2631 .. plot:: gallery/mplot3d/voxels_rgb.py
2632 .. plot:: gallery/mplot3d/voxels_torus.py
2633 .. plot:: gallery/mplot3d/voxels_numpy_logo.py
2634 """
2636 # work out which signature we should be using, and use it to parse
2637 # the arguments. Name must be voxels for the correct error message
2638 if len(args) >= 3:
2639 # underscores indicate position only
2640 def voxels(__x, __y, __z, filled, **kwargs):
2641 return (__x, __y, __z), filled, kwargs
2642 else:
2643 def voxels(filled, **kwargs):
2644 return None, filled, kwargs
2646 xyz, filled, kwargs = voxels(*args, **kwargs)
2648 # check dimensions
2649 if filled.ndim != 3:
2650 raise ValueError("Argument filled must be 3-dimensional")
2651 size = np.array(filled.shape, dtype=np.intp)
2653 # check xyz coordinates, which are one larger than the filled shape
2654 coord_shape = tuple(size + 1)
2655 if xyz is None:
2656 x, y, z = np.indices(coord_shape)
2657 else:
2658 x, y, z = (np.broadcast_to(c, coord_shape) for c in xyz)
2660 def _broadcast_color_arg(color, name):
2661 if np.ndim(color) in (0, 1):
2662 # single color, like "red" or [1, 0, 0]
2663 return np.broadcast_to(color, filled.shape + np.shape(color))
2664 elif np.ndim(color) in (3, 4):
2665 # 3D array of strings, or 4D array with last axis rgb
2666 if np.shape(color)[:3] != filled.shape:
2667 raise ValueError(
2668 "When multidimensional, {} must match the shape of "
2669 "filled".format(name))
2670 return color
2671 else:
2672 raise ValueError("Invalid {} argument".format(name))
2674 # broadcast and default on facecolors
2675 if facecolors is None:
2676 facecolors = self._get_patches_for_fill.get_next_color()
2677 facecolors = _broadcast_color_arg(facecolors, 'facecolors')
2679 # broadcast but no default on edgecolors
2680 edgecolors = _broadcast_color_arg(edgecolors, 'edgecolors')
2682 # scale to the full array, even if the data is only in the center
2683 self.auto_scale_xyz(x, y, z)
2685 # points lying on corners of a square
2686 square = np.array([
2687 [0, 0, 0],
2688 [1, 0, 0],
2689 [1, 1, 0],
2690 [0, 1, 0],
2691 ], dtype=np.intp)
2693 voxel_faces = defaultdict(list)
2695 def permutation_matrices(n):
2696 """Generate cyclic permutation matrices."""
2697 mat = np.eye(n, dtype=np.intp)
2698 for i in range(n):
2699 yield mat
2700 mat = np.roll(mat, 1, axis=0)
2702 # iterate over each of the YZ, ZX, and XY orientations, finding faces
2703 # to render
2704 for permute in permutation_matrices(3):
2705 # find the set of ranges to iterate over
2706 pc, qc, rc = permute.T.dot(size)
2707 pinds = np.arange(pc)
2708 qinds = np.arange(qc)
2709 rinds = np.arange(rc)
2711 square_rot_pos = square.dot(permute.T)
2712 square_rot_neg = square_rot_pos[::-1]
2714 # iterate within the current plane
2715 for p in pinds:
2716 for q in qinds:
2717 # iterate perpendicularly to the current plane, handling
2718 # boundaries. We only draw faces between a voxel and an
2719 # empty space, to avoid drawing internal faces.
2721 # draw lower faces
2722 p0 = permute.dot([p, q, 0])
2723 i0 = tuple(p0)
2724 if filled[i0]:
2725 voxel_faces[i0].append(p0 + square_rot_neg)
2727 # draw middle faces
2728 for r1, r2 in zip(rinds[:-1], rinds[1:]):
2729 p1 = permute.dot([p, q, r1])
2730 p2 = permute.dot([p, q, r2])
2732 i1 = tuple(p1)
2733 i2 = tuple(p2)
2735 if filled[i1] and not filled[i2]:
2736 voxel_faces[i1].append(p2 + square_rot_pos)
2737 elif not filled[i1] and filled[i2]:
2738 voxel_faces[i2].append(p2 + square_rot_neg)
2740 # draw upper faces
2741 pk = permute.dot([p, q, rc-1])
2742 pk2 = permute.dot([p, q, rc])
2743 ik = tuple(pk)
2744 if filled[ik]:
2745 voxel_faces[ik].append(pk2 + square_rot_pos)
2747 # iterate over the faces, and generate a Poly3DCollection for each
2748 # voxel
2749 polygons = {}
2750 for coord, faces_inds in voxel_faces.items():
2751 # convert indices into 3D positions
2752 if xyz is None:
2753 faces = faces_inds
2754 else:
2755 faces = []
2756 for face_inds in faces_inds:
2757 ind = face_inds[:, 0], face_inds[:, 1], face_inds[:, 2]
2758 face = np.empty(face_inds.shape)
2759 face[:, 0] = x[ind]
2760 face[:, 1] = y[ind]
2761 face[:, 2] = z[ind]
2762 faces.append(face)
2764 # shade the faces
2765 facecolor = facecolors[coord]
2766 edgecolor = edgecolors[coord]
2767 if shade:
2768 normals = self._generate_normals(faces)
2769 facecolor = self._shade_colors(facecolor, normals, lightsource)
2770 if edgecolor is not None:
2771 edgecolor = self._shade_colors(
2772 edgecolor, normals, lightsource
2773 )
2775 poly = art3d.Poly3DCollection(
2776 faces, facecolors=facecolor, edgecolors=edgecolor, **kwargs)
2777 self.add_collection3d(poly)
2778 polygons[coord] = poly
2780 return polygons
2782 @_preprocess_data(replace_names=["x", "y", "z", "xerr", "yerr", "zerr"])
2783 def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='',
2784 barsabove=False, errorevery=1, ecolor=None, elinewidth=None,
2785 capsize=None, capthick=None, xlolims=False, xuplims=False,
2786 ylolims=False, yuplims=False, zlolims=False, zuplims=False,
2787 **kwargs):
2788 """
2789 Plot lines and/or markers with errorbars around them.
2791 *x*/*y*/*z* define the data locations, and *xerr*/*yerr*/*zerr* define
2792 the errorbar sizes. By default, this draws the data markers/lines as
2793 well the errorbars. Use fmt='none' to draw errorbars only.
2795 Parameters
2796 ----------
2797 x, y, z : float or array-like
2798 The data positions.
2800 xerr, yerr, zerr : float or array-like, shape (N,) or (2, N), optional
2801 The errorbar sizes:
2803 - scalar: Symmetric +/- values for all data points.
2804 - shape(N,): Symmetric +/-values for each data point.
2805 - shape(2, N): Separate - and + values for each bar. First row
2806 contains the lower errors, the second row contains the upper
2807 errors.
2808 - *None*: No errorbar.
2810 Note that all error arrays should have *positive* values.
2812 fmt : str, default: ''
2813 The format for the data points / data lines. See `.plot` for
2814 details.
2816 Use 'none' (case-insensitive) to plot errorbars without any data
2817 markers.
2819 ecolor : color, default: None
2820 The color of the errorbar lines. If None, use the color of the
2821 line connecting the markers.
2823 elinewidth : float, default: None
2824 The linewidth of the errorbar lines. If None, the linewidth of
2825 the current style is used.
2827 capsize : float, default: :rc:`errorbar.capsize`
2828 The length of the error bar caps in points.
2830 capthick : float, default: None
2831 An alias to the keyword argument *markeredgewidth* (a.k.a. *mew*).
2832 This setting is a more sensible name for the property that
2833 controls the thickness of the error bar cap in points. For
2834 backwards compatibility, if *mew* or *markeredgewidth* are given,
2835 then they will over-ride *capthick*. This may change in future
2836 releases.
2838 barsabove : bool, default: False
2839 If True, will plot the errorbars above the plot
2840 symbols. Default is below.
2842 xlolims, ylolims, zlolims : bool, default: False
2843 These arguments can be used to indicate that a value gives only
2844 lower limits. In that case a caret symbol is used to indicate
2845 this. *lims*-arguments may be scalars, or array-likes of the same
2846 length as the errors. To use limits with inverted axes,
2847 `~.Axes.set_xlim` or `~.Axes.set_ylim` must be called before
2848 `errorbar`. Note the tricky parameter names: setting e.g.
2849 *ylolims* to True means that the y-value is a *lower* limit of the
2850 True value, so, only an *upward*-pointing arrow will be drawn!
2852 xuplims, yuplims, zuplims : bool, default: False
2853 Same as above, but for controlling the upper limits.
2855 errorevery : int or (int, int), default: 1
2856 draws error bars on a subset of the data. *errorevery* =N draws
2857 error bars on the points (x[::N], y[::N], z[::N]).
2858 *errorevery* =(start, N) draws error bars on the points
2859 (x[start::N], y[start::N], z[start::N]). e.g. errorevery=(6, 3)
2860 adds error bars to the data at (x[6], x[9], x[12], x[15], ...).
2861 Used to avoid overlapping error bars when two series share x-axis
2862 values.
2864 Returns
2865 -------
2866 errlines : list
2867 List of `~mpl_toolkits.mplot3d.art3d.Line3DCollection` instances
2868 each containing an errorbar line.
2869 caplines : list
2870 List of `~mpl_toolkits.mplot3d.art3d.Line3D` instances each
2871 containing a capline object.
2872 limmarks : list
2873 List of `~mpl_toolkits.mplot3d.art3d.Line3D` instances each
2874 containing a marker with an upper or lower limit.
2876 Other Parameters
2877 ----------------
2878 data : indexable object, optional
2879 DATA_PARAMETER_PLACEHOLDER
2881 **kwargs
2882 All other keyword arguments for styling errorbar lines are passed
2883 `~mpl_toolkits.mplot3d.art3d.Line3DCollection`.
2885 Examples
2886 --------
2887 .. plot:: gallery/mplot3d/errorbar3d.py
2888 """
2889 had_data = self.has_data()
2891 kwargs = cbook.normalize_kwargs(kwargs, mlines.Line2D)
2892 # Drop anything that comes in as None to use the default instead.
2893 kwargs = {k: v for k, v in kwargs.items() if v is not None}
2894 kwargs.setdefault('zorder', 2)
2896 self._process_unit_info([("x", x), ("y", y), ("z", z)], kwargs,
2897 convert=False)
2899 # make sure all the args are iterable; use lists not arrays to
2900 # preserve units
2901 x = x if np.iterable(x) else [x]
2902 y = y if np.iterable(y) else [y]
2903 z = z if np.iterable(z) else [z]
2905 if not len(x) == len(y) == len(z):
2906 raise ValueError("'x', 'y', and 'z' must have the same size")
2908 everymask = self._errorevery_to_mask(x, errorevery)
2910 label = kwargs.pop("label", None)
2911 kwargs['label'] = '_nolegend_'
2913 # Create the main line and determine overall kwargs for child artists.
2914 # We avoid calling self.plot() directly, or self._get_lines(), because
2915 # that would call self._process_unit_info again, and do other indirect
2916 # data processing.
2917 (data_line, base_style), = self._get_lines._plot_args(
2918 (x, y) if fmt == '' else (x, y, fmt), kwargs, return_kwargs=True)
2919 art3d.line_2d_to_3d(data_line, zs=z)
2921 # Do this after creating `data_line` to avoid modifying `base_style`.
2922 if barsabove:
2923 data_line.set_zorder(kwargs['zorder'] - .1)
2924 else:
2925 data_line.set_zorder(kwargs['zorder'] + .1)
2927 # Add line to plot, or throw it away and use it to determine kwargs.
2928 if fmt.lower() != 'none':
2929 self.add_line(data_line)
2930 else:
2931 data_line = None
2932 # Remove alpha=0 color that _process_plot_format returns.
2933 base_style.pop('color')
2935 if 'color' not in base_style:
2936 base_style['color'] = 'C0'
2937 if ecolor is None:
2938 ecolor = base_style['color']
2940 # Eject any line-specific information from format string, as it's not
2941 # needed for bars or caps.
2942 for key in ['marker', 'markersize', 'markerfacecolor',
2943 'markeredgewidth', 'markeredgecolor', 'markevery',
2944 'linestyle', 'fillstyle', 'drawstyle', 'dash_capstyle',
2945 'dash_joinstyle', 'solid_capstyle', 'solid_joinstyle']:
2946 base_style.pop(key, None)
2948 # Make the style dict for the line collections (the bars).
2949 eb_lines_style = {**base_style, 'color': ecolor}
2951 if elinewidth:
2952 eb_lines_style['linewidth'] = elinewidth
2953 elif 'linewidth' in kwargs:
2954 eb_lines_style['linewidth'] = kwargs['linewidth']
2956 for key in ('transform', 'alpha', 'zorder', 'rasterized'):
2957 if key in kwargs:
2958 eb_lines_style[key] = kwargs[key]
2960 # Make the style dict for caps (the "hats").
2961 eb_cap_style = {**base_style, 'linestyle': 'None'}
2962 if capsize is None:
2963 capsize = mpl.rcParams["errorbar.capsize"]
2964 if capsize > 0:
2965 eb_cap_style['markersize'] = 2. * capsize
2966 if capthick is not None:
2967 eb_cap_style['markeredgewidth'] = capthick
2968 eb_cap_style['color'] = ecolor
2970 def _apply_mask(arrays, mask):
2971 # Return, for each array in *arrays*, the elements for which *mask*
2972 # is True, without using fancy indexing.
2973 return [[*itertools.compress(array, mask)] for array in arrays]
2975 def _extract_errs(err, data, lomask, himask):
2976 # For separate +/- error values we need to unpack err
2977 if len(err.shape) == 2:
2978 low_err, high_err = err
2979 else:
2980 low_err, high_err = err, err
2982 lows = np.where(lomask | ~everymask, data, data - low_err)
2983 highs = np.where(himask | ~everymask, data, data + high_err)
2985 return lows, highs
2987 # collect drawn items while looping over the three coordinates
2988 errlines, caplines, limmarks = [], [], []
2990 # list of endpoint coordinates, used for auto-scaling
2991 coorderrs = []
2993 # define the markers used for errorbar caps and limits below
2994 # the dictionary key is mapped by the `i_xyz` helper dictionary
2995 capmarker = {0: '|', 1: '|', 2: '_'}
2996 i_xyz = {'x': 0, 'y': 1, 'z': 2}
2998 # Calculate marker size from points to quiver length. Because these are
2999 # not markers, and 3D Axes do not use the normal transform stack, this
3000 # is a bit involved. Since the quiver arrows will change size as the
3001 # scene is rotated, they are given a standard size based on viewing
3002 # them directly in planar form.
3003 quiversize = eb_cap_style.get('markersize',
3004 mpl.rcParams['lines.markersize']) ** 2
3005 quiversize *= self.figure.dpi / 72
3006 quiversize = self.transAxes.inverted().transform([
3007 (0, 0), (quiversize, quiversize)])
3008 quiversize = np.mean(np.diff(quiversize, axis=0))
3009 # quiversize is now in Axes coordinates, and to convert back to data
3010 # coordinates, we need to run it through the inverse 3D transform. For
3011 # consistency, this uses a fixed elevation, azimuth, and roll.
3012 with cbook._setattr_cm(self, elev=0, azim=0, roll=0):
3013 invM = np.linalg.inv(self.get_proj())
3014 # elev=azim=roll=0 produces the Y-Z plane, so quiversize in 2D 'x' is
3015 # 'y' in 3D, hence the 1 index.
3016 quiversize = np.dot(invM, np.array([quiversize, 0, 0, 0]))[1]
3017 # Quivers use a fixed 15-degree arrow head, so scale up the length so
3018 # that the size corresponds to the base. In other words, this constant
3019 # corresponds to the equation tan(15) = (base / 2) / (arrow length).
3020 quiversize *= 1.8660254037844388
3021 eb_quiver_style = {**eb_cap_style,
3022 'length': quiversize, 'arrow_length_ratio': 1}
3023 eb_quiver_style.pop('markersize', None)
3025 # loop over x-, y-, and z-direction and draw relevant elements
3026 for zdir, data, err, lolims, uplims in zip(
3027 ['x', 'y', 'z'], [x, y, z], [xerr, yerr, zerr],
3028 [xlolims, ylolims, zlolims], [xuplims, yuplims, zuplims]):
3030 dir_vector = art3d.get_dir_vector(zdir)
3031 i_zdir = i_xyz[zdir]
3033 if err is None:
3034 continue
3036 if not np.iterable(err):
3037 err = [err] * len(data)
3039 err = np.atleast_1d(err)
3041 # arrays fine here, they are booleans and hence not units
3042 lolims = np.broadcast_to(lolims, len(data)).astype(bool)
3043 uplims = np.broadcast_to(uplims, len(data)).astype(bool)
3045 # a nested list structure that expands to (xl,xh),(yl,yh),(zl,zh),
3046 # where x/y/z and l/h correspond to dimensions and low/high
3047 # positions of errorbars in a dimension we're looping over
3048 coorderr = [
3049 _extract_errs(err * dir_vector[i], coord, lolims, uplims)
3050 for i, coord in enumerate([x, y, z])]
3051 (xl, xh), (yl, yh), (zl, zh) = coorderr
3053 # draws capmarkers - flat caps orthogonal to the error bars
3054 nolims = ~(lolims | uplims)
3055 if nolims.any() and capsize > 0:
3056 lo_caps_xyz = _apply_mask([xl, yl, zl], nolims & everymask)
3057 hi_caps_xyz = _apply_mask([xh, yh, zh], nolims & everymask)
3059 # setting '_' for z-caps and '|' for x- and y-caps;
3060 # these markers will rotate as the viewing angle changes
3061 cap_lo = art3d.Line3D(*lo_caps_xyz, ls='',
3062 marker=capmarker[i_zdir],
3063 **eb_cap_style)
3064 cap_hi = art3d.Line3D(*hi_caps_xyz, ls='',
3065 marker=capmarker[i_zdir],
3066 **eb_cap_style)
3067 self.add_line(cap_lo)
3068 self.add_line(cap_hi)
3069 caplines.append(cap_lo)
3070 caplines.append(cap_hi)
3072 if lolims.any():
3073 xh0, yh0, zh0 = _apply_mask([xh, yh, zh], lolims & everymask)
3074 self.quiver(xh0, yh0, zh0, *dir_vector, **eb_quiver_style)
3075 if uplims.any():
3076 xl0, yl0, zl0 = _apply_mask([xl, yl, zl], uplims & everymask)
3077 self.quiver(xl0, yl0, zl0, *-dir_vector, **eb_quiver_style)
3079 errline = art3d.Line3DCollection(np.array(coorderr).T,
3080 **eb_lines_style)
3081 self.add_collection(errline)
3082 errlines.append(errline)
3083 coorderrs.append(coorderr)
3085 coorderrs = np.array(coorderrs)
3087 def _digout_minmax(err_arr, coord_label):
3088 return (np.nanmin(err_arr[:, i_xyz[coord_label], :, :]),
3089 np.nanmax(err_arr[:, i_xyz[coord_label], :, :]))
3091 minx, maxx = _digout_minmax(coorderrs, 'x')
3092 miny, maxy = _digout_minmax(coorderrs, 'y')
3093 minz, maxz = _digout_minmax(coorderrs, 'z')
3094 self.auto_scale_xyz((minx, maxx), (miny, maxy), (minz, maxz), had_data)
3096 # Adapting errorbar containers for 3d case, assuming z-axis points "up"
3097 errorbar_container = mcontainer.ErrorbarContainer(
3098 (data_line, tuple(caplines), tuple(errlines)),
3099 has_xerr=(xerr is not None or yerr is not None),
3100 has_yerr=(zerr is not None),
3101 label=label)
3102 self.containers.append(errorbar_container)
3104 return errlines, caplines, limmarks
3106 def get_tightbbox(self, renderer=None, call_axes_locator=True,
3107 bbox_extra_artists=None, *, for_layout_only=False):
3108 ret = super().get_tightbbox(renderer,
3109 call_axes_locator=call_axes_locator,
3110 bbox_extra_artists=bbox_extra_artists,
3111 for_layout_only=for_layout_only)
3112 batch = [ret]
3113 if self._axis3don:
3114 for axis in self._axis_map.values():
3115 if axis.get_visible():
3116 axis_bb = martist._get_tightbbox_for_layout_only(
3117 axis, renderer)
3118 if axis_bb:
3119 batch.append(axis_bb)
3120 return mtransforms.Bbox.union(batch)
3122 @_preprocess_data()
3123 def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-',
3124 bottom=0, label=None, orientation='z'):
3125 """
3126 Create a 3D stem plot.
3128 A stem plot draws lines perpendicular to a baseline, and places markers
3129 at the heads. By default, the baseline is defined by *x* and *y*, and
3130 stems are drawn vertically from *bottom* to *z*.
3132 Parameters
3133 ----------
3134 x, y, z : array-like
3135 The positions of the heads of the stems. The stems are drawn along
3136 the *orientation*-direction from the baseline at *bottom* (in the
3137 *orientation*-coordinate) to the heads. By default, the *x* and *y*
3138 positions are used for the baseline and *z* for the head position,
3139 but this can be changed by *orientation*.
3141 linefmt : str, default: 'C0-'
3142 A string defining the properties of the vertical lines. Usually,
3143 this will be a color or a color and a linestyle:
3145 ========= =============
3146 Character Line Style
3147 ========= =============
3148 ``'-'`` solid line
3149 ``'--'`` dashed line
3150 ``'-.'`` dash-dot line
3151 ``':'`` dotted line
3152 ========= =============
3154 Note: While it is technically possible to specify valid formats
3155 other than color or color and linestyle (e.g. 'rx' or '-.'), this
3156 is beyond the intention of the method and will most likely not
3157 result in a reasonable plot.
3159 markerfmt : str, default: 'C0o'
3160 A string defining the properties of the markers at the stem heads.
3162 basefmt : str, default: 'C3-'
3163 A format string defining the properties of the baseline.
3165 bottom : float, default: 0
3166 The position of the baseline, in *orientation*-coordinates.
3168 label : str, default: None
3169 The label to use for the stems in legends.
3171 orientation : {'x', 'y', 'z'}, default: 'z'
3172 The direction along which stems are drawn.
3174 data : indexable object, optional
3175 DATA_PARAMETER_PLACEHOLDER
3177 Returns
3178 -------
3179 `.StemContainer`
3180 The container may be treated like a tuple
3181 (*markerline*, *stemlines*, *baseline*)
3183 Examples
3184 --------
3185 .. plot:: gallery/mplot3d/stem3d_demo.py
3186 """
3188 from matplotlib.container import StemContainer
3190 had_data = self.has_data()
3192 _api.check_in_list(['x', 'y', 'z'], orientation=orientation)
3194 xlim = (np.min(x), np.max(x))
3195 ylim = (np.min(y), np.max(y))
3196 zlim = (np.min(z), np.max(z))
3198 # Determine the appropriate plane for the baseline and the direction of
3199 # stemlines based on the value of orientation.
3200 if orientation == 'x':
3201 basex, basexlim = y, ylim
3202 basey, baseylim = z, zlim
3203 lines = [[(bottom, thisy, thisz), (thisx, thisy, thisz)]
3204 for thisx, thisy, thisz in zip(x, y, z)]
3205 elif orientation == 'y':
3206 basex, basexlim = x, xlim
3207 basey, baseylim = z, zlim
3208 lines = [[(thisx, bottom, thisz), (thisx, thisy, thisz)]
3209 for thisx, thisy, thisz in zip(x, y, z)]
3210 else:
3211 basex, basexlim = x, xlim
3212 basey, baseylim = y, ylim
3213 lines = [[(thisx, thisy, bottom), (thisx, thisy, thisz)]
3214 for thisx, thisy, thisz in zip(x, y, z)]
3216 # Determine style for stem lines.
3217 linestyle, linemarker, linecolor = _process_plot_format(linefmt)
3218 if linestyle is None:
3219 linestyle = mpl.rcParams['lines.linestyle']
3221 # Plot everything in required order.
3222 baseline, = self.plot(basex, basey, basefmt, zs=bottom,
3223 zdir=orientation, label='_nolegend_')
3224 stemlines = art3d.Line3DCollection(
3225 lines, linestyles=linestyle, colors=linecolor, label='_nolegend_')
3226 self.add_collection(stemlines)
3227 markerline, = self.plot(x, y, z, markerfmt, label='_nolegend_')
3229 stem_container = StemContainer((markerline, stemlines, baseline),
3230 label=label)
3231 self.add_container(stem_container)
3233 jx, jy, jz = art3d.juggle_axes(basexlim, baseylim, [bottom, bottom],
3234 orientation)
3235 self.auto_scale_xyz([*jx, *xlim], [*jy, *ylim], [*jz, *zlim], had_data)
3237 return stem_container
3239 stem3D = stem
3242def get_test_data(delta=0.05):
3243 """Return a tuple X, Y, Z with a test data set."""
3244 x = y = np.arange(-3.0, 3.0, delta)
3245 X, Y = np.meshgrid(x, y)
3247 Z1 = np.exp(-(X**2 + Y**2) / 2) / (2 * np.pi)
3248 Z2 = (np.exp(-(((X - 1) / 1.5)**2 + ((Y - 1) / 0.5)**2) / 2) /
3249 (2 * np.pi * 0.5 * 1.5))
3250 Z = Z2 - Z1
3252 X = X * 10
3253 Y = Y * 10
3254 Z = Z * 500
3255 return X, Y, Z