D3学习系列
D3学习系列(一) 基础知识与柱形图绘制
D3学习系列(二) 弦图绘制
「前言」
网上关于桑基图的例子也有一些,但是对于初入门的新手并不友好、易懂。如果仅用百度搜索,资料更是少得可怜(这里感谢同事推荐shadowsocks进行科学上网●^●)。当然有些语句没有看懂,anyway先实现了再说~
「什么是桑基图」
桑基图(Sankey diagram),即桑基能量分流图,主要是用来描述能量、人口、经济等的流动情况。因1898年Matthew Henry Phineas Riall Sankey绘制的“蒸汽机的能源效率图”而闻名,此后便以其名字命名为“桑基图”。
桑基图主要关注能量、物料或资本等在系统内部的流动和转移情况。Sankey diagram的特点有:
- 起始流量和结束流量相同
- 在内部,不同的线条代表了不同的流量分流情况,它的宽度成比例地显示此分支占有的流量
- 节点不同的宽度代表了特定状态下的流量大小
在数据可视化中,桑基图有利于展现分类维度间的相关性,以流的形式呈现共享同一类别的元素数量。特别适合表达集群的发展,比如展示特定群体的人数分布等。我们可以欣赏下利用桑基图展示的可视化作品,太美了简直!
<img src="https://img.haomeiwen.com/i4762054/a86a3b3a710eaaaf.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" style="width: 500px"/>
<img src="https://img.haomeiwen.com/i4762054/108913244ee48d91.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" style="width: 500px"/>
「绘制桑基图」
绘制画布SVG
var margin = {top: 1, right: 1, bottom: 6, left: 1},
width = 1000 - margin.left - margin.right,
height = 650 - margin.top - margin.bottom;
var formatNumber = d3.format(",.0f"), // 数字转字符串 逗号分隔,0位小数点
format = function(d) {return formatNumber(d) + "m CHF";};
var color = d3.scale.category20();
var svg = d3.select("#chart")//ID选择器
.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 + ")");
在body中定义一个<p>
元素,令id="chart"
,画布SVG作用在元素<p>
中,保持有一定的margin。
定义桑基布局
var sankey = d3.sankey()
.nodeWidth(25) // 节点宽度
.nodePadding(20) // 矩形垂直方向的间距
.size([width,height]);
var path = sankey.link();
sankey.link()
函数应该是插件sankey.js中定义好的,目的是生成节点相对应的路径。
绑定数据
d3.json("http://benlogan1981.github.io/VerticalSankey/data/ubs.json", function(error, energy) {
sankey
.nodes(energy.nodes) // 绑定节点数据
.links(energy.links) // 绑定路径数据
.layout(32); // iterations ?
};
这里我们通过引用外部JS数据的方式来绑定,之后直接使用energy.
的方式调用,方式如下:d3.json("XXX.json", function(error, energy) {};
注意
如果不启动外部服务器,是没有办法加载外部数据的。由于Python自带的包可以建立简单的Web服务器,便直接用Python:
- 命令行中直接CD到准备做服务器的根目录下,输入命令:
python -m SimpleHTTPServer 8080
(这里使用的2.X版本,3.X版本稍有不同) - 然后就可以在浏览器中输入:
http://localhost:8080/路径
来访问服务器的资源
JSON数据
这里的JSON数据长这样:
{"nodes":[
{"name":"Wealth Management"},
{"name":"WMA"},
...
{"name":"Switzerland"}
],
"links":[
{"source":0,"target":5,"value":100},
{"source":1,"target":5,"value":1800},
....
{"source":4,"target":8,"value":400}
]}
nodes表示节点数据;links表示连线数据,其中source为起始节点,target表示终点节点,value为量的大小。关于.layout()
,查了相关资料,好像是跟计算出来的节点与路径数据的迭代次数(iterations)有关,但是调整参数值并没有发现什么变化。
绘制路径数据links
var link = svg.append("g").selectAll("path")
.data(energy.links)
.enter()
.append("path")
.attr("class","link")
.attr("d",path) // 路径链接已被sankey封装好
.style("stroke-width",function(d){
return Math.max(1,d.dy);
})
.style("stroke",function(d) {
return d.source.color = color(d.source.name.replace(/ .*/,""));
})
.sort(function(a,b){
return b.dy - a.dy;
})
;
link.append("title")
.text(function(d){
return d.source.name + "->" + d.target.name + "\n" + format(d.value) ;
});
stroke-width
参数表示links的宽度,返回的是1和dy中的最大值,但大部分情况都会返回dy。如果换成10(这样每根连线都是一样宽度),看一下效果就知道stroke-width
的作用了:
<img src="https://img.haomeiwen.com/i4762054/86d9661ae63af5b5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" style="width: 500px"/>
stroke
其实是描边的意思,感觉由于header部分设置过了fill:none
,所以才默认为填充的颜色。这里设置<font color="#FF0000">相同起始节点的连线具有相同的颜色</font>,确保达到你想要颜色分类的效果。同时对每条links添加title,鼠标悬停会显示相应内容。
绘制节点数据nodes
var node = svg.append("g").selectAll("g")
.data(energy.nodes)
.enter()
.append("g")
.attr('class', "node")
.attr('transform', function(d){
return "translate(" + d.x + "," + d.y + ")"; //节点的(x,y)坐标
});
node.append("rect")
.attr("height",sankey.nodeWidth())
.attr("width",function(d) { return d.dy; })
.style("fill",function(d) {
return d.color = color(d.name.replace(/ .*/, ""));
})
.style("stroke",function(d) {
return d3.rgb(d.color).darker(2);
})
.append("title")
.text(function(d) {
return d.name + "\n" + format(d.value) ;
});
node.append("text")
.attr("text-anchor","middle")
.attr("x",function(d) {
return d.dy/2;
})
.attr("y",function(d) {
sankey.nodeWidth() / 2;
})
.attr("dy","1em")
.text(function(d) { return d.name; })
.filter(function(d) {
return d.x < width / 2 ;
});
每个rect
元素的高度(height)相同,宽度(width)为相应的dy;stroke
把外框颜色设置成与rect
元素同样的颜色并加深,图片放大可以明显的看出效果),.text
添加鼠标悬停显示相应文字,效果如下:
<img src="https://img.haomeiwen.com/i4762054/68c465a079894385.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/124"/>
添加交互效果
1.用CSS控制悬浮样式,让连接线在鼠标悬停的时候高亮显示:
<style >
.link:hover {
stroke-opacity: .8;
}
</style>
2.节点添加拖动事件
- 定义一个拖动事件,这里仅限于水平方向的移动,移动之后重新布局并生成行的路径
function dragmove(d) {
d3.select(this)
.attr("transform","translate(" + (d.x = Math.max(0,Math.min(width - d.dy, d3.event.x))) + "," + d.y + ")") ;
sankey.relayout(); // 重新布局
link.attr("d",path);
}
- 对node节点添加事件
node.call(d3.behavior.drag() //这一段只知道大概是什么意思
.origin(function(d){
return d;
})
.on("dragstart",function() {
this.parentNode.appendChild(this);
})
.on("drag",dragmove)
);
好了,现在我们就可以做出首页第一张sankey图的效果了,最后再附上自己做的另一张横版sankey图(好像是由于sankey.js插件的问题,导致横竖排版)
源代码
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title></title>
<script src="js/d3.js" charset="utf-8"></script>
<script src="js/d3-sankey-1.js" charset="utf-8"></script>
<style >
body {
background-color: white;
}
#chart {
height: 650px; /* must at least match the svg, to place content after it!*/
}
.node rect {
cursor: move;
fill-opacity: .9;
shape-rendering: crispEdges;
}
.node text {
pointer-events: none;
text-shadow: 0 1px 0 #fff;
}
.link {
fill: none;
/*stroke: #000;*/
stroke-opacity: .5;
}
.link:hover {
stroke-opacity: .8;
}
</style>
</head>
<body>
<p id="chart"></p>
<script>
var margin = {top: 1, right: 1, bottom: 6, left: 1},
width = 1000 - margin.left - margin.right,
height = 650 - margin.top - margin.bottom;
var formatNumber = d3.format(",.0f"), // 数字转字符串 逗号分隔,0位小数点
format = function(d) {return formatNumber(d) + "m CHF";};
var color = d3.scale.category20();
var svg = d3.select("#chart")//ID选择器
.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 + ")")
;
// 布局
var sankey = d3.sankey()
.nodeWidth(25) // 节点宽度
.nodePadding(20) // 矩形垂直方向的间距
.size([width,height])
// .nodes(data.nodes)
// .links(data.links)
// .layout(3)
;
var path = sankey.link();
console.log(path);
d3.json("http://benlogan1981.github.io/VerticalSankey/data/ubs.json", function(error, energy) {
sankey
.nodes(energy.nodes)
.links(energy.links)
.layout(32);
var link = svg.append("g").selectAll("path")
.data(energy.links)
.enter()
.append("path")
.attr("class","link")
.attr("d",path) // 路径链接已被sankey封装好
.style("stroke-width",function(d){
return Math.max(1,d.dy);
})
// .style("stroke",function(d) {
// console.log(d.source.name.replace(/ .*/,""));
// })
.style("stroke",function(d) {
return d.source.color = color(d.source.name.replace(/ .*/,""));
})
.sort(function(a,b){
return b.dy - a.dy;
})
;
link.append("title")
.text(function(d){
return d.source.name + "->" + d.target.name + "\n" + format(d.value) ;
});
var node = svg.append("g").selectAll("g")
.data(energy.nodes)
.enter()
.append("g")
.attr('class', "node")
.attr('transform', function(d){
return "translate(" + d.x + "," + d.y + ")";
})
.call(d3.behavior.drag()
.origin(function(d){
return d;
})
.on("dragstart",function() {
this.parentNode.appendChild(this);
})
.on("drag",dragmove)
)
;
node.append("rect")
.attr("height",sankey.nodeWidth())
.attr("width",function(d) { return d.dy; })
.style("fill",function(d) {
return d.color = color(d.name.replace(/ .*/, ""));
})
.style("stroke",function(d) {
return d3.rgb(d.color).darker(2);
})
.append("title")
.text(function(d) {
return d.name + "\n" + format(d.value) ;
})
;
node.append("text")
.attr("text-anchor","middle")
.attr("x",function(d) {
return d.dy/2;
})
.attr("y",function(d) {
sankey.nodeWidth() / 2;
})
.attr("dy","1em")
.text(function(d) { return d.name; })
.filter(function(d) {
return d.x < width / 2 ;
})
;
function dragmove(d) {
d3.select(this).attr("transform","translate(" + (d.x = Math.max(0,Math.min(width - d.dy, d3.event.x))) + "," + d.y + ")") ;
sankey.relayout();
link.attr("d",path);
}
});
</script>
</body>
</html>
「参考资料」
【D3 Tips and Tricks v4.x】
【D3.js数据可视化实战】--(3)桑基图(sankey)的绘制
【USB 2015 Q1 Results】
网友评论