Coverage for /usr/lib/python3/dist-packages/mpl_toolkits/mplot3d/axes3d.py: 12%

1278 statements  

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

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 

14import functools 

15import itertools 

16import math 

17import textwrap 

18 

19import numpy as np 

20 

21import matplotlib as mpl 

22from matplotlib import _api, cbook, _docstring, _preprocess_data 

23import matplotlib.artist as martist 

24import matplotlib.axes as maxes 

25import matplotlib.collections as mcoll 

26import matplotlib.colors as mcolors 

27import matplotlib.image as mimage 

28import matplotlib.lines as mlines 

29import matplotlib.patches as mpatches 

30import matplotlib.container as mcontainer 

31import matplotlib.transforms as mtransforms 

32from matplotlib.axes import Axes 

33from matplotlib.axes._base import _axis_method_wrapper, _process_plot_format 

34from matplotlib.transforms import Bbox 

35from matplotlib.tri.triangulation import Triangulation 

36 

37from . import art3d 

38from . import proj3d 

39from . import axis3d 

40 

41 

42@_docstring.interpd 

43@_api.define_aliases({ 

44 "xlim": ["xlim3d"], "ylim": ["ylim3d"], "zlim": ["zlim3d"]}) 

45class Axes3D(Axes): 

46 """ 

47 3D Axes object. 

48 

49 .. note:: 

50 

51 As a user, you do not instantiate Axes directly, but use Axes creation 

52 methods instead; e.g. from `.pyplot` or `.Figure`: 

53 `~.pyplot.subplots`, `~.pyplot.subplot_mosaic` or `.Figure.add_axes`. 

54 """ 

55 name = '3d' 

56 

57 _axis_names = ("x", "y", "z") 

58 Axes._shared_axes["z"] = cbook.Grouper() 

59 

60 dist = _api.deprecate_privatize_attribute("3.6") 

61 

62 def __init__( 

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

64 elev=30, azim=-60, roll=0, sharez=None, proj_type='persp', 

65 box_aspect=None, computed_zorder=True, focal_length=None, 

66 **kwargs): 

67 """ 

68 Parameters 

69 ---------- 

70 fig : Figure 

71 The parent figure. 

72 rect : tuple (left, bottom, width, height), default: None. 

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

74 elev : float, default: 30 

75 The elevation angle in degrees rotates the camera above and below 

76 the x-y plane, with a positive angle corresponding to a location 

77 above the plane. 

78 azim : float, default: -60 

79 The azimuthal angle in degrees rotates the camera about the z axis, 

80 with a positive angle corresponding to a right-handed rotation. In 

81 other words, a positive azimuth rotates the camera about the origin 

82 from its location along the +x axis towards the +y axis. 

83 roll : float, default: 0 

84 The roll angle in degrees rotates the camera about the viewing 

85 axis. A positive angle spins the camera clockwise, causing the 

86 scene to rotate counter-clockwise. 

87 sharez : Axes3D, optional 

88 Other Axes to share z-limits with. 

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

90 The projection type, default 'persp'. 

91 box_aspect : 3-tuple of floats, default: None 

92 Changes the physical dimensions of the Axes3D, such that the ratio 

93 of the axis lengths in display units is x:y:z. 

94 If None, defaults to 4:4:3 

95 computed_zorder : bool, default: True 

96 If True, the draw order is computed based on the average position 

97 of the `.Artist`\\s along the view direction. 

98 Set to False if you want to manually control the order in which 

99 Artists are drawn on top of each other using their *zorder* 

100 attribute. This can be used for fine-tuning if the automatic order 

101 does not produce the desired result. Note however, that a manual 

102 zorder will only be correct for a limited view angle. If the figure 

103 is rotated by the user, it will look wrong from certain angles. 

104 auto_add_to_figure : bool, default: False 

105 Prior to Matplotlib 3.4 Axes3D would add themselves 

106 to their host Figure on init. Other Axes class do not 

107 do this. 

108 

109 This behavior is deprecated in 3.4, the default is 

110 changed to False in 3.6. The keyword will be undocumented 

111 and a non-False value will be an error in 3.7. 

112 focal_length : float, default: None 

113 For a projection type of 'persp', the focal length of the virtual 

114 camera. Must be > 0. If None, defaults to 1. 

115 For a projection type of 'ortho', must be set to either None 

116 or infinity (numpy.inf). If None, defaults to infinity. 

117 The focal length can be computed from a desired Field Of View via 

118 the equation: focal_length = 1/tan(FOV/2) 

119 

120 **kwargs 

121 Other optional keyword arguments: 

122 

123 %(Axes3D:kwdoc)s 

124 """ 

125 

126 if rect is None: 

127 rect = [0.0, 0.0, 1.0, 1.0] 

128 

129 self.initial_azim = azim 

130 self.initial_elev = elev 

131 self.initial_roll = roll 

132 self.set_proj_type(proj_type, focal_length) 

133 self.computed_zorder = computed_zorder 

134 

135 self.xy_viewLim = Bbox.unit() 

136 self.zz_viewLim = Bbox.unit() 

137 self.xy_dataLim = Bbox.unit() 

138 # z-limits are encoded in the x-component of the Bbox, y is un-used 

139 self.zz_dataLim = Bbox.unit() 

140 

141 # inhibit autoscale_view until the axes are defined 

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

143 self.view_init(self.initial_elev, self.initial_azim, self.initial_roll) 

144 

145 self._sharez = sharez 

146 if sharez is not None: 

147 self._shared_axes["z"].join(self, sharez) 

148 self._adjustable = 'datalim' 

149 

150 auto_add_to_figure = kwargs.pop('auto_add_to_figure', False) 

151 

152 super().__init__( 

153 fig, rect, frameon=True, box_aspect=box_aspect, *args, **kwargs 

154 ) 

155 # Disable drawing of axes by base class 

156 super().set_axis_off() 

157 # Enable drawing of axes by Axes3D class 

158 self.set_axis_on() 

159 self.M = None 

160 

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

162 self.fmt_zdata = None 

163 

164 self.mouse_init() 

165 self.figure.canvas.callbacks._connect_picklable( 

166 'motion_notify_event', self._on_move) 

167 self.figure.canvas.callbacks._connect_picklable( 

168 'button_press_event', self._button_press) 

169 self.figure.canvas.callbacks._connect_picklable( 

170 'button_release_event', self._button_release) 

171 self.set_top_view() 

172 

173 self.patch.set_linewidth(0) 

174 # Calculate the pseudo-data width and height 

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

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

177 

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

179 # for bounding box calculations 

180 self.spines[:].set_visible(False) 

181 

182 if auto_add_to_figure: 

183 _api.warn_deprecated( 

184 "3.4", removal="3.7", message="Axes3D(fig) adding itself " 

185 "to the figure is deprecated since %(since)s. " 

186 "Pass the keyword argument auto_add_to_figure=False " 

187 "and use fig.add_axes(ax) to suppress this warning. " 

188 "The default value of auto_add_to_figure is changed to " 

189 "False in mpl3.6 and True values will " 

190 "no longer work %(removal)s. This is consistent with " 

191 "other Axes classes.") 

192 fig.add_axes(self) 

193 

194 def set_axis_off(self): 

195 self._axis3don = False 

196 self.stale = True 

197 

198 def set_axis_on(self): 

199 self._axis3don = True 

200 self.stale = True 

201 

202 def convert_zunits(self, z): 

203 """ 

204 For artists in an Axes, if the zaxis has units support, 

205 convert *z* using zaxis unit type 

206 """ 

207 return self.zaxis.convert_units(z) 

208 

209 def set_top_view(self): 

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

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

212 xdwl = 0.95 / self._dist 

213 xdw = 0.9 / self._dist 

214 ydwl = 0.95 / self._dist 

215 ydw = 0.9 / self._dist 

216 # Set the viewing pane. 

217 self.viewLim.intervalx = (-xdwl, xdw) 

218 self.viewLim.intervaly = (-ydwl, ydw) 

219 self.stale = True 

220 

221 def _init_axis(self): 

222 """Init 3D axes; overrides creation of regular X/Y axes.""" 

223 self.xaxis = axis3d.XAxis(self) 

224 self.yaxis = axis3d.YAxis(self) 

225 self.zaxis = axis3d.ZAxis(self) 

226 

227 def get_zaxis(self): 

228 """Return the ``ZAxis`` (`~.axis3d.Axis`) instance.""" 

229 return self.zaxis 

230 

231 get_zgridlines = _axis_method_wrapper("zaxis", "get_gridlines") 

232 get_zticklines = _axis_method_wrapper("zaxis", "get_ticklines") 

233 

234 w_xaxis = _api.deprecated("3.1", alternative="xaxis", removal="3.8")( 

235 property(lambda self: self.xaxis)) 

236 w_yaxis = _api.deprecated("3.1", alternative="yaxis", removal="3.8")( 

237 property(lambda self: self.yaxis)) 

238 w_zaxis = _api.deprecated("3.1", alternative="zaxis", removal="3.8")( 

239 property(lambda self: self.zaxis)) 

240 

241 def unit_cube(self, vals=None): 

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

243 return [(minx, miny, minz), 

244 (maxx, miny, minz), 

245 (maxx, maxy, minz), 

246 (minx, maxy, minz), 

247 (minx, miny, maxz), 

248 (maxx, miny, maxz), 

249 (maxx, maxy, maxz), 

250 (minx, maxy, maxz)] 

251 

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

253 if M is None: 

254 M = self.M 

255 xyzs = self.unit_cube(vals) 

256 tcube = proj3d.proj_points(xyzs, M) 

257 return tcube 

258 

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

260 tc = self.tunit_cube(vals, M) 

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

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

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

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

265 

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

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

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

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

270 

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

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

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

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

275 return edges 

276 

277 def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): 

278 """ 

279 Set the aspect ratios. 

280 

281 Parameters 

282 ---------- 

283 aspect : {'auto', 'equal', 'equalxy', 'equalxz', 'equalyz'} 

284 Possible values: 

285 

286 ========= ================================================== 

287 value description 

288 ========= ================================================== 

289 'auto' automatic; fill the position rectangle with data. 

290 'equal' adapt all the axes to have equal aspect ratios. 

291 'equalxy' adapt the x and y axes to have equal aspect ratios. 

292 'equalxz' adapt the x and z axes to have equal aspect ratios. 

293 'equalyz' adapt the y and z axes to have equal aspect ratios. 

294 ========= ================================================== 

295 

296 adjustable : None 

297 Currently ignored by Axes3D 

298 

299 If not *None*, this defines which parameter will be adjusted to 

300 meet the required aspect. See `.set_adjustable` for further 

301 details. 

302 

303 anchor : None or str or 2-tuple of float, optional 

304 If not *None*, this defines where the Axes will be drawn if there 

305 is extra space due to aspect constraints. The most common way to 

306 to specify the anchor are abbreviations of cardinal directions: 

307 

308 ===== ===================== 

309 value description 

310 ===== ===================== 

311 'C' centered 

312 'SW' lower left corner 

313 'S' middle of bottom edge 

314 'SE' lower right corner 

315 etc. 

316 ===== ===================== 

317 

318 See `~.Axes.set_anchor` for further details. 

319 

320 share : bool, default: False 

321 If ``True``, apply the settings to all shared Axes. 

322 

323 See Also 

324 -------- 

325 mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect 

326 """ 

327 _api.check_in_list(('auto', 'equal', 'equalxy', 'equalyz', 'equalxz'), 

328 aspect=aspect) 

329 super().set_aspect( 

330 aspect='auto', adjustable=adjustable, anchor=anchor, share=share) 

331 self._aspect = aspect 

332 

333 if aspect in ('equal', 'equalxy', 'equalxz', 'equalyz'): 

334 if aspect == 'equal': 

335 ax_indices = [0, 1, 2] 

336 elif aspect == 'equalxy': 

337 ax_indices = [0, 1] 

338 elif aspect == 'equalxz': 

339 ax_indices = [0, 2] 

340 elif aspect == 'equalyz': 

341 ax_indices = [1, 2] 

342 

343 view_intervals = np.array([self.xaxis.get_view_interval(), 

344 self.yaxis.get_view_interval(), 

345 self.zaxis.get_view_interval()]) 

346 mean = np.mean(view_intervals, axis=1) 

347 ptp = np.ptp(view_intervals, axis=1) 

348 delta = max(ptp[ax_indices]) 

349 scale = self._box_aspect[ptp == delta][0] 

350 deltas = delta * self._box_aspect / scale 

351 

352 for i, set_lim in enumerate((self.set_xlim3d, 

353 self.set_ylim3d, 

354 self.set_zlim3d)): 

355 if i in ax_indices: 

356 set_lim(mean[i] - deltas[i]/2., mean[i] + deltas[i]/2.) 

357 

358 def set_box_aspect(self, aspect, *, zoom=1): 

359 """ 

360 Set the Axes box aspect. 

361 

362 The box aspect is the ratio of height to width in display 

363 units for each face of the box when viewed perpendicular to 

364 that face. This is not to be confused with the data aspect (see 

365 `~.Axes3D.set_aspect`). The default ratios are 4:4:3 (x:y:z). 

366 

367 To simulate having equal aspect in data space, set the box 

368 aspect to match your data range in each dimension. 

369 

370 *zoom* controls the overall size of the Axes3D in the figure. 

371 

372 Parameters 

373 ---------- 

374 aspect : 3-tuple of floats or None 

375 Changes the physical dimensions of the Axes3D, such that the ratio 

376 of the axis lengths in display units is x:y:z. 

377 If None, defaults to (4,4,3). 

378 

379 zoom : float, default: 1 

380 Control overall size of the Axes3D in the figure. Must be > 0. 

381 """ 

382 if zoom <= 0: 

383 raise ValueError(f'Argument zoom = {zoom} must be > 0') 

384 

385 if aspect is None: 

386 aspect = np.asarray((4, 4, 3), dtype=float) 

387 else: 

388 aspect = np.asarray(aspect, dtype=float) 

389 _api.check_shape((3,), aspect=aspect) 

390 # default scale tuned to match the mpl32 appearance. 

391 aspect *= 1.8294640721620434 * zoom / np.linalg.norm(aspect) 

392 

393 self._box_aspect = aspect 

394 self.stale = True 

395 

396 def apply_aspect(self, position=None): 

397 if position is None: 

398 position = self.get_position(original=True) 

399 

400 # in the superclass, we would go through and actually deal with axis 

401 # scales and box/datalim. Those are all irrelevant - all we need to do 

402 # is make sure our coordinate system is square. 

403 trans = self.get_figure().transSubfigure 

404 bb = mtransforms.Bbox.unit().transformed(trans) 

405 # this is the physical aspect of the panel (or figure): 

406 fig_aspect = bb.height / bb.width 

407 

408 box_aspect = 1 

409 pb = position.frozen() 

410 pb1 = pb.shrunk_to_aspect(box_aspect, pb, fig_aspect) 

411 self._set_position(pb1.anchored(self.get_anchor(), pb), 'active') 

412 

413 @martist.allow_rasterization 

414 def draw(self, renderer): 

415 if not self.get_visible(): 

416 return 

417 self._unstale_viewLim() 

418 

419 # draw the background patch 

420 self.patch.draw(renderer) 

421 self._frameon = False 

422 

423 # first, set the aspect 

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

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

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

427 # of the Axes 

428 locator = self.get_axes_locator() 

429 self.apply_aspect(locator(self, renderer) if locator else None) 

430 

431 # add the projection matrix to the renderer 

432 self.M = self.get_proj() 

433 

434 collections_and_patches = ( 

435 artist for artist in self._children 

436 if isinstance(artist, (mcoll.Collection, mpatches.Patch)) 

437 and artist.get_visible()) 

438 if self.computed_zorder: 

439 # Calculate projection of collections and patches and zorder 

440 # them. Make sure they are drawn above the grids. 

441 zorder_offset = max(axis.get_zorder() 

442 for axis in self._axis_map.values()) + 1 

443 collection_zorder = patch_zorder = zorder_offset 

444 

445 for artist in sorted(collections_and_patches, 

446 key=lambda artist: artist.do_3d_projection(), 

447 reverse=True): 

448 if isinstance(artist, mcoll.Collection): 

449 artist.zorder = collection_zorder 

450 collection_zorder += 1 

451 elif isinstance(artist, mpatches.Patch): 

452 artist.zorder = patch_zorder 

453 patch_zorder += 1 

454 else: 

455 for artist in collections_and_patches: 

456 artist.do_3d_projection() 

457 

458 if self._axis3don: 

459 # Draw panes first 

460 for axis in self._axis_map.values(): 

461 axis.draw_pane(renderer) 

462 # Then axes 

463 for axis in self._axis_map.values(): 

464 axis.draw(renderer) 

465 

466 # Then rest 

467 super().draw(renderer) 

468 

469 def get_axis_position(self): 

470 vals = self.get_w_lims() 

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

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

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

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

475 return xhigh, yhigh, zhigh 

476 

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

478 pass 

479 

480 get_autoscalez_on = _axis_method_wrapper("zaxis", "_get_autoscale_on") 

481 set_autoscalez_on = _axis_method_wrapper("zaxis", "_set_autoscale_on") 

482 

483 def set_zmargin(self, m): 

484 """ 

485 Set padding of Z data limits prior to autoscaling. 

486 

487 *m* times the data interval will be added to each end of that interval 

488 before it is used in autoscaling. If *m* is negative, this will clip 

489 the data range instead of expanding it. 

490 

491 For example, if your data is in the range [0, 2], a margin of 0.1 will 

492 result in a range [-0.2, 2.2]; a margin of -0.1 will result in a range 

493 of [0.2, 1.8]. 

494 

495 Parameters 

496 ---------- 

497 m : float greater than -0.5 

498 """ 

499 if m <= -0.5: 

500 raise ValueError("margin must be greater than -0.5") 

501 self._zmargin = m 

502 self._request_autoscale_view("z") 

503 self.stale = True 

504 

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

506 """ 

507 Set or retrieve autoscaling margins. 

508 

509 See `.Axes.margins` for full documentation. Because this function 

510 applies to 3D Axes, it also takes a *z* argument, and returns 

511 ``(xmargin, ymargin, zmargin)``. 

512 """ 

513 if margins and (x is not None or y is not None or z is not None): 

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

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

516 elif len(margins) == 1: 

517 x = y = z = margins[0] 

518 elif len(margins) == 3: 

519 x, y, z = margins 

520 elif margins: 

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

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

523 

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

525 if tight is not True: 

526 _api.warn_external(f'ignoring tight={tight!r} in get mode') 

527 return self._xmargin, self._ymargin, self._zmargin 

528 

529 if x is not None: 

530 self.set_xmargin(x) 

531 if y is not None: 

532 self.set_ymargin(y) 

533 if z is not None: 

534 self.set_zmargin(z) 

535 

536 self.autoscale_view( 

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

538 scalez=(z is not None) 

539 ) 

540 

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

542 """ 

543 Convenience method for simple axis view autoscaling. 

544 

545 See `.Axes.autoscale` for full documentation. Because this function 

546 applies to 3D Axes, *axis* can also be set to 'z', and setting *axis* 

547 to 'both' autoscales all three axes. 

548 """ 

549 if enable is None: 

550 scalex = True 

551 scaley = True 

552 scalez = True 

553 else: 

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

555 self.set_autoscalex_on(bool(enable)) 

556 scalex = self.get_autoscalex_on() 

557 else: 

558 scalex = False 

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

560 self.set_autoscaley_on(bool(enable)) 

561 scaley = self.get_autoscaley_on() 

562 else: 

563 scaley = False 

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

565 self.set_autoscalez_on(bool(enable)) 

566 scalez = self.get_autoscalez_on() 

567 else: 

568 scalez = False 

569 if scalex: 

570 self._request_autoscale_view("x", tight=tight) 

571 if scaley: 

572 self._request_autoscale_view("y", tight=tight) 

573 if scalez: 

574 self._request_autoscale_view("z", tight=tight) 

575 

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

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

578 # minimum sized rectangular volume holds the data. 

579 if np.shape(X) == np.shape(Y): 

580 self.xy_dataLim.update_from_data_xy( 

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

582 else: 

583 self.xy_dataLim.update_from_data_x(X, not had_data) 

584 self.xy_dataLim.update_from_data_y(Y, not had_data) 

585 if Z is not None: 

586 self.zz_dataLim.update_from_data_x(Z, not had_data) 

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

588 self.autoscale_view() 

589 

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

591 scalez=True): 

592 """ 

593 Autoscale the view limits using the data limits. 

594 

595 See `.Axes.autoscale_view` for full documentation. Because this 

596 function applies to 3D Axes, it also takes a *scalez* argument. 

597 """ 

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

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

600 if tight is None: 

601 _tight = self._tight 

602 if not _tight: 

603 # if image data only just use the datalim 

604 for artist in self._children: 

605 if isinstance(artist, mimage.AxesImage): 

606 _tight = True 

607 elif isinstance(artist, (mlines.Line2D, mpatches.Patch)): 

608 _tight = False 

609 break 

610 else: 

611 _tight = self._tight = bool(tight) 

612 

613 if scalex and self.get_autoscalex_on(): 

614 self._shared_axes["x"].clean() 

615 x0, x1 = self.xy_dataLim.intervalx 

616 xlocator = self.xaxis.get_major_locator() 

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

618 if self._xmargin > 0: 

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

620 x0 -= delta 

621 x1 += delta 

622 if not _tight: 

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

624 self.set_xbound(x0, x1) 

625 

626 if scaley and self.get_autoscaley_on(): 

627 self._shared_axes["y"].clean() 

628 y0, y1 = self.xy_dataLim.intervaly 

629 ylocator = self.yaxis.get_major_locator() 

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

631 if self._ymargin > 0: 

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

633 y0 -= delta 

634 y1 += delta 

635 if not _tight: 

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

637 self.set_ybound(y0, y1) 

638 

639 if scalez and self.get_autoscalez_on(): 

640 self._shared_axes["z"].clean() 

641 z0, z1 = self.zz_dataLim.intervalx 

642 zlocator = self.zaxis.get_major_locator() 

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

644 if self._zmargin > 0: 

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

646 z0 -= delta 

647 z1 += delta 

648 if not _tight: 

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

650 self.set_zbound(z0, z1) 

651 

652 def get_w_lims(self): 

653 """Get 3D world limits.""" 

654 minx, maxx = self.get_xlim3d() 

655 miny, maxy = self.get_ylim3d() 

656 minz, maxz = self.get_zlim3d() 

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

658 

659 # set_xlim, set_ylim are directly inherited from base Axes. 

660 @_api.make_keyword_only("3.6", "emit") 

661 def set_zlim(self, bottom=None, top=None, emit=True, auto=False, 

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

663 """ 

664 Set 3D z limits. 

665 

666 See `.Axes.set_ylim` for full documentation 

667 """ 

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

669 bottom, top = bottom 

670 if zmin is not None: 

671 if bottom is not None: 

672 raise TypeError("Cannot pass both 'bottom' and 'zmin'") 

673 bottom = zmin 

674 if zmax is not None: 

675 if top is not None: 

676 raise TypeError("Cannot pass both 'top' and 'zmax'") 

677 top = zmax 

678 return self.zaxis._set_lim(bottom, top, emit=emit, auto=auto) 

679 

680 set_xlim3d = maxes.Axes.set_xlim 

681 set_ylim3d = maxes.Axes.set_ylim 

682 set_zlim3d = set_zlim 

683 

684 def get_xlim(self): 

685 # docstring inherited 

686 return tuple(self.xy_viewLim.intervalx) 

687 

688 def get_ylim(self): 

689 # docstring inherited 

690 return tuple(self.xy_viewLim.intervaly) 

691 

692 def get_zlim(self): 

693 """Get 3D z limits.""" 

694 return tuple(self.zz_viewLim.intervalx) 

695 

696 get_zscale = _axis_method_wrapper("zaxis", "get_scale") 

697 

698 # Redefine all three methods to overwrite their docstrings. 

699 set_xscale = _axis_method_wrapper("xaxis", "_set_axes_scale") 

700 set_yscale = _axis_method_wrapper("yaxis", "_set_axes_scale") 

701 set_zscale = _axis_method_wrapper("zaxis", "_set_axes_scale") 

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

703 """ 

704 Set the {}-axis scale. 

705 

706 Parameters 

707 ---------- 

708 value : {{"linear"}} 

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

710 linear scales; other scales yield nonsensical results. 

711 

712 **kwargs 

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

714 none of them is applicable for linear scales. 

715 """.format, 

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

717 

718 get_zticks = _axis_method_wrapper("zaxis", "get_ticklocs") 

719 set_zticks = _axis_method_wrapper("zaxis", "set_ticks") 

720 get_zmajorticklabels = _axis_method_wrapper("zaxis", "get_majorticklabels") 

721 get_zminorticklabels = _axis_method_wrapper("zaxis", "get_minorticklabels") 

722 get_zticklabels = _axis_method_wrapper("zaxis", "get_ticklabels") 

723 set_zticklabels = _axis_method_wrapper( 

724 "zaxis", "_set_ticklabels", 

725 doc_sub={"Axis.set_ticks": "Axes3D.set_zticks"}) 

726 

727 zaxis_date = _axis_method_wrapper("zaxis", "axis_date") 

728 if zaxis_date.__doc__: 

729 zaxis_date.__doc__ += textwrap.dedent(""" 

730 

731 Notes 

732 ----- 

733 This function is merely provided for completeness, but 3D axes do not 

734 support dates for ticks, and so this may not work as expected. 

735 """) 

736 

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

738 """Currently not implemented for 3D axes, and returns *None*.""" 

739 return None 

740 

741 def view_init(self, elev=None, azim=None, roll=None, vertical_axis="z"): 

742 """ 

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

744 

745 This can be used to rotate the axes programmatically. 

746 

747 To look normal to the primary planes, the following elevation and 

748 azimuth angles can be used. A roll angle of 0, 90, 180, or 270 deg 

749 will rotate these views while keeping the axes at right angles. 

750 

751 ========== ==== ==== 

752 view plane elev azim 

753 ========== ==== ==== 

754 XY 90 -90 

755 XZ 0 -90 

756 YZ 0 0 

757 -XY -90 90 

758 -XZ 0 90 

759 -YZ 0 180 

760 ========== ==== ==== 

761 

762 Parameters 

763 ---------- 

764 elev : float, default: None 

765 The elevation angle in degrees rotates the camera above the plane 

766 pierced by the vertical axis, with a positive angle corresponding 

767 to a location above that plane. For example, with the default 

768 vertical axis of 'z', the elevation defines the angle of the camera 

769 location above the x-y plane. 

770 If None, then the initial value as specified in the `Axes3D` 

771 constructor is used. 

772 azim : float, default: None 

773 The azimuthal angle in degrees rotates the camera about the 

774 vertical axis, with a positive angle corresponding to a 

775 right-handed rotation. For example, with the default vertical axis 

776 of 'z', a positive azimuth rotates the camera about the origin from 

777 its location along the +x axis towards the +y axis. 

778 If None, then the initial value as specified in the `Axes3D` 

779 constructor is used. 

780 roll : float, default: None 

781 The roll angle in degrees rotates the camera about the viewing 

782 axis. A positive angle spins the camera clockwise, causing the 

783 scene to rotate counter-clockwise. 

784 If None, then the initial value as specified in the `Axes3D` 

785 constructor is used. 

786 vertical_axis : {"z", "x", "y"}, default: "z" 

787 The axis to align vertically. *azim* rotates about this axis. 

788 """ 

789 

790 self._dist = 10 # The camera distance from origin. Behaves like zoom 

791 

792 if elev is None: 

793 self.elev = self.initial_elev 

794 else: 

795 self.elev = elev 

796 

797 if azim is None: 

798 self.azim = self.initial_azim 

799 else: 

800 self.azim = azim 

801 

802 if roll is None: 

803 self.roll = self.initial_roll 

804 else: 

805 self.roll = roll 

806 

807 self._vertical_axis = _api.check_getitem( 

808 dict(x=0, y=1, z=2), vertical_axis=vertical_axis 

809 ) 

810 

811 def set_proj_type(self, proj_type, focal_length=None): 

812 """ 

813 Set the projection type. 

814 

815 Parameters 

816 ---------- 

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

818 The projection type. 

819 focal_length : float, default: None 

820 For a projection type of 'persp', the focal length of the virtual 

821 camera. Must be > 0. If None, defaults to 1. 

822 The focal length can be computed from a desired Field Of View via 

823 the equation: focal_length = 1/tan(FOV/2) 

824 """ 

825 _api.check_in_list(['persp', 'ortho'], proj_type=proj_type) 

826 if proj_type == 'persp': 

827 if focal_length is None: 

828 focal_length = 1 

829 elif focal_length <= 0: 

830 raise ValueError(f"focal_length = {focal_length} must be " 

831 "greater than 0") 

832 self._focal_length = focal_length 

833 else: # 'ortho': 

834 if focal_length not in (None, np.inf): 

835 raise ValueError(f"focal_length = {focal_length} must be " 

836 f"None for proj_type = {proj_type}") 

837 self._focal_length = np.inf 

838 

839 def _roll_to_vertical(self, arr): 

840 """Roll arrays to match the different vertical axis.""" 

841 return np.roll(arr, self._vertical_axis - 2) 

842 

843 def get_proj(self): 

844 """Create the projection matrix from the current viewing position.""" 

845 

846 # Transform to uniform world coordinates 0-1, 0-1, 0-1 

847 box_aspect = self._roll_to_vertical(self._box_aspect) 

848 worldM = proj3d.world_transformation( 

849 *self.get_xlim3d(), 

850 *self.get_ylim3d(), 

851 *self.get_zlim3d(), 

852 pb_aspect=box_aspect, 

853 ) 

854 

855 # Look into the middle of the new coordinates: 

856 R = 0.5 * box_aspect 

857 

858 # elev stores the elevation angle in the z plane 

859 # azim stores the azimuth angle in the x,y plane 

860 # roll stores the roll angle about the view axis 

861 elev_rad = np.deg2rad(art3d._norm_angle(self.elev)) 

862 azim_rad = np.deg2rad(art3d._norm_angle(self.azim)) 

863 roll_rad = np.deg2rad(art3d._norm_angle(self.roll)) 

864 

865 # Coordinates for a point that rotates around the box of data. 

866 # p0, p1 corresponds to rotating the box only around the 

867 # vertical axis. 

868 # p2 corresponds to rotating the box only around the horizontal 

869 # axis. 

870 p0 = np.cos(elev_rad) * np.cos(azim_rad) 

871 p1 = np.cos(elev_rad) * np.sin(azim_rad) 

872 p2 = np.sin(elev_rad) 

873 

874 # When changing vertical axis the coordinates changes as well. 

875 # Roll the values to get the same behaviour as the default: 

876 ps = self._roll_to_vertical([p0, p1, p2]) 

877 

878 # The coordinates for the eye viewing point. The eye is looking 

879 # towards the middle of the box of data from a distance: 

880 eye = R + self._dist * ps 

881 

882 # TODO: Is this being used somewhere? Can it be removed? 

883 self.eye = eye 

884 self.vvec = R - eye 

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

886 

887 # Define which axis should be vertical. A negative value 

888 # indicates the plot is upside down and therefore the values 

889 # have been reversed: 

890 V = np.zeros(3) 

891 V[self._vertical_axis] = -1 if abs(elev_rad) > 0.5 * np.pi else 1 

892 

893 # Generate the view and projection transformation matrices 

894 if self._focal_length == np.inf: 

895 # Orthographic projection 

896 viewM = proj3d.view_transformation(eye, R, V, roll_rad) 

897 projM = proj3d.ortho_transformation(-self._dist, self._dist) 

898 else: 

899 # Perspective projection 

900 # Scale the eye dist to compensate for the focal length zoom effect 

901 eye_focal = R + self._dist * ps * self._focal_length 

902 viewM = proj3d.view_transformation(eye_focal, R, V, roll_rad) 

903 projM = proj3d.persp_transformation(-self._dist, 

904 self._dist, 

905 self._focal_length) 

906 

907 # Combine all the transformation matrices to get the final projection 

908 M0 = np.dot(viewM, worldM) 

909 M = np.dot(projM, M0) 

910 return M 

911 

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

913 """ 

914 Set the mouse buttons for 3D rotation and zooming. 

915 

916 Parameters 

917 ---------- 

918 rotate_btn : int or list of int, default: 1 

919 The mouse button or buttons to use for 3D rotation of the axes. 

920 zoom_btn : int or list of int, default: 3 

921 The mouse button or buttons to use to zoom the 3D axes. 

922 """ 

923 self.button_pressed = None 

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

925 # a regular list to avoid comparisons against None 

926 # which breaks in recent versions of numpy. 

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

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

929 

930 def disable_mouse_rotation(self): 

931 """Disable mouse buttons for 3D rotation and zooming.""" 

932 self.mouse_init(rotate_btn=[], zoom_btn=[]) 

933 

934 def can_zoom(self): 

935 """ 

936 Return whether this Axes supports the zoom box button functionality. 

937 

938 Axes3D objects do not use the zoom box button. 

939 """ 

940 return False 

941 

942 def can_pan(self): 

943 """ 

944 Return whether this Axes supports the pan/zoom button functionality. 

945 

946 Axes3d objects do not use the pan/zoom button. 

947 """ 

948 return False 

949 

950 def sharez(self, other): 

951 """ 

952 Share the z-axis with *other*. 

953 

954 This is equivalent to passing ``sharex=other`` when constructing the 

955 Axes, and cannot be used if the z-axis is already being shared with 

956 another Axes. 

957 """ 

958 _api.check_isinstance(maxes._base._AxesBase, other=other) 

959 if self._sharez is not None and other is not self._sharez: 

960 raise ValueError("z-axis is already shared") 

961 self._shared_axes["z"].join(self, other) 

962 self._sharez = other 

963 self.zaxis.major = other.zaxis.major # Ticker instances holding 

964 self.zaxis.minor = other.zaxis.minor # locator and formatter. 

965 z0, z1 = other.get_zlim() 

966 self.set_zlim(z0, z1, emit=False, auto=other.get_autoscalez_on()) 

967 self.zaxis._scale = other.zaxis._scale 

968 

969 def clear(self): 

970 # docstring inherited. 

971 super().clear() 

972 if self._focal_length == np.inf: 

973 self._zmargin = mpl.rcParams['axes.zmargin'] 

974 else: 

975 self._zmargin = 0. 

976 self.grid(mpl.rcParams['axes3d.grid']) 

977 

978 def _button_press(self, event): 

979 if event.inaxes == self: 

980 self.button_pressed = event.button 

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

982 toolbar = getattr(self.figure.canvas, "toolbar") 

983 if toolbar and toolbar._nav_stack() is None: 

984 self.figure.canvas.toolbar.push_current() 

985 

986 def _button_release(self, event): 

987 self.button_pressed = None 

988 toolbar = getattr(self.figure.canvas, "toolbar") 

989 if toolbar: 

990 self.figure.canvas.toolbar.push_current() 

991 

992 def _get_view(self): 

993 # docstring inherited 

994 return (self.get_xlim(), self.get_ylim(), self.get_zlim(), 

995 self.elev, self.azim, self.roll) 

996 

997 def _set_view(self, view): 

998 # docstring inherited 

999 xlim, ylim, zlim, elev, azim, roll = view 

1000 self.set(xlim=xlim, ylim=ylim, zlim=zlim) 

1001 self.elev = elev 

1002 self.azim = azim 

1003 self.roll = roll 

1004 

1005 def format_zdata(self, z): 

1006 """ 

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

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

1009 back on the zaxis major formatter 

1010 """ 

1011 try: 

1012 return self.fmt_zdata(z) 

1013 except (AttributeError, TypeError): 

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

1015 val = func(z) 

1016 return val 

1017 

1018 def format_coord(self, xd, yd): 

1019 """ 

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

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

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

1023 """ 

1024 

1025 if self.M is None: 

1026 return '' 

1027 

1028 if self.button_pressed in self._rotate_btn: 

1029 # ignore xd and yd and display angles instead 

1030 norm_elev = art3d._norm_angle(self.elev) 

1031 norm_azim = art3d._norm_angle(self.azim) 

1032 norm_roll = art3d._norm_angle(self.roll) 

1033 return (f"elevation={norm_elev:.0f}\N{DEGREE SIGN}, " 

1034 f"azimuth={norm_azim:.0f}\N{DEGREE SIGN}, " 

1035 f"roll={norm_roll:.0f}\N{DEGREE SIGN}" 

1036 ).replace("-", "\N{MINUS SIGN}") 

1037 

1038 # nearest edge 

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

1040 key=lambda edge: proj3d._line2d_seg_dist( 

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

1042 

1043 # scale the z value to match 

1044 x0, y0, z0 = p0 

1045 x1, y1, z1 = p1 

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

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

1048 dt = d0+d1 

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

1050 

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

1052 

1053 xs = self.format_xdata(x) 

1054 ys = self.format_ydata(y) 

1055 zs = self.format_zdata(z) 

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

1057 

1058 def _on_move(self, event): 

1059 """ 

1060 Mouse moving. 

1061 

1062 By default, button-1 rotates and button-3 zooms; these buttons can be 

1063 modified via `mouse_init`. 

1064 """ 

1065 

1066 if not self.button_pressed: 

1067 return 

1068 

1069 if self.M is None: 

1070 return 

1071 

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

1073 # In case the mouse is out of bounds. 

1074 if x is None: 

1075 return 

1076 

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

1078 w = self._pseudo_w 

1079 h = self._pseudo_h 

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

1081 

1082 # Rotation 

1083 if self.button_pressed in self._rotate_btn: 

1084 # rotate viewing point 

1085 # get the x and y pixel coords 

1086 if dx == 0 and dy == 0: 

1087 return 

1088 

1089 roll = np.deg2rad(self.roll) 

1090 delev = -(dy/h)*180*np.cos(roll) + (dx/w)*180*np.sin(roll) 

1091 dazim = -(dy/h)*180*np.sin(roll) - (dx/w)*180*np.cos(roll) 

1092 self.elev = self.elev + delev 

1093 self.azim = self.azim + dazim 

1094 self.get_proj() 

1095 self.stale = True 

1096 self.figure.canvas.draw_idle() 

1097 

1098 elif self.button_pressed == 2: 

1099 # pan view 

1100 # get the x and y pixel coords 

1101 if dx == 0 and dy == 0: 

1102 return 

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

1104 dx = 1-((w - dx)/w) 

1105 dy = 1-((h - dy)/h) 

1106 elev = np.deg2rad(self.elev) 

1107 azim = np.deg2rad(self.azim) 

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

1109 dxx = (maxx-minx)*(dy*np.sin(elev)*np.cos(azim) + dx*np.sin(azim)) 

1110 dyy = (maxy-miny)*(-dx*np.cos(azim) + dy*np.sin(elev)*np.sin(azim)) 

1111 dzz = (maxz-minz)*(-dy*np.cos(elev)) 

1112 # pan 

1113 self.set_xlim3d(minx + dxx, maxx + dxx) 

1114 self.set_ylim3d(miny + dyy, maxy + dyy) 

1115 self.set_zlim3d(minz + dzz, maxz + dzz) 

1116 self.get_proj() 

1117 self.figure.canvas.draw_idle() 

1118 

1119 # Zoom 

1120 elif self.button_pressed in self._zoom_btn: 

1121 # zoom view 

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

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

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

1125 dx = (maxx-minx)*df 

1126 dy = (maxy-miny)*df 

1127 dz = (maxz-minz)*df 

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

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

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

1131 self.get_proj() 

1132 self.figure.canvas.draw_idle() 

1133 

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

1135 """ 

1136 Set zlabel. See doc for `.set_ylabel` for description. 

1137 """ 

1138 if labelpad is not None: 

1139 self.zaxis.labelpad = labelpad 

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

1141 

1142 def get_zlabel(self): 

1143 """ 

1144 Get the z-label text string. 

1145 """ 

1146 label = self.zaxis.get_label() 

1147 return label.get_text() 

1148 

1149 # Axes rectangle characteristics 

1150 

1151 def get_frame_on(self): 

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

1153 return self._frameon 

1154 

1155 def set_frame_on(self, b): 

1156 """ 

1157 Set whether the 3D axes panels are drawn. 

1158 

1159 Parameters 

1160 ---------- 

1161 b : bool 

1162 """ 

1163 self._frameon = bool(b) 

1164 self.stale = True 

1165 

1166 @_api.rename_parameter("3.5", "b", "visible") 

1167 def grid(self, visible=True, **kwargs): 

1168 """ 

1169 Set / unset 3D grid. 

1170 

1171 .. note:: 

1172 

1173 Currently, this function does not behave the same as 

1174 `.axes.Axes.grid`, but it is intended to eventually support that 

1175 behavior. 

1176 """ 

1177 # TODO: Operate on each axes separately 

1178 if len(kwargs): 

1179 visible = True 

1180 self._draw_grid = visible 

1181 self.stale = True 

1182 

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

1184 """ 

1185 Convenience method for changing the appearance of ticks and 

1186 tick labels. 

1187 

1188 See `.Axes.tick_params` for full documentation. Because this function 

1189 applies to 3D Axes, *axis* can also be set to 'z', and setting *axis* 

1190 to 'both' autoscales all three axes. 

1191 

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

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

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

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

1196 

1197 .. note:: 

1198 Axes3D currently ignores some of these settings. 

1199 """ 

1200 _api.check_in_list(['x', 'y', 'z', 'both'], axis=axis) 

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

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

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

1204 zkw = dict(kwargs) 

1205 zkw.pop('top', None) 

1206 zkw.pop('bottom', None) 

1207 zkw.pop('labeltop', None) 

1208 zkw.pop('labelbottom', None) 

1209 self.zaxis.set_tick_params(**zkw) 

1210 

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

1212 

1213 def invert_zaxis(self): 

1214 """ 

1215 Invert the z-axis. 

1216 """ 

1217 bottom, top = self.get_zlim() 

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

1219 

1220 def zaxis_inverted(self): 

1221 """ 

1222 Returns True if the z-axis is inverted. 

1223 """ 

1224 bottom, top = self.get_zlim() 

1225 return top < bottom 

1226 

1227 def get_zbound(self): 

1228 """ 

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

1230 """ 

1231 bottom, top = self.get_zlim() 

1232 if bottom < top: 

1233 return bottom, top 

1234 else: 

1235 return top, bottom 

1236 

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

1238 """ 

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

1240 

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

1242 It will not change the autoscaling setting (`.get_autoscalez_on()`). 

1243 """ 

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

1245 lower, upper = lower 

1246 

1247 old_lower, old_upper = self.get_zbound() 

1248 if lower is None: 

1249 lower = old_lower 

1250 if upper is None: 

1251 upper = old_upper 

1252 

1253 self.set_zlim(sorted((lower, upper), 

1254 reverse=bool(self.zaxis_inverted())), 

1255 auto=None) 

1256 

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

1258 """ 

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

1260 except for the *zdir* keyword, which sets the direction to be 

1261 used as the z direction. 

1262 """ 

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

1264 art3d.text_2d_to_3d(text, z, zdir) 

1265 return text 

1266 

1267 text3D = text 

1268 text2D = Axes.text 

1269 

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

1271 """ 

1272 Plot 2D or 3D data. 

1273 

1274 Parameters 

1275 ---------- 

1276 xs : 1D array-like 

1277 x coordinates of vertices. 

1278 ys : 1D array-like 

1279 y coordinates of vertices. 

1280 zs : float or 1D array-like 

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

1282 each point. 

1283 zdir : {'x', 'y', 'z'}, default: 'z' 

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

1285 **kwargs 

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

1287 """ 

1288 had_data = self.has_data() 

1289 

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

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

1292 # `_process_plot_var_args`). 

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

1294 zs, *args = args 

1295 if 'zs' in kwargs: 

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

1297 else: 

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

1299 

1300 # Match length 

1301 zs = np.broadcast_to(zs, np.shape(xs)) 

1302 

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

1304 for line in lines: 

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

1306 

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

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

1309 return lines 

1310 

1311 plot3D = plot 

1312 

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

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

1315 """ 

1316 Create a surface plot. 

1317 

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

1319 supports colormapping by supplying the *cmap* argument. 

1320 

1321 .. note:: 

1322 

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

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

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

1326 these numbers of points. 

1327 

1328 .. note:: 

1329 

1330 To maximize rendering speed consider setting *rstride* and *cstride* 

1331 to divisors of the number of rows minus 1 and columns minus 1 

1332 respectively. For example, given 51 rows rstride can be any of the 

1333 divisors of 50. 

1334 

1335 Similarly, a setting of *rstride* and *cstride* equal to 1 (or 

1336 *rcount* and *ccount* equal the number of rows and columns) can use 

1337 the optimized path. 

1338 

1339 Parameters 

1340 ---------- 

1341 X, Y, Z : 2D arrays 

1342 Data values. 

1343 

1344 rcount, ccount : int 

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

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

1347 numbers of points. Defaults to 50. 

1348 

1349 rstride, cstride : int 

1350 Downsampling stride in each direction. These arguments are 

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

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

1353 

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

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

1356 

1357 color : color-like 

1358 Color of the surface patches. 

1359 

1360 cmap : Colormap 

1361 Colormap of the surface patches. 

1362 

1363 facecolors : array-like of colors. 

1364 Colors of each individual patch. 

1365 

1366 norm : Normalize 

1367 Normalization for the colormap. 

1368 

1369 vmin, vmax : float 

1370 Bounds for the normalization. 

1371 

1372 shade : bool, default: True 

1373 Whether to shade the facecolors. Shading is always disabled when 

1374 *cmap* is specified. 

1375 

1376 lightsource : `~matplotlib.colors.LightSource` 

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

1378 

1379 **kwargs 

1380 Other keyword arguments are forwarded to `.Poly3DCollection`. 

1381 """ 

1382 

1383 had_data = self.has_data() 

1384 

1385 if Z.ndim != 2: 

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

1387 

1388 Z = cbook._to_unmasked_float_array(Z) 

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

1390 rows, cols = Z.shape 

1391 

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

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

1394 

1395 if has_stride and has_count: 

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

1397 

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

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

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

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

1402 

1403 if mpl.rcParams['_internal.classic_mode']: 

1404 # Strides have priority over counts in classic mode. 

1405 # So, only compute strides from counts 

1406 # if counts were explicitly given 

1407 compute_strides = has_count 

1408 else: 

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

1410 # Otherwise, compute the strides from the counts. 

1411 compute_strides = not has_stride 

1412 

1413 if compute_strides: 

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

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

1416 

1417 if 'facecolors' in kwargs: 

1418 fcolors = kwargs.pop('facecolors') 

1419 else: 

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

1421 if color is None: 

1422 color = self._get_lines.get_next_color() 

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

1424 fcolors = None 

1425 

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

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

1428 if shade is None: 

1429 raise ValueError("shade cannot be None.") 

1430 

1431 colset = [] # the sampled facecolor 

1432 if (rows - 1) % rstride == 0 and \ 

1433 (cols - 1) % cstride == 0 and \ 

1434 fcolors is None: 

1435 polys = np.stack( 

1436 [cbook._array_patch_perimeters(a, rstride, cstride) 

1437 for a in (X, Y, Z)], 

1438 axis=-1) 

1439 else: 

1440 # evenly spaced, and including both endpoints 

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

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

1443 

1444 polys = [] 

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

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

1447 ps = [ 

1448 # +1 ensures we share edges between polygons 

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

1450 for a in (X, Y, Z) 

1451 ] 

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

1453 ps = np.array(ps).T 

1454 polys.append(ps) 

1455 

1456 if fcolors is not None: 

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

1458 

1459 # In cases where there are NaNs in the data (possibly from masked 

1460 # arrays), artifacts can be introduced. Here check whether NaNs exist 

1461 # and remove the entries if so 

1462 if not isinstance(polys, np.ndarray) or np.isnan(polys).any(): 

1463 new_polys = [] 

1464 new_colset = [] 

1465 

1466 # Depending on fcolors, colset is either an empty list or has as 

1467 # many elements as polys. In the former case new_colset results in 

1468 # a list with None entries, that is discarded later. 

1469 for p, col in itertools.zip_longest(polys, colset): 

1470 new_poly = np.array(p)[~np.isnan(p).any(axis=1)] 

1471 if len(new_poly): 

1472 new_polys.append(new_poly) 

1473 new_colset.append(col) 

1474 

1475 # Replace previous polys and, if fcolors is not None, colset 

1476 polys = new_polys 

1477 if fcolors is not None: 

1478 colset = new_colset 

1479 

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

1481 # than others 

1482 polyc = art3d.Poly3DCollection(polys, **kwargs) 

1483 

1484 if fcolors is not None: 

1485 if shade: 

1486 colset = self._shade_colors( 

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

1488 polyc.set_facecolors(colset) 

1489 polyc.set_edgecolors(colset) 

1490 elif cmap: 

1491 # can't always vectorize, because polys might be jagged 

1492 if isinstance(polys, np.ndarray): 

1493 avg_z = polys[..., 2].mean(axis=-1) 

1494 else: 

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

1496 polyc.set_array(avg_z) 

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

1498 polyc.set_clim(vmin, vmax) 

1499 if norm is not None: 

1500 polyc.set_norm(norm) 

1501 else: 

1502 if shade: 

1503 colset = self._shade_colors( 

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

1505 else: 

1506 colset = color 

1507 polyc.set_facecolors(colset) 

1508 

1509 self.add_collection(polyc) 

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

1511 

1512 return polyc 

1513 

1514 def _generate_normals(self, polygons): 

1515 """ 

1516 Compute the normals of a list of polygons. 

1517 

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

1519 counterclockwise order, following the right hand rule. 

1520 

1521 Uses three points equally spaced around the polygon. 

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

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

1524 approximation. 

1525 

1526 Parameters 

1527 ---------- 

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

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

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

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

1532 be vectorized. 

1533 

1534 Returns 

1535 ------- 

1536 normals : (..., 3) array 

1537 A normal vector estimated for the polygon. 

1538 """ 

1539 if isinstance(polygons, np.ndarray): 

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

1541 # vectorize 

1542 n = polygons.shape[-2] 

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

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

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

1546 else: 

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

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

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

1550 for poly_i, ps in enumerate(polygons): 

1551 n = len(ps) 

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

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

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

1555 return np.cross(v1, v2) 

1556 

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

1558 """ 

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

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

1561 """ 

1562 if lightsource is None: 

1563 # chosen for backwards-compatibility 

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

1565 

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

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

1568 @ lightsource.direction) 

1569 mask = ~np.isnan(shade) 

1570 

1571 if mask.any(): 

1572 # convert dot product to allowed shading fractions 

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

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

1575 

1576 def norm(x): 

1577 return out_norm(in_norm(x)) 

1578 

1579 shade[~mask] = 0 

1580 

1581 color = mcolors.to_rgba_array(color) 

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

1583 # shape of shade should be (M,) 

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

1585 alpha = color[:, 3] 

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

1587 colors[:, 3] = alpha 

1588 else: 

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

1590 

1591 return colors 

1592 

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

1594 """ 

1595 Plot a 3D wireframe. 

1596 

1597 .. note:: 

1598 

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

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

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

1602 these numbers of points. 

1603 

1604 Parameters 

1605 ---------- 

1606 X, Y, Z : 2D arrays 

1607 Data values. 

1608 

1609 rcount, ccount : int 

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

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

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

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

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

1615 

1616 rstride, cstride : int 

1617 Downsampling stride in each direction. These arguments are 

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

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

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

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

1622 wireframe plot. 

1623 

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

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

1626 

1627 **kwargs 

1628 Other keyword arguments are forwarded to `.Line3DCollection`. 

1629 """ 

1630 

1631 had_data = self.has_data() 

1632 if Z.ndim != 2: 

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

1634 # FIXME: Support masked arrays 

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

1636 rows, cols = Z.shape 

1637 

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

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

1640 

1641 if has_stride and has_count: 

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

1643 

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

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

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

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

1648 

1649 if mpl.rcParams['_internal.classic_mode']: 

1650 # Strides have priority over counts in classic mode. 

1651 # So, only compute strides from counts 

1652 # if counts were explicitly given 

1653 if has_count: 

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

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

1656 else: 

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

1658 # Otherwise, compute the strides from the counts. 

1659 if not has_stride: 

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

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

1662 

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

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

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

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

1667 

1668 if rstride: 

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

1670 # Add the last index only if needed 

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

1672 rii += [rows-1] 

1673 else: 

1674 rii = [] 

1675 if cstride: 

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

1677 # Add the last index only if needed 

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

1679 cii += [cols-1] 

1680 else: 

1681 cii = [] 

1682 

1683 if rstride == 0 and cstride == 0: 

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

1685 

1686 # If the inputs were empty, then just 

1687 # reset everything. 

1688 if Z.size == 0: 

1689 rii = [] 

1690 cii = [] 

1691 

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

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

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

1695 

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

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

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

1699 

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

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

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

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

1704 

1705 linec = art3d.Line3DCollection(lines, **kwargs) 

1706 self.add_collection(linec) 

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

1708 

1709 return linec 

1710 

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

1712 lightsource=None, **kwargs): 

1713 """ 

1714 Plot a triangulated surface. 

1715 

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

1717 either:: 

1718 

1719 plot_trisurf(triangulation, ...) 

1720 

1721 where triangulation is a `~matplotlib.tri.Triangulation` object, or:: 

1722 

1723 plot_trisurf(X, Y, ...) 

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

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

1726 

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

1728 `.Triangulation` for a explanation of these possibilities. 

1729 

1730 The remaining arguments are:: 

1731 

1732 plot_trisurf(..., Z) 

1733 

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

1735 in the triangulation. 

1736 

1737 Parameters 

1738 ---------- 

1739 X, Y, Z : array-like 

1740 Data values as 1D arrays. 

1741 color 

1742 Color of the surface patches. 

1743 cmap 

1744 A colormap for the surface patches. 

1745 norm : Normalize 

1746 An instance of Normalize to map values to colors. 

1747 vmin, vmax : float, default: None 

1748 Minimum and maximum value to map. 

1749 shade : bool, default: True 

1750 Whether to shade the facecolors. Shading is always disabled when 

1751 *cmap* is specified. 

1752 lightsource : `~matplotlib.colors.LightSource` 

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

1754 **kwargs 

1755 All other keyword arguments are passed on to 

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

1757 

1758 Examples 

1759 -------- 

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

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

1762 """ 

1763 

1764 had_data = self.has_data() 

1765 

1766 # TODO: Support custom face colours 

1767 if color is None: 

1768 color = self._get_lines.get_next_color() 

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

1770 

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

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

1773 

1774 tri, args, kwargs = \ 

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

1776 try: 

1777 z = kwargs.pop('Z') 

1778 except KeyError: 

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

1780 z, *args = args 

1781 z = np.asarray(z) 

1782 

1783 triangles = tri.get_masked_triangles() 

1784 xt = tri.x[triangles] 

1785 yt = tri.y[triangles] 

1786 zt = z[triangles] 

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

1788 

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

1790 

1791 if cmap: 

1792 # average over the three points of each triangle 

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

1794 polyc.set_array(avg_z) 

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

1796 polyc.set_clim(vmin, vmax) 

1797 if norm is not None: 

1798 polyc.set_norm(norm) 

1799 else: 

1800 if shade: 

1801 normals = self._generate_normals(verts) 

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

1803 else: 

1804 colset = color 

1805 polyc.set_facecolors(colset) 

1806 

1807 self.add_collection(polyc) 

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

1809 

1810 return polyc 

1811 

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

1813 """ 

1814 Extend a contour in 3D by creating 

1815 """ 

1816 

1817 levels = cset.levels 

1818 colls = cset.collections 

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

1820 

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

1822 paths = linec.get_paths() 

1823 if not paths: 

1824 continue 

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

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

1827 

1828 color = linec.get_edgecolor()[0] 

1829 

1830 polyverts = [] 

1831 normals = [] 

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

1833 if nsteps <= 1: 

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

1835 nsteps = 2 

1836 else: 

1837 continue 

1838 

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

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

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

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

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

1844 topverts[0][i2], 

1845 botverts[0][i2], 

1846 botverts[0][i1]]) 

1847 

1848 # all polygons have 4 vertices, so vectorize 

1849 polyverts = np.array(polyverts) 

1850 normals = self._generate_normals(polyverts) 

1851 

1852 colors = self._shade_colors(color, normals) 

1853 colors2 = self._shade_colors(color, normals) 

1854 polycol = art3d.Poly3DCollection(polyverts, 

1855 facecolors=colors, 

1856 edgecolors=colors2) 

1857 polycol.set_sort_zpos(z) 

1858 self.add_collection3d(polycol) 

1859 

1860 for col in colls: 

1861 col.remove() 

1862 

1863 def add_contour_set( 

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

1865 zdir = '-' + zdir 

1866 if extend3d: 

1867 self._3d_extend_contour(cset, stride) 

1868 else: 

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

1870 if offset is not None: 

1871 z = offset 

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

1873 

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

1875 self._add_contourf_set(cset, zdir=zdir, offset=offset) 

1876 

1877 def _add_contourf_set(self, cset, zdir='z', offset=None): 

1878 """ 

1879 Returns 

1880 ------- 

1881 levels : numpy.ndarray 

1882 Levels at which the filled contours are added. 

1883 """ 

1884 zdir = '-' + zdir 

1885 

1886 midpoints = cset.levels[:-1] + np.diff(cset.levels) / 2 

1887 # Linearly interpolate to get levels for any extensions 

1888 if cset._extend_min: 

1889 min_level = cset.levels[0] - np.diff(cset.levels[:2]) / 2 

1890 midpoints = np.insert(midpoints, 0, min_level) 

1891 if cset._extend_max: 

1892 max_level = cset.levels[-1] + np.diff(cset.levels[-2:]) / 2 

1893 midpoints = np.append(midpoints, max_level) 

1894 

1895 for z, linec in zip(midpoints, cset.collections): 

1896 if offset is not None: 

1897 z = offset 

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

1899 linec.set_sort_zpos(z) 

1900 return midpoints 

1901 

1902 @_preprocess_data() 

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

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

1905 """ 

1906 Create a 3D contour plot. 

1907 

1908 Parameters 

1909 ---------- 

1910 X, Y, Z : array-like, 

1911 Input data. See `.Axes.contour` for supported data shapes. 

1912 extend3d : bool, default: False 

1913 Whether to extend contour in 3D. 

1914 stride : int 

1915 Step size for extending contour. 

1916 zdir : {'x', 'y', 'z'}, default: 'z' 

1917 The direction to use. 

1918 offset : float, optional 

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

1920 position in a plane normal to zdir. 

1921 data : indexable object, optional 

1922 DATA_PARAMETER_PLACEHOLDER 

1923 

1924 *args, **kwargs 

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

1926 

1927 Returns 

1928 ------- 

1929 matplotlib.contour.QuadContourSet 

1930 """ 

1931 had_data = self.has_data() 

1932 

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

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

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

1936 

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

1938 return cset 

1939 

1940 contour3D = contour 

1941 

1942 @_preprocess_data() 

1943 def tricontour(self, *args, 

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

1945 """ 

1946 Create a 3D contour plot. 

1947 

1948 .. note:: 

1949 This method currently produces incorrect output due to a 

1950 longstanding bug in 3D PolyCollection rendering. 

1951 

1952 Parameters 

1953 ---------- 

1954 X, Y, Z : array-like 

1955 Input data. See `.Axes.tricontour` for supported data shapes. 

1956 extend3d : bool, default: False 

1957 Whether to extend contour in 3D. 

1958 stride : int 

1959 Step size for extending contour. 

1960 zdir : {'x', 'y', 'z'}, default: 'z' 

1961 The direction to use. 

1962 offset : float, optional 

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

1964 position in a plane normal to zdir. 

1965 data : indexable object, optional 

1966 DATA_PARAMETER_PLACEHOLDER 

1967 *args, **kwargs 

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

1969 

1970 Returns 

1971 ------- 

1972 matplotlib.tri.tricontour.TriContourSet 

1973 """ 

1974 had_data = self.has_data() 

1975 

1976 tri, args, kwargs = Triangulation.get_from_args_and_kwargs( 

1977 *args, **kwargs) 

1978 X = tri.x 

1979 Y = tri.y 

1980 if 'Z' in kwargs: 

1981 Z = kwargs.pop('Z') 

1982 else: 

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

1984 Z, *args = args 

1985 

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

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

1988 

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

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

1991 

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

1993 return cset 

1994 

1995 def _auto_scale_contourf(self, X, Y, Z, zdir, levels, had_data): 

1996 # Autoscale in the zdir based on the levels added, which are 

1997 # different from data range if any contour extensions are present 

1998 dim_vals = {'x': X, 'y': Y, 'z': Z, zdir: levels} 

1999 # Input data and levels have different sizes, but auto_scale_xyz 

2000 # expected same-size input, so manually take min/max limits 

2001 limits = [(np.nanmin(dim_vals[dim]), np.nanmax(dim_vals[dim])) 

2002 for dim in ['x', 'y', 'z']] 

2003 self.auto_scale_xyz(*limits, had_data) 

2004 

2005 @_preprocess_data() 

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

2007 """ 

2008 Create a 3D filled contour plot. 

2009 

2010 Parameters 

2011 ---------- 

2012 X, Y, Z : array-like 

2013 Input data. See `.Axes.contourf` for supported data shapes. 

2014 zdir : {'x', 'y', 'z'}, default: 'z' 

2015 The direction to use. 

2016 offset : float, optional 

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

2018 position in a plane normal to zdir. 

2019 data : indexable object, optional 

2020 DATA_PARAMETER_PLACEHOLDER 

2021 *args, **kwargs 

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

2023 

2024 Returns 

2025 ------- 

2026 matplotlib.contour.QuadContourSet 

2027 """ 

2028 had_data = self.has_data() 

2029 

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

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

2032 levels = self._add_contourf_set(cset, zdir, offset) 

2033 

2034 self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data) 

2035 return cset 

2036 

2037 contourf3D = contourf 

2038 

2039 @_preprocess_data() 

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

2041 """ 

2042 Create a 3D filled contour plot. 

2043 

2044 .. note:: 

2045 This method currently produces incorrect output due to a 

2046 longstanding bug in 3D PolyCollection rendering. 

2047 

2048 Parameters 

2049 ---------- 

2050 X, Y, Z : array-like 

2051 Input data. See `.Axes.tricontourf` for supported data shapes. 

2052 zdir : {'x', 'y', 'z'}, default: 'z' 

2053 The direction to use. 

2054 offset : float, optional 

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

2056 position in a plane normal to zdir. 

2057 data : indexable object, optional 

2058 DATA_PARAMETER_PLACEHOLDER 

2059 *args, **kwargs 

2060 Other arguments are forwarded to 

2061 `matplotlib.axes.Axes.tricontourf`. 

2062 

2063 Returns 

2064 ------- 

2065 matplotlib.tri.tricontour.TriContourSet 

2066 """ 

2067 had_data = self.has_data() 

2068 

2069 tri, args, kwargs = Triangulation.get_from_args_and_kwargs( 

2070 *args, **kwargs) 

2071 X = tri.x 

2072 Y = tri.y 

2073 if 'Z' in kwargs: 

2074 Z = kwargs.pop('Z') 

2075 else: 

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

2077 Z, *args = args 

2078 

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

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

2081 

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

2083 levels = self._add_contourf_set(cset, zdir, offset) 

2084 

2085 self._auto_scale_contourf(X, Y, Z, zdir, levels, had_data) 

2086 return cset 

2087 

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

2089 """ 

2090 Add a 3D collection object to the plot. 

2091 

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

2093 modifying the object and adding z coordinate information. 

2094 

2095 Supported are: 

2096 

2097 - PolyCollection 

2098 - LineCollection 

2099 - PatchCollection 

2100 """ 

2101 zvals = np.atleast_1d(zs) 

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

2103 else 0) # FIXME: arbitrary default 

2104 

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

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

2107 # abstract class to test for and exclude? 

2108 if type(col) is mcoll.PolyCollection: 

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

2110 col.set_sort_zpos(zsortval) 

2111 elif type(col) is mcoll.LineCollection: 

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

2113 col.set_sort_zpos(zsortval) 

2114 elif type(col) is mcoll.PatchCollection: 

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

2116 col.set_sort_zpos(zsortval) 

2117 

2118 collection = super().add_collection(col) 

2119 return collection 

2120 

2121 @_preprocess_data(replace_names=["xs", "ys", "zs", "s", 

2122 "edgecolors", "c", "facecolor", 

2123 "facecolors", "color"]) 

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

2125 *args, **kwargs): 

2126 """ 

2127 Create a scatter plot. 

2128 

2129 Parameters 

2130 ---------- 

2131 xs, ys : array-like 

2132 The data positions. 

2133 zs : float or array-like, default: 0 

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

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

2136 zdir : {'x', 'y', 'z', '-x', '-y', '-z'}, default: 'z' 

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

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

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

2140 

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

2142 

2143 s : float or array-like, default: 20 

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

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

2146 size. 

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

2148 The marker color. Possible values: 

2149 

2150 - A single color format string. 

2151 - A sequence of colors of length n. 

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

2153 *norm*. 

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

2155 

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

2157 depthshade : bool, default: True 

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

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

2160 independently. 

2161 data : indexable object, optional 

2162 DATA_PARAMETER_PLACEHOLDER 

2163 **kwargs 

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

2165 

2166 Returns 

2167 ------- 

2168 paths : `~matplotlib.collections.PathCollection` 

2169 """ 

2170 

2171 had_data = self.has_data() 

2172 zs_orig = zs 

2173 

2174 xs, ys, zs = np.broadcast_arrays( 

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

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

2177 

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

2179 

2180 # For xs and ys, 2D scatter() will do the copying. 

2181 if np.may_share_memory(zs_orig, zs): # Avoid unnecessary copies. 

2182 zs = zs.copy() 

2183 

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

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

2186 depthshade=depthshade) 

2187 

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

2189 self.set_zmargin(0.05) 

2190 

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

2192 

2193 return patches 

2194 

2195 scatter3D = scatter 

2196 

2197 @_preprocess_data() 

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

2199 """ 

2200 Add 2D bar(s). 

2201 

2202 Parameters 

2203 ---------- 

2204 left : 1D array-like 

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

2206 height : 1D array-like 

2207 The height of the bars. 

2208 zs : float or 1D array-like 

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

2210 used for all bars. 

2211 zdir : {'x', 'y', 'z'}, default: 'z' 

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

2213 data : indexable object, optional 

2214 DATA_PARAMETER_PLACEHOLDER 

2215 **kwargs 

2216 Other keyword arguments are forwarded to 

2217 `matplotlib.axes.Axes.bar`. 

2218 

2219 Returns 

2220 ------- 

2221 mpl_toolkits.mplot3d.art3d.Patch3DCollection 

2222 """ 

2223 had_data = self.has_data() 

2224 

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

2226 

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

2228 

2229 verts = [] 

2230 verts_zs = [] 

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

2232 vs = art3d._get_patch_verts(p) 

2233 verts += vs.tolist() 

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

2235 art3d.patch_2d_to_3d(p, z, zdir) 

2236 if 'alpha' in kwargs: 

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

2238 

2239 if len(verts) > 0: 

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

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

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

2243 xs, ys = zip(*verts) 

2244 else: 

2245 xs, ys = [], [] 

2246 

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

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

2249 

2250 return patches 

2251 

2252 @_preprocess_data() 

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

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

2255 """ 

2256 Generate a 3D barplot. 

2257 

2258 This method creates three-dimensional barplot where the width, 

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

2260 

2261 Parameters 

2262 ---------- 

2263 x, y, z : array-like 

2264 The coordinates of the anchor point of the bars. 

2265 

2266 dx, dy, dz : float or array-like 

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

2268 

2269 color : sequence of colors, optional 

2270 The color of the bars can be specified globally or 

2271 individually. This parameter can be: 

2272 

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

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

2275 independently. 

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

2277 bars similarly. 

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

2279 independently. 

2280 

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

2282 the order of the coloring: 

2283 

2284 1. -Z (bottom of box) 

2285 2. +Z (top of box) 

2286 3. -Y 

2287 4. +Y 

2288 5. -X 

2289 6. +X 

2290 

2291 zsort : str, optional 

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

2293 

2294 shade : bool, default: True 

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

2296 to the plot's source of light). 

2297 

2298 lightsource : `~matplotlib.colors.LightSource` 

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

2300 

2301 data : indexable object, optional 

2302 DATA_PARAMETER_PLACEHOLDER 

2303 

2304 **kwargs 

2305 Any additional keyword arguments are passed onto 

2306 `~.art3d.Poly3DCollection`. 

2307 

2308 Returns 

2309 ------- 

2310 collection : `~.art3d.Poly3DCollection` 

2311 A collection of three-dimensional polygons representing the bars. 

2312 """ 

2313 

2314 had_data = self.has_data() 

2315 

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

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

2318 minx = np.min(x) 

2319 maxx = np.max(x + dx) 

2320 miny = np.min(y) 

2321 maxy = np.max(y + dy) 

2322 minz = np.min(z) 

2323 maxz = np.max(z + dz) 

2324 

2325 # shape (6, 4, 3) 

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

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

2328 cuboid = np.array([ 

2329 # -z 

2330 ( 

2331 (0, 0, 0), 

2332 (0, 1, 0), 

2333 (1, 1, 0), 

2334 (1, 0, 0), 

2335 ), 

2336 # +z 

2337 ( 

2338 (0, 0, 1), 

2339 (1, 0, 1), 

2340 (1, 1, 1), 

2341 (0, 1, 1), 

2342 ), 

2343 # -y 

2344 ( 

2345 (0, 0, 0), 

2346 (1, 0, 0), 

2347 (1, 0, 1), 

2348 (0, 0, 1), 

2349 ), 

2350 # +y 

2351 ( 

2352 (0, 1, 0), 

2353 (0, 1, 1), 

2354 (1, 1, 1), 

2355 (1, 1, 0), 

2356 ), 

2357 # -x 

2358 ( 

2359 (0, 0, 0), 

2360 (0, 0, 1), 

2361 (0, 1, 1), 

2362 (0, 1, 0), 

2363 ), 

2364 # +x 

2365 ( 

2366 (1, 0, 0), 

2367 (1, 1, 0), 

2368 (1, 1, 1), 

2369 (1, 0, 1), 

2370 ), 

2371 ]) 

2372 

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

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

2375 

2376 # handle each coordinate separately 

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

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

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

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

2381 

2382 # collapse the first two axes 

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

2384 

2385 facecolors = [] 

2386 if color is None: 

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

2388 

2389 color = list(mcolors.to_rgba_array(color)) 

2390 

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

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

2393 for c in color: 

2394 facecolors.extend([c] * 6) 

2395 else: 

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

2397 facecolors = color 

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

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

2400 

2401 if shade: 

2402 normals = self._generate_normals(polys) 

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

2404 else: 

2405 sfacecolors = facecolors 

2406 

2407 col = art3d.Poly3DCollection(polys, 

2408 zsort=zsort, 

2409 facecolor=sfacecolors, 

2410 *args, **kwargs) 

2411 self.add_collection(col) 

2412 

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

2414 

2415 return col 

2416 

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

2418 # docstring inherited 

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

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

2421 self.title.set_y(0.92 * y) 

2422 return ret 

2423 

2424 @_preprocess_data() 

2425 def quiver(self, X, Y, Z, U, V, W, *, 

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

2427 **kwargs): 

2428 """ 

2429 Plot a 3D field of arrows. 

2430 

2431 The arguments can be array-like or scalars, so long as they can be 

2432 broadcast together. The arguments can also be masked arrays. If an 

2433 element in any of argument is masked, then that corresponding quiver 

2434 element will not be plotted. 

2435 

2436 Parameters 

2437 ---------- 

2438 X, Y, Z : array-like 

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

2440 tail of arrow; see *pivot* kwarg). 

2441 

2442 U, V, W : array-like 

2443 The x, y and z components of the arrow vectors. 

2444 

2445 length : float, default: 1 

2446 The length of each quiver. 

2447 

2448 arrow_length_ratio : float, default: 0.3 

2449 The ratio of the arrow head with respect to the quiver. 

2450 

2451 pivot : {'tail', 'middle', 'tip'}, default: 'tail' 

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

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

2454 

2455 normalize : bool, default: False 

2456 Whether all arrows are normalized to have the same length, or keep 

2457 the lengths defined by *u*, *v*, and *w*. 

2458 

2459 data : indexable object, optional 

2460 DATA_PARAMETER_PLACEHOLDER 

2461 

2462 **kwargs 

2463 Any additional keyword arguments are delegated to 

2464 :class:`.Line3DCollection` 

2465 """ 

2466 

2467 def calc_arrows(UVW, angle=15): 

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

2469 x = UVW[:, 0] 

2470 y = UVW[:, 1] 

2471 norm = np.linalg.norm(UVW[:, :2], axis=1) 

2472 x_p = np.divide(y, norm, where=norm != 0, out=np.zeros_like(x)) 

2473 y_p = np.divide(-x, norm, where=norm != 0, out=np.ones_like(x)) 

2474 # compute the two arrowhead direction unit vectors 

2475 ra = math.radians(angle) 

2476 c = math.cos(ra) 

2477 s = math.sin(ra) 

2478 # construct the rotation matrices of shape (3, 3, n) 

2479 Rpos = np.array( 

2480 [[c + (x_p ** 2) * (1 - c), x_p * y_p * (1 - c), y_p * s], 

2481 [y_p * x_p * (1 - c), c + (y_p ** 2) * (1 - c), -x_p * s], 

2482 [-y_p * s, x_p * s, np.full_like(x_p, c)]]) 

2483 # opposite rotation negates all the sin terms 

2484 Rneg = Rpos.copy() 

2485 Rneg[[0, 1, 2, 2], [2, 2, 0, 1]] *= -1 

2486 # Batch n (3, 3) x (3) matrix multiplications ((3, 3, n) x (n, 3)). 

2487 Rpos_vecs = np.einsum("ij...,...j->...i", Rpos, UVW) 

2488 Rneg_vecs = np.einsum("ij...,...j->...i", Rneg, UVW) 

2489 # Stack into (n, 2, 3) result. 

2490 head_dirs = np.stack([Rpos_vecs, Rneg_vecs], axis=1) 

2491 return head_dirs 

2492 

2493 had_data = self.has_data() 

2494 

2495 input_args = [X, Y, Z, U, V, W] 

2496 

2497 # extract the masks, if any 

2498 masks = [k.mask for k in input_args 

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

2500 # broadcast to match the shape 

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

2502 input_args = bcast[:6] 

2503 masks = bcast[6:] 

2504 if masks: 

2505 # combine the masks into one 

2506 mask = functools.reduce(np.logical_or, masks) 

2507 # put mask on and compress 

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

2509 for k in input_args] 

2510 else: 

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

2512 

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

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

2515 linec = art3d.Line3DCollection([], **kwargs) 

2516 self.add_collection(linec) 

2517 return linec 

2518 

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

2520 arrow_dt = shaft_dt * arrow_length_ratio 

2521 

2522 _api.check_in_list(['tail', 'middle', 'tip'], pivot=pivot) 

2523 if pivot == 'tail': 

2524 shaft_dt -= length 

2525 elif pivot == 'middle': 

2526 shaft_dt -= length / 2 

2527 

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

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

2530 

2531 # Normalize rows of UVW 

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

2533 

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

2535 mask = norm > 0 

2536 XYZ = XYZ[mask] 

2537 if normalize: 

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

2539 else: 

2540 UVW = UVW[mask] 

2541 

2542 if len(XYZ) > 0: 

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

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

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

2546 head_dirs = calc_arrows(UVW) 

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

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

2549 # stack left and right head lines together 

2550 heads = heads.reshape((len(arrow_dt), -1, 3)) 

2551 # transpose to get a list of lines 

2552 heads = heads.swapaxes(0, 1) 

2553 

2554 lines = [*shafts, *heads] 

2555 else: 

2556 lines = [] 

2557 

2558 linec = art3d.Line3DCollection(lines, **kwargs) 

2559 self.add_collection(linec) 

2560 

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

2562 

2563 return linec 

2564 

2565 quiver3D = quiver 

2566 

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

2568 lightsource=None, **kwargs): 

2569 """ 

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

2571**kwargs) 

2572 

2573 Plot a set of filled voxels 

2574 

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

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

2577 Occluded faces are not plotted. 

2578 

2579 Parameters 

2580 ---------- 

2581 filled : 3D np.array of bool 

2582 A 3D array of values, with truthy values indicating which voxels 

2583 to fill 

2584 

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

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

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

2588 *filled*. These can be used to plot non-cubic voxels. 

2589 

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

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

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

2593 arguments can only be passed positionally. 

2594 

2595 facecolors, edgecolors : array-like, optional 

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

2597 passed as keyword arguments. 

2598 These parameters can be: 

2599 

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

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

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

2603 the style default for the edges. 

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

2605 corresponding voxel. The size must match the voxels. 

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

2607 last axis. 

2608 

2609 shade : bool, default: True 

2610 Whether to shade the facecolors. 

2611 

2612 lightsource : `~matplotlib.colors.LightSource` 

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

2614 

2615 **kwargs 

2616 Additional keyword arguments to pass onto 

2617 `~mpl_toolkits.mplot3d.art3d.Poly3DCollection`. 

2618 

2619 Returns 

2620 ------- 

2621 faces : dict 

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

2623 `.Poly3DCollection` of the faces drawn for the voxel 

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

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

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

2627 

2628 Examples 

2629 -------- 

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

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

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

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

2634 """ 

2635 

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

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

2638 if len(args) >= 3: 

2639 # underscores indicate position only 

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

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

2642 else: 

2643 def voxels(filled, **kwargs): 

2644 return None, filled, kwargs 

2645 

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

2647 

2648 # check dimensions 

2649 if filled.ndim != 3: 

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

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

2652 

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

2654 coord_shape = tuple(size + 1) 

2655 if xyz is None: 

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

2657 else: 

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

2659 

2660 def _broadcast_color_arg(color, name): 

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

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

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

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

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

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

2667 raise ValueError( 

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

2669 "filled".format(name)) 

2670 return color 

2671 else: 

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

2673 

2674 # broadcast and default on facecolors 

2675 if facecolors is None: 

2676 facecolors = self._get_patches_for_fill.get_next_color() 

2677 facecolors = _broadcast_color_arg(facecolors, 'facecolors') 

2678 

2679 # broadcast but no default on edgecolors 

2680 edgecolors = _broadcast_color_arg(edgecolors, 'edgecolors') 

2681 

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

2683 self.auto_scale_xyz(x, y, z) 

2684 

2685 # points lying on corners of a square 

2686 square = np.array([ 

2687 [0, 0, 0], 

2688 [1, 0, 0], 

2689 [1, 1, 0], 

2690 [0, 1, 0], 

2691 ], dtype=np.intp) 

2692 

2693 voxel_faces = defaultdict(list) 

2694 

2695 def permutation_matrices(n): 

2696 """Generate cyclic permutation matrices.""" 

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

2698 for i in range(n): 

2699 yield mat 

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

2701 

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

2703 # to render 

2704 for permute in permutation_matrices(3): 

2705 # find the set of ranges to iterate over 

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

2707 pinds = np.arange(pc) 

2708 qinds = np.arange(qc) 

2709 rinds = np.arange(rc) 

2710 

2711 square_rot_pos = square.dot(permute.T) 

2712 square_rot_neg = square_rot_pos[::-1] 

2713 

2714 # iterate within the current plane 

2715 for p in pinds: 

2716 for q in qinds: 

2717 # iterate perpendicularly to the current plane, handling 

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

2719 # empty space, to avoid drawing internal faces. 

2720 

2721 # draw lower faces 

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

2723 i0 = tuple(p0) 

2724 if filled[i0]: 

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

2726 

2727 # draw middle faces 

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

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

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

2731 

2732 i1 = tuple(p1) 

2733 i2 = tuple(p2) 

2734 

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

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

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

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

2739 

2740 # draw upper faces 

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

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

2743 ik = tuple(pk) 

2744 if filled[ik]: 

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

2746 

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

2748 # voxel 

2749 polygons = {} 

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

2751 # convert indices into 3D positions 

2752 if xyz is None: 

2753 faces = faces_inds 

2754 else: 

2755 faces = [] 

2756 for face_inds in faces_inds: 

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

2758 face = np.empty(face_inds.shape) 

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

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

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

2762 faces.append(face) 

2763 

2764 # shade the faces 

2765 facecolor = facecolors[coord] 

2766 edgecolor = edgecolors[coord] 

2767 if shade: 

2768 normals = self._generate_normals(faces) 

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

2770 if edgecolor is not None: 

2771 edgecolor = self._shade_colors( 

2772 edgecolor, normals, lightsource 

2773 ) 

2774 

2775 poly = art3d.Poly3DCollection( 

2776 faces, facecolors=facecolor, edgecolors=edgecolor, **kwargs) 

2777 self.add_collection3d(poly) 

2778 polygons[coord] = poly 

2779 

2780 return polygons 

2781 

2782 @_preprocess_data(replace_names=["x", "y", "z", "xerr", "yerr", "zerr"]) 

2783 def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='', 

2784 barsabove=False, errorevery=1, ecolor=None, elinewidth=None, 

2785 capsize=None, capthick=None, xlolims=False, xuplims=False, 

2786 ylolims=False, yuplims=False, zlolims=False, zuplims=False, 

2787 **kwargs): 

2788 """ 

2789 Plot lines and/or markers with errorbars around them. 

2790 

2791 *x*/*y*/*z* define the data locations, and *xerr*/*yerr*/*zerr* define 

2792 the errorbar sizes. By default, this draws the data markers/lines as 

2793 well the errorbars. Use fmt='none' to draw errorbars only. 

2794 

2795 Parameters 

2796 ---------- 

2797 x, y, z : float or array-like 

2798 The data positions. 

2799 

2800 xerr, yerr, zerr : float or array-like, shape (N,) or (2, N), optional 

2801 The errorbar sizes: 

2802 

2803 - scalar: Symmetric +/- values for all data points. 

2804 - shape(N,): Symmetric +/-values for each data point. 

2805 - shape(2, N): Separate - and + values for each bar. First row 

2806 contains the lower errors, the second row contains the upper 

2807 errors. 

2808 - *None*: No errorbar. 

2809 

2810 Note that all error arrays should have *positive* values. 

2811 

2812 fmt : str, default: '' 

2813 The format for the data points / data lines. See `.plot` for 

2814 details. 

2815 

2816 Use 'none' (case-insensitive) to plot errorbars without any data 

2817 markers. 

2818 

2819 ecolor : color, default: None 

2820 The color of the errorbar lines. If None, use the color of the 

2821 line connecting the markers. 

2822 

2823 elinewidth : float, default: None 

2824 The linewidth of the errorbar lines. If None, the linewidth of 

2825 the current style is used. 

2826 

2827 capsize : float, default: :rc:`errorbar.capsize` 

2828 The length of the error bar caps in points. 

2829 

2830 capthick : float, default: None 

2831 An alias to the keyword argument *markeredgewidth* (a.k.a. *mew*). 

2832 This setting is a more sensible name for the property that 

2833 controls the thickness of the error bar cap in points. For 

2834 backwards compatibility, if *mew* or *markeredgewidth* are given, 

2835 then they will over-ride *capthick*. This may change in future 

2836 releases. 

2837 

2838 barsabove : bool, default: False 

2839 If True, will plot the errorbars above the plot 

2840 symbols. Default is below. 

2841 

2842 xlolims, ylolims, zlolims : bool, default: False 

2843 These arguments can be used to indicate that a value gives only 

2844 lower limits. In that case a caret symbol is used to indicate 

2845 this. *lims*-arguments may be scalars, or array-likes of the same 

2846 length as the errors. To use limits with inverted axes, 

2847 `~.Axes.set_xlim` or `~.Axes.set_ylim` must be called before 

2848 `errorbar`. Note the tricky parameter names: setting e.g. 

2849 *ylolims* to True means that the y-value is a *lower* limit of the 

2850 True value, so, only an *upward*-pointing arrow will be drawn! 

2851 

2852 xuplims, yuplims, zuplims : bool, default: False 

2853 Same as above, but for controlling the upper limits. 

2854 

2855 errorevery : int or (int, int), default: 1 

2856 draws error bars on a subset of the data. *errorevery* =N draws 

2857 error bars on the points (x[::N], y[::N], z[::N]). 

2858 *errorevery* =(start, N) draws error bars on the points 

2859 (x[start::N], y[start::N], z[start::N]). e.g. errorevery=(6, 3) 

2860 adds error bars to the data at (x[6], x[9], x[12], x[15], ...). 

2861 Used to avoid overlapping error bars when two series share x-axis 

2862 values. 

2863 

2864 Returns 

2865 ------- 

2866 errlines : list 

2867 List of `~mpl_toolkits.mplot3d.art3d.Line3DCollection` instances 

2868 each containing an errorbar line. 

2869 caplines : list 

2870 List of `~mpl_toolkits.mplot3d.art3d.Line3D` instances each 

2871 containing a capline object. 

2872 limmarks : list 

2873 List of `~mpl_toolkits.mplot3d.art3d.Line3D` instances each 

2874 containing a marker with an upper or lower limit. 

2875 

2876 Other Parameters 

2877 ---------------- 

2878 data : indexable object, optional 

2879 DATA_PARAMETER_PLACEHOLDER 

2880 

2881 **kwargs 

2882 All other keyword arguments for styling errorbar lines are passed 

2883 `~mpl_toolkits.mplot3d.art3d.Line3DCollection`. 

2884 

2885 Examples 

2886 -------- 

2887 .. plot:: gallery/mplot3d/errorbar3d.py 

2888 """ 

2889 had_data = self.has_data() 

2890 

2891 kwargs = cbook.normalize_kwargs(kwargs, mlines.Line2D) 

2892 # Drop anything that comes in as None to use the default instead. 

2893 kwargs = {k: v for k, v in kwargs.items() if v is not None} 

2894 kwargs.setdefault('zorder', 2) 

2895 

2896 self._process_unit_info([("x", x), ("y", y), ("z", z)], kwargs, 

2897 convert=False) 

2898 

2899 # make sure all the args are iterable; use lists not arrays to 

2900 # preserve units 

2901 x = x if np.iterable(x) else [x] 

2902 y = y if np.iterable(y) else [y] 

2903 z = z if np.iterable(z) else [z] 

2904 

2905 if not len(x) == len(y) == len(z): 

2906 raise ValueError("'x', 'y', and 'z' must have the same size") 

2907 

2908 everymask = self._errorevery_to_mask(x, errorevery) 

2909 

2910 label = kwargs.pop("label", None) 

2911 kwargs['label'] = '_nolegend_' 

2912 

2913 # Create the main line and determine overall kwargs for child artists. 

2914 # We avoid calling self.plot() directly, or self._get_lines(), because 

2915 # that would call self._process_unit_info again, and do other indirect 

2916 # data processing. 

2917 (data_line, base_style), = self._get_lines._plot_args( 

2918 (x, y) if fmt == '' else (x, y, fmt), kwargs, return_kwargs=True) 

2919 art3d.line_2d_to_3d(data_line, zs=z) 

2920 

2921 # Do this after creating `data_line` to avoid modifying `base_style`. 

2922 if barsabove: 

2923 data_line.set_zorder(kwargs['zorder'] - .1) 

2924 else: 

2925 data_line.set_zorder(kwargs['zorder'] + .1) 

2926 

2927 # Add line to plot, or throw it away and use it to determine kwargs. 

2928 if fmt.lower() != 'none': 

2929 self.add_line(data_line) 

2930 else: 

2931 data_line = None 

2932 # Remove alpha=0 color that _process_plot_format returns. 

2933 base_style.pop('color') 

2934 

2935 if 'color' not in base_style: 

2936 base_style['color'] = 'C0' 

2937 if ecolor is None: 

2938 ecolor = base_style['color'] 

2939 

2940 # Eject any line-specific information from format string, as it's not 

2941 # needed for bars or caps. 

2942 for key in ['marker', 'markersize', 'markerfacecolor', 

2943 'markeredgewidth', 'markeredgecolor', 'markevery', 

2944 'linestyle', 'fillstyle', 'drawstyle', 'dash_capstyle', 

2945 'dash_joinstyle', 'solid_capstyle', 'solid_joinstyle']: 

2946 base_style.pop(key, None) 

2947 

2948 # Make the style dict for the line collections (the bars). 

2949 eb_lines_style = {**base_style, 'color': ecolor} 

2950 

2951 if elinewidth: 

2952 eb_lines_style['linewidth'] = elinewidth 

2953 elif 'linewidth' in kwargs: 

2954 eb_lines_style['linewidth'] = kwargs['linewidth'] 

2955 

2956 for key in ('transform', 'alpha', 'zorder', 'rasterized'): 

2957 if key in kwargs: 

2958 eb_lines_style[key] = kwargs[key] 

2959 

2960 # Make the style dict for caps (the "hats"). 

2961 eb_cap_style = {**base_style, 'linestyle': 'None'} 

2962 if capsize is None: 

2963 capsize = mpl.rcParams["errorbar.capsize"] 

2964 if capsize > 0: 

2965 eb_cap_style['markersize'] = 2. * capsize 

2966 if capthick is not None: 

2967 eb_cap_style['markeredgewidth'] = capthick 

2968 eb_cap_style['color'] = ecolor 

2969 

2970 def _apply_mask(arrays, mask): 

2971 # Return, for each array in *arrays*, the elements for which *mask* 

2972 # is True, without using fancy indexing. 

2973 return [[*itertools.compress(array, mask)] for array in arrays] 

2974 

2975 def _extract_errs(err, data, lomask, himask): 

2976 # For separate +/- error values we need to unpack err 

2977 if len(err.shape) == 2: 

2978 low_err, high_err = err 

2979 else: 

2980 low_err, high_err = err, err 

2981 

2982 lows = np.where(lomask | ~everymask, data, data - low_err) 

2983 highs = np.where(himask | ~everymask, data, data + high_err) 

2984 

2985 return lows, highs 

2986 

2987 # collect drawn items while looping over the three coordinates 

2988 errlines, caplines, limmarks = [], [], [] 

2989 

2990 # list of endpoint coordinates, used for auto-scaling 

2991 coorderrs = [] 

2992 

2993 # define the markers used for errorbar caps and limits below 

2994 # the dictionary key is mapped by the `i_xyz` helper dictionary 

2995 capmarker = {0: '|', 1: '|', 2: '_'} 

2996 i_xyz = {'x': 0, 'y': 1, 'z': 2} 

2997 

2998 # Calculate marker size from points to quiver length. Because these are 

2999 # not markers, and 3D Axes do not use the normal transform stack, this 

3000 # is a bit involved. Since the quiver arrows will change size as the 

3001 # scene is rotated, they are given a standard size based on viewing 

3002 # them directly in planar form. 

3003 quiversize = eb_cap_style.get('markersize', 

3004 mpl.rcParams['lines.markersize']) ** 2 

3005 quiversize *= self.figure.dpi / 72 

3006 quiversize = self.transAxes.inverted().transform([ 

3007 (0, 0), (quiversize, quiversize)]) 

3008 quiversize = np.mean(np.diff(quiversize, axis=0)) 

3009 # quiversize is now in Axes coordinates, and to convert back to data 

3010 # coordinates, we need to run it through the inverse 3D transform. For 

3011 # consistency, this uses a fixed elevation, azimuth, and roll. 

3012 with cbook._setattr_cm(self, elev=0, azim=0, roll=0): 

3013 invM = np.linalg.inv(self.get_proj()) 

3014 # elev=azim=roll=0 produces the Y-Z plane, so quiversize in 2D 'x' is 

3015 # 'y' in 3D, hence the 1 index. 

3016 quiversize = np.dot(invM, np.array([quiversize, 0, 0, 0]))[1] 

3017 # Quivers use a fixed 15-degree arrow head, so scale up the length so 

3018 # that the size corresponds to the base. In other words, this constant 

3019 # corresponds to the equation tan(15) = (base / 2) / (arrow length). 

3020 quiversize *= 1.8660254037844388 

3021 eb_quiver_style = {**eb_cap_style, 

3022 'length': quiversize, 'arrow_length_ratio': 1} 

3023 eb_quiver_style.pop('markersize', None) 

3024 

3025 # loop over x-, y-, and z-direction and draw relevant elements 

3026 for zdir, data, err, lolims, uplims in zip( 

3027 ['x', 'y', 'z'], [x, y, z], [xerr, yerr, zerr], 

3028 [xlolims, ylolims, zlolims], [xuplims, yuplims, zuplims]): 

3029 

3030 dir_vector = art3d.get_dir_vector(zdir) 

3031 i_zdir = i_xyz[zdir] 

3032 

3033 if err is None: 

3034 continue 

3035 

3036 if not np.iterable(err): 

3037 err = [err] * len(data) 

3038 

3039 err = np.atleast_1d(err) 

3040 

3041 # arrays fine here, they are booleans and hence not units 

3042 lolims = np.broadcast_to(lolims, len(data)).astype(bool) 

3043 uplims = np.broadcast_to(uplims, len(data)).astype(bool) 

3044 

3045 # a nested list structure that expands to (xl,xh),(yl,yh),(zl,zh), 

3046 # where x/y/z and l/h correspond to dimensions and low/high 

3047 # positions of errorbars in a dimension we're looping over 

3048 coorderr = [ 

3049 _extract_errs(err * dir_vector[i], coord, lolims, uplims) 

3050 for i, coord in enumerate([x, y, z])] 

3051 (xl, xh), (yl, yh), (zl, zh) = coorderr 

3052 

3053 # draws capmarkers - flat caps orthogonal to the error bars 

3054 nolims = ~(lolims | uplims) 

3055 if nolims.any() and capsize > 0: 

3056 lo_caps_xyz = _apply_mask([xl, yl, zl], nolims & everymask) 

3057 hi_caps_xyz = _apply_mask([xh, yh, zh], nolims & everymask) 

3058 

3059 # setting '_' for z-caps and '|' for x- and y-caps; 

3060 # these markers will rotate as the viewing angle changes 

3061 cap_lo = art3d.Line3D(*lo_caps_xyz, ls='', 

3062 marker=capmarker[i_zdir], 

3063 **eb_cap_style) 

3064 cap_hi = art3d.Line3D(*hi_caps_xyz, ls='', 

3065 marker=capmarker[i_zdir], 

3066 **eb_cap_style) 

3067 self.add_line(cap_lo) 

3068 self.add_line(cap_hi) 

3069 caplines.append(cap_lo) 

3070 caplines.append(cap_hi) 

3071 

3072 if lolims.any(): 

3073 xh0, yh0, zh0 = _apply_mask([xh, yh, zh], lolims & everymask) 

3074 self.quiver(xh0, yh0, zh0, *dir_vector, **eb_quiver_style) 

3075 if uplims.any(): 

3076 xl0, yl0, zl0 = _apply_mask([xl, yl, zl], uplims & everymask) 

3077 self.quiver(xl0, yl0, zl0, *-dir_vector, **eb_quiver_style) 

3078 

3079 errline = art3d.Line3DCollection(np.array(coorderr).T, 

3080 **eb_lines_style) 

3081 self.add_collection(errline) 

3082 errlines.append(errline) 

3083 coorderrs.append(coorderr) 

3084 

3085 coorderrs = np.array(coorderrs) 

3086 

3087 def _digout_minmax(err_arr, coord_label): 

3088 return (np.nanmin(err_arr[:, i_xyz[coord_label], :, :]), 

3089 np.nanmax(err_arr[:, i_xyz[coord_label], :, :])) 

3090 

3091 minx, maxx = _digout_minmax(coorderrs, 'x') 

3092 miny, maxy = _digout_minmax(coorderrs, 'y') 

3093 minz, maxz = _digout_minmax(coorderrs, 'z') 

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

3095 

3096 # Adapting errorbar containers for 3d case, assuming z-axis points "up" 

3097 errorbar_container = mcontainer.ErrorbarContainer( 

3098 (data_line, tuple(caplines), tuple(errlines)), 

3099 has_xerr=(xerr is not None or yerr is not None), 

3100 has_yerr=(zerr is not None), 

3101 label=label) 

3102 self.containers.append(errorbar_container) 

3103 

3104 return errlines, caplines, limmarks 

3105 

3106 def get_tightbbox(self, renderer=None, call_axes_locator=True, 

3107 bbox_extra_artists=None, *, for_layout_only=False): 

3108 ret = super().get_tightbbox(renderer, 

3109 call_axes_locator=call_axes_locator, 

3110 bbox_extra_artists=bbox_extra_artists, 

3111 for_layout_only=for_layout_only) 

3112 batch = [ret] 

3113 if self._axis3don: 

3114 for axis in self._axis_map.values(): 

3115 if axis.get_visible(): 

3116 axis_bb = martist._get_tightbbox_for_layout_only( 

3117 axis, renderer) 

3118 if axis_bb: 

3119 batch.append(axis_bb) 

3120 return mtransforms.Bbox.union(batch) 

3121 

3122 @_preprocess_data() 

3123 def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-', 

3124 bottom=0, label=None, orientation='z'): 

3125 """ 

3126 Create a 3D stem plot. 

3127 

3128 A stem plot draws lines perpendicular to a baseline, and places markers 

3129 at the heads. By default, the baseline is defined by *x* and *y*, and 

3130 stems are drawn vertically from *bottom* to *z*. 

3131 

3132 Parameters 

3133 ---------- 

3134 x, y, z : array-like 

3135 The positions of the heads of the stems. The stems are drawn along 

3136 the *orientation*-direction from the baseline at *bottom* (in the 

3137 *orientation*-coordinate) to the heads. By default, the *x* and *y* 

3138 positions are used for the baseline and *z* for the head position, 

3139 but this can be changed by *orientation*. 

3140 

3141 linefmt : str, default: 'C0-' 

3142 A string defining the properties of the vertical lines. Usually, 

3143 this will be a color or a color and a linestyle: 

3144 

3145 ========= ============= 

3146 Character Line Style 

3147 ========= ============= 

3148 ``'-'`` solid line 

3149 ``'--'`` dashed line 

3150 ``'-.'`` dash-dot line 

3151 ``':'`` dotted line 

3152 ========= ============= 

3153 

3154 Note: While it is technically possible to specify valid formats 

3155 other than color or color and linestyle (e.g. 'rx' or '-.'), this 

3156 is beyond the intention of the method and will most likely not 

3157 result in a reasonable plot. 

3158 

3159 markerfmt : str, default: 'C0o' 

3160 A string defining the properties of the markers at the stem heads. 

3161 

3162 basefmt : str, default: 'C3-' 

3163 A format string defining the properties of the baseline. 

3164 

3165 bottom : float, default: 0 

3166 The position of the baseline, in *orientation*-coordinates. 

3167 

3168 label : str, default: None 

3169 The label to use for the stems in legends. 

3170 

3171 orientation : {'x', 'y', 'z'}, default: 'z' 

3172 The direction along which stems are drawn. 

3173 

3174 data : indexable object, optional 

3175 DATA_PARAMETER_PLACEHOLDER 

3176 

3177 Returns 

3178 ------- 

3179 `.StemContainer` 

3180 The container may be treated like a tuple 

3181 (*markerline*, *stemlines*, *baseline*) 

3182 

3183 Examples 

3184 -------- 

3185 .. plot:: gallery/mplot3d/stem3d_demo.py 

3186 """ 

3187 

3188 from matplotlib.container import StemContainer 

3189 

3190 had_data = self.has_data() 

3191 

3192 _api.check_in_list(['x', 'y', 'z'], orientation=orientation) 

3193 

3194 xlim = (np.min(x), np.max(x)) 

3195 ylim = (np.min(y), np.max(y)) 

3196 zlim = (np.min(z), np.max(z)) 

3197 

3198 # Determine the appropriate plane for the baseline and the direction of 

3199 # stemlines based on the value of orientation. 

3200 if orientation == 'x': 

3201 basex, basexlim = y, ylim 

3202 basey, baseylim = z, zlim 

3203 lines = [[(bottom, thisy, thisz), (thisx, thisy, thisz)] 

3204 for thisx, thisy, thisz in zip(x, y, z)] 

3205 elif orientation == 'y': 

3206 basex, basexlim = x, xlim 

3207 basey, baseylim = z, zlim 

3208 lines = [[(thisx, bottom, thisz), (thisx, thisy, thisz)] 

3209 for thisx, thisy, thisz in zip(x, y, z)] 

3210 else: 

3211 basex, basexlim = x, xlim 

3212 basey, baseylim = y, ylim 

3213 lines = [[(thisx, thisy, bottom), (thisx, thisy, thisz)] 

3214 for thisx, thisy, thisz in zip(x, y, z)] 

3215 

3216 # Determine style for stem lines. 

3217 linestyle, linemarker, linecolor = _process_plot_format(linefmt) 

3218 if linestyle is None: 

3219 linestyle = mpl.rcParams['lines.linestyle'] 

3220 

3221 # Plot everything in required order. 

3222 baseline, = self.plot(basex, basey, basefmt, zs=bottom, 

3223 zdir=orientation, label='_nolegend_') 

3224 stemlines = art3d.Line3DCollection( 

3225 lines, linestyles=linestyle, colors=linecolor, label='_nolegend_') 

3226 self.add_collection(stemlines) 

3227 markerline, = self.plot(x, y, z, markerfmt, label='_nolegend_') 

3228 

3229 stem_container = StemContainer((markerline, stemlines, baseline), 

3230 label=label) 

3231 self.add_container(stem_container) 

3232 

3233 jx, jy, jz = art3d.juggle_axes(basexlim, baseylim, [bottom, bottom], 

3234 orientation) 

3235 self.auto_scale_xyz([*jx, *xlim], [*jy, *ylim], [*jz, *zlim], had_data) 

3236 

3237 return stem_container 

3238 

3239 stem3D = stem 

3240 

3241 

3242def get_test_data(delta=0.05): 

3243 """Return a tuple X, Y, Z with a test data set.""" 

3244 x = y = np.arange(-3.0, 3.0, delta) 

3245 X, Y = np.meshgrid(x, y) 

3246 

3247 Z1 = np.exp(-(X**2 + Y**2) / 2) / (2 * np.pi) 

3248 Z2 = (np.exp(-(((X - 1) / 1.5)**2 + ((Y - 1) / 0.5)**2) / 2) / 

3249 (2 * np.pi * 0.5 * 1.5)) 

3250 Z = Z2 - Z1 

3251 

3252 X = X * 10 

3253 Y = Y * 10 

3254 Z = Z * 500 

3255 return X, Y, Z