Zhihao's Studio.

polybrush in d3

Word count: 2,309 / Reading time: 11 min
2014/11/15 Share

前言

你是否也像我一样,厌倦了d3默认的矩形的brush,或者你发现矩形的brush已经不能满足你的需要了;又或者你只是单纯的被上面的图片所吸引。不管怎样,我们将介绍一下polybrush。

这篇博客的名字叫polybrush in d3,现在想来,好像有点错误,因为polybrush并不是d3内置的,它是由Geoffrey T. Bell在2012年开发的d3插件,因其方便、好用,所以广受好评。

但无奈目前中文的相关资料和博客几乎没有,因此我来试着写一下如何使用它,以及它的原理,还会包含源码粗略解读,polybrush.js的github参考项目下载地址是:https://gist.github.com/GerHobbelt/3732612

学习一个东西,大抵有三层境界,第一,会使用它,第二,知道他的原理,第三,知其然也知其所以然。这也是本文接下来三章的组织方式,首先,介绍最基本的如何使用,再去试着解释原理,最后深入到代码级。

如何使用

与brush相同的地方

使用polybrush,几乎和使用d3内置的brush一样方便,仅需一行就可以建立一个polybrush的选择集:var brush = d3.svg.polybrush(); 这和brush几乎是一样的;
同样相同的还有brush的三个过程:brush(),brushStart(),burshFinished() ,在这三个过程中添加相应的处理逻辑的代码,完成指定的action。不熟悉的可以去看我之前写的brush的博客。

与brush不同的地方

与brush不同的地方也很显然,就是如何判定一个元素在还是不在选择集合中。之前的brush出来的形状是一个矩形,矩形的四个角的坐标都有,判断可以用下面的代码来完成:

1
2
3
4
5
6
7
8
9
10
11
12
13
var extent = brush.extent();
var xmin = extent[0]()[0]();
var xmax = extent[1]()[0]();
var ymin = extent[0]()[1]();
var ymax = extent[1]()[1]();
if(d[0]()\>=xmin && d[0]()\<=xmax && d[1]()\>=ymin && d[1]()\<=ymax){
//在brush的区域内
}else{
//不在brush的区域内
}

那现在我们brush出来的那个奇怪的形状该如何来判定呢?还好,Geoffrey T. Bell替你完成了这部分的判定,你只需要调用if(brush.isWithinExtent(x,y))就可以了!至此,你达到了polybrush的第一层境界,即学会了使用它。很显然,我们接下来需要介绍的是如何判断点在还是不在polybrush选中的那块区域里面。

例子

我这实现了一个使用polybrush的最简单的例子,在前面博客中写到的散点图的基础上加上了polybrush的操作。

需要特别注意的是判断是否在brush的区域内,传入的参数不是d【0】和d【1】, 而是坐标,x(d.x),y(d.y),因为传入的d.x d.y已经经过了变换了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
\<!DOCTYPE html\>
\<html\>
\<head lang="en"\>
\<meta charset="UTF-8"\>
\<script type="text/javascript" src="script/d3.v3.js"\> \</script\>
\<script src="polybrush.js"\>\</script\>
\<title\>\</title\>
\<style type="text/css"\>
.brush .extent {
stroke: #000;
stroke-width: 1.5px;
fill: #000;
fill-opacity: 0.3;
}
.point.selected {
fill: red;
stroke: darkred;
stroke-width: 2;
}
.point {
fill: steelblue;
/*stroke: #000;*/
}
.axis path,
.axis line {
fill: none;
stroke: #000;
shape-rendering: crispEdges;
}
\</style\>
\</head\>
\<body\>
\<script\>
var margin = {top:20,right:20,bottom:20,left:20};
var width= 960-margin.left-margin.right;
var height = 500-margin.top-margin.bottom;
var x = d3.scale.linear().range([0,width]());
var y = d3.scale.linear().range([height,0]());
var svg = d3.select('body').append('svg').attr('width',width+margin.left+margin.right).attr('height',height+margin.top+margin.bottom)
.append('g')
.attr('transform',"translate("+margin.left+","+margin.top+")");
d3.csv('tmp.csv',function(error,data){
if(error) throw error;
data.forEach(function(d){
d.x=+d.x;
d.y=+d.y;
})
x.domain(d3.extent(data,function(d){return d.x})).nice();
y.domain(d3.extent(data,function(d){return d.y})).nice();
svg.append('g')
.attr('class','x axis')
.attr('transform','translate(0,'+height+")")
.call(d3.svg.axis().scale(x).orient('bottom'));
svg.append('g')
.attr("class",'y axis')
.call(d3.svg.axis().scale(y).orient('left'));
svg.selectAll('.point')
.data(data).enter()
.append('circle')
.attr('class','point')
// .attr('d',d3.svg.symbol().type('triangle-up'))
.attr('cx', function(d) { return d[0](); })
.attr('cy', function(d) { return d[1](); })
.attr('r', 15)
.attr('transform',function(d){return "translate(" +
""+x(d.x)+","+y(d.y)+")"});
var brush = d3.svg.polybrush()
.x(d3.scale.linear().range([0,width]()))
.y(d3.scale.linear().range([0,height]()))
.on('brushstart',function(){
svg.selectAll('.point').classed('selected',false);
})
.on('brush',function(){
svg.selectAll('.point').classed('selected',function(d){
console.log(d);
if(brush.isWithinExtent(x(d.x), y(d.y)))
{
console.log('helo')
return true;
}else{
console.log('no')
return false;
}
})
})
svg.append('svg:g')
.attr('class','brush')
.call(brush);
})
\</script\>
\</body\>
\</html\>

Ray casting algorithm

判断某个点在还是不在polybrush选中的区域中,用的是图形学中的Ray casting算法。它的思想很简单,作一条以点为起点,且平行于x轴的线,数与brush产生的凹多边形边相交的次数n,根据n的奇偶性来判断点是在里面还是在外面。如果在里面,n是奇数,反之则为偶数。
可以借助下面的这幅图来很好的理解这个算法:

深入源码

终于到第三层境界,也是最难啃的一块硬骨头了。首先,作为一个d3的插件,需要用如下的代码来包住整个的代码块,这是js种的立即函数,引入代码段之后会立即执行,并return 相应的结果。

1
2
3
4
5
6
(function(d3) {
d3.svg.polybrush = function() {
})(d3)

接下来定义了一个类似广播的dispatch,定义了三种类似brush中可以监听的行为,brush start,brush,brushed,这样就可以用.on(“brush”,function(){})来进行监听并提供处理的函数了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
var dispatch = d3.dispatch("brushstart", "brush", "brushend"),
x = null,
y = null,
extent = [](),
firstClick = true,
firstTime = true,
wasDragged = false,
origin = null,
line = d3.svg.line()
.x(function(d) {
return d[0]();
})
.y(function(d) {
return d[1]();
});
d3.rebind(brush, dispatch, "on");

还定义了一些变量,x,y,extent,等,这些变量都是为了配合下面的操作设立的,从名字就可以看出一些变量的作用。
line是由起点和终点组成的,而特定点的x,y坐标在这段代码中被分别指定好。

brush函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var brush = function(g) {
g.each(function() {
var bg, e, fg;
g = d3.select(this);
bg = g.selectAll(".background").data([0]());
fg = g.selectAll(".extent").data([extent]());
g.style("pointer-events", "all").on("click.brush", addAnchor);
bg.enter().append("rect").attr("class", "background").style("visibility", "hidden").style("cursor", "crosshair");
fg.enter().append("path").attr("class", "extent").style("cursor", "move");
if (x) {
e = scaleExtent(x.range());
bg.attr("x", e[0]()).attr("width", e[1]() - e[0]());
}
if (y) {
e = scaleExtent(y.range());
bg.attr("y", e[0]()).attr("height", e[1]() - e[0]());
}
});
};

选出前景背景,并添加对应的class,使得前景和背景区分开。还添加了对前景背景移动时的重新判断。
监听brush过程中的点击事件,进行添加锚点addAnchor的操作。
接下来是两个函数,第一个是绘制已有的路径,第二个是将domain的start和stop按照从小到大的顺序排好。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var drawPath = function() {
return d3.selectAll("g.brush path").attr("d", function(d) {
return line(d) + "Z";
});
};
var scaleExtent = function(domain) {
var start, stop;
start = domain[0]();
stop = domain[domain.length - 1]();
if (start \< stop) {
return [start, stop]();
} else {
return [stop, start]();
}
};

下面还有一些辅助函数,从函数的名字上就可以很清楚的知道函数的作用,这里不多做解释了,没有太多值得说的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
var withinBounds = function(point) {
var rangeX, rangeY, \_x, \_y;
rangeX = scaleExtent(x.range());
rangeY = scaleExtent(y.range());
\_x = Math.max(rangeX[0](), Math.min(rangeX[1](), point[0]()));
\_y = Math.max(rangeY[0](), Math.min(rangeY[1](), point[1]()));
return point[0]() === \_x && point[1]() === \_y;
};
var moveAnchor = function(target) {
var moved, point;
point = d3.mouse(target);
if (firstTime) {
extent.push(point);
firstTime = false;
} else {
if (withinBounds(point)) {
extent.splice(extent.length - 1, 1, point);
}
drawPath();
dispatch.brush();
}
};
var closePath = function() {
var w;
w = d3.select(window);
w.on("dblclick.brush", null).on("mousemove.brush", null);
firstClick = true;
if (extent.length === 2 && extent[0]()[0]() === extent[1]()[0]() && extent[0]()[1]() === extent[1]()[1]()) {
extent.splice(0, extent.length);
}
d3.select(".extent").on("mousedown.brush", moveExtent);
return dispatch.brushend();
};
var addAnchor = function() {
var g, w,
\_this = this;
g = d3.select(this);
w = d3.select(window);
firstTime = true;
if (wasDragged) {
wasDragged = false;
return;
}
if (firstClick) {
extent.splice(0, extent.length);
firstClick = false;
d3.select(".extent").on("mousedown.brush", null);
w.on("mousemove.brush", function() {
return moveAnchor(\_this);
}).on("dblclick.brush", closePath);
dispatch.brushstart();
}
if (extent.length \> 1) {
extent.pop();
}
extent.push(d3.mouse(this));
return drawPath();
};
var dragExtent = function(target) {
var checkBounds, fail, p, point, scaleX, scaleY, updateExtentPoint, \_i, \_j, \_len, \_len1;
point = d3.mouse(target);
scaleX = point[0]() - origin[0]();
scaleY = point[1]() - origin[1]();
fail = false;
origin = point;
updateExtentPoint = function(p) {
p[0]() += scaleX;
p[1]() += scaleY;
};
for (\_i = 0, \_len = extent.length; \_i \< \_len; \_i++) {
p = extent[\_i]();
updateExtentPoint(p);
}
checkBounds = function(p) {
if (!withinBounds(p)) {
fail = true;
}
return fail;
};
for (\_j = 0, \_len1 = extent.length; \_j \< \_len1; \_j++) {
p = extent[\_j]();
checkBounds(p);
}
if (fail) {
return;
}
drawPath();
return dispatch.brush({
mode: "move"
});
};
var dragStop = function() {
var w;
w = d3.select(window);
w.on("mousemove.brush", null).on("mouseup.brush", null);
wasDragged = true;
return dispatch.brushend();
};
var moveExtent = function() {
var \_this = this;
d3.event.stopPropagation();
d3.event.preventDefault();
if (firstClick && !brush.empty()) {
d3.select(window).on("mousemove.brush", function() {
return dragExtent(\_this);
}).on("mouseup.brush", dragStop);
origin = d3.mouse(this);
}
};

最后需要说一下的是isWithinExtent函数,应该可以算作是上一章节中提到的ray casting算法的一个js版本的实现。其中,数奇偶是由ret来完成的。只不过实现中,他是用取反操作!来完成的,没有用自增++操作。

1
2
3
4
5
6
7
8
9
brush.clear = function() {
extent.splice(0, extent.length);
return brush;
};
brush.empty = function() {
return extent.length === 0;
};

clear函数是用来清空brush的区域的,empty是用来判断是否有区域被brush了。和上面的那些没有特别说明的函数一样,这些是辅助完成polybrush操作的函数,可以认为是辅助函数。

总结

至此,算是粗略的完成了polybrush相关的介绍,从开始的如何使用,到它与brush的异同点,到最后的源代码的泛读。希望本文能对读者有所帮助,那会是我最大的欣慰。

CATALOG
  1. 1. 前言
  2. 2. 如何使用
    1. 2.1. 与brush相同的地方
    2. 2.2. 与brush不同的地方
    3. 2.3. 例子
  3. 3. Ray casting algorithm
  4. 4. 深入源码
  5. 5. 总结