art3d.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774
  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. from __future__ import (absolute_import, division, print_function,
  9. unicode_literals)
  10. import six
  11. from six.moves import zip
  12. import math
  13. import numpy as np
  14. from matplotlib import (
  15. artist, cbook, colors as mcolors, lines, text as mtext, path as mpath)
  16. from matplotlib.cbook import _backports
  17. from matplotlib.collections import (
  18. Collection, LineCollection, PolyCollection, PatchCollection,
  19. PathCollection)
  20. from matplotlib.colors import Normalize
  21. from matplotlib.patches import Patch
  22. from . import proj3d
  23. def norm_angle(a):
  24. """Return angle between -180 and +180"""
  25. a = (a + 360) % 360
  26. if a > 180:
  27. a = a - 360
  28. return a
  29. def norm_text_angle(a):
  30. """Return angle between -90 and +90"""
  31. a = (a + 180) % 180
  32. if a > 90:
  33. a = a - 180
  34. return a
  35. def get_dir_vector(zdir):
  36. if zdir == 'x':
  37. return np.array((1, 0, 0))
  38. elif zdir == 'y':
  39. return np.array((0, 1, 0))
  40. elif zdir == 'z':
  41. return np.array((0, 0, 1))
  42. elif zdir is None:
  43. return np.array((0, 0, 0))
  44. elif cbook.iterable(zdir) and len(zdir) == 3:
  45. return zdir
  46. else:
  47. raise ValueError("'x', 'y', 'z', None or vector of length 3 expected")
  48. class Text3D(mtext.Text):
  49. '''
  50. Text object with 3D position and (in the future) direction.
  51. '''
  52. def __init__(self, x=0, y=0, z=0, text='', zdir='z', **kwargs):
  53. '''
  54. *x*, *y*, *z* Position of text
  55. *text* Text string to display
  56. *zdir* Direction of text
  57. Keyword arguments are passed onto :func:`~matplotlib.text.Text`.
  58. '''
  59. mtext.Text.__init__(self, x, y, text, **kwargs)
  60. self.set_3d_properties(z, zdir)
  61. def set_3d_properties(self, z=0, zdir='z'):
  62. x, y = self.get_position()
  63. self._position3d = np.array((x, y, z))
  64. self._dir_vec = get_dir_vector(zdir)
  65. self.stale = True
  66. def draw(self, renderer):
  67. proj = proj3d.proj_trans_points(
  68. [self._position3d, self._position3d + self._dir_vec], renderer.M)
  69. dx = proj[0][1] - proj[0][0]
  70. dy = proj[1][1] - proj[1][0]
  71. if dx==0. and dy==0.:
  72. # atan2 raises ValueError: math domain error on 0,0
  73. angle = 0.
  74. else:
  75. angle = math.degrees(math.atan2(dy, dx))
  76. self.set_position((proj[0][0], proj[1][0]))
  77. self.set_rotation(norm_text_angle(angle))
  78. mtext.Text.draw(self, renderer)
  79. self.stale = False
  80. def text_2d_to_3d(obj, z=0, zdir='z'):
  81. """Convert a Text to a Text3D object."""
  82. obj.__class__ = Text3D
  83. obj.set_3d_properties(z, zdir)
  84. class Line3D(lines.Line2D):
  85. '''
  86. 3D line object.
  87. '''
  88. def __init__(self, xs, ys, zs, *args, **kwargs):
  89. '''
  90. Keyword arguments are passed onto :func:`~matplotlib.lines.Line2D`.
  91. '''
  92. lines.Line2D.__init__(self, [], [], *args, **kwargs)
  93. self._verts3d = xs, ys, zs
  94. def set_3d_properties(self, zs=0, zdir='z'):
  95. xs = self.get_xdata()
  96. ys = self.get_ydata()
  97. try:
  98. # If *zs* is a list or array, then this will fail and
  99. # just proceed to juggle_axes().
  100. zs = float(zs)
  101. zs = [zs for x in xs]
  102. except TypeError:
  103. pass
  104. self._verts3d = juggle_axes(xs, ys, zs, zdir)
  105. self.stale = True
  106. def draw(self, renderer):
  107. xs3d, ys3d, zs3d = self._verts3d
  108. xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, renderer.M)
  109. self.set_data(xs, ys)
  110. lines.Line2D.draw(self, renderer)
  111. self.stale = False
  112. def line_2d_to_3d(line, zs=0, zdir='z'):
  113. '''
  114. Convert a 2D line to 3D.
  115. '''
  116. line.__class__ = Line3D
  117. line.set_3d_properties(zs, zdir)
  118. def path_to_3d_segment(path, zs=0, zdir='z'):
  119. '''Convert a path to a 3D segment.'''
  120. zs = _backports.broadcast_to(zs, len(path))
  121. pathsegs = path.iter_segments(simplify=False, curves=False)
  122. seg = [(x, y, z) for (((x, y), code), z) in zip(pathsegs, zs)]
  123. seg3d = [juggle_axes(x, y, z, zdir) for (x, y, z) in seg]
  124. return seg3d
  125. def paths_to_3d_segments(paths, zs=0, zdir='z'):
  126. '''
  127. Convert paths from a collection object to 3D segments.
  128. '''
  129. zs = _backports.broadcast_to(zs, len(paths))
  130. segs = [path_to_3d_segment(path, pathz, zdir)
  131. for path, pathz in zip(paths, zs)]
  132. return segs
  133. def path_to_3d_segment_with_codes(path, zs=0, zdir='z'):
  134. '''Convert a path to a 3D segment with path codes.'''
  135. zs = _backports.broadcast_to(zs, len(path))
  136. seg = []
  137. codes = []
  138. pathsegs = path.iter_segments(simplify=False, curves=False)
  139. for (((x, y), code), z) in zip(pathsegs, zs):
  140. seg.append((x, y, z))
  141. codes.append(code)
  142. seg3d = [juggle_axes(x, y, z, zdir) for (x, y, z) in seg]
  143. return seg3d, codes
  144. def paths_to_3d_segments_with_codes(paths, zs=0, zdir='z'):
  145. '''
  146. Convert paths from a collection object to 3D segments with path codes.
  147. '''
  148. zs = _backports.broadcast_to(zs, len(paths))
  149. segments = []
  150. codes_list = []
  151. for path, pathz in zip(paths, zs):
  152. segs, codes = path_to_3d_segment_with_codes(path, pathz, zdir)
  153. segments.append(segs)
  154. codes_list.append(codes)
  155. return segments, codes_list
  156. class Line3DCollection(LineCollection):
  157. '''
  158. A collection of 3D lines.
  159. '''
  160. def __init__(self, segments, *args, **kwargs):
  161. '''
  162. Keyword arguments are passed onto :func:`~matplotlib.collections.LineCollection`.
  163. '''
  164. LineCollection.__init__(self, segments, *args, **kwargs)
  165. def set_sort_zpos(self, val):
  166. '''Set the position to use for z-sorting.'''
  167. self._sort_zpos = val
  168. self.stale = True
  169. def set_segments(self, segments):
  170. '''
  171. Set 3D segments
  172. '''
  173. self._segments3d = np.asanyarray(segments)
  174. LineCollection.set_segments(self, [])
  175. def do_3d_projection(self, renderer):
  176. '''
  177. Project the points according to renderer matrix.
  178. '''
  179. xyslist = [
  180. proj3d.proj_trans_points(points, renderer.M) for points in
  181. self._segments3d]
  182. segments_2d = [np.column_stack([xs, ys]) for xs, ys, zs in xyslist]
  183. LineCollection.set_segments(self, segments_2d)
  184. # FIXME
  185. minz = 1e9
  186. for xs, ys, zs in xyslist:
  187. minz = min(minz, min(zs))
  188. return minz
  189. def draw(self, renderer, project=False):
  190. if project:
  191. self.do_3d_projection(renderer)
  192. LineCollection.draw(self, renderer)
  193. def line_collection_2d_to_3d(col, zs=0, zdir='z'):
  194. """Convert a LineCollection to a Line3DCollection object."""
  195. segments3d = paths_to_3d_segments(col.get_paths(), zs, zdir)
  196. col.__class__ = Line3DCollection
  197. col.set_segments(segments3d)
  198. class Patch3D(Patch):
  199. '''
  200. 3D patch object.
  201. '''
  202. def __init__(self, *args, **kwargs):
  203. zs = kwargs.pop('zs', [])
  204. zdir = kwargs.pop('zdir', 'z')
  205. Patch.__init__(self, *args, **kwargs)
  206. self.set_3d_properties(zs, zdir)
  207. def set_3d_properties(self, verts, zs=0, zdir='z'):
  208. zs = _backports.broadcast_to(zs, len(verts))
  209. self._segment3d = [juggle_axes(x, y, z, zdir)
  210. for ((x, y), z) in zip(verts, zs)]
  211. self._facecolor3d = Patch.get_facecolor(self)
  212. def get_path(self):
  213. return self._path2d
  214. def get_facecolor(self):
  215. return self._facecolor2d
  216. def do_3d_projection(self, renderer):
  217. s = self._segment3d
  218. xs, ys, zs = zip(*s)
  219. vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M)
  220. self._path2d = mpath.Path(np.column_stack([vxs, vys]))
  221. # FIXME: coloring
  222. self._facecolor2d = self._facecolor3d
  223. return min(vzs)
  224. def draw(self, renderer):
  225. Patch.draw(self, renderer)
  226. class PathPatch3D(Patch3D):
  227. '''
  228. 3D PathPatch object.
  229. '''
  230. def __init__(self, path, **kwargs):
  231. zs = kwargs.pop('zs', [])
  232. zdir = kwargs.pop('zdir', 'z')
  233. Patch.__init__(self, **kwargs)
  234. self.set_3d_properties(path, zs, zdir)
  235. def set_3d_properties(self, path, zs=0, zdir='z'):
  236. Patch3D.set_3d_properties(self, path.vertices, zs=zs, zdir=zdir)
  237. self._code3d = path.codes
  238. def do_3d_projection(self, renderer):
  239. s = self._segment3d
  240. xs, ys, zs = zip(*s)
  241. vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M)
  242. self._path2d = mpath.Path(np.column_stack([vxs, vys]), self._code3d)
  243. # FIXME: coloring
  244. self._facecolor2d = self._facecolor3d
  245. return min(vzs)
  246. def get_patch_verts(patch):
  247. """Return a list of vertices for the path of a patch."""
  248. trans = patch.get_patch_transform()
  249. path = patch.get_path()
  250. polygons = path.to_polygons(trans)
  251. if len(polygons):
  252. return polygons[0]
  253. else:
  254. return []
  255. def patch_2d_to_3d(patch, z=0, zdir='z'):
  256. """Convert a Patch to a Patch3D object."""
  257. verts = get_patch_verts(patch)
  258. patch.__class__ = Patch3D
  259. patch.set_3d_properties(verts, z, zdir)
  260. def pathpatch_2d_to_3d(pathpatch, z=0, zdir='z'):
  261. """Convert a PathPatch to a PathPatch3D object."""
  262. path = pathpatch.get_path()
  263. trans = pathpatch.get_patch_transform()
  264. mpath = trans.transform_path(path)
  265. pathpatch.__class__ = PathPatch3D
  266. pathpatch.set_3d_properties(mpath, z, zdir)
  267. class Patch3DCollection(PatchCollection):
  268. '''
  269. A collection of 3D patches.
  270. '''
  271. def __init__(self, *args, **kwargs):
  272. """
  273. Create a collection of flat 3D patches with its normal vector
  274. pointed in *zdir* direction, and located at *zs* on the *zdir*
  275. axis. 'zs' can be a scalar or an array-like of the same length as
  276. the number of patches in the collection.
  277. Constructor arguments are the same as for
  278. :class:`~matplotlib.collections.PatchCollection`. In addition,
  279. keywords *zs=0* and *zdir='z'* are available.
  280. Also, the keyword argument "depthshade" is available to
  281. indicate whether or not to shade the patches in order to
  282. give the appearance of depth (default is *True*).
  283. This is typically desired in scatter plots.
  284. """
  285. zs = kwargs.pop('zs', 0)
  286. zdir = kwargs.pop('zdir', 'z')
  287. self._depthshade = kwargs.pop('depthshade', True)
  288. PatchCollection.__init__(self, *args, **kwargs)
  289. self.set_3d_properties(zs, zdir)
  290. def set_sort_zpos(self, val):
  291. '''Set the position to use for z-sorting.'''
  292. self._sort_zpos = val
  293. self.stale = True
  294. def set_3d_properties(self, zs, zdir):
  295. # Force the collection to initialize the face and edgecolors
  296. # just in case it is a scalarmappable with a colormap.
  297. self.update_scalarmappable()
  298. offsets = self.get_offsets()
  299. if len(offsets) > 0:
  300. xs, ys = zip(*offsets)
  301. else:
  302. xs = []
  303. ys = []
  304. self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir)
  305. self._facecolor3d = self.get_facecolor()
  306. self._edgecolor3d = self.get_edgecolor()
  307. self.stale = True
  308. def do_3d_projection(self, renderer):
  309. xs, ys, zs = self._offsets3d
  310. vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M)
  311. fcs = (zalpha(self._facecolor3d, vzs) if self._depthshade else
  312. self._facecolor3d)
  313. fcs = mcolors.to_rgba_array(fcs, self._alpha)
  314. self.set_facecolors(fcs)
  315. ecs = (zalpha(self._edgecolor3d, vzs) if self._depthshade else
  316. self._edgecolor3d)
  317. ecs = mcolors.to_rgba_array(ecs, self._alpha)
  318. self.set_edgecolors(ecs)
  319. PatchCollection.set_offsets(self, np.column_stack([vxs, vys]))
  320. if vzs.size > 0:
  321. return min(vzs)
  322. else:
  323. return np.nan
  324. class Path3DCollection(PathCollection):
  325. '''
  326. A collection of 3D paths.
  327. '''
  328. def __init__(self, *args, **kwargs):
  329. """
  330. Create a collection of flat 3D paths with its normal vector
  331. pointed in *zdir* direction, and located at *zs* on the *zdir*
  332. axis. 'zs' can be a scalar or an array-like of the same length as
  333. the number of paths in the collection.
  334. Constructor arguments are the same as for
  335. :class:`~matplotlib.collections.PathCollection`. In addition,
  336. keywords *zs=0* and *zdir='z'* are available.
  337. Also, the keyword argument "depthshade" is available to
  338. indicate whether or not to shade the patches in order to
  339. give the appearance of depth (default is *True*).
  340. This is typically desired in scatter plots.
  341. """
  342. zs = kwargs.pop('zs', 0)
  343. zdir = kwargs.pop('zdir', 'z')
  344. self._depthshade = kwargs.pop('depthshade', True)
  345. PathCollection.__init__(self, *args, **kwargs)
  346. self.set_3d_properties(zs, zdir)
  347. def set_sort_zpos(self, val):
  348. '''Set the position to use for z-sorting.'''
  349. self._sort_zpos = val
  350. self.stale = True
  351. def set_3d_properties(self, zs, zdir):
  352. # Force the collection to initialize the face and edgecolors
  353. # just in case it is a scalarmappable with a colormap.
  354. self.update_scalarmappable()
  355. offsets = self.get_offsets()
  356. if len(offsets) > 0:
  357. xs, ys = zip(*offsets)
  358. else:
  359. xs = []
  360. ys = []
  361. self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir)
  362. self._facecolor3d = self.get_facecolor()
  363. self._edgecolor3d = self.get_edgecolor()
  364. self.stale = True
  365. def do_3d_projection(self, renderer):
  366. xs, ys, zs = self._offsets3d
  367. vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, renderer.M)
  368. fcs = (zalpha(self._facecolor3d, vzs) if self._depthshade else
  369. self._facecolor3d)
  370. fcs = mcolors.to_rgba_array(fcs, self._alpha)
  371. self.set_facecolors(fcs)
  372. ecs = (zalpha(self._edgecolor3d, vzs) if self._depthshade else
  373. self._edgecolor3d)
  374. ecs = mcolors.to_rgba_array(ecs, self._alpha)
  375. self.set_edgecolors(ecs)
  376. PathCollection.set_offsets(self, np.column_stack([vxs, vys]))
  377. if vzs.size > 0 :
  378. return min(vzs)
  379. else :
  380. return np.nan
  381. def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True):
  382. """
  383. Convert a :class:`~matplotlib.collections.PatchCollection` into a
  384. :class:`Patch3DCollection` object
  385. (or a :class:`~matplotlib.collections.PathCollection` into a
  386. :class:`Path3DCollection` object).
  387. Keywords:
  388. *za* The location or locations to place the patches in the
  389. collection along the *zdir* axis. Defaults to 0.
  390. *zdir* The axis in which to place the patches. Default is "z".
  391. *depthshade* Whether to shade the patches to give a sense of depth.
  392. Defaults to *True*.
  393. """
  394. if isinstance(col, PathCollection):
  395. col.__class__ = Path3DCollection
  396. elif isinstance(col, PatchCollection):
  397. col.__class__ = Patch3DCollection
  398. col._depthshade = depthshade
  399. col.set_3d_properties(zs, zdir)
  400. class Poly3DCollection(PolyCollection):
  401. '''
  402. A collection of 3D polygons.
  403. '''
  404. def __init__(self, verts, *args, **kwargs):
  405. '''
  406. Create a Poly3DCollection.
  407. *verts* should contain 3D coordinates.
  408. Keyword arguments:
  409. zsort, see set_zsort for options.
  410. Note that this class does a bit of magic with the _facecolors
  411. and _edgecolors properties.
  412. '''
  413. zsort = kwargs.pop('zsort', True)
  414. PolyCollection.__init__(self, verts, *args, **kwargs)
  415. self.set_zsort(zsort)
  416. self._codes3d = None
  417. _zsort_functions = {
  418. 'average': np.average,
  419. 'min': np.min,
  420. 'max': np.max,
  421. }
  422. def set_zsort(self, zsort):
  423. '''
  424. Set z-sorting behaviour:
  425. boolean: if True use default 'average'
  426. string: 'average', 'min' or 'max'
  427. '''
  428. if zsort is True:
  429. zsort = 'average'
  430. if zsort is not False:
  431. if zsort in self._zsort_functions:
  432. zsortfunc = self._zsort_functions[zsort]
  433. else:
  434. return False
  435. else:
  436. zsortfunc = None
  437. self._zsort = zsort
  438. self._sort_zpos = None
  439. self._zsortfunc = zsortfunc
  440. self.stale = True
  441. def get_vector(self, segments3d):
  442. """Optimize points for projection"""
  443. si = 0
  444. ei = 0
  445. segis = []
  446. points = []
  447. for p in segments3d:
  448. points.extend(p)
  449. ei = si + len(p)
  450. segis.append((si, ei))
  451. si = ei
  452. if len(segments3d):
  453. xs, ys, zs = zip(*points)
  454. else :
  455. # We need this so that we can skip the bad unpacking from zip()
  456. xs, ys, zs = [], [], []
  457. ones = np.ones(len(xs))
  458. self._vec = np.array([xs, ys, zs, ones])
  459. self._segis = segis
  460. def set_verts(self, verts, closed=True):
  461. '''Set 3D vertices.'''
  462. self.get_vector(verts)
  463. # 2D verts will be updated at draw time
  464. PolyCollection.set_verts(self, [], False)
  465. self._closed = closed
  466. def set_verts_and_codes(self, verts, codes):
  467. '''Sets 3D vertices with path codes'''
  468. # set vertices with closed=False to prevent PolyCollection from
  469. # setting path codes
  470. self.set_verts(verts, closed=False)
  471. # and set our own codes instead.
  472. self._codes3d = codes
  473. def set_3d_properties(self):
  474. # Force the collection to initialize the face and edgecolors
  475. # just in case it is a scalarmappable with a colormap.
  476. self.update_scalarmappable()
  477. self._sort_zpos = None
  478. self.set_zsort(True)
  479. self._facecolors3d = PolyCollection.get_facecolors(self)
  480. self._edgecolors3d = PolyCollection.get_edgecolors(self)
  481. self._alpha3d = PolyCollection.get_alpha(self)
  482. self.stale = True
  483. def set_sort_zpos(self,val):
  484. '''Set the position to use for z-sorting.'''
  485. self._sort_zpos = val
  486. self.stale = True
  487. def do_3d_projection(self, renderer):
  488. '''
  489. Perform the 3D projection for this object.
  490. '''
  491. # FIXME: This may no longer be needed?
  492. if self._A is not None:
  493. self.update_scalarmappable()
  494. self._facecolors3d = self._facecolors
  495. txs, tys, tzs = proj3d.proj_transform_vec(self._vec, renderer.M)
  496. xyzlist = [(txs[si:ei], tys[si:ei], tzs[si:ei])
  497. for si, ei in self._segis]
  498. # This extra fuss is to re-order face / edge colors
  499. cface = self._facecolors3d
  500. cedge = self._edgecolors3d
  501. if len(cface) != len(xyzlist):
  502. cface = cface.repeat(len(xyzlist), axis=0)
  503. if len(cedge) != len(xyzlist):
  504. if len(cedge) == 0:
  505. cedge = cface
  506. else:
  507. cedge = cedge.repeat(len(xyzlist), axis=0)
  508. # if required sort by depth (furthest drawn first)
  509. if self._zsort:
  510. z_segments_2d = sorted(
  511. ((self._zsortfunc(zs), np.column_stack([xs, ys]), fc, ec, idx)
  512. for idx, ((xs, ys, zs), fc, ec)
  513. in enumerate(zip(xyzlist, cface, cedge))),
  514. key=lambda x: x[0], reverse=True)
  515. else:
  516. raise ValueError("whoops")
  517. segments_2d = [s for z, s, fc, ec, idx in z_segments_2d]
  518. if self._codes3d is not None:
  519. codes = [self._codes3d[idx] for z, s, fc, ec, idx in z_segments_2d]
  520. PolyCollection.set_verts_and_codes(self, segments_2d, codes)
  521. else:
  522. PolyCollection.set_verts(self, segments_2d, self._closed)
  523. self._facecolors2d = [fc for z, s, fc, ec, idx in z_segments_2d]
  524. if len(self._edgecolors3d) == len(cface):
  525. self._edgecolors2d = [ec for z, s, fc, ec, idx in z_segments_2d]
  526. else:
  527. self._edgecolors2d = self._edgecolors3d
  528. # Return zorder value
  529. if self._sort_zpos is not None:
  530. zvec = np.array([[0], [0], [self._sort_zpos], [1]])
  531. ztrans = proj3d.proj_transform_vec(zvec, renderer.M)
  532. return ztrans[2][0]
  533. elif tzs.size > 0 :
  534. # FIXME: Some results still don't look quite right.
  535. # In particular, examine contourf3d_demo2.py
  536. # with az = -54 and elev = -45.
  537. return np.min(tzs)
  538. else :
  539. return np.nan
  540. def set_facecolor(self, colors):
  541. PolyCollection.set_facecolor(self, colors)
  542. self._facecolors3d = PolyCollection.get_facecolor(self)
  543. set_facecolors = set_facecolor
  544. def set_edgecolor(self, colors):
  545. PolyCollection.set_edgecolor(self, colors)
  546. self._edgecolors3d = PolyCollection.get_edgecolor(self)
  547. set_edgecolors = set_edgecolor
  548. def set_alpha(self, alpha):
  549. """
  550. Set the alpha tranparencies of the collection. *alpha* must be
  551. a float or *None*.
  552. ACCEPTS: float or None
  553. """
  554. if alpha is not None:
  555. try:
  556. float(alpha)
  557. except TypeError:
  558. raise TypeError('alpha must be a float or None')
  559. artist.Artist.set_alpha(self, alpha)
  560. try:
  561. self._facecolors = mcolors.to_rgba_array(
  562. self._facecolors3d, self._alpha)
  563. except (AttributeError, TypeError, IndexError):
  564. pass
  565. try:
  566. self._edgecolors = mcolors.to_rgba_array(
  567. self._edgecolors3d, self._alpha)
  568. except (AttributeError, TypeError, IndexError):
  569. pass
  570. self.stale = True
  571. def get_facecolors(self):
  572. return self._facecolors2d
  573. get_facecolor = get_facecolors
  574. def get_edgecolors(self):
  575. return self._edgecolors2d
  576. get_edgecolor = get_edgecolors
  577. def draw(self, renderer):
  578. return Collection.draw(self, renderer)
  579. def poly_collection_2d_to_3d(col, zs=0, zdir='z'):
  580. """Convert a PolyCollection to a Poly3DCollection object."""
  581. segments_3d, codes = paths_to_3d_segments_with_codes(col.get_paths(),
  582. zs, zdir)
  583. col.__class__ = Poly3DCollection
  584. col.set_verts_and_codes(segments_3d, codes)
  585. col.set_3d_properties()
  586. def juggle_axes(xs, ys, zs, zdir):
  587. """
  588. Reorder coordinates so that 2D xs, ys can be plotted in the plane
  589. orthogonal to zdir. zdir is normally x, y or z. However, if zdir
  590. starts with a '-' it is interpreted as a compensation for rotate_axes.
  591. """
  592. if zdir == 'x':
  593. return zs, xs, ys
  594. elif zdir == 'y':
  595. return xs, zs, ys
  596. elif zdir[0] == '-':
  597. return rotate_axes(xs, ys, zs, zdir)
  598. else:
  599. return xs, ys, zs
  600. def rotate_axes(xs, ys, zs, zdir):
  601. """
  602. Reorder coordinates so that the axes are rotated with zdir along
  603. the original z axis. Prepending the axis with a '-' does the
  604. inverse transform, so zdir can be x, -x, y, -y, z or -z
  605. """
  606. if zdir == 'x':
  607. return ys, zs, xs
  608. elif zdir == '-x':
  609. return zs, xs, ys
  610. elif zdir == 'y':
  611. return zs, xs, ys
  612. elif zdir == '-y':
  613. return ys, zs, xs
  614. else:
  615. return xs, ys, zs
  616. def get_colors(c, num):
  617. """Stretch the color argument to provide the required number num"""
  618. return _backports.broadcast_to(
  619. mcolors.to_rgba_array(c) if len(c) else [0, 0, 0, 0],
  620. (num, 4))
  621. def zalpha(colors, zs):
  622. """Modify the alphas of the color list according to depth"""
  623. # FIXME: This only works well if the points for *zs* are well-spaced
  624. # in all three dimensions. Otherwise, at certain orientations,
  625. # the min and max zs are very close together.
  626. # Should really normalize against the viewing depth.
  627. colors = get_colors(colors, len(zs))
  628. if len(zs):
  629. norm = Normalize(min(zs), max(zs))
  630. sats = 1 - norm(zs) * 0.7
  631. colors = [(c[0], c[1], c[2], c[3] * s) for c, s in zip(colors, sats)]
  632. return colors