toolkit/javascript/d3/src/behavior/zoom.js
changeset 47 c0b4a8b5a012
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/toolkit/javascript/d3/src/behavior/zoom.js	Thu Apr 10 14:20:23 2014 +0200
@@ -0,0 +1,243 @@
+// TODO unbind zoom behavior?
+d3.behavior.zoom = function() {
+  var xyz = [0, 0, 0],
+      event = d3.dispatch("zoom"),
+      extent = d3_behavior_zoomInfiniteExtent;
+
+  function zoom() {
+    this
+        .on("mousedown.zoom", mousedown)
+        .on("mousewheel.zoom", mousewheel)
+        .on("DOMMouseScroll.zoom", mousewheel)
+        .on("dblclick.zoom", dblclick)
+        .on("touchstart.zoom", touchstart);
+
+    d3.select(window)
+        .on("mousemove.zoom", d3_behavior_zoomMousemove)
+        .on("mouseup.zoom", d3_behavior_zoomMouseup)
+        .on("touchmove.zoom", d3_behavior_zoomTouchmove)
+        .on("touchend.zoom", d3_behavior_zoomTouchup)
+        .on("click.zoom", d3_behavior_zoomClick, true);
+  }
+
+  // snapshot the local context for subsequent dispatch
+  function start() {
+    d3_behavior_zoomXyz = xyz;
+    d3_behavior_zoomExtent = extent;
+    d3_behavior_zoomDispatch = event.zoom;
+    d3_behavior_zoomEventTarget = d3.event.target;
+    d3_behavior_zoomTarget = this;
+    d3_behavior_zoomArguments = arguments;
+  }
+
+  function mousedown() {
+    start.apply(this, arguments);
+    d3_behavior_zoomPanning = d3_behavior_zoomLocation(d3.svg.mouse(d3_behavior_zoomTarget));
+    d3_behavior_zoomMoved = false;
+    d3.event.preventDefault();
+    window.focus();
+  }
+
+  // store starting mouse location
+  function mousewheel() {
+    start.apply(this, arguments);
+    if (!d3_behavior_zoomZooming) d3_behavior_zoomZooming = d3_behavior_zoomLocation(d3.svg.mouse(d3_behavior_zoomTarget));
+    d3_behavior_zoomTo(d3_behavior_zoomDelta() + xyz[2], d3.svg.mouse(d3_behavior_zoomTarget), d3_behavior_zoomZooming);
+  }
+
+  function dblclick() {
+    start.apply(this, arguments);
+    var mouse = d3.svg.mouse(d3_behavior_zoomTarget);
+    d3_behavior_zoomTo(d3.event.shiftKey ? Math.ceil(xyz[2] - 1) : Math.floor(xyz[2] + 1), mouse, d3_behavior_zoomLocation(mouse));
+  }
+
+  // doubletap detection
+  function touchstart() {
+    start.apply(this, arguments);
+    var touches = d3_behavior_zoomTouchup(),
+        touch,
+        now = Date.now();
+    if ((touches.length === 1) && (now - d3_behavior_zoomLast < 300)) {
+      d3_behavior_zoomTo(1 + Math.floor(xyz[2]), touch = touches[0], d3_behavior_zoomLocations[touch.identifier]);
+    }
+    d3_behavior_zoomLast = now;
+  }
+
+  zoom.extent = function(x) {
+    if (!arguments.length) return extent;
+    extent = x == null ? d3_behavior_zoomInfiniteExtent : x;
+    return zoom;
+  };
+
+  zoom.on = function(type, listener) {
+    event.on(type, listener);
+    return zoom;
+  };
+
+  return zoom;
+};
+
+var d3_behavior_zoomDiv,
+    d3_behavior_zoomPanning,
+    d3_behavior_zoomZooming,
+    d3_behavior_zoomLocations = {}, // identifier -> location
+    d3_behavior_zoomLast = 0,
+    d3_behavior_zoomXyz,
+    d3_behavior_zoomExtent,
+    d3_behavior_zoomDispatch,
+    d3_behavior_zoomEventTarget,
+    d3_behavior_zoomTarget,
+    d3_behavior_zoomArguments,
+    d3_behavior_zoomMoved,
+    d3_behavior_zoomStopClick;
+
+function d3_behavior_zoomLocation(point) {
+  return [
+    point[0] - d3_behavior_zoomXyz[0],
+    point[1] - d3_behavior_zoomXyz[1],
+    d3_behavior_zoomXyz[2]
+  ];
+}
+
+// detect the pixels that would be scrolled by this wheel event
+function d3_behavior_zoomDelta() {
+
+  // mousewheel events are totally broken!
+  // https://bugs.webkit.org/show_bug.cgi?id=40441
+  // not only that, but Chrome and Safari differ in re. to acceleration!
+  if (!d3_behavior_zoomDiv) {
+    d3_behavior_zoomDiv = d3.select("body").append("div")
+        .style("visibility", "hidden")
+        .style("top", 0)
+        .style("height", 0)
+        .style("width", 0)
+        .style("overflow-y", "scroll")
+      .append("div")
+        .style("height", "2000px")
+      .node().parentNode;
+  }
+
+  var e = d3.event, delta;
+  try {
+    d3_behavior_zoomDiv.scrollTop = 1000;
+    d3_behavior_zoomDiv.dispatchEvent(e);
+    delta = 1000 - d3_behavior_zoomDiv.scrollTop;
+  } catch (error) {
+    delta = e.wheelDelta || (-e.detail * 5);
+  }
+
+  return delta * .005;
+}
+
+// Note: Since we don't rotate, it's possible for the touches to become
+// slightly detached from their original positions. Thus, we recompute the
+// touch points on touchend as well as touchstart!
+function d3_behavior_zoomTouchup() {
+  var touches = d3.svg.touches(d3_behavior_zoomTarget),
+      i = -1,
+      n = touches.length,
+      touch;
+  while (++i < n) d3_behavior_zoomLocations[(touch = touches[i]).identifier] = d3_behavior_zoomLocation(touch);
+  return touches;
+}
+
+function d3_behavior_zoomTouchmove() {
+  var touches = d3.svg.touches(d3_behavior_zoomTarget);
+  switch (touches.length) {
+
+    // single-touch pan
+    case 1: {
+      var touch = touches[0];
+      d3_behavior_zoomTo(d3_behavior_zoomXyz[2], touch, d3_behavior_zoomLocations[touch.identifier]);
+      break;
+    }
+
+    // double-touch pan + zoom
+    case 2: {
+      var p0 = touches[0],
+          p1 = touches[1],
+          p2 = [(p0[0] + p1[0]) / 2, (p0[1] + p1[1]) / 2],
+          l0 = d3_behavior_zoomLocations[p0.identifier],
+          l1 = d3_behavior_zoomLocations[p1.identifier],
+          l2 = [(l0[0] + l1[0]) / 2, (l0[1] + l1[1]) / 2, l0[2]];
+      d3_behavior_zoomTo(Math.log(d3.event.scale) / Math.LN2 + l0[2], p2, l2);
+      break;
+    }
+  }
+}
+
+function d3_behavior_zoomMousemove() {
+  d3_behavior_zoomZooming = null;
+  if (d3_behavior_zoomPanning) {
+    d3_behavior_zoomMoved = true;
+    d3_behavior_zoomTo(d3_behavior_zoomXyz[2], d3.svg.mouse(d3_behavior_zoomTarget), d3_behavior_zoomPanning);
+  }
+}
+
+function d3_behavior_zoomMouseup() {
+  if (d3_behavior_zoomPanning) {
+    if (d3_behavior_zoomMoved && d3_behavior_zoomEventTarget === d3.event.target) {
+      d3_behavior_zoomStopClick = true;
+    }
+    d3_behavior_zoomMousemove();
+    d3_behavior_zoomPanning = null;
+  }
+}
+
+function d3_behavior_zoomClick() {
+  if (d3_behavior_zoomStopClick && d3_behavior_zoomEventTarget === d3.event.target) {
+    d3.event.stopPropagation();
+    d3.event.preventDefault();
+    d3_behavior_zoomStopClick = false;
+    d3_behavior_zoomEventTarget = null;
+  }
+}
+
+function d3_behavior_zoomTo(z, x0, x1) {
+  z = d3_behavior_zoomExtentClamp(z, 2);
+  var j = Math.pow(2, d3_behavior_zoomXyz[2]),
+      k = Math.pow(2, z),
+      K = Math.pow(2, (d3_behavior_zoomXyz[2] = z) - x1[2]),
+      x_ = d3_behavior_zoomXyz[0],
+      y_ = d3_behavior_zoomXyz[1],
+      x = d3_behavior_zoomXyz[0] = d3_behavior_zoomExtentClamp((x0[0] - x1[0] * K), 0, k),
+      y = d3_behavior_zoomXyz[1] = d3_behavior_zoomExtentClamp((x0[1] - x1[1] * K), 1, k),
+      o = d3.event; // Events can be reentrant (e.g., focus).
+
+  d3.event = {
+    scale: k,
+    translate: [x, y],
+    transform: function(sx, sy) {
+      if (sx) transform(sx, x_, x);
+      if (sy) transform(sy, y_, y);
+    }
+  };
+
+  function transform(scale, a, b) {
+    scale.domain(scale.range().map(function(v) { return scale.invert(((v - b) * j) / k + a); }));
+  }
+
+  try {
+    d3_behavior_zoomDispatch.apply(d3_behavior_zoomTarget, d3_behavior_zoomArguments);
+  } finally {
+    d3.event = o;
+  }
+
+  o.preventDefault();
+}
+
+var d3_behavior_zoomInfiniteExtent = [
+  [-Infinity, Infinity],
+  [-Infinity, Infinity],
+  [-Infinity, Infinity]
+];
+
+function d3_behavior_zoomExtentClamp(x, i, k) {
+  var range = d3_behavior_zoomExtent[i],
+      r0 = range[0],
+      r1 = range[1];
+  return arguments.length === 3
+      ? Math.max(r1 * (r1 === Infinity ? -Infinity : 1 / k - 1),
+        Math.min(r0 === -Infinity ? Infinity : r0, x / k)) * k
+      : Math.max(r0, Math.min(r1, x));
+}