Coverage for /home/martinb/.local/share/virtualenvs/camcops/lib/python3.6/site-packages/mpl_toolkits/mplot3d/axes3d.py : 12%

Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1"""
2axes3d.py, original mplot3d version by John Porter
3Created: 23 Sep 2005
5Parts fixed by Reinier Heeres <reinier@heeres.eu>
6Minor additions by Ben Axelrod <baxelrod@coroware.com>
7Significant updates and revisions by Ben Root <ben.v.root@gmail.com>
9Module containing Axes3D, an object which can plot 3D objects on a
102D matplotlib figure.
11"""
13from collections import defaultdict
14from functools import reduce
15import math
17import numpy as np
19from matplotlib import artist
20import matplotlib.axes as maxes
21import matplotlib.cbook as cbook
22import matplotlib.collections as mcoll
23import matplotlib.colors as mcolors
24import matplotlib.docstring as docstring
25import matplotlib.scale as mscale
26from matplotlib.axes import Axes, rcParams
27from matplotlib.colors import Normalize, LightSource
28from matplotlib.transforms import Bbox
29from matplotlib.tri.triangulation import Triangulation
31from . import art3d
32from . import proj3d
33from . import axis3d
36@cbook.deprecated("3.2", alternative="Bbox.unit()")
37def unit_bbox():
38 box = Bbox(np.array([[0, 0], [1, 1]]))
39 return box
42class Axes3D(Axes):
43 """
44 3D axes object.
45 """
46 name = '3d'
47 _shared_z_axes = cbook.Grouper()
49 @docstring.dedent_interpd
50 def __init__(
51 self, fig, rect=None, *args,
52 azim=-60, elev=30, zscale=None, sharez=None, proj_type='persp',
53 **kwargs):
54 """
55 Parameters
56 ----------
57 fig : Figure
58 The parent figure.
59 rect : (float, float, float, float)
60 The ``(left, bottom, width, height)`` axes position.
61 azim : float, optional
62 Azimuthal viewing angle, defaults to -60.
63 elev : float, optional
64 Elevation viewing angle, defaults to 30.
65 zscale : %(scale_type)s, optional
66 The z scale. Note that currently, only a linear scale is
67 supported.
68 sharez : Axes3D, optional
69 Other axes to share z-limits with.
70 proj_type : {'persp', 'ortho'}
71 The projection type, default 'persp'.
73 Notes
74 -----
75 .. versionadded:: 1.2.1
76 The *sharez* parameter.
77 """
79 if rect is None:
80 rect = [0.0, 0.0, 1.0, 1.0]
81 self._cids = []
83 self.initial_azim = azim
84 self.initial_elev = elev
85 self.set_proj_type(proj_type)
87 self.xy_viewLim = Bbox.unit()
88 self.zz_viewLim = Bbox.unit()
89 self.xy_dataLim = Bbox.unit()
90 self.zz_dataLim = Bbox.unit()
91 # inhibit autoscale_view until the axes are defined
92 # they can't be defined until Axes.__init__ has been called
93 self.view_init(self.initial_elev, self.initial_azim)
94 self._ready = 0
96 self._sharez = sharez
97 if sharez is not None:
98 self._shared_z_axes.join(self, sharez)
99 self._adjustable = 'datalim'
101 super().__init__(fig, rect, frameon=True, *args, **kwargs)
102 # Disable drawing of axes by base class
103 super().set_axis_off()
104 # Enable drawing of axes by Axes3D class
105 self.set_axis_on()
106 self.M = None
108 # func used to format z -- fall back on major formatters
109 self.fmt_zdata = None
111 if zscale is not None:
112 self.set_zscale(zscale)
114 if self.zaxis is not None:
115 self._zcid = self.zaxis.callbacks.connect(
116 'units finalize', lambda: self._on_units_changed(scalez=True))
117 else:
118 self._zcid = None
120 self._ready = 1
121 self.mouse_init()
122 self.set_top_view()
124 self.patch.set_linewidth(0)
125 # Calculate the pseudo-data width and height
126 pseudo_bbox = self.transLimits.inverted().transform([(0, 0), (1, 1)])
127 self._pseudo_w, self._pseudo_h = pseudo_bbox[1] - pseudo_bbox[0]
129 self.figure.add_axes(self)
131 # mplot3d currently manages its own spines and needs these turned off
132 # for bounding box calculations
133 for k in self.spines.keys():
134 self.spines[k].set_visible(False)
136 def set_axis_off(self):
137 self._axis3don = False
138 self.stale = True
140 def set_axis_on(self):
141 self._axis3don = True
142 self.stale = True
144 def convert_zunits(self, z):
145 """
146 For artists in an axes, if the zaxis has units support,
147 convert *z* using zaxis unit type
149 .. versionadded:: 1.2.1
151 """
152 return self.zaxis.convert_units(z)
154 def _process_unit_info(self, xdata=None, ydata=None, zdata=None,
155 kwargs=None):
156 """
157 Look for unit *kwargs* and update the axis instances as necessary
159 """
160 super()._process_unit_info(xdata=xdata, ydata=ydata, kwargs=kwargs)
162 if self.xaxis is None or self.yaxis is None or self.zaxis is None:
163 return
165 if zdata is not None:
166 # we only need to update if there is nothing set yet.
167 if not self.zaxis.have_units():
168 self.zaxis.update_units(xdata)
170 # process kwargs 2nd since these will override default units
171 if kwargs is not None:
172 zunits = kwargs.pop('zunits', self.zaxis.units)
173 if zunits != self.zaxis.units:
174 self.zaxis.set_units(zunits)
175 # If the units being set imply a different converter,
176 # we need to update.
177 if zdata is not None:
178 self.zaxis.update_units(zdata)
180 def set_top_view(self):
181 # this happens to be the right view for the viewing coordinates
182 # moved up and to the left slightly to fit labels and axes
183 xdwl = 0.95 / self.dist
184 xdw = 0.9 / self.dist
185 ydwl = 0.95 / self.dist
186 ydw = 0.9 / self.dist
187 # This is purposely using the 2D Axes's set_xlim and set_ylim,
188 # because we are trying to place our viewing pane.
189 super().set_xlim(-xdwl, xdw, auto=None)
190 super().set_ylim(-ydwl, ydw, auto=None)
192 def _init_axis(self):
193 '''Init 3D axes; overrides creation of regular X/Y axes'''
194 self.xaxis = axis3d.XAxis('x', self.xy_viewLim.intervalx,
195 self.xy_dataLim.intervalx, self)
196 self.yaxis = axis3d.YAxis('y', self.xy_viewLim.intervaly,
197 self.xy_dataLim.intervaly, self)
198 self.zaxis = axis3d.ZAxis('z', self.zz_viewLim.intervalx,
199 self.zz_dataLim.intervalx, self)
200 for ax in self.xaxis, self.yaxis, self.zaxis:
201 ax.init3d()
203 def get_zaxis(self):
204 '''Return the ``ZAxis`` (`~.axis3d.Axis`) instance.'''
205 return self.zaxis
207 @cbook.deprecated("3.1", alternative="xaxis", pending=True)
208 @property
209 def w_xaxis(self):
210 return self.xaxis
212 @cbook.deprecated("3.1", alternative="yaxis", pending=True)
213 @property
214 def w_yaxis(self):
215 return self.yaxis
217 @cbook.deprecated("3.1", alternative="zaxis", pending=True)
218 @property
219 def w_zaxis(self):
220 return self.zaxis
222 def _get_axis_list(self):
223 return super()._get_axis_list() + (self.zaxis, )
225 def unit_cube(self, vals=None):
226 minx, maxx, miny, maxy, minz, maxz = vals or self.get_w_lims()
227 return [(minx, miny, minz),
228 (maxx, miny, minz),
229 (maxx, maxy, minz),
230 (minx, maxy, minz),
231 (minx, miny, maxz),
232 (maxx, miny, maxz),
233 (maxx, maxy, maxz),
234 (minx, maxy, maxz)]
236 def tunit_cube(self, vals=None, M=None):
237 if M is None:
238 M = self.M
239 xyzs = self.unit_cube(vals)
240 tcube = proj3d.proj_points(xyzs, M)
241 return tcube
243 def tunit_edges(self, vals=None, M=None):
244 tc = self.tunit_cube(vals, M)
245 edges = [(tc[0], tc[1]),
246 (tc[1], tc[2]),
247 (tc[2], tc[3]),
248 (tc[3], tc[0]),
250 (tc[0], tc[4]),
251 (tc[1], tc[5]),
252 (tc[2], tc[6]),
253 (tc[3], tc[7]),
255 (tc[4], tc[5]),
256 (tc[5], tc[6]),
257 (tc[6], tc[7]),
258 (tc[7], tc[4])]
259 return edges
261 @artist.allow_rasterization
262 def draw(self, renderer):
263 # draw the background patch
264 self.patch.draw(renderer)
265 self._frameon = False
267 # first, set the aspect
268 # this is duplicated from `axes._base._AxesBase.draw`
269 # but must be called before any of the artist are drawn as
270 # it adjusts the view limits and the size of the bounding box
271 # of the axes
272 locator = self.get_axes_locator()
273 if locator:
274 pos = locator(self, renderer)
275 self.apply_aspect(pos)
276 else:
277 self.apply_aspect()
279 # add the projection matrix to the renderer
280 self.M = self.get_proj()
281 renderer.M = self.M
282 renderer.vvec = self.vvec
283 renderer.eye = self.eye
284 renderer.get_axis_position = self.get_axis_position
286 # Calculate projection of collections and patches and zorder them.
287 # Make sure they are drawn above the grids.
288 zorder_offset = max(axis.get_zorder()
289 for axis in self._get_axis_list()) + 1
290 for i, col in enumerate(
291 sorted(self.collections,
292 key=lambda col: col.do_3d_projection(renderer),
293 reverse=True)):
294 col.zorder = zorder_offset + i
295 for i, patch in enumerate(
296 sorted(self.patches,
297 key=lambda patch: patch.do_3d_projection(renderer),
298 reverse=True)):
299 patch.zorder = zorder_offset + i
301 if self._axis3don:
302 # Draw panes first
303 for axis in self._get_axis_list():
304 axis.draw_pane(renderer)
305 # Then axes
306 for axis in self._get_axis_list():
307 axis.draw(renderer)
309 # Then rest
310 super().draw(renderer)
312 def get_axis_position(self):
313 vals = self.get_w_lims()
314 tc = self.tunit_cube(vals, self.M)
315 xhigh = tc[1][2] > tc[2][2]
316 yhigh = tc[3][2] > tc[2][2]
317 zhigh = tc[0][2] > tc[2][2]
318 return xhigh, yhigh, zhigh
320 def _on_units_changed(self, scalex=False, scaley=False, scalez=False):
321 """
322 Callback for processing changes to axis units.
324 Currently forces updates of data limits and view limits.
325 """
326 self.relim()
327 self.autoscale_view(scalex=scalex, scaley=scaley, scalez=scalez)
329 def update_datalim(self, xys, **kwargs):
330 pass
332 def get_autoscale_on(self):
333 """
334 Get whether autoscaling is applied for all axes on plot commands
336 .. versionadded:: 1.1.0
337 This function was added, but not tested. Please report any bugs.
338 """
339 return super().get_autoscale_on() and self.get_autoscalez_on()
341 def get_autoscalez_on(self):
342 """
343 Get whether autoscaling for the z-axis is applied on plot commands
345 .. versionadded:: 1.1.0
346 This function was added, but not tested. Please report any bugs.
347 """
348 return self._autoscaleZon
350 def set_autoscale_on(self, b):
351 """
352 Set whether autoscaling is applied on plot commands
354 .. versionadded:: 1.1.0
355 This function was added, but not tested. Please report any bugs.
357 Parameters
358 ----------
359 b : bool
360 """
361 super().set_autoscale_on(b)
362 self.set_autoscalez_on(b)
364 def set_autoscalez_on(self, b):
365 """
366 Set whether autoscaling for the z-axis is applied on plot commands
368 .. versionadded:: 1.1.0
370 Parameters
371 ----------
372 b : bool
373 """
374 self._autoscaleZon = b
376 def set_zmargin(self, m):
377 """
378 Set padding of Z data limits prior to autoscaling.
380 *m* times the data interval will be added to each
381 end of that interval before it is used in autoscaling.
383 accepts: float in range 0 to 1
385 .. versionadded:: 1.1.0
386 """
387 if m < 0 or m > 1:
388 raise ValueError("margin must be in range 0 to 1")
389 self._zmargin = m
390 self.stale = True
392 def margins(self, *margins, x=None, y=None, z=None, tight=True):
393 """
394 Convenience method to set or retrieve autoscaling margins.
396 Call signatures::
398 margins()
400 returns xmargin, ymargin, zmargin
402 ::
404 margins(margin)
406 margins(xmargin, ymargin, zmargin)
408 margins(x=xmargin, y=ymargin, z=zmargin)
410 margins(..., tight=False)
412 All forms above set the xmargin, ymargin and zmargin
413 parameters. All keyword parameters are optional. A single
414 positional argument specifies xmargin, ymargin and zmargin.
415 Passing both positional and keyword arguments for xmargin,
416 ymargin, and/or zmargin is invalid.
418 The *tight* parameter
419 is passed to :meth:`autoscale_view`, which is executed after
420 a margin is changed; the default here is *True*, on the
421 assumption that when margins are specified, no additional
422 padding to match tick marks is usually desired. Setting
423 *tight* to *None* will preserve the previous setting.
425 Specifying any margin changes only the autoscaling; for example,
426 if *xmargin* is not None, then *xmargin* times the X data
427 interval will be added to each end of that interval before
428 it is used in autoscaling.
430 .. versionadded:: 1.1.0
431 """
432 if margins and x is not None and y is not None and z is not None:
433 raise TypeError('Cannot pass both positional and keyword '
434 'arguments for x, y, and/or z.')
435 elif len(margins) == 1:
436 x = y = z = margins[0]
437 elif len(margins) == 3:
438 x, y, z = margins
439 elif margins:
440 raise TypeError('Must pass a single positional argument for all '
441 'margins, or one for each margin (x, y, z).')
443 if x is None and y is None and z is None:
444 if tight is not True:
445 cbook._warn_external(f'ignoring tight={tight!r} in get mode')
446 return self._xmargin, self._ymargin, self._zmargin
448 if x is not None:
449 self.set_xmargin(x)
450 if y is not None:
451 self.set_ymargin(y)
452 if z is not None:
453 self.set_zmargin(z)
455 self.autoscale_view(
456 tight=tight, scalex=(x is not None), scaley=(y is not None),
457 scalez=(z is not None)
458 )
460 def autoscale(self, enable=True, axis='both', tight=None):
461 """
462 Convenience method for simple axis view autoscaling.
463 See :meth:`matplotlib.axes.Axes.autoscale` for full explanation.
464 Note that this function behaves the same, but for all
465 three axes. Therefore, 'z' can be passed for *axis*,
466 and 'both' applies to all three axes.
468 .. versionadded:: 1.1.0
469 """
470 if enable is None:
471 scalex = True
472 scaley = True
473 scalez = True
474 else:
475 if axis in ['x', 'both']:
476 self._autoscaleXon = scalex = bool(enable)
477 else:
478 scalex = False
479 if axis in ['y', 'both']:
480 self._autoscaleYon = scaley = bool(enable)
481 else:
482 scaley = False
483 if axis in ['z', 'both']:
484 self._autoscaleZon = scalez = bool(enable)
485 else:
486 scalez = False
487 self.autoscale_view(tight=tight, scalex=scalex, scaley=scaley,
488 scalez=scalez)
490 def auto_scale_xyz(self, X, Y, Z=None, had_data=None):
491 # This updates the bounding boxes as to keep a record as to what the
492 # minimum sized rectangular volume holds the data.
493 X = np.reshape(X, -1)
494 Y = np.reshape(Y, -1)
495 self.xy_dataLim.update_from_data_xy(
496 np.column_stack([X, Y]), not had_data)
497 if Z is not None:
498 Z = np.reshape(Z, -1)
499 self.zz_dataLim.update_from_data_xy(
500 np.column_stack([Z, Z]), not had_data)
501 # Let autoscale_view figure out how to use this data.
502 self.autoscale_view()
504 def autoscale_view(self, tight=None, scalex=True, scaley=True,
505 scalez=True):
506 """
507 Autoscale the view limits using the data limits.
508 See :meth:`matplotlib.axes.Axes.autoscale_view` for documentation.
509 Note that this function applies to the 3D axes, and as such
510 adds the *scalez* to the function arguments.
512 .. versionchanged:: 1.1.0
513 Function signature was changed to better match the 2D version.
514 *tight* is now explicitly a kwarg and placed first.
516 .. versionchanged:: 1.2.1
517 This is now fully functional.
519 """
520 if not self._ready:
521 return
523 # This method looks at the rectangular volume (see above)
524 # of data and decides how to scale the view portal to fit it.
525 if tight is None:
526 # if image data only just use the datalim
527 _tight = self._tight or (
528 len(self.images) > 0
529 and len(self.lines) == len(self.patches) == 0)
530 else:
531 _tight = self._tight = bool(tight)
533 if scalex and self._autoscaleXon:
534 self._shared_x_axes.clean()
535 x0, x1 = self.xy_dataLim.intervalx
536 xlocator = self.xaxis.get_major_locator()
537 x0, x1 = xlocator.nonsingular(x0, x1)
538 if self._xmargin > 0:
539 delta = (x1 - x0) * self._xmargin
540 x0 -= delta
541 x1 += delta
542 if not _tight:
543 x0, x1 = xlocator.view_limits(x0, x1)
544 self.set_xbound(x0, x1)
546 if scaley and self._autoscaleYon:
547 self._shared_y_axes.clean()
548 y0, y1 = self.xy_dataLim.intervaly
549 ylocator = self.yaxis.get_major_locator()
550 y0, y1 = ylocator.nonsingular(y0, y1)
551 if self._ymargin > 0:
552 delta = (y1 - y0) * self._ymargin
553 y0 -= delta
554 y1 += delta
555 if not _tight:
556 y0, y1 = ylocator.view_limits(y0, y1)
557 self.set_ybound(y0, y1)
559 if scalez and self._autoscaleZon:
560 self._shared_z_axes.clean()
561 z0, z1 = self.zz_dataLim.intervalx
562 zlocator = self.zaxis.get_major_locator()
563 z0, z1 = zlocator.nonsingular(z0, z1)
564 if self._zmargin > 0:
565 delta = (z1 - z0) * self._zmargin
566 z0 -= delta
567 z1 += delta
568 if not _tight:
569 z0, z1 = zlocator.view_limits(z0, z1)
570 self.set_zbound(z0, z1)
572 def get_w_lims(self):
573 '''Get 3D world limits.'''
574 minx, maxx = self.get_xlim3d()
575 miny, maxy = self.get_ylim3d()
576 minz, maxz = self.get_zlim3d()
577 return minx, maxx, miny, maxy, minz, maxz
579 def set_xlim3d(self, left=None, right=None, emit=True, auto=False,
580 *, xmin=None, xmax=None):
581 """
582 Set 3D x limits.
584 See :meth:`matplotlib.axes.Axes.set_xlim` for full documentation.
586 """
587 if right is None and np.iterable(left):
588 left, right = left
589 if xmin is not None:
590 cbook.warn_deprecated('3.0', name='`xmin`',
591 alternative='`left`', obj_type='argument')
592 if left is not None:
593 raise TypeError('Cannot pass both `xmin` and `left`')
594 left = xmin
595 if xmax is not None:
596 cbook.warn_deprecated('3.0', name='`xmax`',
597 alternative='`right`', obj_type='argument')
598 if right is not None:
599 raise TypeError('Cannot pass both `xmax` and `right`')
600 right = xmax
602 self._process_unit_info(xdata=(left, right))
603 left = self._validate_converted_limits(left, self.convert_xunits)
604 right = self._validate_converted_limits(right, self.convert_xunits)
606 old_left, old_right = self.get_xlim()
607 if left is None:
608 left = old_left
609 if right is None:
610 right = old_right
612 if left == right:
613 cbook._warn_external(
614 f"Attempting to set identical left == right == {left} results "
615 f"in singular transformations; automatically expanding.")
616 reverse = left > right
617 left, right = self.xaxis.get_major_locator().nonsingular(left, right)
618 left, right = self.xaxis.limit_range_for_scale(left, right)
619 # cast to bool to avoid bad interaction between python 3.8 and np.bool_
620 left, right = sorted([left, right], reverse=bool(reverse))
621 self.xy_viewLim.intervalx = (left, right)
623 if auto is not None:
624 self._autoscaleXon = bool(auto)
626 if emit:
627 self.callbacks.process('xlim_changed', self)
628 # Call all of the other x-axes that are shared with this one
629 for other in self._shared_x_axes.get_siblings(self):
630 if other is not self:
631 other.set_xlim(self.xy_viewLim.intervalx,
632 emit=False, auto=auto)
633 if other.figure != self.figure:
634 other.figure.canvas.draw_idle()
635 self.stale = True
636 return left, right
637 set_xlim = set_xlim3d
639 def set_ylim3d(self, bottom=None, top=None, emit=True, auto=False,
640 *, ymin=None, ymax=None):
641 """
642 Set 3D y limits.
644 See :meth:`matplotlib.axes.Axes.set_ylim` for full documentation.
646 """
647 if top is None and np.iterable(bottom):
648 bottom, top = bottom
649 if ymin is not None:
650 cbook.warn_deprecated('3.0', name='`ymin`',
651 alternative='`bottom`', obj_type='argument')
652 if bottom is not None:
653 raise TypeError('Cannot pass both `ymin` and `bottom`')
654 bottom = ymin
655 if ymax is not None:
656 cbook.warn_deprecated('3.0', name='`ymax`',
657 alternative='`top`', obj_type='argument')
658 if top is not None:
659 raise TypeError('Cannot pass both `ymax` and `top`')
660 top = ymax
662 self._process_unit_info(ydata=(bottom, top))
663 bottom = self._validate_converted_limits(bottom, self.convert_yunits)
664 top = self._validate_converted_limits(top, self.convert_yunits)
666 old_bottom, old_top = self.get_ylim()
667 if bottom is None:
668 bottom = old_bottom
669 if top is None:
670 top = old_top
672 if bottom == top:
673 cbook._warn_external(
674 f"Attempting to set identical bottom == top == {bottom} "
675 f"results in singular transformations; automatically "
676 f"expanding.")
677 swapped = bottom > top
678 bottom, top = self.yaxis.get_major_locator().nonsingular(bottom, top)
679 bottom, top = self.yaxis.limit_range_for_scale(bottom, top)
680 if swapped:
681 bottom, top = top, bottom
682 self.xy_viewLim.intervaly = (bottom, top)
684 if auto is not None:
685 self._autoscaleYon = bool(auto)
687 if emit:
688 self.callbacks.process('ylim_changed', self)
689 # Call all of the other y-axes that are shared with this one
690 for other in self._shared_y_axes.get_siblings(self):
691 if other is not self:
692 other.set_ylim(self.xy_viewLim.intervaly,
693 emit=False, auto=auto)
694 if other.figure != self.figure:
695 other.figure.canvas.draw_idle()
696 self.stale = True
697 return bottom, top
698 set_ylim = set_ylim3d
700 def set_zlim3d(self, bottom=None, top=None, emit=True, auto=False,
701 *, zmin=None, zmax=None):
702 """
703 Set 3D z limits.
705 See :meth:`matplotlib.axes.Axes.set_ylim` for full documentation
707 """
708 if top is None and np.iterable(bottom):
709 bottom, top = bottom
710 if zmin is not None:
711 cbook.warn_deprecated('3.0', name='`zmin`',
712 alternative='`bottom`', obj_type='argument')
713 if bottom is not None:
714 raise TypeError('Cannot pass both `zmin` and `bottom`')
715 bottom = zmin
716 if zmax is not None:
717 cbook.warn_deprecated('3.0', name='`zmax`',
718 alternative='`top`', obj_type='argument')
719 if top is not None:
720 raise TypeError('Cannot pass both `zmax` and `top`')
721 top = zmax
723 self._process_unit_info(zdata=(bottom, top))
724 bottom = self._validate_converted_limits(bottom, self.convert_zunits)
725 top = self._validate_converted_limits(top, self.convert_zunits)
727 old_bottom, old_top = self.get_zlim()
728 if bottom is None:
729 bottom = old_bottom
730 if top is None:
731 top = old_top
733 if bottom == top:
734 cbook._warn_external(
735 f"Attempting to set identical bottom == top == {bottom} "
736 f"results in singular transformations; automatically "
737 f"expanding.")
738 swapped = bottom > top
739 bottom, top = self.zaxis.get_major_locator().nonsingular(bottom, top)
740 bottom, top = self.zaxis.limit_range_for_scale(bottom, top)
741 if swapped:
742 bottom, top = top, bottom
743 self.zz_viewLim.intervalx = (bottom, top)
745 if auto is not None:
746 self._autoscaleZon = bool(auto)
748 if emit:
749 self.callbacks.process('zlim_changed', self)
750 # Call all of the other y-axes that are shared with this one
751 for other in self._shared_z_axes.get_siblings(self):
752 if other is not self:
753 other.set_zlim(self.zz_viewLim.intervalx,
754 emit=False, auto=auto)
755 if other.figure != self.figure:
756 other.figure.canvas.draw_idle()
757 self.stale = True
758 return bottom, top
759 set_zlim = set_zlim3d
761 def get_xlim3d(self):
762 return tuple(self.xy_viewLim.intervalx)
763 get_xlim3d.__doc__ = maxes.Axes.get_xlim.__doc__
764 get_xlim = get_xlim3d
765 if get_xlim.__doc__ is not None:
766 get_xlim.__doc__ += """
767 .. versionchanged:: 1.1.0
768 This function now correctly refers to the 3D x-limits
769 """
771 def get_ylim3d(self):
772 return tuple(self.xy_viewLim.intervaly)
773 get_ylim3d.__doc__ = maxes.Axes.get_ylim.__doc__
774 get_ylim = get_ylim3d
775 if get_ylim.__doc__ is not None:
776 get_ylim.__doc__ += """
777 .. versionchanged:: 1.1.0
778 This function now correctly refers to the 3D y-limits.
779 """
781 def get_zlim3d(self):
782 '''Get 3D z limits.'''
783 return tuple(self.zz_viewLim.intervalx)
784 get_zlim = get_zlim3d
786 def get_zscale(self):
787 """
788 Return the zaxis scale string %s
790 """ % (", ".join(mscale.get_scale_names()))
791 return self.zaxis.get_scale()
793 # We need to slightly redefine these to pass scalez=False
794 # to their calls of autoscale_view.
796 def set_xscale(self, value, **kwargs):
797 self.xaxis._set_scale(value, **kwargs)
798 self.autoscale_view(scaley=False, scalez=False)
799 self._update_transScale()
800 self.stale = True
802 def set_yscale(self, value, **kwargs):
803 self.yaxis._set_scale(value, **kwargs)
804 self.autoscale_view(scalex=False, scalez=False)
805 self._update_transScale()
806 self.stale = True
808 def set_zscale(self, value, **kwargs):
809 self.zaxis._set_scale(value, **kwargs)
810 self.autoscale_view(scalex=False, scaley=False)
811 self._update_transScale()
812 self.stale = True
814 set_xscale.__doc__, set_yscale.__doc__, set_zscale.__doc__ = map(
815 """
816 Set the {}-axis scale.
818 Parameters
819 ----------
820 value : {{"linear"}}
821 The axis scale type to apply. 3D axes currently only support
822 linear scales; other scales yield nonsensical results.
824 **kwargs
825 Keyword arguments are nominally forwarded to the scale class, but
826 none of them is applicable for linear scales.
827 """.format,
828 ["x", "y", "z"])
830 def set_zticks(self, *args, **kwargs):
831 """
832 Set z-axis tick locations.
833 See :meth:`matplotlib.axes.Axes.set_yticks` for more details.
835 .. note::
836 Minor ticks are not supported.
838 .. versionadded:: 1.1.0
839 """
840 return self.zaxis.set_ticks(*args, **kwargs)
842 @cbook._make_keyword_only("3.2", "minor")
843 def get_zticks(self, minor=False):
844 """
845 Return the z ticks as a list of locations
846 See :meth:`matplotlib.axes.Axes.get_yticks` for more details.
848 .. note::
849 Minor ticks are not supported.
851 .. versionadded:: 1.1.0
852 """
853 return self.zaxis.get_ticklocs(minor=minor)
855 def get_zmajorticklabels(self):
856 """
857 Get the ztick labels as a list of Text instances
859 .. versionadded:: 1.1.0
860 """
861 return self.zaxis.get_majorticklabels()
863 def get_zminorticklabels(self):
864 """
865 Get the ztick labels as a list of Text instances
867 .. note::
868 Minor ticks are not supported. This function was added
869 only for completeness.
871 .. versionadded:: 1.1.0
872 """
873 return self.zaxis.get_minorticklabels()
875 def set_zticklabels(self, *args, **kwargs):
876 """
877 Set z-axis tick labels.
878 See :meth:`matplotlib.axes.Axes.set_yticklabels` for more details.
880 .. note::
881 Minor ticks are not supported by Axes3D objects.
883 .. versionadded:: 1.1.0
884 """
885 return self.zaxis.set_ticklabels(*args, **kwargs)
887 def get_zticklabels(self, minor=False):
888 """
889 Get ztick labels as a list of Text instances.
890 See :meth:`matplotlib.axes.Axes.get_yticklabels` for more details.
892 .. note::
893 Minor ticks are not supported.
895 .. versionadded:: 1.1.0
896 """
897 return self.zaxis.get_ticklabels(minor=minor)
899 def zaxis_date(self, tz=None):
900 """
901 Sets up z-axis ticks and labels that treat the z data as dates.
903 *tz* is a timezone string or :class:`tzinfo` instance.
904 Defaults to rc value.
906 .. note::
907 This function is merely provided for completeness.
908 Axes3D objects do not officially support dates for ticks,
909 and so this may or may not work as expected.
911 .. versionadded:: 1.1.0
912 This function was added, but not tested. Please report any bugs.
913 """
914 self.zaxis.axis_date(tz)
916 def get_zticklines(self):
917 """
918 Get ztick lines as a list of Line2D instances.
919 Note that this function is provided merely for completeness.
920 These lines are re-calculated as the display changes.
922 .. versionadded:: 1.1.0
923 """
924 return self.zaxis.get_ticklines()
926 def clabel(self, *args, **kwargs):
927 """
928 This function is currently not implemented for 3D axes.
929 Returns *None*.
930 """
931 return None
933 def view_init(self, elev=None, azim=None):
934 """
935 Set the elevation and azimuth of the axes in degrees (not radians).
937 This can be used to rotate the axes programmatically.
939 'elev' stores the elevation angle in the z plane (in degrees).
940 'azim' stores the azimuth angle in the (x, y) plane (in degrees).
942 if elev or azim are None (default), then the initial value
943 is used which was specified in the :class:`Axes3D` constructor.
944 """
946 self.dist = 10
948 if elev is None:
949 self.elev = self.initial_elev
950 else:
951 self.elev = elev
953 if azim is None:
954 self.azim = self.initial_azim
955 else:
956 self.azim = azim
958 def set_proj_type(self, proj_type):
959 """
960 Set the projection type.
962 Parameters
963 ----------
964 proj_type : {'persp', 'ortho'}
965 """
966 self._projection = cbook._check_getitem({
967 'persp': proj3d.persp_transformation,
968 'ortho': proj3d.ortho_transformation,
969 }, proj_type=proj_type)
971 def get_proj(self):
972 """
973 Create the projection matrix from the current viewing position.
975 elev stores the elevation angle in the z plane
976 azim stores the azimuth angle in the (x, y) plane
978 dist is the distance of the eye viewing point from the object point.
979 """
980 relev, razim = np.pi * self.elev/180, np.pi * self.azim/180
982 xmin, xmax = self.get_xlim3d()
983 ymin, ymax = self.get_ylim3d()
984 zmin, zmax = self.get_zlim3d()
986 # transform to uniform world coordinates 0-1, 0-1, 0-1
987 worldM = proj3d.world_transformation(xmin, xmax,
988 ymin, ymax,
989 zmin, zmax)
991 # look into the middle of the new coordinates
992 R = np.array([0.5, 0.5, 0.5])
994 xp = R[0] + np.cos(razim) * np.cos(relev) * self.dist
995 yp = R[1] + np.sin(razim) * np.cos(relev) * self.dist
996 zp = R[2] + np.sin(relev) * self.dist
997 E = np.array((xp, yp, zp))
999 self.eye = E
1000 self.vvec = R - E
1001 self.vvec = self.vvec / np.linalg.norm(self.vvec)
1003 if abs(relev) > np.pi/2:
1004 # upside down
1005 V = np.array((0, 0, -1))
1006 else:
1007 V = np.array((0, 0, 1))
1008 zfront, zback = -self.dist, self.dist
1010 viewM = proj3d.view_transformation(E, R, V)
1011 projM = self._projection(zfront, zback)
1012 M0 = np.dot(viewM, worldM)
1013 M = np.dot(projM, M0)
1014 return M
1016 def mouse_init(self, rotate_btn=1, zoom_btn=3):
1017 """
1018 Initializes mouse button callbacks to enable 3D rotation of the axes.
1019 Also optionally sets the mouse buttons for 3D rotation and zooming.
1021 Parameters
1022 ----------
1023 rotate_btn : int or list of int
1024 The mouse button or buttons to use for 3D rotation of the axes;
1025 defaults to 1.
1026 zoom_btn : int or list of int
1027 The mouse button or buttons to use to zoom the 3D axes; defaults to
1028 3.
1029 """
1030 self.button_pressed = None
1031 self._cids = [
1032 self.figure.canvas.mpl_connect(
1033 'motion_notify_event', self._on_move),
1034 self.figure.canvas.mpl_connect(
1035 'button_press_event', self._button_press),
1036 self.figure.canvas.mpl_connect(
1037 'button_release_event', self._button_release),
1038 ]
1039 # coerce scalars into array-like, then convert into
1040 # a regular list to avoid comparisons against None
1041 # which breaks in recent versions of numpy.
1042 self._rotate_btn = np.atleast_1d(rotate_btn).tolist()
1043 self._zoom_btn = np.atleast_1d(zoom_btn).tolist()
1045 def can_zoom(self):
1046 """
1047 Return *True* if this axes supports the zoom box button functionality.
1049 3D axes objects do not use the zoom box button.
1050 """
1051 return False
1053 def can_pan(self):
1054 """
1055 Return *True* if this axes supports the pan/zoom button functionality.
1057 3D axes objects do not use the pan/zoom button.
1058 """
1059 return False
1061 def cla(self):
1062 # docstring inherited.
1064 super().cla()
1065 self.zaxis.cla()
1067 if self._sharez is not None:
1068 self.zaxis.major = self._sharez.zaxis.major
1069 self.zaxis.minor = self._sharez.zaxis.minor
1070 z0, z1 = self._sharez.get_zlim()
1071 self.set_zlim(z0, z1, emit=False, auto=None)
1072 self.zaxis._set_scale(self._sharez.zaxis.get_scale())
1073 else:
1074 self.zaxis._set_scale('linear')
1075 try:
1076 self.set_zlim(0, 1)
1077 except TypeError:
1078 pass
1080 self._autoscaleZon = True
1081 self._zmargin = 0
1083 self.grid(rcParams['axes3d.grid'])
1085 def disable_mouse_rotation(self):
1086 """Disable mouse button callbacks."""
1087 # Disconnect the various events we set.
1088 for cid in self._cids:
1089 self.figure.canvas.mpl_disconnect(cid)
1090 self._cids = []
1092 def _button_press(self, event):
1093 if event.inaxes == self:
1094 self.button_pressed = event.button
1095 self.sx, self.sy = event.xdata, event.ydata
1097 def _button_release(self, event):
1098 self.button_pressed = None
1100 def format_zdata(self, z):
1101 """
1102 Return *z* string formatted. This function will use the
1103 :attr:`fmt_zdata` attribute if it is callable, else will fall
1104 back on the zaxis major formatter
1105 """
1106 try:
1107 return self.fmt_zdata(z)
1108 except (AttributeError, TypeError):
1109 func = self.zaxis.get_major_formatter().format_data_short
1110 val = func(z)
1111 return val
1113 def format_coord(self, xd, yd):
1114 """
1115 Given the 2D view coordinates attempt to guess a 3D coordinate.
1116 Looks for the nearest edge to the point and then assumes that
1117 the point is at the same z location as the nearest point on the edge.
1118 """
1120 if self.M is None:
1121 return ''
1123 if self.button_pressed in self._rotate_btn:
1124 return 'azimuth={:.0f} deg, elevation={:.0f} deg '.format(
1125 self.azim, self.elev)
1126 # ignore xd and yd and display angles instead
1128 # nearest edge
1129 p0, p1 = min(self.tunit_edges(),
1130 key=lambda edge: proj3d._line2d_seg_dist(
1131 edge[0], edge[1], (xd, yd)))
1133 # scale the z value to match
1134 x0, y0, z0 = p0
1135 x1, y1, z1 = p1
1136 d0 = np.hypot(x0-xd, y0-yd)
1137 d1 = np.hypot(x1-xd, y1-yd)
1138 dt = d0+d1
1139 z = d1/dt * z0 + d0/dt * z1
1141 x, y, z = proj3d.inv_transform(xd, yd, z, self.M)
1143 xs = self.format_xdata(x)
1144 ys = self.format_ydata(y)
1145 zs = self.format_zdata(z)
1146 return 'x=%s, y=%s, z=%s' % (xs, ys, zs)
1148 def _on_move(self, event):
1149 """Mouse moving
1151 button-1 rotates by default. Can be set explicitly in mouse_init().
1152 button-3 zooms by default. Can be set explicitly in mouse_init().
1153 """
1155 if not self.button_pressed:
1156 return
1158 if self.M is None:
1159 return
1161 x, y = event.xdata, event.ydata
1162 # In case the mouse is out of bounds.
1163 if x is None:
1164 return
1166 dx, dy = x - self.sx, y - self.sy
1167 w = self._pseudo_w
1168 h = self._pseudo_h
1169 self.sx, self.sy = x, y
1171 # Rotation
1172 if self.button_pressed in self._rotate_btn:
1173 # rotate viewing point
1174 # get the x and y pixel coords
1175 if dx == 0 and dy == 0:
1176 return
1177 self.elev = art3d._norm_angle(self.elev - (dy/h)*180)
1178 self.azim = art3d._norm_angle(self.azim - (dx/w)*180)
1179 self.get_proj()
1180 self.stale = True
1181 self.figure.canvas.draw_idle()
1183# elif self.button_pressed == 2:
1184 # pan view
1185 # project xv, yv, zv -> xw, yw, zw
1186 # pan
1187# pass
1189 # Zoom
1190 elif self.button_pressed in self._zoom_btn:
1191 # zoom view
1192 # hmmm..this needs some help from clipping....
1193 minx, maxx, miny, maxy, minz, maxz = self.get_w_lims()
1194 df = 1-((h - dy)/h)
1195 dx = (maxx-minx)*df
1196 dy = (maxy-miny)*df
1197 dz = (maxz-minz)*df
1198 self.set_xlim3d(minx - dx, maxx + dx)
1199 self.set_ylim3d(miny - dy, maxy + dy)
1200 self.set_zlim3d(minz - dz, maxz + dz)
1201 self.get_proj()
1202 self.figure.canvas.draw_idle()
1204 def set_zlabel(self, zlabel, fontdict=None, labelpad=None, **kwargs):
1205 '''
1206 Set zlabel. See doc for :meth:`set_ylabel` for description.
1207 '''
1208 if labelpad is not None:
1209 self.zaxis.labelpad = labelpad
1210 return self.zaxis.set_label_text(zlabel, fontdict, **kwargs)
1212 def get_zlabel(self):
1213 """
1214 Get the z-label text string.
1216 .. versionadded:: 1.1.0
1217 This function was added, but not tested. Please report any bugs.
1218 """
1219 label = self.zaxis.get_label()
1220 return label.get_text()
1222 # Axes rectangle characteristics
1224 def get_frame_on(self):
1225 """Get whether the 3D axes panels are drawn."""
1226 return self._frameon
1228 def set_frame_on(self, b):
1229 """
1230 Set whether the 3D axes panels are drawn.
1232 Parameters
1233 ----------
1234 b : bool
1235 """
1236 self._frameon = bool(b)
1237 self.stale = True
1239 def grid(self, b=True, **kwargs):
1240 '''
1241 Set / unset 3D grid.
1243 .. note::
1245 Currently, this function does not behave the same as
1246 :meth:`matplotlib.axes.Axes.grid`, but it is intended to
1247 eventually support that behavior.
1249 .. versionadded:: 1.1.0
1250 '''
1251 # TODO: Operate on each axes separately
1252 if len(kwargs):
1253 b = True
1254 self._draw_grid = b
1255 self.stale = True
1257 def locator_params(self, axis='both', tight=None, **kwargs):
1258 """
1259 Convenience method for controlling tick locators.
1261 See :meth:`matplotlib.axes.Axes.locator_params` for full
1262 documentation. Note that this is for Axes3D objects,
1263 therefore, setting *axis* to 'both' will result in the
1264 parameters being set for all three axes. Also, *axis*
1265 can also take a value of 'z' to apply parameters to the
1266 z axis.
1268 .. versionadded:: 1.1.0
1269 This function was added, but not tested. Please report any bugs.
1270 """
1271 _x = axis in ['x', 'both']
1272 _y = axis in ['y', 'both']
1273 _z = axis in ['z', 'both']
1274 if _x:
1275 self.xaxis.get_major_locator().set_params(**kwargs)
1276 if _y:
1277 self.yaxis.get_major_locator().set_params(**kwargs)
1278 if _z:
1279 self.zaxis.get_major_locator().set_params(**kwargs)
1280 self.autoscale_view(tight=tight, scalex=_x, scaley=_y, scalez=_z)
1282 def tick_params(self, axis='both', **kwargs):
1283 """
1284 Convenience method for changing the appearance of ticks and
1285 tick labels.
1287 See :meth:`matplotlib.axes.Axes.tick_params` for more complete
1288 documentation.
1290 The only difference is that setting *axis* to 'both' will
1291 mean that the settings are applied to all three axes. Also,
1292 the *axis* parameter also accepts a value of 'z', which
1293 would mean to apply to only the z-axis.
1295 Also, because of how Axes3D objects are drawn very differently
1296 from regular 2D axes, some of these settings may have
1297 ambiguous meaning. For simplicity, the 'z' axis will
1298 accept settings as if it was like the 'y' axis.
1300 .. note::
1301 Axes3D currently ignores some of these settings.
1303 .. versionadded:: 1.1.0
1304 """
1305 cbook._check_in_list(['x', 'y', 'z', 'both'], axis=axis)
1306 if axis in ['x', 'y', 'both']:
1307 super().tick_params(axis, **kwargs)
1308 if axis in ['z', 'both']:
1309 zkw = dict(kwargs)
1310 zkw.pop('top', None)
1311 zkw.pop('bottom', None)
1312 zkw.pop('labeltop', None)
1313 zkw.pop('labelbottom', None)
1314 self.zaxis.set_tick_params(**zkw)
1316 # data limits, ticks, tick labels, and formatting
1318 def invert_zaxis(self):
1319 """
1320 Invert the z-axis.
1322 .. versionadded:: 1.1.0
1323 This function was added, but not tested. Please report any bugs.
1324 """
1325 bottom, top = self.get_zlim()
1326 self.set_zlim(top, bottom, auto=None)
1328 def zaxis_inverted(self):
1329 '''
1330 Returns True if the z-axis is inverted.
1332 .. versionadded:: 1.1.0
1333 '''
1334 bottom, top = self.get_zlim()
1335 return top < bottom
1337 def get_zbound(self):
1338 """
1339 Return the lower and upper z-axis bounds, in increasing order.
1341 .. versionadded:: 1.1.0
1342 """
1343 bottom, top = self.get_zlim()
1344 if bottom < top:
1345 return bottom, top
1346 else:
1347 return top, bottom
1349 def set_zbound(self, lower=None, upper=None):
1350 """
1351 Set the lower and upper numerical bounds of the z-axis.
1352 This method will honor axes inversion regardless of parameter order.
1353 It will not change the :attr:`_autoscaleZon` attribute.
1355 .. versionadded:: 1.1.0
1356 """
1357 if upper is None and np.iterable(lower):
1358 lower, upper = lower
1359 old_lower, old_upper = self.get_zbound()
1360 if lower is None:
1361 lower = old_lower
1362 if upper is None:
1363 upper = old_upper
1365 if self.zaxis_inverted():
1366 if lower < upper:
1367 self.set_zlim(upper, lower, auto=None)
1368 else:
1369 self.set_zlim(lower, upper, auto=None)
1370 else:
1371 if lower < upper:
1372 self.set_zlim(lower, upper, auto=None)
1373 else:
1374 self.set_zlim(upper, lower, auto=None)
1376 def text(self, x, y, z, s, zdir=None, **kwargs):
1377 '''
1378 Add text to the plot. kwargs will be passed on to Axes.text,
1379 except for the `zdir` keyword, which sets the direction to be
1380 used as the z direction.
1381 '''
1382 text = super().text(x, y, s, **kwargs)
1383 art3d.text_2d_to_3d(text, z, zdir)
1384 return text
1386 text3D = text
1387 text2D = Axes.text
1389 def plot(self, xs, ys, *args, zdir='z', **kwargs):
1390 """
1391 Plot 2D or 3D data.
1393 Parameters
1394 ----------
1395 xs : 1D array-like
1396 x coordinates of vertices.
1397 ys : 1D array-like
1398 y coordinates of vertices.
1399 zs : scalar or 1D array-like
1400 z coordinates of vertices; either one for all points or one for
1401 each point.
1402 zdir : {'x', 'y', 'z'}
1403 When plotting 2D data, the direction to use as z ('x', 'y' or 'z');
1404 defaults to 'z'.
1405 **kwargs
1406 Other arguments are forwarded to `matplotlib.axes.Axes.plot`.
1407 """
1408 had_data = self.has_data()
1410 # `zs` can be passed positionally or as keyword; checking whether
1411 # args[0] is a string matches the behavior of 2D `plot` (via
1412 # `_process_plot_var_args`).
1413 if args and not isinstance(args[0], str):
1414 zs, *args = args
1415 if 'zs' in kwargs:
1416 raise TypeError("plot() for multiple values for argument 'z'")
1417 else:
1418 zs = kwargs.pop('zs', 0)
1420 # Match length
1421 zs = np.broadcast_to(zs, len(xs))
1423 lines = super().plot(xs, ys, *args, **kwargs)
1424 for line in lines:
1425 art3d.line_2d_to_3d(line, zs=zs, zdir=zdir)
1427 xs, ys, zs = art3d.juggle_axes(xs, ys, zs, zdir)
1428 self.auto_scale_xyz(xs, ys, zs, had_data)
1429 return lines
1431 plot3D = plot
1433 def plot_surface(self, X, Y, Z, *args, norm=None, vmin=None,
1434 vmax=None, lightsource=None, **kwargs):
1435 """
1436 Create a surface plot.
1438 By default it will be colored in shades of a solid color, but it also
1439 supports color mapping by supplying the *cmap* argument.
1441 .. note::
1443 The *rcount* and *ccount* kwargs, which both default to 50,
1444 determine the maximum number of samples used in each direction. If
1445 the input data is larger, it will be downsampled (by slicing) to
1446 these numbers of points.
1448 Parameters
1449 ----------
1450 X, Y, Z : 2d arrays
1451 Data values.
1453 rcount, ccount : int
1454 Maximum number of samples used in each direction. If the input
1455 data is larger, it will be downsampled (by slicing) to these
1456 numbers of points. Defaults to 50.
1458 .. versionadded:: 2.0
1460 rstride, cstride : int
1461 Downsampling stride in each direction. These arguments are
1462 mutually exclusive with *rcount* and *ccount*. If only one of
1463 *rstride* or *cstride* is set, the other defaults to 10.
1465 'classic' mode uses a default of ``rstride = cstride = 10`` instead
1466 of the new default of ``rcount = ccount = 50``.
1468 color : color-like
1469 Color of the surface patches.
1471 cmap : Colormap
1472 Colormap of the surface patches.
1474 facecolors : array-like of colors.
1475 Colors of each individual patch.
1477 norm : Normalize
1478 Normalization for the colormap.
1480 vmin, vmax : float
1481 Bounds for the normalization.
1483 shade : bool
1484 Whether to shade the facecolors. Defaults to True. Shading is
1485 always disabled when `cmap` is specified.
1487 lightsource : `~matplotlib.colors.LightSource`
1488 The lightsource to use when `shade` is True.
1490 **kwargs
1491 Other arguments are forwarded to `.Poly3DCollection`.
1492 """
1494 had_data = self.has_data()
1496 if Z.ndim != 2:
1497 raise ValueError("Argument Z must be 2-dimensional.")
1498 if np.any(np.isnan(Z)):
1499 cbook._warn_external(
1500 "Z contains NaN values. This may result in rendering "
1501 "artifacts.")
1503 # TODO: Support masked arrays
1504 X, Y, Z = np.broadcast_arrays(X, Y, Z)
1505 rows, cols = Z.shape
1507 has_stride = 'rstride' in kwargs or 'cstride' in kwargs
1508 has_count = 'rcount' in kwargs or 'ccount' in kwargs
1510 if has_stride and has_count:
1511 raise ValueError("Cannot specify both stride and count arguments")
1513 rstride = kwargs.pop('rstride', 10)
1514 cstride = kwargs.pop('cstride', 10)
1515 rcount = kwargs.pop('rcount', 50)
1516 ccount = kwargs.pop('ccount', 50)
1518 if rcParams['_internal.classic_mode']:
1519 # Strides have priority over counts in classic mode.
1520 # So, only compute strides from counts
1521 # if counts were explicitly given
1522 compute_strides = has_count
1523 else:
1524 # If the strides are provided then it has priority.
1525 # Otherwise, compute the strides from the counts.
1526 compute_strides = not has_stride
1528 if compute_strides:
1529 rstride = int(max(np.ceil(rows / rcount), 1))
1530 cstride = int(max(np.ceil(cols / ccount), 1))
1532 if 'facecolors' in kwargs:
1533 fcolors = kwargs.pop('facecolors')
1534 else:
1535 color = kwargs.pop('color', None)
1536 if color is None:
1537 color = self._get_lines.get_next_color()
1538 color = np.array(mcolors.to_rgba(color))
1539 fcolors = None
1541 cmap = kwargs.get('cmap', None)
1542 shade = kwargs.pop('shade', cmap is None)
1543 if shade is None:
1544 cbook.warn_deprecated(
1545 "3.1",
1546 message="Passing shade=None to Axes3D.plot_surface() is "
1547 "deprecated since matplotlib 3.1 and will change its "
1548 "semantic or raise an error in matplotlib 3.3. "
1549 "Please use shade=False instead.")
1551 # evenly spaced, and including both endpoints
1552 row_inds = list(range(0, rows-1, rstride)) + [rows-1]
1553 col_inds = list(range(0, cols-1, cstride)) + [cols-1]
1555 colset = [] # the sampled facecolor
1556 polys = []
1557 for rs, rs_next in zip(row_inds[:-1], row_inds[1:]):
1558 for cs, cs_next in zip(col_inds[:-1], col_inds[1:]):
1559 ps = [
1560 # +1 ensures we share edges between polygons
1561 cbook._array_perimeter(a[rs:rs_next+1, cs:cs_next+1])
1562 for a in (X, Y, Z)
1563 ]
1564 # ps = np.stack(ps, axis=-1)
1565 ps = np.array(ps).T
1566 polys.append(ps)
1568 if fcolors is not None:
1569 colset.append(fcolors[rs][cs])
1571 # note that the striding causes some polygons to have more coordinates
1572 # than others
1573 polyc = art3d.Poly3DCollection(polys, *args, **kwargs)
1575 if fcolors is not None:
1576 if shade:
1577 colset = self._shade_colors(
1578 colset, self._generate_normals(polys), lightsource)
1579 polyc.set_facecolors(colset)
1580 polyc.set_edgecolors(colset)
1581 elif cmap:
1582 # doesn't vectorize because polys is jagged
1583 avg_z = np.array([ps[:, 2].mean() for ps in polys])
1584 polyc.set_array(avg_z)
1585 if vmin is not None or vmax is not None:
1586 polyc.set_clim(vmin, vmax)
1587 if norm is not None:
1588 polyc.set_norm(norm)
1589 else:
1590 if shade:
1591 colset = self._shade_colors(
1592 color, self._generate_normals(polys), lightsource)
1593 else:
1594 colset = color
1595 polyc.set_facecolors(colset)
1597 self.add_collection(polyc)
1598 self.auto_scale_xyz(X, Y, Z, had_data)
1600 return polyc
1602 def _generate_normals(self, polygons):
1603 """
1604 Takes a list of polygons and return an array of their normals.
1606 Normals point towards the viewer for a face with its vertices in
1607 counterclockwise order, following the right hand rule.
1609 Uses three points equally spaced around the polygon.
1610 This normal of course might not make sense for polygons with more than
1611 three points not lying in a plane, but it's a plausible and fast
1612 approximation.
1614 Parameters
1615 ----------
1616 polygons: list of (M_i, 3) array-like, or (..., M, 3) array-like
1617 A sequence of polygons to compute normals for, which can have
1618 varying numbers of vertices. If the polygons all have the same
1619 number of vertices and array is passed, then the operation will
1620 be vectorized.
1622 Returns
1623 -------
1624 normals: (..., 3) array-like
1625 A normal vector estimated for the polygon.
1627 """
1628 if isinstance(polygons, np.ndarray):
1629 # optimization: polygons all have the same number of points, so can
1630 # vectorize
1631 n = polygons.shape[-2]
1632 i1, i2, i3 = 0, n//3, 2*n//3
1633 v1 = polygons[..., i1, :] - polygons[..., i2, :]
1634 v2 = polygons[..., i2, :] - polygons[..., i3, :]
1635 else:
1636 # The subtraction doesn't vectorize because polygons is jagged.
1637 v1 = np.empty((len(polygons), 3))
1638 v2 = np.empty((len(polygons), 3))
1639 for poly_i, ps in enumerate(polygons):
1640 n = len(ps)
1641 i1, i2, i3 = 0, n//3, 2*n//3
1642 v1[poly_i, :] = ps[i1, :] - ps[i2, :]
1643 v2[poly_i, :] = ps[i2, :] - ps[i3, :]
1644 return np.cross(v1, v2)
1646 def _shade_colors(self, color, normals, lightsource=None):
1647 """
1648 Shade *color* using normal vectors given by *normals*.
1649 *color* can also be an array of the same length as *normals*.
1650 """
1651 if lightsource is None:
1652 # chosen for backwards-compatibility
1653 lightsource = LightSource(azdeg=225, altdeg=19.4712)
1655 with np.errstate(invalid="ignore"):
1656 shade = ((normals / np.linalg.norm(normals, axis=1, keepdims=True))
1657 @ lightsource.direction)
1658 mask = ~np.isnan(shade)
1660 if mask.any():
1661 # convert dot product to allowed shading fractions
1662 in_norm = Normalize(-1, 1)
1663 out_norm = Normalize(0.3, 1).inverse
1665 def norm(x):
1666 return out_norm(in_norm(x))
1668 shade[~mask] = 0
1670 color = mcolors.to_rgba_array(color)
1671 # shape of color should be (M, 4) (where M is number of faces)
1672 # shape of shade should be (M,)
1673 # colors should have final shape of (M, 4)
1674 alpha = color[:, 3]
1675 colors = norm(shade)[:, np.newaxis] * color
1676 colors[:, 3] = alpha
1677 else:
1678 colors = np.asanyarray(color).copy()
1680 return colors
1682 def plot_wireframe(self, X, Y, Z, *args, **kwargs):
1683 """
1684 Plot a 3D wireframe.
1686 .. note::
1688 The *rcount* and *ccount* kwargs, which both default to 50,
1689 determine the maximum number of samples used in each direction. If
1690 the input data is larger, it will be downsampled (by slicing) to
1691 these numbers of points.
1693 Parameters
1694 ----------
1695 X, Y, Z : 2d arrays
1696 Data values.
1698 rcount, ccount : int
1699 Maximum number of samples used in each direction. If the input
1700 data is larger, it will be downsampled (by slicing) to these
1701 numbers of points. Setting a count to zero causes the data to be
1702 not sampled in the corresponding direction, producing a 3D line
1703 plot rather than a wireframe plot. Defaults to 50.
1705 .. versionadded:: 2.0
1707 rstride, cstride : int
1708 Downsampling stride in each direction. These arguments are
1709 mutually exclusive with *rcount* and *ccount*. If only one of
1710 *rstride* or *cstride* is set, the other defaults to 1. Setting a
1711 stride to zero causes the data to be not sampled in the
1712 corresponding direction, producing a 3D line plot rather than a
1713 wireframe plot.
1715 'classic' mode uses a default of ``rstride = cstride = 1`` instead
1716 of the new default of ``rcount = ccount = 50``.
1718 **kwargs
1719 Other arguments are forwarded to `.Line3DCollection`.
1720 """
1722 had_data = self.has_data()
1723 if Z.ndim != 2:
1724 raise ValueError("Argument Z must be 2-dimensional.")
1725 # FIXME: Support masked arrays
1726 X, Y, Z = np.broadcast_arrays(X, Y, Z)
1727 rows, cols = Z.shape
1729 has_stride = 'rstride' in kwargs or 'cstride' in kwargs
1730 has_count = 'rcount' in kwargs or 'ccount' in kwargs
1732 if has_stride and has_count:
1733 raise ValueError("Cannot specify both stride and count arguments")
1735 rstride = kwargs.pop('rstride', 1)
1736 cstride = kwargs.pop('cstride', 1)
1737 rcount = kwargs.pop('rcount', 50)
1738 ccount = kwargs.pop('ccount', 50)
1740 if rcParams['_internal.classic_mode']:
1741 # Strides have priority over counts in classic mode.
1742 # So, only compute strides from counts
1743 # if counts were explicitly given
1744 if has_count:
1745 rstride = int(max(np.ceil(rows / rcount), 1)) if rcount else 0
1746 cstride = int(max(np.ceil(cols / ccount), 1)) if ccount else 0
1747 else:
1748 # If the strides are provided then it has priority.
1749 # Otherwise, compute the strides from the counts.
1750 if not has_stride:
1751 rstride = int(max(np.ceil(rows / rcount), 1)) if rcount else 0
1752 cstride = int(max(np.ceil(cols / ccount), 1)) if ccount else 0
1754 # We want two sets of lines, one running along the "rows" of
1755 # Z and another set of lines running along the "columns" of Z.
1756 # This transpose will make it easy to obtain the columns.
1757 tX, tY, tZ = np.transpose(X), np.transpose(Y), np.transpose(Z)
1759 if rstride:
1760 rii = list(range(0, rows, rstride))
1761 # Add the last index only if needed
1762 if rows > 0 and rii[-1] != (rows - 1):
1763 rii += [rows-1]
1764 else:
1765 rii = []
1766 if cstride:
1767 cii = list(range(0, cols, cstride))
1768 # Add the last index only if needed
1769 if cols > 0 and cii[-1] != (cols - 1):
1770 cii += [cols-1]
1771 else:
1772 cii = []
1774 if rstride == 0 and cstride == 0:
1775 raise ValueError("Either rstride or cstride must be non zero")
1777 # If the inputs were empty, then just
1778 # reset everything.
1779 if Z.size == 0:
1780 rii = []
1781 cii = []
1783 xlines = [X[i] for i in rii]
1784 ylines = [Y[i] for i in rii]
1785 zlines = [Z[i] for i in rii]
1787 txlines = [tX[i] for i in cii]
1788 tylines = [tY[i] for i in cii]
1789 tzlines = [tZ[i] for i in cii]
1791 lines = ([list(zip(xl, yl, zl))
1792 for xl, yl, zl in zip(xlines, ylines, zlines)]
1793 + [list(zip(xl, yl, zl))
1794 for xl, yl, zl in zip(txlines, tylines, tzlines)])
1796 linec = art3d.Line3DCollection(lines, *args, **kwargs)
1797 self.add_collection(linec)
1798 self.auto_scale_xyz(X, Y, Z, had_data)
1800 return linec
1802 def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None,
1803 lightsource=None, **kwargs):
1804 """
1805 Plot a triangulated surface.
1807 The (optional) triangulation can be specified in one of two ways;
1808 either::
1810 plot_trisurf(triangulation, ...)
1812 where triangulation is a :class:`~matplotlib.tri.Triangulation`
1813 object, or::
1815 plot_trisurf(X, Y, ...)
1816 plot_trisurf(X, Y, triangles, ...)
1817 plot_trisurf(X, Y, triangles=triangles, ...)
1819 in which case a Triangulation object will be created. See
1820 :class:`~matplotlib.tri.Triangulation` for a explanation of
1821 these possibilities.
1823 The remaining arguments are::
1825 plot_trisurf(..., Z)
1827 where *Z* is the array of values to contour, one per point
1828 in the triangulation.
1830 Parameters
1831 ----------
1832 X, Y, Z : array-like
1833 Data values as 1D arrays.
1834 color
1835 Color of the surface patches.
1836 cmap
1837 A colormap for the surface patches.
1838 norm : Normalize
1839 An instance of Normalize to map values to colors.
1840 vmin, vmax : scalar, optional, default: None
1841 Minimum and maximum value to map.
1842 shade : bool
1843 Whether to shade the facecolors. Defaults to True. Shading is
1844 always disabled when *cmap* is specified.
1845 lightsource : `~matplotlib.colors.LightSource`
1846 The lightsource to use when *shade* is True.
1847 **kwargs
1848 All other arguments are passed on to
1849 :class:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection`
1851 Examples
1852 --------
1853 .. plot:: gallery/mplot3d/trisurf3d.py
1854 .. plot:: gallery/mplot3d/trisurf3d_2.py
1856 .. versionadded:: 1.2.0
1857 """
1859 had_data = self.has_data()
1861 # TODO: Support custom face colours
1862 if color is None:
1863 color = self._get_lines.get_next_color()
1864 color = np.array(mcolors.to_rgba(color))
1866 cmap = kwargs.get('cmap', None)
1867 shade = kwargs.pop('shade', cmap is None)
1869 tri, args, kwargs = \
1870 Triangulation.get_from_args_and_kwargs(*args, **kwargs)
1871 try:
1872 z = kwargs.pop('Z')
1873 except KeyError:
1874 # We do this so Z doesn't get passed as an arg to PolyCollection
1875 z, *args = args
1876 z = np.asarray(z)
1878 triangles = tri.get_masked_triangles()
1879 xt = tri.x[triangles]
1880 yt = tri.y[triangles]
1881 zt = z[triangles]
1882 verts = np.stack((xt, yt, zt), axis=-1)
1884 polyc = art3d.Poly3DCollection(verts, *args, **kwargs)
1886 if cmap:
1887 # average over the three points of each triangle
1888 avg_z = verts[:, :, 2].mean(axis=1)
1889 polyc.set_array(avg_z)
1890 if vmin is not None or vmax is not None:
1891 polyc.set_clim(vmin, vmax)
1892 if norm is not None:
1893 polyc.set_norm(norm)
1894 else:
1895 if shade:
1896 normals = self._generate_normals(verts)
1897 colset = self._shade_colors(color, normals, lightsource)
1898 else:
1899 colset = color
1900 polyc.set_facecolors(colset)
1902 self.add_collection(polyc)
1903 self.auto_scale_xyz(tri.x, tri.y, z, had_data)
1905 return polyc
1907 def _3d_extend_contour(self, cset, stride=5):
1908 '''
1909 Extend a contour in 3D by creating
1910 '''
1912 levels = cset.levels
1913 colls = cset.collections
1914 dz = (levels[1] - levels[0]) / 2
1916 for z, linec in zip(levels, colls):
1917 paths = linec.get_paths()
1918 if not paths:
1919 continue
1920 topverts = art3d._paths_to_3d_segments(paths, z - dz)
1921 botverts = art3d._paths_to_3d_segments(paths, z + dz)
1923 color = linec.get_color()[0]
1925 polyverts = []
1926 normals = []
1927 nsteps = round(len(topverts[0]) / stride)
1928 if nsteps <= 1:
1929 if len(topverts[0]) > 1:
1930 nsteps = 2
1931 else:
1932 continue
1934 stepsize = (len(topverts[0]) - 1) / (nsteps - 1)
1935 for i in range(int(round(nsteps)) - 1):
1936 i1 = int(round(i * stepsize))
1937 i2 = int(round((i + 1) * stepsize))
1938 polyverts.append([topverts[0][i1],
1939 topverts[0][i2],
1940 botverts[0][i2],
1941 botverts[0][i1]])
1943 # all polygons have 4 vertices, so vectorize
1944 polyverts = np.array(polyverts)
1945 normals = self._generate_normals(polyverts)
1947 colors = self._shade_colors(color, normals)
1948 colors2 = self._shade_colors(color, normals)
1949 polycol = art3d.Poly3DCollection(polyverts,
1950 facecolors=colors,
1951 edgecolors=colors2)
1952 polycol.set_sort_zpos(z)
1953 self.add_collection3d(polycol)
1955 for col in colls:
1956 self.collections.remove(col)
1958 def add_contour_set(
1959 self, cset, extend3d=False, stride=5, zdir='z', offset=None):
1960 zdir = '-' + zdir
1961 if extend3d:
1962 self._3d_extend_contour(cset, stride)
1963 else:
1964 for z, linec in zip(cset.levels, cset.collections):
1965 if offset is not None:
1966 z = offset
1967 art3d.line_collection_2d_to_3d(linec, z, zdir=zdir)
1969 def add_contourf_set(self, cset, zdir='z', offset=None):
1970 zdir = '-' + zdir
1971 for z, linec in zip(cset.levels, cset.collections):
1972 if offset is not None:
1973 z = offset
1974 art3d.poly_collection_2d_to_3d(linec, z, zdir=zdir)
1975 linec.set_sort_zpos(z)
1977 def contour(self, X, Y, Z, *args,
1978 extend3d=False, stride=5, zdir='z', offset=None, **kwargs):
1979 """
1980 Create a 3D contour plot.
1982 Parameters
1983 ----------
1984 X, Y, Z : array-likes
1985 Input data.
1986 extend3d : bool
1987 Whether to extend contour in 3D; defaults to False.
1988 stride : int
1989 Step size for extending contour.
1990 zdir : {'x', 'y', 'z'}
1991 The direction to use; defaults to 'z'.
1992 offset : scalar
1993 If specified, plot a projection of the contour lines at this
1994 position in a plane normal to zdir
1995 *args, **kwargs
1996 Other arguments are forwarded to `matplotlib.axes.Axes.contour`.
1998 Returns
1999 -------
2000 matplotlib.contour.QuadContourSet
2001 """
2002 had_data = self.has_data()
2004 jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
2005 cset = super().contour(jX, jY, jZ, *args, **kwargs)
2006 self.add_contour_set(cset, extend3d, stride, zdir, offset)
2008 self.auto_scale_xyz(X, Y, Z, had_data)
2009 return cset
2011 contour3D = contour
2013 def tricontour(self, *args,
2014 extend3d=False, stride=5, zdir='z', offset=None, **kwargs):
2015 """
2016 Create a 3D contour plot.
2018 .. versionchanged:: 1.3.0
2019 Added support for custom triangulations
2021 .. note::
2022 This method currently produces incorrect output due to a
2023 longstanding bug in 3D PolyCollection rendering.
2025 Parameters
2026 ----------
2027 X, Y, Z : array-likes
2028 Input data.
2029 extend3d : bool
2030 Whether to extend contour in 3D; defaults to False.
2031 stride : int
2032 Step size for extending contour.
2033 zdir : {'x', 'y', 'z'}
2034 The direction to use; defaults to 'z'.
2035 offset : scalar
2036 If specified, plot a projection of the contour lines at this
2037 position in a plane normal to zdir
2038 *args, **kwargs
2039 Other arguments are forwarded to `matplotlib.axes.Axes.tricontour`.
2041 Returns
2042 -------
2043 matplotlib.tri.tricontour.TriContourSet
2044 """
2045 had_data = self.has_data()
2047 tri, args, kwargs = Triangulation.get_from_args_and_kwargs(
2048 *args, **kwargs)
2049 X = tri.x
2050 Y = tri.y
2051 if 'Z' in kwargs:
2052 Z = kwargs.pop('Z')
2053 else:
2054 # We do this so Z doesn't get passed as an arg to Axes.tricontour
2055 Z, *args = args
2057 jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
2058 tri = Triangulation(jX, jY, tri.triangles, tri.mask)
2060 cset = super().tricontour(tri, jZ, *args, **kwargs)
2061 self.add_contour_set(cset, extend3d, stride, zdir, offset)
2063 self.auto_scale_xyz(X, Y, Z, had_data)
2064 return cset
2066 def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs):
2067 """
2068 Create a 3D filled contour plot.
2070 Parameters
2071 ----------
2072 X, Y, Z : array-likes
2073 Input data.
2074 zdir : {'x', 'y', 'z'}
2075 The direction to use; defaults to 'z'.
2076 offset : scalar
2077 If specified, plot a projection of the contour lines at this
2078 position in a plane normal to zdir
2079 *args, **kwargs
2080 Other arguments are forwarded to `matplotlib.axes.Axes.contourf`.
2082 Returns
2083 -------
2084 matplotlib.contour.QuadContourSet
2086 Notes
2087 -----
2088 .. versionadded:: 1.1.0
2089 The *zdir* and *offset* parameters.
2090 """
2091 had_data = self.has_data()
2093 jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
2094 cset = super().contourf(jX, jY, jZ, *args, **kwargs)
2095 self.add_contourf_set(cset, zdir, offset)
2097 self.auto_scale_xyz(X, Y, Z, had_data)
2098 return cset
2100 contourf3D = contourf
2102 def tricontourf(self, *args, zdir='z', offset=None, **kwargs):
2103 """
2104 Create a 3D filled contour plot.
2106 .. note::
2107 This method currently produces incorrect output due to a
2108 longstanding bug in 3D PolyCollection rendering.
2110 Parameters
2111 ----------
2112 X, Y, Z : array-likes
2113 Input data.
2114 zdir : {'x', 'y', 'z'}
2115 The direction to use; defaults to 'z'.
2116 offset : scalar
2117 If specified, plot a projection of the contour lines at this
2118 position in a plane normal to zdir
2119 *args, **kwargs
2120 Other arguments are forwarded to
2121 `matplotlib.axes.Axes.tricontourf`.
2123 Returns
2124 -------
2125 matplotlib.tri.tricontour.TriContourSet
2127 Notes
2128 -----
2129 .. versionadded:: 1.1.0
2130 The *zdir* and *offset* parameters.
2131 .. versionchanged:: 1.3.0
2132 Added support for custom triangulations
2133 """
2134 had_data = self.has_data()
2136 tri, args, kwargs = Triangulation.get_from_args_and_kwargs(
2137 *args, **kwargs)
2138 X = tri.x
2139 Y = tri.y
2140 if 'Z' in kwargs:
2141 Z = kwargs.pop('Z')
2142 else:
2143 # We do this so Z doesn't get passed as an arg to Axes.tricontourf
2144 Z, *args = args
2146 jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir)
2147 tri = Triangulation(jX, jY, tri.triangles, tri.mask)
2149 cset = super().tricontourf(tri, jZ, *args, **kwargs)
2150 self.add_contourf_set(cset, zdir, offset)
2152 self.auto_scale_xyz(X, Y, Z, had_data)
2153 return cset
2155 def add_collection3d(self, col, zs=0, zdir='z'):
2156 '''
2157 Add a 3D collection object to the plot.
2159 2D collection types are converted to a 3D version by
2160 modifying the object and adding z coordinate information.
2162 Supported are:
2163 - PolyCollection
2164 - LineCollection
2165 - PatchCollection
2166 '''
2167 zvals = np.atleast_1d(zs)
2168 zsortval = (np.min(zvals) if zvals.size
2169 else 0) # FIXME: arbitrary default
2171 # FIXME: use issubclass() (although, then a 3D collection
2172 # object would also pass.) Maybe have a collection3d
2173 # abstract class to test for and exclude?
2174 if type(col) is mcoll.PolyCollection:
2175 art3d.poly_collection_2d_to_3d(col, zs=zs, zdir=zdir)
2176 col.set_sort_zpos(zsortval)
2177 elif type(col) is mcoll.LineCollection:
2178 art3d.line_collection_2d_to_3d(col, zs=zs, zdir=zdir)
2179 col.set_sort_zpos(zsortval)
2180 elif type(col) is mcoll.PatchCollection:
2181 art3d.patch_collection_2d_to_3d(col, zs=zs, zdir=zdir)
2182 col.set_sort_zpos(zsortval)
2184 super().add_collection(col)
2186 def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True,
2187 *args, **kwargs):
2188 """
2189 Create a scatter plot.
2191 Parameters
2192 ----------
2193 xs, ys : array-like
2194 The data positions.
2195 zs : float or array-like, optional, default: 0
2196 The z-positions. Either an array of the same length as *xs* and
2197 *ys* or a single value to place all points in the same plane.
2198 zdir : {'x', 'y', 'z', '-x', '-y', '-z'}, optional, default: 'z'
2199 The axis direction for the *zs*. This is useful when plotting 2D
2200 data on a 3D Axes. The data must be passed as *xs*, *ys*. Setting
2201 *zdir* to 'y' then plots the data to the x-z-plane.
2203 See also :doc:`/gallery/mplot3d/2dcollections3d`.
2205 s : scalar or array-like, optional, default: 20
2206 The marker size in points**2. Either an array of the same length
2207 as *xs* and *ys* or a single value to make all markers the same
2208 size.
2209 c : color, sequence, or sequence of colors, optional
2210 The marker color. Possible values:
2212 - A single color format string.
2213 - A sequence of colors of length n.
2214 - A sequence of n numbers to be mapped to colors using *cmap* and
2215 *norm*.
2216 - A 2-D array in which the rows are RGB or RGBA.
2218 For more details see the *c* argument of `~.axes.Axes.scatter`.
2219 depthshade : bool, optional, default: True
2220 Whether to shade the scatter markers to give the appearance of
2221 depth. Each call to ``scatter()`` will perform its depthshading
2222 independently.
2223 **kwargs
2224 All other arguments are passed on to `~.axes.Axes.scatter`.
2226 Returns
2227 -------
2228 paths : `~matplotlib.collections.PathCollection`
2229 """
2231 had_data = self.has_data()
2233 xs, ys, zs = np.broadcast_arrays(
2234 *[np.ravel(np.ma.filled(t, np.nan)) for t in [xs, ys, zs]])
2235 s = np.ma.ravel(s) # This doesn't have to match x, y in size.
2237 xs, ys, zs, s, c = cbook.delete_masked_points(xs, ys, zs, s, c)
2239 patches = super().scatter(xs, ys, s=s, c=c, *args, **kwargs)
2240 art3d.patch_collection_2d_to_3d(patches, zs=zs, zdir=zdir,
2241 depthshade=depthshade)
2243 if self._zmargin < 0.05 and xs.size > 0:
2244 self.set_zmargin(0.05)
2246 self.auto_scale_xyz(xs, ys, zs, had_data)
2248 return patches
2250 scatter3D = scatter
2252 def bar(self, left, height, zs=0, zdir='z', *args, **kwargs):
2253 """
2254 Add 2D bar(s).
2256 Parameters
2257 ----------
2258 left : 1D array-like
2259 The x coordinates of the left sides of the bars.
2260 height : 1D array-like
2261 The height of the bars.
2262 zs : scalar or 1D array-like
2263 Z coordinate of bars; if a single value is specified, it will be
2264 used for all bars.
2265 zdir : {'x', 'y', 'z'}
2266 When plotting 2D data, the direction to use as z ('x', 'y' or 'z');
2267 defaults to 'z'.
2268 **kwargs
2269 Other arguments are forwarded to `matplotlib.axes.Axes.bar`.
2271 Returns
2272 -------
2273 mpl_toolkits.mplot3d.art3d.Patch3DCollection
2274 """
2275 had_data = self.has_data()
2277 patches = super().bar(left, height, *args, **kwargs)
2279 zs = np.broadcast_to(zs, len(left))
2281 verts = []
2282 verts_zs = []
2283 for p, z in zip(patches, zs):
2284 vs = art3d._get_patch_verts(p)
2285 verts += vs.tolist()
2286 verts_zs += [z] * len(vs)
2287 art3d.patch_2d_to_3d(p, z, zdir)
2288 if 'alpha' in kwargs:
2289 p.set_alpha(kwargs['alpha'])
2291 if len(verts) > 0:
2292 # the following has to be skipped if verts is empty
2293 # NOTE: Bugs could still occur if len(verts) > 0,
2294 # but the "2nd dimension" is empty.
2295 xs, ys = zip(*verts)
2296 else:
2297 xs, ys = [], []
2299 xs, ys, verts_zs = art3d.juggle_axes(xs, ys, verts_zs, zdir)
2300 self.auto_scale_xyz(xs, ys, verts_zs, had_data)
2302 return patches
2304 def bar3d(self, x, y, z, dx, dy, dz, color=None,
2305 zsort='average', shade=True, lightsource=None, *args, **kwargs):
2306 """Generate a 3D barplot.
2308 This method creates three dimensional barplot where the width,
2309 depth, height, and color of the bars can all be uniquely set.
2311 Parameters
2312 ----------
2313 x, y, z : array-like
2314 The coordinates of the anchor point of the bars.
2316 dx, dy, dz : scalar or array-like
2317 The width, depth, and height of the bars, respectively.
2319 color : sequence of colors, optional
2320 The color of the bars can be specified globally or
2321 individually. This parameter can be:
2323 - A single color, to color all bars the same color.
2324 - An array of colors of length N bars, to color each bar
2325 independently.
2326 - An array of colors of length 6, to color the faces of the
2327 bars similarly.
2328 - An array of colors of length 6 * N bars, to color each face
2329 independently.
2331 When coloring the faces of the boxes specifically, this is
2332 the order of the coloring:
2334 1. -Z (bottom of box)
2335 2. +Z (top of box)
2336 3. -Y
2337 4. +Y
2338 5. -X
2339 6. +X
2341 zsort : str, optional
2342 The z-axis sorting scheme passed onto `~.art3d.Poly3DCollection`
2344 shade : bool, optional (default = True)
2345 When true, this shades the dark sides of the bars (relative
2346 to the plot's source of light).
2348 lightsource : `~matplotlib.colors.LightSource`
2349 The lightsource to use when *shade* is True.
2351 **kwargs
2352 Any additional keyword arguments are passed onto
2353 `~.art3d.Poly3DCollection`.
2355 Returns
2356 -------
2357 collection : `~.art3d.Poly3DCollection`
2358 A collection of three dimensional polygons representing
2359 the bars.
2360 """
2362 had_data = self.has_data()
2364 x, y, z, dx, dy, dz = np.broadcast_arrays(
2365 np.atleast_1d(x), y, z, dx, dy, dz)
2366 minx = np.min(x)
2367 maxx = np.max(x + dx)
2368 miny = np.min(y)
2369 maxy = np.max(y + dy)
2370 minz = np.min(z)
2371 maxz = np.max(z + dz)
2373 # shape (6, 4, 3)
2374 # All faces are oriented facing outwards - when viewed from the
2375 # outside, their vertices are in a counterclockwise ordering.
2376 cuboid = np.array([
2377 # -z
2378 (
2379 (0, 0, 0),
2380 (0, 1, 0),
2381 (1, 1, 0),
2382 (1, 0, 0),
2383 ),
2384 # +z
2385 (
2386 (0, 0, 1),
2387 (1, 0, 1),
2388 (1, 1, 1),
2389 (0, 1, 1),
2390 ),
2391 # -y
2392 (
2393 (0, 0, 0),
2394 (1, 0, 0),
2395 (1, 0, 1),
2396 (0, 0, 1),
2397 ),
2398 # +y
2399 (
2400 (0, 1, 0),
2401 (0, 1, 1),
2402 (1, 1, 1),
2403 (1, 1, 0),
2404 ),
2405 # -x
2406 (
2407 (0, 0, 0),
2408 (0, 0, 1),
2409 (0, 1, 1),
2410 (0, 1, 0),
2411 ),
2412 # +x
2413 (
2414 (1, 0, 0),
2415 (1, 1, 0),
2416 (1, 1, 1),
2417 (1, 0, 1),
2418 ),
2419 ])
2421 # indexed by [bar, face, vertex, coord]
2422 polys = np.empty(x.shape + cuboid.shape)
2424 # handle each coordinate separately
2425 for i, p, dp in [(0, x, dx), (1, y, dy), (2, z, dz)]:
2426 p = p[..., np.newaxis, np.newaxis]
2427 dp = dp[..., np.newaxis, np.newaxis]
2428 polys[..., i] = p + dp * cuboid[..., i]
2430 # collapse the first two axes
2431 polys = polys.reshape((-1,) + polys.shape[2:])
2433 facecolors = []
2434 if color is None:
2435 color = [self._get_patches_for_fill.get_next_color()]
2437 if len(color) == len(x):
2438 # bar colors specified, need to expand to number of faces
2439 for c in color:
2440 facecolors.extend([c] * 6)
2441 else:
2442 # a single color specified, or face colors specified explicitly
2443 facecolors = list(mcolors.to_rgba_array(color))
2444 if len(facecolors) < len(x):
2445 facecolors *= (6 * len(x))
2447 if shade:
2448 normals = self._generate_normals(polys)
2449 sfacecolors = self._shade_colors(facecolors, normals, lightsource)
2450 else:
2451 sfacecolors = facecolors
2453 col = art3d.Poly3DCollection(polys,
2454 zsort=zsort,
2455 facecolor=sfacecolors,
2456 *args, **kwargs)
2457 self.add_collection(col)
2459 self.auto_scale_xyz((minx, maxx), (miny, maxy), (minz, maxz), had_data)
2461 return col
2463 def set_title(self, label, fontdict=None, loc='center', **kwargs):
2464 # docstring inherited
2465 ret = super().set_title(label, fontdict=fontdict, loc=loc, **kwargs)
2466 (x, y) = self.title.get_position()
2467 self.title.set_y(0.92 * y)
2468 return ret
2470 def quiver(self, *args,
2471 length=1, arrow_length_ratio=.3, pivot='tail', normalize=False,
2472 **kwargs):
2473 """
2474 ax.quiver(X, Y, Z, U, V, W, /, length=1, arrow_length_ratio=.3, \
2475pivot='tail', normalize=False, **kwargs)
2477 Plot a 3D field of arrows.
2479 The arguments could be array-like or scalars, so long as they
2480 they can be broadcast together. The arguments can also be
2481 masked arrays. If an element in any of argument is masked, then
2482 that corresponding quiver element will not be plotted.
2484 Parameters
2485 ----------
2486 X, Y, Z : array-like
2487 The x, y and z coordinates of the arrow locations (default is
2488 tail of arrow; see *pivot* kwarg)
2490 U, V, W : array-like
2491 The x, y and z components of the arrow vectors
2493 length : float
2494 The length of each quiver, default to 1.0, the unit is
2495 the same with the axes
2497 arrow_length_ratio : float
2498 The ratio of the arrow head with respect to the quiver,
2499 default to 0.3
2501 pivot : {'tail', 'middle', 'tip'}
2502 The part of the arrow that is at the grid point; the arrow
2503 rotates about this point, hence the name *pivot*.
2504 Default is 'tail'
2506 normalize : bool
2507 When True, all of the arrows will be the same length. This
2508 defaults to False, where the arrows will be different lengths
2509 depending on the values of u, v, w.
2511 **kwargs
2512 Any additional keyword arguments are delegated to
2513 :class:`~matplotlib.collections.LineCollection`
2514 """
2515 def calc_arrow(uvw, angle=15):
2516 """
2517 To calculate the arrow head. uvw should be a unit vector.
2518 We normalize it here:
2519 """
2520 # get unit direction vector perpendicular to (u, v, w)
2521 norm = np.linalg.norm(uvw[:2])
2522 if norm > 0:
2523 x = uvw[1] / norm
2524 y = -uvw[0] / norm
2525 else:
2526 x, y = 0, 1
2528 # compute the two arrowhead direction unit vectors
2529 ra = math.radians(angle)
2530 c = math.cos(ra)
2531 s = math.sin(ra)
2533 # construct the rotation matrices
2534 Rpos = np.array([[c+(x**2)*(1-c), x*y*(1-c), y*s],
2535 [y*x*(1-c), c+(y**2)*(1-c), -x*s],
2536 [-y*s, x*s, c]])
2537 # opposite rotation negates all the sin terms
2538 Rneg = Rpos.copy()
2539 Rneg[[0, 1, 2, 2], [2, 2, 0, 1]] = \
2540 -Rneg[[0, 1, 2, 2], [2, 2, 0, 1]]
2542 # multiply them to get the rotated vector
2543 return Rpos.dot(uvw), Rneg.dot(uvw)
2545 had_data = self.has_data()
2547 # handle args
2548 argi = 6
2549 if len(args) < argi:
2550 raise ValueError('Wrong number of arguments. Expected %d got %d' %
2551 (argi, len(args)))
2553 # first 6 arguments are X, Y, Z, U, V, W
2554 input_args = args[:argi]
2556 # extract the masks, if any
2557 masks = [k.mask for k in input_args
2558 if isinstance(k, np.ma.MaskedArray)]
2559 # broadcast to match the shape
2560 bcast = np.broadcast_arrays(*input_args, *masks)
2561 input_args = bcast[:argi]
2562 masks = bcast[argi:]
2563 if masks:
2564 # combine the masks into one
2565 mask = reduce(np.logical_or, masks)
2566 # put mask on and compress
2567 input_args = [np.ma.array(k, mask=mask).compressed()
2568 for k in input_args]
2569 else:
2570 input_args = [np.ravel(k) for k in input_args]
2572 if any(len(v) == 0 for v in input_args):
2573 # No quivers, so just make an empty collection and return early
2574 linec = art3d.Line3DCollection([], *args[argi:], **kwargs)
2575 self.add_collection(linec)
2576 return linec
2578 shaft_dt = np.array([0., length], dtype=float)
2579 arrow_dt = shaft_dt * arrow_length_ratio
2581 cbook._check_in_list(['tail', 'middle', 'tip'], pivot=pivot)
2582 if pivot == 'tail':
2583 shaft_dt -= length
2584 elif pivot == 'middle':
2585 shaft_dt -= length / 2
2587 XYZ = np.column_stack(input_args[:3])
2588 UVW = np.column_stack(input_args[3:argi]).astype(float)
2590 # Normalize rows of UVW
2591 norm = np.linalg.norm(UVW, axis=1)
2593 # If any row of UVW is all zeros, don't make a quiver for it
2594 mask = norm > 0
2595 XYZ = XYZ[mask]
2596 if normalize:
2597 UVW = UVW[mask] / norm[mask].reshape((-1, 1))
2598 else:
2599 UVW = UVW[mask]
2601 if len(XYZ) > 0:
2602 # compute the shaft lines all at once with an outer product
2603 shafts = (XYZ - np.multiply.outer(shaft_dt, UVW)).swapaxes(0, 1)
2604 # compute head direction vectors, n heads x 2 sides x 3 dimensions
2605 head_dirs = np.array([calc_arrow(d) for d in UVW])
2606 # compute all head lines at once, starting from the shaft ends
2607 heads = shafts[:, :1] - np.multiply.outer(arrow_dt, head_dirs)
2608 # stack left and right head lines together
2609 heads.shape = (len(arrow_dt), -1, 3)
2610 # transpose to get a list of lines
2611 heads = heads.swapaxes(0, 1)
2613 lines = [*shafts, *heads]
2614 else:
2615 lines = []
2617 linec = art3d.Line3DCollection(lines, *args[argi:], **kwargs)
2618 self.add_collection(linec)
2620 self.auto_scale_xyz(XYZ[:, 0], XYZ[:, 1], XYZ[:, 2], had_data)
2622 return linec
2624 quiver3D = quiver
2626 def voxels(self, *args, facecolors=None, edgecolors=None, shade=True,
2627 lightsource=None, **kwargs):
2628 """
2629 ax.voxels([x, y, z,] /, filled, facecolors=None, edgecolors=None, \
2630**kwargs)
2632 Plot a set of filled voxels
2634 All voxels are plotted as 1x1x1 cubes on the axis, with
2635 ``filled[0, 0, 0]`` placed with its lower corner at the origin.
2636 Occluded faces are not plotted.
2638 .. versionadded:: 2.1
2640 Parameters
2641 ----------
2642 filled : 3D np.array of bool
2643 A 3d array of values, with truthy values indicating which voxels
2644 to fill
2646 x, y, z : 3D np.array, optional
2647 The coordinates of the corners of the voxels. This should broadcast
2648 to a shape one larger in every dimension than the shape of
2649 `filled`. These can be used to plot non-cubic voxels.
2651 If not specified, defaults to increasing integers along each axis,
2652 like those returned by :func:`~numpy.indices`.
2653 As indicated by the ``/`` in the function signature, these
2654 arguments can only be passed positionally.
2656 facecolors, edgecolors : array-like, optional
2657 The color to draw the faces and edges of the voxels. Can only be
2658 passed as keyword arguments.
2659 This parameter can be:
2661 - A single color value, to color all voxels the same color. This
2662 can be either a string, or a 1D rgb/rgba array
2663 - ``None``, the default, to use a single color for the faces, and
2664 the style default for the edges.
2665 - A 3D ndarray of color names, with each item the color for the
2666 corresponding voxel. The size must match the voxels.
2667 - A 4D ndarray of rgb/rgba data, with the components along the
2668 last axis.
2670 shade : bool
2671 Whether to shade the facecolors. Defaults to True. Shading is
2672 always disabled when *cmap* is specified.
2674 .. versionadded:: 3.1
2676 lightsource : `~matplotlib.colors.LightSource`
2677 The lightsource to use when *shade* is True.
2679 .. versionadded:: 3.1
2681 **kwargs
2682 Additional keyword arguments to pass onto
2683 :func:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection`
2685 Returns
2686 -------
2687 faces : dict
2688 A dictionary indexed by coordinate, where ``faces[i, j, k]`` is a
2689 `Poly3DCollection` of the faces drawn for the voxel
2690 ``filled[i, j, k]``. If no faces were drawn for a given voxel,
2691 either because it was not asked to be drawn, or it is fully
2692 occluded, then ``(i, j, k) not in faces``.
2694 Examples
2695 --------
2696 .. plot:: gallery/mplot3d/voxels.py
2697 .. plot:: gallery/mplot3d/voxels_rgb.py
2698 .. plot:: gallery/mplot3d/voxels_torus.py
2699 .. plot:: gallery/mplot3d/voxels_numpy_logo.py
2700 """
2702 # work out which signature we should be using, and use it to parse
2703 # the arguments. Name must be voxels for the correct error message
2704 if len(args) >= 3:
2705 # underscores indicate position only
2706 def voxels(__x, __y, __z, filled, **kwargs):
2707 return (__x, __y, __z), filled, kwargs
2708 else:
2709 def voxels(filled, **kwargs):
2710 return None, filled, kwargs
2712 xyz, filled, kwargs = voxels(*args, **kwargs)
2714 # check dimensions
2715 if filled.ndim != 3:
2716 raise ValueError("Argument filled must be 3-dimensional")
2717 size = np.array(filled.shape, dtype=np.intp)
2719 # check xyz coordinates, which are one larger than the filled shape
2720 coord_shape = tuple(size + 1)
2721 if xyz is None:
2722 x, y, z = np.indices(coord_shape)
2723 else:
2724 x, y, z = (np.broadcast_to(c, coord_shape) for c in xyz)
2726 def _broadcast_color_arg(color, name):
2727 if np.ndim(color) in (0, 1):
2728 # single color, like "red" or [1, 0, 0]
2729 return np.broadcast_to(color, filled.shape + np.shape(color))
2730 elif np.ndim(color) in (3, 4):
2731 # 3D array of strings, or 4D array with last axis rgb
2732 if np.shape(color)[:3] != filled.shape:
2733 raise ValueError(
2734 "When multidimensional, {} must match the shape of "
2735 "filled".format(name))
2736 return color
2737 else:
2738 raise ValueError("Invalid {} argument".format(name))
2740 # broadcast and default on facecolors
2741 if facecolors is None:
2742 facecolors = self._get_patches_for_fill.get_next_color()
2743 facecolors = _broadcast_color_arg(facecolors, 'facecolors')
2745 # broadcast but no default on edgecolors
2746 edgecolors = _broadcast_color_arg(edgecolors, 'edgecolors')
2748 # scale to the full array, even if the data is only in the center
2749 self.auto_scale_xyz(x, y, z)
2751 # points lying on corners of a square
2752 square = np.array([
2753 [0, 0, 0],
2754 [1, 0, 0],
2755 [1, 1, 0],
2756 [0, 1, 0],
2757 ], dtype=np.intp)
2759 voxel_faces = defaultdict(list)
2761 def permutation_matrices(n):
2762 """Generator of cyclic permutation matrices."""
2763 mat = np.eye(n, dtype=np.intp)
2764 for i in range(n):
2765 yield mat
2766 mat = np.roll(mat, 1, axis=0)
2768 # iterate over each of the YZ, ZX, and XY orientations, finding faces
2769 # to render
2770 for permute in permutation_matrices(3):
2771 # find the set of ranges to iterate over
2772 pc, qc, rc = permute.T.dot(size)
2773 pinds = np.arange(pc)
2774 qinds = np.arange(qc)
2775 rinds = np.arange(rc)
2777 square_rot_pos = square.dot(permute.T)
2778 square_rot_neg = square_rot_pos[::-1]
2780 # iterate within the current plane
2781 for p in pinds:
2782 for q in qinds:
2783 # iterate perpendicularly to the current plane, handling
2784 # boundaries. We only draw faces between a voxel and an
2785 # empty space, to avoid drawing internal faces.
2787 # draw lower faces
2788 p0 = permute.dot([p, q, 0])
2789 i0 = tuple(p0)
2790 if filled[i0]:
2791 voxel_faces[i0].append(p0 + square_rot_neg)
2793 # draw middle faces
2794 for r1, r2 in zip(rinds[:-1], rinds[1:]):
2795 p1 = permute.dot([p, q, r1])
2796 p2 = permute.dot([p, q, r2])
2798 i1 = tuple(p1)
2799 i2 = tuple(p2)
2801 if filled[i1] and not filled[i2]:
2802 voxel_faces[i1].append(p2 + square_rot_pos)
2803 elif not filled[i1] and filled[i2]:
2804 voxel_faces[i2].append(p2 + square_rot_neg)
2806 # draw upper faces
2807 pk = permute.dot([p, q, rc-1])
2808 pk2 = permute.dot([p, q, rc])
2809 ik = tuple(pk)
2810 if filled[ik]:
2811 voxel_faces[ik].append(pk2 + square_rot_pos)
2813 # iterate over the faces, and generate a Poly3DCollection for each
2814 # voxel
2815 polygons = {}
2816 for coord, faces_inds in voxel_faces.items():
2817 # convert indices into 3D positions
2818 if xyz is None:
2819 faces = faces_inds
2820 else:
2821 faces = []
2822 for face_inds in faces_inds:
2823 ind = face_inds[:, 0], face_inds[:, 1], face_inds[:, 2]
2824 face = np.empty(face_inds.shape)
2825 face[:, 0] = x[ind]
2826 face[:, 1] = y[ind]
2827 face[:, 2] = z[ind]
2828 faces.append(face)
2830 # shade the faces
2831 facecolor = facecolors[coord]
2832 edgecolor = edgecolors[coord]
2833 if shade:
2834 normals = self._generate_normals(faces)
2835 facecolor = self._shade_colors(facecolor, normals, lightsource)
2836 if edgecolor is not None:
2837 edgecolor = self._shade_colors(
2838 edgecolor, normals, lightsource
2839 )
2841 poly = art3d.Poly3DCollection(
2842 faces, facecolors=facecolor, edgecolors=edgecolor, **kwargs)
2843 self.add_collection3d(poly)
2844 polygons[coord] = poly
2846 return polygons
2849def get_test_data(delta=0.05):
2850 '''
2851 Return a tuple X, Y, Z with a test data set.
2852 '''
2853 x = y = np.arange(-3.0, 3.0, delta)
2854 X, Y = np.meshgrid(x, y)
2856 Z1 = np.exp(-(X**2 + Y**2) / 2) / (2 * np.pi)
2857 Z2 = (np.exp(-(((X - 1) / 1.5)**2 + ((Y - 1) / 0.5)**2) / 2) /
2858 (2 * np.pi * 0.5 * 1.5))
2859 Z = Z2 - Z1
2861 X = X * 10
2862 Y = Y * 10
2863 Z = Z * 500
2864 return X, Y, Z