|
1 // TODO unbind zoom behavior? |
|
2 d3.behavior.zoom = function() { |
|
3 var xyz = [0, 0, 0], |
|
4 event = d3.dispatch("zoom"), |
|
5 extent = d3_behavior_zoomInfiniteExtent; |
|
6 |
|
7 function zoom() { |
|
8 this |
|
9 .on("mousedown.zoom", mousedown) |
|
10 .on("mousewheel.zoom", mousewheel) |
|
11 .on("DOMMouseScroll.zoom", mousewheel) |
|
12 .on("dblclick.zoom", dblclick) |
|
13 .on("touchstart.zoom", touchstart); |
|
14 |
|
15 d3.select(window) |
|
16 .on("mousemove.zoom", d3_behavior_zoomMousemove) |
|
17 .on("mouseup.zoom", d3_behavior_zoomMouseup) |
|
18 .on("touchmove.zoom", d3_behavior_zoomTouchmove) |
|
19 .on("touchend.zoom", d3_behavior_zoomTouchup) |
|
20 .on("click.zoom", d3_behavior_zoomClick, true); |
|
21 } |
|
22 |
|
23 // snapshot the local context for subsequent dispatch |
|
24 function start() { |
|
25 d3_behavior_zoomXyz = xyz; |
|
26 d3_behavior_zoomExtent = extent; |
|
27 d3_behavior_zoomDispatch = event.zoom; |
|
28 d3_behavior_zoomEventTarget = d3.event.target; |
|
29 d3_behavior_zoomTarget = this; |
|
30 d3_behavior_zoomArguments = arguments; |
|
31 } |
|
32 |
|
33 function mousedown() { |
|
34 start.apply(this, arguments); |
|
35 d3_behavior_zoomPanning = d3_behavior_zoomLocation(d3.svg.mouse(d3_behavior_zoomTarget)); |
|
36 d3_behavior_zoomMoved = false; |
|
37 d3.event.preventDefault(); |
|
38 window.focus(); |
|
39 } |
|
40 |
|
41 // store starting mouse location |
|
42 function mousewheel() { |
|
43 start.apply(this, arguments); |
|
44 if (!d3_behavior_zoomZooming) d3_behavior_zoomZooming = d3_behavior_zoomLocation(d3.svg.mouse(d3_behavior_zoomTarget)); |
|
45 d3_behavior_zoomTo(d3_behavior_zoomDelta() + xyz[2], d3.svg.mouse(d3_behavior_zoomTarget), d3_behavior_zoomZooming); |
|
46 } |
|
47 |
|
48 function dblclick() { |
|
49 start.apply(this, arguments); |
|
50 var mouse = d3.svg.mouse(d3_behavior_zoomTarget); |
|
51 d3_behavior_zoomTo(d3.event.shiftKey ? Math.ceil(xyz[2] - 1) : Math.floor(xyz[2] + 1), mouse, d3_behavior_zoomLocation(mouse)); |
|
52 } |
|
53 |
|
54 // doubletap detection |
|
55 function touchstart() { |
|
56 start.apply(this, arguments); |
|
57 var touches = d3_behavior_zoomTouchup(), |
|
58 touch, |
|
59 now = Date.now(); |
|
60 if ((touches.length === 1) && (now - d3_behavior_zoomLast < 300)) { |
|
61 d3_behavior_zoomTo(1 + Math.floor(xyz[2]), touch = touches[0], d3_behavior_zoomLocations[touch.identifier]); |
|
62 } |
|
63 d3_behavior_zoomLast = now; |
|
64 } |
|
65 |
|
66 zoom.extent = function(x) { |
|
67 if (!arguments.length) return extent; |
|
68 extent = x == null ? d3_behavior_zoomInfiniteExtent : x; |
|
69 return zoom; |
|
70 }; |
|
71 |
|
72 zoom.on = function(type, listener) { |
|
73 event.on(type, listener); |
|
74 return zoom; |
|
75 }; |
|
76 |
|
77 return zoom; |
|
78 }; |
|
79 |
|
80 var d3_behavior_zoomDiv, |
|
81 d3_behavior_zoomPanning, |
|
82 d3_behavior_zoomZooming, |
|
83 d3_behavior_zoomLocations = {}, // identifier -> location |
|
84 d3_behavior_zoomLast = 0, |
|
85 d3_behavior_zoomXyz, |
|
86 d3_behavior_zoomExtent, |
|
87 d3_behavior_zoomDispatch, |
|
88 d3_behavior_zoomEventTarget, |
|
89 d3_behavior_zoomTarget, |
|
90 d3_behavior_zoomArguments, |
|
91 d3_behavior_zoomMoved, |
|
92 d3_behavior_zoomStopClick; |
|
93 |
|
94 function d3_behavior_zoomLocation(point) { |
|
95 return [ |
|
96 point[0] - d3_behavior_zoomXyz[0], |
|
97 point[1] - d3_behavior_zoomXyz[1], |
|
98 d3_behavior_zoomXyz[2] |
|
99 ]; |
|
100 } |
|
101 |
|
102 // detect the pixels that would be scrolled by this wheel event |
|
103 function d3_behavior_zoomDelta() { |
|
104 |
|
105 // mousewheel events are totally broken! |
|
106 // https://bugs.webkit.org/show_bug.cgi?id=40441 |
|
107 // not only that, but Chrome and Safari differ in re. to acceleration! |
|
108 if (!d3_behavior_zoomDiv) { |
|
109 d3_behavior_zoomDiv = d3.select("body").append("div") |
|
110 .style("visibility", "hidden") |
|
111 .style("top", 0) |
|
112 .style("height", 0) |
|
113 .style("width", 0) |
|
114 .style("overflow-y", "scroll") |
|
115 .append("div") |
|
116 .style("height", "2000px") |
|
117 .node().parentNode; |
|
118 } |
|
119 |
|
120 var e = d3.event, delta; |
|
121 try { |
|
122 d3_behavior_zoomDiv.scrollTop = 1000; |
|
123 d3_behavior_zoomDiv.dispatchEvent(e); |
|
124 delta = 1000 - d3_behavior_zoomDiv.scrollTop; |
|
125 } catch (error) { |
|
126 delta = e.wheelDelta || (-e.detail * 5); |
|
127 } |
|
128 |
|
129 return delta * .005; |
|
130 } |
|
131 |
|
132 // Note: Since we don't rotate, it's possible for the touches to become |
|
133 // slightly detached from their original positions. Thus, we recompute the |
|
134 // touch points on touchend as well as touchstart! |
|
135 function d3_behavior_zoomTouchup() { |
|
136 var touches = d3.svg.touches(d3_behavior_zoomTarget), |
|
137 i = -1, |
|
138 n = touches.length, |
|
139 touch; |
|
140 while (++i < n) d3_behavior_zoomLocations[(touch = touches[i]).identifier] = d3_behavior_zoomLocation(touch); |
|
141 return touches; |
|
142 } |
|
143 |
|
144 function d3_behavior_zoomTouchmove() { |
|
145 var touches = d3.svg.touches(d3_behavior_zoomTarget); |
|
146 switch (touches.length) { |
|
147 |
|
148 // single-touch pan |
|
149 case 1: { |
|
150 var touch = touches[0]; |
|
151 d3_behavior_zoomTo(d3_behavior_zoomXyz[2], touch, d3_behavior_zoomLocations[touch.identifier]); |
|
152 break; |
|
153 } |
|
154 |
|
155 // double-touch pan + zoom |
|
156 case 2: { |
|
157 var p0 = touches[0], |
|
158 p1 = touches[1], |
|
159 p2 = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2], |
|
160 l0 = d3_behavior_zoomLocations[p0.identifier], |
|
161 l1 = d3_behavior_zoomLocations[p1.identifier], |
|
162 l2 = [(l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2, l0[2]]; |
|
163 d3_behavior_zoomTo(Math.log(d3.event.scale) / Math.LN2 + l0[2], p2, l2); |
|
164 break; |
|
165 } |
|
166 } |
|
167 } |
|
168 |
|
169 function d3_behavior_zoomMousemove() { |
|
170 d3_behavior_zoomZooming = null; |
|
171 if (d3_behavior_zoomPanning) { |
|
172 d3_behavior_zoomMoved = true; |
|
173 d3_behavior_zoomTo(d3_behavior_zoomXyz[2], d3.svg.mouse(d3_behavior_zoomTarget), d3_behavior_zoomPanning); |
|
174 } |
|
175 } |
|
176 |
|
177 function d3_behavior_zoomMouseup() { |
|
178 if (d3_behavior_zoomPanning) { |
|
179 if (d3_behavior_zoomMoved && d3_behavior_zoomEventTarget === d3.event.target) { |
|
180 d3_behavior_zoomStopClick = true; |
|
181 } |
|
182 d3_behavior_zoomMousemove(); |
|
183 d3_behavior_zoomPanning = null; |
|
184 } |
|
185 } |
|
186 |
|
187 function d3_behavior_zoomClick() { |
|
188 if (d3_behavior_zoomStopClick && d3_behavior_zoomEventTarget === d3.event.target) { |
|
189 d3.event.stopPropagation(); |
|
190 d3.event.preventDefault(); |
|
191 d3_behavior_zoomStopClick = false; |
|
192 d3_behavior_zoomEventTarget = null; |
|
193 } |
|
194 } |
|
195 |
|
196 function d3_behavior_zoomTo(z, x0, x1) { |
|
197 z = d3_behavior_zoomExtentClamp(z, 2); |
|
198 var j = Math.pow(2, d3_behavior_zoomXyz[2]), |
|
199 k = Math.pow(2, z), |
|
200 K = Math.pow(2, (d3_behavior_zoomXyz[2] = z) - x1[2]), |
|
201 x_ = d3_behavior_zoomXyz[0], |
|
202 y_ = d3_behavior_zoomXyz[1], |
|
203 x = d3_behavior_zoomXyz[0] = d3_behavior_zoomExtentClamp((x0[0] - x1[0] * K), 0, k), |
|
204 y = d3_behavior_zoomXyz[1] = d3_behavior_zoomExtentClamp((x0[1] - x1[1] * K), 1, k), |
|
205 o = d3.event; // Events can be reentrant (e.g., focus). |
|
206 |
|
207 d3.event = { |
|
208 scale: k, |
|
209 translate: [x, y], |
|
210 transform: function(sx, sy) { |
|
211 if (sx) transform(sx, x_, x); |
|
212 if (sy) transform(sy, y_, y); |
|
213 } |
|
214 }; |
|
215 |
|
216 function transform(scale, a, b) { |
|
217 scale.domain(scale.range().map(function(v) { return scale.invert(((v - b) * j) / k + a); })); |
|
218 } |
|
219 |
|
220 try { |
|
221 d3_behavior_zoomDispatch.apply(d3_behavior_zoomTarget, d3_behavior_zoomArguments); |
|
222 } finally { |
|
223 d3.event = o; |
|
224 } |
|
225 |
|
226 o.preventDefault(); |
|
227 } |
|
228 |
|
229 var d3_behavior_zoomInfiniteExtent = [ |
|
230 [-Infinity, Infinity], |
|
231 [-Infinity, Infinity], |
|
232 [-Infinity, Infinity] |
|
233 ]; |
|
234 |
|
235 function d3_behavior_zoomExtentClamp(x, i, k) { |
|
236 var range = d3_behavior_zoomExtent[i], |
|
237 r0 = range[0], |
|
238 r1 = range[1]; |
|
239 return arguments.length === 3 |
|
240 ? Math.max(r1 * (r1 === Infinity ? -Infinity : 1 / k - 1), |
|
241 Math.min(r0 === -Infinity ? Infinity : r0, x / k)) * k |
|
242 : Math.max(r0, Math.min(r1, x)); |
|
243 } |