基于D3.js绘制的桑基图
桑基图(Sankey Diagram
)是一种特殊类型的流图,它通过宽度不等的箭头直观地表示不同元素之间的流量大小,通常用于展示能量、材料、成本或其他资源的转移。桑基图的特点如下:
- 流量可视化:桑基图通过箭头的宽度来表示流量的大小,宽度越大,流量越大。这使得比较不同路径的流量变得直观。
- 流向清晰:它能够清晰地展示从一个节点到另一个节点的流向,非常适合展示转换过程或分配情况。
- 层次关系:桑基图可以很好地表示层次结构,例如在能源转换、数据流或组织结构中,上层的输出可以作为下层的输入。
- 交叉比较:由于流量的大小直接体现在视觉上,用户可以轻松地比较不同路径或节点之间的流量差异。
本文使用D3.js(版本5)绘制桑基图,提供变隐现,节点拖动,节点相关性高亮功能。
老规矩,先看结果:
一、桑基图json
格式
在绘制桑基图之前,先了解桑基图的json
结构。包含两个节点,nodes
是节点名字通过id作为节点唯一标识,level
是节点等级;links
是用来记录连边的。source
为始边,target
为终边,value
为权重。
[{
"topic1": {
"nodes": [{
"id": "node1",
"name": "topic1",
"level": "1"
}
......
],
"links": [{
"source": "topic1",
"target": "李信",
"value": 8
}
......
]
}
}]
二、 桑基图样式
<style>
html,
body {
width: 100%;
height: 100%;
}
/* svg */
.svgsankey {
background-color: black;
}
/* svg容器 */
.sankey {
width: 100%;
height: 100%;
}
/* 节点矩形 */
.node rect {
cursor: move;
fill-opacity: .9;
shape-rendering: crispEdges;
}
/* 节点文本 */
.node text {
pointer-events: none;
fill: rgb(131, 19, 19);
/* text-shadow: 0 1px 0 #fff;*/
}
/* 链接 */
.selected .link {
stroke-opacity: .5;
}
</style>
三、基础配置
主要设置画布长宽,颜色比例尺,透明度变化前后的值。
const width = 800;
const height = 600;
//边距
let margin = { top: 10,right: 10,bottom: 10,left: 10};
let opacityLow = 1, //新透明度
opacityDefault = 0.3; //默认透明度
let maxLevel = 1;
// 定义颜色比例尺
let color = d3.scaleOrdinal(["rgb(255, 138, 128)", "rgb(255, 255, 0)", "rgb(234, 128, 252)",
"rgb(29, 233, 182)", "rgb(130, 177, 255)", "rgb(132, 255, 255)",
"rgb(167, 255, 235)"
])
添加画布
let svg = d3.select(".sankey")
.append("svg")
.attr("class", "svgsankey")
.attr("viewBox", "0 0 820 620")
.attr("width", "100%")
.attr("height", "100%")
let g = svg.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
桑基图配置。
// 设置sankey图表属性
let sankey = d3.sankey()
.nodeWidth(20)
.nodePadding(10)
.size([width - 10, height - 10]);
let path = sankey.link();
读取数据后,进行数据处理,nodeMap
包含的内容格式为 {节点名:节点名对应的数据}
d3.json("json/data.json").then(function(alldata) {
let graph = alldata[0]["topic1"]
let nodeMap = {};
// 遍历节点并找到最大级别
graph.nodes.forEach(function(node) {
node.hidden = false;// 假设在节点数据中添加了一个hidden属性,默认为false
nodeMap[node.name] = node;
if (node.level > maxLevel) {
maxLevel = node.level;
}
});
/*处理成桑基图需要的链接格式*/
graph.links = graph.links.map(function(x) {
return {
source: nodeMap[x.source],
target: nodeMap[x.target],
value: x.value
};
});
})
把节点数据和链接数据应用到桑基图上。
sankey.nodes(graph.nodes)
.links(graph.links)
.layout(35);
此时打开页面会发现什么都没有,这是因为桑基图只有数据,并没有添加对应的svg元素。
四、 绘制桑基图链接边
边样式配置。
//添加链接
let link = g.append("g")
.selectAll(".link")
.data(graph.links)
.enter()
.append("path")
.attr("class", "link")
.attr('linkNodes', (d) => d.source.id + '-' + d.target.id)
.style("fill", function(d) {
d.source.color = color(d.source.name);
return "none"
})
.style("stroke", function(d, i) {
return d3.rgb(d.source.color); //颜色比例尺
})
.attr("d", path)
.style("stroke-width", function(d) {
return Math.max(1, d.dy);
})
.style("opacity", opacityDefault)
绘制结果:
五、绘制桑基图节点
这里使得节点组下,每组g
元素下面都包含一个rect
标签,同时添加rect
样式配置。
// 添加节点
let node = g.append("g")
.selectAll(".node")
.data(graph.nodes)
.enter()
.append("g")
.attr("class", "node")
.attr("transform", function(d) {
// return d.level!=="3"? "translate(" + d.x + "," + d.y + ")":"translate(" + (d.x-20)+ "," + d.y + ")"
return "translate(" + d.x + "," + d.y + ")";
})
let rect = node
.append("rect")
.attr('index', (d) => d.id)
.attr('linkNodes', (d) => {
const nextNodes = d.sourceLinks.map((link) => link.target.id).join('');
const prevNodes = d.targetLinks.map((link) => link.source.id).join('');
return nextNodes + d.id + prevNodes;
})
.attr("height", function(d) {
return d.dy;
})
.attr("width", sankey.nodeWidth())
.style("fill", function(d) {
return d.color = color(d.name);
})
.style("stroke", function(d) {
return d3.rgb(d.color).darker(10);
})
.style('opacity', opacityDefault)
绘制结果:
下图中,鼠标移动或点击将不会有交互,因为还没有添加鼠标事件。
5.1 给节点添加标题
给每个添加节点的标题,保证第三级节点的名字在节点左边,这是为了防止文字超出svg显示范围。
node.append("text")
.attr("x", -6)
.attr("y", d => d.dy / 2)
.attr("dy", ".35em")
.attr("text-anchor", "end")
.attr("transform", null)
.style("fill", "white")
.text(d => d.name)
.filter(d => d.x < width / 2)
.style("font-size", function(d) {
return d.level == 3 ? "9px" : "12px"
})
添加标题后:
5.2 给节点添加拖拽功能
给节点添加拖拽功能。
node.call(d3.drag()
.on("drag", dragmove)
);
// 移动节点的功能
function dragmove(d) {
d3.select(this)
.attr("transform",
"translate(" + (
d.x = Math.max(0, Math.min(width - d.dx, d3.event.x))
) + "," + (
d.y = Math.max(0, Math.min(height - d.dy, d3.event.y))
) + ")");
sankey.relayout();
link.attr("d", path);
}
移动节点:
5.3 给链接添加隐现功能
给节点添加隐现功能,隐藏被点击节点的后续链接。
node.on('click', function(d) {
// 切换当前节点的hidden状态
d.hidden = !d.hidden;
let clickcolor = d3.select(this)
.select("rect")
.style("fill")
//判断当前节点的颜色
if (clickcolor === "red") {
d3.select(this).select("rect")
.style("fill", function(d) {
return d3.rgb(d.color);
});
} else {
d3.select(this)
.select("rect")
.style("fill", function(d) {
return "red"
// return d3.rgb(d.color);
});
}
// 更新边的样式
g.selectAll('.link')
.style('stroke-opacity', function(link) {
// 如果边是从当前节点出发的,根据hidden状态设置透明度
if (link.source.id === d.id) {
return d.hidden ? 0 : 1;
}
// 如果边不是从当前节点出发的,检查边的源节点是否隐藏,如果隐藏则不透明度为0,否则为1
return link.source.hidden ? 0 : 1;
});
});
结果展示:
六、总结
本次绘制的桑基图,对于不追求高交互的用户,基本满足使用。提供了全网首发的链接隐藏功能。
不包括相关边和节点高亮功能。其它功能会在付费文章中更新。