桑基图(sankey)的绘制

来源:互联网 发布:键盘记录软件手机 编辑:程序博客网 时间:2024/06/06 23:47
转载自:http://blog.csdn.net/tianxuzhang/article/details/49624701
  • 1 什么是桑基图
  • 2 用D3绘制简单的Sankey图
  • 3 添加文字
  • 4 圆形节点
  • 5 添加交互效果

  • 注:本文未经作者允许严禁转载和演绎

1 什么是桑基图?

桑基图流图 (flow diagram )的一种,用来描述能量,人口,经济等的流动情况。最早由爱尔兰人Matthew Henry Phineas Riall Sankey 提出。Sankey是一名船长也是工程师,1898年Sankey在土木工程师学会会报纪要的一篇关于蒸汽机能源效率的文章中首次推出了第一个能量流动图,后来被命名为Sankey图,中文音译为桑基图。

桑基图主要关注能量、物料或资本等在系统内部的流动和转移情况。起始流量和结束流量相同;在内部,不同的线条代表了不同的流量分流情况,它的宽度成比例地显示此分支占有的流量;节点不同的宽度代表了特定状态下的流量大小。

2 用D3绘制简单的Sankey图

D3提供一个Sankey插件,可以用来根据输入的节点-连接数据转换为桑基图布局所用的中间数据。这些中间数据可以很方便地结合SVG的矩形元素(rect)绘制节点,结合路径元素(path)就可以绘制链接。使得用D3绘制Sankey图很方便。图1展示了绘制效果。




 

图1 绘制简单的桑基图 

下面我们来详细介绍如果实现sankey图的绘制:

首先,绘制sankey图需要引入D3库以及Sankey插件库:

<code class="language-html hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"><<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">script</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">src</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"../d3.js"</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">charset</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"utf-8"</span>></span><span class="javascript" style="box-sizing: border-box;"><<span class="hljs-regexp" style="color: rgb(0, 136, 0); box-sizing: border-box;">/script ><script src="../</span>sankey.js<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"></span></span><span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"></<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">script</span>></span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li></ul>
然后,我们需要准备sankey图所需要的数据,这些数据按照一定的格式描述了网络中的节点-链接关系:
<code class="language-javascript hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// 节点和连接数据</span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">var</span> data = {  <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'nodes'</span>: [    {name: <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"0"</span>},    {name: <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"1"</span>},    {name: <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"2"</span>},    {name: <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"3"</span>},    {name: <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"4"</span>},    {name: <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"5"</span>},    {name: <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"6"</span>},    {name: <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"7"</span>},    {name: <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"8"</span>}  ],  <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'links'</span>: [    {source: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>, target: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">3</span>, value: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">10</span>},    {source: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>, target: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">4</span>, value: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">10</span>},    {source: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span>, target: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">4</span>, value: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">5</span>},    {source: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>, target: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">5</span>, value: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">5</span>},    {source: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">3</span>, target: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">6</span>, value: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">5</span>},    {source: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">3</span>, target: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">7</span>, value: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">5</span>},    {source: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">4</span>, target: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">7</span>, value: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">10</span>},    {source: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">4</span>, target: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">8</span>, value: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">5</span>},    {source: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">5</span>, target: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">8</span>, value: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">5</span>}  ]};</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li></ul>

这里的数据定义为JavaScript脚本中的变量,当然也可以由其他形式的数据转换,例如加载JSON数据文件等。但转换后的最终形式必须如上所示,即含有一个节点数组和一个一个链接数组。节点数组中每个元素是一个对象,含有一个name属性表示节点的名称;链接数组中每个对象都含有sourcetargetvalue三个属性,分别代表了来源节点的索引(从0开始),目标节点的索引,以及流量。 
接下来,我们可以配置sankey布局的属性值:

<code class="language-javascript hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// 定义桑基布局</span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">var</span> sankey = d3.sankey()        .nodeWidth(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">80</span>)         .nodePadding(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">40</span>)         .size([width, height])         .nodes(data.nodes)          .links(data.links)        .layout(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">3</span>);</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li></ul>

其中各个函数的解释如下:

  • nodeWidth表示节点水平宽度,这个属性可以用来设置矩形的宽度等;
  • nodePadding表示矩形的垂直方向的间距;
  • size表示整个sankey图占用的空间大小;
  • nodes加载如上所述的节点数组;
  • 同理links函数加载链接数组;
  • layout中的参数表示桑基布局用来优化流布局的时间。

定义完了sankey布局之后最重要的就可以得到一个链接路径生成器:

<code class="language-javascript hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// 路径数据生成器</span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">var</span> path = sankey.link();</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li></ul>
 

图2 由sankey布局转换后的节点和连接数据 

最后,就可以绑定数据绘制矩形节点和路径了:

<code class="language-javascript hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// 绘制连接数据</span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">var</span> links = svg.append(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"g"</span>).selectAll(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"path"</span>)            .data(data.links)            .enter()<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// 绑定节点数据</span><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">var</span> nodes = svg.append(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"g"</span>).selectAll(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">".node"</span>)                .data(data.nodes)                .enter();<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// 绘制连接线</span>links.append(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"path"</span>)    .attr({    fill: <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"none"</span>,   <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//填充色</span>            stroke: <span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span><span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d,i)</span>{</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> color(i); },  <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//描边色</span>        <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"stroke-opacity"</span>: <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0.5</span>,  <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//描边透明度</span>          d: path,  <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//路径数据</span>          id: <span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span><span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d,i)</span>{</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'link'</span> +i }  <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//ID</span>     })     .style(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"stroke-width"</span>, <span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span> <span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d)</span> {</span>  <span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">//连线的宽度</span>          <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">Math</span>.max(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>, d.dy);     });<span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// 绘制圆形节点   </span>nodes.append(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"circle"</span>)    .attr(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"transform"</span>,<span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span> <span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d)</span> {</span>          <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"translate("</span> + d.x + <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">","</span> + d.y + <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">")"</span>;       })        .attr(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"r"</span>, <span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span><span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d)</span> {</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> d.dy / <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span>; })        .attr(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"cx"</span>, <span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span><span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d)</span> {</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> d.dx/<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span>; })        .attr(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"cy"</span>, <span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span><span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d)</span> {</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> d.dy / <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span>; })        .style(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"fill"</span>, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"tomato"</span>)        .style(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"stroke"</span>, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"gray"</span>);</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li><li style="box-sizing: border-box; padding: 0px 5px;">14</li><li style="box-sizing: border-box; padding: 0px 5px;">15</li><li style="box-sizing: border-box; padding: 0px 5px;">16</li><li style="box-sizing: border-box; padding: 0px 5px;">17</li><li style="box-sizing: border-box; padding: 0px 5px;">18</li><li style="box-sizing: border-box; padding: 0px 5px;">19</li><li style="box-sizing: border-box; padding: 0px 5px;">20</li><li style="box-sizing: border-box; padding: 0px 5px;">21</li><li style="box-sizing: border-box; padding: 0px 5px;">22</li><li style="box-sizing: border-box; padding: 0px 5px;">23</li><li style="box-sizing: border-box; padding: 0px 5px;">24</li><li style="box-sizing: border-box; padding: 0px 5px;">25</li><li style="box-sizing: border-box; padding: 0px 5px;">26</li><li style="box-sizing: border-box; padding: 0px 5px;">27</li><li style="box-sizing: border-box; padding: 0px 5px;">28</li><li style="box-sizing: border-box; padding: 0px 5px;">29</li><li style="box-sizing: border-box; padding: 0px 5px;">30</li><li style="box-sizing: border-box; padding: 0px 5px;">31</li><li style="box-sizing: border-box; padding: 0px 5px;">32</li><li style="box-sizing: border-box; padding: 0px 5px;">33</li></ul>

3 添加文字

为了方便理解sankey图的流量的流动情况,我们还可以为节点和链接添加文字提示, 
给节点添加文字很简单,直接给之前绑定了数据并添加了占位符的nodes对象添加text元素并设置文本内容和坐标即可:

<code class="language-javascript hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// 绘制节点文本</span>  nodes.append(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"text"</span>)        .attr({            x: <span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span> <span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d)</span> {</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> d.x+sankey.nodeWidth() / <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span>; },            y: <span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span> <span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d)</span> {</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> d.y+d.dy / <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span>; }        })        .text(<span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span> <span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d)</span> {</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> d.name; }); </code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li></ul>

给链接添加文本稍微复杂一点,需要给text元素再添加textPath子元素,并用xlink:href属性和对应的链接路径关联起来,用startOffset属性可以设置文字相对于链接路径的位置,例如这里的50%表示居中显示。

<code class="language-javascript hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// 绘制连接文本</span>  links.append(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'text'</span>)        .append(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'textPath'</span>)        .attr(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'xlink:href'</span>, <span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span> <span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d,i)</span> {</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'#link'</span> + i; })        .attr(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'startOffset'</span>,<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'50%'</span>)        .text(<span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span> <span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d)</span> {</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'流量:'</span> + d.value; })</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li></ul>

添加完文字之后可以清晰地展现流量变化情况,如图3所示:




 

图3 为sankey图添加文字提示 

4 圆形节点

当然,也可以将sankey图中的矩形换成圆形。关键是在定义sankey布局的时候指定的节点宽度不能太大,例如本例指定为1像素:

<code class="language-javascript hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;">.nodeWidth(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">1</span>)</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li></ul>

然后,可以把圆平移到sankey布局计算得到的节点坐标处,并用节点的偏移量指定圆的圆心坐标和半径。

<code class="language-javascript hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// 绘制圆形节点   </span>nodes.append(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"circle"</span>)      .attr(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"transform"</span>,<span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span> <span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d)</span> {</span>            <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"translate("</span> + d.x + <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">","</span> + d.y + <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">")"</span>;      })      .attr(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"r"</span>, <span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span><span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d)</span> {</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> d.dy / <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span>; })      .attr(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"cx"</span>, <span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span><span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d)</span> {</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> d.dx / <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span>; })      .attr(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"cy"</span>, <span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span><span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d)</span> {</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> d.dy / <span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">2</span>; })      .style(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"fill"</span>, <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"tomato"</span>);</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li></ul>

还需要相应地调整下显示的尺寸,最后绘制效果如图4所示:




 

图4 绘制圆形节点的sankey图 

5 添加交互效果

有时候为了增加sankey图的易用性,可能需要给节点和连接线添加一些交互效果。下面介绍给连接线添加鼠标悬停事件,以及给节点添加拖动事件。

用CSS控制悬浮样式,让连接线在鼠标悬停的时候高亮显示:

<code class="language-HTML hljs xml has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"><<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">style</span> <span class="hljs-attribute" style="box-sizing: border-box; color: rgb(102, 0, 102);">type</span>=<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 136, 0);">"text/css"</span>></span><span class="css" style="box-sizing: border-box;">    <span class="hljs-tag" style="color: rgb(0, 0, 0); box-sizing: border-box;">path</span><span class="hljs-pseudo" style="color: rgb(0, 0, 0); box-sizing: border-box;">:hover</span><span class="hljs-rules" style="box-sizing: border-box;">{        <span class="hljs-rule" style="box-sizing: border-box;"><span class="hljs-attribute" style="box-sizing: border-box;">stroke-opacity</span>:<span class="hljs-value" style="box-sizing: border-box; color: rgb(0, 102, 102);"> <span class="hljs-number" style="box-sizing: border-box;">0.9</span></span></span>;    <span class="hljs-rule" style="box-sizing: border-box;">}</span></span></span><span class="hljs-tag" style="color: rgb(0, 102, 102); box-sizing: border-box;"></<span class="hljs-title" style="box-sizing: border-box; color: rgb(0, 0, 136);">style</span>></span></code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li></ul>

效果如图5所示: 




 

图5 连接线在鼠标悬停时高亮显示 




绘制节点时给节点添加拖动行为,并绑定拖动事件监听器。

<code class="language-javascript hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// 绘制矩形节点   </span>nodes.append(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"rect"</span>)          .attr({                x: <span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span> <span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d)</span> {</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> d.x; },                y: <span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span> <span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d)</span> {</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> d.y; },                height: <span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span> <span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d)</span> {</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> d.dy; },                width: sankey.nodeWidth(),                fill: <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"tomato"</span>          })          .call(d3.behavior.drag()                    .origin(<span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span><span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d)</span> {</span> <span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">return</span> d; })                    .on(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"drag"</span>, dragmove));</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li><li style="box-sizing: border-box; padding: 0px 5px;">11</li><li style="box-sizing: border-box; padding: 0px 5px;">12</li><li style="box-sizing: border-box; padding: 0px 5px;">13</li></ul>

这里的.origin(function(d) { return d; })是为了防止拖动时出现跳动,相应的拖动事件监听器为:

<code class="language-javascript hljs  has-numbering" style="display: block; padding: 0px; color: inherit; box-sizing: border-box; font-family: 'Source Code Pro', monospace;font-size:undefined; white-space: pre; border-radius: 0px; word-wrap: normal; background: transparent;"><span class="hljs-comment" style="color: rgb(136, 0, 0); box-sizing: border-box;">// 拖动事件响应函数</span><span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">function</span> <span class="hljs-title" style="box-sizing: border-box;">dragmove</span><span class="hljs-params" style="color: rgb(102, 0, 102); box-sizing: border-box;">(d)</span> {</span>     d3.select(<span class="hljs-keyword" style="color: rgb(0, 0, 136); box-sizing: border-box;">this</span>).attr({        <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"x"</span>: (d.x = <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">Math</span>.max(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>, <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">Math</span>.min(width - d.dx, d3.event.x))),        <span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">"y"</span>: (d.y = <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">Math</span>.max(<span class="hljs-number" style="color: rgb(0, 102, 102); box-sizing: border-box;">0</span>, <span class="hljs-built_in" style="color: rgb(102, 0, 102); box-sizing: border-box;">Math</span>.min(height - d.dy, d3.event.y)))     });     sankey.relayout();     paths.attr(<span class="hljs-string" style="color: rgb(0, 136, 0); box-sizing: border-box;">'d'</span>,path);}</code><ul class="pre-numbering" style="box-sizing: border-box; position: absolute; width: 50px; top: 0px; left: 0px; margin: 0px; padding: 6px 0px 40px; border-right-width: 1px; border-right-style: solid; border-right-color: rgb(221, 221, 221); list-style: none; text-align: right; background-color: rgb(238, 238, 238);"><li style="box-sizing: border-box; padding: 0px 5px;">1</li><li style="box-sizing: border-box; padding: 0px 5px;">2</li><li style="box-sizing: border-box; padding: 0px 5px;">3</li><li style="box-sizing: border-box; padding: 0px 5px;">4</li><li style="box-sizing: border-box; padding: 0px 5px;">5</li><li style="box-sizing: border-box; padding: 0px 5px;">6</li><li style="box-sizing: border-box; padding: 0px 5px;">7</li><li style="box-sizing: border-box; padding: 0px 5px;">8</li><li style="box-sizing: border-box; padding: 0px 5px;">9</li><li style="box-sizing: border-box; padding: 0px 5px;">10</li></ul>

也就是在拖动过程中重新计算矩形的坐标位置,并重新启动桑基布局,之后给路径元素绑定新的路径数据。这里拖动时文本如何移动就留给读者作为练习题,最后效果如图6所示: 




 

图6 可拖动的sankey图 

好了!桑基图就介绍到这了,感谢关注!

原创粉丝点击