Skip to content

Commit e3d59bd

Browse files
committed
First cut at a brush component.
The d3.svg.brush component allows one- or two-dimensional rectangular brushing. A future commit will allow the brushed region to be resized by grabbing an edge, and also provide some way of reporting the selection (duh)!
1 parent eda09f4 commit e3d59bd

10 files changed

Lines changed: 529 additions & 19 deletions

File tree

Makefile

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,8 @@ d3.svg.js: \
147147
src/svg/mouse.js \
148148
src/svg/touches.js \
149149
src/svg/symbol.js \
150-
src/svg/axis.js
150+
src/svg/axis.js \
151+
src/svg/brush.js
151152

152153
d3.behavior.js: \
153154
src/behavior/behavior.js \

d3.js

Lines changed: 149 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -744,6 +744,11 @@ function d3_ease_bounce(t) {
744744
: 7.5625 * (t -= 2.625 / 2.75) * t + .984375;
745745
}
746746
d3.event = null;
747+
748+
function d3_eventCancel() {
749+
d3.event.stopPropagation();
750+
d3.event.preventDefault();
751+
}
747752
d3.interpolate = function(a, b) {
748753
var i = d3.interpolators.length, f;
749754
while (--i >= 0 && !(f = d3.interpolators[i](a, b)));
@@ -3833,6 +3838,147 @@ function d3_svg_axisSubdivide(scale, ticks, m) {
38333838
}
38343839
return subticks;
38353840
}
3841+
d3.svg.brush = function() {
3842+
var event = d3.dispatch("brush"),
3843+
x, // x-scale, optional
3844+
y, // y-scale, optional
3845+
selection = [[0, 0], [0, 0]]; // [x0, y0], [x1, y1]
3846+
3847+
function brush(selection) {
3848+
selection.each(function(d, i, j) {
3849+
var g = d3.select(this).on("mousedown.brush", down),
3850+
bg = g.selectAll("rect.background").data([d]),
3851+
fg = g.selectAll("rect.selection").data([d]),
3852+
e;
3853+
3854+
// An invisible, mouseable area for starting a new brush.
3855+
bg.enter().append("svg:rect")
3856+
.attr("class", "background")
3857+
.style("visibility", "hidden")
3858+
.style("pointer-events", "all");
3859+
3860+
// The current brush selection, made visible.
3861+
fg.enter().append("svg:rect")
3862+
.attr("class", "selection");
3863+
3864+
// Initialize the background to fill the defined range.
3865+
// If the range isn't defined, we let the caller post-process.
3866+
if (x) {
3867+
e = d3_scaleExtent(x.range());
3868+
bg.attr("x", e[0]).attr("width", e[1] - e[0]);
3869+
}
3870+
if (y) {
3871+
e = d3_scaleExtent(y.range());
3872+
bg.attr("y", e[0]).attr("height", e[1] - e[0]);
3873+
}
3874+
});
3875+
}
3876+
3877+
function down() {
3878+
d3_svg_brushEvent = event.brush;
3879+
d3_svg_brushTarget = this;
3880+
d3_svg_brushArguments = arguments;
3881+
d3_svg_brushX = x;
3882+
d3_svg_brushY = y;
3883+
d3_svg_brushSelection = selection;
3884+
d3_svg_brushOffset = d3.svg.mouse(d3_svg_brushTarget);
3885+
d3_eventCancel();
3886+
3887+
// If the selection was clicked on, use dragging rather than brushing;
3888+
// store the offset between the mouse and selection origin instead.
3889+
if (d3_svg_brushDrag = d3.select(d3.event.target).classed("selection")) {
3890+
d3_svg_brushOffset[0] = selection[0][0] - d3_svg_brushOffset[0];
3891+
d3_svg_brushOffset[1] = selection[0][1] - d3_svg_brushOffset[1];
3892+
}
3893+
}
3894+
3895+
brush.x = function(z) {
3896+
if (!arguments.length) return x;
3897+
x = z;
3898+
return brush;
3899+
};
3900+
3901+
brush.y = function(z) {
3902+
if (!arguments.length) return y;
3903+
y = z;
3904+
return brush;
3905+
};
3906+
3907+
brush.on = function(type, listener) {
3908+
event.on(type, listener);
3909+
return brush;
3910+
};
3911+
3912+
d3.select(window)
3913+
.on("mousemove.brush", d3_svg_brushMove)
3914+
.on("mouseup.brush", d3_svg_brushUp);
3915+
3916+
return brush;
3917+
};
3918+
3919+
var d3_svg_brushEvent,
3920+
d3_svg_brushTarget,
3921+
d3_svg_brushArguments,
3922+
d3_svg_brushX,
3923+
d3_svg_brushY,
3924+
d3_svg_brushDrag,
3925+
d3_svg_brushOffset;
3926+
3927+
function d3_svg_brushMove() {
3928+
if (d3_svg_brushOffset) {
3929+
var mouse = d3.svg.mouse(d3_svg_brushTarget),
3930+
selection = d3.select(d3_svg_brushTarget).select(".selection");
3931+
if (d3_svg_brushX) selection.call(d3_svg_brushMove1, mouse, d3_svg_brushX, 0, "x", "width");
3932+
if (d3_svg_brushY) selection.call(d3_svg_brushMove1, mouse, d3_svg_brushY, 1, "y", "height");
3933+
d3_svg_brushEvent.apply(d3_svg_brushTarget, d3_svg_brushArguments);
3934+
}
3935+
}
3936+
3937+
function d3_svg_brushMove1(selection, mouse, scale, i, position, length) {
3938+
var range = d3_scaleExtent(scale.range()),
3939+
offset = d3_svg_brushOffset[i],
3940+
size = d3_svg_brushSelection[1][i] - d3_svg_brushSelection[0][i],
3941+
min,
3942+
max;
3943+
3944+
// When dragging, reduce the range by the selection size and offset.
3945+
if (d3_svg_brushDrag) {
3946+
range[0] -= offset;
3947+
range[1] -= size + offset;
3948+
}
3949+
3950+
// Clamp the mouse so that the selection fits within the range extent.
3951+
min = Math.max(range[0], Math.min(range[1], mouse[i]));
3952+
3953+
// Compute the new selection bounds.
3954+
if (d3_svg_brushDrag) {
3955+
max = (min += offset) + size;
3956+
} else if (offset < min) {
3957+
max = min;
3958+
min = offset;
3959+
} else {
3960+
max = offset;
3961+
}
3962+
3963+
// Update the selection rect and stored bounds.
3964+
selection
3965+
.attr(position, d3_svg_brushSelection[0][i] = min)
3966+
.attr(length, (d3_svg_brushSelection[1][i] = max) - min);
3967+
}
3968+
3969+
function d3_svg_brushUp() {
3970+
if (d3_svg_brushOffset) {
3971+
d3_svg_brushMove();
3972+
d3_svg_brushEvent =
3973+
d3_svg_brushTarget =
3974+
d3_svg_brushArguments =
3975+
d3_svg_brushX =
3976+
d3_svg_brushY =
3977+
d3_svg_brushDrag =
3978+
d3_svg_brushOffset = null;
3979+
d3_eventCancel();
3980+
}
3981+
}
38363982
d3.behavior = {};
38373983
d3.behavior.drag = function() {
38383984
var event = d3.dispatch("drag", "dragstart", "dragend");
@@ -3915,7 +4061,7 @@ function d3_behavior_dragMove() {
39154061
if (!parent) return d3_behavior_dragUp();
39164062

39174063
d3_behavior_dragDispatch("drag");
3918-
d3_behavior_dragCancel();
4064+
d3_eventCancel();
39194065
}
39204066

39214067
function d3_behavior_dragUp() {
@@ -3927,22 +4073,17 @@ function d3_behavior_dragUp() {
39274073
// Also prevent the subsequent click from propagating (e.g., for anchors).
39284074
if (d3_behavior_dragMoved && d3_behavior_dragEventTarget === d3.event.target) {
39294075
d3_behavior_dragStopClick = true;
3930-
d3_behavior_dragCancel();
4076+
d3_eventCancel();
39314077
}
39324078
}
39334079

39344080
function d3_behavior_dragClick() {
39354081
if (d3_behavior_dragStopClick && d3_behavior_dragEventTarget === d3.event.target) {
3936-
d3_behavior_dragCancel();
4082+
d3_eventCancel();
39374083
d3_behavior_dragStopClick = false;
39384084
d3_behavior_dragEventTarget = null;
39394085
}
39404086
}
3941-
3942-
function d3_behavior_dragCancel() {
3943-
d3.event.stopPropagation();
3944-
d3.event.preventDefault();
3945-
}
39464087
// TODO unbind zoom behavior?
39474088
// TODO unbind listener?
39484089
d3.behavior.zoom = function() {

d3.min.js

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

examples/brush/brush-parallel.html

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
5+
<title>Brush</title>
6+
<script type="text/javascript" src="../../d3.js"></script>
7+
<style type="text/css">
8+
9+
svg {
10+
font: 10px sans-serif;
11+
}
12+
13+
.axis path, .axis line {
14+
fill: none;
15+
stroke: #000;
16+
shape-rendering: crispEdges;
17+
}
18+
19+
.brush .selection {
20+
stroke: #fff;
21+
fill-opacity: .125;
22+
shape-rendering: crispEdges;
23+
}
24+
25+
</style>
26+
</head>
27+
<body>
28+
<script type="text/javascript">
29+
30+
var m = [10, 10, 20, 40],
31+
w = 960 - m[1] - m[3],
32+
h = 500 - m[0] - m[2];
33+
34+
var dimensions = ["A", "B", "C", "D", "E"],
35+
x = d3.scale.ordinal().domain(dimensions).rangePoints([0, w]),
36+
y = d3.scale.linear().range([h, 0]);
37+
38+
var svg = d3.select("body").append("svg:svg")
39+
.attr("width", w + m[1] + m[3])
40+
.attr("height", h + m[0] + m[2])
41+
.append("svg:g")
42+
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
43+
44+
var g = svg.selectAll(".dimension")
45+
.data(dimensions)
46+
.enter().append("svg:g")
47+
.attr("class", "dimension")
48+
.attr("transform", function(d) { return "translate(" + x(d) + ")"; });
49+
50+
g.append("svg:g")
51+
.attr("class", "axis")
52+
.call(d3.svg.axis().scale(y).orient("right"));
53+
54+
// svg.append("svg:g")
55+
// .attr("class", "brush")
56+
// .call(d3.svg.brush().x(x).y(y));
57+
58+
</script>
59+
</body>
60+
</html>

examples/brush/brush-x.html

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
5+
<title>Brush</title>
6+
<script type="text/javascript" src="../../d3.js"></script>
7+
<style type="text/css">
8+
9+
svg {
10+
font: 10px sans-serif;
11+
}
12+
13+
.axis path, .axis line {
14+
fill: none;
15+
stroke: #000;
16+
shape-rendering: crispEdges;
17+
}
18+
19+
.brush .selection {
20+
stroke: #fff;
21+
fill-opacity: .125;
22+
shape-rendering: crispEdges;
23+
}
24+
25+
</style>
26+
</head>
27+
<body>
28+
<script type="text/javascript">
29+
30+
var m = [10, 10, 20, 10],
31+
w = 960 - m[1] - m[3],
32+
h = 500 - m[0] - m[2];
33+
34+
var x = d3.scale.linear().range([0, w]);
35+
36+
var svg = d3.select("body").append("svg:svg")
37+
.attr("width", w + m[1] + m[3])
38+
.attr("height", h + m[0] + m[2])
39+
.append("svg:g")
40+
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
41+
42+
svg.append("svg:g")
43+
.attr("class", "x axis")
44+
.attr("transform", "translate(0," + h + ")")
45+
.call(d3.svg.axis().scale(x).orient("bottom"));
46+
47+
svg.append("svg:g")
48+
.attr("class", "brush")
49+
.call(d3.svg.brush().x(x))
50+
.selectAll("rect")
51+
.attr("height", h);
52+
53+
</script>
54+
</body>
55+
</html>

examples/brush/brush-y.html

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
<!DOCTYPE html>
2+
<html>
3+
<head>
4+
<meta http-equiv="Content-Type" content="text/html;charset=utf-8"/>
5+
<title>Brush</title>
6+
<script type="text/javascript" src="../../d3.js"></script>
7+
<style type="text/css">
8+
9+
svg {
10+
font: 10px sans-serif;
11+
}
12+
13+
.axis path, .axis line {
14+
fill: none;
15+
stroke: #000;
16+
shape-rendering: crispEdges;
17+
}
18+
19+
.brush .selection {
20+
stroke: #fff;
21+
fill-opacity: .125;
22+
shape-rendering: crispEdges;
23+
}
24+
25+
</style>
26+
</head>
27+
<body>
28+
<script type="text/javascript">
29+
30+
var m = [10, 10, 10, 40],
31+
w = 960 - m[1] - m[3],
32+
h = 500 - m[0] - m[2];
33+
34+
var y = d3.scale.linear().range([h, 0]);
35+
36+
var svg = d3.select("body").append("svg:svg")
37+
.attr("width", w + m[1] + m[3])
38+
.attr("height", h + m[0] + m[2])
39+
.append("svg:g")
40+
.attr("transform", "translate(" + m[3] + "," + m[0] + ")");
41+
42+
svg.append("svg:g")
43+
.attr("class", "y axis")
44+
.call(d3.svg.axis().scale(y).orient("left"));
45+
46+
svg.append("svg:g")
47+
.attr("class", "brush")
48+
.call(d3.svg.brush().y(y))
49+
.selectAll("rect")
50+
.attr("width", w);
51+
52+
</script>
53+
</body>
54+
</html>

0 commit comments

Comments
 (0)