@@ -157,7 +157,7 @@ def __init__(
157
157
self .fmt_zdata = None
158
158
159
159
self .mouse_init ()
160
- self .figure .canvas .callbacks ._connect_picklable (
160
+ self ._move_cid = self . figure .canvas .callbacks ._connect_picklable (
161
161
'motion_notify_event' , self ._on_move )
162
162
self .figure .canvas .callbacks ._connect_picklable (
163
163
'button_press_event' , self ._button_press )
@@ -924,18 +924,14 @@ def disable_mouse_rotation(self):
924
924
def can_zoom (self ):
925
925
"""
926
926
Return whether this Axes supports the zoom box button functionality.
927
-
928
- Axes3D objects do not use the zoom box button.
929
927
"""
930
- return False
928
+ return True
931
929
932
930
def can_pan (self ):
933
931
"""
934
- Return whether this Axes supports the pan/zoom button functionality.
935
-
936
- Axes3d objects do not use the pan/zoom button.
932
+ Return whether this Axes supports the pan button functionality.
937
933
"""
938
- return False
934
+ return True
939
935
940
936
def cla (self ):
941
937
# docstring inherited.
@@ -1055,6 +1051,11 @@ def _on_move(self, event):
1055
1051
if not self .button_pressed :
1056
1052
return
1057
1053
1054
+ if self .get_navigate_mode () is not None :
1055
+ # we don't want to rotate if we are zooming/panning
1056
+ # from the toolbar
1057
+ return
1058
+
1058
1059
if self .M is None :
1059
1060
return
1060
1061
@@ -1066,7 +1067,6 @@ def _on_move(self, event):
1066
1067
dx , dy = x - self .sx , y - self .sy
1067
1068
w = self ._pseudo_w
1068
1069
h = self ._pseudo_h
1069
- self .sx , self .sy = x , y
1070
1070
1071
1071
# Rotation
1072
1072
if self .button_pressed in self ._rotate_btn :
@@ -1082,28 +1082,14 @@ def _on_move(self, event):
1082
1082
self .azim = self .azim + dazim
1083
1083
self .get_proj ()
1084
1084
self .stale = True
1085
- self .figure .canvas .draw_idle ()
1086
1085
1087
1086
elif self .button_pressed == 2 :
1088
- # pan view
1089
- # get the x and y pixel coords
1090
- if dx == 0 and dy == 0 :
1091
- return
1092
- minx , maxx , miny , maxy , minz , maxz = self .get_w_lims ()
1093
- dx = 1 - ((w - dx )/ w )
1094
- dy = 1 - ((h - dy )/ h )
1095
- elev = np .deg2rad (self .elev )
1096
- azim = np .deg2rad (self .azim )
1097
- # project xv, yv, zv -> xw, yw, zw
1098
- dxx = (maxx - minx )* (dy * np .sin (elev )* np .cos (azim ) + dx * np .sin (azim ))
1099
- dyy = (maxy - miny )* (- dx * np .cos (azim ) + dy * np .sin (elev )* np .sin (azim ))
1100
- dzz = (maxz - minz )* (- dy * np .cos (elev ))
1101
- # pan
1102
- self .set_xlim3d (minx + dxx , maxx + dxx )
1103
- self .set_ylim3d (miny + dyy , maxy + dyy )
1104
- self .set_zlim3d (minz + dzz , maxz + dzz )
1105
- self .get_proj ()
1106
- self .figure .canvas .draw_idle ()
1087
+ # Start the pan event with pixel coordinates
1088
+ px , py = self .transData .transform ([self .sx , self .sy ])
1089
+ self .start_pan (px , py , 2 )
1090
+ # pan view (takes pixel coordinate input)
1091
+ self .drag_pan (2 , None , event .x , event .y )
1092
+ self .end_pan ()
1107
1093
1108
1094
# Zoom
1109
1095
elif self .button_pressed in self ._zoom_btn :
@@ -1118,7 +1104,182 @@ def _on_move(self, event):
1118
1104
self .set_ylim3d (miny - dy , maxy + dy )
1119
1105
self .set_zlim3d (minz - dz , maxz + dz )
1120
1106
self .get_proj ()
1121
- self .figure .canvas .draw_idle ()
1107
+
1108
+ # Store the event coordinates for the next time through.
1109
+ self .sx , self .sy = x , y
1110
+ # Always request a draw update at the end of interaction
1111
+ self .figure .canvas .draw_idle ()
1112
+
1113
+ def drag_pan (self , button , key , x , y ):
1114
+ # docstring inherited
1115
+
1116
+ # Get the coordinates from the move event
1117
+ p = self ._pan_start
1118
+ (xdata , ydata ), (xdata_start , ydata_start ) = p .trans_inverse .transform (
1119
+ [(x , y ), (p .x , p .y )])
1120
+ self .sx , self .sy = xdata , ydata
1121
+ # Calling start_pan() to set the x/y of this event as the starting
1122
+ # move location for the next event
1123
+ self .start_pan (x , y , button )
1124
+ dx , dy = xdata - xdata_start , ydata - ydata_start
1125
+ if dx == 0 and dy == 0 :
1126
+ return
1127
+
1128
+ # Now pan the view by updating the limits
1129
+ w = self ._pseudo_w
1130
+ h = self ._pseudo_h
1131
+
1132
+ minx , maxx , miny , maxy , minz , maxz = self .get_w_lims ()
1133
+ dx = 1 - ((w - dx ) / w )
1134
+ dy = 1 - ((h - dy ) / h )
1135
+ elev = np .deg2rad (self .elev )
1136
+ azim = np .deg2rad (self .azim )
1137
+ # project xv, yv, zv -> xw, yw, zw
1138
+ dxx = (maxx - minx ) * (dy * np .sin (elev )
1139
+ * np .cos (azim ) + dx * np .sin (azim ))
1140
+ dyy = (maxy - miny ) * (- dx * np .cos (azim )
1141
+ + dy * np .sin (elev ) * np .sin (azim ))
1142
+ dzz = (maxz - minz ) * (- dy * np .cos (elev ))
1143
+ # pan
1144
+ self .set_xlim3d (minx + dxx , maxx + dxx )
1145
+ self .set_ylim3d (miny + dyy , maxy + dyy )
1146
+ self .set_zlim3d (minz + dzz , maxz + dzz )
1147
+ self .get_proj ()
1148
+
1149
+ def _set_view_from_bbox (self , bbox , direction = 'in' ,
1150
+ mode = None , twinx = False , twiny = False ):
1151
+ # docstring inherited
1152
+
1153
+ # bbox is (start_x, start_y, event.x, event.y) in screen coords
1154
+ # _prepare_view_from_bbox will give us back new *data* coords
1155
+ # (in the 2D transform space, not 3D world coords)
1156
+ new_xbound , new_ybound = self ._prepare_view_from_bbox (
1157
+ bbox , direction = direction , mode = mode , twinx = twinx , twiny = twiny )
1158
+ # We need to get the Zoom bbox limits relative to the Axes limits
1159
+ # 1) Axes bottom-left -> Zoom box bottom-left
1160
+ # 2) Axes top-right -> Zoom box top-right
1161
+ axes_to_data_trans = self .transAxes + self .transData .inverted ()
1162
+ axes_data_bbox = axes_to_data_trans .transform ([(0 , 0 ), (1 , 1 )])
1163
+ # dx, dy gives us the vector difference from the axes to the
1164
+ dx1 , dy1 = (axes_data_bbox [0 ][0 ] - new_xbound [0 ],
1165
+ axes_data_bbox [0 ][1 ] - new_ybound [0 ])
1166
+ dx2 , dy2 = (axes_data_bbox [1 ][0 ] - new_xbound [1 ],
1167
+ axes_data_bbox [1 ][1 ] - new_ybound [1 ])
1168
+
1169
+ def data_2d_to_world_3d (dx , dy ):
1170
+ # Takes the vector (dx, dy) in transData coords and
1171
+ # transforms that to each of the 3 world data coords
1172
+ # (x, y, z) for calculating the offset
1173
+ w = self ._pseudo_w
1174
+ h = self ._pseudo_h
1175
+
1176
+ dx = 1 - ((w - dx ) / w )
1177
+ dy = 1 - ((h - dy ) / h )
1178
+ elev = np .deg2rad (self .elev )
1179
+ azim = np .deg2rad (self .azim )
1180
+ # project xv, yv, zv -> xw, yw, zw
1181
+ dxx = (dy * np .sin (elev )
1182
+ * np .cos (azim ) + dx * np .sin (azim ))
1183
+ dyy = (- dx * np .cos (azim )
1184
+ + dy * np .sin (elev ) * np .sin (azim ))
1185
+ dzz = (- dy * np .cos (elev ))
1186
+ return dxx , dyy , dzz
1187
+
1188
+ # These are the amounts to bring the projection in or out by from
1189
+ # each side (1 left, 2 right) because we aren't necessarily zooming
1190
+ # into the center of the projection.
1191
+ dxx1 , dyy1 , dzz1 = data_2d_to_world_3d (dx1 , dy1 )
1192
+ dxx2 , dyy2 , dzz2 = data_2d_to_world_3d (dx2 , dy2 )
1193
+ # update the min and max limits of the world
1194
+ minx , maxx , miny , maxy , minz , maxz = self .get_w_lims ()
1195
+ self .set_xlim3d (minx + dxx1 * (maxx - minx ),
1196
+ maxx + dxx2 * (maxx - minx ))
1197
+ self .set_ylim3d (miny + dyy1 * (maxy - miny ),
1198
+ maxy + dyy2 * (maxy - miny ))
1199
+ self .set_zlim3d (minz + dzz1 * (maxz - minz ),
1200
+ maxz + dzz2 * (maxz - minz ))
1201
+ self .get_proj ()
1202
+
1203
+ def _prepare_view_from_bbox (self , bbox , direction = 'in' ,
1204
+ mode = None , twinx = False , twiny = False ):
1205
+ """
1206
+ Helper function to prepare the new bounds from a bbox.
1207
+
1208
+ This helper function returns the new x and y bounds from the zoom
1209
+ bbox. This a convenience method to abstract the bbox logic
1210
+ out of the base setter.
1211
+ """
1212
+ if len (bbox ) == 3 :
1213
+ xp , yp , scl = bbox # Zooming code
1214
+ if scl == 0 : # Should not happen
1215
+ scl = 1.
1216
+ if scl > 1 :
1217
+ direction = 'in'
1218
+ else :
1219
+ direction = 'out'
1220
+ scl = 1 / scl
1221
+ # get the limits of the axes
1222
+ (xmin , ymin ), (xmax , ymax ) = self .transData .transform (
1223
+ np .transpose ([self .get_xlim (), self .get_ylim ()]))
1224
+ # set the range
1225
+ xwidth = xmax - xmin
1226
+ ywidth = ymax - ymin
1227
+ xcen = (xmax + xmin ) * .5
1228
+ ycen = (ymax + ymin ) * .5
1229
+ xzc = (xp * (scl - 1 ) + xcen ) / scl
1230
+ yzc = (yp * (scl - 1 ) + ycen ) / scl
1231
+ bbox = [xzc - xwidth / 2. / scl , yzc - ywidth / 2. / scl ,
1232
+ xzc + xwidth / 2. / scl , yzc + ywidth / 2. / scl ]
1233
+ elif len (bbox ) != 4 :
1234
+ # should be len 3 or 4 but nothing else
1235
+ _api .warn_external (
1236
+ "Warning in _set_view_from_bbox: bounding box is not a tuple "
1237
+ "of length 3 or 4. Ignoring the view change." )
1238
+ return
1239
+
1240
+ # Original limits
1241
+ # Can't use get_x/y bounds because those aren't in 2D space
1242
+ pseudo_bbox = self .transLimits .inverted ().transform ([(0 , 0 ), (1 , 1 )])
1243
+ (xmin0 , ymin0 ), (xmax0 , ymax0 ) = pseudo_bbox
1244
+ # The zoom box in screen coords.
1245
+ startx , starty , stopx , stopy = bbox
1246
+ # Convert to data coords.
1247
+ (startx , starty ), (stopx , stopy ) = self .transData .inverted ().transform (
1248
+ [(startx , starty ), (stopx , stopy )])
1249
+ # Clip to axes limits.
1250
+ xmin , xmax = np .clip (sorted ([startx , stopx ]), xmin0 , xmax0 )
1251
+ ymin , ymax = np .clip (sorted ([starty , stopy ]), ymin0 , ymax0 )
1252
+ # Don't double-zoom twinned axes or if zooming only the other axis.
1253
+ if twinx or mode == "y" :
1254
+ xmin , xmax = xmin0 , xmax0
1255
+ if twiny or mode == "x" :
1256
+ ymin , ymax = ymin0 , ymax0
1257
+
1258
+ if direction == "in" :
1259
+ new_xbound = xmin , xmax
1260
+ new_ybound = ymin , ymax
1261
+
1262
+ elif direction == "out" :
1263
+ x_trf = self .xaxis .get_transform ()
1264
+ sxmin0 , sxmax0 , sxmin , sxmax = x_trf .transform (
1265
+ [xmin0 , xmax0 , xmin , xmax ]) # To screen space.
1266
+ factor = (sxmax0 - sxmin0 ) / (sxmax - sxmin ) # Unzoom factor.
1267
+ # Move original bounds away by
1268
+ # (factor) x (distance between unzoom box and Axes bbox).
1269
+ sxmin1 = sxmin0 - factor * (sxmin - sxmin0 )
1270
+ sxmax1 = sxmax0 + factor * (sxmax0 - sxmax )
1271
+ # And back to data space.
1272
+ new_xbound = x_trf .inverted ().transform ([sxmin1 , sxmax1 ])
1273
+
1274
+ y_trf = self .yaxis .get_transform ()
1275
+ symin0 , symax0 , symin , symax = y_trf .transform (
1276
+ [ymin0 , ymax0 , ymin , ymax ])
1277
+ factor = (symax0 - symin0 ) / (symax - symin )
1278
+ symin1 = symin0 - factor * (symin - symin0 )
1279
+ symax1 = symax0 + factor * (symax0 - symax )
1280
+ new_ybound = y_trf .inverted ().transform ([symin1 , symax1 ])
1281
+
1282
+ return new_xbound , new_ybound
1122
1283
1123
1284
def set_zlabel (self , zlabel , fontdict = None , labelpad = None , ** kwargs ):
1124
1285
"""
0 commit comments