66
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 does not for 3d:

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

Is there a different syntax for the 3d case, or it's not implemented?

Trenton McKinney
  • 43,885
  • 25
  • 111
  • 113
hatmatrix
  • 39,823
  • 42
  • 131
  • 224
  • 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

9 Answers9

43

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
  • 46,467
  • 24
  • 178
  • 218
Matt Panzer
  • 891
  • 8
  • 7
  • 1
    That's really helpful, just to mention there is a missing closing bracket ")" at the end of line ax.set_box_aspect.. – Yasmin Oct 22 '20 at 12:19
  • 8
    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
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
  • 7,741
  • 1
  • 37
  • 44
  • 1
    I think this is the most elegant solution of the various proposed on SO. – Kel Solaar May 09 '15 at 12:08
  • 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
19

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
  • 43,885
  • 25
  • 111
  • 113
Dan
  • 44,224
  • 16
  • 81
  • 148
  • 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 Jones 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
14

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
  • 43,885
  • 25
  • 111
  • 113
Jens Nyman
  • 1,167
  • 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
11

I think setting the correct "box aspect" is a good solution:

ax.set_box_aspect(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(dataX,dataY,dataZ)

https://matplotlib.org/stable/api/_as_gen/mpl_toolkits.mplot3d.axes3d.Axes3D.html?highlight=3d%20set_box_aspect#mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect

Trenton McKinney
  • 43,885
  • 25
  • 111
  • 113
Nicolas MARTIN
  • 119
  • 1
  • 3
  • 2
    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
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
  • 285
  • 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
  • @sebix this really helped. This should be the answer – icemtel Aug 28 '20 at 08:26
4

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
  • 75
  • 5
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
  • 46,467
  • 24
  • 178
  • 218
Scott B
  • 2,452
  • 6
  • 28
  • 43
  • 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
0

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
  • 8,771
  • 3
  • 67
  • 74