toolkit/javascript/d3/src/svg/brush.js
author Nicolas Sauret <nicolas.sauret@iri.centrepompidou.fr>
Thu, 10 Apr 2014 14:20:23 +0200
changeset 47 c0b4a8b5a012
permissions -rw-r--r--
add toolkit.html + démonstrateurs

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"
};