Coverage for /usr/lib/python3/dist-packages/mpl_toolkits/mplot3d/art3d.py: 21%
433 statements
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
« prev ^ index » next coverage.py v7.4.4, created at 2025-06-14 15:25 +0200
1# art3d.py, original mplot3d version by John Porter
2# Parts rewritten by Reinier Heeres <reinier@heeres.eu>
3# Minor additions by Ben Axelrod <baxelrod@coroware.com>
5"""
6Module containing 3D artist code and functions to convert 2D
7artists into 3D versions which can be added to an Axes3D.
8"""
10import math
12import numpy as np
14from matplotlib import (
15 artist, cbook, colors as mcolors, lines, text as mtext, path as mpath)
16from matplotlib.collections import (
17 LineCollection, PolyCollection, PatchCollection, PathCollection)
18from matplotlib.colors import Normalize
19from matplotlib.patches import Patch
20from . import proj3d
23def _norm_angle(a):
24 """Return the given angle normalized to -180 < *a* <= 180 degrees."""
25 a = (a + 360) % 360
26 if a > 180:
27 a = a - 360
28 return a
31def _norm_text_angle(a):
32 """Return the given angle normalized to -90 < *a* <= 90 degrees."""
33 a = (a + 180) % 180
34 if a > 90:
35 a = a - 180
36 return a
39def get_dir_vector(zdir):
40 """
41 Return a direction vector.
43 Parameters
44 ----------
45 zdir : {'x', 'y', 'z', None, 3-tuple}
46 The direction. Possible values are:
48 - 'x': equivalent to (1, 0, 0)
49 - 'y': equivalent to (0, 1, 0)
50 - 'z': equivalent to (0, 0, 1)
51 - *None*: equivalent to (0, 0, 0)
52 - an iterable (x, y, z) is converted to a NumPy array, if not already
54 Returns
55 -------
56 x, y, z : array-like
57 The direction vector.
58 """
59 if zdir == 'x':
60 return np.array((1, 0, 0))
61 elif zdir == 'y':
62 return np.array((0, 1, 0))
63 elif zdir == 'z':
64 return np.array((0, 0, 1))
65 elif zdir is None:
66 return np.array((0, 0, 0))
67 elif np.iterable(zdir) and len(zdir) == 3:
68 return np.array(zdir)
69 else:
70 raise ValueError("'x', 'y', 'z', None or vector of length 3 expected")
73class Text3D(mtext.Text):
74 """
75 Text object with 3D position and direction.
77 Parameters
78 ----------
79 x, y, z
80 The position of the text.
81 text : str
82 The text string to display.
83 zdir : {'x', 'y', 'z', None, 3-tuple}
84 The direction of the text. See `.get_dir_vector` for a description of
85 the values.
87 Other Parameters
88 ----------------
89 **kwargs
90 All other parameters are passed on to `~matplotlib.text.Text`.
91 """
93 def __init__(self, x=0, y=0, z=0, text='', zdir='z', **kwargs):
94 mtext.Text.__init__(self, x, y, text, **kwargs)
95 self.set_3d_properties(z, zdir)
97 def get_position_3d(self):
98 """Return the (x, y, z) position of the text."""
99 return self._x, self._y, self._z
101 def set_position_3d(self, xyz, zdir=None):
102 """
103 Set the (*x*, *y*, *z*) position of the text.
105 Parameters
106 ----------
107 xyz : (float, float, float)
108 The position in 3D space.
109 zdir : {'x', 'y', 'z', None, 3-tuple}
110 The direction of the text. If unspecified, the zdir will not be
111 changed.
112 """
113 super().set_position(xyz[:2])
114 self.set_z(xyz[2])
115 if zdir is not None:
116 self._dir_vec = get_dir_vector(zdir)
118 def set_z(self, z):
119 """
120 Set the *z* position of the text.
122 Parameters
123 ----------
124 z : float
125 """
126 self._z = z
127 self.stale = True
129 def set_3d_properties(self, z=0, zdir='z'):
130 self._z = z
131 self._dir_vec = get_dir_vector(zdir)
132 self.stale = True
134 @artist.allow_rasterization
135 def draw(self, renderer):
136 position3d = np.array((self._x, self._y, self._z))
137 proj = proj3d.proj_trans_points(
138 [position3d, position3d + self._dir_vec], self.axes.M)
139 dx = proj[0][1] - proj[0][0]
140 dy = proj[1][1] - proj[1][0]
141 angle = math.degrees(math.atan2(dy, dx))
142 with cbook._setattr_cm(self, _x=proj[0][0], _y=proj[1][0],
143 _rotation=_norm_text_angle(angle)):
144 mtext.Text.draw(self, renderer)
145 self.stale = False
147 def get_tightbbox(self, renderer=None):
148 # Overwriting the 2d Text behavior which is not valid for 3d.
149 # For now, just return None to exclude from layout calculation.
150 return None
153def text_2d_to_3d(obj, z=0, zdir='z'):
154 """Convert a Text to a Text3D object."""
155 obj.__class__ = Text3D
156 obj.set_3d_properties(z, zdir)
159class Line3D(lines.Line2D):
160 """
161 3D line object.
162 """
164 def __init__(self, xs, ys, zs, *args, **kwargs):
165 """
166 Keyword arguments are passed onto :func:`~matplotlib.lines.Line2D`.
167 """
168 super().__init__([], [], *args, **kwargs)
169 self._verts3d = xs, ys, zs
171 def set_3d_properties(self, zs=0, zdir='z'):
172 xs = self.get_xdata()
173 ys = self.get_ydata()
174 zs = cbook._to_unmasked_float_array(zs).ravel()
175 zs = np.broadcast_to(zs, len(xs))
176 self._verts3d = juggle_axes(xs, ys, zs, zdir)
177 self.stale = True
179 def set_data_3d(self, *args):
180 """
181 Set the x, y and z data
183 Parameters
184 ----------
185 x : array-like
186 The x-data to be plotted.
187 y : array-like
188 The y-data to be plotted.
189 z : array-like
190 The z-data to be plotted.
192 Notes
193 -----
194 Accepts x, y, z arguments or a single array-like (x, y, z)
195 """
196 if len(args) == 1:
197 self._verts3d = args[0]
198 else:
199 self._verts3d = args
200 self.stale = True
202 def get_data_3d(self):
203 """
204 Get the current data
206 Returns
207 -------
208 verts3d : length-3 tuple or array-like
209 The current data as a tuple or array-like.
210 """
211 return self._verts3d
213 @artist.allow_rasterization
214 def draw(self, renderer):
215 xs3d, ys3d, zs3d = self._verts3d
216 xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, self.axes.M)
217 self.set_data(xs, ys)
218 super().draw(renderer)
219 self.stale = False
222def line_2d_to_3d(line, zs=0, zdir='z'):
223 """Convert a 2D line to 3D."""
225 line.__class__ = Line3D
226 line.set_3d_properties(zs, zdir)
229def _path_to_3d_segment(path, zs=0, zdir='z'):
230 """Convert a path to a 3D segment."""
232 zs = np.broadcast_to(zs, len(path))
233 pathsegs = path.iter_segments(simplify=False, curves=False)
234 seg = [(x, y, z) for (((x, y), code), z) in zip(pathsegs, zs)]
235 seg3d = [juggle_axes(x, y, z, zdir) for (x, y, z) in seg]
236 return seg3d
239def _paths_to_3d_segments(paths, zs=0, zdir='z'):
240 """Convert paths from a collection object to 3D segments."""
242 if not np.iterable(zs):
243 zs = np.broadcast_to(zs, len(paths))
244 else:
245 if len(zs) != len(paths):
246 raise ValueError('Number of z-coordinates does not match paths.')
248 segs = [_path_to_3d_segment(path, pathz, zdir)
249 for path, pathz in zip(paths, zs)]
250 return segs
253def _path_to_3d_segment_with_codes(path, zs=0, zdir='z'):
254 """Convert a path to a 3D segment with path codes."""
256 zs = np.broadcast_to(zs, len(path))
257 pathsegs = path.iter_segments(simplify=False, curves=False)
258 seg_codes = [((x, y, z), code) for ((x, y), code), z in zip(pathsegs, zs)]
259 if seg_codes:
260 seg, codes = zip(*seg_codes)
261 seg3d = [juggle_axes(x, y, z, zdir) for (x, y, z) in seg]
262 else:
263 seg3d = []
264 codes = []
265 return seg3d, list(codes)
268def _paths_to_3d_segments_with_codes(paths, zs=0, zdir='z'):
269 """
270 Convert paths from a collection object to 3D segments with path codes.
271 """
273 zs = np.broadcast_to(zs, len(paths))
274 segments_codes = [_path_to_3d_segment_with_codes(path, pathz, zdir)
275 for path, pathz in zip(paths, zs)]
276 if segments_codes:
277 segments, codes = zip(*segments_codes)
278 else:
279 segments, codes = [], []
280 return list(segments), list(codes)
283class Line3DCollection(LineCollection):
284 """
285 A collection of 3D lines.
286 """
288 def set_sort_zpos(self, val):
289 """Set the position to use for z-sorting."""
290 self._sort_zpos = val
291 self.stale = True
293 def set_segments(self, segments):
294 """
295 Set 3D segments.
296 """
297 self._segments3d = segments
298 super().set_segments([])
300 def do_3d_projection(self):
301 """
302 Project the points according to renderer matrix.
303 """
304 xyslist = [proj3d.proj_trans_points(points, self.axes.M)
305 for points in self._segments3d]
306 segments_2d = [np.column_stack([xs, ys]) for xs, ys, zs in xyslist]
307 LineCollection.set_segments(self, segments_2d)
309 # FIXME
310 minz = 1e9
311 for xs, ys, zs in xyslist:
312 minz = min(minz, min(zs))
313 return minz
316def line_collection_2d_to_3d(col, zs=0, zdir='z'):
317 """Convert a LineCollection to a Line3DCollection object."""
318 segments3d = _paths_to_3d_segments(col.get_paths(), zs, zdir)
319 col.__class__ = Line3DCollection
320 col.set_segments(segments3d)
323class Patch3D(Patch):
324 """
325 3D patch object.
326 """
328 def __init__(self, *args, zs=(), zdir='z', **kwargs):
329 super().__init__(*args, **kwargs)
330 self.set_3d_properties(zs, zdir)
332 def set_3d_properties(self, verts, zs=0, zdir='z'):
333 zs = np.broadcast_to(zs, len(verts))
334 self._segment3d = [juggle_axes(x, y, z, zdir)
335 for ((x, y), z) in zip(verts, zs)]
337 def get_path(self):
338 return self._path2d
340 def do_3d_projection(self):
341 s = self._segment3d
342 xs, ys, zs = zip(*s)
343 vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs,
344 self.axes.M)
345 self._path2d = mpath.Path(np.column_stack([vxs, vys]))
346 return min(vzs)
349class PathPatch3D(Patch3D):
350 """
351 3D PathPatch object.
352 """
354 def __init__(self, path, *, zs=(), zdir='z', **kwargs):
355 # Not super().__init__!
356 Patch.__init__(self, **kwargs)
357 self.set_3d_properties(path, zs, zdir)
359 def set_3d_properties(self, path, zs=0, zdir='z'):
360 Patch3D.set_3d_properties(self, path.vertices, zs=zs, zdir=zdir)
361 self._code3d = path.codes
363 def do_3d_projection(self):
364 s = self._segment3d
365 xs, ys, zs = zip(*s)
366 vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs,
367 self.axes.M)
368 self._path2d = mpath.Path(np.column_stack([vxs, vys]), self._code3d)
369 return min(vzs)
372def _get_patch_verts(patch):
373 """Return a list of vertices for the path of a patch."""
374 trans = patch.get_patch_transform()
375 path = patch.get_path()
376 polygons = path.to_polygons(trans)
377 return polygons[0] if len(polygons) else np.array([])
380def patch_2d_to_3d(patch, z=0, zdir='z'):
381 """Convert a Patch to a Patch3D object."""
382 verts = _get_patch_verts(patch)
383 patch.__class__ = Patch3D
384 patch.set_3d_properties(verts, z, zdir)
387def pathpatch_2d_to_3d(pathpatch, z=0, zdir='z'):
388 """Convert a PathPatch to a PathPatch3D object."""
389 path = pathpatch.get_path()
390 trans = pathpatch.get_patch_transform()
392 mpath = trans.transform_path(path)
393 pathpatch.__class__ = PathPatch3D
394 pathpatch.set_3d_properties(mpath, z, zdir)
397class Patch3DCollection(PatchCollection):
398 """
399 A collection of 3D patches.
400 """
402 def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs):
403 """
404 Create a collection of flat 3D patches with its normal vector
405 pointed in *zdir* direction, and located at *zs* on the *zdir*
406 axis. 'zs' can be a scalar or an array-like of the same length as
407 the number of patches in the collection.
409 Constructor arguments are the same as for
410 :class:`~matplotlib.collections.PatchCollection`. In addition,
411 keywords *zs=0* and *zdir='z'* are available.
413 Also, the keyword argument *depthshade* is available to indicate
414 whether to shade the patches in order to give the appearance of depth
415 (default is *True*). This is typically desired in scatter plots.
416 """
417 self._depthshade = depthshade
418 super().__init__(*args, **kwargs)
419 self.set_3d_properties(zs, zdir)
421 def get_depthshade(self):
422 return self._depthshade
424 def set_depthshade(self, depthshade):
425 """
426 Set whether depth shading is performed on collection members.
428 Parameters
429 ----------
430 depthshade : bool
431 Whether to shade the patches in order to give the appearance of
432 depth.
433 """
434 self._depthshade = depthshade
435 self.stale = True
437 def set_sort_zpos(self, val):
438 """Set the position to use for z-sorting."""
439 self._sort_zpos = val
440 self.stale = True
442 def set_3d_properties(self, zs, zdir):
443 # Force the collection to initialize the face and edgecolors
444 # just in case it is a scalarmappable with a colormap.
445 self.update_scalarmappable()
446 offsets = self.get_offsets()
447 if len(offsets) > 0:
448 xs, ys = offsets.T
449 else:
450 xs = []
451 ys = []
452 self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir)
453 self._z_markers_idx = slice(-1)
454 self._vzs = None
455 self.stale = True
457 def do_3d_projection(self):
458 xs, ys, zs = self._offsets3d
459 vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs,
460 self.axes.M)
461 self._vzs = vzs
462 super().set_offsets(np.column_stack([vxs, vys]))
464 if vzs.size > 0:
465 return min(vzs)
466 else:
467 return np.nan
469 def _maybe_depth_shade_and_sort_colors(self, color_array):
470 color_array = (
471 _zalpha(color_array, self._vzs)
472 if self._vzs is not None and self._depthshade
473 else color_array
474 )
475 if len(color_array) > 1:
476 color_array = color_array[self._z_markers_idx]
477 return mcolors.to_rgba_array(color_array, self._alpha)
479 def get_facecolor(self):
480 return self._maybe_depth_shade_and_sort_colors(super().get_facecolor())
482 def get_edgecolor(self):
483 # We need this check here to make sure we do not double-apply the depth
484 # based alpha shading when the edge color is "face" which means the
485 # edge colour should be identical to the face colour.
486 if cbook._str_equal(self._edgecolors, 'face'):
487 return self.get_facecolor()
488 return self._maybe_depth_shade_and_sort_colors(super().get_edgecolor())
491class Path3DCollection(PathCollection):
492 """
493 A collection of 3D paths.
494 """
496 def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs):
497 """
498 Create a collection of flat 3D paths with its normal vector
499 pointed in *zdir* direction, and located at *zs* on the *zdir*
500 axis. 'zs' can be a scalar or an array-like of the same length as
501 the number of paths in the collection.
503 Constructor arguments are the same as for
504 :class:`~matplotlib.collections.PathCollection`. In addition,
505 keywords *zs=0* and *zdir='z'* are available.
507 Also, the keyword argument *depthshade* is available to indicate
508 whether to shade the patches in order to give the appearance of depth
509 (default is *True*). This is typically desired in scatter plots.
510 """
511 self._depthshade = depthshade
512 self._in_draw = False
513 super().__init__(*args, **kwargs)
514 self.set_3d_properties(zs, zdir)
516 def draw(self, renderer):
517 with cbook._setattr_cm(self, _in_draw=True):
518 super().draw(renderer)
520 def set_sort_zpos(self, val):
521 """Set the position to use for z-sorting."""
522 self._sort_zpos = val
523 self.stale = True
525 def set_3d_properties(self, zs, zdir):
526 # Force the collection to initialize the face and edgecolors
527 # just in case it is a scalarmappable with a colormap.
528 self.update_scalarmappable()
529 offsets = self.get_offsets()
530 if len(offsets) > 0:
531 xs, ys = offsets.T
532 else:
533 xs = []
534 ys = []
535 self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir)
536 # In the base draw methods we access the attributes directly which
537 # means we can not resolve the shuffling in the getter methods like
538 # we do for the edge and face colors.
539 #
540 # This means we need to carry around a cache of the unsorted sizes and
541 # widths (postfixed with 3d) and in `do_3d_projection` set the
542 # depth-sorted version of that data into the private state used by the
543 # base collection class in its draw method.
544 #
545 # Grab the current sizes and linewidths to preserve them.
546 self._sizes3d = self._sizes
547 self._linewidths3d = np.array(self._linewidths)
548 xs, ys, zs = self._offsets3d
550 # Sort the points based on z coordinates
551 # Performance optimization: Create a sorted index array and reorder
552 # points and point properties according to the index array
553 self._z_markers_idx = slice(-1)
554 self._vzs = None
555 self.stale = True
557 def set_sizes(self, sizes, dpi=72.0):
558 super().set_sizes(sizes, dpi)
559 if not self._in_draw:
560 self._sizes3d = sizes
562 def set_linewidth(self, lw):
563 super().set_linewidth(lw)
564 if not self._in_draw:
565 self._linewidths3d = np.array(self._linewidths)
567 def get_depthshade(self):
568 return self._depthshade
570 def set_depthshade(self, depthshade):
571 """
572 Set whether depth shading is performed on collection members.
574 Parameters
575 ----------
576 depthshade : bool
577 Whether to shade the patches in order to give the appearance of
578 depth.
579 """
580 self._depthshade = depthshade
581 self.stale = True
583 def do_3d_projection(self):
584 xs, ys, zs = self._offsets3d
585 vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs,
586 self.axes.M)
587 # Sort the points based on z coordinates
588 # Performance optimization: Create a sorted index array and reorder
589 # points and point properties according to the index array
590 z_markers_idx = self._z_markers_idx = np.argsort(vzs)[::-1]
591 self._vzs = vzs
593 # we have to special case the sizes because of code in collections.py
594 # as the draw method does
595 # self.set_sizes(self._sizes, self.figure.dpi)
596 # so we can not rely on doing the sorting on the way out via get_*
598 if len(self._sizes3d) > 1:
599 self._sizes = self._sizes3d[z_markers_idx]
601 if len(self._linewidths3d) > 1:
602 self._linewidths = self._linewidths3d[z_markers_idx]
604 # Re-order items
605 vzs = vzs[z_markers_idx]
606 vxs = vxs[z_markers_idx]
607 vys = vys[z_markers_idx]
609 PathCollection.set_offsets(self, np.column_stack((vxs, vys)))
611 return np.min(vzs) if vzs.size else np.nan
613 def _maybe_depth_shade_and_sort_colors(self, color_array):
614 color_array = (
615 _zalpha(color_array, self._vzs)
616 if self._vzs is not None and self._depthshade
617 else color_array
618 )
619 if len(color_array) > 1:
620 color_array = color_array[self._z_markers_idx]
621 return mcolors.to_rgba_array(color_array, self._alpha)
623 def get_facecolor(self):
624 return self._maybe_depth_shade_and_sort_colors(super().get_facecolor())
626 def get_edgecolor(self):
627 # We need this check here to make sure we do not double-apply the depth
628 # based alpha shading when the edge color is "face" which means the
629 # edge colour should be identical to the face colour.
630 if cbook._str_equal(self._edgecolors, 'face'):
631 return self.get_facecolor()
632 return self._maybe_depth_shade_and_sort_colors(super().get_edgecolor())
635def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True):
636 """
637 Convert a :class:`~matplotlib.collections.PatchCollection` into a
638 :class:`Patch3DCollection` object
639 (or a :class:`~matplotlib.collections.PathCollection` into a
640 :class:`Path3DCollection` object).
642 Parameters
643 ----------
644 za
645 The location or locations to place the patches in the collection along
646 the *zdir* axis. Default: 0.
647 zdir
648 The axis in which to place the patches. Default: "z".
649 depthshade
650 Whether to shade the patches to give a sense of depth. Default: *True*.
652 """
653 if isinstance(col, PathCollection):
654 col.__class__ = Path3DCollection
655 elif isinstance(col, PatchCollection):
656 col.__class__ = Patch3DCollection
657 col._depthshade = depthshade
658 col._in_draw = False
659 col.set_3d_properties(zs, zdir)
662class Poly3DCollection(PolyCollection):
663 """
664 A collection of 3D polygons.
666 .. note::
667 **Filling of 3D polygons**
669 There is no simple definition of the enclosed surface of a 3D polygon
670 unless the polygon is planar.
672 In practice, Matplotlib fills the 2D projection of the polygon. This
673 gives a correct filling appearance only for planar polygons. For all
674 other polygons, you'll find orientations in which the edges of the
675 polygon intersect in the projection. This will lead to an incorrect
676 visualization of the 3D area.
678 If you need filled areas, it is recommended to create them via
679 `~mpl_toolkits.mplot3d.axes3d.Axes3D.plot_trisurf`, which creates a
680 triangulation and thus generates consistent surfaces.
681 """
683 def __init__(self, verts, *args, zsort='average', **kwargs):
684 """
685 Parameters
686 ----------
687 verts : list of (N, 3) array-like
688 Each element describes a polygon as a sequence of ``N_i`` points
689 ``(x, y, z)``.
690 zsort : {'average', 'min', 'max'}, default: 'average'
691 The calculation method for the z-order.
692 See `~.Poly3DCollection.set_zsort` for details.
693 *args, **kwargs
694 All other parameters are forwarded to `.PolyCollection`.
696 Notes
697 -----
698 Note that this class does a bit of magic with the _facecolors
699 and _edgecolors properties.
700 """
701 super().__init__(verts, *args, **kwargs)
702 if isinstance(verts, np.ndarray):
703 if verts.ndim != 3:
704 raise ValueError('verts must be a list of (N, 3) array-like')
705 else:
706 if any(len(np.shape(vert)) != 2 for vert in verts):
707 raise ValueError('verts must be a list of (N, 3) array-like')
708 self.set_zsort(zsort)
709 self._codes3d = None
711 _zsort_functions = {
712 'average': np.average,
713 'min': np.min,
714 'max': np.max,
715 }
717 def set_zsort(self, zsort):
718 """
719 Set the calculation method for the z-order.
721 Parameters
722 ----------
723 zsort : {'average', 'min', 'max'}
724 The function applied on the z-coordinates of the vertices in the
725 viewer's coordinate system, to determine the z-order.
726 """
727 self._zsortfunc = self._zsort_functions[zsort]
728 self._sort_zpos = None
729 self.stale = True
731 def get_vector(self, segments3d):
732 """Optimize points for projection."""
733 if len(segments3d):
734 xs, ys, zs = np.row_stack(segments3d).T
735 else: # row_stack can't stack zero arrays.
736 xs, ys, zs = [], [], []
737 ones = np.ones(len(xs))
738 self._vec = np.array([xs, ys, zs, ones])
740 indices = [0, *np.cumsum([len(segment) for segment in segments3d])]
741 self._segslices = [*map(slice, indices[:-1], indices[1:])]
743 def set_verts(self, verts, closed=True):
744 """Set 3D vertices."""
745 self.get_vector(verts)
746 # 2D verts will be updated at draw time
747 super().set_verts([], False)
748 self._closed = closed
750 def set_verts_and_codes(self, verts, codes):
751 """Set 3D vertices with path codes."""
752 # set vertices with closed=False to prevent PolyCollection from
753 # setting path codes
754 self.set_verts(verts, closed=False)
755 # and set our own codes instead.
756 self._codes3d = codes
758 def set_3d_properties(self):
759 # Force the collection to initialize the face and edgecolors
760 # just in case it is a scalarmappable with a colormap.
761 self.update_scalarmappable()
762 self._sort_zpos = None
763 self.set_zsort('average')
764 self._facecolor3d = PolyCollection.get_facecolor(self)
765 self._edgecolor3d = PolyCollection.get_edgecolor(self)
766 self._alpha3d = PolyCollection.get_alpha(self)
767 self.stale = True
769 def set_sort_zpos(self, val):
770 """Set the position to use for z-sorting."""
771 self._sort_zpos = val
772 self.stale = True
774 def do_3d_projection(self):
775 """
776 Perform the 3D projection for this object.
777 """
778 if self._A is not None:
779 # force update of color mapping because we re-order them
780 # below. If we do not do this here, the 2D draw will call
781 # this, but we will never port the color mapped values back
782 # to the 3D versions.
783 #
784 # We hold the 3D versions in a fixed order (the order the user
785 # passed in) and sort the 2D version by view depth.
786 self.update_scalarmappable()
787 if self._face_is_mapped:
788 self._facecolor3d = self._facecolors
789 if self._edge_is_mapped:
790 self._edgecolor3d = self._edgecolors
791 txs, tys, tzs = proj3d._proj_transform_vec(self._vec, self.axes.M)
792 xyzlist = [(txs[sl], tys[sl], tzs[sl]) for sl in self._segslices]
794 # This extra fuss is to re-order face / edge colors
795 cface = self._facecolor3d
796 cedge = self._edgecolor3d
797 if len(cface) != len(xyzlist):
798 cface = cface.repeat(len(xyzlist), axis=0)
799 if len(cedge) != len(xyzlist):
800 if len(cedge) == 0:
801 cedge = cface
802 else:
803 cedge = cedge.repeat(len(xyzlist), axis=0)
805 if xyzlist:
806 # sort by depth (furthest drawn first)
807 z_segments_2d = sorted(
808 ((self._zsortfunc(zs), np.column_stack([xs, ys]), fc, ec, idx)
809 for idx, ((xs, ys, zs), fc, ec)
810 in enumerate(zip(xyzlist, cface, cedge))),
811 key=lambda x: x[0], reverse=True)
813 _, segments_2d, self._facecolors2d, self._edgecolors2d, idxs = \
814 zip(*z_segments_2d)
815 else:
816 segments_2d = []
817 self._facecolors2d = np.empty((0, 4))
818 self._edgecolors2d = np.empty((0, 4))
819 idxs = []
821 if self._codes3d is not None:
822 codes = [self._codes3d[idx] for idx in idxs]
823 PolyCollection.set_verts_and_codes(self, segments_2d, codes)
824 else:
825 PolyCollection.set_verts(self, segments_2d, self._closed)
827 if len(self._edgecolor3d) != len(cface):
828 self._edgecolors2d = self._edgecolor3d
830 # Return zorder value
831 if self._sort_zpos is not None:
832 zvec = np.array([[0], [0], [self._sort_zpos], [1]])
833 ztrans = proj3d._proj_transform_vec(zvec, self.axes.M)
834 return ztrans[2][0]
835 elif tzs.size > 0:
836 # FIXME: Some results still don't look quite right.
837 # In particular, examine contourf3d_demo2.py
838 # with az = -54 and elev = -45.
839 return np.min(tzs)
840 else:
841 return np.nan
843 def set_facecolor(self, colors):
844 # docstring inherited
845 super().set_facecolor(colors)
846 self._facecolor3d = PolyCollection.get_facecolor(self)
848 def set_edgecolor(self, colors):
849 # docstring inherited
850 super().set_edgecolor(colors)
851 self._edgecolor3d = PolyCollection.get_edgecolor(self)
853 def set_alpha(self, alpha):
854 # docstring inherited
855 artist.Artist.set_alpha(self, alpha)
856 try:
857 self._facecolor3d = mcolors.to_rgba_array(
858 self._facecolor3d, self._alpha)
859 except (AttributeError, TypeError, IndexError):
860 pass
861 try:
862 self._edgecolors = mcolors.to_rgba_array(
863 self._edgecolor3d, self._alpha)
864 except (AttributeError, TypeError, IndexError):
865 pass
866 self.stale = True
868 def get_facecolor(self):
869 # docstring inherited
870 # self._facecolors2d is not initialized until do_3d_projection
871 if not hasattr(self, '_facecolors2d'):
872 self.axes.M = self.axes.get_proj()
873 self.do_3d_projection()
874 return self._facecolors2d
876 def get_edgecolor(self):
877 # docstring inherited
878 # self._edgecolors2d is not initialized until do_3d_projection
879 if not hasattr(self, '_edgecolors2d'):
880 self.axes.M = self.axes.get_proj()
881 self.do_3d_projection()
882 return self._edgecolors2d
885def poly_collection_2d_to_3d(col, zs=0, zdir='z'):
886 """Convert a PolyCollection to a Poly3DCollection object."""
887 segments_3d, codes = _paths_to_3d_segments_with_codes(
888 col.get_paths(), zs, zdir)
889 col.__class__ = Poly3DCollection
890 col.set_verts_and_codes(segments_3d, codes)
891 col.set_3d_properties()
894def juggle_axes(xs, ys, zs, zdir):
895 """
896 Reorder coordinates so that 2D xs, ys can be plotted in the plane
897 orthogonal to zdir. zdir is normally x, y or z. However, if zdir
898 starts with a '-' it is interpreted as a compensation for rotate_axes.
899 """
900 if zdir == 'x':
901 return zs, xs, ys
902 elif zdir == 'y':
903 return xs, zs, ys
904 elif zdir[0] == '-':
905 return rotate_axes(xs, ys, zs, zdir)
906 else:
907 return xs, ys, zs
910def rotate_axes(xs, ys, zs, zdir):
911 """
912 Reorder coordinates so that the axes are rotated with zdir along
913 the original z axis. Prepending the axis with a '-' does the
914 inverse transform, so zdir can be x, -x, y, -y, z or -z
915 """
916 if zdir == 'x':
917 return ys, zs, xs
918 elif zdir == '-x':
919 return zs, xs, ys
921 elif zdir == 'y':
922 return zs, xs, ys
923 elif zdir == '-y':
924 return ys, zs, xs
926 else:
927 return xs, ys, zs
930def _zalpha(colors, zs):
931 """Modify the alphas of the color list according to depth."""
932 # FIXME: This only works well if the points for *zs* are well-spaced
933 # in all three dimensions. Otherwise, at certain orientations,
934 # the min and max zs are very close together.
935 # Should really normalize against the viewing depth.
936 if len(colors) == 0 or len(zs) == 0:
937 return np.zeros((0, 4))
938 norm = Normalize(min(zs), max(zs))
939 sats = 1 - norm(zs) * 0.7
940 rgba = np.broadcast_to(mcolors.to_rgba_array(colors), (len(zs), 4))
941 return np.column_stack([rgba[:, :3], rgba[:, 3] * sats])