New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
set_aspect for 3D plots #17172
Comments
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 That is because |
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
|
You'll need to post attachments directly; GitHub will not add them from email replies. |
Here:
[cid:image001.png@01D614F1.E0B265B0]
Sent from Mail<https://go.microsoft.com/fwlink/?LinkId=550986> for Windows 10
|
Directly on GitHub. Inline to the email is still an attachment. |
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?
|
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 |
Sorry about the image. True, but one gets nonsense (at least with respect to spheres) no matter what one does without set_aspect.
|
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 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 = np.asarray([min(xyzlim[0]),max(xyzlim[1])])
ax.set_xlim3d(XYZlim)
ax.set_ylim3d(XYZlim)
ax.set_zlim3d(XYZlim * 3/4)
ax.set_title(f"{matplotlib.__version__}")
plt.show() I 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 :) |
Labeled as release critical to make sure we a decision about this one way or the other. |
I'll check what the state of my branch was - I was at least able to rebase it I think. |
Do note that the scales on the three axes showing a sphere should also match the sphere dimensions, those in 17172 do match but with the ¾ scaling on the z-axis they don’t as per your latest.
|
My branch is at eric-wieser@set_pb_aspect. I think I never finished rebasing the whole thing, and stalled on #16473. |
@vondreele There are two independent aspect ratios here: the data limits and the projection bounding box. If they are equal, a sphere will look like a sphere independent of what the aspect ratios are. In the 2D case we have the ability to enforce the aspect ratio by adjusting the position / size of the From an API point of view this is hard to manage because we have an over constrained system (the data limits, the projection bounding box aspect ratio, and the ratio between those two). Users want to be able to set any of them independently, but then we have to decide what to do if the users sets inconsistent values. |
I appreciate the problem. I draw crystal structures (non orthogonal 3D coordinate systems) in openGL. Issue there is to transform so atoms appear as spheres, bonds as cylinders, etc. and do it in perspective and allow mouse driven viewing direction.
|
Interesting. I don't immediately see the difference between 2D and 3D. In 2D we have a data aspect and a box aspect. I would think that at least in theory this very same concept can apply to 3D axes as well. |
@ImportanceOfBeingErnest I would be very happy to be wrong about it generalizing in a direct way:) @vondreele But I don't think we have a notion of a "sphere sized in screen space" (like we do with markers in the 2D case). I suspect we know enough in the transform stack to set up a meshgrid to behave that way, but I don't know how to do that off the top of my head. Something that is not clear to me is if in the gsas use case you care about the actually spatial extent of the sphere or just the visual appearance and being centered on the correct place in dataspace. A lot of the discussion around this issue has (I think) been centered on making sure that the data-extents are what the user set. |
Actually the sphere we may draw does have a specific size (not a unit sphere) - see my drawings. Other drawings may be ellipsoids (or other objects) with specific dimensions. The axes scales should match, but the axes all can cover the same range (i.e. 1x1x1 ratio) in our application. In our use the sphere/ellipsoid is centered at the origin; other folks may want them elsewhere. |
@vondreele Yes, I think that if you make the aspect ratios of your data limits match the projection bounding box ratios any 3D object should appear undistorted . In the current state with the hard coded bounding box this means your limits must be 4:4:3 to avoid distortion, if we allow the bounding box aspect ratio to be set then you could set it to match your axis limits and get the same thing. Now that I type that out it seems pretty un-defensible to not provide some sort of API for that. I probably should have extended the range of x/y rather than shirking z but shrinking z was less typing... |
Agreed - I only omitted it in the original PR because I didn't want to invent API in the same patch as fixing bugs. Does such an API already exist for 2d plots? The PR I link above adds |
By the way, non Cartesian geometries (as encountered in crystallography) in openGL are handled by a 3x3 matrix. The mpl bounding box limits are just the diagonal elements of this transformation matrix.
|
We now have |
I'm not convinced, but perhaps the safe bet is:
|
@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 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. |
closes matplotlib#17172 Instead of adding a new method, reuse `box_aspect` expecting a 3 vector. The default value of `None` is slightly special handled to make sure we hit the old behavior without having to put long floating point numbers in the source.
@eric-wieser I have become convinced that you are correct and overloading from itertools import product, combinations
import matplotlib
import matplotlib.pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
import numpy as np
plt.ion()
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__}")
ax.set_box_aspect((1, 1, 1))
plt.show() |
closes matplotlib#17172 Instead of adding a new method, reuse `box_aspect` expecting a 3 vector. The default value of `None` is slightly special handled to make sure we hit the old behavior without having to put long floating point numbers in the source.
How does that compare to the 2d API? |
closes matplotlib#17172 Instead of adding a new method, reuse `box_aspect` expecting a 3 vector. The default value of `None` is slightly special handled to make sure we hit the old behavior without having to put long floating point numbers in the source.
@eric-wieser It ended up being the same, but with a length 3 vector rather than a scalar. |
I think the documentation for
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. |
This contrived one-liner sets the box aspect ratio from the current axes limits to achieve the ax.set_box_aspect([ub - lb for lb, ub in (getattr(ax, f'get_{a}lim')() for a in 'xyz')]) |
Due to this issue: matplotlib/matplotlib#17172 setting aspect equal doesn't work on 3D axes. Removing for the moment.
Due to this issue: matplotlib/matplotlib#17172 setting aspect equal doesn't work on 3D axes. Removing for the moment.
Bug report
Bug summary
set_aspect does not work for 3D surface plots
Expected outcome
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.
The text was updated successfully, but these errors were encountered: