Skip to content

Commit 75e605f

Browse files
committed
Provide adjustable='box' for set_aspect('equal') for 3D axes.
1 parent bf9a451 commit 75e605f

File tree

4 files changed

+76
-25
lines changed

4 files changed

+76
-25
lines changed

doc/users/next_whats_new/3d_plot_aspects.rst

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ Set equal aspect ratio for 3D plots
22
-----------------------------------
33

44
Users can set the aspect ratio for the X, Y, Z axes of a 3D plot to be 'equal',
5-
'equalxy', 'equalxz', or 'equalyz' rather than the default of 'auto'.
5+
'equalxy', 'equalxz', or 'equalyz' rather than the default of 'auto'. Just like
6+
in case of 2D plots, either the data limits or the bounding box can be adjusted
7+
to achieve the desired aspect.
68

79
.. plot::
810
:include-source: true
@@ -26,7 +28,9 @@ Users can set the aspect ratio for the X, Y, Z axes of a 3D plot to be 'equal',
2628
# Set the aspect ratios
2729
for i, ax in enumerate(axs):
2830
ax.set_box_aspect((3, 4, 5))
29-
ax.set_aspect(aspects[i])
30-
ax.set_title("set_aspect('{aspects[i]}')")
31+
ax.set_aspect(aspects[i], adjustable='datalim')
32+
# Alternatively: ax.set_aspect(aspects[i], adjustable='box')
33+
# which will modify the box aspect ratio instead of the data limits.
34+
ax.set_title(f"set_aspect('{aspects[i]}')")
3135

3236
plt.show()

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 50 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -286,9 +286,7 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
286286
'equalyz' adapt the y and z axes to have equal aspect ratios.
287287
========= ==================================================
288288
289-
adjustable : None
290-
Currently ignored by Axes3D
291-
289+
adjustable : None or {'box', 'datalim'}, optional
292290
If not *None*, this defines which parameter will be adjusted to
293291
meet the required aspect. See `.set_adjustable` for further
294292
details.
@@ -319,34 +317,65 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
319317
"""
320318
_api.check_in_list(('auto', 'equal', 'equalxy', 'equalyz', 'equalxz'),
321319
aspect=aspect)
320+
if adjustable is None:
321+
adjustable = self._adjustable
322+
_api.check_in_list(('box', 'datalim'), adjustable=adjustable)
322323
super().set_aspect(
323324
aspect='auto', adjustable=adjustable, anchor=anchor, share=share)
324325
self._aspect = aspect
325326

326327
if aspect in ('equal', 'equalxy', 'equalxz', 'equalyz'):
327-
if aspect == 'equal':
328-
ax_indices = [0, 1, 2]
329-
elif aspect == 'equalxy':
330-
ax_indices = [0, 1]
331-
elif aspect == 'equalxz':
332-
ax_indices = [0, 2]
333-
elif aspect == 'equalyz':
334-
ax_indices = [1, 2]
328+
ax_idx = self._equal_aspect_axis_indices(aspect)
335329

336330
view_intervals = np.array([self.xaxis.get_view_interval(),
337331
self.yaxis.get_view_interval(),
338332
self.zaxis.get_view_interval()])
339-
mean = np.mean(view_intervals, axis=1)
340333
ptp = np.ptp(view_intervals, axis=1)
341-
delta = max(ptp[ax_indices])
342-
scale = self._box_aspect[ptp == delta][0]
343-
deltas = delta * self._box_aspect / scale
344-
345-
for i, set_lim in enumerate((self.set_xlim3d,
346-
self.set_ylim3d,
347-
self.set_zlim3d)):
348-
if i in ax_indices:
349-
set_lim(mean[i] - deltas[i]/2., mean[i] + deltas[i]/2.)
334+
if adjustable == 'datalim':
335+
mean = np.mean(view_intervals, axis=1)
336+
delta = max(ptp[ax_idx])
337+
scale = self._box_aspect[ptp == delta][0]
338+
deltas = delta * self._box_aspect / scale
339+
340+
for i, set_lim in enumerate((self.set_xlim3d,
341+
self.set_ylim3d,
342+
self.set_zlim3d)):
343+
if i in ax_idx:
344+
set_lim(mean[i] - deltas[i]/2., mean[i] + deltas[i]/2.)
345+
else: # 'box'
346+
# Change the box aspect such that the ratio of the length of
347+
# the unmodified axis to the length of the diagonal
348+
# perpendicular to it remains unchanged.
349+
box_aspect = np.array(self._box_aspect)
350+
box_aspect[ax_idx] = ptp[ax_idx]
351+
remaining_ax_idx = {0, 1, 2}.difference(ax_idx)
352+
if remaining_ax_idx:
353+
remaining = remaining_ax_idx.pop()
354+
old_diag = np.linalg.norm(self._box_aspect[ax_idx])
355+
new_diag = np.linalg.norm(box_aspect[ax_idx])
356+
box_aspect[remaining] *= new_diag / old_diag
357+
self.set_box_aspect(box_aspect)
358+
359+
def _equal_aspect_axis_indices(self, aspect):
360+
"""
361+
Get the indices for which of the x, y, z axes are constrained to have
362+
equal aspect ratios.
363+
364+
Parameters
365+
----------
366+
aspect : {'auto', 'equal', 'equalxy', 'equalxz', 'equalyz'}
367+
See descriptions in docstring for `.set_aspect()`.
368+
"""
369+
ax_indices = [] # aspect == 'auto'
370+
if aspect == 'equal':
371+
ax_indices = [0, 1, 2]
372+
elif aspect == 'equalxy':
373+
ax_indices = [0, 1]
374+
elif aspect == 'equalxz':
375+
ax_indices = [0, 2]
376+
elif aspect == 'equalyz':
377+
ax_indices = [1, 2]
378+
return ax_indices
350379

351380
def set_box_aspect(self, aspect, *, zoom=1):
352381
"""

lib/mpl_toolkits/tests/test_mplot3d.py

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,25 @@ def test_aspects():
4444
ax.plot3D(*zip(start*scale, end*scale))
4545
for i, ax in enumerate(axs):
4646
ax.set_box_aspect((3, 4, 5))
47-
ax.set_aspect(aspects[i])
47+
ax.set_aspect(aspects[i], adjustable='datalim')
48+
49+
50+
@mpl3d_image_comparison(['aspects_adjust_box.png'], remove_text=False)
51+
def test_aspects_adjust_box():
52+
aspects = ('auto', 'equal', 'equalxy', 'equalyz', 'equalxz')
53+
fig, axs = plt.subplots(1, len(aspects), subplot_kw={'projection': '3d'},
54+
figsize=(11, 3))
55+
56+
# Draw rectangular cuboid with side lengths [4, 3, 5]
57+
r = [0, 1]
58+
scale = np.array([4, 3, 5])
59+
pts = itertools.combinations(np.array(list(itertools.product(r, r, r))), 2)
60+
for start, end in pts:
61+
if np.sum(np.abs(start - end)) == r[1] - r[0]:
62+
for ax in axs:
63+
ax.plot3D(*zip(start*scale, end*scale))
64+
for i, ax in enumerate(axs):
65+
ax.set_aspect(aspects[i], adjustable='box')
4866

4967

5068
def test_axes3d_repr():

0 commit comments

Comments
 (0)