Loading...
D3.js 入门教程系列
原文链接 https://ronghuaxueleng.github.io/2016/09/24/d3-D3-js-%E5%85%A5%E9%97%A8%E6%95%99%E7%A8%8B%E7%B3%BB%E5%88%97/
注:以下为加速网络访问所做的原文缓存,经过重新格式化,可能存在格式方面的问题,或偶有遗漏信息,请以原文为准。
初识D3
D3是指数据驱动文档(Data-Driven Documents),根据D3的官方定义:
D3.js是一个JavaScript库,它可以通过数据来操作文档。D3可以通过使用HTML、SVG和CSS把数据鲜活形象地展现出来。D3严格遵循Web标准,因而可以让你的程序轻松兼容现代主流浏览器并避免对特定框架的依赖。同时,它提供了强大的可视化组件,可以让使用者以数据驱动的方式去操作DOM。----D3维基(2013年8月)
从一个Hello Wordld示例来看看d3如何运行 点击查看在线DEMO
function render(data){
var demo = d3.select('body')
.selectAll('p')
.data(data, function (d) {
return d;
});
demo.enter()
.append('p')
.style('width', 0+'px')
.style('background', '#eee');
demo.text(function (d) {
return d;
})
.transition()
.duration(1000)
.style('width', function (d) {
return d+'px';
});
demo.exit()
.transition()
.duration(1000)
.style('width', 0+'px')
.remove();
}
render([100,300,500]);
setTimeout(function(){
render([300,500,1000]);
},1000)
通过链式风格的方式让整个运行步骤非常清晰,D3的API设计跟我们平时用的Jquery很像,所以理解起来比较容易。
Hello World示例展示了数据如何驱动页面展示,在render方法里通过数据的变化,它做到了初始化新数据
,更新已有数据
,删除无效数据
这样一个完整的环节;
D3数据绑定
我们从Hello World示例代码来看D3如何做数据绑定
var demo = d3.select('body')
.selectAll('p')
.data(data, function (d) {
return d;
});
这段代码执行了以下步骤:
1、通过 d3.select('body’) 将页面上的body标签选中;
2、selectAll('p') 返回了一个p标签的空选集(selection);
3、空选集通过 data()方法将数据和p标签绑定,并产生三个虚拟的子集: enter, update,exit;
其中enter()包含了待绑定数据及相应的标签的占位符; update()包含了已绑定数据的p标签; exit()包含了待移除数据p标签; 我们通过一张图来形象的理解enter, update , exit
enter数据
demo.enter()
.append('p')
.style('width', 0+'px')
.style('background', '#eee')
这段代码为 enter() 中的占位符创建p标签,绑定相应的数据,并初始化样式;
update数据
demo
.text(function (d) {
return d;
})
.transition()
.duration(1000)
.style('width', function (d) {
return d+'px';
})
这段代码为 update() 中的p标签写入数字文本,并绑定transition动画,使它以动画形式展现;
是不是没看到 update() 的影子,没关系,update()会被隐式调用,之所以前面要提到 update 是为了更好的理解这三种状态;
exit数据
demo.exit()
.transition()
.duration(1000)
.style('width', 0+'px')
.remove();
这段代码为 exit() 中的p标签绑定transition动画,使它以动画形式移除;
D3的这种数据绑定方式的设计,使enter()里只是专心处理新增的元素,而update 和exit分别专注于处理更新和待删除部分,这意味着你不用把所有DOM元素删了重绘,因此得以轻松应对实时变化的数据,甚至支持一些交互(如拖动)与渐变的效果!
D3选择器
用过kissy的都知道它的选择器有 Node.one
和 Node.all
两个,前一个是选择第一个结果,后一个是选择所有结果;
D3的选择器跟kissy类似,只是名字换成了 d3.select
和 d3.selectAll
,应该比较好理解;
例如:
选中body标签
var body = d3.select('body');
选中所有p标签
var p = d3.selectAll('p');
不一样的地方在于转换为原生dom时,Kissy是Node.all('body')[0]
而D3是d3.selectAll('body')[0][0]
D3选择器还有以下这些方法帮助我们对节点或数据做一些操作
方法名 | 含义 | 示例 |
---|---|---|
selection.append | 创建并追加一个新元素 | p.append('span') |
selection.attr | 取得或设置属性的值 | p.attr('class','demo') |
selection.call | 为当前选择调用一个函数 | p.call(function(d){d.text('demo')}) |
selection.classed | 添加或移除CSS类 | p.classed('demo',true) |
selection.data | 为一组元素分别取得或设置数据 | p.data([1,2,3],function(d){return d;}) |
selection.datum | 为单个元素取得或设置数据 | p.datum(1) |
selection.each | 为每个选中的元素调用一个函数 | p.data([1,2,3]).each(function(d,i){console.log(d)}) |
selection.empty | 如果选择是空则返回true | console.log(p.empty()) |
selection.enter | 为缺失的元素返回占位符 | p.enter() |
selection.exit | 返回不再需要的元素 | p.exit() |
selection.filter | 基于数据过滤选择 | p.data([1,2,3]).filter(function(d,i){return d%2== 0}) |
selection.html | 取得或设置innerHTML内容 | p.html('1 2') |
selection.insert | 在已存在元素之前创建并插入一个元素 | p.insert('span') |
selection.interrupt | 如果有过渡的话,立即中断当前的过渡 | p.interrupt() |
selection.node | 返回选择中的第一个节点 | p.node().innerHTML = 'demo' |
selection.on | 为交互添加或移除事件监听器 | p.on('click',function(d){console.log(d)}) |
selection.order | 重排列文档中的元素,以匹配选择 | var div = d3.select("body").selectAll("div") .data(["a", "b", "f"]);div.enter().append("div") .text(String); var div =d3.select("body").selectAll("div") .data(["a", "b", "c", "d", "e", "f"],String); div.enter().append("div") .text(String); div.order(); |
selection.property | 取得或设置行内属性 | d3.select('input').property('checked') |
selection.remove | 从当前文档中移除当前元素 | p.remove() |
selection.select | 为每个选中元素的在选择一个后代元素 | p.select('span') |
selection.selectAll | 为每个选中元素的在选择多个后代元素 | p.selectAll('span) |
selection.size | 返回选择中的元素数 | p.size() |
selection.sort | 基于数据排列文档中的元素 | p.data([1,3,2]).sort(function (a,b) {return a>b;}) |
selection.style | 取得或设置样式属性 | p.style('width','100px') |
selection.text | 取得或设置文本内容 | p.text('demo') |
selection.transition | 在选中元素上开启过渡 | p.transition() |
D3 创建SVG
D3图表大都是由SVG来实现的,所以在继续学习前需要了解一些SVG的知识,可以通过看svg教程来学习一下。
D3可以生成一般的SVG形状,它也内置了很多SVG图表,方便我们直接生成实用的图表。
我们先来创建一个SVG容器,其它SVG图形都需要在容器里来创建:
var body = d3.select('body');
var svg = body.append('svg').attr('width',200).attr('height',200);
普通SVG
用D3来生成一般的SVG图形,可以直接用 append + attr 就可以实现,例如:
线条(line):
var line = svg.append('line')
.attr('x1',100)
.attr('y1',100)
.style({fill:'none',stroke:'#000'});
圆(circle):
var circle = svg.append('circle')
.attr('cx',50)
.attr('cy',50)
.attr('r',50)
.style({fill:'#000'});
SVG生成器
D3为我们提供了更复杂的svg图形生成器,结合数据我们可以轻松绘制出复杂的SVG,例如:
折线图(d3.svg.line):
var data = [[
{x:0,y:100},
{x:50,y:80},
{x:100,y:150},
{x:150,y:50},
{x:200,y:120}
]];
var line = d3.svg.line()
.x(function(d){return d.x})
.y(function(d){return 200-d.y});
var lineChart = svg.selectAll('path').data(data);
lineChart.enter()
.append('path')
.attr('d',function(d){
return line(d)
})
.style({fill:'none',stroke:'#000'});
面积图(d3.svg.area):
var data = [[
{x:0,y:100},
{x:50,y:80},
{x:100,y:150},
{x:150,y:50},
{x:200,y:120}
]];
var area = d3.svg.area()
.x(function(d){return d.x})
.y0(function(d){return 200})
.y1(function(d){return 200-d.y});
var areaChart = svg.selectAll('path').data(data);
areaChart.enter()
.append('path')
.attr('d',function(d){
return area(d)
}).style({fill:'#000'});
圆弧图(d3.svg.arc):
var angle = 2*Math.PI;
var data = [{startAngle:0,endAngle:0.2*angle},{startAngle:0.5*angle,endAngle:0.8*angle}];
var arc = d3.svg.arc().innerRadius(50).outerRadius(100);
var arcChart = svg.selectAll('path').data(data);
arcChart.enter()
.append('path')
.attr('transform','translate(100,100)')
.attr('d',function(d){
return arc(d)
}).style({fill:'#000'});
径向折线图(d3.svg.line.radial):
var angle = 2*Math.PI;
var data = [[100,80,60,90,90,100]];
var lineRadial = d3.svg.line.radial()
.radius(function(d){return d})
.angle(function(d,i){
if(i == 5){
return 0
};
return (i/5)*angle
});
var lineRadialChart = svg.selectAll('path').data(data);
lineRadialChart.enter()
.append('path')
.attr('transform','translate(100,100)')
.attr('d',function(d){
return lineRadial(d)
}).style({fill:'none',stroke:'#000'});
径向面积图(d3.svg.area.radial):
var angle = 2*Math.PI;
var data = [[100,80,60,90,90,100]];
var areaRadial = d3.svg.area.radial()
.innerRadius(function(d) { return d/2; })
.outerRadius(function(d) { return d; })
.angle(function(d,i){
if(i == 5){
return 0
};
return (i/5)*angle
});
var areaRadialChart = svg.selectAll('path').data(data);
areaRadialChart.enter()
.append('path')
.attr('transform','translate(100,100)')
.attr('d',function(d){
return areaRadial(d)
}).style({fill:'#000'});
弦图(d3.svg.chord):
var angle = 2*Math.PI;
var chordRadial = d3.svg.chord()
.radius(100)
.source({startAngle:0,endAngle:0.1*angle}).target({startAngle:0.4*angle,endAngle:0.8*angle});
var chordRadialChart = svg.append('path')
.attr('transform','translate(100,100)')
.attr('d',function(d){
return chordRadial(d)
}).style({fill:'#000'});
对角线图(d3.svg.diagonal):
var angle = 2*Math.PI;
var diagonal = d3.svg.diagonal()
.source({x:10,y:30})
.target({x:180,y:200});
var diagonalRadialChart = svg.append('path')
.attr('d',function(d){
return diagonal(d)
})
.style({fill:'none',stroke:'#000'});
符号(d3.svg.symbol):
var symbol = d3.svg.symbol()
.type(function(d,i){
return type[d-1]
}).size(function(d){return d*50;});
var data = [1,2,3,4,5];
var type = [
'circle',
'cross',
'diamond',
'square',
'triangle-down',
'triangle-up'
];
var symbolChart = svg.selectAll('path')
.data(data).enter()
.append('path');
symbolChart
.attr('transform',function(d,i){
var t = i*20+10;
return 'translate('+ t +','+ t +')'})
.attr('d',function(d){
return symbol(d)
}).style({fill:'none',stroke:'#000'});
坐标轴(d3.svg.axis):
var axisx = d3.svg.axis()
.scale(
d3.scale.linear()
.domain([0,100])
.range([0,140])
)
.ticks(5);
var axisxChart = svg.append('g')
.attr('transform','translate(30,170)')
.call(axisx)
.style({
fill:'none',
'stroke-width':1,
stroke:'#000',
'font-size':12
});
var axisy = d3.svg.axis()
.scale(
d3.scale.linear()
.domain([0,100])
.range([140,0])
)
.ticks(5)
.orient('left');
var axisyChart = svg.append('g')
.attr('transform','translate(30,30)')
.call(axisy)
.style({
fill:'none',
'stroke-width':1,
stroke:'#000',
'font-size':12
});
brush(d3.svg.brush):
svg.style('background','#eee');
var linear = d3.scale.linear()
.domain([0,100])
.range([0,200]);
var brush = d3.svg.brush()
.x(linear)
.on('brush',function(){
console.log(brush.extent());
});
var g = svg.append('g')
.call(brush).selectAll('rect')
.attr('height',200);
D3制作图表的过程就是将各种SVG图形拼接在一起的过程;
D3 SVG图表示例
之前有说到“D3制作图表的过程就是将各种SVG图形拼接在一起的过程”,具体来说折线图表就是折线图
+坐标轴
的组合,面积图是折线图
+坐标轴
+面积图
一个完整的SVG图表,是包含了各种数据、SVG图形、样式、交互组成的组合体,我们以面积图表来做一个示例展示:
CSS
svg{
font-size: 12px;
}
.line{
fill:none;
stroke-width:1;
stroke:#000;
}
.axis line{
stroke: #000;
stroke-width: 1;
}
.axis .domain{
fill:none;
stroke-width:1;
stroke:#000;
}
.area{
fill: #eee;
}
数据
var _data = [
[
{"x":0,"y":4},
{"x":1,"y":4},
{"x":2,"y":6},
{"x":3,"y":0},
{"x":4,"y":7},
{"x":5,"y":6},
{"x":6,"y":1},
{"x":7,"y":1},
{"x":8,"y":7},
{"x":9,"y":5},
{"x":10,"y":3}
]
]
创建SVG容器
var _width = 600;
var _height = 300;
var _margin = {top:30,right:30,bottom:30,left:30};
var _xStart = _margin.left;
var _xEnd = _width - _margin.right;
var _yStart = _height - _margin.bottom;
var _yEnd = _margin.top;
var _svg = d3.select('body').append('svg')
.attr('width',_width)
.attr('height',_height);
var _bodyG = _svg.append('g')
.attr('transform','translate('+ _xStart +','+ _yEnd +')');
设置数据比例尺
var _quadrantWidth = _width - _margin.left - _margin.right;
var _quadrantHeight = _height - _margin.top - _margin.bottom;
var _x = d3.scale.linear().domain([0,10]).range([0,_quadrantWidth])
var _y = d3.scale.linear().domain([0,10]).range([_quadrantHeight,0])
生成X轴SVG图
var _xAxis = d3.svg.axis()
.scale(_x)
.orient('bottom');
_svg.append('g')
.classed('x axis',true)
.attr('transform', 'translate('+ _xStart +', '+ _yStart +')')
.call(_xAxis);
生成Y轴SVG图
var _yAxis = d3.svg.axis()
.scale(_y)
.orient('left');
_svg.append('g')
.classed('y axis',true)
.attr('transform','translate('+ _xStart +','+ _yEnd +')')
.call(_yAxis);
生成折线SVG图
var _line = d3.svg.line()
.x(function(d){return _x(d.x)})
.y(function(d){return _y(d.y)});
var _linePath = _bodyG.selectAll('.line').data(_data);
_linePath
.enter()
.append('path')
.classed('line',true);
_linePath
.attr('d', function(d){
return _line(d);
});
生成面积SVG图
var _area = d3.svg.area()
.x(function(d){
return _x(d.x);
})
.y0(function(d){
return _quadrantHeight;
})
.y1(function(d){
return _y(d.y);
})
var _areaPath = _bodyG.selectAll('.area').data(_data);
_areaPath
.enter()
.append('path')
.classed('area',true);
_areaPath
.attr('d', function(d){
return _area(d);
});
在这个示例中完整的展现了一个D3图表是怎么将数据与图形做绑定,以及如何将不同的SVG组合成一个完整的图表,还用到了目前为止没讲过的D3比例尺,这个在下节中来学习;
D3比例尺
概念
比例尺是一组把输入域
映射为输出范围
的函数,它将真实值
跟实际显示值
做了一个映射,在之前的例子中,我们已经用到了线性比例尺;
var _x = d3.scale.linear().domain([0,10]).range([0,100])
它的意思是将0-10
之间的输入域以线性的方式映射到0-100
的输出范围上,也就是说传入的值为10,显示的时候显示为100;
_x(0); // 返回0
_x(5); // 返回50
_x(10); // 返回100
常用方法
_x.nice()
告诉比例尺取得为range() 设置的任何值域,把两端的值扩展到最接近的数,比如,值域[0.20147987687960267,0.996679553296417]
的扩展值域为[0.2,1]
,这个方法对正常人都有用,因为人不是计算机,看到0.20147987687960267这样的数你一定会头大。
_x.rangeRound()
用rangeRound() 代替range() 后,则比例尺输出的所有值都会舍入到最接近的整数值,对输出值取整有利于图形对应精确的像素值,避免边缘出现模糊不清的锯齿。
_x.clamp()
默认情况下,线性比例尺可以返回指定范围之外的值。例如,假如给定的值位于输入值域之外,那么比例尺也会返回一个位于输出范围之外的值。不过,在比例尺上调用clamp(true)
后,就可以强制所有输出值都位于指定的范围内。这意味着超出范围的值,会被取整到范围的最低值或最高值(总之是最接近的那个值)。
其它比例尺:
d3.scale.linear - 线性比例尺
将输入域值按线性等比的方式分隔作为输出范围
示例:d3.scale.linear().domain([0,100]).range([0,700])
应用场景:按比例、成直线的展现或者规则、光滑的运动
d3.scale.identity - 线性恒等比例尺
线性恒等比例尺是线性比例尺的特殊类型,实现输入域与输出范围 1:1 的缩放,其方法都是恒等函数
示例:d3.scale.identity().domain([0,100])
应用场景:在处理像素坐标时有用,输入值是多少像素,在输出显示时也是多少像素
d3.scale.quantize - 量化比例尺
量化比例尺是线性比例尺的变体,输入的域是连续的,使用离散的range,输入数据被分割成不同的片段,也即是分类,如10个人按设定的年龄分类。
示例:d3.scale.quantize().domain([0,10]).range([0,10,20,30,40,50,60,70]);
应用场景:想把数据分类的情形(!还没想到最适合的业务场景
)
var quantize = d3.scale.quantize().domain([0,10]).range([0,10,20,30,40,50,60,70]);
quantize(0) ==> 0
quantize(1) ==> 0
quantize(2) ==> 10
quantize(3) ==> 20
quantize(4) ==> 30
quantize(5) ==> 40
quantize(6) ==> 40
quantize(7) ==> 50
quantize(8) ==> 60
quantize(9) ==> 70
quantize(10) ==> 70
d3.scale.quantile - 分位数比例尺
分位数比例尺是线性比例尺的变体,无论输入数据怎么分布,都会被映射成离散值
示例:d3.scale.quantile().domain([0,10,20,30,40,50,60,70]).range([0,100,200,300,400,500,700]);
应用场景:已经对数据分类的情形(!似懂非懂,需要继续研究
)
d3.scale.log - 对数比例尺
对数比例尺与线性比例尺相似,区别是对数比例尺首先对输入数据进行对数变换 。
示例:d3.scale.log().domain([1,100]).range([0,700]).base(2);
应用场景:输入值以对数级变化的数据集(!不太熟悉对数,需要继续研究
)
d3.scale.pow - 指数比例尺
指数比例尺与线性比例尺相似,区别是pow比例尺首先对输入数据进行指数变换,默认指数为1,所以默认情况下也是数值 1:1 的缩放。
示例:d3.scale.pow().domain([0,100]).range([0,700]).exponent(2);
应用场景:输入值以指数级变化的数据集
d3.scale.sqrt - 平方根比例尺
平方根比例尺是pow比例尺的特殊类型,相当于d3.scale.pow().exponent(0.5)
示例:d3.scale.sqrt().domain([0,100]).range([0,700])
应用场景:输入值以平方根变化的数据集
d3.scale.threshold - 临界值比例尺
临界值比例尺与量化比例尺类似,不过允许将任意的子集映射到离散的range。
示例:d3.scale.threshold().domain([0,1,2,3,4,5,6,7]).range([0,0,100,200,300,400,500,600,700]);
应用场景:暂不清楚(!还没完全明白,需要继续研究
)
d3.scale.ordinal - 序数比例尺
使用非定量值(如类名)作为输出的序数比例尺
示例:d3.scale.ordinal().domain(["A", "B", "C", "D", "E", "F", "G", "H"])
.range([0,100,200,300,400,500,600,700]);
应用场景:输入值与转出值需要一一对应的场景
d3.scale.category10()
构造一个新的颜色序数比例尺,使用10种类型的颜色:
#1f77b4 #ff7f0e #2ca02c #d62728 #9467bd #8c564b #e377c2 #7f7f7f #bcbd22 #17becf
d3.scale.category20()
构造一个新的颜色序数比例尺,使用20种类型的颜色:
#1f77b4 #aec7e8 #ff7f0e #ffbb78 #2ca02c #98df8a #d62728 #ff9896 #9467bd #c5b0d5 #8c564b #c49c94 #e377c2 #f7b6d2 #7f7f7f #c7c7c7 #bcbd22 #dbdb8d #17becf #9edae5
d3.scale.category20b()
构造一个新的颜色序数比例尺,使用20种类型的颜色:
#393b79 #5254a3 #6b6ecf #9c9ede #637939 #8ca252 #b5cf6b #cedb9c #8c6d31 #bd9e39 #e7ba52 #e7cb94 #843c39 #ad494a #d6616b #e7969c #7b4173 #a55194 #ce6dbd #de9ed6
d3.scale.category20c()
构造一个新的颜色序数比例尺,使用20种类型的颜色:
#3182bd #6baed6 #9ecae1 #c6dbef #e6550d #fd8d3c #fdae6b #fdd0a2 #31a354 #74c476 #a1d99b #c7e9c0 #756bb1 #9e9ac8 #bcbddc #dadaeb #636363 #969696 #bdbdbd #d9d9d9
d3.time.scale - 日期和时间值比例尺
将日期和时间映射到具体的数值
示例:d3.time.scale().domain([new Date('2015-1-1'), new
Date('2016-12-1')]).range([0,700]).nice();
应用场景:对日期刻度作特殊处理的场景
D3过渡动画
前面我们做的各种图表都是静态的,现在没个动效的图表都不好意思拿出手,好在D3为我们提供了过渡动画;
看一个示例(查看在线演示):
var data = [100,200];
var body = d3.select('body');
var svg = body.append('svg').attr('width',500).attr('height',500);
var g = svg.selectAll('rect').data(data);
g.enter().append('rect')
.attr('y',function(d,i){
return (i+1)*50;
})
.attr('x',50)
.attr('width',10)
.attr('height',20)
.attr('fill','#000')
g.transition()
.delay(1000)
.duration(1000)
.attr('width',function(d){
return d;
})
启动过渡效果只需要添加 transition() 这个即可,跟过渡相关的方法如下:
.duration()
指定整个过渡持续多少时间,单位为毫秒。如 .duration(2000) ,是持续2000毫秒,即2秒。
.ease()
指定过渡的方式,常用的过渡方式有:
- linear 普通的线性变化
- circle 慢慢地到达过渡的最终状态
- elastic 带有弹跳的到达最终状态
- bounce 在最终状态处弹跳几次 调用时,形如: .ease(“bounce”)
.delay()
指定延迟的时间,表示一定时间后才开始过渡,单位同样为毫秒。这个函数可以对整体指定延迟,也可以对个别指定延迟。
对整体指定时,如:
.transition()
.duration(1000)
.delay(500)
这样指定,将会延迟500毫秒播放一个1000毫秒的动画,故整个动画长度为1500毫秒。
.transition()
.duration(1000)
.delay(funtion(d,i){
return 200*i;
})
这样指定的话,假设有10个元素,那么第1个元素不延迟(因为 i = 0),那么第2个元素延迟200毫秒,第3个延迟400毫秒,依次类推….整个动画的长度为200*(10-1) + 1000 = 2800 毫秒。
D3布局介绍
布局是 D3 中一个十分重要的概念,可以理解成“制作常见图形的函数”,有了它制作各种相对复杂的图表就方便多了。
D3 与其它很多可视化工具不同,相对来说较底层,对初学者来说不太方便,但是一旦掌握了,就比其他工具更加得心应手。
布局的作用是:将不适合用于绘图的数据转换成了适合用于绘图的数据。
D3 总共提供了 12 个布局:
- 饼状图(Pie)
- 力导向图(Force)
- 弦图(Chord)
- 捆图(Bundle)
- 直方图(Histogram)
堆栈图(Stack)
集群图(Cluster)
打包图(Pack)
分区图(Partition)
树状图(Tree)
矩阵树图(Treemap)
层级图(Hierarchy)
12 个布局中,层级图(Hierarchy)不能直接使用。集群图、打包图、分区图、树状图、矩阵树图是由层级图扩展来的。如此一来,能够使用的布局是 11个(有 5 个是由层级图扩展而来)。这些布局的作用都是将某种数据转换成另一种数据,而转换后的数据是利于可视化的。
D3布局-饼状图
d3.layout.pie()
在第4章D3 创建SVG里我们讲过 圆弧图的制作方法,我们需要准备这样的数据才能画出圆弧:
var angle = 2*Math.PI;
var data = [
{
startAngle: 0,
endAngle: 0.2*angle
},
{
startAngle: 0.5*angle,
endAngle: 0.8*angle
}
];
如果都是自己去计算startAngle
或endAngle
,那太繁琐了,这时候就可以用到饼状布局来生成需要的数据。
*千万记住:布局不是要直接绘图,而是为了得到绘图所需的数据。*
饼图(Pie)的API说明
- pie.endAngle -取得或设置饼布局整体的结束角度。
- pie.padAngle - 取得或设置饼布局填充角度。
- pie.sort - 控制饼片段的顺时针方向的顺序。
- pie.startAngle - 取得或设置饼布局整体的开始角度。
- pie.value - 取得或设置值访问器函数。
- pie - 计算饼图或圆环图中弧的开始和结束角度。
绘制步骤
我们通过一个制作一个饼状图来讲解饼状布局。
数据
var dataset = [ 30 , 10 , 43 , 55 , 13 ];
数据转换
var pie = d3.layout.pie();
var data = pie(dataset);
通过饼状布局转换数据后,data数据被转换成如下形式:
绘制图形
创建svg容器
var width = 400;
var height = 400;
var outerRadius = 150; //外半径
var innerRadius = 0; //内半径,为0则中间没有空白
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
创建弧形svg生成器
var arc = d3.svg.arc() //弧生成器
.innerRadius(innerRadius) //设置内半径
.outerRadius(outerRadius); //设置外半径
定义颜色比例尺
var color = d3.scale.category10();
定义弧形路径容器
var arcs = svg.selectAll("g")
.data(data)
.enter()
.append("g")
.attr("transform", "translate(" + (width / 2) + "," + (width / 2) + ")");
生成弧形路径并填充颜色
arcs.append("path")
.attr("fill", function(d, i) {
return color(i);
})
.attr("d", function(d) {
return arc(d);
});
生成弧形文本并旋转
arcs.append("text")
.attr("transform", function(d) {
return "translate(" + arc.centroid(d) + ")";
})
.attr("text-anchor", "middle")
.text(function(d) {
return d.data;
});
arc.centroid(d)
能算出弧线的中心,所以用来放置文本最合适了。
要注意,text() 里返回的是 d.data,而不是d。因为被绑定的数据是对象,里面有d.startAngle、d.endAngle、d.data等,其中 d.data 才是转换前的整数的值。
D3布局-力学图
d3.layout.force()
力学图(也称为导向图,也有叫网络拓补图的,反正就是通过排斥得到关系远近的结构)在社交网络研究、信息传播途径等群体关系研究中应用非常广泛,它可以直观地反映群体与群体之间联系的渠道、交集多少,群体内部成员的联系强度等。
力学(Force)的API说明
- force.on - 监听布局位置的变化。(仅支持"start","step","end"三种事件)
- force.nodes - 获得或设置布局中的节点(node)阵列组。
- force.links - 获得或设置布局中节点间的连接(Link)阵列组。.
- force.size - 获取或设置布局的 宽 和 高 的大小.
- force.linkDistance - 获取或设置节点间的连接线距离.
- force.linkStrength - 获取或设置节点间的连接强度.
- force.friction - 获取或设置摩擦系数.
- force.charge - 获取或设置节点的电荷数.(电荷数决定结点是互相排斥还是吸引)
- force.gravity - 获取或设置节点的引力强度.
- force.theta - 获取或设置电荷间互相作用的强度.
- force.start - 开启或恢复结点间的位置影响.
- force.resume - 设置冷却系数为0.1,并重新调用start()函数.
- force.stop - 立刻终止结点间的位置影响.(等同于将冷却系数设置为0)
- force.alpha - 获取或设置布局的冷却系数.(冷却系数为0时,节点间不再互相影响)
- force.tick - 让布局运行到下一步.
- force.drag - 获取当前布局的拖拽对象实例以便进一步绑定处理函数.
一个基本的力学图有三个要素:力学结构,节点标记和节点连线。力学结构规范节点的行为,连线和节点显示节点信息和关系。
绘制步骤
我们通过一个制作一个力学图来讲解力学布局。
数据
var dataset = [
{name: "桂林", source: 0, target: 1},
{name: "广州", source: 1, target: 2},
{name: "厦门", source: 2, target: 3},
{name: "杭州", source: 3, target: 4},
{name: "上海", source: 4, target: 5},
{name: "青岛", source: 5, target: 6},
{name: "天津", source: 6, target: 0}
];
数据转换
var width = 400;
var height = 400;
var force = d3.layout.force()
.nodes(dataset) //指定节点数组
.links(dataset) //指定连线数组
.size([width, height]) //指定范围
.linkDistance(80) //指定连线长度
.charge([-400]); //相互之间的作用力
force.start(); //开始作用
通过转换数据后,数据被转换成如下形式:
转换后,数据对象里多了一些变量。其意义如下:
index:节点的索引号
px, py:节点上一个时刻的坐标
x, y:节点的当前坐标
weight:节点的权重
绘制图形
有了转换后的数据,就可以作图了。分别绘制三种图形元素:
line,线段,表示连线。
circle,圆,表示节点。
text,文字,描述节点。
生成svg容器
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", height);
生成连线
var svg_edges = svg.selectAll("line")
.data(dataset)
.enter()
.append("line")
.style("stroke", "#ccc")
.style("stroke-width", 1);
定义颜色比例尺
var color = d3.scale.category20();
生成圆形节点
var svg_nodes = svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("r", 20)
.style("fill", function(d, i) {
return color(i);
})
.call(force.drag); //使得节点能够拖动
生成节点文字
var svg_texts = svg.selectAll("text")
.data(dataset)
.enter()
.append("text")
.style("fill", "black")
.attr("dx", 20)
.attr("dy", 8)
.text(function(d) {
return d.name;
});
事件触发
由于力导向图是不断运动的,每一时刻都在发生更新,因此,必须不断更新节点和连线的位置。
力导向图布局 force 有一个事件tick,每进行到一个时刻,都要调用它,更新的内容就写在它的监听器里就好。
force.on("tick", function(){ //对于每一个时间间隔
//更新连线坐标
svg_edges.attr("x1",function(d){ return d.source.x; })
.attr("y1",function(d){ return d.source.y; })
.attr("x2",function(d){ return d.target.x; })
.attr("y2",function(d){ return d.target.y; });
//更新节点坐标
svg_nodes.attr("cx",function(d){ return d.x; })
.attr("cy",function(d){ return d.y; });
//更新文字坐标
svg_texts.attr("x", function(d){ return d.x; })
.attr("y", function(d){ return d.y; });
});
D3布局-弦图
d3.layout.chord()
弦图是一种用于描述节点之间联系的图表。
弦图(Chord)的API说明
- chord.chords - 取回计算的弦角度。
- chord.groups - 取回计算的分组角度。
- chord.matrix - 取得或设置布局需要的矩阵数据。
- chord.padding - 取得或设置弦片段间的角填充。
- chord.sortChords - 取得或设置用于弦的比较器(Z轴顺序)。
- chord.sortGroups - 取得或设置用于分组的比较器。
- chord.sortSubgroups - 取得或设置用于子分组的比较器。
绘制步骤
我们通过一个制作一个弦图来讲解弦布局。
[![][36]][36]
数据
var city_name = ["北京", "上海", "广州", "深圳", "香港"];
var population = [
[1000, 3045, 4567, 1234, 3714],
[3214, 2000, 2060, 124, 3234],
[8761, 6545, 3000, 8045, 647],
[3211, 1067, 3214, 4000, 1006],
[2146, 1034, 6745, 4764, 5000]
];
数据是一些城市名和一些数字,这些数字表示城市人口的来源,用表格表示如下:
北京 | 上海 | 广州 | 深圳 | 香港 | |
---|---|---|---|---|---|
北京 | 1000 | 3045 | 4567 | 1234 | 3714 |
上海 | 3214 | 2000 | 2060 | 124 | 3234 |
广州 | 8761 | 6545 | 3000 | 8045 | 647 |
深圳 | 3211 | 1067 | 3214 | 4000 | 1006 |
香港 | 2146 | 1034 | 6745 | 4764 | 5000 |
左边第一列是被统计人口的城市,上边第一行是被统计的来源城市
数据转换
var chord_layout = d3.layout.chord()
.padding(0.03) //节点之间的间隔
.matrix(population); //输入矩阵
var groups = chord_layout.groups();
var chords = chord_layout.chords();
population 经过转换后,实际上分成了两部分:groups 和 chords。前者是节点,后者是连线,也就是弦。
通过弦布局转换数据后,数据被转换成如下形式:
绘制图形
绘制容器
var width = 600;
var height = 600;
var innerRadius = width / 2 * 0.7;
var outerRadius = innerRadius * 1.1;
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
定义颜色比例尺
var color20 = d3.scale.category20();
创建圆弧SVG生成器
var outer_arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
绘制圆弧路径
var g_outer = svg.append("g");
g_outer.selectAll("path")
.data(groups)
.enter()
.append("path")
.style("fill", function(d) {
return color20(d.index);
})
.style("stroke", function(d) {
return color20(d.index);
})
.attr("d", outer_arc);
绘制弦图文字
g_outer.selectAll("text")
.data(groups)
.enter()
.append("text")
.each(function(d, i) {
d.angle = (d.startAngle + d.endAngle) / 2;
d.name = city_name[i];
})
.attr("dy", ".35em")
.attr("transform", function(d) {
return "rotate(" + (d.angle * 180 / Math.PI) + ")" +
"translate(0," + -1.0 * (outerRadius + 10) + ")" +
((d.angle > Math.PI * 3 / 4 && d.angle < Math.PI * 5 / 4) ? "rotate(180)" : "");
})
.text(function(d) {
return d.name;
});
创建弦图SVG生成器
var inner_chord = d3.svg.chord()
.radius(innerRadius);
绘制弦图路径
svg.append("g")
.attr("class", "chord")
.selectAll("path")
.data(chords)
.enter()
.append("path")
.attr("d", inner_chord)
.style("fill", function(d) {
return color20(d.source.index);
})
.style("opacity", 1)
.on("mouseover", function(d, i) {
d3.select(this)
.style("fill", "yellow");
})
.on("mouseout", function(d, i) {
d3.select(this)
.transition()
.duration(1000)
.style("fill", color20(d.source.index));
});
D3布局-集群图
d3.layout.cluster()
集群图是一种用于表示包含与被包含关系的图表。
集群图(Cluster)的API说明
- cluster.children - 取得或者设置子节点的访问器函数。
- cluster.links - 技术树节点之间的父子连接。
- cluster.nodeSize - 为每个节点指定固定的尺寸。
- cluster.nodes - 计算集群布局并返回节点数组。
- cluster.separation - 取得或设置邻接节点的分隔函数。
- cluster.size - 取得或设置布局的尺寸。
- cluster.sort - 取得或设置兄弟节点的比较器函数。
- cluster - cluster.nodes的别名。
绘制步骤
我们通过一个制作一个集群图来讲解集群布局。
数据
var dataset = {
"name": "中国",
"children": [{
"name": "浙江",
"children": [{"name": "杭州"}, {"name": "宁波"}, {"name": "温州"}, {"name": "绍兴"}]
},
{
"name": "广西",
"children": [{"name": "桂林"}, {"name": "南宁"}, {"name": "柳州"}, {"name": "防城港"}]
},
{
"name": "黑龙江",
"children": [{"name": "哈尔滨"}, {"name": "齐齐哈尔"}, {"name": "牡丹江"}, {"name": "大庆"}]
},
{
"name": "新疆",
"children": [{"name": "乌鲁木齐"}, {"name": "克拉玛依"}, {"name": "吐鲁番"}, {"name": "哈密"}]
}
]
}
数据转换
var width = 1000,
height = 500;
var cluster = d3.layout.cluster()
.size([width, height - 100]);
var nodes = cluster.nodes(dataset); // 节点
var links = cluster.links(nodes); // 连线
size() 设定尺寸,即转换后的各节点的坐标在哪一个范围内。
通过集群布局转换数据后,数据被转换成如下形式:
绘制图形
生成SVG容器
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(0,50)");
创建对角线SVG生成器
var diagonal = d3.svg.diagonal()
生成连线路径
var link = svg.selectAll(".link")
.data(links)
.enter()
.append("path")
.attr("class", "link")
.attr("d", diagonal);
生成圆形节点和文本
var node = svg.selectAll(".node")
.data(nodes)
.enter()
.append("g")
.attr("class", "node")
.attr("transform", function(d) {
return "translate(" + d.x + "," + d.y + ")";
})
node.append("circle")
.attr("r", 4.5);
node.append("text")
.attr("dx", function(d) {
return d.children ? -8 : 8;
})
.attr("dy", 3)
.style("text-anchor", function(d) {
return d.children ? "end" : "start";
})
.text(function(d) {
return d.name;
});
D3布局-树状图
d3.layout.tree()
树状图( Tree )用于表示层级、上下级、包含与被包含关系,其布局的用法与集群图几乎完全相同,本章简单通过两个图的对比来讲述两种图表之间的不同之处。
树状图
集群图
D3布局-打包图
d3.layout.pack()
打包图用于表示包含与被包含的关系,也可表示各对象的权重,通常用一圆套一圆来表示前者,用圆的大小来表示后者。
打包图(Pack)的API说明
- pack.children - 取得或设置子节点的访问器。
- pack.links - 计算树节点中的父子链接。
- pack.nodes - 计算包布局并返回节点数组。
- pack.padding - 指定布局间距(以像素为单位)
- pack.radius - 指定节点半径(不是由值派生来的)
- pack.size - 指定布局尺寸。
- pack.sort - 控制兄弟节点的遍历顺序。
- pack.value - 取得或设置用于圆尺寸的值访问器。
- pack - pack.nodes的别名。
绘制步骤
我们通过一个制作一个打包图来讲解打包布局。
[![][40]][40]
数据
var dataset = {
"name": "中国",
"children": [{
"name": "浙江",
"children": [{"name": "杭州"}, {"name": "宁波"}, {"name": "温州"}, {"name": "绍兴"}]
},
{
"name": "广西",
"children": [{"name": "桂林"}, {"name": "南宁"}, {"name": "柳州"}, {"name": "防城港"}]
},
{
"name": "黑龙江",
"children": [{"name": "哈尔滨"}, {"name": "齐齐哈尔"}, {"name": "牡丹江"}, {"name": "大庆"}]
},
{
"name": "新疆",
"children": [{"name": "乌鲁木齐"}, {"name": "克拉玛依"}, {"name": "吐鲁番"}, {"name": "哈密"}]
}
]
}
数据转换
var width = 500;
var height = 500;
var pack = d3.layout.pack()
.size([width, height])
.radius(20);
var nodes = pack.nodes(data);
var links = pack.links(nodes);
分别将数据转换成了结点 nodes 和 连线 links。
这里只需要用到结点数据,数据被转换成了如下:
绘制图形
生成SVG容器
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("transform", "translate(0,0)");
生成打包圆形SVG
svg.selectAll("circle")
.data(nodes)
.enter()
.append("circle")
.attr("fill", "rgb(31, 119, 180)")
.attr("fill-opacity", "0.4")
.attr("cx", function(d) {
return d.x;
})
.attr("cy", function(d) {
return d.y;
})
.attr("r", function(d) {
return d.r;
})
.on("mouseover", function(d, i) {
d3.select(this)
.attr("fill", "yellow");
})
.on("mouseout", function(d, i) {
d3.select(this)
.attr("fill", "rgb(31, 119, 180)");
});
生成节点文本
svg.selectAll("text")
.data(nodes)
.enter()
.append("text")
.attr("font-size", "10px")
.attr("fill", "white")
.attr("fill-opacity", function(d) {
if (d.depth == 2)
return "0.9";
else
return "0";
})
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
})
.attr("dx", -12)
.attr("dy", 1)
.text(function(d) {
return d.name;
});
D3布局-堆栈图
d3.layout.stack()
什么是堆栈图。
例如,有如下情况:
某公司,销售三种产品:个人电脑、智能手机、软件。
2005年,三种产品的利润分别为3000、2000、1100万。
2006年,三种产品的利润分别为1300、4000、1700万。
计算可得,2005年总利润为6100万,2006年为7000万。
如果要将2005年的总利润用柱形表示,那么应该画三个矩形,三个矩形堆叠在一起。这时候就有一个问题:每一个矩形的起始y坐标是多少?高应该是多少?
堆栈图布局(Stack Layout)能够计算二维数组每一数据层的基线,以方便将各数据层叠加起来,最适合用来处理以上这种场景。
绘制步骤
数据
var dataset = [
{
name: "PC" ,
sales: [
{ year:2005, profit: 3000 },
{ year:2006, profit: 1300 },
{ year:2007, profit: 3700 },
{ year:2008, profit: 4900 },
{ year:2009, profit: 700 }
]
},
{
name: "SmartPhone" ,
sales: [
{ year:2005, profit: 2000 },
{ year:2006, profit: 4000 },
{ year:2007, profit: 1810 },
{ year:2008, profit: 6540 },
{ year:2009, profit: 2820 }
]
},
{
name: "Software" ,
sales: [
{ year:2005, profit: 1100 },
{ year:2006, profit: 1700 },
{ year:2007, profit: 1680 },
{ year:2008, profit: 4000 },
{ year:2009, profit: 4900 }
]
}
];
数据转换
var stack = d3.layout.stack()
.values(function(d){ return d.sales; })
.x(function(d){ return d.year; })
.y(function(d){ return d.profit; });
var data = stack(dataset);
values方法指定需要转换的数据集;
x方法指定数据集中X轴的字段;
y方法指定数据集中Y轴的字段;
转换后的数据如下:
如图,sales的每一项都多了两个值:y0和y。y0即该层起始坐标,y是高度。x坐标有就是year,这些坐标都是在左上角为起点计算的,这点要注意。
绘制图形
生成SVG容器
var width = 700;
var height = 500;
var svg = d3.select('body')
.append('svg')
.attr('width', width)
.attr('height', height);
生成图表容器
var padding = {
top: 50,
right: 100,
bottom: 50,
left: 50
};
var charts = svg.append('g')
.attr('transform', 'translate(' + padding.left + ',' + padding.top + ')');
生成颜色比例尺
var colors = d3.scale.category10();
生成X轴和Y轴比例尺
var maxProfit = d3.max(data[data.length - 1].sales, function(d) {
return d.y0 + d.y;
});
var xScale = d3.scale.ordinal().domain([2005, 2006, 2007, 2008, 2009]).rangeBands([0, width - padding.left - padding.right], 0.3);
var yScale = d3.scale.linear().domain([0, maxProfit]).range([0, height - padding.top - padding.bottom]);
为每种类型数据创建容器
var stack = charts.selectAll('.stakc')
.data(data)
.enter()
.append('g')
.classed('stack', true)
.attr('fill', function(d, i) {
return colors(i);
})
为每种类型数据创建矩形图
var rect = stack.selectAll('rect')
.data(function(d) {
return d.sales;
})
.enter()
.append('rect')
.attr('x', function(d) {
return xScale(d.year);
})
.attr('y', function(d) {
return height - padding.top - padding.bottom - yScale(d.y0 + d.y);
})
.attr('width', xScale.rangeBand())
.attr('height', function(d) {
return yScale(d.y);
})
生成坐标轴
var xAxis = d3.svg.axis().scale(xScale);
var yAxis = d3.svg.axis().scale(yScale.range([height - padding.top - padding.bottom, 0])).orient('left');
charts.append('g')
.classed('x axis', true)
.attr('transform', 'translate(0,' + (height - padding.top - padding.bottom) + ')')
.call(xAxis)
charts.append('g')
.classed('y axis', true)
.call(yAxis)
生成数据类型图示
stack.append('circle')
.attr('cx', function(d) {
return width - padding.left - padding.right * 0.9
})
.attr('cy', function(d, i) {
return i * 50
})
.attr('r', 5)
stack.append('text')
.attr('x', function(d) {
return width - padding.left - padding.right * 0.8
})
.attr('y', function(d, i) {
return i * 50
})
.attr('dy', 5)
.text(function(d) {
return d.name;
})
D3布局-矩阵图
d3.layout.treemap()
矩阵图(Treemap)的API说明
- treemap.children - 取得或设置孩子访问器。
- treemap.links - 计算树节点中的父子链接。
- treemap.mode - 改变布局的算法。
- treemap.nodes - 计算矩形树布局并返回节点数组。
- treemap.padding - 指定父子之间的间距。
- treemap.round - 启用或者禁用四舍五入像素值。
- treemap.size - 指定布局的尺寸。
- treemap.sort - 控制兄弟节点的遍历顺序。
- treemap.sticky - 让布局对稳定的更新是粘滞的(sticky)。
- treemap.value - 取得或设置用来指定矩形树中矩形单元尺寸的值访问器。
- treemap - treemap.nodes的别名。
绘制步骤
数据
var dataset = {
"name": "中国",
"children":
[
{
"name": "浙江",
"children":
[
{"name":"杭州", "gdp":8343},
{"name":"宁波", "gdp":7128},
{"name":"温州", "gdp":4003},
{"name":"绍兴", "gdp":3620},
{"name":"湖州", "gdp":1803},
{"name":"嘉兴", "gdp":3147},
{"name":"金华", "gdp":2958},
{"name":"衢州", "gdp":1056},
{"name":"舟山", "gdp":1021},
{"name":"台州", "gdp":3153},
{"name":"丽水", "gdp":983}
]
},
{
"name": "广东",
"children":
[
{"name":"杭州", "gdp":8343},
{"name":"宁波", "gdp":7128},
{"name":"温州", "gdp":4003},
{"name":"绍兴", "gdp":3620},
{"name":"湖州", "gdp":1803},
{"name":"嘉兴", "gdp":3147},
{"name":"金华", "gdp":2958},
{"name":"衢州", "gdp":1056},
{"name":"舟山", "gdp":1021},
{"name":"台州", "gdp":3153},
{"name":"丽水", "gdp":983}
]
},
{
"name": "福建",
"children":
[
{"name":"杭州", "gdp":8343},
{"name":"宁波", "gdp":7128},
{"name":"温州", "gdp":4003},
{"name":"绍兴", "gdp":3620},
{"name":"湖州", "gdp":1803},
{"name":"嘉兴", "gdp":3147},
{"name":"金华", "gdp":2958},
{"name":"衢州", "gdp":1056},
{"name":"舟山", "gdp":1021},
{"name":"台州", "gdp":3153},
{"name":"丽水", "gdp":983}
]
}
]
}
矩阵布局用的还是标准的层级关系数据结构,name、children字段为必须,数据值字段可自定或用默认的value,本例中用的是gdp,所以在接下来的数据转换中需要用value方法指定数据值字段。
数据转换
var width = 800;
var height = 500;
var treemap = d3.layout.treemap()
.size([width, height])
.value(function(d) {
return d.gdp;
});
var nodes = treemap.nodes(dataset);
var links = treemap.links(nodes);
转换数据后,节点数组的输出结果如图所示。
其中,节点对象的属性包括:
parent:父节点
children:子节点
depth:节点的深度
value:节点的value值,由value访问器决定
x:节点的x坐标
y:节点的y坐标
dx:x方向的宽度
dy:y方向的宽度
本例中将不会用到liks数据对象;
绘制图形
生成SVG容器
var svg = d3.select('body').append('svg')
.attr('width',width)
.attr('height',height)
生成颜色比例尺
var color = d3.scale.category10();
为每种类型数据创建容器
var groups = svg.selectAll("g")
.data(nodes.filter(function(d) {
return !d.children;
}))
.enter()
.append("g");
为每种类型数据创建矩形图
var rects = groups.append("rect")
.attr("class", "nodeRect")
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
})
.attr("width", function(d) {
return d.dx;
})
.attr("height", function(d) {
return d.dy;
})
.style("fill", function(d, i) {
return color(d.parent.name);
});
生成文本
var texts = groups.append("text")
.attr("class", "nodeName")
.attr("x", function(d) {
return d.x;
})
.attr("y", function(d) {
return d.y;
})
.attr("dx", "0.5em")
.attr("dy", "1.5em")
.text(function(d) {
return d.name + " " + d.gdp;
});
D3布局-分区图(矩形)
d3.layout.partition()
分区图可以展示为方形或者圆形,从原理上来说它是树状结构的一种可视化展现形式,表示包含与被包含的关系;
分区图(Partition)的API说明
- partition.children - 取得或设置孩子访问器。
- partition.links - 计算树节点中的父子链接。
- partition.nodes - 计算分区布局并返回节点数组。
- partition.size - 指定布局的尺寸。
- partition.sort - 控制兄弟节点的遍历顺序。
- partition.value - 取得或设置用来指定圆尺寸的值访问器。
- partition - partition.nodes的别名。
绘制步骤
数据
var dataset = {
"name": "中国",
"children":
[
{
"name": "浙江",
"children":
[
{"name":"嘉兴", "gdp":3147},
{"name":"金华", "gdp":2958},
{"name":"衢州", "gdp":1056}
]
},
{
"name": "广东",
"children":
[
{"name":"杭州", "gdp":8343},
{"name":"绍兴", "gdp":3620},
{"name":"湖州", "gdp":1803}
]
},
{
"name": "福建",
"children":
[
{"name":"舟山", "gdp":1021},
{"name":"台州", "gdp":3153},
{"name":"丽水", "gdp":983}
]
}
]
}
数据转换
var width = 500;
var height = 500;
var partition = d3.layout.partition()
.sort(null) //设定内部的顶点的排序函数,null 表示不排序
.size([width,height]) //设定转换后图形的范围
.value(function(d) {
return d.gdp;
}); //设定表示分区大小的值
var nodes = partition.nodes(dataset);
var links = partition.links(nodes);
转换数据后,节点数组的输出结果如图所示。
其中,节点对象的属性包括:
x: 顶点的 x 坐标位置
y: 顶点的 y 坐标位置
dx: 顶点的宽度 dx
dy: 顶点的高度 dy
本例中将不会用到liks数据对象;
绘制图形
生成SVG容器
var svg = d3.select('body').append('svg')
.attr('width',width)
.attr('height',height)
生成颜色比例尺
var color = d3.scale.category10();
为节点数据创建容器
var rects = svg.selectAll("g")
.data(nodes)
.enter().append("g");
为节点数据创建矩形图
rects.append("rect")
.attr("x", function(d) { return d.x; }) // 顶点的 x 坐标
.attr("y", function(d) { return d.y; }) // 顶点的 y 坐标
.attr("width", function(d) { return d.dx; }) // 顶点的宽度 dx
.attr("height", function(d) { return d.dy; }) //顶点的高度 dy
.style("stroke", "#fff")
.style("fill", function(d) { return color(d.name); })
生成文本
rects.append("text")
.attr("transform",function(d,i){
return "translate(" + (d.x+5) + "," + (d.y+5) + ") rotate(90) ";
})
.text(function(d,i) {
return d.name;
});
D3布局-分区图(圆形)
d3.layout.partition()
上节中展示的是基本的方形分区图,这节我们要做一个圆形的分区图,圆形图跟方形图基本相同,只有布局函数的 size 函数和绘制图形的部分稍有区别;
绘制步骤
数据
var dataset = {
"name": "中国",
"children":
[
{
"name": "浙江",
"children":
[
{"name":"嘉兴", "gdp":3147},
{"name":"金华", "gdp":2958},
{"name":"衢州", "gdp":1056}
]
},
{
"name": "广东",
"children":
[
{"name":"杭州", "gdp":8343},
{"name":"绍兴", "gdp":3620},
{"name":"湖州", "gdp":1803}
]
},
{
"name": "福建",
"children":
[
{"name":"舟山", "gdp":1021},
{"name":"台州", "gdp":3153},
{"name":"丽水", "gdp":983}
]
}
]
}
数据转换
var width = 500;
var height = 500;
var radius = Math.min(width, height) / 2
var partition = d3.layout
.partition()
.sort(null) //设定内部的顶点的排序函数,null 表示不排序
.size([2 * Math.PI, radius * radius]) //第一个值为 2 * PI ,第二个值为圆半径的平方,暂时不去深究为什么这么做,只需记得这么用即可
.value(function(d) { return d.gdp; }); //设定表示分区大小的值
var nodes = partition.nodes(dataset);
var links = partition.links(nodes);
转换数据后,节点数组的输出结果如图所示。
其中,节点对象的属性包括:
x: 绕圆心方向的起始位置
y: 由圆心向外方向的结束位置
dx: 起始位置宽度
dy: 结束位置宽度
本例中将不会用到liks数据对象;
绘制图形
生成SVG容器
var svg = d3.select('body').append('svg')
.attr('class','axis')
.attr('width',width)
.attr('height',height)
.append('g')
.attr("transform", "translate(" + radius + "," + radius + ")");
生成颜色比例尺
var color = d3.scale.category20();
创建圆弧SVG生成器
var arc = d3.svg.arc()
.startAngle(function(d) { return d.x; })
.endAngle(function(d) { return d.x + d.dx; })
.innerRadius(function(d) { return Math.sqrt(d.y); })
.outerRadius(function(d) { return Math.sqrt(d.y + d.dy); });
为节点数据创建容器
var arcs = svg.selectAll("g")
.data(nodes)
.enter().append("g");
为节点数据创建圆弧路径
arcs.append("path")
.attr("d", arc)
.style("stroke", "#fff")
.style("fill", function(d) { return color(d.name); })
生成文本
arcs.append("text")
.attr("text-anchor","middle")
.attr("transform",function(d,i){
//第一个元素不动
if( i == 0 ) return ;
//其它的平移
return "translate(" + arc.centroid(d) + ")"
})
.text(function(d) { return d.name; });
D3布局-直方图
d3.layout.histogram()
直方图用于描述概率分布,假设有数组 a = [10, 11, 11.5, 12.5, 13, 15, 19, 20],现在把10~20的数值范围分为5段,即: 10~12, 12~14, 14~16, 16~18, 18~20 那么数组 a 的各数值都落在这几段区域的哪一部分呢?经过计算,可以知道,这5段分别具有的元素个数为: 3, 2, 1, 0 , 2 将这个用图形展示出来的,就是直方图。
分区图(Histogram)的API说明
- histogram.bins - 分隔数。
- histogram.frequency - 若值为 true,则统计的是个数;若值为 false,则统计的是概率
- histogram.range - 取得或设置值得范围。
- histogram.value - 取得或设置值访问器。
- histogram - 使用量化的箱计算数据的分布。
绘制步骤
数据
var dataset = [
0,1,2,3,
10,11,12,13,14,15,16,17,18,19,
20,21,
30,32,37,
40,45,46,49,
50,52,55,
60,69,
70,77,
80,81,
90,91,99,
100
];
数据转换
var partition = d3.layout.histogram()
.range([0,100])
.bins(10)
.frequency(true)
var data = partition(dataset);
转换后的数据如图所示。
数据参数的含义如下:
0,2,3….:落到此区间的数值
length:数值的个数
x: 区间的起始位置
dx: 区间的宽度
y: 落到此区间的数值的数量(如果 frequency 为 true);落到此区间的概率(如果 frequency 为 false)
绘制图形
生成SVG容器
var width = 500;
var height = 200;
var svg = d3.select('body').append('svg')
.attr('width',width)
.attr('height',height)
生成x轴和y轴比例尺
var x = d3.scale.linear().domain([0,100]).range([0,width])
var y = d3.scale.linear().domain([0,10]).range([0,height])
为区间数据创建容器
var bar = svg.selectAll('g').data(data)
.enter()
.append('g')
.attr('transform',function(d,i){
return 'translate('+ x(d.x) +','+ (height-y(d.y)) +')';
});
为区间数据创建矩形
bar.append('rect')
.attr('x',0)
.attr('width',function(d,i){
return x(d.dx)-1
})
.attr('height',function(d,i){
return y(d.y);
})
.attr('fill','steelblue');
生成区间值文本
bar.append('text')
.attr('x',5)
.attr('y',20)
.style({
'font-size':12,
'fill': '#fff'
})
.text(function(d,i){
return d[0]+' - '+d[d.length-1]
})
D3布局-捆图
d3.layout.bundle()
下图是航班查询网站全球航班雷达(FlightRadar24)显示的今日长三角地区的飞机飞行图:
图中我们能看到每时每刻有多少飞行在我们头上飞,有北京-杭州、杭州-日本、上海-北京的航班等等,如果你想知道哪个机场最繁忙,那就可以用到捆图来展现了;
捆图(Bundle)是D3布局中即简单又复杂的一种布局方式,说简单,它只有2个函数来实现数据转换,说复杂,它的实现需要结合其它层级布局来实现,如:集群图、打包图、分区图、树状图、矩阵树图。最常见的是与集群图一起使用,使用集群图布局计算节点的位置,再用捆图布局计算连线路径。也就是说,捆图布局只干一件事:计算连线的路径。
在网上能找到的捆图示例较少,可以参见官方的示例:http://bl.ocks.org/mbostock/1044242
本文我们用通俗的方式来讲解捆图的用法;
绘制步骤
数据
// 城市列表
var cities = {
name: '',
children: [
{name: "北京"},{name: "昆明"},
{name: "成都"},{name: "西安"},
{name: "上海"},{name: "杭州"},
{name: "广州"},{name: "桂林"},
{name: "太原"},{name: "美国"}
]
};
// 航班路线
var railway = [
{source: "美国", target: "上海"},
{source: "北京", target: "上海"},
{source: "北京", target: "广州"},
{source: "北京", target: "杭州"},
{source: "美国", target: "杭州"},
{source: "北京", target: "西安"},
{source: "北京", target: "成都"},
{source: "美国", target: "北京"},
{source: "北京", target: "太原"},
{source: "北京", target: "桂林"},
{source: "北京", target: "昆明"},
{source: "北京", target: "成都"},
{source: "上海", target: "杭州"},
{source: "昆明", target: "成都"},
{source: "西安", target: "太原"}
];
数据转换
var width = 500;
var height = 500;
// 需要一个方法将航班路线数据跟城市列表数据对接起来
var map = function ( nodes, links ){
var hash = [];
for(var i = 0; i < nodes.length; i++){
hash[nodes[i].name] = nodes[i];
}
var resultLinks = [];
for(var i = 0; i < links.length; i++){
resultLinks.push({
source: hash[ links[i].source ],
target: hash[ links[i].target ]
});
}
return resultLinks;
}
// 分别创建一个集群图布局和一个捆图布局,360表示角度,width/2 - 50 表示捆图圆半径
var cluster = d3.layout.cluster().size([360, width/2 - 50])
var bundle = d3.layout.bundle();
var nodes = cluster.nodes(cities);
var oLinks = map(nodes, railway);
var links = bundle(oLinks);
转换后的数据如图所示。
绘制图形
生成SVG容器
var svg = d3.select('body').append('svg')
.attr('width',width)
.attr('height',height)
创建径向折线SVG生成器
var line = d3.svg.line.radial()
.interpolate("bundle") //在line.interpolate()所预定义的插值模式中,有一种就叫做bundle,正是为捆图准备的。
.radius(function(d) { return d.y; })
.angle(function(d) { return d.x / 360 * 2 * Math.PI; });
生成捆图容器
var gBundle = svg.append("g")
.attr("transform", "translate(" + (width/2) + "," + (height/2) + ")");
颜色比例尺
var color = d3.scale.category20c();
生成连线路径
var link = gBundle.selectAll(".link")
.data(links)
.enter()
.append("path")
.classed('link',true)
.style({
'fill': 'none',
'stroke': 'rgba(0,0,0,0.5)',
'stroke-width': 5
})
.attr("d", line);
生成城市节点容器
var node = gBundle.selectAll('.node')
.data( nodes.filter(function(d) { return !d.children; }) ) //排除数据中最顶层的空数据
.enter()
.append('g')
.classed('node',true)
.attr("transform", function(d) {
return "rotate(" + (d.x- 90) + ") translate("+ d.y + ")"; //这里为什么是d.x- 90,我也还不明白,暂先用着,后续再深入了解
});
在节点容器中创建圆形和文本
node.append('circle')
.attr('r',20)
.attr('fill', function(d,i){
return color(i);
})
node.append('text')
.attr('dy','0.2em')
.style("text-anchor", "middle")
.text(function(d,i){
return d.name;
})
D3交互事件
前20节中,我们讲了各种静态D3图表的制作,好的图表应该是要与用户进行互动的,这节来讲讲D3的交互事件;
d3.event
selection选择器事件监听的写法跟平时我们用的jQuery类似,如:
selection.on(‘click’,function(d,i){
alert(‘ok’);
})
但请注意,点击事件触发的匿名函数参数不是我们常用的e或event,而是绑定在选择器上的数据d和数据的序列号i,这点非常重要。
注册事件
我们只要调用全局对象d3.event
,它是DOM事件,并实现了标准事件字段,像时间戳timeStamp和键代码keyCode,以及preventDefault()方法和的stopPropagation()方法。当然你可以使用原生事件的pageX and pageY;
在后面的在线示例中我们是这样使用d3.event的
mouseenter: function(d,i) {
var e = d3.event;
var t = d3.select(this)
t.attr('fill','#000');
text1.text('当前圆半径:'+ d.r +'px');
}
注意:通过直接调用d.r可以取到当前选择器上绑定的半径数据;
给选择的元素重复注册事件监听,新的事件被替换之前注册的事件。为注册多个监听器,可以跟一个可选的命名空间,如“click.foo”和“click.bar”。
注销事件
要删除事件监听器,只需要传递null给listener,如示例中的如下代码:
click: function(d,i) {
var t = d3.select(this);
t.on('click',null).remove();
}
d3.mouse(container)
通过使用全局方法d3.mouse可以方便的获取到当前事件对象的x/y坐标值,container为原生DOM对象;
d3.behavior.drag
拖动行为,通过监听拖拽事件控制SVG的交互行为,以下代码出自最后面的示例;
// 创建拖动行为
var drag = d3.behavior.drag()
.on("drag", function(d,i) {
// 监听拖拽事件
var e = d3.event;
var t = d3.select(this)
t.attr({
cx: e.x,
cy: e.y
});
});
circle1.call(drag);
d3.behavior.zoom
缩放行为,通过监听缩放事件控制SVG的交互行为,以下代码出自最后面的示例;
// 创建缩放行为
var zoom = d3.behavior.zoom()
.on("zoom", function(){
// 监听缩放事件
circle2.attr('transform',"translate(" + d3.event.translate + ")scale(" + d3.event.scale + ")");
});
circle2.call(zoom)
[查看在线演示][85]
D3几何 - 泰森多边形
d3.geom.voronoi()
在日常生活中有这样的场景,在一个城市中有很多的便利店,当你走在城市的任意地方,怎么才能找到离你了近的便利店呢?这种场景就适合用泰森多边形来解决。
通过一个示例来展示。
绘制步骤
数据
var dataset = [[18,162], [114,403], [261,98] ]; //假设这是三家便利店
数据转换
var width = 500;
var height = 500;
var voronoi = d3.geom.voronoi()
.clipExtent([[0, 0], [width, height]]);//从画面左上角到右下角为显示区间
var data = voronoi(dataset);
转换后的数据如图所示。
通过转换,将数据变成折线图的每个顶点坐标,这样我们就可以通过将顶点连接起来组成一个个不规则区域;
绘制图形
生成SVG容器
var svg = d3.select('body').append('svg')
.attr('width',width)
.attr('height',height)
颜色比例尺
var color = d3.scale.category10();
生成折线路径
svg.append("g").selectAll("path")
.data(data)
.enter()
.append("path")
.attr('fill',function(d,i){
return color(i);
})
.attr("d", function(d,i){
// 通过d.join("L")方法将每个顶点连接起来
return "M" + d.join("L") + "Z";
});
生成便利店位置
svg.append('g').selectAll('circle')
.data(dataset)
.enter()
.append('circle')
.attr('cx', function(d,i){
return d[0];
})
.attr('cy', function(d,i){
return d[1];
})
.attr('r', 5)
.attr('fill','#fff')
注意在生成便利店位置时,我们用的数据是原始的数据;
D3几何 - 四叉树
d3.geom.quadtree
四叉树也被称为Q树(Q-Tree)。四叉树广泛应用于图像处理、空间数据索引、2D中的快速碰撞检测、存储稀疏数据等,对游戏编程,这会很有用。
四叉树(Q-Tree)是一种树形数据结构。四叉树的定义是:它的每个节点下至多可以有四个子节点,通常把一部分二维空间细分为四个象限或区域并把该区域里的相关信息存入到四叉树节点中。
四叉树的每一个节点代表一个矩形区域(如上图黑色的根节点代表最外围黑色边框的矩形区域),每一个矩形区域又可划分为四个小矩形区域,这四个小矩形区域作为四个子节点所代表的矩形区域。
绘制步骤
通过一个示例来展示。
数据
var width = 500,
height = 500;
// 生成一份模拟数据,表示图上的10个点坐标
var dataset = d3.range(10).map(function() {
return [Math.random() * width, Math.random() * height];
});
数据转换
var quadtree = d3.geom.quadtree()
.extent([[0, 0], [width, height]])
var root = quadtree(dataset);
var data = [];
// 遍历四叉树中的每个节点,获得rect的坐标数据
root.visit(function(node, x1, y1, x2, y2) {
node.x1 = x1;
node.y1 = y1;
node.x2 = x2;
node.y2 = y2;
data.push(node);
});
通过quadtree转换后的数据如图所示:
转换后的数据都在nodes中,为了生成矩形图,我们需要用root.visit方法再次将数据做一层转换,最终用来生成SVG的数据如下:
绘制图形
生成SVG容器
var svg = d3.select('body').append('svg')
.attr('width',width)
.attr('height',height)
颜色比例尺
var color = d3.scale.category10();
生成矩形
var rect = svg.selectAll("rect")
.data(data)
.enter()
.append("rect")
.attr("fill", "none")
.attr('stroke',function(d,i){
return color(i)
})
.attr('stroke-width',1)
.attr("x", function(d) { return d.x1; })
.attr("y", function(d) { return d.y1; })
.attr("width", function(d) { return d.x2 - d.x1; })
.attr("height", function(d) { return d.y2 - d.y1; });
生成随机数据中的坐标点
var point = svg.selectAll("circle")
.data(dataset)
.enter()
.append("circle")
.attr("cx", function(d) { return d[0]; })
.attr("cy", function(d) { return d[1]; })
.attr("r", 3);
D3几何 - 多边形
d3.geom.polygon
polygon函数有3个用途:
- polygon.area() 用来计算多边形面积;
- polygon.centroid() 用来计算多边形中心坐标;
- polygon.clip(subject) 用来处理2个多边形重叠时,返回重叠区域多边形,用来剪切的多边形需要是逆时针方向且是凸多边形;
绘制步骤
通过一个示例来展示。
数据
// dataset0 用来做裁切主体,它的坐标需要是逆时针方向
var dataset0 = [[100,100],[100,300],[300,300],[300,100]];
var dataset1 = [[200,50],[400,50],[400,400],[200,400]];
数据转换
var data0 = d3.geom.polygon(dataset0);
var data1 = d3.geom.polygon(dataset1);
var data2 = data1.slice();//复制一个data1,用来展示重叠的区域
data0.clip(data2);
通过d3.geom.polygon(dataset1)转换后的数据data1,主要是给数据加了3个方法,如图所示:
裁切后的数据data2就是2个多边形的重叠区域:
绘制图形
生成SVG容器
var width = 500;
var height = 500;
var svg = d3.select('body').append('svg').attr({
width: width,
height: height
});
把3个多边形都输出看效果
svg.append('polygon').attr({
points: data0,
stroke: 'black',
fill: '#ccc'
})
svg.append('polygon').attr({
points: data1,
stroke: 'black',
fill: '#eee'
})
svg.append('polygon').attr({
points: data2,
stroke: 'red',
fill: 'yellow'
})
D3几何 - 凸包
d3.geom.hull
hull函数用于计算指定坐标点的外围边界,用一个场景来描述,某片草原上有100只羊在不同的位置,我们需要将羊群所在的草场圈起来,那只要将羊群最外围的羊通过一根线连起来就可以实现,同理,hull就是用来计算这个外围边界的方法;
绘制步骤
通过一个示例来展示。
数据,随机生成100只羊的坐标
var width = 500;
var height = 500;
var randomX = d3.random.normal(width / 2, 60);
var randomY = d3.random.normal(height / 2, 60);
var dataset = d3.range(100).map(function() {
return [randomX(), randomY()];
});
数据转换
var data = d3.geom.hull(dataset);
转换前的数据,如图所示:
转换后的数据,只保留了边界上的点,如图所示:
绘制图形
生成SVG容器
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
生成羊群边界路径,注意这里用的是datum方法,因为这里不需要遍历数据
var hull = svg.append("path")
.datum(data)
.attr({
d: function(d){
return 'M'+ d.join('L') +'Z';
},
fill: 'steelblue',
stroke: '#000',
});
生成羊群坐标点,注意这里的数据是原始坐标数据dataset
var circle = svg.selectAll('circle')
.data(dataset)
.enter()
.append('circle')
.attr({
r: 3,
fill: '#fff',
stroke: '#000',
cx: function(d){
return d[0];
},
cy: function(d){
return d[1];
}
})
D3地理地图
d3.geo
在学习D3如何制作地图前,我们需要了解下地图的数据格式geoJSON,GeoJSON是一种对各种地理数据结构进行编码的格式,基于Javascript对象表示法的地理空间信息数据交换格式。
本节我们将制作一幅中国地图,使用到的地图数据文件是从 Natural Earth 上的获取,经过提取后制作而成的,在本节入门篇中不细述地图数据的获取过程,将直接使用已经做好的数据,可通过以下链接下载查看中国地图数据。
绘制步骤
加载数据
d3.json('geoChina.json',function(error,data) {
…...
})
设置投影方式
var width = 1000;
var height = 800;
var projection = d3.geo.mercator()
.center([107, 31]) //center() 设定地图的中心位置,[107,31] 指的是经度和纬度
.scale(850) //scale() 设定放大的比例
.translate([width/2, height/2]);
创建一个地理路径生成器
var path = d3.geo.path()
.projection(projection);
绘制图形
生成SVG容器
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
生成地图路径并添加交互事件
var color = d3.scale.category20();
svg.selectAll("path")
.data( data.features )
.enter()
.append("path")
.attr("stroke","#000")
.attr("stroke-width",1)
.attr("fill", function(d,i){
return color(i);
})
.attr("d", path ) //使用地理路径生成器
.on("mouseover",function(d,i){
d3.select(this)
.attr("fill","yellow");
})
.on("mouseout",function(d,i){
d3.select(this)
.attr("fill",color(i));
});