@plotly/d3
Version:
430 lines (371 loc) • 13.1 kB
JavaScript
import "../core/identity";
import "../core/document";
import "../core/rebind";
import "../event/drag";
import "../event/event";
import "../event/mouse";
import "../scale/scale";
import "../selection/selection";
import "svg";
d3.svg.brush = function() {
var event = d3_eventDispatch(brush, "brushstart", "brush", "brushend"),
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];
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);
// 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");
// The visible brush extent; style this as you like!
g.selectAll(".extent")
.data([0])
.enter().append("rect")
.attr("class", "extent")
.style("cursor", "move");
// More invisible rects for resizing the extent.
var resize = g.selectAll(".resize")
.data(resizes, d3_identity);
// Remove any superfluous resizers.
resize.exit().remove();
resize.enter().append("g")
.attr("class", function(d) { return "resize " + d; })
.style("cursor", function(d) { return d3_svg_brushCursor[d]; })
.append("rect")
.attr("x", function(d) { return /[ew]$/.test(d) ? -3 : null; })
.attr("y", function(d) { return /^[ns]/.test(d) ? -3 : null; })
.attr("width", 6)
.attr("height", 6)
.style("visibility", "hidden");
// Show or hide the resizers.
resize.style("display", brush.empty() ? "none" : null);
// 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);
});
}
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() {
var xi = d3_interpolateArray(xExtent, extent1.x),
yi = d3_interpolateArray(yExtent, extent1.y);
xExtentDomain = yExtentDomain = null; // transition state
return function(t) {
xExtent = extent1.x = xi(t);
yExtent = extent1.y = 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[+/e$/.test(d)] + "," + yExtent[+/^s/.test(d)] + ")";
});
}
function redrawX(g) {
g.select(".extent").attr("x", xExtent[0]);
g.selectAll(".extent,.n>rect,.s>rect").attr("width", xExtent[1] - xExtent[0]);
}
function redrawY(g) {
g.select(".extent").attr("y", yExtent[0]);
g.selectAll(".extent,.e>rect,.w>rect").attr("height", yExtent[1] - yExtent[0]);
}
function brushstart() {
var target = this,
eventTarget = d3.select(d3.event.target),
event_ = event.of(target, arguments),
g = d3.select(target),
resizing = eventTarget.datum(),
resizingX = !/^(n|s)$/.test(resizing) && x,
resizingY = !/^(e|w)$/.test(resizing) && y,
dragging = eventTarget.classed("extent"),
dragRestore = d3_event_dragSuppress(target),
center,
origin = d3.mouse(target),
offset;
var w = d3.select(d3_window(target))
.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) {
origin[0] = xExtent[0] - origin[0];
origin[1] = yExtent[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);
offset = [xExtent[1 - ex] - origin[0], yExtent[1 - ey] - origin[1]];
origin[0] = xExtent[ex];
origin[1] = yExtent[ey];
}
// If the ALT key is down when starting a brush, the center is at the mouse.
else if (d3.event.altKey) center = origin.slice();
// Propagate the active cursor to the body for the drag duration.
g.style("pointer-events", "none").selectAll(".resize").style("display", null);
d3.select("body").style("cursor", eventTarget.style("cursor"));
// Notify listeners.
event_({type: "brushstart"});
brushmove();
function keydown() {
if (d3.event.keyCode == 32) {
if (!dragging) {
center = null;
origin[0] -= xExtent[1];
origin[1] -= yExtent[1];
dragging = 2;
}
d3_eventPreventDefault();
}
}
function keyup() {
if (d3.event.keyCode == 32 && dragging == 2) {
origin[0] += xExtent[1];
origin[1] += yExtent[1];
dragging = 0;
d3_eventPreventDefault();
}
}
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[0] + xExtent[1]) / 2, (yExtent[0] + yExtent[1]) / 2];
// Update the origin, for when the ALT key is released.
origin[0] = xExtent[+(point[0] < center[0])];
origin[1] = yExtent[+(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);
moved = true;
}
if (resizingY && move1(point, y, 1)) {
redrawY(g);
moved = true;
}
// Final redraw and notify listeners.
if (moved) {
redraw(g);
event_({type: "brush", mode: dragging ? "move" : "resize"});
}
}
function move1(point, scale, i) {
var range = d3_scaleRange(scale),
r0 = range[0],
r1 = range[1],
position = origin[i],
extent = i ? yExtent : xExtent,
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 = (i ? yClamp : xClamp) ? Math.max(r0, Math.min(r1, point[i])) : point[i];
// 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[i] - 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 (i) yExtentDomain = null;
else xExtentDomain = null;
extent[0] = min;
extent[1] = max;
return true;
}
}
function brushend() {
brushmove();
// reset the cursor styles
g.style("pointer-events", "all").selectAll(".resize").style("display", brush.empty() ? "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);
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.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;
// Invert the pixel extent to data-space.
if (!arguments.length) {
if (x) {
if (xExtentDomain) {
x0 = xExtentDomain[0], x1 = xExtentDomain[1];
} else {
x0 = xExtent[0], x1 = xExtent[1];
if (x.invert) x0 = x.invert(x0), x1 = x.invert(x1);
if (x1 < x0) t = x0, x0 = x1, x1 = t;
}
}
if (y) {
if (yExtentDomain) {
y0 = yExtentDomain[0], y1 = yExtentDomain[1];
} else {
y0 = yExtent[0], y1 = yExtent[1];
if (y.invert) y0 = y.invert(y0), y1 = y.invert(y1);
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];
xExtentDomain = [x0, x1];
if (x.invert) x0 = x(x0), x1 = x(x1);
if (x1 < x0) t = x0, x0 = x1, x1 = t;
if (x0 != xExtent[0] || x1 != xExtent[1]) xExtent = [x0, x1]; // copy-on-write
}
if (y) {
y0 = z[0], y1 = z[1];
if (x) y0 = y0[1], y1 = y1[1];
yExtentDomain = [y0, y1];
if (y.invert) y0 = y(y0), y1 = y(y1);
if (y1 < y0) t = y0, y0 = y1, y1 = t;
if (y0 != yExtent[0] || y1 != yExtent[1]) yExtent = [y0, y1]; // copy-on-write
}
return brush;
};
brush.clear = function() {
if (!brush.empty()) {
xExtent = [0, 0], yExtent = [0, 0]; // copy-on-write
xExtentDomain = yExtentDomain = null;
}
return brush;
};
brush.empty = function() {
return !!x && xExtent[0] == xExtent[1]
|| !!y && yExtent[0] == yExtent[1];
};
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"],
[]
];