Spaces:
Sleeping
Sleeping
| import "../core/document"; | |
| import "../core/rebind"; | |
| import "../event/drag"; | |
| import "../event/event"; | |
| import "../event/mouse"; | |
| import "../event/touches"; | |
| import "../selection/selection"; | |
| import "../interpolate/zoom"; | |
| import "behavior"; | |
| d3.behavior.zoom = function() { | |
| var view = {x: 0, y: 0, k: 1}, | |
| translate0, // translate when we started zooming (to avoid drift) | |
| center0, // implicit desired position of translate0 after zooming | |
| center, // explicit desired position of translate0 after zooming | |
| size = [960, 500], // viewport size; required for zoom interpolation | |
| scaleExtent = d3_behavior_zoomInfinity, | |
| duration = 250, | |
| zooming = 0, | |
| mousedown = "mousedown.zoom", | |
| mousemove = "mousemove.zoom", | |
| mouseup = "mouseup.zoom", | |
| mousewheelTimer, | |
| touchstart = "touchstart.zoom", | |
| touchtime, // time of last touchstart (to detect double-tap) | |
| event = d3_eventDispatch(zoom, "zoomstart", "zoom", "zoomend"), | |
| x0, | |
| x1, | |
| y0, | |
| y1; | |
| // Lazily determine the DOM’s support for Wheel events. | |
| // https://developer.mozilla.org/en-US/docs/Mozilla_event_reference/wheel | |
| if (!d3_behavior_zoomWheel) { | |
| d3_behavior_zoomWheel = "onwheel" in d3_document ? (d3_behavior_zoomDelta = function() { return -d3.event.deltaY * (d3.event.deltaMode ? 120 : 1); }, "wheel") | |
| : "onmousewheel" in d3_document ? (d3_behavior_zoomDelta = function() { return d3.event.wheelDelta; }, "mousewheel") | |
| : (d3_behavior_zoomDelta = function() { return -d3.event.detail; }, "MozMousePixelScroll"); | |
| } | |
| function zoom(g) { | |
| g .on(mousedown, mousedowned) | |
| .on(d3_behavior_zoomWheel + ".zoom", mousewheeled) | |
| .on("dblclick.zoom", dblclicked) | |
| .on(touchstart, touchstarted); | |
| } | |
| zoom.event = function(g) { | |
| g.each(function() { | |
| var dispatch = event.of(this, arguments), | |
| view1 = view; | |
| if (d3_transitionInheritId) { | |
| d3.select(this).transition() | |
| .each("start.zoom", function() { | |
| view = this.__chart__ || {x: 0, y: 0, k: 1}; // pre-transition state | |
| zoomstarted(dispatch); | |
| }) | |
| .tween("zoom:zoom", function() { | |
| var dx = size[0], | |
| dy = size[1], | |
| cx = center0 ? center0[0] : dx / 2, | |
| cy = center0 ? center0[1] : dy / 2, | |
| i = d3.interpolateZoom( | |
| [(cx - view.x) / view.k, (cy - view.y) / view.k, dx / view.k], | |
| [(cx - view1.x) / view1.k, (cy - view1.y) / view1.k, dx / view1.k] | |
| ); | |
| return function(t) { | |
| var l = i(t), k = dx / l[2]; | |
| this.__chart__ = view = {x: cx - l[0] * k, y: cy - l[1] * k, k: k}; | |
| zoomed(dispatch); | |
| }; | |
| }) | |
| .each("interrupt.zoom", function() { | |
| zoomended(dispatch); | |
| }) | |
| .each("end.zoom", function() { | |
| zoomended(dispatch); | |
| }); | |
| } else { | |
| this.__chart__ = view; | |
| zoomstarted(dispatch); | |
| zoomed(dispatch); | |
| zoomended(dispatch); | |
| } | |
| }); | |
| } | |
| zoom.translate = function(_) { | |
| if (!arguments.length) return [view.x, view.y]; | |
| view = {x: +_[0], y: +_[1], k: view.k}; // copy-on-write | |
| rescale(); | |
| return zoom; | |
| }; | |
| zoom.scale = function(_) { | |
| if (!arguments.length) return view.k; | |
| view = {x: view.x, y: view.y, k: null}; // copy-on-write | |
| scaleTo(+_); | |
| rescale(); | |
| return zoom; | |
| }; | |
| zoom.scaleExtent = function(_) { | |
| if (!arguments.length) return scaleExtent; | |
| scaleExtent = _ == null ? d3_behavior_zoomInfinity : [+_[0], +_[1]]; | |
| return zoom; | |
| }; | |
| zoom.center = function(_) { | |
| if (!arguments.length) return center; | |
| center = _ && [+_[0], +_[1]]; | |
| return zoom; | |
| }; | |
| zoom.size = function(_) { | |
| if (!arguments.length) return size; | |
| size = _ && [+_[0], +_[1]]; | |
| return zoom; | |
| }; | |
| zoom.duration = function(_) { | |
| if (!arguments.length) return duration; | |
| duration = +_; // TODO function based on interpolateZoom distance? | |
| return zoom; | |
| }; | |
| zoom.x = function(z) { | |
| if (!arguments.length) return x1; | |
| x1 = z; | |
| x0 = z.copy(); | |
| view = {x: 0, y: 0, k: 1}; // copy-on-write | |
| return zoom; | |
| }; | |
| zoom.y = function(z) { | |
| if (!arguments.length) return y1; | |
| y1 = z; | |
| y0 = z.copy(); | |
| view = {x: 0, y: 0, k: 1}; // copy-on-write | |
| return zoom; | |
| }; | |
| function location(p) { | |
| return [(p[0] - view.x) / view.k, (p[1] - view.y) / view.k]; | |
| } | |
| function point(l) { | |
| return [l[0] * view.k + view.x, l[1] * view.k + view.y]; | |
| } | |
| function scaleTo(s) { | |
| view.k = Math.max(scaleExtent[0], Math.min(scaleExtent[1], s)); | |
| } | |
| function translateTo(p, l) { | |
| l = point(l); | |
| view.x += p[0] - l[0]; | |
| view.y += p[1] - l[1]; | |
| } | |
| function zoomTo(that, p, l, k) { | |
| that.__chart__ = {x: view.x, y: view.y, k: view.k}; | |
| scaleTo(Math.pow(2, k)); | |
| translateTo(center0 = p, l); | |
| that = d3.select(that); | |
| if (duration > 0) that = that.transition().duration(duration); | |
| that.call(zoom.event); | |
| } | |
| function rescale() { | |
| if (x1) x1.domain(x0.range().map(function(x) { return (x - view.x) / view.k; }).map(x0.invert)); | |
| if (y1) y1.domain(y0.range().map(function(y) { return (y - view.y) / view.k; }).map(y0.invert)); | |
| } | |
| function zoomstarted(dispatch) { | |
| if (!zooming++) dispatch({type: "zoomstart"}); | |
| } | |
| function zoomed(dispatch) { | |
| rescale(); | |
| dispatch({type: "zoom", scale: view.k, translate: [view.x, view.y]}); | |
| } | |
| function zoomended(dispatch) { | |
| if (!--zooming) dispatch({type: "zoomend"}), center0 = null; | |
| } | |
| function mousedowned() { | |
| var that = this, | |
| dispatch = event.of(that, arguments), | |
| dragged = 0, | |
| subject = d3.select(d3_window(that)).on(mousemove, moved).on(mouseup, ended), | |
| location0 = location(d3.mouse(that)), | |
| dragRestore = d3_event_dragSuppress(that); | |
| d3_selection_interrupt.call(that); | |
| zoomstarted(dispatch); | |
| function moved() { | |
| dragged = 1; | |
| translateTo(d3.mouse(that), location0); | |
| zoomed(dispatch); | |
| } | |
| function ended() { | |
| subject.on(mousemove, null).on(mouseup, null); | |
| dragRestore(dragged); | |
| zoomended(dispatch); | |
| } | |
| } | |
| // These closures persist for as long as at least one touch is active. | |
| function touchstarted() { | |
| var that = this, | |
| dispatch = event.of(that, arguments), | |
| locations0 = {}, // touchstart locations | |
| distance0 = 0, // distance² between initial touches | |
| scale0, // scale when we started touching | |
| zoomName = ".zoom-" + d3.event.changedTouches[0].identifier, | |
| touchmove = "touchmove" + zoomName, | |
| touchend = "touchend" + zoomName, | |
| targets = [], | |
| subject = d3.select(that), | |
| dragRestore = d3_event_dragSuppress(that); | |
| started(); | |
| zoomstarted(dispatch); | |
| // Workaround for Chrome issue 412723: the touchstart listener must be set | |
| // after the touchmove listener. | |
| subject.on(mousedown, null).on(touchstart, started); // prevent duplicate events | |
| // Updates locations of any touches in locations0. | |
| function relocate() { | |
| var touches = d3.touches(that); | |
| scale0 = view.k; | |
| touches.forEach(function(t) { | |
| if (t.identifier in locations0) locations0[t.identifier] = location(t); | |
| }); | |
| return touches; | |
| } | |
| // Temporarily override touchstart while gesture is active. | |
| function started() { | |
| // Listen for touchmove and touchend on the target of touchstart. | |
| var target = d3.event.target; | |
| d3.select(target).on(touchmove, moved).on(touchend, ended); | |
| targets.push(target); | |
| // Only track touches started on the same subject element. | |
| var changed = d3.event.changedTouches; | |
| for (var i = 0, n = changed.length; i < n; ++i) { | |
| locations0[changed[i].identifier] = null; | |
| } | |
| var touches = relocate(), | |
| now = Date.now(); | |
| if (touches.length === 1) { | |
| if (now - touchtime < 500) { // dbltap | |
| var p = touches[0]; | |
| zoomTo(that, p, locations0[p.identifier], Math.floor(Math.log(view.k) / Math.LN2) + 1); | |
| d3_eventPreventDefault(); | |
| } | |
| touchtime = now; | |
| } else if (touches.length > 1) { | |
| var p = touches[0], q = touches[1], | |
| dx = p[0] - q[0], dy = p[1] - q[1]; | |
| distance0 = dx * dx + dy * dy; | |
| } | |
| } | |
| function moved() { | |
| var touches = d3.touches(that), | |
| p0, l0, | |
| p1, l1; | |
| d3_selection_interrupt.call(that); | |
| for (var i = 0, n = touches.length; i < n; ++i, l1 = null) { | |
| p1 = touches[i]; | |
| if (l1 = locations0[p1.identifier]) { | |
| if (l0) break; | |
| p0 = p1, l0 = l1; | |
| } | |
| } | |
| if (l1) { | |
| var distance1 = (distance1 = p1[0] - p0[0]) * distance1 + (distance1 = p1[1] - p0[1]) * distance1, | |
| scale1 = distance0 && Math.sqrt(distance1 / distance0); | |
| p0 = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2]; | |
| l0 = [(l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2]; | |
| scaleTo(scale1 * scale0); | |
| } | |
| touchtime = null; | |
| translateTo(p0, l0); | |
| zoomed(dispatch); | |
| } | |
| function ended() { | |
| // If there are any globally-active touches remaining, remove the ended | |
| // touches from locations0. | |
| if (d3.event.touches.length) { | |
| var changed = d3.event.changedTouches; | |
| for (var i = 0, n = changed.length; i < n; ++i) { | |
| delete locations0[changed[i].identifier]; | |
| } | |
| // If locations0 is not empty, then relocate and continue listening for | |
| // touchmove and touchend. | |
| for (var identifier in locations0) { | |
| return void relocate(); // locations may have detached due to rotation | |
| } | |
| } | |
| // Otherwise, remove touchmove and touchend listeners. | |
| d3.selectAll(targets).on(zoomName, null); | |
| subject.on(mousedown, mousedowned).on(touchstart, touchstarted); | |
| dragRestore(); | |
| zoomended(dispatch); | |
| } | |
| } | |
| function mousewheeled() { | |
| var dispatch = event.of(this, arguments); | |
| if (mousewheelTimer) clearTimeout(mousewheelTimer); | |
| else d3_selection_interrupt.call(this), translate0 = location(center0 = center || d3.mouse(this)), zoomstarted(dispatch); | |
| mousewheelTimer = setTimeout(function() { mousewheelTimer = null; zoomended(dispatch); }, 50); | |
| d3_eventPreventDefault(); | |
| scaleTo(Math.pow(2, d3_behavior_zoomDelta() * 0.002) * view.k); | |
| translateTo(center0, translate0); | |
| zoomed(dispatch); | |
| } | |
| function dblclicked() { | |
| var p = d3.mouse(this), | |
| k = Math.log(view.k) / Math.LN2; | |
| zoomTo(this, p, location(p), d3.event.shiftKey ? Math.ceil(k) - 1 : Math.floor(k) + 1); | |
| } | |
| return d3.rebind(zoom, event, "on"); | |
| }; | |
| var d3_behavior_zoomInfinity = [0, Infinity], // default scale extent | |
| d3_behavior_zoomDelta, // initialized lazily | |
| d3_behavior_zoomWheel; | |