Skip to content

Commit 48a4871

Browse files
committed
Smooth and efficient zooming and panning.
Based on the interpolate-zoom plugin. Required adding a zoom.size method to specify the viewport size of the zoomable area.
1 parent 079db13 commit 48a4871

6 files changed

Lines changed: 138 additions & 43 deletions

File tree

d3.js

Lines changed: 56 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,11 +1137,48 @@ d3 = function() {
11371137
};
11381138
return d3.rebind(drag, event, "on");
11391139
};
1140+
var π = Math.PI, ε = 1e-6, ε2 = ε * ε, d3_radians = π / 180, d3_degrees = 180 / π;
1141+
function d3_sgn(x) {
1142+
return x > 0 ? 1 : x < 0 ? -1 : 0;
1143+
}
1144+
function d3_acos(x) {
1145+
return x > 1 ? 0 : x < -1 ? π : Math.acos(x);
1146+
}
1147+
function d3_asin(x) {
1148+
return x > 1 ? π / 2 : x < -1 ? -π / 2 : Math.asin(x);
1149+
}
1150+
function d3_sinh(x) {
1151+
return (Math.exp(x) - Math.exp(-x)) / 2;
1152+
}
1153+
function d3_cosh(x) {
1154+
return (Math.exp(x) + Math.exp(-x)) / 2;
1155+
}
1156+
function d3_tanh(x) {
1157+
return d3_sinh(x) / d3_cosh(x);
1158+
}
1159+
function d3_haversin(x) {
1160+
return (x = Math.sin(x / 2)) * x;
1161+
}
1162+
var ρ = Math.SQRT2, ρ2 = 2, ρ4 = 4;
1163+
d3.interpolateZoom = function(p0, p1) {
1164+
var ux0 = p0[0], uy0 = p0[1], w0 = p0[2], ux1 = p1[0], uy1 = p1[1], w1 = p1[2];
1165+
var dx = ux1 - ux0, dy = uy1 - uy0, d2 = dx * dx + dy * dy, d1 = Math.sqrt(d2), b0 = (w1 * w1 - w0 * w0 + ρ4 * d2) / (2 * w0 * ρ2 * d1), b1 = (w1 * w1 - w0 * w0 - ρ4 * d2) / (2 * w1 * ρ2 * d1), r0 = Math.log(Math.sqrt(b0 * b0 + 1) - b0), r1 = Math.log(Math.sqrt(b1 * b1 + 1) - b1), dr = r1 - r0, S = (dr || Math.log(w1 / w0)) / ρ;
1166+
function interpolate(t) {
1167+
var s = t * S;
1168+
if (dr) {
1169+
var coshr0 = d3_cosh(r0), u = w0 / (ρ2 * d1) * (coshr0 * d3_tanh(ρ * s + r0) - d3_sinh(r0));
1170+
return [ ux0 + u * dx, uy0 + u * dy, w0 * coshr0 / d3_cosh(ρ * s + r0) ];
1171+
}
1172+
return [ ux0 + t * dx, uy0 + t * dy, w0 * Math.exp(ρ * s) ];
1173+
}
1174+
interpolate.duration = S * 1e3;
1175+
return interpolate;
1176+
};
11401177
d3.behavior.zoom = function() {
11411178
var view = {
11421179
s: 1,
11431180
t: [ 0, 0 ]
1144-
}, translate0, center, scaleExtent = d3_behavior_zoomInfinity, mousedown = "mousedown.zoom", mousemove = "mousemove.zoom", mouseup = "mouseup.zoom", mousewheelTimer, touchstart = "touchstart.zoom", touchmove = "touchmove.zoom", touchend = "touchend.zoom", touchtime, event = d3_eventDispatch(zoom, "zoomstart", "zoom", "zoomend"), x0, x1, y0, y1;
1181+
}, translate0, center, size = [ 960, 500 ], scaleExtent = d3_behavior_zoomInfinity, mousedown = "mousedown.zoom", mousemove = "mousemove.zoom", mouseup = "mouseup.zoom", mousewheelTimer, touchstart = "touchstart.zoom", touchmove = "touchmove.zoom", touchend = "touchend.zoom", touchtime, event = d3_eventDispatch(zoom, "zoomstart", "zoom", "zoomend"), x0, x1, y0, y1;
11451182
function zoom(g) {
11461183
g.on(mousedown, mousedowned).on(d3_behavior_zoomWheel + ".zoom", mousewheeled).on(mousemove, mousewheelreset).on("dblclick.zoom", dblclicked).on(touchstart, touchstarted);
11471184
}
@@ -1157,9 +1194,13 @@ d3 = function() {
11571194
view = view0;
11581195
zoomstarted(event_);
11591196
}).tween("zoom:zoom", function() {
1160-
var i = d3_interpolate(view, view1);
1197+
var dx = size[0], dy = size[1], cx = dx / 2, cy = dy / 2, i = d3.interpolateZoom([ (cx - view.t[0]) / view.s, (cy - view.t[1]) / view.s, dx / view.s ], [ (cx - view1.t[0]) / view1.s, (cy - view1.t[1]) / view1.s, dx / view1.s ]);
11611198
return function(t) {
1162-
this.__chart__ = view = i(t);
1199+
var l = i(t), k = dx / l[2];
1200+
this.__chart__ = view = {
1201+
s: k,
1202+
t: [ cx - l[0] * k, cy - l[1] * k ]
1203+
};
11631204
zoomed(event_);
11641205
};
11651206
}).each("end.zoom", function() {
@@ -1172,32 +1213,37 @@ d3 = function() {
11721213
}
11731214
});
11741215
};
1175-
zoom.translate = function(x) {
1216+
zoom.translate = function(_) {
11761217
if (!arguments.length) return view.t;
11771218
view = {
11781219
s: view.s,
1179-
t: x.map(Number)
1220+
t: [ +_[0], +_[1] ]
11801221
};
11811222
rescale();
11821223
return zoom;
11831224
};
1184-
zoom.scale = function(x) {
1225+
zoom.scale = function(_) {
11851226
if (!arguments.length) return view.s;
11861227
view = {
1187-
s: +x,
1228+
s: +_,
11881229
t: view.t.slice()
11891230
};
11901231
rescale();
11911232
return zoom;
11921233
};
1193-
zoom.scaleExtent = function(x) {
1234+
zoom.scaleExtent = function(_) {
11941235
if (!arguments.length) return scaleExtent;
1195-
scaleExtent = x == null ? d3_behavior_zoomInfinity : x.map(Number);
1236+
scaleExtent = _ == null ? d3_behavior_zoomInfinity : [ +_[0], +_[1] ];
11961237
return zoom;
11971238
};
11981239
zoom.center = function(_) {
11991240
if (!arguments.length) return center;
1200-
center = _ && _.map(Number);
1241+
center = _ && [ +_[0], +_[1] ];
1242+
return zoom;
1243+
};
1244+
zoom.size = function(_) {
1245+
if (!arguments.length) return size;
1246+
size = _ && [ +_[0], +_[1] ];
12011247
return zoom;
12021248
};
12031249
zoom.x = function(z) {
@@ -1411,25 +1457,6 @@ d3 = function() {
14111457
}
14121458
return d3_rgb(vv(h + 120), vv(h), vv(h - 120));
14131459
}
1414-
var π = Math.PI, ε = 1e-6, ε2 = ε * ε, d3_radians = π / 180, d3_degrees = 180 / π;
1415-
function d3_sgn(x) {
1416-
return x > 0 ? 1 : x < 0 ? -1 : 0;
1417-
}
1418-
function d3_acos(x) {
1419-
return x > 1 ? 0 : x < -1 ? π : Math.acos(x);
1420-
}
1421-
function d3_asin(x) {
1422-
return x > 1 ? π / 2 : x < -1 ? -π / 2 : Math.asin(x);
1423-
}
1424-
function d3_sinh(x) {
1425-
return (Math.exp(x) - Math.exp(-x)) / 2;
1426-
}
1427-
function d3_cosh(x) {
1428-
return (Math.exp(x) + Math.exp(-x)) / 2;
1429-
}
1430-
function d3_haversin(x) {
1431-
return (x = Math.sin(x / 2)) * x;
1432-
}
14331460
d3.hcl = function(h, c, l) {
14341461
return arguments.length === 1 ? h instanceof d3_Hcl ? d3_hcl(h.h, h.c, h.l) : h instanceof d3_Lab ? d3_lab_hcl(h.l, h.a, h.b) : d3_lab_hcl((h = d3_rgb_lab((h = d3.rgb(h)).r, h.g, h.b)).l, h.a, h.b) : d3_hcl(+h, +c, +l);
14351462
};

d3.min.js

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/behavior/zoom.js

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,12 +5,14 @@ import "../event/event";
55
import "../event/mouse";
66
import "../event/touches";
77
import "../selection/selection";
8+
import "../interpolate/zoom";
89
import "behavior";
910

1011
d3.behavior.zoom = function() {
1112
var view = {s: 1, t: [0, 0]},
1213
translate0, // translate when we started zooming (to avoid drift)
1314
center, // desired position of translate0 after zooming
15+
size = [960, 500], // viewport size; required for zoom interpolation
1416
scaleExtent = d3_behavior_zoomInfinity,
1517
mousedown = "mousedown.zoom",
1618
mousemove = "mousemove.zoom",
@@ -47,9 +49,17 @@ d3.behavior.zoom = function() {
4749
zoomstarted(event_);
4850
})
4951
.tween("zoom:zoom", function() {
50-
var i = d3_interpolate(view, view1);
52+
var dx = size[0],
53+
dy = size[1],
54+
cx = dx / 2,
55+
cy = dy / 2,
56+
i = d3.interpolateZoom(
57+
[(cx - view.t[0]) / view.s, (cy - view.t[1]) / view.s, dx / view.s],
58+
[(cx - view1.t[0]) / view1.s, (cy - view1.t[1]) / view1.s, dx / view1.s]
59+
);
5160
return function(t) {
52-
this.__chart__ = view = i(t); // transition and post-transition state
61+
var l = i(t), k = dx / l[2];
62+
this.__chart__ = view = {s: k, t: [cx - l[0] * k, cy - l[1] * k]};
5363
zoomed(event_);
5464
};
5565
})
@@ -64,29 +74,35 @@ d3.behavior.zoom = function() {
6474
});
6575
}
6676

67-
zoom.translate = function(x) {
77+
zoom.translate = function(_) {
6878
if (!arguments.length) return view.t;
69-
view = {s: view.s, t: x.map(Number)}; // copy-on-write
79+
view = {s: view.s, t: [+_[0], +_[1]]}; // copy-on-write
7080
rescale();
7181
return zoom;
7282
};
7383

74-
zoom.scale = function(x) {
84+
zoom.scale = function(_) {
7585
if (!arguments.length) return view.s;
76-
view = {s: +x, t: view.t.slice()}; // copy-on-write
86+
view = {s: +_, t: view.t.slice()}; // copy-on-write
7787
rescale();
7888
return zoom;
7989
};
8090

81-
zoom.scaleExtent = function(x) {
91+
zoom.scaleExtent = function(_) {
8292
if (!arguments.length) return scaleExtent;
83-
scaleExtent = x == null ? d3_behavior_zoomInfinity : x.map(Number);
93+
scaleExtent = _ == null ? d3_behavior_zoomInfinity : [+_[0], +_[1]];
8494
return zoom;
8595
};
8696

8797
zoom.center = function(_) {
8898
if (!arguments.length) return center;
89-
center = _ && _.map(Number);
99+
center = _ && [+_[0], +_[1]];
100+
return zoom;
101+
};
102+
103+
zoom.size = function(_) {
104+
if (!arguments.length) return size;
105+
size = _ && [+_[0], +_[1]];
90106
return zoom;
91107
};
92108

src/interpolate/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,4 @@ import "round";
1111
import "string";
1212
import "transform";
1313
import "uninterpolate";
14+
import "zoom";

src/interpolate/zoom.js

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
import "../math/trigonometry";
2+
3+
var ρ = Math.SQRT2,
4+
ρ2 = 2,
5+
ρ4 = 4;
6+
7+
// p0 = [ux0, uy0, w0]
8+
// p1 = [ux1, uy1, w1]
9+
d3.interpolateZoom = function(p0, p1) {
10+
var ux0 = p0[0], uy0 = p0[1], w0 = p0[2],
11+
ux1 = p1[0], uy1 = p1[1], w1 = p1[2];
12+
13+
var dx = ux1 - ux0,
14+
dy = uy1 - uy0,
15+
d2 = dx * dx + dy * dy,
16+
d1 = Math.sqrt(d2),
17+
b0 = (w1 * w1 - w0 * w0 + ρ4 * d2) / (2 * w0 * ρ2 * d1),
18+
b1 = (w1 * w1 - w0 * w0 - ρ4 * d2) / (2 * w1 * ρ2 * d1),
19+
r0 = Math.log(Math.sqrt(b0 * b0 + 1) - b0),
20+
r1 = Math.log(Math.sqrt(b1 * b1 + 1) - b1),
21+
dr = r1 - r0,
22+
S = (dr || Math.log(w1 / w0)) / ρ;
23+
24+
function interpolate(t) {
25+
var s = t * S;
26+
if (dr) {
27+
// General case.
28+
var coshr0 = d3_cosh(r0),
29+
u = w0 / (ρ2 * d1) * (coshr0 * d3_tanh(ρ * s + r0) - d3_sinh(r0));
30+
return [
31+
ux0 + u * dx,
32+
uy0 + u * dy,
33+
w0 * coshr0 / d3_cosh(ρ * s + r0)
34+
];
35+
}
36+
// Special case for u0 ~= u1.
37+
return [
38+
ux0 + t * dx,
39+
uy0 + t * dy,
40+
w0 * Math.exp(ρ * s)
41+
];
42+
}
43+
44+
interpolate.duration = S * 1000;
45+
46+
return interpolate;
47+
};

src/math/trigonometry.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,10 @@ function d3_cosh(x) {
2424
return (Math.exp(x) + Math.exp(-x)) / 2;
2525
}
2626

27+
function d3_tanh(x) {
28+
return d3_sinh(x) / d3_cosh(x);
29+
}
30+
2731
function d3_haversin(x) {
2832
return (x = Math.sin(x / 2)) * x;
2933
}

0 commit comments

Comments
 (0)