matplotlib (equal unit length): with 'equal' aspect ratio z-axis is not equal to x- and y-
Question:
When I set up an equal aspect ratio for a 3d graph, the z-axis
does not change to ‘equal’. So this:
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()
Gives me the following:
Where obviously the unit length of z-axis is not equal to x- and y- units.
How can I make the unit length of all three axes equal? All the solutions I found did not work.
Answers:
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:
But if you add the bounding box, you obtain a correct scaling:
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()
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!
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.
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'
.
-
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()
When I set up an equal aspect ratio for a 3d graph, the z-axis
does not change to ‘equal’. So this:
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()
Gives me the following:
Where obviously the unit length of z-axis is not equal to x- and y- units.
How can I make the unit length of all three axes equal? All the solutions I found did not work.
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:
But if you add the bounding box, you obtain a correct scaling:
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()
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!
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.
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'
.
-
for the time beeing
ax.set_aspect('equal')
araises an error (version3.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))
andasx, 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()