Skip to content

Commit

Permalink
ENH: use box_aspect to support setting pb_aspect
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
tacaswell committed Jun 4, 2020
1 parent 8c2529a commit 6915f69
Show file tree
Hide file tree
Showing 4 changed files with 91 additions and 14 deletions.
11 changes: 9 additions & 2 deletions doc/users/next_whats_new/2019-03-25-mplot3d-projection.rst
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@
Plots made with :class:`~mpl_toolkits.mplot3d.axes3d.Axes3D` were previously
stretched to fit a square bounding box. As this stretching was done after
the projection from 3D to 2D, it resulted in distorted images if non-square
bounding boxes were used.
bounding boxes were used. As of 3.3, this no longer occurs.

As of this release, this no longer occurs.
Currently modes of setting the aspect (via
`~mpl_toolkits.mplot3d.axes3d.Axes3D.set_aspect`), in data space, are
not supported for Axes3D but maybe in the future. If you want to
simulate having equal aspect in data space, set the ratio of your data
limits to match the value of `~.get_box_aspect`. To control these
ratios use the `~mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect`
method which accepts th ratios at as a 3-tuple of X:Y:Z. The default
aspect ratio is 4:4:3.
61 changes: 49 additions & 12 deletions lib/mpl_toolkits/mplot3d/axes3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ class Axes3D(Axes):
def __init__(
self, fig, rect=None, *args,
azim=-60, elev=30, sharez=None, proj_type='persp',
box_aspect=None,
**kwargs):
"""
Parameters
Expand Down Expand Up @@ -91,11 +92,7 @@ def __init__(
self.zz_viewLim = Bbox.unit()
self.xy_dataLim = Bbox.unit()
self.zz_dataLim = Bbox.unit()
if 'pb_aspect' in kwargs:
self.pb_aspect = np.asarray(kwargs['pb_aspect'])
else:
# chosen for similarity with the previous initial view
self.pb_aspect = np.array([4, 4, 3]) / 3.5

# inhibit autoscale_view until the axes are defined
# they can't be defined until Axes.__init__ has been called
self.view_init(self.initial_elev, self.initial_azim)
Expand All @@ -105,7 +102,9 @@ def __init__(
self._shared_z_axes.join(self, sharez)
self._adjustable = 'datalim'

super().__init__(fig, rect, frameon=True, *args, **kwargs)
super().__init__(
fig, rect, frameon=True, box_aspect=box_aspect, *args, **kwargs
)
# Disable drawing of axes by base class
super().set_axis_off()
# Enable drawing of axes by Axes3D class
Expand Down Expand Up @@ -309,6 +308,9 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
share : bool, default: False
If ``True``, apply the settings to all shared Axes.
See Also
--------
mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect
"""
if aspect != 'auto':
raise NotImplementedError(
Expand Down Expand Up @@ -347,8 +349,43 @@ def set_anchor(self, anchor, share=False):
ax._anchor = anchor
ax.stale = True

def set_pb_aspect(self, pb_aspect, zoom=1):
self.pb_aspect = pb_aspect * 1.8 * zoom / proj3d.mod(pb_aspect)
def set_box_aspect(self, aspect, zoom=1):
"""
Set the axes box aspect.
The box aspect is the ratio of the axes height to the axes width in
physical units. This is not to be confused with the data
aspect, set via `~.Axes.set_aspect`.
Parameters
----------
aspect : 3-tuple of floats on None
Changes the physical dimensions of the Axes, such that the ratio
of the size of the axis in physical units is x:y:z
The input will be normalized to a unit vector.
If None, it is approximately ::
ax.set_box_aspect(aspect=(4, 4, 3), zoom=1)
zoom : float
Control the "zoom" of the
See Also
--------
mpl_toolkits.mplot3d.axes3d.Axes3D.set_aspect
for a description of aspect handling.
"""
if aspect is None:
aspect = np.asarray((4, 4, 3), dtype=float)
else:
aspect = np.asarray(aspect, dtype=float)
# default scale tuned to match the mpl32 appearance.
aspect *= 1.8294640721620434 * zoom / np.linalg.norm(aspect)

self._box_aspect = aspect
self.stale = True

def apply_aspect(self, position=None):
if position is None:
Expand Down Expand Up @@ -426,6 +463,7 @@ def get_axis_position(self):
return xhigh, yhigh, zhigh

def _on_units_changed(self, scalex=False, scaley=False, scalez=False):

"""
Callback for processing changes to axis units.
Expand Down Expand Up @@ -973,8 +1011,6 @@ def set_proj_type(self, proj_type):

def get_proj(self):
"""Create the projection matrix from the current viewing position."""
# chosen for similarity with the initial view before gh-8896

# elev stores the elevation angle in the z plane
# azim stores the azimuth angle in the x,y plane
#
Expand All @@ -990,10 +1026,11 @@ def get_proj(self):
# transform to uniform world coordinates 0-1, 0-1, 0-1
worldM = proj3d.world_transformation(xmin, xmax,
ymin, ymax,
zmin, zmax, pb_aspect=self.pb_aspect)
zmin, zmax,
pb_aspect=self._box_aspect)

# look into the middle of the new coordinates
R = self.pb_aspect / 2
R = self._box_aspect / 2

xp = R[0] + np.cos(razim) * np.cos(relev) * self.dist
yp = R[1] + np.sin(razim) * np.cos(relev) * self.dist
Expand Down
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
33 changes: 33 additions & 0 deletions lib/mpl_toolkits/tests/test_mplot3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -961,3 +961,36 @@ def test_minor_ticks():
ax.set_yticklabels(["third"], minor=True)
ax.set_zticks([0.50], minor=True)
ax.set_zticklabels(["half"], minor=True)


@image_comparison(["equal_box_aspect.png"], style="mpl20")
def test_equal_box_aspect():
from itertools import product, combinations

fig = plt.figure()
ax = fig.add_subplot(111, projection="3d")

# Make data
u = np.linspace(0, 2 * np.pi, 100)
v = np.linspace(0, np.pi, 100)
x = np.outer(np.cos(u), np.sin(v))
y = np.outer(np.sin(u), np.sin(v))
z = np.outer(np.ones(np.size(u)), np.cos(v))

# Plot the surface
ax.plot_surface(x, y, z)

# draw cube
r = [-1, 1]
for s, e in combinations(np.array(list(product(r, r, r))), 2):
if np.sum(np.abs(s - e)) == r[1] - r[0]:
ax.plot3D(*zip(s, e), color="b")

# Make axes limits
xyzlim = np.array([ax.get_xlim3d(), ax.get_ylim3d(), ax.get_zlim3d()]).T
XYZlim = [min(xyzlim[0]), max(xyzlim[1])]
ax.set_xlim3d(XYZlim)
ax.set_ylim3d(XYZlim)
ax.set_zlim3d(XYZlim)
ax.axis('off')
ax.set_box_aspect((1, 1, 1))

0 comments on commit 6915f69

Please sign in to comment.