diff --git a/lib/matplotlib/axes/_base.py b/lib/matplotlib/axes/_base.py index 0b3f8b20a738..6054965e90ce 100644 --- a/lib/matplotlib/axes/_base.py +++ b/lib/matplotlib/axes/_base.py @@ -3132,6 +3132,167 @@ def set_navigate_mode(self, b): """ self._navigate_mode = b + def _get_view(self): + """ + Save information required to reproduce the current view. + + Called before a view is changed, such as during a pan or zoom + initiated by the user. You may return any information you deem + necessary to describe the view. + + .. note:: + + Intended to be overridden by new projection types, but if not, the + default implementation saves the view limits. You *must* implement + :meth:`_set_view` if you implement this method. + """ + xmin, xmax = self.get_xlim() + ymin, ymax = self.get_ylim() + return (xmin, xmax, ymin, ymax) + + def _set_view(self, view): + """ + Apply a previously saved view. + + Called when restoring a view, such as with the navigation buttons. + + .. note:: + + Intended to be overridden by new projection types, but if not, the + default implementation restores the view limits. You *must* + implement :meth:`_get_view` if you implement this method. + """ + xmin, xmax, ymin, ymax = view + self.set_xlim((xmin, xmax)) + self.set_ylim((ymin, ymax)) + + def _set_view_from_bbox(self, bbox, original_view, direction='in', + mode=None, twinx=False, twiny=False): + """ + Update view from a selection bbox. + + .. note:: + + Intended to be overridden by new projection types, but if not, the + default implementation sets the view limits to the bbox directly. + + Parameters + ---------- + + bbox : tuple + The selected bounding box limits, in *display* coordinates. + + original_view : any + A view saved from before initiating the selection, the result of + calling :meth:`_get_view`. + + direction : str + The direction to apply the bounding box. + * `'in'` - The bounding box describes the view directly, i.e., + it zooms in. + * `'out'` - The bounding box describes the size to make the + existing view, i.e., it zooms out. + + mode : str or None + The selection mode, whether to apply the bounding box in only the + `'x'` direction, `'y'` direction or both (`None`). + + twinx : bool + Whether this axis is twinned in the *x*-direction. + + twiny : bool + Whether this axis is twinned in the *y*-direction. + """ + + lastx, lasty, x, y = bbox + + x0, y0, x1, y1 = original_view + + # zoom to rect + inverse = self.transData.inverted() + lastx, lasty = inverse.transform_point((lastx, lasty)) + x, y = inverse.transform_point((x, y)) + Xmin, Xmax = self.get_xlim() + Ymin, Ymax = self.get_ylim() + + if twinx: + x0, x1 = Xmin, Xmax + else: + if Xmin < Xmax: + if x < lastx: + x0, x1 = x, lastx + else: + x0, x1 = lastx, x + if x0 < Xmin: + x0 = Xmin + if x1 > Xmax: + x1 = Xmax + else: + if x > lastx: + x0, x1 = x, lastx + else: + x0, x1 = lastx, x + if x0 > Xmin: + x0 = Xmin + if x1 < Xmax: + x1 = Xmax + + if twiny: + y0, y1 = Ymin, Ymax + else: + if Ymin < Ymax: + if y < lasty: + y0, y1 = y, lasty + else: + y0, y1 = lasty, y + if y0 < Ymin: + y0 = Ymin + if y1 > Ymax: + y1 = Ymax + else: + if y > lasty: + y0, y1 = y, lasty + else: + y0, y1 = lasty, y + if y0 > Ymin: + y0 = Ymin + if y1 < Ymax: + y1 = Ymax + + if direction == 'in': + if mode == 'x': + self.set_xlim((x0, x1)) + elif mode == 'y': + self.set_ylim((y0, y1)) + else: + self.set_xlim((x0, x1)) + self.set_ylim((y0, y1)) + elif direction == 'out': + if self.get_xscale() == 'log': + alpha = np.log(Xmax / Xmin) / np.log(x1 / x0) + rx1 = pow(Xmin / x0, alpha) * Xmin + rx2 = pow(Xmax / x0, alpha) * Xmin + else: + alpha = (Xmax - Xmin) / (x1 - x0) + rx1 = alpha * (Xmin - x0) + Xmin + rx2 = alpha * (Xmax - x0) + Xmin + if self.get_yscale() == 'log': + alpha = np.log(Ymax / Ymin) / np.log(y1 / y0) + ry1 = pow(Ymin / y0, alpha) * Ymin + ry2 = pow(Ymax / y0, alpha) * Ymin + else: + alpha = (Ymax - Ymin) / (y1 - y0) + ry1 = alpha * (Ymin - y0) + Ymin + ry2 = alpha * (Ymax - y0) + Ymin + + if mode == 'x': + self.set_xlim((rx1, rx2)) + elif mode == 'y': + self.set_ylim((ry1, ry2)) + else: + self.set_xlim((rx1, rx2)) + self.set_ylim((ry1, ry2)) + def start_pan(self, x, y, button): """ Called when a pan operation has started. diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index d0b59e5b7524..4a99cf1723f4 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2925,8 +2925,7 @@ def press_zoom(self, event): for i, a in enumerate(self.canvas.figure.get_axes()): if (x is not None and y is not None and a.in_axes(event) and a.get_navigate() and a.can_zoom()): - self._xypress.append((x, y, a, i, a.viewLim.frozen(), - a.transData.frozen())) + self._xypress.append((x, y, a, i, a._get_view())) id1 = self.canvas.mpl_connect('motion_notify_event', self.drag_zoom) id2 = self.canvas.mpl_connect('key_press_event', @@ -2949,17 +2948,15 @@ def _switch_off_zoom_mode(self, event): def push_current(self): """push the current view limits and position onto the stack""" - lims = [] + views = [] pos = [] for a in self.canvas.figure.get_axes(): - xmin, xmax = a.get_xlim() - ymin, ymax = a.get_ylim() - lims.append((xmin, xmax, ymin, ymax)) + views.append(a._get_view()) # Store both the original and modified positions pos.append(( a.get_position(True).frozen(), a.get_position().frozen())) - self._views.push(lims) + self._views.push(views) self._positions.push(pos) self.set_history_buttons() @@ -2999,7 +2996,7 @@ def drag_zoom(self, event): if self._xypress: x, y = event.x, event.y - lastx, lasty, a, ind, lim, trans = self._xypress[0] + lastx, lasty, a, ind, view = self._xypress[0] # adjust x, last, y, last x1, y1, x2, y2 = a.bbox.extents @@ -3028,7 +3025,7 @@ def release_zoom(self, event): for cur_xypress in self._xypress: x, y = event.x, event.y - lastx, lasty, a, ind, lim, trans = cur_xypress + lastx, lasty, a, ind, view = cur_xypress # ignore singular clicks - 5 pixels is a threshold if abs(x - lastx) < 5 or abs(y - lasty) < 5: self._xypress = None @@ -3036,15 +3033,6 @@ def release_zoom(self, event): self.draw() return - x0, y0, x1, y1 = lim.extents - - # zoom to rect - inverse = a.transData.inverted() - lastx, lasty = inverse.transform_point((lastx, lasty)) - x, y = inverse.transform_point((x, y)) - Xmin, Xmax = a.get_xlim() - Ymin, Ymax = a.get_ylim() - # detect twinx,y axes and avoid double zooming twinx, twiny = False, False if last_a: @@ -3055,83 +3043,15 @@ def release_zoom(self, event): twiny = True last_a.append(a) - if twinx: - x0, x1 = Xmin, Xmax - else: - if Xmin < Xmax: - if x < lastx: - x0, x1 = x, lastx - else: - x0, x1 = lastx, x - if x0 < Xmin: - x0 = Xmin - if x1 > Xmax: - x1 = Xmax - else: - if x > lastx: - x0, x1 = x, lastx - else: - x0, x1 = lastx, x - if x0 > Xmin: - x0 = Xmin - if x1 < Xmax: - x1 = Xmax - - if twiny: - y0, y1 = Ymin, Ymax - else: - if Ymin < Ymax: - if y < lasty: - y0, y1 = y, lasty - else: - y0, y1 = lasty, y - if y0 < Ymin: - y0 = Ymin - if y1 > Ymax: - y1 = Ymax - else: - if y > lasty: - y0, y1 = y, lasty - else: - y0, y1 = lasty, y - if y0 > Ymin: - y0 = Ymin - if y1 < Ymax: - y1 = Ymax - if self._button_pressed == 1: - if self._zoom_mode == "x": - a.set_xlim((x0, x1)) - elif self._zoom_mode == "y": - a.set_ylim((y0, y1)) - else: - a.set_xlim((x0, x1)) - a.set_ylim((y0, y1)) + direction = 'in' elif self._button_pressed == 3: - if a.get_xscale() == 'log': - alpha = np.log(Xmax / Xmin) / np.log(x1 / x0) - rx1 = pow(Xmin / x0, alpha) * Xmin - rx2 = pow(Xmax / x0, alpha) * Xmin - else: - alpha = (Xmax - Xmin) / (x1 - x0) - rx1 = alpha * (Xmin - x0) + Xmin - rx2 = alpha * (Xmax - x0) + Xmin - if a.get_yscale() == 'log': - alpha = np.log(Ymax / Ymin) / np.log(y1 / y0) - ry1 = pow(Ymin / y0, alpha) * Ymin - ry2 = pow(Ymax / y0, alpha) * Ymin - else: - alpha = (Ymax - Ymin) / (y1 - y0) - ry1 = alpha * (Ymin - y0) + Ymin - ry2 = alpha * (Ymax - y0) + Ymin - - if self._zoom_mode == "x": - a.set_xlim((rx1, rx2)) - elif self._zoom_mode == "y": - a.set_ylim((ry1, ry2)) - else: - a.set_xlim((rx1, rx2)) - a.set_ylim((ry1, ry2)) + direction = 'out' + else: + continue + + a._set_view_from_bbox((lastx, lasty, x, y), view, direction, + self._zoom_mode, twinx, twiny) self.draw() self._xypress = None @@ -3164,16 +3084,14 @@ def _update_view(self): position stack for each axes """ - lims = self._views() - if lims is None: + views = self._views() + if views is None: return pos = self._positions() if pos is None: return for i, a in enumerate(self.canvas.figure.get_axes()): - xmin, xmax, ymin, ymax = lims[i] - a.set_xlim((xmin, xmax)) - a.set_ylim((ymin, ymax)) + a._set_view(views[i]) # Restore both the original and modified positions a.set_position(pos[i][0], 'original') a.set_position(pos[i][1], 'active') diff --git a/lib/matplotlib/backend_tools.py b/lib/matplotlib/backend_tools.py index eb221a9803a0..79a9bdc43a57 100644 --- a/lib/matplotlib/backend_tools.py +++ b/lib/matplotlib/backend_tools.py @@ -466,16 +466,14 @@ def update_view(self): position stack for each axes """ - lims = self.views[self.figure]() - if lims is None: + views = self.views[self.figure]() + if views is None: return pos = self.positions[self.figure]() if pos is None: return for i, a in enumerate(self.figure.get_axes()): - xmin, xmax, ymin, ymax = lims[i] - a.set_xlim((xmin, xmax)) - a.set_ylim((ymin, ymax)) + a._set_view(views[i]) # Restore both the original and modified positions a.set_position(pos[i][0], 'original') a.set_position(pos[i][1], 'active') @@ -485,17 +483,15 @@ def update_view(self): def push_current(self): """push the current view limits and position onto the stack""" - lims = [] + views = [] pos = [] for a in self.figure.get_axes(): - xmin, xmax = a.get_xlim() - ymin, ymax = a.get_ylim() - lims.append((xmin, xmax, ymin, ymax)) + views.append(a._get_view()) # Store both the original and modified positions pos.append(( a.get_position(True).frozen(), a.get_position().frozen())) - self.views[self.figure].push(lims) + self.views[self.figure].push(views) self.positions[self.figure].push(pos) def refresh_locators(self): @@ -696,8 +692,7 @@ def _press(self, event): for i, a in enumerate(self.figure.get_axes()): if (x is not None and y is not None and a.in_axes(event) and a.get_navigate() and a.can_zoom()): - self._xypress.append((x, y, a, i, a.viewLim.frozen(), - a.transData.frozen())) + self._xypress.append((x, y, a, i, a._get_view())) id1 = self.figure.canvas.mpl_connect( 'motion_notify_event', self._mouse_move) @@ -722,7 +717,7 @@ def _mouse_move(self, event): if self._xypress: x, y = event.x, event.y - lastx, lasty, a, _ind, _lim, _trans = self._xypress[0] + lastx, lasty, a, _ind, _view = self._xypress[0] # adjust x, last, y, last x1, y1, x2, y2 = a.bbox.extents @@ -755,21 +750,12 @@ def _release(self, event): for cur_xypress in self._xypress: x, y = event.x, event.y - lastx, lasty, a, _ind, lim, _trans = cur_xypress + lastx, lasty, a, _ind, view = cur_xypress # ignore singular clicks - 5 pixels is a threshold if abs(x - lastx) < 5 or abs(y - lasty) < 5: self._cancel_action() return - x0, y0, x1, y1 = lim.extents - - # zoom to rect - inverse = a.transData.inverted() - lastx, lasty = inverse.transform_point((lastx, lasty)) - x, y = inverse.transform_point((x, y)) - Xmin, Xmax = a.get_xlim() - Ymin, Ymax = a.get_ylim() - # detect twinx,y axes and avoid double zooming twinx, twiny = False, False if last_a: @@ -780,83 +766,15 @@ def _release(self, event): twiny = True last_a.append(a) - if twinx: - x0, x1 = Xmin, Xmax - else: - if Xmin < Xmax: - if x < lastx: - x0, x1 = x, lastx - else: - x0, x1 = lastx, x - if x0 < Xmin: - x0 = Xmin - if x1 > Xmax: - x1 = Xmax - else: - if x > lastx: - x0, x1 = x, lastx - else: - x0, x1 = lastx, x - if x0 > Xmin: - x0 = Xmin - if x1 < Xmax: - x1 = Xmax - - if twiny: - y0, y1 = Ymin, Ymax - else: - if Ymin < Ymax: - if y < lasty: - y0, y1 = y, lasty - else: - y0, y1 = lasty, y - if y0 < Ymin: - y0 = Ymin - if y1 > Ymax: - y1 = Ymax - else: - if y > lasty: - y0, y1 = y, lasty - else: - y0, y1 = lasty, y - if y0 > Ymin: - y0 = Ymin - if y1 < Ymax: - y1 = Ymax - if self._button_pressed == 1: - if self._zoom_mode == "x": - a.set_xlim((x0, x1)) - elif self._zoom_mode == "y": - a.set_ylim((y0, y1)) - else: - a.set_xlim((x0, x1)) - a.set_ylim((y0, y1)) + direction = 'in' elif self._button_pressed == 3: - if a.get_xscale() == 'log': - alpha = np.log(Xmax / Xmin) / np.log(x1 / x0) - rx1 = pow(Xmin / x0, alpha) * Xmin - rx2 = pow(Xmax / x0, alpha) * Xmin - else: - alpha = (Xmax - Xmin) / (x1 - x0) - rx1 = alpha * (Xmin - x0) + Xmin - rx2 = alpha * (Xmax - x0) + Xmin - if a.get_yscale() == 'log': - alpha = np.log(Ymax / Ymin) / np.log(y1 / y0) - ry1 = pow(Ymin / y0, alpha) * Ymin - ry2 = pow(Ymax / y0, alpha) * Ymin - else: - alpha = (Ymax - Ymin) / (y1 - y0) - ry1 = alpha * (Ymin - y0) + Ymin - ry2 = alpha * (Ymax - y0) + Ymin - - if self._zoom_mode == "x": - a.set_xlim((rx1, rx2)) - elif self._zoom_mode == "y": - a.set_ylim((ry1, ry2)) - else: - a.set_xlim((rx1, rx2)) - a.set_ylim((ry1, ry2)) + direction = 'out' + else: + continue + + a._set_view_from_bbox((lastx, lasty, x, y), view, direction, + self._zoom_mode, twinx, twiny) self._zoom_mode = None self.toolmanager.get_tool(_views_positions).push_current()