Coverage for /pythoncovmergedfiles/medio/medio/usr/local/lib/python3.9/dist-packages/mpl_toolkits/mplot3d/art3d.py: 20%

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

502 statements  

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""" 

6Module containing 3D artist code and functions to convert 2D 

7artists into 3D versions which can be added to an Axes3D. 

8""" 

9 

10import math 

11 

12import numpy as np 

13 

14from contextlib import contextmanager 

15 

16from matplotlib import ( 

17 artist, cbook, colors as mcolors, lines, text as mtext, 

18 path as mpath) 

19from matplotlib.collections import ( 

20 Collection, LineCollection, PolyCollection, PatchCollection, PathCollection) 

21from matplotlib.colors import Normalize 

22from matplotlib.patches import Patch 

23from . import proj3d 

24 

25 

26def _norm_angle(a): 

27 """Return the given angle normalized to -180 < *a* <= 180 degrees.""" 

28 a = (a + 360) % 360 

29 if a > 180: 

30 a = a - 360 

31 return a 

32 

33 

34def _norm_text_angle(a): 

35 """Return the given angle normalized to -90 < *a* <= 90 degrees.""" 

36 a = (a + 180) % 180 

37 if a > 90: 

38 a = a - 180 

39 return a 

40 

41 

42def get_dir_vector(zdir): 

43 """ 

44 Return a direction vector. 

45 

46 Parameters 

47 ---------- 

48 zdir : {'x', 'y', 'z', None, 3-tuple} 

49 The direction. Possible values are: 

50 

51 - 'x': equivalent to (1, 0, 0) 

52 - 'y': equivalent to (0, 1, 0) 

53 - 'z': equivalent to (0, 0, 1) 

54 - *None*: equivalent to (0, 0, 0) 

55 - an iterable (x, y, z) is converted to an array 

56 

57 Returns 

58 ------- 

59 x, y, z : array 

60 The direction vector. 

61 """ 

62 if zdir == 'x': 

63 return np.array((1, 0, 0)) 

64 elif zdir == 'y': 

65 return np.array((0, 1, 0)) 

66 elif zdir == 'z': 

67 return np.array((0, 0, 1)) 

68 elif zdir is None: 

69 return np.array((0, 0, 0)) 

70 elif np.iterable(zdir) and len(zdir) == 3: 

71 return np.array(zdir) 

72 else: 

73 raise ValueError("'x', 'y', 'z', None or vector of length 3 expected") 

74 

75 

76class Text3D(mtext.Text): 

77 """ 

78 Text object with 3D position and direction. 

79 

80 Parameters 

81 ---------- 

82 x, y, z : float 

83 The position of the text. 

84 text : str 

85 The text string to display. 

86 zdir : {'x', 'y', 'z', None, 3-tuple} 

87 The direction of the text. See `.get_dir_vector` for a description of 

88 the values. 

89 

90 Other Parameters 

91 ---------------- 

92 **kwargs 

93 All other parameters are passed on to `~matplotlib.text.Text`. 

94 """ 

95 

96 def __init__(self, x=0, y=0, z=0, text='', zdir='z', **kwargs): 

97 mtext.Text.__init__(self, x, y, text, **kwargs) 

98 self.set_3d_properties(z, zdir) 

99 

100 def get_position_3d(self): 

101 """Return the (x, y, z) position of the text.""" 

102 return self._x, self._y, self._z 

103 

104 def set_position_3d(self, xyz, zdir=None): 

105 """ 

106 Set the (*x*, *y*, *z*) position of the text. 

107 

108 Parameters 

109 ---------- 

110 xyz : (float, float, float) 

111 The position in 3D space. 

112 zdir : {'x', 'y', 'z', None, 3-tuple} 

113 The direction of the text. If unspecified, the *zdir* will not be 

114 changed. See `.get_dir_vector` for a description of the values. 

115 """ 

116 super().set_position(xyz[:2]) 

117 self.set_z(xyz[2]) 

118 if zdir is not None: 

119 self._dir_vec = get_dir_vector(zdir) 

120 

121 def set_z(self, z): 

122 """ 

123 Set the *z* position of the text. 

124 

125 Parameters 

126 ---------- 

127 z : float 

128 """ 

129 self._z = z 

130 self.stale = True 

131 

132 def set_3d_properties(self, z=0, zdir='z'): 

133 """ 

134 Set the *z* position and direction of the text. 

135 

136 Parameters 

137 ---------- 

138 z : float 

139 The z-position in 3D space. 

140 zdir : {'x', 'y', 'z', 3-tuple} 

141 The direction of the text. Default: 'z'. 

142 See `.get_dir_vector` for a description of the values. 

143 """ 

144 self._z = z 

145 self._dir_vec = get_dir_vector(zdir) 

146 self.stale = True 

147 

148 @artist.allow_rasterization 

149 def draw(self, renderer): 

150 position3d = np.array((self._x, self._y, self._z)) 

151 proj = proj3d._proj_trans_points( 

152 [position3d, position3d + self._dir_vec], self.axes.M) 

153 dx = proj[0][1] - proj[0][0] 

154 dy = proj[1][1] - proj[1][0] 

155 angle = math.degrees(math.atan2(dy, dx)) 

156 with cbook._setattr_cm(self, _x=proj[0][0], _y=proj[1][0], 

157 _rotation=_norm_text_angle(angle)): 

158 mtext.Text.draw(self, renderer) 

159 self.stale = False 

160 

161 def get_tightbbox(self, renderer=None): 

162 # Overwriting the 2d Text behavior which is not valid for 3d. 

163 # For now, just return None to exclude from layout calculation. 

164 return None 

165 

166 

167def text_2d_to_3d(obj, z=0, zdir='z'): 

168 """ 

169 Convert a `.Text` to a `.Text3D` object. 

170 

171 Parameters 

172 ---------- 

173 z : float 

174 The z-position in 3D space. 

175 zdir : {'x', 'y', 'z', 3-tuple} 

176 The direction of the text. Default: 'z'. 

177 See `.get_dir_vector` for a description of the values. 

178 """ 

179 obj.__class__ = Text3D 

180 obj.set_3d_properties(z, zdir) 

181 

182 

183class Line3D(lines.Line2D): 

184 """ 

185 3D line object. 

186 

187 .. note:: Use `get_data_3d` to obtain the data associated with the line. 

188 `~.Line2D.get_data`, `~.Line2D.get_xdata`, and `~.Line2D.get_ydata` return 

189 the x- and y-coordinates of the projected 2D-line, not the x- and y-data of 

190 the 3D-line. Similarly, use `set_data_3d` to set the data, not 

191 `~.Line2D.set_data`, `~.Line2D.set_xdata`, and `~.Line2D.set_ydata`. 

192 """ 

193 

194 def __init__(self, xs, ys, zs, *args, **kwargs): 

195 """ 

196 

197 Parameters 

198 ---------- 

199 xs : array-like 

200 The x-data to be plotted. 

201 ys : array-like 

202 The y-data to be plotted. 

203 zs : array-like 

204 The z-data to be plotted. 

205 *args, **kwargs 

206 Additional arguments are passed to `~matplotlib.lines.Line2D`. 

207 """ 

208 super().__init__([], [], *args, **kwargs) 

209 self.set_data_3d(xs, ys, zs) 

210 

211 def set_3d_properties(self, zs=0, zdir='z'): 

212 """ 

213 Set the *z* position and direction of the line. 

214 

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 """ 

224 xs = self.get_xdata() 

225 ys = self.get_ydata() 

226 zs = cbook._to_unmasked_float_array(zs).ravel() 

227 zs = np.broadcast_to(zs, len(xs)) 

228 self._verts3d = juggle_axes(xs, ys, zs, zdir) 

229 self.stale = True 

230 

231 def set_data_3d(self, *args): 

232 """ 

233 Set the x, y and z data 

234 

235 Parameters 

236 ---------- 

237 x : array-like 

238 The x-data to be plotted. 

239 y : array-like 

240 The y-data to be plotted. 

241 z : array-like 

242 The z-data to be plotted. 

243 

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 

256 def get_data_3d(self): 

257 """ 

258 Get the current data 

259 

260 Returns 

261 ------- 

262 verts3d : length-3 tuple or array-like 

263 The current data as a tuple or array-like. 

264 """ 

265 return self._verts3d 

266 

267 @artist.allow_rasterization 

268 def draw(self, renderer): 

269 xs3d, ys3d, zs3d = self._verts3d 

270 xs, ys, zs = proj3d.proj_transform(xs3d, ys3d, zs3d, self.axes.M) 

271 self.set_data(xs, ys) 

272 super().draw(renderer) 

273 self.stale = False 

274 

275 

276def line_2d_to_3d(line, zs=0, zdir='z'): 

277 """ 

278 Convert a `.Line2D` to a `.Line3D` object. 

279 

280 Parameters 

281 ---------- 

282 zs : float 

283 The location along the *zdir* axis in 3D space to position the line. 

284 zdir : {'x', 'y', 'z'} 

285 Plane to plot line orthogonal to. Default: 'z'. 

286 See `.get_dir_vector` for a description of the values. 

287 """ 

288 

289 line.__class__ = Line3D 

290 line.set_3d_properties(zs, zdir) 

291 

292 

293def _path_to_3d_segment(path, zs=0, zdir='z'): 

294 """Convert a path to a 3D segment.""" 

295 

296 zs = np.broadcast_to(zs, len(path)) 

297 pathsegs = path.iter_segments(simplify=False, curves=False) 

298 seg = [(x, y, z) for (((x, y), code), z) in zip(pathsegs, zs)] 

299 seg3d = [juggle_axes(x, y, z, zdir) for (x, y, z) in seg] 

300 return seg3d 

301 

302 

303def _paths_to_3d_segments(paths, zs=0, zdir='z'): 

304 """Convert paths from a collection object to 3D segments.""" 

305 

306 if not np.iterable(zs): 

307 zs = np.broadcast_to(zs, len(paths)) 

308 else: 

309 if len(zs) != len(paths): 

310 raise ValueError('Number of z-coordinates does not match paths.') 

311 

312 segs = [_path_to_3d_segment(path, pathz, zdir) 

313 for path, pathz in zip(paths, zs)] 

314 return segs 

315 

316 

317def _path_to_3d_segment_with_codes(path, zs=0, zdir='z'): 

318 """Convert a path to a 3D segment with path codes.""" 

319 

320 zs = np.broadcast_to(zs, len(path)) 

321 pathsegs = path.iter_segments(simplify=False, curves=False) 

322 seg_codes = [((x, y, z), code) for ((x, y), code), z in zip(pathsegs, zs)] 

323 if seg_codes: 

324 seg, codes = zip(*seg_codes) 

325 seg3d = [juggle_axes(x, y, z, zdir) for (x, y, z) in seg] 

326 else: 

327 seg3d = [] 

328 codes = [] 

329 return seg3d, list(codes) 

330 

331 

332def _paths_to_3d_segments_with_codes(paths, zs=0, zdir='z'): 

333 """ 

334 Convert paths from a collection object to 3D segments with path codes. 

335 """ 

336 

337 zs = np.broadcast_to(zs, len(paths)) 

338 segments_codes = [_path_to_3d_segment_with_codes(path, pathz, zdir) 

339 for path, pathz in zip(paths, zs)] 

340 if segments_codes: 

341 segments, codes = zip(*segments_codes) 

342 else: 

343 segments, codes = [], [] 

344 return list(segments), list(codes) 

345 

346 

347class Collection3D(Collection): 

348 """A collection of 3D paths.""" 

349 

350 def do_3d_projection(self): 

351 """Project the points according to renderer matrix.""" 

352 xyzs_list = [proj3d.proj_transform(*vs.T, self.axes.M) 

353 for vs, _ in self._3dverts_codes] 

354 self._paths = [mpath.Path(np.column_stack([xs, ys]), cs) 

355 for (xs, ys, _), (_, cs) in zip(xyzs_list, self._3dverts_codes)] 

356 zs = np.concatenate([zs for _, _, zs in xyzs_list]) 

357 return zs.min() if len(zs) else 1e9 

358 

359 

360def collection_2d_to_3d(col, zs=0, zdir='z'): 

361 """Convert a `.Collection` to a `.Collection3D` object.""" 

362 zs = np.broadcast_to(zs, len(col.get_paths())) 

363 col._3dverts_codes = [ 

364 (np.column_stack(juggle_axes( 

365 *np.column_stack([p.vertices, np.broadcast_to(z, len(p.vertices))]).T, 

366 zdir)), 

367 p.codes) 

368 for p, z in zip(col.get_paths(), zs)] 

369 col.__class__ = cbook._make_class_factory(Collection3D, "{}3D")(type(col)) 

370 

371 

372class Line3DCollection(LineCollection): 

373 """ 

374 A collection of 3D lines. 

375 """ 

376 

377 def set_sort_zpos(self, val): 

378 """Set the position to use for z-sorting.""" 

379 self._sort_zpos = val 

380 self.stale = True 

381 

382 def set_segments(self, segments): 

383 """ 

384 Set 3D segments. 

385 """ 

386 self._segments3d = segments 

387 super().set_segments([]) 

388 

389 def do_3d_projection(self): 

390 """ 

391 Project the points according to renderer matrix. 

392 """ 

393 xyslist = [proj3d._proj_trans_points(points, self.axes.M) 

394 for points in self._segments3d] 

395 segments_2d = [np.column_stack([xs, ys]) for xs, ys, zs in xyslist] 

396 LineCollection.set_segments(self, segments_2d) 

397 

398 # FIXME 

399 minz = 1e9 

400 for xs, ys, zs in xyslist: 

401 minz = min(minz, min(zs)) 

402 return minz 

403 

404 

405def line_collection_2d_to_3d(col, zs=0, zdir='z'): 

406 """Convert a `.LineCollection` to a `.Line3DCollection` object.""" 

407 segments3d = _paths_to_3d_segments(col.get_paths(), zs, zdir) 

408 col.__class__ = Line3DCollection 

409 col.set_segments(segments3d) 

410 

411 

412class Patch3D(Patch): 

413 """ 

414 3D patch object. 

415 """ 

416 

417 def __init__(self, *args, zs=(), zdir='z', **kwargs): 

418 """ 

419 Parameters 

420 ---------- 

421 verts : 

422 zs : float 

423 The location along the *zdir* axis in 3D space to position the 

424 patch. 

425 zdir : {'x', 'y', 'z'} 

426 Plane to plot patch orthogonal to. Default: 'z'. 

427 See `.get_dir_vector` for a description of the values. 

428 """ 

429 super().__init__(*args, **kwargs) 

430 self.set_3d_properties(zs, zdir) 

431 

432 def set_3d_properties(self, verts, zs=0, zdir='z'): 

433 """ 

434 Set the *z* position and direction of the patch. 

435 

436 Parameters 

437 ---------- 

438 verts : 

439 zs : float 

440 The location along the *zdir* axis in 3D space to position the 

441 patch. 

442 zdir : {'x', 'y', 'z'} 

443 Plane to plot patch orthogonal to. Default: 'z'. 

444 See `.get_dir_vector` for a description of the values. 

445 """ 

446 zs = np.broadcast_to(zs, len(verts)) 

447 self._segment3d = [juggle_axes(x, y, z, zdir) 

448 for ((x, y), z) in zip(verts, zs)] 

449 

450 def get_path(self): 

451 # docstring inherited 

452 # self._path2d is not initialized until do_3d_projection 

453 if not hasattr(self, '_path2d'): 

454 self.axes.M = self.axes.get_proj() 

455 self.do_3d_projection() 

456 return self._path2d 

457 

458 def do_3d_projection(self): 

459 s = self._segment3d 

460 xs, ys, zs = zip(*s) 

461 vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, 

462 self.axes.M) 

463 self._path2d = mpath.Path(np.column_stack([vxs, vys])) 

464 return min(vzs) 

465 

466 

467class PathPatch3D(Patch3D): 

468 """ 

469 3D PathPatch object. 

470 """ 

471 

472 def __init__(self, path, *, zs=(), zdir='z', **kwargs): 

473 """ 

474 Parameters 

475 ---------- 

476 path : 

477 zs : float 

478 The location along the *zdir* axis in 3D space to position the 

479 path patch. 

480 zdir : {'x', 'y', 'z', 3-tuple} 

481 Plane to plot path patch orthogonal to. Default: 'z'. 

482 See `.get_dir_vector` for a description of the values. 

483 """ 

484 # Not super().__init__! 

485 Patch.__init__(self, **kwargs) 

486 self.set_3d_properties(path, zs, zdir) 

487 

488 def set_3d_properties(self, path, zs=0, zdir='z'): 

489 """ 

490 Set the *z* position and direction of the path patch. 

491 

492 Parameters 

493 ---------- 

494 path : 

495 zs : float 

496 The location along the *zdir* axis in 3D space to position the 

497 path patch. 

498 zdir : {'x', 'y', 'z', 3-tuple} 

499 Plane to plot path patch orthogonal to. Default: 'z'. 

500 See `.get_dir_vector` for a description of the values. 

501 """ 

502 Patch3D.set_3d_properties(self, path.vertices, zs=zs, zdir=zdir) 

503 self._code3d = path.codes 

504 

505 def do_3d_projection(self): 

506 s = self._segment3d 

507 xs, ys, zs = zip(*s) 

508 vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, 

509 self.axes.M) 

510 self._path2d = mpath.Path(np.column_stack([vxs, vys]), self._code3d) 

511 return min(vzs) 

512 

513 

514def _get_patch_verts(patch): 

515 """Return a list of vertices for the path of a patch.""" 

516 trans = patch.get_patch_transform() 

517 path = patch.get_path() 

518 polygons = path.to_polygons(trans) 

519 return polygons[0] if len(polygons) else np.array([]) 

520 

521 

522def patch_2d_to_3d(patch, z=0, zdir='z'): 

523 """Convert a `.Patch` to a `.Patch3D` object.""" 

524 verts = _get_patch_verts(patch) 

525 patch.__class__ = Patch3D 

526 patch.set_3d_properties(verts, z, zdir) 

527 

528 

529def pathpatch_2d_to_3d(pathpatch, z=0, zdir='z'): 

530 """Convert a `.PathPatch` to a `.PathPatch3D` object.""" 

531 path = pathpatch.get_path() 

532 trans = pathpatch.get_patch_transform() 

533 

534 mpath = trans.transform_path(path) 

535 pathpatch.__class__ = PathPatch3D 

536 pathpatch.set_3d_properties(mpath, z, zdir) 

537 

538 

539class Patch3DCollection(PatchCollection): 

540 """ 

541 A collection of 3D patches. 

542 """ 

543 

544 def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs): 

545 """ 

546 Create a collection of flat 3D patches with its normal vector 

547 pointed in *zdir* direction, and located at *zs* on the *zdir* 

548 axis. 'zs' can be a scalar or an array-like of the same length as 

549 the number of patches in the collection. 

550 

551 Constructor arguments are the same as for 

552 :class:`~matplotlib.collections.PatchCollection`. In addition, 

553 keywords *zs=0* and *zdir='z'* are available. 

554 

555 Also, the keyword argument *depthshade* is available to indicate 

556 whether to shade the patches in order to give the appearance of depth 

557 (default is *True*). This is typically desired in scatter plots. 

558 """ 

559 self._depthshade = depthshade 

560 super().__init__(*args, **kwargs) 

561 self.set_3d_properties(zs, zdir) 

562 

563 def get_depthshade(self): 

564 return self._depthshade 

565 

566 def set_depthshade(self, depthshade): 

567 """ 

568 Set whether depth shading is performed on collection members. 

569 

570 Parameters 

571 ---------- 

572 depthshade : bool 

573 Whether to shade the patches in order to give the appearance of 

574 depth. 

575 """ 

576 self._depthshade = depthshade 

577 self.stale = True 

578 

579 def set_sort_zpos(self, val): 

580 """Set the position to use for z-sorting.""" 

581 self._sort_zpos = val 

582 self.stale = True 

583 

584 def set_3d_properties(self, zs, zdir): 

585 """ 

586 Set the *z* positions and direction of the patches. 

587 

588 Parameters 

589 ---------- 

590 zs : float or array of floats 

591 The location or locations to place the patches in the collection 

592 along the *zdir* axis. 

593 zdir : {'x', 'y', 'z'} 

594 Plane to plot patches orthogonal to. 

595 All patches must have the same direction. 

596 See `.get_dir_vector` for a description of the values. 

597 """ 

598 # Force the collection to initialize the face and edgecolors 

599 # just in case it is a scalarmappable with a colormap. 

600 self.update_scalarmappable() 

601 offsets = self.get_offsets() 

602 if len(offsets) > 0: 

603 xs, ys = offsets.T 

604 else: 

605 xs = [] 

606 ys = [] 

607 self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir) 

608 self._z_markers_idx = slice(-1) 

609 self._vzs = None 

610 self.stale = True 

611 

612 def do_3d_projection(self): 

613 xs, ys, zs = self._offsets3d 

614 vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, 

615 self.axes.M) 

616 self._vzs = vzs 

617 super().set_offsets(np.column_stack([vxs, vys])) 

618 

619 if vzs.size > 0: 

620 return min(vzs) 

621 else: 

622 return np.nan 

623 

624 def _maybe_depth_shade_and_sort_colors(self, color_array): 

625 color_array = ( 

626 _zalpha(color_array, self._vzs) 

627 if self._vzs is not None and self._depthshade 

628 else color_array 

629 ) 

630 if len(color_array) > 1: 

631 color_array = color_array[self._z_markers_idx] 

632 return mcolors.to_rgba_array(color_array, self._alpha) 

633 

634 def get_facecolor(self): 

635 return self._maybe_depth_shade_and_sort_colors(super().get_facecolor()) 

636 

637 def get_edgecolor(self): 

638 # We need this check here to make sure we do not double-apply the depth 

639 # based alpha shading when the edge color is "face" which means the 

640 # edge colour should be identical to the face colour. 

641 if cbook._str_equal(self._edgecolors, 'face'): 

642 return self.get_facecolor() 

643 return self._maybe_depth_shade_and_sort_colors(super().get_edgecolor()) 

644 

645 

646class Path3DCollection(PathCollection): 

647 """ 

648 A collection of 3D paths. 

649 """ 

650 

651 def __init__(self, *args, zs=0, zdir='z', depthshade=True, **kwargs): 

652 """ 

653 Create a collection of flat 3D paths with its normal vector 

654 pointed in *zdir* direction, and located at *zs* on the *zdir* 

655 axis. 'zs' can be a scalar or an array-like of the same length as 

656 the number of paths in the collection. 

657 

658 Constructor arguments are the same as for 

659 :class:`~matplotlib.collections.PathCollection`. In addition, 

660 keywords *zs=0* and *zdir='z'* are available. 

661 

662 Also, the keyword argument *depthshade* is available to indicate 

663 whether to shade the patches in order to give the appearance of depth 

664 (default is *True*). This is typically desired in scatter plots. 

665 """ 

666 self._depthshade = depthshade 

667 self._in_draw = False 

668 super().__init__(*args, **kwargs) 

669 self.set_3d_properties(zs, zdir) 

670 self._offset_zordered = None 

671 

672 def draw(self, renderer): 

673 with self._use_zordered_offset(): 

674 with cbook._setattr_cm(self, _in_draw=True): 

675 super().draw(renderer) 

676 

677 def set_sort_zpos(self, val): 

678 """Set the position to use for z-sorting.""" 

679 self._sort_zpos = val 

680 self.stale = True 

681 

682 def set_3d_properties(self, zs, zdir): 

683 """ 

684 Set the *z* positions and direction of the paths. 

685 

686 Parameters 

687 ---------- 

688 zs : float or array of floats 

689 The location or locations to place the paths in the collection 

690 along the *zdir* axis. 

691 zdir : {'x', 'y', 'z'} 

692 Plane to plot paths orthogonal to. 

693 All paths must have the same direction. 

694 See `.get_dir_vector` for a description of the values. 

695 """ 

696 # Force the collection to initialize the face and edgecolors 

697 # just in case it is a scalarmappable with a colormap. 

698 self.update_scalarmappable() 

699 offsets = self.get_offsets() 

700 if len(offsets) > 0: 

701 xs, ys = offsets.T 

702 else: 

703 xs = [] 

704 ys = [] 

705 self._offsets3d = juggle_axes(xs, ys, np.atleast_1d(zs), zdir) 

706 # In the base draw methods we access the attributes directly which 

707 # means we cannot resolve the shuffling in the getter methods like 

708 # we do for the edge and face colors. 

709 # 

710 # This means we need to carry around a cache of the unsorted sizes and 

711 # widths (postfixed with 3d) and in `do_3d_projection` set the 

712 # depth-sorted version of that data into the private state used by the 

713 # base collection class in its draw method. 

714 # 

715 # Grab the current sizes and linewidths to preserve them. 

716 self._sizes3d = self._sizes 

717 self._linewidths3d = np.array(self._linewidths) 

718 xs, ys, zs = self._offsets3d 

719 

720 # Sort the points based on z coordinates 

721 # Performance optimization: Create a sorted index array and reorder 

722 # points and point properties according to the index array 

723 self._z_markers_idx = slice(-1) 

724 self._vzs = None 

725 self.stale = True 

726 

727 def set_sizes(self, sizes, dpi=72.0): 

728 super().set_sizes(sizes, dpi) 

729 if not self._in_draw: 

730 self._sizes3d = sizes 

731 

732 def set_linewidth(self, lw): 

733 super().set_linewidth(lw) 

734 if not self._in_draw: 

735 self._linewidths3d = np.array(self._linewidths) 

736 

737 def get_depthshade(self): 

738 return self._depthshade 

739 

740 def set_depthshade(self, depthshade): 

741 """ 

742 Set whether depth shading is performed on collection members. 

743 

744 Parameters 

745 ---------- 

746 depthshade : bool 

747 Whether to shade the patches in order to give the appearance of 

748 depth. 

749 """ 

750 self._depthshade = depthshade 

751 self.stale = True 

752 

753 def do_3d_projection(self): 

754 xs, ys, zs = self._offsets3d 

755 vxs, vys, vzs, vis = proj3d.proj_transform_clip(xs, ys, zs, 

756 self.axes.M) 

757 # Sort the points based on z coordinates 

758 # Performance optimization: Create a sorted index array and reorder 

759 # points and point properties according to the index array 

760 z_markers_idx = self._z_markers_idx = np.argsort(vzs)[::-1] 

761 self._vzs = vzs 

762 

763 # we have to special case the sizes because of code in collections.py 

764 # as the draw method does 

765 # self.set_sizes(self._sizes, self.figure.dpi) 

766 # so we cannot rely on doing the sorting on the way out via get_* 

767 

768 if len(self._sizes3d) > 1: 

769 self._sizes = self._sizes3d[z_markers_idx] 

770 

771 if len(self._linewidths3d) > 1: 

772 self._linewidths = self._linewidths3d[z_markers_idx] 

773 

774 PathCollection.set_offsets(self, np.column_stack((vxs, vys))) 

775 

776 # Re-order items 

777 vzs = vzs[z_markers_idx] 

778 vxs = vxs[z_markers_idx] 

779 vys = vys[z_markers_idx] 

780 

781 # Store ordered offset for drawing purpose 

782 self._offset_zordered = np.column_stack((vxs, vys)) 

783 

784 return np.min(vzs) if vzs.size else np.nan 

785 

786 @contextmanager 

787 def _use_zordered_offset(self): 

788 if self._offset_zordered is None: 

789 # Do nothing 

790 yield 

791 else: 

792 # Swap offset with z-ordered offset 

793 old_offset = self._offsets 

794 super().set_offsets(self._offset_zordered) 

795 try: 

796 yield 

797 finally: 

798 self._offsets = old_offset 

799 

800 def _maybe_depth_shade_and_sort_colors(self, color_array): 

801 color_array = ( 

802 _zalpha(color_array, self._vzs) 

803 if self._vzs is not None and self._depthshade 

804 else color_array 

805 ) 

806 if len(color_array) > 1: 

807 color_array = color_array[self._z_markers_idx] 

808 return mcolors.to_rgba_array(color_array, self._alpha) 

809 

810 def get_facecolor(self): 

811 return self._maybe_depth_shade_and_sort_colors(super().get_facecolor()) 

812 

813 def get_edgecolor(self): 

814 # We need this check here to make sure we do not double-apply the depth 

815 # based alpha shading when the edge color is "face" which means the 

816 # edge colour should be identical to the face colour. 

817 if cbook._str_equal(self._edgecolors, 'face'): 

818 return self.get_facecolor() 

819 return self._maybe_depth_shade_and_sort_colors(super().get_edgecolor()) 

820 

821 

822def patch_collection_2d_to_3d(col, zs=0, zdir='z', depthshade=True): 

823 """ 

824 Convert a `.PatchCollection` into a `.Patch3DCollection` object 

825 (or a `.PathCollection` into a `.Path3DCollection` object). 

826 

827 Parameters 

828 ---------- 

829 col : `~matplotlib.collections.PatchCollection` or \ 

830`~matplotlib.collections.PathCollection` 

831 The collection to convert. 

832 zs : float or array of floats 

833 The location or locations to place the patches in the collection along 

834 the *zdir* axis. Default: 0. 

835 zdir : {'x', 'y', 'z'} 

836 The axis in which to place the patches. Default: "z". 

837 See `.get_dir_vector` for a description of the values. 

838 depthshade : bool, default: True 

839 Whether to shade the patches to give a sense of depth. 

840 

841 """ 

842 if isinstance(col, PathCollection): 

843 col.__class__ = Path3DCollection 

844 col._offset_zordered = None 

845 elif isinstance(col, PatchCollection): 

846 col.__class__ = Patch3DCollection 

847 col._depthshade = depthshade 

848 col._in_draw = False 

849 col.set_3d_properties(zs, zdir) 

850 

851 

852class Poly3DCollection(PolyCollection): 

853 """ 

854 A collection of 3D polygons. 

855 

856 .. note:: 

857 **Filling of 3D polygons** 

858 

859 There is no simple definition of the enclosed surface of a 3D polygon 

860 unless the polygon is planar. 

861 

862 In practice, Matplotlib fills the 2D projection of the polygon. This 

863 gives a correct filling appearance only for planar polygons. For all 

864 other polygons, you'll find orientations in which the edges of the 

865 polygon intersect in the projection. This will lead to an incorrect 

866 visualization of the 3D area. 

867 

868 If you need filled areas, it is recommended to create them via 

869 `~mpl_toolkits.mplot3d.axes3d.Axes3D.plot_trisurf`, which creates a 

870 triangulation and thus generates consistent surfaces. 

871 """ 

872 

873 def __init__(self, verts, *args, zsort='average', shade=False, 

874 lightsource=None, **kwargs): 

875 """ 

876 Parameters 

877 ---------- 

878 verts : list of (N, 3) array-like 

879 The sequence of polygons [*verts0*, *verts1*, ...] where each 

880 element *verts_i* defines the vertices of polygon *i* as a 2D 

881 array-like of shape (N, 3). 

882 zsort : {'average', 'min', 'max'}, default: 'average' 

883 The calculation method for the z-order. 

884 See `~.Poly3DCollection.set_zsort` for details. 

885 shade : bool, default: False 

886 Whether to shade *facecolors* and *edgecolors*. When activating 

887 *shade*, *facecolors* and/or *edgecolors* must be provided. 

888 

889 .. versionadded:: 3.7 

890 

891 lightsource : `~matplotlib.colors.LightSource`, optional 

892 The lightsource to use when *shade* is True. 

893 

894 .. versionadded:: 3.7 

895 

896 *args, **kwargs 

897 All other parameters are forwarded to `.PolyCollection`. 

898 

899 Notes 

900 ----- 

901 Note that this class does a bit of magic with the _facecolors 

902 and _edgecolors properties. 

903 """ 

904 if shade: 

905 normals = _generate_normals(verts) 

906 facecolors = kwargs.get('facecolors', None) 

907 if facecolors is not None: 

908 kwargs['facecolors'] = _shade_colors( 

909 facecolors, normals, lightsource 

910 ) 

911 

912 edgecolors = kwargs.get('edgecolors', None) 

913 if edgecolors is not None: 

914 kwargs['edgecolors'] = _shade_colors( 

915 edgecolors, normals, lightsource 

916 ) 

917 if facecolors is None and edgecolors is None: 

918 raise ValueError( 

919 "You must provide facecolors, edgecolors, or both for " 

920 "shade to work.") 

921 super().__init__(verts, *args, **kwargs) 

922 if isinstance(verts, np.ndarray): 

923 if verts.ndim != 3: 

924 raise ValueError('verts must be a list of (N, 3) array-like') 

925 else: 

926 if any(len(np.shape(vert)) != 2 for vert in verts): 

927 raise ValueError('verts must be a list of (N, 3) array-like') 

928 self.set_zsort(zsort) 

929 self._codes3d = None 

930 

931 _zsort_functions = { 

932 'average': np.average, 

933 'min': np.min, 

934 'max': np.max, 

935 } 

936 

937 def set_zsort(self, zsort): 

938 """ 

939 Set the calculation method for the z-order. 

940 

941 Parameters 

942 ---------- 

943 zsort : {'average', 'min', 'max'} 

944 The function applied on the z-coordinates of the vertices in the 

945 viewer's coordinate system, to determine the z-order. 

946 """ 

947 self._zsortfunc = self._zsort_functions[zsort] 

948 self._sort_zpos = None 

949 self.stale = True 

950 

951 def get_vector(self, segments3d): 

952 """Optimize points for projection.""" 

953 if len(segments3d): 

954 xs, ys, zs = np.vstack(segments3d).T 

955 else: # vstack can't stack zero arrays. 

956 xs, ys, zs = [], [], [] 

957 ones = np.ones(len(xs)) 

958 self._vec = np.array([xs, ys, zs, ones]) 

959 

960 indices = [0, *np.cumsum([len(segment) for segment in segments3d])] 

961 self._segslices = [*map(slice, indices[:-1], indices[1:])] 

962 

963 def set_verts(self, verts, closed=True): 

964 """ 

965 Set 3D vertices. 

966 

967 Parameters 

968 ---------- 

969 verts : list of (N, 3) array-like 

970 The sequence of polygons [*verts0*, *verts1*, ...] where each 

971 element *verts_i* defines the vertices of polygon *i* as a 2D 

972 array-like of shape (N, 3). 

973 closed : bool, default: True 

974 Whether the polygon should be closed by adding a CLOSEPOLY 

975 connection at the end. 

976 """ 

977 self.get_vector(verts) 

978 # 2D verts will be updated at draw time 

979 super().set_verts([], False) 

980 self._closed = closed 

981 

982 def set_verts_and_codes(self, verts, codes): 

983 """Set 3D vertices with path codes.""" 

984 # set vertices with closed=False to prevent PolyCollection from 

985 # setting path codes 

986 self.set_verts(verts, closed=False) 

987 # and set our own codes instead. 

988 self._codes3d = codes 

989 

990 def set_3d_properties(self): 

991 # Force the collection to initialize the face and edgecolors 

992 # just in case it is a scalarmappable with a colormap. 

993 self.update_scalarmappable() 

994 self._sort_zpos = None 

995 self.set_zsort('average') 

996 self._facecolor3d = PolyCollection.get_facecolor(self) 

997 self._edgecolor3d = PolyCollection.get_edgecolor(self) 

998 self._alpha3d = PolyCollection.get_alpha(self) 

999 self.stale = True 

1000 

1001 def set_sort_zpos(self, val): 

1002 """Set the position to use for z-sorting.""" 

1003 self._sort_zpos = val 

1004 self.stale = True 

1005 

1006 def do_3d_projection(self): 

1007 """ 

1008 Perform the 3D projection for this object. 

1009 """ 

1010 if self._A is not None: 

1011 # force update of color mapping because we re-order them 

1012 # below. If we do not do this here, the 2D draw will call 

1013 # this, but we will never port the color mapped values back 

1014 # to the 3D versions. 

1015 # 

1016 # We hold the 3D versions in a fixed order (the order the user 

1017 # passed in) and sort the 2D version by view depth. 

1018 self.update_scalarmappable() 

1019 if self._face_is_mapped: 

1020 self._facecolor3d = self._facecolors 

1021 if self._edge_is_mapped: 

1022 self._edgecolor3d = self._edgecolors 

1023 txs, tys, tzs = proj3d._proj_transform_vec(self._vec, self.axes.M) 

1024 xyzlist = [(txs[sl], tys[sl], tzs[sl]) for sl in self._segslices] 

1025 

1026 # This extra fuss is to re-order face / edge colors 

1027 cface = self._facecolor3d 

1028 cedge = self._edgecolor3d 

1029 if len(cface) != len(xyzlist): 

1030 cface = cface.repeat(len(xyzlist), axis=0) 

1031 if len(cedge) != len(xyzlist): 

1032 if len(cedge) == 0: 

1033 cedge = cface 

1034 else: 

1035 cedge = cedge.repeat(len(xyzlist), axis=0) 

1036 

1037 if xyzlist: 

1038 # sort by depth (furthest drawn first) 

1039 z_segments_2d = sorted( 

1040 ((self._zsortfunc(zs), np.column_stack([xs, ys]), fc, ec, idx) 

1041 for idx, ((xs, ys, zs), fc, ec) 

1042 in enumerate(zip(xyzlist, cface, cedge))), 

1043 key=lambda x: x[0], reverse=True) 

1044 

1045 _, segments_2d, self._facecolors2d, self._edgecolors2d, idxs = \ 

1046 zip(*z_segments_2d) 

1047 else: 

1048 segments_2d = [] 

1049 self._facecolors2d = np.empty((0, 4)) 

1050 self._edgecolors2d = np.empty((0, 4)) 

1051 idxs = [] 

1052 

1053 if self._codes3d is not None: 

1054 codes = [self._codes3d[idx] for idx in idxs] 

1055 PolyCollection.set_verts_and_codes(self, segments_2d, codes) 

1056 else: 

1057 PolyCollection.set_verts(self, segments_2d, self._closed) 

1058 

1059 if len(self._edgecolor3d) != len(cface): 

1060 self._edgecolors2d = self._edgecolor3d 

1061 

1062 # Return zorder value 

1063 if self._sort_zpos is not None: 

1064 zvec = np.array([[0], [0], [self._sort_zpos], [1]]) 

1065 ztrans = proj3d._proj_transform_vec(zvec, self.axes.M) 

1066 return ztrans[2][0] 

1067 elif tzs.size > 0: 

1068 # FIXME: Some results still don't look quite right. 

1069 # In particular, examine contourf3d_demo2.py 

1070 # with az = -54 and elev = -45. 

1071 return np.min(tzs) 

1072 else: 

1073 return np.nan 

1074 

1075 def set_facecolor(self, colors): 

1076 # docstring inherited 

1077 super().set_facecolor(colors) 

1078 self._facecolor3d = PolyCollection.get_facecolor(self) 

1079 

1080 def set_edgecolor(self, colors): 

1081 # docstring inherited 

1082 super().set_edgecolor(colors) 

1083 self._edgecolor3d = PolyCollection.get_edgecolor(self) 

1084 

1085 def set_alpha(self, alpha): 

1086 # docstring inherited 

1087 artist.Artist.set_alpha(self, alpha) 

1088 try: 

1089 self._facecolor3d = mcolors.to_rgba_array( 

1090 self._facecolor3d, self._alpha) 

1091 except (AttributeError, TypeError, IndexError): 

1092 pass 

1093 try: 

1094 self._edgecolors = mcolors.to_rgba_array( 

1095 self._edgecolor3d, self._alpha) 

1096 except (AttributeError, TypeError, IndexError): 

1097 pass 

1098 self.stale = True 

1099 

1100 def get_facecolor(self): 

1101 # docstring inherited 

1102 # self._facecolors2d is not initialized until do_3d_projection 

1103 if not hasattr(self, '_facecolors2d'): 

1104 self.axes.M = self.axes.get_proj() 

1105 self.do_3d_projection() 

1106 return np.asarray(self._facecolors2d) 

1107 

1108 def get_edgecolor(self): 

1109 # docstring inherited 

1110 # self._edgecolors2d is not initialized until do_3d_projection 

1111 if not hasattr(self, '_edgecolors2d'): 

1112 self.axes.M = self.axes.get_proj() 

1113 self.do_3d_projection() 

1114 return np.asarray(self._edgecolors2d) 

1115 

1116 

1117def poly_collection_2d_to_3d(col, zs=0, zdir='z'): 

1118 """ 

1119 Convert a `.PolyCollection` into a `.Poly3DCollection` object. 

1120 

1121 Parameters 

1122 ---------- 

1123 col : `~matplotlib.collections.PolyCollection` 

1124 The collection to convert. 

1125 zs : float or array of floats 

1126 The location or locations to place the polygons in the collection along 

1127 the *zdir* axis. Default: 0. 

1128 zdir : {'x', 'y', 'z'} 

1129 The axis in which to place the patches. Default: 'z'. 

1130 See `.get_dir_vector` for a description of the values. 

1131 """ 

1132 segments_3d, codes = _paths_to_3d_segments_with_codes( 

1133 col.get_paths(), zs, zdir) 

1134 col.__class__ = Poly3DCollection 

1135 col.set_verts_and_codes(segments_3d, codes) 

1136 col.set_3d_properties() 

1137 

1138 

1139def juggle_axes(xs, ys, zs, zdir): 

1140 """ 

1141 Reorder coordinates so that 2D *xs*, *ys* can be plotted in the plane 

1142 orthogonal to *zdir*. *zdir* is normally 'x', 'y' or 'z'. However, if 

1143 *zdir* starts with a '-' it is interpreted as a compensation for 

1144 `rotate_axes`. 

1145 """ 

1146 if zdir == 'x': 

1147 return zs, xs, ys 

1148 elif zdir == 'y': 

1149 return xs, zs, ys 

1150 elif zdir[0] == '-': 

1151 return rotate_axes(xs, ys, zs, zdir) 

1152 else: 

1153 return xs, ys, zs 

1154 

1155 

1156def rotate_axes(xs, ys, zs, zdir): 

1157 """ 

1158 Reorder coordinates so that the axes are rotated with *zdir* along 

1159 the original z axis. Prepending the axis with a '-' does the 

1160 inverse transform, so *zdir* can be 'x', '-x', 'y', '-y', 'z' or '-z'. 

1161 """ 

1162 if zdir in ('x', '-y'): 

1163 return ys, zs, xs 

1164 elif zdir in ('-x', 'y'): 

1165 return zs, xs, ys 

1166 else: 

1167 return xs, ys, zs 

1168 

1169 

1170def _zalpha(colors, zs): 

1171 """Modify the alphas of the color list according to depth.""" 

1172 # FIXME: This only works well if the points for *zs* are well-spaced 

1173 # in all three dimensions. Otherwise, at certain orientations, 

1174 # the min and max zs are very close together. 

1175 # Should really normalize against the viewing depth. 

1176 if len(colors) == 0 or len(zs) == 0: 

1177 return np.zeros((0, 4)) 

1178 norm = Normalize(min(zs), max(zs)) 

1179 sats = 1 - norm(zs) * 0.7 

1180 rgba = np.broadcast_to(mcolors.to_rgba_array(colors), (len(zs), 4)) 

1181 return np.column_stack([rgba[:, :3], rgba[:, 3] * sats]) 

1182 

1183 

1184def _generate_normals(polygons): 

1185 """ 

1186 Compute the normals of a list of polygons, one normal per polygon. 

1187 

1188 Normals point towards the viewer for a face with its vertices in 

1189 counterclockwise order, following the right hand rule. 

1190 

1191 Uses three points equally spaced around the polygon. This method assumes 

1192 that the points are in a plane. Otherwise, more than one shade is required, 

1193 which is not supported. 

1194 

1195 Parameters 

1196 ---------- 

1197 polygons : list of (M_i, 3) array-like, or (..., M, 3) array-like 

1198 A sequence of polygons to compute normals for, which can have 

1199 varying numbers of vertices. If the polygons all have the same 

1200 number of vertices and array is passed, then the operation will 

1201 be vectorized. 

1202 

1203 Returns 

1204 ------- 

1205 normals : (..., 3) array 

1206 A normal vector estimated for the polygon. 

1207 """ 

1208 if isinstance(polygons, np.ndarray): 

1209 # optimization: polygons all have the same number of points, so can 

1210 # vectorize 

1211 n = polygons.shape[-2] 

1212 i1, i2, i3 = 0, n//3, 2*n//3 

1213 v1 = polygons[..., i1, :] - polygons[..., i2, :] 

1214 v2 = polygons[..., i2, :] - polygons[..., i3, :] 

1215 else: 

1216 # The subtraction doesn't vectorize because polygons is jagged. 

1217 v1 = np.empty((len(polygons), 3)) 

1218 v2 = np.empty((len(polygons), 3)) 

1219 for poly_i, ps in enumerate(polygons): 

1220 n = len(ps) 

1221 ps = np.asarray(ps) 

1222 i1, i2, i3 = 0, n//3, 2*n//3 

1223 v1[poly_i, :] = ps[i1, :] - ps[i2, :] 

1224 v2[poly_i, :] = ps[i2, :] - ps[i3, :] 

1225 return np.cross(v1, v2) 

1226 

1227 

1228def _shade_colors(color, normals, lightsource=None): 

1229 """ 

1230 Shade *color* using normal vectors given by *normals*, 

1231 assuming a *lightsource* (using default position if not given). 

1232 *color* can also be an array of the same length as *normals*. 

1233 """ 

1234 if lightsource is None: 

1235 # chosen for backwards-compatibility 

1236 lightsource = mcolors.LightSource(azdeg=225, altdeg=19.4712) 

1237 

1238 with np.errstate(invalid="ignore"): 

1239 shade = ((normals / np.linalg.norm(normals, axis=1, keepdims=True)) 

1240 @ lightsource.direction) 

1241 mask = ~np.isnan(shade) 

1242 

1243 if mask.any(): 

1244 # convert dot product to allowed shading fractions 

1245 in_norm = mcolors.Normalize(-1, 1) 

1246 out_norm = mcolors.Normalize(0.3, 1).inverse 

1247 

1248 def norm(x): 

1249 return out_norm(in_norm(x)) 

1250 

1251 shade[~mask] = 0 

1252 

1253 color = mcolors.to_rgba_array(color) 

1254 # shape of color should be (M, 4) (where M is number of faces) 

1255 # shape of shade should be (M,) 

1256 # colors should have final shape of (M, 4) 

1257 alpha = color[:, 3] 

1258 colors = norm(shade)[:, np.newaxis] * color 

1259 colors[:, 3] = alpha 

1260 else: 

1261 colors = np.asanyarray(color).copy() 

1262 

1263 return colors