You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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>

解决方法

核心思路是手动计算节点的高度和链接的位置,替代默认布局中不合理的分配逻辑,具体步骤如下:

  1. 计算节点的总流入/流出值:为每个节点统计所有输入链接的总和与输出链接的总和,取最大值作为节点的基准高度值,确保能容纳所有关联链接。
  2. 按比例分配节点高度:基于所有节点的基准高度值总和,计算每个节点在可用画布高度中的实际占比,确定节点的dy(高度)和y(纵坐标)。
  3. 调整链接的位置与高度:根据链接值占对应节点总流入/流出的比例,计算链接在节点上的起始位置和高度,保证链接与节点完全匹配。

修改后的完整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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.07 04:46:15