前言 你是否也像我一样,厌倦了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不同的地方也很显然,就是如何判定一个元素在还是不在选择集合中。之前的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的区域内
}
例子 我这实现了一个使用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,等,这些变量都是为了配合下面的操作设立的,从名字就可以看出一些变量的作用。
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]());
}
  });
};
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的异同点,到最后的源代码的泛读。希望本文能对读者有所帮助,那会是我最大的欣慰。
            
    
    
    
    
    
        
    
    
   
    
    
    
    
    
 为正常使用来必力评论功能请激活JavaScript