GeezerVison is showing github.com — 3d plots with aspect='equal' · Issue #1077 · matplotlib/matplotlib
3d plots with aspect='equal' #1077Descriptionaspect='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.
Reacted by Seunghoon Park, Juan Luis Cano Rodríguez, Samuel Ainsworth, Pavel Haluza, Tomas Petricek, morriswmz, Bas Nijholt, Daniel Sank, Tianye Li, barabanus and 25 more Reacted by Tomomas ActivityWeatherGod commented on Aug 20, 2012 Reacted by Thomas A Caswell Reacted by Danny Wang, Mateen Ulhaq, shengdie, tazr, chuyicui, Yuchen Zhong, Daniel Soutar, Morten Lind, Salih Karagoz, Jimmy Paulos and 39 more akhmerov commented on Aug 20, 2012 ContributorAuthor More actions 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. Reacted by Samuel Ainsworth, Peter Dijkstra, Alex, chuyicui, barabanus, Javier Dehesa, Daniel Soutar, Morten Lind, Coelancanth, Tal Golan and 49 more Reacted by Roman Kovtyukh, Tim, Rafael de la Fuente Herrezuelo, Pan Yu and BUGMAN Reacted by Pan Yu vallettea commented on Jan 23, 2013 I would also be interested by this feature.
By the way, did you found a way to tackle the problem? astrofrog commented on Feb 24, 2013 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:

and adding
then gives

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

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) Reacted by Andrew, Juan Luis Cano Rodríguez, Nick LaFarge, Samuel Ainsworth, Peter Dijkstra, Mateen Ulhaq, Jonathan King, darkdragon-001, barabanus, Daniel Soutar and 13 more tehingo commented on 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. Reacted by Mateen Ulhaq, barabanus, chtapodi, Thomas Mansencal, Juliano Ferrari Gianlupi, Jacek Grzybowski, RachelChen, Max Schwendeler, laurap81, ZhouWJ and 7 more akhmerov commented on Mar 25, 2013 ContributorAuthor More actions 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. Reacted by gfant and Nico Gastellu Hanlin-Dong commented on Jan 6, 2016 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 commented on Jan 6, 2016 ContributorAuthor More actions @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 commented on Jan 18, 2016 samuela commented on May 6, 2018 
I just got this plot after calling ax.set_aspect('equal'). Definitely not equal based on the ticks! timhoffm commented on May 6, 2018 Related and possibly a first step for fixing this: #8896 Reacted by Anton Akhmerov jklymak commented on Mar 3, 2019 Closing due to #13474 and the fact that the non-optimal behaviour is discussed in previous issues and PRs references above... akhmerov commented on Mar 3, 2019 ContributorAuthor More actions @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? Reacted by Corey, Mateen Ulhaq, Daniel Soutar, bokorn, jkfindeisen, batbone, Alan Covarrubias, Lars Funke and Dazhi Cheng solarjoe commented on Mar 3, 2019 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. Reacted by Anton Akhmerov jklymak commented on Mar 3, 2019 Correct. OTOH I don't know of anyone actively developing mplot3d, so if someone wants this feature, it would require a volunteer.. zh-plus commented on Aug 14, 2019 solarjoe commented on Aug 14, 2019 Reacted by Alfredo Canziani jondo commented on Jun 2, 2021 In order to get a future notification of aspect='equal' working, which open issue (or unmerged PR) should I subscribe to? Reacted by Samuel Ainsworth tacaswell commented on Jun 3, 2021 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 commented on Jun 21, 2021 logari81 commented on 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]))
Reacted by Joao Cunha, Yunfeng Wang, lkbarth, vadimcn, alient12 and Jesse Mehami oscargus commented on Aug 2, 2022 There is a PR that hopefully will be included in 3.6. #23409
MetadataMetadataDevelopmentNo branches or pull requestsIssue actions
|