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