1"""
2axes3d.py, original mplot3d version by John Porter
3Created: 23 Sep 2005
4
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>
8
9Module containing Axes3D, an object which can plot 3D objects on a
102D matplotlib figure.
11"""
12
13from collections import defaultdict
14import itertools
15import math
16import textwrap
17
18import numpy as np
19
20import matplotlib as mpl
21from matplotlib import _api, cbook, _docstring, _preprocess_data
22import matplotlib.artist as martist
23import matplotlib.collections as mcoll
24import matplotlib.colors as mcolors
25import matplotlib.image as mimage
26import matplotlib.lines as mlines
27import matplotlib.patches as mpatches
28import matplotlib.container as mcontainer
29import matplotlib.transforms as mtransforms
30from matplotlib.axes import Axes
31from matplotlib.axes._base import _axis_method_wrapper, _process_plot_format
32from matplotlib.transforms import Bbox
33from matplotlib.tri._triangulation import Triangulation
34
35from . import art3d
36from . import proj3d
37from . import axis3d
38
39
40@_docstring.interpd
41@_api.define_aliases({
42 "xlim": ["xlim3d"], "ylim": ["ylim3d"], "zlim": ["zlim3d"]})
43class Axes3D(Axes):
44 """
45 3D Axes object.
46
47 .. note::
48
49 As a user, you do not instantiate Axes directly, but use Axes creation
50 methods instead; e.g. from `.pyplot` or `.Figure`:
51 `~.pyplot.subplots`, `~.pyplot.subplot_mosaic` or `.Figure.add_axes`.
52 """
53 name = '3d'
54
55 _axis_names = ("x", "y", "z")
56 Axes._shared_axes["z"] = cbook.Grouper()
57 Axes._shared_axes["view"] = cbook.Grouper()
58
59 def __init__(
60 self, fig, rect=None, *args,
61 elev=30, azim=-60, roll=0, sharez=None, proj_type='persp',
62 box_aspect=None, computed_zorder=True, focal_length=None,
63 shareview=None,
64 **kwargs):
65 """
66 Parameters
67 ----------
68 fig : Figure
69 The parent figure.
70 rect : tuple (left, bottom, width, height), default: None.
71 The ``(left, bottom, width, height)`` Axes position.
72 elev : float, default: 30
73 The elevation angle in degrees rotates the camera above and below
74 the x-y plane, with a positive angle corresponding to a location
75 above the plane.
76 azim : float, default: -60
77 The azimuthal angle in degrees rotates the camera about the z axis,
78 with a positive angle corresponding to a right-handed rotation. In
79 other words, a positive azimuth rotates the camera about the origin
80 from its location along the +x axis towards the +y axis.
81 roll : float, default: 0
82 The roll angle in degrees rotates the camera about the viewing
83 axis. A positive angle spins the camera clockwise, causing the
84 scene to rotate counter-clockwise.
85 sharez : Axes3D, optional
86 Other Axes to share z-limits with. Note that it is not possible to
87 unshare axes.
88 proj_type : {'persp', 'ortho'}
89 The projection type, default 'persp'.
90 box_aspect : 3-tuple of floats, default: None
91 Changes the physical dimensions of the Axes3D, such that the ratio
92 of the axis lengths in display units is x:y:z.
93 If None, defaults to 4:4:3
94 computed_zorder : bool, default: True
95 If True, the draw order is computed based on the average position
96 of the `.Artist`\\s along the view direction.
97 Set to False if you want to manually control the order in which
98 Artists are drawn on top of each other using their *zorder*
99 attribute. This can be used for fine-tuning if the automatic order
100 does not produce the desired result. Note however, that a manual
101 zorder will only be correct for a limited view angle. If the figure
102 is rotated by the user, it will look wrong from certain angles.
103 focal_length : float, default: None
104 For a projection type of 'persp', the focal length of the virtual
105 camera. Must be > 0. If None, defaults to 1.
106 For a projection type of 'ortho', must be set to either None
107 or infinity (numpy.inf). If None, defaults to infinity.
108 The focal length can be computed from a desired Field Of View via
109 the equation: focal_length = 1/tan(FOV/2)
110 shareview : Axes3D, optional
111 Other Axes to share view angles with. Note that it is not possible
112 to unshare axes.
113
114 **kwargs
115 Other optional keyword arguments:
116
117 %(Axes3D:kwdoc)s
118 """
119
120 if rect is None:
121 rect = [0.0, 0.0, 1.0, 1.0]
122
123 self.initial_azim = azim
124 self.initial_elev = elev
125 self.initial_roll = roll
126 self.set_proj_type(proj_type, focal_length)
127 self.computed_zorder = computed_zorder
128
129 self.xy_viewLim = Bbox.unit()
130 self.zz_viewLim = Bbox.unit()
131 xymargin = 0.05 * 10/11 # match mpl3.8 appearance
132 self.xy_dataLim = Bbox([[xymargin, xymargin],
133 [1 - xymargin, 1 - xymargin]])
134 # z-limits are encoded in the x-component of the Bbox, y is un-used
135 self.zz_dataLim = Bbox.unit()
136
137 # inhibit autoscale_view until the axes are defined
138 # they can't be defined until Axes.__init__ has been called
139 self.view_init(self.initial_elev, self.initial_azim, self.initial_roll)
140
141 self._sharez = sharez
142 if sharez is not None:
143 self._shared_axes["z"].join(self, sharez)
144 self._adjustable = 'datalim'
145
146 self._shareview = shareview
147 if shareview is not None:
148 self._shared_axes["view"].join(self, shareview)
149
150 if kwargs.pop('auto_add_to_figure', False):
151 raise AttributeError(
152 'auto_add_to_figure is no longer supported for Axes3D. '
153 'Use fig.add_axes(ax) instead.'
154 )
155
156 super().__init__(
157 fig, rect, frameon=True, box_aspect=box_aspect, *args, **kwargs
158 )
159 # Disable drawing of axes by base class
160 super().set_axis_off()
161 # Enable drawing of axes by Axes3D class
162 self.set_axis_on()
163 self.M = None
164 self.invM = None
165
166 self._view_margin = 1/48 # default value to match mpl3.8
167 self.autoscale_view()
168
169 # func used to format z -- fall back on major formatters
170 self.fmt_zdata = None
171
172 self.mouse_init()
173 self.figure.canvas.callbacks._connect_picklable(
174 'motion_notify_event', self._on_move)
175 self.figure.canvas.callbacks._connect_picklable(
176 'button_press_event', self._button_press)
177 self.figure.canvas.callbacks._connect_picklable(
178 'button_release_event', self._button_release)
179 self.set_top_view()
180
181 self.patch.set_linewidth(0)
182 # Calculate the pseudo-data width and height
183 pseudo_bbox = self.transLimits.inverted().transform([(0, 0), (1, 1)])
184 self._pseudo_w, self._pseudo_h = pseudo_bbox[1] - pseudo_bbox[0]
185
186 # mplot3d currently manages its own spines and needs these turned off
187 # for bounding box calculations
188 self.spines[:].set_visible(False)
189
190 def set_axis_off(self):
191 self._axis3don = False
192 self.stale = True
193
194 def set_axis_on(self):
195 self._axis3don = True
196 self.stale = True
197
198 def convert_zunits(self, z):
199 """
200 For artists in an Axes, if the zaxis has units support,
201 convert *z* using zaxis unit type
202 """
203 return self.zaxis.convert_units(z)
204
205 def set_top_view(self):
206 # this happens to be the right view for the viewing coordinates
207 # moved up and to the left slightly to fit labels and axes
208 xdwl = 0.95 / self._dist
209 xdw = 0.9 / self._dist
210 ydwl = 0.95 / self._dist
211 ydw = 0.9 / self._dist
212 # Set the viewing pane.
213 self.viewLim.intervalx = (-xdwl, xdw)
214 self.viewLim.intervaly = (-ydwl, ydw)
215 self.stale = True
216
217 def _init_axis(self):
218 """Init 3D Axes; overrides creation of regular X/Y Axes."""
219 self.xaxis = axis3d.XAxis(self)
220 self.yaxis = axis3d.YAxis(self)
221 self.zaxis = axis3d.ZAxis(self)
222
223 def get_zaxis(self):
224 """Return the ``ZAxis`` (`~.axis3d.Axis`) instance."""
225 return self.zaxis
226
227 get_zgridlines = _axis_method_wrapper("zaxis", "get_gridlines")
228 get_zticklines = _axis_method_wrapper("zaxis", "get_ticklines")
229
230 def _transformed_cube(self, vals):
231 """Return cube with limits from *vals* transformed by self.M."""
232 minx, maxx, miny, maxy, minz, maxz = vals
233 xyzs = [(minx, miny, minz),
234 (maxx, miny, minz),
235 (maxx, maxy, minz),
236 (minx, maxy, minz),
237 (minx, miny, maxz),
238 (maxx, miny, maxz),
239 (maxx, maxy, maxz),
240 (minx, maxy, maxz)]
241 return proj3d._proj_points(xyzs, self.M)
242
243 def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
244 """
245 Set the aspect ratios.
246
247 Parameters
248 ----------
249 aspect : {'auto', 'equal', 'equalxy', 'equalxz', 'equalyz'}
250 Possible values:
251
252 ========= ==================================================
253 value description
254 ========= ==================================================
255 'auto' automatic; fill the position rectangle with data.
256 'equal' adapt all the axes to have equal aspect ratios.
257 'equalxy' adapt the x and y axes to have equal aspect ratios.
258 'equalxz' adapt the x and z axes to have equal aspect ratios.
259 'equalyz' adapt the y and z axes to have equal aspect ratios.
260 ========= ==================================================
261
262 adjustable : None or {'box', 'datalim'}, optional
263 If not *None*, this defines which parameter will be adjusted to
264 meet the required aspect. See `.set_adjustable` for further
265 details.
266
267 anchor : None or str or 2-tuple of float, optional
268 If not *None*, this defines where the Axes will be drawn if there
269 is extra space due to aspect constraints. The most common way to
270 specify the anchor are abbreviations of cardinal directions:
271
272 ===== =====================
273 value description
274 ===== =====================
275 'C' centered
276 'SW' lower left corner
277 'S' middle of bottom edge
278 'SE' lower right corner
279 etc.
280 ===== =====================
281
282 See `~.Axes.set_anchor` for further details.
283
284 share : bool, default: False
285 If ``True``, apply the settings to all shared Axes.
286
287 See Also
288 --------
289 mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect
290 """
291 _api.check_in_list(('auto', 'equal', 'equalxy', 'equalyz', 'equalxz'),
292 aspect=aspect)
293 super().set_aspect(
294 aspect='auto', adjustable=adjustable, anchor=anchor, share=share)
295 self._aspect = aspect
296
297 if aspect in ('equal', 'equalxy', 'equalxz', 'equalyz'):
298 ax_indices = self._equal_aspect_axis_indices(aspect)
299
300 view_intervals = np.array([self.xaxis.get_view_interval(),
301 self.yaxis.get_view_interval(),
302 self.zaxis.get_view_interval()])
303 ptp = np.ptp(view_intervals, axis=1)
304 if self._adjustable == 'datalim':
305 mean = np.mean(view_intervals, axis=1)
306 scale = max(ptp[ax_indices] / self._box_aspect[ax_indices])
307 deltas = scale * self._box_aspect
308
309 for i, set_lim in enumerate((self.set_xlim3d,
310 self.set_ylim3d,
311 self.set_zlim3d)):
312 if i in ax_indices:
313 set_lim(mean[i] - deltas[i]/2., mean[i] + deltas[i]/2.,
314 auto=True, view_margin=None)
315 else: # 'box'
316 # Change the box aspect such that the ratio of the length of
317 # the unmodified axis to the length of the diagonal
318 # perpendicular to it remains unchanged.
319 box_aspect = np.array(self._box_aspect)
320 box_aspect[ax_indices] = ptp[ax_indices]
321 remaining_ax_indices = {0, 1, 2}.difference(ax_indices)
322 if remaining_ax_indices:
323 remaining = remaining_ax_indices.pop()
324 old_diag = np.linalg.norm(self._box_aspect[ax_indices])
325 new_diag = np.linalg.norm(box_aspect[ax_indices])
326 box_aspect[remaining] *= new_diag / old_diag
327 self.set_box_aspect(box_aspect)
328
329 def _equal_aspect_axis_indices(self, aspect):
330 """
331 Get the indices for which of the x, y, z axes are constrained to have
332 equal aspect ratios.
333
334 Parameters
335 ----------
336 aspect : {'auto', 'equal', 'equalxy', 'equalxz', 'equalyz'}
337 See descriptions in docstring for `.set_aspect()`.
338 """
339 ax_indices = [] # aspect == 'auto'
340 if aspect == 'equal':
341 ax_indices = [0, 1, 2]
342 elif aspect == 'equalxy':
343 ax_indices = [0, 1]
344 elif aspect == 'equalxz':
345 ax_indices = [0, 2]
346 elif aspect == 'equalyz':
347 ax_indices = [1, 2]
348 return ax_indices
349
350 def set_box_aspect(self, aspect, *, zoom=1):
351 """
352 Set the Axes box aspect.
353
354 The box aspect is the ratio of height to width in display
355 units for each face of the box when viewed perpendicular to
356 that face. This is not to be confused with the data aspect (see
357 `~.Axes3D.set_aspect`). The default ratios are 4:4:3 (x:y:z).
358
359 To simulate having equal aspect in data space, set the box
360 aspect to match your data range in each dimension.
361
362 *zoom* controls the overall size of the Axes3D in the figure.
363
364 Parameters
365 ----------
366 aspect : 3-tuple of floats or None
367 Changes the physical dimensions of the Axes3D, such that the ratio
368 of the axis lengths in display units is x:y:z.
369 If None, defaults to (4, 4, 3).
370
371 zoom : float, default: 1
372 Control overall size of the Axes3D in the figure. Must be > 0.
373 """
374 if zoom <= 0:
375 raise ValueError(f'Argument zoom = {zoom} must be > 0')
376
377 if aspect is None:
378 aspect = np.asarray((4, 4, 3), dtype=float)
379 else:
380 aspect = np.asarray(aspect, dtype=float)
381 _api.check_shape((3,), aspect=aspect)
382 # The scale 1.8294640721620434 is tuned to match the mpl3.2 appearance.
383 # The 25/24 factor is to compensate for the change in automargin
384 # behavior in mpl3.9. This comes from the padding of 1/48 on both sides
385 # of the axes in mpl3.8.
386 aspect *= 1.8294640721620434 * 25/24 * zoom / np.linalg.norm(aspect)
387
388 self._box_aspect = self._roll_to_vertical(aspect, reverse=True)
389 self.stale = True
390
391 def apply_aspect(self, position=None):
392 if position is None:
393 position = self.get_position(original=True)
394
395 # in the superclass, we would go through and actually deal with axis
396 # scales and box/datalim. Those are all irrelevant - all we need to do
397 # is make sure our coordinate system is square.
398 trans = self.get_figure().transSubfigure
399 bb = mtransforms.Bbox.unit().transformed(trans)
400 # this is the physical aspect of the panel (or figure):
401 fig_aspect = bb.height / bb.width
402
403 box_aspect = 1
404 pb = position.frozen()
405 pb1 = pb.shrunk_to_aspect(box_aspect, pb, fig_aspect)
406 self._set_position(pb1.anchored(self.get_anchor(), pb), 'active')
407
408 @martist.allow_rasterization
409 def draw(self, renderer):
410 if not self.get_visible():
411 return
412 self._unstale_viewLim()
413
414 # draw the background patch
415 self.patch.draw(renderer)
416 self._frameon = False
417
418 # first, set the aspect
419 # this is duplicated from `axes._base._AxesBase.draw`
420 # but must be called before any of the artist are drawn as
421 # it adjusts the view limits and the size of the bounding box
422 # of the Axes
423 locator = self.get_axes_locator()
424 self.apply_aspect(locator(self, renderer) if locator else None)
425
426 # add the projection matrix to the renderer
427 self.M = self.get_proj()
428 self.invM = np.linalg.inv(self.M)
429
430 collections_and_patches = (
431 artist for artist in self._children
432 if isinstance(artist, (mcoll.Collection, mpatches.Patch))
433 and artist.get_visible())
434 if self.computed_zorder:
435 # Calculate projection of collections and patches and zorder
436 # them. Make sure they are drawn above the grids.
437 zorder_offset = max(axis.get_zorder()
438 for axis in self._axis_map.values()) + 1
439 collection_zorder = patch_zorder = zorder_offset
440
441 for artist in sorted(collections_and_patches,
442 key=lambda artist: artist.do_3d_projection(),
443 reverse=True):
444 if isinstance(artist, mcoll.Collection):
445 artist.zorder = collection_zorder
446 collection_zorder += 1
447 elif isinstance(artist, mpatches.Patch):
448 artist.zorder = patch_zorder
449 patch_zorder += 1
450 else:
451 for artist in collections_and_patches:
452 artist.do_3d_projection()
453
454 if self._axis3don:
455 # Draw panes first
456 for axis in self._axis_map.values():
457 axis.draw_pane(renderer)
458 # Then gridlines
459 for axis in self._axis_map.values():
460 axis.draw_grid(renderer)
461 # Then axes, labels, text, and ticks
462 for axis in self._axis_map.values():
463 axis.draw(renderer)
464
465 # Then rest
466 super().draw(renderer)
467
468 def get_axis_position(self):
469 tc = self._transformed_cube(self.get_w_lims())
470 xhigh = tc[1][2] > tc[2][2]
471 yhigh = tc[3][2] > tc[2][2]
472 zhigh = tc[0][2] > tc[2][2]
473 return xhigh, yhigh, zhigh
474
475 def update_datalim(self, xys, **kwargs):
476 """
477 Not implemented in `~mpl_toolkits.mplot3d.axes3d.Axes3D`.
478 """
479 pass
480
481 get_autoscalez_on = _axis_method_wrapper("zaxis", "_get_autoscale_on")
482 set_autoscalez_on = _axis_method_wrapper("zaxis", "_set_autoscale_on")
483
484 def get_zmargin(self):
485 """
486 Retrieve autoscaling margin of the z-axis.
487
488 .. versionadded:: 3.9
489
490 Returns
491 -------
492 zmargin : float
493
494 See Also
495 --------
496 mpl_toolkits.mplot3d.axes3d.Axes3D.set_zmargin
497 """
498 return self._zmargin
499
500 def set_zmargin(self, m):
501 """
502 Set padding of Z data limits prior to autoscaling.
503
504 *m* times the data interval will be added to each end of that interval
505 before it is used in autoscaling. If *m* is negative, this will clip
506 the data range instead of expanding it.
507
508 For example, if your data is in the range [0, 2], a margin of 0.1 will
509 result in a range [-0.2, 2.2]; a margin of -0.1 will result in a range
510 of [0.2, 1.8].
511
512 Parameters
513 ----------
514 m : float greater than -0.5
515 """
516 if m <= -0.5:
517 raise ValueError("margin must be greater than -0.5")
518 self._zmargin = m
519 self._request_autoscale_view("z")
520 self.stale = True
521
522 def margins(self, *margins, x=None, y=None, z=None, tight=True):
523 """
524 Set or retrieve autoscaling margins.
525
526 See `.Axes.margins` for full documentation. Because this function
527 applies to 3D Axes, it also takes a *z* argument, and returns
528 ``(xmargin, ymargin, zmargin)``.
529 """
530 if margins and (x is not None or y is not None or z is not None):
531 raise TypeError('Cannot pass both positional and keyword '
532 'arguments for x, y, and/or z.')
533 elif len(margins) == 1:
534 x = y = z = margins[0]
535 elif len(margins) == 3:
536 x, y, z = margins
537 elif margins:
538 raise TypeError('Must pass a single positional argument for all '
539 'margins, or one for each margin (x, y, z).')
540
541 if x is None and y is None and z is None:
542 if tight is not True:
543 _api.warn_external(f'ignoring tight={tight!r} in get mode')
544 return self._xmargin, self._ymargin, self._zmargin
545
546 if x is not None:
547 self.set_xmargin(x)
548 if y is not None:
549 self.set_ymargin(y)
550 if z is not None:
551 self.set_zmargin(z)
552
553 self.autoscale_view(
554 tight=tight, scalex=(x is not None), scaley=(y is not None),
555 scalez=(z is not None)
556 )
557
558 def autoscale(self, enable=True, axis='both', tight=None):
559 """
560 Convenience method for simple axis view autoscaling.
561
562 See `.Axes.autoscale` for full documentation. Because this function
563 applies to 3D Axes, *axis* can also be set to 'z', and setting *axis*
564 to 'both' autoscales all three axes.
565 """
566 if enable is None:
567 scalex = True
568 scaley = True
569 scalez = True
570 else:
571 if axis in ['x', 'both']:
572 self.set_autoscalex_on(enable)
573 scalex = self.get_autoscalex_on()
574 else:
575 scalex = False
576 if axis in ['y', 'both']:
577 self.set_autoscaley_on(enable)
578 scaley = self.get_autoscaley_on()
579 else:
580 scaley = False
581 if axis in ['z', 'both']:
582 self.set_autoscalez_on(enable)
583 scalez = self.get_autoscalez_on()
584 else:
585 scalez = False
586 if scalex:
587 self._request_autoscale_view("x", tight=tight)
588 if scaley:
589 self._request_autoscale_view("y", tight=tight)
590 if scalez:
591 self._request_autoscale_view("z", tight=tight)
592
593 def auto_scale_xyz(self, X, Y, Z=None, had_data=None):
594 # This updates the bounding boxes as to keep a record as to what the
595 # minimum sized rectangular volume holds the data.
596 if np.shape(X) == np.shape(Y):
597 self.xy_dataLim.update_from_data_xy(
598 np.column_stack([np.ravel(X), np.ravel(Y)]), not had_data)
599 else:
600 self.xy_dataLim.update_from_data_x(X, not had_data)
601 self.xy_dataLim.update_from_data_y(Y, not had_data)
602 if Z is not None:
603 self.zz_dataLim.update_from_data_x(Z, not had_data)
604 # Let autoscale_view figure out how to use this data.
605 self.autoscale_view()
606
607 def autoscale_view(self, tight=None,
608 scalex=True, scaley=True, scalez=True):
609 """
610 Autoscale the view limits using the data limits.
611
612 See `.Axes.autoscale_view` for full documentation. Because this
613 function applies to 3D Axes, it also takes a *scalez* argument.
614 """
615 # This method looks at the rectangular volume (see above)
616 # of data and decides how to scale the view portal to fit it.
617 if tight is None:
618 _tight = self._tight
619 if not _tight:
620 # if image data only just use the datalim
621 for artist in self._children:
622 if isinstance(artist, mimage.AxesImage):
623 _tight = True
624 elif isinstance(artist, (mlines.Line2D, mpatches.Patch)):
625 _tight = False
626 break
627 else:
628 _tight = self._tight = bool(tight)
629
630 if scalex and self.get_autoscalex_on():
631 x0, x1 = self.xy_dataLim.intervalx
632 xlocator = self.xaxis.get_major_locator()
633 x0, x1 = xlocator.nonsingular(x0, x1)
634 if self._xmargin > 0:
635 delta = (x1 - x0) * self._xmargin
636 x0 -= delta
637 x1 += delta
638 if not _tight:
639 x0, x1 = xlocator.view_limits(x0, x1)
640 self.set_xbound(x0, x1, self._view_margin)
641
642 if scaley and self.get_autoscaley_on():
643 y0, y1 = self.xy_dataLim.intervaly
644 ylocator = self.yaxis.get_major_locator()
645 y0, y1 = ylocator.nonsingular(y0, y1)
646 if self._ymargin > 0:
647 delta = (y1 - y0) * self._ymargin
648 y0 -= delta
649 y1 += delta
650 if not _tight:
651 y0, y1 = ylocator.view_limits(y0, y1)
652 self.set_ybound(y0, y1, self._view_margin)
653
654 if scalez and self.get_autoscalez_on():
655 z0, z1 = self.zz_dataLim.intervalx
656 zlocator = self.zaxis.get_major_locator()
657 z0, z1 = zlocator.nonsingular(z0, z1)
658 if self._zmargin > 0:
659 delta = (z1 - z0) * self._zmargin
660 z0 -= delta
661 z1 += delta
662 if not _tight:
663 z0, z1 = zlocator.view_limits(z0, z1)
664 self.set_zbound(z0, z1, self._view_margin)
665
666 def get_w_lims(self):
667 """Get 3D world limits."""
668 minx, maxx = self.get_xlim3d()
669 miny, maxy = self.get_ylim3d()
670 minz, maxz = self.get_zlim3d()
671 return minx, maxx, miny, maxy, minz, maxz
672
673 def _set_bound3d(self, get_bound, set_lim, axis_inverted,
674 lower=None, upper=None, view_margin=None):
675 """
676 Set 3D axis bounds.
677 """
678 if upper is None and np.iterable(lower):
679 lower, upper = lower
680
681 old_lower, old_upper = get_bound()
682 if lower is None:
683 lower = old_lower
684 if upper is None:
685 upper = old_upper
686
687 set_lim(sorted((lower, upper), reverse=bool(axis_inverted())),
688 auto=None, view_margin=view_margin)
689
690 def set_xbound(self, lower=None, upper=None, view_margin=None):
691 """
692 Set the lower and upper numerical bounds of the x-axis.
693
694 This method will honor axis inversion regardless of parameter order.
695 It will not change the autoscaling setting (`.get_autoscalex_on()`).
696
697 Parameters
698 ----------
699 lower, upper : float or None
700 The lower and upper bounds. If *None*, the respective axis bound
701 is not modified.
702 view_margin : float or None
703 The margin to apply to the bounds. If *None*, the margin is handled
704 by `.set_xlim`.
705
706 See Also
707 --------
708 get_xbound
709 get_xlim, set_xlim
710 invert_xaxis, xaxis_inverted
711 """
712 self._set_bound3d(self.get_xbound, self.set_xlim, self.xaxis_inverted,
713 lower, upper, view_margin)
714
715 def set_ybound(self, lower=None, upper=None, view_margin=None):
716 """
717 Set the lower and upper numerical bounds of the y-axis.
718
719 This method will honor axis inversion regardless of parameter order.
720 It will not change the autoscaling setting (`.get_autoscaley_on()`).
721
722 Parameters
723 ----------
724 lower, upper : float or None
725 The lower and upper bounds. If *None*, the respective axis bound
726 is not modified.
727 view_margin : float or None
728 The margin to apply to the bounds. If *None*, the margin is handled
729 by `.set_ylim`.
730
731 See Also
732 --------
733 get_ybound
734 get_ylim, set_ylim
735 invert_yaxis, yaxis_inverted
736 """
737 self._set_bound3d(self.get_ybound, self.set_ylim, self.yaxis_inverted,
738 lower, upper, view_margin)
739
740 def set_zbound(self, lower=None, upper=None, view_margin=None):
741 """
742 Set the lower and upper numerical bounds of the z-axis.
743 This method will honor axis inversion regardless of parameter order.
744 It will not change the autoscaling setting (`.get_autoscaley_on()`).
745
746 Parameters
747 ----------
748 lower, upper : float or None
749 The lower and upper bounds. If *None*, the respective axis bound
750 is not modified.
751 view_margin : float or None
752 The margin to apply to the bounds. If *None*, the margin is handled
753 by `.set_zlim`.
754
755 See Also
756 --------
757 get_zbound
758 get_zlim, set_zlim
759 invert_zaxis, zaxis_inverted
760 """
761 self._set_bound3d(self.get_zbound, self.set_zlim, self.zaxis_inverted,
762 lower, upper, view_margin)
763
764 def _set_lim3d(self, axis, lower=None, upper=None, *, emit=True,
765 auto=False, view_margin=None, axmin=None, axmax=None):
766 """
767 Set 3D axis limits.
768 """
769 if upper is None:
770 if np.iterable(lower):
771 lower, upper = lower
772 elif axmax is None:
773 upper = axis.get_view_interval()[1]
774 if lower is None and axmin is None:
775 lower = axis.get_view_interval()[0]
776 if axmin is not None:
777 if lower is not None:
778 raise TypeError("Cannot pass both 'lower' and 'min'")
779 lower = axmin
780 if axmax is not None:
781 if upper is not None:
782 raise TypeError("Cannot pass both 'upper' and 'max'")
783 upper = axmax
784 if np.isinf(lower) or np.isinf(upper):
785 raise ValueError(f"Axis limits {lower}, {upper} cannot be infinite")
786 if view_margin is None:
787 if mpl.rcParams['axes3d.automargin']:
788 view_margin = self._view_margin
789 else:
790 view_margin = 0
791 delta = (upper - lower) * view_margin
792 lower -= delta
793 upper += delta
794 return axis._set_lim(lower, upper, emit=emit, auto=auto)
795
796 def set_xlim(self, left=None, right=None, *, emit=True, auto=False,
797 view_margin=None, xmin=None, xmax=None):
798 """
799 Set the 3D x-axis view limits.
800
801 Parameters
802 ----------
803 left : float, optional
804 The left xlim in data coordinates. Passing *None* leaves the
805 limit unchanged.
806
807 The left and right xlims may also be passed as the tuple
808 (*left*, *right*) as the first positional argument (or as
809 the *left* keyword argument).
810
811 .. ACCEPTS: (left: float, right: float)
812
813 right : float, optional
814 The right xlim in data coordinates. Passing *None* leaves the
815 limit unchanged.
816
817 emit : bool, default: True
818 Whether to notify observers of limit change.
819
820 auto : bool or None, default: False
821 Whether to turn on autoscaling of the x-axis. *True* turns on,
822 *False* turns off, *None* leaves unchanged.
823
824 view_margin : float, optional
825 The additional margin to apply to the limits.
826
827 xmin, xmax : float, optional
828 They are equivalent to left and right respectively, and it is an
829 error to pass both *xmin* and *left* or *xmax* and *right*.
830
831 Returns
832 -------
833 left, right : (float, float)
834 The new x-axis limits in data coordinates.
835
836 See Also
837 --------
838 get_xlim
839 set_xbound, get_xbound
840 invert_xaxis, xaxis_inverted
841
842 Notes
843 -----
844 The *left* value may be greater than the *right* value, in which
845 case the x-axis values will decrease from *left* to *right*.
846
847 Examples
848 --------
849 >>> set_xlim(left, right)
850 >>> set_xlim((left, right))
851 >>> left, right = set_xlim(left, right)
852
853 One limit may be left unchanged.
854
855 >>> set_xlim(right=right_lim)
856
857 Limits may be passed in reverse order to flip the direction of
858 the x-axis. For example, suppose ``x`` represents depth of the
859 ocean in m. The x-axis limits might be set like the following
860 so 5000 m depth is at the left of the plot and the surface,
861 0 m, is at the right.
862
863 >>> set_xlim(5000, 0)
864 """
865 return self._set_lim3d(self.xaxis, left, right, emit=emit, auto=auto,
866 view_margin=view_margin, axmin=xmin, axmax=xmax)
867
868 def set_ylim(self, bottom=None, top=None, *, emit=True, auto=False,
869 view_margin=None, ymin=None, ymax=None):
870 """
871 Set the 3D y-axis view limits.
872
873 Parameters
874 ----------
875 bottom : float, optional
876 The bottom ylim in data coordinates. Passing *None* leaves the
877 limit unchanged.
878
879 The bottom and top ylims may also be passed as the tuple
880 (*bottom*, *top*) as the first positional argument (or as
881 the *bottom* keyword argument).
882
883 .. ACCEPTS: (bottom: float, top: float)
884
885 top : float, optional
886 The top ylim in data coordinates. Passing *None* leaves the
887 limit unchanged.
888
889 emit : bool, default: True
890 Whether to notify observers of limit change.
891
892 auto : bool or None, default: False
893 Whether to turn on autoscaling of the y-axis. *True* turns on,
894 *False* turns off, *None* leaves unchanged.
895
896 view_margin : float, optional
897 The additional margin to apply to the limits.
898
899 ymin, ymax : float, optional
900 They are equivalent to bottom and top respectively, and it is an
901 error to pass both *ymin* and *bottom* or *ymax* and *top*.
902
903 Returns
904 -------
905 bottom, top : (float, float)
906 The new y-axis limits in data coordinates.
907
908 See Also
909 --------
910 get_ylim
911 set_ybound, get_ybound
912 invert_yaxis, yaxis_inverted
913
914 Notes
915 -----
916 The *bottom* value may be greater than the *top* value, in which
917 case the y-axis values will decrease from *bottom* to *top*.
918
919 Examples
920 --------
921 >>> set_ylim(bottom, top)
922 >>> set_ylim((bottom, top))
923 >>> bottom, top = set_ylim(bottom, top)
924
925 One limit may be left unchanged.
926
927 >>> set_ylim(top=top_lim)
928
929 Limits may be passed in reverse order to flip the direction of
930 the y-axis. For example, suppose ``y`` represents depth of the
931 ocean in m. The y-axis limits might be set like the following
932 so 5000 m depth is at the bottom of the plot and the surface,
933 0 m, is at the top.
934
935 >>> set_ylim(5000, 0)
936 """
937 return self._set_lim3d(self.yaxis, bottom, top, emit=emit, auto=auto,
938 view_margin=view_margin, axmin=ymin, axmax=ymax)
939
940 def set_zlim(self, bottom=None, top=None, *, emit=True, auto=False,
941 view_margin=None, zmin=None, zmax=None):
942 """
943 Set the 3D z-axis view limits.
944
945 Parameters
946 ----------
947 bottom : float, optional
948 The bottom zlim in data coordinates. Passing *None* leaves the
949 limit unchanged.
950
951 The bottom and top zlims may also be passed as the tuple
952 (*bottom*, *top*) as the first positional argument (or as
953 the *bottom* keyword argument).
954
955 .. ACCEPTS: (bottom: float, top: float)
956
957 top : float, optional
958 The top zlim in data coordinates. Passing *None* leaves the
959 limit unchanged.
960
961 emit : bool, default: True
962 Whether to notify observers of limit change.
963
964 auto : bool or None, default: False
965 Whether to turn on autoscaling of the z-axis. *True* turns on,
966 *False* turns off, *None* leaves unchanged.
967
968 view_margin : float, optional
969 The additional margin to apply to the limits.
970
971 zmin, zmax : float, optional
972 They are equivalent to bottom and top respectively, and it is an
973 error to pass both *zmin* and *bottom* or *zmax* and *top*.
974
975 Returns
976 -------
977 bottom, top : (float, float)
978 The new z-axis limits in data coordinates.
979
980 See Also
981 --------
982 get_zlim
983 set_zbound, get_zbound
984 invert_zaxis, zaxis_inverted
985
986 Notes
987 -----
988 The *bottom* value may be greater than the *top* value, in which
989 case the z-axis values will decrease from *bottom* to *top*.
990
991 Examples
992 --------
993 >>> set_zlim(bottom, top)
994 >>> set_zlim((bottom, top))
995 >>> bottom, top = set_zlim(bottom, top)
996
997 One limit may be left unchanged.
998
999 >>> set_zlim(top=top_lim)
1000
1001 Limits may be passed in reverse order to flip the direction of
1002 the z-axis. For example, suppose ``z`` represents depth of the
1003 ocean in m. The z-axis limits might be set like the following
1004 so 5000 m depth is at the bottom of the plot and the surface,
1005 0 m, is at the top.
1006
1007 >>> set_zlim(5000, 0)
1008 """
1009 return self._set_lim3d(self.zaxis, bottom, top, emit=emit, auto=auto,
1010 view_margin=view_margin, axmin=zmin, axmax=zmax)
1011
1012 set_xlim3d = set_xlim
1013 set_ylim3d = set_ylim
1014 set_zlim3d = set_zlim
1015
1016 def get_xlim(self):
1017 # docstring inherited
1018 return tuple(self.xy_viewLim.intervalx)
1019
1020 def get_ylim(self):
1021 # docstring inherited
1022 return tuple(self.xy_viewLim.intervaly)
1023
1024 def get_zlim(self):
1025 """
1026 Return the 3D z-axis view limits.
1027
1028 Returns
1029 -------
1030 left, right : (float, float)
1031 The current z-axis limits in data coordinates.
1032
1033 See Also
1034 --------
1035 set_zlim
1036 set_zbound, get_zbound
1037 invert_zaxis, zaxis_inverted
1038
1039 Notes
1040 -----
1041 The z-axis may be inverted, in which case the *left* value will
1042 be greater than the *right* value.
1043 """
1044 return tuple(self.zz_viewLim.intervalx)
1045
1046 get_zscale = _axis_method_wrapper("zaxis", "get_scale")
1047
1048 # Redefine all three methods to overwrite their docstrings.
1049 set_xscale = _axis_method_wrapper("xaxis", "_set_axes_scale")
1050 set_yscale = _axis_method_wrapper("yaxis", "_set_axes_scale")
1051 set_zscale = _axis_method_wrapper("zaxis", "_set_axes_scale")
1052 set_xscale.__doc__, set_yscale.__doc__, set_zscale.__doc__ = map(
1053 """
1054 Set the {}-axis scale.
1055
1056 Parameters
1057 ----------
1058 value : {{"linear"}}
1059 The axis scale type to apply. 3D Axes currently only support
1060 linear scales; other scales yield nonsensical results.
1061
1062 **kwargs
1063 Keyword arguments are nominally forwarded to the scale class, but
1064 none of them is applicable for linear scales.
1065 """.format,
1066 ["x", "y", "z"])
1067
1068 get_zticks = _axis_method_wrapper("zaxis", "get_ticklocs")
1069 set_zticks = _axis_method_wrapper("zaxis", "set_ticks")
1070 get_zmajorticklabels = _axis_method_wrapper("zaxis", "get_majorticklabels")
1071 get_zminorticklabels = _axis_method_wrapper("zaxis", "get_minorticklabels")
1072 get_zticklabels = _axis_method_wrapper("zaxis", "get_ticklabels")
1073 set_zticklabels = _axis_method_wrapper(
1074 "zaxis", "set_ticklabels",
1075 doc_sub={"Axis.set_ticks": "Axes3D.set_zticks"})
1076
1077 zaxis_date = _axis_method_wrapper("zaxis", "axis_date")
1078 if zaxis_date.__doc__:
1079 zaxis_date.__doc__ += textwrap.dedent("""
1080
1081 Notes
1082 -----
1083 This function is merely provided for completeness, but 3D Axes do not
1084 support dates for ticks, and so this may not work as expected.
1085 """)
1086
1087 def clabel(self, *args, **kwargs):
1088 """Currently not implemented for 3D Axes, and returns *None*."""
1089 return None
1090
1091 def view_init(self, elev=None, azim=None, roll=None, vertical_axis="z",
1092 share=False):
1093 """
1094 Set the elevation and azimuth of the Axes in degrees (not radians).
1095
1096 This can be used to rotate the Axes programmatically.
1097
1098 To look normal to the primary planes, the following elevation and
1099 azimuth angles can be used. A roll angle of 0, 90, 180, or 270 deg
1100 will rotate these views while keeping the axes at right angles.
1101
1102 ========== ==== ====
1103 view plane elev azim
1104 ========== ==== ====
1105 XY 90 -90
1106 XZ 0 -90
1107 YZ 0 0
1108 -XY -90 90
1109 -XZ 0 90
1110 -YZ 0 180
1111 ========== ==== ====
1112
1113 Parameters
1114 ----------
1115 elev : float, default: None
1116 The elevation angle in degrees rotates the camera above the plane
1117 pierced by the vertical axis, with a positive angle corresponding
1118 to a location above that plane. For example, with the default
1119 vertical axis of 'z', the elevation defines the angle of the camera
1120 location above the x-y plane.
1121 If None, then the initial value as specified in the `Axes3D`
1122 constructor is used.
1123 azim : float, default: None
1124 The azimuthal angle in degrees rotates the camera about the
1125 vertical axis, with a positive angle corresponding to a
1126 right-handed rotation. For example, with the default vertical axis
1127 of 'z', a positive azimuth rotates the camera about the origin from
1128 its location along the +x axis towards the +y axis.
1129 If None, then the initial value as specified in the `Axes3D`
1130 constructor is used.
1131 roll : float, default: None
1132 The roll angle in degrees rotates the camera about the viewing
1133 axis. A positive angle spins the camera clockwise, causing the
1134 scene to rotate counter-clockwise.
1135 If None, then the initial value as specified in the `Axes3D`
1136 constructor is used.
1137 vertical_axis : {"z", "x", "y"}, default: "z"
1138 The axis to align vertically. *azim* rotates about this axis.
1139 share : bool, default: False
1140 If ``True``, apply the settings to all Axes with shared views.
1141 """
1142
1143 self._dist = 10 # The camera distance from origin. Behaves like zoom
1144
1145 if elev is None:
1146 elev = self.initial_elev
1147 if azim is None:
1148 azim = self.initial_azim
1149 if roll is None:
1150 roll = self.initial_roll
1151 vertical_axis = _api.check_getitem(
1152 {name: idx for idx, name in enumerate(self._axis_names)},
1153 vertical_axis=vertical_axis,
1154 )
1155
1156 if share:
1157 axes = {sibling for sibling
1158 in self._shared_axes['view'].get_siblings(self)}
1159 else:
1160 axes = [self]
1161
1162 for ax in axes:
1163 ax.elev = elev
1164 ax.azim = azim
1165 ax.roll = roll
1166 ax._vertical_axis = vertical_axis
1167
1168 def set_proj_type(self, proj_type, focal_length=None):
1169 """
1170 Set the projection type.
1171
1172 Parameters
1173 ----------
1174 proj_type : {'persp', 'ortho'}
1175 The projection type.
1176 focal_length : float, default: None
1177 For a projection type of 'persp', the focal length of the virtual
1178 camera. Must be > 0. If None, defaults to 1.
1179 The focal length can be computed from a desired Field Of View via
1180 the equation: focal_length = 1/tan(FOV/2)
1181 """
1182 _api.check_in_list(['persp', 'ortho'], proj_type=proj_type)
1183 if proj_type == 'persp':
1184 if focal_length is None:
1185 focal_length = 1
1186 elif focal_length <= 0:
1187 raise ValueError(f"focal_length = {focal_length} must be "
1188 "greater than 0")
1189 self._focal_length = focal_length
1190 else: # 'ortho':
1191 if focal_length not in (None, np.inf):
1192 raise ValueError(f"focal_length = {focal_length} must be "
1193 f"None for proj_type = {proj_type}")
1194 self._focal_length = np.inf
1195
1196 def _roll_to_vertical(
1197 self, arr: "np.typing.ArrayLike", reverse: bool = False
1198 ) -> np.ndarray:
1199 """
1200 Roll arrays to match the different vertical axis.
1201
1202 Parameters
1203 ----------
1204 arr : ArrayLike
1205 Array to roll.
1206 reverse : bool, default: False
1207 Reverse the direction of the roll.
1208 """
1209 if reverse:
1210 return np.roll(arr, (self._vertical_axis - 2) * -1)
1211 else:
1212 return np.roll(arr, (self._vertical_axis - 2))
1213
1214 def get_proj(self):
1215 """Create the projection matrix from the current viewing position."""
1216
1217 # Transform to uniform world coordinates 0-1, 0-1, 0-1
1218 box_aspect = self._roll_to_vertical(self._box_aspect)
1219 worldM = proj3d.world_transformation(
1220 *self.get_xlim3d(),
1221 *self.get_ylim3d(),
1222 *self.get_zlim3d(),
1223 pb_aspect=box_aspect,
1224 )
1225
1226 # Look into the middle of the world coordinates:
1227 R = 0.5 * box_aspect
1228
1229 # elev: elevation angle in the z plane.
1230 # azim: azimuth angle in the xy plane.
1231 # Coordinates for a point that rotates around the box of data.
1232 # p0, p1 corresponds to rotating the box only around the vertical axis.
1233 # p2 corresponds to rotating the box only around the horizontal axis.
1234 elev_rad = np.deg2rad(self.elev)
1235 azim_rad = np.deg2rad(self.azim)
1236 p0 = np.cos(elev_rad) * np.cos(azim_rad)
1237 p1 = np.cos(elev_rad) * np.sin(azim_rad)
1238 p2 = np.sin(elev_rad)
1239
1240 # When changing vertical axis the coordinates changes as well.
1241 # Roll the values to get the same behaviour as the default:
1242 ps = self._roll_to_vertical([p0, p1, p2])
1243
1244 # The coordinates for the eye viewing point. The eye is looking
1245 # towards the middle of the box of data from a distance:
1246 eye = R + self._dist * ps
1247
1248 # Calculate the viewing axes for the eye position
1249 u, v, w = self._calc_view_axes(eye)
1250 self._view_u = u # _view_u is towards the right of the screen
1251 self._view_v = v # _view_v is towards the top of the screen
1252 self._view_w = w # _view_w is out of the screen
1253
1254 # Generate the view and projection transformation matrices
1255 if self._focal_length == np.inf:
1256 # Orthographic projection
1257 viewM = proj3d._view_transformation_uvw(u, v, w, eye)
1258 projM = proj3d._ortho_transformation(-self._dist, self._dist)
1259 else:
1260 # Perspective projection
1261 # Scale the eye dist to compensate for the focal length zoom effect
1262 eye_focal = R + self._dist * ps * self._focal_length
1263 viewM = proj3d._view_transformation_uvw(u, v, w, eye_focal)
1264 projM = proj3d._persp_transformation(-self._dist,
1265 self._dist,
1266 self._focal_length)
1267
1268 # Combine all the transformation matrices to get the final projection
1269 M0 = np.dot(viewM, worldM)
1270 M = np.dot(projM, M0)
1271 return M
1272
1273 def mouse_init(self, rotate_btn=1, pan_btn=2, zoom_btn=3):
1274 """
1275 Set the mouse buttons for 3D rotation and zooming.
1276
1277 Parameters
1278 ----------
1279 rotate_btn : int or list of int, default: 1
1280 The mouse button or buttons to use for 3D rotation of the Axes.
1281 pan_btn : int or list of int, default: 2
1282 The mouse button or buttons to use to pan the 3D Axes.
1283 zoom_btn : int or list of int, default: 3
1284 The mouse button or buttons to use to zoom the 3D Axes.
1285 """
1286 self.button_pressed = None
1287 # coerce scalars into array-like, then convert into
1288 # a regular list to avoid comparisons against None
1289 # which breaks in recent versions of numpy.
1290 self._rotate_btn = np.atleast_1d(rotate_btn).tolist()
1291 self._pan_btn = np.atleast_1d(pan_btn).tolist()
1292 self._zoom_btn = np.atleast_1d(zoom_btn).tolist()
1293
1294 def disable_mouse_rotation(self):
1295 """Disable mouse buttons for 3D rotation, panning, and zooming."""
1296 self.mouse_init(rotate_btn=[], pan_btn=[], zoom_btn=[])
1297
1298 def can_zoom(self):
1299 # doc-string inherited
1300 return True
1301
1302 def can_pan(self):
1303 # doc-string inherited
1304 return True
1305
1306 def sharez(self, other):
1307 """
1308 Share the z-axis with *other*.
1309
1310 This is equivalent to passing ``sharez=other`` when constructing the
1311 Axes, and cannot be used if the z-axis is already being shared with
1312 another Axes. Note that it is not possible to unshare axes.
1313 """
1314 _api.check_isinstance(Axes3D, other=other)
1315 if self._sharez is not None and other is not self._sharez:
1316 raise ValueError("z-axis is already shared")
1317 self._shared_axes["z"].join(self, other)
1318 self._sharez = other
1319 self.zaxis.major = other.zaxis.major # Ticker instances holding
1320 self.zaxis.minor = other.zaxis.minor # locator and formatter.
1321 z0, z1 = other.get_zlim()
1322 self.set_zlim(z0, z1, emit=False, auto=other.get_autoscalez_on())
1323 self.zaxis._scale = other.zaxis._scale
1324
1325 def shareview(self, other):
1326 """
1327 Share the view angles with *other*.
1328
1329 This is equivalent to passing ``shareview=other`` when constructing the
1330 Axes, and cannot be used if the view angles are already being shared
1331 with another Axes. Note that it is not possible to unshare axes.
1332 """
1333 _api.check_isinstance(Axes3D, other=other)
1334 if self._shareview is not None and other is not self._shareview:
1335 raise ValueError("view angles are already shared")
1336 self._shared_axes["view"].join(self, other)
1337 self._shareview = other
1338 vertical_axis = self._axis_names[other._vertical_axis]
1339 self.view_init(elev=other.elev, azim=other.azim, roll=other.roll,
1340 vertical_axis=vertical_axis, share=True)
1341
1342 def clear(self):
1343 # docstring inherited.
1344 super().clear()
1345 if self._focal_length == np.inf:
1346 self._zmargin = mpl.rcParams['axes.zmargin']
1347 else:
1348 self._zmargin = 0.
1349
1350 xymargin = 0.05 * 10/11 # match mpl3.8 appearance
1351 self.xy_dataLim = Bbox([[xymargin, xymargin],
1352 [1 - xymargin, 1 - xymargin]])
1353 # z-limits are encoded in the x-component of the Bbox, y is un-used
1354 self.zz_dataLim = Bbox.unit()
1355 self._view_margin = 1/48 # default value to match mpl3.8
1356 self.autoscale_view()
1357
1358 self.grid(mpl.rcParams['axes3d.grid'])
1359
1360 def _button_press(self, event):
1361 if event.inaxes == self:
1362 self.button_pressed = event.button
1363 self._sx, self._sy = event.xdata, event.ydata
1364 toolbar = self.figure.canvas.toolbar
1365 if toolbar and toolbar._nav_stack() is None:
1366 toolbar.push_current()
1367
1368 def _button_release(self, event):
1369 self.button_pressed = None
1370 toolbar = self.figure.canvas.toolbar
1371 # backend_bases.release_zoom and backend_bases.release_pan call
1372 # push_current, so check the navigation mode so we don't call it twice
1373 if toolbar and self.get_navigate_mode() is None:
1374 toolbar.push_current()
1375
1376 def _get_view(self):
1377 # docstring inherited
1378 return {
1379 "xlim": self.get_xlim(), "autoscalex_on": self.get_autoscalex_on(),
1380 "ylim": self.get_ylim(), "autoscaley_on": self.get_autoscaley_on(),
1381 "zlim": self.get_zlim(), "autoscalez_on": self.get_autoscalez_on(),
1382 }, (self.elev, self.azim, self.roll)
1383
1384 def _set_view(self, view):
1385 # docstring inherited
1386 props, (elev, azim, roll) = view
1387 self.set(**props)
1388 self.elev = elev
1389 self.azim = azim
1390 self.roll = roll
1391
1392 def format_zdata(self, z):
1393 """
1394 Return *z* string formatted. This function will use the
1395 :attr:`fmt_zdata` attribute if it is callable, else will fall
1396 back on the zaxis major formatter
1397 """
1398 try:
1399 return self.fmt_zdata(z)
1400 except (AttributeError, TypeError):
1401 func = self.zaxis.get_major_formatter().format_data_short
1402 val = func(z)
1403 return val
1404
1405 def format_coord(self, xv, yv, renderer=None):
1406 """
1407 Return a string giving the current view rotation angles, or the x, y, z
1408 coordinates of the point on the nearest axis pane underneath the mouse
1409 cursor, depending on the mouse button pressed.
1410 """
1411 coords = ''
1412
1413 if self.button_pressed in self._rotate_btn:
1414 # ignore xv and yv and display angles instead
1415 coords = self._rotation_coords()
1416
1417 elif self.M is not None:
1418 coords = self._location_coords(xv, yv, renderer)
1419
1420 return coords
1421
1422 def _rotation_coords(self):
1423 """
1424 Return the rotation angles as a string.
1425 """
1426 norm_elev = art3d._norm_angle(self.elev)
1427 norm_azim = art3d._norm_angle(self.azim)
1428 norm_roll = art3d._norm_angle(self.roll)
1429 coords = (f"elevation={norm_elev:.0f}\N{DEGREE SIGN}, "
1430 f"azimuth={norm_azim:.0f}\N{DEGREE SIGN}, "
1431 f"roll={norm_roll:.0f}\N{DEGREE SIGN}"
1432 ).replace("-", "\N{MINUS SIGN}")
1433 return coords
1434
1435 def _location_coords(self, xv, yv, renderer):
1436 """
1437 Return the location on the axis pane underneath the cursor as a string.
1438 """
1439 p1, pane_idx = self._calc_coord(xv, yv, renderer)
1440 xs = self.format_xdata(p1[0])
1441 ys = self.format_ydata(p1[1])
1442 zs = self.format_zdata(p1[2])
1443 if pane_idx == 0:
1444 coords = f'x pane={xs}, y={ys}, z={zs}'
1445 elif pane_idx == 1:
1446 coords = f'x={xs}, y pane={ys}, z={zs}'
1447 elif pane_idx == 2:
1448 coords = f'x={xs}, y={ys}, z pane={zs}'
1449 return coords
1450
1451 def _get_camera_loc(self):
1452 """
1453 Returns the current camera location in data coordinates.
1454 """
1455 cx, cy, cz, dx, dy, dz = self._get_w_centers_ranges()
1456 c = np.array([cx, cy, cz])
1457 r = np.array([dx, dy, dz])
1458
1459 if self._focal_length == np.inf: # orthographic projection
1460 focal_length = 1e9 # large enough to be effectively infinite
1461 else: # perspective projection
1462 focal_length = self._focal_length
1463 eye = c + self._view_w * self._dist * r / self._box_aspect * focal_length
1464 return eye
1465
1466 def _calc_coord(self, xv, yv, renderer=None):
1467 """
1468 Given the 2D view coordinates, find the point on the nearest axis pane
1469 that lies directly below those coordinates. Returns a 3D point in data
1470 coordinates.
1471 """
1472 if self._focal_length == np.inf: # orthographic projection
1473 zv = 1
1474 else: # perspective projection
1475 zv = -1 / self._focal_length
1476
1477 # Convert point on view plane to data coordinates
1478 p1 = np.array(proj3d.inv_transform(xv, yv, zv, self.invM)).ravel()
1479
1480 # Get the vector from the camera to the point on the view plane
1481 vec = self._get_camera_loc() - p1
1482
1483 # Get the pane locations for each of the axes
1484 pane_locs = []
1485 for axis in self._axis_map.values():
1486 xys, loc = axis.active_pane()
1487 pane_locs.append(loc)
1488
1489 # Find the distance to the nearest pane by projecting the view vector
1490 scales = np.zeros(3)
1491 for i in range(3):
1492 if vec[i] == 0:
1493 scales[i] = np.inf
1494 else:
1495 scales[i] = (p1[i] - pane_locs[i]) / vec[i]
1496 pane_idx = np.argmin(abs(scales))
1497 scale = scales[pane_idx]
1498
1499 # Calculate the point on the closest pane
1500 p2 = p1 - scale*vec
1501 return p2, pane_idx
1502
1503 def _on_move(self, event):
1504 """
1505 Mouse moving.
1506
1507 By default, button-1 rotates, button-2 pans, and button-3 zooms;
1508 these buttons can be modified via `mouse_init`.
1509 """
1510
1511 if not self.button_pressed:
1512 return
1513
1514 if self.get_navigate_mode() is not None:
1515 # we don't want to rotate if we are zooming/panning
1516 # from the toolbar
1517 return
1518
1519 if self.M is None:
1520 return
1521
1522 x, y = event.xdata, event.ydata
1523 # In case the mouse is out of bounds.
1524 if x is None or event.inaxes != self:
1525 return
1526
1527 dx, dy = x - self._sx, y - self._sy
1528 w = self._pseudo_w
1529 h = self._pseudo_h
1530
1531 # Rotation
1532 if self.button_pressed in self._rotate_btn:
1533 # rotate viewing point
1534 # get the x and y pixel coords
1535 if dx == 0 and dy == 0:
1536 return
1537
1538 roll = np.deg2rad(self.roll)
1539 delev = -(dy/h)*180*np.cos(roll) + (dx/w)*180*np.sin(roll)
1540 dazim = -(dy/h)*180*np.sin(roll) - (dx/w)*180*np.cos(roll)
1541 elev = self.elev + delev
1542 azim = self.azim + dazim
1543 roll = self.roll
1544 vertical_axis = self._axis_names[self._vertical_axis]
1545 self.view_init(
1546 elev=elev,
1547 azim=azim,
1548 roll=roll,
1549 vertical_axis=vertical_axis,
1550 share=True,
1551 )
1552 self.stale = True
1553
1554 # Pan
1555 elif self.button_pressed in self._pan_btn:
1556 # Start the pan event with pixel coordinates
1557 px, py = self.transData.transform([self._sx, self._sy])
1558 self.start_pan(px, py, 2)
1559 # pan view (takes pixel coordinate input)
1560 self.drag_pan(2, None, event.x, event.y)
1561 self.end_pan()
1562
1563 # Zoom
1564 elif self.button_pressed in self._zoom_btn:
1565 # zoom view (dragging down zooms in)
1566 scale = h/(h - dy)
1567 self._scale_axis_limits(scale, scale, scale)
1568
1569 # Store the event coordinates for the next time through.
1570 self._sx, self._sy = x, y
1571 # Always request a draw update at the end of interaction
1572 self.figure.canvas.draw_idle()
1573
1574 def drag_pan(self, button, key, x, y):
1575 # docstring inherited
1576
1577 # Get the coordinates from the move event
1578 p = self._pan_start
1579 (xdata, ydata), (xdata_start, ydata_start) = p.trans_inverse.transform(
1580 [(x, y), (p.x, p.y)])
1581 self._sx, self._sy = xdata, ydata
1582 # Calling start_pan() to set the x/y of this event as the starting
1583 # move location for the next event
1584 self.start_pan(x, y, button)
1585 du, dv = xdata - xdata_start, ydata - ydata_start
1586 dw = 0
1587 if key == 'x':
1588 dv = 0
1589 elif key == 'y':
1590 du = 0
1591 if du == 0 and dv == 0:
1592 return
1593
1594 # Transform the pan from the view axes to the data axes
1595 R = np.array([self._view_u, self._view_v, self._view_w])
1596 R = -R / self._box_aspect * self._dist
1597 duvw_projected = R.T @ np.array([du, dv, dw])
1598
1599 # Calculate pan distance
1600 minx, maxx, miny, maxy, minz, maxz = self.get_w_lims()
1601 dx = (maxx - minx) * duvw_projected[0]
1602 dy = (maxy - miny) * duvw_projected[1]
1603 dz = (maxz - minz) * duvw_projected[2]
1604
1605 # Set the new axis limits
1606 self.set_xlim3d(minx + dx, maxx + dx, auto=None)
1607 self.set_ylim3d(miny + dy, maxy + dy, auto=None)
1608 self.set_zlim3d(minz + dz, maxz + dz, auto=None)
1609
1610 def _calc_view_axes(self, eye):
1611 """
1612 Get the unit vectors for the viewing axes in data coordinates.
1613 `u` is towards the right of the screen
1614 `v` is towards the top of the screen
1615 `w` is out of the screen
1616 """
1617 elev_rad = np.deg2rad(art3d._norm_angle(self.elev))
1618 roll_rad = np.deg2rad(art3d._norm_angle(self.roll))
1619
1620 # Look into the middle of the world coordinates
1621 R = 0.5 * self._roll_to_vertical(self._box_aspect)
1622
1623 # Define which axis should be vertical. A negative value
1624 # indicates the plot is upside down and therefore the values
1625 # have been reversed:
1626 V = np.zeros(3)
1627 V[self._vertical_axis] = -1 if abs(elev_rad) > np.pi/2 else 1
1628
1629 u, v, w = proj3d._view_axes(eye, R, V, roll_rad)
1630 return u, v, w
1631
1632 def _set_view_from_bbox(self, bbox, direction='in',
1633 mode=None, twinx=False, twiny=False):
1634 """
1635 Zoom in or out of the bounding box.
1636
1637 Will center the view in the center of the bounding box, and zoom by
1638 the ratio of the size of the bounding box to the size of the Axes3D.
1639 """
1640 (start_x, start_y, stop_x, stop_y) = bbox
1641 if mode == 'x':
1642 start_y = self.bbox.min[1]
1643 stop_y = self.bbox.max[1]
1644 elif mode == 'y':
1645 start_x = self.bbox.min[0]
1646 stop_x = self.bbox.max[0]
1647
1648 # Clip to bounding box limits
1649 start_x, stop_x = np.clip(sorted([start_x, stop_x]),
1650 self.bbox.min[0], self.bbox.max[0])
1651 start_y, stop_y = np.clip(sorted([start_y, stop_y]),
1652 self.bbox.min[1], self.bbox.max[1])
1653
1654 # Move the center of the view to the center of the bbox
1655 zoom_center_x = (start_x + stop_x)/2
1656 zoom_center_y = (start_y + stop_y)/2
1657
1658 ax_center_x = (self.bbox.max[0] + self.bbox.min[0])/2
1659 ax_center_y = (self.bbox.max[1] + self.bbox.min[1])/2
1660
1661 self.start_pan(zoom_center_x, zoom_center_y, 2)
1662 self.drag_pan(2, None, ax_center_x, ax_center_y)
1663 self.end_pan()
1664
1665 # Calculate zoom level
1666 dx = abs(start_x - stop_x)
1667 dy = abs(start_y - stop_y)
1668 scale_u = dx / (self.bbox.max[0] - self.bbox.min[0])
1669 scale_v = dy / (self.bbox.max[1] - self.bbox.min[1])
1670
1671 # Keep aspect ratios equal
1672 scale = max(scale_u, scale_v)
1673
1674 # Zoom out
1675 if direction == 'out':
1676 scale = 1 / scale
1677
1678 self._zoom_data_limits(scale, scale, scale)
1679
1680 def _zoom_data_limits(self, scale_u, scale_v, scale_w):
1681 """
1682 Zoom in or out of a 3D plot.
1683
1684 Will scale the data limits by the scale factors. These will be
1685 transformed to the x, y, z data axes based on the current view angles.
1686 A scale factor > 1 zooms out and a scale factor < 1 zooms in.
1687
1688 For an Axes that has had its aspect ratio set to 'equal', 'equalxy',
1689 'equalyz', or 'equalxz', the relevant axes are constrained to zoom
1690 equally.
1691
1692 Parameters
1693 ----------
1694 scale_u : float
1695 Scale factor for the u view axis (view screen horizontal).
1696 scale_v : float
1697 Scale factor for the v view axis (view screen vertical).
1698 scale_w : float
1699 Scale factor for the w view axis (view screen depth).
1700 """
1701 scale = np.array([scale_u, scale_v, scale_w])
1702
1703 # Only perform frame conversion if unequal scale factors
1704 if not np.allclose(scale, scale_u):
1705 # Convert the scale factors from the view frame to the data frame
1706 R = np.array([self._view_u, self._view_v, self._view_w])
1707 S = scale * np.eye(3)
1708 scale = np.linalg.norm(R.T @ S, axis=1)
1709
1710 # Set the constrained scale factors to the factor closest to 1
1711 if self._aspect in ('equal', 'equalxy', 'equalxz', 'equalyz'):
1712 ax_idxs = self._equal_aspect_axis_indices(self._aspect)
1713 min_ax_idxs = np.argmin(np.abs(scale[ax_idxs] - 1))
1714 scale[ax_idxs] = scale[ax_idxs][min_ax_idxs]
1715
1716 self._scale_axis_limits(scale[0], scale[1], scale[2])
1717
1718 def _scale_axis_limits(self, scale_x, scale_y, scale_z):
1719 """
1720 Keeping the center of the x, y, and z data axes fixed, scale their
1721 limits by scale factors. A scale factor > 1 zooms out and a scale
1722 factor < 1 zooms in.
1723
1724 Parameters
1725 ----------
1726 scale_x : float
1727 Scale factor for the x data axis.
1728 scale_y : float
1729 Scale factor for the y data axis.
1730 scale_z : float
1731 Scale factor for the z data axis.
1732 """
1733 # Get the axis centers and ranges
1734 cx, cy, cz, dx, dy, dz = self._get_w_centers_ranges()
1735
1736 # Set the scaled axis limits
1737 self.set_xlim3d(cx - dx*scale_x/2, cx + dx*scale_x/2, auto=None)
1738 self.set_ylim3d(cy - dy*scale_y/2, cy + dy*scale_y/2, auto=None)
1739 self.set_zlim3d(cz - dz*scale_z/2, cz + dz*scale_z/2, auto=None)
1740
1741 def _get_w_centers_ranges(self):
1742 """Get 3D world centers and axis ranges."""
1743 # Calculate center of axis limits
1744 minx, maxx, miny, maxy, minz, maxz = self.get_w_lims()
1745 cx = (maxx + minx)/2
1746 cy = (maxy + miny)/2
1747 cz = (maxz + minz)/2
1748
1749 # Calculate range of axis limits
1750 dx = (maxx - minx)
1751 dy = (maxy - miny)
1752 dz = (maxz - minz)
1753 return cx, cy, cz, dx, dy, dz
1754
1755 def set_zlabel(self, zlabel, fontdict=None, labelpad=None, **kwargs):
1756 """
1757 Set zlabel. See doc for `.set_ylabel` for description.
1758 """
1759 if labelpad is not None:
1760 self.zaxis.labelpad = labelpad
1761 return self.zaxis.set_label_text(zlabel, fontdict, **kwargs)
1762
1763 def get_zlabel(self):
1764 """
1765 Get the z-label text string.
1766 """
1767 label = self.zaxis.get_label()
1768 return label.get_text()
1769
1770 # Axes rectangle characteristics
1771
1772 # The frame_on methods are not available for 3D axes.
1773 # Python will raise a TypeError if they are called.
1774 get_frame_on = None
1775 set_frame_on = None
1776
1777 def grid(self, visible=True, **kwargs):
1778 """
1779 Set / unset 3D grid.
1780
1781 .. note::
1782
1783 Currently, this function does not behave the same as
1784 `.axes.Axes.grid`, but it is intended to eventually support that
1785 behavior.
1786 """
1787 # TODO: Operate on each axes separately
1788 if len(kwargs):
1789 visible = True
1790 self._draw_grid = visible
1791 self.stale = True
1792
1793 def tick_params(self, axis='both', **kwargs):
1794 """
1795 Convenience method for changing the appearance of ticks and
1796 tick labels.
1797
1798 See `.Axes.tick_params` for full documentation. Because this function
1799 applies to 3D Axes, *axis* can also be set to 'z', and setting *axis*
1800 to 'both' autoscales all three axes.
1801
1802 Also, because of how Axes3D objects are drawn very differently
1803 from regular 2D Axes, some of these settings may have
1804 ambiguous meaning. For simplicity, the 'z' axis will
1805 accept settings as if it was like the 'y' axis.
1806
1807 .. note::
1808 Axes3D currently ignores some of these settings.
1809 """
1810 _api.check_in_list(['x', 'y', 'z', 'both'], axis=axis)
1811 if axis in ['x', 'y', 'both']:
1812 super().tick_params(axis, **kwargs)
1813 if axis in ['z', 'both']:
1814 zkw = dict(kwargs)
1815 zkw.pop('top', None)
1816 zkw.pop('bottom', None)
1817 zkw.pop('labeltop', None)
1818 zkw.pop('labelbottom', None)
1819 self.zaxis.set_tick_params(**zkw)
1820
1821 # data limits, ticks, tick labels, and formatting
1822
1823 def invert_zaxis(self):
1824 """
1825 Invert the z-axis.
1826
1827 See Also
1828 --------
1829 zaxis_inverted
1830 get_zlim, set_zlim
1831 get_zbound, set_zbound
1832 """
1833 bottom, top = self.get_zlim()
1834 self.set_zlim(top, bottom, auto=None)
1835
1836 zaxis_inverted = _axis_method_wrapper("zaxis", "get_inverted")
1837
1838 def get_zbound(self):
1839 """
1840 Return the lower and upper z-axis bounds, in increasing order.
1841
1842 See Also
1843 --------
1844 set_zbound
1845 get_zlim, set_zlim
1846 invert_zaxis, zaxis_inverted
1847 """
1848 lower, upper = self.get_zlim()
1849 if lower < upper:
1850 return lower, upper
1851 else:
1852 return upper, lower
1853
1854 def text(self, x, y, z, s, zdir=None, **kwargs):
1855 """
1856 Add the text *s* to the 3D Axes at location *x*, *y*, *z* in data coordinates.
1857
1858 Parameters
1859 ----------
1860 x, y, z : float
1861 The position to place the text.
1862 s : str
1863 The text.
1864 zdir : {'x', 'y', 'z', 3-tuple}, optional
1865 The direction to be used as the z-direction. Default: 'z'.
1866 See `.get_dir_vector` for a description of the values.
1867 **kwargs
1868 Other arguments are forwarded to `matplotlib.axes.Axes.text`.
1869
1870 Returns
1871 -------
1872 `.Text3D`
1873 The created `.Text3D` instance.
1874 """
1875 text = super().text(x, y, s, **kwargs)
1876 art3d.text_2d_to_3d(text, z, zdir)
1877 return text
1878
1879 text3D = text
1880 text2D = Axes.text
1881
1882 def plot(self, xs, ys, *args, zdir='z', **kwargs):
1883 """
1884 Plot 2D or 3D data.
1885
1886 Parameters
1887 ----------
1888 xs : 1D array-like
1889 x coordinates of vertices.
1890 ys : 1D array-like
1891 y coordinates of vertices.
1892 zs : float or 1D array-like
1893 z coordinates of vertices; either one for all points or one for
1894 each point.
1895 zdir : {'x', 'y', 'z'}, default: 'z'
1896 When plotting 2D data, the direction to use as z.
1897 **kwargs
1898 Other arguments are forwarded to `matplotlib.axes.Axes.plot`.
1899 """
1900 had_data = self.has_data()
1901
1902 # `zs` can be passed positionally or as keyword; checking whether
1903 # args[0] is a string matches the behavior of 2D `plot` (via
1904 # `_process_plot_var_args`).
1905 if args and not isinstance(args[0], str):
1906 zs, *args = args
1907 if 'zs' in kwargs:
1908 raise TypeError("plot() for multiple values for argument 'zs'")
1909 else:
1910 zs = kwargs.pop('zs', 0)
1911
1912 xs, ys, zs = cbook._broadcast_with_masks(xs, ys, zs)
1913
1914 lines = super().plot(xs, ys, *args, **kwargs)
1915 for line in lines:
1916 art3d.line_2d_to_3d(line, zs=zs, zdir=zdir)
1917
1918 xs, ys, zs = art3d.juggle_axes(xs, ys, zs, zdir)
1919 self.auto_scale_xyz(xs, ys, zs, had_data)
1920 return lines
1921
1922 plot3D = plot
1923
1924 def plot_surface(self, X, Y, Z, *, norm=None, vmin=None,
1925 vmax=None, lightsource=None, **kwargs):
1926 """
1927 Create a surface plot.
1928
1929 By default, it will be colored in shades of a solid color, but it also
1930 supports colormapping by supplying the *cmap* argument.
1931
1932 .. note::
1933
1934 The *rcount* and *ccount* kwargs, which both default to 50,
1935 determine the maximum number of samples used in each direction. If
1936 the input data is larger, it will be downsampled (by slicing) to
1937 these numbers of points.
1938
1939 .. note::
1940
1941 To maximize rendering speed consider setting *rstride* and *cstride*
1942 to divisors of the number of rows minus 1 and columns minus 1
1943 respectively. For example, given 51 rows rstride can be any of the
1944 divisors of 50.
1945
1946 Similarly, a setting of *rstride* and *cstride* equal to 1 (or
1947 *rcount* and *ccount* equal the number of rows and columns) can use
1948 the optimized path.
1949
1950 Parameters
1951 ----------
1952 X, Y, Z : 2D arrays
1953 Data values.
1954
1955 rcount, ccount : int
1956 Maximum number of samples used in each direction. If the input
1957 data is larger, it will be downsampled (by slicing) to these
1958 numbers of points. Defaults to 50.
1959
1960 rstride, cstride : int
1961 Downsampling stride in each direction. These arguments are
1962 mutually exclusive with *rcount* and *ccount*. If only one of
1963 *rstride* or *cstride* is set, the other defaults to 10.
1964
1965 'classic' mode uses a default of ``rstride = cstride = 10`` instead
1966 of the new default of ``rcount = ccount = 50``.
1967
1968 color : :mpltype:`color`
1969 Color of the surface patches.
1970
1971 cmap : Colormap, optional
1972 Colormap of the surface patches.
1973
1974 facecolors : list of :mpltype:`color`
1975 Colors of each individual patch.
1976
1977 norm : `~matplotlib.colors.Normalize`, optional
1978 Normalization for the colormap.
1979
1980 vmin, vmax : float, optional
1981 Bounds for the normalization.
1982
1983 shade : bool, default: True
1984 Whether to shade the facecolors. Shading is always disabled when
1985 *cmap* is specified.
1986
1987 lightsource : `~matplotlib.colors.LightSource`, optional
1988 The lightsource to use when *shade* is True.
1989
1990 **kwargs
1991 Other keyword arguments are forwarded to `.Poly3DCollection`.
1992 """
1993
1994 had_data = self.has_data()
1995
1996 if Z.ndim != 2:
1997 raise ValueError("Argument Z must be 2-dimensional.")
1998
1999 Z = cbook._to_unmasked_float_array(Z)
2000 X, Y, Z = np.broadcast_arrays(X, Y, Z)
2001 rows, cols = Z.shape
2002
2003 has_stride = 'rstride' in kwargs or 'cstride' in kwargs
2004 has_count = 'rcount' in kwargs or 'ccount' in kwargs
2005
2006 if has_stride and has_count:
2007 raise ValueError("Cannot specify both stride and count arguments")
2008
2009 rstride = kwargs.pop('rstride', 10)
2010 cstride = kwargs.pop('cstride', 10)
2011 rcount = kwargs.pop('rcount', 50)
2012 ccount = kwargs.pop('ccount', 50)
2013
2014 if mpl.rcParams['_internal.classic_mode']:
2015 # Strides have priority over counts in classic mode.
2016 # So, only compute strides from counts
2017 # if counts were explicitly given
2018 compute_strides = has_count
2019 else:
2020 # If the strides are provided then it has priority.
2021 # Otherwise, compute the strides from the counts.
2022 compute_strides = not has_stride
2023
2024 if compute_strides:
2025 rstride = int(max(np.ceil(rows / rcount), 1))
2026 cstride = int(max(np.ceil(cols / ccount), 1))
2027
2028 fcolors = kwargs.pop('facecolors', None)
2029
2030 cmap = kwargs.get('cmap', None)
2031 shade = kwargs.pop('shade', cmap is None)
2032 if shade is None:
2033 raise ValueError("shade cannot be None.")
2034
2035 colset = [] # the sampled facecolor
2036 if (rows - 1) % rstride == 0 and \
2037 (cols - 1) % cstride == 0 and \
2038 fcolors is None:
2039 polys = np.stack(
2040 [cbook._array_patch_perimeters(a, rstride, cstride)
2041 for a in (X, Y, Z)],
2042 axis=-1)
2043 else:
2044 # evenly spaced, and including both endpoints
2045 row_inds = list(range(0, rows-1, rstride)) + [rows-1]
2046 col_inds = list(range(0, cols-1, cstride)) + [cols-1]
2047
2048 polys = []
2049 for rs, rs_next in zip(row_inds[:-1], row_inds[1:]):
2050 for cs, cs_next in zip(col_inds[:-1], col_inds[1:]):
2051 ps = [
2052 # +1 ensures we share edges between polygons
2053 cbook._array_perimeter(a[rs:rs_next+1, cs:cs_next+1])
2054 for a in (X, Y, Z)
2055 ]
2056 # ps = np.stack(ps, axis=-1)
2057 ps = np.array(ps).T
2058 polys.append(ps)
2059
2060 if fcolors is not None:
2061 colset.append(fcolors[rs][cs])
2062
2063 # In cases where there are non-finite values in the data (possibly NaNs from
2064 # masked arrays), artifacts can be introduced. Here check whether such values
2065 # are present and remove them.
2066 if not isinstance(polys, np.ndarray) or not np.isfinite(polys).all():
2067 new_polys = []
2068 new_colset = []
2069
2070 # Depending on fcolors, colset is either an empty list or has as
2071 # many elements as polys. In the former case new_colset results in
2072 # a list with None entries, that is discarded later.
2073 for p, col in itertools.zip_longest(polys, colset):
2074 new_poly = np.array(p)[np.isfinite(p).all(axis=1)]
2075 if len(new_poly):
2076 new_polys.append(new_poly)
2077 new_colset.append(col)
2078
2079 # Replace previous polys and, if fcolors is not None, colset
2080 polys = new_polys
2081 if fcolors is not None:
2082 colset = new_colset
2083
2084 # note that the striding causes some polygons to have more coordinates
2085 # than others
2086
2087 if fcolors is not None:
2088 polyc = art3d.Poly3DCollection(
2089 polys, edgecolors=colset, facecolors=colset, shade=shade,
2090 lightsource=lightsource, **kwargs)
2091 elif cmap:
2092 polyc = art3d.Poly3DCollection(polys, **kwargs)
2093 # can't always vectorize, because polys might be jagged
2094 if isinstance(polys, np.ndarray):
2095 avg_z = polys[..., 2].mean(axis=-1)
2096 else:
2097 avg_z = np.array([ps[:, 2].mean() for ps in polys])
2098 polyc.set_array(avg_z)
2099 if vmin is not None or vmax is not None:
2100 polyc.set_clim(vmin, vmax)
2101 if norm is not None:
2102 polyc.set_norm(norm)
2103 else:
2104 color = kwargs.pop('color', None)
2105 if color is None:
2106 color = self._get_lines.get_next_color()
2107 color = np.array(mcolors.to_rgba(color))
2108
2109 polyc = art3d.Poly3DCollection(
2110 polys, facecolors=color, shade=shade,
2111 lightsource=lightsource, **kwargs)
2112
2113 self.add_collection(polyc)
2114 self.auto_scale_xyz(X, Y, Z, had_data)
2115
2116 return polyc
2117
2118 def plot_wireframe(self, X, Y, Z, **kwargs):
2119 """
2120 Plot a 3D wireframe.
2121
2122 .. note::
2123
2124 The *rcount* and *ccount* kwargs, which both default to 50,
2125 determine the maximum number of samples used in each direction. If
2126 the input data is larger, it will be downsampled (by slicing) to
2127 these numbers of points.
2128
2129 Parameters
2130 ----------
2131 X, Y, Z : 2D arrays
2132 Data values.
2133
2134 rcount, ccount : int
2135 Maximum number of samples used in each direction. If the input
2136 data is larger, it will be downsampled (by slicing) to these
2137 numbers of points. Setting a count to zero causes the data to be
2138 not sampled in the corresponding direction, producing a 3D line
2139 plot rather than a wireframe plot. Defaults to 50.
2140
2141 rstride, cstride : int
2142 Downsampling stride in each direction. These arguments are
2143 mutually exclusive with *rcount* and *ccount*. If only one of
2144 *rstride* or *cstride* is set, the other defaults to 1. Setting a
2145 stride to zero causes the data to be not sampled in the
2146 corresponding direction, producing a 3D line plot rather than a
2147 wireframe plot.
2148
2149 'classic' mode uses a default of ``rstride = cstride = 1`` instead
2150 of the new default of ``rcount = ccount = 50``.
2151
2152 **kwargs
2153 Other keyword arguments are forwarded to `.Line3DCollection`.
2154 """
2155
2156 had_data = self.has_data()
2157 if Z.ndim != 2:
2158 raise ValueError("Argument Z must be 2-dimensional.")
2159 # FIXME: Support masked arrays
2160 X, Y, Z = np.broadcast_arrays(X, Y, Z)
2161 rows, cols = Z.shape
2162
2163 has_stride = 'rstride' in kwargs or 'cstride' in kwargs
2164 has_count = 'rcount' in kwargs or 'ccount' in kwargs
2165
2166 if has_stride and has_count:
2167 raise ValueError("Cannot specify both stride and count arguments")
2168
2169 rstride = kwargs.pop('rstride', 1)
2170 cstride = kwargs.pop('cstride', 1)
2171 rcount = kwargs.pop('rcount', 50)
2172 ccount = kwargs.pop('ccount', 50)
2173
2174 if mpl.rcParams['_internal.classic_mode']:
2175 # Strides have priority over counts in classic mode.
2176 # So, only compute strides from counts
2177 # if counts were explicitly given
2178 if has_count:
2179 rstride = int(max(np.ceil(rows / rcount), 1)) if rcount else 0
2180 cstride = int(max(np.ceil(cols / ccount), 1)) if ccount else 0
2181 else:
2182 # If the strides are provided then it has priority.
2183 # Otherwise, compute the strides from the counts.
2184 if not has_stride:
2185 rstride = int(max(np.ceil(rows / rcount), 1)) if rcount else 0
2186 cstride = int(max(np.ceil(cols / ccount), 1)) if ccount else 0
2187
2188 # We want two sets of lines, one running along the "rows" of
2189 # Z and another set of lines running along the "columns" of Z.
2190 # This transpose will make it easy to obtain the columns.
2191 tX, tY, tZ = np.transpose(X), np.transpose(Y), np.transpose(Z)
2192
2193 if rstride:
2194 rii = list(range(0, rows, rstride))
2195 # Add the last index only if needed
2196 if rows > 0 and rii[-1] != (rows - 1):
2197 rii += [rows-1]
2198 else:
2199 rii = []
2200 if cstride:
2201 cii = list(range(0, cols, cstride))
2202 # Add the last index only if needed
2203 if cols > 0 and cii[-1] != (cols - 1):
2204 cii += [cols-1]
2205 else:
2206 cii = []
2207
2208 if rstride == 0 and cstride == 0:
2209 raise ValueError("Either rstride or cstride must be non zero")
2210
2211 # If the inputs were empty, then just
2212 # reset everything.
2213 if Z.size == 0:
2214 rii = []
2215 cii = []
2216
2217 xlines = [X[i] for i in rii]
2218 ylines = [Y[i] for i in rii]
2219 zlines = [Z[i] for i in rii]
2220
2221 txlines = [tX[i] for i in cii]
2222 tylines = [tY[i] for i in cii]
2223 tzlines = [tZ[i] for i in cii]
2224
2225 lines = ([list(zip(xl, yl, zl))
2226 for xl, yl, zl in zip(xlines, ylines, zlines)]
2227 + [list(zip(xl, yl, zl))
2228 for xl, yl, zl in zip(txlines, tylines, tzlines)])
2229
2230 linec = art3d.Line3DCollection(lines, **kwargs)
2231 self.add_collection(linec)
2232 self.auto_scale_xyz(X, Y, Z, had_data)
2233
2234 return linec
2235
2236 def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None,
2237 lightsource=None, **kwargs):
2238 """
2239 Plot a triangulated surface.
2240
2241 The (optional) triangulation can be specified in one of two ways;
2242 either::
2243
2244 plot_trisurf(triangulation, ...)
2245
2246 where triangulation is a `~matplotlib.tri.Triangulation` object, or::
2247
2248 plot_trisurf(X, Y, ...)
2249 plot_trisurf(X, Y, triangles, ...)
2250 plot_trisurf(X, Y, triangles=triangles, ...)
2251
2252 in which case a Triangulation object will be created. See
2253 `.Triangulation` for an explanation of these possibilities.
2254
2255 The remaining arguments are::
2256
2257 plot_trisurf(..., Z)
2258
2259 where *Z* is the array of values to contour, one per point
2260 in the triangulation.
2261
2262 Parameters
2263 ----------
2264 X, Y, Z : array-like
2265 Data values as 1D arrays.
2266 color
2267 Color of the surface patches.
2268 cmap
2269 A colormap for the surface patches.
2270 norm : `~matplotlib.colors.Normalize`, optional
2271 An instance of Normalize to map values to colors.
2272 vmin, vmax : float, optional
2273 Minimum and maximum value to map.
2274 shade : bool, default: True
2275 Whether to shade the facecolors. Shading is always disabled when
2276 *cmap* is specified.
2277 lightsource : `~matplotlib.colors.LightSource`, optional
2278 The lightsource to use when *shade* is True.
2279 **kwargs
2280 All other keyword arguments are passed on to
2281 :class:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection`
2282
2283 Examples
2284 --------
2285 .. plot:: gallery/mplot3d/trisurf3d.py
2286 .. plot:: gallery/mplot3d/trisurf3d_2.py
2287 """
2288
2289 had_data = self.has_data()
2290
2291 # TODO: Support custom face colours
2292 if color is None:
2293 color = self._get_lines.get_next_color()
2294 color = np.array(mcolors.to_rgba(color))
2295
2296 cmap = kwargs.get('cmap', None)
2297 shade = kwargs.pop('shade', cmap is None)
2298
2299 tri, args, kwargs = \
2300 Triangulation.get_from_args_and_kwargs(*args, **kwargs)
2301 try:
2302 z = kwargs.pop('Z')
2303 except KeyError:
2304 # We do this so Z doesn't get passed as an arg to PolyCollection
2305 z, *args = args
2306 z = np.asarray(z)
2307
2308 triangles = tri.get_masked_triangles()
2309 xt = tri.x[triangles]
2310 yt = tri.y[triangles]
2311 zt = z[triangles]
2312 verts = np.stack((xt, yt, zt), axis=-1)
2313
2314 if cmap:
2315 polyc = art3d.Poly3DCollection(verts, *args, **kwargs)
2316 # average over the three points of each triangle
2317 avg_z = verts[:, :, 2].mean(axis=1)
2318 polyc.set_array(avg_z)
2319 if vmin is not None or vmax is not None:
2320 polyc.set_clim(vmin, vmax)
2321 if norm is not None:
2322 polyc.set_norm(norm)
2323 else:
2324 polyc = art3d.Poly3DCollection(
2325 verts, *args, shade=shade, lightsource=lightsource,
2326 facecolors=color, **kwargs)
2327
2328 self.add_collection(polyc)
2329 self.auto_scale_xyz(tri.x, tri.y, z, had_data)
2330
2331 return polyc
2332
2333 def _3d_extend_contour(self, cset, stride=5):
2334 """
2335 Extend a contour in 3D by creating
2336 """
2337
2338 dz = (cset.levels[1] - cset.levels[0]) / 2
2339 polyverts = []
2340 colors = []
2341 for idx, level in enumerate(cset.levels):
2342 path = cset.get_paths()[idx]
2343 subpaths = [*path._iter_connected_components()]
2344 color = cset.get_edgecolor()[idx]
2345 top = art3d._paths_to_3d_segments(subpaths, level - dz)
2346 bot = art3d._paths_to_3d_segments(subpaths, level + dz)
2347 if not len(top[0]):
2348 continue
2349 nsteps = max(round(len(top[0]) / stride), 2)
2350 stepsize = (len(top[0]) - 1) / (nsteps - 1)
2351 polyverts.extend([
2352 (top[0][round(i * stepsize)], top[0][round((i + 1) * stepsize)],
2353 bot[0][round((i + 1) * stepsize)], bot[0][round(i * stepsize)])
2354 for i in range(round(nsteps) - 1)])
2355 colors.extend([color] * (round(nsteps) - 1))
2356 self.add_collection3d(art3d.Poly3DCollection(
2357 np.array(polyverts), # All polygons have 4 vertices, so vectorize.
2358 facecolors=colors, edgecolors=colors, shade=True))
2359 cset.remove()
2360
2361 def add_contour_set(
2362 self, cset, extend3d=False, stride=5, zdir='z', offset=None):
2363 zdir = '-' + zdir
2364 if extend3d:
2365 self._3d_extend_contour(cset, stride)
2366 else:
2367 art3d.collection_2d_to_3d(
2368 cset, zs=offset if offset is not None else cset.levels, zdir=zdir)
2369
2370 def add_contourf_set(self, cset, zdir='z', offset=None):
2371 self._add_contourf_set(cset, zdir=zdir, offset=offset)
2372
2373 def _add_contourf_set(self, cset, zdir='z', offset=None):
2374 """
2375 Returns
2376 -------
2377 levels : `numpy.ndarray`
2378 Levels at which the filled contours are added.
2379 """
2380 zdir = '-' + zdir
2381
2382 midpoints = cset.levels[:-1] + np.diff(cset.levels) / 2
2383 # Linearly interpolate to get levels for any extensions
2384 if cset._extend_min:
2385 min_level = cset.levels[0] - np.diff(cset.levels[:2]) / 2
2386 midpoints = np.insert(midpoints, 0, min_level)
2387 if cset._extend_max:
2388 max_level = cset.levels[-1] + np.diff(cset.levels[-2:]) / 2
2389 midpoints = np.append(midpoints, max_level)
2390
2391 art3d.collection_2d_to_3d(
2392 cset, zs=offset if offset is not None else midpoints, zdir=zdir)
2393 return midpoints
2394
2395 @_preprocess_data()
2396 def contour(self, X, Y, Z, *args,
2397 extend3d=False, stride=5, zdir='z', offset=None, **kwargs):
2398 """
2399 Create a 3D contour plot.
2400
2401 Parameters
2402 ----------
2403 X, Y, Z : array-like,
2404 Input data. See `.Axes.contour` for supported data shapes.
2405 extend3d : bool, default: False
2406 Whether to extend contour in 3D.
2407 stride : int, default: 5
2408 Step size for extending contour.
2409 zdir : {'x', 'y', 'z'}, default: 'z'
2410 The direction to use.
2411 offset : float, optional
2412 If specified, plot a projection of the contour lines at this
2413 position in a plane normal to *zdir*.
2414 data : indexable object, optional
2415 DATA_PARAMETER_PLACEHOLDER
2416
2417 *args, **kwargs
2418 Other arguments are forwarded to `matplotlib.axes.Axes.contour`.
2419
2420 Returns
2421 -------
2422 matplotlib.contour.QuadContourSet
2423 """
2424 had_data = self.has_data()
2425
2426 jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
2427 cset = super().contour(jX, jY, jZ, *args, **kwargs)
2428 self.add_contour_set(cset, extend3d, stride, zdir, offset)
2429
2430 self.auto_scale_xyz(X, Y, Z, had_data)
2431 return cset
2432
2433 contour3D = contour
2434
2435 @_preprocess_data()
2436 def tricontour(self, *args,
2437 extend3d=False, stride=5, zdir='z', offset=None, **kwargs):
2438 """
2439 Create a 3D contour plot.
2440
2441 .. note::
2442 This method currently produces incorrect output due to a
2443 longstanding bug in 3D PolyCollection rendering.
2444
2445 Parameters
2446 ----------
2447 X, Y, Z : array-like
2448 Input data. See `.Axes.tricontour` for supported data shapes.
2449 extend3d : bool, default: False
2450 Whether to extend contour in 3D.
2451 stride : int, default: 5
2452 Step size for extending contour.
2453 zdir : {'x', 'y', 'z'}, default: 'z'
2454 The direction to use.
2455 offset : float, optional
2456 If specified, plot a projection of the contour lines at this
2457 position in a plane normal to *zdir*.
2458 data : indexable object, optional
2459 DATA_PARAMETER_PLACEHOLDER
2460 *args, **kwargs
2461 Other arguments are forwarded to `matplotlib.axes.Axes.tricontour`.
2462
2463 Returns
2464 -------
2465 matplotlib.tri._tricontour.TriContourSet
2466 """
2467 had_data = self.has_data()
2468
2469 tri, args, kwargs = Triangulation.get_from_args_and_kwargs(
2470 *args, **kwargs)
2471 X = tri.x
2472 Y = tri.y
2473 if 'Z' in kwargs:
2474 Z = kwargs.pop('Z')
2475 else:
2476 # We do this so Z doesn't get passed as an arg to Axes.tricontour
2477 Z, *args = args
2478
2479 jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
2480 tri = Triangulation(jX, jY, tri.triangles, tri.mask)
2481
2482 cset = super().tricontour(tri, jZ, *args, **kwargs)
2483 self.add_contour_set(cset, extend3d, stride, zdir, offset)
2484
2485 self.auto_scale_xyz(X, Y, Z, had_data)
2486 return cset
2487
2488 def _auto_scale_contourf(self, X, Y, Z, zdir, levels, had_data):
2489 # Autoscale in the zdir based on the levels added, which are
2490 # different from data range if any contour extensions are present
2491 dim_vals = {'x': X, 'y': Y, 'z': Z, zdir: levels}
2492 # Input data and levels have different sizes, but auto_scale_xyz
2493 # expected same-size input, so manually take min/max limits
2494 limits = [(np.nanmin(dim_vals[dim]), np.nanmax(dim_vals[dim]))
2495 for dim in ['x', 'y', 'z']]
2496 self.auto_scale_xyz(*limits, had_data)
2497
2498 @_preprocess_data()
2499 def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs):
2500 """
2501 Create a 3D filled contour plot.
2502
2503 Parameters
2504 ----------
2505 X, Y, Z : array-like
2506 Input data. See `.Axes.contourf` for supported data shapes.
2507 zdir : {'x', 'y', 'z'}, default: 'z'
2508 The direction to use.
2509 offset : float, optional
2510 If specified, plot a projection of the contour lines at this
2511 position in a plane normal to *zdir*.
2512 data : indexable object, optional
2513 DATA_PARAMETER_PLACEHOLDER
2514 *args, **kwargs
2515 Other arguments are forwarded to `matplotlib.axes.Axes.contourf`.
2516
2517 Returns
2518 -------
2519 matplotlib.contour.QuadContourSet
2520 """
2521 had_data = self.has_data()
2522
2523 jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
2524 cset = super().contourf(jX, jY, jZ, *args, **kwargs)
2525 levels = self._add_contourf_set(cset, zdir, offset)
2526
2527 self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data)
2528 return cset
2529
2530 contourf3D = contourf
2531
2532 @_preprocess_data()
2533 def tricontourf(self, *args, zdir='z', offset=None, **kwargs):
2534 """
2535 Create a 3D filled contour plot.
2536
2537 .. note::
2538 This method currently produces incorrect output due to a
2539 longstanding bug in 3D PolyCollection rendering.
2540
2541 Parameters
2542 ----------
2543 X, Y, Z : array-like
2544 Input data. See `.Axes.tricontourf` for supported data shapes.
2545 zdir : {'x', 'y', 'z'}, default: 'z'
2546 The direction to use.
2547 offset : float, optional
2548 If specified, plot a projection of the contour lines at this
2549 position in a plane normal to zdir.
2550 data : indexable object, optional
2551 DATA_PARAMETER_PLACEHOLDER
2552 *args, **kwargs
2553 Other arguments are forwarded to
2554 `matplotlib.axes.Axes.tricontourf`.
2555
2556 Returns
2557 -------
2558 matplotlib.tri._tricontour.TriContourSet
2559 """
2560 had_data = self.has_data()
2561
2562 tri, args, kwargs = Triangulation.get_from_args_and_kwargs(
2563 *args, **kwargs)
2564 X = tri.x
2565 Y = tri.y
2566 if 'Z' in kwargs:
2567 Z = kwargs.pop('Z')
2568 else:
2569 # We do this so Z doesn't get passed as an arg to Axes.tricontourf
2570 Z, *args = args
2571
2572 jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
2573 tri = Triangulation(jX, jY, tri.triangles, tri.mask)
2574
2575 cset = super().tricontourf(tri, jZ, *args, **kwargs)
2576 levels = self._add_contourf_set(cset, zdir, offset)
2577
2578 self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data)
2579 return cset
2580
2581 def add_collection3d(self, col, zs=0, zdir='z', autolim=True):
2582 """
2583 Add a 3D collection object to the plot.
2584
2585 2D collection types are converted to a 3D version by
2586 modifying the object and adding z coordinate information,
2587 *zs* and *zdir*.
2588
2589 Supported 2D collection types are:
2590
2591 - `.PolyCollection`
2592 - `.LineCollection`
2593 - `.PatchCollection` (currently not supporting *autolim*)
2594
2595 Parameters
2596 ----------
2597 col : `.Collection`
2598 A 2D collection object.
2599 zs : float or array-like, default: 0
2600 The z-positions to be used for the 2D objects.
2601 zdir : {'x', 'y', 'z'}, default: 'z'
2602 The direction to use for the z-positions.
2603 autolim : bool, default: True
2604 Whether to update the data limits.
2605 """
2606 had_data = self.has_data()
2607
2608 zvals = np.atleast_1d(zs)
2609 zsortval = (np.min(zvals) if zvals.size
2610 else 0) # FIXME: arbitrary default
2611
2612 # FIXME: use issubclass() (although, then a 3D collection
2613 # object would also pass.) Maybe have a collection3d
2614 # abstract class to test for and exclude?
2615 if type(col) is mcoll.PolyCollection:
2616 art3d.poly_collection_2d_to_3d(col, zs=zs, zdir=zdir)
2617 col.set_sort_zpos(zsortval)
2618 elif type(col) is mcoll.LineCollection:
2619 art3d.line_collection_2d_to_3d(col, zs=zs, zdir=zdir)
2620 col.set_sort_zpos(zsortval)
2621 elif type(col) is mcoll.PatchCollection:
2622 art3d.patch_collection_2d_to_3d(col, zs=zs, zdir=zdir)
2623 col.set_sort_zpos(zsortval)
2624
2625 if autolim:
2626 if isinstance(col, art3d.Line3DCollection):
2627 self.auto_scale_xyz(*np.array(col._segments3d).transpose(),
2628 had_data=had_data)
2629 elif isinstance(col, art3d.Poly3DCollection):
2630 self.auto_scale_xyz(*col._vec[:-1], had_data=had_data)
2631 elif isinstance(col, art3d.Patch3DCollection):
2632 pass
2633 # FIXME: Implement auto-scaling function for Patch3DCollection
2634 # Currently unable to do so due to issues with Patch3DCollection
2635 # See https://github.com/matplotlib/matplotlib/issues/14298 for details
2636
2637 collection = super().add_collection(col)
2638 return collection
2639
2640 @_preprocess_data(replace_names=["xs", "ys", "zs", "s",
2641 "edgecolors", "c", "facecolor",
2642 "facecolors", "color"])
2643 def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True,
2644 *args, **kwargs):
2645 """
2646 Create a scatter plot.
2647
2648 Parameters
2649 ----------
2650 xs, ys : array-like
2651 The data positions.
2652 zs : float or array-like, default: 0
2653 The z-positions. Either an array of the same length as *xs* and
2654 *ys* or a single value to place all points in the same plane.
2655 zdir : {'x', 'y', 'z', '-x', '-y', '-z'}, default: 'z'
2656 The axis direction for the *zs*. This is useful when plotting 2D
2657 data on a 3D Axes. The data must be passed as *xs*, *ys*. Setting
2658 *zdir* to 'y' then plots the data to the x-z-plane.
2659
2660 See also :doc:`/gallery/mplot3d/2dcollections3d`.
2661
2662 s : float or array-like, default: 20
2663 The marker size in points**2. Either an array of the same length
2664 as *xs* and *ys* or a single value to make all markers the same
2665 size.
2666 c : :mpltype:`color`, sequence, or sequence of colors, optional
2667 The marker color. Possible values:
2668
2669 - A single color format string.
2670 - A sequence of colors of length n.
2671 - A sequence of n numbers to be mapped to colors using *cmap* and
2672 *norm*.
2673 - A 2D array in which the rows are RGB or RGBA.
2674
2675 For more details see the *c* argument of `~.axes.Axes.scatter`.
2676 depthshade : bool, default: True
2677 Whether to shade the scatter markers to give the appearance of
2678 depth. Each call to ``scatter()`` will perform its depthshading
2679 independently.
2680 data : indexable object, optional
2681 DATA_PARAMETER_PLACEHOLDER
2682 **kwargs
2683 All other keyword arguments are passed on to `~.axes.Axes.scatter`.
2684
2685 Returns
2686 -------
2687 paths : `~matplotlib.collections.PathCollection`
2688 """
2689
2690 had_data = self.has_data()
2691 zs_orig = zs
2692
2693 xs, ys, zs = cbook._broadcast_with_masks(xs, ys, zs)
2694 s = np.ma.ravel(s) # This doesn't have to match x, y in size.
2695
2696 xs, ys, zs, s, c, color = cbook.delete_masked_points(
2697 xs, ys, zs, s, c, kwargs.get('color', None)
2698 )
2699 if kwargs.get("color") is not None:
2700 kwargs['color'] = color
2701
2702 # For xs and ys, 2D scatter() will do the copying.
2703 if np.may_share_memory(zs_orig, zs): # Avoid unnecessary copies.
2704 zs = zs.copy()
2705
2706 patches = super().scatter(xs, ys, s=s, c=c, *args, **kwargs)
2707 art3d.patch_collection_2d_to_3d(patches, zs=zs, zdir=zdir,
2708 depthshade=depthshade)
2709
2710 if self._zmargin < 0.05 and xs.size > 0:
2711 self.set_zmargin(0.05)
2712
2713 self.auto_scale_xyz(xs, ys, zs, had_data)
2714
2715 return patches
2716
2717 scatter3D = scatter
2718
2719 @_preprocess_data()
2720 def bar(self, left, height, zs=0, zdir='z', *args, **kwargs):
2721 """
2722 Add 2D bar(s).
2723
2724 Parameters
2725 ----------
2726 left : 1D array-like
2727 The x coordinates of the left sides of the bars.
2728 height : 1D array-like
2729 The height of the bars.
2730 zs : float or 1D array-like, default: 0
2731 Z coordinate of bars; if a single value is specified, it will be
2732 used for all bars.
2733 zdir : {'x', 'y', 'z'}, default: 'z'
2734 When plotting 2D data, the direction to use as z ('x', 'y' or 'z').
2735 data : indexable object, optional
2736 DATA_PARAMETER_PLACEHOLDER
2737 **kwargs
2738 Other keyword arguments are forwarded to
2739 `matplotlib.axes.Axes.bar`.
2740
2741 Returns
2742 -------
2743 mpl_toolkits.mplot3d.art3d.Patch3DCollection
2744 """
2745 had_data = self.has_data()
2746
2747 patches = super().bar(left, height, *args, **kwargs)
2748
2749 zs = np.broadcast_to(zs, len(left), subok=True)
2750
2751 verts = []
2752 verts_zs = []
2753 for p, z in zip(patches, zs):
2754 vs = art3d._get_patch_verts(p)
2755 verts += vs.tolist()
2756 verts_zs += [z] * len(vs)
2757 art3d.patch_2d_to_3d(p, z, zdir)
2758 if 'alpha' in kwargs:
2759 p.set_alpha(kwargs['alpha'])
2760
2761 if len(verts) > 0:
2762 # the following has to be skipped if verts is empty
2763 # NOTE: Bugs could still occur if len(verts) > 0,
2764 # but the "2nd dimension" is empty.
2765 xs, ys = zip(*verts)
2766 else:
2767 xs, ys = [], []
2768
2769 xs, ys, verts_zs = art3d.juggle_axes(xs, ys, verts_zs, zdir)
2770 self.auto_scale_xyz(xs, ys, verts_zs, had_data)
2771
2772 return patches
2773
2774 @_preprocess_data()
2775 def bar3d(self, x, y, z, dx, dy, dz, color=None,
2776 zsort='average', shade=True, lightsource=None, *args, **kwargs):
2777 """
2778 Generate a 3D barplot.
2779
2780 This method creates three-dimensional barplot where the width,
2781 depth, height, and color of the bars can all be uniquely set.
2782
2783 Parameters
2784 ----------
2785 x, y, z : array-like
2786 The coordinates of the anchor point of the bars.
2787
2788 dx, dy, dz : float or array-like
2789 The width, depth, and height of the bars, respectively.
2790
2791 color : sequence of colors, optional
2792 The color of the bars can be specified globally or
2793 individually. This parameter can be:
2794
2795 - A single color, to color all bars the same color.
2796 - An array of colors of length N bars, to color each bar
2797 independently.
2798 - An array of colors of length 6, to color the faces of the
2799 bars similarly.
2800 - An array of colors of length 6 * N bars, to color each face
2801 independently.
2802
2803 When coloring the faces of the boxes specifically, this is
2804 the order of the coloring:
2805
2806 1. -Z (bottom of box)
2807 2. +Z (top of box)
2808 3. -Y
2809 4. +Y
2810 5. -X
2811 6. +X
2812
2813 zsort : {'average', 'min', 'max'}, default: 'average'
2814 The z-axis sorting scheme passed onto `~.art3d.Poly3DCollection`
2815
2816 shade : bool, default: True
2817 When true, this shades the dark sides of the bars (relative
2818 to the plot's source of light).
2819
2820 lightsource : `~matplotlib.colors.LightSource`, optional
2821 The lightsource to use when *shade* is True.
2822
2823 data : indexable object, optional
2824 DATA_PARAMETER_PLACEHOLDER
2825
2826 **kwargs
2827 Any additional keyword arguments are passed onto
2828 `~.art3d.Poly3DCollection`.
2829
2830 Returns
2831 -------
2832 collection : `~.art3d.Poly3DCollection`
2833 A collection of three-dimensional polygons representing the bars.
2834 """
2835
2836 had_data = self.has_data()
2837
2838 x, y, z, dx, dy, dz = np.broadcast_arrays(
2839 np.atleast_1d(x), y, z, dx, dy, dz)
2840 minx = np.min(x)
2841 maxx = np.max(x + dx)
2842 miny = np.min(y)
2843 maxy = np.max(y + dy)
2844 minz = np.min(z)
2845 maxz = np.max(z + dz)
2846
2847 # shape (6, 4, 3)
2848 # All faces are oriented facing outwards - when viewed from the
2849 # outside, their vertices are in a counterclockwise ordering.
2850 cuboid = np.array([
2851 # -z
2852 (
2853 (0, 0, 0),
2854 (0, 1, 0),
2855 (1, 1, 0),
2856 (1, 0, 0),
2857 ),
2858 # +z
2859 (
2860 (0, 0, 1),
2861 (1, 0, 1),
2862 (1, 1, 1),
2863 (0, 1, 1),
2864 ),
2865 # -y
2866 (
2867 (0, 0, 0),
2868 (1, 0, 0),
2869 (1, 0, 1),
2870 (0, 0, 1),
2871 ),
2872 # +y
2873 (
2874 (0, 1, 0),
2875 (0, 1, 1),
2876 (1, 1, 1),
2877 (1, 1, 0),
2878 ),
2879 # -x
2880 (
2881 (0, 0, 0),
2882 (0, 0, 1),
2883 (0, 1, 1),
2884 (0, 1, 0),
2885 ),
2886 # +x
2887 (
2888 (1, 0, 0),
2889 (1, 1, 0),
2890 (1, 1, 1),
2891 (1, 0, 1),
2892 ),
2893 ])
2894
2895 # indexed by [bar, face, vertex, coord]
2896 polys = np.empty(x.shape + cuboid.shape)
2897
2898 # handle each coordinate separately
2899 for i, p, dp in [(0, x, dx), (1, y, dy), (2, z, dz)]:
2900 p = p[..., np.newaxis, np.newaxis]
2901 dp = dp[..., np.newaxis, np.newaxis]
2902 polys[..., i] = p + dp * cuboid[..., i]
2903
2904 # collapse the first two axes
2905 polys = polys.reshape((-1,) + polys.shape[2:])
2906
2907 facecolors = []
2908 if color is None:
2909 color = [self._get_patches_for_fill.get_next_color()]
2910
2911 color = list(mcolors.to_rgba_array(color))
2912
2913 if len(color) == len(x):
2914 # bar colors specified, need to expand to number of faces
2915 for c in color:
2916 facecolors.extend([c] * 6)
2917 else:
2918 # a single color specified, or face colors specified explicitly
2919 facecolors = color
2920 if len(facecolors) < len(x):
2921 facecolors *= (6 * len(x))
2922
2923 col = art3d.Poly3DCollection(polys,
2924 zsort=zsort,
2925 facecolors=facecolors,
2926 shade=shade,
2927 lightsource=lightsource,
2928 *args, **kwargs)
2929 self.add_collection(col)
2930
2931 self.auto_scale_xyz((minx, maxx), (miny, maxy), (minz, maxz), had_data)
2932
2933 return col
2934
2935 def set_title(self, label, fontdict=None, loc='center', **kwargs):
2936 # docstring inherited
2937 ret = super().set_title(label, fontdict=fontdict, loc=loc, **kwargs)
2938 (x, y) = self.title.get_position()
2939 self.title.set_y(0.92 * y)
2940 return ret
2941
2942 @_preprocess_data()
2943 def quiver(self, X, Y, Z, U, V, W, *,
2944 length=1, arrow_length_ratio=.3, pivot='tail', normalize=False,
2945 **kwargs):
2946 """
2947 Plot a 3D field of arrows.
2948
2949 The arguments can be array-like or scalars, so long as they can be
2950 broadcast together. The arguments can also be masked arrays. If an
2951 element in any of argument is masked, then that corresponding quiver
2952 element will not be plotted.
2953
2954 Parameters
2955 ----------
2956 X, Y, Z : array-like
2957 The x, y and z coordinates of the arrow locations (default is
2958 tail of arrow; see *pivot* kwarg).
2959
2960 U, V, W : array-like
2961 The x, y and z components of the arrow vectors.
2962
2963 length : float, default: 1
2964 The length of each quiver.
2965
2966 arrow_length_ratio : float, default: 0.3
2967 The ratio of the arrow head with respect to the quiver.
2968
2969 pivot : {'tail', 'middle', 'tip'}, default: 'tail'
2970 The part of the arrow that is at the grid point; the arrow
2971 rotates about this point, hence the name *pivot*.
2972
2973 normalize : bool, default: False
2974 Whether all arrows are normalized to have the same length, or keep
2975 the lengths defined by *u*, *v*, and *w*.
2976
2977 data : indexable object, optional
2978 DATA_PARAMETER_PLACEHOLDER
2979
2980 **kwargs
2981 Any additional keyword arguments are delegated to
2982 :class:`.Line3DCollection`
2983 """
2984
2985 def calc_arrows(UVW):
2986 # get unit direction vector perpendicular to (u, v, w)
2987 x = UVW[:, 0]
2988 y = UVW[:, 1]
2989 norm = np.linalg.norm(UVW[:, :2], axis=1)
2990 x_p = np.divide(y, norm, where=norm != 0, out=np.zeros_like(x))
2991 y_p = np.divide(-x, norm, where=norm != 0, out=np.ones_like(x))
2992 # compute the two arrowhead direction unit vectors
2993 rangle = math.radians(15)
2994 c = math.cos(rangle)
2995 s = math.sin(rangle)
2996 # construct the rotation matrices of shape (3, 3, n)
2997 r13 = y_p * s
2998 r32 = x_p * s
2999 r12 = x_p * y_p * (1 - c)
3000 Rpos = np.array(
3001 [[c + (x_p ** 2) * (1 - c), r12, r13],
3002 [r12, c + (y_p ** 2) * (1 - c), -r32],
3003 [-r13, r32, np.full_like(x_p, c)]])
3004 # opposite rotation negates all the sin terms
3005 Rneg = Rpos.copy()
3006 Rneg[[0, 1, 2, 2], [2, 2, 0, 1]] *= -1
3007 # Batch n (3, 3) x (3) matrix multiplications ((3, 3, n) x (n, 3)).
3008 Rpos_vecs = np.einsum("ij...,...j->...i", Rpos, UVW)
3009 Rneg_vecs = np.einsum("ij...,...j->...i", Rneg, UVW)
3010 # Stack into (n, 2, 3) result.
3011 return np.stack([Rpos_vecs, Rneg_vecs], axis=1)
3012
3013 had_data = self.has_data()
3014
3015 input_args = cbook._broadcast_with_masks(X, Y, Z, U, V, W,
3016 compress=True)
3017
3018 if any(len(v) == 0 for v in input_args):
3019 # No quivers, so just make an empty collection and return early
3020 linec = art3d.Line3DCollection([], **kwargs)
3021 self.add_collection(linec)
3022 return linec
3023
3024 shaft_dt = np.array([0., length], dtype=float)
3025 arrow_dt = shaft_dt * arrow_length_ratio
3026
3027 _api.check_in_list(['tail', 'middle', 'tip'], pivot=pivot)
3028 if pivot == 'tail':
3029 shaft_dt -= length
3030 elif pivot == 'middle':
3031 shaft_dt -= length / 2
3032
3033 XYZ = np.column_stack(input_args[:3])
3034 UVW = np.column_stack(input_args[3:]).astype(float)
3035
3036 # Normalize rows of UVW
3037 if normalize:
3038 norm = np.linalg.norm(UVW, axis=1)
3039 norm[norm == 0] = 1
3040 UVW = UVW / norm.reshape((-1, 1))
3041
3042 if len(XYZ) > 0:
3043 # compute the shaft lines all at once with an outer product
3044 shafts = (XYZ - np.multiply.outer(shaft_dt, UVW)).swapaxes(0, 1)
3045 # compute head direction vectors, n heads x 2 sides x 3 dimensions
3046 head_dirs = calc_arrows(UVW)
3047 # compute all head lines at once, starting from the shaft ends
3048 heads = shafts[:, :1] - np.multiply.outer(arrow_dt, head_dirs)
3049 # stack left and right head lines together
3050 heads = heads.reshape((len(arrow_dt), -1, 3))
3051 # transpose to get a list of lines
3052 heads = heads.swapaxes(0, 1)
3053
3054 lines = [*shafts, *heads[::2], *heads[1::2]]
3055 else:
3056 lines = []
3057
3058 linec = art3d.Line3DCollection(lines, **kwargs)
3059 self.add_collection(linec)
3060
3061 self.auto_scale_xyz(XYZ[:, 0], XYZ[:, 1], XYZ[:, 2], had_data)
3062
3063 return linec
3064
3065 quiver3D = quiver
3066
3067 def voxels(self, *args, facecolors=None, edgecolors=None, shade=True,
3068 lightsource=None, **kwargs):
3069 """
3070 ax.voxels([x, y, z,] /, filled, facecolors=None, edgecolors=None, \
3071**kwargs)
3072
3073 Plot a set of filled voxels
3074
3075 All voxels are plotted as 1x1x1 cubes on the axis, with
3076 ``filled[0, 0, 0]`` placed with its lower corner at the origin.
3077 Occluded faces are not plotted.
3078
3079 Parameters
3080 ----------
3081 filled : 3D np.array of bool
3082 A 3D array of values, with truthy values indicating which voxels
3083 to fill
3084
3085 x, y, z : 3D np.array, optional
3086 The coordinates of the corners of the voxels. This should broadcast
3087 to a shape one larger in every dimension than the shape of
3088 *filled*. These can be used to plot non-cubic voxels.
3089
3090 If not specified, defaults to increasing integers along each axis,
3091 like those returned by :func:`~numpy.indices`.
3092 As indicated by the ``/`` in the function signature, these
3093 arguments can only be passed positionally.
3094
3095 facecolors, edgecolors : array-like, optional
3096 The color to draw the faces and edges of the voxels. Can only be
3097 passed as keyword arguments.
3098 These parameters can be:
3099
3100 - A single color value, to color all voxels the same color. This
3101 can be either a string, or a 1D RGB/RGBA array
3102 - ``None``, the default, to use a single color for the faces, and
3103 the style default for the edges.
3104 - A 3D `~numpy.ndarray` of color names, with each item the color
3105 for the corresponding voxel. The size must match the voxels.
3106 - A 4D `~numpy.ndarray` of RGB/RGBA data, with the components
3107 along the last axis.
3108
3109 shade : bool, default: True
3110 Whether to shade the facecolors.
3111
3112 lightsource : `~matplotlib.colors.LightSource`, optional
3113 The lightsource to use when *shade* is True.
3114
3115 **kwargs
3116 Additional keyword arguments to pass onto
3117 `~mpl_toolkits.mplot3d.art3d.Poly3DCollection`.
3118
3119 Returns
3120 -------
3121 faces : dict
3122 A dictionary indexed by coordinate, where ``faces[i, j, k]`` is a
3123 `.Poly3DCollection` of the faces drawn for the voxel
3124 ``filled[i, j, k]``. If no faces were drawn for a given voxel,
3125 either because it was not asked to be drawn, or it is fully
3126 occluded, then ``(i, j, k) not in faces``.
3127
3128 Examples
3129 --------
3130 .. plot:: gallery/mplot3d/voxels.py
3131 .. plot:: gallery/mplot3d/voxels_rgb.py
3132 .. plot:: gallery/mplot3d/voxels_torus.py
3133 .. plot:: gallery/mplot3d/voxels_numpy_logo.py
3134 """
3135
3136 # work out which signature we should be using, and use it to parse
3137 # the arguments. Name must be voxels for the correct error message
3138 if len(args) >= 3:
3139 # underscores indicate position only
3140 def voxels(__x, __y, __z, filled, **kwargs):
3141 return (__x, __y, __z), filled, kwargs
3142 else:
3143 def voxels(filled, **kwargs):
3144 return None, filled, kwargs
3145
3146 xyz, filled, kwargs = voxels(*args, **kwargs)
3147
3148 # check dimensions
3149 if filled.ndim != 3:
3150 raise ValueError("Argument filled must be 3-dimensional")
3151 size = np.array(filled.shape, dtype=np.intp)
3152
3153 # check xyz coordinates, which are one larger than the filled shape
3154 coord_shape = tuple(size + 1)
3155 if xyz is None:
3156 x, y, z = np.indices(coord_shape)
3157 else:
3158 x, y, z = (np.broadcast_to(c, coord_shape) for c in xyz)
3159
3160 def _broadcast_color_arg(color, name):
3161 if np.ndim(color) in (0, 1):
3162 # single color, like "red" or [1, 0, 0]
3163 return np.broadcast_to(color, filled.shape + np.shape(color))
3164 elif np.ndim(color) in (3, 4):
3165 # 3D array of strings, or 4D array with last axis rgb
3166 if np.shape(color)[:3] != filled.shape:
3167 raise ValueError(
3168 f"When multidimensional, {name} must match the shape "
3169 "of filled")
3170 return color
3171 else:
3172 raise ValueError(f"Invalid {name} argument")
3173
3174 # broadcast and default on facecolors
3175 if facecolors is None:
3176 facecolors = self._get_patches_for_fill.get_next_color()
3177 facecolors = _broadcast_color_arg(facecolors, 'facecolors')
3178
3179 # broadcast but no default on edgecolors
3180 edgecolors = _broadcast_color_arg(edgecolors, 'edgecolors')
3181
3182 # scale to the full array, even if the data is only in the center
3183 self.auto_scale_xyz(x, y, z)
3184
3185 # points lying on corners of a square
3186 square = np.array([
3187 [0, 0, 0],
3188 [1, 0, 0],
3189 [1, 1, 0],
3190 [0, 1, 0],
3191 ], dtype=np.intp)
3192
3193 voxel_faces = defaultdict(list)
3194
3195 def permutation_matrices(n):
3196 """Generate cyclic permutation matrices."""
3197 mat = np.eye(n, dtype=np.intp)
3198 for i in range(n):
3199 yield mat
3200 mat = np.roll(mat, 1, axis=0)
3201
3202 # iterate over each of the YZ, ZX, and XY orientations, finding faces
3203 # to render
3204 for permute in permutation_matrices(3):
3205 # find the set of ranges to iterate over
3206 pc, qc, rc = permute.T.dot(size)
3207 pinds = np.arange(pc)
3208 qinds = np.arange(qc)
3209 rinds = np.arange(rc)
3210
3211 square_rot_pos = square.dot(permute.T)
3212 square_rot_neg = square_rot_pos[::-1]
3213
3214 # iterate within the current plane
3215 for p in pinds:
3216 for q in qinds:
3217 # iterate perpendicularly to the current plane, handling
3218 # boundaries. We only draw faces between a voxel and an
3219 # empty space, to avoid drawing internal faces.
3220
3221 # draw lower faces
3222 p0 = permute.dot([p, q, 0])
3223 i0 = tuple(p0)
3224 if filled[i0]:
3225 voxel_faces[i0].append(p0 + square_rot_neg)
3226
3227 # draw middle faces
3228 for r1, r2 in zip(rinds[:-1], rinds[1:]):
3229 p1 = permute.dot([p, q, r1])
3230 p2 = permute.dot([p, q, r2])
3231
3232 i1 = tuple(p1)
3233 i2 = tuple(p2)
3234
3235 if filled[i1] and not filled[i2]:
3236 voxel_faces[i1].append(p2 + square_rot_pos)
3237 elif not filled[i1] and filled[i2]:
3238 voxel_faces[i2].append(p2 + square_rot_neg)
3239
3240 # draw upper faces
3241 pk = permute.dot([p, q, rc-1])
3242 pk2 = permute.dot([p, q, rc])
3243 ik = tuple(pk)
3244 if filled[ik]:
3245 voxel_faces[ik].append(pk2 + square_rot_pos)
3246
3247 # iterate over the faces, and generate a Poly3DCollection for each
3248 # voxel
3249 polygons = {}
3250 for coord, faces_inds in voxel_faces.items():
3251 # convert indices into 3D positions
3252 if xyz is None:
3253 faces = faces_inds
3254 else:
3255 faces = []
3256 for face_inds in faces_inds:
3257 ind = face_inds[:, 0], face_inds[:, 1], face_inds[:, 2]
3258 face = np.empty(face_inds.shape)
3259 face[:, 0] = x[ind]
3260 face[:, 1] = y[ind]
3261 face[:, 2] = z[ind]
3262 faces.append(face)
3263
3264 # shade the faces
3265 facecolor = facecolors[coord]
3266 edgecolor = edgecolors[coord]
3267
3268 poly = art3d.Poly3DCollection(
3269 faces, facecolors=facecolor, edgecolors=edgecolor,
3270 shade=shade, lightsource=lightsource, **kwargs)
3271 self.add_collection3d(poly)
3272 polygons[coord] = poly
3273
3274 return polygons
3275
3276 @_preprocess_data(replace_names=["x", "y", "z", "xerr", "yerr", "zerr"])
3277 def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='',
3278 barsabove=False, errorevery=1, ecolor=None, elinewidth=None,
3279 capsize=None, capthick=None, xlolims=False, xuplims=False,
3280 ylolims=False, yuplims=False, zlolims=False, zuplims=False,
3281 **kwargs):
3282 """
3283 Plot lines and/or markers with errorbars around them.
3284
3285 *x*/*y*/*z* define the data locations, and *xerr*/*yerr*/*zerr* define
3286 the errorbar sizes. By default, this draws the data markers/lines as
3287 well the errorbars. Use fmt='none' to draw errorbars only.
3288
3289 Parameters
3290 ----------
3291 x, y, z : float or array-like
3292 The data positions.
3293
3294 xerr, yerr, zerr : float or array-like, shape (N,) or (2, N), optional
3295 The errorbar sizes:
3296
3297 - scalar: Symmetric +/- values for all data points.
3298 - shape(N,): Symmetric +/-values for each data point.
3299 - shape(2, N): Separate - and + values for each bar. First row
3300 contains the lower errors, the second row contains the upper
3301 errors.
3302 - *None*: No errorbar.
3303
3304 Note that all error arrays should have *positive* values.
3305
3306 fmt : str, default: ''
3307 The format for the data points / data lines. See `.plot` for
3308 details.
3309
3310 Use 'none' (case-insensitive) to plot errorbars without any data
3311 markers.
3312
3313 ecolor : :mpltype:`color`, default: None
3314 The color of the errorbar lines. If None, use the color of the
3315 line connecting the markers.
3316
3317 elinewidth : float, default: None
3318 The linewidth of the errorbar lines. If None, the linewidth of
3319 the current style is used.
3320
3321 capsize : float, default: :rc:`errorbar.capsize`
3322 The length of the error bar caps in points.
3323
3324 capthick : float, default: None
3325 An alias to the keyword argument *markeredgewidth* (a.k.a. *mew*).
3326 This setting is a more sensible name for the property that
3327 controls the thickness of the error bar cap in points. For
3328 backwards compatibility, if *mew* or *markeredgewidth* are given,
3329 then they will over-ride *capthick*. This may change in future
3330 releases.
3331
3332 barsabove : bool, default: False
3333 If True, will plot the errorbars above the plot
3334 symbols. Default is below.
3335
3336 xlolims, ylolims, zlolims : bool, default: False
3337 These arguments can be used to indicate that a value gives only
3338 lower limits. In that case a caret symbol is used to indicate
3339 this. *lims*-arguments may be scalars, or array-likes of the same
3340 length as the errors. To use limits with inverted axes,
3341 `~.set_xlim`, `~.set_ylim`, or `~.set_zlim` must be
3342 called before `errorbar`. Note the tricky parameter names: setting
3343 e.g. *ylolims* to True means that the y-value is a *lower* limit of
3344 the True value, so, only an *upward*-pointing arrow will be drawn!
3345
3346 xuplims, yuplims, zuplims : bool, default: False
3347 Same as above, but for controlling the upper limits.
3348
3349 errorevery : int or (int, int), default: 1
3350 draws error bars on a subset of the data. *errorevery* =N draws
3351 error bars on the points (x[::N], y[::N], z[::N]).
3352 *errorevery* =(start, N) draws error bars on the points
3353 (x[start::N], y[start::N], z[start::N]). e.g. *errorevery* =(6, 3)
3354 adds error bars to the data at (x[6], x[9], x[12], x[15], ...).
3355 Used to avoid overlapping error bars when two series share x-axis
3356 values.
3357
3358 Returns
3359 -------
3360 errlines : list
3361 List of `~mpl_toolkits.mplot3d.art3d.Line3DCollection` instances
3362 each containing an errorbar line.
3363 caplines : list
3364 List of `~mpl_toolkits.mplot3d.art3d.Line3D` instances each
3365 containing a capline object.
3366 limmarks : list
3367 List of `~mpl_toolkits.mplot3d.art3d.Line3D` instances each
3368 containing a marker with an upper or lower limit.
3369
3370 Other Parameters
3371 ----------------
3372 data : indexable object, optional
3373 DATA_PARAMETER_PLACEHOLDER
3374
3375 **kwargs
3376 All other keyword arguments for styling errorbar lines are passed
3377 `~mpl_toolkits.mplot3d.art3d.Line3DCollection`.
3378
3379 Examples
3380 --------
3381 .. plot:: gallery/mplot3d/errorbar3d.py
3382 """
3383 had_data = self.has_data()
3384
3385 kwargs = cbook.normalize_kwargs(kwargs, mlines.Line2D)
3386 # Drop anything that comes in as None to use the default instead.
3387 kwargs = {k: v for k, v in kwargs.items() if v is not None}
3388 kwargs.setdefault('zorder', 2)
3389
3390 self._process_unit_info([("x", x), ("y", y), ("z", z)], kwargs,
3391 convert=False)
3392
3393 # make sure all the args are iterable; use lists not arrays to
3394 # preserve units
3395 x = x if np.iterable(x) else [x]
3396 y = y if np.iterable(y) else [y]
3397 z = z if np.iterable(z) else [z]
3398
3399 if not len(x) == len(y) == len(z):
3400 raise ValueError("'x', 'y', and 'z' must have the same size")
3401
3402 everymask = self._errorevery_to_mask(x, errorevery)
3403
3404 label = kwargs.pop("label", None)
3405 kwargs['label'] = '_nolegend_'
3406
3407 # Create the main line and determine overall kwargs for child artists.
3408 # We avoid calling self.plot() directly, or self._get_lines(), because
3409 # that would call self._process_unit_info again, and do other indirect
3410 # data processing.
3411 (data_line, base_style), = self._get_lines._plot_args(
3412 self, (x, y) if fmt == '' else (x, y, fmt), kwargs, return_kwargs=True)
3413 art3d.line_2d_to_3d(data_line, zs=z)
3414
3415 # Do this after creating `data_line` to avoid modifying `base_style`.
3416 if barsabove:
3417 data_line.set_zorder(kwargs['zorder'] - .1)
3418 else:
3419 data_line.set_zorder(kwargs['zorder'] + .1)
3420
3421 # Add line to plot, or throw it away and use it to determine kwargs.
3422 if fmt.lower() != 'none':
3423 self.add_line(data_line)
3424 else:
3425 data_line = None
3426 # Remove alpha=0 color that _process_plot_format returns.
3427 base_style.pop('color')
3428
3429 if 'color' not in base_style:
3430 base_style['color'] = 'C0'
3431 if ecolor is None:
3432 ecolor = base_style['color']
3433
3434 # Eject any line-specific information from format string, as it's not
3435 # needed for bars or caps.
3436 for key in ['marker', 'markersize', 'markerfacecolor',
3437 'markeredgewidth', 'markeredgecolor', 'markevery',
3438 'linestyle', 'fillstyle', 'drawstyle', 'dash_capstyle',
3439 'dash_joinstyle', 'solid_capstyle', 'solid_joinstyle']:
3440 base_style.pop(key, None)
3441
3442 # Make the style dict for the line collections (the bars).
3443 eb_lines_style = {**base_style, 'color': ecolor}
3444
3445 if elinewidth:
3446 eb_lines_style['linewidth'] = elinewidth
3447 elif 'linewidth' in kwargs:
3448 eb_lines_style['linewidth'] = kwargs['linewidth']
3449
3450 for key in ('transform', 'alpha', 'zorder', 'rasterized'):
3451 if key in kwargs:
3452 eb_lines_style[key] = kwargs[key]
3453
3454 # Make the style dict for caps (the "hats").
3455 eb_cap_style = {**base_style, 'linestyle': 'None'}
3456 if capsize is None:
3457 capsize = mpl.rcParams["errorbar.capsize"]
3458 if capsize > 0:
3459 eb_cap_style['markersize'] = 2. * capsize
3460 if capthick is not None:
3461 eb_cap_style['markeredgewidth'] = capthick
3462 eb_cap_style['color'] = ecolor
3463
3464 def _apply_mask(arrays, mask):
3465 # Return, for each array in *arrays*, the elements for which *mask*
3466 # is True, without using fancy indexing.
3467 return [[*itertools.compress(array, mask)] for array in arrays]
3468
3469 def _extract_errs(err, data, lomask, himask):
3470 # For separate +/- error values we need to unpack err
3471 if len(err.shape) == 2:
3472 low_err, high_err = err
3473 else:
3474 low_err, high_err = err, err
3475
3476 lows = np.where(lomask | ~everymask, data, data - low_err)
3477 highs = np.where(himask | ~everymask, data, data + high_err)
3478
3479 return lows, highs
3480
3481 # collect drawn items while looping over the three coordinates
3482 errlines, caplines, limmarks = [], [], []
3483
3484 # list of endpoint coordinates, used for auto-scaling
3485 coorderrs = []
3486
3487 # define the markers used for errorbar caps and limits below
3488 # the dictionary key is mapped by the `i_xyz` helper dictionary
3489 capmarker = {0: '|', 1: '|', 2: '_'}
3490 i_xyz = {'x': 0, 'y': 1, 'z': 2}
3491
3492 # Calculate marker size from points to quiver length. Because these are
3493 # not markers, and 3D Axes do not use the normal transform stack, this
3494 # is a bit involved. Since the quiver arrows will change size as the
3495 # scene is rotated, they are given a standard size based on viewing
3496 # them directly in planar form.
3497 quiversize = eb_cap_style.get('markersize',
3498 mpl.rcParams['lines.markersize']) ** 2
3499 quiversize *= self.figure.dpi / 72
3500 quiversize = self.transAxes.inverted().transform([
3501 (0, 0), (quiversize, quiversize)])
3502 quiversize = np.mean(np.diff(quiversize, axis=0))
3503 # quiversize is now in Axes coordinates, and to convert back to data
3504 # coordinates, we need to run it through the inverse 3D transform. For
3505 # consistency, this uses a fixed elevation, azimuth, and roll.
3506 with cbook._setattr_cm(self, elev=0, azim=0, roll=0):
3507 invM = np.linalg.inv(self.get_proj())
3508 # elev=azim=roll=0 produces the Y-Z plane, so quiversize in 2D 'x' is
3509 # 'y' in 3D, hence the 1 index.
3510 quiversize = np.dot(invM, [quiversize, 0, 0, 0])[1]
3511 # Quivers use a fixed 15-degree arrow head, so scale up the length so
3512 # that the size corresponds to the base. In other words, this constant
3513 # corresponds to the equation tan(15) = (base / 2) / (arrow length).
3514 quiversize *= 1.8660254037844388
3515 eb_quiver_style = {**eb_cap_style,
3516 'length': quiversize, 'arrow_length_ratio': 1}
3517 eb_quiver_style.pop('markersize', None)
3518
3519 # loop over x-, y-, and z-direction and draw relevant elements
3520 for zdir, data, err, lolims, uplims in zip(
3521 ['x', 'y', 'z'], [x, y, z], [xerr, yerr, zerr],
3522 [xlolims, ylolims, zlolims], [xuplims, yuplims, zuplims]):
3523
3524 dir_vector = art3d.get_dir_vector(zdir)
3525 i_zdir = i_xyz[zdir]
3526
3527 if err is None:
3528 continue
3529
3530 if not np.iterable(err):
3531 err = [err] * len(data)
3532
3533 err = np.atleast_1d(err)
3534
3535 # arrays fine here, they are booleans and hence not units
3536 lolims = np.broadcast_to(lolims, len(data)).astype(bool)
3537 uplims = np.broadcast_to(uplims, len(data)).astype(bool)
3538
3539 # a nested list structure that expands to (xl,xh),(yl,yh),(zl,zh),
3540 # where x/y/z and l/h correspond to dimensions and low/high
3541 # positions of errorbars in a dimension we're looping over
3542 coorderr = [
3543 _extract_errs(err * dir_vector[i], coord, lolims, uplims)
3544 for i, coord in enumerate([x, y, z])]
3545 (xl, xh), (yl, yh), (zl, zh) = coorderr
3546
3547 # draws capmarkers - flat caps orthogonal to the error bars
3548 nolims = ~(lolims | uplims)
3549 if nolims.any() and capsize > 0:
3550 lo_caps_xyz = _apply_mask([xl, yl, zl], nolims & everymask)
3551 hi_caps_xyz = _apply_mask([xh, yh, zh], nolims & everymask)
3552
3553 # setting '_' for z-caps and '|' for x- and y-caps;
3554 # these markers will rotate as the viewing angle changes
3555 cap_lo = art3d.Line3D(*lo_caps_xyz, ls='',
3556 marker=capmarker[i_zdir],
3557 **eb_cap_style)
3558 cap_hi = art3d.Line3D(*hi_caps_xyz, ls='',
3559 marker=capmarker[i_zdir],
3560 **eb_cap_style)
3561 self.add_line(cap_lo)
3562 self.add_line(cap_hi)
3563 caplines.append(cap_lo)
3564 caplines.append(cap_hi)
3565
3566 if lolims.any():
3567 xh0, yh0, zh0 = _apply_mask([xh, yh, zh], lolims & everymask)
3568 self.quiver(xh0, yh0, zh0, *dir_vector, **eb_quiver_style)
3569 if uplims.any():
3570 xl0, yl0, zl0 = _apply_mask([xl, yl, zl], uplims & everymask)
3571 self.quiver(xl0, yl0, zl0, *-dir_vector, **eb_quiver_style)
3572
3573 errline = art3d.Line3DCollection(np.array(coorderr).T,
3574 **eb_lines_style)
3575 self.add_collection(errline)
3576 errlines.append(errline)
3577 coorderrs.append(coorderr)
3578
3579 coorderrs = np.array(coorderrs)
3580
3581 def _digout_minmax(err_arr, coord_label):
3582 return (np.nanmin(err_arr[:, i_xyz[coord_label], :, :]),
3583 np.nanmax(err_arr[:, i_xyz[coord_label], :, :]))
3584
3585 minx, maxx = _digout_minmax(coorderrs, 'x')
3586 miny, maxy = _digout_minmax(coorderrs, 'y')
3587 minz, maxz = _digout_minmax(coorderrs, 'z')
3588 self.auto_scale_xyz((minx, maxx), (miny, maxy), (minz, maxz), had_data)
3589
3590 # Adapting errorbar containers for 3d case, assuming z-axis points "up"
3591 errorbar_container = mcontainer.ErrorbarContainer(
3592 (data_line, tuple(caplines), tuple(errlines)),
3593 has_xerr=(xerr is not None or yerr is not None),
3594 has_yerr=(zerr is not None),
3595 label=label)
3596 self.containers.append(errorbar_container)
3597
3598 return errlines, caplines, limmarks
3599
3600 @_api.make_keyword_only("3.8", "call_axes_locator")
3601 def get_tightbbox(self, renderer=None, call_axes_locator=True,
3602 bbox_extra_artists=None, *, for_layout_only=False):
3603 ret = super().get_tightbbox(renderer,
3604 call_axes_locator=call_axes_locator,
3605 bbox_extra_artists=bbox_extra_artists,
3606 for_layout_only=for_layout_only)
3607 batch = [ret]
3608 if self._axis3don:
3609 for axis in self._axis_map.values():
3610 if axis.get_visible():
3611 axis_bb = martist._get_tightbbox_for_layout_only(
3612 axis, renderer)
3613 if axis_bb:
3614 batch.append(axis_bb)
3615 return mtransforms.Bbox.union(batch)
3616
3617 @_preprocess_data()
3618 def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-',
3619 bottom=0, label=None, orientation='z'):
3620 """
3621 Create a 3D stem plot.
3622
3623 A stem plot draws lines perpendicular to a baseline, and places markers
3624 at the heads. By default, the baseline is defined by *x* and *y*, and
3625 stems are drawn vertically from *bottom* to *z*.
3626
3627 Parameters
3628 ----------
3629 x, y, z : array-like
3630 The positions of the heads of the stems. The stems are drawn along
3631 the *orientation*-direction from the baseline at *bottom* (in the
3632 *orientation*-coordinate) to the heads. By default, the *x* and *y*
3633 positions are used for the baseline and *z* for the head position,
3634 but this can be changed by *orientation*.
3635
3636 linefmt : str, default: 'C0-'
3637 A string defining the properties of the vertical lines. Usually,
3638 this will be a color or a color and a linestyle:
3639
3640 ========= =============
3641 Character Line Style
3642 ========= =============
3643 ``'-'`` solid line
3644 ``'--'`` dashed line
3645 ``'-.'`` dash-dot line
3646 ``':'`` dotted line
3647 ========= =============
3648
3649 Note: While it is technically possible to specify valid formats
3650 other than color or color and linestyle (e.g. 'rx' or '-.'), this
3651 is beyond the intention of the method and will most likely not
3652 result in a reasonable plot.
3653
3654 markerfmt : str, default: 'C0o'
3655 A string defining the properties of the markers at the stem heads.
3656
3657 basefmt : str, default: 'C3-'
3658 A format string defining the properties of the baseline.
3659
3660 bottom : float, default: 0
3661 The position of the baseline, in *orientation*-coordinates.
3662
3663 label : str, optional
3664 The label to use for the stems in legends.
3665
3666 orientation : {'x', 'y', 'z'}, default: 'z'
3667 The direction along which stems are drawn.
3668
3669 data : indexable object, optional
3670 DATA_PARAMETER_PLACEHOLDER
3671
3672 Returns
3673 -------
3674 `.StemContainer`
3675 The container may be treated like a tuple
3676 (*markerline*, *stemlines*, *baseline*)
3677
3678 Examples
3679 --------
3680 .. plot:: gallery/mplot3d/stem3d_demo.py
3681 """
3682
3683 from matplotlib.container import StemContainer
3684
3685 had_data = self.has_data()
3686
3687 _api.check_in_list(['x', 'y', 'z'], orientation=orientation)
3688
3689 xlim = (np.min(x), np.max(x))
3690 ylim = (np.min(y), np.max(y))
3691 zlim = (np.min(z), np.max(z))
3692
3693 # Determine the appropriate plane for the baseline and the direction of
3694 # stemlines based on the value of orientation.
3695 if orientation == 'x':
3696 basex, basexlim = y, ylim
3697 basey, baseylim = z, zlim
3698 lines = [[(bottom, thisy, thisz), (thisx, thisy, thisz)]
3699 for thisx, thisy, thisz in zip(x, y, z)]
3700 elif orientation == 'y':
3701 basex, basexlim = x, xlim
3702 basey, baseylim = z, zlim
3703 lines = [[(thisx, bottom, thisz), (thisx, thisy, thisz)]
3704 for thisx, thisy, thisz in zip(x, y, z)]
3705 else:
3706 basex, basexlim = x, xlim
3707 basey, baseylim = y, ylim
3708 lines = [[(thisx, thisy, bottom), (thisx, thisy, thisz)]
3709 for thisx, thisy, thisz in zip(x, y, z)]
3710
3711 # Determine style for stem lines.
3712 linestyle, linemarker, linecolor = _process_plot_format(linefmt)
3713 if linestyle is None:
3714 linestyle = mpl.rcParams['lines.linestyle']
3715
3716 # Plot everything in required order.
3717 baseline, = self.plot(basex, basey, basefmt, zs=bottom,
3718 zdir=orientation, label='_nolegend_')
3719 stemlines = art3d.Line3DCollection(
3720 lines, linestyles=linestyle, colors=linecolor, label='_nolegend_')
3721 self.add_collection(stemlines)
3722 markerline, = self.plot(x, y, z, markerfmt, label='_nolegend_')
3723
3724 stem_container = StemContainer((markerline, stemlines, baseline),
3725 label=label)
3726 self.add_container(stem_container)
3727
3728 jx, jy, jz = art3d.juggle_axes(basexlim, baseylim, [bottom, bottom],
3729 orientation)
3730 self.auto_scale_xyz([*jx, *xlim], [*jy, *ylim], [*jz, *zlim], had_data)
3731
3732 return stem_container
3733
3734 stem3D = stem
3735
3736
3737def get_test_data(delta=0.05):
3738 """Return a tuple X, Y, Z with a test data set."""
3739 x = y = np.arange(-3.0, 3.0, delta)
3740 X, Y = np.meshgrid(x, y)
3741
3742 Z1 = np.exp(-(X**2 + Y**2) / 2) / (2 * np.pi)
3743 Z2 = (np.exp(-(((X - 1) / 1.5)**2 + ((Y - 1) / 0.5)**2) / 2) /
3744 (2 * np.pi * 0.5 * 1.5))
3745 Z = Z2 - Z1
3746
3747 X = X * 10
3748 Y = Y * 10
3749 Z = Z * 500
3750 return X, Y, Z