@@ -54,7 +54,7 @@ class Axes3D(Axes):
54
54
55
55
def __init__ (
56
56
self , fig , rect = None , * args ,
57
- azim = - 60 , elev = 30 , sharez = None , proj_type = 'persp' ,
57
+ elev = 30 , azim = - 60 , roll = 0 , sharez = None , proj_type = 'persp' ,
58
58
box_aspect = None , computed_zorder = True ,
59
59
** kwargs ):
60
60
"""
@@ -64,10 +64,12 @@ def __init__(
64
64
The parent figure.
65
65
rect : (float, float, float, float)
66
66
The ``(left, bottom, width, height)`` axes position.
67
- azim : float, default: -60
68
- Azimuthal viewing angle.
69
67
elev : float, default: 30
70
68
Elevation viewing angle.
69
+ azim : float, default: -60
70
+ Azimuthal viewing angle.
71
+ roll : float, default: 0
72
+ Roll viewing angle.
71
73
sharez : Axes3D, optional
72
74
Other axes to share z-limits with.
73
75
proj_type : {'persp', 'ortho'}
@@ -101,6 +103,7 @@ def __init__(
101
103
102
104
self .initial_azim = azim
103
105
self .initial_elev = elev
106
+ self .initial_roll = roll
104
107
self .set_proj_type (proj_type )
105
108
self .computed_zorder = computed_zorder
106
109
@@ -112,7 +115,7 @@ def __init__(
112
115
113
116
# inhibit autoscale_view until the axes are defined
114
117
# they can't be defined until Axes.__init__ has been called
115
- self .view_init (self .initial_elev , self .initial_azim )
118
+ self .view_init (self .initial_elev , self .initial_azim , self . initial_roll )
116
119
117
120
self ._sharez = sharez
118
121
if sharez is not None :
@@ -976,7 +979,7 @@ def clabel(self, *args, **kwargs):
976
979
"""Currently not implemented for 3D axes, and returns *None*."""
977
980
return None
978
981
979
- def view_init (self , elev = None , azim = None , vertical_axis = "z" ):
982
+ def view_init (self , elev = None , azim = None , roll = None , vertical_axis = "z" ):
980
983
"""
981
984
Set the elevation and azimuth of the axes in degrees (not radians).
982
985
@@ -992,6 +995,10 @@ def view_init(self, elev=None, azim=None, vertical_axis="z"):
992
995
The azimuth angle in the horizontal plane in degrees.
993
996
If None then the initial value as specified in the `Axes3D`
994
997
constructor is used.
998
+ roll : float, default: None
999
+ The roll angle about the viewing direction in degrees.
1000
+ If None then the initial value as specified in the `Axes3D`
1001
+ constructor is used.
995
1002
vertical_axis : {"z", "x", "y"}, default: "z"
996
1003
The axis to align vertically. *azim* rotates about this axis.
997
1004
"""
@@ -1008,6 +1015,11 @@ def view_init(self, elev=None, azim=None, vertical_axis="z"):
1008
1015
else :
1009
1016
self .azim = azim
1010
1017
1018
+ if roll is None :
1019
+ self .roll = self .initial_roll
1020
+ else :
1021
+ self .roll = roll
1022
+
1011
1023
self ._vertical_axis = _api .check_getitem (
1012
1024
dict (x = 0 , y = 1 , z = 2 ), vertical_axis = vertical_axis
1013
1025
)
@@ -1046,8 +1058,10 @@ def get_proj(self):
1046
1058
1047
1059
# elev stores the elevation angle in the z plane
1048
1060
# azim stores the azimuth angle in the x,y plane
1049
- elev_rad = np .deg2rad (self .elev )
1050
- azim_rad = np .deg2rad (self .azim )
1061
+ # roll stores the roll angle about the view axis
1062
+ elev_rad = np .deg2rad (art3d ._norm_angle (self .elev ))
1063
+ azim_rad = np .deg2rad (art3d ._norm_angle (self .azim ))
1064
+ roll_rad = np .deg2rad (art3d ._norm_angle (self .roll ))
1051
1065
1052
1066
# Coordinates for a point that rotates around the box of data.
1053
1067
# p0, p1 corresponds to rotating the box only around the
@@ -1077,7 +1091,7 @@ def get_proj(self):
1077
1091
V = np .zeros (3 )
1078
1092
V [self ._vertical_axis ] = - 1 if abs (elev_rad ) > 0.5 * np .pi else 1
1079
1093
1080
- viewM = proj3d .view_transformation (eye , R , V )
1094
+ viewM = proj3d .view_transformation (eye , R , V , roll_rad )
1081
1095
projM = self ._projection (- self .dist , self .dist )
1082
1096
M0 = np .dot (viewM , worldM )
1083
1097
M = np .dot (projM , M0 )
@@ -1165,14 +1179,15 @@ def _button_release(self, event):
1165
1179
def _get_view (self ):
1166
1180
# docstring inherited
1167
1181
return (self .get_xlim (), self .get_ylim (), self .get_zlim (),
1168
- self .elev , self .azim )
1182
+ self .elev , self .azim , self . roll )
1169
1183
1170
1184
def _set_view (self , view ):
1171
1185
# docstring inherited
1172
- xlim , ylim , zlim , elev , azim = view
1186
+ xlim , ylim , zlim , elev , azim , roll = view
1173
1187
self .set (xlim = xlim , ylim = ylim , zlim = zlim )
1174
1188
self .elev = elev
1175
1189
self .azim = azim
1190
+ self .roll = roll
1176
1191
1177
1192
def format_zdata (self , z ):
1178
1193
"""
@@ -1199,8 +1214,12 @@ def format_coord(self, xd, yd):
1199
1214
1200
1215
if self .button_pressed in self ._rotate_btn :
1201
1216
# ignore xd and yd and display angles instead
1202
- return (f"azimuth={ self .azim :.0f} \N{DEGREE SIGN} , "
1203
- f"elevation={ self .elev :.0f} \N{DEGREE SIGN} "
1217
+ norm_elev = art3d ._norm_angle (self .elev )
1218
+ norm_azim = art3d ._norm_angle (self .azim )
1219
+ norm_roll = art3d ._norm_angle (self .roll )
1220
+ return (f"elevation={ norm_elev :.0f} \N{DEGREE SIGN} , "
1221
+ f"azimuth={ norm_azim :.0f} \N{DEGREE SIGN} , "
1222
+ f"roll={ norm_roll :.0f} \N{DEGREE SIGN} "
1204
1223
).replace ("-" , "\N{MINUS SIGN} " )
1205
1224
1206
1225
# nearest edge
@@ -1253,8 +1272,12 @@ def _on_move(self, event):
1253
1272
# get the x and y pixel coords
1254
1273
if dx == 0 and dy == 0 :
1255
1274
return
1256
- self .elev = art3d ._norm_angle (self .elev - (dy / h )* 180 )
1257
- self .azim = art3d ._norm_angle (self .azim - (dx / w )* 180 )
1275
+
1276
+ roll = np .deg2rad (self .roll )
1277
+ delev = - (dy / h )* 180 * np .cos (roll ) + (dx / w )* 180 * np .sin (roll )
1278
+ dazim = - (dy / h )* 180 * np .sin (roll ) - (dx / w )* 180 * np .cos (roll )
1279
+ self .elev = self .elev + delev
1280
+ self .azim = self .azim + dazim
1258
1281
self .get_proj ()
1259
1282
self .stale = True
1260
1283
self .figure .canvas .draw_idle ()
@@ -1267,7 +1290,8 @@ def _on_move(self, event):
1267
1290
minx , maxx , miny , maxy , minz , maxz = self .get_w_lims ()
1268
1291
dx = 1 - ((w - dx )/ w )
1269
1292
dy = 1 - ((h - dy )/ h )
1270
- elev , azim = np .deg2rad (self .elev ), np .deg2rad (self .azim )
1293
+ elev = np .deg2rad (self .elev )
1294
+ azim = np .deg2rad (self .azim )
1271
1295
# project xv, yv, zv -> xw, yw, zw
1272
1296
dxx = (maxx - minx )* (dy * np .sin (elev )* np .cos (azim ) + dx * np .sin (azim ))
1273
1297
dyy = (maxy - miny )* (- dx * np .cos (azim ) + dy * np .sin (elev )* np .sin (azim ))
@@ -3249,11 +3273,11 @@ def _extract_errs(err, data, lomask, himask):
3249
3273
quiversize = np .mean (np .diff (quiversize , axis = 0 ))
3250
3274
# quiversize is now in Axes coordinates, and to convert back to data
3251
3275
# coordinates, we need to run it through the inverse 3D transform. For
3252
- # consistency, this uses a fixed azimuth and elevation .
3253
- with cbook ._setattr_cm (self , azim = 0 , elev = 0 ):
3276
+ # consistency, this uses a fixed elevation, azimuth, and roll .
3277
+ with cbook ._setattr_cm (self , elev = 0 , azim = 0 , roll = 0 ):
3254
3278
invM = np .linalg .inv (self .get_proj ())
3255
- # azim=elev =0 produces the Y-Z plane, so quiversize in 2D 'x' is 'y' in
3256
- # 3D, hence the 1 index.
3279
+ # elev= azim=roll =0 produces the Y-Z plane, so quiversize in 2D 'x' is
3280
+ # 'y' in 3D, hence the 1 index.
3257
3281
quiversize = np .dot (invM , np .array ([quiversize , 0 , 0 , 0 ]))[1 ]
3258
3282
# Quivers use a fixed 15-degree arrow head, so scale up the length so
3259
3283
# that the size corresponds to the base. In other words, this constant
0 commit comments