Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Appearance settings

Commit 2f6d98c

Browse filesBrowse 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
Copy full SHA for 2f6d98c

File tree

Expand file treeCollapse file tree

2 files changed

+96
-93
lines changed
Filter options
Expand file treeCollapse file tree

2 files changed

+96
-93
lines changed

‎lib/matplotlib/axes/_base.py

Copy file name to clipboardExpand all lines: lib/matplotlib/axes/_base.py
+50-92Lines 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

Copy file name to clipboardExpand all lines: lib/matplotlib/tests/test_backend_bases.py
+46-1Lines 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 (0)
Morty Proxy This is a proxified and sanitized view of the page, visit original site.