Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1""" 

2axes3d.py, original mplot3d version by John Porter 

3Created: 23 Sep 2005 

4 

5Parts fixed by Reinier Heeres <reinier@heeres.eu> 

6Minor additions by Ben Axelrod <baxelrod@coroware.com> 

7Significant updates and revisions by Ben Root <ben.v.root@gmail.com> 

8 

9Module containing Axes3D, an object which can plot 3D objects on a 

102D matplotlib figure. 

11""" 

12 

13from collections import defaultdict 

14from functools import reduce 

15import math 

16 

17import numpy as np 

18 

19from matplotlib import artist 

20import matplotlib.axes as maxes 

21import matplotlib.cbook as cbook 

22import matplotlib.collections as mcoll 

23import matplotlib.colors as mcolors 

24import matplotlib.docstring as docstring 

25import matplotlib.scale as mscale 

26from matplotlib.axes import Axes, rcParams 

27from matplotlib.colors import Normalize, LightSource 

28from matplotlib.transforms import Bbox 

29from matplotlib.tri.triangulation import Triangulation 

30 

31from . import art3d 

32from . import proj3d 

33from . import axis3d 

34 

35 

36@cbook.deprecated("3.2", alternative="Bbox.unit()") 

37def unit_bbox(): 

38 box = Bbox(np.array([[0, 0], [1, 1]])) 

39 return box 

40 

41 

42class Axes3D(Axes): 

43 """ 

44 3D axes object. 

45 """ 

46 name = '3d' 

47 _shared_z_axes = cbook.Grouper() 

48 

49 @docstring.dedent_interpd 

50 def __init__( 

51 self, fig, rect=None, *args, 

52 azim=-60, elev=30, zscale=None, sharez=None, proj_type='persp', 

53 **kwargs): 

54 """ 

55 Parameters 

56 ---------- 

57 fig : Figure 

58 The parent figure. 

59 rect : (float, float, float, float) 

60 The ``(left, bottom, width, height)`` axes position. 

61 azim : float, optional 

62 Azimuthal viewing angle, defaults to -60. 

63 elev : float, optional 

64 Elevation viewing angle, defaults to 30. 

65 zscale : %(scale_type)s, optional 

66 The z scale. Note that currently, only a linear scale is 

67 supported. 

68 sharez : Axes3D, optional 

69 Other axes to share z-limits with. 

70 proj_type : {'persp', 'ortho'} 

71 The projection type, default 'persp'. 

72 

73 Notes 

74 ----- 

75 .. versionadded:: 1.2.1 

76 The *sharez* parameter. 

77 """ 

78 

79 if rect is None: 

80 rect = [0.0, 0.0, 1.0, 1.0] 

81 self._cids = [] 

82 

83 self.initial_azim = azim 

84 self.initial_elev = elev 

85 self.set_proj_type(proj_type) 

86 

87 self.xy_viewLim = Bbox.unit() 

88 self.zz_viewLim = Bbox.unit() 

89 self.xy_dataLim = Bbox.unit() 

90 self.zz_dataLim = Bbox.unit() 

91 # inhibit autoscale_view until the axes are defined 

92 # they can't be defined until Axes.__init__ has been called 

93 self.view_init(self.initial_elev, self.initial_azim) 

94 self._ready = 0 

95 

96 self._sharez = sharez 

97 if sharez is not None: 

98 self._shared_z_axes.join(self, sharez) 

99 self._adjustable = 'datalim' 

100 

101 super().__init__(fig, rect, frameon=True, *args, **kwargs) 

102 # Disable drawing of axes by base class 

103 super().set_axis_off() 

104 # Enable drawing of axes by Axes3D class 

105 self.set_axis_on() 

106 self.M = None 

107 

108 # func used to format z -- fall back on major formatters 

109 self.fmt_zdata = None 

110 

111 if zscale is not None: 

112 self.set_zscale(zscale) 

113 

114 if self.zaxis is not None: 

115 self._zcid = self.zaxis.callbacks.connect( 

116 'units finalize', lambda: self._on_units_changed(scalez=True)) 

117 else: 

118 self._zcid = None 

119 

120 self._ready = 1 

121 self.mouse_init() 

122 self.set_top_view() 

123 

124 self.patch.set_linewidth(0) 

125 # Calculate the pseudo-data width and height 

126 pseudo_bbox = self.transLimits.inverted().transform([(0, 0), (1, 1)]) 

127 self._pseudo_w, self._pseudo_h = pseudo_bbox[1] - pseudo_bbox[0] 

128 

129 self.figure.add_axes(self) 

130 

131 # mplot3d currently manages its own spines and needs these turned off 

132 # for bounding box calculations 

133 for k in self.spines.keys(): 

134 self.spines[k].set_visible(False) 

135 

136 def set_axis_off(self): 

137 self._axis3don = False 

138 self.stale = True 

139 

140 def set_axis_on(self): 

141 self._axis3don = True 

142 self.stale = True 

143 

144 def convert_zunits(self, z): 

145 """ 

146 For artists in an axes, if the zaxis has units support, 

147 convert *z* using zaxis unit type 

148 

149 .. versionadded:: 1.2.1 

150 

151 """ 

152 return self.zaxis.convert_units(z) 

153 

154 def _process_unit_info(self, xdata=None, ydata=None, zdata=None, 

155 kwargs=None): 

156 """ 

157 Look for unit *kwargs* and update the axis instances as necessary 

158 

159 """ 

160 super()._process_unit_info(xdata=xdata, ydata=ydata, kwargs=kwargs) 

161 

162 if self.xaxis is None or self.yaxis is None or self.zaxis is None: 

163 return 

164 

165 if zdata is not None: 

166 # we only need to update if there is nothing set yet. 

167 if not self.zaxis.have_units(): 

168 self.zaxis.update_units(xdata) 

169 

170 # process kwargs 2nd since these will override default units 

171 if kwargs is not None: 

172 zunits = kwargs.pop('zunits', self.zaxis.units) 

173 if zunits != self.zaxis.units: 

174 self.zaxis.set_units(zunits) 

175 # If the units being set imply a different converter, 

176 # we need to update. 

177 if zdata is not None: 

178 self.zaxis.update_units(zdata) 

179 

180 def set_top_view(self): 

181 # this happens to be the right view for the viewing coordinates 

182 # moved up and to the left slightly to fit labels and axes 

183 xdwl = 0.95 / self.dist 

184 xdw = 0.9 / self.dist 

185 ydwl = 0.95 / self.dist 

186 ydw = 0.9 / self.dist 

187 # This is purposely using the 2D Axes's set_xlim and set_ylim, 

188 # because we are trying to place our viewing pane. 

189 super().set_xlim(-xdwl, xdw, auto=None) 

190 super().set_ylim(-ydwl, ydw, auto=None) 

191 

192 def _init_axis(self): 

193 '''Init 3D axes; overrides creation of regular X/Y axes''' 

194 self.xaxis = axis3d.XAxis('x', self.xy_viewLim.intervalx, 

195 self.xy_dataLim.intervalx, self) 

196 self.yaxis = axis3d.YAxis('y', self.xy_viewLim.intervaly, 

197 self.xy_dataLim.intervaly, self) 

198 self.zaxis = axis3d.ZAxis('z', self.zz_viewLim.intervalx, 

199 self.zz_dataLim.intervalx, self) 

200 for ax in self.xaxis, self.yaxis, self.zaxis: 

201 ax.init3d() 

202 

203 def get_zaxis(self): 

204 '''Return the ``ZAxis`` (`~.axis3d.Axis`) instance.''' 

205 return self.zaxis 

206 

207 @cbook.deprecated("3.1", alternative="xaxis", pending=True) 

208 @property 

209 def w_xaxis(self): 

210 return self.xaxis 

211 

212 @cbook.deprecated("3.1", alternative="yaxis", pending=True) 

213 @property 

214 def w_yaxis(self): 

215 return self.yaxis 

216 

217 @cbook.deprecated("3.1", alternative="zaxis", pending=True) 

218 @property 

219 def w_zaxis(self): 

220 return self.zaxis 

221 

222 def _get_axis_list(self): 

223 return super()._get_axis_list() + (self.zaxis, ) 

224 

225 def unit_cube(self, vals=None): 

226 minx, maxx, miny, maxy, minz, maxz = vals or self.get_w_lims() 

227 return [(minx, miny, minz), 

228 (maxx, miny, minz), 

229 (maxx, maxy, minz), 

230 (minx, maxy, minz), 

231 (minx, miny, maxz), 

232 (maxx, miny, maxz), 

233 (maxx, maxy, maxz), 

234 (minx, maxy, maxz)] 

235 

236 def tunit_cube(self, vals=None, M=None): 

237 if M is None: 

238 M = self.M 

239 xyzs = self.unit_cube(vals) 

240 tcube = proj3d.proj_points(xyzs, M) 

241 return tcube 

242 

243 def tunit_edges(self, vals=None, M=None): 

244 tc = self.tunit_cube(vals, M) 

245 edges = [(tc[0], tc[1]), 

246 (tc[1], tc[2]), 

247 (tc[2], tc[3]), 

248 (tc[3], tc[0]), 

249 

250 (tc[0], tc[4]), 

251 (tc[1], tc[5]), 

252 (tc[2], tc[6]), 

253 (tc[3], tc[7]), 

254 

255 (tc[4], tc[5]), 

256 (tc[5], tc[6]), 

257 (tc[6], tc[7]), 

258 (tc[7], tc[4])] 

259 return edges 

260 

261 @artist.allow_rasterization 

262 def draw(self, renderer): 

263 # draw the background patch 

264 self.patch.draw(renderer) 

265 self._frameon = False 

266 

267 # first, set the aspect 

268 # this is duplicated from `axes._base._AxesBase.draw` 

269 # but must be called before any of the artist are drawn as 

270 # it adjusts the view limits and the size of the bounding box 

271 # of the axes 

272 locator = self.get_axes_locator() 

273 if locator: 

274 pos = locator(self, renderer) 

275 self.apply_aspect(pos) 

276 else: 

277 self.apply_aspect() 

278 

279 # add the projection matrix to the renderer 

280 self.M = self.get_proj() 

281 renderer.M = self.M 

282 renderer.vvec = self.vvec 

283 renderer.eye = self.eye 

284 renderer.get_axis_position = self.get_axis_position 

285 

286 # Calculate projection of collections and patches and zorder them. 

287 # Make sure they are drawn above the grids. 

288 zorder_offset = max(axis.get_zorder() 

289 for axis in self._get_axis_list()) + 1 

290 for i, col in enumerate( 

291 sorted(self.collections, 

292 key=lambda col: col.do_3d_projection(renderer), 

293 reverse=True)): 

294 col.zorder = zorder_offset + i 

295 for i, patch in enumerate( 

296 sorted(self.patches, 

297 key=lambda patch: patch.do_3d_projection(renderer), 

298 reverse=True)): 

299 patch.zorder = zorder_offset + i 

300 

301 if self._axis3don: 

302 # Draw panes first 

303 for axis in self._get_axis_list(): 

304 axis.draw_pane(renderer) 

305 # Then axes 

306 for axis in self._get_axis_list(): 

307 axis.draw(renderer) 

308 

309 # Then rest 

310 super().draw(renderer) 

311 

312 def get_axis_position(self): 

313 vals = self.get_w_lims() 

314 tc = self.tunit_cube(vals, self.M) 

315 xhigh = tc[1][2] > tc[2][2] 

316 yhigh = tc[3][2] > tc[2][2] 

317 zhigh = tc[0][2] > tc[2][2] 

318 return xhigh, yhigh, zhigh 

319 

320 def _on_units_changed(self, scalex=False, scaley=False, scalez=False): 

321 """ 

322 Callback for processing changes to axis units. 

323 

324 Currently forces updates of data limits and view limits. 

325 """ 

326 self.relim() 

327 self.autoscale_view(scalex=scalex, scaley=scaley, scalez=scalez) 

328 

329 def update_datalim(self, xys, **kwargs): 

330 pass 

331 

332 def get_autoscale_on(self): 

333 """ 

334 Get whether autoscaling is applied for all axes on plot commands 

335 

336 .. versionadded:: 1.1.0 

337 This function was added, but not tested. Please report any bugs. 

338 """ 

339 return super().get_autoscale_on() and self.get_autoscalez_on() 

340 

341 def get_autoscalez_on(self): 

342 """ 

343 Get whether autoscaling for the z-axis is applied on plot commands 

344 

345 .. versionadded:: 1.1.0 

346 This function was added, but not tested. Please report any bugs. 

347 """ 

348 return self._autoscaleZon 

349 

350 def set_autoscale_on(self, b): 

351 """ 

352 Set whether autoscaling is applied on plot commands 

353 

354 .. versionadded:: 1.1.0 

355 This function was added, but not tested. Please report any bugs. 

356 

357 Parameters 

358 ---------- 

359 b : bool 

360 """ 

361 super().set_autoscale_on(b) 

362 self.set_autoscalez_on(b) 

363 

364 def set_autoscalez_on(self, b): 

365 """ 

366 Set whether autoscaling for the z-axis is applied on plot commands 

367 

368 .. versionadded:: 1.1.0 

369 

370 Parameters 

371 ---------- 

372 b : bool 

373 """ 

374 self._autoscaleZon = b 

375 

376 def set_zmargin(self, m): 

377 """ 

378 Set padding of Z data limits prior to autoscaling. 

379 

380 *m* times the data interval will be added to each 

381 end of that interval before it is used in autoscaling. 

382 

383 accepts: float in range 0 to 1 

384 

385 .. versionadded:: 1.1.0 

386 """ 

387 if m < 0 or m > 1: 

388 raise ValueError("margin must be in range 0 to 1") 

389 self._zmargin = m 

390 self.stale = True 

391 

392 def margins(self, *margins, x=None, y=None, z=None, tight=True): 

393 """ 

394 Convenience method to set or retrieve autoscaling margins. 

395 

396 Call signatures:: 

397 

398 margins() 

399 

400 returns xmargin, ymargin, zmargin 

401 

402 :: 

403 

404 margins(margin) 

405 

406 margins(xmargin, ymargin, zmargin) 

407 

408 margins(x=xmargin, y=ymargin, z=zmargin) 

409 

410 margins(..., tight=False) 

411 

412 All forms above set the xmargin, ymargin and zmargin 

413 parameters. All keyword parameters are optional. A single 

414 positional argument specifies xmargin, ymargin and zmargin. 

415 Passing both positional and keyword arguments for xmargin, 

416 ymargin, and/or zmargin is invalid. 

417 

418 The *tight* parameter 

419 is passed to :meth:`autoscale_view`, which is executed after 

420 a margin is changed; the default here is *True*, on the 

421 assumption that when margins are specified, no additional 

422 padding to match tick marks is usually desired. Setting 

423 *tight* to *None* will preserve the previous setting. 

424 

425 Specifying any margin changes only the autoscaling; for example, 

426 if *xmargin* is not None, then *xmargin* times the X data 

427 interval will be added to each end of that interval before 

428 it is used in autoscaling. 

429 

430 .. versionadded:: 1.1.0 

431 """ 

432 if margins and x is not None and y is not None and z is not None: 

433 raise TypeError('Cannot pass both positional and keyword ' 

434 'arguments for x, y, and/or z.') 

435 elif len(margins) == 1: 

436 x = y = z = margins[0] 

437 elif len(margins) == 3: 

438 x, y, z = margins 

439 elif margins: 

440 raise TypeError('Must pass a single positional argument for all ' 

441 'margins, or one for each margin (x, y, z).') 

442 

443 if x is None and y is None and z is None: 

444 if tight is not True: 

445 cbook._warn_external(f'ignoring tight={tight!r} in get mode') 

446 return self._xmargin, self._ymargin, self._zmargin 

447 

448 if x is not None: 

449 self.set_xmargin(x) 

450 if y is not None: 

451 self.set_ymargin(y) 

452 if z is not None: 

453 self.set_zmargin(z) 

454 

455 self.autoscale_view( 

456 tight=tight, scalex=(x is not None), scaley=(y is not None), 

457 scalez=(z is not None) 

458 ) 

459 

460 def autoscale(self, enable=True, axis='both', tight=None): 

461 """ 

462 Convenience method for simple axis view autoscaling. 

463 See :meth:`matplotlib.axes.Axes.autoscale` for full explanation. 

464 Note that this function behaves the same, but for all 

465 three axes. Therefore, 'z' can be passed for *axis*, 

466 and 'both' applies to all three axes. 

467 

468 .. versionadded:: 1.1.0 

469 """ 

470 if enable is None: 

471 scalex = True 

472 scaley = True 

473 scalez = True 

474 else: 

475 if axis in ['x', 'both']: 

476 self._autoscaleXon = scalex = bool(enable) 

477 else: 

478 scalex = False 

479 if axis in ['y', 'both']: 

480 self._autoscaleYon = scaley = bool(enable) 

481 else: 

482 scaley = False 

483 if axis in ['z', 'both']: 

484 self._autoscaleZon = scalez = bool(enable) 

485 else: 

486 scalez = False 

487 self.autoscale_view(tight=tight, scalex=scalex, scaley=scaley, 

488 scalez=scalez) 

489 

490 def auto_scale_xyz(self, X, Y, Z=None, had_data=None): 

491 # This updates the bounding boxes as to keep a record as to what the 

492 # minimum sized rectangular volume holds the data. 

493 X = np.reshape(X, -1) 

494 Y = np.reshape(Y, -1) 

495 self.xy_dataLim.update_from_data_xy( 

496 np.column_stack([X, Y]), not had_data) 

497 if Z is not None: 

498 Z = np.reshape(Z, -1) 

499 self.zz_dataLim.update_from_data_xy( 

500 np.column_stack([Z, Z]), not had_data) 

501 # Let autoscale_view figure out how to use this data. 

502 self.autoscale_view() 

503 

504 def autoscale_view(self, tight=None, scalex=True, scaley=True, 

505 scalez=True): 

506 """ 

507 Autoscale the view limits using the data limits. 

508 See :meth:`matplotlib.axes.Axes.autoscale_view` for documentation. 

509 Note that this function applies to the 3D axes, and as such 

510 adds the *scalez* to the function arguments. 

511 

512 .. versionchanged:: 1.1.0 

513 Function signature was changed to better match the 2D version. 

514 *tight* is now explicitly a kwarg and placed first. 

515 

516 .. versionchanged:: 1.2.1 

517 This is now fully functional. 

518 

519 """ 

520 if not self._ready: 

521 return 

522 

523 # This method looks at the rectangular volume (see above) 

524 # of data and decides how to scale the view portal to fit it. 

525 if tight is None: 

526 # if image data only just use the datalim 

527 _tight = self._tight or ( 

528 len(self.images) > 0 

529 and len(self.lines) == len(self.patches) == 0) 

530 else: 

531 _tight = self._tight = bool(tight) 

532 

533 if scalex and self._autoscaleXon: 

534 self._shared_x_axes.clean() 

535 x0, x1 = self.xy_dataLim.intervalx 

536 xlocator = self.xaxis.get_major_locator() 

537 x0, x1 = xlocator.nonsingular(x0, x1) 

538 if self._xmargin > 0: 

539 delta = (x1 - x0) * self._xmargin 

540 x0 -= delta 

541 x1 += delta 

542 if not _tight: 

543 x0, x1 = xlocator.view_limits(x0, x1) 

544 self.set_xbound(x0, x1) 

545 

546 if scaley and self._autoscaleYon: 

547 self._shared_y_axes.clean() 

548 y0, y1 = self.xy_dataLim.intervaly 

549 ylocator = self.yaxis.get_major_locator() 

550 y0, y1 = ylocator.nonsingular(y0, y1) 

551 if self._ymargin > 0: 

552 delta = (y1 - y0) * self._ymargin 

553 y0 -= delta 

554 y1 += delta 

555 if not _tight: 

556 y0, y1 = ylocator.view_limits(y0, y1) 

557 self.set_ybound(y0, y1) 

558 

559 if scalez and self._autoscaleZon: 

560 self._shared_z_axes.clean() 

561 z0, z1 = self.zz_dataLim.intervalx 

562 zlocator = self.zaxis.get_major_locator() 

563 z0, z1 = zlocator.nonsingular(z0, z1) 

564 if self._zmargin > 0: 

565 delta = (z1 - z0) * self._zmargin 

566 z0 -= delta 

567 z1 += delta 

568 if not _tight: 

569 z0, z1 = zlocator.view_limits(z0, z1) 

570 self.set_zbound(z0, z1) 

571 

572 def get_w_lims(self): 

573 '''Get 3D world limits.''' 

574 minx, maxx = self.get_xlim3d() 

575 miny, maxy = self.get_ylim3d() 

576 minz, maxz = self.get_zlim3d() 

577 return minx, maxx, miny, maxy, minz, maxz 

578 

579 def set_xlim3d(self, left=None, right=None, emit=True, auto=False, 

580 *, xmin=None, xmax=None): 

581 """ 

582 Set 3D x limits. 

583 

584 See :meth:`matplotlib.axes.Axes.set_xlim` for full documentation. 

585 

586 """ 

587 if right is None and np.iterable(left): 

588 left, right = left 

589 if xmin is not None: 

590 cbook.warn_deprecated('3.0', name='`xmin`', 

591 alternative='`left`', obj_type='argument') 

592 if left is not None: 

593 raise TypeError('Cannot pass both `xmin` and `left`') 

594 left = xmin 

595 if xmax is not None: 

596 cbook.warn_deprecated('3.0', name='`xmax`', 

597 alternative='`right`', obj_type='argument') 

598 if right is not None: 

599 raise TypeError('Cannot pass both `xmax` and `right`') 

600 right = xmax 

601 

602 self._process_unit_info(xdata=(left, right)) 

603 left = self._validate_converted_limits(left, self.convert_xunits) 

604 right = self._validate_converted_limits(right, self.convert_xunits) 

605 

606 old_left, old_right = self.get_xlim() 

607 if left is None: 

608 left = old_left 

609 if right is None: 

610 right = old_right 

611 

612 if left == right: 

613 cbook._warn_external( 

614 f"Attempting to set identical left == right == {left} results " 

615 f"in singular transformations; automatically expanding.") 

616 reverse = left > right 

617 left, right = self.xaxis.get_major_locator().nonsingular(left, right) 

618 left, right = self.xaxis.limit_range_for_scale(left, right) 

619 # cast to bool to avoid bad interaction between python 3.8 and np.bool_ 

620 left, right = sorted([left, right], reverse=bool(reverse)) 

621 self.xy_viewLim.intervalx = (left, right) 

622 

623 if auto is not None: 

624 self._autoscaleXon = bool(auto) 

625 

626 if emit: 

627 self.callbacks.process('xlim_changed', self) 

628 # Call all of the other x-axes that are shared with this one 

629 for other in self._shared_x_axes.get_siblings(self): 

630 if other is not self: 

631 other.set_xlim(self.xy_viewLim.intervalx, 

632 emit=False, auto=auto) 

633 if other.figure != self.figure: 

634 other.figure.canvas.draw_idle() 

635 self.stale = True 

636 return left, right 

637 set_xlim = set_xlim3d 

638 

639 def set_ylim3d(self, bottom=None, top=None, emit=True, auto=False, 

640 *, ymin=None, ymax=None): 

641 """ 

642 Set 3D y limits. 

643 

644 See :meth:`matplotlib.axes.Axes.set_ylim` for full documentation. 

645 

646 """ 

647 if top is None and np.iterable(bottom): 

648 bottom, top = bottom 

649 if ymin is not None: 

650 cbook.warn_deprecated('3.0', name='`ymin`', 

651 alternative='`bottom`', obj_type='argument') 

652 if bottom is not None: 

653 raise TypeError('Cannot pass both `ymin` and `bottom`') 

654 bottom = ymin 

655 if ymax is not None: 

656 cbook.warn_deprecated('3.0', name='`ymax`', 

657 alternative='`top`', obj_type='argument') 

658 if top is not None: 

659 raise TypeError('Cannot pass both `ymax` and `top`') 

660 top = ymax 

661 

662 self._process_unit_info(ydata=(bottom, top)) 

663 bottom = self._validate_converted_limits(bottom, self.convert_yunits) 

664 top = self._validate_converted_limits(top, self.convert_yunits) 

665 

666 old_bottom, old_top = self.get_ylim() 

667 if bottom is None: 

668 bottom = old_bottom 

669 if top is None: 

670 top = old_top 

671 

672 if bottom == top: 

673 cbook._warn_external( 

674 f"Attempting to set identical bottom == top == {bottom} " 

675 f"results in singular transformations; automatically " 

676 f"expanding.") 

677 swapped = bottom > top 

678 bottom, top = self.yaxis.get_major_locator().nonsingular(bottom, top) 

679 bottom, top = self.yaxis.limit_range_for_scale(bottom, top) 

680 if swapped: 

681 bottom, top = top, bottom 

682 self.xy_viewLim.intervaly = (bottom, top) 

683 

684 if auto is not None: 

685 self._autoscaleYon = bool(auto) 

686 

687 if emit: 

688 self.callbacks.process('ylim_changed', self) 

689 # Call all of the other y-axes that are shared with this one 

690 for other in self._shared_y_axes.get_siblings(self): 

691 if other is not self: 

692 other.set_ylim(self.xy_viewLim.intervaly, 

693 emit=False, auto=auto) 

694 if other.figure != self.figure: 

695 other.figure.canvas.draw_idle() 

696 self.stale = True 

697 return bottom, top 

698 set_ylim = set_ylim3d 

699 

700 def set_zlim3d(self, bottom=None, top=None, emit=True, auto=False, 

701 *, zmin=None, zmax=None): 

702 """ 

703 Set 3D z limits. 

704 

705 See :meth:`matplotlib.axes.Axes.set_ylim` for full documentation 

706 

707 """ 

708 if top is None and np.iterable(bottom): 

709 bottom, top = bottom 

710 if zmin is not None: 

711 cbook.warn_deprecated('3.0', name='`zmin`', 

712 alternative='`bottom`', obj_type='argument') 

713 if bottom is not None: 

714 raise TypeError('Cannot pass both `zmin` and `bottom`') 

715 bottom = zmin 

716 if zmax is not None: 

717 cbook.warn_deprecated('3.0', name='`zmax`', 

718 alternative='`top`', obj_type='argument') 

719 if top is not None: 

720 raise TypeError('Cannot pass both `zmax` and `top`') 

721 top = zmax 

722 

723 self._process_unit_info(zdata=(bottom, top)) 

724 bottom = self._validate_converted_limits(bottom, self.convert_zunits) 

725 top = self._validate_converted_limits(top, self.convert_zunits) 

726 

727 old_bottom, old_top = self.get_zlim() 

728 if bottom is None: 

729 bottom = old_bottom 

730 if top is None: 

731 top = old_top 

732 

733 if bottom == top: 

734 cbook._warn_external( 

735 f"Attempting to set identical bottom == top == {bottom} " 

736 f"results in singular transformations; automatically " 

737 f"expanding.") 

738 swapped = bottom > top 

739 bottom, top = self.zaxis.get_major_locator().nonsingular(bottom, top) 

740 bottom, top = self.zaxis.limit_range_for_scale(bottom, top) 

741 if swapped: 

742 bottom, top = top, bottom 

743 self.zz_viewLim.intervalx = (bottom, top) 

744 

745 if auto is not None: 

746 self._autoscaleZon = bool(auto) 

747 

748 if emit: 

749 self.callbacks.process('zlim_changed', self) 

750 # Call all of the other y-axes that are shared with this one 

751 for other in self._shared_z_axes.get_siblings(self): 

752 if other is not self: 

753 other.set_zlim(self.zz_viewLim.intervalx, 

754 emit=False, auto=auto) 

755 if other.figure != self.figure: 

756 other.figure.canvas.draw_idle() 

757 self.stale = True 

758 return bottom, top 

759 set_zlim = set_zlim3d 

760 

761 def get_xlim3d(self): 

762 return tuple(self.xy_viewLim.intervalx) 

763 get_xlim3d.__doc__ = maxes.Axes.get_xlim.__doc__ 

764 get_xlim = get_xlim3d 

765 if get_xlim.__doc__ is not None: 

766 get_xlim.__doc__ += """ 

767 .. versionchanged:: 1.1.0 

768 This function now correctly refers to the 3D x-limits 

769 """ 

770 

771 def get_ylim3d(self): 

772 return tuple(self.xy_viewLim.intervaly) 

773 get_ylim3d.__doc__ = maxes.Axes.get_ylim.__doc__ 

774 get_ylim = get_ylim3d 

775 if get_ylim.__doc__ is not None: 

776 get_ylim.__doc__ += """ 

777 .. versionchanged:: 1.1.0 

778 This function now correctly refers to the 3D y-limits. 

779 """ 

780 

781 def get_zlim3d(self): 

782 '''Get 3D z limits.''' 

783 return tuple(self.zz_viewLim.intervalx) 

784 get_zlim = get_zlim3d 

785 

786 def get_zscale(self): 

787 """ 

788 Return the zaxis scale string %s 

789 

790 """ % (", ".join(mscale.get_scale_names())) 

791 return self.zaxis.get_scale() 

792 

793 # We need to slightly redefine these to pass scalez=False 

794 # to their calls of autoscale_view. 

795 

796 def set_xscale(self, value, **kwargs): 

797 self.xaxis._set_scale(value, **kwargs) 

798 self.autoscale_view(scaley=False, scalez=False) 

799 self._update_transScale() 

800 self.stale = True 

801 

802 def set_yscale(self, value, **kwargs): 

803 self.yaxis._set_scale(value, **kwargs) 

804 self.autoscale_view(scalex=False, scalez=False) 

805 self._update_transScale() 

806 self.stale = True 

807 

808 def set_zscale(self, value, **kwargs): 

809 self.zaxis._set_scale(value, **kwargs) 

810 self.autoscale_view(scalex=False, scaley=False) 

811 self._update_transScale() 

812 self.stale = True 

813 

814 set_xscale.__doc__, set_yscale.__doc__, set_zscale.__doc__ = map( 

815 """ 

816 Set the {}-axis scale. 

817 

818 Parameters 

819 ---------- 

820 value : {{"linear"}} 

821 The axis scale type to apply. 3D axes currently only support 

822 linear scales; other scales yield nonsensical results. 

823 

824 **kwargs 

825 Keyword arguments are nominally forwarded to the scale class, but 

826 none of them is applicable for linear scales. 

827 """.format, 

828 ["x", "y", "z"]) 

829 

830 def set_zticks(self, *args, **kwargs): 

831 """ 

832 Set z-axis tick locations. 

833 See :meth:`matplotlib.axes.Axes.set_yticks` for more details. 

834 

835 .. note:: 

836 Minor ticks are not supported. 

837 

838 .. versionadded:: 1.1.0 

839 """ 

840 return self.zaxis.set_ticks(*args, **kwargs) 

841 

842 @cbook._make_keyword_only("3.2", "minor") 

843 def get_zticks(self, minor=False): 

844 """ 

845 Return the z ticks as a list of locations 

846 See :meth:`matplotlib.axes.Axes.get_yticks` for more details. 

847 

848 .. note:: 

849 Minor ticks are not supported. 

850 

851 .. versionadded:: 1.1.0 

852 """ 

853 return self.zaxis.get_ticklocs(minor=minor) 

854 

855 def get_zmajorticklabels(self): 

856 """ 

857 Get the ztick labels as a list of Text instances 

858 

859 .. versionadded:: 1.1.0 

860 """ 

861 return self.zaxis.get_majorticklabels() 

862 

863 def get_zminorticklabels(self): 

864 """ 

865 Get the ztick labels as a list of Text instances 

866 

867 .. note:: 

868 Minor ticks are not supported. This function was added 

869 only for completeness. 

870 

871 .. versionadded:: 1.1.0 

872 """ 

873 return self.zaxis.get_minorticklabels() 

874 

875 def set_zticklabels(self, *args, **kwargs): 

876 """ 

877 Set z-axis tick labels. 

878 See :meth:`matplotlib.axes.Axes.set_yticklabels` for more details. 

879 

880 .. note:: 

881 Minor ticks are not supported by Axes3D objects. 

882 

883 .. versionadded:: 1.1.0 

884 """ 

885 return self.zaxis.set_ticklabels(*args, **kwargs) 

886 

887 def get_zticklabels(self, minor=False): 

888 """ 

889 Get ztick labels as a list of Text instances. 

890 See :meth:`matplotlib.axes.Axes.get_yticklabels` for more details. 

891 

892 .. note:: 

893 Minor ticks are not supported. 

894 

895 .. versionadded:: 1.1.0 

896 """ 

897 return self.zaxis.get_ticklabels(minor=minor) 

898 

899 def zaxis_date(self, tz=None): 

900 """ 

901 Sets up z-axis ticks and labels that treat the z data as dates. 

902 

903 *tz* is a timezone string or :class:`tzinfo` instance. 

904 Defaults to rc value. 

905 

906 .. note:: 

907 This function is merely provided for completeness. 

908 Axes3D objects do not officially support dates for ticks, 

909 and so this may or may not work as expected. 

910 

911 .. versionadded:: 1.1.0 

912 This function was added, but not tested. Please report any bugs. 

913 """ 

914 self.zaxis.axis_date(tz) 

915 

916 def get_zticklines(self): 

917 """ 

918 Get ztick lines as a list of Line2D instances. 

919 Note that this function is provided merely for completeness. 

920 These lines are re-calculated as the display changes. 

921 

922 .. versionadded:: 1.1.0 

923 """ 

924 return self.zaxis.get_ticklines() 

925 

926 def clabel(self, *args, **kwargs): 

927 """ 

928 This function is currently not implemented for 3D axes. 

929 Returns *None*. 

930 """ 

931 return None 

932 

933 def view_init(self, elev=None, azim=None): 

934 """ 

935 Set the elevation and azimuth of the axes in degrees (not radians). 

936 

937 This can be used to rotate the axes programmatically. 

938 

939 'elev' stores the elevation angle in the z plane (in degrees). 

940 'azim' stores the azimuth angle in the (x, y) plane (in degrees). 

941 

942 if elev or azim are None (default), then the initial value 

943 is used which was specified in the :class:`Axes3D` constructor. 

944 """ 

945 

946 self.dist = 10 

947 

948 if elev is None: 

949 self.elev = self.initial_elev 

950 else: 

951 self.elev = elev 

952 

953 if azim is None: 

954 self.azim = self.initial_azim 

955 else: 

956 self.azim = azim 

957 

958 def set_proj_type(self, proj_type): 

959 """ 

960 Set the projection type. 

961 

962 Parameters 

963 ---------- 

964 proj_type : {'persp', 'ortho'} 

965 """ 

966 self._projection = cbook._check_getitem({ 

967 'persp': proj3d.persp_transformation, 

968 'ortho': proj3d.ortho_transformation, 

969 }, proj_type=proj_type) 

970 

971 def get_proj(self): 

972 """ 

973 Create the projection matrix from the current viewing position. 

974 

975 elev stores the elevation angle in the z plane 

976 azim stores the azimuth angle in the (x, y) plane 

977 

978 dist is the distance of the eye viewing point from the object point. 

979 """ 

980 relev, razim = np.pi * self.elev/180, np.pi * self.azim/180 

981 

982 xmin, xmax = self.get_xlim3d() 

983 ymin, ymax = self.get_ylim3d() 

984 zmin, zmax = self.get_zlim3d() 

985 

986 # transform to uniform world coordinates 0-1, 0-1, 0-1 

987 worldM = proj3d.world_transformation(xmin, xmax, 

988 ymin, ymax, 

989 zmin, zmax) 

990 

991 # look into the middle of the new coordinates 

992 R = np.array([0.5, 0.5, 0.5]) 

993 

994 xp = R[0] + np.cos(razim) * np.cos(relev) * self.dist 

995 yp = R[1] + np.sin(razim) * np.cos(relev) * self.dist 

996 zp = R[2] + np.sin(relev) * self.dist 

997 E = np.array((xp, yp, zp)) 

998 

999 self.eye = E 

1000 self.vvec = R - E 

1001 self.vvec = self.vvec / np.linalg.norm(self.vvec) 

1002 

1003 if abs(relev) > np.pi/2: 

1004 # upside down 

1005 V = np.array((0, 0, -1)) 

1006 else: 

1007 V = np.array((0, 0, 1)) 

1008 zfront, zback = -self.dist, self.dist 

1009 

1010 viewM = proj3d.view_transformation(E, R, V) 

1011 projM = self._projection(zfront, zback) 

1012 M0 = np.dot(viewM, worldM) 

1013 M = np.dot(projM, M0) 

1014 return M 

1015 

1016 def mouse_init(self, rotate_btn=1, zoom_btn=3): 

1017 """ 

1018 Initializes mouse button callbacks to enable 3D rotation of the axes. 

1019 Also optionally sets the mouse buttons for 3D rotation and zooming. 

1020 

1021 Parameters 

1022 ---------- 

1023 rotate_btn : int or list of int 

1024 The mouse button or buttons to use for 3D rotation of the axes; 

1025 defaults to 1. 

1026 zoom_btn : int or list of int 

1027 The mouse button or buttons to use to zoom the 3D axes; defaults to 

1028 3. 

1029 """ 

1030 self.button_pressed = None 

1031 self._cids = [ 

1032 self.figure.canvas.mpl_connect( 

1033 'motion_notify_event', self._on_move), 

1034 self.figure.canvas.mpl_connect( 

1035 'button_press_event', self._button_press), 

1036 self.figure.canvas.mpl_connect( 

1037 'button_release_event', self._button_release), 

1038 ] 

1039 # coerce scalars into array-like, then convert into 

1040 # a regular list to avoid comparisons against None 

1041 # which breaks in recent versions of numpy. 

1042 self._rotate_btn = np.atleast_1d(rotate_btn).tolist() 

1043 self._zoom_btn = np.atleast_1d(zoom_btn).tolist() 

1044 

1045 def can_zoom(self): 

1046 """ 

1047 Return *True* if this axes supports the zoom box button functionality. 

1048 

1049 3D axes objects do not use the zoom box button. 

1050 """ 

1051 return False 

1052 

1053 def can_pan(self): 

1054 """ 

1055 Return *True* if this axes supports the pan/zoom button functionality. 

1056 

1057 3D axes objects do not use the pan/zoom button. 

1058 """ 

1059 return False 

1060 

1061 def cla(self): 

1062 # docstring inherited. 

1063 

1064 super().cla() 

1065 self.zaxis.cla() 

1066 

1067 if self._sharez is not None: 

1068 self.zaxis.major = self._sharez.zaxis.major 

1069 self.zaxis.minor = self._sharez.zaxis.minor 

1070 z0, z1 = self._sharez.get_zlim() 

1071 self.set_zlim(z0, z1, emit=False, auto=None) 

1072 self.zaxis._set_scale(self._sharez.zaxis.get_scale()) 

1073 else: 

1074 self.zaxis._set_scale('linear') 

1075 try: 

1076 self.set_zlim(0, 1) 

1077 except TypeError: 

1078 pass 

1079 

1080 self._autoscaleZon = True 

1081 self._zmargin = 0 

1082 

1083 self.grid(rcParams['axes3d.grid']) 

1084 

1085 def disable_mouse_rotation(self): 

1086 """Disable mouse button callbacks.""" 

1087 # Disconnect the various events we set. 

1088 for cid in self._cids: 

1089 self.figure.canvas.mpl_disconnect(cid) 

1090 self._cids = [] 

1091 

1092 def _button_press(self, event): 

1093 if event.inaxes == self: 

1094 self.button_pressed = event.button 

1095 self.sx, self.sy = event.xdata, event.ydata 

1096 

1097 def _button_release(self, event): 

1098 self.button_pressed = None 

1099 

1100 def format_zdata(self, z): 

1101 """ 

1102 Return *z* string formatted. This function will use the 

1103 :attr:`fmt_zdata` attribute if it is callable, else will fall 

1104 back on the zaxis major formatter 

1105 """ 

1106 try: 

1107 return self.fmt_zdata(z) 

1108 except (AttributeError, TypeError): 

1109 func = self.zaxis.get_major_formatter().format_data_short 

1110 val = func(z) 

1111 return val 

1112 

1113 def format_coord(self, xd, yd): 

1114 """ 

1115 Given the 2D view coordinates attempt to guess a 3D coordinate. 

1116 Looks for the nearest edge to the point and then assumes that 

1117 the point is at the same z location as the nearest point on the edge. 

1118 """ 

1119 

1120 if self.M is None: 

1121 return '' 

1122 

1123 if self.button_pressed in self._rotate_btn: 

1124 return 'azimuth={:.0f} deg, elevation={:.0f} deg '.format( 

1125 self.azim, self.elev) 

1126 # ignore xd and yd and display angles instead 

1127 

1128 # nearest edge 

1129 p0, p1 = min(self.tunit_edges(), 

1130 key=lambda edge: proj3d._line2d_seg_dist( 

1131 edge[0], edge[1], (xd, yd))) 

1132 

1133 # scale the z value to match 

1134 x0, y0, z0 = p0 

1135 x1, y1, z1 = p1 

1136 d0 = np.hypot(x0-xd, y0-yd) 

1137 d1 = np.hypot(x1-xd, y1-yd) 

1138 dt = d0+d1 

1139 z = d1/dt * z0 + d0/dt * z1 

1140 

1141 x, y, z = proj3d.inv_transform(xd, yd, z, self.M) 

1142 

1143 xs = self.format_xdata(x) 

1144 ys = self.format_ydata(y) 

1145 zs = self.format_zdata(z) 

1146 return 'x=%s, y=%s, z=%s' % (xs, ys, zs) 

1147 

1148 def _on_move(self, event): 

1149 """Mouse moving 

1150 

1151 button-1 rotates by default. Can be set explicitly in mouse_init(). 

1152 button-3 zooms by default. Can be set explicitly in mouse_init(). 

1153 """ 

1154 

1155 if not self.button_pressed: 

1156 return 

1157 

1158 if self.M is None: 

1159 return 

1160 

1161 x, y = event.xdata, event.ydata 

1162 # In case the mouse is out of bounds. 

1163 if x is None: 

1164 return 

1165 

1166 dx, dy = x - self.sx, y - self.sy 

1167 w = self._pseudo_w 

1168 h = self._pseudo_h 

1169 self.sx, self.sy = x, y 

1170 

1171 # Rotation 

1172 if self.button_pressed in self._rotate_btn: 

1173 # rotate viewing point 

1174 # get the x and y pixel coords 

1175 if dx == 0 and dy == 0: 

1176 return 

1177 self.elev = art3d._norm_angle(self.elev - (dy/h)*180) 

1178 self.azim = art3d._norm_angle(self.azim - (dx/w)*180) 

1179 self.get_proj() 

1180 self.stale = True 

1181 self.figure.canvas.draw_idle() 

1182 

1183# elif self.button_pressed == 2: 

1184 # pan view 

1185 # project xv, yv, zv -> xw, yw, zw 

1186 # pan 

1187# pass 

1188 

1189 # Zoom 

1190 elif self.button_pressed in self._zoom_btn: 

1191 # zoom view 

1192 # hmmm..this needs some help from clipping.... 

1193 minx, maxx, miny, maxy, minz, maxz = self.get_w_lims() 

1194 df = 1-((h - dy)/h) 

1195 dx = (maxx-minx)*df 

1196 dy = (maxy-miny)*df 

1197 dz = (maxz-minz)*df 

1198 self.set_xlim3d(minx - dx, maxx + dx) 

1199 self.set_ylim3d(miny - dy, maxy + dy) 

1200 self.set_zlim3d(minz - dz, maxz + dz) 

1201 self.get_proj() 

1202 self.figure.canvas.draw_idle() 

1203 

1204 def set_zlabel(self, zlabel, fontdict=None, labelpad=None, **kwargs): 

1205 ''' 

1206 Set zlabel. See doc for :meth:`set_ylabel` for description. 

1207 ''' 

1208 if labelpad is not None: 

1209 self.zaxis.labelpad = labelpad 

1210 return self.zaxis.set_label_text(zlabel, fontdict, **kwargs) 

1211 

1212 def get_zlabel(self): 

1213 """ 

1214 Get the z-label text string. 

1215 

1216 .. versionadded:: 1.1.0 

1217 This function was added, but not tested. Please report any bugs. 

1218 """ 

1219 label = self.zaxis.get_label() 

1220 return label.get_text() 

1221 

1222 # Axes rectangle characteristics 

1223 

1224 def get_frame_on(self): 

1225 """Get whether the 3D axes panels are drawn.""" 

1226 return self._frameon 

1227 

1228 def set_frame_on(self, b): 

1229 """ 

1230 Set whether the 3D axes panels are drawn. 

1231 

1232 Parameters 

1233 ---------- 

1234 b : bool 

1235 """ 

1236 self._frameon = bool(b) 

1237 self.stale = True 

1238 

1239 def grid(self, b=True, **kwargs): 

1240 ''' 

1241 Set / unset 3D grid. 

1242 

1243 .. note:: 

1244 

1245 Currently, this function does not behave the same as 

1246 :meth:`matplotlib.axes.Axes.grid`, but it is intended to 

1247 eventually support that behavior. 

1248 

1249 .. versionadded:: 1.1.0 

1250 ''' 

1251 # TODO: Operate on each axes separately 

1252 if len(kwargs): 

1253 b = True 

1254 self._draw_grid = b 

1255 self.stale = True 

1256 

1257 def locator_params(self, axis='both', tight=None, **kwargs): 

1258 """ 

1259 Convenience method for controlling tick locators. 

1260 

1261 See :meth:`matplotlib.axes.Axes.locator_params` for full 

1262 documentation. Note that this is for Axes3D objects, 

1263 therefore, setting *axis* to 'both' will result in the 

1264 parameters being set for all three axes. Also, *axis* 

1265 can also take a value of 'z' to apply parameters to the 

1266 z axis. 

1267 

1268 .. versionadded:: 1.1.0 

1269 This function was added, but not tested. Please report any bugs. 

1270 """ 

1271 _x = axis in ['x', 'both'] 

1272 _y = axis in ['y', 'both'] 

1273 _z = axis in ['z', 'both'] 

1274 if _x: 

1275 self.xaxis.get_major_locator().set_params(**kwargs) 

1276 if _y: 

1277 self.yaxis.get_major_locator().set_params(**kwargs) 

1278 if _z: 

1279 self.zaxis.get_major_locator().set_params(**kwargs) 

1280 self.autoscale_view(tight=tight, scalex=_x, scaley=_y, scalez=_z) 

1281 

1282 def tick_params(self, axis='both', **kwargs): 

1283 """ 

1284 Convenience method for changing the appearance of ticks and 

1285 tick labels. 

1286 

1287 See :meth:`matplotlib.axes.Axes.tick_params` for more complete 

1288 documentation. 

1289 

1290 The only difference is that setting *axis* to 'both' will 

1291 mean that the settings are applied to all three axes. Also, 

1292 the *axis* parameter also accepts a value of 'z', which 

1293 would mean to apply to only the z-axis. 

1294 

1295 Also, because of how Axes3D objects are drawn very differently 

1296 from regular 2D axes, some of these settings may have 

1297 ambiguous meaning. For simplicity, the 'z' axis will 

1298 accept settings as if it was like the 'y' axis. 

1299 

1300 .. note:: 

1301 Axes3D currently ignores some of these settings. 

1302 

1303 .. versionadded:: 1.1.0 

1304 """ 

1305 cbook._check_in_list(['x', 'y', 'z', 'both'], axis=axis) 

1306 if axis in ['x', 'y', 'both']: 

1307 super().tick_params(axis, **kwargs) 

1308 if axis in ['z', 'both']: 

1309 zkw = dict(kwargs) 

1310 zkw.pop('top', None) 

1311 zkw.pop('bottom', None) 

1312 zkw.pop('labeltop', None) 

1313 zkw.pop('labelbottom', None) 

1314 self.zaxis.set_tick_params(**zkw) 

1315 

1316 # data limits, ticks, tick labels, and formatting 

1317 

1318 def invert_zaxis(self): 

1319 """ 

1320 Invert the z-axis. 

1321 

1322 .. versionadded:: 1.1.0 

1323 This function was added, but not tested. Please report any bugs. 

1324 """ 

1325 bottom, top = self.get_zlim() 

1326 self.set_zlim(top, bottom, auto=None) 

1327 

1328 def zaxis_inverted(self): 

1329 ''' 

1330 Returns True if the z-axis is inverted. 

1331 

1332 .. versionadded:: 1.1.0 

1333 ''' 

1334 bottom, top = self.get_zlim() 

1335 return top < bottom 

1336 

1337 def get_zbound(self): 

1338 """ 

1339 Return the lower and upper z-axis bounds, in increasing order. 

1340 

1341 .. versionadded:: 1.1.0 

1342 """ 

1343 bottom, top = self.get_zlim() 

1344 if bottom < top: 

1345 return bottom, top 

1346 else: 

1347 return top, bottom 

1348 

1349 def set_zbound(self, lower=None, upper=None): 

1350 """ 

1351 Set the lower and upper numerical bounds of the z-axis. 

1352 This method will honor axes inversion regardless of parameter order. 

1353 It will not change the :attr:`_autoscaleZon` attribute. 

1354 

1355 .. versionadded:: 1.1.0 

1356 """ 

1357 if upper is None and np.iterable(lower): 

1358 lower, upper = lower 

1359 old_lower, old_upper = self.get_zbound() 

1360 if lower is None: 

1361 lower = old_lower 

1362 if upper is None: 

1363 upper = old_upper 

1364 

1365 if self.zaxis_inverted(): 

1366 if lower < upper: 

1367 self.set_zlim(upper, lower, auto=None) 

1368 else: 

1369 self.set_zlim(lower, upper, auto=None) 

1370 else: 

1371 if lower < upper: 

1372 self.set_zlim(lower, upper, auto=None) 

1373 else: 

1374 self.set_zlim(upper, lower, auto=None) 

1375 

1376 def text(self, x, y, z, s, zdir=None, **kwargs): 

1377 ''' 

1378 Add text to the plot. kwargs will be passed on to Axes.text, 

1379 except for the `zdir` keyword, which sets the direction to be 

1380 used as the z direction. 

1381 ''' 

1382 text = super().text(x, y, s, **kwargs) 

1383 art3d.text_2d_to_3d(text, z, zdir) 

1384 return text 

1385 

1386 text3D = text 

1387 text2D = Axes.text 

1388 

1389 def plot(self, xs, ys, *args, zdir='z', **kwargs): 

1390 """ 

1391 Plot 2D or 3D data. 

1392 

1393 Parameters 

1394 ---------- 

1395 xs : 1D array-like 

1396 x coordinates of vertices. 

1397 ys : 1D array-like 

1398 y coordinates of vertices. 

1399 zs : scalar or 1D array-like 

1400 z coordinates of vertices; either one for all points or one for 

1401 each point. 

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

1403 When plotting 2D data, the direction to use as z ('x', 'y' or 'z'); 

1404 defaults to 'z'. 

1405 **kwargs 

1406 Other arguments are forwarded to `matplotlib.axes.Axes.plot`. 

1407 """ 

1408 had_data = self.has_data() 

1409 

1410 # `zs` can be passed positionally or as keyword; checking whether 

1411 # args[0] is a string matches the behavior of 2D `plot` (via 

1412 # `_process_plot_var_args`). 

1413 if args and not isinstance(args[0], str): 

1414 zs, *args = args 

1415 if 'zs' in kwargs: 

1416 raise TypeError("plot() for multiple values for argument 'z'") 

1417 else: 

1418 zs = kwargs.pop('zs', 0) 

1419 

1420 # Match length 

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

1422 

1423 lines = super().plot(xs, ys, *args, **kwargs) 

1424 for line in lines: 

1425 art3d.line_2d_to_3d(line, zs=zs, zdir=zdir) 

1426 

1427 xs, ys, zs = art3d.juggle_axes(xs, ys, zs, zdir) 

1428 self.auto_scale_xyz(xs, ys, zs, had_data) 

1429 return lines 

1430 

1431 plot3D = plot 

1432 

1433 def plot_surface(self, X, Y, Z, *args, norm=None, vmin=None, 

1434 vmax=None, lightsource=None, **kwargs): 

1435 """ 

1436 Create a surface plot. 

1437 

1438 By default it will be colored in shades of a solid color, but it also 

1439 supports color mapping by supplying the *cmap* argument. 

1440 

1441 .. note:: 

1442 

1443 The *rcount* and *ccount* kwargs, which both default to 50, 

1444 determine the maximum number of samples used in each direction. If 

1445 the input data is larger, it will be downsampled (by slicing) to 

1446 these numbers of points. 

1447 

1448 Parameters 

1449 ---------- 

1450 X, Y, Z : 2d arrays 

1451 Data values. 

1452 

1453 rcount, ccount : int 

1454 Maximum number of samples used in each direction. If the input 

1455 data is larger, it will be downsampled (by slicing) to these 

1456 numbers of points. Defaults to 50. 

1457 

1458 .. versionadded:: 2.0 

1459 

1460 rstride, cstride : int 

1461 Downsampling stride in each direction. These arguments are 

1462 mutually exclusive with *rcount* and *ccount*. If only one of 

1463 *rstride* or *cstride* is set, the other defaults to 10. 

1464 

1465 'classic' mode uses a default of ``rstride = cstride = 10`` instead 

1466 of the new default of ``rcount = ccount = 50``. 

1467 

1468 color : color-like 

1469 Color of the surface patches. 

1470 

1471 cmap : Colormap 

1472 Colormap of the surface patches. 

1473 

1474 facecolors : array-like of colors. 

1475 Colors of each individual patch. 

1476 

1477 norm : Normalize 

1478 Normalization for the colormap. 

1479 

1480 vmin, vmax : float 

1481 Bounds for the normalization. 

1482 

1483 shade : bool 

1484 Whether to shade the facecolors. Defaults to True. Shading is 

1485 always disabled when `cmap` is specified. 

1486 

1487 lightsource : `~matplotlib.colors.LightSource` 

1488 The lightsource to use when `shade` is True. 

1489 

1490 **kwargs 

1491 Other arguments are forwarded to `.Poly3DCollection`. 

1492 """ 

1493 

1494 had_data = self.has_data() 

1495 

1496 if Z.ndim != 2: 

1497 raise ValueError("Argument Z must be 2-dimensional.") 

1498 if np.any(np.isnan(Z)): 

1499 cbook._warn_external( 

1500 "Z contains NaN values. This may result in rendering " 

1501 "artifacts.") 

1502 

1503 # TODO: Support masked arrays 

1504 X, Y, Z = np.broadcast_arrays(X, Y, Z) 

1505 rows, cols = Z.shape 

1506 

1507 has_stride = 'rstride' in kwargs or 'cstride' in kwargs 

1508 has_count = 'rcount' in kwargs or 'ccount' in kwargs 

1509 

1510 if has_stride and has_count: 

1511 raise ValueError("Cannot specify both stride and count arguments") 

1512 

1513 rstride = kwargs.pop('rstride', 10) 

1514 cstride = kwargs.pop('cstride', 10) 

1515 rcount = kwargs.pop('rcount', 50) 

1516 ccount = kwargs.pop('ccount', 50) 

1517 

1518 if rcParams['_internal.classic_mode']: 

1519 # Strides have priority over counts in classic mode. 

1520 # So, only compute strides from counts 

1521 # if counts were explicitly given 

1522 compute_strides = has_count 

1523 else: 

1524 # If the strides are provided then it has priority. 

1525 # Otherwise, compute the strides from the counts. 

1526 compute_strides = not has_stride 

1527 

1528 if compute_strides: 

1529 rstride = int(max(np.ceil(rows / rcount), 1)) 

1530 cstride = int(max(np.ceil(cols / ccount), 1)) 

1531 

1532 if 'facecolors' in kwargs: 

1533 fcolors = kwargs.pop('facecolors') 

1534 else: 

1535 color = kwargs.pop('color', None) 

1536 if color is None: 

1537 color = self._get_lines.get_next_color() 

1538 color = np.array(mcolors.to_rgba(color)) 

1539 fcolors = None 

1540 

1541 cmap = kwargs.get('cmap', None) 

1542 shade = kwargs.pop('shade', cmap is None) 

1543 if shade is None: 

1544 cbook.warn_deprecated( 

1545 "3.1", 

1546 message="Passing shade=None to Axes3D.plot_surface() is " 

1547 "deprecated since matplotlib 3.1 and will change its " 

1548 "semantic or raise an error in matplotlib 3.3. " 

1549 "Please use shade=False instead.") 

1550 

1551 # evenly spaced, and including both endpoints 

1552 row_inds = list(range(0, rows-1, rstride)) + [rows-1] 

1553 col_inds = list(range(0, cols-1, cstride)) + [cols-1] 

1554 

1555 colset = [] # the sampled facecolor 

1556 polys = [] 

1557 for rs, rs_next in zip(row_inds[:-1], row_inds[1:]): 

1558 for cs, cs_next in zip(col_inds[:-1], col_inds[1:]): 

1559 ps = [ 

1560 # +1 ensures we share edges between polygons 

1561 cbook._array_perimeter(a[rs:rs_next+1, cs:cs_next+1]) 

1562 for a in (X, Y, Z) 

1563 ] 

1564 # ps = np.stack(ps, axis=-1) 

1565 ps = np.array(ps).T 

1566 polys.append(ps) 

1567 

1568 if fcolors is not None: 

1569 colset.append(fcolors[rs][cs]) 

1570 

1571 # note that the striding causes some polygons to have more coordinates 

1572 # than others 

1573 polyc = art3d.Poly3DCollection(polys, *args, **kwargs) 

1574 

1575 if fcolors is not None: 

1576 if shade: 

1577 colset = self._shade_colors( 

1578 colset, self._generate_normals(polys), lightsource) 

1579 polyc.set_facecolors(colset) 

1580 polyc.set_edgecolors(colset) 

1581 elif cmap: 

1582 # doesn't vectorize because polys is jagged 

1583 avg_z = np.array([ps[:, 2].mean() for ps in polys]) 

1584 polyc.set_array(avg_z) 

1585 if vmin is not None or vmax is not None: 

1586 polyc.set_clim(vmin, vmax) 

1587 if norm is not None: 

1588 polyc.set_norm(norm) 

1589 else: 

1590 if shade: 

1591 colset = self._shade_colors( 

1592 color, self._generate_normals(polys), lightsource) 

1593 else: 

1594 colset = color 

1595 polyc.set_facecolors(colset) 

1596 

1597 self.add_collection(polyc) 

1598 self.auto_scale_xyz(X, Y, Z, had_data) 

1599 

1600 return polyc 

1601 

1602 def _generate_normals(self, polygons): 

1603 """ 

1604 Takes a list of polygons and return an array of their normals. 

1605 

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

1607 counterclockwise order, following the right hand rule. 

1608 

1609 Uses three points equally spaced around the polygon. 

1610 This normal of course might not make sense for polygons with more than 

1611 three points not lying in a plane, but it's a plausible and fast 

1612 approximation. 

1613 

1614 Parameters 

1615 ---------- 

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

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

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

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

1620 be vectorized. 

1621 

1622 Returns 

1623 ------- 

1624 normals: (..., 3) array-like 

1625 A normal vector estimated for the polygon. 

1626 

1627 """ 

1628 if isinstance(polygons, np.ndarray): 

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

1630 # vectorize 

1631 n = polygons.shape[-2] 

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

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

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

1635 else: 

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

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

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

1639 for poly_i, ps in enumerate(polygons): 

1640 n = len(ps) 

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

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

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

1644 return np.cross(v1, v2) 

1645 

1646 def _shade_colors(self, color, normals, lightsource=None): 

1647 """ 

1648 Shade *color* using normal vectors given by *normals*. 

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

1650 """ 

1651 if lightsource is None: 

1652 # chosen for backwards-compatibility 

1653 lightsource = LightSource(azdeg=225, altdeg=19.4712) 

1654 

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

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

1657 @ lightsource.direction) 

1658 mask = ~np.isnan(shade) 

1659 

1660 if mask.any(): 

1661 # convert dot product to allowed shading fractions 

1662 in_norm = Normalize(-1, 1) 

1663 out_norm = Normalize(0.3, 1).inverse 

1664 

1665 def norm(x): 

1666 return out_norm(in_norm(x)) 

1667 

1668 shade[~mask] = 0 

1669 

1670 color = mcolors.to_rgba_array(color) 

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

1672 # shape of shade should be (M,) 

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

1674 alpha = color[:, 3] 

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

1676 colors[:, 3] = alpha 

1677 else: 

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

1679 

1680 return colors 

1681 

1682 def plot_wireframe(self, X, Y, Z, *args, **kwargs): 

1683 """ 

1684 Plot a 3D wireframe. 

1685 

1686 .. note:: 

1687 

1688 The *rcount* and *ccount* kwargs, which both default to 50, 

1689 determine the maximum number of samples used in each direction. If 

1690 the input data is larger, it will be downsampled (by slicing) to 

1691 these numbers of points. 

1692 

1693 Parameters 

1694 ---------- 

1695 X, Y, Z : 2d arrays 

1696 Data values. 

1697 

1698 rcount, ccount : int 

1699 Maximum number of samples used in each direction. If the input 

1700 data is larger, it will be downsampled (by slicing) to these 

1701 numbers of points. Setting a count to zero causes the data to be 

1702 not sampled in the corresponding direction, producing a 3D line 

1703 plot rather than a wireframe plot. Defaults to 50. 

1704 

1705 .. versionadded:: 2.0 

1706 

1707 rstride, cstride : int 

1708 Downsampling stride in each direction. These arguments are 

1709 mutually exclusive with *rcount* and *ccount*. If only one of 

1710 *rstride* or *cstride* is set, the other defaults to 1. Setting a 

1711 stride to zero causes the data to be not sampled in the 

1712 corresponding direction, producing a 3D line plot rather than a 

1713 wireframe plot. 

1714 

1715 'classic' mode uses a default of ``rstride = cstride = 1`` instead 

1716 of the new default of ``rcount = ccount = 50``. 

1717 

1718 **kwargs 

1719 Other arguments are forwarded to `.Line3DCollection`. 

1720 """ 

1721 

1722 had_data = self.has_data() 

1723 if Z.ndim != 2: 

1724 raise ValueError("Argument Z must be 2-dimensional.") 

1725 # FIXME: Support masked arrays 

1726 X, Y, Z = np.broadcast_arrays(X, Y, Z) 

1727 rows, cols = Z.shape 

1728 

1729 has_stride = 'rstride' in kwargs or 'cstride' in kwargs 

1730 has_count = 'rcount' in kwargs or 'ccount' in kwargs 

1731 

1732 if has_stride and has_count: 

1733 raise ValueError("Cannot specify both stride and count arguments") 

1734 

1735 rstride = kwargs.pop('rstride', 1) 

1736 cstride = kwargs.pop('cstride', 1) 

1737 rcount = kwargs.pop('rcount', 50) 

1738 ccount = kwargs.pop('ccount', 50) 

1739 

1740 if rcParams['_internal.classic_mode']: 

1741 # Strides have priority over counts in classic mode. 

1742 # So, only compute strides from counts 

1743 # if counts were explicitly given 

1744 if has_count: 

1745 rstride = int(max(np.ceil(rows / rcount), 1)) if rcount else 0 

1746 cstride = int(max(np.ceil(cols / ccount), 1)) if ccount else 0 

1747 else: 

1748 # If the strides are provided then it has priority. 

1749 # Otherwise, compute the strides from the counts. 

1750 if not has_stride: 

1751 rstride = int(max(np.ceil(rows / rcount), 1)) if rcount else 0 

1752 cstride = int(max(np.ceil(cols / ccount), 1)) if ccount else 0 

1753 

1754 # We want two sets of lines, one running along the "rows" of 

1755 # Z and another set of lines running along the "columns" of Z. 

1756 # This transpose will make it easy to obtain the columns. 

1757 tX, tY, tZ = np.transpose(X), np.transpose(Y), np.transpose(Z) 

1758 

1759 if rstride: 

1760 rii = list(range(0, rows, rstride)) 

1761 # Add the last index only if needed 

1762 if rows > 0 and rii[-1] != (rows - 1): 

1763 rii += [rows-1] 

1764 else: 

1765 rii = [] 

1766 if cstride: 

1767 cii = list(range(0, cols, cstride)) 

1768 # Add the last index only if needed 

1769 if cols > 0 and cii[-1] != (cols - 1): 

1770 cii += [cols-1] 

1771 else: 

1772 cii = [] 

1773 

1774 if rstride == 0 and cstride == 0: 

1775 raise ValueError("Either rstride or cstride must be non zero") 

1776 

1777 # If the inputs were empty, then just 

1778 # reset everything. 

1779 if Z.size == 0: 

1780 rii = [] 

1781 cii = [] 

1782 

1783 xlines = [X[i] for i in rii] 

1784 ylines = [Y[i] for i in rii] 

1785 zlines = [Z[i] for i in rii] 

1786 

1787 txlines = [tX[i] for i in cii] 

1788 tylines = [tY[i] for i in cii] 

1789 tzlines = [tZ[i] for i in cii] 

1790 

1791 lines = ([list(zip(xl, yl, zl)) 

1792 for xl, yl, zl in zip(xlines, ylines, zlines)] 

1793 + [list(zip(xl, yl, zl)) 

1794 for xl, yl, zl in zip(txlines, tylines, tzlines)]) 

1795 

1796 linec = art3d.Line3DCollection(lines, *args, **kwargs) 

1797 self.add_collection(linec) 

1798 self.auto_scale_xyz(X, Y, Z, had_data) 

1799 

1800 return linec 

1801 

1802 def plot_trisurf(self, *args, color=None, norm=None, vmin=None, vmax=None, 

1803 lightsource=None, **kwargs): 

1804 """ 

1805 Plot a triangulated surface. 

1806 

1807 The (optional) triangulation can be specified in one of two ways; 

1808 either:: 

1809 

1810 plot_trisurf(triangulation, ...) 

1811 

1812 where triangulation is a :class:`~matplotlib.tri.Triangulation` 

1813 object, or:: 

1814 

1815 plot_trisurf(X, Y, ...) 

1816 plot_trisurf(X, Y, triangles, ...) 

1817 plot_trisurf(X, Y, triangles=triangles, ...) 

1818 

1819 in which case a Triangulation object will be created. See 

1820 :class:`~matplotlib.tri.Triangulation` for a explanation of 

1821 these possibilities. 

1822 

1823 The remaining arguments are:: 

1824 

1825 plot_trisurf(..., Z) 

1826 

1827 where *Z* is the array of values to contour, one per point 

1828 in the triangulation. 

1829 

1830 Parameters 

1831 ---------- 

1832 X, Y, Z : array-like 

1833 Data values as 1D arrays. 

1834 color 

1835 Color of the surface patches. 

1836 cmap 

1837 A colormap for the surface patches. 

1838 norm : Normalize 

1839 An instance of Normalize to map values to colors. 

1840 vmin, vmax : scalar, optional, default: None 

1841 Minimum and maximum value to map. 

1842 shade : bool 

1843 Whether to shade the facecolors. Defaults to True. Shading is 

1844 always disabled when *cmap* is specified. 

1845 lightsource : `~matplotlib.colors.LightSource` 

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

1847 **kwargs 

1848 All other arguments are passed on to 

1849 :class:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection` 

1850 

1851 Examples 

1852 -------- 

1853 .. plot:: gallery/mplot3d/trisurf3d.py 

1854 .. plot:: gallery/mplot3d/trisurf3d_2.py 

1855 

1856 .. versionadded:: 1.2.0 

1857 """ 

1858 

1859 had_data = self.has_data() 

1860 

1861 # TODO: Support custom face colours 

1862 if color is None: 

1863 color = self._get_lines.get_next_color() 

1864 color = np.array(mcolors.to_rgba(color)) 

1865 

1866 cmap = kwargs.get('cmap', None) 

1867 shade = kwargs.pop('shade', cmap is None) 

1868 

1869 tri, args, kwargs = \ 

1870 Triangulation.get_from_args_and_kwargs(*args, **kwargs) 

1871 try: 

1872 z = kwargs.pop('Z') 

1873 except KeyError: 

1874 # We do this so Z doesn't get passed as an arg to PolyCollection 

1875 z, *args = args 

1876 z = np.asarray(z) 

1877 

1878 triangles = tri.get_masked_triangles() 

1879 xt = tri.x[triangles] 

1880 yt = tri.y[triangles] 

1881 zt = z[triangles] 

1882 verts = np.stack((xt, yt, zt), axis=-1) 

1883 

1884 polyc = art3d.Poly3DCollection(verts, *args, **kwargs) 

1885 

1886 if cmap: 

1887 # average over the three points of each triangle 

1888 avg_z = verts[:, :, 2].mean(axis=1) 

1889 polyc.set_array(avg_z) 

1890 if vmin is not None or vmax is not None: 

1891 polyc.set_clim(vmin, vmax) 

1892 if norm is not None: 

1893 polyc.set_norm(norm) 

1894 else: 

1895 if shade: 

1896 normals = self._generate_normals(verts) 

1897 colset = self._shade_colors(color, normals, lightsource) 

1898 else: 

1899 colset = color 

1900 polyc.set_facecolors(colset) 

1901 

1902 self.add_collection(polyc) 

1903 self.auto_scale_xyz(tri.x, tri.y, z, had_data) 

1904 

1905 return polyc 

1906 

1907 def _3d_extend_contour(self, cset, stride=5): 

1908 ''' 

1909 Extend a contour in 3D by creating 

1910 ''' 

1911 

1912 levels = cset.levels 

1913 colls = cset.collections 

1914 dz = (levels[1] - levels[0]) / 2 

1915 

1916 for z, linec in zip(levels, colls): 

1917 paths = linec.get_paths() 

1918 if not paths: 

1919 continue 

1920 topverts = art3d._paths_to_3d_segments(paths, z - dz) 

1921 botverts = art3d._paths_to_3d_segments(paths, z + dz) 

1922 

1923 color = linec.get_color()[0] 

1924 

1925 polyverts = [] 

1926 normals = [] 

1927 nsteps = round(len(topverts[0]) / stride) 

1928 if nsteps <= 1: 

1929 if len(topverts[0]) > 1: 

1930 nsteps = 2 

1931 else: 

1932 continue 

1933 

1934 stepsize = (len(topverts[0]) - 1) / (nsteps - 1) 

1935 for i in range(int(round(nsteps)) - 1): 

1936 i1 = int(round(i * stepsize)) 

1937 i2 = int(round((i + 1) * stepsize)) 

1938 polyverts.append([topverts[0][i1], 

1939 topverts[0][i2], 

1940 botverts[0][i2], 

1941 botverts[0][i1]]) 

1942 

1943 # all polygons have 4 vertices, so vectorize 

1944 polyverts = np.array(polyverts) 

1945 normals = self._generate_normals(polyverts) 

1946 

1947 colors = self._shade_colors(color, normals) 

1948 colors2 = self._shade_colors(color, normals) 

1949 polycol = art3d.Poly3DCollection(polyverts, 

1950 facecolors=colors, 

1951 edgecolors=colors2) 

1952 polycol.set_sort_zpos(z) 

1953 self.add_collection3d(polycol) 

1954 

1955 for col in colls: 

1956 self.collections.remove(col) 

1957 

1958 def add_contour_set( 

1959 self, cset, extend3d=False, stride=5, zdir='z', offset=None): 

1960 zdir = '-' + zdir 

1961 if extend3d: 

1962 self._3d_extend_contour(cset, stride) 

1963 else: 

1964 for z, linec in zip(cset.levels, cset.collections): 

1965 if offset is not None: 

1966 z = offset 

1967 art3d.line_collection_2d_to_3d(linec, z, zdir=zdir) 

1968 

1969 def add_contourf_set(self, cset, zdir='z', offset=None): 

1970 zdir = '-' + zdir 

1971 for z, linec in zip(cset.levels, cset.collections): 

1972 if offset is not None: 

1973 z = offset 

1974 art3d.poly_collection_2d_to_3d(linec, z, zdir=zdir) 

1975 linec.set_sort_zpos(z) 

1976 

1977 def contour(self, X, Y, Z, *args, 

1978 extend3d=False, stride=5, zdir='z', offset=None, **kwargs): 

1979 """ 

1980 Create a 3D contour plot. 

1981 

1982 Parameters 

1983 ---------- 

1984 X, Y, Z : array-likes 

1985 Input data. 

1986 extend3d : bool 

1987 Whether to extend contour in 3D; defaults to False. 

1988 stride : int 

1989 Step size for extending contour. 

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

1991 The direction to use; defaults to 'z'. 

1992 offset : scalar 

1993 If specified, plot a projection of the contour lines at this 

1994 position in a plane normal to zdir 

1995 *args, **kwargs 

1996 Other arguments are forwarded to `matplotlib.axes.Axes.contour`. 

1997 

1998 Returns 

1999 ------- 

2000 matplotlib.contour.QuadContourSet 

2001 """ 

2002 had_data = self.has_data() 

2003 

2004 jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir) 

2005 cset = super().contour(jX, jY, jZ, *args, **kwargs) 

2006 self.add_contour_set(cset, extend3d, stride, zdir, offset) 

2007 

2008 self.auto_scale_xyz(X, Y, Z, had_data) 

2009 return cset 

2010 

2011 contour3D = contour 

2012 

2013 def tricontour(self, *args, 

2014 extend3d=False, stride=5, zdir='z', offset=None, **kwargs): 

2015 """ 

2016 Create a 3D contour plot. 

2017 

2018 .. versionchanged:: 1.3.0 

2019 Added support for custom triangulations 

2020 

2021 .. note:: 

2022 This method currently produces incorrect output due to a 

2023 longstanding bug in 3D PolyCollection rendering. 

2024 

2025 Parameters 

2026 ---------- 

2027 X, Y, Z : array-likes 

2028 Input data. 

2029 extend3d : bool 

2030 Whether to extend contour in 3D; defaults to False. 

2031 stride : int 

2032 Step size for extending contour. 

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

2034 The direction to use; defaults to 'z'. 

2035 offset : scalar 

2036 If specified, plot a projection of the contour lines at this 

2037 position in a plane normal to zdir 

2038 *args, **kwargs 

2039 Other arguments are forwarded to `matplotlib.axes.Axes.tricontour`. 

2040 

2041 Returns 

2042 ------- 

2043 matplotlib.tri.tricontour.TriContourSet 

2044 """ 

2045 had_data = self.has_data() 

2046 

2047 tri, args, kwargs = Triangulation.get_from_args_and_kwargs( 

2048 *args, **kwargs) 

2049 X = tri.x 

2050 Y = tri.y 

2051 if 'Z' in kwargs: 

2052 Z = kwargs.pop('Z') 

2053 else: 

2054 # We do this so Z doesn't get passed as an arg to Axes.tricontour 

2055 Z, *args = args 

2056 

2057 jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir) 

2058 tri = Triangulation(jX, jY, tri.triangles, tri.mask) 

2059 

2060 cset = super().tricontour(tri, jZ, *args, **kwargs) 

2061 self.add_contour_set(cset, extend3d, stride, zdir, offset) 

2062 

2063 self.auto_scale_xyz(X, Y, Z, had_data) 

2064 return cset 

2065 

2066 def contourf(self, X, Y, Z, *args, zdir='z', offset=None, **kwargs): 

2067 """ 

2068 Create a 3D filled contour plot. 

2069 

2070 Parameters 

2071 ---------- 

2072 X, Y, Z : array-likes 

2073 Input data. 

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

2075 The direction to use; defaults to 'z'. 

2076 offset : scalar 

2077 If specified, plot a projection of the contour lines at this 

2078 position in a plane normal to zdir 

2079 *args, **kwargs 

2080 Other arguments are forwarded to `matplotlib.axes.Axes.contourf`. 

2081 

2082 Returns 

2083 ------- 

2084 matplotlib.contour.QuadContourSet 

2085 

2086 Notes 

2087 ----- 

2088 .. versionadded:: 1.1.0 

2089 The *zdir* and *offset* parameters. 

2090 """ 

2091 had_data = self.has_data() 

2092 

2093 jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir) 

2094 cset = super().contourf(jX, jY, jZ, *args, **kwargs) 

2095 self.add_contourf_set(cset, zdir, offset) 

2096 

2097 self.auto_scale_xyz(X, Y, Z, had_data) 

2098 return cset 

2099 

2100 contourf3D = contourf 

2101 

2102 def tricontourf(self, *args, zdir='z', offset=None, **kwargs): 

2103 """ 

2104 Create a 3D filled contour plot. 

2105 

2106 .. note:: 

2107 This method currently produces incorrect output due to a 

2108 longstanding bug in 3D PolyCollection rendering. 

2109 

2110 Parameters 

2111 ---------- 

2112 X, Y, Z : array-likes 

2113 Input data. 

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

2115 The direction to use; defaults to 'z'. 

2116 offset : scalar 

2117 If specified, plot a projection of the contour lines at this 

2118 position in a plane normal to zdir 

2119 *args, **kwargs 

2120 Other arguments are forwarded to 

2121 `matplotlib.axes.Axes.tricontourf`. 

2122 

2123 Returns 

2124 ------- 

2125 matplotlib.tri.tricontour.TriContourSet 

2126 

2127 Notes 

2128 ----- 

2129 .. versionadded:: 1.1.0 

2130 The *zdir* and *offset* parameters. 

2131 .. versionchanged:: 1.3.0 

2132 Added support for custom triangulations 

2133 """ 

2134 had_data = self.has_data() 

2135 

2136 tri, args, kwargs = Triangulation.get_from_args_and_kwargs( 

2137 *args, **kwargs) 

2138 X = tri.x 

2139 Y = tri.y 

2140 if 'Z' in kwargs: 

2141 Z = kwargs.pop('Z') 

2142 else: 

2143 # We do this so Z doesn't get passed as an arg to Axes.tricontourf 

2144 Z, *args = args 

2145 

2146 jX, jY, jZ = art3d.rotate_axes(X, Y, Z, zdir) 

2147 tri = Triangulation(jX, jY, tri.triangles, tri.mask) 

2148 

2149 cset = super().tricontourf(tri, jZ, *args, **kwargs) 

2150 self.add_contourf_set(cset, zdir, offset) 

2151 

2152 self.auto_scale_xyz(X, Y, Z, had_data) 

2153 return cset 

2154 

2155 def add_collection3d(self, col, zs=0, zdir='z'): 

2156 ''' 

2157 Add a 3D collection object to the plot. 

2158 

2159 2D collection types are converted to a 3D version by 

2160 modifying the object and adding z coordinate information. 

2161 

2162 Supported are: 

2163 - PolyCollection 

2164 - LineCollection 

2165 - PatchCollection 

2166 ''' 

2167 zvals = np.atleast_1d(zs) 

2168 zsortval = (np.min(zvals) if zvals.size 

2169 else 0) # FIXME: arbitrary default 

2170 

2171 # FIXME: use issubclass() (although, then a 3D collection 

2172 # object would also pass.) Maybe have a collection3d 

2173 # abstract class to test for and exclude? 

2174 if type(col) is mcoll.PolyCollection: 

2175 art3d.poly_collection_2d_to_3d(col, zs=zs, zdir=zdir) 

2176 col.set_sort_zpos(zsortval) 

2177 elif type(col) is mcoll.LineCollection: 

2178 art3d.line_collection_2d_to_3d(col, zs=zs, zdir=zdir) 

2179 col.set_sort_zpos(zsortval) 

2180 elif type(col) is mcoll.PatchCollection: 

2181 art3d.patch_collection_2d_to_3d(col, zs=zs, zdir=zdir) 

2182 col.set_sort_zpos(zsortval) 

2183 

2184 super().add_collection(col) 

2185 

2186 def scatter(self, xs, ys, zs=0, zdir='z', s=20, c=None, depthshade=True, 

2187 *args, **kwargs): 

2188 """ 

2189 Create a scatter plot. 

2190 

2191 Parameters 

2192 ---------- 

2193 xs, ys : array-like 

2194 The data positions. 

2195 zs : float or array-like, optional, default: 0 

2196 The z-positions. Either an array of the same length as *xs* and 

2197 *ys* or a single value to place all points in the same plane. 

2198 zdir : {'x', 'y', 'z', '-x', '-y', '-z'}, optional, default: 'z' 

2199 The axis direction for the *zs*. This is useful when plotting 2D 

2200 data on a 3D Axes. The data must be passed as *xs*, *ys*. Setting 

2201 *zdir* to 'y' then plots the data to the x-z-plane. 

2202 

2203 See also :doc:`/gallery/mplot3d/2dcollections3d`. 

2204 

2205 s : scalar or array-like, optional, default: 20 

2206 The marker size in points**2. Either an array of the same length 

2207 as *xs* and *ys* or a single value to make all markers the same 

2208 size. 

2209 c : color, sequence, or sequence of colors, optional 

2210 The marker color. Possible values: 

2211 

2212 - A single color format string. 

2213 - A sequence of colors of length n. 

2214 - A sequence of n numbers to be mapped to colors using *cmap* and 

2215 *norm*. 

2216 - A 2-D array in which the rows are RGB or RGBA. 

2217 

2218 For more details see the *c* argument of `~.axes.Axes.scatter`. 

2219 depthshade : bool, optional, default: True 

2220 Whether to shade the scatter markers to give the appearance of 

2221 depth. Each call to ``scatter()`` will perform its depthshading 

2222 independently. 

2223 **kwargs 

2224 All other arguments are passed on to `~.axes.Axes.scatter`. 

2225 

2226 Returns 

2227 ------- 

2228 paths : `~matplotlib.collections.PathCollection` 

2229 """ 

2230 

2231 had_data = self.has_data() 

2232 

2233 xs, ys, zs = np.broadcast_arrays( 

2234 *[np.ravel(np.ma.filled(t, np.nan)) for t in [xs, ys, zs]]) 

2235 s = np.ma.ravel(s) # This doesn't have to match x, y in size. 

2236 

2237 xs, ys, zs, s, c = cbook.delete_masked_points(xs, ys, zs, s, c) 

2238 

2239 patches = super().scatter(xs, ys, s=s, c=c, *args, **kwargs) 

2240 art3d.patch_collection_2d_to_3d(patches, zs=zs, zdir=zdir, 

2241 depthshade=depthshade) 

2242 

2243 if self._zmargin < 0.05 and xs.size > 0: 

2244 self.set_zmargin(0.05) 

2245 

2246 self.auto_scale_xyz(xs, ys, zs, had_data) 

2247 

2248 return patches 

2249 

2250 scatter3D = scatter 

2251 

2252 def bar(self, left, height, zs=0, zdir='z', *args, **kwargs): 

2253 """ 

2254 Add 2D bar(s). 

2255 

2256 Parameters 

2257 ---------- 

2258 left : 1D array-like 

2259 The x coordinates of the left sides of the bars. 

2260 height : 1D array-like 

2261 The height of the bars. 

2262 zs : scalar or 1D array-like 

2263 Z coordinate of bars; if a single value is specified, it will be 

2264 used for all bars. 

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

2266 When plotting 2D data, the direction to use as z ('x', 'y' or 'z'); 

2267 defaults to 'z'. 

2268 **kwargs 

2269 Other arguments are forwarded to `matplotlib.axes.Axes.bar`. 

2270 

2271 Returns 

2272 ------- 

2273 mpl_toolkits.mplot3d.art3d.Patch3DCollection 

2274 """ 

2275 had_data = self.has_data() 

2276 

2277 patches = super().bar(left, height, *args, **kwargs) 

2278 

2279 zs = np.broadcast_to(zs, len(left)) 

2280 

2281 verts = [] 

2282 verts_zs = [] 

2283 for p, z in zip(patches, zs): 

2284 vs = art3d._get_patch_verts(p) 

2285 verts += vs.tolist() 

2286 verts_zs += [z] * len(vs) 

2287 art3d.patch_2d_to_3d(p, z, zdir) 

2288 if 'alpha' in kwargs: 

2289 p.set_alpha(kwargs['alpha']) 

2290 

2291 if len(verts) > 0: 

2292 # the following has to be skipped if verts is empty 

2293 # NOTE: Bugs could still occur if len(verts) > 0, 

2294 # but the "2nd dimension" is empty. 

2295 xs, ys = zip(*verts) 

2296 else: 

2297 xs, ys = [], [] 

2298 

2299 xs, ys, verts_zs = art3d.juggle_axes(xs, ys, verts_zs, zdir) 

2300 self.auto_scale_xyz(xs, ys, verts_zs, had_data) 

2301 

2302 return patches 

2303 

2304 def bar3d(self, x, y, z, dx, dy, dz, color=None, 

2305 zsort='average', shade=True, lightsource=None, *args, **kwargs): 

2306 """Generate a 3D barplot. 

2307 

2308 This method creates three dimensional barplot where the width, 

2309 depth, height, and color of the bars can all be uniquely set. 

2310 

2311 Parameters 

2312 ---------- 

2313 x, y, z : array-like 

2314 The coordinates of the anchor point of the bars. 

2315 

2316 dx, dy, dz : scalar or array-like 

2317 The width, depth, and height of the bars, respectively. 

2318 

2319 color : sequence of colors, optional 

2320 The color of the bars can be specified globally or 

2321 individually. This parameter can be: 

2322 

2323 - A single color, to color all bars the same color. 

2324 - An array of colors of length N bars, to color each bar 

2325 independently. 

2326 - An array of colors of length 6, to color the faces of the 

2327 bars similarly. 

2328 - An array of colors of length 6 * N bars, to color each face 

2329 independently. 

2330 

2331 When coloring the faces of the boxes specifically, this is 

2332 the order of the coloring: 

2333 

2334 1. -Z (bottom of box) 

2335 2. +Z (top of box) 

2336 3. -Y 

2337 4. +Y 

2338 5. -X 

2339 6. +X 

2340 

2341 zsort : str, optional 

2342 The z-axis sorting scheme passed onto `~.art3d.Poly3DCollection` 

2343 

2344 shade : bool, optional (default = True) 

2345 When true, this shades the dark sides of the bars (relative 

2346 to the plot's source of light). 

2347 

2348 lightsource : `~matplotlib.colors.LightSource` 

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

2350 

2351 **kwargs 

2352 Any additional keyword arguments are passed onto 

2353 `~.art3d.Poly3DCollection`. 

2354 

2355 Returns 

2356 ------- 

2357 collection : `~.art3d.Poly3DCollection` 

2358 A collection of three dimensional polygons representing 

2359 the bars. 

2360 """ 

2361 

2362 had_data = self.has_data() 

2363 

2364 x, y, z, dx, dy, dz = np.broadcast_arrays( 

2365 np.atleast_1d(x), y, z, dx, dy, dz) 

2366 minx = np.min(x) 

2367 maxx = np.max(x + dx) 

2368 miny = np.min(y) 

2369 maxy = np.max(y + dy) 

2370 minz = np.min(z) 

2371 maxz = np.max(z + dz) 

2372 

2373 # shape (6, 4, 3) 

2374 # All faces are oriented facing outwards - when viewed from the 

2375 # outside, their vertices are in a counterclockwise ordering. 

2376 cuboid = np.array([ 

2377 # -z 

2378 ( 

2379 (0, 0, 0), 

2380 (0, 1, 0), 

2381 (1, 1, 0), 

2382 (1, 0, 0), 

2383 ), 

2384 # +z 

2385 ( 

2386 (0, 0, 1), 

2387 (1, 0, 1), 

2388 (1, 1, 1), 

2389 (0, 1, 1), 

2390 ), 

2391 # -y 

2392 ( 

2393 (0, 0, 0), 

2394 (1, 0, 0), 

2395 (1, 0, 1), 

2396 (0, 0, 1), 

2397 ), 

2398 # +y 

2399 ( 

2400 (0, 1, 0), 

2401 (0, 1, 1), 

2402 (1, 1, 1), 

2403 (1, 1, 0), 

2404 ), 

2405 # -x 

2406 ( 

2407 (0, 0, 0), 

2408 (0, 0, 1), 

2409 (0, 1, 1), 

2410 (0, 1, 0), 

2411 ), 

2412 # +x 

2413 ( 

2414 (1, 0, 0), 

2415 (1, 1, 0), 

2416 (1, 1, 1), 

2417 (1, 0, 1), 

2418 ), 

2419 ]) 

2420 

2421 # indexed by [bar, face, vertex, coord] 

2422 polys = np.empty(x.shape + cuboid.shape) 

2423 

2424 # handle each coordinate separately 

2425 for i, p, dp in [(0, x, dx), (1, y, dy), (2, z, dz)]: 

2426 p = p[..., np.newaxis, np.newaxis] 

2427 dp = dp[..., np.newaxis, np.newaxis] 

2428 polys[..., i] = p + dp * cuboid[..., i] 

2429 

2430 # collapse the first two axes 

2431 polys = polys.reshape((-1,) + polys.shape[2:]) 

2432 

2433 facecolors = [] 

2434 if color is None: 

2435 color = [self._get_patches_for_fill.get_next_color()] 

2436 

2437 if len(color) == len(x): 

2438 # bar colors specified, need to expand to number of faces 

2439 for c in color: 

2440 facecolors.extend([c] * 6) 

2441 else: 

2442 # a single color specified, or face colors specified explicitly 

2443 facecolors = list(mcolors.to_rgba_array(color)) 

2444 if len(facecolors) < len(x): 

2445 facecolors *= (6 * len(x)) 

2446 

2447 if shade: 

2448 normals = self._generate_normals(polys) 

2449 sfacecolors = self._shade_colors(facecolors, normals, lightsource) 

2450 else: 

2451 sfacecolors = facecolors 

2452 

2453 col = art3d.Poly3DCollection(polys, 

2454 zsort=zsort, 

2455 facecolor=sfacecolors, 

2456 *args, **kwargs) 

2457 self.add_collection(col) 

2458 

2459 self.auto_scale_xyz((minx, maxx), (miny, maxy), (minz, maxz), had_data) 

2460 

2461 return col 

2462 

2463 def set_title(self, label, fontdict=None, loc='center', **kwargs): 

2464 # docstring inherited 

2465 ret = super().set_title(label, fontdict=fontdict, loc=loc, **kwargs) 

2466 (x, y) = self.title.get_position() 

2467 self.title.set_y(0.92 * y) 

2468 return ret 

2469 

2470 def quiver(self, *args, 

2471 length=1, arrow_length_ratio=.3, pivot='tail', normalize=False, 

2472 **kwargs): 

2473 """ 

2474 ax.quiver(X, Y, Z, U, V, W, /, length=1, arrow_length_ratio=.3, \ 

2475pivot='tail', normalize=False, **kwargs) 

2476 

2477 Plot a 3D field of arrows. 

2478 

2479 The arguments could be array-like or scalars, so long as they 

2480 they can be broadcast together. The arguments can also be 

2481 masked arrays. If an element in any of argument is masked, then 

2482 that corresponding quiver element will not be plotted. 

2483 

2484 Parameters 

2485 ---------- 

2486 X, Y, Z : array-like 

2487 The x, y and z coordinates of the arrow locations (default is 

2488 tail of arrow; see *pivot* kwarg) 

2489 

2490 U, V, W : array-like 

2491 The x, y and z components of the arrow vectors 

2492 

2493 length : float 

2494 The length of each quiver, default to 1.0, the unit is 

2495 the same with the axes 

2496 

2497 arrow_length_ratio : float 

2498 The ratio of the arrow head with respect to the quiver, 

2499 default to 0.3 

2500 

2501 pivot : {'tail', 'middle', 'tip'} 

2502 The part of the arrow that is at the grid point; the arrow 

2503 rotates about this point, hence the name *pivot*. 

2504 Default is 'tail' 

2505 

2506 normalize : bool 

2507 When True, all of the arrows will be the same length. This 

2508 defaults to False, where the arrows will be different lengths 

2509 depending on the values of u, v, w. 

2510 

2511 **kwargs 

2512 Any additional keyword arguments are delegated to 

2513 :class:`~matplotlib.collections.LineCollection` 

2514 """ 

2515 def calc_arrow(uvw, angle=15): 

2516 """ 

2517 To calculate the arrow head. uvw should be a unit vector. 

2518 We normalize it here: 

2519 """ 

2520 # get unit direction vector perpendicular to (u, v, w) 

2521 norm = np.linalg.norm(uvw[:2]) 

2522 if norm > 0: 

2523 x = uvw[1] / norm 

2524 y = -uvw[0] / norm 

2525 else: 

2526 x, y = 0, 1 

2527 

2528 # compute the two arrowhead direction unit vectors 

2529 ra = math.radians(angle) 

2530 c = math.cos(ra) 

2531 s = math.sin(ra) 

2532 

2533 # construct the rotation matrices 

2534 Rpos = np.array([[c+(x**2)*(1-c), x*y*(1-c), y*s], 

2535 [y*x*(1-c), c+(y**2)*(1-c), -x*s], 

2536 [-y*s, x*s, c]]) 

2537 # opposite rotation negates all the sin terms 

2538 Rneg = Rpos.copy() 

2539 Rneg[[0, 1, 2, 2], [2, 2, 0, 1]] = \ 

2540 -Rneg[[0, 1, 2, 2], [2, 2, 0, 1]] 

2541 

2542 # multiply them to get the rotated vector 

2543 return Rpos.dot(uvw), Rneg.dot(uvw) 

2544 

2545 had_data = self.has_data() 

2546 

2547 # handle args 

2548 argi = 6 

2549 if len(args) < argi: 

2550 raise ValueError('Wrong number of arguments. Expected %d got %d' % 

2551 (argi, len(args))) 

2552 

2553 # first 6 arguments are X, Y, Z, U, V, W 

2554 input_args = args[:argi] 

2555 

2556 # extract the masks, if any 

2557 masks = [k.mask for k in input_args 

2558 if isinstance(k, np.ma.MaskedArray)] 

2559 # broadcast to match the shape 

2560 bcast = np.broadcast_arrays(*input_args, *masks) 

2561 input_args = bcast[:argi] 

2562 masks = bcast[argi:] 

2563 if masks: 

2564 # combine the masks into one 

2565 mask = reduce(np.logical_or, masks) 

2566 # put mask on and compress 

2567 input_args = [np.ma.array(k, mask=mask).compressed() 

2568 for k in input_args] 

2569 else: 

2570 input_args = [np.ravel(k) for k in input_args] 

2571 

2572 if any(len(v) == 0 for v in input_args): 

2573 # No quivers, so just make an empty collection and return early 

2574 linec = art3d.Line3DCollection([], *args[argi:], **kwargs) 

2575 self.add_collection(linec) 

2576 return linec 

2577 

2578 shaft_dt = np.array([0., length], dtype=float) 

2579 arrow_dt = shaft_dt * arrow_length_ratio 

2580 

2581 cbook._check_in_list(['tail', 'middle', 'tip'], pivot=pivot) 

2582 if pivot == 'tail': 

2583 shaft_dt -= length 

2584 elif pivot == 'middle': 

2585 shaft_dt -= length / 2 

2586 

2587 XYZ = np.column_stack(input_args[:3]) 

2588 UVW = np.column_stack(input_args[3:argi]).astype(float) 

2589 

2590 # Normalize rows of UVW 

2591 norm = np.linalg.norm(UVW, axis=1) 

2592 

2593 # If any row of UVW is all zeros, don't make a quiver for it 

2594 mask = norm > 0 

2595 XYZ = XYZ[mask] 

2596 if normalize: 

2597 UVW = UVW[mask] / norm[mask].reshape((-1, 1)) 

2598 else: 

2599 UVW = UVW[mask] 

2600 

2601 if len(XYZ) > 0: 

2602 # compute the shaft lines all at once with an outer product 

2603 shafts = (XYZ - np.multiply.outer(shaft_dt, UVW)).swapaxes(0, 1) 

2604 # compute head direction vectors, n heads x 2 sides x 3 dimensions 

2605 head_dirs = np.array([calc_arrow(d) for d in UVW]) 

2606 # compute all head lines at once, starting from the shaft ends 

2607 heads = shafts[:, :1] - np.multiply.outer(arrow_dt, head_dirs) 

2608 # stack left and right head lines together 

2609 heads.shape = (len(arrow_dt), -1, 3) 

2610 # transpose to get a list of lines 

2611 heads = heads.swapaxes(0, 1) 

2612 

2613 lines = [*shafts, *heads] 

2614 else: 

2615 lines = [] 

2616 

2617 linec = art3d.Line3DCollection(lines, *args[argi:], **kwargs) 

2618 self.add_collection(linec) 

2619 

2620 self.auto_scale_xyz(XYZ[:, 0], XYZ[:, 1], XYZ[:, 2], had_data) 

2621 

2622 return linec 

2623 

2624 quiver3D = quiver 

2625 

2626 def voxels(self, *args, facecolors=None, edgecolors=None, shade=True, 

2627 lightsource=None, **kwargs): 

2628 """ 

2629 ax.voxels([x, y, z,] /, filled, facecolors=None, edgecolors=None, \ 

2630**kwargs) 

2631 

2632 Plot a set of filled voxels 

2633 

2634 All voxels are plotted as 1x1x1 cubes on the axis, with 

2635 ``filled[0, 0, 0]`` placed with its lower corner at the origin. 

2636 Occluded faces are not plotted. 

2637 

2638 .. versionadded:: 2.1 

2639 

2640 Parameters 

2641 ---------- 

2642 filled : 3D np.array of bool 

2643 A 3d array of values, with truthy values indicating which voxels 

2644 to fill 

2645 

2646 x, y, z : 3D np.array, optional 

2647 The coordinates of the corners of the voxels. This should broadcast 

2648 to a shape one larger in every dimension than the shape of 

2649 `filled`. These can be used to plot non-cubic voxels. 

2650 

2651 If not specified, defaults to increasing integers along each axis, 

2652 like those returned by :func:`~numpy.indices`. 

2653 As indicated by the ``/`` in the function signature, these 

2654 arguments can only be passed positionally. 

2655 

2656 facecolors, edgecolors : array-like, optional 

2657 The color to draw the faces and edges of the voxels. Can only be 

2658 passed as keyword arguments. 

2659 This parameter can be: 

2660 

2661 - A single color value, to color all voxels the same color. This 

2662 can be either a string, or a 1D rgb/rgba array 

2663 - ``None``, the default, to use a single color for the faces, and 

2664 the style default for the edges. 

2665 - A 3D ndarray of color names, with each item the color for the 

2666 corresponding voxel. The size must match the voxels. 

2667 - A 4D ndarray of rgb/rgba data, with the components along the 

2668 last axis. 

2669 

2670 shade : bool 

2671 Whether to shade the facecolors. Defaults to True. Shading is 

2672 always disabled when *cmap* is specified. 

2673 

2674 .. versionadded:: 3.1 

2675 

2676 lightsource : `~matplotlib.colors.LightSource` 

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

2678 

2679 .. versionadded:: 3.1 

2680 

2681 **kwargs 

2682 Additional keyword arguments to pass onto 

2683 :func:`~mpl_toolkits.mplot3d.art3d.Poly3DCollection` 

2684 

2685 Returns 

2686 ------- 

2687 faces : dict 

2688 A dictionary indexed by coordinate, where ``faces[i, j, k]`` is a 

2689 `Poly3DCollection` of the faces drawn for the voxel 

2690 ``filled[i, j, k]``. If no faces were drawn for a given voxel, 

2691 either because it was not asked to be drawn, or it is fully 

2692 occluded, then ``(i, j, k) not in faces``. 

2693 

2694 Examples 

2695 -------- 

2696 .. plot:: gallery/mplot3d/voxels.py 

2697 .. plot:: gallery/mplot3d/voxels_rgb.py 

2698 .. plot:: gallery/mplot3d/voxels_torus.py 

2699 .. plot:: gallery/mplot3d/voxels_numpy_logo.py 

2700 """ 

2701 

2702 # work out which signature we should be using, and use it to parse 

2703 # the arguments. Name must be voxels for the correct error message 

2704 if len(args) >= 3: 

2705 # underscores indicate position only 

2706 def voxels(__x, __y, __z, filled, **kwargs): 

2707 return (__x, __y, __z), filled, kwargs 

2708 else: 

2709 def voxels(filled, **kwargs): 

2710 return None, filled, kwargs 

2711 

2712 xyz, filled, kwargs = voxels(*args, **kwargs) 

2713 

2714 # check dimensions 

2715 if filled.ndim != 3: 

2716 raise ValueError("Argument filled must be 3-dimensional") 

2717 size = np.array(filled.shape, dtype=np.intp) 

2718 

2719 # check xyz coordinates, which are one larger than the filled shape 

2720 coord_shape = tuple(size + 1) 

2721 if xyz is None: 

2722 x, y, z = np.indices(coord_shape) 

2723 else: 

2724 x, y, z = (np.broadcast_to(c, coord_shape) for c in xyz) 

2725 

2726 def _broadcast_color_arg(color, name): 

2727 if np.ndim(color) in (0, 1): 

2728 # single color, like "red" or [1, 0, 0] 

2729 return np.broadcast_to(color, filled.shape + np.shape(color)) 

2730 elif np.ndim(color) in (3, 4): 

2731 # 3D array of strings, or 4D array with last axis rgb 

2732 if np.shape(color)[:3] != filled.shape: 

2733 raise ValueError( 

2734 "When multidimensional, {} must match the shape of " 

2735 "filled".format(name)) 

2736 return color 

2737 else: 

2738 raise ValueError("Invalid {} argument".format(name)) 

2739 

2740 # broadcast and default on facecolors 

2741 if facecolors is None: 

2742 facecolors = self._get_patches_for_fill.get_next_color() 

2743 facecolors = _broadcast_color_arg(facecolors, 'facecolors') 

2744 

2745 # broadcast but no default on edgecolors 

2746 edgecolors = _broadcast_color_arg(edgecolors, 'edgecolors') 

2747 

2748 # scale to the full array, even if the data is only in the center 

2749 self.auto_scale_xyz(x, y, z) 

2750 

2751 # points lying on corners of a square 

2752 square = np.array([ 

2753 [0, 0, 0], 

2754 [1, 0, 0], 

2755 [1, 1, 0], 

2756 [0, 1, 0], 

2757 ], dtype=np.intp) 

2758 

2759 voxel_faces = defaultdict(list) 

2760 

2761 def permutation_matrices(n): 

2762 """Generator of cyclic permutation matrices.""" 

2763 mat = np.eye(n, dtype=np.intp) 

2764 for i in range(n): 

2765 yield mat 

2766 mat = np.roll(mat, 1, axis=0) 

2767 

2768 # iterate over each of the YZ, ZX, and XY orientations, finding faces 

2769 # to render 

2770 for permute in permutation_matrices(3): 

2771 # find the set of ranges to iterate over 

2772 pc, qc, rc = permute.T.dot(size) 

2773 pinds = np.arange(pc) 

2774 qinds = np.arange(qc) 

2775 rinds = np.arange(rc) 

2776 

2777 square_rot_pos = square.dot(permute.T) 

2778 square_rot_neg = square_rot_pos[::-1] 

2779 

2780 # iterate within the current plane 

2781 for p in pinds: 

2782 for q in qinds: 

2783 # iterate perpendicularly to the current plane, handling 

2784 # boundaries. We only draw faces between a voxel and an 

2785 # empty space, to avoid drawing internal faces. 

2786 

2787 # draw lower faces 

2788 p0 = permute.dot([p, q, 0]) 

2789 i0 = tuple(p0) 

2790 if filled[i0]: 

2791 voxel_faces[i0].append(p0 + square_rot_neg) 

2792 

2793 # draw middle faces 

2794 for r1, r2 in zip(rinds[:-1], rinds[1:]): 

2795 p1 = permute.dot([p, q, r1]) 

2796 p2 = permute.dot([p, q, r2]) 

2797 

2798 i1 = tuple(p1) 

2799 i2 = tuple(p2) 

2800 

2801 if filled[i1] and not filled[i2]: 

2802 voxel_faces[i1].append(p2 + square_rot_pos) 

2803 elif not filled[i1] and filled[i2]: 

2804 voxel_faces[i2].append(p2 + square_rot_neg) 

2805 

2806 # draw upper faces 

2807 pk = permute.dot([p, q, rc-1]) 

2808 pk2 = permute.dot([p, q, rc]) 

2809 ik = tuple(pk) 

2810 if filled[ik]: 

2811 voxel_faces[ik].append(pk2 + square_rot_pos) 

2812 

2813 # iterate over the faces, and generate a Poly3DCollection for each 

2814 # voxel 

2815 polygons = {} 

2816 for coord, faces_inds in voxel_faces.items(): 

2817 # convert indices into 3D positions 

2818 if xyz is None: 

2819 faces = faces_inds 

2820 else: 

2821 faces = [] 

2822 for face_inds in faces_inds: 

2823 ind = face_inds[:, 0], face_inds[:, 1], face_inds[:, 2] 

2824 face = np.empty(face_inds.shape) 

2825 face[:, 0] = x[ind] 

2826 face[:, 1] = y[ind] 

2827 face[:, 2] = z[ind] 

2828 faces.append(face) 

2829 

2830 # shade the faces 

2831 facecolor = facecolors[coord] 

2832 edgecolor = edgecolors[coord] 

2833 if shade: 

2834 normals = self._generate_normals(faces) 

2835 facecolor = self._shade_colors(facecolor, normals, lightsource) 

2836 if edgecolor is not None: 

2837 edgecolor = self._shade_colors( 

2838 edgecolor, normals, lightsource 

2839 ) 

2840 

2841 poly = art3d.Poly3DCollection( 

2842 faces, facecolors=facecolor, edgecolors=edgecolor, **kwargs) 

2843 self.add_collection3d(poly) 

2844 polygons[coord] = poly 

2845 

2846 return polygons 

2847 

2848 

2849def get_test_data(delta=0.05): 

2850 ''' 

2851 Return a tuple X, Y, Z with a test data set. 

2852 ''' 

2853 x = y = np.arange(-3.0, 3.0, delta) 

2854 X, Y = np.meshgrid(x, y) 

2855 

2856 Z1 = np.exp(-(X**2 + Y**2) / 2) / (2 * np.pi) 

2857 Z2 = (np.exp(-(((X - 1) / 1.5)**2 + ((Y - 1) / 0.5)**2) / 2) / 

2858 (2 * np.pi * 0.5 * 1.5)) 

2859 Z = Z2 - Z1 

2860 

2861 X = X * 10 

2862 Y = Y * 10 

2863 Z = Z * 500 

2864 return X, Y, Z