diff --git a/doc/api/next_api_changes/deprecations/23449-SS.rst b/doc/api/next_api_changes/deprecations/23449-SS.rst new file mode 100644 index 000000000000..cc5123fc0b7d --- /dev/null +++ b/doc/api/next_api_changes/deprecations/23449-SS.rst @@ -0,0 +1,3 @@ +``axes3d.vvec``, ``axes3d.eye``, ``axes3d.sx``, and ``axes3d.sy`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +... are deprecated without replacement. diff --git a/doc/users/next_whats_new/3d_plot_pan_zoom.rst b/doc/users/next_whats_new/3d_plot_pan_zoom.rst new file mode 100644 index 000000000000..a94dfe4c207d --- /dev/null +++ b/doc/users/next_whats_new/3d_plot_pan_zoom.rst @@ -0,0 +1,8 @@ +3D plot pan and zoom buttons +---------------------------- + +The pan and zoom buttons in the toolbar of 3D plots are now enabled. +Unselect both to rotate the plot. When the zoom button is pressed, +zoom in by using the left mouse button to draw a bounding box, and +out by using the right mouse button to draw the box. When zooming a +3D plot, the current view aspect ratios are kept fixed. diff --git a/lib/mpl_toolkits/mplot3d/axes3d.py b/lib/mpl_toolkits/mplot3d/axes3d.py index 3177ed42ca25..e9b57f63476f 100644 --- a/lib/mpl_toolkits/mplot3d/axes3d.py +++ b/lib/mpl_toolkits/mplot3d/axes3d.py @@ -52,6 +52,10 @@ class Axes3D(Axes): Axes._shared_axes["z"] = cbook.Grouper() dist = _api.deprecate_privatize_attribute("3.6") + vvec = _api.deprecate_privatize_attribute("3.7") + eye = _api.deprecate_privatize_attribute("3.7") + sx = _api.deprecate_privatize_attribute("3.7") + sy = _api.deprecate_privatize_attribute("3.7") def __init__( self, fig, rect=None, *args, @@ -326,7 +330,7 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): self._aspect = aspect if aspect in ('equal', 'equalxy', 'equalxz', 'equalyz'): - ax_idx = self._equal_aspect_axis_indices(aspect) + ax_indices = self._equal_aspect_axis_indices(aspect) view_intervals = np.array([self.xaxis.get_view_interval(), self.yaxis.get_view_interval(), @@ -334,26 +338,26 @@ def set_aspect(self, aspect, adjustable=None, anchor=None, share=False): ptp = np.ptp(view_intervals, axis=1) if adjustable == 'datalim': mean = np.mean(view_intervals, axis=1) - delta = max(ptp[ax_idx]) + delta = max(ptp[ax_indices]) scale = self._box_aspect[ptp == delta][0] deltas = delta * self._box_aspect / scale for i, set_lim in enumerate((self.set_xlim3d, self.set_ylim3d, self.set_zlim3d)): - if i in ax_idx: + if i in ax_indices: set_lim(mean[i] - deltas[i]/2., mean[i] + deltas[i]/2.) else: # 'box' # Change the box aspect such that the ratio of the length of # the unmodified axis to the length of the diagonal # perpendicular to it remains unchanged. box_aspect = np.array(self._box_aspect) - box_aspect[ax_idx] = ptp[ax_idx] - remaining_ax_idx = {0, 1, 2}.difference(ax_idx) - if remaining_ax_idx: - remaining = remaining_ax_idx.pop() - old_diag = np.linalg.norm(self._box_aspect[ax_idx]) - new_diag = np.linalg.norm(box_aspect[ax_idx]) + box_aspect[ax_indices] = ptp[ax_indices] + remaining_ax_indices = {0, 1, 2}.difference(ax_indices) + if remaining_ax_indices: + remaining = remaining_ax_indices.pop() + old_diag = np.linalg.norm(self._box_aspect[ax_indices]) + new_diag = np.linalg.norm(box_aspect[ax_indices]) box_aspect[remaining] *= new_diag / old_diag self.set_box_aspect(box_aspect) @@ -876,15 +880,13 @@ def get_proj(self): pb_aspect=box_aspect, ) - # Look into the middle of the new coordinates: + # Look into the middle of the world coordinates: R = 0.5 * box_aspect # elev stores the elevation angle in the z plane # azim stores the azimuth angle in the x,y plane - # roll stores the roll angle about the view axis elev_rad = np.deg2rad(art3d._norm_angle(self.elev)) azim_rad = np.deg2rad(art3d._norm_angle(self.azim)) - roll_rad = np.deg2rad(art3d._norm_angle(self.roll)) # Coordinates for a point that rotates around the box of data. # p0, p1 corresponds to rotating the box only around the @@ -903,27 +905,27 @@ def get_proj(self): # towards the middle of the box of data from a distance: eye = R + self._dist * ps - # TODO: Is this being used somewhere? Can it be removed? - self.eye = eye - self.vvec = R - eye - self.vvec = self.vvec / np.linalg.norm(self.vvec) + # vvec, self._vvec and self._eye are unused, remove when deprecated + vvec = R - eye + self._eye = eye + self._vvec = vvec / np.linalg.norm(vvec) - # Define which axis should be vertical. A negative value - # indicates the plot is upside down and therefore the values - # have been reversed: - V = np.zeros(3) - V[self._vertical_axis] = -1 if abs(elev_rad) > 0.5 * np.pi else 1 + # Calculate the viewing axes for the eye position + u, v, w = self._calc_view_axes(eye) + self._view_u = u # _view_u is towards the right of the screen + self._view_v = v # _view_v is towards the top of the screen + self._view_w = w # _view_w is out of the screen # Generate the view and projection transformation matrices if self._focal_length == np.inf: # Orthographic projection - viewM = proj3d.view_transformation(eye, R, V, roll_rad) + viewM = proj3d._view_transformation_uvw(u, v, w, eye) projM = proj3d.ortho_transformation(-self._dist, self._dist) else: # Perspective projection # Scale the eye dist to compensate for the focal length zoom effect eye_focal = R + self._dist * ps * self._focal_length - viewM = proj3d.view_transformation(eye_focal, R, V, roll_rad) + viewM = proj3d._view_transformation_uvw(u, v, w, eye_focal) projM = proj3d.persp_transformation(-self._dist, self._dist, self._focal_length) @@ -933,7 +935,7 @@ def get_proj(self): M = np.dot(projM, M0) return M - def mouse_init(self, rotate_btn=1, zoom_btn=3): + def mouse_init(self, rotate_btn=1, pan_btn=2, zoom_btn=3): """ Set the mouse buttons for 3D rotation and zooming. @@ -941,6 +943,8 @@ def mouse_init(self, rotate_btn=1, zoom_btn=3): ---------- rotate_btn : int or list of int, default: 1 The mouse button or buttons to use for 3D rotation of the axes. + pan_btn : int or list of int, default: 2 + The mouse button or buttons to use to pan the 3D axes. zoom_btn : int or list of int, default: 3 The mouse button or buttons to use to zoom the 3D axes. """ @@ -949,27 +953,24 @@ def mouse_init(self, rotate_btn=1, zoom_btn=3): # a regular list to avoid comparisons against None # which breaks in recent versions of numpy. self._rotate_btn = np.atleast_1d(rotate_btn).tolist() + self._pan_btn = np.atleast_1d(pan_btn).tolist() self._zoom_btn = np.atleast_1d(zoom_btn).tolist() def disable_mouse_rotation(self): - """Disable mouse buttons for 3D rotation and zooming.""" - self.mouse_init(rotate_btn=[], zoom_btn=[]) + """Disable mouse buttons for 3D rotation, panning, and zooming.""" + self.mouse_init(rotate_btn=[], pan_btn=[], zoom_btn=[]) def can_zoom(self): """ Return whether this Axes supports the zoom box button functionality. - - Axes3D objects do not use the zoom box button. """ - return False + return True def can_pan(self): """ - Return whether this Axes supports the pan/zoom button functionality. - - Axes3d objects do not use the pan/zoom button. + Return whether this Axes supports the pan button functionality. """ - return False + return True def sharez(self, other): """ @@ -1002,7 +1003,7 @@ def clear(self): def _button_press(self, event): if event.inaxes == self: self.button_pressed = event.button - self.sx, self.sy = event.xdata, event.ydata + self._sx, self._sy = event.xdata, event.ydata toolbar = getattr(self.figure.canvas, "toolbar") if toolbar and toolbar._nav_stack() is None: self.figure.canvas.toolbar.push_current() @@ -1010,7 +1011,9 @@ def _button_press(self, event): def _button_release(self, event): self.button_pressed = None toolbar = getattr(self.figure.canvas, "toolbar") - if toolbar: + # backend_bases.release_zoom and backend_bases.release_pan call + # push_current, so check the navigation mode so we don't call it twice + if toolbar and self.get_navigate_mode() is None: self.figure.canvas.toolbar.push_current() def _get_view(self): @@ -1083,25 +1086,29 @@ def _on_move(self, event): """ Mouse moving. - By default, button-1 rotates and button-3 zooms; these buttons can be - modified via `mouse_init`. + By default, button-1 rotates, button-2 pans, and button-3 zooms; + these buttons can be modified via `mouse_init`. """ if not self.button_pressed: return + if self.get_navigate_mode() is not None: + # we don't want to rotate if we are zooming/panning + # from the toolbar + return + if self.M is None: return x, y = event.xdata, event.ydata # In case the mouse is out of bounds. - if x is None: + if x is None or event.inaxes != self: return - dx, dy = x - self.sx, y - self.sy + dx, dy = x - self._sx, y - self._sy w = self._pseudo_w h = self._pseudo_h - self.sx, self.sy = x, y # Rotation if self.button_pressed in self._rotate_btn: @@ -1115,45 +1122,199 @@ def _on_move(self, event): dazim = -(dy/h)*180*np.sin(roll) - (dx/w)*180*np.cos(roll) self.elev = self.elev + delev self.azim = self.azim + dazim - self.get_proj() self.stale = True - self.figure.canvas.draw_idle() - elif self.button_pressed == 2: - # pan view - # get the x and y pixel coords - if dx == 0 and dy == 0: - return - minx, maxx, miny, maxy, minz, maxz = self.get_w_lims() - dx = 1-((w - dx)/w) - dy = 1-((h - dy)/h) - elev = np.deg2rad(self.elev) - azim = np.deg2rad(self.azim) - # project xv, yv, zv -> xw, yw, zw - dxx = (maxx-minx)*(dy*np.sin(elev)*np.cos(azim) + dx*np.sin(azim)) - dyy = (maxy-miny)*(-dx*np.cos(azim) + dy*np.sin(elev)*np.sin(azim)) - dzz = (maxz-minz)*(-dy*np.cos(elev)) - # pan - self.set_xlim3d(minx + dxx, maxx + dxx) - self.set_ylim3d(miny + dyy, maxy + dyy) - self.set_zlim3d(minz + dzz, maxz + dzz) - self.get_proj() - self.figure.canvas.draw_idle() + elif self.button_pressed in self._pan_btn: + # Start the pan event with pixel coordinates + px, py = self.transData.transform([self._sx, self._sy]) + self.start_pan(px, py, 2) + # pan view (takes pixel coordinate input) + self.drag_pan(2, None, event.x, event.y) + self.end_pan() # Zoom elif self.button_pressed in self._zoom_btn: - # zoom view - # hmmm..this needs some help from clipping.... - minx, maxx, miny, maxy, minz, maxz = self.get_w_lims() - df = 1-((h - dy)/h) - dx = (maxx-minx)*df - dy = (maxy-miny)*df - dz = (maxz-minz)*df - self.set_xlim3d(minx - dx, maxx + dx) - self.set_ylim3d(miny - dy, maxy + dy) - self.set_zlim3d(minz - dz, maxz + dz) - self.get_proj() - self.figure.canvas.draw_idle() + # zoom view (dragging down zooms in) + scale = h/(h - dy) + self._scale_axis_limits(scale, scale, scale) + + # Store the event coordinates for the next time through. + self._sx, self._sy = x, y + # Always request a draw update at the end of interaction + self.figure.canvas.draw_idle() + + def drag_pan(self, button, key, x, y): + # docstring inherited + + # Get the coordinates from the move event + p = self._pan_start + (xdata, ydata), (xdata_start, ydata_start) = p.trans_inverse.transform( + [(x, y), (p.x, p.y)]) + self._sx, self._sy = xdata, ydata + # Calling start_pan() to set the x/y of this event as the starting + # move location for the next event + self.start_pan(x, y, button) + du, dv = xdata - xdata_start, ydata - ydata_start + dw = 0 + if key == 'x': + dv = 0 + elif key == 'y': + du = 0 + if du == 0 and dv == 0: + return + + # Transform the pan from the view axes to the data axes + R = np.array([self._view_u, self._view_v, self._view_w]) + R = -R / self._box_aspect * self._dist + duvw_projected = R.T @ np.array([du, dv, dw]) + + # Calculate pan distance + minx, maxx, miny, maxy, minz, maxz = self.get_w_lims() + dx = (maxx - minx) * duvw_projected[0] + dy = (maxy - miny) * duvw_projected[1] + dz = (maxz - minz) * duvw_projected[2] + + # Set the new axis limits + self.set_xlim3d(minx + dx, maxx + dx) + self.set_ylim3d(miny + dy, maxy + dy) + self.set_zlim3d(minz + dz, maxz + dz) + + def _calc_view_axes(self, eye): + """ + Get the unit vectors for the viewing axes in data coordinates. + `u` is towards the right of the screen + `v` is towards the top of the screen + `w` is out of the screen + """ + elev_rad = np.deg2rad(art3d._norm_angle(self.elev)) + roll_rad = np.deg2rad(art3d._norm_angle(self.roll)) + + # Look into the middle of the world coordinates + R = 0.5 * self._roll_to_vertical(self._box_aspect) + + # Define which axis should be vertical. A negative value + # indicates the plot is upside down and therefore the values + # have been reversed: + V = np.zeros(3) + V[self._vertical_axis] = -1 if abs(elev_rad) > np.pi/2 else 1 + + u, v, w = proj3d._view_axes(eye, R, V, roll_rad) + return u, v, w + + def _set_view_from_bbox(self, bbox, direction='in', + mode=None, twinx=False, twiny=False): + """ + Zoom in or out of the bounding box. + Will center the view on the center of the bounding box, and zoom by + the ratio of the size of the bounding box to the size of the Axes3D. + """ + (start_x, start_y, stop_x, stop_y) = bbox + if mode == 'x': + start_y = self.bbox.min[1] + stop_y = self.bbox.max[1] + elif mode == 'y': + start_x = self.bbox.min[0] + stop_x = self.bbox.max[0] + + # Clip to bounding box limits + start_x, stop_x = np.clip(sorted([start_x, stop_x]), + self.bbox.min[0], self.bbox.max[0]) + start_y, stop_y = np.clip(sorted([start_y, stop_y]), + self.bbox.min[1], self.bbox.max[1]) + + # Move the center of the view to the center of the bbox + zoom_center_x = (start_x + stop_x)/2 + zoom_center_y = (start_y + stop_y)/2 + + ax_center_x = (self.bbox.max[0] + self.bbox.min[0])/2 + ax_center_y = (self.bbox.max[1] + self.bbox.min[1])/2 + + self.start_pan(zoom_center_x, zoom_center_y, 2) + self.drag_pan(2, None, ax_center_x, ax_center_y) + self.end_pan() + + # Calculate zoom level + dx = abs(start_x - stop_x) + dy = abs(start_y - stop_y) + scale_u = dx / (self.bbox.max[0] - self.bbox.min[0]) + scale_v = dy / (self.bbox.max[1] - self.bbox.min[1]) + + # Keep aspect ratios equal + scale = max(scale_u, scale_v) + + # Zoom out + if direction == 'out': + scale = 1 / scale + + self._zoom_data_limits(scale, scale, scale) + + def _zoom_data_limits(self, scale_u, scale_v, scale_w): + """ + Zoom in or out of a 3D plot. + Will scale the data limits by the scale factors. These will be + transformed to the x, y, z data axes based on the current view angles. + A scale factor > 1 zooms out and a scale factor < 1 zooms in. + + For an axes that has had its aspect ratio set to 'equal', 'equalxy', + 'equalyz', or 'equalxz', the relevant axes are constrained to zoom + equally. + + Parameters + ---------- + scale_u : float + Scale factor for the u view axis (view screen horizontal). + scale_v : float + Scale factor for the v view axis (view screen vertical). + scale_w : float + Scale factor for the w view axis (view screen depth). + """ + scale = np.array([scale_u, scale_v, scale_w]) + + # Only perform frame conversion if unequal scale factors + if not np.allclose(scale, scale_u): + # Convert the scale factors from the view frame to the data frame + R = np.array([self._view_u, self._view_v, self._view_w]) + S = scale * np.eye(3) + scale = np.linalg.norm(R.T @ S, axis=1) + + # Set the constrained scale factors to the factor closest to 1 + if self._aspect in ('equal', 'equalxy', 'equalxz', 'equalyz'): + ax_idxs = self._equal_aspect_axis_indices(self._aspect) + min_ax_idxs = np.argmin(np.abs(scale[ax_idxs] - 1)) + scale[ax_idxs] = scale[ax_idxs][min_ax_idxs] + + self._scale_axis_limits(scale[0], scale[1], scale[2]) + + def _scale_axis_limits(self, scale_x, scale_y, scale_z): + """ + Keeping the center of the x, y, and z data axes fixed, scale their + limits by scale factors. A scale factor > 1 zooms out and a scale + factor < 1 zooms in. + + Parameters + ---------- + scale_x : float + Scale factor for the x data axis. + scale_y : float + Scale factor for the y data axis. + scale_z : float + Scale factor for the z data axis. + """ + # Get the axis limits and centers + minx, maxx, miny, maxy, minz, maxz = self.get_w_lims() + cx = (maxx + minx)/2 + cy = (maxy + miny)/2 + cz = (maxz + minz)/2 + + # Scale the data range + dx = (maxx - minx)*scale_x + dy = (maxy - miny)*scale_y + dz = (maxz - minz)*scale_z + + # Set the scaled axis limits + self.set_xlim3d(cx - dx/2, cx + dx/2) + self.set_ylim3d(cy - dy/2, cy + dy/2) + self.set_zlim3d(cz - dz/2, cz + dz/2) def set_zlabel(self, zlabel, fontdict=None, labelpad=None, **kwargs): """ diff --git a/lib/mpl_toolkits/mplot3d/proj3d.py b/lib/mpl_toolkits/mplot3d/proj3d.py index 2f23e3779b06..c9659456f3be 100644 --- a/lib/mpl_toolkits/mplot3d/proj3d.py +++ b/lib/mpl_toolkits/mplot3d/proj3d.py @@ -72,26 +72,86 @@ def rotation_about_vector(v, angle): return R -def view_transformation(E, R, V, roll): - n = (E - R) - n = n/np.linalg.norm(n) - u = np.cross(V, n) +def _view_axes(E, R, V, roll): + """ + Get the unit viewing axes in data coordinates. + + Parameters + ---------- + E : 3-element numpy array + The coordinates of the eye/camera. + R : 3-element numpy array + The coordinates of the center of the view box. + V : 3-element numpy array + Unit vector in the direction of the vertical axis. + roll : float + The roll angle in radians. + + Returns + ------- + u : 3-element numpy array + Unit vector pointing towards the right of the screen. + v : 3-element numpy array + Unit vector pointing towards the top of the screen. + w : 3-element numpy array + Unit vector pointing out of the screen. + """ + w = (E - R) + w = w/np.linalg.norm(w) + u = np.cross(V, w) u = u/np.linalg.norm(u) - v = np.cross(n, u) # Will be a unit vector + v = np.cross(w, u) # Will be a unit vector # Save some computation for the default roll=0 if roll != 0: # A positive rotation of the camera is a negative rotation of the world - Rroll = rotation_about_vector(n, -roll) + Rroll = rotation_about_vector(w, -roll) u = np.dot(Rroll, u) v = np.dot(Rroll, v) + return u, v, w + +def _view_transformation_uvw(u, v, w, E): + """ + Return the view transformation matrix. + + Parameters + ---------- + u : 3-element numpy array + Unit vector pointing towards the right of the screen. + v : 3-element numpy array + Unit vector pointing towards the top of the screen. + w : 3-element numpy array + Unit vector pointing out of the screen. + E : 3-element numpy array + The coordinates of the eye/camera. + """ Mr = np.eye(4) Mt = np.eye(4) - Mr[:3, :3] = [u, v, n] + Mr[:3, :3] = [u, v, w] Mt[:3, -1] = -E + M = np.dot(Mr, Mt) + return M + - return np.dot(Mr, Mt) +def view_transformation(E, R, V, roll): + """ + Return the view transformation matrix. + + Parameters + ---------- + E : 3-element numpy array + The coordinates of the eye/camera. + R : 3-element numpy array + The coordinates of the center of the view box. + V : 3-element numpy array + Unit vector in the direction of the vertical axis. + roll : float + The roll angle in radians. + """ + u, v, w = _view_axes(E, R, V, roll) + M = _view_transformation_uvw(u, v, w, E) + return M def persp_transformation(zfront, zback, focal_length): diff --git a/lib/mpl_toolkits/tests/test_mplot3d.py b/lib/mpl_toolkits/tests/test_mplot3d.py index 7e29e52053ee..db371438cdfb 100644 --- a/lib/mpl_toolkits/tests/test_mplot3d.py +++ b/lib/mpl_toolkits/tests/test_mplot3d.py @@ -5,7 +5,8 @@ from mpl_toolkits.mplot3d import Axes3D, axes3d, proj3d, art3d import matplotlib as mpl -from matplotlib.backend_bases import MouseButton +from matplotlib.backend_bases import (MouseButton, MouseEvent, + NavigationToolbar2) from matplotlib import cm from matplotlib import colors as mcolors, patches as mpatch from matplotlib.testing.decorators import image_comparison, check_figures_equal @@ -977,7 +978,8 @@ def _test_proj_make_M(): R = np.array([100, 100, 100]) V = np.array([0, 0, 1]) roll = 0 - viewM = proj3d.view_transformation(E, R, V, roll) + u, v, w = proj3d._view_axes(E, R, V, roll) + viewM = proj3d._view_transformation_uvw(u, v, w, E) perspM = proj3d.persp_transformation(100, -100, 1) M = np.dot(perspM, viewM) return M @@ -1043,7 +1045,8 @@ def test_proj_axes_cube_ortho(): R = np.array([0, 0, 0]) V = np.array([0, 0, 1]) roll = 0 - viewM = proj3d.view_transformation(E, R, V, roll) + u, v, w = proj3d._view_axes(E, R, V, roll) + viewM = proj3d._view_transformation_uvw(u, v, w, E) orthoM = proj3d.ortho_transformation(-1, 1) M = np.dot(orthoM, viewM) @@ -1690,6 +1693,82 @@ def convert_lim(dmin, dmax): assert z_center != pytest.approx(z_center0) +@pytest.mark.parametrize("tool,button,key,expected", + [("zoom", MouseButton.LEFT, None, # zoom in + ((0.00, 0.06), (0.01, 0.07), (0.02, 0.08))), + ("zoom", MouseButton.LEFT, 'x', # zoom in + ((-0.01, 0.10), (-0.03, 0.08), (-0.06, 0.06))), + ("zoom", MouseButton.LEFT, 'y', # zoom in + ((-0.07, 0.04), (-0.03, 0.08), (0.00, 0.11))), + ("zoom", MouseButton.RIGHT, None, # zoom out + ((-0.09, 0.15), (-0.07, 0.17), (-0.06, 0.18))), + ("pan", MouseButton.LEFT, None, + ((-0.70, -0.58), (-1.03, -0.91), (-1.27, -1.15))), + ("pan", MouseButton.LEFT, 'x', + ((-0.96, -0.84), (-0.58, -0.46), (-0.06, 0.06))), + ("pan", MouseButton.LEFT, 'y', + ((0.20, 0.32), (-0.51, -0.39), (-1.27, -1.15)))]) +def test_toolbar_zoom_pan(tool, button, key, expected): + # NOTE: The expected zoom values are rough ballparks of moving in the view + # to make sure we are getting the right direction of motion. + # The specific values can and should change if the zoom movement + # scaling factor gets updated. + fig = plt.figure() + ax = fig.add_subplot(projection='3d') + ax.scatter(0, 0, 0) + fig.canvas.draw() + xlim0, ylim0, zlim0 = ax.get_xlim3d(), ax.get_ylim3d(), ax.get_zlim3d() + + # Mouse from (0, 0) to (1, 1) + d0 = (0, 0) + d1 = (1, 1) + # Convert to screen coordinates ("s"). Events are defined only with pixel + # precision, so round the pixel values, and below, check against the + # corresponding xdata/ydata, which are close but not equal to d0/d1. + s0 = ax.transData.transform(d0).astype(int) + s1 = ax.transData.transform(d1).astype(int) + + # Set up the mouse movements + start_event = MouseEvent( + "button_press_event", fig.canvas, *s0, button, key=key) + stop_event = MouseEvent( + "button_release_event", fig.canvas, *s1, button, key=key) + + tb = NavigationToolbar2(fig.canvas) + if tool == "zoom": + tb.zoom() + tb.press_zoom(start_event) + tb.drag_zoom(stop_event) + tb.release_zoom(stop_event) + else: + tb.pan() + tb.press_pan(start_event) + tb.drag_pan(stop_event) + tb.release_pan(stop_event) + + # Should be close, but won't be exact due to screen integer resolution + xlim, ylim, zlim = expected + assert ax.get_xlim3d() == pytest.approx(xlim, abs=0.01) + assert ax.get_ylim3d() == pytest.approx(ylim, abs=0.01) + assert ax.get_zlim3d() == pytest.approx(zlim, abs=0.01) + + # Ensure that back, forward, and home buttons work + tb.back() + assert ax.get_xlim3d() == pytest.approx(xlim0) + assert ax.get_ylim3d() == pytest.approx(ylim0) + assert ax.get_zlim3d() == pytest.approx(zlim0) + + tb.forward() + assert ax.get_xlim3d() == pytest.approx(xlim, abs=0.01) + assert ax.get_ylim3d() == pytest.approx(ylim, abs=0.01) + assert ax.get_zlim3d() == pytest.approx(zlim, abs=0.01) + + tb.home() + assert ax.get_xlim3d() == pytest.approx(xlim0) + assert ax.get_ylim3d() == pytest.approx(ylim0) + assert ax.get_zlim3d() == pytest.approx(zlim0) + + @mpl.style.context('default') @check_figures_equal(extensions=["png"]) def test_scalarmap_update(fig_test, fig_ref): pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy