art3d.py 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854
  1. # art3d.py, original mplot3d version by John Porter
  2. # Parts rewritten by Reinier Heeres <reinier@heeres.eu>
  3. # Minor additions by Ben Axelrod <baxelrod@coroware.com>
  4. """
  5. Module containing 3D artist code and functions to convert 2D
  6. artists into 3D versions which can be added to an Axes3D.
  7. """
  8. import math
  9. import numpy as np
  10. from matplotlib import (
  11. artist, cbook, colors as mcolors, lines, text as mtext, path as mpath)
  12. from matplotlib.collections import (
  13. LineCollection, PolyCollection, PatchCollection, PathCollection)
  14. from matplotlib.colors import Normalize
  15. from matplotlib.patches import Patch
  16. from . import proj3d
  17. def _norm_angle(a):
  18. """Return the given angle normalized to -180 < *a* <= 180 degrees."""
  19. a = (a + 360) % 360
  20. if a > 180:
  21. a = a - 360
  22. return a
  23. @cbook.deprecated("3.1")
  24. def norm_angle(a):
  25. """Return the given angle normalized to -180 < *a* <= 180 degrees."""
  26. return _norm_angle(a)
  27. def _norm_text_angle(a):
  28. """Return the given angle normalized to -90 < *a* <= 90 degrees."""
  29. a = (a + 180) % 180
  30. if a > 90:
  31. a = a - 180
  32. return a
  33. @cbook.deprecated("3.1")
  34. def norm_text_angle(a):
  35. """Return the given angle normalized to -90 < *a* <= 90 degrees."""
  36. return _norm_text_angle(a)
  37. def get_dir_vector(zdir):
  38. """
  39. Return a direction vector.
  40. Parameters
  41. ----------
  42. zdir : {'x', 'y', 'z', None, 3-tuple}
  43. The direction. Possible values are:
  44. - 'x': equivalent to (1, 0, 0)
  45. - 'y': equivalent to (0, 1, 0)
  46. - 'z': equivalent to (0, 0, 1)
  47. - *None*: equivalent to (0, 0, 0)
  48. - an iterable (x, y, z) is returned unchanged.
  49. Returns
  50. -------
  51. x, y, z : array-like
  52. The direction vector. This is either a numpy.array or *zdir* itself if
  53. *zdir* is already a length-3 iterable.
  54. """
  55. if zdir == 'x':
  56. return np.array((1, 0, 0))
  57. elif zdir == 'y':
  58. return np.array((0, 1, 0))
  59. elif zdir == 'z':
  60. return np.array((0, 0, 1))
  61. elif zdir is None:
  62. return np.array((0, 0, 0))
  63. elif np.iterable(zdir) and len(zdir) == 3:
  64. return zdir
  65. else:
  66. raise ValueError("'x', 'y', 'z', None or vector of length 3 expected")
  67. class Text3D(mtext.Text):
  68. """
  69. Text object with 3D position and direction.
  70. Parameters
  71. ----------
  72. x, y, z
  73. The position of the text.
  74. text : str
  75. The text string to display.
  76. zdir : {'x', 'y', 'z', None, 3-tuple}
  77. The direction of the text. See `.get_dir_vector` for a description of
  78. the values.
  79. Other Parameters
  80. ----------------
  81. **kwargs
  82. All other parameters are passed on to `~matplotlib.text.Text`.
  83. """
  84. def __init__(self, x=0, y=0, z=0, text='', zdir='z', **kwargs):
  85. mtext.Text.__init__(self, x, y, text, **kwargs)
  86. self.set_3d_properties(z, zdir)
  87. def set_3d_properties(self, z=0, zdir='z'):
  88. x, y = self.get_position()
  89. self._position3d = np.array((x, y, z))
  90. self._dir_vec = get_dir_vector(zdir)
  91. self.stale = True
  92. @artist.allow_rasterization
  93. def draw(self, renderer):
  94. proj = proj3d.proj_trans_points(
  95. [self._position3d, self._position3d + self._dir_vec], renderer.M)
  96. dx = proj[0][1] - proj[0][0]
  97. dy = proj[1][1] - proj[1][0]
  98. angle = math.degrees(math.atan2(dy, dx))
  99. self.set_position((proj[0][0], proj[1][0]))
  100. self.set_rotation(_norm_text_angle(angle))
  101. mtext.Text.draw(self, renderer)
  102. self.stale = False
  103. def get_tightbbox(self, renderer):
  104. # Overwriting the 2d Text behavior which is not valid for 3d.
  105. # For now, just return None to exclude from layout calculation.
  106. return None
  107. def text_2d_to_3d(obj, z=0, zdir='z'):
  108. """Convert a Text to a Text3D object."""
  109. obj.__class__ = Text3D
  110. obj.set_3d_properties(z, zdir)
  111. class Line3D(lines.Line2D):
  112. """
  113. 3D line object.
  114. """
  115. def __init__(self, xs, ys, zs, *args, **kwargs):
  116. """
  117. Keyword arguments are passed onto :func:`~matplotlib.lines.Line2D`.
  118. """
  119. lines.Line2D.__init__(self, [], [], *args, **kwargs)
  120. self._verts3d = xs, ys, zs
  121. def set_3d_properties(self, zs=0, zdir='z'):
  122. xs = self.get_xdata()
  123. ys = self.get_ydata()
  124. try:
  125. # If *zs* is a list or array, then this will fail and
  126. # just proceed to juggle_axes().
  127. zs = np.full_like(xs, fill_value=float(zs))
  128. except TypeError:
  129. pass
  130. self._verts3d = juggle_axes(xs, ys, zs, zdir)
  131. self.stale = True
  132. def set_data_3d(self, *args):
  133. """
  134. Set the x, y and z data
  135. Parameters
  136. ----------
  137. x : array-like
  138. The x-data to be plotted.
  139. y : array-like
  140. The y-data to be plotted.
  141. z : array-like
  142. The z-data to be plotted.
  143. Notes
  144. -----
  145. Accepts x, y, z arguments or a single array-like (x, y, z)
  146. """
  147. if len(args) == 1:
  148. self._verts3d = args[0]
  149. else:
  150. self._verts3d = args
  151. self.stale = True
  152. def get_data_3d(self):
  153. """
  154. Get the current data
  155. Returns
  156. -------
  157. verts3d : length-3 tuple or array-likes
  158. The current data as a tuple or array-likes.
  159. """
  160. return self._verts3d
  161. @artist.allow_rasterization
  162. def draw(self, renderer):
  163. xs3d, ys3d, zs3d = self._verts3d
  164. xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, renderer.M)
  165. self.set_data(xs, ys)
  166. lines.Line2D.draw(self, renderer)
  167. self.stale = False
  168. def line_2d_to_3d(line, zs=0, zdir='z'):
  169. """Convert a 2D line to 3D."""
  170. line.__class__ = Line3D
  171. line.set_3d_properties(zs, zdir)
  172. def _path_to_3d_segment(path, zs=0, zdir='z'):
  173. """Convert a path to a 3D segment."""
  174. zs = np.broadcast_to(zs, len(path))
  175. pathsegs = path.iter_segments(simplify=False, curves=False)
  176. seg = [(x, y, z) for (((x, y), code), z) in zip(pathsegs, zs)]
  177. seg3d = [juggle_axes(x, y, z, zdir) for (x, y, z) in seg]
  178. return seg3d
  179. @cbook.deprecated("3.1")
  180. def path_to_3d_segment(path, zs=0, zdir='z'):
  181. """Convert a path to a 3D segment."""
  182. return _path_to_3d_segment(path, zs=zs, zdir=zdir)
  183. def _paths_to_3d_segments(paths, zs=0, zdir='z'):
  184. """Convert paths from a collection object to 3D segments."""
  185. zs = np.broadcast_to(zs, len(paths))
  186. segs = [_path_to_3d_segment(path, pathz, zdir)
  187. for path, pathz in zip(paths, zs)]
  188. return segs
  189. @cbook.deprecated("3.1")
  190. def paths_to_3d_segments(paths, zs=0, zdir='z'):
  191. """Convert paths from a collection object to 3D segments."""
  192. return _paths_to_3d_segments(paths, zs=zs, zdir=zdir)
  193. def _path_to_3d_segment_with_codes(path, zs=0, zdir='z'):
  194. """Convert a path to a 3D segment with path codes."""
  195. zs = np.broadcast_to(zs, len(path))
  196. pathsegs = path.iter_segments(simplify=False, curves=False)
  197. seg_codes = [((x, y, z), code) for ((x, y), code), z in zip(pathsegs, zs)]
  198. if seg_codes:
  199. seg, codes = zip(*seg_codes)
  200. seg3d = [juggle_axes(x, y, z, zdir) for (x, y, z) in seg]
  201. else:
  202. seg3d = []
  203. codes = []
  204. return seg3d, list(codes)
  205. @cbook.deprecated("3.1")
  206. def path_to_3d_segment_with_codes(path, zs=0, zdir='z'):
  207. """Convert a path to a 3D segment with path codes."""
  208. return _path_to_3d_segment_with_codes(path, zs=zs, zdir=zdir)
  209. def _paths_to_3d_segments_with_codes(paths, zs=0, zdir='z'):
  210. """
  211. Convert paths from a collection object to 3D segments with path codes.
  212. """
  213. zs = np.broadcast_to(zs, len(paths))
  214. segments_codes = [_path_to_3d_segment_with_codes(path, pathz, zdir)
  215. for path, pathz in zip(paths, zs)]
  216. if segments_codes:
  217. segments, codes = zip(*segments_codes)
  218. else:
  219. segments, codes = [], []
  220. return list(segments), list(codes)
  221. @cbook.deprecated("3.1")
  222. def paths_to_3d_segments_with_codes(paths, zs=0, zdir='z'):
  223. """
  224. Convert paths from a collection object to 3D segments with path codes.
  225. """
  226. return _paths_to_3d_segments_with_codes(paths, zs=zs, zdir=zdir)
  227. class Line3DCollection(LineCollection):
  228. """
  229. A collection of 3D lines.
  230. """
  231. def set_sort_zpos(self, val):
  232. """Set the position to use for z-sorting."""
  233. self._sort_zpos = val
  234. self.stale = True
  235. def set_segments(self, segments):
  236. """
  237. Set 3D segments.
  238. """
  239. self._segments3d = np.asanyarray(segments)
  240. LineCollection.set_segments(self, [])
  241. def do_3d_projection(self, renderer):
  242. """
  243. Project the points according to renderer matrix.
  244. """
  245. xyslist = [
  246. proj3d.proj_trans_points(points, renderer.M) for points in
  247. self._segments3d]
  248. segments_2d = [np.column_stack([xs, ys]) for xs, ys, zs in xyslist]
  249. LineCollection.set_segments(self, segments_2d)
  250. # FIXME
  251. minz = 1e9
  252. for xs, ys, zs in xyslist:
  253. minz = min(minz, min(zs))
  254. return minz
  255. @artist.allow_rasterization
  256. def draw(self, renderer, project=False):
  257. if project:
  258. self.do_3d_projection(renderer)
  259. LineCollection.draw(self, renderer)
  260. def line_collection_2d_to_3d(col, zs=0, zdir='z'):
  261. """Convert a LineCollection to a Line3DCollection object."""
  262. segments3d = _paths_to_3d_segments(col.get_paths(), zs, zdir)
  263. col.__class__ = Line3DCollection
  264. col.set_segments(segments3d)
  265. class Patch3D(Patch):
  266. """
  267. 3D patch object.
  268. """
  269. def __init__(self, *args, zs=(), zdir='z', **kwargs):
  270. Patch.__init__(self, *args, **kwargs)
  271. self.set_3d_properties(zs, zdir)
  272. def set_3d_properties(self, verts, zs=0, zdir='z'):
  273. zs = np.broadcast_to(zs, len(verts))
  274. self._segment3d = [juggle_axes(x, y, z, zdir)
  275. for ((x, y), z) in zip(verts, zs)]
  276. self._facecolor3d = Patch.get_facecolor(self)
  277. def get_path(self):
  278. return self._path2d
  279. def get_facecolor(self):
  280. return self._facecolor2d
  281. def do_3d_projection(self, renderer):
  282. s = self._segment3d
  283. xs, ys, zs = zip(*s)
  284. vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M)
  285. self._path2d = mpath.Path(np.column_stack([vxs, vys]))
  286. # FIXME: coloring
  287. self._facecolor2d = self._facecolor3d
  288. return min(vzs)
  289. class PathPatch3D(Patch3D):
  290. """
  291. 3D PathPatch object.
  292. """
  293. def __init__(self, path, *, zs=(), zdir='z', **kwargs):
  294. Patch.__init__(self, **kwargs)
  295. self.set_3d_properties(path, zs, zdir)
  296. def set_3d_properties(self, path, zs=0, zdir='z'):
  297. Patch3D.set_3d_properties(self, path.vertices, zs=zs, zdir=zdir)
  298. self._code3d = path.codes
  299. def do_3d_projection(self, renderer):
  300. s = self._segment3d
  301. xs, ys, zs = zip(*s)
  302. vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M)
  303. self._path2d = mpath.Path(np.column_stack([vxs, vys]), self._code3d)
  304. # FIXME: coloring
  305. self._facecolor2d = self._facecolor3d
  306. return min(vzs)
  307. def _get_patch_verts(patch):
  308. """Return a list of vertices for the path of a patch."""
  309. trans = patch.get_patch_transform()
  310. path = patch.get_path()
  311. polygons = path.to_polygons(trans)
  312. if len(polygons):
  313. return polygons[0]
  314. else:
  315. return []
  316. @cbook.deprecated("3.1")
  317. def get_patch_verts(patch):
  318. """Return a list of vertices for the path of a patch."""
  319. return _get_patch_verts(patch)
  320. def patch_2d_to_3d(patch, z=0, zdir='z'):
  321. """Convert a Patch to a Patch3D object."""
  322. verts = _get_patch_verts(patch)
  323. patch.__class__ = Patch3D
  324. patch.set_3d_properties(verts, z, zdir)
  325. def pathpatch_2d_to_3d(pathpatch, z=0, zdir='z'):
  326. """Convert a PathPatch to a PathPatch3D object."""
  327. path = pathpatch.get_path()
  328. trans = pathpatch.get_patch_transform()
  329. mpath = trans.transform_path(path)
  330. pathpatch.__class__ = PathPatch3D
  331. pathpatch.set_3d_properties(mpath, z, zdir)
  332. class Patch3DCollection(PatchCollection):
  333. """
  334. A collection of 3D patches.
  335. """
  336. def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs):
  337. """
  338. Create a collection of flat 3D patches with its normal vector
  339. pointed in *zdir* direction, and located at *zs* on the *zdir*
  340. axis. 'zs' can be a scalar or an array-like of the same length as
  341. the number of patches in the collection.
  342. Constructor arguments are the same as for
  343. :class:`~matplotlib.collections.PatchCollection`. In addition,
  344. keywords *zs=0* and *zdir='z'* are available.
  345. Also, the keyword argument "depthshade" is available to
  346. indicate whether or not to shade the patches in order to
  347. give the appearance of depth (default is *True*).
  348. This is typically desired in scatter plots.
  349. """
  350. self._depthshade = depthshade
  351. super().__init__(*args, **kwargs)
  352. self.set_3d_properties(zs, zdir)
  353. def set_sort_zpos(self, val):
  354. """Set the position to use for z-sorting."""
  355. self._sort_zpos = val
  356. self.stale = True
  357. def set_3d_properties(self, zs, zdir):
  358. # Force the collection to initialize the face and edgecolors
  359. # just in case it is a scalarmappable with a colormap.
  360. self.update_scalarmappable()
  361. offsets = self.get_offsets()
  362. if len(offsets) > 0:
  363. xs, ys = offsets.T
  364. else:
  365. xs = []
  366. ys = []
  367. self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir)
  368. self._facecolor3d = self.get_facecolor()
  369. self._edgecolor3d = self.get_edgecolor()
  370. self.stale = True
  371. def do_3d_projection(self, renderer):
  372. xs, ys, zs = self._offsets3d
  373. vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M)
  374. fcs = (_zalpha(self._facecolor3d, vzs) if self._depthshade else
  375. self._facecolor3d)
  376. fcs = mcolors.to_rgba_array(fcs, self._alpha)
  377. self.set_facecolors(fcs)
  378. ecs = (_zalpha(self._edgecolor3d, vzs) if self._depthshade else
  379. self._edgecolor3d)
  380. ecs = mcolors.to_rgba_array(ecs, self._alpha)
  381. self.set_edgecolors(ecs)
  382. PatchCollection.set_offsets(self, np.column_stack([vxs, vys]))
  383. if vzs.size > 0:
  384. return min(vzs)
  385. else:
  386. return np.nan
  387. class Path3DCollection(PathCollection):
  388. """
  389. A collection of 3D paths.
  390. """
  391. def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs):
  392. """
  393. Create a collection of flat 3D paths with its normal vector
  394. pointed in *zdir* direction, and located at *zs* on the *zdir*
  395. axis. 'zs' can be a scalar or an array-like of the same length as
  396. the number of paths in the collection.
  397. Constructor arguments are the same as for
  398. :class:`~matplotlib.collections.PathCollection`. In addition,
  399. keywords *zs=0* and *zdir='z'* are available.
  400. Also, the keyword argument "depthshade" is available to
  401. indicate whether or not to shade the patches in order to
  402. give the appearance of depth (default is *True*).
  403. This is typically desired in scatter plots.
  404. """
  405. self._depthshade = depthshade
  406. super().__init__(*args, **kwargs)
  407. self.set_3d_properties(zs, zdir)
  408. def set_sort_zpos(self, val):
  409. """Set the position to use for z-sorting."""
  410. self._sort_zpos = val
  411. self.stale = True
  412. def set_3d_properties(self, zs, zdir):
  413. # Force the collection to initialize the face and edgecolors
  414. # just in case it is a scalarmappable with a colormap.
  415. self.update_scalarmappable()
  416. offsets = self.get_offsets()
  417. if len(offsets) > 0:
  418. xs, ys = offsets.T
  419. else:
  420. xs = []
  421. ys = []
  422. self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir)
  423. self._facecolor3d = self.get_facecolor()
  424. self._edgecolor3d = self.get_edgecolor()
  425. self.stale = True
  426. def do_3d_projection(self, renderer):
  427. xs, ys, zs = self._offsets3d
  428. vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M)
  429. fcs = (_zalpha(self._facecolor3d, vzs) if self._depthshade else
  430. self._facecolor3d)
  431. fcs = mcolors.to_rgba_array(fcs, self._alpha)
  432. self.set_facecolors(fcs)
  433. ecs = (_zalpha(self._edgecolor3d, vzs) if self._depthshade else
  434. self._edgecolor3d)
  435. ecs = mcolors.to_rgba_array(ecs, self._alpha)
  436. self.set_edgecolors(ecs)
  437. PathCollection.set_offsets(self, np.column_stack([vxs, vys]))
  438. return np.min(vzs) if vzs.size else np.nan
  439. def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True):
  440. """
  441. Convert a :class:`~matplotlib.collections.PatchCollection` into a
  442. :class:`Patch3DCollection` object
  443. (or a :class:`~matplotlib.collections.PathCollection` into a
  444. :class:`Path3DCollection` object).
  445. Parameters
  446. ----------
  447. za
  448. The location or locations to place the patches in the collection along
  449. the *zdir* axis. Default: 0.
  450. zdir
  451. The axis in which to place the patches. Default: "z".
  452. depthshade
  453. Whether to shade the patches to give a sense of depth. Default: *True*.
  454. """
  455. if isinstance(col, PathCollection):
  456. col.__class__ = Path3DCollection
  457. elif isinstance(col, PatchCollection):
  458. col.__class__ = Patch3DCollection
  459. col._depthshade = depthshade
  460. col.set_3d_properties(zs, zdir)
  461. class Poly3DCollection(PolyCollection):
  462. """
  463. A collection of 3D polygons.
  464. .. note::
  465. **Filling of 3D polygons**
  466. There is no simple definition of the enclosed surface of a 3D polygon
  467. unless the polygon is planar.
  468. In practice, Matplotlib performs the filling on the 2D projection of
  469. the polygon. This gives a correct filling appearance only for planar
  470. polygons. For all other polygons, you'll find orientations in which
  471. the edges of the polygon intersect in the projection. This will lead
  472. to an incorrect visualization of the 3D area.
  473. If you need filled areas, it is recommended to create them via
  474. `~mpl_toolkits.mplot3d.axes3d.Axes3D.plot_trisurf`, which creates a
  475. triangulation and thus generates consistent surfaces.
  476. """
  477. def __init__(self, verts, *args, zsort='average', **kwargs):
  478. """
  479. Parameters
  480. ----------
  481. verts : list of array-like Nx3
  482. Each element describes a polygon as a sequnce of ``N_i`` points
  483. ``(x, y, z)``.
  484. zsort : {'average', 'min', 'max'}, default: 'average'
  485. The calculation method for the z-order.
  486. See `~.Poly3DCollection.set_zsort` for details.
  487. *args, **kwargs
  488. All other parameters are forwarded to `.PolyCollection`.
  489. Notes
  490. -----
  491. Note that this class does a bit of magic with the _facecolors
  492. and _edgecolors properties.
  493. """
  494. super().__init__(verts, *args, **kwargs)
  495. self.set_zsort(zsort)
  496. self._codes3d = None
  497. _zsort_functions = {
  498. 'average': np.average,
  499. 'min': np.min,
  500. 'max': np.max,
  501. }
  502. def set_zsort(self, zsort):
  503. """
  504. Sets the calculation method for the z-order.
  505. Parameters
  506. ----------
  507. zsort : {'average', 'min', 'max'}
  508. The function applied on the z-coordinates of the vertices in the
  509. viewer's coordinate system, to determine the z-order. *True* is
  510. deprecated and equivalent to 'average'.
  511. """
  512. if zsort is True:
  513. cbook.warn_deprecated(
  514. "3.1", message="Passing True to mean 'average' for set_zsort "
  515. "is deprecated and support will be removed in Matplotlib 3.3; "
  516. "pass 'average' instead.")
  517. zsort = 'average'
  518. self._zsortfunc = self._zsort_functions[zsort]
  519. self._sort_zpos = None
  520. self.stale = True
  521. def get_vector(self, segments3d):
  522. """Optimize points for projection."""
  523. if len(segments3d):
  524. xs, ys, zs = np.row_stack(segments3d).T
  525. else: # row_stack can't stack zero arrays.
  526. xs, ys, zs = [], [], []
  527. ones = np.ones(len(xs))
  528. self._vec = np.array([xs, ys, zs, ones])
  529. indices = [0, *np.cumsum([len(segment) for segment in segments3d])]
  530. self._segslices = [*map(slice, indices[:-1], indices[1:])]
  531. def set_verts(self, verts, closed=True):
  532. """Set 3D vertices."""
  533. self.get_vector(verts)
  534. # 2D verts will be updated at draw time
  535. PolyCollection.set_verts(self, [], False)
  536. self._closed = closed
  537. def set_verts_and_codes(self, verts, codes):
  538. """Sets 3D vertices with path codes."""
  539. # set vertices with closed=False to prevent PolyCollection from
  540. # setting path codes
  541. self.set_verts(verts, closed=False)
  542. # and set our own codes instead.
  543. self._codes3d = codes
  544. def set_3d_properties(self):
  545. # Force the collection to initialize the face and edgecolors
  546. # just in case it is a scalarmappable with a colormap.
  547. self.update_scalarmappable()
  548. self._sort_zpos = None
  549. self.set_zsort('average')
  550. self._facecolors3d = PolyCollection.get_facecolor(self)
  551. self._edgecolors3d = PolyCollection.get_edgecolor(self)
  552. self._alpha3d = PolyCollection.get_alpha(self)
  553. self.stale = True
  554. def set_sort_zpos(self, val):
  555. """Set the position to use for z-sorting."""
  556. self._sort_zpos = val
  557. self.stale = True
  558. def do_3d_projection(self, renderer):
  559. """
  560. Perform the 3D projection for this object.
  561. """
  562. # FIXME: This may no longer be needed?
  563. if self._A is not None:
  564. self.update_scalarmappable()
  565. self._facecolors3d = self._facecolors
  566. txs, tys, tzs = proj3d._proj_transform_vec(self._vec, renderer.M)
  567. xyzlist = [(txs[sl], tys[sl], tzs[sl]) for sl in self._segslices]
  568. # This extra fuss is to re-order face / edge colors
  569. cface = self._facecolors3d
  570. cedge = self._edgecolors3d
  571. if len(cface) != len(xyzlist):
  572. cface = cface.repeat(len(xyzlist), axis=0)
  573. if len(cedge) != len(xyzlist):
  574. if len(cedge) == 0:
  575. cedge = cface
  576. else:
  577. cedge = cedge.repeat(len(xyzlist), axis=0)
  578. # sort by depth (furthest drawn first)
  579. z_segments_2d = sorted(
  580. ((self._zsortfunc(zs), np.column_stack([xs, ys]), fc, ec, idx)
  581. for idx, ((xs, ys, zs), fc, ec)
  582. in enumerate(zip(xyzlist, cface, cedge))),
  583. key=lambda x: x[0], reverse=True)
  584. segments_2d = [s for z, s, fc, ec, idx in z_segments_2d]
  585. if self._codes3d is not None:
  586. codes = [self._codes3d[idx] for z, s, fc, ec, idx in z_segments_2d]
  587. PolyCollection.set_verts_and_codes(self, segments_2d, codes)
  588. else:
  589. PolyCollection.set_verts(self, segments_2d, self._closed)
  590. self._facecolors2d = [fc for z, s, fc, ec, idx in z_segments_2d]
  591. if len(self._edgecolors3d) == len(cface):
  592. self._edgecolors2d = [ec for z, s, fc, ec, idx in z_segments_2d]
  593. else:
  594. self._edgecolors2d = self._edgecolors3d
  595. # Return zorder value
  596. if self._sort_zpos is not None:
  597. zvec = np.array([[0], [0], [self._sort_zpos], [1]])
  598. ztrans = proj3d._proj_transform_vec(zvec, renderer.M)
  599. return ztrans[2][0]
  600. elif tzs.size > 0:
  601. # FIXME: Some results still don't look quite right.
  602. # In particular, examine contourf3d_demo2.py
  603. # with az = -54 and elev = -45.
  604. return np.min(tzs)
  605. else:
  606. return np.nan
  607. def set_facecolor(self, colors):
  608. PolyCollection.set_facecolor(self, colors)
  609. self._facecolors3d = PolyCollection.get_facecolor(self)
  610. def set_edgecolor(self, colors):
  611. PolyCollection.set_edgecolor(self, colors)
  612. self._edgecolors3d = PolyCollection.get_edgecolor(self)
  613. def set_alpha(self, alpha):
  614. # docstring inherited
  615. artist.Artist.set_alpha(self, alpha)
  616. try:
  617. self._facecolors3d = mcolors.to_rgba_array(
  618. self._facecolors3d, self._alpha)
  619. except (AttributeError, TypeError, IndexError):
  620. pass
  621. try:
  622. self._edgecolors = mcolors.to_rgba_array(
  623. self._edgecolors3d, self._alpha)
  624. except (AttributeError, TypeError, IndexError):
  625. pass
  626. self.stale = True
  627. def get_facecolor(self):
  628. return self._facecolors2d
  629. def get_edgecolor(self):
  630. return self._edgecolors2d
  631. def poly_collection_2d_to_3d(col, zs=0, zdir='z'):
  632. """Convert a PolyCollection to a Poly3DCollection object."""
  633. segments_3d, codes = _paths_to_3d_segments_with_codes(
  634. col.get_paths(), zs, zdir)
  635. col.__class__ = Poly3DCollection
  636. col.set_verts_and_codes(segments_3d, codes)
  637. col.set_3d_properties()
  638. def juggle_axes(xs, ys, zs, zdir):
  639. """
  640. Reorder coordinates so that 2D xs, ys can be plotted in the plane
  641. orthogonal to zdir. zdir is normally x, y or z. However, if zdir
  642. starts with a '-' it is interpreted as a compensation for rotate_axes.
  643. """
  644. if zdir == 'x':
  645. return zs, xs, ys
  646. elif zdir == 'y':
  647. return xs, zs, ys
  648. elif zdir[0] == '-':
  649. return rotate_axes(xs, ys, zs, zdir)
  650. else:
  651. return xs, ys, zs
  652. def rotate_axes(xs, ys, zs, zdir):
  653. """
  654. Reorder coordinates so that the axes are rotated with zdir along
  655. the original z axis. Prepending the axis with a '-' does the
  656. inverse transform, so zdir can be x, -x, y, -y, z or -z
  657. """
  658. if zdir == 'x':
  659. return ys, zs, xs
  660. elif zdir == '-x':
  661. return zs, xs, ys
  662. elif zdir == 'y':
  663. return zs, xs, ys
  664. elif zdir == '-y':
  665. return ys, zs, xs
  666. else:
  667. return xs, ys, zs
  668. def _get_colors(c, num):
  669. """Stretch the color argument to provide the required number *num*."""
  670. return np.broadcast_to(
  671. mcolors.to_rgba_array(c) if len(c) else [0, 0, 0, 0],
  672. (num, 4))
  673. @cbook.deprecated("3.1")
  674. def get_colors(c, num):
  675. """Stretch the color argument to provide the required number *num*."""
  676. return _get_colors(c, num)
  677. def _zalpha(colors, zs):
  678. """Modify the alphas of the color list according to depth."""
  679. # FIXME: This only works well if the points for *zs* are well-spaced
  680. # in all three dimensions. Otherwise, at certain orientations,
  681. # the min and max zs are very close together.
  682. # Should really normalize against the viewing depth.
  683. if len(zs) == 0:
  684. return np.zeros((0, 4))
  685. norm = Normalize(min(zs), max(zs))
  686. sats = 1 - norm(zs) * 0.7
  687. rgba = np.broadcast_to(mcolors.to_rgba_array(colors), (len(zs), 4))
  688. return np.column_stack([rgba[:, :3], rgba[:, 3] * sats])
  689. @cbook.deprecated("3.1")
  690. def zalpha(colors, zs):
  691. """Modify the alphas of the color list according to depth."""
  692. return _zalpha(colors, zs)