|
d3.svg.multibrush = function() { |
|
|
|
// From d3/scale/scale.js |
|
function d3_scaleExtent(domain) { |
|
var start = domain[0], stop = domain[domain.length - 1]; |
|
return start < stop ? [ start, stop ] : [ stop, start ]; |
|
} |
|
function d3_scaleRange(scale) { |
|
return scale.rangeExtent ? scale.rangeExtent() : d3_scaleExtent(scale.range()); |
|
} |
|
|
|
// From d3 |
|
var d3_event_dragSelect = "onselectstart" in document ? null : d3_vendorSymbol(document.documentElement.style, "userSelect"), d3_event_dragId = 0; |
|
function d3_event_dragSuppress() { |
|
var name = ".dragsuppress-" + ++d3_event_dragId, click = "click" + name, w = d3.select(window).on("touchmove" + name, d3.event.preventDefault()).on("dragstart" + name, d3.event.preventDefault()).on("selectstart" + name, d3.event.preventDefault()); |
|
if (d3_event_dragSelect) { |
|
var style = d3_documentElement.style, select = style[d3_event_dragSelect]; |
|
style[d3_event_dragSelect] = "none"; |
|
} |
|
return function(suppressClick) { |
|
w.on(name, null); |
|
if (d3_event_dragSelect) style[d3_event_dragSelect] = select; |
|
if (suppressClick) { |
|
function off() { |
|
w.on(click, null); |
|
} |
|
w.on(click, function() { |
|
d3.event.preventDefault(); |
|
off(); |
|
}, true); |
|
setTimeout(off, 0); |
|
} |
|
}; |
|
} |
|
|
|
var event = d3.dispatch("brushstart", "brush", "brushend"), |
|
brushElement, |
|
x = null, // x-scale, optional |
|
y = null, // y-scale, optional |
|
xExtent = [[0, 0]], // [x0, x1] in integer pixels |
|
yExtent = [[0, 0]], // [y0, y1] in integer pixels |
|
xExtentDomain = [], // x-extent in data space |
|
yExtentDomain = [], // y-extent in data space |
|
xClamp = true, // whether to clamp the x-extent to the range |
|
yClamp = true, // whether to clamp the y-extent to the range |
|
resizes = d3_svg_brushResizes[0], |
|
resizeAdaption = function () {}, // Function to 'call' on new resize selection |
|
extentAdaption = function () {}; // Function to 'call' on new extent selection |
|
|
|
event.of = function(thiz, argumentz) { |
|
return function(e1) { |
|
try { |
|
var e0 = |
|
e1.sourceEvent = d3.event; |
|
e1.target = brush; |
|
d3.event = e1; |
|
event[e1.type].apply(thiz, argumentz); |
|
} finally { |
|
d3.event = e0; |
|
} |
|
}; |
|
}; |
|
|
|
function brush(g) { |
|
g.each(function() { |
|
|
|
// Prepare the brush container for events. |
|
var g = d3.select(this) |
|
.style("pointer-events", "all") |
|
.style("-webkit-tap-highlight-color", "rgba(0,0,0,0)") |
|
.on("mousedown.brush", brushstart) |
|
.on("touchstart.brush", brushstart); |
|
|
|
brushElement = g; |
|
|
|
// An invisible, mouseable area for starting a new brush. |
|
var background = g.selectAll(".background") |
|
.data([0]); |
|
|
|
background.enter().append("rect") |
|
.attr("class", "background") |
|
.style("visibility", "hidden") |
|
.style("cursor", "crosshair"); |
|
|
|
drawExtents(g); |
|
|
|
// When called on a transition, use a transition to update. |
|
var gUpdate = d3.transition(g), |
|
backgroundUpdate = d3.transition(background), |
|
range; |
|
|
|
// Initialize the background to fill the defined range. |
|
// If the range isn't defined, you can post-process. |
|
if (x) { |
|
range = d3_scaleRange(x); |
|
backgroundUpdate.attr("x", range[0]).attr("width", range[1] - range[0]); |
|
redrawX(gUpdate); |
|
} |
|
if (y) { |
|
range = d3_scaleRange(y); |
|
backgroundUpdate.attr("y", range[0]).attr("height", range[1] - range[0]); |
|
redrawY(gUpdate); |
|
} |
|
redraw(gUpdate); |
|
}); |
|
} |
|
|
|
function drawExtents(g) { |
|
var ex = xExtent.length > yExtent.length ? xExtent : yExtent, |
|
i = ex.length |
|
extentArr = ex.map(function(d,i) { return i; }), |
|
extentResizes = d3.merge(ex.map(function(d,i) { return resizes.map(function(r) { return [r, i]; }); })); |
|
|
|
if(!g) g = brushElement; |
|
|
|
// The visible brush extent; style this as you like! |
|
var extent = g.selectAll(".extent") |
|
.data(extentArr, function (d) { return d; }); |
|
|
|
extent.exit().remove(); |
|
|
|
extent.enter().append("rect") |
|
.attr("class", "extent") |
|
.style("cursor", "move") |
|
.call(extentAdaption); |
|
|
|
// More invisible rects for resizing the extent. |
|
var resize = g.selectAll(".resize") |
|
.data(extentResizes, function (d) { return d[0] + d[1]; }); |
|
|
|
// Remove any superfluous resizers. |
|
resize.exit().remove(); |
|
|
|
var newResize = resize.enter().append("g") |
|
.attr("class", function(d) { return "resize " + d[0]; }) |
|
.style("cursor", function(d) { return d3_svg_brushCursor[d[0]]; }); |
|
|
|
newResize.append("rect") |
|
.attr("x", function(d) { return /[ew]$/.test(d[0]) ? -3 : null; }) |
|
.attr("y", function(d) { return /^[ns]/.test(d[0]) ? -3 : null; }) |
|
.attr("width", 6) |
|
.attr("height", 6) |
|
.style("visibility", "hidden"); |
|
|
|
newResize.call(resizeAdaption); |
|
|
|
// Show or hide the resizers. |
|
resize.style("display", function (d) { return brush.empty(d[1]) ? "none" : null; }); |
|
} |
|
|
|
brush.event = function(g) { |
|
g.each(function() { |
|
var event_ = event.of(this, arguments), |
|
extent1 = {x: xExtent, y: yExtent, i: xExtentDomain, j: yExtentDomain}, |
|
extent0 = this.__chart__ || extent1; |
|
this.__chart__ = extent1; |
|
if (d3_transitionInheritId) { |
|
d3.select(this).transition() |
|
.each("start.brush", function() { |
|
xExtentDomain = extent0.i; // pre-transition state |
|
yExtentDomain = extent0.j; |
|
xExtent = extent0.x; |
|
yExtent = extent0.y; |
|
event_({type: "brushstart"}); |
|
}) |
|
.tween("brush:brush", function() { |
|
// TODO: transitions for all extents |
|
var xi = d3_interpolateArray(xExtent[0], extent1.x[0]), |
|
yi = d3_interpolateArray(yExtent[0], extent1.y[0]); |
|
xExtentDomain[0] = yExtentDomain[0] = null; // transition state |
|
return function(t) { |
|
xExtent[0] = extent1.x[0] = xi(t); |
|
yExtent[0] = extent1.y[0] = yi(t); |
|
event_({type: "brush", mode: "resize"}); |
|
}; |
|
}) |
|
.each("end.brush", function() { |
|
xExtentDomain = extent1.i; // post-transition state |
|
yExtentDomain = extent1.j; |
|
event_({type: "brush", mode: "resize"}); |
|
event_({type: "brushend"}); |
|
}); |
|
} else { |
|
event_({type: "brushstart"}); |
|
event_({type: "brush", mode: "resize"}); |
|
event_({type: "brushend"}); |
|
} |
|
}); |
|
}; |
|
|
|
function redraw(g) { |
|
g.selectAll(".resize").attr("transform", function(d) { |
|
return "translate(" + xExtent[d[1]][+/e$/.test(d[0])] + "," + yExtent[d[1]][+/^s/.test(d[0])] + ")"; |
|
}); |
|
} |
|
|
|
function redrawX(g) { |
|
g.selectAll(".extent").attr("x", function (d) { return xExtent[d][0]; }); |
|
g.selectAll(".extent,.n>rect,.s>rect").attr("width", function(d) { return xExtent[d][1] - xExtent[d][0]; }); |
|
} |
|
|
|
function redrawY(g) { |
|
g.selectAll(".extent").attr("y", function (d) { return yExtent[d][0]; }); |
|
g.selectAll(".extent,.e>rect,.w>rect").attr("height", function (d) { return yExtent[d][1] - yExtent[d][0]; }); |
|
} |
|
|
|
function brushstart() { |
|
var target = this, |
|
eventTarget = d3.select(d3.event.target), |
|
event_ = event.of(target, arguments), |
|
g = d3.select(target), |
|
resizing = eventTarget.datum()[0], |
|
resizingX = !/^(n|s)$/.test(resizing) && x, |
|
resizingY = !/^(e|w)$/.test(resizing) && y, |
|
dragging = eventTarget.classed("extent"), |
|
dragRestore = d3_event_dragSuppress(), |
|
center, |
|
origin = d3.mouse(target), |
|
offset, |
|
i; |
|
|
|
var w = d3.select(window) |
|
.on("keydown.brush", keydown) |
|
.on("keyup.brush", keyup); |
|
|
|
if (d3.event.changedTouches) { |
|
w.on("touchmove.brush", brushmove).on("touchend.brush", brushend); |
|
} else { |
|
w.on("mousemove.brush", brushmove).on("mouseup.brush", brushend); |
|
} |
|
|
|
// Interrupt the transition, if any. |
|
g.interrupt().selectAll("*").interrupt(); |
|
|
|
// If the extent was clicked on, drag rather than brush; |
|
// store the point between the mouse and extent origin instead. |
|
if (dragging) { |
|
i = eventTarget.datum(); |
|
origin[0] = xExtent[i][0] - origin[0]; |
|
origin[1] = yExtent[i][0] - origin[1]; |
|
} |
|
|
|
// If a resizer was clicked on, record which side is to be resized. |
|
// Also, set the origin to the opposite side. |
|
else if (resizing) { |
|
var ex = +/w$/.test(resizing), |
|
ey = +/^n/.test(resizing); |
|
|
|
i = eventTarget.datum()[1]; |
|
offset = [xExtent[i][1 - ex] - origin[0], yExtent[i][1 - ey] - origin[1]]; |
|
origin[0] = xExtent[i][ex]; |
|
origin[1] = yExtent[i][ey]; |
|
} |
|
|
|
else { |
|
i = xExtent.length - 1; // Figure out the count of the new extent. |
|
xExtent.push([0,0]); |
|
yExtent.push([0,0]); |
|
|
|
// If the ALT key is down when starting a brush, the center is at the mouse. |
|
if (d3.event.altKey) center = origin.slice(); |
|
} |
|
|
|
// Propagate the active cursor to the body for the drag duration. |
|
g.style("pointer-events", "none"); |
|
d3.select("body").style("cursor", eventTarget.style("cursor")); |
|
|
|
// Show resizers as long as we're not dragging or resizing. |
|
if(!dragging && !resizing) g.selectAll(".resize").style("display", null) |
|
|
|
// Notify listeners. |
|
event_({type: "brushstart"}); |
|
brushmove(); |
|
|
|
function keydown() { |
|
if (d3.event.keyCode == 32) { |
|
if (!dragging) { |
|
center = null; |
|
origin[0] -= xExtent[i][1]; |
|
origin[1] -= yExtent[i][1]; |
|
dragging = 2; |
|
} |
|
d3.event.preventDefault(); |
|
} |
|
} |
|
|
|
function keyup() { |
|
if (d3.event.keyCode == 32 && dragging == 2) { |
|
origin[0] += xExtent[i][1]; |
|
origin[1] += yExtent[i][1]; |
|
dragging = 0; |
|
d3.event.preventDefault(); |
|
} |
|
} |
|
|
|
function brushmove() { |
|
var point = d3.mouse(target), |
|
moved = false; |
|
|
|
// Preserve the offset for thick resizers. |
|
if (offset) { |
|
point[0] += offset[0]; |
|
point[1] += offset[1]; |
|
} |
|
|
|
if (!dragging) { |
|
|
|
// If needed, determine the center from the current extent. |
|
if (d3.event.altKey) { |
|
if (!center) center = [(xExtent[i][0] + xExtent[i][1]) / 2, (yExtent[i][0] + yExtent[i][1]) / 2]; |
|
|
|
// Update the origin, for when the ALT key is released. |
|
origin[0] = xExtent[i][+(point[0] < center[0])]; |
|
origin[1] = yExtent[i][+(point[1] < center[1])]; |
|
} |
|
|
|
// When the ALT key is released, we clear the center. |
|
else center = null; |
|
} |
|
|
|
// Update the brush extent for each dimension. |
|
if (resizingX && move1(point, x, 0)) { |
|
redrawX(g, i); |
|
moved = true; |
|
} |
|
if (resizingY && move1(point, y, 1)) { |
|
redrawY(g, i); |
|
moved = true; |
|
} |
|
|
|
// Final redraw and notify listeners. |
|
if (moved) { |
|
redraw(g); |
|
event_({type: "brush", mode: dragging ? "move" : "resize"}); |
|
} |
|
} |
|
|
|
function move1(point, scale, j) { |
|
var range = d3_scaleRange(scale), |
|
r0 = range[0], |
|
r1 = range[1], |
|
position = origin[j], |
|
extent = j ? yExtent[i] : xExtent[i], |
|
size = extent[1] - extent[0], |
|
min, |
|
max; |
|
|
|
// When dragging, reduce the range by the extent size and position. |
|
if (dragging) { |
|
r0 -= position; |
|
r1 -= size + position; |
|
} |
|
|
|
// Clamp the point (unless clamp set to false) so that the extent fits within the range extent. |
|
min = (j ? yClamp : xClamp) ? Math.max(r0, Math.min(r1, point[j])) : point[j]; |
|
|
|
// Compute the new extent bounds. |
|
if (dragging) { |
|
max = (min += position) + size; |
|
} else { |
|
|
|
// If the ALT key is pressed, then preserve the center of the extent. |
|
if (center) position = Math.max(r0, Math.min(r1, 2 * center[j] - min)); |
|
|
|
// Compute the min and max of the position and point. |
|
if (position < min) { |
|
max = min; |
|
min = position; |
|
} else { |
|
max = position; |
|
} |
|
} |
|
|
|
// Update the stored bounds. |
|
if (extent[0] != min || extent[1] != max) { |
|
if (j) yExtentDomain[i] = null; |
|
else xExtentDomain[i] = null; |
|
extent[0] = min; |
|
extent[1] = max; |
|
return true; |
|
} |
|
} |
|
|
|
function brushend() { |
|
brushmove(); |
|
|
|
// If the current extent is empty, clear everything. |
|
if(x && xExtent[i][0] == xExtent[i][1] || |
|
y && yExtent[i][0] == yExtent[i][1]) { |
|
brush.clear(); |
|
} |
|
|
|
// reset the cursor styles |
|
g.style("pointer-events", "all").selectAll(".resize").style("display", function(d) { return brush.empty(d[1]) ? "none" : null; }); |
|
d3.select("body").style("cursor", null); |
|
|
|
w .on("mousemove.brush", null) |
|
.on("mouseup.brush", null) |
|
.on("touchmove.brush", null) |
|
.on("touchend.brush", null) |
|
.on("keydown.brush", null) |
|
.on("keyup.brush", null); |
|
|
|
drawExtents(); |
|
|
|
dragRestore(); |
|
event_({type: "brushend"}); |
|
} |
|
} |
|
|
|
brush.x = function(z) { |
|
if (!arguments.length) return x; |
|
x = z; |
|
resizes = d3_svg_brushResizes[!x << 1 | !y]; // fore! |
|
return brush; |
|
}; |
|
|
|
brush.y = function(z) { |
|
if (!arguments.length) return y; |
|
y = z; |
|
resizes = d3_svg_brushResizes[!x << 1 | !y]; // fore! |
|
return brush; |
|
}; |
|
|
|
brush.resizeAdaption = function(z) { |
|
if (!arguments.length) return resizeAdaption; |
|
resizeAdaption = z; |
|
return brush; |
|
} |
|
|
|
brush.extentAdaption = function(z) { |
|
if (!arguments.length) return extentAdaption; |
|
extentAdaption = z; |
|
return brush; |
|
} |
|
|
|
brush.clamp = function(z) { |
|
if (!arguments.length) return x && y ? [xClamp, yClamp] : x ? xClamp : y ? yClamp : null; |
|
if (x && y) xClamp = !!z[0], yClamp = !!z[1]; |
|
else if (x) xClamp = !!z; |
|
else if (y) yClamp = !!z; |
|
return brush; |
|
}; |
|
|
|
brush.extent = function(z) { |
|
var x0, x1, y0, y1, t; |
|
var xOutput, yOutput, xyOutput = []; |
|
|
|
// Invert the pixel extent to data-space. |
|
if (!arguments.length) { |
|
if (x) { |
|
if (xExtentDomain[0]) { |
|
xOutput = xExtentDomain; |
|
} else { |
|
xOutput = xExtent.map(function (d) { |
|
if (x.invert) return [ x.invert(d[0]), x.invert(d[1]) ]; |
|
return d; |
|
}).map(function (d) { |
|
if (d[1] < d[0]) return [ d[1], d[0] ]; |
|
return d; |
|
}).filter(function (d) { return d[1] - d[0] != 0; }); |
|
} |
|
} |
|
if (y) { |
|
if (yExtentDomain[0]) { |
|
yOutput = yExtentDomain; |
|
} else { |
|
yOutput = yExtent.map(function (d) { |
|
if(y.invert) return [ y.invert(d[0]), y.invert(d[1]) ]; |
|
return d; |
|
}).map(function (d) { |
|
if (d[1] < d[0]) return [ d[1], d[0] ]; |
|
return d; |
|
}).filter(function (d) { return d[1] - d[0] != 0; }); |
|
} |
|
} |
|
if(x && y) { |
|
xOutput.forEach(function (d, i) { |
|
xyOutput.push([[d[0], yOutput[i][0]], [d[1], yOutput[i][1]]]); |
|
}); |
|
} |
|
return x && y ? xyOutput : x ? xOutput : y && yOutput; |
|
} |
|
|
|
// Scale the data-space extent to pixels. |
|
if (x) { |
|
xOutput = z; |
|
if (y) xOutput = xOutput.map(function (d) { |
|
return [d[0][0], d[1][0]]; |
|
}); |
|
xExtentDomain = xOutput; |
|
xOutput = xOutput.map(function (d) { |
|
if (x.invert) return [x(d[0]), x(d[1])]; |
|
return d; |
|
}).map(function (d) { |
|
if(d[1] < d[0]) return [d[1], d[0]]; |
|
return d; |
|
}); |
|
xExtent = xOutput; |
|
if(!y) yExtent = xOutput.map(function() { return [0,0]; }); |
|
} |
|
if (y) { |
|
yOutput = z; |
|
if (x) yOutput = yOutput.map(function (d) { |
|
return [d[0][1], d[1][1]]; |
|
}); |
|
yExtentDomain = yOutput; |
|
yOutput = yOutput.map(function (d) { |
|
if (y.invert) return [y(d[0]), y(d[1])]; |
|
return d; |
|
}).map(function (d) { |
|
if(d[1] < d[0]) return [d[1], d[0]]; |
|
return d; |
|
}); |
|
yExtent = yOutput; |
|
if(!x) xExtent = yOutput.map(function () { return [0,0]; }); |
|
} |
|
|
|
// Handle the case where the extents are set to empty arrays. |
|
if(xExtent.length === 0) xExtent = [[0,0]]; |
|
if(yExtent.length === 0) yExtent = [[0,0]]; |
|
|
|
return brush; |
|
}; |
|
|
|
brush.clear = function() { |
|
xExtent = [[0, 0]], yExtent = [[0, 0]]; |
|
xExtentDomain = yExtentDomain = []; |
|
drawExtents(); |
|
if(x) redrawX(brushElement); |
|
if(y) redrawY(brushElement); |
|
return brush; |
|
}; |
|
|
|
brush.empty = function(i) { |
|
if(xExtent.length == 1 && yExtent.length == 1) i = 0; |
|
if(i !== undefined) { |
|
return !!x && xExtent[i][0] == xExtent[i][1] |
|
|| !!y && yExtent[i][0] == yExtent[i][1]; |
|
} else { |
|
return false; |
|
} |
|
}; |
|
|
|
return d3.rebind(brush, event, "on"); |
|
}; |
|
|
|
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" |
|
}; |
|
|
|
var d3_svg_brushResizes = [ |
|
["n", "e", "s", "w", "nw", "ne", "se", "sw"], |
|
["e", "w"], |
|
["n", "s"], |
|
[] |
|
]; |