diff --git a/lib/matplotlib/path.py b/lib/matplotlib/path.py index 4c9a3f69c324..3feff8e71b68 100644 --- a/lib/matplotlib/path.py +++ b/lib/matplotlib/path.py @@ -127,7 +127,8 @@ def __init__(self, vertices, codes=None, _interpolation_steps=1, vertices = _to_unmasked_float_array(vertices) if vertices.ndim != 2 or vertices.shape[1] != 2: raise ValueError( - "'vertices' must be a 2D list or array with shape Nx2") + "'vertices' must be an array-like of shape (N, 2) not {}" + .format(vertices.shape)) if codes is not None: codes = np.asarray(codes, self.code_type) diff --git a/lib/mpl_toolkits/mplot3d/art3d.py b/lib/mpl_toolkits/mplot3d/art3d.py index 20285ee516f4..6d3868c8697b 100644 --- a/lib/mpl_toolkits/mplot3d/art3d.py +++ b/lib/mpl_toolkits/mplot3d/art3d.py @@ -71,6 +71,24 @@ def get_dir_vector(zdir): raise ValueError("'x', 'y', 'z', None or vector of length 3 expected") +def _array_split(arr, indices_or_sections, remove_empty=False): + """Fix numpy.split to preserve the dimension of empty subarrays.""" + arr_chunks = np.split(arr, indices_or_sections) + + if arr_chunks[-1].size == 0: + if not remove_empty: + # Preserve the 2D dimensionality of the last chunk that can be + # empty (numpy <=1.10 replaces empty chunks by a 1D empty array) + + # TODO: The following can be removed when + # support for numpy <=1.10 is dropped. + arr_chunks[-1] = np.empty(shape=(0, arr.shape[1]), dtype=arr.dtype) + else: + del arr_chunks[-1] + + return arr_chunks + + class Text3D(mtext.Text): """ Text object with 3D position and direction. @@ -141,12 +159,7 @@ 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 = np.full_like(xs, fill_value=float(zs)) - except TypeError: - pass + zs = np.broadcast_to(zs, len(xs)) self._verts3d = juggle_axes(xs, ys, zs, zdir) self.stale = True @@ -167,52 +180,73 @@ def line_2d_to_3d(line, zs=0, zdir='z'): def path_to_3d_segment(path, zs=0, zdir='z'): - """Convert a path to a 3D segment.""" + """Convert a path to a 3D segment. + .. versionchanged :: 3.1 + Return type changed from a list to a numpy.array + """ zs = np.broadcast_to(zs, len(path)) + pathsegs = path.iter_segments(simplify=False, curves=False) - seg = [(x, y, z) for (((x, y), code), z) in zip(pathsegs, zs)] - seg3d = [juggle_axes(x, y, z, zdir) for (x, y, z) in seg] - return seg3d + if len(path): + xs, ys = zip(*((x, y) for (x, y), code in pathsegs)) + else: + xs, ys = [], [] + + seg3d = juggle_axes(xs, ys, zs, zdir) + return np.array(seg3d).T def paths_to_3d_segments(paths, zs=0, zdir='z'): - """Convert paths from a collection object to 3D segments.""" + """Convert paths from a collection object to 3D segments. + + .. versionchanged :: 3.1 + Return type changed from a list to a numpy.array + """ zs = np.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.""" + """Convert a path to a 3D segment with path codes. + .. versionchanged :: 3.1 + Return type changed from a list to a numpy.array + """ zs = np.broadcast_to(zs, len(path)) + pathsegs = path.iter_segments(simplify=False, curves=False) - seg_codes = [((x, y, z), code) for ((x, y), code), z in zip(pathsegs, zs)] - if seg_codes: - seg, codes = zip(*seg_codes) - seg3d = [juggle_axes(x, y, z, zdir) for (x, y, z) in seg] + if len(path): + xs, ys, codes = zip(*((x, y, code) for (x, y), code in pathsegs)) else: - seg3d = [] - codes = [] - return seg3d, list(codes) + xs, ys, codes = [], [], [] + + seg3d = juggle_axes(xs, ys, zs, zdir) + return np.array(seg3d).T, 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. + + .. versionchanged :: 3.1 + Return type changed from a tuple of lists to a tuple of numpy.array """ zs = np.broadcast_to(zs, len(paths)) - segments_codes = [path_to_3d_segment_with_codes(path, pathz, zdir) - for path, pathz in zip(paths, zs)] - if segments_codes: - segments, codes = zip(*segments_codes) + + path_generator = (path_to_3d_segment_with_codes(path, pathz, zdir) + for path, pathz in zip(paths, zs)) + if len(paths) and len(zs): + segments, codes_list = zip(*path_generator) + + return np.asarray(segments), np.asarray(codes_list) else: - segments, codes = [], [] - return list(segments), list(codes) + return np.empty((0, 3), dtype=np.float64), np.array([], dtype=np.uint8) class Line3DCollection(LineCollection): @@ -229,23 +263,34 @@ def set_segments(self, segments): """ Set 3D segments. """ - self._segments3d = np.asanyarray(segments) + self._segments3d = [] + if len(segments) > 0: + self._seg_sizes = np.array([len(c) for c in segments]) + # Store the points in a single array for easier projection + n_segments = np.sum(self._seg_sizes) + + self._segments3d_data = np.empty((n_segments, 4)) + self._segments3d_data[:, :3] = np.vstack(segments) + self._segments3d_data[:, 3] = 1 + + # For convenience, store a view of the array in the original shape + self._segments3d = _array_split(self._segments3d_data[:, :3], + np.cumsum(self._seg_sizes)) + 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] + if len(self._segments3d) == 0: + # FIXME + return 1e9 + xys = proj3d.proj_transform_vec(self._segments3d_data.T, renderer.M) + segments_2d = _array_split(xys[:2].T, + np.cumsum(self._seg_sizes)) LineCollection.set_segments(self, segments_2d) - - # FIXME - minz = 1e9 - for xs, ys, zs in xyslist: - minz = min(minz, min(zs)) + minz = np.min(xys[2]) return minz @artist.allow_rasterization @@ -273,8 +318,8 @@ def __init__(self, *args, zs=(), zdir='z', **kwargs): def set_3d_properties(self, verts, zs=0, zdir='z'): zs = np.broadcast_to(zs, len(verts)) - self._segment3d = [juggle_axes(x, y, z, zdir) - for ((x, y), z) in zip(verts, zs)] + xs, ys = verts.T + self._segment3d = juggle_axes(xs, ys, zs, zdir) self._facecolor3d = Patch.get_facecolor(self) def get_path(self): @@ -284,13 +329,17 @@ 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])) + # pad ones + if self._segment3d: + segments_3d_len = len(self._segment3d[0]) + else: + segments_3d_len = 0 + s = np.vstack(self._segment3d + (np.ones(segments_3d_len),)) + vxyzis = proj3d.proj_transform_vec_clip(s, renderer.M) + self._path2d = mpath.Path(vxyzis[0:2].T) # FIXME: coloring self._facecolor2d = self._facecolor3d - return min(vzs) + return min(vxyzis[2]) class PathPatch3D(Patch3D): @@ -307,13 +356,17 @@ def set_3d_properties(self, path, zs=0, zdir='z'): 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) + # pad ones + if self._segment3d: + segments_3d_len = len(self._segment3d[0]) + else: + segments_3d_len = 0 + s = np.vstack(self._segment3d + (np.ones(segments_3d_len),)) + vxyzis = proj3d.proj_transform_vec_clip(s, renderer.M) + self._path2d = mpath.Path(vxyzis[0:2].T, self._code3d) # FIXME: coloring self._facecolor2d = self._facecolor3d - return min(vzs) + return min(vxyzis[2]) def get_patch_verts(patch): @@ -390,8 +443,10 @@ def set_3d_properties(self, zs, zdir): 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) + # pad ones + s = np.vstack(self._offsets3d, np.ones(self._offsets3d.shape[1])) + vxyzis = proj3d.proj_transform_vec_clip(s, renderer.M) + vzs = vxyzis[2] fcs = (zalpha(self._facecolor3d, vzs) if self._depthshade else self._facecolor3d) @@ -402,7 +457,7 @@ def do_3d_projection(self, renderer): self._edgecolor3d) ecs = mcolors.to_rgba_array(ecs, self._alpha) self.set_edgecolors(ecs) - PatchCollection.set_offsets(self, np.column_stack([vxs, vys])) + PatchCollection.set_offsets(self, vxyzis[0:2].T) if vzs.size > 0: return min(vzs) @@ -457,7 +512,8 @@ def set_3d_properties(self, zs, zdir): def do_3d_projection(self, renderer): xs, ys, zs = self._offsets3d - vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M) + vxyzis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M) + vzs = vxyzis[2] fcs = (zalpha(self._facecolor3d, vzs) if self._depthshade else self._facecolor3d) @@ -468,7 +524,7 @@ def do_3d_projection(self, renderer): self._edgecolor3d) ecs = mcolors.to_rgba_array(ecs, self._alpha) self.set_edgecolors(ecs) - PathCollection.set_offsets(self, np.column_stack([vxs, vys])) + PathCollection.set_offsets(self, vxyzis[0:2].T) return np.min(vzs) if vzs.size else np.nan @@ -548,26 +604,19 @@ def set_zsort(self, zsort): 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 = [], [], [] + """Optimize points for projection""" - ones = np.ones(len(xs)) - self._vec = np.array([xs, ys, zs, ones]) - self._segis = segis + self._seg_sizes = np.array([len(c) for c in segments3d], dtype=np.int) + self._vec = [] + + # Store the points in a single array for easier projection + n_segments = np.sum(self._seg_sizes) + self._vec = np.empty((4, n_segments)) + # Put all segments in a big array + # TODO: avoid copy in np.vstack when segments3d is a 3D numpy array + if n_segments: + self._vec[:3, :] = np.vstack(segments3d).T + self._vec[3, :] = 1 def set_verts(self, verts, closed=True): """Set 3D vertices.""" @@ -609,9 +658,11 @@ def do_3d_projection(self, renderer): 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] + xys = proj3d.proj_transform_vec(self._vec, renderer.M).T + + xyzlist = _array_split(xys[:, :3], + np.cumsum(self._seg_sizes), + remove_empty=True) # This extra fuss is to re-order face / edge colors cface = self._facecolors3d @@ -625,22 +676,19 @@ def do_3d_projection(self, renderer): cedge = cedge.repeat(len(xyzlist), axis=0) # sort by depth (furthest drawn first) - z_segments_2d = sorted( - ((self._zsortfunc(zs), np.column_stack([xs, ys]), fc, ec, idx) - for idx, ((xs, ys, zs), fc, ec) - in enumerate(zip(xyzlist, cface, cedge))), - key=lambda x: x[0], reverse=True) + z_argsort = np.argsort( + [self._zsortfunc(xyz[:, 2]) for xyz in xyzlist])[::-1] - segments_2d = [s for z, s, fc, ec, idx in z_segments_2d] + segments_2d = [xyzlist[i][:, 0:2] for i in z_argsort] if self._codes3d is not None: - codes = [self._codes3d[idx] for z, s, fc, ec, idx in z_segments_2d] + codes = self._codes3d[z_argsort] 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] + self._facecolors2d = cface[z_argsort] if len(self._edgecolors3d) == len(cface): - self._edgecolors2d = [ec for z, s, fc, ec, idx in z_segments_2d] + self._edgecolors2d = cedge[z_argsort] else: self._edgecolors2d = self._edgecolors3d @@ -649,11 +697,12 @@ def do_3d_projection(self, renderer): 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: + + elif xys[:, 2].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) + return np.min(xys[:, 2]) else: return np.nan @@ -733,21 +782,20 @@ def rotate_axes(xs, ys, zs, zdir): 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 np.broadcast_to( - mcolors.to_rgba_array(c) if len(c) else [0, 0, 0, 0], - (num, 4)) + if not len(c): + return np.zeros((num, 4)) + else: + return np.broadcast_to(mcolors.to_rgba_array(c), (num, 4)) def zalpha(colors, zs): diff --git a/lib/mpl_toolkits/mplot3d/proj3d.py b/lib/mpl_toolkits/mplot3d/proj3d.py index 64df8db655f1..53b798ead0f5 100644 --- a/lib/mpl_toolkits/mplot3d/proj3d.py +++ b/lib/mpl_toolkits/mplot3d/proj3d.py @@ -129,22 +129,25 @@ def ortho_transformation(zfront, zback): def proj_transform_vec(vec, M): + if len(vec) != 4: + raise ValueError('expected len(vec) of 4, received {}' + .format(vec)) + 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 + vecw[:3] /= vecw[3] + return vecw[:3] 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 + # Determine clipping before rescaling 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 + vecw[:3] /= vecw[3] + # Integrating tis in the numpy array for optimization purposes + vecw[3] = tis + return vecw def inv_transform(xs, ys, zs, M): @@ -188,6 +191,9 @@ def proj_points(points, M): def proj_trans_points(points, M): + """ + Apply transformation matrix M on a set of points + """ xs, ys, zs = zip(*points) return proj_transform(xs, ys, zs, M)