Coverage for /usr/lib/python3/dist-packages/mpl_toolkits/mplot3d/art3d.py: 21%

433 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2025-06-14 15:25 +0200

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 matplotlib import ( 

15 artist, cbook, colors as mcolors, lines, text as mtext, path as mpath) 

16from matplotlib.collections import ( 

17 LineCollection, PolyCollection, PatchCollection, PathCollection) 

18from matplotlib.colors import Normalize 

19from matplotlib.patches import Patch 

20from . import proj3d 

21 

22 

23def _norm_angle(a): 

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

25 a = (a + 360) % 360 

26 if a > 180: 

27 a = a - 360 

28 return a 

29 

30 

31def _norm_text_angle(a): 

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

33 a = (a + 180) % 180 

34 if a > 90: 

35 a = a - 180 

36 return a 

37 

38 

39def get_dir_vector(zdir): 

40 """ 

41 Return a direction vector. 

42 

43 Parameters 

44 ---------- 

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

46 The direction. Possible values are: 

47 

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

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

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

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

52 - an iterable (x, y, z) is converted to a NumPy array, if not already 

53 

54 Returns 

55 ------- 

56 x, y, z : array-like 

57 The direction vector. 

58 """ 

59 if zdir == 'x': 

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

61 elif zdir == 'y': 

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

63 elif zdir == 'z': 

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

65 elif zdir is None: 

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

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

68 return np.array(zdir) 

69 else: 

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

71 

72 

73class Text3D(mtext.Text): 

74 """ 

75 Text object with 3D position and direction. 

76 

77 Parameters 

78 ---------- 

79 x, y, z 

80 The position of the text. 

81 text : str 

82 The text string to display. 

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

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

85 the values. 

86 

87 Other Parameters 

88 ---------------- 

89 **kwargs 

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

91 """ 

92 

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

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

95 self.set_3d_properties(z, zdir) 

96 

97 def get_position_3d(self): 

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

99 return self._x, self._y, self._z 

100 

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

102 """ 

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

104 

105 Parameters 

106 ---------- 

107 xyz : (float, float, float) 

108 The position in 3D space. 

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

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

111 changed. 

112 """ 

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

114 self.set_z(xyz[2]) 

115 if zdir is not None: 

116 self._dir_vec = get_dir_vector(zdir) 

117 

118 def set_z(self, z): 

119 """ 

120 Set the *z* position of the text. 

121 

122 Parameters 

123 ---------- 

124 z : float 

125 """ 

126 self._z = z 

127 self.stale = True 

128 

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

130 self._z = z 

131 self._dir_vec = get_dir_vector(zdir) 

132 self.stale = True 

133 

134 @artist.allow_rasterization 

135 def draw(self, renderer): 

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

137 proj = proj3d.proj_trans_points( 

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

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

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

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

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

143 _rotation=_norm_text_angle(angle)): 

144 mtext.Text.draw(self, renderer) 

145 self.stale = False 

146 

147 def get_tightbbox(self, renderer=None): 

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

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

150 return None 

151 

152 

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

154 """Convert a Text to a Text3D object.""" 

155 obj.__class__ = Text3D 

156 obj.set_3d_properties(z, zdir) 

157 

158 

159class Line3D(lines.Line2D): 

160 """ 

161 3D line object. 

162 """ 

163 

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

165 """ 

166 Keyword arguments are passed onto :func:`~matplotlib.lines.Line2D`. 

167 """ 

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

169 self._verts3d = xs, ys, zs 

170 

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

172 xs = self.get_xdata() 

173 ys = self.get_ydata() 

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

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

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

177 self.stale = True 

178 

179 def set_data_3d(self, *args): 

180 """ 

181 Set the x, y and z data 

182 

183 Parameters 

184 ---------- 

185 x : array-like 

186 The x-data to be plotted. 

187 y : array-like 

188 The y-data to be plotted. 

189 z : array-like 

190 The z-data to be plotted. 

191 

192 Notes 

193 ----- 

194 Accepts x, y, z arguments or a single array-like (x, y, z) 

195 """ 

196 if len(args) == 1: 

197 self._verts3d = args[0] 

198 else: 

199 self._verts3d = args 

200 self.stale = True 

201 

202 def get_data_3d(self): 

203 """ 

204 Get the current data 

205 

206 Returns 

207 ------- 

208 verts3d : length-3 tuple or array-like 

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

210 """ 

211 return self._verts3d 

212 

213 @artist.allow_rasterization 

214 def draw(self, renderer): 

215 xs3d, ys3d, zs3d = self._verts3d 

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

217 self.set_data(xs, ys) 

218 super().draw(renderer) 

219 self.stale = False 

220 

221 

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

223 """Convert a 2D line to 3D.""" 

224 

225 line.__class__ = Line3D 

226 line.set_3d_properties(zs, zdir) 

227 

228 

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

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

231 

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

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

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

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

236 return seg3d 

237 

238 

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

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

241 

242 if not np.iterable(zs): 

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

244 else: 

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

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

247 

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

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

250 return segs 

251 

252 

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

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

255 

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

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

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

259 if seg_codes: 

260 seg, codes = zip(*seg_codes) 

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

262 else: 

263 seg3d = [] 

264 codes = [] 

265 return seg3d, list(codes) 

266 

267 

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

269 """ 

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

271 """ 

272 

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

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

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

276 if segments_codes: 

277 segments, codes = zip(*segments_codes) 

278 else: 

279 segments, codes = [], [] 

280 return list(segments), list(codes) 

281 

282 

283class Line3DCollection(LineCollection): 

284 """ 

285 A collection of 3D lines. 

286 """ 

287 

288 def set_sort_zpos(self, val): 

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

290 self._sort_zpos = val 

291 self.stale = True 

292 

293 def set_segments(self, segments): 

294 """ 

295 Set 3D segments. 

296 """ 

297 self._segments3d = segments 

298 super().set_segments([]) 

299 

300 def do_3d_projection(self): 

301 """ 

302 Project the points according to renderer matrix. 

303 """ 

304 xyslist = [proj3d.proj_trans_points(points, self.axes.M) 

305 for points in self._segments3d] 

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

307 LineCollection.set_segments(self, segments_2d) 

308 

309 # FIXME 

310 minz = 1e9 

311 for xs, ys, zs in xyslist: 

312 minz = min(minz, min(zs)) 

313 return minz 

314 

315 

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

317 """Convert a LineCollection to a Line3DCollection object.""" 

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

319 col.__class__ = Line3DCollection 

320 col.set_segments(segments3d) 

321 

322 

323class Patch3D(Patch): 

324 """ 

325 3D patch object. 

326 """ 

327 

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

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

330 self.set_3d_properties(zs, zdir) 

331 

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

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

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

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

336 

337 def get_path(self): 

338 return self._path2d 

339 

340 def do_3d_projection(self): 

341 s = self._segment3d 

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

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

344 self.axes.M) 

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

346 return min(vzs) 

347 

348 

349class PathPatch3D(Patch3D): 

350 """ 

351 3D PathPatch object. 

352 """ 

353 

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

355 # Not super().__init__! 

356 Patch.__init__(self, **kwargs) 

357 self.set_3d_properties(path, zs, zdir) 

358 

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

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

361 self._code3d = path.codes 

362 

363 def do_3d_projection(self): 

364 s = self._segment3d 

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

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

367 self.axes.M) 

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

369 return min(vzs) 

370 

371 

372def _get_patch_verts(patch): 

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

374 trans = patch.get_patch_transform() 

375 path = patch.get_path() 

376 polygons = path.to_polygons(trans) 

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

378 

379 

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

381 """Convert a Patch to a Patch3D object.""" 

382 verts = _get_patch_verts(patch) 

383 patch.__class__ = Patch3D 

384 patch.set_3d_properties(verts, z, zdir) 

385 

386 

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

388 """Convert a PathPatch to a PathPatch3D object.""" 

389 path = pathpatch.get_path() 

390 trans = pathpatch.get_patch_transform() 

391 

392 mpath = trans.transform_path(path) 

393 pathpatch.__class__ = PathPatch3D 

394 pathpatch.set_3d_properties(mpath, z, zdir) 

395 

396 

397class Patch3DCollection(PatchCollection): 

398 """ 

399 A collection of 3D patches. 

400 """ 

401 

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

403 """ 

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

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

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

407 the number of patches in the collection. 

408 

409 Constructor arguments are the same as for 

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

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

412 

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

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

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

416 """ 

417 self._depthshade = depthshade 

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

419 self.set_3d_properties(zs, zdir) 

420 

421 def get_depthshade(self): 

422 return self._depthshade 

423 

424 def set_depthshade(self, depthshade): 

425 """ 

426 Set whether depth shading is performed on collection members. 

427 

428 Parameters 

429 ---------- 

430 depthshade : bool 

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

432 depth. 

433 """ 

434 self._depthshade = depthshade 

435 self.stale = True 

436 

437 def set_sort_zpos(self, val): 

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

439 self._sort_zpos = val 

440 self.stale = True 

441 

442 def set_3d_properties(self, zs, zdir): 

443 # Force the collection to initialize the face and edgecolors 

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

445 self.update_scalarmappable() 

446 offsets = self.get_offsets() 

447 if len(offsets) > 0: 

448 xs, ys = offsets.T 

449 else: 

450 xs = [] 

451 ys = [] 

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

453 self._z_markers_idx = slice(-1) 

454 self._vzs = None 

455 self.stale = True 

456 

457 def do_3d_projection(self): 

458 xs, ys, zs = self._offsets3d 

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

460 self.axes.M) 

461 self._vzs = vzs 

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

463 

464 if vzs.size > 0: 

465 return min(vzs) 

466 else: 

467 return np.nan 

468 

469 def _maybe_depth_shade_and_sort_colors(self, color_array): 

470 color_array = ( 

471 _zalpha(color_array, self._vzs) 

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

473 else color_array 

474 ) 

475 if len(color_array) > 1: 

476 color_array = color_array[self._z_markers_idx] 

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

478 

479 def get_facecolor(self): 

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

481 

482 def get_edgecolor(self): 

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

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

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

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

487 return self.get_facecolor() 

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

489 

490 

491class Path3DCollection(PathCollection): 

492 """ 

493 A collection of 3D paths. 

494 """ 

495 

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

497 """ 

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

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

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

501 the number of paths in the collection. 

502 

503 Constructor arguments are the same as for 

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

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

506 

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

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

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

510 """ 

511 self._depthshade = depthshade 

512 self._in_draw = False 

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

514 self.set_3d_properties(zs, zdir) 

515 

516 def draw(self, renderer): 

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

518 super().draw(renderer) 

519 

520 def set_sort_zpos(self, val): 

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

522 self._sort_zpos = val 

523 self.stale = True 

524 

525 def set_3d_properties(self, zs, zdir): 

526 # Force the collection to initialize the face and edgecolors 

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

528 self.update_scalarmappable() 

529 offsets = self.get_offsets() 

530 if len(offsets) > 0: 

531 xs, ys = offsets.T 

532 else: 

533 xs = [] 

534 ys = [] 

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

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

537 # means we can not resolve the shuffling in the getter methods like 

538 # we do for the edge and face colors. 

539 # 

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

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

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

543 # base collection class in its draw method. 

544 # 

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

546 self._sizes3d = self._sizes 

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

548 xs, ys, zs = self._offsets3d 

549 

550 # Sort the points based on z coordinates 

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

552 # points and point properties according to the index array 

553 self._z_markers_idx = slice(-1) 

554 self._vzs = None 

555 self.stale = True 

556 

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

558 super().set_sizes(sizes, dpi) 

559 if not self._in_draw: 

560 self._sizes3d = sizes 

561 

562 def set_linewidth(self, lw): 

563 super().set_linewidth(lw) 

564 if not self._in_draw: 

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

566 

567 def get_depthshade(self): 

568 return self._depthshade 

569 

570 def set_depthshade(self, depthshade): 

571 """ 

572 Set whether depth shading is performed on collection members. 

573 

574 Parameters 

575 ---------- 

576 depthshade : bool 

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

578 depth. 

579 """ 

580 self._depthshade = depthshade 

581 self.stale = True 

582 

583 def do_3d_projection(self): 

584 xs, ys, zs = self._offsets3d 

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

586 self.axes.M) 

587 # Sort the points based on z coordinates 

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

589 # points and point properties according to the index array 

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

591 self._vzs = vzs 

592 

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

594 # as the draw method does 

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

596 # so we can not rely on doing the sorting on the way out via get_* 

597 

598 if len(self._sizes3d) > 1: 

599 self._sizes = self._sizes3d[z_markers_idx] 

600 

601 if len(self._linewidths3d) > 1: 

602 self._linewidths = self._linewidths3d[z_markers_idx] 

603 

604 # Re-order items 

605 vzs = vzs[z_markers_idx] 

606 vxs = vxs[z_markers_idx] 

607 vys = vys[z_markers_idx] 

608 

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

610 

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

612 

613 def _maybe_depth_shade_and_sort_colors(self, color_array): 

614 color_array = ( 

615 _zalpha(color_array, self._vzs) 

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

617 else color_array 

618 ) 

619 if len(color_array) > 1: 

620 color_array = color_array[self._z_markers_idx] 

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

622 

623 def get_facecolor(self): 

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

625 

626 def get_edgecolor(self): 

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

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

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

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

631 return self.get_facecolor() 

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

633 

634 

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

636 """ 

637 Convert a :class:`~matplotlib.collections.PatchCollection` into a 

638 :class:`Patch3DCollection` object 

639 (or a :class:`~matplotlib.collections.PathCollection` into a 

640 :class:`Path3DCollection` object). 

641 

642 Parameters 

643 ---------- 

644 za 

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

646 the *zdir* axis. Default: 0. 

647 zdir 

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

649 depthshade 

650 Whether to shade the patches to give a sense of depth. Default: *True*. 

651 

652 """ 

653 if isinstance(col, PathCollection): 

654 col.__class__ = Path3DCollection 

655 elif isinstance(col, PatchCollection): 

656 col.__class__ = Patch3DCollection 

657 col._depthshade = depthshade 

658 col._in_draw = False 

659 col.set_3d_properties(zs, zdir) 

660 

661 

662class Poly3DCollection(PolyCollection): 

663 """ 

664 A collection of 3D polygons. 

665 

666 .. note:: 

667 **Filling of 3D polygons** 

668 

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

670 unless the polygon is planar. 

671 

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

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

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

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

676 visualization of the 3D area. 

677 

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

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

680 triangulation and thus generates consistent surfaces. 

681 """ 

682 

683 def __init__(self, verts, *args, zsort='average', **kwargs): 

684 """ 

685 Parameters 

686 ---------- 

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

688 Each element describes a polygon as a sequence of ``N_i`` points 

689 ``(x, y, z)``. 

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

691 The calculation method for the z-order. 

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

693 *args, **kwargs 

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

695 

696 Notes 

697 ----- 

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

699 and _edgecolors properties. 

700 """ 

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

702 if isinstance(verts, np.ndarray): 

703 if verts.ndim != 3: 

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

705 else: 

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

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

708 self.set_zsort(zsort) 

709 self._codes3d = None 

710 

711 _zsort_functions = { 

712 'average': np.average, 

713 'min': np.min, 

714 'max': np.max, 

715 } 

716 

717 def set_zsort(self, zsort): 

718 """ 

719 Set the calculation method for the z-order. 

720 

721 Parameters 

722 ---------- 

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

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

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

726 """ 

727 self._zsortfunc = self._zsort_functions[zsort] 

728 self._sort_zpos = None 

729 self.stale = True 

730 

731 def get_vector(self, segments3d): 

732 """Optimize points for projection.""" 

733 if len(segments3d): 

734 xs, ys, zs = np.row_stack(segments3d).T 

735 else: # row_stack can't stack zero arrays. 

736 xs, ys, zs = [], [], [] 

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

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

739 

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

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

742 

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

744 """Set 3D vertices.""" 

745 self.get_vector(verts) 

746 # 2D verts will be updated at draw time 

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

748 self._closed = closed 

749 

750 def set_verts_and_codes(self, verts, codes): 

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

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

753 # setting path codes 

754 self.set_verts(verts, closed=False) 

755 # and set our own codes instead. 

756 self._codes3d = codes 

757 

758 def set_3d_properties(self): 

759 # Force the collection to initialize the face and edgecolors 

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

761 self.update_scalarmappable() 

762 self._sort_zpos = None 

763 self.set_zsort('average') 

764 self._facecolor3d = PolyCollection.get_facecolor(self) 

765 self._edgecolor3d = PolyCollection.get_edgecolor(self) 

766 self._alpha3d = PolyCollection.get_alpha(self) 

767 self.stale = True 

768 

769 def set_sort_zpos(self, val): 

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

771 self._sort_zpos = val 

772 self.stale = True 

773 

774 def do_3d_projection(self): 

775 """ 

776 Perform the 3D projection for this object. 

777 """ 

778 if self._A is not None: 

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

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

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

782 # to the 3D versions. 

783 # 

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

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

786 self.update_scalarmappable() 

787 if self._face_is_mapped: 

788 self._facecolor3d = self._facecolors 

789 if self._edge_is_mapped: 

790 self._edgecolor3d = self._edgecolors 

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

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

793 

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

795 cface = self._facecolor3d 

796 cedge = self._edgecolor3d 

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

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

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

800 if len(cedge) == 0: 

801 cedge = cface 

802 else: 

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

804 

805 if xyzlist: 

806 # sort by depth (furthest drawn first) 

807 z_segments_2d = sorted( 

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

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

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

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

812 

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

814 zip(*z_segments_2d) 

815 else: 

816 segments_2d = [] 

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

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

819 idxs = [] 

820 

821 if self._codes3d is not None: 

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

823 PolyCollection.set_verts_and_codes(self, segments_2d, codes) 

824 else: 

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

826 

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

828 self._edgecolors2d = self._edgecolor3d 

829 

830 # Return zorder value 

831 if self._sort_zpos is not None: 

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

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

834 return ztrans[2][0] 

835 elif tzs.size > 0: 

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

837 # In particular, examine contourf3d_demo2.py 

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

839 return np.min(tzs) 

840 else: 

841 return np.nan 

842 

843 def set_facecolor(self, colors): 

844 # docstring inherited 

845 super().set_facecolor(colors) 

846 self._facecolor3d = PolyCollection.get_facecolor(self) 

847 

848 def set_edgecolor(self, colors): 

849 # docstring inherited 

850 super().set_edgecolor(colors) 

851 self._edgecolor3d = PolyCollection.get_edgecolor(self) 

852 

853 def set_alpha(self, alpha): 

854 # docstring inherited 

855 artist.Artist.set_alpha(self, alpha) 

856 try: 

857 self._facecolor3d = mcolors.to_rgba_array( 

858 self._facecolor3d, self._alpha) 

859 except (AttributeError, TypeError, IndexError): 

860 pass 

861 try: 

862 self._edgecolors = mcolors.to_rgba_array( 

863 self._edgecolor3d, self._alpha) 

864 except (AttributeError, TypeError, IndexError): 

865 pass 

866 self.stale = True 

867 

868 def get_facecolor(self): 

869 # docstring inherited 

870 # self._facecolors2d is not initialized until do_3d_projection 

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

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

873 self.do_3d_projection() 

874 return self._facecolors2d 

875 

876 def get_edgecolor(self): 

877 # docstring inherited 

878 # self._edgecolors2d is not initialized until do_3d_projection 

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

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

881 self.do_3d_projection() 

882 return self._edgecolors2d 

883 

884 

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

886 """Convert a PolyCollection to a Poly3DCollection object.""" 

887 segments_3d, codes = _paths_to_3d_segments_with_codes( 

888 col.get_paths(), zs, zdir) 

889 col.__class__ = Poly3DCollection 

890 col.set_verts_and_codes(segments_3d, codes) 

891 col.set_3d_properties() 

892 

893 

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

895 """ 

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

897 orthogonal to zdir. zdir is normally x, y or z. However, if zdir 

898 starts with a '-' it is interpreted as a compensation for rotate_axes. 

899 """ 

900 if zdir == 'x': 

901 return zs, xs, ys 

902 elif zdir == 'y': 

903 return xs, zs, ys 

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

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

906 else: 

907 return xs, ys, zs 

908 

909 

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

911 """ 

912 Reorder coordinates so that the axes are rotated with zdir along 

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

914 inverse transform, so zdir can be x, -x, y, -y, z or -z 

915 """ 

916 if zdir == 'x': 

917 return ys, zs, xs 

918 elif zdir == '-x': 

919 return zs, xs, ys 

920 

921 elif zdir == 'y': 

922 return zs, xs, ys 

923 elif zdir == '-y': 

924 return ys, zs, xs 

925 

926 else: 

927 return xs, ys, zs 

928 

929 

930def _zalpha(colors, zs): 

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

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

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

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

935 # Should really normalize against the viewing depth. 

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

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

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

939 sats = 1 - norm(zs) * 0.7 

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

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