首页
/ 彻底解决桑基图节点重叠:ECharts布局算法深度优化指南

彻底解决桑基图节点重叠:ECharts布局算法深度优化指南

2026-02-05 04:22:12作者:邬祺芯Juliet

你是否也曾为桑基图(Sankey Diagram)中的节点重叠问题而头疼?当数据量增大时,节点相互挤压、标签重叠,不仅影响美观,更降低了数据可读性。本文将深入剖析ECharts桑基图布局算法的工作原理,提供3种切实可行的优化方案,帮助你彻底解决节点重叠问题。

读完本文你将获得:

  • 理解ECharts桑基图布局算法的核心原理
  • 掌握3种减少节点重叠的实用优化技巧
  • 学会根据数据特征选择最佳布局参数
  • 获得可直接套用的代码示例

桑基图布局算法原理解析

ECharts的桑基图布局算法主要在src/chart/sankey/sankeyLayout.ts文件中实现,采用了"分层布局"思想,主要分为三个步骤:

graph TD
    A[计算节点值] --> B[计算节点广度/水平位置];
    B --> C[计算节点深度/垂直位置];
    C --> D[计算边的深度];

节点值计算

布局算法首先通过computeNodeValues函数计算每个节点的值,取节点的入边总和、出边总和与节点自身值三者中的最大值:

function computeNodeValues(nodes: GraphNode[]) {
    zrUtil.each(nodes, function (node) {
        const value1 = sum(node.outEdges, getEdgeValue);
        const value2 = sum(node.inEdges, getEdgeValue);
        const nodeRawValue = node.getValue() as number || 0;
        const value = Math.max(value1, value2, nodeRawValue);
        node.setLayout({value: value}, true);
    });
}

节点广度计算

computeNodeBreadths函数中,算法使用拓扑排序(Kahn算法)确定节点的水平位置,这一步决定了节点在哪一列:

// 使用拓扑排序计算节点初始位置
while (zeroIndegrees.length) {
    for (let idx = 0; idx < zeroIndegrees.length; idx++) {
        const node = zeroIndegrees[idx];
        // 设置节点深度(水平位置)
        node.setLayout({depth: isItemDepth ? item.depth : x}, true);
        // ...处理节点的出边
    }
    ++x;
    zeroIndegrees = nextTargetNode;
    nextTargetNode = [];
}

节点深度计算

节点垂直位置的计算是布局的核心,在computeNodeDepths函数中实现,采用Gauss-Seidel迭代法调整节点位置以减少交叉:

// 使用Gauss-Seidel迭代法优化节点位置
for (let alpha = 1; iterations > 0; iterations--) {
    alpha *= 0.99;  // 松弛因子,逐渐减小调整幅度
    relaxRightToLeft(nodesByBreadth, alpha, orient);  // 从右向左调整
    resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);  // 解决碰撞
    relaxLeftToRight(nodesByBreadth, alpha, orient);  // 从左向右调整
    resolveCollisions(nodesByBreadth, nodeGap, height, width, orient);  // 解决碰撞
}

节点重叠问题的根本原因分析

通过阅读src/chart/sankey/sankeyLayout.ts源码,我们发现节点重叠主要源于以下几个方面:

1. 初始布局算法的局限性

ECharts桑基图使用initializeNodeDepth函数进行初始布局,简单地将同层节点按顺序排列:

zrUtil.each(nodesByBreadth, function (nodes) {
    zrUtil.each(nodes, function (node, i) {
        const nodeDy = node.getLayout().value * minKy;
        if (orient === 'vertical') {
            node.setLayout({x: i}, true);  // 简单按索引排列
            node.setLayout({dx: nodeDy}, true);
        }
        // ...
    });
});

这种简单排列在节点数量多时极易导致重叠。

2. 碰撞检测与解决机制不完善

虽然算法中有resolveCollisions函数尝试解决节点碰撞,但它仅在同一层内进行调整,没有考虑跨层节点的相互影响:

function resolveCollisions(
    nodesByBreadth: GraphNode[][],
    nodeGap: number,
    height: number,
    width: number,
    orient: LayoutOrient
) {
    const keyAttr = orient === 'vertical' ? 'x' : 'y';
    zrUtil.each(nodesByBreadth, function (nodes) {
        // 仅在当前层内解决碰撞
        nodes.sort(function (a, b) {
            return a.getLayout()[keyAttr] - b.getLayout()[keyAttr];
        });
        // ...
    });
}

3. 迭代次数不足

布局算法的迭代优化次数由layoutIterations参数控制,默认值可能不足以收敛到最优解:

const iterations = filteredNodes.length !== 0 ? 0 : seriesModel.get('layoutIterations');

减少节点重叠的优化方案

针对以上分析,我提出三种切实可行的优化方案:

方案一:优化节点间距与迭代次数

最简单直接的方法是调整nodeGap(节点间距)和layoutIterations(布局迭代次数)参数。ECharts默认的节点间距为8,迭代次数为32。

option = {
    series: [{
        type: 'sankey',
        nodeWidth: 20,        // 节点宽度
        nodeGap: 15,          // 增加节点间距,默认8
        layoutIterations: 100,// 增加迭代次数,默认32
        // ...
    }]
};

原理:增大节点间距为节点提供更多空间;增加迭代次数使布局算法有更多时间收敛到更优解。

适用场景:节点数量适中,重叠情况不严重时。

方案二:自定义节点对齐方式

ECharts提供了nodeAlign参数控制节点对齐方式,默认值为"left",可设置为"right""justify"

option = {
    series: [{
        type: 'sankey',
        nodeAlign: 'justify',  // 对齐方式:left/right/justify
        // ...
    }]
};

不同对齐方式的效果对比:

graph TD
    A[left 左对齐] -->|优点| A1[适合流程类数据展示];
    A -->|缺点| A2[右侧易产生空白];
    
    B[right 右对齐] -->|优点| B1[与左对齐互补];
    B -->|缺点| B2[左侧易产生空白];
    
    C[justify 两端对齐] -->|优点| C1[空间利用率最高];
    C -->|缺点| C2[可能导致节点分布不均];

justify对齐方式会尝试将所有没有出边的"汇点"放在最右侧,这在adjustNodeWithNodeAlign函数中实现:

function adjustNodeWithNodeAlign(
    nodes: GraphNode[],
    nodeAlign: SankeySeriesOption['nodeAlign'],
    orient: LayoutOrient,
    maxDepth: number
) {
    if (nodeAlign === 'justify') {
        moveSinksRight(nodes, maxDepth);  // 将汇点移至最右侧
    }
    // ...
}

方案三:自定义布局算法

对于复杂场景,可通过ECharts的自定义系列或修改源码实现更智能的布局。以下是一种基于力导向算法的优化思路:

// 伪代码:基于力导向的节点布局优化
function forceLayoutOptimization(nodes, edges) {
    nodes.forEach(node => {
        // 初始化节点位置
        node.x = initialX(node);
        node.y = initialY(node);
        node.vx = 0; // 速度
        node.vy = 0;
    });
    
    for (let i = 0; i < 100; i++) {
        // 计算节点间斥力
        nodes.forEach(node => {
            nodes.forEach(otherNode => {
                if (node !== otherNode) {
                    const dx = node.x - otherNode.x;
                    const dy = node.y - otherNode.y;
                    const distance = Math.sqrt(dx * dx + dy * dy);
                    const minDistance = 20; // 最小距离,避免重叠
                    
                    if (distance < minDistance) {
                        // 应用斥力
                        const force = (minDistance - distance) * 0.1;
                        node.vx += dx * force;
                        node.vy += dy * force;
                    }
                }
            });
        });
        
        // 计算边的引力
        edges.forEach(edge => {
            const source = edge.source;
            const target = edge.target;
            const dx = source.x - target.x;
            const dy = source.y - target.y;
            const distance = Math.sqrt(dx * dx + dy * dy);
            const idealDistance = 100; // 理想边长度
            
            // 应用引力
            const force = (distance - idealDistance) * 0.01;
            source.vx -= dx * force;
            source.vy -= dy * force;
            target.vx += dx * force;
            target.vy += dy * force;
        });
        
        // 更新节点位置
        nodes.forEach(node => {
            // 阻尼,防止速度过快
            node.vx *= 0.9;
            node.vy *= 0.9;
            
            node.x += node.vx;
            node.y += node.vy;
            
            // 边界约束
            node.x = Math.max(0, Math.min(node.x, width));
            node.y = Math.max(0, Math.min(node.y, height));
        });
    }
}

适用场景:节点数量多、结构复杂,其他方法无法解决的严重重叠问题。

实际应用示例与效果对比

示例1:基础优化(调整参数)

以下是使用test/sankey-jump.html测试数据的优化前后对比:

优化前

// 默认参数
{
    nodeWidth: 25,
    nodeGap: 8,       // 默认节点间距
    layoutIterations: 32 // 默认迭代次数
}

优化后

// 优化参数
{
    nodeWidth: 25,
    nodeGap: 15,       // 增加节点间距
    layoutIterations: 100 // 增加迭代次数
}

通过简单调整参数,节点重叠情况得到明显改善。

示例2:节点对齐方式优化

使用test/sankey-nodeAlign-left.html中的能源数据,对比不同对齐方式的效果:

// 左对齐(默认)
{
    nodeAlign: 'left',
    // 其他参数不变
}

// 两端对齐(优化)
{
    nodeAlign: 'justify',
    // 其他参数不变
}

采用justify对齐方式后,右侧节点分布更均匀,减少了局部拥挤现象。

总结与展望

桑基图的节点重叠问题是影响数据可视化效果的常见痛点,通过本文介绍的三种优化方案,可有效减少甚至消除节点重叠:

  1. 参数调优:适用于大多数场景,简单有效,无需修改代码
  2. 对齐方式优化:根据数据特点选择合适的对齐方式,平衡美观与可读性
  3. 自定义布局算法:针对复杂场景,提供更灵活的布局策略

ECharts的桑基图布局算法在src/chart/sankey/sankeyLayout.ts中实现,未来版本可能会进一步优化节点布局逻辑。建议关注ECharts官方更新,及时应用更先进的布局算法。

希望本文提供的优化方案能帮助你创建更清晰、更专业的桑基图可视化效果。如有任何问题或优化建议,欢迎在评论区留言讨论!

登录后查看全文
热门项目推荐
相关项目推荐