diff options
author | shumkovnd <shumkovnd@yandex-team.com> | 2023-11-10 14:39:34 +0300 |
---|---|---|
committer | shumkovnd <shumkovnd@yandex-team.com> | 2023-11-10 16:42:24 +0300 |
commit | 77eb2d3fdcec5c978c64e025ced2764c57c00285 (patch) | |
tree | c51edb0748ca8d4a08d7c7323312c27ba1a8b79a /contrib/python/matplotlib/py2/mpl_toolkits/mplot3d | |
parent | dd6d20cadb65582270ac23f4b3b14ae189704b9d (diff) | |
download | ydb-77eb2d3fdcec5c978c64e025ced2764c57c00285.tar.gz |
KIKIMR-19287: add task_stats_drawing script
Diffstat (limited to 'contrib/python/matplotlib/py2/mpl_toolkits/mplot3d')
5 files changed, 4425 insertions, 0 deletions
diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/__init__.py b/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/__init__.py new file mode 100644 index 0000000000..cd9c2139d2 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/__init__.py @@ -0,0 +1,6 @@ +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six + +from .axes3d import Axes3D diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/art3d.py b/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/art3d.py new file mode 100644 index 0000000000..ef55dd693e --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/art3d.py @@ -0,0 +1,774 @@ +# 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. +''' +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six +from six.moves import zip + +import math + +import numpy as np + +from matplotlib import ( + artist, cbook, colors as mcolors, lines, text as mtext, path as mpath) +from matplotlib.cbook import _backports +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 angle between -180 and +180""" + a = (a + 360) % 360 + if a > 180: + a = a - 360 + return a + + +def norm_text_angle(a): + """Return angle between -90 and +90""" + a = (a + 180) % 180 + if a > 90: + a = a - 180 + return a + + +def get_dir_vector(zdir): + 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 cbook.iterable(zdir) and len(zdir) == 3: + return zdir + else: + raise ValueError("'x', 'y', 'z', None or vector of length 3 expected") + + +class Text3D(mtext.Text): + ''' + Text object with 3D position and (in the future) direction. + ''' + + def __init__(self, x=0, y=0, z=0, text='', zdir='z', **kwargs): + ''' + *x*, *y*, *z* Position of text + *text* Text string to display + *zdir* Direction of text + + Keyword arguments are passed onto :func:`~matplotlib.text.Text`. + ''' + mtext.Text.__init__(self, x, y, text, **kwargs) + self.set_3d_properties(z, zdir) + + def set_3d_properties(self, z=0, zdir='z'): + x, y = self.get_position() + self._position3d = np.array((x, y, z)) + self._dir_vec = get_dir_vector(zdir) + self.stale = True + + def draw(self, renderer): + proj = proj3d.proj_trans_points( + [self._position3d, self._position3d + self._dir_vec], renderer.M) + dx = proj[0][1] - proj[0][0] + dy = proj[1][1] - proj[1][0] + if dx==0. and dy==0.: + # atan2 raises ValueError: math domain error on 0,0 + angle = 0. + else: + angle = math.degrees(math.atan2(dy, dx)) + self.set_position((proj[0][0], proj[1][0])) + self.set_rotation(norm_text_angle(angle)) + mtext.Text.draw(self, renderer) + self.stale = False + + +def text_2d_to_3d(obj, z=0, zdir='z'): + """Convert a Text to a Text3D object.""" + obj.__class__ = Text3D + obj.set_3d_properties(z, zdir) + + +class Line3D(lines.Line2D): + ''' + 3D line object. + ''' + + def __init__(self, xs, ys, zs, *args, **kwargs): + ''' + Keyword arguments are passed onto :func:`~matplotlib.lines.Line2D`. + ''' + lines.Line2D.__init__(self, [], [], *args, **kwargs) + self._verts3d = xs, ys, zs + + def set_3d_properties(self, zs=0, zdir='z'): + xs = self.get_xdata() + ys = self.get_ydata() + + try: + # If *zs* is a list or array, then this will fail and + # just proceed to juggle_axes(). + zs = float(zs) + zs = [zs for x in xs] + except TypeError: + pass + self._verts3d = juggle_axes(xs, ys, zs, zdir) + self.stale = True + + def draw(self, renderer): + xs3d, ys3d, zs3d = self._verts3d + xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, renderer.M) + self.set_data(xs, ys) + lines.Line2D.draw(self, renderer) + self.stale = False + + +def line_2d_to_3d(line, zs=0, zdir='z'): + ''' + Convert a 2D line to 3D. + ''' + 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 = _backports.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. + ''' + + zs = _backports.broadcast_to(zs, len(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 = _backports.broadcast_to(zs, len(path)) + seg = [] + codes = [] + pathsegs = path.iter_segments(simplify=False, curves=False) + for (((x, y), code), z) in zip(pathsegs, zs): + seg.append((x, y, z)) + codes.append(code) + seg3d = [juggle_axes(x, y, z, zdir) for (x, y, z) in seg] + return seg3d, 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 = _backports.broadcast_to(zs, len(paths)) + segments = [] + codes_list = [] + for path, pathz in zip(paths, zs): + segs, codes = path_to_3d_segment_with_codes(path, pathz, zdir) + segments.append(segs) + codes_list.append(codes) + return segments, codes_list + + +class Line3DCollection(LineCollection): + ''' + A collection of 3D lines. + ''' + + def __init__(self, segments, *args, **kwargs): + ''' + Keyword arguments are passed onto :func:`~matplotlib.collections.LineCollection`. + ''' + LineCollection.__init__(self, segments, *args, **kwargs) + + 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 = np.asanyarray(segments) + LineCollection.set_segments(self, []) + + def do_3d_projection(self, renderer): + ''' + Project the points according to renderer matrix. + ''' + xyslist = [ + proj3d.proj_trans_points(points, renderer.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 draw(self, renderer, project=False): + if project: + self.do_3d_projection(renderer) + LineCollection.draw(self, renderer) + + +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, **kwargs): + zs = kwargs.pop('zs', []) + zdir = kwargs.pop('zdir', 'z') + Patch.__init__(self, *args, **kwargs) + self.set_3d_properties(zs, zdir) + + def set_3d_properties(self, verts, zs=0, zdir='z'): + zs = _backports.broadcast_to(zs, len(verts)) + self._segment3d = [juggle_axes(x, y, z, zdir) + for ((x, y), z) in zip(verts, zs)] + self._facecolor3d = Patch.get_facecolor(self) + + def get_path(self): + return self._path2d + + def get_facecolor(self): + return self._facecolor2d + + def do_3d_projection(self, renderer): + s = self._segment3d + xs, ys, zs = zip(*s) + vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M) + self._path2d = mpath.Path(np.column_stack([vxs, vys])) + # FIXME: coloring + self._facecolor2d = self._facecolor3d + return min(vzs) + + def draw(self, renderer): + Patch.draw(self, renderer) + + +class PathPatch3D(Patch3D): + ''' + 3D PathPatch object. + ''' + + def __init__(self, path, **kwargs): + zs = kwargs.pop('zs', []) + zdir = kwargs.pop('zdir', 'z') + Patch.__init__(self, **kwargs) + self.set_3d_properties(path, zs, zdir) + + def set_3d_properties(self, path, zs=0, zdir='z'): + Patch3D.set_3d_properties(self, path.vertices, zs=zs, zdir=zdir) + self._code3d = path.codes + + def do_3d_projection(self, renderer): + s = self._segment3d + xs, ys, zs = zip(*s) + vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M) + self._path2d = mpath.Path(np.column_stack([vxs, vys]), self._code3d) + # FIXME: coloring + self._facecolor2d = self._facecolor3d + 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) + if len(polygons): + return polygons[0] + else: + return [] + + +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, **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 or not to shade the patches in order to + give the appearance of depth (default is *True*). + This is typically desired in scatter plots. + """ + zs = kwargs.pop('zs', 0) + zdir = kwargs.pop('zdir', 'z') + self._depthshade = kwargs.pop('depthshade', True) + PatchCollection.__init__(self, *args, **kwargs) + self.set_3d_properties(zs, zdir) + + 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): + # 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 = zip(*offsets) + else: + xs = [] + ys = [] + self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir) + self._facecolor3d = self.get_facecolor() + self._edgecolor3d = self.get_edgecolor() + self.stale = True + + def do_3d_projection(self, renderer): + xs, ys, zs = self._offsets3d + vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M) + + fcs = (zalpha(self._facecolor3d, vzs) if self._depthshade else + self._facecolor3d) + fcs = mcolors.to_rgba_array(fcs, self._alpha) + self.set_facecolors(fcs) + + ecs = (zalpha(self._edgecolor3d, vzs) if self._depthshade else + self._edgecolor3d) + ecs = mcolors.to_rgba_array(ecs, self._alpha) + self.set_edgecolors(ecs) + PatchCollection.set_offsets(self, np.column_stack([vxs, vys])) + + if vzs.size > 0: + return min(vzs) + else: + return np.nan + + +class Path3DCollection(PathCollection): + ''' + A collection of 3D paths. + ''' + + def __init__(self, *args, **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 or not to shade the patches in order to + give the appearance of depth (default is *True*). + This is typically desired in scatter plots. + """ + zs = kwargs.pop('zs', 0) + zdir = kwargs.pop('zdir', 'z') + self._depthshade = kwargs.pop('depthshade', True) + PathCollection.__init__(self, *args, **kwargs) + self.set_3d_properties(zs, zdir) + + 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): + # 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 = zip(*offsets) + else: + xs = [] + ys = [] + self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir) + self._facecolor3d = self.get_facecolor() + self._edgecolor3d = self.get_edgecolor() + self.stale = True + + def do_3d_projection(self, renderer): + xs, ys, zs = self._offsets3d + vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M) + + fcs = (zalpha(self._facecolor3d, vzs) if self._depthshade else + self._facecolor3d) + fcs = mcolors.to_rgba_array(fcs, self._alpha) + self.set_facecolors(fcs) + + ecs = (zalpha(self._edgecolor3d, vzs) if self._depthshade else + self._edgecolor3d) + ecs = mcolors.to_rgba_array(ecs, self._alpha) + self.set_edgecolors(ecs) + PathCollection.set_offsets(self, np.column_stack([vxs, vys])) + + if vzs.size > 0 : + return min(vzs) + else : + return np.nan + + +def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True): + """ + Convert a :class:`~matplotlib.collections.PatchCollection` into a + :class:`Patch3DCollection` object + (or a :class:`~matplotlib.collections.PathCollection` into a + :class:`Path3DCollection` object). + + Keywords: + + *za* The location or locations to place the patches in the + collection along the *zdir* axis. Defaults to 0. + + *zdir* The axis in which to place the patches. Default is "z". + + *depthshade* Whether to shade the patches to give a sense of depth. + Defaults to *True*. + + """ + if isinstance(col, PathCollection): + col.__class__ = Path3DCollection + elif isinstance(col, PatchCollection): + col.__class__ = Patch3DCollection + col._depthshade = depthshade + col.set_3d_properties(zs, zdir) + + +class Poly3DCollection(PolyCollection): + ''' + A collection of 3D polygons. + ''' + + def __init__(self, verts, *args, **kwargs): + ''' + Create a Poly3DCollection. + + *verts* should contain 3D coordinates. + + Keyword arguments: + zsort, see set_zsort for options. + + Note that this class does a bit of magic with the _facecolors + and _edgecolors properties. + ''' + zsort = kwargs.pop('zsort', True) + PolyCollection.__init__(self, verts, *args, **kwargs) + self.set_zsort(zsort) + self._codes3d = None + + _zsort_functions = { + 'average': np.average, + 'min': np.min, + 'max': np.max, + } + + def set_zsort(self, zsort): + ''' + Set z-sorting behaviour: + boolean: if True use default 'average' + string: 'average', 'min' or 'max' + ''' + + if zsort is True: + zsort = 'average' + + if zsort is not False: + if zsort in self._zsort_functions: + zsortfunc = self._zsort_functions[zsort] + else: + return False + else: + zsortfunc = None + + self._zsort = zsort + self._sort_zpos = None + self._zsortfunc = zsortfunc + self.stale = True + + def get_vector(self, segments3d): + """Optimize points for projection""" + si = 0 + ei = 0 + segis = [] + points = [] + for p in segments3d: + points.extend(p) + ei = si + len(p) + segis.append((si, ei)) + si = ei + + if len(segments3d): + xs, ys, zs = zip(*points) + else : + # We need this so that we can skip the bad unpacking from zip() + xs, ys, zs = [], [], [] + + ones = np.ones(len(xs)) + self._vec = np.array([xs, ys, zs, ones]) + self._segis = segis + + def set_verts(self, verts, closed=True): + '''Set 3D vertices.''' + self.get_vector(verts) + # 2D verts will be updated at draw time + PolyCollection.set_verts(self, [], False) + self._closed = closed + + def set_verts_and_codes(self, verts, codes): + '''Sets 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(True) + self._facecolors3d = PolyCollection.get_facecolors(self) + self._edgecolors3d = PolyCollection.get_edgecolors(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, renderer): + ''' + Perform the 3D projection for this object. + ''' + # FIXME: This may no longer be needed? + if self._A is not None: + self.update_scalarmappable() + self._facecolors3d = self._facecolors + + txs, tys, tzs = proj3d.proj_transform_vec(self._vec, renderer.M) + xyzlist = [(txs[si:ei], tys[si:ei], tzs[si:ei]) + for si, ei in self._segis] + + # This extra fuss is to re-order face / edge colors + cface = self._facecolors3d + cedge = self._edgecolors3d + 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 required sort by depth (furthest drawn first) + if self._zsort: + 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) + else: + raise ValueError("whoops") + + segments_2d = [s for z, s, fc, ec, idx in z_segments_2d] + if self._codes3d is not None: + codes = [self._codes3d[idx] for z, s, fc, ec, idx in z_segments_2d] + PolyCollection.set_verts_and_codes(self, segments_2d, codes) + else: + PolyCollection.set_verts(self, segments_2d, self._closed) + + self._facecolors2d = [fc for z, s, fc, ec, idx in z_segments_2d] + if len(self._edgecolors3d) == len(cface): + self._edgecolors2d = [ec for z, s, fc, ec, idx in z_segments_2d] + else: + self._edgecolors2d = self._edgecolors3d + + # 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, renderer.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): + PolyCollection.set_facecolor(self, colors) + self._facecolors3d = PolyCollection.get_facecolor(self) + set_facecolors = set_facecolor + + def set_edgecolor(self, colors): + PolyCollection.set_edgecolor(self, colors) + self._edgecolors3d = PolyCollection.get_edgecolor(self) + set_edgecolors = set_edgecolor + + def set_alpha(self, alpha): + """ + Set the alpha tranparencies of the collection. *alpha* must be + a float or *None*. + + ACCEPTS: float or None + """ + if alpha is not None: + try: + float(alpha) + except TypeError: + raise TypeError('alpha must be a float or None') + artist.Artist.set_alpha(self, alpha) + try: + self._facecolors = mcolors.to_rgba_array( + self._facecolors3d, self._alpha) + except (AttributeError, TypeError, IndexError): + pass + try: + self._edgecolors = mcolors.to_rgba_array( + self._edgecolors3d, self._alpha) + except (AttributeError, TypeError, IndexError): + pass + self.stale = True + + def get_facecolors(self): + return self._facecolors2d + get_facecolor = get_facecolors + + def get_edgecolors(self): + return self._edgecolors2d + get_edgecolor = get_edgecolors + + def draw(self, renderer): + return Collection.draw(self, renderer) + + +def poly_collection_2d_to_3d(col, zs=0, zdir='z'): + """Convert a PolyCollection to a Poly3DCollection object.""" + 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 == 'x': + return ys, zs, xs + elif zdir == '-x': + return zs, xs, ys + + elif zdir == 'y': + return zs, xs, ys + elif zdir == '-y': + return ys, zs, xs + + else: + return xs, ys, zs + + +def get_colors(c, num): + """Stretch the color argument to provide the required number num""" + return _backports.broadcast_to( + mcolors.to_rgba_array(c) if len(c) else [0, 0, 0, 0], + (num, 4)) + + +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. + colors = get_colors(colors, len(zs)) + if len(zs): + norm = Normalize(min(zs), max(zs)) + sats = 1 - norm(zs) * 0.7 + colors = [(c[0], c[1], c[2], c[3] * s) for c, s in zip(colors, sats)] + return colors diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/axes3d.py b/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/axes3d.py new file mode 100644 index 0000000000..b99a090c62 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/axes3d.py @@ -0,0 +1,2958 @@ +""" +axes3d.py, original mplot3d version by John Porter +Created: 23 Sep 2005 + +Parts fixed by Reinier Heeres <reinier@heeres.eu> +Minor additions by Ben Axelrod <baxelrod@coroware.com> +Significant updates and revisions by Ben Root <ben.v.root@gmail.com> + +Module containing Axes3D, an object which can plot 3D objects on a +2D matplotlib figure. +""" +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six +from six.moves import map, xrange, zip, reduce + +import math +import warnings +from collections import defaultdict + +import numpy as np + +import matplotlib.axes as maxes +import matplotlib.cbook as cbook +import matplotlib.collections as mcoll +import matplotlib.colors as mcolors +import matplotlib.docstring as docstring +import matplotlib.scale as mscale +import matplotlib.transforms as mtransforms +from matplotlib.axes import Axes, rcParams +from matplotlib.cbook import _backports +from matplotlib.colors import Normalize, LightSource +from matplotlib.transforms import Bbox +from matplotlib.tri.triangulation import Triangulation + +from . import art3d +from . import proj3d +from . import axis3d + + +def unit_bbox(): + box = Bbox(np.array([[0, 0], [1, 1]])) + return box + + +class Axes3D(Axes): + """ + 3D axes object. + """ + name = '3d' + _shared_z_axes = cbook.Grouper() + + def __init__(self, fig, rect=None, *args, **kwargs): + ''' + Build an :class:`Axes3D` instance in + :class:`~matplotlib.figure.Figure` *fig* with + *rect=[left, bottom, width, height]* in + :class:`~matplotlib.figure.Figure` coordinates + + Optional keyword arguments: + + ================ ========================================= + Keyword Description + ================ ========================================= + *azim* Azimuthal viewing angle (default -60) + *elev* Elevation viewing angle (default 30) + *zscale* [%(scale)s] + *sharez* Other axes to share z-limits with + *proj_type* 'persp' or 'ortho' (default 'persp') + ================ ========================================= + + .. versionadded :: 1.2.1 + *sharez* + + ''' % {'scale': ' | '.join([repr(x) for x in mscale.get_scale_names()])} + + if rect is None: + rect = [0.0, 0.0, 1.0, 1.0] + self._cids = [] + + self.initial_azim = kwargs.pop('azim', -60) + self.initial_elev = kwargs.pop('elev', 30) + zscale = kwargs.pop('zscale', None) + sharez = kwargs.pop('sharez', None) + self.set_proj_type(kwargs.pop('proj_type', 'persp')) + + self.xy_viewLim = unit_bbox() + self.zz_viewLim = unit_bbox() + self.xy_dataLim = unit_bbox() + self.zz_dataLim = unit_bbox() + # inihibit autoscale_view until the axes are defined + # they can't be defined until Axes.__init__ has been called + self.view_init(self.initial_elev, self.initial_azim) + self._ready = 0 + + self._sharez = sharez + if sharez is not None: + self._shared_z_axes.join(self, sharez) + self._adjustable = 'datalim' + + super(Axes3D, self).__init__(fig, rect, + frameon=True, + *args, **kwargs) + # Disable drawing of axes by base class + super(Axes3D, self).set_axis_off() + # Enable drawing of axes by Axes3D class + self.set_axis_on() + self.M = None + + # func used to format z -- fall back on major formatters + self.fmt_zdata = None + + if zscale is not None: + self.set_zscale(zscale) + + if self.zaxis is not None: + self._zcid = self.zaxis.callbacks.connect( + 'units finalize', lambda: self._on_units_changed(scalez=True)) + else: + self._zcid = None + + self._ready = 1 + self.mouse_init() + self.set_top_view() + + self.patch.set_linewidth(0) + # Calculate the pseudo-data width and height + pseudo_bbox = self.transLimits.inverted().transform([(0, 0), (1, 1)]) + self._pseudo_w, self._pseudo_h = pseudo_bbox[1] - pseudo_bbox[0] + + self.figure.add_axes(self) + + def set_axis_off(self): + self._axis3don = False + self.stale = True + + def set_axis_on(self): + self._axis3don = True + self.stale = True + + def have_units(self): + """ + Return *True* if units are set on the *x*, *y*, or *z* axes + + """ + return (self.xaxis.have_units() or self.yaxis.have_units() or + self.zaxis.have_units()) + + def convert_zunits(self, z): + """ + For artists in an axes, if the zaxis has units support, + convert *z* using zaxis unit type + + .. versionadded :: 1.2.1 + + """ + return self.zaxis.convert_units(z) + + def _process_unit_info(self, xdata=None, ydata=None, zdata=None, + kwargs=None): + """ + Look for unit *kwargs* and update the axis instances as necessary + + """ + super(Axes3D, self)._process_unit_info(xdata=xdata, ydata=ydata, + kwargs=kwargs) + + if self.xaxis is None or self.yaxis is None or self.zaxis is None: + return + + if zdata is not None: + # we only need to update if there is nothing set yet. + if not self.zaxis.have_units(): + self.zaxis.update_units(xdata) + + # process kwargs 2nd since these will override default units + if kwargs is not None: + zunits = kwargs.pop('zunits', self.zaxis.units) + if zunits != self.zaxis.units: + self.zaxis.set_units(zunits) + # If the units being set imply a different converter, + # we need to update. + if zdata is not None: + self.zaxis.update_units(zdata) + + def set_top_view(self): + # this happens to be the right view for the viewing coordinates + # moved up and to the left slightly to fit labels and axes + xdwl = (0.95/self.dist) + xdw = (0.9/self.dist) + ydwl = (0.95/self.dist) + ydw = (0.9/self.dist) + + # This is purposely using the 2D Axes's set_xlim and set_ylim, + # because we are trying to place our viewing pane. + super(Axes3D, self).set_xlim(-xdwl, xdw, auto=None) + super(Axes3D, self).set_ylim(-ydwl, ydw, auto=None) + + def _init_axis(self): + '''Init 3D axes; overrides creation of regular X/Y axes''' + self.w_xaxis = axis3d.XAxis('x', self.xy_viewLim.intervalx, + self.xy_dataLim.intervalx, self) + self.xaxis = self.w_xaxis + self.w_yaxis = axis3d.YAxis('y', self.xy_viewLim.intervaly, + self.xy_dataLim.intervaly, self) + self.yaxis = self.w_yaxis + self.w_zaxis = axis3d.ZAxis('z', self.zz_viewLim.intervalx, + self.zz_dataLim.intervalx, self) + self.zaxis = self.w_zaxis + + for ax in self.xaxis, self.yaxis, self.zaxis: + ax.init3d() + + def get_children(self): + return [self.zaxis, ] + super(Axes3D, self).get_children() + + def _get_axis_list(self): + return super(Axes3D, self)._get_axis_list() + (self.zaxis, ) + + def unit_cube(self, vals=None): + minx, maxx, miny, maxy, minz, maxz = vals or self.get_w_lims() + xs, ys, zs = ([minx, maxx, maxx, minx, minx, maxx, maxx, minx], + [miny, miny, maxy, maxy, miny, miny, maxy, maxy], + [minz, minz, minz, minz, maxz, maxz, maxz, maxz]) + return list(zip(xs, ys, zs)) + + def tunit_cube(self, vals=None, M=None): + if M is None: + M = self.M + xyzs = self.unit_cube(vals) + tcube = proj3d.proj_points(xyzs, M) + return tcube + + def tunit_edges(self, vals=None, M=None): + tc = self.tunit_cube(vals, M) + edges = [(tc[0], tc[1]), + (tc[1], tc[2]), + (tc[2], tc[3]), + (tc[3], tc[0]), + + (tc[0], tc[4]), + (tc[1], tc[5]), + (tc[2], tc[6]), + (tc[3], tc[7]), + + (tc[4], tc[5]), + (tc[5], tc[6]), + (tc[6], tc[7]), + (tc[7], tc[4])] + return edges + + def draw(self, renderer): + # draw the background patch + self.patch.draw(renderer) + self._frameon = False + + # first, set the aspect + # this is duplicated from `axes._base._AxesBase.draw` + # but must be called before any of the artist are drawn as + # it adjusts the view limits and the size of the bounding box + # of the axes + locator = self.get_axes_locator() + if locator: + pos = locator(self, renderer) + self.apply_aspect(pos) + else: + self.apply_aspect() + + # add the projection matrix to the renderer + self.M = self.get_proj() + renderer.M = self.M + renderer.vvec = self.vvec + renderer.eye = self.eye + renderer.get_axis_position = self.get_axis_position + + # Calculate projection of collections and zorder them + for i, col in enumerate( + sorted(self.collections, + key=lambda col: col.do_3d_projection(renderer), + reverse=True)): + col.zorder = i + + # Calculate projection of patches and zorder them + for i, patch in enumerate( + sorted(self.patches, + key=lambda patch: patch.do_3d_projection(renderer), + reverse=True)): + patch.zorder = i + + if self._axis3don: + axes = (self.xaxis, self.yaxis, self.zaxis) + # Draw panes first + for ax in axes: + ax.draw_pane(renderer) + # Then axes + for ax in axes: + ax.draw(renderer) + + # Then rest + super(Axes3D, self).draw(renderer) + + def get_axis_position(self): + vals = self.get_w_lims() + tc = self.tunit_cube(vals, self.M) + xhigh = tc[1][2] > tc[2][2] + yhigh = tc[3][2] > tc[2][2] + zhigh = tc[0][2] > tc[2][2] + return xhigh, yhigh, zhigh + + def _on_units_changed(self, scalex=False, scaley=False, scalez=False): + """ + Callback for processing changes to axis units. + + Currently forces updates of data limits and view limits. + """ + self.relim() + self.autoscale_view(scalex=scalex, scaley=scaley, scalez=scalez) + + def update_datalim(self, xys, **kwargs): + pass + + def get_autoscale_on(self): + """ + Get whether autoscaling is applied for all axes on plot commands + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + return super(Axes3D, self).get_autoscale_on() and self.get_autoscalez_on() + + def get_autoscalez_on(self): + """ + Get whether autoscaling for the z-axis is applied on plot commands + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + return self._autoscaleZon + + def set_autoscale_on(self, b): + """ + Set whether autoscaling is applied on plot commands + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + + Parameters + ---------- + b : bool + .. ACCEPTS: bool + """ + super(Axes3D, self).set_autoscale_on(b) + self.set_autoscalez_on(b) + + def set_autoscalez_on(self, b): + """ + Set whether autoscaling for the z-axis is applied on plot commands + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + + Parameters + ---------- + b : bool + .. ACCEPTS: bool + """ + self._autoscaleZon = b + + def set_zmargin(self, m): + """ + Set padding of Z data limits prior to autoscaling. + + *m* times the data interval will be added to each + end of that interval before it is used in autoscaling. + + accepts: float in range 0 to 1 + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + if m < 0 or m > 1 : + raise ValueError("margin must be in range 0 to 1") + self._zmargin = m + self.stale = True + + def margins(self, *args, **kw): + """ + Convenience method to set or retrieve autoscaling margins. + + signatures:: + margins() + + returns xmargin, ymargin, zmargin + + :: + + margins(margin) + + margins(xmargin, ymargin, zmargin) + + margins(x=xmargin, y=ymargin, z=zmargin) + + margins(..., tight=False) + + All forms above set the xmargin, ymargin and zmargin + parameters. All keyword parameters are optional. A single argument + specifies xmargin, ymargin and zmargin. The *tight* parameter + is passed to :meth:`autoscale_view`, which is executed after + a margin is changed; the default here is *True*, on the + assumption that when margins are specified, no additional + padding to match tick marks is usually desired. Setting + *tight* to *None* will preserve the previous setting. + + Specifying any margin changes only the autoscaling; for example, + if *xmargin* is not None, then *xmargin* times the X data + interval will be added to each end of that interval before + it is used in autoscaling. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + if not args and not kw: + return self._xmargin, self._ymargin, self._zmargin + + tight = kw.pop('tight', True) + mx = kw.pop('x', None) + my = kw.pop('y', None) + mz = kw.pop('z', None) + if not args: + pass + elif len(args) == 1: + mx = my = mz = args[0] + elif len(args) == 2: + warnings.warn( + "Passing exactly two positional arguments to Axes3D.margins " + "is deprecated. If needed, pass them as keyword arguments " + "instead", cbook.mplDeprecation) + mx, my = args + elif len(args) == 3: + mx, my, mz = args + else: + raise ValueError( + "Axes3D.margins takes at most three positional arguments") + if mx is not None: + self.set_xmargin(mx) + if my is not None: + self.set_ymargin(my) + if mz is not None: + self.set_zmargin(mz) + + scalex = mx is not None + scaley = my is not None + scalez = mz is not None + + self.autoscale_view(tight=tight, scalex=scalex, scaley=scaley, + scalez=scalez) + + def autoscale(self, enable=True, axis='both', tight=None): + """ + Convenience method for simple axis view autoscaling. + See :meth:`matplotlib.axes.Axes.autoscale` for full explanation. + Note that this function behaves the same, but for all + three axes. Therefore, 'z' can be passed for *axis*, + and 'both' applies to all three axes. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + if enable is None: + scalex = True + scaley = True + scalez = True + else: + if axis in ['x', 'both']: + self._autoscaleXon = scalex = bool(enable) + else: + scalex = False + if axis in ['y', 'both']: + self._autoscaleYon = scaley = bool(enable) + else: + scaley = False + if axis in ['z', 'both']: + self._autoscaleZon = scalez = bool(enable) + else: + scalez = False + self.autoscale_view(tight=tight, scalex=scalex, scaley=scaley, + scalez=scalez) + + def auto_scale_xyz(self, X, Y, Z=None, had_data=None): + x, y, z = map(np.asarray, (X, Y, Z)) + try: + x, y = x.flatten(), y.flatten() + if Z is not None: + z = z.flatten() + except AttributeError: + raise + + # This updates the bounding boxes as to keep a record as + # to what the minimum sized rectangular volume holds the + # data. + self.xy_dataLim.update_from_data_xy(np.array([x, y]).T, not had_data) + if z is not None: + self.zz_dataLim.update_from_data_xy(np.array([z, z]).T, not had_data) + + # Let autoscale_view figure out how to use this data. + self.autoscale_view() + + def autoscale_view(self, tight=None, scalex=True, scaley=True, + scalez=True): + """ + Autoscale the view limits using the data limits. + See :meth:`matplotlib.axes.Axes.autoscale_view` for documentation. + Note that this function applies to the 3D axes, and as such + adds the *scalez* to the function arguments. + + .. versionchanged :: 1.1.0 + Function signature was changed to better match the 2D version. + *tight* is now explicitly a kwarg and placed first. + + .. versionchanged :: 1.2.1 + This is now fully functional. + + """ + if not self._ready: + return + + # This method looks at the rectangular volume (see above) + # of data and decides how to scale the view portal to fit it. + if tight is None: + # if image data only just use the datalim + _tight = self._tight or (len(self.images)>0 and + len(self.lines)==0 and + len(self.patches)==0) + else: + _tight = self._tight = bool(tight) + + if scalex and self._autoscaleXon: + xshared = self._shared_x_axes.get_siblings(self) + dl = [ax.dataLim for ax in xshared] + bb = mtransforms.BboxBase.union(dl) + x0, x1 = self.xy_dataLim.intervalx + xlocator = self.xaxis.get_major_locator() + try: + x0, x1 = xlocator.nonsingular(x0, x1) + except AttributeError: + x0, x1 = mtransforms.nonsingular(x0, x1, increasing=False, + expander=0.05) + if self._xmargin > 0: + delta = (x1 - x0) * self._xmargin + x0 -= delta + x1 += delta + if not _tight: + x0, x1 = xlocator.view_limits(x0, x1) + self.set_xbound(x0, x1) + + if scaley and self._autoscaleYon: + yshared = self._shared_y_axes.get_siblings(self) + dl = [ax.dataLim for ax in yshared] + bb = mtransforms.BboxBase.union(dl) + y0, y1 = self.xy_dataLim.intervaly + ylocator = self.yaxis.get_major_locator() + try: + y0, y1 = ylocator.nonsingular(y0, y1) + except AttributeError: + y0, y1 = mtransforms.nonsingular(y0, y1, increasing=False, + expander=0.05) + if self._ymargin > 0: + delta = (y1 - y0) * self._ymargin + y0 -= delta + y1 += delta + if not _tight: + y0, y1 = ylocator.view_limits(y0, y1) + self.set_ybound(y0, y1) + + if scalez and self._autoscaleZon: + zshared = self._shared_z_axes.get_siblings(self) + dl = [ax.dataLim for ax in zshared] + bb = mtransforms.BboxBase.union(dl) + z0, z1 = self.zz_dataLim.intervalx + zlocator = self.zaxis.get_major_locator() + try: + z0, z1 = zlocator.nonsingular(z0, z1) + except AttributeError: + z0, z1 = mtransforms.nonsingular(z0, z1, increasing=False, + expander=0.05) + if self._zmargin > 0: + delta = (z1 - z0) * self._zmargin + z0 -= delta + z1 += delta + if not _tight: + z0, z1 = zlocator.view_limits(z0, z1) + self.set_zbound(z0, z1) + + def get_w_lims(self): + '''Get 3D world limits.''' + minx, maxx = self.get_xlim3d() + miny, maxy = self.get_ylim3d() + minz, maxz = self.get_zlim3d() + return minx, maxx, miny, maxy, minz, maxz + + def _determine_lims(self, xmin=None, xmax=None, *args, **kwargs): + if xmax is None and cbook.iterable(xmin): + xmin, xmax = xmin + if xmin == xmax: + xmin -= 0.05 + xmax += 0.05 + return (xmin, xmax) + + def set_xlim3d(self, left=None, right=None, emit=True, auto=False, **kw): + """ + Set 3D x limits. + + See :meth:`matplotlib.axes.Axes.set_xlim` for full documentation. + + """ + if 'xmin' in kw: + left = kw.pop('xmin') + if 'xmax' in kw: + right = kw.pop('xmax') + if kw: + raise ValueError("unrecognized kwargs: %s" % list(kw)) + + if right is None and cbook.iterable(left): + left, right = left + + self._process_unit_info(xdata=(left, right)) + left = self._validate_converted_limits(left, self.convert_xunits) + right = self._validate_converted_limits(right, self.convert_xunits) + + old_left, old_right = self.get_xlim() + if left is None: + left = old_left + if right is None: + right = old_right + + if left == right: + warnings.warn(('Attempting to set identical left==right results\n' + 'in singular transformations; automatically expanding.\n' + 'left=%s, right=%s') % (left, right)) + left, right = mtransforms.nonsingular(left, right, increasing=False) + left, right = self.xaxis.limit_range_for_scale(left, right) + self.xy_viewLim.intervalx = (left, right) + + if auto is not None: + self._autoscaleXon = bool(auto) + + if emit: + self.callbacks.process('xlim_changed', self) + # Call all of the other x-axes that are shared with this one + for other in self._shared_x_axes.get_siblings(self): + if other is not self: + other.set_xlim(self.xy_viewLim.intervalx, + emit=False, auto=auto) + if (other.figure != self.figure and + other.figure.canvas is not None): + other.figure.canvas.draw_idle() + self.stale = True + return left, right + set_xlim = set_xlim3d + + def set_ylim3d(self, bottom=None, top=None, emit=True, auto=False, **kw): + """ + Set 3D y limits. + + See :meth:`matplotlib.axes.Axes.set_ylim` for full documentation. + + """ + if 'ymin' in kw: + bottom = kw.pop('ymin') + if 'ymax' in kw: + top = kw.pop('ymax') + if kw: + raise ValueError("unrecognized kwargs: %s" % list(kw)) + + if top is None and cbook.iterable(bottom): + bottom, top = bottom + + self._process_unit_info(ydata=(bottom, top)) + bottom = self._validate_converted_limits(bottom, self.convert_yunits) + top = self._validate_converted_limits(top, self.convert_yunits) + + old_bottom, old_top = self.get_ylim() + if bottom is None: + bottom = old_bottom + if top is None: + top = old_top + + if top == bottom: + warnings.warn(('Attempting to set identical bottom==top results\n' + 'in singular transformations; automatically expanding.\n' + 'bottom=%s, top=%s') % (bottom, top)) + bottom, top = mtransforms.nonsingular(bottom, top, increasing=False) + bottom, top = self.yaxis.limit_range_for_scale(bottom, top) + self.xy_viewLim.intervaly = (bottom, top) + + if auto is not None: + self._autoscaleYon = bool(auto) + + if emit: + self.callbacks.process('ylim_changed', self) + # Call all of the other y-axes that are shared with this one + for other in self._shared_y_axes.get_siblings(self): + if other is not self: + other.set_ylim(self.xy_viewLim.intervaly, + emit=False, auto=auto) + if (other.figure != self.figure and + other.figure.canvas is not None): + other.figure.canvas.draw_idle() + self.stale = True + return bottom, top + set_ylim = set_ylim3d + + def set_zlim3d(self, bottom=None, top=None, emit=True, auto=False, **kw): + """ + Set 3D z limits. + + See :meth:`matplotlib.axes.Axes.set_ylim` for full documentation + + """ + if 'zmin' in kw: + bottom = kw.pop('zmin') + if 'zmax' in kw: + top = kw.pop('zmax') + if kw: + raise ValueError("unrecognized kwargs: %s" % list(kw)) + + if top is None and cbook.iterable(bottom): + bottom, top = bottom + + self._process_unit_info(zdata=(bottom, top)) + bottom = self._validate_converted_limits(bottom, self.convert_zunits) + top = self._validate_converted_limits(top, self.convert_zunits) + + old_bottom, old_top = self.get_zlim() + if bottom is None: + bottom = old_bottom + if top is None: + top = old_top + + if top == bottom: + warnings.warn(('Attempting to set identical bottom==top results\n' + 'in singular transformations; automatically expanding.\n' + 'bottom=%s, top=%s') % (bottom, top)) + bottom, top = mtransforms.nonsingular(bottom, top, increasing=False) + bottom, top = self.zaxis.limit_range_for_scale(bottom, top) + self.zz_viewLim.intervalx = (bottom, top) + + if auto is not None: + self._autoscaleZon = bool(auto) + + if emit: + self.callbacks.process('zlim_changed', self) + # Call all of the other y-axes that are shared with this one + for other in self._shared_z_axes.get_siblings(self): + if other is not self: + other.set_zlim(self.zz_viewLim.intervalx, + emit=False, auto=auto) + if (other.figure != self.figure and + other.figure.canvas is not None): + other.figure.canvas.draw_idle() + self.stale = True + return bottom, top + set_zlim = set_zlim3d + + def get_xlim3d(self): + return tuple(self.xy_viewLim.intervalx) + get_xlim3d.__doc__ = maxes.Axes.get_xlim.__doc__ + get_xlim = get_xlim3d + if get_xlim.__doc__ is not None: + get_xlim.__doc__ += """ + .. versionchanged :: 1.1.0 + This function now correctly refers to the 3D x-limits + """ + + def get_ylim3d(self): + return tuple(self.xy_viewLim.intervaly) + get_ylim3d.__doc__ = maxes.Axes.get_ylim.__doc__ + get_ylim = get_ylim3d + if get_ylim.__doc__ is not None: + get_ylim.__doc__ += """ + .. versionchanged :: 1.1.0 + This function now correctly refers to the 3D y-limits. + """ + + def get_zlim3d(self): + '''Get 3D z limits.''' + return tuple(self.zz_viewLim.intervalx) + get_zlim = get_zlim3d + + def get_zscale(self): + """ + Return the zaxis scale string %s + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ % (", ".join(mscale.get_scale_names())) + return self.zaxis.get_scale() + + # We need to slightly redefine these to pass scalez=False + # to their calls of autoscale_view. + def set_xscale(self, value, **kwargs): + self.xaxis._set_scale(value, **kwargs) + self.autoscale_view(scaley=False, scalez=False) + self._update_transScale() + if maxes.Axes.set_xscale.__doc__ is not None: + set_xscale.__doc__ = maxes.Axes.set_xscale.__doc__ + """ + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + + def set_yscale(self, value, **kwargs): + self.yaxis._set_scale(value, **kwargs) + self.autoscale_view(scalex=False, scalez=False) + self._update_transScale() + self.stale = True + if maxes.Axes.set_yscale.__doc__ is not None: + set_yscale.__doc__ = maxes.Axes.set_yscale.__doc__ + """ + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + + @docstring.dedent_interpd + def set_zscale(self, value, **kwargs): + """ + Set the scaling of the z-axis: %(scale)s + + ACCEPTS: [%(scale)s] + + Different kwargs are accepted, depending on the scale: + %(scale_docs)s + + .. note :: + Currently, Axes3D objects only supports linear scales. + Other scales may or may not work, and support for these + is improving with each release. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + self.zaxis._set_scale(value, **kwargs) + self.autoscale_view(scalex=False, scaley=False) + self._update_transScale() + self.stale = True + + def set_zticks(self, *args, **kwargs): + """ + Set z-axis tick locations. + See :meth:`matplotlib.axes.Axes.set_yticks` for more details. + + .. note:: + Minor ticks are not supported. + + .. versionadded:: 1.1.0 + """ + return self.zaxis.set_ticks(*args, **kwargs) + + def get_zticks(self, minor=False): + """ + Return the z ticks as a list of locations + See :meth:`matplotlib.axes.Axes.get_yticks` for more details. + + .. note:: + Minor ticks are not supported. + + .. versionadded:: 1.1.0 + """ + return self.zaxis.get_ticklocs(minor=minor) + + def get_zmajorticklabels(self): + """ + Get the ztick labels as a list of Text instances + + .. versionadded :: 1.1.0 + """ + return cbook.silent_list('Text zticklabel', + self.zaxis.get_majorticklabels()) + + def get_zminorticklabels(self): + """ + Get the ztick labels as a list of Text instances + + .. note:: + Minor ticks are not supported. This function was added + only for completeness. + + .. versionadded :: 1.1.0 + """ + return cbook.silent_list('Text zticklabel', + self.zaxis.get_minorticklabels()) + + def set_zticklabels(self, *args, **kwargs): + """ + Set z-axis tick labels. + See :meth:`matplotlib.axes.Axes.set_yticklabels` for more details. + + .. note:: + Minor ticks are not supported by Axes3D objects. + + .. versionadded:: 1.1.0 + """ + return self.zaxis.set_ticklabels(*args, **kwargs) + + def get_zticklabels(self, minor=False): + """ + Get ztick labels as a list of Text instances. + See :meth:`matplotlib.axes.Axes.get_yticklabels` for more details. + + .. note:: + Minor ticks are not supported. + + .. versionadded:: 1.1.0 + """ + return cbook.silent_list('Text zticklabel', + self.zaxis.get_ticklabels(minor=minor)) + + def zaxis_date(self, tz=None): + """ + Sets up z-axis ticks and labels that treat the z data as dates. + + *tz* is a timezone string or :class:`tzinfo` instance. + Defaults to rc value. + + .. note:: + This function is merely provided for completeness. + Axes3D objects do not officially support dates for ticks, + and so this may or may not work as expected. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + self.zaxis.axis_date(tz) + + def get_zticklines(self): + """ + Get ztick lines as a list of Line2D instances. + Note that this function is provided merely for completeness. + These lines are re-calculated as the display changes. + + .. versionadded:: 1.1.0 + """ + return self.zaxis.get_ticklines() + + def clabel(self, *args, **kwargs): + """ + This function is currently not implemented for 3D axes. + Returns *None*. + """ + return None + + def view_init(self, elev=None, azim=None): + """ + Set the elevation and azimuth of the axes. + + This can be used to rotate the axes programmatically. + + 'elev' stores the elevation angle in the z plane. + 'azim' stores the azimuth angle in the x,y plane. + + if elev or azim are None (default), then the initial value + is used which was specified in the :class:`Axes3D` constructor. + """ + + self.dist = 10 + + if elev is None: + self.elev = self.initial_elev + else: + self.elev = elev + + if azim is None: + self.azim = self.initial_azim + else: + self.azim = azim + + def set_proj_type(self, proj_type): + """ + Set the projection type. + + Parameters + ---------- + proj_type : str + Type of projection, accepts 'persp' and 'ortho'. + + """ + if proj_type == 'persp': + self._projection = proj3d.persp_transformation + elif proj_type == 'ortho': + self._projection = proj3d.ortho_transformation + else: + raise ValueError("unrecognized projection: %s" % proj_type) + + def get_proj(self): + """ + Create the projection matrix from the current viewing position. + + elev stores the elevation angle in the z plane + azim stores the azimuth angle in the x,y plane + + dist is the distance of the eye viewing point from the object + point. + + """ + relev, razim = np.pi * self.elev/180, np.pi * self.azim/180 + + xmin, xmax = self.get_xlim3d() + ymin, ymax = self.get_ylim3d() + zmin, zmax = self.get_zlim3d() + + # transform to uniform world coordinates 0-1.0,0-1.0,0-1.0 + worldM = proj3d.world_transformation(xmin, xmax, + ymin, ymax, + zmin, zmax) + + # look into the middle of the new coordinates + R = np.array([0.5, 0.5, 0.5]) + + xp = R[0] + np.cos(razim) * np.cos(relev) * self.dist + yp = R[1] + np.sin(razim) * np.cos(relev) * self.dist + zp = R[2] + np.sin(relev) * self.dist + E = np.array((xp, yp, zp)) + + self.eye = E + self.vvec = R - E + self.vvec = self.vvec / proj3d.mod(self.vvec) + + if abs(relev) > np.pi/2: + # upside down + V = np.array((0, 0, -1)) + else: + V = np.array((0, 0, 1)) + zfront, zback = -self.dist, self.dist + + viewM = proj3d.view_transformation(E, R, V) + projM = self._projection(zfront, zback) + M0 = np.dot(viewM, worldM) + M = np.dot(projM, M0) + return M + + def mouse_init(self, rotate_btn=1, zoom_btn=3): + """Initializes mouse button callbacks to enable 3D rotation of + the axes. Also optionally sets the mouse buttons for 3D rotation + and zooming. + + ============ ======================================================= + Argument Description + ============ ======================================================= + *rotate_btn* The integer or list of integers specifying which mouse + button or buttons to use for 3D rotation of the axes. + Default = 1. + + *zoom_btn* The integer or list of integers specifying which mouse + button or buttons to use to zoom the 3D axes. + Default = 3. + ============ ======================================================= + + """ + self.button_pressed = None + canv = self.figure.canvas + if canv is not None: + c1 = canv.mpl_connect('motion_notify_event', self._on_move) + c2 = canv.mpl_connect('button_press_event', self._button_press) + c3 = canv.mpl_connect('button_release_event', self._button_release) + self._cids = [c1, c2, c3] + else: + warnings.warn( + "Axes3D.figure.canvas is 'None', mouse rotation disabled. " + "Set canvas then call Axes3D.mouse_init().") + + # coerce scalars into array-like, then convert into + # a regular list to avoid comparisons against None + # which breaks in recent versions of numpy. + self._rotate_btn = np.atleast_1d(rotate_btn).tolist() + self._zoom_btn = np.atleast_1d(zoom_btn).tolist() + + def can_zoom(self): + """ + Return *True* if this axes supports the zoom box button functionality. + + 3D axes objects do not use the zoom box button. + """ + return False + + def can_pan(self): + """ + Return *True* if this axes supports the pan/zoom button functionality. + + 3D axes objects do not use the pan/zoom button. + """ + return False + + def cla(self): + """ + Clear axes + """ + # Disabling mouse interaction might have been needed a long + # time ago, but I can't find a reason for it now - BVR (2012-03) + #self.disable_mouse_rotation() + super(Axes3D, self).cla() + self.zaxis.cla() + + if self._sharez is not None: + self.zaxis.major = self._sharez.zaxis.major + self.zaxis.minor = self._sharez.zaxis.minor + z0, z1 = self._sharez.get_zlim() + self.set_zlim(z0, z1, emit=False, auto=None) + self.zaxis._set_scale(self._sharez.zaxis.get_scale()) + else: + self.zaxis._set_scale('linear') + try: + self.set_zlim(0, 1) + except TypeError: + pass + + self._autoscaleZon = True + self._zmargin = 0 + + self.grid(rcParams['axes3d.grid']) + + def disable_mouse_rotation(self): + """Disable mouse button callbacks. + """ + # Disconnect the various events we set. + for cid in self._cids: + self.figure.canvas.mpl_disconnect(cid) + + self._cids = [] + + def _button_press(self, event): + if event.inaxes == self: + self.button_pressed = event.button + self.sx, self.sy = event.xdata, event.ydata + + def _button_release(self, event): + self.button_pressed = None + + def format_zdata(self, z): + """ + Return *z* string formatted. This function will use the + :attr:`fmt_zdata` attribute if it is callable, else will fall + back on the zaxis major formatter + """ + try: return self.fmt_zdata(z) + except (AttributeError, TypeError): + func = self.zaxis.get_major_formatter().format_data_short + val = func(z) + return val + + def format_coord(self, xd, yd): + """ + Given the 2D view coordinates attempt to guess a 3D coordinate. + Looks for the nearest edge to the point and then assumes that + the point is at the same z location as the nearest point on the edge. + """ + + if self.M is None: + return '' + + if self.button_pressed in self._rotate_btn: + return 'azimuth=%d deg, elevation=%d deg ' % (self.azim, self.elev) + # ignore xd and yd and display angles instead + + # nearest edge + p0, p1 = min(self.tunit_edges(), + key=lambda edge: proj3d.line2d_seg_dist( + edge[0], edge[1], (xd, yd))) + + # scale the z value to match + x0, y0, z0 = p0 + x1, y1, z1 = p1 + d0 = np.hypot(x0-xd, y0-yd) + d1 = np.hypot(x1-xd, y1-yd) + dt = d0+d1 + z = d1/dt * z0 + d0/dt * z1 + + x, y, z = proj3d.inv_transform(xd, yd, z, self.M) + + xs = self.format_xdata(x) + ys = self.format_ydata(y) + zs = self.format_zdata(z) + return 'x=%s, y=%s, z=%s' % (xs, ys, zs) + + def _on_move(self, event): + """Mouse moving + + button-1 rotates by default. Can be set explicitly in mouse_init(). + button-3 zooms by default. Can be set explicitly in mouse_init(). + """ + + if not self.button_pressed: + return + + if self.M is None: + return + + x, y = event.xdata, event.ydata + # In case the mouse is out of bounds. + if x is None: + return + + dx, dy = x - self.sx, y - self.sy + w = self._pseudo_w + h = self._pseudo_h + self.sx, self.sy = x, y + + # Rotation + if self.button_pressed in self._rotate_btn: + # rotate viewing point + # get the x and y pixel coords + if dx == 0 and dy == 0: + return + self.elev = art3d.norm_angle(self.elev - (dy/h)*180) + self.azim = art3d.norm_angle(self.azim - (dx/w)*180) + self.get_proj() + self.stale = True + self.figure.canvas.draw_idle() + +# elif self.button_pressed == 2: + # pan view + # project xv,yv,zv -> xw,yw,zw + # pan +# pass + + # Zoom + elif self.button_pressed in self._zoom_btn: + # zoom view + # hmmm..this needs some help from clipping.... + minx, maxx, miny, maxy, minz, maxz = self.get_w_lims() + df = 1-((h - dy)/h) + dx = (maxx-minx)*df + dy = (maxy-miny)*df + dz = (maxz-minz)*df + self.set_xlim3d(minx - dx, maxx + dx) + self.set_ylim3d(miny - dy, maxy + dy) + self.set_zlim3d(minz - dz, maxz + dz) + self.get_proj() + self.figure.canvas.draw_idle() + + def set_zlabel(self, zlabel, fontdict=None, labelpad=None, **kwargs): + ''' + Set zlabel. See doc for :meth:`set_ylabel` for description. + + ''' + if labelpad is not None : self.zaxis.labelpad = labelpad + return self.zaxis.set_label_text(zlabel, fontdict, **kwargs) + + def get_zlabel(self): + """ + Get the z-label text string. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + label = self.zaxis.get_label() + return label.get_text() + + #### Axes rectangle characteristics + + def get_frame_on(self): + """ + Get whether the 3D axes panels are drawn. + + .. versionadded :: 1.1.0 + """ + return self._frameon + + def set_frame_on(self, b): + """ + Set whether the 3D axes panels are drawn. + + .. versionadded :: 1.1.0 + + Parameters + ---------- + b : bool + .. ACCEPTS: bool + """ + self._frameon = bool(b) + self.stale = True + + def get_axisbelow(self): + """ + Get whether axis below is true or not. + + For axes3d objects, this will always be *True* + + .. versionadded :: 1.1.0 + This function was added for completeness. + """ + return True + + def set_axisbelow(self, b): + """ + Set whether axis ticks and gridlines are above or below most artists. + + For axes3d objects, this will ignore any settings and just use *True* + + .. versionadded :: 1.1.0 + This function was added for completeness. + + Parameters + ---------- + b : bool + .. ACCEPTS: bool + """ + self._axisbelow = True + self.stale = True + + def grid(self, b=True, **kwargs): + ''' + Set / unset 3D grid. + + .. note:: + + Currently, this function does not behave the same as + :meth:`matplotlib.axes.Axes.grid`, but it is intended to + eventually support that behavior. + + .. versionchanged :: 1.1.0 + This function was changed, but not tested. Please report any bugs. + ''' + # TODO: Operate on each axes separately + if len(kwargs): + b = True + self._draw_grid = cbook._string_to_bool(b) + self.stale = True + + def ticklabel_format(self, **kwargs): + """ + Convenience method for manipulating the ScalarFormatter + used by default for linear axes in Axed3D objects. + + See :meth:`matplotlib.axes.Axes.ticklabel_format` for full + documentation. Note that this version applies to all three + axes of the Axes3D object. Therefore, the *axis* argument + will also accept a value of 'z' and the value of 'both' will + apply to all three axes. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + style = kwargs.pop('style', '').lower() + scilimits = kwargs.pop('scilimits', None) + useOffset = kwargs.pop('useOffset', None) + axis = kwargs.pop('axis', 'both').lower() + if scilimits is not None: + try: + m, n = scilimits + m+n+1 # check that both are numbers + except (ValueError, TypeError): + raise ValueError("scilimits must be a sequence of 2 integers") + if style[:3] == 'sci': + sb = True + elif style in ['plain', 'comma']: + sb = False + if style == 'plain': + cb = False + else: + cb = True + raise NotImplementedError("comma style remains to be added") + elif style == '': + sb = None + else: + raise ValueError("%s is not a valid style value") + try: + if sb is not None: + if axis in ['both', 'z']: + self.xaxis.major.formatter.set_scientific(sb) + if axis in ['both', 'y']: + self.yaxis.major.formatter.set_scientific(sb) + if axis in ['both', 'z'] : + self.zaxis.major.formatter.set_scientific(sb) + if scilimits is not None: + if axis in ['both', 'x']: + self.xaxis.major.formatter.set_powerlimits(scilimits) + if axis in ['both', 'y']: + self.yaxis.major.formatter.set_powerlimits(scilimits) + if axis in ['both', 'z']: + self.zaxis.major.formatter.set_powerlimits(scilimits) + if useOffset is not None: + if axis in ['both', 'x']: + self.xaxis.major.formatter.set_useOffset(useOffset) + if axis in ['both', 'y']: + self.yaxis.major.formatter.set_useOffset(useOffset) + if axis in ['both', 'z']: + self.zaxis.major.formatter.set_useOffset(useOffset) + except AttributeError: + raise AttributeError( + "This method only works with the ScalarFormatter.") + + def locator_params(self, axis='both', tight=None, **kwargs): + """ + Convenience method for controlling tick locators. + + See :meth:`matplotlib.axes.Axes.locator_params` for full + documentation Note that this is for Axes3D objects, + therefore, setting *axis* to 'both' will result in the + parameters being set for all three axes. Also, *axis* + can also take a value of 'z' to apply parameters to the + z axis. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + _x = axis in ['x', 'both'] + _y = axis in ['y', 'both'] + _z = axis in ['z', 'both'] + if _x: + self.xaxis.get_major_locator().set_params(**kwargs) + if _y: + self.yaxis.get_major_locator().set_params(**kwargs) + if _z: + self.zaxis.get_major_locator().set_params(**kwargs) + self.autoscale_view(tight=tight, scalex=_x, scaley=_y, scalez=_z) + + def tick_params(self, axis='both', **kwargs): + """ + Convenience method for changing the appearance of ticks and + tick labels. + + See :meth:`matplotlib.axes.Axes.tick_params` for more complete + documentation. + + The only difference is that setting *axis* to 'both' will + mean that the settings are applied to all three axes. Also, + the *axis* parameter also accepts a value of 'z', which + would mean to apply to only the z-axis. + + Also, because of how Axes3D objects are drawn very differently + from regular 2D axes, some of these settings may have + ambiguous meaning. For simplicity, the 'z' axis will + accept settings as if it was like the 'y' axis. + + .. note:: + While this function is currently implemented, the core part + of the Axes3D object may ignore some of these settings. + Future releases will fix this. Priority will be given to + those who file bugs. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + super(Axes3D, self).tick_params(axis, **kwargs) + if axis in ['z', 'both'] : + zkw = dict(kwargs) + zkw.pop('top', None) + zkw.pop('bottom', None) + zkw.pop('labeltop', None) + zkw.pop('labelbottom', None) + self.zaxis.set_tick_params(**zkw) + + ### data limits, ticks, tick labels, and formatting + + def invert_zaxis(self): + """ + Invert the z-axis. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + bottom, top = self.get_zlim() + self.set_zlim(top, bottom, auto=None) + + def zaxis_inverted(self): + ''' + Returns True if the z-axis is inverted. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + ''' + bottom, top = self.get_zlim() + return top < bottom + + def get_zbound(self): + """ + Returns the z-axis numerical bounds where:: + + lowerBound < upperBound + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + bottom, top = self.get_zlim() + if bottom < top: + return bottom, top + else: + return top, bottom + + def set_zbound(self, lower=None, upper=None): + """ + Set the lower and upper numerical bounds of the z-axis. + This method will honor axes inversion regardless of parameter order. + It will not change the :attr:`_autoscaleZon` attribute. + + .. versionadded :: 1.1.0 + This function was added, but not tested. Please report any bugs. + """ + if upper is None and cbook.iterable(lower): + lower,upper = lower + + old_lower,old_upper = self.get_zbound() + + if lower is None: lower = old_lower + if upper is None: upper = old_upper + + if self.zaxis_inverted(): + if lower < upper: + self.set_zlim(upper, lower, auto=None) + else: + self.set_zlim(lower, upper, auto=None) + else : + if lower < upper: + self.set_zlim(lower, upper, auto=None) + else : + self.set_zlim(upper, lower, auto=None) + + def text(self, x, y, z, s, zdir=None, **kwargs): + ''' + Add text to the plot. kwargs will be passed on to Axes.text, + except for the `zdir` keyword, which sets the direction to be + used as the z direction. + ''' + text = super(Axes3D, self).text(x, y, s, **kwargs) + art3d.text_2d_to_3d(text, z, zdir) + return text + + text3D = text + text2D = Axes.text + + def plot(self, xs, ys, *args, **kwargs): + ''' + Plot 2D or 3D data. + + ========== ================================================ + Argument Description + ========== ================================================ + *xs*, *ys* x, y coordinates of vertices + + *zs* z value(s), either one for all points or one for + each point. + *zdir* Which direction to use as z ('x', 'y' or 'z') + when plotting a 2D set. + ========== ================================================ + + Other arguments are passed on to + :func:`~matplotlib.axes.Axes.plot` + ''' + had_data = self.has_data() + + # `zs` can be passed positionally or as keyword; checking whether + # args[0] is a string matches the behavior of 2D `plot` (via + # `_process_plot_var_args`). + if args and not isinstance(args[0], six.string_types): + zs = args[0] + args = args[1:] + if 'zs' in kwargs: + raise TypeError("plot() for multiple values for argument 'z'") + else: + zs = kwargs.pop('zs', 0) + zdir = kwargs.pop('zdir', 'z') + + # Match length + zs = _backports.broadcast_to(zs, len(xs)) + + lines = super(Axes3D, self).plot(xs, ys, *args, **kwargs) + for line in lines: + art3d.line_2d_to_3d(line, zs=zs, zdir=zdir) + + xs, ys, zs = art3d.juggle_axes(xs, ys, zs, zdir) + self.auto_scale_xyz(xs, ys, zs, had_data) + return lines + + plot3D = plot + + def plot_surface(self, X, Y, Z, *args, **kwargs): + """ + Create a surface plot. + + By default it will be colored in shades of a solid color, but it also + supports color mapping by supplying the *cmap* argument. + + .. note:: + + The *rcount* and *ccount* kwargs, which both default to 50, + determine the maximum number of samples used in each direction. If + the input data is larger, it will be downsampled (by slicing) to + these numbers of points. + + Parameters + ---------- + X, Y, Z : 2d arrays + Data values. + + rcount, ccount : int + Maximum number of samples used in each direction. If the input + data is larger, it will be downsampled (by slicing) to these + numbers of points. Defaults to 50. + + .. versionadded:: 2.0 + + rstride, cstride : int + Downsampling stride in each direction. These arguments are + mutually exclusive with *rcount* and *ccount*. If only one of + *rstride* or *cstride* is set, the other defaults to 10. + + 'classic' mode uses a default of ``rstride = cstride = 10`` instead + of the new default of ``rcount = ccount = 50``. + + color : color-like + Color of the surface patches. + + cmap : Colormap + Colormap of the surface patches. + + facecolors : array-like of colors. + Colors of each individual patch. + + norm : Normalize + Normalization for the colormap. + + vmin, vmax : float + Bounds for the normalization. + + shade : bool + Whether to shade the face colors. + + **kwargs : + Other arguments are forwarded to `.Poly3DCollection`. + """ + + had_data = self.has_data() + + if Z.ndim != 2: + raise ValueError("Argument Z must be 2-dimensional.") + # TODO: Support masked arrays + X, Y, Z = np.broadcast_arrays(X, Y, Z) + rows, cols = Z.shape + + has_stride = 'rstride' in kwargs or 'cstride' in kwargs + has_count = 'rcount' in kwargs or 'ccount' in kwargs + + if has_stride and has_count: + raise ValueError("Cannot specify both stride and count arguments") + + rstride = kwargs.pop('rstride', 10) + cstride = kwargs.pop('cstride', 10) + rcount = kwargs.pop('rcount', 50) + ccount = kwargs.pop('ccount', 50) + + if rcParams['_internal.classic_mode']: + # Strides have priority over counts in classic mode. + # So, only compute strides from counts + # if counts were explicitly given + if has_count: + rstride = int(max(np.ceil(rows / rcount), 1)) + cstride = int(max(np.ceil(cols / ccount), 1)) + else: + # If the strides are provided then it has priority. + # Otherwise, compute the strides from the counts. + if not has_stride: + rstride = int(max(np.ceil(rows / rcount), 1)) + cstride = int(max(np.ceil(cols / ccount), 1)) + + if 'facecolors' in kwargs: + fcolors = kwargs.pop('facecolors') + else: + color = kwargs.pop('color', None) + if color is None: + color = self._get_lines.get_next_color() + color = np.array(mcolors.to_rgba(color)) + fcolors = None + + cmap = kwargs.get('cmap', None) + norm = kwargs.pop('norm', None) + vmin = kwargs.pop('vmin', None) + vmax = kwargs.pop('vmax', None) + linewidth = kwargs.get('linewidth', None) + shade = kwargs.pop('shade', cmap is None) + lightsource = kwargs.pop('lightsource', None) + + # Shade the data + if shade and cmap is not None and fcolors is not None: + fcolors = self._shade_colors_lightsource(Z, cmap, lightsource) + + polys = [] + # Only need these vectors to shade if there is no cmap + if cmap is None and shade : + totpts = int(np.ceil((rows - 1) / rstride) * + np.ceil((cols - 1) / cstride)) + v1 = np.empty((totpts, 3)) + v2 = np.empty((totpts, 3)) + # This indexes the vertex points + which_pt = 0 + + + #colset contains the data for coloring: either average z or the facecolor + colset = [] + for rs in xrange(0, rows-1, rstride): + for cs in xrange(0, cols-1, cstride): + ps = [] + for a in (X, Y, Z): + ztop = a[rs,cs:min(cols, cs+cstride+1)] + zleft = a[rs+1:min(rows, rs+rstride+1), + min(cols-1, cs+cstride)] + zbase = a[min(rows-1, rs+rstride), cs:min(cols, cs+cstride+1):][::-1] + zright = a[rs:min(rows-1, rs+rstride):, cs][::-1] + z = np.concatenate((ztop, zleft, zbase, zright)) + ps.append(z) + + # The construction leaves the array with duplicate points, which + # are removed here. + ps = list(zip(*ps)) + lastp = np.array([]) + ps2 = [ps[0]] + [ps[i] for i in xrange(1, len(ps)) if ps[i] != ps[i-1]] + avgzsum = sum(p[2] for p in ps2) + polys.append(ps2) + + if fcolors is not None: + colset.append(fcolors[rs][cs]) + else: + colset.append(avgzsum / len(ps2)) + + # Only need vectors to shade if no cmap + if cmap is None and shade: + i1, i2, i3 = 0, int(len(ps2)/3), int(2*len(ps2)/3) + v1[which_pt] = np.array(ps2[i1]) - np.array(ps2[i2]) + v2[which_pt] = np.array(ps2[i2]) - np.array(ps2[i3]) + which_pt += 1 + if cmap is None and shade: + normals = np.cross(v1, v2) + else : + normals = [] + + polyc = art3d.Poly3DCollection(polys, *args, **kwargs) + + if fcolors is not None: + if shade: + colset = self._shade_colors(colset, normals) + polyc.set_facecolors(colset) + polyc.set_edgecolors(colset) + elif cmap: + colset = np.array(colset) + polyc.set_array(colset) + if vmin is not None or vmax is not None: + polyc.set_clim(vmin, vmax) + if norm is not None: + polyc.set_norm(norm) + else: + if shade: + colset = self._shade_colors(color, normals) + else: + colset = color + polyc.set_facecolors(colset) + + self.add_collection(polyc) + self.auto_scale_xyz(X, Y, Z, had_data) + + return polyc + + def _generate_normals(self, polygons): + ''' + Generate normals for polygons by using the first three points. + This normal of course might not make sense for polygons with + more than three points not lying in a plane. + ''' + + normals = [] + for verts in polygons: + v1 = np.array(verts[0]) - np.array(verts[1]) + v2 = np.array(verts[2]) - np.array(verts[0]) + normals.append(np.cross(v1, v2)) + return normals + + def _shade_colors(self, color, normals): + ''' + Shade *color* using normal vectors given by *normals*. + *color* can also be an array of the same length as *normals*. + ''' + + shade = np.array([np.dot(n / proj3d.mod(n), [-1, -1, 0.5]) + if proj3d.mod(n) else np.nan + for n in normals]) + mask = ~np.isnan(shade) + + if len(shade[mask]) > 0: + norm = Normalize(min(shade[mask]), max(shade[mask])) + shade[~mask] = min(shade[mask]) + 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 = (0.5 + norm(shade)[:, np.newaxis] * 0.5) * color + colors[:, 3] = alpha + else: + colors = np.asanyarray(color).copy() + + return colors + + def _shade_colors_lightsource(self, data, cmap, lightsource): + if lightsource is None: + lightsource = LightSource(azdeg=135, altdeg=55) + return lightsource.shade(data, cmap) + + def plot_wireframe(self, X, Y, Z, *args, **kwargs): + """ + Plot a 3D wireframe. + + .. note:: + + The *rcount* and *ccount* kwargs, which both default to 50, + determine the maximum number of samples used in each direction. If + the input data is larger, it will be downsampled (by slicing) to + these numbers of points. + + Parameters + ---------- + X, Y, Z : 2d arrays + Data values. + + rcount, ccount : int + Maximum number of samples used in each direction. If the input + data is larger, it will be downsampled (by slicing) to these + numbers of points. Setting a count to zero causes the data to be + not sampled in the corresponding direction, producing a 3D line + plot rather than a wireframe plot. Defaults to 50. + + .. versionadded:: 2.0 + + rstride, cstride : int + Downsampling stride in each direction. These arguments are + mutually exclusive with *rcount* and *ccount*. If only one of + *rstride* or *cstride* is set, the other defaults to 1. Setting a + stride to zero causes the data to be not sampled in the + corresponding direction, producing a 3D line plot rather than a + wireframe plot. + + 'classic' mode uses a default of ``rstride = cstride = 1`` instead + of the new default of ``rcount = ccount = 50``. + + **kwargs : + Other arguments are forwarded to `.Line3DCollection`. + """ + + had_data = self.has_data() + if Z.ndim != 2: + raise ValueError("Argument Z must be 2-dimensional.") + # FIXME: Support masked arrays + X, Y, Z = np.broadcast_arrays(X, Y, Z) + rows, cols = Z.shape + + has_stride = 'rstride' in kwargs or 'cstride' in kwargs + has_count = 'rcount' in kwargs or 'ccount' in kwargs + + if has_stride and has_count: + raise ValueError("Cannot specify both stride and count arguments") + + rstride = kwargs.pop('rstride', 1) + cstride = kwargs.pop('cstride', 1) + rcount = kwargs.pop('rcount', 50) + ccount = kwargs.pop('ccount', 50) + + if rcParams['_internal.classic_mode']: + # Strides have priority over counts in classic mode. + # So, only compute strides from counts + # if counts were explicitly given + if has_count: + rstride = int(max(np.ceil(rows / rcount), 1)) if rcount else 0 + cstride = int(max(np.ceil(cols / ccount), 1)) if ccount else 0 + else: + # If the strides are provided then it has priority. + # Otherwise, compute the strides from the counts. + if not has_stride: + rstride = int(max(np.ceil(rows / rcount), 1)) if rcount else 0 + cstride = int(max(np.ceil(cols / ccount), 1)) if ccount else 0 + + # We want two sets of lines, one running along the "rows" of + # Z and another set of lines running along the "columns" of Z. + # This transpose will make it easy to obtain the columns. + tX, tY, tZ = np.transpose(X), np.transpose(Y), np.transpose(Z) + + if rstride: + rii = list(xrange(0, rows, rstride)) + # Add the last index only if needed + if rows > 0 and rii[-1] != (rows - 1): + rii += [rows-1] + else: + rii = [] + if cstride: + cii = list(xrange(0, cols, cstride)) + # Add the last index only if needed + if cols > 0 and cii[-1] != (cols - 1): + cii += [cols-1] + else: + cii = [] + + if rstride == 0 and cstride == 0: + raise ValueError("Either rstride or cstride must be non zero") + + # If the inputs were empty, then just + # reset everything. + if Z.size == 0: + rii = [] + cii = [] + + xlines = [X[i] for i in rii] + ylines = [Y[i] for i in rii] + zlines = [Z[i] for i in rii] + + txlines = [tX[i] for i in cii] + tylines = [tY[i] for i in cii] + tzlines = [tZ[i] for i in cii] + + lines = ([list(zip(xl, yl, zl)) + for xl, yl, zl in zip(xlines, ylines, zlines)] + + [list(zip(xl, yl, zl)) + for xl, yl, zl in zip(txlines, tylines, tzlines)]) + + linec = art3d.Line3DCollection(lines, *args, **kwargs) + self.add_collection(linec) + self.auto_scale_xyz(X, Y, Z, had_data) + + return linec + + def plot_trisurf(self, *args, **kwargs): + """ + ============= ================================================ + Argument Description + ============= ================================================ + *X*, *Y*, *Z* Data values as 1D arrays + *color* Color of the surface patches + *cmap* A colormap for the surface patches. + *norm* An instance of Normalize to map values to colors + *vmin* Minimum value to map + *vmax* Maximum value to map + *shade* Whether to shade the facecolors + ============= ================================================ + + The (optional) triangulation can be specified in one of two ways; + either:: + + plot_trisurf(triangulation, ...) + + where triangulation is a :class:`~matplotlib.tri.Triangulation` + object, or:: + + plot_trisurf(X, Y, ...) + plot_trisurf(X, Y, triangles, ...) + plot_trisurf(X, Y, triangles=triangles, ...) + + in which case a Triangulation object will be created. See + :class:`~matplotlib.tri.Triangulation` for a explanation of + these possibilities. + + The remaining arguments are:: + + plot_trisurf(..., Z) + + where *Z* is the array of values to contour, one per point + in the triangulation. + + Other arguments are passed on to + :class:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection` + + **Examples:** + + .. plot:: gallery/mplot3d/trisurf3d.py + .. plot:: gallery/mplot3d/trisurf3d_2.py + + .. versionadded:: 1.2.0 + This plotting function was added for the v1.2.0 release. + """ + + had_data = self.has_data() + + # TODO: Support custom face colours + color = kwargs.pop('color', None) + if color is None: + color = self._get_lines.get_next_color() + color = np.array(mcolors.to_rgba(color)) + + cmap = kwargs.get('cmap', None) + norm = kwargs.pop('norm', None) + vmin = kwargs.pop('vmin', None) + vmax = kwargs.pop('vmax', None) + linewidth = kwargs.get('linewidth', None) + shade = kwargs.pop('shade', cmap is None) + lightsource = kwargs.pop('lightsource', None) + + tri, args, kwargs = Triangulation.get_from_args_and_kwargs(*args, **kwargs) + if 'Z' in kwargs: + z = np.asarray(kwargs.pop('Z')) + else: + z = np.asarray(args[0]) + # We do this so Z doesn't get passed as an arg to PolyCollection + args = args[1:] + + triangles = tri.get_masked_triangles() + xt = tri.x[triangles] + yt = tri.y[triangles] + zt = z[triangles] + + # verts = np.stack((xt, yt, zt), axis=-1) + verts = np.concatenate(( + xt[..., np.newaxis], yt[..., np.newaxis], zt[..., np.newaxis] + ), axis=-1) + + polyc = art3d.Poly3DCollection(verts, *args, **kwargs) + + if cmap: + # average over the three points of each triangle + avg_z = verts[:, :, 2].mean(axis=1) + polyc.set_array(avg_z) + if vmin is not None or vmax is not None: + polyc.set_clim(vmin, vmax) + if norm is not None: + polyc.set_norm(norm) + else: + if shade: + v1 = verts[:, 0, :] - verts[:, 1, :] + v2 = verts[:, 1, :] - verts[:, 2, :] + normals = np.cross(v1, v2) + colset = self._shade_colors(color, normals) + else: + colset = color + polyc.set_facecolors(colset) + + self.add_collection(polyc) + self.auto_scale_xyz(tri.x, tri.y, z, had_data) + + return polyc + + def _3d_extend_contour(self, cset, stride=5): + ''' + Extend a contour in 3D by creating + ''' + + levels = cset.levels + colls = cset.collections + dz = (levels[1] - levels[0]) / 2 + + for z, linec in zip(levels, colls): + topverts = art3d.paths_to_3d_segments(linec.get_paths(), z - dz) + botverts = art3d.paths_to_3d_segments(linec.get_paths(), z + dz) + + color = linec.get_color()[0] + + polyverts = [] + normals = [] + nsteps = np.round(len(topverts[0]) / stride) + if nsteps <= 1: + if len(topverts[0]) > 1: + nsteps = 2 + else: + continue + + stepsize = (len(topverts[0]) - 1) / (nsteps - 1) + for i in range(int(np.round(nsteps)) - 1): + i1 = int(np.round(i * stepsize)) + i2 = int(np.round((i + 1) * stepsize)) + polyverts.append([topverts[0][i1], + topverts[0][i2], + botverts[0][i2], + botverts[0][i1]]) + + v1 = np.array(topverts[0][i1]) - np.array(topverts[0][i2]) + v2 = np.array(topverts[0][i1]) - np.array(botverts[0][i1]) + normals.append(np.cross(v1, v2)) + + colors = self._shade_colors(color, normals) + colors2 = self._shade_colors(color, normals) + polycol = art3d.Poly3DCollection(polyverts, + facecolors=colors, + edgecolors=colors2) + polycol.set_sort_zpos(z) + self.add_collection3d(polycol) + + for col in colls: + self.collections.remove(col) + + def add_contour_set(self, cset, extend3d=False, stride=5, zdir='z', offset=None): + zdir = '-' + zdir + if extend3d: + self._3d_extend_contour(cset, stride) + else: + for z, linec in zip(cset.levels, cset.collections): + if offset is not None: + z = offset + art3d.line_collection_2d_to_3d(linec, z, zdir=zdir) + + def add_contourf_set(self, cset, zdir='z', offset=None): + zdir = '-' + zdir + for z, linec in zip(cset.levels, cset.collections): + if offset is not None : + z = offset + art3d.poly_collection_2d_to_3d(linec, z, zdir=zdir) + linec.set_sort_zpos(z) + + def contour(self, X, Y, Z, *args, **kwargs): + ''' + Create a 3D contour plot. + + ========== ================================================ + Argument Description + ========== ================================================ + *X*, *Y*, Data values as numpy.arrays + *Z* + *extend3d* Whether to extend contour in 3D (default: False) + *stride* Stride (step size) for extending contour + *zdir* The direction to use: x, y or z (default) + *offset* If specified plot a projection of the contour + lines on this position in plane normal to zdir + ========== ================================================ + + The positional and other keyword arguments are passed on to + :func:`~matplotlib.axes.Axes.contour` + + Returns a :class:`~matplotlib.axes.Axes.contour` + ''' + + extend3d = kwargs.pop('extend3d', False) + stride = kwargs.pop('stride', 5) + zdir = kwargs.pop('zdir', 'z') + offset = kwargs.pop('offset', None) + + had_data = self.has_data() + + jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir) + cset = super(Axes3D, self).contour(jX, jY, jZ, *args, **kwargs) + self.add_contour_set(cset, extend3d, stride, zdir, offset) + + self.auto_scale_xyz(X, Y, Z, had_data) + return cset + + contour3D = contour + + def tricontour(self, *args, **kwargs): + """ + Create a 3D contour plot. + + ========== ================================================ + Argument Description + ========== ================================================ + *X*, *Y*, Data values as numpy.arrays + *Z* + *extend3d* Whether to extend contour in 3D (default: False) + *stride* Stride (step size) for extending contour + *zdir* The direction to use: x, y or z (default) + *offset* If specified plot a projection of the contour + lines on this position in plane normal to zdir + ========== ================================================ + + Other keyword arguments are passed on to + :func:`~matplotlib.axes.Axes.tricontour` + + Returns a :class:`~matplotlib.axes.Axes.contour` + + .. versionchanged:: 1.3.0 + Added support for custom triangulations + + EXPERIMENTAL: This method currently produces incorrect output due to a + longstanding bug in 3D PolyCollection rendering. + """ + + extend3d = kwargs.pop('extend3d', False) + stride = kwargs.pop('stride', 5) + zdir = kwargs.pop('zdir', 'z') + offset = kwargs.pop('offset', None) + + had_data = self.has_data() + + tri, args, kwargs = Triangulation.get_from_args_and_kwargs( + *args, **kwargs) + X = tri.x + Y = tri.y + if 'Z' in kwargs: + Z = kwargs.pop('Z') + else: + Z = args[0] + # We do this so Z doesn't get passed as an arg to Axes.tricontour + args = args[1:] + + jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir) + tri = Triangulation(jX, jY, tri.triangles, tri.mask) + + cset = super(Axes3D, self).tricontour(tri, jZ, *args, **kwargs) + self.add_contour_set(cset, extend3d, stride, zdir, offset) + + self.auto_scale_xyz(X, Y, Z, had_data) + return cset + + def contourf(self, X, Y, Z, *args, **kwargs): + ''' + Create a 3D contourf plot. + + ========== ================================================ + Argument Description + ========== ================================================ + *X*, *Y*, Data values as numpy.arrays + *Z* + *zdir* The direction to use: x, y or z (default) + *offset* If specified plot a projection of the filled contour + on this position in plane normal to zdir + ========== ================================================ + + The positional and keyword arguments are passed on to + :func:`~matplotlib.axes.Axes.contourf` + + Returns a :class:`~matplotlib.axes.Axes.contourf` + + .. versionchanged :: 1.1.0 + The *zdir* and *offset* kwargs were added. + ''' + + zdir = kwargs.pop('zdir', 'z') + offset = kwargs.pop('offset', None) + + had_data = self.has_data() + + jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir) + cset = super(Axes3D, self).contourf(jX, jY, jZ, *args, **kwargs) + self.add_contourf_set(cset, zdir, offset) + + self.auto_scale_xyz(X, Y, Z, had_data) + return cset + + contourf3D = contourf + + def tricontourf(self, *args, **kwargs): + """ + Create a 3D contourf plot. + + ========== ================================================ + Argument Description + ========== ================================================ + *X*, *Y*, Data values as numpy.arrays + *Z* + *zdir* The direction to use: x, y or z (default) + *offset* If specified plot a projection of the contour + lines on this position in plane normal to zdir + ========== ================================================ + + Other keyword arguments are passed on to + :func:`~matplotlib.axes.Axes.tricontour` + + Returns a :class:`~matplotlib.axes.Axes.contour` + + .. versionchanged :: 1.3.0 + Added support for custom triangulations + + EXPERIMENTAL: This method currently produces incorrect output due to a + longstanding bug in 3D PolyCollection rendering. + """ + zdir = kwargs.pop('zdir', 'z') + offset = kwargs.pop('offset', None) + + had_data = self.has_data() + + tri, args, kwargs = Triangulation.get_from_args_and_kwargs( + *args, **kwargs) + X = tri.x + Y = tri.y + if 'Z' in kwargs: + Z = kwargs.pop('Z') + else: + Z = args[0] + # We do this so Z doesn't get passed as an arg to Axes.tricontourf + args = args[1:] + + jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir) + tri = Triangulation(jX, jY, tri.triangles, tri.mask) + + cset = super(Axes3D, self).tricontourf(tri, jZ, *args, **kwargs) + self.add_contourf_set(cset, zdir, offset) + + self.auto_scale_xyz(X, Y, Z, had_data) + return cset + + def add_collection3d(self, col, zs=0, zdir='z'): + ''' + Add a 3D collection object to the plot. + + 2D collection types are converted to a 3D version by + modifying the object and adding z coordinate information. + + Supported are: + - PolyCollection + - LineCollection + - PatchCollection + ''' + zvals = np.atleast_1d(zs) + if len(zvals) > 0 : + zsortval = min(zvals) + else : + zsortval = 0 # FIXME: Fairly arbitrary. Is there a better value? + + # FIXME: use issubclass() (although, then a 3D collection + # object would also pass.) Maybe have a collection3d + # abstract class to test for and exclude? + if type(col) is mcoll.PolyCollection: + art3d.poly_collection_2d_to_3d(col, zs=zs, zdir=zdir) + col.set_sort_zpos(zsortval) + elif type(col) is mcoll.LineCollection: + art3d.line_collection_2d_to_3d(col, zs=zs, zdir=zdir) + col.set_sort_zpos(zsortval) + elif type(col) is mcoll.PatchCollection: + art3d.patch_collection_2d_to_3d(col, zs=zs, zdir=zdir) + col.set_sort_zpos(zsortval) + + super(Axes3D, self).add_collection(col) + + def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, + *args, **kwargs): + ''' + Create a scatter plot. + + ============ ======================================================== + Argument Description + ============ ======================================================== + *xs*, *ys* Positions of data points. + *zs* Either an array of the same length as *xs* and + *ys* or a single value to place all points in + the same plane. Default is 0. + *zdir* Which direction to use as z ('x', 'y' or 'z') + when plotting a 2D set. + *s* Size in points^2. It is a scalar or an array of the + same length as *x* and *y*. + + *c* A color. *c* can be a single color format string, or a + sequence of color specifications of length *N*, or a + sequence of *N* numbers to be mapped to colors using the + *cmap* and *norm* specified via kwargs (see below). Note + that *c* should not be a single numeric RGB or RGBA + sequence because that is indistinguishable from an array + of values to be colormapped. *c* can be a 2-D array in + which the rows are RGB or RGBA, however, including the + case of a single row to specify the same color for + all points. + + *depthshade* + Whether or not to shade the scatter markers to give + the appearance of depth. Default is *True*. + ============ ======================================================== + + Keyword arguments are passed on to + :func:`~matplotlib.axes.Axes.scatter`. + + Returns a :class:`~mpl_toolkits.mplot3d.art3d.Patch3DCollection` + ''' + + had_data = self.has_data() + + xs, ys, zs = np.broadcast_arrays( + *[np.ravel(np.ma.filled(t, np.nan)) for t in [xs, ys, zs]]) + s = np.ma.ravel(s) # This doesn't have to match x, y in size. + + xs, ys, zs, s, c = cbook.delete_masked_points(xs, ys, zs, s, c) + + patches = super(Axes3D, self).scatter( + xs, ys, s=s, c=c, *args, **kwargs) + is_2d = not cbook.iterable(zs) + zs = _backports.broadcast_to(zs, len(xs)) + art3d.patch_collection_2d_to_3d(patches, zs=zs, zdir=zdir, + depthshade=depthshade) + + if self._zmargin < 0.05 and xs.size > 0: + self.set_zmargin(0.05) + + #FIXME: why is this necessary? + if not is_2d: + self.auto_scale_xyz(xs, ys, zs, had_data) + + return patches + + scatter3D = scatter + + def bar(self, left, height, zs=0, zdir='z', *args, **kwargs): + ''' + Add 2D bar(s). + + ========== ================================================ + Argument Description + ========== ================================================ + *left* The x coordinates of the left sides of the bars. + *height* The height of the bars. + *zs* Z coordinate of bars, if one value is specified + they will all be placed at the same z. + *zdir* Which direction to use as z ('x', 'y' or 'z') + when plotting a 2D set. + ========== ================================================ + + Keyword arguments are passed onto :func:`~matplotlib.axes.Axes.bar`. + + Returns a :class:`~mpl_toolkits.mplot3d.art3d.Patch3DCollection` + ''' + + had_data = self.has_data() + + patches = super(Axes3D, self).bar(left, height, *args, **kwargs) + + zs = _backports.broadcast_to(zs, len(left)) + + verts = [] + verts_zs = [] + for p, z in zip(patches, zs): + vs = art3d.get_patch_verts(p) + verts += vs.tolist() + verts_zs += [z] * len(vs) + art3d.patch_2d_to_3d(p, z, zdir) + if 'alpha' in kwargs: + p.set_alpha(kwargs['alpha']) + + if len(verts) > 0 : + # the following has to be skipped if verts is empty + # NOTE: Bugs could still occur if len(verts) > 0, + # but the "2nd dimension" is empty. + xs, ys = list(zip(*verts)) + else : + xs, ys = [], [] + + xs, ys, verts_zs = art3d.juggle_axes(xs, ys, verts_zs, zdir) + self.auto_scale_xyz(xs, ys, verts_zs, had_data) + + return patches + + def bar3d(self, x, y, z, dx, dy, dz, color=None, + zsort='average', shade=True, *args, **kwargs): + """Generate a 3D barplot. + + This method creates three dimensional barplot where the width, + depth, height, and color of the bars can all be uniquely set. + + Parameters + ---------- + x, y, z : array-like + The coordinates of the anchor point of the bars. + + dx, dy, dz : scalar or array-like + The width, depth, and height of the bars, respectively. + + color : sequence of valid color specifications, optional + The color of the bars can be specified globally or + individually. This parameter can be: + + - A single color value, to color all bars the same color. + - An array of colors of length N bars, to color each bar + independently. + - An array of colors of length 6, to color the faces of the + bars similarly. + - An array of colors of length 6 * N bars, to color each face + independently. + + When coloring the faces of the boxes specifically, this is + the order of the coloring: + + 1. -Z (bottom of box) + 2. +Z (top of box) + 3. -Y + 4. +Y + 5. -X + 6. +X + + zsort : str, optional + The z-axis sorting scheme passed onto + :func:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection` + + shade : bool, optional (default = True) + When true, this shades the dark sides of the bars (relative + to the plot's source of light). + + Any additional keyword arguments are passed onto + :func:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection` + + Returns + ------- + collection : Poly3DCollection + A collection of three dimensional polygons representing + the bars. + """ + + had_data = self.has_data() + + x, y, z, dx, dy, dz = np.broadcast_arrays( + np.atleast_1d(x), y, z, dx, dy, dz) + minx = np.min(x) + maxx = np.max(x + dx) + miny = np.min(y) + maxy = np.max(y + dy) + minz = np.min(z) + maxz = np.max(z + dz) + + polys = [] + for xi, yi, zi, dxi, dyi, dzi in zip(x, y, z, dx, dy, dz): + polys.extend([ + ((xi, yi, zi), (xi + dxi, yi, zi), + (xi + dxi, yi + dyi, zi), (xi, yi + dyi, zi)), + ((xi, yi, zi + dzi), (xi + dxi, yi, zi + dzi), + (xi + dxi, yi + dyi, zi + dzi), (xi, yi + dyi, zi + dzi)), + + ((xi, yi, zi), (xi + dxi, yi, zi), + (xi + dxi, yi, zi + dzi), (xi, yi, zi + dzi)), + ((xi, yi + dyi, zi), (xi + dxi, yi + dyi, zi), + (xi + dxi, yi + dyi, zi + dzi), (xi, yi + dyi, zi + dzi)), + + ((xi, yi, zi), (xi, yi + dyi, zi), + (xi, yi + dyi, zi + dzi), (xi, yi, zi + dzi)), + ((xi + dxi, yi, zi), (xi + dxi, yi + dyi, zi), + (xi + dxi, yi + dyi, zi + dzi), (xi + dxi, yi, zi + dzi)), + ]) + + facecolors = [] + if color is None: + color = [self._get_patches_for_fill.get_next_color()] + + if len(color) == len(x): + # bar colors specified, need to expand to number of faces + for c in color: + facecolors.extend([c] * 6) + else: + # a single color specified, or face colors specified explicitly + facecolors = list(mcolors.to_rgba_array(color)) + if len(facecolors) < len(x): + facecolors *= (6 * len(x)) + + if shade: + normals = self._generate_normals(polys) + sfacecolors = self._shade_colors(facecolors, normals) + else: + sfacecolors = facecolors + + col = art3d.Poly3DCollection(polys, + zsort=zsort, + facecolor=sfacecolors, + *args, **kwargs) + self.add_collection(col) + + self.auto_scale_xyz((minx, maxx), (miny, maxy), (minz, maxz), had_data) + + return col + + def set_title(self, label, fontdict=None, loc='center', **kwargs): + ret = super(Axes3D, self).set_title(label, fontdict=fontdict, loc=loc, + **kwargs) + (x, y) = self.title.get_position() + self.title.set_y(0.92 * y) + return ret + set_title.__doc__ = maxes.Axes.set_title.__doc__ + + def quiver(self, *args, **kwargs): + """ + Plot a 3D field of arrows. + + call signatures:: + + quiver(X, Y, Z, U, V, W, **kwargs) + + Arguments: + + *X*, *Y*, *Z*: + The x, y and z coordinates of the arrow locations (default is + tail of arrow; see *pivot* kwarg) + + *U*, *V*, *W*: + The x, y and z components of the arrow vectors + + The arguments could be array-like or scalars, so long as they + they can be broadcast together. The arguments can also be + masked arrays. If an element in any of argument is masked, then + that corresponding quiver element will not be plotted. + + Keyword arguments: + + *length*: [1.0 | float] + The length of each quiver, default to 1.0, the unit is + the same with the axes + + *arrow_length_ratio*: [0.3 | float] + The ratio of the arrow head with respect to the quiver, + default to 0.3 + + *pivot*: [ 'tail' | 'middle' | 'tip' ] + The part of the arrow that is at the grid point; the arrow + rotates about this point, hence the name *pivot*. + Default is 'tail' + + *normalize*: bool + When True, all of the arrows will be the same length. This + defaults to False, where the arrows will be different lengths + depending on the values of u,v,w. + + Any additional keyword arguments are delegated to + :class:`~matplotlib.collections.LineCollection` + + """ + def calc_arrow(uvw, angle=15): + """ + To calculate the arrow head. uvw should be a unit vector. + We normalize it here: + """ + # get unit direction vector perpendicular to (u,v,w) + norm = np.linalg.norm(uvw[:2]) + if norm > 0: + x = uvw[1] / norm + y = -uvw[0] / norm + else: + x, y = 0, 1 + + # compute the two arrowhead direction unit vectors + ra = math.radians(angle) + c = math.cos(ra) + s = math.sin(ra) + + # construct the rotation matrices + Rpos = np.array([[c+(x**2)*(1-c), x*y*(1-c), y*s], + [y*x*(1-c), c+(y**2)*(1-c), -x*s], + [-y*s, x*s, c]]) + # opposite rotation negates all the sin terms + Rneg = Rpos.copy() + Rneg[[0,1,2,2],[2,2,0,1]] = -Rneg[[0,1,2,2],[2,2,0,1]] + + # multiply them to get the rotated vector + return Rpos.dot(uvw), Rneg.dot(uvw) + + had_data = self.has_data() + + # handle kwargs + # shaft length + length = kwargs.pop('length', 1) + # arrow length ratio to the shaft length + arrow_length_ratio = kwargs.pop('arrow_length_ratio', 0.3) + # pivot point + pivot = kwargs.pop('pivot', 'tail') + # normalize + normalize = kwargs.pop('normalize', False) + + # handle args + argi = 6 + if len(args) < argi: + raise ValueError('Wrong number of arguments. Expected %d got %d' % + (argi, len(args))) + + # first 6 arguments are X, Y, Z, U, V, W + input_args = args[:argi] + # if any of the args are scalar, convert into list + input_args = [[k] if isinstance(k, (int, float)) else k + for k in input_args] + + # extract the masks, if any + masks = [k.mask for k in input_args if isinstance(k, np.ma.MaskedArray)] + # broadcast to match the shape + bcast = np.broadcast_arrays(*(input_args + masks)) + input_args = bcast[:argi] + masks = bcast[argi:] + if masks: + # combine the masks into one + mask = reduce(np.logical_or, masks) + # put mask on and compress + input_args = [np.ma.array(k, mask=mask).compressed() + for k in input_args] + else: + input_args = [k.flatten() for k in input_args] + + if any(len(v) == 0 for v in input_args): + # No quivers, so just make an empty collection and return early + linec = art3d.Line3DCollection([], *args[argi:], **kwargs) + self.add_collection(linec) + return linec + + # Following assertions must be true before proceeding + # must all be ndarray + assert all(isinstance(k, np.ndarray) for k in input_args) + # must all in same shape + assert len({k.shape for k in input_args}) == 1 + + shaft_dt = np.linspace(0, length, num=2) + arrow_dt = shaft_dt * arrow_length_ratio + + if pivot == 'tail': + shaft_dt -= length + elif pivot == 'middle': + shaft_dt -= length/2. + elif pivot != 'tip': + raise ValueError('Invalid pivot argument: ' + str(pivot)) + + XYZ = np.column_stack(input_args[:3]) + UVW = np.column_stack(input_args[3:argi]).astype(float) + + # Normalize rows of UVW + # Note: with numpy 1.9+, could use np.linalg.norm(UVW, axis=1) + norm = np.sqrt(np.sum(UVW**2, axis=1)) + + # If any row of UVW is all zeros, don't make a quiver for it + mask = norm > 0 + XYZ = XYZ[mask] + if normalize: + UVW = UVW[mask] / norm[mask].reshape((-1, 1)) + else: + UVW = UVW[mask] + + if len(XYZ) > 0: + # compute the shaft lines all at once with an outer product + shafts = (XYZ - np.multiply.outer(shaft_dt, UVW)).swapaxes(0, 1) + # compute head direction vectors, n heads by 2 sides by 3 dimensions + head_dirs = np.array([calc_arrow(d) for d in UVW]) + # compute all head lines at once, starting from where the shaft ends + heads = shafts[:, :1] - np.multiply.outer(arrow_dt, head_dirs) + # stack left and right head lines together + heads.shape = (len(arrow_dt), -1, 3) + # transpose to get a list of lines + heads = heads.swapaxes(0, 1) + + lines = list(shafts) + list(heads) + else: + lines = [] + + linec = art3d.Line3DCollection(lines, *args[argi:], **kwargs) + self.add_collection(linec) + + self.auto_scale_xyz(XYZ[:, 0], XYZ[:, 1], XYZ[:, 2], had_data) + + return linec + + quiver3D = quiver + + def voxels(self, *args, **kwargs): + """ + ax.voxels([x, y, z,] /, filled, **kwargs) + + Plot a set of filled voxels + + All voxels are plotted as 1x1x1 cubes on the axis, with filled[0,0,0] + placed with its lower corner at the origin. Occluded faces are not + plotted. + + Call signatures:: + + voxels(filled, facecolors=fc, edgecolors=ec, **kwargs) + voxels(x, y, z, filled, facecolors=fc, edgecolors=ec, **kwargs) + + .. versionadded:: 2.1 + + Parameters + ---------- + filled : 3D np.array of bool + A 3d array of values, with truthy values indicating which voxels + to fill + + x, y, z : 3D np.array, optional + The coordinates of the corners of the voxels. This should broadcast + to a shape one larger in every dimension than the shape of `filled`. + These can be used to plot non-cubic voxels. + + If not specified, defaults to increasing integers along each axis, + like those returned by :func:`~numpy.indices`. + As indicated by the ``/`` in the function signature, these arguments + can only be passed positionally. + + facecolors, edgecolors : array_like, optional + The color to draw the faces and edges of the voxels. Can only be + passed as keyword arguments. + This parameter can be: + + - A single color value, to color all voxels the same color. This + can be either a string, or a 1D rgb/rgba array + - ``None``, the default, to use a single color for the faces, and + the style default for the edges. + - A 3D ndarray of color names, with each item the color for the + corresponding voxel. The size must match the voxels. + - A 4D ndarray of rgb/rgba data, with the components along the + last axis. + + **kwargs + Additional keyword arguments to pass onto + :func:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection` + + Returns + ------- + faces : dict + A dictionary indexed by coordinate, where ``faces[i,j,k]`` is a + `Poly3DCollection` of the faces drawn for the voxel + ``filled[i,j,k]``. If no faces were drawn for a given voxel, either + because it was not asked to be drawn, or it is fully occluded, then + ``(i,j,k) not in faces``. + + Examples + -------- + .. plot:: gallery/mplot3d/voxels.py + .. plot:: gallery/mplot3d/voxels_rgb.py + .. plot:: gallery/mplot3d/voxels_torus.py + .. plot:: gallery/mplot3d/voxels_numpy_logo.py + """ + + # work out which signature we should be using, and use it to parse + # the arguments. Name must be voxels for the correct error message + if len(args) >= 3: + # underscores indicate position only + def voxels(__x, __y, __z, filled, **kwargs): + return (__x, __y, __z), filled, kwargs + else: + def voxels(filled, **kwargs): + return None, filled, kwargs + + xyz, filled, kwargs = voxels(*args, **kwargs) + + # check dimensions + if filled.ndim != 3: + raise ValueError("Argument filled must be 3-dimensional") + size = np.array(filled.shape, dtype=np.intp) + + # check xyz coordinates, which are one larger than the filled shape + coord_shape = tuple(size + 1) + if xyz is None: + x, y, z = np.indices(coord_shape) + else: + x, y, z = (_backports.broadcast_to(c, coord_shape) for c in xyz) + + def _broadcast_color_arg(color, name): + if np.ndim(color) in (0, 1): + # single color, like "red" or [1, 0, 0] + return _backports.broadcast_to( + color, filled.shape + np.shape(color)) + elif np.ndim(color) in (3, 4): + # 3D array of strings, or 4D array with last axis rgb + if np.shape(color)[:3] != filled.shape: + raise ValueError( + "When multidimensional, {} must match the shape of " + "filled".format(name)) + return color + else: + raise ValueError("Invalid {} argument".format(name)) + + # intercept the facecolors, handling defaults and broacasting + facecolors = kwargs.pop('facecolors', None) + if facecolors is None: + facecolors = self._get_patches_for_fill.get_next_color() + facecolors = _broadcast_color_arg(facecolors, 'facecolors') + + # broadcast but no default on edgecolors + edgecolors = kwargs.pop('edgecolors', None) + edgecolors = _broadcast_color_arg(edgecolors, 'edgecolors') + + # always scale to the full array, even if the data is only in the center + self.auto_scale_xyz(x, y, z) + + # points lying on corners of a square + square = np.array([ + [0, 0, 0], + [0, 1, 0], + [1, 1, 0], + [1, 0, 0] + ], dtype=np.intp) + + voxel_faces = defaultdict(list) + + def permutation_matrices(n): + """ Generator of cyclic permutation matices """ + mat = np.eye(n, dtype=np.intp) + for i in range(n): + yield mat + mat = np.roll(mat, 1, axis=0) + + # iterate over each of the YZ, ZX, and XY orientations, finding faces to + # render + for permute in permutation_matrices(3): + # find the set of ranges to iterate over + pc, qc, rc = permute.T.dot(size) + pinds = np.arange(pc) + qinds = np.arange(qc) + rinds = np.arange(rc) + + square_rot = square.dot(permute.T) + + # iterate within the current plane + for p in pinds: + for q in qinds: + # iterate perpendicularly to the current plane, handling + # boundaries. We only draw faces between a voxel and an + # empty space, to avoid drawing internal faces. + + # draw lower faces + p0 = permute.dot([p, q, 0]) + i0 = tuple(p0) + if filled[i0]: + voxel_faces[i0].append(p0 + square_rot) + + # draw middle faces + for r1, r2 in zip(rinds[:-1], rinds[1:]): + p1 = permute.dot([p, q, r1]) + p2 = permute.dot([p, q, r2]) + + i1 = tuple(p1) + i2 = tuple(p2) + + if filled[i1] and not filled[i2]: + voxel_faces[i1].append(p2 + square_rot) + elif not filled[i1] and filled[i2]: + voxel_faces[i2].append(p2 + square_rot) + + # draw upper faces + pk = permute.dot([p, q, rc-1]) + pk2 = permute.dot([p, q, rc]) + ik = tuple(pk) + if filled[ik]: + voxel_faces[ik].append(pk2 + square_rot) + + # iterate over the faces, and generate a Poly3DCollection for each voxel + polygons = {} + for coord, faces_inds in voxel_faces.items(): + # convert indices into 3D positions + if xyz is None: + faces = faces_inds + else: + faces = [] + for face_inds in faces_inds: + ind = face_inds[:, 0], face_inds[:, 1], face_inds[:, 2] + face = np.empty(face_inds.shape) + face[:, 0] = x[ind] + face[:, 1] = y[ind] + face[:, 2] = z[ind] + faces.append(face) + + poly = art3d.Poly3DCollection(faces, + facecolors=facecolors[coord], + edgecolors=edgecolors[coord], + **kwargs + ) + self.add_collection3d(poly) + polygons[coord] = poly + + return polygons + + +def get_test_data(delta=0.05): + ''' + Return a tuple X, Y, Z with a test data set. + ''' + x = y = np.arange(-3.0, 3.0, delta) + X, Y = np.meshgrid(x, y) + + Z1 = np.exp(-(X**2 + Y**2) / 2) / (2 * np.pi) + Z2 = (np.exp(-(((X - 1) / 1.5)**2 + ((Y - 1) / 0.5)**2) / 2) / + (2 * np.pi * 0.5 * 1.5)) + Z = Z2 - Z1 + + X = X * 10 + Y = Y * 10 + Z = Z * 500 + return X, Y, Z + + +######################################################## +# Register Axes3D as a 'projection' object available +# for use just like any other axes +######################################################## +import matplotlib.projections as proj +proj.projection_registry.register(Axes3D) diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/axis3d.py b/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/axis3d.py new file mode 100644 index 0000000000..50b81df912 --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/axis3d.py @@ -0,0 +1,484 @@ +# axis3d.py, original mplot3d version by John Porter +# Created: 23 Sep 2005 +# Parts rewritten by Reinier Heeres <reinier@heeres.eu> + +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six + +import math +import copy + +from matplotlib import lines as mlines, axis as maxis, patches as mpatches +from matplotlib import rcParams +from . import art3d +from . import proj3d + +import numpy as np + +def get_flip_min_max(coord, index, mins, maxs): + if coord[index] == mins[index]: + return maxs[index] + else: + return mins[index] + +def move_from_center(coord, centers, deltas, axmask=(True, True, True)): + '''Return a coordinate that is moved by "deltas" away from the center.''' + coord = copy.copy(coord) + for i in range(3): + if not axmask[i]: + continue + if coord[i] < centers[i]: + coord[i] -= deltas[i] + else: + coord[i] += deltas[i] + return coord + +def tick_update_position(tick, tickxs, tickys, labelpos): + '''Update tick line and label position and style.''' + + for (label, on) in [(tick.label1, tick.label1On), + (tick.label2, tick.label2On)]: + if on: + label.set_position(labelpos) + + tick.tick1On, tick.tick2On = True, False + tick.tick1line.set_linestyle('-') + tick.tick1line.set_marker('') + tick.tick1line.set_data(tickxs, tickys) + tick.gridline.set_data(0, 0) + +class Axis(maxis.XAxis): + + # These points from the unit cube make up the x, y and z-planes + _PLANES = ( + (0, 3, 7, 4), (1, 2, 6, 5), # yz planes + (0, 1, 5, 4), (3, 2, 6, 7), # xz planes + (0, 1, 2, 3), (4, 5, 6, 7), # xy planes + ) + + # Some properties for the axes + _AXINFO = { + 'x': {'i': 0, 'tickdir': 1, 'juggled': (1, 0, 2), + 'color': (0.95, 0.95, 0.95, 0.5)}, + 'y': {'i': 1, 'tickdir': 0, 'juggled': (0, 1, 2), + 'color': (0.90, 0.90, 0.90, 0.5)}, + 'z': {'i': 2, 'tickdir': 0, 'juggled': (0, 2, 1), + 'color': (0.925, 0.925, 0.925, 0.5)}, + } + + def __init__(self, adir, v_intervalx, d_intervalx, axes, *args, **kwargs): + # adir identifies which axes this is + self.adir = adir + # data and viewing intervals for this direction + self.d_interval = d_intervalx + self.v_interval = v_intervalx + + # This is a temporary member variable. + # Do not depend on this existing in future releases! + self._axinfo = self._AXINFO[adir].copy() + if rcParams['_internal.classic_mode']: + self._axinfo.update( + {'label': {'va': 'center', + 'ha': 'center'}, + 'tick': {'inward_factor': 0.2, + 'outward_factor': 0.1, + 'linewidth': rcParams['lines.linewidth'], + 'color': 'k'}, + 'axisline': {'linewidth': 0.75, + 'color': (0, 0, 0, 1)}, + 'grid': {'color': (0.9, 0.9, 0.9, 1), + 'linewidth': 1.0, + 'linestyle': '-'}, + }) + else: + self._axinfo.update( + {'label': {'va': 'center', + 'ha': 'center'}, + 'tick': {'inward_factor': 0.2, + 'outward_factor': 0.1, + 'linewidth': rcParams.get( + adir + 'tick.major.width', + rcParams['xtick.major.width']), + 'color': rcParams.get( + adir + 'tick.color', + rcParams['xtick.color'])}, + 'axisline': {'linewidth': rcParams['axes.linewidth'], + 'color': rcParams['axes.edgecolor']}, + 'grid': {'color': rcParams['grid.color'], + 'linewidth': rcParams['grid.linewidth'], + 'linestyle': rcParams['grid.linestyle']}, + }) + + maxis.XAxis.__init__(self, axes, *args, **kwargs) + self.set_rotate_label(kwargs.get('rotate_label', None)) + + def init3d(self): + self.line = mlines.Line2D( + xdata=(0, 0), ydata=(0, 0), + linewidth=self._axinfo['axisline']['linewidth'], + color=self._axinfo['axisline']['color'], + antialiased=True) + + # Store dummy data in Polygon object + self.pane = mpatches.Polygon( + np.array([[0, 0], [0, 1], [1, 0], [0, 0]]), + closed=False, alpha=0.8, facecolor='k', edgecolor='k') + self.set_pane_color(self._axinfo['color']) + + self.axes._set_artist_props(self.line) + self.axes._set_artist_props(self.pane) + self.gridlines = art3d.Line3DCollection([]) + self.axes._set_artist_props(self.gridlines) + self.axes._set_artist_props(self.label) + self.axes._set_artist_props(self.offsetText) + # Need to be able to place the label at the correct location + self.label._transform = self.axes.transData + self.offsetText._transform = self.axes.transData + + def get_tick_positions(self): + majorLocs = self.major.locator() + self.major.formatter.set_locs(majorLocs) + majorLabels = [self.major.formatter(val, i) + for i, val in enumerate(majorLocs)] + return majorLabels, majorLocs + + def get_major_ticks(self, numticks=None): + ticks = maxis.XAxis.get_major_ticks(self, numticks) + for t in ticks: + t.tick1line.set_transform(self.axes.transData) + t.tick2line.set_transform(self.axes.transData) + t.gridline.set_transform(self.axes.transData) + t.label1.set_transform(self.axes.transData) + t.label2.set_transform(self.axes.transData) + return ticks + + def set_pane_pos(self, xys): + xys = np.asarray(xys) + xys = xys[:,:2] + self.pane.xy = xys + self.stale = True + + def set_pane_color(self, color): + '''Set pane color to a RGBA tuple.''' + self._axinfo['color'] = color + self.pane.set_edgecolor(color) + self.pane.set_facecolor(color) + self.pane.set_alpha(color[-1]) + self.stale = True + + def set_rotate_label(self, val): + ''' + Whether to rotate the axis label: True, False or None. + If set to None the label will be rotated if longer than 4 chars. + ''' + self._rotate_label = val + self.stale = True + + def get_rotate_label(self, text): + if self._rotate_label is not None: + return self._rotate_label + else: + return len(text) > 4 + + def _get_coord_info(self, renderer): + minx, maxx, miny, maxy, minz, maxz = self.axes.get_w_lims() + if minx > maxx: + minx, maxx = maxx, minx + if miny > maxy: + miny, maxy = maxy, miny + if minz > maxz: + minz, maxz = maxz, minz + mins = np.array((minx, miny, minz)) + maxs = np.array((maxx, maxy, maxz)) + centers = (maxs + mins) / 2. + deltas = (maxs - mins) / 12. + mins = mins - deltas / 4. + maxs = maxs + deltas / 4. + + vals = mins[0], maxs[0], mins[1], maxs[1], mins[2], maxs[2] + tc = self.axes.tunit_cube(vals, renderer.M) + avgz = [tc[p1][2] + tc[p2][2] + tc[p3][2] + tc[p4][2] + for p1, p2, p3, p4 in self._PLANES] + highs = np.array([avgz[2*i] < avgz[2*i+1] for i in range(3)]) + + return mins, maxs, centers, deltas, tc, highs + + def draw_pane(self, renderer): + renderer.open_group('pane3d') + + mins, maxs, centers, deltas, tc, highs = self._get_coord_info(renderer) + + info = self._axinfo + index = info['i'] + if not highs[index]: + plane = self._PLANES[2 * index] + else: + plane = self._PLANES[2 * index + 1] + xys = [tc[p] for p in plane] + self.set_pane_pos(xys) + self.pane.draw(renderer) + + renderer.close_group('pane3d') + + def draw(self, renderer): + self.label._transform = self.axes.transData + renderer.open_group('axis3d') + + # code from XAxis + majorTicks = self.get_major_ticks() + majorLocs = self.major.locator() + + info = self._axinfo + index = info['i'] + + # filter locations here so that no extra grid lines are drawn + locmin, locmax = self.get_view_interval() + if locmin > locmax: + locmin, locmax = locmax, locmin + + # Rudimentary clipping + majorLocs = [loc for loc in majorLocs if + locmin <= loc <= locmax] + self.major.formatter.set_locs(majorLocs) + majorLabels = [self.major.formatter(val, i) + for i, val in enumerate(majorLocs)] + + mins, maxs, centers, deltas, tc, highs = self._get_coord_info(renderer) + + # Determine grid lines + minmax = np.where(highs, maxs, mins) + + # Draw main axis line + juggled = info['juggled'] + edgep1 = minmax.copy() + edgep1[juggled[0]] = get_flip_min_max(edgep1, juggled[0], mins, maxs) + + edgep2 = edgep1.copy() + edgep2[juggled[1]] = get_flip_min_max(edgep2, juggled[1], mins, maxs) + pep = proj3d.proj_trans_points([edgep1, edgep2], renderer.M) + centpt = proj3d.proj_transform( + centers[0], centers[1], centers[2], renderer.M) + self.line.set_data((pep[0][0], pep[0][1]), (pep[1][0], pep[1][1])) + self.line.draw(renderer) + + # Grid points where the planes meet + xyz0 = [] + for val in majorLocs: + coord = minmax.copy() + coord[index] = val + xyz0.append(coord) + + # Draw labels + peparray = np.asanyarray(pep) + # The transAxes transform is used because the Text object + # rotates the text relative to the display coordinate system. + # Therefore, if we want the labels to remain parallel to the + # axis regardless of the aspect ratio, we need to convert the + # edge points of the plane to display coordinates and calculate + # an angle from that. + # TODO: Maybe Text objects should handle this themselves? + dx, dy = (self.axes.transAxes.transform([peparray[0:2, 1]]) - + self.axes.transAxes.transform([peparray[0:2, 0]]))[0] + + lxyz = 0.5*(edgep1 + edgep2) + + # A rough estimate; points are ambiguous since 3D plots rotate + ax_scale = self.axes.bbox.size / self.figure.bbox.size + ax_inches = np.multiply(ax_scale, self.figure.get_size_inches()) + ax_points_estimate = sum(72. * ax_inches) + deltas_per_point = 48. / ax_points_estimate + default_offset = 21. + labeldeltas = ( + (self.labelpad + default_offset) * deltas_per_point * deltas) + axmask = [True, True, True] + axmask[index] = False + lxyz = move_from_center(lxyz, centers, labeldeltas, axmask) + tlx, tly, tlz = proj3d.proj_transform(lxyz[0], lxyz[1], lxyz[2], + renderer.M) + self.label.set_position((tlx, tly)) + if self.get_rotate_label(self.label.get_text()): + angle = art3d.norm_text_angle(math.degrees(math.atan2(dy, dx))) + self.label.set_rotation(angle) + self.label.set_va(info['label']['va']) + self.label.set_ha(info['label']['ha']) + self.label.draw(renderer) + + + # Draw Offset text + + # Which of the two edge points do we want to + # use for locating the offset text? + if juggled[2] == 2 : + outeredgep = edgep1 + outerindex = 0 + else : + outeredgep = edgep2 + outerindex = 1 + + pos = copy.copy(outeredgep) + pos = move_from_center(pos, centers, labeldeltas, axmask) + olx, oly, olz = proj3d.proj_transform( + pos[0], pos[1], pos[2], renderer.M) + self.offsetText.set_text( self.major.formatter.get_offset() ) + self.offsetText.set_position( (olx, oly) ) + angle = art3d.norm_text_angle(math.degrees(math.atan2(dy, dx))) + self.offsetText.set_rotation(angle) + # Must set rotation mode to "anchor" so that + # the alignment point is used as the "fulcrum" for rotation. + self.offsetText.set_rotation_mode('anchor') + + #---------------------------------------------------------------------- + # Note: the following statement for determining the proper alignment of + # the offset text. This was determined entirely by trial-and-error + # and should not be in any way considered as "the way". There are + # still some edge cases where alignment is not quite right, but this + # seems to be more of a geometry issue (in other words, I might be + # using the wrong reference points). + # + # (TT, FF, TF, FT) are the shorthand for the tuple of + # (centpt[info['tickdir']] <= peparray[info['tickdir'], outerindex], + # centpt[index] <= peparray[index, outerindex]) + # + # Three-letters (e.g., TFT, FTT) are short-hand for the array of bools + # from the variable 'highs'. + # --------------------------------------------------------------------- + if centpt[info['tickdir']] > peparray[info['tickdir'], outerindex] : + # if FT and if highs has an even number of Trues + if (centpt[index] <= peparray[index, outerindex] + and ((len(highs.nonzero()[0]) % 2) == 0)) : + # Usually, this means align right, except for the FTT case, + # in which offset for axis 1 and 2 are aligned left. + if highs.tolist() == [False, True, True] and index in (1, 2) : + align = 'left' + else : + align = 'right' + else : + # The FF case + align = 'left' + else : + # if TF and if highs has an even number of Trues + if (centpt[index] > peparray[index, outerindex] + and ((len(highs.nonzero()[0]) % 2) == 0)) : + # Usually mean align left, except if it is axis 2 + if index == 2 : + align = 'right' + else : + align = 'left' + else : + # The TT case + align = 'right' + + self.offsetText.set_va('center') + self.offsetText.set_ha(align) + self.offsetText.draw(renderer) + + # Draw grid lines + if len(xyz0) > 0: + # Grid points at end of one plane + xyz1 = copy.deepcopy(xyz0) + newindex = (index + 1) % 3 + newval = get_flip_min_max(xyz1[0], newindex, mins, maxs) + for i in range(len(majorLocs)): + xyz1[i][newindex] = newval + + # Grid points at end of the other plane + xyz2 = copy.deepcopy(xyz0) + newindex = (index + 2) % 3 + newval = get_flip_min_max(xyz2[0], newindex, mins, maxs) + for i in range(len(majorLocs)): + xyz2[i][newindex] = newval + + lines = list(zip(xyz1, xyz0, xyz2)) + if self.axes._draw_grid: + self.gridlines.set_segments(lines) + self.gridlines.set_color([info['grid']['color']] * len(lines)) + self.gridlines.set_linewidth( + [info['grid']['linewidth']] * len(lines)) + self.gridlines.set_linestyle( + [info['grid']['linestyle']] * len(lines)) + self.gridlines.draw(renderer, project=True) + + # Draw ticks + tickdir = info['tickdir'] + tickdelta = deltas[tickdir] + if highs[tickdir]: + ticksign = 1 + else: + ticksign = -1 + + for tick, loc, label in zip(majorTicks, majorLocs, majorLabels): + if tick is None: + continue + + # Get tick line positions + pos = copy.copy(edgep1) + pos[index] = loc + pos[tickdir] = ( + edgep1[tickdir] + + info['tick']['outward_factor'] * ticksign * tickdelta) + x1, y1, z1 = proj3d.proj_transform(pos[0], pos[1], pos[2], + renderer.M) + pos[tickdir] = ( + edgep1[tickdir] + - info['tick']['inward_factor'] * ticksign * tickdelta) + x2, y2, z2 = proj3d.proj_transform(pos[0], pos[1], pos[2], + renderer.M) + + # Get position of label + default_offset = 8. # A rough estimate + labeldeltas = ( + (tick.get_pad() + default_offset) * deltas_per_point * deltas) + + axmask = [True, True, True] + axmask[index] = False + pos[tickdir] = edgep1[tickdir] + pos = move_from_center(pos, centers, labeldeltas, axmask) + lx, ly, lz = proj3d.proj_transform(pos[0], pos[1], pos[2], + renderer.M) + + tick_update_position(tick, (x1, x2), (y1, y2), (lx, ly)) + tick.tick1line.set_linewidth(info['tick']['linewidth']) + tick.tick1line.set_color(info['tick']['color']) + tick.set_label1(label) + tick.set_label2(label) + tick.draw(renderer) + + renderer.close_group('axis3d') + self.stale = False + + def get_view_interval(self): + """return the Interval instance for this 3d axis view limits""" + return self.v_interval + + def set_view_interval(self, vmin, vmax, ignore=False): + if ignore: + self.v_interval = vmin, vmax + else: + Vmin, Vmax = self.get_view_interval() + self.v_interval = min(vmin, Vmin), max(vmax, Vmax) + + # TODO: Get this to work properly when mplot3d supports + # the transforms framework. + def get_tightbbox(self, renderer) : + # Currently returns None so that Axis.get_tightbbox + # doesn't return junk info. + return None + +# Use classes to look at different data limits + +class XAxis(Axis): + def get_data_interval(self): + 'return the Interval instance for this axis data limits' + return self.axes.xy_dataLim.intervalx + +class YAxis(Axis): + def get_data_interval(self): + 'return the Interval instance for this axis data limits' + return self.axes.xy_dataLim.intervaly + +class ZAxis(Axis): + def get_data_interval(self): + 'return the Interval instance for this axis data limits' + return self.axes.zz_dataLim.intervalx diff --git a/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/proj3d.py b/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/proj3d.py new file mode 100644 index 0000000000..a084e7f36a --- /dev/null +++ b/contrib/python/matplotlib/py2/mpl_toolkits/mplot3d/proj3d.py @@ -0,0 +1,203 @@ +# 3dproj.py +# +""" +Various transforms used for by the 3D code +""" +from __future__ import (absolute_import, division, print_function, + unicode_literals) + +import six +from six.moves import zip + +import numpy as np +import numpy.linalg as linalg + + + +def line2d(p0, p1): + """ + Return 2D equation of line in the form ax+by+c = 0 + """ + # x + x1 = 0 + x0, y0 = p0[:2] + x1, y1 = p1[:2] + # + if x0 == x1: + a = -1 + b = 0 + c = x1 + elif y0 == y1: + a = 0 + b = 1 + c = -y1 + else: + a = (y0-y1) + b = (x0-x1) + c = (x0*y1 - x1*y0) + return a, b, c + +def line2d_dist(l, p): + """ + Distance from line to point + line is a tuple of coefficients a,b,c + """ + a, b, c = l + x0, y0 = p + return abs((a*x0 + b*y0 + c)/np.sqrt(a**2+b**2)) + + +def line2d_seg_dist(p1, p2, p0): + """distance(s) from line defined by p1 - p2 to point(s) p0 + + p0[0] = x(s) + p0[1] = y(s) + + intersection point p = p1 + u*(p2-p1) + and intersection point lies within segment if u is between 0 and 1 + """ + + x21 = p2[0] - p1[0] + y21 = p2[1] - p1[1] + x01 = np.asarray(p0[0]) - p1[0] + y01 = np.asarray(p0[1]) - p1[1] + + u = (x01*x21 + y01*y21) / (x21**2 + y21**2) + u = np.clip(u, 0, 1) + d = np.sqrt((x01 - u*x21)**2 + (y01 - u*y21)**2) + + return d + + +def mod(v): + """3d vector length""" + return np.sqrt(v[0]**2+v[1]**2+v[2]**2) + +def world_transformation(xmin, xmax, + ymin, ymax, + zmin, zmax): + dx, dy, dz = (xmax-xmin), (ymax-ymin), (zmax-zmin) + return np.array([ + [1.0/dx,0,0,-xmin/dx], + [0,1.0/dy,0,-ymin/dy], + [0,0,1.0/dz,-zmin/dz], + [0,0,0,1.0]]) + + +def view_transformation(E, R, V): + n = (E - R) + ## new +# n /= mod(n) +# u = np.cross(V,n) +# u /= mod(u) +# v = np.cross(n,u) +# Mr = np.diag([1.]*4) +# Mt = np.diag([1.]*4) +# Mr[:3,:3] = u,v,n +# Mt[:3,-1] = -E + ## end new + + ## old + n = n / mod(n) + u = np.cross(V, n) + u = u / mod(u) + v = np.cross(n, u) + Mr = [[u[0],u[1],u[2],0], + [v[0],v[1],v[2],0], + [n[0],n[1],n[2],0], + [0, 0, 0, 1], + ] + # + Mt = [[1, 0, 0, -E[0]], + [0, 1, 0, -E[1]], + [0, 0, 1, -E[2]], + [0, 0, 0, 1]] + ## end old + + return np.dot(Mr, Mt) + +def persp_transformation(zfront, zback): + a = (zfront+zback)/(zfront-zback) + b = -2*(zfront*zback)/(zfront-zback) + return np.array([[1,0,0,0], + [0,1,0,0], + [0,0,a,b], + [0,0,-1,0] + ]) + +def ortho_transformation(zfront, zback): + # note: w component in the resulting vector will be (zback-zfront), not 1 + a = -(zfront + zback) + b = -(zfront - zback) + return np.array([[2,0,0,0], + [0,2,0,0], + [0,0,-2,0], + [0,0,a,b] + ]) + +def proj_transform_vec(vec, M): + vecw = np.dot(M, vec) + w = vecw[3] + # clip here.. + txs, tys, tzs = vecw[0]/w, vecw[1]/w, vecw[2]/w + return txs, tys, tzs + +def proj_transform_vec_clip(vec, M): + vecw = np.dot(M, vec) + w = vecw[3] + # clip here. + txs, tys, tzs = vecw[0] / w, vecw[1] / w, vecw[2] / w + tis = (0 <= vecw[0]) & (vecw[0] <= 1) & (0 <= vecw[1]) & (vecw[1] <= 1) + if np.any(tis): + tis = vecw[1] < 1 + return txs, tys, tzs, tis + +def inv_transform(xs, ys, zs, M): + iM = linalg.inv(M) + vec = vec_pad_ones(xs, ys, zs) + vecr = np.dot(iM, vec) + try: + vecr = vecr/vecr[3] + except OverflowError: + pass + return vecr[0], vecr[1], vecr[2] + +def vec_pad_ones(xs, ys, zs): + return np.array([xs, ys, zs, np.ones_like(xs)]) + +def proj_transform(xs, ys, zs, M): + """ + Transform the points by the projection matrix + """ + vec = vec_pad_ones(xs, ys, zs) + return proj_transform_vec(vec, M) + +def proj_transform_clip(xs, ys, zs, M): + """ + Transform the points by the projection matrix + and return the clipping result + returns txs,tys,tzs,tis + """ + vec = vec_pad_ones(xs, ys, zs) + return proj_transform_vec_clip(vec, M) +transform = proj_transform + +def proj_points(points, M): + return np.column_stack(proj_trans_points(points, M)) + +def proj_trans_points(points, M): + xs, ys, zs = zip(*points) + return proj_transform(xs, ys, zs, M) + +def proj_trans_clip_points(points, M): + xs, ys, zs = zip(*points) + return proj_transform_clip(xs, ys, zs, M) + + +def rot_x(V, alpha): + cosa, sina = np.cos(alpha), np.sin(alpha) + M1 = np.array([[1,0,0,0], + [0,cosa,-sina,0], + [0,sina,cosa,0], + [0,0,0,1]]) + + return np.dot(M1, V) |