8000 Consistent azim-elev-roll order with minimal changes · matplotlib/matplotlib@511ad71 · GitHub
[go: up one dir, main page]

Skip to content

Commit 511ad71

Browse files
committed
Consistent azim-elev-roll order with minimal changes
Change order of mplot3d view angles to azim-elev-roll, the order in which rotations occur, but keep the elev, azim positional order in view_init(), for backwards compatibility; use keyword arguments throughout, to avoid confusion: - in axes3d.py - in tests - in documentation - in examples Implement changes requested for PR #28395: - remove incohesive section from view_angles.rst - move some of the information to .mplot3d.axes3d.Axes3D.view_init - remove redundant kwargs as in .view_init(elev=elev, azim=azim, roll=roll) - cleanup test_axes3d_primary_views() - remove outdated next_whats_new item
1 parent 0b85e9b commit 511ad71

File tree

8 files changed

+135
-90
lines changed

8 files changed

+135
-90
lines changed

doc/api/toolkits/mplot3d/view_angles.rst

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ How to define the view angle
88
============================
99

8000
1010
The position of the viewport "camera" in a 3D plot is defined by three angles:
11-
*elevation*, *azimuth*, and *roll*. From the resulting position, it always
11+
*azimuth*, *elevation*, and *roll*. From the resulting position, it always
1212
points towards the center of the plot box volume. The angle direction is a
1313
common convention, and is shared with
1414
`PyVista <https://docs.pyvista.org/api/core/camera.html>`_ and
@@ -32,7 +32,7 @@ as well as roll, and all three angles can be set programmatically::
3232
Primary view planes
3333
===================
3434

35-
To look directly at the primary view planes, the required elevation, azimuth,
35+
To look directly at the primary view planes, the required azimuth, elevation,
3636
and roll angles are shown in the diagram of an "unfolded" plot below. These are
3737
further documented in the `.mplot3d.axes3d.Axes3D.view_init` API.
3838

galleries/examples/mplot3d/2dcollections3d.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,6 @@
4343

4444
# Customize the view angle so it's easier to see that the scatter points lie
4545
# on the plane y=0
46-
ax.view_init(elev=20., azim=-35, roll=0)
46+
ax.view_init(elev=20, azim=-35, roll=0)
4747

4848
plt.show()

galleries/examples/mplot3d/box3d.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@
6868
)
6969

7070
# Set zoom and angle view
71-
ax.view_init(40, -30, 0)
71+
ax.view_init(elev=40, azim=-30, roll=0)
7272
ax.set_box_aspect(None, zoom=0.9)
7373

7474
# Colorbar

galleries/examples/mplot3d/rotate_axes3d_sgskip.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -33,19 +33,19 @@
3333
angle_norm = (angle + 180) % 360 - 180
3434

3535
# Cycle through a full rotation of elevation, then azimuth, roll, and all
36-
elev = azim = roll = 0
36+
azim = elev = roll = 0
3737
if angle <= 360:
3838
elev = angle_norm
3939
elif angle <= 360*2:
4040
azim = angle_norm
4141
elif angle <= 360*3:
4242
roll = angle_norm
4343
else:
44-
elev = azim = roll = angle_norm
44+
azim = elev = roll = angle_norm
4545

4646
# Update the axis view and title
47-
ax.view_init(elev, azim, roll)
48-
plt.title('Elevation: %d°, Azimuth: %d°, Roll: %d°' % (elev, azim, roll))
47+
ax.view_init(elev=elev, azim=azim, roll=roll)
48+
plt.title('Azimuth: %d°, Elevation: %d°, Roll: %d°' % (azim, elev, roll))
4949

5050
plt.draw()
5151
plt.pause(.001)

galleries/examples/mplot3d/view_planes_3d.py

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
======================
55
66
This example generates an "unfolded" 3D plot that shows each of the primary 3D
7-
view planes. The elevation, azimuth, and roll angles required for each view are
7+
view planes. The azimuth, elevation, and roll angles required for each view are
88
labeled. You could print out this image and fold it into a box where each plane
99
forms a side of the box.
1010
"""
@@ -16,13 +16,13 @@ def annotate_axes(ax, text, fontsize=18):
1616
ax.text(x=0.5, y=0.5, z=0.5, s=text,
1717
va="center", ha="center", fontsize=fontsize, color="black")
1818

19-
# (plane, (elev, azim, roll))
20-
views = [('XY', (90, -90, 0)),
21-
('XZ', (0, -90, 0)),
22-
('YZ', (0, 0, 0)),
23-
('-XY', (-90, 90, 0)),
24-
('-XZ', (0, 90, 0)),
25-
('-YZ', (0, 180, 0))]
19+
# (plane, (azim, elev, roll))
20+
views = [('XY', (-90, 90, 0)),
21+
('XZ', (-90, 0, 0)),
22+
('YZ', (0, 0, 0)),
23+
('-XY', (90, -90, 0)),
24+
('-XZ', (90, 0, 0)),
25+
('-YZ', (180, 0, 0))]
2626

2727
layout = [['XY', '.', 'L', '.'],
2828
['XZ', 'YZ', '-XZ', '-YZ'],
@@ -34,10 +34,10 @@ def annotate_axes(ax, text, fontsize=18):
3434
axd[plane].set_ylabel('y')
3535
axd[plane].set_zlabel('z')
3636
axd[plane].set_proj_type('ortho')
37-
axd[plane].view_init(elev=angles[0], azim=angles[1], roll=angles[2])
37+
axd[plane].view_init(elev=angles[1], azim=angles[0], roll=angles[2])
3838
axd[plane].set_box_aspect(None, zoom=1.25)
3939

40-
label = f'{plane}\n{angles}'
40+
label = f'{plane}\nazim={angles[0]}\nelev={angles[1]}\nroll={angles[2]}'
4141
annotate_axes(axd[plane], label, fontsize=14)
4242

4343
for plane in ('XY', '-XY'):

lib/mpl_toolkits/mplot3d/axes3d.py

Lines changed: 84 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -139,7 +139,11 @@ def __init__(
139139

140140
# inhibit autoscale_view until the axes are defined
141141
# they can't be defined until Axes.__init__ has been called
142-
self.view_init(self.initial_elev, self.initial_azim, self.initial_roll)
142+
self.view_init(
143+
elev=self.initial_elev,
144+
azim=self.initial_azim,
145+
roll=self.initial_roll,
146+
)
143147

144148
self._sharez = sharez
145149
if sharez is not None:
@@ -1094,25 +1098,66 @@ def clabel(self, *args, **kwargs):
10941098
def view_init(self, elev=None, azim=None, roll=None, vertical_axis="z",
10951099
share=False):
10961100
"""
1097-
Set the elevation and azimuth of the Axes in degrees (not radians).
1101+
Set the azimuth, elevation, and roll of the Axes, in degrees (not radians).
10981102
10991103
This can be used to rotate the Axes programmatically.
11001104
1101-
To look normal to the primary planes, the following elevation and
1102-
azimuth angles can be used. A roll angle of 0, 90, 180, or 270 deg
1103-
will rotate these views while keeping the axes at right angles.
1105+
To look normal to the primary planes, the following azimuth and
1106+
elevation angles can be used:
11041107
11051108
========== ==== ====
1106-
view plane elev azim
1109+
view plane azim elev
11071110
========== ==== ====
1108-
XY 90 -90
1109-
XZ 0 -90
1110-
YZ 0 0
1111-
-XY -90 90
1112-
-XZ 0 90
1113-
-YZ 0 180
1111+
XY -90 90
1112+
XZ -90 0
1113+
YZ 0 0
1114+
-XY 90 -90
1115+
-XZ 90 0
1116+
-YZ 180 0
11141117
========== ==== ====
11151118
1119+
A roll angle of 0, 90, 180, or 270 degrees will rotate these views
1120+
while keeping the axes at right angles.
1121+
1122+
The *azim*, *elev*, *roll* angles correspond to rotations of the scene
1123+
observed by a stationary camera, as follows (assuming a default vertical
1124+
axis of 'z'). First, a left-handed rotation about the z axis is applied
1125+
(*azim*), then a right-handed rotation about the (camera) y axis (*elev*),
1126+
then a right-handed rotation about the (camera) x axis (*roll*). Here,
1127+
the z, y, and x axis are fixed axes (not the axes that rotate together
1128+
with the original scene).
1129+
1130+
If you would like to make the connection with quaternions (because
1131+
`Euler angles are horrible
1132+
<https://github.com/moble/quaternion/wiki/Euler-angles-are-horrible>`_):
1133+
the *azim*, *elev*, *roll* angles relate to the (intrinsic) rotation of
1134+
the plot via:
1135+
1136+
*q* = exp(+roll **x̂** / 2) exp(+elev **ŷ** / 2) exp(−azim **ẑ** / 2)
1137+
1138+
(with angles given in radians instead of degrees). That is, the angles
1139+
are a kind of `Tait-Bryan angles
1140+
<https://en.wikipedia.org/wiki/Euler_angles#Tait%E2%80%93Bryan_angles>`_:
1141+
−z, +y', +x", rather than classic `Euler angles
1142+
<https://en.wikipedia.org/wiki/Euler_angles>`_.
1143+
1144+
To avoid confusion, it makes sense to provide the view angles as keyword
1145+
arguments:
1146+
``.view_init(azim=-60, elev=30, roll=0, ...)``
1147+
This specific order is consistent with the order in which the rotations
1148+
actually are applied. Moreover, this particular order appears to be most
1149+
common, see :ghissue:`28353`, and it is consistent with the ordering in
1150+
`matplotlib.colors.LightSource`.
1151+
1152+
For backwards compatibility, positional arguments in the old sequence
1153+
(first ``elev``, then ``azim``) will still be accepted; but preferably,
1154+
use keyword arguments, to avoid confusion as to which angle is which.
1155+
Unfortunately, the order of the positional arguments does not match
1156+
the actual order of the applied rotations, and it differs from that
1157+
used in other programs (``azim, elev``). It would be nice if the sensible
1158+
(keyword) ordering could take over eventually.
1159+
1160+
11161161
Parameters
11171162
----------
11181163
elev : float, default: None
@@ -1145,10 +1190,10 @@ def view_init(self, elev=None, azim=None, roll=None, vertical_axis="z",
11451190

11461191
self._dist = 10 # The camera distance from origin. Behaves like zoom
11471192

1148-
if elev is None:
1149-
elev = self.initial_elev
11501193
if azim is None:
11511194
azim = self.initial_azim
1195+
if elev is None:
1196+
elev = self.initial_elev
11521197
if roll is None:
11531198
roll = self.initial_roll
11541199
vertical_axis = _api.check_getitem(
@@ -1163,8 +1208,8 @@ def view_init(self, elev=None, azim=None, roll=None, vertical_axis="z",
11631208
axes = [self]
11641209

11651210
for ax in axes:
1166-
ax.elev = elev
11671211
ax.azim = azim
1212+
ax.elev = elev
11681213
ax.roll = roll
11691214
ax._vertical_axis = vertical_axis
11701215

@@ -1229,15 +1274,15 @@ def get_proj(self):
12291274
# Look into the middle of the world coordinates:
12301275
R = 0.5 * box_aspect
12311276

1232-
# elev: elevation angle in the z plane.
12331277
# azim: azimuth angle in the xy plane.
1278+
# elev: elevation angle in the z plane.
12341279
# Coordinates for a point that rotates around the box of data.
12351280
# p0, p1 corresponds to rotating the box only around the vertical axis.
12361281
# p2 corresponds to rotating the box only around the horizontal axis.
1237-
elev_rad = np.deg2rad(self.elev)
12381282
azim_rad = np.deg2rad(self.azim)
1239-
p0 = np.cos(elev_rad) * np.cos(azim_rad)
1240-
p1 = np.cos(elev_rad) * np.sin(azim_rad)
1283+
elev_rad = np.deg2rad(self.elev)
1284+
p0 = np.cos(azim_rad) * np.cos(elev_rad)
1285+
p1 = np.sin(azim_rad) * np.cos(elev_rad)
12411286
p2 = np.sin(elev_rad)
12421287

12431288
# When changing vertical axis the coordinates changes as well.
@@ -1339,8 +1384,13 @@ def shareview(self, other):
13391384
self._shared_axes["view"].join(self, other)
13401385
self._shareview = other
13411386
vertical_axis = self._axis_names[other._vertical_axis]
1342-
self.view_init(elev=other.elev, azim=other.azim, roll=other.roll,
1343-
vertical_axis=vertical_axis, share=True)
1387+
self.view_init(
1388+
elev=other.elev,
1389+
azim=other.azim,
1390+
roll=other.roll,
1391+
vertical_axis=vertical_axis,
1392+
share=True,
1393+
)
13441394

13451395
def clear(self):
13461396
# docstring inherited.
@@ -1392,8 +1442,8 @@ def _set_view(self, view):
13921442
# docstring inherited
13931443
props, (elev, azim, roll) = view
13941444
self.set(**props)
1395-
self.elev = elev
13961445
self.azim = azim
1446+
self.elev = elev
13971447
self.roll = roll
13981448

13991449
def format_zdata(self, z):
@@ -1430,11 +1480,11 @@ def _rotation_coords(self):
14301480
"""
14311481
Return the rotation angles as a string.
14321482
"""
1433-
norm_elev = art3d._norm_angle(self.elev)
14341483
norm_azim = art3d._norm_angle(self.azim)
1484+
norm_elev = art3d._norm_angle(self.elev)
14351485
norm_roll = art3d._norm_angle(self.roll)
1436-
coords = (f"elevation={norm_elev:.0f}\N{DEGREE SIGN}, "
1437-
f"azimuth={norm_azim:.0f}\N{DEGREE SIGN}, "
1486+
coords = (f"azimuth={norm_azim:.0f}\N{DEGREE SIGN}, "
1487+
f"elevation={norm_elev:.0f}\N{DEGREE SIGN}, "
14381488
f"roll={norm_roll:.0f}\N{DEGREE SIGN}"
14391489
).replace("-", "\N{MINUS SIGN}")
14401490
return coords
@@ -1561,10 +1611,10 @@ def _on_move(self, event):
15611611
return
15621612

15631613
# Convert to quaternion
1564-
elev = np.deg2rad(self.elev)
15651614
azim = np.deg2rad(self.azim)
1615+
elev = np.deg2rad(self.elev)
15661616
roll = np.deg2rad(self.roll)
1567-
q = _Quaternion.from_cardan_angles(elev, azim, roll)
1617+
q = _Quaternion.from_cardan_angles(azim, elev, roll)
15681618

15691619
# Update quaternion - a variation on Ken Shoemake's ARCBALL
15701620
current_vec = self._arcball(self._sx/w, self._sy/h)
@@ -1573,18 +1623,13 @@ def _on_move(self, event):
15731623
q = dq * q
15741624

15751625
# Convert to elev, azim, roll
1576-
elev, azim, roll = q.as_cardan_angles()
1626+
azim, elev, roll = q.as_cardan_angles()
15771627
azim = np.rad2deg(azim)
15781628
elev = np.rad2deg(elev)
15791629
roll = np.rad2deg(roll)
15801630
vertical_axis = self._axis_names[self._vertical_axis]
1581-
self.view_init(
1582-
elev=elev,
1583-
azim=azim,
1584-
roll=roll,
1585-
vertical_axis=vertical_axis,
1586-
share=True,
1587-
)
1631+
self.view_init(elev, azim, roll, vertical_axis=vertical_axis,
1632+
share=True)
15881633
self.stale = True
15891634

15901635
# Pan
@@ -3662,10 +3707,10 @@ def _extract_errs(err, data, lomask, himask):
36623707
quiversize = np.mean(np.diff(quiversize, axis=0))
36633708
# quiversize is now in Axes coordinates, and to convert back to data
36643709
# coordinates, we need to run it through the inverse 3D transform. For
3665-
# consistency, this uses a fixed elevation, azimuth, and roll.
3710+
# consistency, this uses a fixed azimuth, elevation, and roll.
36663711
with cbook._setattr_cm(self, elev=0, azim=0, roll=0):
36673712
invM = np.linalg.inv(self.get_proj())
3668-
# elev=azim=roll=0 produces the Y-Z plane, so quiversize in 2D 'x' is
3713+
# azim=elev=roll=0 produces the Y-Z plane, so quiversize in 2D 'x' is
36693714
# 'y' in 3D, hence the 1 index.
36703715
quiversize = np.dot(invM, [quiversize, 0, 0, 0])[1]
36713716
# Quivers use a fixed 15-degree arrow head, so scale up the length so
@@ -4000,7 +4045,7 @@ def rotate_from_to(cls, r1, r2):
40004045
return q
40014046

40024047
@classmethod
4003-
def from_cardan_angles(cls, elev, azim, roll):
4048+
def from_cardan_angles(cls, azim, elev, roll):
40044049
"""
40054050
Converts the angles to a quaternion
40064051
q = exp((roll/2)*e_x)*exp((elev/2)*e_y)*exp((-azim/2)*e_z)
@@ -4027,4 +4072,4 @@ def as_cardan_angles(self):
40274072
azim = np.arctan2(2*(-qw*qz+qx*qy), qw*qw+qx*qx-qy*qy-qz*qz)
40284073
elev = np.arcsin( 2*( qw*qy+qz*qx)/(qw*qw+qx*qx+qy*qy+qz*qz)) # noqa E201
40294074
roll = np.arctan2(2*( qw*qx-qy*qz), qw*qw-qx*qx-qy*qy+qz*qz) # noqa E201
4030-
return elev, azim, roll
4075+
return azim, elev, roll

lib/mpl_toolkits/mplot3d/tests/test_art3d.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,9 @@ def test_scatter_3d_projection_conservation():
1010
fig = plt.figure()
1111
ax = fig.add_subplot(projection='3d')
1212
# fix axes3d projection
13-
ax.roll = 0
14-
ax.elev = 0
1513
ax.azim = -45
14+
ax.elev = 0
15+
ax.roll = 0
1616
ax.stale = True
1717

1818
x = [0, 1, 2, 3, 4]

0 commit comments

Comments
 (0)
0