Skip to content

3d plots with aspect='equal' #1077

Closed
Closed

Description

@akhmerov

aspect='equal' argument is completely misused by axes3D.
To verify:

import matplotlib.pyplot as plt
from mpl_toolkits import mplot3d
fig = plt.figure()
ax = fig.add_subplot((111), aspect='equal', projection='3d')
ax.scatter((1, 2), (1, 1.2), (1, 2)) 
  • The figure is rendered with all axes scaled to the data scale, thus ignoring the aspect setting.
  • If one tries to rotate the figure, using GUI, it is rescaled to fix into a very uneven rectangle, presumably calculated from x-scale vs y-scale, and hence with aspect ratio 1:5.

Activity

  1. WeatherGod commented on Aug 20, 2012

    @WeatherGod
    Member

    an "aspect" kwarg doesn't make a lot of sense for 3d plots, which is why even matlab doesn't do it. I have an old branch that provides some very basic functionality in this direction, but I never finished. See here:

    http://matplotlib.1069221.n5.nabble.com/mplot3d-and-daspect-tp11521p11523.html

    PRs against this branch would be welcomed!

  2. akhmerov commented on Aug 20, 2012

    @akhmerov
    ContributorAuthor

    Why doesn't it make sense? If I want to e.g. mark coordinates in a building, or any system where three axes have the same dimension, I do want that the same axes is chosen for each of the axes (that's where I actually encountered the bug). And of course regardless of that, the current handling is bugged.

  3. vallettea commented on Jan 23, 2013

    @vallettea

    I would also be interested by this feature.
    By the way, did you found a way to tackle the problem?

  4. astrofrog commented on Feb 24, 2013

    @astrofrog
    Contributor

    We also came across this issue - if I run the following code:

    from mpl_toolkits.mplot3d import Axes3D
    import matplotlib.pyplot as plt
    import numpy as np
    from itertools import product, combinations
    
    fig = plt.figure()
    ax = fig.add_subplot(1,1,1,projection='3d')
    
    #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")
    

    I get:

    cube

    and adding

    ax.set_aspect('equal')
    

    then gives

    cube_equal

    which is simply wrong, not just sub-optimal. The cube lines should always be parallel to the axes. In my opinion, it should look more like

    cube_shouldbe

    But at the very least, if set_aspect('equal') is going to do the wrong thing, it would be better if it raised a NotImplemented error.

    (this issue was originally pointed out to me by @koepferl)

  5. tehingo commented on Mar 25, 2013

    @tehingo

    My research entails lots of 3D plotting on unit spheres. Needless to say, a unit sphere is a perfect sphere and to display it properly all axes need to have the same scale. This is another use case that requires this functionality.

    I would love to help out, but don't have the time to dig into the code right now. I really hope this feature gets some priority for future releases.

  6. akhmerov commented on Mar 25, 2013

    @akhmerov
    ContributorAuthor

    Just in case somebody encounters this bug, and needs a workaround: the way I handle this is by manually calling ax.auto_scale_xyz before rendering.

  7. added this to the v1.4.x milestone on Aug 18, 2014
  8. modified the milestones: v1.4.x, on Aug 18, 2014
  9. Hanlin-Dong commented on Jan 6, 2016

    @Hanlin-Dong

    Actually there are several ways of showing a 3D model on a 2D screen, mainly including axonometric projection and perspective projection. There are also many kinds of axonometric projections. So even the unit length of three axises are the same, they look different on the screen on many circumstances. A cubic box may look not that cubic if drawn in different ways. So if the 3D plot class is to be improved, projection method should be determined beforehand.

  10. akhmerov commented on Jan 6, 2016

    @akhmerov
    ContributorAuthor

    @Hanlin-Dong I agree, but no matter how the projection is done, the projection for the data must match the projection for the axes. In the example above it doesn't, hence the bug report. Also the bug report isn't about the projection as much as the impossibility to correctly set the aspect ratios.

  11. solarjoe commented on Jan 18, 2016

    @solarjoe

    I also think that this feature is quite important for 3d plots.

    True, it might not be that important if you visualize data without
    an underlying real-world geometry. But I have to visualize the
    heat distribution on motor parts in 3D and this looks quite strange if the
    aspect ratio is incorrect. I guess the same it true with the buildings that
    @akhmerov mentioned.

    @WeatherGod mentioned above that Matlab does not provide this feature, but
    I am quite sure it does.
    This is the manpage, it also shows 3D examples for "normal" and "equal".
    http://de.mathworks.com/help/matlab/creating_plots/aspect-ratio-for-2-d-axes.html
    More information on how it is implemented as axis property:
    http://de.mathworks.com/help/matlab/ref/axes-properties.html#prop_DataAspectRatio

    I guess most of it is already implemented and working in matplotlib but I came across
    a small issue, see my post and example code in #1104.

  12. samuela commented on May 6, 2018

    @samuela

    screen shot 2018-05-05 at 8 30 52 pm

    I just got this plot after calling ax.set_aspect('equal'). Definitely not equal based on the ticks!

  13. timhoffm commented on May 6, 2018

    @timhoffm
    Member

    Related and possibly a first step for fixing this: #8896

  14. 11 remaining items

  15. jklymak commented on Mar 3, 2019

    @jklymak
    Member

    Closing due to #13474 and the fact that the non-optimal behaviour is discussed in previous issues and PRs references above...

  16. akhmerov commented on Mar 3, 2019

    @akhmerov
    ContributorAuthor

    @jklymak what do you mean by the non-optimal behavior? Does closing this issue mean that 3D plots with equal aspect ratio aren't on the roadmap?

  17. solarjoe commented on Mar 3, 2019

    @solarjoe

    I hope he is closing because of (still open) #8896 just to clean the issue tracker, as #13474 is only a workaround on how to deal with the behaviour until it is implemented.

  18. jklymak commented on Mar 3, 2019

    @jklymak
    Member

    Correct. OTOH I don't know of anyone actively developing mplot3d, so if someone wants this feature, it would require a volunteer..

  19. zh-plus commented on Aug 14, 2019

    @zh-plus
  20. solarjoe commented on Aug 14, 2019

    @solarjoe
  21. jondo commented on Jun 2, 2021

    @jondo

    In order to get a future notification of aspect='equal' working, which open issue (or unmerged PR) should I subscribe to?

  22. tacaswell commented on Jun 3, 2021

    @tacaswell
    Member

    Please see the note at https://matplotlib.org/stable/users/prev_whats_new/whats_new_3.3.0.html#axes3d-no-longer-distorts-the-3d-plot-to-match-the-2d-aspect-ratio (from #17515) for how to get this effect.

    If setting this with a string is important please open a new issue with what you would expect "equal" to do under various conditions / adjustable settings.


    The code that would need to be changed to implement this is

    def set_aspect(self, aspect, adjustable=None, anchor=None, share=False):
    """
    Set the aspect ratios.
    Axes 3D does not current support any aspect but 'auto' which fills
    the axes with the data limits.
    To simulate having equal aspect in data space, set the ratio
    of your data limits to match the value of `.get_box_aspect`.
    To control box aspect ratios use `~.Axes3D.set_box_aspect`.
    Parameters
    ----------
    aspect : {'auto'}
    Possible values:
    ========= ==================================================
    value description
    ========= ==================================================
    'auto' automatic; fill the position rectangle with data.
    ========= ==================================================
    adjustable : None
    Currently ignored by Axes3D
    If not *None*, this defines which parameter will be adjusted to
    meet the required aspect. See `.set_adjustable` for further
    details.
    anchor : None or str or 2-tuple of float, optional
    If not *None*, this defines where the Axes will be drawn if there
    is extra space due to aspect constraints. The most common way to
    to specify the anchor are abbreviations of cardinal directions:
    ===== =====================
    value description
    ===== =====================
    'C' centered
    'SW' lower left corner
    'S' middle of bottom edge
    'SE' lower right corner
    etc.
    ===== =====================
    See `.set_anchor` for further details.
    share : bool, default: False
    If ``True``, apply the settings to all shared Axes.
    See Also
    --------
    mpl_toolkits.mplot3d.axes3d.Axes3D.set_box_aspect
    """
    if aspect != 'auto':
    raise NotImplementedError(
    "Axes3D currently only supports the aspect argument "
    f"'auto'. You passed in {aspect!r}."
    )
    if share:
    axes = {*self._shared_x_axes.get_siblings(self),
    *self._shared_y_axes.get_siblings(self),
    *self._shared_z_axes.get_siblings(self),
    }
    else:
    axes = {self}
    for ax in axes:
    ax._aspect = aspect
    ax.stale = True
    if anchor is not None:
    self.set_anchor(anchor, share=share)

    and
    def apply_aspect(self, position=None):
    if position is None:
    position = self.get_position(original=True)
    # in the superclass, we would go through and actually deal with axis
    # scales and box/datalim. Those are all irrelevant - all we need to do
    # is make sure our coordinate system is square.
    trans = self.get_figure().transSubfigure
    bb = mtransforms.Bbox.from_bounds(0, 0, 1, 1).transformed(trans)
    # this is the physical aspect of the panel (or figure):
    fig_aspect = bb.height / bb.width
    box_aspect = 1
    pb = position.frozen()
    pb1 = pb.shrunk_to_aspect(box_aspect, pb, fig_aspect)
    self._set_position(pb1.anchored(self.get_anchor(), pb), 'active')

    and likely
    def set_box_aspect(self, aspect, *, zoom=1):
    """
    Set the axes box aspect.
    The box aspect is the ratio of height to width in display
    units for each face of the box when viewed perpendicular to
    that face. This is not to be confused with the data aspect
    (which for Axes3D is always 'auto'). The default ratios are
    4:4:3 (x:y:z).
    To simulate having equal aspect in data space, set the box
    aspect to match your data range in each dimension.
    *zoom* controls the overall size of the Axes3D in the figure.
    Parameters
    ----------
    aspect : 3-tuple of floats or None
    Changes the physical dimensions of the Axes3D, such that the ratio
    of the axis lengths in display units is x:y:z.
    If None, defaults to 4:4:3
    zoom : float
    Control overall size of the Axes3D in the figure.
    """
    if aspect is None:
    aspect = np.asarray((4, 4, 3), dtype=float)
    else:
    orig_aspect = aspect
    aspect = np.asarray(aspect, dtype=float)
    if aspect.shape != (3,):
    raise ValueError(
    "You must pass a 3-tuple that can be cast to floats. "
    f"You passed {orig_aspect!r}"
    )
    # default scale tuned to match the mpl32 appearance.
    aspect *= 1.8294640721620434 * zoom / np.linalg.norm(aspect)
    self._box_aspect = aspect
    self.stale = True

    It is not obvious to me what the correct heuristics are to adjust the data limits / box aspect are under the different adjustable regimes (or if the adjustable values should be the same as in 2D?).

    I suspect that this is 10-30 (apply the standard factor of pi, but would also be very happy to be over estimating) hours of work (most of it reading the mplot3d code) to implement for someone who knows Python well.

  23. tacaswell commented on Jun 21, 2021

    @tacaswell
    Member
  24. logari81 commented on Aug 2, 2022

    @logari81

    all what axis('equal') should do is this:

    xlim = ax.get_xlim3d()
    ylim = ax.get_ylim3d()
    zlim = ax.get_zlim3d()
    ax.set_box_aspect((xlim[1]-xlim[0], ylim[1]-ylim[0], zlim[1]-zlim[0]))
    
  25. oscargus commented on Aug 2, 2022

    @oscargus
    Member

    There is a PR that hopefully will be included in 3.6. #23409

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

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions