Skip to content

Commit 50bf5d3

Browse files
committed
Allow decorative brush resizers.
The brush component now allows you to add decorative elements to serve as brush resizers, while keeping the default behavior of a few invisible pixels on the edge of the extent. Any element you add to ".resize" containers can serve as visible handles for the brush. This commit also fixes two small bugs with the brush component. First, we now track the offset between the extent edge and the mouse, rather than immediately jumping the extent edge to the mouse position; this was barely noticeable previously due to the resizers being so thin, but it is very noticeable with large resizers. Second, the default behavior for the SPACE key is not prevented whilst dragging.
1 parent 7a4a8ec commit 50bf5d3

8 files changed

Lines changed: 250 additions & 107 deletions

File tree

d3.js

Lines changed: 64 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -4087,12 +4087,15 @@ d3.svg.brush = function() {
40874087
.style("cursor", "move");
40884088

40894089
// More invisible rects for resizing the extent.
4090-
tz.enter().append("rect")
4090+
tz.enter().append("g")
40914091
.attr("class", function(d) { return "resize " + d; })
4092+
.style("cursor", function(d) { return d3_svg_brushCursor[d]; })
4093+
.append("rect")
4094+
.attr("x", function(d) { return /e$/.test(d) ? -3 : -2; })
4095+
.attr("y", function(d) { return /^s/.test(d) ? -4 : -3; })
40924096
.attr("width", 6)
40934097
.attr("height", 6)
4094-
.style("visibility", "hidden")
4095-
.style("cursor", function(d) { return d3_svg_brushCursor[d]; });
4098+
.style("visibility", "hidden");
40964099

40974100
// Update the resizers.
40984101
tz.style("pointer-events", brush.empty() ? "none" : "all");
@@ -4112,41 +4115,44 @@ d3.svg.brush = function() {
41124115
bg.attr("y", e[0]).attr("height", e[1] - e[0]);
41134116
d3_svg_brushRedrawY(g, extent);
41144117
}
4118+
d3_svg_brushRedraw(g, extent);
41154119
});
41164120
}
41174121

41184122
function down() {
4119-
var target = d3.select(d3.event.target);
4123+
var target = d3.event.target,
4124+
resize;
41204125

41214126
// Store some global state for the duration of the brush gesture.
41224127
d3_svg_brush = brush;
41234128
d3_svg_brushTarget = this;
41244129
d3_svg_brushExtent = extent;
4125-
d3_svg_brushOffset = d3.svg.mouse(d3_svg_brushTarget);
4130+
d3_svg_brushPoint = d3.svg.mouse(d3_svg_brushTarget);
41264131

41274132
// If the extent was clicked on, drag rather than brush;
4128-
// store the offset between the mouse and extent origin instead.
4129-
if (d3_svg_brushDrag = target.classed("extent")) {
4130-
d3_svg_brushOffset[0] = extent[0][0] - d3_svg_brushOffset[0];
4131-
d3_svg_brushOffset[1] = extent[0][1] - d3_svg_brushOffset[1];
4133+
// store the point between the mouse and extent origin instead.
4134+
if (d3_svg_brushDrag = d3.select(target).classed("extent")) {
4135+
d3_svg_brushPoint[0] = extent[0][0] - d3_svg_brushPoint[0];
4136+
d3_svg_brushPoint[1] = extent[0][1] - d3_svg_brushPoint[1];
41324137
}
41334138

41344139
// If a resizer was clicked on, record which side is to be resized.
4135-
// Also, set the offset to the opposite side.
4136-
else if (target.classed("resize")) {
4137-
d3_svg_brushResize = d3.event.target.__data__;
4138-
d3_svg_brushOffset[0] = extent[+/w$/.test(d3_svg_brushResize)][0];
4139-
d3_svg_brushOffset[1] = extent[+/^n/.test(d3_svg_brushResize)][1];
4140+
// Also, set the point to the opposite side.
4141+
else if (resize = target.__data__) {
4142+
var ex = +/w$/.test(resize), ey = +/^n/.test(resize);
4143+
d3_svg_brushOffset = [extent[1 - ex][0] - d3_svg_brushPoint[0], extent[1 - ey][1] - d3_svg_brushPoint[1]];
4144+
d3_svg_brushPoint[0] = extent[ex][0];
4145+
d3_svg_brushPoint[1] = extent[ey][1];
41404146
}
41414147

41424148
// If the ALT key is down when starting a brush, the center is at the mouse.
41434149
else if (d3.event.altKey) {
4144-
d3_svg_brushCenter = d3_svg_brushOffset.slice();
4150+
d3_svg_brushCenter = d3_svg_brushPoint.slice();
41454151
}
41464152

41474153
// Restrict which dimensions are resized.
4148-
d3_svg_brushX = !/^(n|s)$/.test(d3_svg_brushResize) && x;
4149-
d3_svg_brushY = !/^(e|w)$/.test(d3_svg_brushResize) && y;
4154+
d3_svg_brushX = !/^(n|s)$/.test(resize) && x;
4155+
d3_svg_brushY = !/^(e|w)$/.test(resize) && y;
41504156

41514157
// Notify listeners.
41524158
d3_svg_brushDispatch = dispatcher(this, arguments);
@@ -4245,48 +4251,58 @@ var d3_svg_brush,
42454251
d3_svg_brushY,
42464252
d3_svg_brushExtent,
42474253
d3_svg_brushDrag,
4248-
d3_svg_brushResize,
42494254
d3_svg_brushCenter,
4255+
d3_svg_brushPoint,
42504256
d3_svg_brushOffset;
42514257

4258+
function d3_svg_brushRedraw(g, extent) {
4259+
g.selectAll(".resize").attr("transform", function(d) {
4260+
return "translate(" + extent[+/e$/.test(d)][0] + "," + extent[+/^s/.test(d)][1] + ")";
4261+
});
4262+
}
4263+
42524264
function d3_svg_brushRedrawX(g, extent) {
42534265
g.select(".extent").attr("x", extent[0][0]);
4254-
g.selectAll(".n,.s,.w,.nw,.sw").attr("x", extent[0][0] - 2);
4255-
g.selectAll(".e,.ne,.se").attr("x", extent[1][0] - 3);
4256-
g.selectAll(".extent,.n,.s").attr("width", extent[1][0] - extent[0][0]);
4266+
g.selectAll(".extent,.n>rect,.s>rect").attr("width", extent[1][0] - extent[0][0]);
42574267
}
42584268

42594269
function d3_svg_brushRedrawY(g, extent) {
42604270
g.select(".extent").attr("y", extent[0][1]);
4261-
g.selectAll(".n,.e,.w,.nw,.ne").attr("y", extent[0][1] - 3);
4262-
g.selectAll(".s,.se,.sw").attr("y", extent[1][1] - 4);
4263-
g.selectAll(".extent,.e,.w").attr("height", extent[1][1] - extent[0][1]);
4271+
g.selectAll(".extent,.e>rect,.w>rect").attr("height", extent[1][1] - extent[0][1]);
42644272
}
42654273

42664274
function d3_svg_brushKeydown() {
4267-
if (d3.event.keyCode == 32 && d3_svg_brushTarget && !d3_svg_brushDrag) {
4268-
d3_svg_brushCenter = null;
4269-
d3_svg_brushOffset[0] -= d3_svg_brushExtent[1][0];
4270-
d3_svg_brushOffset[1] -= d3_svg_brushExtent[1][1];
4271-
d3_svg_brushDrag = 2;
4275+
if (d3.event.keyCode == 32 && d3_svg_brushTarget) {
4276+
if (!d3_svg_brushDrag) {
4277+
d3_svg_brushCenter = null;
4278+
d3_svg_brushPoint[0] -= d3_svg_brushExtent[1][0];
4279+
d3_svg_brushPoint[1] -= d3_svg_brushExtent[1][1];
4280+
d3_svg_brushDrag = 2;
4281+
}
42724282
d3_eventCancel();
42734283
}
42744284
}
42754285

42764286
function d3_svg_brushKeyup() {
42774287
if (d3.event.keyCode == 32 && d3_svg_brushDrag == 2) {
4278-
d3_svg_brushOffset[0] += d3_svg_brushExtent[1][0];
4279-
d3_svg_brushOffset[1] += d3_svg_brushExtent[1][1];
4288+
d3_svg_brushPoint[0] += d3_svg_brushExtent[1][0];
4289+
d3_svg_brushPoint[1] += d3_svg_brushExtent[1][1];
42804290
d3_svg_brushDrag = 0;
42814291
d3_eventCancel();
42824292
}
42834293
}
42844294

42854295
function d3_svg_brushMove() {
4286-
if (d3_svg_brushOffset) {
4296+
if (d3_svg_brushPoint) {
42874297
var mouse = d3.svg.mouse(d3_svg_brushTarget),
42884298
g = d3.select(d3_svg_brushTarget);
42894299

4300+
// Preserve the offset for thick resizers.
4301+
if (d3_svg_brushOffset) {
4302+
mouse[0] += d3_svg_brushOffset[0];
4303+
mouse[1] += d3_svg_brushOffset[1];
4304+
}
4305+
42904306
if (!d3_svg_brushDrag) {
42914307

42924308
// If needed, determine the center from the current extent.
@@ -4298,9 +4314,9 @@ function d3_svg_brushMove() {
42984314
];
42994315
}
43004316

4301-
// Update the offset, for when the ALT key is released.
4302-
d3_svg_brushOffset[0] = d3_svg_brushExtent[+(mouse[0] < d3_svg_brushCenter[0])][0];
4303-
d3_svg_brushOffset[1] = d3_svg_brushExtent[+(mouse[1] < d3_svg_brushCenter[1])][1];
4317+
// Update the point, for when the ALT key is released.
4318+
d3_svg_brushPoint[0] = d3_svg_brushExtent[+(mouse[0] < d3_svg_brushCenter[0])][0];
4319+
d3_svg_brushPoint[1] = d3_svg_brushExtent[+(mouse[1] < d3_svg_brushCenter[1])][1];
43044320
}
43054321

43064322
// When the ALT key is released, we clear the center.
@@ -4316,6 +4332,7 @@ function d3_svg_brushMove() {
43164332
d3_svg_brushMove1(mouse, d3_svg_brushY, 1);
43174333
d3_svg_brushRedrawY(g, d3_svg_brushExtent);
43184334
}
4335+
d3_svg_brushRedraw(g, d3_svg_brushExtent);
43194336

43204337
// Notify listeners.
43214338
d3_svg_brushDispatch("brush");
@@ -4326,34 +4343,34 @@ function d3_svg_brushMove1(mouse, scale, i) {
43264343
var range = d3_scaleRange(scale),
43274344
r0 = range[0],
43284345
r1 = range[1],
4329-
offset = d3_svg_brushOffset[i],
4346+
point = d3_svg_brushPoint[i],
43304347
size = d3_svg_brushExtent[1][i] - d3_svg_brushExtent[0][i],
43314348
min,
43324349
max;
43334350

4334-
// When dragging, reduce the range by the extent size and offset.
4351+
// When dragging, reduce the range by the extent size and point.
43354352
if (d3_svg_brushDrag) {
4336-
r0 -= offset;
4337-
r1 -= size + offset;
4353+
r0 -= point;
4354+
r1 -= size + point;
43384355
}
43394356

43404357
// Clamp the mouse so that the extent fits within the range extent.
43414358
min = Math.max(r0, Math.min(r1, mouse[i]));
43424359

43434360
// Compute the new extent bounds.
43444361
if (d3_svg_brushDrag) {
4345-
max = (min += offset) + size;
4362+
max = (min += point) + size;
43464363
} else {
43474364

43484365
// If the ALT key is pressed, then preserve the center of the extent.
4349-
if (d3_svg_brushCenter) offset = Math.max(r0, Math.min(r1, 2 * d3_svg_brushCenter[i] - min));
4366+
if (d3_svg_brushCenter) point = Math.max(r0, Math.min(r1, 2 * d3_svg_brushCenter[i] - min));
43504367

4351-
// Compute the min and max of the offset and mouse.
4352-
if (offset < min) {
4368+
// Compute the min and max of the point and mouse.
4369+
if (point < min) {
43534370
max = min;
4354-
min = offset;
4371+
min = point;
43554372
} else {
4356-
max = offset;
4373+
max = point;
43574374
}
43584375
}
43594376

@@ -4363,7 +4380,7 @@ function d3_svg_brushMove1(mouse, scale, i) {
43634380
}
43644381

43654382
function d3_svg_brushUp() {
4366-
if (d3_svg_brushOffset) {
4383+
if (d3_svg_brushPoint) {
43674384
d3_svg_brushMove();
43684385
d3.select(d3_svg_brushTarget).selectAll(".resize").style("pointer-events", d3_svg_brush.empty() ? "none" : "all");
43694386
d3_svg_brushDispatch("brushend");
@@ -4374,8 +4391,8 @@ function d3_svg_brushUp() {
43744391
d3_svg_brushY =
43754392
d3_svg_brushExtent =
43764393
d3_svg_brushDrag =
4377-
d3_svg_brushResize =
43784394
d3_svg_brushCenter =
4395+
d3_svg_brushPoint =
43794396
d3_svg_brushOffset = null;
43804397
d3_eventCancel();
43814398
}

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-ordinal.html

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
.attr("class", "brush")
7070
.call(d3.svg.brush().x(x)
7171
.on("brushstart", brushstart)
72-
.on("brush", brush)
72+
.on("brush", brushmove)
7373
.on("brushend", brushend))
7474
.selectAll("rect")
7575
.attr("height", h);
@@ -78,7 +78,7 @@
7878
svg.classed("selecting", true);
7979
}
8080

81-
function brush() {
81+
function brushmove() {
8282
var s = d3.event.target.extent();
8383
symbol.classed("selected", function(d) { return s[0] <= (d = x(d)) && d <= s[1]; });
8484
}
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
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+
.selecting circle {
14+
fill-opacity: .2;
15+
}
16+
17+
.selecting circle.selected {
18+
stroke: #f00;
19+
}
20+
21+
.resize path {
22+
fill: #666;
23+
fill-opacity: .8;
24+
stroke: #000;
25+
stroke-width: 1.5px;
26+
display: none;
27+
}
28+
29+
.selecting .resize path {
30+
display: inherit;
31+
}
32+
33+
.axis path, .axis line {
34+
fill: none;
35+
stroke: #000;
36+
shape-rendering: crispEdges;
37+
}
38+
39+
.brush .extent {
40+
fill-opacity: .125;
41+
shape-rendering: crispEdges;
42+
}
43+
44+
</style>
45+
</head>
46+
<body>
47+
<script type="text/javascript">
48+
49+
var data = d3.range(800).map(Math.random);
50+
51+
var margin = {top: 4, right: 50, bottom: 20, left: 50},
52+
width = 960 - margin.left - margin.right,
53+
height = 120 - margin.top - margin.bottom;
54+
55+
var x = d3.scale.linear().range([0, width]),
56+
y = d3.random.normal(height / 2, height / 8);
57+
58+
var brush = d3.svg.brush().x(x)
59+
.on("brushstart", brushstart)
60+
.on("brush", brushmove)
61+
.on("brushend", brushend);
62+
63+
var svg = d3.select("body").append("svg")
64+
.attr("width", width + margin.left + margin.right)
65+
.attr("height", height + margin.top + margin.bottom)
66+
.append("g")
67+
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
68+
69+
svg.append("g")
70+
.attr("class", "x axis")
71+
.attr("transform", "translate(0," + height + ")")
72+
.call(d3.svg.axis().scale(x).orient("bottom"));
73+
74+
var circle = svg.append("g").selectAll("circle")
75+
.data(data)
76+
.enter().append("circle")
77+
.attr("transform", function(d) { return "translate(" + x(d) + "," + y() + ")"; })
78+
.attr("r", 3.5);
79+
80+
var brushg = svg.append("g")
81+
.attr("class", "brush")
82+
.call(brush);
83+
84+
brushg.selectAll(".resize").append("path")
85+
.attr("transform", "translate(0," + height / 2 + ")")
86+
.attr("d", d3.svg.arc()
87+
.outerRadius(height / 2)
88+
.startAngle(0)
89+
.endAngle(function(d, i) { return i ? -Math.PI : Math.PI; }));
90+
91+
brushg.selectAll("rect")
92+
.attr("height", height);
93+
94+
function brushstart() {
95+
svg.classed("selecting", true);
96+
}
97+
98+
function brushmove() {
99+
var s = brush.extent();
100+
circle.classed("selected", function(d) { return s[0] <= d && d <= s[1]; });
101+
}
102+
103+
function brushend() {
104+
svg.classed("selecting", !d3.event.target.empty());
105+
}
106+
107+
</script>
108+
</body>
109+
</html>

examples/brush/brush-x.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@
5959
.attr("transform", "translate(0," + h + ")")
6060
.call(d3.svg.axis().scale(x).orient("bottom"));
6161

62-
var circle = svg.selectAll("circle")
62+
var circle = svg.append("g").selectAll("circle")
6363
.data(data)
6464
.enter().append("circle")
6565
.attr("transform", function(d) { return "translate(" + x(d) + "," + y() + ")"; })
@@ -69,7 +69,7 @@
6969
.attr("class", "brush")
7070
.call(d3.svg.brush().x(x)
7171
.on("brushstart", brushstart)
72-
.on("brush", brush)
72+
.on("brush", brushmove)
7373
.on("brushend", brushend))
7474
.selectAll("rect")
7575
.attr("height", h);
@@ -78,7 +78,7 @@
7878
svg.classed("selecting", true);
7979
}
8080

81-
function brush() {
81+
function brushmove() {
8282
var s = d3.event.target.extent();
8383
circle.classed("selected", function(d) { return s[0] <= d && d <= s[1]; });
8484
}

0 commit comments

Comments
 (0)