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

Shortcuts on this page

r m x   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1349 statements  

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 itertools 

15import math 

16import textwrap 

17 

18import numpy as np 

19 

20import matplotlib as mpl 

21from matplotlib import _api, cbook, _docstring, _preprocess_data 

22import matplotlib.artist as martist 

23import matplotlib.collections as mcoll 

24import matplotlib.colors as mcolors 

25import matplotlib.image as mimage 

26import matplotlib.lines as mlines 

27import matplotlib.patches as mpatches 

28import matplotlib.container as mcontainer 

29import matplotlib.transforms as mtransforms 

30from matplotlib.axes import Axes 

31from matplotlib.axes._base import _axis_method_wrapper, _process_plot_format 

32from matplotlib.transforms import Bbox 

33from matplotlib.tri._triangulation import Triangulation 

34 

35from . import art3d 

36from . import proj3d 

37from . import axis3d 

38 

39 

40@_docstring.interpd 

41@_api.define_aliases({ 

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

43class Axes3D(Axes): 

44 """ 

45 3D Axes object. 

46 

47 .. note:: 

48 

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

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

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

52 """ 

53 name = '3d' 

54 

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

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

57 Axes._shared_axes["view"] = cbook.Grouper() 

58 

59 def __init__( 

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

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

62 box_aspect=None, computed_zorder=True, focal_length=None, 

63 shareview=None, 

64 **kwargs): 

65 """ 

66 Parameters 

67 ---------- 

68 fig : Figure 

69 The parent figure. 

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

71 The ``(left, bottom, width, height)`` Axes position. 

72 elev : float, default: 30 

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

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

75 above the plane. 

76 azim : float, default: -60 

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

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

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

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

81 roll : float, default: 0 

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

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

84 scene to rotate counter-clockwise. 

85 sharez : Axes3D, optional 

86 Other Axes to share z-limits with. Note that it is not possible to 

87 unshare axes. 

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

89 The projection type, default 'persp'. 

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

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

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

93 If None, defaults to 4:4:3 

94 computed_zorder : bool, default: True 

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

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

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

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

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

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

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

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

103 focal_length : float, default: None 

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

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

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

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

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

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

110 shareview : Axes3D, optional 

111 Other Axes to share view angles with. Note that it is not possible 

112 to unshare axes. 

113 

114 **kwargs 

115 Other optional keyword arguments: 

116 

117 %(Axes3D:kwdoc)s 

118 """ 

119 

120 if rect is None: 

121 rect = [0.0, 0.0, 1.0, 1.0] 

122 

123 self.initial_azim = azim 

124 self.initial_elev = elev 

125 self.initial_roll = roll 

126 self.set_proj_type(proj_type, focal_length) 

127 self.computed_zorder = computed_zorder 

128 

129 self.xy_viewLim = Bbox.unit() 

130 self.zz_viewLim = Bbox.unit() 

131 xymargin = 0.05 * 10/11 # match mpl3.8 appearance 

132 self.xy_dataLim = Bbox([[xymargin, xymargin], 

133 [1 - xymargin, 1 - xymargin]]) 

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

135 self.zz_dataLim = Bbox.unit() 

136 

137 # inhibit autoscale_view until the axes are defined 

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

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

140 

141 self._sharez = sharez 

142 if sharez is not None: 

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

144 self._adjustable = 'datalim' 

145 

146 self._shareview = shareview 

147 if shareview is not None: 

148 self._shared_axes["view"].join(self, shareview) 

149 

150 if kwargs.pop('auto_add_to_figure', False): 

151 raise AttributeError( 

152 'auto_add_to_figure is no longer supported for Axes3D. ' 

153 'Use fig.add_axes(ax) instead.' 

154 ) 

155 

156 super().__init__( 

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

158 ) 

159 # Disable drawing of axes by base class 

160 super().set_axis_off() 

161 # Enable drawing of axes by Axes3D class 

162 self.set_axis_on() 

163 self.M = None 

164 self.invM = None 

165 

166 self._view_margin = 1/48 # default value to match mpl3.8 

167 self.autoscale_view() 

168 

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

170 self.fmt_zdata = None 

171 

172 self.mouse_init() 

173 self.figure.canvas.callbacks._connect_picklable( 

174 'motion_notify_event', self._on_move) 

175 self.figure.canvas.callbacks._connect_picklable( 

176 'button_press_event', self._button_press) 

177 self.figure.canvas.callbacks._connect_picklable( 

178 'button_release_event', self._button_release) 

179 self.set_top_view() 

180 

181 self.patch.set_linewidth(0) 

182 # Calculate the pseudo-data width and height 

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

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

185 

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

187 # for bounding box calculations 

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

189 

190 def set_axis_off(self): 

191 self._axis3don = False 

192 self.stale = True 

193 

194 def set_axis_on(self): 

195 self._axis3don = True 

196 self.stale = True 

197 

198 def convert_zunits(self, z): 

199 """ 

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

201 convert *z* using zaxis unit type 

202 """ 

203 return self.zaxis.convert_units(z) 

204 

205 def set_top_view(self): 

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

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

208 xdwl = 0.95 / self._dist 

209 xdw = 0.9 / self._dist 

210 ydwl = 0.95 / self._dist 

211 ydw = 0.9 / self._dist 

212 # Set the viewing pane. 

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

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

215 self.stale = True 

216 

217 def _init_axis(self): 

218 """Init 3D Axes; overrides creation of regular X/Y Axes.""" 

219 self.xaxis = axis3d.XAxis(self) 

220 self.yaxis = axis3d.YAxis(self) 

221 self.zaxis = axis3d.ZAxis(self) 

222 

223 def get_zaxis(self): 

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

225 return self.zaxis 

226 

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

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

229 

230 def _transformed_cube(self, vals): 

231 """Return cube with limits from *vals* transformed by self.M.""" 

232 minx, maxx, miny, maxy, minz, maxz = vals 

233 xyzs = [(minx, miny, minz), 

234 (maxx, miny, minz), 

235 (maxx, maxy, minz), 

236 (minx, maxy, minz), 

237 (minx, miny, maxz), 

238 (maxx, miny, maxz), 

239 (maxx, maxy, maxz), 

240 (minx, maxy, maxz)] 

241 return proj3d._proj_points(xyzs, self.M) 

242 

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

244 """ 

245 Set the aspect ratios. 

246 

247 Parameters 

248 ---------- 

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

250 Possible values: 

251 

252 ========= ================================================== 

253 value description 

254 ========= ================================================== 

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

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

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

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

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

260 ========= ================================================== 

261 

262 adjustable : None or {'box', 'datalim'}, optional 

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

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

265 details. 

266 

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

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

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

270 specify the anchor are abbreviations of cardinal directions: 

271 

272 ===== ===================== 

273 value description 

274 ===== ===================== 

275 'C' centered 

276 'SW' lower left corner 

277 'S' middle of bottom edge 

278 'SE' lower right corner 

279 etc. 

280 ===== ===================== 

281 

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

283 

284 share : bool, default: False 

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

286 

287 See Also 

288 -------- 

289 mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect 

290 """ 

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

292 aspect=aspect) 

293 super().set_aspect( 

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

295 self._aspect = aspect 

296 

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

298 ax_indices = self._equal_aspect_axis_indices(aspect) 

299 

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

301 self.yaxis.get_view_interval(), 

302 self.zaxis.get_view_interval()]) 

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

304 if self._adjustable == 'datalim': 

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

306 scale = max(ptp[ax_indices] / self._box_aspect[ax_indices]) 

307 deltas = scale * self._box_aspect 

308 

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

310 self.set_ylim3d, 

311 self.set_zlim3d)): 

312 if i in ax_indices: 

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

314 auto=True, view_margin=None) 

315 else: # 'box' 

316 # Change the box aspect such that the ratio of the length of 

317 # the unmodified axis to the length of the diagonal 

318 # perpendicular to it remains unchanged. 

319 box_aspect = np.array(self._box_aspect) 

320 box_aspect[ax_indices] = ptp[ax_indices] 

321 remaining_ax_indices = {0, 1, 2}.difference(ax_indices) 

322 if remaining_ax_indices: 

323 remaining = remaining_ax_indices.pop() 

324 old_diag = np.linalg.norm(self._box_aspect[ax_indices]) 

325 new_diag = np.linalg.norm(box_aspect[ax_indices]) 

326 box_aspect[remaining] *= new_diag / old_diag 

327 self.set_box_aspect(box_aspect) 

328 

329 def _equal_aspect_axis_indices(self, aspect): 

330 """ 

331 Get the indices for which of the x, y, z axes are constrained to have 

332 equal aspect ratios. 

333 

334 Parameters 

335 ---------- 

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

337 See descriptions in docstring for `.set_aspect()`. 

338 """ 

339 ax_indices = [] # aspect == 'auto' 

340 if aspect == 'equal': 

341 ax_indices = [0, 1, 2] 

342 elif aspect == 'equalxy': 

343 ax_indices = [0, 1] 

344 elif aspect == 'equalxz': 

345 ax_indices = [0, 2] 

346 elif aspect == 'equalyz': 

347 ax_indices = [1, 2] 

348 return ax_indices 

349 

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

351 """ 

352 Set the Axes box aspect. 

353 

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

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

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

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

358 

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

360 aspect to match your data range in each dimension. 

361 

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

363 

364 Parameters 

365 ---------- 

366 aspect : 3-tuple of floats or None 

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

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

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

370 

371 zoom : float, default: 1 

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

373 """ 

374 if zoom <= 0: 

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

376 

377 if aspect is None: 

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

379 else: 

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

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

382 # The scale 1.8294640721620434 is tuned to match the mpl3.2 appearance. 

383 # The 25/24 factor is to compensate for the change in automargin 

384 # behavior in mpl3.9. This comes from the padding of 1/48 on both sides 

385 # of the axes in mpl3.8. 

386 aspect *= 1.8294640721620434 * 25/24 * zoom / np.linalg.norm(aspect) 

387 

388 self._box_aspect = self._roll_to_vertical(aspect, reverse=True) 

389 self.stale = True 

390 

391 def apply_aspect(self, position=None): 

392 if position is None: 

393 position = self.get_position(original=True) 

394 

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

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

397 # is make sure our coordinate system is square. 

398 trans = self.get_figure().transSubfigure 

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

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

401 fig_aspect = bb.height / bb.width 

402 

403 box_aspect = 1 

404 pb = position.frozen() 

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

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

407 

408 @martist.allow_rasterization 

409 def draw(self, renderer): 

410 if not self.get_visible(): 

411 return 

412 self._unstale_viewLim() 

413 

414 # draw the background patch 

415 self.patch.draw(renderer) 

416 self._frameon = False 

417 

418 # first, set the aspect 

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

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

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

422 # of the Axes 

423 locator = self.get_axes_locator() 

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

425 

426 # add the projection matrix to the renderer 

427 self.M = self.get_proj() 

428 self.invM = np.linalg.inv(self.M) 

429 

430 collections_and_patches = ( 

431 artist for artist in self._children 

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

433 and artist.get_visible()) 

434 if self.computed_zorder: 

435 # Calculate projection of collections and patches and zorder 

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

437 zorder_offset = max(axis.get_zorder() 

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

439 collection_zorder = patch_zorder = zorder_offset 

440 

441 for artist in sorted(collections_and_patches, 

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

443 reverse=True): 

444 if isinstance(artist, mcoll.Collection): 

445 artist.zorder = collection_zorder 

446 collection_zorder += 1 

447 elif isinstance(artist, mpatches.Patch): 

448 artist.zorder = patch_zorder 

449 patch_zorder += 1 

450 else: 

451 for artist in collections_and_patches: 

452 artist.do_3d_projection() 

453 

454 if self._axis3don: 

455 # Draw panes first 

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

457 axis.draw_pane(renderer) 

458 # Then gridlines 

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

460 axis.draw_grid(renderer) 

461 # Then axes, labels, text, and ticks 

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

463 axis.draw(renderer) 

464 

465 # Then rest 

466 super().draw(renderer) 

467 

468 def get_axis_position(self): 

469 tc = self._transformed_cube(self.get_w_lims()) 

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

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

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

473 return xhigh, yhigh, zhigh 

474 

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

476 """ 

477 Not implemented in `~mpl_toolkits.mplot3d.axes3d.Axes3D`. 

478 """ 

479 pass 

480 

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

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

483 

484 def get_zmargin(self): 

485 """ 

486 Retrieve autoscaling margin of the z-axis. 

487 

488 .. versionadded:: 3.9 

489 

490 Returns 

491 ------- 

492 zmargin : float 

493 

494 See Also 

495 -------- 

496 mpl_toolkits.mplot3d.axes3d.Axes3D.set_zmargin 

497 """ 

498 return self._zmargin 

499 

500 def set_zmargin(self, m): 

501 """ 

502 Set padding of Z data limits prior to autoscaling. 

503 

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

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

506 the data range instead of expanding it. 

507 

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

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

510 of [0.2, 1.8]. 

511 

512 Parameters 

513 ---------- 

514 m : float greater than -0.5 

515 """ 

516 if m <= -0.5: 

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

518 self._zmargin = m 

519 self._request_autoscale_view("z") 

520 self.stale = True 

521 

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

523 """ 

524 Set or retrieve autoscaling margins. 

525 

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

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

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

529 """ 

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

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

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

533 elif len(margins) == 1: 

534 x = y = z = margins[0] 

535 elif len(margins) == 3: 

536 x, y, z = margins 

537 elif margins: 

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

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

540 

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

542 if tight is not True: 

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

544 return self._xmargin, self._ymargin, self._zmargin 

545 

546 if x is not None: 

547 self.set_xmargin(x) 

548 if y is not None: 

549 self.set_ymargin(y) 

550 if z is not None: 

551 self.set_zmargin(z) 

552 

553 self.autoscale_view( 

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

555 scalez=(z is not None) 

556 ) 

557 

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

559 """ 

560 Convenience method for simple axis view autoscaling. 

561 

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

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

564 to 'both' autoscales all three axes. 

565 """ 

566 if enable is None: 

567 scalex = True 

568 scaley = True 

569 scalez = True 

570 else: 

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

572 self.set_autoscalex_on(enable) 

573 scalex = self.get_autoscalex_on() 

574 else: 

575 scalex = False 

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

577 self.set_autoscaley_on(enable) 

578 scaley = self.get_autoscaley_on() 

579 else: 

580 scaley = False 

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

582 self.set_autoscalez_on(enable) 

583 scalez = self.get_autoscalez_on() 

584 else: 

585 scalez = False 

586 if scalex: 

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

588 if scaley: 

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

590 if scalez: 

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

592 

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

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

595 # minimum sized rectangular volume holds the data. 

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

597 self.xy_dataLim.update_from_data_xy( 

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

599 else: 

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

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

602 if Z is not None: 

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

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

605 self.autoscale_view() 

606 

607 def autoscale_view(self, tight=None, 

608 scalex=True, scaley=True, scalez=True): 

609 """ 

610 Autoscale the view limits using the data limits. 

611 

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

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

614 """ 

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

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

617 if tight is None: 

618 _tight = self._tight 

619 if not _tight: 

620 # if image data only just use the datalim 

621 for artist in self._children: 

622 if isinstance(artist, mimage.AxesImage): 

623 _tight = True 

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

625 _tight = False 

626 break 

627 else: 

628 _tight = self._tight = bool(tight) 

629 

630 if scalex and self.get_autoscalex_on(): 

631 x0, x1 = self.xy_dataLim.intervalx 

632 xlocator = self.xaxis.get_major_locator() 

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

634 if self._xmargin > 0: 

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

636 x0 -= delta 

637 x1 += delta 

638 if not _tight: 

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

640 self.set_xbound(x0, x1, self._view_margin) 

641 

642 if scaley and self.get_autoscaley_on(): 

643 y0, y1 = self.xy_dataLim.intervaly 

644 ylocator = self.yaxis.get_major_locator() 

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

646 if self._ymargin > 0: 

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

648 y0 -= delta 

649 y1 += delta 

650 if not _tight: 

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

652 self.set_ybound(y0, y1, self._view_margin) 

653 

654 if scalez and self.get_autoscalez_on(): 

655 z0, z1 = self.zz_dataLim.intervalx 

656 zlocator = self.zaxis.get_major_locator() 

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

658 if self._zmargin > 0: 

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

660 z0 -= delta 

661 z1 += delta 

662 if not _tight: 

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

664 self.set_zbound(z0, z1, self._view_margin) 

665 

666 def get_w_lims(self): 

667 """Get 3D world limits.""" 

668 minx, maxx = self.get_xlim3d() 

669 miny, maxy = self.get_ylim3d() 

670 minz, maxz = self.get_zlim3d() 

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

672 

673 def _set_bound3d(self, get_bound, set_lim, axis_inverted, 

674 lower=None, upper=None, view_margin=None): 

675 """ 

676 Set 3D axis bounds. 

677 """ 

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

679 lower, upper = lower 

680 

681 old_lower, old_upper = get_bound() 

682 if lower is None: 

683 lower = old_lower 

684 if upper is None: 

685 upper = old_upper 

686 

687 set_lim(sorted((lower, upper), reverse=bool(axis_inverted())), 

688 auto=None, view_margin=view_margin) 

689 

690 def set_xbound(self, lower=None, upper=None, view_margin=None): 

691 """ 

692 Set the lower and upper numerical bounds of the x-axis. 

693 

694 This method will honor axis inversion regardless of parameter order. 

695 It will not change the autoscaling setting (`.get_autoscalex_on()`). 

696 

697 Parameters 

698 ---------- 

699 lower, upper : float or None 

700 The lower and upper bounds. If *None*, the respective axis bound 

701 is not modified. 

702 view_margin : float or None 

703 The margin to apply to the bounds. If *None*, the margin is handled 

704 by `.set_xlim`. 

705 

706 See Also 

707 -------- 

708 get_xbound 

709 get_xlim, set_xlim 

710 invert_xaxis, xaxis_inverted 

711 """ 

712 self._set_bound3d(self.get_xbound, self.set_xlim, self.xaxis_inverted, 

713 lower, upper, view_margin) 

714 

715 def set_ybound(self, lower=None, upper=None, view_margin=None): 

716 """ 

717 Set the lower and upper numerical bounds of the y-axis. 

718 

719 This method will honor axis inversion regardless of parameter order. 

720 It will not change the autoscaling setting (`.get_autoscaley_on()`). 

721 

722 Parameters 

723 ---------- 

724 lower, upper : float or None 

725 The lower and upper bounds. If *None*, the respective axis bound 

726 is not modified. 

727 view_margin : float or None 

728 The margin to apply to the bounds. If *None*, the margin is handled 

729 by `.set_ylim`. 

730 

731 See Also 

732 -------- 

733 get_ybound 

734 get_ylim, set_ylim 

735 invert_yaxis, yaxis_inverted 

736 """ 

737 self._set_bound3d(self.get_ybound, self.set_ylim, self.yaxis_inverted, 

738 lower, upper, view_margin) 

739 

740 def set_zbound(self, lower=None, upper=None, view_margin=None): 

741 """ 

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

743 This method will honor axis inversion regardless of parameter order. 

744 It will not change the autoscaling setting (`.get_autoscaley_on()`). 

745 

746 Parameters 

747 ---------- 

748 lower, upper : float or None 

749 The lower and upper bounds. If *None*, the respective axis bound 

750 is not modified. 

751 view_margin : float or None 

752 The margin to apply to the bounds. If *None*, the margin is handled 

753 by `.set_zlim`. 

754 

755 See Also 

756 -------- 

757 get_zbound 

758 get_zlim, set_zlim 

759 invert_zaxis, zaxis_inverted 

760 """ 

761 self._set_bound3d(self.get_zbound, self.set_zlim, self.zaxis_inverted, 

762 lower, upper, view_margin) 

763 

764 def _set_lim3d(self, axis, lower=None, upper=None, *, emit=True, 

765 auto=False, view_margin=None, axmin=None, axmax=None): 

766 """ 

767 Set 3D axis limits. 

768 """ 

769 if upper is None: 

770 if np.iterable(lower): 

771 lower, upper = lower 

772 elif axmax is None: 

773 upper = axis.get_view_interval()[1] 

774 if lower is None and axmin is None: 

775 lower = axis.get_view_interval()[0] 

776 if axmin is not None: 

777 if lower is not None: 

778 raise TypeError("Cannot pass both 'lower' and 'min'") 

779 lower = axmin 

780 if axmax is not None: 

781 if upper is not None: 

782 raise TypeError("Cannot pass both 'upper' and 'max'") 

783 upper = axmax 

784 if np.isinf(lower) or np.isinf(upper): 

785 raise ValueError(f"Axis limits {lower}, {upper} cannot be infinite") 

786 if view_margin is None: 

787 if mpl.rcParams['axes3d.automargin']: 

788 view_margin = self._view_margin 

789 else: 

790 view_margin = 0 

791 delta = (upper - lower) * view_margin 

792 lower -= delta 

793 upper += delta 

794 return axis._set_lim(lower, upper, emit=emit, auto=auto) 

795 

796 def set_xlim(self, left=None, right=None, *, emit=True, auto=False, 

797 view_margin=None, xmin=None, xmax=None): 

798 """ 

799 Set the 3D x-axis view limits. 

800 

801 Parameters 

802 ---------- 

803 left : float, optional 

804 The left xlim in data coordinates. Passing *None* leaves the 

805 limit unchanged. 

806 

807 The left and right xlims may also be passed as the tuple 

808 (*left*, *right*) as the first positional argument (or as 

809 the *left* keyword argument). 

810 

811 .. ACCEPTS: (left: float, right: float) 

812 

813 right : float, optional 

814 The right xlim in data coordinates. Passing *None* leaves the 

815 limit unchanged. 

816 

817 emit : bool, default: True 

818 Whether to notify observers of limit change. 

819 

820 auto : bool or None, default: False 

821 Whether to turn on autoscaling of the x-axis. *True* turns on, 

822 *False* turns off, *None* leaves unchanged. 

823 

824 view_margin : float, optional 

825 The additional margin to apply to the limits. 

826 

827 xmin, xmax : float, optional 

828 They are equivalent to left and right respectively, and it is an 

829 error to pass both *xmin* and *left* or *xmax* and *right*. 

830 

831 Returns 

832 ------- 

833 left, right : (float, float) 

834 The new x-axis limits in data coordinates. 

835 

836 See Also 

837 -------- 

838 get_xlim 

839 set_xbound, get_xbound 

840 invert_xaxis, xaxis_inverted 

841 

842 Notes 

843 ----- 

844 The *left* value may be greater than the *right* value, in which 

845 case the x-axis values will decrease from *left* to *right*. 

846 

847 Examples 

848 -------- 

849 >>> set_xlim(left, right) 

850 >>> set_xlim((left, right)) 

851 >>> left, right = set_xlim(left, right) 

852 

853 One limit may be left unchanged. 

854 

855 >>> set_xlim(right=right_lim) 

856 

857 Limits may be passed in reverse order to flip the direction of 

858 the x-axis. For example, suppose ``x`` represents depth of the 

859 ocean in m. The x-axis limits might be set like the following 

860 so 5000 m depth is at the left of the plot and the surface, 

861 0 m, is at the right. 

862 

863 >>> set_xlim(5000, 0) 

864 """ 

865 return self._set_lim3d(self.xaxis, left, right, emit=emit, auto=auto, 

866 view_margin=view_margin, axmin=xmin, axmax=xmax) 

867 

868 def set_ylim(self, bottom=None, top=None, *, emit=True, auto=False, 

869 view_margin=None, ymin=None, ymax=None): 

870 """ 

871 Set the 3D y-axis view limits. 

872 

873 Parameters 

874 ---------- 

875 bottom : float, optional 

876 The bottom ylim in data coordinates. Passing *None* leaves the 

877 limit unchanged. 

878 

879 The bottom and top ylims may also be passed as the tuple 

880 (*bottom*, *top*) as the first positional argument (or as 

881 the *bottom* keyword argument). 

882 

883 .. ACCEPTS: (bottom: float, top: float) 

884 

885 top : float, optional 

886 The top ylim in data coordinates. Passing *None* leaves the 

887 limit unchanged. 

888 

889 emit : bool, default: True 

890 Whether to notify observers of limit change. 

891 

892 auto : bool or None, default: False 

893 Whether to turn on autoscaling of the y-axis. *True* turns on, 

894 *False* turns off, *None* leaves unchanged. 

895 

896 view_margin : float, optional 

897 The additional margin to apply to the limits. 

898 

899 ymin, ymax : float, optional 

900 They are equivalent to bottom and top respectively, and it is an 

901 error to pass both *ymin* and *bottom* or *ymax* and *top*. 

902 

903 Returns 

904 ------- 

905 bottom, top : (float, float) 

906 The new y-axis limits in data coordinates. 

907 

908 See Also 

909 -------- 

910 get_ylim 

911 set_ybound, get_ybound 

912 invert_yaxis, yaxis_inverted 

913 

914 Notes 

915 ----- 

916 The *bottom* value may be greater than the *top* value, in which 

917 case the y-axis values will decrease from *bottom* to *top*. 

918 

919 Examples 

920 -------- 

921 >>> set_ylim(bottom, top) 

922 >>> set_ylim((bottom, top)) 

923 >>> bottom, top = set_ylim(bottom, top) 

924 

925 One limit may be left unchanged. 

926 

927 >>> set_ylim(top=top_lim) 

928 

929 Limits may be passed in reverse order to flip the direction of 

930 the y-axis. For example, suppose ``y`` represents depth of the 

931 ocean in m. The y-axis limits might be set like the following 

932 so 5000 m depth is at the bottom of the plot and the surface, 

933 0 m, is at the top. 

934 

935 >>> set_ylim(5000, 0) 

936 """ 

937 return self._set_lim3d(self.yaxis, bottom, top, emit=emit, auto=auto, 

938 view_margin=view_margin, axmin=ymin, axmax=ymax) 

939 

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

941 view_margin=None, zmin=None, zmax=None): 

942 """ 

943 Set the 3D z-axis view limits. 

944 

945 Parameters 

946 ---------- 

947 bottom : float, optional 

948 The bottom zlim in data coordinates. Passing *None* leaves the 

949 limit unchanged. 

950 

951 The bottom and top zlims may also be passed as the tuple 

952 (*bottom*, *top*) as the first positional argument (or as 

953 the *bottom* keyword argument). 

954 

955 .. ACCEPTS: (bottom: float, top: float) 

956 

957 top : float, optional 

958 The top zlim in data coordinates. Passing *None* leaves the 

959 limit unchanged. 

960 

961 emit : bool, default: True 

962 Whether to notify observers of limit change. 

963 

964 auto : bool or None, default: False 

965 Whether to turn on autoscaling of the z-axis. *True* turns on, 

966 *False* turns off, *None* leaves unchanged. 

967 

968 view_margin : float, optional 

969 The additional margin to apply to the limits. 

970 

971 zmin, zmax : float, optional 

972 They are equivalent to bottom and top respectively, and it is an 

973 error to pass both *zmin* and *bottom* or *zmax* and *top*. 

974 

975 Returns 

976 ------- 

977 bottom, top : (float, float) 

978 The new z-axis limits in data coordinates. 

979 

980 See Also 

981 -------- 

982 get_zlim 

983 set_zbound, get_zbound 

984 invert_zaxis, zaxis_inverted 

985 

986 Notes 

987 ----- 

988 The *bottom* value may be greater than the *top* value, in which 

989 case the z-axis values will decrease from *bottom* to *top*. 

990 

991 Examples 

992 -------- 

993 >>> set_zlim(bottom, top) 

994 >>> set_zlim((bottom, top)) 

995 >>> bottom, top = set_zlim(bottom, top) 

996 

997 One limit may be left unchanged. 

998 

999 >>> set_zlim(top=top_lim) 

1000 

1001 Limits may be passed in reverse order to flip the direction of 

1002 the z-axis. For example, suppose ``z`` represents depth of the 

1003 ocean in m. The z-axis limits might be set like the following 

1004 so 5000 m depth is at the bottom of the plot and the surface, 

1005 0 m, is at the top. 

1006 

1007 >>> set_zlim(5000, 0) 

1008 """ 

1009 return self._set_lim3d(self.zaxis, bottom, top, emit=emit, auto=auto, 

1010 view_margin=view_margin, axmin=zmin, axmax=zmax) 

1011 

1012 set_xlim3d = set_xlim 

1013 set_ylim3d = set_ylim 

1014 set_zlim3d = set_zlim 

1015 

1016 def get_xlim(self): 

1017 # docstring inherited 

1018 return tuple(self.xy_viewLim.intervalx) 

1019 

1020 def get_ylim(self): 

1021 # docstring inherited 

1022 return tuple(self.xy_viewLim.intervaly) 

1023 

1024 def get_zlim(self): 

1025 """ 

1026 Return the 3D z-axis view limits. 

1027 

1028 Returns 

1029 ------- 

1030 left, right : (float, float) 

1031 The current z-axis limits in data coordinates. 

1032 

1033 See Also 

1034 -------- 

1035 set_zlim 

1036 set_zbound, get_zbound 

1037 invert_zaxis, zaxis_inverted 

1038 

1039 Notes 

1040 ----- 

1041 The z-axis may be inverted, in which case the *left* value will 

1042 be greater than the *right* value. 

1043 """ 

1044 return tuple(self.zz_viewLim.intervalx) 

1045 

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

1047 

1048 # Redefine all three methods to overwrite their docstrings. 

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

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

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

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

1053 """ 

1054 Set the {}-axis scale. 

1055 

1056 Parameters 

1057 ---------- 

1058 value : {{"linear"}} 

1059 The axis scale type to apply. 3D Axes currently only support 

1060 linear scales; other scales yield nonsensical results. 

1061 

1062 **kwargs 

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

1064 none of them is applicable for linear scales. 

1065 """.format, 

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

1067 

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

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

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

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

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

1073 set_zticklabels = _axis_method_wrapper( 

1074 "zaxis", "set_ticklabels", 

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

1076 

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

1078 if zaxis_date.__doc__: 

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

1080 

1081 Notes 

1082 ----- 

1083 This function is merely provided for completeness, but 3D Axes do not 

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

1085 """) 

1086 

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

1088 """Currently not implemented for 3D Axes, and returns *None*.""" 

1089 return None 

1090 

1091 def view_init(self, elev=None, azim=None, roll=None, vertical_axis="z", 

1092 share=False): 

1093 """ 

1094 Set the elevation and azimuth of the Axes in degrees (not radians). 

1095 

1096 This can be used to rotate the Axes programmatically. 

1097 

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

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

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

1101 

1102 ========== ==== ==== 

1103 view plane elev azim 

1104 ========== ==== ==== 

1105 XY 90 -90 

1106 XZ 0 -90 

1107 YZ 0 0 

1108 -XY -90 90 

1109 -XZ 0 90 

1110 -YZ 0 180 

1111 ========== ==== ==== 

1112 

1113 Parameters 

1114 ---------- 

1115 elev : float, default: None 

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

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

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

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

1120 location above the x-y plane. 

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

1122 constructor is used. 

1123 azim : float, default: None 

1124 The azimuthal angle in degrees rotates the camera about the 

1125 vertical axis, with a positive angle corresponding to a 

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

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

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

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

1130 constructor is used. 

1131 roll : float, default: None 

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

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

1134 scene to rotate counter-clockwise. 

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

1136 constructor is used. 

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

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

1139 share : bool, default: False 

1140 If ``True``, apply the settings to all Axes with shared views. 

1141 """ 

1142 

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

1144 

1145 if elev is None: 

1146 elev = self.initial_elev 

1147 if azim is None: 

1148 azim = self.initial_azim 

1149 if roll is None: 

1150 roll = self.initial_roll 

1151 vertical_axis = _api.check_getitem( 

1152 {name: idx for idx, name in enumerate(self._axis_names)}, 

1153 vertical_axis=vertical_axis, 

1154 ) 

1155 

1156 if share: 

1157 axes = {sibling for sibling 

1158 in self._shared_axes['view'].get_siblings(self)} 

1159 else: 

1160 axes = [self] 

1161 

1162 for ax in axes: 

1163 ax.elev = elev 

1164 ax.azim = azim 

1165 ax.roll = roll 

1166 ax._vertical_axis = vertical_axis 

1167 

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

1169 """ 

1170 Set the projection type. 

1171 

1172 Parameters 

1173 ---------- 

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

1175 The projection type. 

1176 focal_length : float, default: None 

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

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

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

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

1181 """ 

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

1183 if proj_type == 'persp': 

1184 if focal_length is None: 

1185 focal_length = 1 

1186 elif focal_length <= 0: 

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

1188 "greater than 0") 

1189 self._focal_length = focal_length 

1190 else: # 'ortho': 

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

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

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

1194 self._focal_length = np.inf 

1195 

1196 def _roll_to_vertical( 

1197 self, arr: "np.typing.ArrayLike", reverse: bool = False 

1198 ) -> np.ndarray: 

1199 """ 

1200 Roll arrays to match the different vertical axis. 

1201 

1202 Parameters 

1203 ---------- 

1204 arr : ArrayLike 

1205 Array to roll. 

1206 reverse : bool, default: False 

1207 Reverse the direction of the roll. 

1208 """ 

1209 if reverse: 

1210 return np.roll(arr, (self._vertical_axis - 2) * -1) 

1211 else: 

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

1213 

1214 def get_proj(self): 

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

1216 

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

1218 box_aspect = self._roll_to_vertical(self._box_aspect) 

1219 worldM = proj3d.world_transformation( 

1220 *self.get_xlim3d(), 

1221 *self.get_ylim3d(), 

1222 *self.get_zlim3d(), 

1223 pb_aspect=box_aspect, 

1224 ) 

1225 

1226 # Look into the middle of the world coordinates: 

1227 R = 0.5 * box_aspect 

1228 

1229 # elev: elevation angle in the z plane. 

1230 # azim: azimuth angle in the xy plane. 

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

1232 # p0, p1 corresponds to rotating the box only around the vertical axis. 

1233 # p2 corresponds to rotating the box only around the horizontal axis. 

1234 elev_rad = np.deg2rad(self.elev) 

1235 azim_rad = np.deg2rad(self.azim) 

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

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

1238 p2 = np.sin(elev_rad) 

1239 

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

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

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

1243 

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

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

1246 eye = R + self._dist * ps 

1247 

1248 # Calculate the viewing axes for the eye position 

1249 u, v, w = self._calc_view_axes(eye) 

1250 self._view_u = u # _view_u is towards the right of the screen 

1251 self._view_v = v # _view_v is towards the top of the screen 

1252 self._view_w = w # _view_w is out of the screen 

1253 

1254 # Generate the view and projection transformation matrices 

1255 if self._focal_length == np.inf: 

1256 # Orthographic projection 

1257 viewM = proj3d._view_transformation_uvw(u, v, w, eye) 

1258 projM = proj3d._ortho_transformation(-self._dist, self._dist) 

1259 else: 

1260 # Perspective projection 

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

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

1263 viewM = proj3d._view_transformation_uvw(u, v, w, eye_focal) 

1264 projM = proj3d._persp_transformation(-self._dist, 

1265 self._dist, 

1266 self._focal_length) 

1267 

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

1269 M0 = np.dot(viewM, worldM) 

1270 M = np.dot(projM, M0) 

1271 return M 

1272 

1273 def mouse_init(self, rotate_btn=1, pan_btn=2, zoom_btn=3): 

1274 """ 

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

1276 

1277 Parameters 

1278 ---------- 

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

1280 The mouse button or buttons to use for 3D rotation of the Axes. 

1281 pan_btn : int or list of int, default: 2 

1282 The mouse button or buttons to use to pan the 3D Axes. 

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

1284 The mouse button or buttons to use to zoom the 3D Axes. 

1285 """ 

1286 self.button_pressed = None 

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

1288 # a regular list to avoid comparisons against None 

1289 # which breaks in recent versions of numpy. 

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

1291 self._pan_btn = np.atleast_1d(pan_btn).tolist() 

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

1293 

1294 def disable_mouse_rotation(self): 

1295 """Disable mouse buttons for 3D rotation, panning, and zooming.""" 

1296 self.mouse_init(rotate_btn=[], pan_btn=[], zoom_btn=[]) 

1297 

1298 def can_zoom(self): 

1299 # doc-string inherited 

1300 return True 

1301 

1302 def can_pan(self): 

1303 # doc-string inherited 

1304 return True 

1305 

1306 def sharez(self, other): 

1307 """ 

1308 Share the z-axis with *other*. 

1309 

1310 This is equivalent to passing ``sharez=other`` when constructing the 

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

1312 another Axes. Note that it is not possible to unshare axes. 

1313 """ 

1314 _api.check_isinstance(Axes3D, other=other) 

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

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

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

1318 self._sharez = other 

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

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

1321 z0, z1 = other.get_zlim() 

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

1323 self.zaxis._scale = other.zaxis._scale 

1324 

1325 def shareview(self, other): 

1326 """ 

1327 Share the view angles with *other*. 

1328 

1329 This is equivalent to passing ``shareview=other`` when constructing the 

1330 Axes, and cannot be used if the view angles are already being shared 

1331 with another Axes. Note that it is not possible to unshare axes. 

1332 """ 

1333 _api.check_isinstance(Axes3D, other=other) 

1334 if self._shareview is not None and other is not self._shareview: 

1335 raise ValueError("view angles are already shared") 

1336 self._shared_axes["view"].join(self, other) 

1337 self._shareview = other 

1338 vertical_axis = self._axis_names[other._vertical_axis] 

1339 self.view_init(elev=other.elev, azim=other.azim, roll=other.roll, 

1340 vertical_axis=vertical_axis, share=True) 

1341 

1342 def clear(self): 

1343 # docstring inherited. 

1344 super().clear() 

1345 if self._focal_length == np.inf: 

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

1347 else: 

1348 self._zmargin = 0. 

1349 

1350 xymargin = 0.05 * 10/11 # match mpl3.8 appearance 

1351 self.xy_dataLim = Bbox([[xymargin, xymargin], 

1352 [1 - xymargin, 1 - xymargin]]) 

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

1354 self.zz_dataLim = Bbox.unit() 

1355 self._view_margin = 1/48 # default value to match mpl3.8 

1356 self.autoscale_view() 

1357 

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

1359 

1360 def _button_press(self, event): 

1361 if event.inaxes == self: 

1362 self.button_pressed = event.button 

1363 self._sx, self._sy = event.xdata, event.ydata 

1364 toolbar = self.figure.canvas.toolbar 

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

1366 toolbar.push_current() 

1367 

1368 def _button_release(self, event): 

1369 self.button_pressed = None 

1370 toolbar = self.figure.canvas.toolbar 

1371 # backend_bases.release_zoom and backend_bases.release_pan call 

1372 # push_current, so check the navigation mode so we don't call it twice 

1373 if toolbar and self.get_navigate_mode() is None: 

1374 toolbar.push_current() 

1375 

1376 def _get_view(self): 

1377 # docstring inherited 

1378 return { 

1379 "xlim": self.get_xlim(), "autoscalex_on": self.get_autoscalex_on(), 

1380 "ylim": self.get_ylim(), "autoscaley_on": self.get_autoscaley_on(), 

1381 "zlim": self.get_zlim(), "autoscalez_on": self.get_autoscalez_on(), 

1382 }, (self.elev, self.azim, self.roll) 

1383 

1384 def _set_view(self, view): 

1385 # docstring inherited 

1386 props, (elev, azim, roll) = view 

1387 self.set(**props) 

1388 self.elev = elev 

1389 self.azim = azim 

1390 self.roll = roll 

1391 

1392 def format_zdata(self, z): 

1393 """ 

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

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

1396 back on the zaxis major formatter 

1397 """ 

1398 try: 

1399 return self.fmt_zdata(z) 

1400 except (AttributeError, TypeError): 

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

1402 val = func(z) 

1403 return val 

1404 

1405 def format_coord(self, xv, yv, renderer=None): 

1406 """ 

1407 Return a string giving the current view rotation angles, or the x, y, z 

1408 coordinates of the point on the nearest axis pane underneath the mouse 

1409 cursor, depending on the mouse button pressed. 

1410 """ 

1411 coords = '' 

1412 

1413 if self.button_pressed in self._rotate_btn: 

1414 # ignore xv and yv and display angles instead 

1415 coords = self._rotation_coords() 

1416 

1417 elif self.M is not None: 

1418 coords = self._location_coords(xv, yv, renderer) 

1419 

1420 return coords 

1421 

1422 def _rotation_coords(self): 

1423 """ 

1424 Return the rotation angles as a string. 

1425 """ 

1426 norm_elev = art3d._norm_angle(self.elev) 

1427 norm_azim = art3d._norm_angle(self.azim) 

1428 norm_roll = art3d._norm_angle(self.roll) 

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

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

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

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

1433 return coords 

1434 

1435 def _location_coords(self, xv, yv, renderer): 

1436 """ 

1437 Return the location on the axis pane underneath the cursor as a string. 

1438 """ 

1439 p1, pane_idx = self._calc_coord(xv, yv, renderer) 

1440 xs = self.format_xdata(p1[0]) 

1441 ys = self.format_ydata(p1[1]) 

1442 zs = self.format_zdata(p1[2]) 

1443 if pane_idx == 0: 

1444 coords = f'x pane={xs}, y={ys}, z={zs}' 

1445 elif pane_idx == 1: 

1446 coords = f'x={xs}, y pane={ys}, z={zs}' 

1447 elif pane_idx == 2: 

1448 coords = f'x={xs}, y={ys}, z pane={zs}' 

1449 return coords 

1450 

1451 def _get_camera_loc(self): 

1452 """ 

1453 Returns the current camera location in data coordinates. 

1454 """ 

1455 cx, cy, cz, dx, dy, dz = self._get_w_centers_ranges() 

1456 c = np.array([cx, cy, cz]) 

1457 r = np.array([dx, dy, dz]) 

1458 

1459 if self._focal_length == np.inf: # orthographic projection 

1460 focal_length = 1e9 # large enough to be effectively infinite 

1461 else: # perspective projection 

1462 focal_length = self._focal_length 

1463 eye = c + self._view_w * self._dist * r / self._box_aspect * focal_length 

1464 return eye 

1465 

1466 def _calc_coord(self, xv, yv, renderer=None): 

1467 """ 

1468 Given the 2D view coordinates, find the point on the nearest axis pane 

1469 that lies directly below those coordinates. Returns a 3D point in data 

1470 coordinates. 

1471 """ 

1472 if self._focal_length == np.inf: # orthographic projection 

1473 zv = 1 

1474 else: # perspective projection 

1475 zv = -1 / self._focal_length 

1476 

1477 # Convert point on view plane to data coordinates 

1478 p1 = np.array(proj3d.inv_transform(xv, yv, zv, self.invM)).ravel() 

1479 

1480 # Get the vector from the camera to the point on the view plane 

1481 vec = self._get_camera_loc() - p1 

1482 

1483 # Get the pane locations for each of the axes 

1484 pane_locs = [] 

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

1486 xys, loc = axis.active_pane() 

1487 pane_locs.append(loc) 

1488 

1489 # Find the distance to the nearest pane by projecting the view vector 

1490 scales = np.zeros(3) 

1491 for i in range(3): 

1492 if vec[i] == 0: 

1493 scales[i] = np.inf 

1494 else: 

1495 scales[i] = (p1[i] - pane_locs[i]) / vec[i] 

1496 pane_idx = np.argmin(abs(scales)) 

1497 scale = scales[pane_idx] 

1498 

1499 # Calculate the point on the closest pane 

1500 p2 = p1 - scale*vec 

1501 return p2, pane_idx 

1502 

1503 def _on_move(self, event): 

1504 """ 

1505 Mouse moving. 

1506 

1507 By default, button-1 rotates, button-2 pans, and button-3 zooms; 

1508 these buttons can be modified via `mouse_init`. 

1509 """ 

1510 

1511 if not self.button_pressed: 

1512 return 

1513 

1514 if self.get_navigate_mode() is not None: 

1515 # we don't want to rotate if we are zooming/panning 

1516 # from the toolbar 

1517 return 

1518 

1519 if self.M is None: 

1520 return 

1521 

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

1523 # In case the mouse is out of bounds. 

1524 if x is None or event.inaxes != self: 

1525 return 

1526 

1527 dx, dy = x - self._sx, y - self._sy 

1528 w = self._pseudo_w 

1529 h = self._pseudo_h 

1530 

1531 # Rotation 

1532 if self.button_pressed in self._rotate_btn: 

1533 # rotate viewing point 

1534 # get the x and y pixel coords 

1535 if dx == 0 and dy == 0: 

1536 return 

1537 

1538 roll = np.deg2rad(self.roll) 

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

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

1541 elev = self.elev + delev 

1542 azim = self.azim + dazim 

1543 roll = self.roll 

1544 vertical_axis = self._axis_names[self._vertical_axis] 

1545 self.view_init( 

1546 elev=elev, 

1547 azim=azim, 

1548 roll=roll, 

1549 vertical_axis=vertical_axis, 

1550 share=True, 

1551 ) 

1552 self.stale = True 

1553 

1554 # Pan 

1555 elif self.button_pressed in self._pan_btn: 

1556 # Start the pan event with pixel coordinates 

1557 px, py = self.transData.transform([self._sx, self._sy]) 

1558 self.start_pan(px, py, 2) 

1559 # pan view (takes pixel coordinate input) 

1560 self.drag_pan(2, None, event.x, event.y) 

1561 self.end_pan() 

1562 

1563 # Zoom 

1564 elif self.button_pressed in self._zoom_btn: 

1565 # zoom view (dragging down zooms in) 

1566 scale = h/(h - dy) 

1567 self._scale_axis_limits(scale, scale, scale) 

1568 

1569 # Store the event coordinates for the next time through. 

1570 self._sx, self._sy = x, y 

1571 # Always request a draw update at the end of interaction 

1572 self.figure.canvas.draw_idle() 

1573 

1574 def drag_pan(self, button, key, x, y): 

1575 # docstring inherited 

1576 

1577 # Get the coordinates from the move event 

1578 p = self._pan_start 

1579 (xdata, ydata), (xdata_start, ydata_start) = p.trans_inverse.transform( 

1580 [(x, y), (p.x, p.y)]) 

1581 self._sx, self._sy = xdata, ydata 

1582 # Calling start_pan() to set the x/y of this event as the starting 

1583 # move location for the next event 

1584 self.start_pan(x, y, button) 

1585 du, dv = xdata - xdata_start, ydata - ydata_start 

1586 dw = 0 

1587 if key == 'x': 

1588 dv = 0 

1589 elif key == 'y': 

1590 du = 0 

1591 if du == 0 and dv == 0: 

1592 return 

1593 

1594 # Transform the pan from the view axes to the data axes 

1595 R = np.array([self._view_u, self._view_v, self._view_w]) 

1596 R = -R / self._box_aspect * self._dist 

1597 duvw_projected = R.T @ np.array([du, dv, dw]) 

1598 

1599 # Calculate pan distance 

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

1601 dx = (maxx - minx) * duvw_projected[0] 

1602 dy = (maxy - miny) * duvw_projected[1] 

1603 dz = (maxz - minz) * duvw_projected[2] 

1604 

1605 # Set the new axis limits 

1606 self.set_xlim3d(minx + dx, maxx + dx, auto=None) 

1607 self.set_ylim3d(miny + dy, maxy + dy, auto=None) 

1608 self.set_zlim3d(minz + dz, maxz + dz, auto=None) 

1609 

1610 def _calc_view_axes(self, eye): 

1611 """ 

1612 Get the unit vectors for the viewing axes in data coordinates. 

1613 `u` is towards the right of the screen 

1614 `v` is towards the top of the screen 

1615 `w` is out of the screen 

1616 """ 

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

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

1619 

1620 # Look into the middle of the world coordinates 

1621 R = 0.5 * self._roll_to_vertical(self._box_aspect) 

1622 

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

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

1625 # have been reversed: 

1626 V = np.zeros(3) 

1627 V[self._vertical_axis] = -1 if abs(elev_rad) > np.pi/2 else 1 

1628 

1629 u, v, w = proj3d._view_axes(eye, R, V, roll_rad) 

1630 return u, v, w 

1631 

1632 def _set_view_from_bbox(self, bbox, direction='in', 

1633 mode=None, twinx=False, twiny=False): 

1634 """ 

1635 Zoom in or out of the bounding box. 

1636 

1637 Will center the view in the center of the bounding box, and zoom by 

1638 the ratio of the size of the bounding box to the size of the Axes3D. 

1639 """ 

1640 (start_x, start_y, stop_x, stop_y) = bbox 

1641 if mode == 'x': 

1642 start_y = self.bbox.min[1] 

1643 stop_y = self.bbox.max[1] 

1644 elif mode == 'y': 

1645 start_x = self.bbox.min[0] 

1646 stop_x = self.bbox.max[0] 

1647 

1648 # Clip to bounding box limits 

1649 start_x, stop_x = np.clip(sorted([start_x, stop_x]), 

1650 self.bbox.min[0], self.bbox.max[0]) 

1651 start_y, stop_y = np.clip(sorted([start_y, stop_y]), 

1652 self.bbox.min[1], self.bbox.max[1]) 

1653 

1654 # Move the center of the view to the center of the bbox 

1655 zoom_center_x = (start_x + stop_x)/2 

1656 zoom_center_y = (start_y + stop_y)/2 

1657 

1658 ax_center_x = (self.bbox.max[0] + self.bbox.min[0])/2 

1659 ax_center_y = (self.bbox.max[1] + self.bbox.min[1])/2 

1660 

1661 self.start_pan(zoom_center_x, zoom_center_y, 2) 

1662 self.drag_pan(2, None, ax_center_x, ax_center_y) 

1663 self.end_pan() 

1664 

1665 # Calculate zoom level 

1666 dx = abs(start_x - stop_x) 

1667 dy = abs(start_y - stop_y) 

1668 scale_u = dx / (self.bbox.max[0] - self.bbox.min[0]) 

1669 scale_v = dy / (self.bbox.max[1] - self.bbox.min[1]) 

1670 

1671 # Keep aspect ratios equal 

1672 scale = max(scale_u, scale_v) 

1673 

1674 # Zoom out 

1675 if direction == 'out': 

1676 scale = 1 / scale 

1677 

1678 self._zoom_data_limits(scale, scale, scale) 

1679 

1680 def _zoom_data_limits(self, scale_u, scale_v, scale_w): 

1681 """ 

1682 Zoom in or out of a 3D plot. 

1683 

1684 Will scale the data limits by the scale factors. These will be 

1685 transformed to the x, y, z data axes based on the current view angles. 

1686 A scale factor > 1 zooms out and a scale factor < 1 zooms in. 

1687 

1688 For an Axes that has had its aspect ratio set to 'equal', 'equalxy', 

1689 'equalyz', or 'equalxz', the relevant axes are constrained to zoom 

1690 equally. 

1691 

1692 Parameters 

1693 ---------- 

1694 scale_u : float 

1695 Scale factor for the u view axis (view screen horizontal). 

1696 scale_v : float 

1697 Scale factor for the v view axis (view screen vertical). 

1698 scale_w : float 

1699 Scale factor for the w view axis (view screen depth). 

1700 """ 

1701 scale = np.array([scale_u, scale_v, scale_w]) 

1702 

1703 # Only perform frame conversion if unequal scale factors 

1704 if not np.allclose(scale, scale_u): 

1705 # Convert the scale factors from the view frame to the data frame 

1706 R = np.array([self._view_u, self._view_v, self._view_w]) 

1707 S = scale * np.eye(3) 

1708 scale = np.linalg.norm(R.T @ S, axis=1) 

1709 

1710 # Set the constrained scale factors to the factor closest to 1 

1711 if self._aspect in ('equal', 'equalxy', 'equalxz', 'equalyz'): 

1712 ax_idxs = self._equal_aspect_axis_indices(self._aspect) 

1713 min_ax_idxs = np.argmin(np.abs(scale[ax_idxs] - 1)) 

1714 scale[ax_idxs] = scale[ax_idxs][min_ax_idxs] 

1715 

1716 self._scale_axis_limits(scale[0], scale[1], scale[2]) 

1717 

1718 def _scale_axis_limits(self, scale_x, scale_y, scale_z): 

1719 """ 

1720 Keeping the center of the x, y, and z data axes fixed, scale their 

1721 limits by scale factors. A scale factor > 1 zooms out and a scale 

1722 factor < 1 zooms in. 

1723 

1724 Parameters 

1725 ---------- 

1726 scale_x : float 

1727 Scale factor for the x data axis. 

1728 scale_y : float 

1729 Scale factor for the y data axis. 

1730 scale_z : float 

1731 Scale factor for the z data axis. 

1732 """ 

1733 # Get the axis centers and ranges 

1734 cx, cy, cz, dx, dy, dz = self._get_w_centers_ranges() 

1735 

1736 # Set the scaled axis limits 

1737 self.set_xlim3d(cx - dx*scale_x/2, cx + dx*scale_x/2, auto=None) 

1738 self.set_ylim3d(cy - dy*scale_y/2, cy + dy*scale_y/2, auto=None) 

1739 self.set_zlim3d(cz - dz*scale_z/2, cz + dz*scale_z/2, auto=None) 

1740 

1741 def _get_w_centers_ranges(self): 

1742 """Get 3D world centers and axis ranges.""" 

1743 # Calculate center of axis limits 

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

1745 cx = (maxx + minx)/2 

1746 cy = (maxy + miny)/2 

1747 cz = (maxz + minz)/2 

1748 

1749 # Calculate range of axis limits 

1750 dx = (maxx - minx) 

1751 dy = (maxy - miny) 

1752 dz = (maxz - minz) 

1753 return cx, cy, cz, dx, dy, dz 

1754 

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

1756 """ 

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

1758 """ 

1759 if labelpad is not None: 

1760 self.zaxis.labelpad = labelpad 

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

1762 

1763 def get_zlabel(self): 

1764 """ 

1765 Get the z-label text string. 

1766 """ 

1767 label = self.zaxis.get_label() 

1768 return label.get_text() 

1769 

1770 # Axes rectangle characteristics 

1771 

1772 # The frame_on methods are not available for 3D axes. 

1773 # Python will raise a TypeError if they are called. 

1774 get_frame_on = None 

1775 set_frame_on = None 

1776 

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

1778 """ 

1779 Set / unset 3D grid. 

1780 

1781 .. note:: 

1782 

1783 Currently, this function does not behave the same as 

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

1785 behavior. 

1786 """ 

1787 # TODO: Operate on each axes separately 

1788 if len(kwargs): 

1789 visible = True 

1790 self._draw_grid = visible 

1791 self.stale = True 

1792 

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

1794 """ 

1795 Convenience method for changing the appearance of ticks and 

1796 tick labels. 

1797 

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

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

1800 to 'both' autoscales all three axes. 

1801 

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

1803 from regular 2D Axes, some of these settings may have 

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

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

1806 

1807 .. note:: 

1808 Axes3D currently ignores some of these settings. 

1809 """ 

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

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

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

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

1814 zkw = dict(kwargs) 

1815 zkw.pop('top', None) 

1816 zkw.pop('bottom', None) 

1817 zkw.pop('labeltop', None) 

1818 zkw.pop('labelbottom', None) 

1819 self.zaxis.set_tick_params(**zkw) 

1820 

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

1822 

1823 def invert_zaxis(self): 

1824 """ 

1825 Invert the z-axis. 

1826 

1827 See Also 

1828 -------- 

1829 zaxis_inverted 

1830 get_zlim, set_zlim 

1831 get_zbound, set_zbound 

1832 """ 

1833 bottom, top = self.get_zlim() 

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

1835 

1836 zaxis_inverted = _axis_method_wrapper("zaxis", "get_inverted") 

1837 

1838 def get_zbound(self): 

1839 """ 

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

1841 

1842 See Also 

1843 -------- 

1844 set_zbound 

1845 get_zlim, set_zlim 

1846 invert_zaxis, zaxis_inverted 

1847 """ 

1848 lower, upper = self.get_zlim() 

1849 if lower < upper: 

1850 return lower, upper 

1851 else: 

1852 return upper, lower 

1853 

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

1855 """ 

1856 Add the text *s* to the 3D Axes at location *x*, *y*, *z* in data coordinates. 

1857 

1858 Parameters 

1859 ---------- 

1860 x, y, z : float 

1861 The position to place the text. 

1862 s : str 

1863 The text. 

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

1865 The direction to be used as the z-direction. Default: 'z'. 

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

1867 **kwargs 

1868 Other arguments are forwarded to `matplotlib.axes.Axes.text`. 

1869 

1870 Returns 

1871 ------- 

1872 `.Text3D` 

1873 The created `.Text3D` instance. 

1874 """ 

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

1876 art3d.text_2d_to_3d(text, z, zdir) 

1877 return text 

1878 

1879 text3D = text 

1880 text2D = Axes.text 

1881 

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

1883 """ 

1884 Plot 2D or 3D data. 

1885 

1886 Parameters 

1887 ---------- 

1888 xs : 1D array-like 

1889 x coordinates of vertices. 

1890 ys : 1D array-like 

1891 y coordinates of vertices. 

1892 zs : float or 1D array-like 

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

1894 each point. 

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

1896 When plotting 2D data, the direction to use as z. 

1897 **kwargs 

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

1899 """ 

1900 had_data = self.has_data() 

1901 

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

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

1904 # `_process_plot_var_args`). 

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

1906 zs, *args = args 

1907 if 'zs' in kwargs: 

1908 raise TypeError("plot() for multiple values for argument 'zs'") 

1909 else: 

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

1911 

1912 xs, ys, zs = cbook._broadcast_with_masks(xs, ys, zs) 

1913 

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

1915 for line in lines: 

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

1917 

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

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

1920 return lines 

1921 

1922 plot3D = plot 

1923 

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

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

1926 """ 

1927 Create a surface plot. 

1928 

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

1930 supports colormapping by supplying the *cmap* argument. 

1931 

1932 .. note:: 

1933 

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

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

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

1937 these numbers of points. 

1938 

1939 .. note:: 

1940 

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

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

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

1944 divisors of 50. 

1945 

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

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

1948 the optimized path. 

1949 

1950 Parameters 

1951 ---------- 

1952 X, Y, Z : 2D arrays 

1953 Data values. 

1954 

1955 rcount, ccount : int 

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

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

1958 numbers of points. Defaults to 50. 

1959 

1960 rstride, cstride : int 

1961 Downsampling stride in each direction. These arguments are 

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

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

1964 

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

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

1967 

1968 color : :mpltype:`color` 

1969 Color of the surface patches. 

1970 

1971 cmap : Colormap, optional 

1972 Colormap of the surface patches. 

1973 

1974 facecolors : list of :mpltype:`color` 

1975 Colors of each individual patch. 

1976 

1977 norm : `~matplotlib.colors.Normalize`, optional 

1978 Normalization for the colormap. 

1979 

1980 vmin, vmax : float, optional 

1981 Bounds for the normalization. 

1982 

1983 shade : bool, default: True 

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

1985 *cmap* is specified. 

1986 

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

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

1989 

1990 **kwargs 

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

1992 """ 

1993 

1994 had_data = self.has_data() 

1995 

1996 if Z.ndim != 2: 

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

1998 

1999 Z = cbook._to_unmasked_float_array(Z) 

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

2001 rows, cols = Z.shape 

2002 

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

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

2005 

2006 if has_stride and has_count: 

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

2008 

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

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

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

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

2013 

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

2015 # Strides have priority over counts in classic mode. 

2016 # So, only compute strides from counts 

2017 # if counts were explicitly given 

2018 compute_strides = has_count 

2019 else: 

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

2021 # Otherwise, compute the strides from the counts. 

2022 compute_strides = not has_stride 

2023 

2024 if compute_strides: 

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

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

2027 

2028 fcolors = kwargs.pop('facecolors', None) 

2029 

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

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

2032 if shade is None: 

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

2034 

2035 colset = [] # the sampled facecolor 

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

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

2038 fcolors is None: 

2039 polys = np.stack( 

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

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

2042 axis=-1) 

2043 else: 

2044 # evenly spaced, and including both endpoints 

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

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

2047 

2048 polys = [] 

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

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

2051 ps = [ 

2052 # +1 ensures we share edges between polygons 

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

2054 for a in (X, Y, Z) 

2055 ] 

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

2057 ps = np.array(ps).T 

2058 polys.append(ps) 

2059 

2060 if fcolors is not None: 

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

2062 

2063 # In cases where there are non-finite values in the data (possibly NaNs from 

2064 # masked arrays), artifacts can be introduced. Here check whether such values 

2065 # are present and remove them. 

2066 if not isinstance(polys, np.ndarray) or not np.isfinite(polys).all(): 

2067 new_polys = [] 

2068 new_colset = [] 

2069 

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

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

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

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

2074 new_poly = np.array(p)[np.isfinite(p).all(axis=1)] 

2075 if len(new_poly): 

2076 new_polys.append(new_poly) 

2077 new_colset.append(col) 

2078 

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

2080 polys = new_polys 

2081 if fcolors is not None: 

2082 colset = new_colset 

2083 

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

2085 # than others 

2086 

2087 if fcolors is not None: 

2088 polyc = art3d.Poly3DCollection( 

2089 polys, edgecolors=colset, facecolors=colset, shade=shade, 

2090 lightsource=lightsource, **kwargs) 

2091 elif cmap: 

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

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

2094 if isinstance(polys, np.ndarray): 

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

2096 else: 

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

2098 polyc.set_array(avg_z) 

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

2100 polyc.set_clim(vmin, vmax) 

2101 if norm is not None: 

2102 polyc.set_norm(norm) 

2103 else: 

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

2105 if color is None: 

2106 color = self._get_lines.get_next_color() 

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

2108 

2109 polyc = art3d.Poly3DCollection( 

2110 polys, facecolors=color, shade=shade, 

2111 lightsource=lightsource, **kwargs) 

2112 

2113 self.add_collection(polyc) 

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

2115 

2116 return polyc 

2117 

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

2119 """ 

2120 Plot a 3D wireframe. 

2121 

2122 .. note:: 

2123 

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

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

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

2127 these numbers of points. 

2128 

2129 Parameters 

2130 ---------- 

2131 X, Y, Z : 2D arrays 

2132 Data values. 

2133 

2134 rcount, ccount : int 

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

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

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

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

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

2140 

2141 rstride, cstride : int 

2142 Downsampling stride in each direction. These arguments are 

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

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

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

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

2147 wireframe plot. 

2148 

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

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

2151 

2152 **kwargs 

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

2154 """ 

2155 

2156 had_data = self.has_data() 

2157 if Z.ndim != 2: 

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

2159 # FIXME: Support masked arrays 

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

2161 rows, cols = Z.shape 

2162 

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

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

2165 

2166 if has_stride and has_count: 

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

2168 

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

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

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

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

2173 

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

2175 # Strides have priority over counts in classic mode. 

2176 # So, only compute strides from counts 

2177 # if counts were explicitly given 

2178 if has_count: 

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

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

2181 else: 

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

2183 # Otherwise, compute the strides from the counts. 

2184 if not has_stride: 

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

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

2187 

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

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

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

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

2192 

2193 if rstride: 

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

2195 # Add the last index only if needed 

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

2197 rii += [rows-1] 

2198 else: 

2199 rii = [] 

2200 if cstride: 

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

2202 # Add the last index only if needed 

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

2204 cii += [cols-1] 

2205 else: 

2206 cii = [] 

2207 

2208 if rstride == 0 and cstride == 0: 

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

2210 

2211 # If the inputs were empty, then just 

2212 # reset everything. 

2213 if Z.size == 0: 

2214 rii = [] 

2215 cii = [] 

2216 

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

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

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

2220 

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

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

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

2224 

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

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

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

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

2229 

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

2231 self.add_collection(linec) 

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

2233 

2234 return linec 

2235 

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

2237 lightsource=None, **kwargs): 

2238 """ 

2239 Plot a triangulated surface. 

2240 

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

2242 either:: 

2243 

2244 plot_trisurf(triangulation, ...) 

2245 

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

2247 

2248 plot_trisurf(X, Y, ...) 

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

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

2251 

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

2253 `.Triangulation` for an explanation of these possibilities. 

2254 

2255 The remaining arguments are:: 

2256 

2257 plot_trisurf(..., Z) 

2258 

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

2260 in the triangulation. 

2261 

2262 Parameters 

2263 ---------- 

2264 X, Y, Z : array-like 

2265 Data values as 1D arrays. 

2266 color 

2267 Color of the surface patches. 

2268 cmap 

2269 A colormap for the surface patches. 

2270 norm : `~matplotlib.colors.Normalize`, optional 

2271 An instance of Normalize to map values to colors. 

2272 vmin, vmax : float, optional 

2273 Minimum and maximum value to map. 

2274 shade : bool, default: True 

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

2276 *cmap* is specified. 

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

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

2279 **kwargs 

2280 All other keyword arguments are passed on to 

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

2282 

2283 Examples 

2284 -------- 

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

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

2287 """ 

2288 

2289 had_data = self.has_data() 

2290 

2291 # TODO: Support custom face colours 

2292 if color is None: 

2293 color = self._get_lines.get_next_color() 

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

2295 

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

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

2298 

2299 tri, args, kwargs = \ 

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

2301 try: 

2302 z = kwargs.pop('Z') 

2303 except KeyError: 

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

2305 z, *args = args 

2306 z = np.asarray(z) 

2307 

2308 triangles = tri.get_masked_triangles() 

2309 xt = tri.x[triangles] 

2310 yt = tri.y[triangles] 

2311 zt = z[triangles] 

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

2313 

2314 if cmap: 

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

2316 # average over the three points of each triangle 

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

2318 polyc.set_array(avg_z) 

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

2320 polyc.set_clim(vmin, vmax) 

2321 if norm is not None: 

2322 polyc.set_norm(norm) 

2323 else: 

2324 polyc = art3d.Poly3DCollection( 

2325 verts, *args, shade=shade, lightsource=lightsource, 

2326 facecolors=color, **kwargs) 

2327 

2328 self.add_collection(polyc) 

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

2330 

2331 return polyc 

2332 

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

2334 """ 

2335 Extend a contour in 3D by creating 

2336 """ 

2337 

2338 dz = (cset.levels[1] - cset.levels[0]) / 2 

2339 polyverts = [] 

2340 colors = [] 

2341 for idx, level in enumerate(cset.levels): 

2342 path = cset.get_paths()[idx] 

2343 subpaths = [*path._iter_connected_components()] 

2344 color = cset.get_edgecolor()[idx] 

2345 top = art3d._paths_to_3d_segments(subpaths, level - dz) 

2346 bot = art3d._paths_to_3d_segments(subpaths, level + dz) 

2347 if not len(top[0]): 

2348 continue 

2349 nsteps = max(round(len(top[0]) / stride), 2) 

2350 stepsize = (len(top[0]) - 1) / (nsteps - 1) 

2351 polyverts.extend([ 

2352 (top[0][round(i * stepsize)], top[0][round((i + 1) * stepsize)], 

2353 bot[0][round((i + 1) * stepsize)], bot[0][round(i * stepsize)]) 

2354 for i in range(round(nsteps) - 1)]) 

2355 colors.extend([color] * (round(nsteps) - 1)) 

2356 self.add_collection3d(art3d.Poly3DCollection( 

2357 np.array(polyverts), # All polygons have 4 vertices, so vectorize. 

2358 facecolors=colors, edgecolors=colors, shade=True)) 

2359 cset.remove() 

2360 

2361 def add_contour_set( 

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

2363 zdir = '-' + zdir 

2364 if extend3d: 

2365 self._3d_extend_contour(cset, stride) 

2366 else: 

2367 art3d.collection_2d_to_3d( 

2368 cset, zs=offset if offset is not None else cset.levels, zdir=zdir) 

2369 

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

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

2372 

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

2374 """ 

2375 Returns 

2376 ------- 

2377 levels : `numpy.ndarray` 

2378 Levels at which the filled contours are added. 

2379 """ 

2380 zdir = '-' + zdir 

2381 

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

2383 # Linearly interpolate to get levels for any extensions 

2384 if cset._extend_min: 

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

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

2387 if cset._extend_max: 

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

2389 midpoints = np.append(midpoints, max_level) 

2390 

2391 art3d.collection_2d_to_3d( 

2392 cset, zs=offset if offset is not None else midpoints, zdir=zdir) 

2393 return midpoints 

2394 

2395 @_preprocess_data() 

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

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

2398 """ 

2399 Create a 3D contour plot. 

2400 

2401 Parameters 

2402 ---------- 

2403 X, Y, Z : array-like, 

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

2405 extend3d : bool, default: False 

2406 Whether to extend contour in 3D. 

2407 stride : int, default: 5 

2408 Step size for extending contour. 

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

2410 The direction to use. 

2411 offset : float, optional 

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

2413 position in a plane normal to *zdir*. 

2414 data : indexable object, optional 

2415 DATA_PARAMETER_PLACEHOLDER 

2416 

2417 *args, **kwargs 

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

2419 

2420 Returns 

2421 ------- 

2422 matplotlib.contour.QuadContourSet 

2423 """ 

2424 had_data = self.has_data() 

2425 

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

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

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

2429 

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

2431 return cset 

2432 

2433 contour3D = contour 

2434 

2435 @_preprocess_data() 

2436 def tricontour(self, *args, 

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

2438 """ 

2439 Create a 3D contour plot. 

2440 

2441 .. note:: 

2442 This method currently produces incorrect output due to a 

2443 longstanding bug in 3D PolyCollection rendering. 

2444 

2445 Parameters 

2446 ---------- 

2447 X, Y, Z : array-like 

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

2449 extend3d : bool, default: False 

2450 Whether to extend contour in 3D. 

2451 stride : int, default: 5 

2452 Step size for extending contour. 

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

2454 The direction to use. 

2455 offset : float, optional 

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

2457 position in a plane normal to *zdir*. 

2458 data : indexable object, optional 

2459 DATA_PARAMETER_PLACEHOLDER 

2460 *args, **kwargs 

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

2462 

2463 Returns 

2464 ------- 

2465 matplotlib.tri._tricontour.TriContourSet 

2466 """ 

2467 had_data = self.has_data() 

2468 

2469 tri, args, kwargs = Triangulation.get_from_args_and_kwargs( 

2470 *args, **kwargs) 

2471 X = tri.x 

2472 Y = tri.y 

2473 if 'Z' in kwargs: 

2474 Z = kwargs.pop('Z') 

2475 else: 

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

2477 Z, *args = args 

2478 

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

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

2481 

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

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

2484 

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

2486 return cset 

2487 

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

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

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

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

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

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

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

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

2496 self.auto_scale_xyz(*limits, had_data) 

2497 

2498 @_preprocess_data() 

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

2500 """ 

2501 Create a 3D filled contour plot. 

2502 

2503 Parameters 

2504 ---------- 

2505 X, Y, Z : array-like 

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

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

2508 The direction to use. 

2509 offset : float, optional 

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

2511 position in a plane normal to *zdir*. 

2512 data : indexable object, optional 

2513 DATA_PARAMETER_PLACEHOLDER 

2514 *args, **kwargs 

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

2516 

2517 Returns 

2518 ------- 

2519 matplotlib.contour.QuadContourSet 

2520 """ 

2521 had_data = self.has_data() 

2522 

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

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

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

2526 

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

2528 return cset 

2529 

2530 contourf3D = contourf 

2531 

2532 @_preprocess_data() 

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

2534 """ 

2535 Create a 3D filled contour plot. 

2536 

2537 .. note:: 

2538 This method currently produces incorrect output due to a 

2539 longstanding bug in 3D PolyCollection rendering. 

2540 

2541 Parameters 

2542 ---------- 

2543 X, Y, Z : array-like 

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

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

2546 The direction to use. 

2547 offset : float, optional 

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

2549 position in a plane normal to zdir. 

2550 data : indexable object, optional 

2551 DATA_PARAMETER_PLACEHOLDER 

2552 *args, **kwargs 

2553 Other arguments are forwarded to 

2554 `matplotlib.axes.Axes.tricontourf`. 

2555 

2556 Returns 

2557 ------- 

2558 matplotlib.tri._tricontour.TriContourSet 

2559 """ 

2560 had_data = self.has_data() 

2561 

2562 tri, args, kwargs = Triangulation.get_from_args_and_kwargs( 

2563 *args, **kwargs) 

2564 X = tri.x 

2565 Y = tri.y 

2566 if 'Z' in kwargs: 

2567 Z = kwargs.pop('Z') 

2568 else: 

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

2570 Z, *args = args 

2571 

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

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

2574 

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

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

2577 

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

2579 return cset 

2580 

2581 def add_collection3d(self, col, zs=0, zdir='z', autolim=True): 

2582 """ 

2583 Add a 3D collection object to the plot. 

2584 

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

2586 modifying the object and adding z coordinate information, 

2587 *zs* and *zdir*. 

2588 

2589 Supported 2D collection types are: 

2590 

2591 - `.PolyCollection` 

2592 - `.LineCollection` 

2593 - `.PatchCollection` (currently not supporting *autolim*) 

2594 

2595 Parameters 

2596 ---------- 

2597 col : `.Collection` 

2598 A 2D collection object. 

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

2600 The z-positions to be used for the 2D objects. 

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

2602 The direction to use for the z-positions. 

2603 autolim : bool, default: True 

2604 Whether to update the data limits. 

2605 """ 

2606 had_data = self.has_data() 

2607 

2608 zvals = np.atleast_1d(zs) 

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

2610 else 0) # FIXME: arbitrary default 

2611 

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

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

2614 # abstract class to test for and exclude? 

2615 if type(col) is mcoll.PolyCollection: 

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

2617 col.set_sort_zpos(zsortval) 

2618 elif type(col) is mcoll.LineCollection: 

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

2620 col.set_sort_zpos(zsortval) 

2621 elif type(col) is mcoll.PatchCollection: 

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

2623 col.set_sort_zpos(zsortval) 

2624 

2625 if autolim: 

2626 if isinstance(col, art3d.Line3DCollection): 

2627 self.auto_scale_xyz(*np.array(col._segments3d).transpose(), 

2628 had_data=had_data) 

2629 elif isinstance(col, art3d.Poly3DCollection): 

2630 self.auto_scale_xyz(*col._vec[:-1], had_data=had_data) 

2631 elif isinstance(col, art3d.Patch3DCollection): 

2632 pass 

2633 # FIXME: Implement auto-scaling function for Patch3DCollection 

2634 # Currently unable to do so due to issues with Patch3DCollection 

2635 # See https://github.com/matplotlib/matplotlib/issues/14298 for details 

2636 

2637 collection = super().add_collection(col) 

2638 return collection 

2639 

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

2641 "edgecolors", "c", "facecolor", 

2642 "facecolors", "color"]) 

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

2644 *args, **kwargs): 

2645 """ 

2646 Create a scatter plot. 

2647 

2648 Parameters 

2649 ---------- 

2650 xs, ys : array-like 

2651 The data positions. 

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

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

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

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

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

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

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

2659 

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

2661 

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

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

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

2665 size. 

2666 c : :mpltype:`color`, sequence, or sequence of colors, optional 

2667 The marker color. Possible values: 

2668 

2669 - A single color format string. 

2670 - A sequence of colors of length n. 

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

2672 *norm*. 

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

2674 

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

2676 depthshade : bool, default: True 

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

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

2679 independently. 

2680 data : indexable object, optional 

2681 DATA_PARAMETER_PLACEHOLDER 

2682 **kwargs 

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

2684 

2685 Returns 

2686 ------- 

2687 paths : `~matplotlib.collections.PathCollection` 

2688 """ 

2689 

2690 had_data = self.has_data() 

2691 zs_orig = zs 

2692 

2693 xs, ys, zs = cbook._broadcast_with_masks(xs, ys, zs) 

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

2695 

2696 xs, ys, zs, s, c, color = cbook.delete_masked_points( 

2697 xs, ys, zs, s, c, kwargs.get('color', None) 

2698 ) 

2699 if kwargs.get("color") is not None: 

2700 kwargs['color'] = color 

2701 

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

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

2704 zs = zs.copy() 

2705 

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

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

2708 depthshade=depthshade) 

2709 

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

2711 self.set_zmargin(0.05) 

2712 

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

2714 

2715 return patches 

2716 

2717 scatter3D = scatter 

2718 

2719 @_preprocess_data() 

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

2721 """ 

2722 Add 2D bar(s). 

2723 

2724 Parameters 

2725 ---------- 

2726 left : 1D array-like 

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

2728 height : 1D array-like 

2729 The height of the bars. 

2730 zs : float or 1D array-like, default: 0 

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

2732 used for all bars. 

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

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

2735 data : indexable object, optional 

2736 DATA_PARAMETER_PLACEHOLDER 

2737 **kwargs 

2738 Other keyword arguments are forwarded to 

2739 `matplotlib.axes.Axes.bar`. 

2740 

2741 Returns 

2742 ------- 

2743 mpl_toolkits.mplot3d.art3d.Patch3DCollection 

2744 """ 

2745 had_data = self.has_data() 

2746 

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

2748 

2749 zs = np.broadcast_to(zs, len(left), subok=True) 

2750 

2751 verts = [] 

2752 verts_zs = [] 

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

2754 vs = art3d._get_patch_verts(p) 

2755 verts += vs.tolist() 

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

2757 art3d.patch_2d_to_3d(p, z, zdir) 

2758 if 'alpha' in kwargs: 

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

2760 

2761 if len(verts) > 0: 

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

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

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

2765 xs, ys = zip(*verts) 

2766 else: 

2767 xs, ys = [], [] 

2768 

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

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

2771 

2772 return patches 

2773 

2774 @_preprocess_data() 

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

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

2777 """ 

2778 Generate a 3D barplot. 

2779 

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

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

2782 

2783 Parameters 

2784 ---------- 

2785 x, y, z : array-like 

2786 The coordinates of the anchor point of the bars. 

2787 

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

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

2790 

2791 color : sequence of colors, optional 

2792 The color of the bars can be specified globally or 

2793 individually. This parameter can be: 

2794 

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

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

2797 independently. 

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

2799 bars similarly. 

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

2801 independently. 

2802 

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

2804 the order of the coloring: 

2805 

2806 1. -Z (bottom of box) 

2807 2. +Z (top of box) 

2808 3. -Y 

2809 4. +Y 

2810 5. -X 

2811 6. +X 

2812 

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

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

2815 

2816 shade : bool, default: True 

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

2818 to the plot's source of light). 

2819 

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

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

2822 

2823 data : indexable object, optional 

2824 DATA_PARAMETER_PLACEHOLDER 

2825 

2826 **kwargs 

2827 Any additional keyword arguments are passed onto 

2828 `~.art3d.Poly3DCollection`. 

2829 

2830 Returns 

2831 ------- 

2832 collection : `~.art3d.Poly3DCollection` 

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

2834 """ 

2835 

2836 had_data = self.has_data() 

2837 

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

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

2840 minx = np.min(x) 

2841 maxx = np.max(x + dx) 

2842 miny = np.min(y) 

2843 maxy = np.max(y + dy) 

2844 minz = np.min(z) 

2845 maxz = np.max(z + dz) 

2846 

2847 # shape (6, 4, 3) 

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

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

2850 cuboid = np.array([ 

2851 # -z 

2852 ( 

2853 (0, 0, 0), 

2854 (0, 1, 0), 

2855 (1, 1, 0), 

2856 (1, 0, 0), 

2857 ), 

2858 # +z 

2859 ( 

2860 (0, 0, 1), 

2861 (1, 0, 1), 

2862 (1, 1, 1), 

2863 (0, 1, 1), 

2864 ), 

2865 # -y 

2866 ( 

2867 (0, 0, 0), 

2868 (1, 0, 0), 

2869 (1, 0, 1), 

2870 (0, 0, 1), 

2871 ), 

2872 # +y 

2873 ( 

2874 (0, 1, 0), 

2875 (0, 1, 1), 

2876 (1, 1, 1), 

2877 (1, 1, 0), 

2878 ), 

2879 # -x 

2880 ( 

2881 (0, 0, 0), 

2882 (0, 0, 1), 

2883 (0, 1, 1), 

2884 (0, 1, 0), 

2885 ), 

2886 # +x 

2887 ( 

2888 (1, 0, 0), 

2889 (1, 1, 0), 

2890 (1, 1, 1), 

2891 (1, 0, 1), 

2892 ), 

2893 ]) 

2894 

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

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

2897 

2898 # handle each coordinate separately 

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

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

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

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

2903 

2904 # collapse the first two axes 

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

2906 

2907 facecolors = [] 

2908 if color is None: 

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

2910 

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

2912 

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

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

2915 for c in color: 

2916 facecolors.extend([c] * 6) 

2917 else: 

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

2919 facecolors = color 

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

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

2922 

2923 col = art3d.Poly3DCollection(polys, 

2924 zsort=zsort, 

2925 facecolors=facecolors, 

2926 shade=shade, 

2927 lightsource=lightsource, 

2928 *args, **kwargs) 

2929 self.add_collection(col) 

2930 

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

2932 

2933 return col 

2934 

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

2936 # docstring inherited 

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

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

2939 self.title.set_y(0.92 * y) 

2940 return ret 

2941 

2942 @_preprocess_data() 

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

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

2945 **kwargs): 

2946 """ 

2947 Plot a 3D field of arrows. 

2948 

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

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

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

2952 element will not be plotted. 

2953 

2954 Parameters 

2955 ---------- 

2956 X, Y, Z : array-like 

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

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

2959 

2960 U, V, W : array-like 

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

2962 

2963 length : float, default: 1 

2964 The length of each quiver. 

2965 

2966 arrow_length_ratio : float, default: 0.3 

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

2968 

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

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

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

2972 

2973 normalize : bool, default: False 

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

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

2976 

2977 data : indexable object, optional 

2978 DATA_PARAMETER_PLACEHOLDER 

2979 

2980 **kwargs 

2981 Any additional keyword arguments are delegated to 

2982 :class:`.Line3DCollection` 

2983 """ 

2984 

2985 def calc_arrows(UVW): 

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

2987 x = UVW[:, 0] 

2988 y = UVW[:, 1] 

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

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

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

2992 # compute the two arrowhead direction unit vectors 

2993 rangle = math.radians(15) 

2994 c = math.cos(rangle) 

2995 s = math.sin(rangle) 

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

2997 r13 = y_p * s 

2998 r32 = x_p * s 

2999 r12 = x_p * y_p * (1 - c) 

3000 Rpos = np.array( 

3001 [[c + (x_p ** 2) * (1 - c), r12, r13], 

3002 [r12, c + (y_p ** 2) * (1 - c), -r32], 

3003 [-r13, r32, np.full_like(x_p, c)]]) 

3004 # opposite rotation negates all the sin terms 

3005 Rneg = Rpos.copy() 

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

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

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

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

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

3011 return np.stack([Rpos_vecs, Rneg_vecs], axis=1) 

3012 

3013 had_data = self.has_data() 

3014 

3015 input_args = cbook._broadcast_with_masks(X, Y, Z, U, V, W, 

3016 compress=True) 

3017 

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

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

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

3021 self.add_collection(linec) 

3022 return linec 

3023 

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

3025 arrow_dt = shaft_dt * arrow_length_ratio 

3026 

3027 _api.check_in_list(['tail', 'middle', 'tip'], pivot=pivot) 

3028 if pivot == 'tail': 

3029 shaft_dt -= length 

3030 elif pivot == 'middle': 

3031 shaft_dt -= length / 2 

3032 

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

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

3035 

3036 # Normalize rows of UVW 

3037 if normalize: 

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

3039 norm[norm == 0] = 1 

3040 UVW = UVW / norm.reshape((-1, 1)) 

3041 

3042 if len(XYZ) > 0: 

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

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

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

3046 head_dirs = calc_arrows(UVW) 

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

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

3049 # stack left and right head lines together 

3050 heads = heads.reshape((len(arrow_dt), -1, 3)) 

3051 # transpose to get a list of lines 

3052 heads = heads.swapaxes(0, 1) 

3053 

3054 lines = [*shafts, *heads[::2], *heads[1::2]] 

3055 else: 

3056 lines = [] 

3057 

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

3059 self.add_collection(linec) 

3060 

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

3062 

3063 return linec 

3064 

3065 quiver3D = quiver 

3066 

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

3068 lightsource=None, **kwargs): 

3069 """ 

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

3071**kwargs) 

3072 

3073 Plot a set of filled voxels 

3074 

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

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

3077 Occluded faces are not plotted. 

3078 

3079 Parameters 

3080 ---------- 

3081 filled : 3D np.array of bool 

3082 A 3D array of values, with truthy values indicating which voxels 

3083 to fill 

3084 

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

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

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

3088 *filled*. These can be used to plot non-cubic voxels. 

3089 

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

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

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

3093 arguments can only be passed positionally. 

3094 

3095 facecolors, edgecolors : array-like, optional 

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

3097 passed as keyword arguments. 

3098 These parameters can be: 

3099 

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

3101 can be either a string, or a 1D RGB/RGBA array 

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

3103 the style default for the edges. 

3104 - A 3D `~numpy.ndarray` of color names, with each item the color 

3105 for the corresponding voxel. The size must match the voxels. 

3106 - A 4D `~numpy.ndarray` of RGB/RGBA data, with the components 

3107 along the last axis. 

3108 

3109 shade : bool, default: True 

3110 Whether to shade the facecolors. 

3111 

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

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

3114 

3115 **kwargs 

3116 Additional keyword arguments to pass onto 

3117 `~mpl_toolkits.mplot3d.art3d.Poly3DCollection`. 

3118 

3119 Returns 

3120 ------- 

3121 faces : dict 

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

3123 `.Poly3DCollection` of the faces drawn for the voxel 

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

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

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

3127 

3128 Examples 

3129 -------- 

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

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

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

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

3134 """ 

3135 

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

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

3138 if len(args) >= 3: 

3139 # underscores indicate position only 

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

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

3142 else: 

3143 def voxels(filled, **kwargs): 

3144 return None, filled, kwargs 

3145 

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

3147 

3148 # check dimensions 

3149 if filled.ndim != 3: 

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

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

3152 

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

3154 coord_shape = tuple(size + 1) 

3155 if xyz is None: 

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

3157 else: 

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

3159 

3160 def _broadcast_color_arg(color, name): 

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

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

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

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

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

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

3167 raise ValueError( 

3168 f"When multidimensional, {name} must match the shape " 

3169 "of filled") 

3170 return color 

3171 else: 

3172 raise ValueError(f"Invalid {name} argument") 

3173 

3174 # broadcast and default on facecolors 

3175 if facecolors is None: 

3176 facecolors = self._get_patches_for_fill.get_next_color() 

3177 facecolors = _broadcast_color_arg(facecolors, 'facecolors') 

3178 

3179 # broadcast but no default on edgecolors 

3180 edgecolors = _broadcast_color_arg(edgecolors, 'edgecolors') 

3181 

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

3183 self.auto_scale_xyz(x, y, z) 

3184 

3185 # points lying on corners of a square 

3186 square = np.array([ 

3187 [0, 0, 0], 

3188 [1, 0, 0], 

3189 [1, 1, 0], 

3190 [0, 1, 0], 

3191 ], dtype=np.intp) 

3192 

3193 voxel_faces = defaultdict(list) 

3194 

3195 def permutation_matrices(n): 

3196 """Generate cyclic permutation matrices.""" 

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

3198 for i in range(n): 

3199 yield mat 

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

3201 

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

3203 # to render 

3204 for permute in permutation_matrices(3): 

3205 # find the set of ranges to iterate over 

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

3207 pinds = np.arange(pc) 

3208 qinds = np.arange(qc) 

3209 rinds = np.arange(rc) 

3210 

3211 square_rot_pos = square.dot(permute.T) 

3212 square_rot_neg = square_rot_pos[::-1] 

3213 

3214 # iterate within the current plane 

3215 for p in pinds: 

3216 for q in qinds: 

3217 # iterate perpendicularly to the current plane, handling 

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

3219 # empty space, to avoid drawing internal faces. 

3220 

3221 # draw lower faces 

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

3223 i0 = tuple(p0) 

3224 if filled[i0]: 

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

3226 

3227 # draw middle faces 

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

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

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

3231 

3232 i1 = tuple(p1) 

3233 i2 = tuple(p2) 

3234 

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

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

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

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

3239 

3240 # draw upper faces 

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

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

3243 ik = tuple(pk) 

3244 if filled[ik]: 

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

3246 

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

3248 # voxel 

3249 polygons = {} 

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

3251 # convert indices into 3D positions 

3252 if xyz is None: 

3253 faces = faces_inds 

3254 else: 

3255 faces = [] 

3256 for face_inds in faces_inds: 

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

3258 face = np.empty(face_inds.shape) 

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

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

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

3262 faces.append(face) 

3263 

3264 # shade the faces 

3265 facecolor = facecolors[coord] 

3266 edgecolor = edgecolors[coord] 

3267 

3268 poly = art3d.Poly3DCollection( 

3269 faces, facecolors=facecolor, edgecolors=edgecolor, 

3270 shade=shade, lightsource=lightsource, **kwargs) 

3271 self.add_collection3d(poly) 

3272 polygons[coord] = poly 

3273 

3274 return polygons 

3275 

3276 @_preprocess_data(replace_names=["x", "y", "z", "xerr", "yerr", "zerr"]) 

3277 def errorbar(self, x, y, z, zerr=None, yerr=None, xerr=None, fmt='', 

3278 barsabove=False, errorevery=1, ecolor=None, elinewidth=None, 

3279 capsize=None, capthick=None, xlolims=False, xuplims=False, 

3280 ylolims=False, yuplims=False, zlolims=False, zuplims=False, 

3281 **kwargs): 

3282 """ 

3283 Plot lines and/or markers with errorbars around them. 

3284 

3285 *x*/*y*/*z* define the data locations, and *xerr*/*yerr*/*zerr* define 

3286 the errorbar sizes. By default, this draws the data markers/lines as 

3287 well the errorbars. Use fmt='none' to draw errorbars only. 

3288 

3289 Parameters 

3290 ---------- 

3291 x, y, z : float or array-like 

3292 The data positions. 

3293 

3294 xerr, yerr, zerr : float or array-like, shape (N,) or (2, N), optional 

3295 The errorbar sizes: 

3296 

3297 - scalar: Symmetric +/- values for all data points. 

3298 - shape(N,): Symmetric +/-values for each data point. 

3299 - shape(2, N): Separate - and + values for each bar. First row 

3300 contains the lower errors, the second row contains the upper 

3301 errors. 

3302 - *None*: No errorbar. 

3303 

3304 Note that all error arrays should have *positive* values. 

3305 

3306 fmt : str, default: '' 

3307 The format for the data points / data lines. See `.plot` for 

3308 details. 

3309 

3310 Use 'none' (case-insensitive) to plot errorbars without any data 

3311 markers. 

3312 

3313 ecolor : :mpltype:`color`, default: None 

3314 The color of the errorbar lines. If None, use the color of the 

3315 line connecting the markers. 

3316 

3317 elinewidth : float, default: None 

3318 The linewidth of the errorbar lines. If None, the linewidth of 

3319 the current style is used. 

3320 

3321 capsize : float, default: :rc:`errorbar.capsize` 

3322 The length of the error bar caps in points. 

3323 

3324 capthick : float, default: None 

3325 An alias to the keyword argument *markeredgewidth* (a.k.a. *mew*). 

3326 This setting is a more sensible name for the property that 

3327 controls the thickness of the error bar cap in points. For 

3328 backwards compatibility, if *mew* or *markeredgewidth* are given, 

3329 then they will over-ride *capthick*. This may change in future 

3330 releases. 

3331 

3332 barsabove : bool, default: False 

3333 If True, will plot the errorbars above the plot 

3334 symbols. Default is below. 

3335 

3336 xlolims, ylolims, zlolims : bool, default: False 

3337 These arguments can be used to indicate that a value gives only 

3338 lower limits. In that case a caret symbol is used to indicate 

3339 this. *lims*-arguments may be scalars, or array-likes of the same 

3340 length as the errors. To use limits with inverted axes, 

3341 `~.set_xlim`, `~.set_ylim`, or `~.set_zlim` must be 

3342 called before `errorbar`. Note the tricky parameter names: setting 

3343 e.g. *ylolims* to True means that the y-value is a *lower* limit of 

3344 the True value, so, only an *upward*-pointing arrow will be drawn! 

3345 

3346 xuplims, yuplims, zuplims : bool, default: False 

3347 Same as above, but for controlling the upper limits. 

3348 

3349 errorevery : int or (int, int), default: 1 

3350 draws error bars on a subset of the data. *errorevery* =N draws 

3351 error bars on the points (x[::N], y[::N], z[::N]). 

3352 *errorevery* =(start, N) draws error bars on the points 

3353 (x[start::N], y[start::N], z[start::N]). e.g. *errorevery* =(6, 3) 

3354 adds error bars to the data at (x[6], x[9], x[12], x[15], ...). 

3355 Used to avoid overlapping error bars when two series share x-axis 

3356 values. 

3357 

3358 Returns 

3359 ------- 

3360 errlines : list 

3361 List of `~mpl_toolkits.mplot3d.art3d.Line3DCollection` instances 

3362 each containing an errorbar line. 

3363 caplines : list 

3364 List of `~mpl_toolkits.mplot3d.art3d.Line3D` instances each 

3365 containing a capline object. 

3366 limmarks : list 

3367 List of `~mpl_toolkits.mplot3d.art3d.Line3D` instances each 

3368 containing a marker with an upper or lower limit. 

3369 

3370 Other Parameters 

3371 ---------------- 

3372 data : indexable object, optional 

3373 DATA_PARAMETER_PLACEHOLDER 

3374 

3375 **kwargs 

3376 All other keyword arguments for styling errorbar lines are passed 

3377 `~mpl_toolkits.mplot3d.art3d.Line3DCollection`. 

3378 

3379 Examples 

3380 -------- 

3381 .. plot:: gallery/mplot3d/errorbar3d.py 

3382 """ 

3383 had_data = self.has_data() 

3384 

3385 kwargs = cbook.normalize_kwargs(kwargs, mlines.Line2D) 

3386 # Drop anything that comes in as None to use the default instead. 

3387 kwargs = {k: v for k, v in kwargs.items() if v is not None} 

3388 kwargs.setdefault('zorder', 2) 

3389 

3390 self._process_unit_info([("x", x), ("y", y), ("z", z)], kwargs, 

3391 convert=False) 

3392 

3393 # make sure all the args are iterable; use lists not arrays to 

3394 # preserve units 

3395 x = x if np.iterable(x) else [x] 

3396 y = y if np.iterable(y) else [y] 

3397 z = z if np.iterable(z) else [z] 

3398 

3399 if not len(x) == len(y) == len(z): 

3400 raise ValueError("'x', 'y', and 'z' must have the same size") 

3401 

3402 everymask = self._errorevery_to_mask(x, errorevery) 

3403 

3404 label = kwargs.pop("label", None) 

3405 kwargs['label'] = '_nolegend_' 

3406 

3407 # Create the main line and determine overall kwargs for child artists. 

3408 # We avoid calling self.plot() directly, or self._get_lines(), because 

3409 # that would call self._process_unit_info again, and do other indirect 

3410 # data processing. 

3411 (data_line, base_style), = self._get_lines._plot_args( 

3412 self, (x, y) if fmt == '' else (x, y, fmt), kwargs, return_kwargs=True) 

3413 art3d.line_2d_to_3d(data_line, zs=z) 

3414 

3415 # Do this after creating `data_line` to avoid modifying `base_style`. 

3416 if barsabove: 

3417 data_line.set_zorder(kwargs['zorder'] - .1) 

3418 else: 

3419 data_line.set_zorder(kwargs['zorder'] + .1) 

3420 

3421 # Add line to plot, or throw it away and use it to determine kwargs. 

3422 if fmt.lower() != 'none': 

3423 self.add_line(data_line) 

3424 else: 

3425 data_line = None 

3426 # Remove alpha=0 color that _process_plot_format returns. 

3427 base_style.pop('color') 

3428 

3429 if 'color' not in base_style: 

3430 base_style['color'] = 'C0' 

3431 if ecolor is None: 

3432 ecolor = base_style['color'] 

3433 

3434 # Eject any line-specific information from format string, as it's not 

3435 # needed for bars or caps. 

3436 for key in ['marker', 'markersize', 'markerfacecolor', 

3437 'markeredgewidth', 'markeredgecolor', 'markevery', 

3438 'linestyle', 'fillstyle', 'drawstyle', 'dash_capstyle', 

3439 'dash_joinstyle', 'solid_capstyle', 'solid_joinstyle']: 

3440 base_style.pop(key, None) 

3441 

3442 # Make the style dict for the line collections (the bars). 

3443 eb_lines_style = {**base_style, 'color': ecolor} 

3444 

3445 if elinewidth: 

3446 eb_lines_style['linewidth'] = elinewidth 

3447 elif 'linewidth' in kwargs: 

3448 eb_lines_style['linewidth'] = kwargs['linewidth'] 

3449 

3450 for key in ('transform', 'alpha', 'zorder', 'rasterized'): 

3451 if key in kwargs: 

3452 eb_lines_style[key] = kwargs[key] 

3453 

3454 # Make the style dict for caps (the "hats"). 

3455 eb_cap_style = {**base_style, 'linestyle': 'None'} 

3456 if capsize is None: 

3457 capsize = mpl.rcParams["errorbar.capsize"] 

3458 if capsize > 0: 

3459 eb_cap_style['markersize'] = 2. * capsize 

3460 if capthick is not None: 

3461 eb_cap_style['markeredgewidth'] = capthick 

3462 eb_cap_style['color'] = ecolor 

3463 

3464 def _apply_mask(arrays, mask): 

3465 # Return, for each array in *arrays*, the elements for which *mask* 

3466 # is True, without using fancy indexing. 

3467 return [[*itertools.compress(array, mask)] for array in arrays] 

3468 

3469 def _extract_errs(err, data, lomask, himask): 

3470 # For separate +/- error values we need to unpack err 

3471 if len(err.shape) == 2: 

3472 low_err, high_err = err 

3473 else: 

3474 low_err, high_err = err, err 

3475 

3476 lows = np.where(lomask | ~everymask, data, data - low_err) 

3477 highs = np.where(himask | ~everymask, data, data + high_err) 

3478 

3479 return lows, highs 

3480 

3481 # collect drawn items while looping over the three coordinates 

3482 errlines, caplines, limmarks = [], [], [] 

3483 

3484 # list of endpoint coordinates, used for auto-scaling 

3485 coorderrs = [] 

3486 

3487 # define the markers used for errorbar caps and limits below 

3488 # the dictionary key is mapped by the `i_xyz` helper dictionary 

3489 capmarker = {0: '|', 1: '|', 2: '_'} 

3490 i_xyz = {'x': 0, 'y': 1, 'z': 2} 

3491 

3492 # Calculate marker size from points to quiver length. Because these are 

3493 # not markers, and 3D Axes do not use the normal transform stack, this 

3494 # is a bit involved. Since the quiver arrows will change size as the 

3495 # scene is rotated, they are given a standard size based on viewing 

3496 # them directly in planar form. 

3497 quiversize = eb_cap_style.get('markersize', 

3498 mpl.rcParams['lines.markersize']) ** 2 

3499 quiversize *= self.figure.dpi / 72 

3500 quiversize = self.transAxes.inverted().transform([ 

3501 (0, 0), (quiversize, quiversize)]) 

3502 quiversize = np.mean(np.diff(quiversize, axis=0)) 

3503 # quiversize is now in Axes coordinates, and to convert back to data 

3504 # coordinates, we need to run it through the inverse 3D transform. For 

3505 # consistency, this uses a fixed elevation, azimuth, and roll. 

3506 with cbook._setattr_cm(self, elev=0, azim=0, roll=0): 

3507 invM = np.linalg.inv(self.get_proj()) 

3508 # elev=azim=roll=0 produces the Y-Z plane, so quiversize in 2D 'x' is 

3509 # 'y' in 3D, hence the 1 index. 

3510 quiversize = np.dot(invM, [quiversize, 0, 0, 0])[1] 

3511 # Quivers use a fixed 15-degree arrow head, so scale up the length so 

3512 # that the size corresponds to the base. In other words, this constant 

3513 # corresponds to the equation tan(15) = (base / 2) / (arrow length). 

3514 quiversize *= 1.8660254037844388 

3515 eb_quiver_style = {**eb_cap_style, 

3516 'length': quiversize, 'arrow_length_ratio': 1} 

3517 eb_quiver_style.pop('markersize', None) 

3518 

3519 # loop over x-, y-, and z-direction and draw relevant elements 

3520 for zdir, data, err, lolims, uplims in zip( 

3521 ['x', 'y', 'z'], [x, y, z], [xerr, yerr, zerr], 

3522 [xlolims, ylolims, zlolims], [xuplims, yuplims, zuplims]): 

3523 

3524 dir_vector = art3d.get_dir_vector(zdir) 

3525 i_zdir = i_xyz[zdir] 

3526 

3527 if err is None: 

3528 continue 

3529 

3530 if not np.iterable(err): 

3531 err = [err] * len(data) 

3532 

3533 err = np.atleast_1d(err) 

3534 

3535 # arrays fine here, they are booleans and hence not units 

3536 lolims = np.broadcast_to(lolims, len(data)).astype(bool) 

3537 uplims = np.broadcast_to(uplims, len(data)).astype(bool) 

3538 

3539 # a nested list structure that expands to (xl,xh),(yl,yh),(zl,zh), 

3540 # where x/y/z and l/h correspond to dimensions and low/high 

3541 # positions of errorbars in a dimension we're looping over 

3542 coorderr = [ 

3543 _extract_errs(err * dir_vector[i], coord, lolims, uplims) 

3544 for i, coord in enumerate([x, y, z])] 

3545 (xl, xh), (yl, yh), (zl, zh) = coorderr 

3546 

3547 # draws capmarkers - flat caps orthogonal to the error bars 

3548 nolims = ~(lolims | uplims) 

3549 if nolims.any() and capsize > 0: 

3550 lo_caps_xyz = _apply_mask([xl, yl, zl], nolims & everymask) 

3551 hi_caps_xyz = _apply_mask([xh, yh, zh], nolims & everymask) 

3552 

3553 # setting '_' for z-caps and '|' for x- and y-caps; 

3554 # these markers will rotate as the viewing angle changes 

3555 cap_lo = art3d.Line3D(*lo_caps_xyz, ls='', 

3556 marker=capmarker[i_zdir], 

3557 **eb_cap_style) 

3558 cap_hi = art3d.Line3D(*hi_caps_xyz, ls='', 

3559 marker=capmarker[i_zdir], 

3560 **eb_cap_style) 

3561 self.add_line(cap_lo) 

3562 self.add_line(cap_hi) 

3563 caplines.append(cap_lo) 

3564 caplines.append(cap_hi) 

3565 

3566 if lolims.any(): 

3567 xh0, yh0, zh0 = _apply_mask([xh, yh, zh], lolims & everymask) 

3568 self.quiver(xh0, yh0, zh0, *dir_vector, **eb_quiver_style) 

3569 if uplims.any(): 

3570 xl0, yl0, zl0 = _apply_mask([xl, yl, zl], uplims & everymask) 

3571 self.quiver(xl0, yl0, zl0, *-dir_vector, **eb_quiver_style) 

3572 

3573 errline = art3d.Line3DCollection(np.array(coorderr).T, 

3574 **eb_lines_style) 

3575 self.add_collection(errline) 

3576 errlines.append(errline) 

3577 coorderrs.append(coorderr) 

3578 

3579 coorderrs = np.array(coorderrs) 

3580 

3581 def _digout_minmax(err_arr, coord_label): 

3582 return (np.nanmin(err_arr[:, i_xyz[coord_label], :, :]), 

3583 np.nanmax(err_arr[:, i_xyz[coord_label], :, :])) 

3584 

3585 minx, maxx = _digout_minmax(coorderrs, 'x') 

3586 miny, maxy = _digout_minmax(coorderrs, 'y') 

3587 minz, maxz = _digout_minmax(coorderrs, 'z') 

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

3589 

3590 # Adapting errorbar containers for 3d case, assuming z-axis points "up" 

3591 errorbar_container = mcontainer.ErrorbarContainer( 

3592 (data_line, tuple(caplines), tuple(errlines)), 

3593 has_xerr=(xerr is not None or yerr is not None), 

3594 has_yerr=(zerr is not None), 

3595 label=label) 

3596 self.containers.append(errorbar_container) 

3597 

3598 return errlines, caplines, limmarks 

3599 

3600 @_api.make_keyword_only("3.8", "call_axes_locator") 

3601 def get_tightbbox(self, renderer=None, call_axes_locator=True, 

3602 bbox_extra_artists=None, *, for_layout_only=False): 

3603 ret = super().get_tightbbox(renderer, 

3604 call_axes_locator=call_axes_locator, 

3605 bbox_extra_artists=bbox_extra_artists, 

3606 for_layout_only=for_layout_only) 

3607 batch = [ret] 

3608 if self._axis3don: 

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

3610 if axis.get_visible(): 

3611 axis_bb = martist._get_tightbbox_for_layout_only( 

3612 axis, renderer) 

3613 if axis_bb: 

3614 batch.append(axis_bb) 

3615 return mtransforms.Bbox.union(batch) 

3616 

3617 @_preprocess_data() 

3618 def stem(self, x, y, z, *, linefmt='C0-', markerfmt='C0o', basefmt='C3-', 

3619 bottom=0, label=None, orientation='z'): 

3620 """ 

3621 Create a 3D stem plot. 

3622 

3623 A stem plot draws lines perpendicular to a baseline, and places markers 

3624 at the heads. By default, the baseline is defined by *x* and *y*, and 

3625 stems are drawn vertically from *bottom* to *z*. 

3626 

3627 Parameters 

3628 ---------- 

3629 x, y, z : array-like 

3630 The positions of the heads of the stems. The stems are drawn along 

3631 the *orientation*-direction from the baseline at *bottom* (in the 

3632 *orientation*-coordinate) to the heads. By default, the *x* and *y* 

3633 positions are used for the baseline and *z* for the head position, 

3634 but this can be changed by *orientation*. 

3635 

3636 linefmt : str, default: 'C0-' 

3637 A string defining the properties of the vertical lines. Usually, 

3638 this will be a color or a color and a linestyle: 

3639 

3640 ========= ============= 

3641 Character Line Style 

3642 ========= ============= 

3643 ``'-'`` solid line 

3644 ``'--'`` dashed line 

3645 ``'-.'`` dash-dot line 

3646 ``':'`` dotted line 

3647 ========= ============= 

3648 

3649 Note: While it is technically possible to specify valid formats 

3650 other than color or color and linestyle (e.g. 'rx' or '-.'), this 

3651 is beyond the intention of the method and will most likely not 

3652 result in a reasonable plot. 

3653 

3654 markerfmt : str, default: 'C0o' 

3655 A string defining the properties of the markers at the stem heads. 

3656 

3657 basefmt : str, default: 'C3-' 

3658 A format string defining the properties of the baseline. 

3659 

3660 bottom : float, default: 0 

3661 The position of the baseline, in *orientation*-coordinates. 

3662 

3663 label : str, optional 

3664 The label to use for the stems in legends. 

3665 

3666 orientation : {'x', 'y', 'z'}, default: 'z' 

3667 The direction along which stems are drawn. 

3668 

3669 data : indexable object, optional 

3670 DATA_PARAMETER_PLACEHOLDER 

3671 

3672 Returns 

3673 ------- 

3674 `.StemContainer` 

3675 The container may be treated like a tuple 

3676 (*markerline*, *stemlines*, *baseline*) 

3677 

3678 Examples 

3679 -------- 

3680 .. plot:: gallery/mplot3d/stem3d_demo.py 

3681 """ 

3682 

3683 from matplotlib.container import StemContainer 

3684 

3685 had_data = self.has_data() 

3686 

3687 _api.check_in_list(['x', 'y', 'z'], orientation=orientation) 

3688 

3689 xlim = (np.min(x), np.max(x)) 

3690 ylim = (np.min(y), np.max(y)) 

3691 zlim = (np.min(z), np.max(z)) 

3692 

3693 # Determine the appropriate plane for the baseline and the direction of 

3694 # stemlines based on the value of orientation. 

3695 if orientation == 'x': 

3696 basex, basexlim = y, ylim 

3697 basey, baseylim = z, zlim 

3698 lines = [[(bottom, thisy, thisz), (thisx, thisy, thisz)] 

3699 for thisx, thisy, thisz in zip(x, y, z)] 

3700 elif orientation == 'y': 

3701 basex, basexlim = x, xlim 

3702 basey, baseylim = z, zlim 

3703 lines = [[(thisx, bottom, thisz), (thisx, thisy, thisz)] 

3704 for thisx, thisy, thisz in zip(x, y, z)] 

3705 else: 

3706 basex, basexlim = x, xlim 

3707 basey, baseylim = y, ylim 

3708 lines = [[(thisx, thisy, bottom), (thisx, thisy, thisz)] 

3709 for thisx, thisy, thisz in zip(x, y, z)] 

3710 

3711 # Determine style for stem lines. 

3712 linestyle, linemarker, linecolor = _process_plot_format(linefmt) 

3713 if linestyle is None: 

3714 linestyle = mpl.rcParams['lines.linestyle'] 

3715 

3716 # Plot everything in required order. 

3717 baseline, = self.plot(basex, basey, basefmt, zs=bottom, 

3718 zdir=orientation, label='_nolegend_') 

3719 stemlines = art3d.Line3DCollection( 

3720 lines, linestyles=linestyle, colors=linecolor, label='_nolegend_') 

3721 self.add_collection(stemlines) 

3722 markerline, = self.plot(x, y, z, markerfmt, label='_nolegend_') 

3723 

3724 stem_container = StemContainer((markerline, stemlines, baseline), 

3725 label=label) 

3726 self.add_container(stem_container) 

3727 

3728 jx, jy, jz = art3d.juggle_axes(basexlim, baseylim, [bottom, bottom], 

3729 orientation) 

3730 self.auto_scale_xyz([*jx, *xlim], [*jy, *ylim], [*jz, *zlim], had_data) 

3731 

3732 return stem_container 

3733 

3734 stem3D = stem 

3735 

3736 

3737def get_test_data(delta=0.05): 

3738 """Return a tuple X, Y, Z with a test data set.""" 

3739 x = y = np.arange(-3.0, 3.0, delta) 

3740 X, Y = np.meshgrid(x, y) 

3741 

3742 Z1 = np.exp(-(X**2 + Y**2) / 2) / (2 * np.pi) 

3743 Z2 = (np.exp(-(((X - 1) / 1.5)**2 + ((Y - 1) / 0.5)**2) / 2) / 

3744 (2 * np.pi * 0.5 * 1.5)) 

3745 Z = Z2 - Z1 

3746 

3747 X = X * 10 

3748 Y = Y * 10 

3749 Z = Z * 500 

3750 return X, Y, Z