Skip to content

Commit 6915f69

Browse files
committed
ENH: use box_aspect to support setting pb_aspect
closes #17172 Instead of adding a new method, reuse `box_aspect` expecting a 3 vector. The very long float is to keep the tests passing and preserve the behavior currently on master branch.
1 parent 8c2529a commit 6915f69

File tree

4 files changed

+91
-14
lines changed

4 files changed

+91
-14
lines changed

doc/users/next_whats_new/2019-03-25-mplot3d-projection.rst

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,13 @@
44
Plots made with :class:`~mpl_toolkits.mplot3d.axes3d.Axes3D` were previously
55
stretched to fit a square bounding box. As this stretching was done after
66
the projection from 3D to 2D, it resulted in distorted images if non-square
7-
bounding boxes were used.
7+
bounding boxes were used. As of 3.3, this no longer occurs.
88

9-
As of this release, this no longer occurs.
9+
Currently modes of setting the aspect (via
10+
`~mpl_toolkits.mplot3d.axes3d.Axes3D.set_aspect`), in data space, are
11+
not supported for Axes3D but maybe in the future. If you want to
12+
simulate having equal aspect in data space, set the ratio of your data
13+
limits to match the value of `~.get_box_aspect`. To control these
14+
ratios use the `~mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect`
15+
method which accepts th ratios at as a 3-tuple of X:Y:Z. The default
16+
aspect ratio is 4:4:3.

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 49 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ class Axes3D(Axes):
5353
def __init__(
5454
self, fig, rect=None, *args,
5555
azim=-60, elev=30, sharez=None, proj_type='persp',
56+
box_aspect=None,
5657
**kwargs):
5758
"""
5859
Parameters
@@ -91,11 +92,7 @@ def __init__(
9192
self.zz_viewLim = Bbox.unit()
9293
self.xy_dataLim = Bbox.unit()
9394
self.zz_dataLim = Bbox.unit()
94-
if 'pb_aspect' in kwargs:
95-
self.pb_aspect = np.asarray(kwargs['pb_aspect'])
96-
else:
97-
# chosen for similarity with the previous initial view
98-
self.pb_aspect = np.array([4, 4, 3]) / 3.5
95+
9996
# inhibit autoscale_view until the axes are defined
10097
# they can't be defined until Axes.__init__ has been called
10198
self.view_init(self.initial_elev, self.initial_azim)
@@ -105,7 +102,9 @@ def __init__(
105102
self._shared_z_axes.join(self, sharez)
106103
self._adjustable = 'datalim'
107104

108-
super().__init__(fig, rect, frameon=True, *args, **kwargs)
105+
super().__init__(
106+
fig, rect, frameon=True, box_aspect=box_aspect, *args, **kwargs
107+
)
109108
# Disable drawing of axes by base class
110109
super().set_axis_off()
111110
# Enable drawing of axes by Axes3D class
@@ -309,6 +308,9 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
309308
share : bool, default: False
310309
If ``True``, apply the settings to all shared Axes.
311310
311+
See Also
312+
--------
313+
mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect
312314
"""
313315
if aspect != 'auto':
314316
raise NotImplementedError(
@@ -347,8 +349,43 @@ def set_anchor(self, anchor, share=False):
347349
ax._anchor = anchor
348350
ax.stale = True
349351

350-
def set_pb_aspect(self, pb_aspect, zoom=1):
351-
self.pb_aspect = pb_aspect * 1.8 * zoom / proj3d.mod(pb_aspect)
352+
def set_box_aspect(self, aspect, zoom=1):
353+
"""
354+
Set the axes box aspect.
355+
356+
The box aspect is the ratio of the axes height to the axes width in
357+
physical units. This is not to be confused with the data
358+
aspect, set via `~.Axes.set_aspect`.
359+
360+
Parameters
361+
----------
362+
aspect : 3-tuple of floats on None
363+
Changes the physical dimensions of the Axes, such that the ratio
364+
of the size of the axis in physical units is x:y:z
365+
366+
The input will be normalized to a unit vector.
367+
368+
If None, it is approximately ::
369+
370+
ax.set_box_aspect(aspect=(4, 4, 3), zoom=1)
371+
372+
zoom : float
373+
Control the "zoom" of the
374+
375+
See Also
376+
--------
377+
mpl_toolkits.mplot3d.axes3d.Axes3D.set_aspect
378+
for a description of aspect handling.
379+
"""
380+
if aspect is None:
381+
aspect = np.asarray((4, 4, 3), dtype=float)
382+
else:
383+
aspect = np.asarray(aspect, dtype=float)
384+
# default scale tuned to match the mpl32 appearance.
385+
aspect *= 1.8294640721620434 * zoom / np.linalg.norm(aspect)
386+
387+
self._box_aspect = aspect
388+
self.stale = True
352389

353390
def apply_aspect(self, position=None):
354391
if position is None:
@@ -426,6 +463,7 @@ def get_axis_position(self):
426463
return xhigh, yhigh, zhigh
427464

428465
def _on_units_changed(self, scalex=False, scaley=False, scalez=False):
466+
429467
"""
430468
Callback for processing changes to axis units.
431469
@@ -973,8 +1011,6 @@ def set_proj_type(self, proj_type):
9731011

9741012
def get_proj(self):
9751013
"""Create the projection matrix from the current viewing position."""
976-
# chosen for similarity with the initial view before gh-8896
977-
9781014
# elev stores the elevation angle in the z plane
9791015
# azim stores the azimuth angle in the x,y plane
9801016
#
@@ -990,10 +1026,11 @@ def get_proj(self):
9901026
# transform to uniform world coordinates 0-1, 0-1, 0-1
9911027
worldM = proj3d.world_transformation(xmin, xmax,
9921028
ymin, ymax,
993-
zmin, zmax, pb_aspect=self.pb_aspect)
1029+
zmin, zmax,
1030+
pb_aspect=self._box_aspect)
9941031

9951032
# look into the middle of the new coordinates
996-
R = self.pb_aspect / 2
1033+
R = self._box_aspect / 2
9971034

9981035
xp = R[0] + np.cos(razim) * np.cos(relev) * self.dist
9991036
yp = R[1] + np.sin(razim) * np.cos(relev) * self.dist

lib/mpl_toolkits/tests/test_mplot3d.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -961,3 +961,36 @@ def test_minor_ticks():
961961
ax.set_yticklabels(["third"], minor=True)
962962
ax.set_zticks([0.50], minor=True)
963963
ax.set_zticklabels(["half"], minor=True)
964+
965+
966+
@image_comparison(["equal_box_aspect.png"], style="mpl20")
967+
def test_equal_box_aspect():
968+
from itertools import product, combinations
969+
970+
fig = plt.figure()
971+
ax = fig.add_subplot(111, projection="3d")
972+
973+
# Make data
974+
u = np.linspace(0, 2 * np.pi, 100)
975+
v = np.linspace(0, np.pi, 100)
976+
x = np.outer(np.cos(u), np.sin(v))
977+
y = np.outer(np.sin(u), np.sin(v))
978+
z = np.outer(np.ones(np.size(u)), np.cos(v))
979+
980+
# Plot the surface
981+
ax.plot_surface(x, y, z)
982+
983+
# draw cube
984+
r = [-1, 1]
985+
for s, e in combinations(np.array(list(product(r, r, r))), 2):
986+
if np.sum(np.abs(s - e)) == r[1] - r[0]:
987+
ax.plot3D(*zip(s, e), color="b")
988+
989+
# Make axes limits
990+
xyzlim = np.array([ax.get_xlim3d(), ax.get_ylim3d(), ax.get_zlim3d()]).T
991+
XYZlim = [min(xyzlim[0]), max(xyzlim[1])]
992+
ax.set_xlim3d(XYZlim)
993+
ax.set_ylim3d(XYZlim)
994+
ax.set_zlim3d(XYZlim)
995+
ax.axis('off')
996+
ax.set_box_aspect((1, 1, 1))

0 commit comments

Comments
 (0)