Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit 2f6d98c

Browse files
committed
Simplify and generalize _set_view_from_bbox.
_set_view_from_bbox is called to figure out the new axes limits when the mouse is released in interactive zoom. - In the common zoom-in case, simplify the code by using `set_x/ybound` which maintains the previously existing axes inversion (if any), rather than having a bunch of conditions to sort the arguments correctly before passing them to `set_x/ylim`. - In the (more rarely used?) zoom-out case (which is triggered by right-clicking, and zooms the axes out so that the previous axes limits fit in the drawn box), use the axis transform directly rather than just special-casing log-scale; this is necessary to make zoom-out work for other scales such as logit. - Tiny cleanups to the `len(bbox) == 3` case, which is only used by toolmanager to implement zoom-by-scroll, but isn't used by the default toolbar zoom. Also add a test for this previously untested path.
1 parent ab2d200 commit 2f6d98c

File tree

2 files changed

+96
-93
lines changed

2 files changed

+96
-93
lines changed

lib/matplotlib/axes/_base.py

Lines changed: 50 additions & 92 deletions
Original file line numberDiff line numberDiff line change
@@ -3828,18 +3828,15 @@ def _set_view_from_bbox(self, bbox, direction='in',
38283828
twiny : bool
38293829
Whether this axis is twinned in the *y*-direction.
38303830
"""
3831-
Xmin, Xmax = self.get_xlim()
3832-
Ymin, Ymax = self.get_ylim()
3833-
38343831
if len(bbox) == 3:
3835-
# Zooming code
3836-
xp, yp, scl = bbox
3832+
Xmin, Xmax = self.get_xlim()
3833+
Ymin, Ymax = self.get_ylim()
3834+
3835+
xp, yp, scl = bbox # Zooming code
38373836

3838-
# Should not happen
3839-
if scl == 0:
3837+
if scl == 0: # Should not happen
38403838
scl = 1.
38413839

3842-
# direction = 'in'
38433840
if scl > 1:
38443841
direction = 'in'
38453842
else:
@@ -3868,90 +3865,51 @@ def _set_view_from_bbox(self, bbox, direction='in',
38683865
"of length 3 or 4. Ignoring the view change.")
38693866
return
38703867

3871-
# Just grab bounding box
3872-
lastx, lasty, x, y = bbox
3873-
3874-
# zoom to rect
3875-
inverse = self.transData.inverted()
3876-
(lastx, lasty), (x, y) = inverse.transform([(lastx, lasty), (x, y)])
3877-
3878-
if twinx:
3879-
x0, x1 = Xmin, Xmax
3880-
else:
3881-
if Xmin < Xmax:
3882-
if x < lastx:
3883-
x0, x1 = x, lastx
3884-
else:
3885-
x0, x1 = lastx, x
3886-
if x0 < Xmin:
3887-
x0 = Xmin
3888-
if x1 > Xmax:
3889-
x1 = Xmax
3890-
else:
3891-
if x > lastx:
3892-
x0, x1 = x, lastx
3893-
else:
3894-
x0, x1 = lastx, x
3895-
if x0 > Xmin:
3896-
x0 = Xmin
3897-
if x1 < Xmax:
3898-
x1 = Xmax
3899-
3900-
if twiny:
3901-
y0, y1 = Ymin, Ymax
3902-
else:
3903-
if Ymin < Ymax:
3904-
if y < lasty:
3905-
y0, y1 = y, lasty
3906-
else:
3907-
y0, y1 = lasty, y
3908-
if y0 < Ymin:
3909-
y0 = Ymin
3910-
if y1 > Ymax:
3911-
y1 = Ymax
3912-
else:
3913-
if y > lasty:
3914-
y0, y1 = y, lasty
3915-
else:
3916-
y0, y1 = lasty, y
3917-
if y0 > Ymin:
3918-
y0 = Ymin
3919-
if y1 < Ymax:
3920-
y1 = Ymax
3921-
3922-
if direction == 'in':
3923-
if mode == 'x':
3924-
self.set_xlim((x0, x1))
3925-
elif mode == 'y':
3926-
self.set_ylim((y0, y1))
3927-
else:
3928-
self.set_xlim((x0, x1))
3929-
self.set_ylim((y0, y1))
3930-
elif direction == 'out':
3931-
if self.get_xscale() == 'log':
3932-
alpha = np.log(Xmax / Xmin) / np.log(x1 / x0)
3933-
rx1 = pow(Xmin / x0, alpha) * Xmin
3934-
rx2 = pow(Xmax / x0, alpha) * Xmin
3935-
else:
3936-
alpha = (Xmax - Xmin) / (x1 - x0)
3937-
rx1 = alpha * (Xmin - x0) + Xmin
3938-
rx2 = alpha * (Xmax - x0) + Xmin
3939-
if self.get_yscale() == 'log':
3940-
alpha = np.log(Ymax / Ymin) / np.log(y1 / y0)
3941-
ry1 = pow(Ymin / y0, alpha) * Ymin
3942-
ry2 = pow(Ymax / y0, alpha) * Ymin
3943-
else:
3944-
alpha = (Ymax - Ymin) / (y1 - y0)
3945-
ry1 = alpha * (Ymin - y0) + Ymin
3946-
ry2 = alpha * (Ymax - y0) + Ymin
3947-
3948-
if mode == 'x':
3949-
self.set_xlim((rx1, rx2))
3950-
elif mode == 'y':
3951-
self.set_ylim((ry1, ry2))
3952-
else:
3953-
self.set_xlim((rx1, rx2))
3954-
self.set_ylim((ry1, ry2))
3868+
# Original limits.
3869+
xmin0, xmax0 = self.get_xbound()
3870+
ymin0, ymax0 = self.get_ybound()
3871+
# The zoom box in screen coords.
3872+
startx, starty, stopx, stopy = bbox
3873+
# Convert to data coords.
3874+
(startx, starty), (stopx, stopy) = self.transData.inverted().transform(
3875+
[(startx, starty), (stopx, stopy)])
3876+
# Clip to axes limits.
3877+
xmin, xmax = np.clip(sorted([startx, stopx]), xmin0, xmax0)
3878+
ymin, ymax = np.clip(sorted([starty, stopy]), ymin0, ymax0)
3879+
# Don't double-zoom twinned axes or if zooming only the other axis.
3880+
if twinx or mode == "y":
3881+
xmin, xmax = xmin0, xmax0
3882+
if twiny or mode == "x":
3883+
ymin, ymax = ymin0, ymax0
3884+
3885+
if direction == "in":
3886+
new_xbound = xmin, xmax
3887+
new_ybound = ymin, ymax
3888+
3889+
elif direction == "out":
3890+
x_trf = self.xaxis.get_transform()
3891+
sxmin0, sxmax0, sxmin, sxmax = x_trf.transform(
3892+
[xmin0, xmax0, xmin, xmax]) # To screen space.
3893+
factor = (sxmax0 - sxmin0) / (sxmax - sxmin) # Unzoom factor.
3894+
# Move original bounds away by
3895+
# (factor) x (distance between unzoom box and axes bbox).
3896+
sxmin1 = sxmin0 - factor * (sxmin - sxmin0)
3897+
sxmax1 = sxmax0 + factor * (sxmax0 - sxmax)
3898+
# And back to data space.
3899+
new_xbound = x_trf.inverted().transform([sxmin1, sxmax1])
3900+
3901+
y_trf = self.yaxis.get_transform()
3902+
symin0, symax0, symin, symax = y_trf.transform(
3903+
[ymin0, ymax0, ymin, ymax])
3904+
factor = (symax0 - symin0) / (symax - symin)
3905+
symin1 = symin0 - factor * (symin - symin0)
3906+
symax1 = symax0 + factor * (symax0 - symax)
3907+
new_ybound = y_trf.inverted().transform([symin1, symax1])
3908+
3909+
if not twinx and mode != "y":
3910+
self.set_xbound(new_xbound)
3911+
if not twiny and mode != "x":
3912+
self.set_ybound(new_ybound)
39553913

39563914
def start_pan(self, x, y, button):
39573915
"""

lib/matplotlib/tests/test_backend_bases.py

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import re
22

33
from matplotlib.backend_bases import (
4-
FigureCanvasBase, LocationEvent, RendererBase)
4+
FigureCanvasBase, LocationEvent, MouseButton, MouseEvent,
5+
NavigationToolbar2, RendererBase)
56
import matplotlib.pyplot as plt
67
import matplotlib.transforms as transforms
78
import matplotlib.path as path
@@ -99,3 +100,47 @@ def test_location_event_position(x, y):
99100
ax.format_coord(x, y))
100101
ax.fmt_xdata = ax.fmt_ydata = lambda x: "foo"
101102
assert re.match("x=foo +y=foo", ax.format_coord(x, y))
103+
104+
105+
def test_interactive_zoom():
106+
fig, ax = plt.subplots()
107+
ax.set(xscale="logit")
108+
109+
class NT2(NavigationToolbar2):
110+
def _init_toolbar(self): pass
111+
112+
tb = NT2(fig.canvas)
113+
tb.zoom()
114+
115+
xlim0 = ax.get_xlim()
116+
ylim0 = ax.get_ylim()
117+
118+
# Zoom from x=1e-6, y=0.1 to x=1-1e-5, 0.8 (data coordinates, "d").
119+
d0 = (1e-6, 0.1)
120+
d1 = (1-1e-5, 0.8)
121+
# Convert to screen coordinates ("s"). Events are defined only with pixel
122+
# precision, so round the pixel values, and below, check against the
123+
# corresponding xdata/ydata, which are close but not equal to d0/d1.
124+
s0 = ax.transData.transform(d0).astype(int)
125+
s1 = ax.transData.transform(d1).astype(int)
126+
127+
# Zoom in.
128+
start_event = MouseEvent(
129+
"button_press_event", fig.canvas, *s0, MouseButton.LEFT)
130+
fig.canvas.callbacks.process(start_event.name, start_event)
131+
stop_event = MouseEvent(
132+
"button_release_event", fig.canvas, *s1, MouseButton.LEFT)
133+
fig.canvas.callbacks.process(stop_event.name, stop_event)
134+
assert ax.get_xlim() == (start_event.xdata, stop_event.xdata)
135+
assert ax.get_ylim() == (start_event.ydata, stop_event.ydata)
136+
137+
# Zoom out.
138+
start_event = MouseEvent(
139+
"button_press_event", fig.canvas, *s1, MouseButton.RIGHT)
140+
fig.canvas.callbacks.process(start_event.name, start_event)
141+
stop_event = MouseEvent(
142+
"button_release_event", fig.canvas, *s0, MouseButton.RIGHT)
143+
fig.canvas.callbacks.process(stop_event.name, stop_event)
144+
# Absolute tolerance much less than original xmin (1e-7).
145+
assert ax.get_xlim() == pytest.approx(xlim0, rel=0, abs=1e-10)
146+
assert ax.get_ylim() == pytest.approx(ylim0, rel=0, abs=1e-10)

0 commit comments

Comments
 (0)