Skip to content

Commit 7c6a74c

Browse files
authored
Provide adjustable='box' to 3D axes aspect ratio setting (#23552)
* Provided `adjustable='box'` option to set 3D aspect ratio. * "What's New": `adjustable` argument of 3D plots aspect ratio.
1 parent 8179d12 commit 7c6a74c

File tree

4 files changed

+103
-22
lines changed

4 files changed

+103
-22
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
*adjustable* keyword argument for setting equal aspect ratios in 3D
2+
-------------------------------------------------------------------
3+
4+
While setting equal aspect ratios for 3D plots, users can choose to modify
5+
either the data limits or the bounding box.
6+
7+
.. plot::
8+
:include-source: true
9+
10+
import matplotlib.pyplot as plt
11+
import numpy as np
12+
from itertools import combinations, product
13+
14+
aspects = ('auto', 'equal', 'equalxy', 'equalyz', 'equalxz')
15+
fig, axs = plt.subplots(1, len(aspects), subplot_kw={'projection': '3d'},
16+
figsize=(12, 6))
17+
18+
# Draw rectangular cuboid with side lengths [4, 3, 5]
19+
r = [0, 1]
20+
scale = np.array([4, 3, 5])
21+
pts = combinations(np.array(list(product(r, r, r))), 2)
22+
for start, end in pts:
23+
if np.sum(np.abs(start - end)) == r[1] - r[0]:
24+
for ax in axs:
25+
ax.plot3D(*zip(start*scale, end*scale), color='C0')
26+
27+
# Set the aspect ratios
28+
for i, ax in enumerate(axs):
29+
ax.set_aspect(aspects[i], adjustable='datalim')
30+
# Alternatively: ax.set_aspect(aspects[i], adjustable='box')
31+
# which will change the box aspect ratio instead of axis data limits.
32+
ax.set_title(f"set_aspect('{aspects[i]}')")
33+
34+
plt.show()

lib/mpl_toolkits/mplot3d/axes3d.py

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

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

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

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

lib/mpl_toolkits/tests/test_mplot3d.py

+19-1
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)