75
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?

Mateen Ulhaq
  • 24,552
  • 19
  • 101
  • 135
hatmatrix
  • 42,883
  • 45
  • 137
  • 231
  • Does this answer your question? [matplotlib (equal unit length): with 'equal' aspect ratio z-axis is not equal to x- and y-](https://stackoverflow.com/questions/13685386/matplotlib-equal-unit-length-with-equal-aspect-ratio-z-axis-is-not-equal-to) – buzjwa Jun 08 '20 at 15:12
  • The selected answer should be [this one](https://stackoverflow.com/a/64453375/774575). – mins Apr 01 '23 at 13:34

12 Answers12

59

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)
Nico Schlömer
  • 53,797
  • 27
  • 201
  • 249
Matt Panzer
  • 1,141
  • 10
  • 7
  • 14
    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))` – tfpf Dec 25 '20 at 08:36
  • Probably use `xs[~np.isnan(xs)]` and so on to avoid nans. – CodePrinz Dec 08 '21 at 16:27
  • 3
    @p8me Added! However, see https://github.com/matplotlib/matplotlib/pull/23409: the feature may soon be supported. – tfpf Jul 10 '22 at 12:44
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)
Ben
  • 9,184
  • 1
  • 43
  • 56
  • easier to `ax.auto_scale_xyz(*np.column_stack((centers - r, centers + r)))` – panda-34 Apr 18 '16 at 03:53
  • 1
    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). – normanius Jan 17 '20 at 20:09
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.

Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
Dan
  • 45,079
  • 17
  • 88
  • 157
  • Which version do you use? I'm using 1.3.1 where it does not work. – sebix Sep 24 '14 at 13:17
  • @sebix, I'm afraid I don't remember and no longer have access to that project. But it would have been the latest python 2.7.x compatible version as of when I answered this – Dan Sep 25 '14 at 14:10
  • 2
    This doesn't set the aspect ratio of the actual plot. Just the enclosing figure. – Jacob Mar 20 '20 at 00:23
  • 1
    This is the only solution that worked properly for me on Windows. – Prasad Raghavendra Apr 30 '20 at 22:27
  • 1
    @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? – Dan May 01 '20 at 14:56
  • 1
    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' – Prasad Raghavendra May 01 '20 at 15:04
  • @Dan Yes. I can't set aspect ratio either using set_aspect('equal') or so if I remember correctly. Only your solution works. – Prasad Raghavendra May 01 '20 at 15:07
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.

Mateen Ulhaq
  • 24,552
  • 19
  • 101
  • 135
Nicolas MARTIN
  • 147
  • 1
  • 3
  • 5
    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. – John Henckel Dec 29 '21 at 16:50
  • This does nothing, please provide actual plotted values and a snapshot of the result. – mins Apr 01 '23 at 13:30
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')
Trenton McKinney
  • 56,955
  • 33
  • 144
  • 158
Jens Nyman
  • 1,186
  • 2
  • 14
  • 16
  • This is a good hack until matplotlib supports the aspect lock. Worked for me. – BlessedKey Jun 30 '12 at 09:59
  • 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? – astromax Apr 23 '13 at 17:24
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])
Crazymoomin
  • 305
  • 2
  • 9
  • 13
    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)` – sebix Sep 24 '14 at 13:33
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.

tfpf
  • 612
  • 1
  • 9
  • 19
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()

brezyl
  • 85
  • 1
  • 5
5

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'.

Mateen Ulhaq
  • 24,552
  • 19
  • 101
  • 135
Scott
  • 504
  • 6
  • 17
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).

Nico Schlömer
  • 53,797
  • 27
  • 201
  • 249
Scott B
  • 2,542
  • 7
  • 30
  • 44
  • 9
    The link is broken, but can be retrieved via the [Wayback Machine](https://web.archive.org/web/20141011215154/http://comments.gmane.org/gmane.comp.python.matplotlib.general/27415). 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. – Seanny123 Mar 30 '17 at 02:33
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.

Zciurus
  • 786
  • 4
  • 23
-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))
John Henckel
  • 10,274
  • 3
  • 79
  • 79