神刀安全网

D3学习系列(三) 桑基图


D3学习系列

D3学习系列(一) 基础知识与柱形图绘制
D3学习系列(二) 弦图绘制

D3学习系列(三) 桑基图

「前言」

网上关于桑基图的例子也有一些,但是对于初入门的新手并不友好、易懂。如果仅用百度搜索,资料更是少得可怜(这里感谢同事推荐shadowsocks进行科学上网●^●)。当然有些语句没有看懂,anyway先实现了再说~

「什么是桑基图」

桑基图(Sankey diagram),即桑基能量分流图,主要是用来描述能量、人口、经济等的流动情况。因1898年Matthew Henry Phineas Riall Sankey绘制的“蒸汽机的能源效率图”而闻名,此后便以其名字命名为“桑基图”。

桑基图主要关注能量、物料或资本等在系统内部的流动和转移情况。Sankey diagram的特点有:

  • 起始流量和结束流量相同
  • 在内部,不同的线条代表了不同的流量分流情况,它的宽度成比例地显示此分支占有的流量
  • 节点不同的宽度代表了特定状态下的流量大小

在数据可视化中,桑基图有利于展现分类维度间的相关性,以流的形式呈现共享同一类别的元素数量。特别适合表达集群的发展,比如展示特定群体的人数分布等。我们可以欣赏下利用桑基图展示的可视化作品,太美了简直!

D3学习系列(三) 桑基图

D3学习系列(三) 桑基图

D3学习系列(三) 桑基图

「绘制桑基图」

绘制画布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的作用了:

D3学习系列(三) 桑基图

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添加鼠标悬停显示相应文字,效果如下:

D3学习系列(三) 桑基图

添加交互效果

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插件的问题,导致横竖排版)

D3学习系列(三) 桑基图

源代码

<!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】

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » D3学习系列(三) 桑基图

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址