@@ -55,7 +55,7 @@ class Axes3D(Axes):
55
55
56
56
def __init__ (
57
57
self , fig , rect = None , * args ,
58
- azim = - 60 , elev = 30 , sharez = None , proj_type = 'persp' ,
58
+ elev = 30 , azim = - 60 , roll = 0 , sharez = None , proj_type = 'persp' ,
59
59
box_aspect = None , computed_zorder = True ,
60
60
** kwargs ):
61
61
"""
@@ -65,10 +65,19 @@ def __init__(
65
65
The parent figure.
66
66
rect : (float, float, float, float)
67
67
The ``(left, bottom, width, height)`` axes position.
68
- azim : float, default: -60
69
- Azimuthal viewing angle.
70
68
elev : float, default: 30
71
- Elevation viewing angle.
69
+ The elevation angle in degrees rotates the camera above and below
70
+ the x-y plane, with a positive angle corresponding to a location
71
+ above the plane.
72
+ azim : float, default: -60
73
+ The azimuthal angle in degrees rotates the camera about the z axis,
74
+ with a positive angle corresponding to a right-handed rotation. In
75
+ other words, a positive azimuth rotates the camera about the origin
76
+ from its location along the +x axis towards the +y axis.
77
+ roll : float, default: 0
78
+ The roll angle in degrees rotates the camera about the viewing
79
+ axis. A positive angle spins the camera clockwise, causing the
80
+ scene to rotate counter-clockwise.
72
81
sharez : Axes3D, optional
73
82
Other axes to share z-limits with.
74
83
proj_type : {'persp', 'ortho'}
@@ -102,6 +111,7 @@ def __init__(
102
111
103
112
self .initial_azim = azim
104
113
self .initial_elev = elev
114
+ self .initial_roll = roll
105
115
self .set_proj_type (proj_type )
106
116
self .computed_zorder = computed_zorder
107
117
@@ -113,7 +123,7 @@ def __init__(
113
123
114
124
# inhibit autoscale_view until the axes are defined
115
125
# they can't be defined until Axes.__init__ has been called
116
- self .view_init (self .initial_elev , self .initial_azim )
126
+ self .view_init (self .initial_elev , self .initial_azim , self . initial_roll )
117
127
118
128
self ._sharez = sharez
119
129
if sharez is not None :
@@ -983,7 +993,7 @@ def clabel(self, *args, **kwargs):
983
993
"""Currently not implemented for 3D axes, and returns *None*."""
984
994
return None
985
995
986
- def view_init (self , elev = None , azim = None , vertical_axis = "z" ):
996
+ def view_init (self , elev = None , azim = None , roll = None , vertical_axis = "z" ):
987
997
"""
988
998
Set the elevation and azimuth of the axes in degrees (not radians).
989
999
@@ -992,12 +1002,26 @@ def view_init(self, elev=None, azim=None, vertical_axis="z"):
992
1002
Parameters
993
1003
----------
994
1004
elev : float, default: None
995
- The elevation angle in the vertical plane in degrees.
996
- If None then the initial value as specified in the `Axes3D`
1005
+ The elevation angle in degrees rotates the camera above the plane
1006
+ pierced by the vertical axis, with a positive angle corresponding
1007
+ to a location above that plane. For example, with the default
1008
+ vertical axis of 'z', the elevation defines the angle of the camera
1009
+ location above the x-y plane.
1010
+ If None, then the initial value as specified in the `Axes3D`
997
1011
constructor is used.
998
1012
azim : float, default: None
999
- The azimuth angle in the horizontal plane in degrees.
1000
- If None then the initial value as specified in the `Axes3D`
1013
+ The azimuthal angle in degrees rotates the camera about the
1014
+ vertical axis, with a positive angle corresponding to a
1015
+ right-handed rotation. For example, with the default vertical axis
1016
+ of 'z', a positive azimuth rotates the camera about the origin from
1017
+ its location along the +x axis towards the +y axis.
1018
+ If None, then the initial value as specified in the `Axes3D`
1019
+ constructor is used.
1020
+ roll : float, default: None
1021
+ The roll angle in degrees rotates the camera about the viewing
1022
+ axis. A positive angle spins the camera clockwise, causing the
1023
+ scene to rotate counter-clockwise.
1024
+ If None, then the initial value as specified in the `Axes3D`
1001
1025
constructor is used.
1002
1026
vertical_axis : {"z", "x", "y"}, default: "z"
1003
1027
The axis to align vertically. *azim* rotates about this axis.
@@ -1015,6 +1039,11 @@ def view_init(self, elev=None, azim=None, vertical_axis="z"):
1015
1039
else :
1016
1040
self .azim = azim
1017
1041
1042
+ if roll is None :
1043
+ self .roll = self .initial_roll
1044
+ else :
1045
+ self .roll = roll
1046
+
1018
1047
self ._vertical_axis = _api .check_getitem (
1019
1048
dict (x = 0 , y = 1 , z = 2 ), vertical_axis = vertical_axis
1020
1049
)
@@ -1053,8 +1082,10 @@ def get_proj(self):
1053
1082
1054
1083
# elev stores the elevation angle in the z plane
1055
1084
# azim stores the azimuth angle in the x,y plane
1056
- elev_rad = np .deg2rad (self .elev )
1057
- azim_rad = np .deg2rad (self .azim )
1085
+ # roll stores the roll angle about the view axis
1086
+ elev_rad = np .deg2rad (art3d ._norm_angle (self .elev ))
1087
+ azim_rad = np .deg2rad (art3d ._norm_angle (self .azim ))
1088
+ roll_rad = np .deg2rad (art3d ._norm_angle (self .roll ))
1058
1089
1059
1090
# Coordinates for a point that rotates around the box of data.
1060
1091
# p0, p1 corresponds to rotating the box only around the
@@ -1084,7 +1115,7 @@ def get_proj(self):
1084
1115
V = np .zeros (3 )
1085
1116
V [self ._vertical_axis ] = - 1 if abs (elev_rad ) > 0.5 * np .pi else 1
1086
1117
1087
- viewM = proj3d .view_transformation (eye , R , V )
1118
+ viewM = proj3d .view_transformation (eye , R , V , roll_rad )
1088
1119
projM = self ._projection (- self .dist , self .dist )
1089
1120
M0 = np .dot (viewM , worldM )
1090
1121
M = np .dot (projM , M0 )
@@ -1172,14 +1203,15 @@ def _button_release(self, event):
1172
1203
def _get_view (self ):
1173
1204
# docstring inherited
1174
1205
return (self .get_xlim (), self .get_ylim (), self .get_zlim (),
1175
- self .elev , self .azim )
1206
+ self .elev , self .azim , self . roll )
1176
1207
1177
1208
def _set_view (self , view ):
1178
1209
# docstring inherited
1179
- xlim , ylim , zlim , elev , azim = view
1210
+ xlim , ylim , zlim , elev , azim , roll = view
1180
1211
self .set (xlim = xlim , ylim = ylim , zlim = zlim )
1181
1212
self .elev = elev
1182
1213
self .azim = azim
1214
+ self .roll = roll
1183
1215
1184
1216
def format_zdata (self , z ):
1185
1217
"""
@@ -1206,8 +1238,12 @@ def format_coord(self, xd, yd):
1206
1238
1207
1239
if self .button_pressed in self ._rotate_btn :
1208
1240
# ignore xd and yd and display angles instead
1209
- return (f"azimuth={ self .azim :.0f} \N{DEGREE SIGN} , "
1210
- f"elevation={ self .elev :.0f} \N{DEGREE SIGN} "
1241
+ norm_elev = art3d ._norm_angle (self .elev )
1242
+ norm_azim = art3d ._norm_angle (self .azim )
1243
+ norm_roll = art3d ._norm_angle (self .roll )
1244
+ return (f"elevation={ norm_elev :.0f} \N{DEGREE SIGN} , "
1245
+ f"azimuth={ norm_azim :.0f} \N{DEGREE SIGN} , "
1246
+ f"roll={ norm_roll :.0f} \N{DEGREE SIGN} "
1211
1247
).replace ("-" , "\N{MINUS SIGN} " )
1212
1248
1213
1249
# nearest edge
@@ -1260,8 +1296,12 @@ def _on_move(self, event):
1260
1296
# get the x and y pixel coords
1261
1297
if dx == 0 and dy == 0 :
1262
1298
return
1263
- self .elev = art3d ._norm_angle (self .elev - (dy / h )* 180 )
1264
- self .azim = art3d ._norm_angle (self .azim - (dx / w )* 180 )
1299
+
1300
+ roll = np .deg2rad (self .roll )
1301
+ delev = - (dy / h )* 180 * np .cos (roll ) + (dx / w )* 180 * np .sin (roll )
1302
+ dazim = - (dy / h )* 180 * np .sin (roll ) - (dx / w )* 180 * np .cos (roll )
1303
+ self .elev = self .elev + delev
1304
+ self .azim = self .azim + dazim
1265
1305
self .get_proj ()
1266
1306
self .stale = True
1267
1307
self .figure .canvas .draw_idle ()
@@ -1274,7 +1314,8 @@ def _on_move(self, event):
1274
1314
minx , maxx , miny , maxy , minz , maxz = self .get_w_lims ()
1275
1315
dx = 1 - ((w - dx )/ w )
1276
1316
dy = 1 - ((h - dy )/ h )
1277
- elev , azim = np .deg2rad (self .elev ), np .deg2rad (self .azim )
1317
+ elev = np .deg2rad (self .elev )
1318
+ azim = np .deg2rad (self .azim )
1278
1319
# project xv, yv, zv -> xw, yw, zw
1279
1320
dxx = (maxx - minx )* (dy * np .sin (elev )* np .cos (azim ) + dx * np .sin (azim ))
1280
1321
dyy = (maxy - miny )* (- dx * np .cos (azim ) + dy * np .sin (elev )* np .sin (azim ))
@@ -3256,11 +3297,11 @@ def _extract_errs(err, data, lomask, himask):
3256
3297
quiversize = np .mean (np .diff (quiversize , axis = 0 ))
3257
3298
# quiversize is now in Axes coordinates, and to convert back to data
3258
3299
# coordinates, we need to run it through the inverse 3D transform. For
3259
- # consistency, this uses a fixed azimuth and elevation .
3260
- with cbook ._setattr_cm (self , azim = 0 , elev = 0 ):
3300
+ # consistency, this uses a fixed elevation, azimuth, and roll .
3301
+ with cbook ._setattr_cm (self , elev = 0 , azim = 0 , roll = 0 ):
3261
3302
invM = np .linalg .inv (self .get_proj ())
3262
- # azim=elev =0 produces the Y-Z plane, so quiversize in 2D 'x' is 'y' in
3263
- # 3D, hence the 1 index.
3303
+ # elev= azim=roll =0 produces the Y-Z plane, so quiversize in 2D 'x' is
3304
+ # 'y' in 3D, hence the 1 index.
3264
3305
quiversize = np .dot (invM , np .array ([quiversize , 0 , 0 , 0 ]))[1 ]
3265
3306
# Quivers use a fixed 15-degree arrow head, so scale up the length so
3266
3307
# that the size corresponds to the base. In other words, this constant
0 commit comments