D3 Sankey图链接未覆盖节点全高的调整需求(不修改数据)
解决D3.js Sankey图节点与链接不匹配问题
问题场景
使用D3.js构建Sankey图时,当节点的总输入与总输出数值不相等时(例如示例中节点B输入为2、输出为1),默认生成的链接无法完全匹配节点高度,导致视觉上的不协调。需求是不修改原始输入数据,让节点高度由关联的链接值决定,同时保证链接与对应输入/输出节点完全匹配。
示例原始代码
JavaScript 代码
// set the dimensions and margins of the data var margin = { top: 10, right: 10, bottom: 10, left: 10 }, width = 300 - margin.left - margin.right, height = 300 - margin.top - margin.bottom; // append the svg object to the body of the page var svg = d3.select("#graph").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 + ")"); // Set the sankey diagram properties var sankey = d3.sankey() .nodeWidth(10) .nodePadding(10) .size([width, height]); data = { "nodes": [ { "node": 0, "name": "A" }, { "node": 1, "name": "B" }, { "node": 2, "name": "C" } ], "links": [ { "source": 0, "target": 1, "value": 2 }, { "source": 1, "target": 2, "value": 1 } ] } // Constructs a new Sankey generator with the default settings. sankey .nodes(data.nodes) .links(data.links) .layout(1); // add in the links svg.append("g") .selectAll(".link") .data(data.links) .enter() .append("path") .attr("class", "link") .attr("d", sankey.link()) .style("stroke-width", function (d) { return Math.max(1, d.dy); }) // add in the nodes var node = svg.append("g") .selectAll(".node") .data(data.nodes) .enter() .append("g") .attr("class", "node") .attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; }) // add the rectangles for the nodes node.append("rect") .attr("height", function (d) { return d.dy; }) .attr("width", sankey.nodeWidth()) // add in the title for the nodes node.append("text") .attr("x", -6) .attr("y", function (d) { return d.dy / 2; }) .attr("dy", ".35em") .attr("text-anchor", "end") .attr("transform", null) .text(function (d) { return d.name; }) .filter(function (d) { return d.x < width / 2; }) .attr("x", 6 + sankey.nodeWidth()) .attr("text-anchor", "start")
HTML 代码
<body> <style> .link { fill: none; stroke: #000; stroke-opacity: .2; } .node rect { fill: #ccc; stroke: #333; } </style> <div id="graph"></div> <script src="https://d3js.org/d3.v4.min.js"></script> <script src="https://cdn.jsdelivr.net/gh/holtzy/D3-graph-gallery@master/LIB/sankey.js"></script> <script src="test.js"></script> </body>
解决方法
核心思路是手动计算节点的高度和链接的位置,替代默认布局中不合理的分配逻辑,具体步骤如下:
- 计算节点的总流入/流出值:为每个节点统计所有输入链接的总和与输出链接的总和,取最大值作为节点的基准高度值,确保能容纳所有关联链接。
- 按比例分配节点高度:基于所有节点的基准高度值总和,计算每个节点在可用画布高度中的实际占比,确定节点的
dy(高度)和y(纵坐标)。 - 调整链接的位置与高度:根据链接值占对应节点总流入/流出的比例,计算链接在节点上的起始位置和高度,保证链接与节点完全匹配。
修改后的完整JavaScript代码
// set the dimensions and margins of the data var margin = { top: 10, right: 10, bottom: 10, left: 10 }, width = 300 - margin.left - margin.right, height = 300 - margin.top - margin.bottom; // append the svg object to the body of the page var svg = d3.select("#graph").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 + ")"); // Set the sankey diagram properties var sankey = d3.sankey() .nodeWidth(10) .nodePadding(10) .size([width, height]); data = { "nodes": [ { "node": 0, "name": "A" }, { "node": 1, "name": "B" }, { "node": 2, "name": "C" } ], "links": [ { "source": 0, "target": 1, "value": 2 }, { "source": 1, "target": 2, "value": 1 } ] } // 1. 计算每个节点的总流入、总流出,取最大值作为节点基准高度 data.nodes.forEach(node => { node.totalIn = data.links.filter(l => l.target.node === node.node).reduce((sum, l) => sum + l.value, 0); node.totalOut = data.links.filter(l => l.source.node === node.node).reduce((sum, l) => sum + l.value, 0); node.nodeValue = Math.max(node.totalIn, node.totalOut); }); // 用sankey布局计算节点的x位置(保留默认的水平分布逻辑) sankey .nodes(data.nodes) .links(data.links); // 2. 按比例计算节点的实际高度和纵坐标 const totalValue = data.nodes.reduce((sum, n) => sum + n.nodeValue, 0); const availableHeight = height - (data.nodes.length - 1) * sankey.nodePadding(); let currentY = 0; data.nodes.forEach(node => { node.dy = (node.nodeValue / totalValue) * availableHeight; node.y = currentY; currentY += node.dy + sankey.nodePadding(); }); // 3. 调整链接的位置和高度,匹配对应节点 data.links.forEach(link => { // 处理源节点的链接位置 const sourceTotal = link.source.totalOut; let sourceY = link.source.y; // 累加当前链接之前的所有输出链接高度,确定起始位置 data.links.filter(l => l.source.node === link.source.node && l !== link).forEach(l => { sourceY += (l.value / sourceTotal) * link.source.dy; }); link.y0 = sourceY; link.dy = (link.value / sourceTotal) * link.source.dy; // 处理目标节点的链接位置 const targetTotal = link.target.totalIn; let targetY = link.target.y; // 累加当前链接之前的所有输入链接高度,确定起始位置 data.links.filter(l => l.target.node === link.target.node && l !== link).forEach(l => { targetY += (l.value / targetTotal) * link.target.dy; }); link.y1 = targetY; }); // add in the links svg.append("g") .selectAll(".link") .data(data.links) .enter() .append("path") .attr("class", "link") .attr("d", sankey.link()) .style("stroke-width", function (d) { return Math.max(1, d.dy); }) // add in the nodes var node = svg.append("g") .selectAll(".node") .data(data.nodes) .enter() .append("g") .attr("class", "node") .attr("transform", function (d) { return "translate(" + d.x + "," + d.y + ")"; }) // add the rectangles for the nodes node.append("rect") .attr("height", function (d) { return d.dy; }) .attr("width", sankey.nodeWidth()) // add in the title for the nodes node.append("text") .attr("x", -6) .attr("y", function (d) { return d.dy / 2; }) .attr("dy", ".35em") .attr("text-anchor", "end") .attr("transform", null) .text(function (d) { return d.name; }) .filter(function (d) { return d.x < width / 2; }) .attr("x", 6 + sankey.nodeWidth()) .attr("text-anchor", "start")
效果说明
修改后:
- 节点A的高度由输出值2决定,链接完全覆盖节点高度
- 节点B的高度由输入值2决定,输出链接仅占节点高度的1/2,与输出值匹配
- 节点C的高度由输入值1决定,链接完全覆盖节点高度
内容的提问来源于stack exchange,提问作者rikyeah
相关产品推荐
相关产品推荐

