If a sphere is drawn with plot_surface then it should appear as a sphere and not an ellipse that depends on the window sizing. This was achieved in mpl 3.0.3 by calling
set_aspect('equal').
It is now broken in mpl 3.1.1-3.1.3 because the following was inserted in matplotlib/axes/_base.py (lines1279:1282) by the developers
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')
To get set_aspect to work (correctly, I might add), I have to comment out these lines in my local version of matplotlib. A plotted sphere appears as a sphere independent of the window sizing.
See #1077 and #13474 for why this change was made - I'll leave this open as a feature request to add the functionality back, but I think it's a fairly non-trivial feature to implement.
Dear David,
Thanks for your note. I did read the messages & I think there may have been some confusion as to what set_aspect(“equal”) actually did. My use is to maintain the axes despite changes in the display window size; essential if one wants a sphere to stay spherical or an ellipsoid to maintain its axes ratios. To fix I can simply comment out the exception code in matplotlib but it is difficult to get users of our software to do the same. Perhaps the functionality could be better described in the documentation so as to not confuse it with set_xlim3d, etc. and perhaps replace “equal” with “fixed” because it (when allowed to) does perform the job I need it to do perfectly.
Best,
Robert Von Dreele
Just to make sure we're on the same page here. This comment in #1077 shows that .set_aspect('equal') was just plain wrong.
That is because .set_aspect('equal') was never actually implemented. It just set the aspect in 2D space. In other words, if you ever created a plot with .set_aspect('equal') in the past, that plot would have depicted an incorrect object. A sphere would not ever have been spherical (though it may have looked close enough to not notice).
Dear Elan & David,
Again to be sure we’re on the same page please consider the following attached set of screen shots of drawing a sphere. [cid:image002.png@01D614DE.D8D60C10]
The top row is without a working set_aspect(“equal”) and the bottom is with it working. As the plot window is changed in the upper set the plot fills the available space distorting the sphere while in the lower set the plot maintains it’s aspect as drawn and the sphere is drawn as a sphere; it does not try to fill the window. This is the behavior I want & is what set_aspect(“equal”) provides. It does work. BTW- rotating the plot around shows that what is drawn is a sphere within the resolution of my display – there is no distortion.
A sample bit of code we use to make such plots is:
Plot.plot_surface(X,Y,Z,rstride=1,cstride=1,color='g',linewidth=1)
xyzlim = np.array([Plot.get_xlim3d(),Plot.get_ylim3d(),Plot.get_zlim3d()]).T
XYZlim = [min(xyzlim[0]),max(xyzlim[1])]
Plot.set_xlim3d(XYZlim)
Plot.set_ylim3d(XYZlim)
Plot.set_zlim3d(XYZlim)
Plot.set_aspect('equal')
In this case X,Y,Z describe points on the surface of a sphere (could be something else); notice the use of get_xlim3d to establish the axis dimensions. As far as I can tell from the discussion, folks were trying to use set_aspect(“equal”) to do this. Perhaps the real problem is that it is misnamed and/or the documentation is wrong.
Best,
Robert Von Dreele
I see a bit clearer now. The problem is, that while in mpl 3.0 you could (mis)use the 2D-axes' equal aspect to actually achieve a perfect sphere on screen, this is not possible anymore, even in current master with Fix mplot3d projection #8896, or #16472 for that matter, being applied - which should have made the situation better. That is, I was under the impression that #8896 (really #16472) would fix the projection, such that a sphere would always appear circular on screen?
from itertools import product, combinations
import matplotlib
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
fig = plt.figure()
ax = fig.add_subplot(111, projection='3d')
# Make data
u = np.linspace(0, 2 * np.pi, 100)
v = np.linspace(0, np.pi, 100)
x = np.outer(np.cos(u), np.sin(v))
y = np.outer(np.sin(u), np.sin(v))
z = np.outer(np.ones(np.size(u)), np.cos(v))
# Plot the surface
ax.plot_surface(x, y, z)
#draw cube
r = [-1, 1]
for s, e in combinations(np.array(list(product(r,r,r))), 2):
if np.sum(np.abs(s-e)) == r[1]-r[0]:
ax.plot3D(*zip(s,e), color="b")
# Make axes limits
xyzlim = np.array([ax.get_xlim3d(),ax.get_ylim3d(),ax.get_zlim3d()]).T
XYZlim = [min(xyzlim[0]),max(xyzlim[1])]
ax.set_xlim3d(XYZlim)
ax.set_ylim3d(XYZlim)
ax.set_zlim3d(XYZlim)
try:
ax.set_aspect('equal')
except NotImplementedError:
pass
ax.set_title(f"{matplotlib.__version__}")
plt.show()
Using your code and my version of mpl 3.1.3 (where I removed the NonImplemented exception). I get
[cid:image003.png@01D61560.F3CA6660]
independent of window size. Looks like mpl 3.2.1 fails as well based on your example. I modified your code to explore the problem. It is clear that set_aspect only works (e.g. to draw round spheres) if it is accompanied by setting the 3 axis limits to be the same. If the axis limits are different then the drawing is distorted with or without use of set_aspect; this is the “rectangular coordinates problem” mentioned previously. Path forward? I’d vote to reinstate set_aspect with caveats in the documentation.
Again, be reminded that we cannot see images attached to emails.
It looks like the case with all axes limits being the same and set_aspect("equal") was the only case that did not produce non-sensical results previously, right? That may be a bit thin for reinstating that otherwise non-working option.
ok, so #16472 will be in 3.3 (which is targeted to be out at the end of the month). It looks like that PR a) fixed the bug with apply_aspect that made sure that the 3D axes did not stretch with the figure size (which is the what the top row of #17172 (comment) is showing), however this now pins the aspect ratio of the projected bounding box to be [4, 4, 3] independent of the data limits are which gives the squashing effect that @ImportanceOfBeingErnest . If you manually adjust the z limits to be 3/4 of the x and y limits then you get a spherical looking unit sphere.
change the hard-coded projection bounding box ratios to be 1:1:1 (not great because still hard-coded, would have to regenerate a whole bunch of images, slight API change (because the 4:4:3 limits were chosen to be close to the old default view), but it may the behavior some fraction of our users expect (judging by the traffic the original issue got))
leave the current state of the code and document the 4:4:3 ratio to the hilt. Pro: no code changes or image regeneration, but it is more complexity on users, a bit funny to document, and will make ti more awkward to change in the future
add an rcparam to control this and lock in the projection bounding box ratio at axes creation time. Pro: no image re-generation, gives users less fussy control, con: another rcparam
add new API to Axes3D to let the user set the projection bounding box to what ever they want. @eric-wieser commented that he had started to work on this and it got messy. This probably should not be laundered through the normal set_aspect due requiring a vector rather than a scalar to describe the aspect ratio. This may be the best long term, but I worry about being able to get the API on this right on a short timeline.
something else I have not thought of, but listing bad options has historically been a good way to solicit better options ;)
None of these (except the hypothetical better suggestion) are great, I think we are balancing what we can get done in the next week or so (to hit our 3.3 deadlines) but still ship with some flexibility on this.
My current preference is the rcparam (balances non-fussy user control against quick to do). If we add API in the future, we will probably have to add the rcparam anyway so we are not painting ourselves into a corner.
Agreed - I only omitted it in the original PR because I didn't want to invent API in the same patch as fixing bugs.
@eric-wieser That definitely was not meant as a criticism of you, that was poking at myself attempting to defend the status-quo in this thread!
Does such an API already exist for 2d plots?
I would trace back through what apply_aspect does in the 2D case. We have a 2 positions on each axes, an aspect ratio, a setting if we adjust the data limits or view limits, and special casing to make sure that twined axes always are always overlapping. I am not sure I have a clear picture of all of the details of the top of my head (@efiring is the local expert on this I believe).
I think starting with them as their own methods only on Axes3D is a safe place to start as is making sure the non-operative 2D methods are intentionally broken. If we decided we want to make them alias later that is easier than realizing we really needed both all along.
@eric-wieser I have become convinced that you are correct and overloading set_aspect is the right way to do this. For now in #17515 I only added the ability to set the box aspect ratio. I know this is less convenient, but I think this will un-block @vondreele .
I think the documentation for set_box_aspect can be improved. I had difficulty understanding how to implement this part:
To simulate having equal aspect in data space, set the box aspect to match your data range in each dimension.
Fortunately I happened to come across this github issue and I could refer to @tacaswell's example above. I think it is hard for the average user to understand this feature without some additional details.
Reacted by Jacob Hultman, Cory Simon, Siyuan_Feng, Luka Papež, Matevž Fabjančič, Kyle Robert Steffen, Colin Caprani, Adi Shavit, Thomas Wagenaar, Damien LaRocque and 9 more
Reacted by mueslo, Hongxiang Chen, Jean-Eudes-le-retour, Francisco Costa, Santiago F. Caracciolo and Arun Ravi
Activity
dstansby commented on Apr 17, 2020
See #1077 and #13474 for why this change was made - I'll leave this open as a feature request to add the functionality back, but I think it's a fairly non-trivial feature to implement.
vondreele commented on Apr 17, 2020
ImportanceOfBeingErnest commented on Apr 17, 2020
Just to make sure we're on the same page here. This comment in #1077 shows that .set_aspect('equal') was just plain wrong.
That is because .set_aspect('equal') was never actually implemented. It just set the aspect in 2D space. In other words, if you ever created a plot with .set_aspect('equal') in the past, that plot would have depicted an incorrect object. A sphere would not ever have been spherical (though it may have looked close enough to not notice).
vondreele commented on Apr 17, 2020
QuLogic commented on Apr 18, 2020
You'll need to post attachments directly; GitHub will not add them from email replies.
vondreele commented on Apr 18, 2020
QuLogic commented on Apr 18, 2020
Directly on GitHub. Inline to the email is still an attachment.
vondreele commented on Apr 18, 2020
Let me try this way:

The top row is with no set_aspect("equal") the bottom is with a working set_aspect("equal")
This is what should happen.
ImportanceOfBeingErnest commented on Apr 18, 2020
I see a bit clearer now. The problem is, that while in mpl 3.0 you could (mis)use the 2D-axes' equal aspect to actually achieve a perfect sphere on screen, this is not possible anymore, even in current master with Fix mplot3d projection #8896, or #16472 for that matter, being applied - which should have made the situation better. That is, I was under the impression that #8896 (really #16472) would fix the projection, such that a sphere would always appear circular on screen?
from itertools import product, combinations import matplotlib import matplotlib.pyplot as plt from mpl_toolkits.mplot3d import Axes3D import numpy as np fig = plt.figure() ax = fig.add_subplot(111, projection='3d') # Make data u = np.linspace(0, 2 * np.pi, 100) v = np.linspace(0, np.pi, 100) x = np.outer(np.cos(u), np.sin(v)) y = np.outer(np.sin(u), np.sin(v)) z = np.outer(np.ones(np.size(u)), np.cos(v)) # Plot the surface ax.plot_surface(x, y, z) #draw cube r = [-1, 1] for s, e in combinations(np.array(list(product(r,r,r))), 2): if np.sum(np.abs(s-e)) == r[1]-r[0]: ax.plot3D(*zip(s,e), color="b") # Make axes limits xyzlim = np.array([ax.get_xlim3d(),ax.get_ylim3d(),ax.get_zlim3d()]).T XYZlim = [min(xyzlim[0]),max(xyzlim[1])] ax.set_xlim3d(XYZlim) ax.set_ylim3d(XYZlim) ax.set_zlim3d(XYZlim) try: ax.set_aspect('equal') except NotImplementedError: pass ax.set_title(f"{matplotlib.__version__}") plt.show()vondreele commented on Apr 18, 2020
ImportanceOfBeingErnest commented on Apr 18, 2020
Again, be reminded that we cannot see images attached to emails.
It looks like the case with all axes limits being the same and set_aspect("equal") was the only case that did not produce non-sensical results previously, right? That may be a bit thin for reinstating that otherwise non-working option.
vondreele commented on Apr 18, 2020
tacaswell commented on Apr 22, 2020
ok, so #16472 will be in 3.3 (which is targeted to be out at the end of the month). It looks like that PR a) fixed the bug with apply_aspect that made sure that the 3D axes did not stretch with the figure size (which is the what the top row of #17172 (comment) is showing), however this now pins the aspect ratio of the projected bounding box to be [4, 4, 3] independent of the data limits are which gives the squashing effect that @ImportanceOfBeingErnest . If you manually adjust the z limits to be 3/4 of the x and y limits then you get a spherical looking unit sphere.
DetailsI think our options here are:
None of these (except the hypothetical better suggestion) are great, I think we are balancing what we can get done in the next week or so (to hit our 3.3 deadlines) but still ship with some flexibility on this.
My current preference is the rcparam (balances non-fussy user control against quick to do). If we add API in the future, we will probably have to add the rcparam anyway so we are not painting ourselves into a corner.
side note, thank you for the perfect MWE @ImportanceOfBeingErnest :)
15 remaining items
tacaswell commented on Apr 24, 2020
@eric-wieser That definitely was not meant as a criticism of you, that was poking at myself attempting to defend the status-quo in this thread!
I would trace back through what apply_aspect does in the 2D case. We have a 2 positions on each axes, an aspect ratio, a setting if we adjust the data limits or view limits, and special casing to make sure that twined axes always are always overlapping. I am not sure I have a clear picture of all of the details of the top of my head (@efiring is the local expert on this I believe).
I think starting with them as their own methods only on Axes3D is a safe place to start as is making sure the non-operative 2D methods are intentionally broken. If we decided we want to make them alias later that is easier than realizing we really needed both all along.
ENH: use box_aspect to support setting pb_aspect
tacaswell commented on May 27, 2020
@eric-wieser I have become convinced that you are correct and overloading set_aspect is the right way to do this. For now in #17515 I only added the ability to set the box aspect ratio. I know this is less convenient, but I think this will un-block @vondreele .
ENH: use box_aspect to support setting pb_aspect
eric-wieser commented on May 28, 2020
How does that compare to the 2d API?
ENH: use box_aspect to support setting pb_aspect
ENH: use box_aspect to support setting pb_aspect
tacaswell commented on Jun 9, 2020
@eric-wieser It ended up being the same, but with a length 3 vector rather than a scalar.
jhultman commented on Mar 29, 2021
I think the documentation for set_box_aspect can be improved. I had difficulty understanding how to implement this part:
Fortunately I happened to come across this github issue and I could refer to @tacaswell's example above. I think it is hard for the average user to understand this feature without some additional details.
jhrmnn commented on Apr 30, 2021
This contrived one-liner sets the box aspect ratio from the current axes limits to achieve the "equal" behavior:
Update visualize.py
Update visualize.py
scottshambaugh commented on Jul 16, 2025
This issue still ranks pretty highly in search results, so I want to flag that set_aspect does now work for 3D plots as of v3.6.0: https://matplotlib.org/stable/api/_as_gen/mpl_toolkits.mplot3d.axes3d.Axes3D.set_aspect.html
vondreele commented on Jul 16, 2025
For matplotlib 3.8.4, we use set_box_aspect((1,1,1)) & it properly draws spheres that maintain their shape.