art3d.py 49 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427
  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 contextlib import contextmanager
  11. from matplotlib import (
  12. _api, artist, cbook, colors as mcolors, lines, text as mtext,
  13. path as mpath)
  14. from matplotlib.collections import (
  15. Collection, LineCollection, PolyCollection, PatchCollection, PathCollection)
  16. from matplotlib.colors import Normalize
  17. from matplotlib.patches import Patch
  18. from . import proj3d
  19. def _norm_angle(a):
  20. """Return the given angle normalized to -180 < *a* <= 180 degrees."""
  21. a = (a + 360) % 360
  22. if a > 180:
  23. a = a - 360
  24. return a
  25. def _norm_text_angle(a):
  26. """Return the given angle normalized to -90 < *a* <= 90 degrees."""
  27. a = (a + 180) % 180
  28. if a > 90:
  29. a = a - 180
  30. return a
  31. def get_dir_vector(zdir):
  32. """
  33. Return a direction vector.
  34. Parameters
  35. ----------
  36. zdir : {'x', 'y', 'z', None, 3-tuple}
  37. The direction. Possible values are:
  38. - 'x': equivalent to (1, 0, 0)
  39. - 'y': equivalent to (0, 1, 0)
  40. - 'z': equivalent to (0, 0, 1)
  41. - *None*: equivalent to (0, 0, 0)
  42. - an iterable (x, y, z) is converted to an array
  43. Returns
  44. -------
  45. x, y, z : array
  46. The direction vector.
  47. """
  48. if zdir == 'x':
  49. return np.array((1, 0, 0))
  50. elif zdir == 'y':
  51. return np.array((0, 1, 0))
  52. elif zdir == 'z':
  53. return np.array((0, 0, 1))
  54. elif zdir is None:
  55. return np.array((0, 0, 0))
  56. elif np.iterable(zdir) and len(zdir) == 3:
  57. return np.array(zdir)
  58. else:
  59. raise ValueError("'x', 'y', 'z', None or vector of length 3 expected")
  60. def _viewlim_mask(xs, ys, zs, axes):
  61. """
  62. Return original points with points outside the axes view limits masked.
  63. Parameters
  64. ----------
  65. xs, ys, zs : array-like
  66. The points to mask.
  67. axes : Axes3D
  68. The axes to use for the view limits.
  69. Returns
  70. -------
  71. xs_masked, ys_masked, zs_masked : np.ma.array
  72. The masked points.
  73. """
  74. mask = np.logical_or.reduce((xs < axes.xy_viewLim.xmin,
  75. xs > axes.xy_viewLim.xmax,
  76. ys < axes.xy_viewLim.ymin,
  77. ys > axes.xy_viewLim.ymax,
  78. zs < axes.zz_viewLim.xmin,
  79. zs > axes.zz_viewLim.xmax))
  80. xs_masked = np.ma.array(xs, mask=mask)
  81. ys_masked = np.ma.array(ys, mask=mask)
  82. zs_masked = np.ma.array(zs, mask=mask)
  83. return xs_masked, ys_masked, zs_masked
  84. class Text3D(mtext.Text):
  85. """
  86. Text object with 3D position and direction.
  87. Parameters
  88. ----------
  89. x, y, z : float
  90. The position of the text.
  91. text : str
  92. The text string to display.
  93. zdir : {'x', 'y', 'z', None, 3-tuple}
  94. The direction of the text. See `.get_dir_vector` for a description of
  95. the values.
  96. axlim_clip : bool, default: False
  97. Whether to hide text outside the axes view limits.
  98. Other Parameters
  99. ----------------
  100. **kwargs
  101. All other parameters are passed on to `~matplotlib.text.Text`.
  102. """
  103. def __init__(self, x=0, y=0, z=0, text='', zdir='z', axlim_clip=False,
  104. **kwargs):
  105. mtext.Text.__init__(self, x, y, text, **kwargs)
  106. self.set_3d_properties(z, zdir, axlim_clip)
  107. def get_position_3d(self):
  108. """Return the (x, y, z) position of the text."""
  109. return self._x, self._y, self._z
  110. def set_position_3d(self, xyz, zdir=None):
  111. """
  112. Set the (*x*, *y*, *z*) position of the text.
  113. Parameters
  114. ----------
  115. xyz : (float, float, float)
  116. The position in 3D space.
  117. zdir : {'x', 'y', 'z', None, 3-tuple}
  118. The direction of the text. If unspecified, the *zdir* will not be
  119. changed. See `.get_dir_vector` for a description of the values.
  120. """
  121. super().set_position(xyz[:2])
  122. self.set_z(xyz[2])
  123. if zdir is not None:
  124. self._dir_vec = get_dir_vector(zdir)
  125. def set_z(self, z):
  126. """
  127. Set the *z* position of the text.
  128. Parameters
  129. ----------
  130. z : float
  131. """
  132. self._z = z
  133. self.stale = True
  134. def set_3d_properties(self, z=0, zdir='z', axlim_clip=False):
  135. """
  136. Set the *z* position and direction of the text.
  137. Parameters
  138. ----------
  139. z : float
  140. The z-position in 3D space.
  141. zdir : {'x', 'y', 'z', 3-tuple}
  142. The direction of the text. Default: 'z'.
  143. See `.get_dir_vector` for a description of the values.
  144. axlim_clip : bool, default: False
  145. Whether to hide text outside the axes view limits.
  146. """
  147. self._z = z
  148. self._dir_vec = get_dir_vector(zdir)
  149. self._axlim_clip = axlim_clip
  150. self.stale = True
  151. @artist.allow_rasterization
  152. def draw(self, renderer):
  153. if self._axlim_clip:
  154. xs, ys, zs = _viewlim_mask(self._x, self._y, self._z, self.axes)
  155. position3d = np.ma.row_stack((xs, ys, zs)).ravel().filled(np.nan)
  156. else:
  157. xs, ys, zs = self._x, self._y, self._z
  158. position3d = np.asanyarray([xs, ys, zs])
  159. proj = proj3d._proj_trans_points(
  160. [position3d, position3d + self._dir_vec], self.axes.M)
  161. dx = proj[0][1] - proj[0][0]
  162. dy = proj[1][1] - proj[1][0]
  163. angle = math.degrees(math.atan2(dy, dx))
  164. with cbook._setattr_cm(self, _x=proj[0][0], _y=proj[1][0],
  165. _rotation=_norm_text_angle(angle)):
  166. mtext.Text.draw(self, renderer)
  167. self.stale = False
  168. def get_tightbbox(self, renderer=None):
  169. # Overwriting the 2d Text behavior which is not valid for 3d.
  170. # For now, just return None to exclude from layout calculation.
  171. return None
  172. def text_2d_to_3d(obj, z=0, zdir='z', axlim_clip=False):
  173. """
  174. Convert a `.Text` to a `.Text3D` object.
  175. Parameters
  176. ----------
  177. z : float
  178. The z-position in 3D space.
  179. zdir : {'x', 'y', 'z', 3-tuple}
  180. The direction of the text. Default: 'z'.
  181. See `.get_dir_vector` for a description of the values.
  182. axlim_clip : bool, default: False
  183. Whether to hide text outside the axes view limits.
  184. """
  185. obj.__class__ = Text3D
  186. obj.set_3d_properties(z, zdir, axlim_clip)
  187. class Line3D(lines.Line2D):
  188. """
  189. 3D line object.
  190. .. note:: Use `get_data_3d` to obtain the data associated with the line.
  191. `~.Line2D.get_data`, `~.Line2D.get_xdata`, and `~.Line2D.get_ydata` return
  192. the x- and y-coordinates of the projected 2D-line, not the x- and y-data of
  193. the 3D-line. Similarly, use `set_data_3d` to set the data, not
  194. `~.Line2D.set_data`, `~.Line2D.set_xdata`, and `~.Line2D.set_ydata`.
  195. """
  196. def __init__(self, xs, ys, zs, *args, axlim_clip=False, **kwargs):
  197. """
  198. Parameters
  199. ----------
  200. xs : array-like
  201. The x-data to be plotted.
  202. ys : array-like
  203. The y-data to be plotted.
  204. zs : array-like
  205. The z-data to be plotted.
  206. *args, **kwargs
  207. Additional arguments are passed to `~matplotlib.lines.Line2D`.
  208. """
  209. super().__init__([], [], *args, **kwargs)
  210. self.set_data_3d(xs, ys, zs)
  211. self._axlim_clip = axlim_clip
  212. def set_3d_properties(self, zs=0, zdir='z', axlim_clip=False):
  213. """
  214. Set the *z* position and direction of the line.
  215. Parameters
  216. ----------
  217. zs : float or array of floats
  218. The location along the *zdir* axis in 3D space to position the
  219. line.
  220. zdir : {'x', 'y', 'z'}
  221. Plane to plot line orthogonal to. Default: 'z'.
  222. See `.get_dir_vector` for a description of the values.
  223. axlim_clip : bool, default: False
  224. Whether to hide lines with an endpoint outside the axes view limits.
  225. """
  226. xs = self.get_xdata()
  227. ys = self.get_ydata()
  228. zs = cbook._to_unmasked_float_array(zs).ravel()
  229. zs = np.broadcast_to(zs, len(xs))
  230. self._verts3d = juggle_axes(xs, ys, zs, zdir)
  231. self._axlim_clip = axlim_clip
  232. self.stale = True
  233. def set_data_3d(self, *args):
  234. """
  235. Set the x, y and z data
  236. Parameters
  237. ----------
  238. x : array-like
  239. The x-data to be plotted.
  240. y : array-like
  241. The y-data to be plotted.
  242. z : array-like
  243. The z-data to be plotted.
  244. Notes
  245. -----
  246. Accepts x, y, z arguments or a single array-like (x, y, z)
  247. """
  248. if len(args) == 1:
  249. args = args[0]
  250. for name, xyz in zip('xyz', args):
  251. if not np.iterable(xyz):
  252. raise RuntimeError(f'{name} must be a sequence')
  253. self._verts3d = args
  254. self.stale = True
  255. def get_data_3d(self):
  256. """
  257. Get the current data
  258. Returns
  259. -------
  260. verts3d : length-3 tuple or array-like
  261. The current data as a tuple or array-like.
  262. """
  263. return self._verts3d
  264. @artist.allow_rasterization
  265. def draw(self, renderer):
  266. if self._axlim_clip:
  267. xs3d, ys3d, zs3d = _viewlim_mask(*self._verts3d, self.axes)
  268. else:
  269. xs3d, ys3d, zs3d = self._verts3d
  270. xs, ys, zs, tis = proj3d._proj_transform_clip(xs3d, ys3d, zs3d,
  271. self.axes.M,
  272. self.axes._focal_length)
  273. self.set_data(xs, ys)
  274. super().draw(renderer)
  275. self.stale = False
  276. def line_2d_to_3d(line, zs=0, zdir='z', axlim_clip=False):
  277. """
  278. Convert a `.Line2D` to a `.Line3D` object.
  279. Parameters
  280. ----------
  281. zs : float
  282. The location along the *zdir* axis in 3D space to position the line.
  283. zdir : {'x', 'y', 'z'}
  284. Plane to plot line orthogonal to. Default: 'z'.
  285. See `.get_dir_vector` for a description of the values.
  286. axlim_clip : bool, default: False
  287. Whether to hide lines with an endpoint outside the axes view limits.
  288. """
  289. line.__class__ = Line3D
  290. line.set_3d_properties(zs, zdir, axlim_clip)
  291. def _path_to_3d_segment(path, zs=0, zdir='z'):
  292. """Convert a path to a 3D segment."""
  293. zs = np.broadcast_to(zs, len(path))
  294. pathsegs = path.iter_segments(simplify=False, curves=False)
  295. seg = [(x, y, z) for (((x, y), code), z) in zip(pathsegs, zs)]
  296. seg3d = [juggle_axes(x, y, z, zdir) for (x, y, z) in seg]
  297. return seg3d
  298. def _paths_to_3d_segments(paths, zs=0, zdir='z'):
  299. """Convert paths from a collection object to 3D segments."""
  300. if not np.iterable(zs):
  301. zs = np.broadcast_to(zs, len(paths))
  302. else:
  303. if len(zs) != len(paths):
  304. raise ValueError('Number of z-coordinates does not match paths.')
  305. segs = [_path_to_3d_segment(path, pathz, zdir)
  306. for path, pathz in zip(paths, zs)]
  307. return segs
  308. def _path_to_3d_segment_with_codes(path, zs=0, zdir='z'):
  309. """Convert a path to a 3D segment with path codes."""
  310. zs = np.broadcast_to(zs, len(path))
  311. pathsegs = path.iter_segments(simplify=False, curves=False)
  312. seg_codes = [((x, y, z), code) for ((x, y), code), z in zip(pathsegs, zs)]
  313. if seg_codes:
  314. seg, codes = zip(*seg_codes)
  315. seg3d = [juggle_axes(x, y, z, zdir) for (x, y, z) in seg]
  316. else:
  317. seg3d = []
  318. codes = []
  319. return seg3d, list(codes)
  320. def _paths_to_3d_segments_with_codes(paths, zs=0, zdir='z'):
  321. """
  322. Convert paths from a collection object to 3D segments with path codes.
  323. """
  324. zs = np.broadcast_to(zs, len(paths))
  325. segments_codes = [_path_to_3d_segment_with_codes(path, pathz, zdir)
  326. for path, pathz in zip(paths, zs)]
  327. if segments_codes:
  328. segments, codes = zip(*segments_codes)
  329. else:
  330. segments, codes = [], []
  331. return list(segments), list(codes)
  332. class Collection3D(Collection):
  333. """A collection of 3D paths."""
  334. def do_3d_projection(self):
  335. """Project the points according to renderer matrix."""
  336. vs_list = [vs for vs, _ in self._3dverts_codes]
  337. if self._axlim_clip:
  338. vs_list = [np.ma.row_stack(_viewlim_mask(*vs.T, self.axes)).T
  339. for vs in vs_list]
  340. xyzs_list = [proj3d.proj_transform(*vs.T, self.axes.M) for vs in vs_list]
  341. self._paths = [mpath.Path(np.ma.column_stack([xs, ys]), cs)
  342. for (xs, ys, _), (_, cs) in zip(xyzs_list, self._3dverts_codes)]
  343. zs = np.concatenate([zs for _, _, zs in xyzs_list])
  344. return zs.min() if len(zs) else 1e9
  345. def collection_2d_to_3d(col, zs=0, zdir='z', axlim_clip=False):
  346. """Convert a `.Collection` to a `.Collection3D` object."""
  347. zs = np.broadcast_to(zs, len(col.get_paths()))
  348. col._3dverts_codes = [
  349. (np.column_stack(juggle_axes(
  350. *np.column_stack([p.vertices, np.broadcast_to(z, len(p.vertices))]).T,
  351. zdir)),
  352. p.codes)
  353. for p, z in zip(col.get_paths(), zs)]
  354. col.__class__ = cbook._make_class_factory(Collection3D, "{}3D")(type(col))
  355. col._axlim_clip = axlim_clip
  356. class Line3DCollection(LineCollection):
  357. """
  358. A collection of 3D lines.
  359. """
  360. def __init__(self, lines, axlim_clip=False, **kwargs):
  361. super().__init__(lines, **kwargs)
  362. self._axlim_clip = axlim_clip
  363. def set_sort_zpos(self, val):
  364. """Set the position to use for z-sorting."""
  365. self._sort_zpos = val
  366. self.stale = True
  367. def set_segments(self, segments):
  368. """
  369. Set 3D segments.
  370. """
  371. self._segments3d = segments
  372. super().set_segments([])
  373. def do_3d_projection(self):
  374. """
  375. Project the points according to renderer matrix.
  376. """
  377. segments = self._segments3d
  378. if self._axlim_clip:
  379. all_points = np.ma.vstack(segments)
  380. masked_points = np.ma.column_stack([*_viewlim_mask(*all_points.T,
  381. self.axes)])
  382. segment_lengths = [np.shape(segment)[0] for segment in segments]
  383. segments = np.split(masked_points, np.cumsum(segment_lengths[:-1]))
  384. xyslist = [proj3d._proj_trans_points(points, self.axes.M)
  385. for points in segments]
  386. segments_2d = [np.ma.column_stack([xs, ys]) for xs, ys, zs in xyslist]
  387. LineCollection.set_segments(self, segments_2d)
  388. # FIXME
  389. minz = 1e9
  390. for xs, ys, zs in xyslist:
  391. minz = min(minz, min(zs))
  392. return minz
  393. def line_collection_2d_to_3d(col, zs=0, zdir='z', axlim_clip=False):
  394. """Convert a `.LineCollection` to a `.Line3DCollection` object."""
  395. segments3d = _paths_to_3d_segments(col.get_paths(), zs, zdir)
  396. col.__class__ = Line3DCollection
  397. col.set_segments(segments3d)
  398. col._axlim_clip = axlim_clip
  399. class Patch3D(Patch):
  400. """
  401. 3D patch object.
  402. """
  403. def __init__(self, *args, zs=(), zdir='z', axlim_clip=False, **kwargs):
  404. """
  405. Parameters
  406. ----------
  407. verts :
  408. zs : float
  409. The location along the *zdir* axis in 3D space to position the
  410. patch.
  411. zdir : {'x', 'y', 'z'}
  412. Plane to plot patch orthogonal to. Default: 'z'.
  413. See `.get_dir_vector` for a description of the values.
  414. axlim_clip : bool, default: False
  415. Whether to hide patches with a vertex outside the axes view limits.
  416. """
  417. super().__init__(*args, **kwargs)
  418. self.set_3d_properties(zs, zdir, axlim_clip)
  419. def set_3d_properties(self, verts, zs=0, zdir='z', axlim_clip=False):
  420. """
  421. Set the *z* position and direction of the patch.
  422. Parameters
  423. ----------
  424. verts :
  425. zs : float
  426. The location along the *zdir* axis in 3D space to position the
  427. patch.
  428. zdir : {'x', 'y', 'z'}
  429. Plane to plot patch orthogonal to. Default: 'z'.
  430. See `.get_dir_vector` for a description of the values.
  431. axlim_clip : bool, default: False
  432. Whether to hide patches with a vertex outside the axes view limits.
  433. """
  434. zs = np.broadcast_to(zs, len(verts))
  435. self._segment3d = [juggle_axes(x, y, z, zdir)
  436. for ((x, y), z) in zip(verts, zs)]
  437. self._axlim_clip = axlim_clip
  438. def get_path(self):
  439. # docstring inherited
  440. # self._path2d is not initialized until do_3d_projection
  441. if not hasattr(self, '_path2d'):
  442. self.axes.M = self.axes.get_proj()
  443. self.do_3d_projection()
  444. return self._path2d
  445. def do_3d_projection(self):
  446. s = self._segment3d
  447. if self._axlim_clip:
  448. xs, ys, zs = _viewlim_mask(*zip(*s), self.axes)
  449. else:
  450. xs, ys, zs = zip(*s)
  451. vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs,
  452. self.axes.M,
  453. self.axes._focal_length)
  454. self._path2d = mpath.Path(np.ma.column_stack([vxs, vys]))
  455. return min(vzs)
  456. class PathPatch3D(Patch3D):
  457. """
  458. 3D PathPatch object.
  459. """
  460. def __init__(self, path, *, zs=(), zdir='z', axlim_clip=False, **kwargs):
  461. """
  462. Parameters
  463. ----------
  464. path :
  465. zs : float
  466. The location along the *zdir* axis in 3D space to position the
  467. path patch.
  468. zdir : {'x', 'y', 'z', 3-tuple}
  469. Plane to plot path patch orthogonal to. Default: 'z'.
  470. See `.get_dir_vector` for a description of the values.
  471. axlim_clip : bool, default: False
  472. Whether to hide path patches with a point outside the axes view limits.
  473. """
  474. # Not super().__init__!
  475. Patch.__init__(self, **kwargs)
  476. self.set_3d_properties(path, zs, zdir, axlim_clip)
  477. def set_3d_properties(self, path, zs=0, zdir='z', axlim_clip=False):
  478. """
  479. Set the *z* position and direction of the path patch.
  480. Parameters
  481. ----------
  482. path :
  483. zs : float
  484. The location along the *zdir* axis in 3D space to position the
  485. path patch.
  486. zdir : {'x', 'y', 'z', 3-tuple}
  487. Plane to plot path patch orthogonal to. Default: 'z'.
  488. See `.get_dir_vector` for a description of the values.
  489. axlim_clip : bool, default: False
  490. Whether to hide path patches with a point outside the axes view limits.
  491. """
  492. Patch3D.set_3d_properties(self, path.vertices, zs=zs, zdir=zdir,
  493. axlim_clip=axlim_clip)
  494. self._code3d = path.codes
  495. def do_3d_projection(self):
  496. s = self._segment3d
  497. if self._axlim_clip:
  498. xs, ys, zs = _viewlim_mask(*zip(*s), self.axes)
  499. else:
  500. xs, ys, zs = zip(*s)
  501. vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs,
  502. self.axes.M,
  503. self.axes._focal_length)
  504. self._path2d = mpath.Path(np.ma.column_stack([vxs, vys]), self._code3d)
  505. return min(vzs)
  506. def _get_patch_verts(patch):
  507. """Return a list of vertices for the path of a patch."""
  508. trans = patch.get_patch_transform()
  509. path = patch.get_path()
  510. polygons = path.to_polygons(trans)
  511. return polygons[0] if len(polygons) else np.array([])
  512. def patch_2d_to_3d(patch, z=0, zdir='z', axlim_clip=False):
  513. """Convert a `.Patch` to a `.Patch3D` object."""
  514. verts = _get_patch_verts(patch)
  515. patch.__class__ = Patch3D
  516. patch.set_3d_properties(verts, z, zdir, axlim_clip)
  517. def pathpatch_2d_to_3d(pathpatch, z=0, zdir='z'):
  518. """Convert a `.PathPatch` to a `.PathPatch3D` object."""
  519. path = pathpatch.get_path()
  520. trans = pathpatch.get_patch_transform()
  521. mpath = trans.transform_path(path)
  522. pathpatch.__class__ = PathPatch3D
  523. pathpatch.set_3d_properties(mpath, z, zdir)
  524. class Patch3DCollection(PatchCollection):
  525. """
  526. A collection of 3D patches.
  527. """
  528. def __init__(self, *args,
  529. zs=0, zdir='z', depthshade=True, axlim_clip=False, **kwargs):
  530. """
  531. Create a collection of flat 3D patches with its normal vector
  532. pointed in *zdir* direction, and located at *zs* on the *zdir*
  533. axis. 'zs' can be a scalar or an array-like of the same length as
  534. the number of patches in the collection.
  535. Constructor arguments are the same as for
  536. :class:`~matplotlib.collections.PatchCollection`. In addition,
  537. keywords *zs=0* and *zdir='z'* are available.
  538. Also, the keyword argument *depthshade* is available to indicate
  539. whether to shade the patches in order to give the appearance of depth
  540. (default is *True*). This is typically desired in scatter plots.
  541. """
  542. self._depthshade = depthshade
  543. super().__init__(*args, **kwargs)
  544. self.set_3d_properties(zs, zdir, axlim_clip)
  545. def get_depthshade(self):
  546. return self._depthshade
  547. def set_depthshade(self, depthshade):
  548. """
  549. Set whether depth shading is performed on collection members.
  550. Parameters
  551. ----------
  552. depthshade : bool
  553. Whether to shade the patches in order to give the appearance of
  554. depth.
  555. """
  556. self._depthshade = depthshade
  557. self.stale = True
  558. def set_sort_zpos(self, val):
  559. """Set the position to use for z-sorting."""
  560. self._sort_zpos = val
  561. self.stale = True
  562. def set_3d_properties(self, zs, zdir, axlim_clip=False):
  563. """
  564. Set the *z* positions and direction of the patches.
  565. Parameters
  566. ----------
  567. zs : float or array of floats
  568. The location or locations to place the patches in the collection
  569. along the *zdir* axis.
  570. zdir : {'x', 'y', 'z'}
  571. Plane to plot patches orthogonal to.
  572. All patches must have the same direction.
  573. See `.get_dir_vector` for a description of the values.
  574. axlim_clip : bool, default: False
  575. Whether to hide patches with a vertex outside the axes view limits.
  576. """
  577. # Force the collection to initialize the face and edgecolors
  578. # just in case it is a scalarmappable with a colormap.
  579. self.update_scalarmappable()
  580. offsets = self.get_offsets()
  581. if len(offsets) > 0:
  582. xs, ys = offsets.T
  583. else:
  584. xs = []
  585. ys = []
  586. self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir)
  587. self._z_markers_idx = slice(-1)
  588. self._vzs = None
  589. self._axlim_clip = axlim_clip
  590. self.stale = True
  591. def do_3d_projection(self):
  592. if self._axlim_clip:
  593. xs, ys, zs = _viewlim_mask(*self._offsets3d, self.axes)
  594. else:
  595. xs, ys, zs = self._offsets3d
  596. vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs,
  597. self.axes.M,
  598. self.axes._focal_length)
  599. self._vzs = vzs
  600. super().set_offsets(np.ma.column_stack([vxs, vys]))
  601. if vzs.size > 0:
  602. return min(vzs)
  603. else:
  604. return np.nan
  605. def _maybe_depth_shade_and_sort_colors(self, color_array):
  606. color_array = (
  607. _zalpha(color_array, self._vzs)
  608. if self._vzs is not None and self._depthshade
  609. else color_array
  610. )
  611. if len(color_array) > 1:
  612. color_array = color_array[self._z_markers_idx]
  613. return mcolors.to_rgba_array(color_array, self._alpha)
  614. def get_facecolor(self):
  615. return self._maybe_depth_shade_and_sort_colors(super().get_facecolor())
  616. def get_edgecolor(self):
  617. # We need this check here to make sure we do not double-apply the depth
  618. # based alpha shading when the edge color is "face" which means the
  619. # edge colour should be identical to the face colour.
  620. if cbook._str_equal(self._edgecolors, 'face'):
  621. return self.get_facecolor()
  622. return self._maybe_depth_shade_and_sort_colors(super().get_edgecolor())
  623. class Path3DCollection(PathCollection):
  624. """
  625. A collection of 3D paths.
  626. """
  627. def __init__(self, *args,
  628. zs=0, zdir='z', depthshade=True, axlim_clip=False, **kwargs):
  629. """
  630. Create a collection of flat 3D paths with its normal vector
  631. pointed in *zdir* direction, and located at *zs* on the *zdir*
  632. axis. 'zs' can be a scalar or an array-like of the same length as
  633. the number of paths in the collection.
  634. Constructor arguments are the same as for
  635. :class:`~matplotlib.collections.PathCollection`. In addition,
  636. keywords *zs=0* and *zdir='z'* are available.
  637. Also, the keyword argument *depthshade* is available to indicate
  638. whether to shade the patches in order to give the appearance of depth
  639. (default is *True*). This is typically desired in scatter plots.
  640. """
  641. self._depthshade = depthshade
  642. self._in_draw = False
  643. super().__init__(*args, **kwargs)
  644. self.set_3d_properties(zs, zdir, axlim_clip)
  645. self._offset_zordered = None
  646. def draw(self, renderer):
  647. with self._use_zordered_offset():
  648. with cbook._setattr_cm(self, _in_draw=True):
  649. super().draw(renderer)
  650. def set_sort_zpos(self, val):
  651. """Set the position to use for z-sorting."""
  652. self._sort_zpos = val
  653. self.stale = True
  654. def set_3d_properties(self, zs, zdir, axlim_clip=False):
  655. """
  656. Set the *z* positions and direction of the paths.
  657. Parameters
  658. ----------
  659. zs : float or array of floats
  660. The location or locations to place the paths in the collection
  661. along the *zdir* axis.
  662. zdir : {'x', 'y', 'z'}
  663. Plane to plot paths orthogonal to.
  664. All paths must have the same direction.
  665. See `.get_dir_vector` for a description of the values.
  666. axlim_clip : bool, default: False
  667. Whether to hide paths with a vertex outside the axes view limits.
  668. """
  669. # Force the collection to initialize the face and edgecolors
  670. # just in case it is a scalarmappable with a colormap.
  671. self.update_scalarmappable()
  672. offsets = self.get_offsets()
  673. if len(offsets) > 0:
  674. xs, ys = offsets.T
  675. else:
  676. xs = []
  677. ys = []
  678. self._zdir = zdir
  679. self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir)
  680. # In the base draw methods we access the attributes directly which
  681. # means we cannot resolve the shuffling in the getter methods like
  682. # we do for the edge and face colors.
  683. #
  684. # This means we need to carry around a cache of the unsorted sizes and
  685. # widths (postfixed with 3d) and in `do_3d_projection` set the
  686. # depth-sorted version of that data into the private state used by the
  687. # base collection class in its draw method.
  688. #
  689. # Grab the current sizes and linewidths to preserve them.
  690. self._sizes3d = self._sizes
  691. self._linewidths3d = np.array(self._linewidths)
  692. xs, ys, zs = self._offsets3d
  693. # Sort the points based on z coordinates
  694. # Performance optimization: Create a sorted index array and reorder
  695. # points and point properties according to the index array
  696. self._z_markers_idx = slice(-1)
  697. self._vzs = None
  698. self._axlim_clip = axlim_clip
  699. self.stale = True
  700. def set_sizes(self, sizes, dpi=72.0):
  701. super().set_sizes(sizes, dpi)
  702. if not self._in_draw:
  703. self._sizes3d = sizes
  704. def set_linewidth(self, lw):
  705. super().set_linewidth(lw)
  706. if not self._in_draw:
  707. self._linewidths3d = np.array(self._linewidths)
  708. def get_depthshade(self):
  709. return self._depthshade
  710. def set_depthshade(self, depthshade):
  711. """
  712. Set whether depth shading is performed on collection members.
  713. Parameters
  714. ----------
  715. depthshade : bool
  716. Whether to shade the patches in order to give the appearance of
  717. depth.
  718. """
  719. self._depthshade = depthshade
  720. self.stale = True
  721. def do_3d_projection(self):
  722. if self._axlim_clip:
  723. xs, ys, zs = _viewlim_mask(*self._offsets3d, self.axes)
  724. else:
  725. xs, ys, zs = self._offsets3d
  726. vxs, vys, vzs, vis = proj3d._proj_transform_clip(xs, ys, zs,
  727. self.axes.M,
  728. self.axes._focal_length)
  729. # Sort the points based on z coordinates
  730. # Performance optimization: Create a sorted index array and reorder
  731. # points and point properties according to the index array
  732. z_markers_idx = self._z_markers_idx = np.ma.argsort(vzs)[::-1]
  733. self._vzs = vzs
  734. # we have to special case the sizes because of code in collections.py
  735. # as the draw method does
  736. # self.set_sizes(self._sizes, self.figure.dpi)
  737. # so we cannot rely on doing the sorting on the way out via get_*
  738. if len(self._sizes3d) > 1:
  739. self._sizes = self._sizes3d[z_markers_idx]
  740. if len(self._linewidths3d) > 1:
  741. self._linewidths = self._linewidths3d[z_markers_idx]
  742. PathCollection.set_offsets(self, np.ma.column_stack((vxs, vys)))
  743. # Re-order items
  744. vzs = vzs[z_markers_idx]
  745. vxs = vxs[z_markers_idx]
  746. vys = vys[z_markers_idx]
  747. # Store ordered offset for drawing purpose
  748. self._offset_zordered = np.ma.column_stack((vxs, vys))
  749. return np.min(vzs) if vzs.size else np.nan
  750. @contextmanager
  751. def _use_zordered_offset(self):
  752. if self._offset_zordered is None:
  753. # Do nothing
  754. yield
  755. else:
  756. # Swap offset with z-ordered offset
  757. old_offset = self._offsets
  758. super().set_offsets(self._offset_zordered)
  759. try:
  760. yield
  761. finally:
  762. self._offsets = old_offset
  763. def _maybe_depth_shade_and_sort_colors(self, color_array):
  764. color_array = (
  765. _zalpha(color_array, self._vzs)
  766. if self._vzs is not None and self._depthshade
  767. else color_array
  768. )
  769. if len(color_array) > 1:
  770. color_array = color_array[self._z_markers_idx]
  771. return mcolors.to_rgba_array(color_array, self._alpha)
  772. def get_facecolor(self):
  773. return self._maybe_depth_shade_and_sort_colors(super().get_facecolor())
  774. def get_edgecolor(self):
  775. # We need this check here to make sure we do not double-apply the depth
  776. # based alpha shading when the edge color is "face" which means the
  777. # edge colour should be identical to the face colour.
  778. if cbook._str_equal(self._edgecolors, 'face'):
  779. return self.get_facecolor()
  780. return self._maybe_depth_shade_and_sort_colors(super().get_edgecolor())
  781. def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True, axlim_clip=False):
  782. """
  783. Convert a `.PatchCollection` into a `.Patch3DCollection` object
  784. (or a `.PathCollection` into a `.Path3DCollection` object).
  785. Parameters
  786. ----------
  787. col : `~matplotlib.collections.PatchCollection` or \
  788. `~matplotlib.collections.PathCollection`
  789. The collection to convert.
  790. zs : float or array of floats
  791. The location or locations to place the patches in the collection along
  792. the *zdir* axis. Default: 0.
  793. zdir : {'x', 'y', 'z'}
  794. The axis in which to place the patches. Default: "z".
  795. See `.get_dir_vector` for a description of the values.
  796. depthshade : bool, default: True
  797. Whether to shade the patches to give a sense of depth.
  798. axlim_clip : bool, default: False
  799. Whether to hide patches with a vertex outside the axes view limits.
  800. """
  801. if isinstance(col, PathCollection):
  802. col.__class__ = Path3DCollection
  803. col._offset_zordered = None
  804. elif isinstance(col, PatchCollection):
  805. col.__class__ = Patch3DCollection
  806. col._depthshade = depthshade
  807. col._in_draw = False
  808. col.set_3d_properties(zs, zdir, axlim_clip)
  809. class Poly3DCollection(PolyCollection):
  810. """
  811. A collection of 3D polygons.
  812. .. note::
  813. **Filling of 3D polygons**
  814. There is no simple definition of the enclosed surface of a 3D polygon
  815. unless the polygon is planar.
  816. In practice, Matplotlib fills the 2D projection of the polygon. This
  817. gives a correct filling appearance only for planar polygons. For all
  818. other polygons, you'll find orientations in which the edges of the
  819. polygon intersect in the projection. This will lead to an incorrect
  820. visualization of the 3D area.
  821. If you need filled areas, it is recommended to create them via
  822. `~mpl_toolkits.mplot3d.axes3d.Axes3D.plot_trisurf`, which creates a
  823. triangulation and thus generates consistent surfaces.
  824. """
  825. def __init__(self, verts, *args, zsort='average', shade=False,
  826. lightsource=None, axlim_clip=False, **kwargs):
  827. """
  828. Parameters
  829. ----------
  830. verts : list of (N, 3) array-like
  831. The sequence of polygons [*verts0*, *verts1*, ...] where each
  832. element *verts_i* defines the vertices of polygon *i* as a 2D
  833. array-like of shape (N, 3).
  834. zsort : {'average', 'min', 'max'}, default: 'average'
  835. The calculation method for the z-order.
  836. See `~.Poly3DCollection.set_zsort` for details.
  837. shade : bool, default: False
  838. Whether to shade *facecolors* and *edgecolors*. When activating
  839. *shade*, *facecolors* and/or *edgecolors* must be provided.
  840. .. versionadded:: 3.7
  841. lightsource : `~matplotlib.colors.LightSource`, optional
  842. The lightsource to use when *shade* is True.
  843. .. versionadded:: 3.7
  844. axlim_clip : bool, default: False
  845. Whether to hide polygons with a vertex outside the view limits.
  846. *args, **kwargs
  847. All other parameters are forwarded to `.PolyCollection`.
  848. Notes
  849. -----
  850. Note that this class does a bit of magic with the _facecolors
  851. and _edgecolors properties.
  852. """
  853. if shade:
  854. normals = _generate_normals(verts)
  855. facecolors = kwargs.get('facecolors', None)
  856. if facecolors is not None:
  857. kwargs['facecolors'] = _shade_colors(
  858. facecolors, normals, lightsource
  859. )
  860. edgecolors = kwargs.get('edgecolors', None)
  861. if edgecolors is not None:
  862. kwargs['edgecolors'] = _shade_colors(
  863. edgecolors, normals, lightsource
  864. )
  865. if facecolors is None and edgecolors is None:
  866. raise ValueError(
  867. "You must provide facecolors, edgecolors, or both for "
  868. "shade to work.")
  869. super().__init__(verts, *args, **kwargs)
  870. if isinstance(verts, np.ndarray):
  871. if verts.ndim != 3:
  872. raise ValueError('verts must be a list of (N, 3) array-like')
  873. else:
  874. if any(len(np.shape(vert)) != 2 for vert in verts):
  875. raise ValueError('verts must be a list of (N, 3) array-like')
  876. self.set_zsort(zsort)
  877. self._codes3d = None
  878. self._axlim_clip = axlim_clip
  879. _zsort_functions = {
  880. 'average': np.average,
  881. 'min': np.min,
  882. 'max': np.max,
  883. }
  884. def set_zsort(self, zsort):
  885. """
  886. Set the calculation method for the z-order.
  887. Parameters
  888. ----------
  889. zsort : {'average', 'min', 'max'}
  890. The function applied on the z-coordinates of the vertices in the
  891. viewer's coordinate system, to determine the z-order.
  892. """
  893. self._zsortfunc = self._zsort_functions[zsort]
  894. self._sort_zpos = None
  895. self.stale = True
  896. @_api.deprecated("3.10")
  897. def get_vector(self, segments3d):
  898. return self._get_vector(segments3d)
  899. def _get_vector(self, segments3d):
  900. """Optimize points for projection."""
  901. if len(segments3d):
  902. xs, ys, zs = np.vstack(segments3d).T
  903. else: # vstack can't stack zero arrays.
  904. xs, ys, zs = [], [], []
  905. ones = np.ones(len(xs))
  906. self._vec = np.array([xs, ys, zs, ones])
  907. indices = [0, *np.cumsum([len(segment) for segment in segments3d])]
  908. self._segslices = [*map(slice, indices[:-1], indices[1:])]
  909. def set_verts(self, verts, closed=True):
  910. """
  911. Set 3D vertices.
  912. Parameters
  913. ----------
  914. verts : list of (N, 3) array-like
  915. The sequence of polygons [*verts0*, *verts1*, ...] where each
  916. element *verts_i* defines the vertices of polygon *i* as a 2D
  917. array-like of shape (N, 3).
  918. closed : bool, default: True
  919. Whether the polygon should be closed by adding a CLOSEPOLY
  920. connection at the end.
  921. """
  922. self._get_vector(verts)
  923. # 2D verts will be updated at draw time
  924. super().set_verts([], False)
  925. self._closed = closed
  926. def set_verts_and_codes(self, verts, codes):
  927. """Set 3D vertices with path codes."""
  928. # set vertices with closed=False to prevent PolyCollection from
  929. # setting path codes
  930. self.set_verts(verts, closed=False)
  931. # and set our own codes instead.
  932. self._codes3d = codes
  933. def set_3d_properties(self, axlim_clip=False):
  934. # Force the collection to initialize the face and edgecolors
  935. # just in case it is a scalarmappable with a colormap.
  936. self.update_scalarmappable()
  937. self._sort_zpos = None
  938. self.set_zsort('average')
  939. self._facecolor3d = PolyCollection.get_facecolor(self)
  940. self._edgecolor3d = PolyCollection.get_edgecolor(self)
  941. self._alpha3d = PolyCollection.get_alpha(self)
  942. self.stale = True
  943. def set_sort_zpos(self, val):
  944. """Set the position to use for z-sorting."""
  945. self._sort_zpos = val
  946. self.stale = True
  947. def do_3d_projection(self):
  948. """
  949. Perform the 3D projection for this object.
  950. """
  951. if self._A is not None:
  952. # force update of color mapping because we re-order them
  953. # below. If we do not do this here, the 2D draw will call
  954. # this, but we will never port the color mapped values back
  955. # to the 3D versions.
  956. #
  957. # We hold the 3D versions in a fixed order (the order the user
  958. # passed in) and sort the 2D version by view depth.
  959. self.update_scalarmappable()
  960. if self._face_is_mapped:
  961. self._facecolor3d = self._facecolors
  962. if self._edge_is_mapped:
  963. self._edgecolor3d = self._edgecolors
  964. if self._axlim_clip:
  965. xs, ys, zs = _viewlim_mask(*self._vec[0:3], self.axes)
  966. if self._vec.shape[0] == 4: # Will be 3 (xyz) or 4 (xyzw)
  967. w_masked = np.ma.masked_where(zs.mask, self._vec[3])
  968. vec = np.ma.array([xs, ys, zs, w_masked])
  969. else:
  970. vec = np.ma.array([xs, ys, zs])
  971. else:
  972. vec = self._vec
  973. txs, tys, tzs = proj3d._proj_transform_vec(vec, self.axes.M)
  974. xyzlist = [(txs[sl], tys[sl], tzs[sl]) for sl in self._segslices]
  975. # This extra fuss is to re-order face / edge colors
  976. cface = self._facecolor3d
  977. cedge = self._edgecolor3d
  978. if len(cface) != len(xyzlist):
  979. cface = cface.repeat(len(xyzlist), axis=0)
  980. if len(cedge) != len(xyzlist):
  981. if len(cedge) == 0:
  982. cedge = cface
  983. else:
  984. cedge = cedge.repeat(len(xyzlist), axis=0)
  985. if xyzlist:
  986. # sort by depth (furthest drawn first)
  987. z_segments_2d = sorted(
  988. ((self._zsortfunc(zs.data), np.ma.column_stack([xs, ys]), fc, ec, idx)
  989. for idx, ((xs, ys, zs), fc, ec)
  990. in enumerate(zip(xyzlist, cface, cedge))),
  991. key=lambda x: x[0], reverse=True)
  992. _, segments_2d, self._facecolors2d, self._edgecolors2d, idxs = \
  993. zip(*z_segments_2d)
  994. else:
  995. segments_2d = []
  996. self._facecolors2d = np.empty((0, 4))
  997. self._edgecolors2d = np.empty((0, 4))
  998. idxs = []
  999. if self._codes3d is not None:
  1000. codes = [self._codes3d[idx] for idx in idxs]
  1001. PolyCollection.set_verts_and_codes(self, segments_2d, codes)
  1002. else:
  1003. PolyCollection.set_verts(self, segments_2d, self._closed)
  1004. if len(self._edgecolor3d) != len(cface):
  1005. self._edgecolors2d = self._edgecolor3d
  1006. # Return zorder value
  1007. if self._sort_zpos is not None:
  1008. zvec = np.array([[0], [0], [self._sort_zpos], [1]])
  1009. ztrans = proj3d._proj_transform_vec(zvec, self.axes.M)
  1010. return ztrans[2][0]
  1011. elif tzs.size > 0:
  1012. # FIXME: Some results still don't look quite right.
  1013. # In particular, examine contourf3d_demo2.py
  1014. # with az = -54 and elev = -45.
  1015. return np.min(tzs)
  1016. else:
  1017. return np.nan
  1018. def set_facecolor(self, colors):
  1019. # docstring inherited
  1020. super().set_facecolor(colors)
  1021. self._facecolor3d = PolyCollection.get_facecolor(self)
  1022. def set_edgecolor(self, colors):
  1023. # docstring inherited
  1024. super().set_edgecolor(colors)
  1025. self._edgecolor3d = PolyCollection.get_edgecolor(self)
  1026. def set_alpha(self, alpha):
  1027. # docstring inherited
  1028. artist.Artist.set_alpha(self, alpha)
  1029. try:
  1030. self._facecolor3d = mcolors.to_rgba_array(
  1031. self._facecolor3d, self._alpha)
  1032. except (AttributeError, TypeError, IndexError):
  1033. pass
  1034. try:
  1035. self._edgecolors = mcolors.to_rgba_array(
  1036. self._edgecolor3d, self._alpha)
  1037. except (AttributeError, TypeError, IndexError):
  1038. pass
  1039. self.stale = True
  1040. def get_facecolor(self):
  1041. # docstring inherited
  1042. # self._facecolors2d is not initialized until do_3d_projection
  1043. if not hasattr(self, '_facecolors2d'):
  1044. self.axes.M = self.axes.get_proj()
  1045. self.do_3d_projection()
  1046. return np.asarray(self._facecolors2d)
  1047. def get_edgecolor(self):
  1048. # docstring inherited
  1049. # self._edgecolors2d is not initialized until do_3d_projection
  1050. if not hasattr(self, '_edgecolors2d'):
  1051. self.axes.M = self.axes.get_proj()
  1052. self.do_3d_projection()
  1053. return np.asarray(self._edgecolors2d)
  1054. def poly_collection_2d_to_3d(col, zs=0, zdir='z', axlim_clip=False):
  1055. """
  1056. Convert a `.PolyCollection` into a `.Poly3DCollection` object.
  1057. Parameters
  1058. ----------
  1059. col : `~matplotlib.collections.PolyCollection`
  1060. The collection to convert.
  1061. zs : float or array of floats
  1062. The location or locations to place the polygons in the collection along
  1063. the *zdir* axis. Default: 0.
  1064. zdir : {'x', 'y', 'z'}
  1065. The axis in which to place the patches. Default: 'z'.
  1066. See `.get_dir_vector` for a description of the values.
  1067. """
  1068. segments_3d, codes = _paths_to_3d_segments_with_codes(
  1069. col.get_paths(), zs, zdir)
  1070. col.__class__ = Poly3DCollection
  1071. col.set_verts_and_codes(segments_3d, codes)
  1072. col.set_3d_properties()
  1073. col._axlim_clip = axlim_clip
  1074. def juggle_axes(xs, ys, zs, zdir):
  1075. """
  1076. Reorder coordinates so that 2D *xs*, *ys* can be plotted in the plane
  1077. orthogonal to *zdir*. *zdir* is normally 'x', 'y' or 'z'. However, if
  1078. *zdir* starts with a '-' it is interpreted as a compensation for
  1079. `rotate_axes`.
  1080. """
  1081. if zdir == 'x':
  1082. return zs, xs, ys
  1083. elif zdir == 'y':
  1084. return xs, zs, ys
  1085. elif zdir[0] == '-':
  1086. return rotate_axes(xs, ys, zs, zdir)
  1087. else:
  1088. return xs, ys, zs
  1089. def rotate_axes(xs, ys, zs, zdir):
  1090. """
  1091. Reorder coordinates so that the axes are rotated with *zdir* along
  1092. the original z axis. Prepending the axis with a '-' does the
  1093. inverse transform, so *zdir* can be 'x', '-x', 'y', '-y', 'z' or '-z'.
  1094. """
  1095. if zdir in ('x', '-y'):
  1096. return ys, zs, xs
  1097. elif zdir in ('-x', 'y'):
  1098. return zs, xs, ys
  1099. else:
  1100. return xs, ys, zs
  1101. def _zalpha(colors, zs):
  1102. """Modify the alphas of the color list according to depth."""
  1103. # FIXME: This only works well if the points for *zs* are well-spaced
  1104. # in all three dimensions. Otherwise, at certain orientations,
  1105. # the min and max zs are very close together.
  1106. # Should really normalize against the viewing depth.
  1107. if len(colors) == 0 or len(zs) == 0:
  1108. return np.zeros((0, 4))
  1109. norm = Normalize(min(zs), max(zs))
  1110. sats = 1 - norm(zs) * 0.7
  1111. rgba = np.broadcast_to(mcolors.to_rgba_array(colors), (len(zs), 4))
  1112. return np.column_stack([rgba[:, :3], rgba[:, 3] * sats])
  1113. def _all_points_on_plane(xs, ys, zs, atol=1e-8):
  1114. """
  1115. Check if all points are on the same plane. Note that NaN values are
  1116. ignored.
  1117. Parameters
  1118. ----------
  1119. xs, ys, zs : array-like
  1120. The x, y, and z coordinates of the points.
  1121. atol : float, default: 1e-8
  1122. The tolerance for the equality check.
  1123. """
  1124. xs, ys, zs = np.asarray(xs), np.asarray(ys), np.asarray(zs)
  1125. points = np.column_stack([xs, ys, zs])
  1126. points = points[~np.isnan(points).any(axis=1)]
  1127. # Check for the case where we have less than 3 unique points
  1128. points = np.unique(points, axis=0)
  1129. if len(points) <= 3:
  1130. return True
  1131. # Calculate the vectors from the first point to all other points
  1132. vs = (points - points[0])[1:]
  1133. vs = vs / np.linalg.norm(vs, axis=1)[:, np.newaxis]
  1134. # Filter out parallel vectors
  1135. vs = np.unique(vs, axis=0)
  1136. if len(vs) <= 2:
  1137. return True
  1138. # Filter out parallel and antiparallel vectors to the first vector
  1139. cross_norms = np.linalg.norm(np.cross(vs[0], vs[1:]), axis=1)
  1140. zero_cross_norms = np.where(np.isclose(cross_norms, 0, atol=atol))[0] + 1
  1141. vs = np.delete(vs, zero_cross_norms, axis=0)
  1142. if len(vs) <= 2:
  1143. return True
  1144. # Calculate the normal vector from the first three points
  1145. n = np.cross(vs[0], vs[1])
  1146. n = n / np.linalg.norm(n)
  1147. # If the dot product of the normal vector and all other vectors is zero,
  1148. # all points are on the same plane
  1149. dots = np.dot(n, vs.transpose())
  1150. return np.allclose(dots, 0, atol=atol)
  1151. def _generate_normals(polygons):
  1152. """
  1153. Compute the normals of a list of polygons, one normal per polygon.
  1154. Normals point towards the viewer for a face with its vertices in
  1155. counterclockwise order, following the right hand rule.
  1156. Uses three points equally spaced around the polygon. This method assumes
  1157. that the points are in a plane. Otherwise, more than one shade is required,
  1158. which is not supported.
  1159. Parameters
  1160. ----------
  1161. polygons : list of (M_i, 3) array-like, or (..., M, 3) array-like
  1162. A sequence of polygons to compute normals for, which can have
  1163. varying numbers of vertices. If the polygons all have the same
  1164. number of vertices and array is passed, then the operation will
  1165. be vectorized.
  1166. Returns
  1167. -------
  1168. normals : (..., 3) array
  1169. A normal vector estimated for the polygon.
  1170. """
  1171. if isinstance(polygons, np.ndarray):
  1172. # optimization: polygons all have the same number of points, so can
  1173. # vectorize
  1174. n = polygons.shape[-2]
  1175. i1, i2, i3 = 0, n//3, 2*n//3
  1176. v1 = polygons[..., i1, :] - polygons[..., i2, :]
  1177. v2 = polygons[..., i2, :] - polygons[..., i3, :]
  1178. else:
  1179. # The subtraction doesn't vectorize because polygons is jagged.
  1180. v1 = np.empty((len(polygons), 3))
  1181. v2 = np.empty((len(polygons), 3))
  1182. for poly_i, ps in enumerate(polygons):
  1183. n = len(ps)
  1184. ps = np.asarray(ps)
  1185. i1, i2, i3 = 0, n//3, 2*n//3
  1186. v1[poly_i, :] = ps[i1, :] - ps[i2, :]
  1187. v2[poly_i, :] = ps[i2, :] - ps[i3, :]
  1188. return np.cross(v1, v2)
  1189. def _shade_colors(color, normals, lightsource=None):
  1190. """
  1191. Shade *color* using normal vectors given by *normals*,
  1192. assuming a *lightsource* (using default position if not given).
  1193. *color* can also be an array of the same length as *normals*.
  1194. """
  1195. if lightsource is None:
  1196. # chosen for backwards-compatibility
  1197. lightsource = mcolors.LightSource(azdeg=225, altdeg=19.4712)
  1198. with np.errstate(invalid="ignore"):
  1199. shade = ((normals / np.linalg.norm(normals, axis=1, keepdims=True))
  1200. @ lightsource.direction)
  1201. mask = ~np.isnan(shade)
  1202. if mask.any():
  1203. # convert dot product to allowed shading fractions
  1204. in_norm = mcolors.Normalize(-1, 1)
  1205. out_norm = mcolors.Normalize(0.3, 1).inverse
  1206. def norm(x):
  1207. return out_norm(in_norm(x))
  1208. shade[~mask] = 0
  1209. color = mcolors.to_rgba_array(color)
  1210. # shape of color should be (M, 4) (where M is number of faces)
  1211. # shape of shade should be (M,)
  1212. # colors should have final shape of (M, 4)
  1213. alpha = color[:, 3]
  1214. colors = norm(shade)[:, np.newaxis] * color
  1215. colors[:, 3] = alpha
  1216. else:
  1217. colors = np.asanyarray(color).copy()
  1218. return colors