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

Skip to content

Commit 47a15d9

Browse files
authored
Merge pull request #17067 from anntzer/zoomlogit
Simplify and generalize _set_view_from_bbox.
2 parents 1259895 + 2f6d98c commit 47a15d9

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
@@ -3849,18 +3849,15 @@ def _set_view_from_bbox(self, bbox, direction='in',
38493849
twiny : bool
38503850
Whether this axis is twinned in the *y*-direction.
38513851
"""
3852-
Xmin, Xmax = self.get_xlim()
3853-
Ymin, Ymax = self.get_ylim()
3854-
38553852
if len(bbox) == 3:
3856-
# Zooming code
3857-
xp, yp, scl = bbox
3853+
Xmin, Xmax = self.get_xlim()
3854+
Ymin, Ymax = self.get_ylim()
3855+
3856+
xp, yp, scl = bbox # Zooming code
38583857

3859-
# Should not happen
3860-
if scl == 0:
3858+
if scl == 0: # Should not happen
38613859
scl = 1.
38623860

3863-
# direction = 'in'
38643861
if scl > 1:
38653862
direction = 'in'
38663863
else:
@@ -3889,90 +3886,51 @@ def _set_view_from_bbox(self, bbox, direction='in',
38893886
"of length 3 or 4. Ignoring the view change.")
38903887
return
38913888

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

39773935
def start_pan(self, x, y, button):
39783936
"""

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)