Matplotlib (等单位长度) : 具有“等”的长宽比 z 轴不等于 x-和 y-

当我为一个三维图形设置一个相等的宽高比时,z-axis不会变成“相等”。所以这个:

fig = pylab.figure()
mesFig = fig.gca(projection='3d', adjustable='box')
mesFig.axis('equal')
mesFig.plot(xC, yC, zC, 'r.')
mesFig.plot(xO, yO, zO, 'b.')
pyplot.show()

给了我以下信息:

img1

显然 z 轴的单位长度不等于 x 和 y。

我怎样才能使所有三个轴的单位长度相等? 我找到的所有解决方案都不起作用。

103312 次浏览

I believe matplotlib does not yet set correctly equal axis in 3D... But I found a trick some times ago (I don't remember where) that I've adapted using it. The concept is to create a fake cubic bounding box around your data. You can test it with the following code:

from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import matplotlib.pyplot as plt
import numpy as np


fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.set_aspect('equal')


X = np.random.rand(100)*10+5
Y = np.random.rand(100)*5+2.5
Z = np.random.rand(100)*50+25


scat = ax.scatter(X, Y, Z)


# Create cubic bounding box to simulate equal aspect ratio
max_range = np.array([X.max()-X.min(), Y.max()-Y.min(), Z.max()-Z.min()]).max()
Xb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][0].flatten() + 0.5*(X.max()+X.min())
Yb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][1].flatten() + 0.5*(Y.max()+Y.min())
Zb = 0.5*max_range*np.mgrid[-1:2:2,-1:2:2,-1:2:2][2].flatten() + 0.5*(Z.max()+Z.min())
# Comment or uncomment following both lines to test the fake bounding box:
for xb, yb, zb in zip(Xb, Yb, Zb):
ax.plot([xb], [yb], [zb], 'w')


plt.grid()
plt.show()

z data are about an order of magnitude larger than x and y, but even with equal axis option, matplotlib autoscale z axis:

bad

But if you add the bounding box, you obtain a correct scaling:

enter image description here

I simplified Remy F's solution by using the set_x/y/zlim functions.

from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import matplotlib.pyplot as plt
import numpy as np


fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.set_aspect('equal')


X = np.random.rand(100)*10+5
Y = np.random.rand(100)*5+2.5
Z = np.random.rand(100)*50+25


scat = ax.scatter(X, Y, Z)


max_range = np.array([X.max()-X.min(), Y.max()-Y.min(), Z.max()-Z.min()]).max() / 2.0


mid_x = (X.max()+X.min()) * 0.5
mid_y = (Y.max()+Y.min()) * 0.5
mid_z = (Z.max()+Z.min()) * 0.5
ax.set_xlim(mid_x - max_range, mid_x + max_range)
ax.set_ylim(mid_y - max_range, mid_y + max_range)
ax.set_zlim(mid_z - max_range, mid_z + max_range)


plt.show()

enter image description here

I like the above solutions, but they do have the drawback that you need to keep track of the ranges and means over all your data. This could be cumbersome if you have multiple data sets that will be plotted together. To fix this, I made use of the ax.get_[xyz]lim3d() methods and put the whole thing into a standalone function that can be called just once before you call plt.show(). Here is the new version:

from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
import matplotlib.pyplot as plt
import numpy as np


def set_axes_equal(ax):
'''Make axes of 3D plot have equal scale so that spheres appear as spheres,
cubes as cubes, etc..  This is one possible solution to Matplotlib's
ax.set_aspect('equal') and ax.axis('equal') not working for 3D.


Input
ax: a matplotlib axis, e.g., as output from plt.gca().
'''


x_limits = ax.get_xlim3d()
y_limits = ax.get_ylim3d()
z_limits = ax.get_zlim3d()


x_range = abs(x_limits[1] - x_limits[0])
x_middle = np.mean(x_limits)
y_range = abs(y_limits[1] - y_limits[0])
y_middle = np.mean(y_limits)
z_range = abs(z_limits[1] - z_limits[0])
z_middle = np.mean(z_limits)


# The plot bounding box is a sphere in the sense of the infinity
# norm, hence I call half the max range the plot radius.
plot_radius = 0.5*max([x_range, y_range, z_range])


ax.set_xlim3d([x_middle - plot_radius, x_middle + plot_radius])
ax.set_ylim3d([y_middle - plot_radius, y_middle + plot_radius])
ax.set_zlim3d([z_middle - plot_radius, z_middle + plot_radius])


fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.set_aspect('equal')


X = np.random.rand(100)*10+5
Y = np.random.rand(100)*5+2.5
Z = np.random.rand(100)*50+25


scat = ax.scatter(X, Y, Z)


set_axes_equal(ax)
plt.show()

EDIT: user2525140's code should work perfectly fine, although this answer supposedly attempted to fix a non--existant error. The answer below is just a duplicate (alternative) implementation:

def set_aspect_equal_3d(ax):
"""Fix equal aspect bug for 3D plots."""


xlim = ax.get_xlim3d()
ylim = ax.get_ylim3d()
zlim = ax.get_zlim3d()


from numpy import mean
xmean = mean(xlim)
ymean = mean(ylim)
zmean = mean(zlim)


plot_radius = max([abs(lim - mean_)
for lims, mean_ in ((xlim, xmean),
(ylim, ymean),
(zlim, zmean))
for lim in lims])


ax.set_xlim3d([xmean - plot_radius, xmean + plot_radius])
ax.set_ylim3d([ymean - plot_radius, ymean + plot_radius])
ax.set_zlim3d([zmean - plot_radius, zmean + plot_radius])

Adapted from @karlo's answer to make things even cleaner:

def set_axes_equal(ax: plt.Axes):
"""Set 3D plot axes to equal scale.


Make axes of 3D plot have equal scale so that spheres appear as
spheres and cubes as cubes.  Required since `ax.axis('equal')`
and `ax.set_aspect('equal')` don't work on 3D.
"""
limits = np.array([
ax.get_xlim3d(),
ax.get_ylim3d(),
ax.get_zlim3d(),
])
origin = np.mean(limits, axis=1)
radius = 0.5 * np.max(np.abs(limits[:, 1] - limits[:, 0]))
_set_axes_radius(ax, origin, radius)


def _set_axes_radius(ax, origin, radius):
x, y, z = origin
ax.set_xlim3d([x - radius, x + radius])
ax.set_ylim3d([y - radius, y + radius])
ax.set_zlim3d([z - radius, z + radius])

Usage:

fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.set_aspect('equal')         # important!


# ...draw here...


set_axes_equal(ax)             # important!
plt.show()

EDIT: This answer does not work on more recent versions of Matplotlib due to the changes merged in pull-request #13474, which is tracked in issue #17172 and issue #1077. As a temporary workaround to this, one can remove the newly added lines in lib/matplotlib/axes/_base.py:

  class _AxesBase(martist.Artist):
...


def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
...


+         if (not cbook._str_equal(aspect, 'auto')) and self.name == '3d':
+             raise NotImplementedError(
+                 'It is not currently possible to manually set the aspect '
+                 'on 3D axes')

Simple fix!

I've managed to get this working in version 3.3.1.

It looks like this issue has perhaps been resolved in PR#17172; You can use the ax.set_box_aspect([1,1,1]) function to ensure the aspect is correct (see the notes for the set_aspect function). When used in conjunction with the bounding box function(s) provided by @karlo and/or @Matee Ulhaq, the plots now look correct in 3D!

matplotlib 3d plot with equal axes

Minimum Working Example

import matplotlib.pyplot as plt
import mpl_toolkits.mplot3d
import numpy as np


# Functions from @Mateen Ulhaq and @karlo
def set_axes_equal(ax: plt.Axes):
"""Set 3D plot axes to equal scale.


Make axes of 3D plot have equal scale so that spheres appear as
spheres and cubes as cubes.  Required since `ax.axis('equal')`
and `ax.set_aspect('equal')` don't work on 3D.
"""
limits = np.array([
ax.get_xlim3d(),
ax.get_ylim3d(),
ax.get_zlim3d(),
])
origin = np.mean(limits, axis=1)
radius = 0.5 * np.max(np.abs(limits[:, 1] - limits[:, 0]))
_set_axes_radius(ax, origin, radius)


def _set_axes_radius(ax, origin, radius):
x, y, z = origin
ax.set_xlim3d([x - radius, x + radius])
ax.set_ylim3d([y - radius, y + radius])
ax.set_zlim3d([z - radius, z + radius])


# Generate and plot a unit sphere
u = np.linspace(0, 2*np.pi, 100)
v = np.linspace(0, np.pi, 100)
x = np.outer(np.cos(u), np.sin(v)) # np.outer() -> outer vector product
y = np.outer(np.sin(u), np.sin(v))
z = np.outer(np.ones(np.size(u)), np.cos(v))


fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.plot_surface(x, y, z)


ax.set_box_aspect([1,1,1]) # IMPORTANT - this is the new, key line
# ax.set_proj_type('ortho') # OPTIONAL - default is perspective (shown in image above)
set_axes_equal(ax) # IMPORTANT - this is also required
plt.show()

As of matplotlib 3.3.0, Axes3D.set_box_aspect seems to be the recommended approach.

import numpy as np


xs, ys, zs = <your data>
ax = <your axes>


# Option 1: aspect ratio is 1:1:1 in data space
ax.set_box_aspect((np.ptp(xs), np.ptp(ys), np.ptp(zs)))


# Option 2: aspect ratio 1:1:1 in view space
ax.set_box_aspect((1, 1, 1))

I think this feature has been added to matplotlib since these answers have been posted. In case anyone is still searching a solution this is how I do it:

import matplotlib.pyplot as plt
import numpy as np
    

fig = plt.figure(figsize=plt.figaspect(1)*2)
ax = fig.add_subplot(projection='3d', proj_type='ortho')
    

X = np.random.rand(100)
Y = np.random.rand(100)
Z = np.random.rand(100)
    

ax.scatter(X, Y, Z, color='b')

The key bit of code is figsize=plt.figaspect(1) which sets the aspect ratio of the figure to 1 by 1. The *2 after figaspect(1) scales the figure by a factor of two. You can set this scaling factor to whatever you want.

NOTE: This only works for figures with one plot.

Random 3D scatter Plot

As of matplotlib 3.6.0, this feature has been added with the command ax.set_aspect('equal'). Other options are 'equalxy', 'equalxz', and 'equalyz', to set only two directions to equal aspect ratios. This changes the data limits, example below.

In the upcoming 3.7.0, you will be able to change the plot box aspect ratios rather than the data limits via the command ax.set_aspect('equal', adjustable='box'). To get the original behavior, use adjustable='datalim'.

enter image description here

  • for the time beeing ax.set_aspect('equal') araises an error (version 3.5.1 with Anaconda).

  • ax.set_aspect('auto',adjustable='datalim') did not give a convincing solution either.

  • a lean work-aorund with ax.set_box_aspect((asx,asy,asz)) and asx, asy, asz = np.ptp(X), np.ptp(Y), np.ptp(Z) seems to be feasible (see my code snippet)

  • Let's hope that version 3.7 with the features @Scott mentioned will be successful soon.

    import numpy as np
    import matplotlib.pyplot as plt
    from mpl_toolkits.mplot3d import Axes3D
    
    
    #---- generate data
    nn = 100
    X = np.random.randn(nn)*20 +  0
    Y = np.random.randn(nn)*50 + 30
    Z = np.random.randn(nn)*10 + -5
    
    
    #---- check aspect ratio
    asx, asy, asz = np.ptp(X), np.ptp(Y), np.ptp(Z)
    
    
    fig = plt.figure(figsize=(15,15))
    ax = fig.add_subplot(projection='3d')
    
    
    #---- set box aspect ratio
    ax.set_box_aspect((asx,asy,asz))
    scat = ax.scatter(X, Y, Z, c=X+Y+Z, s=500, alpha=0.8)
    
    
    ax.set_xlabel('X-axis'); ax.set_ylabel('Y-axis'); ax.set_zlabel('Z-axis')
    plt.show()
    

enter image description here