aboutsummaryrefslogtreecommitdiffstats
path: root/contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/art3d.py
diff options
context:
space:
mode:
authorshumkovnd <shumkovnd@yandex-team.com>2023-11-10 14:39:34 +0300
committershumkovnd <shumkovnd@yandex-team.com>2023-11-10 16:42:24 +0300
commit77eb2d3fdcec5c978c64e025ced2764c57c00285 (patch)
treec51edb0748ca8d4a08d7c7323312c27ba1a8b79a /contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/art3d.py
parentdd6d20cadb65582270ac23f4b3b14ae189704b9d (diff)
downloadydb-77eb2d3fdcec5c978c64e025ced2764c57c00285.tar.gz
KIKIMR-19287: add task_stats_drawing script
Diffstat (limited to 'contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/art3d.py')
-rw-r--r--contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/art3d.py1252
1 files changed, 1252 insertions, 0 deletions
diff --git a/contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/art3d.py b/contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/art3d.py
new file mode 100644
index 0000000000..4aff115b0c
--- /dev/null
+++ b/contrib/python/matplotlib/py3/mpl_toolkits/mplot3d/art3d.py
@@ -0,0 +1,1252 @@
+# art3d.py, original mplot3d version by John Porter
+# Parts rewritten by Reinier Heeres <reinier@heeres.eu>
+# Minor additions by Ben Axelrod <baxelrod@coroware.com>
+
+"""
+Module containing 3D artist code and functions to convert 2D
+artists into 3D versions which can be added to an Axes3D.
+"""
+
+import math
+
+import numpy as np
+
+from contextlib import contextmanager
+
+from matplotlib import (
+ artist, cbook, colors as mcolors, lines, text as mtext,
+ path as mpath)
+from matplotlib.collections import (
+ Collection, LineCollection, PolyCollection, PatchCollection, PathCollection)
+from matplotlib.colors import Normalize
+from matplotlib.patches import Patch
+from . import proj3d
+
+
+def _norm_angle(a):
+ """Return the given angle normalized to -180 < *a* <= 180 degrees."""
+ a = (a + 360) % 360
+ if a > 180:
+ a = a - 360
+ return a
+
+
+def _norm_text_angle(a):
+ """Return the given angle normalized to -90 < *a* <= 90 degrees."""
+ a = (a + 180) % 180
+ if a > 90:
+ a = a - 180
+ return a
+
+
+def get_dir_vector(zdir):
+ """
+ Return a direction vector.
+
+ Parameters
+ ----------
+ zdir : {'x', 'y', 'z', None, 3-tuple}
+ The direction. Possible values are:
+
+ - 'x': equivalent to (1, 0, 0)
+ - 'y': equivalent to (0, 1, 0)
+ - 'z': equivalent to (0, 0, 1)
+ - *None*: equivalent to (0, 0, 0)
+ - an iterable (x, y, z) is converted to an array
+
+ Returns
+ -------
+ x, y, z : array
+ The direction vector.
+ """
+ if zdir == 'x':
+ return np.array((1, 0, 0))
+ elif zdir == 'y':
+ return np.array((0, 1, 0))
+ elif zdir == 'z':
+ return np.array((0, 0, 1))
+ elif zdir is None:
+ return np.array((0, 0, 0))
+ elif np.iterable(zdir) and len(zdir) == 3:
+ return np.array(zdir)
+ else:
+ raise ValueError("'x', 'y', 'z', None or vector of length 3 expected")
+
+
+class Text3D(mtext.Text):
+ """
+ Text object with 3D position and direction.
+
+ Parameters
+ ----------
+ x, y, z : float
+ The position of the text.
+ text : str
+ The text string to display.
+ zdir : {'x', 'y', 'z', None, 3-tuple}
+ The direction of the text. See `.get_dir_vector` for a description of
+ the values.
+
+ Other Parameters
+ ----------------
+ **kwargs
+ All other parameters are passed on to `~matplotlib.text.Text`.
+ """
+
+ def __init__(self, x=0, y=0, z=0, text='', zdir='z', **kwargs):
+ mtext.Text.__init__(self, x, y, text, **kwargs)
+ self.set_3d_properties(z, zdir)
+
+ def get_position_3d(self):
+ """Return the (x, y, z) position of the text."""
+ return self._x, self._y, self._z
+
+ def set_position_3d(self, xyz, zdir=None):
+ """
+ Set the (*x*, *y*, *z*) position of the text.
+
+ Parameters
+ ----------
+ xyz : (float, float, float)
+ The position in 3D space.
+ zdir : {'x', 'y', 'z', None, 3-tuple}
+ The direction of the text. If unspecified, the *zdir* will not be
+ changed. See `.get_dir_vector` for a description of the values.
+ """
+ super().set_position(xyz[:2])
+ self.set_z(xyz[2])
+ if zdir is not None:
+ self._dir_vec = get_dir_vector(zdir)
+
+ def set_z(self, z):
+ """
+ Set the *z* position of the text.
+
+ Parameters
+ ----------
+ z : float
+ """
+ self._z = z
+ self.stale = True
+
+ def set_3d_properties(self, z=0, zdir='z'):
+ """
+ Set the *z* position and direction of the text.
+
+ Parameters
+ ----------
+ z : float
+ The z-position in 3D space.
+ zdir : {'x', 'y', 'z', 3-tuple}
+ The direction of the text. Default: 'z'.
+ See `.get_dir_vector` for a description of the values.
+ """
+ self._z = z
+ self._dir_vec = get_dir_vector(zdir)
+ self.stale = True
+
+ @artist.allow_rasterization
+ def draw(self, renderer):
+ position3d = np.array((self._x, self._y, self._z))
+ proj = proj3d._proj_trans_points(
+ [position3d, position3d + self._dir_vec], self.axes.M)
+ dx = proj[0][1] - proj[0][0]
+ dy = proj[1][1] - proj[1][0]
+ angle = math.degrees(math.atan2(dy, dx))
+ with cbook._setattr_cm(self, _x=proj[0][0], _y=proj[1][0],
+ _rotation=_norm_text_angle(angle)):
+ mtext.Text.draw(self, renderer)
+ self.stale = False
+
+ def get_tightbbox(self, renderer=None):
+ # Overwriting the 2d Text behavior which is not valid for 3d.
+ # For now, just return None to exclude from layout calculation.
+ return None
+
+
+def text_2d_to_3d(obj, z=0, zdir='z'):
+ """
+ Convert a `.Text` to a `.Text3D` object.
+
+ Parameters
+ ----------
+ z : float
+ The z-position in 3D space.
+ zdir : {'x', 'y', 'z', 3-tuple}
+ The direction of the text. Default: 'z'.
+ See `.get_dir_vector` for a description of the values.
+ """
+ obj.__class__ = Text3D
+ obj.set_3d_properties(z, zdir)
+
+
+class Line3D(lines.Line2D):
+ """
+ 3D line object.
+
+ .. note:: Use `get_data_3d` to obtain the data associated with the line.
+ `~.Line2D.get_data`, `~.Line2D.get_xdata`, and `~.Line2D.get_ydata` return
+ the x- and y-coordinates of the projected 2D-line, not the x- and y-data of
+ the 3D-line. Similarly, use `set_data_3d` to set the data, not
+ `~.Line2D.set_data`, `~.Line2D.set_xdata`, and `~.Line2D.set_ydata`.
+ """
+
+ def __init__(self, xs, ys, zs, *args, **kwargs):
+ """
+
+ Parameters
+ ----------
+ xs : array-like
+ The x-data to be plotted.
+ ys : array-like
+ The y-data to be plotted.
+ zs : array-like
+ The z-data to be plotted.
+ *args, **kwargs
+ Additional arguments are passed to `~matplotlib.lines.Line2D`.
+ """
+ super().__init__([], [], *args, **kwargs)
+ self.set_data_3d(xs, ys, zs)
+
+ def set_3d_properties(self, zs=0, zdir='z'):
+ """
+ Set the *z* position and direction of the line.
+
+ Parameters
+ ----------
+ zs : float or array of floats
+ The location along the *zdir* axis in 3D space to position the
+ line.
+ zdir : {'x', 'y', 'z'}
+ Plane to plot line orthogonal to. Default: 'z'.
+ See `.get_dir_vector` for a description of the values.
+ """
+ xs = self.get_xdata()
+ ys = self.get_ydata()
+ zs = cbook._to_unmasked_float_array(zs).ravel()
+ zs = np.broadcast_to(zs, len(xs))
+ self._verts3d = juggle_axes(xs, ys, zs, zdir)
+ self.stale = True
+
+ def set_data_3d(self, *args):
+ """
+ Set the x, y and z data
+
+ Parameters
+ ----------
+ x : array-like
+ The x-data to be plotted.
+ y : array-like
+ The y-data to be plotted.
+ z : array-like
+ The z-data to be plotted.
+
+ Notes
+ -----
+ Accepts x, y, z arguments or a single array-like (x, y, z)
+ """
+ if len(args) == 1:
+ args = args[0]
+ for name, xyz in zip('xyz', args):
+ if not np.iterable(xyz):
+ raise RuntimeError(f'{name} must be a sequence')
+ self._verts3d = args
+ self.stale = True
+
+ def get_data_3d(self):
+ """
+ Get the current data
+
+ Returns
+ -------
+ verts3d : length-3 tuple or array-like
+ The current data as a tuple or array-like.
+ """
+ return self._verts3d
+
+ @artist.allow_rasterization
+ def draw(self, renderer):
+ xs3d, ys3d, zs3d = self._verts3d
+ xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, self.axes.M)
+ self.set_data(xs, ys)
+ super().draw(renderer)
+ self.stale = False
+
+
+def line_2d_to_3d(line, zs=0, zdir='z'):
+ """
+ Convert a `.Line2D` to a `.Line3D` object.
+
+ Parameters
+ ----------
+ zs : float
+ The location along the *zdir* axis in 3D space to position the line.
+ zdir : {'x', 'y', 'z'}
+ Plane to plot line orthogonal to. Default: 'z'.
+ See `.get_dir_vector` for a description of the values.
+ """
+
+ line.__class__ = Line3D
+ line.set_3d_properties(zs, zdir)
+
+
+def _path_to_3d_segment(path, zs=0, zdir='z'):
+ """Convert a path to a 3D segment."""
+
+ zs = np.broadcast_to(zs, len(path))
+ pathsegs = path.iter_segments(simplify=False, curves=False)
+ seg = [(x, y, z) for (((x, y), code), z) in zip(pathsegs, zs)]
+ seg3d = [juggle_axes(x, y, z, zdir) for (x, y, z) in seg]
+ return seg3d
+
+
+def _paths_to_3d_segments(paths, zs=0, zdir='z'):
+ """Convert paths from a collection object to 3D segments."""
+
+ if not np.iterable(zs):
+ zs = np.broadcast_to(zs, len(paths))
+ else:
+ if len(zs) != len(paths):
+ raise ValueError('Number of z-coordinates does not match paths.')
+
+ segs = [_path_to_3d_segment(path, pathz, zdir)
+ for path, pathz in zip(paths, zs)]
+ return segs
+
+
+def _path_to_3d_segment_with_codes(path, zs=0, zdir='z'):
+ """Convert a path to a 3D segment with path codes."""
+
+ zs = np.broadcast_to(zs, len(path))
+ pathsegs = path.iter_segments(simplify=False, curves=False)
+ seg_codes = [((x, y, z), code) for ((x, y), code), z in zip(pathsegs, zs)]
+ if seg_codes:
+ seg, codes = zip(*seg_codes)
+ seg3d = [juggle_axes(x, y, z, zdir) for (x, y, z) in seg]
+ else:
+ seg3d = []
+ codes = []
+ return seg3d, list(codes)
+
+
+def _paths_to_3d_segments_with_codes(paths, zs=0, zdir='z'):
+ """
+ Convert paths from a collection object to 3D segments with path codes.
+ """
+
+ zs = np.broadcast_to(zs, len(paths))
+ segments_codes = [_path_to_3d_segment_with_codes(path, pathz, zdir)
+ for path, pathz in zip(paths, zs)]
+ if segments_codes:
+ segments, codes = zip(*segments_codes)
+ else:
+ segments, codes = [], []
+ return list(segments), list(codes)
+
+
+class Collection3D(Collection):
+ """A collection of 3D paths."""
+
+ def do_3d_projection(self):
+ """Project the points according to renderer matrix."""
+ xyzs_list = [proj3d.proj_transform(*vs.T, self.axes.M)
+ for vs, _ in self._3dverts_codes]
+ self._paths = [mpath.Path(np.column_stack([xs, ys]), cs)
+ for (xs, ys, _), (_, cs) in zip(xyzs_list, self._3dverts_codes)]
+ zs = np.concatenate([zs for _, _, zs in xyzs_list])
+ return zs.min() if len(zs) else 1e9
+
+
+def collection_2d_to_3d(col, zs=0, zdir='z'):
+ """Convert a `.Collection` to a `.Collection3D` object."""
+ zs = np.broadcast_to(zs, len(col.get_paths()))
+ col._3dverts_codes = [
+ (np.column_stack(juggle_axes(
+ *np.column_stack([p.vertices, np.broadcast_to(z, len(p.vertices))]).T,
+ zdir)),
+ p.codes)
+ for p, z in zip(col.get_paths(), zs)]
+ col.__class__ = cbook._make_class_factory(Collection3D, "{}3D")(type(col))
+
+
+class Line3DCollection(LineCollection):
+ """
+ A collection of 3D lines.
+ """
+
+ def set_sort_zpos(self, val):
+ """Set the position to use for z-sorting."""
+ self._sort_zpos = val
+ self.stale = True
+
+ def set_segments(self, segments):
+ """
+ Set 3D segments.
+ """
+ self._segments3d = segments
+ super().set_segments([])
+
+ def do_3d_projection(self):
+ """
+ Project the points according to renderer matrix.
+ """
+ xyslist = [proj3d._proj_trans_points(points, self.axes.M)
+ for points in self._segments3d]
+ segments_2d = [np.column_stack([xs, ys]) for xs, ys, zs in xyslist]
+ LineCollection.set_segments(self, segments_2d)
+
+ # FIXME
+ minz = 1e9
+ for xs, ys, zs in xyslist:
+ minz = min(minz, min(zs))
+ return minz
+
+
+def line_collection_2d_to_3d(col, zs=0, zdir='z'):
+ """Convert a `.LineCollection` to a `.Line3DCollection` object."""
+ segments3d = _paths_to_3d_segments(col.get_paths(), zs, zdir)
+ col.__class__ = Line3DCollection
+ col.set_segments(segments3d)
+
+
+class Patch3D(Patch):
+ """
+ 3D patch object.
+ """
+
+ def __init__(self, *args, zs=(), zdir='z', **kwargs):
+ """
+ Parameters
+ ----------
+ verts :
+ zs : float
+ The location along the *zdir* axis in 3D space to position the
+ patch.
+ zdir : {'x', 'y', 'z'}
+ Plane to plot patch orthogonal to. Default: 'z'.
+ See `.get_dir_vector` for a description of the values.
+ """
+ super().__init__(*args, **kwargs)
+ self.set_3d_properties(zs, zdir)
+
+ def set_3d_properties(self, verts, zs=0, zdir='z'):
+ """
+ Set the *z* position and direction of the patch.
+
+ Parameters
+ ----------
+ verts :
+ zs : float
+ The location along the *zdir* axis in 3D space to position the
+ patch.
+ zdir : {'x', 'y', 'z'}
+ Plane to plot patch orthogonal to. Default: 'z'.
+ See `.get_dir_vector` for a description of the values.
+ """
+ zs = np.broadcast_to(zs, len(verts))
+ self._segment3d = [juggle_axes(x, y, z, zdir)
+ for ((x, y), z) in zip(verts, zs)]
+
+ def get_path(self):
+ return self._path2d
+
+ def do_3d_projection(self):
+ s = self._segment3d
+ xs, ys, zs = zip(*s)
+ vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs,
+ self.axes.M)
+ self._path2d = mpath.Path(np.column_stack([vxs, vys]))
+ return min(vzs)
+
+
+class PathPatch3D(Patch3D):
+ """
+ 3D PathPatch object.
+ """
+
+ def __init__(self, path, *, zs=(), zdir='z', **kwargs):
+ """
+ Parameters
+ ----------
+ path :
+ zs : float
+ The location along the *zdir* axis in 3D space to position the
+ path patch.
+ zdir : {'x', 'y', 'z', 3-tuple}
+ Plane to plot path patch orthogonal to. Default: 'z'.
+ See `.get_dir_vector` for a description of the values.
+ """
+ # Not super().__init__!
+ Patch.__init__(self, **kwargs)
+ self.set_3d_properties(path, zs, zdir)
+
+ def set_3d_properties(self, path, zs=0, zdir='z'):
+ """
+ Set the *z* position and direction of the path patch.
+
+ Parameters
+ ----------
+ path :
+ zs : float
+ The location along the *zdir* axis in 3D space to position the
+ path patch.
+ zdir : {'x', 'y', 'z', 3-tuple}
+ Plane to plot path patch orthogonal to. Default: 'z'.
+ See `.get_dir_vector` for a description of the values.
+ """
+ Patch3D.set_3d_properties(self, path.vertices, zs=zs, zdir=zdir)
+ self._code3d = path.codes
+
+ def do_3d_projection(self):
+ s = self._segment3d
+ xs, ys, zs = zip(*s)
+ vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs,
+ self.axes.M)
+ self._path2d = mpath.Path(np.column_stack([vxs, vys]), self._code3d)
+ return min(vzs)
+
+
+def _get_patch_verts(patch):
+ """Return a list of vertices for the path of a patch."""
+ trans = patch.get_patch_transform()
+ path = patch.get_path()
+ polygons = path.to_polygons(trans)
+ return polygons[0] if len(polygons) else np.array([])
+
+
+def patch_2d_to_3d(patch, z=0, zdir='z'):
+ """Convert a `.Patch` to a `.Patch3D` object."""
+ verts = _get_patch_verts(patch)
+ patch.__class__ = Patch3D
+ patch.set_3d_properties(verts, z, zdir)
+
+
+def pathpatch_2d_to_3d(pathpatch, z=0, zdir='z'):
+ """Convert a `.PathPatch` to a `.PathPatch3D` object."""
+ path = pathpatch.get_path()
+ trans = pathpatch.get_patch_transform()
+
+ mpath = trans.transform_path(path)
+ pathpatch.__class__ = PathPatch3D
+ pathpatch.set_3d_properties(mpath, z, zdir)
+
+
+class Patch3DCollection(PatchCollection):
+ """
+ A collection of 3D patches.
+ """
+
+ def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs):
+ """
+ Create a collection of flat 3D patches with its normal vector
+ pointed in *zdir* direction, and located at *zs* on the *zdir*
+ axis. 'zs' can be a scalar or an array-like of the same length as
+ the number of patches in the collection.
+
+ Constructor arguments are the same as for
+ :class:`~matplotlib.collections.PatchCollection`. In addition,
+ keywords *zs=0* and *zdir='z'* are available.
+
+ Also, the keyword argument *depthshade* is available to indicate
+ whether to shade the patches in order to give the appearance of depth
+ (default is *True*). This is typically desired in scatter plots.
+ """
+ self._depthshade = depthshade
+ super().__init__(*args, **kwargs)
+ self.set_3d_properties(zs, zdir)
+
+ def get_depthshade(self):
+ return self._depthshade
+
+ def set_depthshade(self, depthshade):
+ """
+ Set whether depth shading is performed on collection members.
+
+ Parameters
+ ----------
+ depthshade : bool
+ Whether to shade the patches in order to give the appearance of
+ depth.
+ """
+ self._depthshade = depthshade
+ self.stale = True
+
+ def set_sort_zpos(self, val):
+ """Set the position to use for z-sorting."""
+ self._sort_zpos = val
+ self.stale = True
+
+ def set_3d_properties(self, zs, zdir):
+ """
+ Set the *z* positions and direction of the patches.
+
+ Parameters
+ ----------
+ zs : float or array of floats
+ The location or locations to place the patches in the collection
+ along the *zdir* axis.
+ zdir : {'x', 'y', 'z'}
+ Plane to plot patches orthogonal to.
+ All patches must have the same direction.
+ See `.get_dir_vector` for a description of the values.
+ """
+ # Force the collection to initialize the face and edgecolors
+ # just in case it is a scalarmappable with a colormap.
+ self.update_scalarmappable()
+ offsets = self.get_offsets()
+ if len(offsets) > 0:
+ xs, ys = offsets.T
+ else:
+ xs = []
+ ys = []
+ self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir)
+ self._z_markers_idx = slice(-1)
+ self._vzs = None
+ self.stale = True
+
+ def do_3d_projection(self):
+ xs, ys, zs = self._offsets3d
+ vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs,
+ self.axes.M)
+ self._vzs = vzs
+ super().set_offsets(np.column_stack([vxs, vys]))
+
+ if vzs.size > 0:
+ return min(vzs)
+ else:
+ return np.nan
+
+ def _maybe_depth_shade_and_sort_colors(self, color_array):
+ color_array = (
+ _zalpha(color_array, self._vzs)
+ if self._vzs is not None and self._depthshade
+ else color_array
+ )
+ if len(color_array) > 1:
+ color_array = color_array[self._z_markers_idx]
+ return mcolors.to_rgba_array(color_array, self._alpha)
+
+ def get_facecolor(self):
+ return self._maybe_depth_shade_and_sort_colors(super().get_facecolor())
+
+ def get_edgecolor(self):
+ # We need this check here to make sure we do not double-apply the depth
+ # based alpha shading when the edge color is "face" which means the
+ # edge colour should be identical to the face colour.
+ if cbook._str_equal(self._edgecolors, 'face'):
+ return self.get_facecolor()
+ return self._maybe_depth_shade_and_sort_colors(super().get_edgecolor())
+
+
+class Path3DCollection(PathCollection):
+ """
+ A collection of 3D paths.
+ """
+
+ def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs):
+ """
+ Create a collection of flat 3D paths with its normal vector
+ pointed in *zdir* direction, and located at *zs* on the *zdir*
+ axis. 'zs' can be a scalar or an array-like of the same length as
+ the number of paths in the collection.
+
+ Constructor arguments are the same as for
+ :class:`~matplotlib.collections.PathCollection`. In addition,
+ keywords *zs=0* and *zdir='z'* are available.
+
+ Also, the keyword argument *depthshade* is available to indicate
+ whether to shade the patches in order to give the appearance of depth
+ (default is *True*). This is typically desired in scatter plots.
+ """
+ self._depthshade = depthshade
+ self._in_draw = False
+ super().__init__(*args, **kwargs)
+ self.set_3d_properties(zs, zdir)
+ self._offset_zordered = None
+
+ def draw(self, renderer):
+ with self._use_zordered_offset():
+ with cbook._setattr_cm(self, _in_draw=True):
+ super().draw(renderer)
+
+ def set_sort_zpos(self, val):
+ """Set the position to use for z-sorting."""
+ self._sort_zpos = val
+ self.stale = True
+
+ def set_3d_properties(self, zs, zdir):
+ """
+ Set the *z* positions and direction of the paths.
+
+ Parameters
+ ----------
+ zs : float or array of floats
+ The location or locations to place the paths in the collection
+ along the *zdir* axis.
+ zdir : {'x', 'y', 'z'}
+ Plane to plot paths orthogonal to.
+ All paths must have the same direction.
+ See `.get_dir_vector` for a description of the values.
+ """
+ # Force the collection to initialize the face and edgecolors
+ # just in case it is a scalarmappable with a colormap.
+ self.update_scalarmappable()
+ offsets = self.get_offsets()
+ if len(offsets) > 0:
+ xs, ys = offsets.T
+ else:
+ xs = []
+ ys = []
+ self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir)
+ # In the base draw methods we access the attributes directly which
+ # means we cannot resolve the shuffling in the getter methods like
+ # we do for the edge and face colors.
+ #
+ # This means we need to carry around a cache of the unsorted sizes and
+ # widths (postfixed with 3d) and in `do_3d_projection` set the
+ # depth-sorted version of that data into the private state used by the
+ # base collection class in its draw method.
+ #
+ # Grab the current sizes and linewidths to preserve them.
+ self._sizes3d = self._sizes
+ self._linewidths3d = np.array(self._linewidths)
+ xs, ys, zs = self._offsets3d
+
+ # Sort the points based on z coordinates
+ # Performance optimization: Create a sorted index array and reorder
+ # points and point properties according to the index array
+ self._z_markers_idx = slice(-1)
+ self._vzs = None
+ self.stale = True
+
+ def set_sizes(self, sizes, dpi=72.0):
+ super().set_sizes(sizes, dpi)
+ if not self._in_draw:
+ self._sizes3d = sizes
+
+ def set_linewidth(self, lw):
+ super().set_linewidth(lw)
+ if not self._in_draw:
+ self._linewidths3d = np.array(self._linewidths)
+
+ def get_depthshade(self):
+ return self._depthshade
+
+ def set_depthshade(self, depthshade):
+ """
+ Set whether depth shading is performed on collection members.
+
+ Parameters
+ ----------
+ depthshade : bool
+ Whether to shade the patches in order to give the appearance of
+ depth.
+ """
+ self._depthshade = depthshade
+ self.stale = True
+
+ def do_3d_projection(self):
+ xs, ys, zs = self._offsets3d
+ vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs,
+ self.axes.M)
+ # Sort the points based on z coordinates
+ # Performance optimization: Create a sorted index array and reorder
+ # points and point properties according to the index array
+ z_markers_idx = self._z_markers_idx = np.argsort(vzs)[::-1]
+ self._vzs = vzs
+
+ # we have to special case the sizes because of code in collections.py
+ # as the draw method does
+ # self.set_sizes(self._sizes, self.figure.dpi)
+ # so we cannot rely on doing the sorting on the way out via get_*
+
+ if len(self._sizes3d) > 1:
+ self._sizes = self._sizes3d[z_markers_idx]
+
+ if len(self._linewidths3d) > 1:
+ self._linewidths = self._linewidths3d[z_markers_idx]
+
+ PathCollection.set_offsets(self, np.column_stack((vxs, vys)))
+
+ # Re-order items
+ vzs = vzs[z_markers_idx]
+ vxs = vxs[z_markers_idx]
+ vys = vys[z_markers_idx]
+
+ # Store ordered offset for drawing purpose
+ self._offset_zordered = np.column_stack((vxs, vys))
+
+ return np.min(vzs) if vzs.size else np.nan
+
+ @contextmanager
+ def _use_zordered_offset(self):
+ if self._offset_zordered is None:
+ # Do nothing
+ yield
+ else:
+ # Swap offset with z-ordered offset
+ old_offset = self._offsets
+ super().set_offsets(self._offset_zordered)
+ try:
+ yield
+ finally:
+ self._offsets = old_offset
+
+ def _maybe_depth_shade_and_sort_colors(self, color_array):
+ color_array = (
+ _zalpha(color_array, self._vzs)
+ if self._vzs is not None and self._depthshade
+ else color_array
+ )
+ if len(color_array) > 1:
+ color_array = color_array[self._z_markers_idx]
+ return mcolors.to_rgba_array(color_array, self._alpha)
+
+ def get_facecolor(self):
+ return self._maybe_depth_shade_and_sort_colors(super().get_facecolor())
+
+ def get_edgecolor(self):
+ # We need this check here to make sure we do not double-apply the depth
+ # based alpha shading when the edge color is "face" which means the
+ # edge colour should be identical to the face colour.
+ if cbook._str_equal(self._edgecolors, 'face'):
+ return self.get_facecolor()
+ return self._maybe_depth_shade_and_sort_colors(super().get_edgecolor())
+
+
+def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True):
+ """
+ Convert a `.PatchCollection` into a `.Patch3DCollection` object
+ (or a `.PathCollection` into a `.Path3DCollection` object).
+
+ Parameters
+ ----------
+ zs : float or array of floats
+ The location or locations to place the patches in the collection along
+ the *zdir* axis. Default: 0.
+ zdir : {'x', 'y', 'z'}
+ The axis in which to place the patches. Default: "z".
+ See `.get_dir_vector` for a description of the values.
+ depthshade
+ Whether to shade the patches to give a sense of depth. Default: *True*.
+
+ """
+ if isinstance(col, PathCollection):
+ col.__class__ = Path3DCollection
+ col._offset_zordered = None
+ elif isinstance(col, PatchCollection):
+ col.__class__ = Patch3DCollection
+ col._depthshade = depthshade
+ col._in_draw = False
+ col.set_3d_properties(zs, zdir)
+
+
+class Poly3DCollection(PolyCollection):
+ """
+ A collection of 3D polygons.
+
+ .. note::
+ **Filling of 3D polygons**
+
+ There is no simple definition of the enclosed surface of a 3D polygon
+ unless the polygon is planar.
+
+ In practice, Matplotlib fills the 2D projection of the polygon. This
+ gives a correct filling appearance only for planar polygons. For all
+ other polygons, you'll find orientations in which the edges of the
+ polygon intersect in the projection. This will lead to an incorrect
+ visualization of the 3D area.
+
+ If you need filled areas, it is recommended to create them via
+ `~mpl_toolkits.mplot3d.axes3d.Axes3D.plot_trisurf`, which creates a
+ triangulation and thus generates consistent surfaces.
+ """
+
+ def __init__(self, verts, *args, zsort='average', shade=False,
+ lightsource=None, **kwargs):
+ """
+ Parameters
+ ----------
+ verts : list of (N, 3) array-like
+ The sequence of polygons [*verts0*, *verts1*, ...] where each
+ element *verts_i* defines the vertices of polygon *i* as a 2D
+ array-like of shape (N, 3).
+ zsort : {'average', 'min', 'max'}, default: 'average'
+ The calculation method for the z-order.
+ See `~.Poly3DCollection.set_zsort` for details.
+ shade : bool, default: False
+ Whether to shade *facecolors* and *edgecolors*. When activating
+ *shade*, *facecolors* and/or *edgecolors* must be provided.
+
+ .. versionadded:: 3.7
+
+ lightsource : `~matplotlib.colors.LightSource`, optional
+ The lightsource to use when *shade* is True.
+
+ .. versionadded:: 3.7
+
+ *args, **kwargs
+ All other parameters are forwarded to `.PolyCollection`.
+
+ Notes
+ -----
+ Note that this class does a bit of magic with the _facecolors
+ and _edgecolors properties.
+ """
+ if shade:
+ normals = _generate_normals(verts)
+ facecolors = kwargs.get('facecolors', None)
+ if facecolors is not None:
+ kwargs['facecolors'] = _shade_colors(
+ facecolors, normals, lightsource
+ )
+
+ edgecolors = kwargs.get('edgecolors', None)
+ if edgecolors is not None:
+ kwargs['edgecolors'] = _shade_colors(
+ edgecolors, normals, lightsource
+ )
+ if facecolors is None and edgecolors is None:
+ raise ValueError(
+ "You must provide facecolors, edgecolors, or both for "
+ "shade to work.")
+ super().__init__(verts, *args, **kwargs)
+ if isinstance(verts, np.ndarray):
+ if verts.ndim != 3:
+ raise ValueError('verts must be a list of (N, 3) array-like')
+ else:
+ if any(len(np.shape(vert)) != 2 for vert in verts):
+ raise ValueError('verts must be a list of (N, 3) array-like')
+ self.set_zsort(zsort)
+ self._codes3d = None
+
+ _zsort_functions = {
+ 'average': np.average,
+ 'min': np.min,
+ 'max': np.max,
+ }
+
+ def set_zsort(self, zsort):
+ """
+ Set the calculation method for the z-order.
+
+ Parameters
+ ----------
+ zsort : {'average', 'min', 'max'}
+ The function applied on the z-coordinates of the vertices in the
+ viewer's coordinate system, to determine the z-order.
+ """
+ self._zsortfunc = self._zsort_functions[zsort]
+ self._sort_zpos = None
+ self.stale = True
+
+ def get_vector(self, segments3d):
+ """Optimize points for projection."""
+ if len(segments3d):
+ xs, ys, zs = np.vstack(segments3d).T
+ else: # vstack can't stack zero arrays.
+ xs, ys, zs = [], [], []
+ ones = np.ones(len(xs))
+ self._vec = np.array([xs, ys, zs, ones])
+
+ indices = [0, *np.cumsum([len(segment) for segment in segments3d])]
+ self._segslices = [*map(slice, indices[:-1], indices[1:])]
+
+ def set_verts(self, verts, closed=True):
+ """
+ Set 3D vertices.
+
+ Parameters
+ ----------
+ verts : list of (N, 3) array-like
+ The sequence of polygons [*verts0*, *verts1*, ...] where each
+ element *verts_i* defines the vertices of polygon *i* as a 2D
+ array-like of shape (N, 3).
+ closed : bool, default: True
+ Whether the polygon should be closed by adding a CLOSEPOLY
+ connection at the end.
+ """
+ self.get_vector(verts)
+ # 2D verts will be updated at draw time
+ super().set_verts([], False)
+ self._closed = closed
+
+ def set_verts_and_codes(self, verts, codes):
+ """Set 3D vertices with path codes."""
+ # set vertices with closed=False to prevent PolyCollection from
+ # setting path codes
+ self.set_verts(verts, closed=False)
+ # and set our own codes instead.
+ self._codes3d = codes
+
+ def set_3d_properties(self):
+ # Force the collection to initialize the face and edgecolors
+ # just in case it is a scalarmappable with a colormap.
+ self.update_scalarmappable()
+ self._sort_zpos = None
+ self.set_zsort('average')
+ self._facecolor3d = PolyCollection.get_facecolor(self)
+ self._edgecolor3d = PolyCollection.get_edgecolor(self)
+ self._alpha3d = PolyCollection.get_alpha(self)
+ self.stale = True
+
+ def set_sort_zpos(self, val):
+ """Set the position to use for z-sorting."""
+ self._sort_zpos = val
+ self.stale = True
+
+ def do_3d_projection(self):
+ """
+ Perform the 3D projection for this object.
+ """
+ if self._A is not None:
+ # force update of color mapping because we re-order them
+ # below. If we do not do this here, the 2D draw will call
+ # this, but we will never port the color mapped values back
+ # to the 3D versions.
+ #
+ # We hold the 3D versions in a fixed order (the order the user
+ # passed in) and sort the 2D version by view depth.
+ self.update_scalarmappable()
+ if self._face_is_mapped:
+ self._facecolor3d = self._facecolors
+ if self._edge_is_mapped:
+ self._edgecolor3d = self._edgecolors
+ txs, tys, tzs = proj3d._proj_transform_vec(self._vec, self.axes.M)
+ xyzlist = [(txs[sl], tys[sl], tzs[sl]) for sl in self._segslices]
+
+ # This extra fuss is to re-order face / edge colors
+ cface = self._facecolor3d
+ cedge = self._edgecolor3d
+ if len(cface) != len(xyzlist):
+ cface = cface.repeat(len(xyzlist), axis=0)
+ if len(cedge) != len(xyzlist):
+ if len(cedge) == 0:
+ cedge = cface
+ else:
+ cedge = cedge.repeat(len(xyzlist), axis=0)
+
+ if xyzlist:
+ # sort by depth (furthest drawn first)
+ z_segments_2d = sorted(
+ ((self._zsortfunc(zs), np.column_stack([xs, ys]), fc, ec, idx)
+ for idx, ((xs, ys, zs), fc, ec)
+ in enumerate(zip(xyzlist, cface, cedge))),
+ key=lambda x: x[0], reverse=True)
+
+ _, segments_2d, self._facecolors2d, self._edgecolors2d, idxs = \
+ zip(*z_segments_2d)
+ else:
+ segments_2d = []
+ self._facecolors2d = np.empty((0, 4))
+ self._edgecolors2d = np.empty((0, 4))
+ idxs = []
+
+ if self._codes3d is not None:
+ codes = [self._codes3d[idx] for idx in idxs]
+ PolyCollection.set_verts_and_codes(self, segments_2d, codes)
+ else:
+ PolyCollection.set_verts(self, segments_2d, self._closed)
+
+ if len(self._edgecolor3d) != len(cface):
+ self._edgecolors2d = self._edgecolor3d
+
+ # Return zorder value
+ if self._sort_zpos is not None:
+ zvec = np.array([[0], [0], [self._sort_zpos], [1]])
+ ztrans = proj3d._proj_transform_vec(zvec, self.axes.M)
+ return ztrans[2][0]
+ elif tzs.size > 0:
+ # FIXME: Some results still don't look quite right.
+ # In particular, examine contourf3d_demo2.py
+ # with az = -54 and elev = -45.
+ return np.min(tzs)
+ else:
+ return np.nan
+
+ def set_facecolor(self, colors):
+ # docstring inherited
+ super().set_facecolor(colors)
+ self._facecolor3d = PolyCollection.get_facecolor(self)
+
+ def set_edgecolor(self, colors):
+ # docstring inherited
+ super().set_edgecolor(colors)
+ self._edgecolor3d = PolyCollection.get_edgecolor(self)
+
+ def set_alpha(self, alpha):
+ # docstring inherited
+ artist.Artist.set_alpha(self, alpha)
+ try:
+ self._facecolor3d = mcolors.to_rgba_array(
+ self._facecolor3d, self._alpha)
+ except (AttributeError, TypeError, IndexError):
+ pass
+ try:
+ self._edgecolors = mcolors.to_rgba_array(
+ self._edgecolor3d, self._alpha)
+ except (AttributeError, TypeError, IndexError):
+ pass
+ self.stale = True
+
+ def get_facecolor(self):
+ # docstring inherited
+ # self._facecolors2d is not initialized until do_3d_projection
+ if not hasattr(self, '_facecolors2d'):
+ self.axes.M = self.axes.get_proj()
+ self.do_3d_projection()
+ return np.asarray(self._facecolors2d)
+
+ def get_edgecolor(self):
+ # docstring inherited
+ # self._edgecolors2d is not initialized until do_3d_projection
+ if not hasattr(self, '_edgecolors2d'):
+ self.axes.M = self.axes.get_proj()
+ self.do_3d_projection()
+ return np.asarray(self._edgecolors2d)
+
+
+def poly_collection_2d_to_3d(col, zs=0, zdir='z'):
+ """
+ Convert a `.PolyCollection` into a `.Poly3DCollection` object.
+
+ Parameters
+ ----------
+ zs : float or array of floats
+ The location or locations to place the polygons in the collection along
+ the *zdir* axis. Default: 0.
+ zdir : {'x', 'y', 'z'}
+ The axis in which to place the patches. Default: 'z'.
+ See `.get_dir_vector` for a description of the values.
+ """
+ segments_3d, codes = _paths_to_3d_segments_with_codes(
+ col.get_paths(), zs, zdir)
+ col.__class__ = Poly3DCollection
+ col.set_verts_and_codes(segments_3d, codes)
+ col.set_3d_properties()
+
+
+def juggle_axes(xs, ys, zs, zdir):
+ """
+ Reorder coordinates so that 2D *xs*, *ys* can be plotted in the plane
+ orthogonal to *zdir*. *zdir* is normally 'x', 'y' or 'z'. However, if
+ *zdir* starts with a '-' it is interpreted as a compensation for
+ `rotate_axes`.
+ """
+ if zdir == 'x':
+ return zs, xs, ys
+ elif zdir == 'y':
+ return xs, zs, ys
+ elif zdir[0] == '-':
+ return rotate_axes(xs, ys, zs, zdir)
+ else:
+ return xs, ys, zs
+
+
+def rotate_axes(xs, ys, zs, zdir):
+ """
+ Reorder coordinates so that the axes are rotated with *zdir* along
+ the original z axis. Prepending the axis with a '-' does the
+ inverse transform, so *zdir* can be 'x', '-x', 'y', '-y', 'z' or '-z'.
+ """
+ if zdir in ('x', '-y'):
+ return ys, zs, xs
+ elif zdir in ('-x', 'y'):
+ return zs, xs, ys
+ else:
+ return xs, ys, zs
+
+
+def _zalpha(colors, zs):
+ """Modify the alphas of the color list according to depth."""
+ # FIXME: This only works well if the points for *zs* are well-spaced
+ # in all three dimensions. Otherwise, at certain orientations,
+ # the min and max zs are very close together.
+ # Should really normalize against the viewing depth.
+ if len(colors) == 0 or len(zs) == 0:
+ return np.zeros((0, 4))
+ norm = Normalize(min(zs), max(zs))
+ sats = 1 - norm(zs) * 0.7
+ rgba = np.broadcast_to(mcolors.to_rgba_array(colors), (len(zs), 4))
+ return np.column_stack([rgba[:, :3], rgba[:, 3] * sats])
+
+
+def _generate_normals(polygons):
+ """
+ Compute the normals of a list of polygons, one normal per polygon.
+
+ Normals point towards the viewer for a face with its vertices in
+ counterclockwise order, following the right hand rule.
+
+ Uses three points equally spaced around the polygon. This method assumes
+ that the points are in a plane. Otherwise, more than one shade is required,
+ which is not supported.
+
+ Parameters
+ ----------
+ polygons : list of (M_i, 3) array-like, or (..., M, 3) array-like
+ A sequence of polygons to compute normals for, which can have
+ varying numbers of vertices. If the polygons all have the same
+ number of vertices and array is passed, then the operation will
+ be vectorized.
+
+ Returns
+ -------
+ normals : (..., 3) array
+ A normal vector estimated for the polygon.
+ """
+ if isinstance(polygons, np.ndarray):
+ # optimization: polygons all have the same number of points, so can
+ # vectorize
+ n = polygons.shape[-2]
+ i1, i2, i3 = 0, n//3, 2*n//3
+ v1 = polygons[..., i1, :] - polygons[..., i2, :]
+ v2 = polygons[..., i2, :] - polygons[..., i3, :]
+ else:
+ # The subtraction doesn't vectorize because polygons is jagged.
+ v1 = np.empty((len(polygons), 3))
+ v2 = np.empty((len(polygons), 3))
+ for poly_i, ps in enumerate(polygons):
+ n = len(ps)
+ i1, i2, i3 = 0, n//3, 2*n//3
+ v1[poly_i, :] = ps[i1, :] - ps[i2, :]
+ v2[poly_i, :] = ps[i2, :] - ps[i3, :]
+ return np.cross(v1, v2)
+
+
+def _shade_colors(color, normals, lightsource=None):
+ """
+ Shade *color* using normal vectors given by *normals*,
+ assuming a *lightsource* (using default position if not given).
+ *color* can also be an array of the same length as *normals*.
+ """
+ if lightsource is None:
+ # chosen for backwards-compatibility
+ lightsource = mcolors.LightSource(azdeg=225, altdeg=19.4712)
+
+ with np.errstate(invalid="ignore"):
+ shade = ((normals / np.linalg.norm(normals, axis=1, keepdims=True))
+ @ lightsource.direction)
+ mask = ~np.isnan(shade)
+
+ if mask.any():
+ # convert dot product to allowed shading fractions
+ in_norm = mcolors.Normalize(-1, 1)
+ out_norm = mcolors.Normalize(0.3, 1).inverse
+
+ def norm(x):
+ return out_norm(in_norm(x))
+
+ shade[~mask] = 0
+
+ color = mcolors.to_rgba_array(color)
+ # shape of color should be (M, 4) (where M is number of faces)
+ # shape of shade should be (M,)
+ # colors should have final shape of (M, 4)
+ alpha = color[:, 3]
+ colors = norm(shade)[:, np.newaxis] * color
+ colors[:, 3] = alpha
+ else:
+ colors = np.asanyarray(color).copy()
+
+ return colors