79
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D

Setting the aspect ratio works for 2d plots:

ax = plt.axes()
ax.plot([0,1], [0,10])
ax.set_aspect('equal', 'box')

But it does not work for 3d:

ax = plt.axes(projection='3d')
ax.plot([0,1], [0,1], [0,10])
ax.set_aspect('equal', 'box')

How do I set the aspect ratio for 3d?

2

13 Answers 13

68

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

import numpy as np
import matplotlib.pyplot as plt

xs, ys, zs = ...
ax = plt.axes(projection='3d')

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

ax.plot(xs, ys, zs)
Sign up to request clarification or add additional context in comments.

3 Comments

This is the only solution that worked for me. None of the others did. For my program, I modified it to choose the arguments automatically. limits = np.array([getattr(self.ax, f'get_{axis}lim')() for axis in 'xyz']); ax.set_box_aspect(np.ptp(limits, axis = 1))
Probably use xs[~np.isnan(xs)] and so on to avoid nans.
@p8me Added! However, see github.com/matplotlib/matplotlib/pull/23409: the feature may soon be supported.
29

I didn't try all of these answers, but this kludge did it for me:

def axisEqual3D(ax):
    extents = np.array([getattr(ax, 'get_{}lim'.format(dim))() for dim in 'xyz'])
    sz = extents[:,1] - extents[:,0]
    centers = np.mean(extents, axis=1)
    maxsize = max(abs(sz))
    r = maxsize/2
    for ctr, dim in zip(centers, 'xyz'):
        getattr(ax, 'set_{}lim'.format(dim))(ctr - r, ctr + r)

2 Comments

easier to ax.auto_scale_xyz(*np.column_stack((centers - r, centers + r)))
While this sets the limits per axis to the same values, this solution unfortunately doesn't fix the different axis scales. A sphere still is displayed as an ellipsoid (at least on the default MacOSX backend).
20

Looks like this feature has since been added so thought I'd add an answer for people who come by this thread in the future like I did:

fig = plt.figure(figsize=plt.figaspect(0.5)*1.5) #Adjusts the aspect ratio and enlarges the figure (text does not enlarge)
ax = fig.add_subplot(projection='3d')

figaspect(0.5) makes the figure twice as wide as it is tall. Then the *1.5 increases the size of the figure. The labels etc won't increase so this is a way to make the graph look less cluttered by the labels.

7 Comments

Which version do you use? I'm using 1.3.1 where it does not work.
This doesn't set the aspect ratio of the actual plot. Just the enclosing figure.
This is the only solution that worked properly for me on Windows.
@PrasadRaghavendra what versions of python and matplotlib did you use? It would be good to have an idea of when this works. Also, are you able to verify what Jacob Jones has said above?
Python 3.7.6 (default, Jan 8 2020, 20:23:39) [MSC v.1916 64 bit (AMD64)] :: Ana conda, Inc. on win32 Type "help", "copyright", "credits" or "license" for more information. >>> import matplotlib >>> matplotlib.__version__ '3.1.2'
|
13

If you know the bounds, eg. +-3 centered around (0,0,0), you can add invisible points like this:

import numpy as np
import pylab as pl
from mpl_toolkits.mplot3d import Axes3D
fig = pl.figure()
ax = fig.add_subplot(projection='3d')
ax.set_aspect('equal')
MAX = 3
for direction in (-1, 1):
    for point in np.diag(direction * MAX * np.array([1,1,1])):
        ax.plot([point[0]], [point[1]], [point[2]], 'w')

2 Comments

This is a good hack until matplotlib supports the aspect lock. Worked for me.
Good idea - worked for me. Just my opinion, but this doesn't seem to be an aspect ratio problem, this is a bounding box issue. Is there some way to simply set the extent?
13

To stretch the axes so that all the data points fit inside a box, use ax.set_box_aspect to set aspect = (1, 1, 1).

import matplotlib.pyplot as plt

fig = plt.figure()
ax = fig.add_subplot(projection='3d')
ax.set_box_aspect(aspect=(1, 1, 1))
ax.plot(xs, ys, zs)

Note that with this method, 1 unit in the x direction is not necessarily 1 unit in the y direction.

2 Comments

This answer is wrong. The set_box_aspect simply changes the length of the x,y,z axes in the display. It does not change the scale of the axes. The OP is asking how to set all three axes to have the same scale in data space in the same way that set_aspect('equal') works in 2d graphs.
This does nothing, please provide actual plotted values and a snapshot of the result.
9

If you know the bounds you can also set the aspect ratio this way:

ax.auto_scale_xyz([minbound, maxbound], [minbound, maxbound], [minbound, maxbound])

1 Comment

Or, doing it automatically: scaling = np.array([getattr(ax, 'get_{}lim'.format(dim))() for dim in 'xyz']); ax.auto_scale_xyz(*[[np.min(scaling), np.max(scaling)]]*3)
9

As of matplotlib 3.6.0, this feature has been added to ax.set_aspect. Use:

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.

enter image description here


In the upcoming 3.7.0, you will be able to change the plot box aspect ratios rather than the data limits via:

ax.set_aspect('equal', adjustable='box')

(Thanks to @tfpf on another answer here for implementing that!) To get the original behavior, use adjustable='datalim'.

2 Comments

how did you draw these blue cubes for visualisation?
@SohailSi see the code on the what's new entry here: matplotlib.org/3.6.0/users/prev_whats_new/…
6

A follow-up to Matt Panzer's answer. (This was originally a comment on said answer.)

limits = np.array([getattr(ax, f'get_{axis}lim')() for axis in 'xyz'])
ax.set_box_aspect(np.ptp(limits, axis=1))

Now that this pull request has been merged, when the next release of Matplotlib drops, you should be able to just use ax.set_aspect('equal'). I will try to remember and update this answer when that happens.

Update: Matplotlib 3.6 has been released; ax.set_aspect('equal') will now work as expected.

Comments

5

Another helpful (hopefully) solution when, for example, it is necessary to update an already existing figure:

world_limits = ax.get_w_lims()
ax.set_box_aspect((world_limits[1]-world_limits[0],world_limits[3]-world_limits[2],world_limits[5]-world_limits[4]))

get_w_lims()

set_box_aspect()

Comments

4

My understanding is basically that this isn't implemented yet (see this bug in GitHub). I'm also hoping that it is implemented soon. See This link for a possible solution (I haven't tested it myself).

1 Comment

The link is broken, but can be retrieved via the Wayback Machine. However, it would be better if you included the relevant code in your answer instead of requiring future people to search through the mailing list archive.
2

This works with 3.7:

# Do all your plots here
ax.set_aspect('equal')

It works, but it needs to be set after all your plots are done. If you call the method when creating the figure/axis, then it will create a cube and adjust the scale of the axes to fit all plots within this cube, and therefore the scales won't be equal.

An example, NED frame basis vectors, rotated by +45° around z.

enter image description here

To have the same ticks on all axes, another annoyance, use something like:

for axis in [ax.xaxis, ax.yaxis, ax.zaxis]:
    axis.set_major_locator(plt.MultipleLocator(0.5))

Comments

1

Matt Panzer's answer worked for me, but it took me a while to figure out an issue I had. If you're plotting multiple datasets into the same graph, you have to calculate the peak-to-peak values for the entire range of datapoints.

I used the following code to solve it for my case:

x1, y1, z1 = ..., ..., ...
x2, y2, z2 = ..., ..., ...   

ax.set_box_aspect((
    max(np.ptp(x1), np.ptp(x2)), 
    max(np.ptp(y1), np.ptp(y2)), 
    max(np.ptp(z1), np.ptp(y2))
))

ax.plot(x1, y1, z1)
ax.scatter(x2, y2, z2)

Note that this solution is not perfect. It will not work if x1 contains the most negative number and x2 contains the most positive one. Only if either x1 or x2 contains the greatest peak-to-peak range.

If you know numpy better than I do, feel free to edit this answer so it works in a more general case.

Comments

-1

I tried several methods, such as ax.set_box_aspect(aspect = (1,1,1)) and it does not work. I want a sphere to show up as a sphere -- not ellipsoid. I wrote this function and tried it on a variety of data. It is a hack and it is not perfect, but pretty close.

def set_aspect_equal(ax):
    """ 
    Fix the 3D graph to have similar scale on all the axes.
    Call this after you do all the plot3D, but before show
    """
    X = ax.get_xlim3d()
    Y = ax.get_ylim3d()
    Z = ax.get_zlim3d()
    a = [X[1]-X[0],Y[1]-Y[0],Z[1]-Z[0]]
    b = np.amax(a)
    ax.set_xlim3d(X[0]-(b-a[0])/2,X[1]+(b-a[0])/2)
    ax.set_ylim3d(Y[0]-(b-a[1])/2,Y[1]+(b-a[1])/2)
    ax.set_zlim3d(Z[0]-(b-a[2])/2,Z[1]+(b-a[2])/2)
    ax.set_box_aspect(aspect = (1,1,1))

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.