set matplotlib 3d plot aspect ratio
Asked Answered
R

12

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

Repressive answered 15/11, 2011 at 2:35 Comment(2)
Does this answer your question? matplotlib (equal unit length): with 'equal' aspect ratio z-axis is not equal to x- and y-Penetralia
The selected answer should be this one.Maraschino
U
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).

Undergarment answered 15/11, 2011 at 19:22 Comment(1)
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.Chelicera
P
61

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)
Phlebitis answered 20/10, 2020 at 21:10 Comment(3)
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))Scooter
Probably use xs[~np.isnan(xs)] and so on to avoid nans.Journalistic
@p8me Added! However, see github.com/matplotlib/matplotlib/pull/23409: the feature may soon be supported.Scooter
T
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)
Telltale answered 8/10, 2013 at 13:15 Comment(2)
easier to ax.auto_scale_xyz(*np.column_stack((centers - r, centers + r)))Levins
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).Sicard
B
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.

Broadcasting answered 11/9, 2012 at 13:55 Comment(7)
Which version do you use? I'm using 1.3.1 where it does not work.Mixologist
@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 thisBroadcasting
This doesn't set the aspect ratio of the actual plot. Just the enclosing figure.Drinking
This is the only solution that worked properly for me on Windows.Alaric
@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?Broadcasting
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'Alaric
@Broadcasting Yes. I can't set aspect ratio either using set_aspect('equal') or so if I remember correctly. Only your solution works.Alaric
C
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')
Chappelka answered 19/2, 2012 at 12:45 Comment(2)
This is a good hack until matplotlib supports the aspect lock. Worked for me.Hangbird
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?Disforest
M
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.

Mastigophoran answered 26/2, 2021 at 8:28 Comment(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.Sepalous
This does nothing, please provide actual plotted values and a snapshot of the result.Maraschino
S
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])
Smudge answered 12/11, 2013 at 15:25 Comment(1)
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)Mixologist
J
7

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

Jew answered 16/9, 2022 at 19:13 Comment(2)
how did you draw these blue cubes for visualisation?Strickman
@SohailSi see the code on the what's new entry here: matplotlib.org/3.6.0/users/prev_whats_new/…Jew
S
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.

Scooter answered 10/7, 2022 at 12:42 Comment(0)
T
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()

Threonine answered 7/12, 2020 at 12:37 Comment(0)
U
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).

Undergarment answered 15/11, 2011 at 19:22 Comment(1)
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.Chelicera
F
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.

Francisco answered 11/1, 2023 at 7:37 Comment(0)
S
-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))
Sepalous answered 29/12, 2021 at 16:31 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.