D3全称Data-Driven-Documents,这里说的不是暗黑 III,d3是一款可视化js库,其主要用途是用HTML或者SVG生动地展现数据。
相信网站开发者大都接入过ga来分析各种数据,例如pv图。ga的图都是基于SVG的,下面笔者就用d3来一步一步实现类似ga的pv线形图,并假设读者具有一定的SVG基础(没有?没关系,w3school帮你快速上手)。
step1:引入d3.js
到github d3下载最新版d3,然后在html代码增加标签
1
2
|
<script src=
"path/to/d3.js"
></script>
<span id=
"more-5049"
></span>
|
step2:创建SVG容器
1
2
3
4
5
6
7
8
9
10
11
12
|
var
margin = {top: 20, right: 20, bottom: 30, left: 50},
width = document.body.clientWidth - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var
container = d3.select(
'body'
)
.append(
'svg'
)
.attr(
'width'
, width + margin.left + margin.right)
.attr(
'height'
, height + margin.top + margin.bottom);
var
svg = container.append(
'g'
)
.attr(
'class'
,
'content'
)
.attr(
'transform'
,
'translate('
+ margin.left +
','
+ margin.top +
')'
);
|
margin、width、height定义了svg节点的位置和尺寸,后面会用到。d3.select类似jquery的选择器,并且d3的语法也支持串联调用,append(‘svg’)将svg追加到body的尾部,同时为svg节点设置了宽度和高度值,attr也有get和set两种用法。
svg的g元素类似于div,在这里作为一组元素的容器,后面加入的元素都放在g里面,g可以设置统一的css,里面的子元素会继承可继承css属性。margin和position对g的定位不起作用,只能使用translate通过位移来定位。
step3:定位坐标轴
既然d3是数据驱动的,那必须要有数据啊,没有数据肿么能搞呢。好吧,首先模拟一份数据,就模拟本月的pv数据吧,即12月每天的pv数据,日期采用yy-mm-dd的格式,pv随机一个100以内的整数。
1
2
3
4
5
|
var
data = Array.apply(0, Array(31)).map(
function
(item, i) {
// 产生31条数据
i++;
return
{date:
'2013-12-'
+ (i < 10 ?
'0'
+ i : i), pv: parseInt(Math.random() * 100)}
});
|
然后定义坐标轴的一些参数
1
2
3
4
5
6
7
|
var
x = d3.time.scale()
.domain(d3.extent(data,
function
(d) {
return
d.day; }))
.range([0, width]);
var
y = d3.scale.linear()
.domain([0, d3.max(data,
function
(d) {
return
d.value; })])
.range([height, 0]);
|
横坐标是日期,这里使用d3.time自动帮我们在时间和字符串之间做转换。y轴使用普通的线性缩放坐标轴。其实这里的x和y也是一个function,后续会用到。
domain规定了坐标轴上值的范围,d3.extent从数组里选出最小值和最大值,d3.max选数组里面最大值。range规定了坐标轴端点的位置,svg的坐标原点是左上角,向右为正,向下为正,而y轴正方向为由下向上,所以(0, height)才是图表的坐标原点。
然后使用d3的axis定制坐标轴
1
2
3
4
5
6
7
8
9
|
var
xAxis = d3.svg.axis()
.scale(x)
.orient(
'bottom'
)
.ticks(30);
var
yAxis = d3.svg.axis()
.scale(y)
.orient(
'left'
)
.ticks(10);
|
orient有四个参数(left、right、top、bottom)定义了坐标轴的位置,这里很好理解。
ticks定义了坐标轴上除最小值和最大值以外最多有多少个刻度,因为一个月最多有31天,ticks(30)就足以展示每天的刻度了。
然后就可以把坐标轴加进svg容器了
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// 横坐标
svg.append(
'g'
)
.attr(
'class'
,
'x axis'
)
.attr(
'transform'
,
'translate(0,'
+ height +
')'
)
.call(xAxis)
// 增加坐标值说明
.append(
'text'
)
.text(
'日期'
)
.attr(
'transform'
,
'translate('
+ width +
', 0)'
);
// 纵坐标
svg.append(
'g'
)
.attr(
'class'
,
'y axis'
)
.call(yAxis)
.append(
'text'
)
.text(
'次/天'
);
加上坐标轴之后的效果图应该是这样
|
step4:画线
有了坐标轴之后我们可以加上图表的主体部分了,pv图应该是一条折线图。怎么加折线呢,d3提供了丰富的图表元素,需要折线只需要append(‘path’)即可,了解svg的都知道,path的d属性是最重要的,决定了折线的“路径”,这里就不详细讲解path了。
我们只有一个数组的数据,怎么转化成需要的d呢,别担心,d3帮我们做了这部分工作。首先需要用d3.svg.line生成一个“线条函数”,然后将数据传给该函数即可生成我们想要的d,我们需要做的就是定制这个“线条函数”的两条坐标轴分别由数据的哪部分决定。
下面看代码
1
2
3
4
|
var
line = d3.svg.line()
.x(
function
(d) {
return
x(d.date); })
.y(
function
(d) {
return
y(d.pv); })
.interpolate(
'monotone'
);
|
上面的代码很好理解,设置了x坐标轴由date属性决定,y坐标轴由pv属性决定,最后还调用了interpolate,该方法会改变线条相邻两点之间的链接方式以及是否闭合,接受的参数有linear,step-before,step-after,basis,basis-open,basis-closed,bundle,cardinal,cardinal-open,cardinal-closed,monotone,读者可以一一尝试,看看线条有什么不一样。
“线条函数”生成好了,可以应用到path上了
1
2
3
|
var
path = svg.append(
'path'
)
.attr(
'class'
,
'line'
)
.attr(
'd'
, line(data));
|
此时的图应该是这样了
step5:打点
到这里其实基本的图形已经实现了,只用了应该不到20行代码,不过这也太丑了点吧,而且完全木有交互啊。
别急,ga的pv图在每个数据点都会有一个小点来占位,其实本来我们的数据就是离散的,图上也应该是离散的一些点,不过为了图表好看,也为了方便查看数据的走势,折线图显然更形象一些。
下面就在折线上增加相应的点,点我们可以用circle,要增加元素用append即可
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
var
g = svg.selectAll(
'circle'
)
.data(data)
.enter()
.append(
'g'
)
.append(
'circle'
)
.attr(
'class'
,
'linecircle'
)
.attr(
'cx'
, line.x())
.attr(
'cy'
, line.y())
.attr(
'r'
, 3.5)
.on(
'mouseover'
,
function
() {
d3.select(
this
).transition().duration(500).attr(
'r'
, 5);
})
.on(
'mouseout'
,
function
() {
d3.select(
this
).transition().duration(500).attr(
'r'
, 3.5);
});
|
这里的代码可能复杂一点,因为circle不止一个,需要使用selectAll,而circle现在是还不存在的。selectAll(‘circle’)的作用可以理解成先预定若干个circle的位置,等有数据了再插入svg容器里。
enter就表明有数据来了,将每个circle放到单独的g里面,这里没有特殊的用意,就像html里面习惯用div来装其他元素一样。
为circle设置一些属性,cx、cy代表圆心x、y坐标,line.x()和line.y()会返回折线上相应点的x、y坐标,这样添加的circle就依附在折线上了。r表示圆半径,同时为circle添加了两个鼠标事件,这样鼠标在circle上移动和移出的时候增加了圆半径变化的一个动画。
效果图
step6:增加tips
现在看整体数据倒是可以了,不过看某天的具体数据还是太不方便了,如果在circle上直接标注出具体的数据又太挫了。
咋办?嘿嘿,参考ga呗。ga在鼠标经过某点的纵坐标所在的直线的时候就会在改点附近出现具体的数据tips,赞,既能清晰地看到整体的走势又能看到每天的具体数据。
先上效果图
图中用一个圆角矩形和两行文字组成了一个简单的tips
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
var
tips = svg.append(
'g'
).attr(
'class'
,
'tips'
);
tips.append(
'rect'
)
.attr(
'class'
,
'tips-border'
)
.attr(
'width'
, 200)
.attr(
'height'
, 50)
.attr(
'rx'
, 10)
.attr(
'ry'
, 10);
var
wording1 = tips.append(
'text'
)
.attr(
'class'
,
'tips-text'
)
.attr(
'x'
, 10)
.attr(
'y'
, 20)
.text(
''
);
var
wording2 = tips.append(
'text'
)
.attr(
'class'
,
'tips-text'
)
.attr(
'x'
, 10)
.attr(
'y'
, 40)
.text(
''
);
|
为啥要用矩形呢,为啥不直接在g上设置圆角效果呢?实践证明对g设置的width、height、border-radius均无效,无赖只能使用svg的rect元素了。
rx、ry是圆角两个方向的半径,原理同border-radius。展示文字用text元素即可,这里的x和y还是坐标,不过是相对于父元素g的坐标。
最后的关键是怎么让tips出现在该出现的位置和展示对的数据,即鼠标经过某个点的纵坐标所在的直线是tips出现在改点附近,且展示改点的数据。
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
|
container
.on(
'mousemove'
,
function
() {
var
m = d3.mouse(
this
),
cx = m[0] - margin.left;
var
x0 = x.invert(cx);
var
i = (d3.bisector(
function
(d) {
return
d.date;
}).left)(data, x0, 1);
var
d0 = data[i - 1],
d1 = data[i] || {},
d = x0 - d0.date > d1.date - x0 ? d1 : d0;
function
formatWording(d) {
return
'日期:'
+ d3.time.format(
'%Y-%m-%d'
)(d.date);
}
wording1.text(formatWording(d));
wording2.text(
'PV:'
+ d.pv);
var
x1 = x(d.date),
y1 = y(d.pv);
// 处理超出边界的情况
var
dx = x1 > width ? x1 - width + 200 : x1 + 200 > width ? 200 : 0;
var
dy = y1 > height ? y1 - height + 50 : y1 + 50 > height ? 50 : 0;
x1 -= dx;
y1 -= dy;
d3.select(
'.tips'
)
.attr(
'transform'
,
'translate('
+ x1 +
','
+ y1 +
')'
);
d3.select(
'.tips'
).style(
'display'
,
'block'
);
})
.on(
'mouseout'
,
function
() {
d3.select(
'.tips'
).style(
'display'
,
'none'
);
});
|
这段长长的代码需要重点解释一下,首先是d3.mouse(this),这个方法会返回当前鼠标的坐标,是一个数组,分别是x和y坐标。
下面这一步最重要的一点来了,x.invert(cx)跟据传入的横坐标数值返回该横坐标的实际数据上的值,在本例中返回一个日期。
下面的i是根据返回的日期反向得到data数组中的元素位置。有了这个i一切都好办了,接下来的代码是为了判断鼠标在两个日期之间离哪个更近。
后面的代码都很简单了,拿到了tips应该出现的x、y坐标之后设置tips的transform即可,再控制tips的display属性就达到了最后的效果。
完整代码地址:http://pan.baidu.com/s/1jirPO,提取码:m0qb