Skip to content

Fix mplot3d projection #8896

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Feb 7, 2020
9 changes: 9 additions & 0 deletions doc/users/next_whats_new/2019-03-25-mplot3d-projection.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
``Axes3D`` no longer distorts the 3d plot to match the 2d aspect ratio
----------------------------------------------------------------------

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.

As of this release, this no longer occurs.
6 changes: 3 additions & 3 deletions lib/matplotlib/axes/_base.py
Original file line number Diff line number Diff line change
Expand Up @@ -1499,9 +1499,9 @@ def apply_aspect(self, position=None):
"""
Adjust the Axes for a specified data aspect ratio.

Depending on `.get_adjustable` this will modify either the Axes box
(position) or the view limits. In the former case, `.get_anchor`
will affect the position.
Depending on `.get_adjustable` this will modify either the
Axes box (position) or the view limits. In the former case,
`~matplotlib.axes.Axes.get_anchor` will affect the position.

Notes
-----
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
21 changes: 19 additions & 2 deletions lib/mpl_toolkits/mplot3d/axes3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,20 @@ def tunit_edges(self, vals=None, M=None):
(tc[7], tc[4])]
return edges

def apply_aspect(self, position=None):
if position is None:
position = self.get_position(original=True)

# in the superclass, we would go through and actually deal with axis
# scales and box/datalim. Those are all irrelevant - all we need to do
# is make sure our coordinate system is square.
figW, figH = self.get_figure().get_size_inches()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why use the figure's aspect ratio? What if the figure has 3 subplots all in a row?

Copy link
Contributor Author

@eric-wieser eric-wieser Aug 2, 2017

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Because all sizes in matplotlib are relative to the figure. pb.shrunk_to_aspect seems to take the ratio of the top-level parent (the figure), and the desired ratio. This is because the end result is the size relative to the parent, as all objects in matplotlib seem to be sized - so to ensure a given aspect ratio, you need to account for that of the parent

Note that this is the same as the normal aspect=equal code, so is presumably correct.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ping @efiring, as I think you are the resident expert on all the ways this could possibly go wrong.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it makes sense. I have no intention of spending the time required to understand mplot3d well enough to make a detailed review, but in the absence of any such review I am inclined to trust that this PR is a major improvement, as it appears to be fixing a basic design flaw.

fig_aspect = figH / figW
box_aspect = 1
pb = position.frozen()
pb1 = pb.shrunk_to_aspect(box_aspect, pb, fig_aspect)
self.set_position(pb1.anchored(self.get_anchor(), pb), 'active')

@artist.allow_rasterization
def draw(self, renderer):
# draw the background patch
Expand Down Expand Up @@ -966,6 +980,9 @@ def get_proj(self):

dist is the distance of the eye viewing point from the object point.
"""
# chosen for similarity with the initial view before gh-8896
pb_aspect = np.array([4, 4, 3]) / 3.5
Comment on lines +983 to +984
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I imagine that there are going to be some requests for an API to change this, for people who liked their 1:1:1 plot boxes. I started working on that two years ago, but it opened a can of worms that I didn't want to open concurrently with this PR. At some point I'll see if I can rebase those changes.


relev, razim = np.pi * self.elev/180, np.pi * self.azim/180

xmin, xmax = self.get_xlim3d()
Expand All @@ -975,10 +992,10 @@ 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)
zmin, zmax, pb_aspect=pb_aspect)

# look into the middle of the new coordinates
R = np.array([0.5, 0.5, 0.5])
R = pb_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
16 changes: 14 additions & 2 deletions lib/mpl_toolkits/mplot3d/proj3d.py
Original file line number Diff line number Diff line change
Expand Up @@ -86,8 +86,20 @@ def mod(v):

def world_transformation(xmin, xmax,
ymin, ymax,
zmin, zmax):
dx, dy, dz = (xmax-xmin), (ymax-ymin), (zmax-zmin)
zmin, zmax, pb_aspect=None):
"""
produce a matrix that scales homogenous coords in the specified ranges
to [0, 1], or [0, pb_aspect[i]] if the plotbox aspect ratio is specified
"""
dx = xmax - xmin
dy = ymax - ymin
dz = zmax - zmin
if pb_aspect is not None:
ax, ay, az = pb_aspect
dx /= ax
dy /= ay
dz /= az

return np.array([[1/dx, 0, 0, -xmin/dx],
[0, 1/dy, 0, -ymin/dy],
[0, 0, 1/dz, -zmin/dz],
Expand Down
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified lib/mpl_toolkits/tests/baseline_images/test_mplot3d/bar3d.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified lib/mpl_toolkits/tests/baseline_images/test_mplot3d/lines3d.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file modified lib/mpl_toolkits/tests/baseline_images/test_mplot3d/text3d.png
Loading