8000 3d plots with aspect='equal' · Issue #1077 · matplotlib/matplotlib · GitHub
[go: up one dir, main page]

Skip to content
8000

3d plots with aspect='equal' #1077

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

Closed
akhmerov opened this issue Aug 12, 2012 · 30 comments
Closed

3d plots with aspect='equal' #1077

akhmerov opened this issue Aug 12, 2012 · 30 comments

Comments

@akhmerov
Copy link
Contributor

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.
@WeatherGod
Copy link
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!

@akhmerov
Copy link
Contributor Author

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.

@vallettea
Copy link

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

@astrofrog
Copy link
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)

@tehingo
Copy link
tehingo commented Mar 25, 2013

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.

@akhmerov
Copy link
Contributor Author

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.

@tacaswell tacaswell added this to the v1.4.x milestone Aug 18, 2014
@tacaswell tacaswell modified the milestones: v1.4.x, unassigned Aug 18, 2014
@Hanlin-Dong
Copy link

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.

@akhmerov
Copy link
Contributor Author
akhmerov commented Jan 6, 2016

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

@solarjoe
Copy link

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.

@samuela
Copy link
samuela commented May 6, 2018

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!

@timhoffm
Copy link
Member
timhoffm commented May 6, 2018

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

@samuela
Copy link
samuela commented May 6, 2018

I've also heard that there's a proposed fix (workaround?) in this branch: https://github.com/WeatherGod/matplotlib/tree/mplot3d/pbaspect. Not sure what the status of that is.

@samuela
Copy link
samuela commented May 7, 2018

cc @nfoti

@YodaEmbedding
Copy link
YodaEmbedding commented Jun 3, 2018

Just thought I'd chime in with my own SO post.

As shown below, a square does not look like a square:

@dstansby
Copy link
Member

Given that setting aspect to 'equal' doesn't work on 3D axes at the moment, would a sensible course of action in the meantime be to at least raise a warning, and maybe a not implemented error when the user attempts to set a 3D axes to aspect='equal'?

@timhoffm
Copy link
Member

👍 a warning would be reasonable and sufficient.

@jklymak jklymak modified the milestones: unassigned, v3.1 Jul 26, 2018
@samuela
Copy link
samuela commented Jul 26, 2018

@dstansby My vote is for a NotImplementedError. This is a dangerous footgun and should not be allowed as long as it's not actually implemented.

@droundy
Copy link
droundy commented Feb 20, 2019

With regard to the objection that we can't have "aspect equal" without requiring a specific projection, I'd like to point out that under any projection a sphere looks like a circle, which should sufficiently define "aspect equal" in any case.

@jklymak
Copy link
Member
jklymak commented 8000 Mar 3, 2019

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

@zh-plus

This comment has been minimized.

@solarjoe
Copy link

The original link is https://stackoverflow.com/questions/13685386/matplotlib-equal-unit-length-with-equal-aspect-ratio-z-axis-is-not-equal-to and has already been mentioned above.

aarondettmann added a commit to airinnova/pytornado that referenced this issue Oct 18, 2019
* Method set_aspect('equal') does not work in latest version of 'matplotlib'
* See: matplotlib/matplotlib#1077
aarondettmann added a commit to cfsengineering/CEASIOMpy that referenced this issue Oct 18, 2019
We forced the use of matplotlib version 3.0.2 because newer versions of
matplotlib would raise a 'NotImplementedError' if a set_aspect('equal')
method was called.

There is no module which relies on said specific version anymore. We can therefore
relax the requirement. More on the set_aspect() issue in matplotlib:

matplotlib/matplotlib#1077

I also added 'tigl3' as an explicit dependency (again). I think all
packages which are explicitly imported/used should be listed in
``environment.yml``, otherwise it becomes hard to track what is required
and what is not. See also the note I made in the ``environment.yml`` file.
Feel free to comment on this, if you disagree :)
ftherrien pushed a commit to ftherrien/p2ptrans that referenced this issue Jan 15, 2020
The NotImplemented error was caused by the fact that set_aspect now causes an error in matplotlib (see matplotlib/matplotlib#1077 and matplotlib/matplotlib#13474). Fixed by making the figure itself square such that the 3D axis are square.

Also change the fortan module's name from p2ptrans to fmodules which prevented from importing p2ptrans as a python module.
@jondo
Copy link
jondo commented Jun 2, 2021

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

@tacaswell
Copy link
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.

@tacaswell
Copy link
Member

@logari81
Copy link
logari81 commented Aug 2, 2022

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

@oscargus
Copy link
Member
oscargus commented Aug 2, 2022

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
Projects
None yet
Development

No branches or pull requests

0