美文网首页D3程序员
D3学习系列(三) 桑基图

D3学习系列(三) 桑基图

作者: 卷福不卷 | 来源:发表于2017-04-15 19:50 被阅读1711次

    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】

    相关文章

      网友评论

        本文标题:D3学习系列(三) 桑基图

        本文链接:https://www.haomeiwen.com/subject/tuwnattx.html