toolkit/javascript/d3/src/svg/brush.js
changeset 47 c0b4a8b5a012
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/toolkit/javascript/d3/src/svg/brush.js	Thu Apr 10 14:20:23 2014 +0200
@@ -0,0 +1,335 @@
+d3.svg.brush = function() {
+  var event = d3.dispatch("brushstart", "brush", "brushend"),
+      x, // x-scale, optional
+      y, // y-scale, optional
+      extent = [[0, 0], [0, 0]]; // [x0, y0], [x1, y1]
+
+  function brush(g) {
+    var resizes = x && y ? ["n", "e", "s", "w", "nw", "ne", "se", "sw"]
+        : x ? ["e", "w"]
+        : y ? ["n", "s"]
+        : [];
+
+    g.each(function() {
+      var g = d3.select(this).on("mousedown.brush", down),
+          bg = g.selectAll(".background").data([,]),
+          fg = g.selectAll(".extent").data([,]),
+          tz = g.selectAll(".resize").data(resizes, String),
+          e;
+
+      // An invisible, mouseable area for starting a new brush.
+      bg.enter().append("svg:rect")
+          .attr("class", "background")
+          .style("visibility", "hidden")
+          .style("pointer-events", "all")
+          .style("cursor", "crosshair");
+
+      // The visible brush extent; style this as you like!
+      fg.enter().append("svg:rect")
+          .attr("class", "extent")
+          .style("cursor", "move");
+
+      // More invisible rects for resizing the extent.
+      tz.enter().append("svg:rect")
+          .attr("class", function(d) { return "resize " + d; })
+          .attr("width", 6)
+          .attr("height", 6)
+          .style("visibility", "hidden")
+          .style("pointer-events", brush.empty() ? "none" : "all")
+          .style("cursor", function(d) { return d3_svg_brushCursor[d]; });
+
+      // Remove any superfluous resizers.
+      tz.exit().remove();
+
+      // Initialize the background to fill the defined range.
+      // If the range isn't defined, you can post-process.
+      if (x) {
+        e = d3_scaleExtent(x.range());
+        bg.attr("x", e[0]).attr("width", e[1] - e[0]);
+        d3_svg_brushRedrawX(g, extent);
+      }
+      if (y) {
+        e = d3_scaleExtent(y.range());
+        bg.attr("y", e[0]).attr("height", e[1] - e[0]);
+        d3_svg_brushRedrawY(g, extent);
+      }
+    });
+  }
+
+  function down() {
+    var target = d3.select(d3.event.target);
+
+    // Store some global state for the duration of the brush gesture.
+    d3_svg_brush = brush;
+    d3_svg_brushTarget = this;
+    d3_svg_brushExtent = extent;
+    d3_svg_brushOffset = d3.svg.mouse(d3_svg_brushTarget);
+
+    // If the extent was clicked on, drag rather than brush;
+    // store the offset between the mouse and extent origin instead.
+    if (d3_svg_brushDrag = target.classed("extent")) {
+      d3_svg_brushOffset[0] = extent[0][0] - d3_svg_brushOffset[0];
+      d3_svg_brushOffset[1] = extent[0][1] - d3_svg_brushOffset[1];
+    }
+
+    // If a resizer was clicked on, record which side is to be resized.
+    // Also, set the offset to the opposite side.
+    else if (target.classed("resize")) {
+      d3_svg_brushResize = d3.event.target.__data__;
+      d3_svg_brushOffset[0] = extent[+/w$/.test(d3_svg_brushResize)][0];
+      d3_svg_brushOffset[1] = extent[+/^n/.test(d3_svg_brushResize)][1];
+    }
+
+    // If the ALT key is down when starting a brush, the center is at the mouse.
+    else if (d3.event.altKey) {
+      d3_svg_brushCenter = d3_svg_brushOffset.slice();
+    }
+
+    // Restrict which dimensions are resized.
+    d3_svg_brushX = !/^(n|s)$/.test(d3_svg_brushResize) && x;
+    d3_svg_brushY = !/^(e|w)$/.test(d3_svg_brushResize) && y;
+
+    // Notify listeners.
+    d3_svg_brushDispatch = dispatcher(this, arguments);
+    d3_svg_brushDispatch("brushstart");
+    d3_svg_brushMove();
+    d3_eventCancel();
+  }
+
+  function dispatcher(that, argumentz) {
+    return function(type) {
+      var e = d3.event;
+      try {
+        d3.event = {type: type, target: brush};
+        event[type].apply(that, argumentz);
+      } finally {
+        d3.event = e;
+      }
+    };
+  }
+
+  brush.x = function(z) {
+    if (!arguments.length) return x;
+    x = z;
+    return brush;
+  };
+
+  brush.y = function(z) {
+    if (!arguments.length) return y;
+    y = z;
+    return brush;
+  };
+
+  brush.extent = function(z) {
+    var x0, x1, y0, y1, t;
+
+    // Invert the pixel extent to data-space.
+    if (!arguments.length) {
+      if (x) {
+        x0 = x.invert(extent[0][0]), x1 = x.invert(extent[1][0]);
+        if (x1 < x0) t = x0, x0 = x1, x1 = t;
+      }
+      if (y) {
+        y0 = y.invert(extent[0][1]), y1 = y.invert(extent[1][1]);
+        if (y1 < y0) t = y0, y0 = y1, y1 = t;
+      }
+      return x && y ? [[x0, y0], [x1, y1]] : x ? [x0, x1] : y && [y0, y1];
+    }
+
+    // Scale the data-space extent to pixels.
+    if (x) {
+      x0 = z[0], x1 = z[1];
+      if (y) x0 = x0[0], x1 = x1[0];
+      x0 = x(x0), x1 = x(x1);
+      if (x1 < x0) t = x0, x0 = x1, x1 = t;
+      extent[0][0] = x0, extent[1][0] = x1;
+    }
+    if (y) {
+      y0 = z[0], y1 = z[1];
+      if (x) y0 = y0[1], y1 = y1[1];
+      y0 = y(y0), y1 = y(y1);
+      if (y1 < y0) t = y0, y0 = y1, y1 = t;
+      extent[0][1] = y0, extent[1][1] = y1;
+    }
+
+    return brush;
+  };
+
+  brush.clear = function() {
+    extent[0][0] =
+    extent[0][1] =
+    extent[1][0] =
+    extent[1][1] = 0;
+    return brush;
+  };
+
+  brush.empty = function() {
+    return (x && extent[0][0] === extent[1][0])
+        || (y && extent[0][1] === extent[1][1]);
+  };
+
+  brush.on = function(type, listener) {
+    event.on(type, listener);
+    return brush;
+  };
+
+  d3.select(window)
+      .on("mousemove.brush", d3_svg_brushMove)
+      .on("mouseup.brush", d3_svg_brushUp)
+      .on("keydown.brush", d3_svg_brushKeydown)
+      .on("keyup.brush", d3_svg_brushKeyup);
+
+  return brush;
+};
+
+var d3_svg_brush,
+    d3_svg_brushDispatch,
+    d3_svg_brushTarget,
+    d3_svg_brushX,
+    d3_svg_brushY,
+    d3_svg_brushExtent,
+    d3_svg_brushDrag,
+    d3_svg_brushResize,
+    d3_svg_brushCenter,
+    d3_svg_brushOffset;
+
+function d3_svg_brushRedrawX(g, extent) {
+  g.select(".extent").attr("x", extent[0][0]);
+  g.selectAll(".n,.s,.w,.nw,.sw").attr("x", extent[0][0] - 2);
+  g.selectAll(".e,.ne,.se").attr("x", extent[1][0] - 3);
+  g.selectAll(".extent,.n,.s").attr("width", extent[1][0] - extent[0][0]);
+}
+
+function d3_svg_brushRedrawY(g, extent) {
+  g.select(".extent").attr("y", extent[0][1]);
+  g.selectAll(".n,.e,.w,.nw,.ne").attr("y", extent[0][1] - 3);
+  g.selectAll(".s,.se,.sw").attr("y", extent[1][1] - 4);
+  g.selectAll(".extent,.e,.w").attr("height", extent[1][1] - extent[0][1]);
+}
+
+function d3_svg_brushKeydown() {
+  if (d3.event.keyCode == 32 && d3_svg_brushTarget && !d3_svg_brushDrag) {
+    d3_svg_brushCenter = null;
+    d3_svg_brushOffset[0] -= d3_svg_brushExtent[1][0];
+    d3_svg_brushOffset[1] -= d3_svg_brushExtent[1][1];
+    d3_svg_brushDrag = 2;
+    d3_eventCancel();
+  }
+}
+
+function d3_svg_brushKeyup() {
+  if (d3.event.keyCode == 32 && d3_svg_brushDrag == 2) {
+    d3_svg_brushOffset[0] += d3_svg_brushExtent[1][0];
+    d3_svg_brushOffset[1] += d3_svg_brushExtent[1][1];
+    d3_svg_brushDrag = 0;
+    d3_eventCancel();
+  }
+}
+
+function d3_svg_brushMove() {
+  if (d3_svg_brushOffset) {
+    var mouse = d3.svg.mouse(d3_svg_brushTarget),
+        g = d3.select(d3_svg_brushTarget);
+
+    if (!d3_svg_brushDrag) {
+
+      // If needed, determine the center from the current extent.
+      if (d3.event.altKey) {
+        if (!d3_svg_brushCenter) {
+          d3_svg_brushCenter = [
+            (d3_svg_brushExtent[0][0] + d3_svg_brushExtent[1][0]) / 2,
+            (d3_svg_brushExtent[0][1] + d3_svg_brushExtent[1][1]) / 2
+          ];
+        }
+
+        // Update the offset, for when the ALT key is released.
+        d3_svg_brushOffset[0] = d3_svg_brushExtent[+(mouse[0] < d3_svg_brushCenter[0])][0];
+        d3_svg_brushOffset[1] = d3_svg_brushExtent[+(mouse[1] < d3_svg_brushCenter[1])][1];
+      }
+
+      // When the ALT key is released, we clear the center.
+      else d3_svg_brushCenter = null;
+    }
+
+    // Update the brush extent for each dimension.
+    if (d3_svg_brushX) {
+      d3_svg_brushMove1(mouse, d3_svg_brushX, 0);
+      d3_svg_brushRedrawX(g, d3_svg_brushExtent);
+    }
+    if (d3_svg_brushY) {
+      d3_svg_brushMove1(mouse, d3_svg_brushY, 1);
+      d3_svg_brushRedrawY(g, d3_svg_brushExtent);
+    }
+
+    // Notify listeners.
+    d3_svg_brushDispatch("brush");
+  }
+}
+
+function d3_svg_brushMove1(mouse, scale, i) {
+  var range = d3_scaleExtent(scale.range()),
+      offset = d3_svg_brushOffset[i],
+      size = d3_svg_brushExtent[1][i] - d3_svg_brushExtent[0][i],
+      min,
+      max;
+
+  // When dragging, reduce the range by the extent size and offset.
+  if (d3_svg_brushDrag) {
+    range[0] -= offset;
+    range[1] -= size + offset;
+  }
+
+  // Clamp the mouse so that the extent fits within the range extent.
+  min = Math.max(range[0], Math.min(range[1], mouse[i]));
+
+  // Compute the new extent bounds.
+  if (d3_svg_brushDrag) {
+    max = (min += offset) + size;
+  } else {
+
+    // If the ALT key is pressed, then preserve the center of the extent.
+    if (d3_svg_brushCenter) offset = Math.max(range[0], Math.min(range[1], 2 * d3_svg_brushCenter[i] - min));
+
+    // Compute the min and max of the offset and mouse.
+    if (offset < min) {
+      max = min;
+      min = offset;
+    } else {
+      max = offset;
+    }
+  }
+
+  // Update the stored bounds.
+  d3_svg_brushExtent[0][i] = min;
+  d3_svg_brushExtent[1][i] = max;
+}
+
+function d3_svg_brushUp() {
+  if (d3_svg_brushOffset) {
+    d3_svg_brushMove();
+    d3.select(d3_svg_brushTarget).selectAll(".resize").style("pointer-events", d3_svg_brush.empty() ? "none" : "all");
+    d3_svg_brushDispatch("brushend");
+    d3_svg_brush =
+    d3_svg_brushDispatch =
+    d3_svg_brushTarget =
+    d3_svg_brushX =
+    d3_svg_brushY =
+    d3_svg_brushExtent =
+    d3_svg_brushDrag =
+    d3_svg_brushResize =
+    d3_svg_brushCenter =
+    d3_svg_brushOffset = null;
+    d3_eventCancel();
+  }
+}
+
+var d3_svg_brushCursor = {
+  n: "ns-resize",
+  e: "ew-resize",
+  s: "ns-resize",
+  w: "ew-resize",
+  nw: "nwse-resize",
+  ne: "nesw-resize",
+  se: "nwse-resize",
+  sw: "nesw-resize"
+};