FazBrowse GitHub Viewer | Trending |
URL:
| Home
Tools: [Download Repo ZIP]   [Original HTTPS Page]

set_aspect for 3D plots · Issue #17172 · matplotlib/matplotlib · GitHub

Repository navigation

set_aspect for 3D plots #17172

Closed
Closed

Description

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.

Activity

  1. dstansby commented on Apr 17, 2020

    Member

    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.

  2. vondreele commented on Apr 17, 2020

    Author
  3. ImportanceOfBeingErnest commented on Apr 17, 2020

    Member

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

  4. vondreele commented on Apr 17, 2020

    Author
  5. QuLogic commented on Apr 18, 2020

    Member

    You'll need to post attachments directly; GitHub will not add them from email replies.

  6. vondreele commented on Apr 18, 2020

    Author
  7. QuLogic commented on Apr 18, 2020

    Member

    Directly on GitHub. Inline to the email is still an attachment.

  8. vondreele commented on Apr 18, 2020

    Author

    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.

  9. ImportanceOfBeingErnest commented on Apr 18, 2020

    Member

    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()
    
  10. vondreele commented on Apr 18, 2020

    Author
  11. ImportanceOfBeingErnest commented on Apr 18, 2020

    Member

    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.

  12. vondreele commented on Apr 18, 2020

    Author
  13. tacaswell commented on Apr 22, 2020

    Member

    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.

    Details
    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:

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


    side note, thank you for the perfect MWE @ImportanceOfBeingErnest :)

  14. added this to the v3.3.0 milestone on Apr 22, 2020
  15. 15 remaining items

  16. tacaswell commented on Apr 24, 2020

    Member

    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.

  17. added a commit that references this issue on May 27, 2020
    fb91f71
  18. tacaswell commented on May 27, 2020

    Member

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

    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()

  19. added a commit that references this issue on May 27, 2020
    1e5b11a
  20. eric-wieser commented on May 28, 2020

    Contributor

    How does that compare to the 2d API?

  21. added 2 commits that reference this issue on May 30, 2020
    98933a3
    6915f69
  22. tacaswell commented on Jun 9, 2020

    Member

    @eric-wieser It ended up being the same, but with a length 3 vector rather than a scalar.

  23. 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:

    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.

  24. 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:

    ax.set_box_aspect([ub - lb for lb, ub in (getattr(ax, f'get_{a}lim')() for a in 'xyz')])
  25. scottshambaugh commented on Jul 16, 2025

    Contributor

    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

  26. vondreele commented on Jul 16, 2025

    Author

    For matplotlib 3.8.4, we use set_box_aspect((1,1,1)) & it properly draws spheres that maintain their shape.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    Release criticalFor bugs that make the library unusable (segfaults, incorrect plots, etc) and major regressions.topic: mplot3d

    Type

    No type

    Projects

    No projects

      Milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions


      Back | FazBrowse Home | New Git URL