简介:这份资源面向需要在MATLAB中绘制流向图的开发者与数据分析学习者,提供自定义函数drawSankey,用于弥补MATLAB标准绘图库缺少Sankey图支持的空白。Sankey图以连接线宽度表示流量大小,常用于能量转换、物质流动与信息传递等场景,适合环境科学、经济学、社会学等领域的可视化需求。压缩包共2个文件,包含1个m脚本与1个txt说明,整体约4KB,其中m文件承载函数定义、参数处理、数据计算与图形绘制逻辑,txt文件则给出许可协议与使用条件。已有286人学习关注。通过阅读源码,读者可掌握节点列表、流量矩阵等输入组织方式,理解如何借助plot、patch等图形函数构建自定义图表,并学习颜色、线宽、布局等视觉参数的调整思路,为后续扩展其他自定义图形打下基础。
1. 从一张挤成一团的能量流图说起:drawSankey 到底能干什么
如果你画过能量守恒、碳流、资金流向或者用户路径转化,大概率遇到过这种尴尬:数据明明只有七八个节点,用普通折线图画出来却像一团乱麻,谁也看不清从哪来到哪去。桑基图(Sankey Diagram)就是为这种“流量分配”场景生的——线宽代表流量大小,节点代表环节,一眼就能看出主路径和损耗点。但 MATLAB 官方并没有内置桑基图函数,很多人要么手动画,要么转去 Python 的 plotly,来回导数据非常折腾。
drawSankey就是补这个缺口的:一个纯 MATLAB 实现的桑基图绘制工具,输入节点和流量矩阵,直接出图,支持节点位置、颜色、标签、线宽缩放等参数调节。它适合做能源系统分析、物料平衡、经费流向、用户漏斗这类需要“看流量分配”的从业者,也适合不想为了画一张图就切到别的语言的研究生。下面我按“能跑起来 → 参数怎么调 → 哪里会翻车 → 怎么用得更狠”的顺序,把这份资源拆开讲。
2. drawSankey 的输入结构与最小可运行示例
2.1 桑基图的数据本质:节点表 + 流量矩阵
桑基图在数学上就是一张有向加权图。drawSankey的常见输入形式是两部分:一个是节点名称列表(cell 数组或 string 数组),一个是 N×N 的流量矩阵,flow(i,j)表示从节点 i 流向节点 j 的流量。这个矩阵不要求对称,也不要求每行每列和相等——因为桑基图允许有外部输入和外部输出,节点本身可以“凭空产生”或“凭空消失”流量,这正是它比普通网络图灵活的地方。
我一般会先把原始数据整理成这种矩阵形式,再交给drawSankey。整理时有个原则:先确定哪些是源节点(只有流出)、哪些是汇节点(只有流入)、哪些是中间节点(既有流入又有流出)。这个分类直接决定后面节点排布是否合理,如果源和汇混在一起,图会显得很乱。
2.2 最小可运行示例:三节点能量流
下面这段代码是我用来验证环境是否正常的最小示例,三个节点:输入能源、转换环节、有效输出,外加一个损耗节点。
% 节点名称,按顺序对应流量矩阵的行列索引 nodes = {'输入能源', '转换环节', '有效输出', '损耗'}; % 4x4 流量矩阵,flow(i,j) 表示 i -> j 的流量 flow = [0, 100, 0, 0; % 输入能源 -> 转换环节 100 0, 0, 70, 30; % 转换环节 -> 有效输出 70,损耗 30 0, 0, 0, 0; % 有效输出无流出 0, 0, 0, 0]; % 损耗无流出 % 调用 drawSankey,具体函数名以资源内实际文件为准 drawSankey(nodes, flow);逻辑说明:第一行nodes定义了四个环节,顺序必须和矩阵行列一一对应,否则标签会错位。第二行flow是核心,flow(1,2)=100表示输入能源全部流向转换环节;flow(2,3)=70、flow(2,4)=30表示转换后 70 份有效、30 份损耗。第三行调用时,如果资源里的主函数名是drawSankey就直接用,如果是plotSankey或别的名字,改成对应名字即可。
参数说明:这个最小示例只传了两个必填参数。实际使用时,drawSankey通常还接受'NodeColors'、'NodeWidth'、'Gap'、'FontSize'等名值对参数。我建议第一次跑通后再逐个加参数,不要一上来就堆一堆配置,否则出错时分不清是数据问题还是参数问题。
提示:如果你的 MATLAB 版本较老,注意检查函数里是否用了
string类型或arguments块,这两样在 R2016b 之前不支持,遇到报错优先怀疑版本兼容。
3. 参数调优:节点排布、颜色映射与线宽缩放
3.1 节点顺序与层级排布:决定图可读性的第一因素
桑基图好不好看,七成取决于节点顺序。drawSankey默认按你传入的nodes顺序从左到右排,但真实数据里节点往往有层级关系,比如“原料 → 加工 → 成品 → 销售 → 终端”,如果顺序打乱,连线就会大量交叉,图直接废掉。
我的做法是:先按层级给节点分组,同层节点连续排列,层与层之间按流量大小降序。比如能源系统里,第一层是各种输入能源,第二层是转换设备,第三层是输出形式。这样连线基本不会回头交叉。如果资源支持'NodeOrder'或'Layer'参数,直接传入分组索引;如果不支持,就在调用前手动重排nodes和flow的行列顺序,效果一样。
这里有个容易忽略的点:重排流量矩阵时,行和列必须同步重排,只改行不改列会导致流量对不上。我一般写个小函数做这件事:
function [nodes2, flow2] = reorderNodes(nodes, flow, idx) % idx 是新的顺序索引,例如 [1 3 2 4] nodes2 = nodes(idx); flow2 = flow(idx, idx); % 行列同步重排,缺一不可 end逻辑说明:flow(idx, idx)这一句同时完成了行重排和列重排,保证flow2(i,j)仍然表示新顺序下第 i 个节点到第 j 个节点的流量。参数idx就是你想要的节点顺序,按层级和流量大小排好即可。
3.2 颜色与透明度:让主路径自己跳出来
默认配色往往所有节点一个颜色,图能看但不够直观。drawSankey一般支持给每个节点单独指定颜色,常见做法是用 colormap 按流量大小映射,或者按类别手动指定。
我通常按“源节点暖色、中间节点中性色、汇节点冷色”来分,这样视觉上天然区分输入和输出。如果资源支持'LinkAlpha'或透明度参数,把连线透明度调到 0.4~0.6,重叠的流线也能看清走向。下面是一个按流量大小映射节点颜色的示例:
% 计算每个节点的总吞吐量(流入+流出) throughput = sum(flow, 1) + sum(flow, 2)'; % 归一化到 [0,1],用于 colormap 索引 tNorm = (throughput - min(throughput)) / (max(throughput) - min(throughput) + eps); % 用 parula 色图生成每个节点的 RGB cmap = parula(256); nodeColors = cmap(round(tNorm * 255) + 1, :); drawSankey(nodes, flow, 'NodeColors', nodeColors, 'LinkAlpha', 0.5);逻辑说明:throughput把每个节点的流入流出加总,代表这个节点承载的总流量;tNorm做 min-max 归一化,避免量纲差异导致颜色全挤在一端;cmap(round(...)+1,:)是 MATLAB 里用色图取色的标准写法,加 1 是因为索引从 1 开始。参数'NodeColors'传 N×3 的 RGB 矩阵,'LinkAlpha'控制连线透明度,具体参数名以资源内文档为准。
注意:如果
throughput里有 0,归一化时分母可能为 0,我加了eps兜底。这个坑我在物料平衡图里踩过,某个节点完全没有流量,结果整张图颜色全白。
3.3 线宽缩放:流量差异大时怎么不糊成一片
桑基图的线宽通常和流量成正比,但当最大流量和最小流量差两三个数量级时,小流量线会细到看不见。drawSankey如果有'WidthScale'或'MinWidth'参数,可以设一个最小线宽保底,同时对流量做非线性缩放(比如开方或对数)。
我的经验是:流量差异在 10 倍以内用线性,超过 10 倍用开方,超过 100 倍用对数。这样既保留大小关系,又不至于小流量完全消失。如果资源没有内置缩放,可以在传入前自己处理流量矩阵:
% 对数缩放,加 1 避免 log(0) flowScaled = log1p(flow); % 或者开方缩放 % flowScaled = sqrt(flow); drawSankey(nodes, flowScaled);逻辑说明:log1p(x)等价于log(1+x),在 x 接近 0 时数值更稳定,这是处理流量跨度大的常用技巧。缩放后线宽关系变了,但拓扑结构不变,适合先看整体格局。如果要在图上标注真实流量值,记得用原始flow而不是缩放后的值。
4. 避坑与排查:drawSankey 最常见的五类翻车
4.1 现象:图是出来了,但所有节点挤在一条竖线上
原因:流量矩阵里存在全零行或全零列,导致布局算法无法确定节点层级,只能堆在一起。解决:调用前检查sum(flow,1)和sum(flow,2),把全零节点暂时移除或合并到相邻节点,画完再补标签。
4.2 现象:连线交叉严重,完全看不出流向
原因:节点顺序没有按层级排列,源节点和汇节点交错。解决:按 3.1 的方法重排nodes和flow,确保同层节点连续、层间按流量降序。这一步做完,交叉通常能减少七成以上。
4.3 现象:报错“索引超出矩阵维度”或“维度不一致”
原因:nodes的数量和flow的阶数不匹配,或者重排时只改了行没改列。解决:调用前加一句assert(numel(nodes) == size(flow,1) && size(flow,1) == size(flow,2)),把问题挡在绘图之前。
4.4 现象:中文标签显示成方框或乱码
原因:MATLAB 默认字体不含中文字形,或者drawSankey内部用了固定字体。解决:在调用前设置set(0, 'DefaultAxesFontName', 'SimHei')或对应系统里的中文字体,如果函数内部硬编码了字体,找到那一行改成支持中文的字体。
4.5 现象:保存成图片后线宽和屏幕上不一致
原因:导出时用了saveas默认分辨率,线宽按像素渲染被压缩。解决:改用exportgraphics或print指定-r300分辨率,同时把'WidthScale'适当调大,保证导出后小流量线仍然可见。
5. 进阶用法:把 drawSankey 接进批量分析流程
5.1 批量出图:循环 + 动态文件名
做参数扫描或多年份对比时,一张张手画不现实。我一般把drawSankey包在一个循环里,每次改流量矩阵,用exportgraphics存成带标识的文件名:
years = 2020:2024; for k = 1:numel(years) flowYear = loadFlowData(years(k)); % 自定义函数,读取当年流量 drawSankey(nodes, flowYear, 'NodeColors', nodeColors); title(sprintf('%d 年能量流', years(k))); exportgraphics(gcf, sprintf('sankey_%d.png', years(k)), 'Resolution', 300); close(gcf); % 及时关闭,避免句柄堆积 end逻辑说明:loadFlowData是占位函数,实际替换成你的数据读取逻辑;exportgraphics是 R2020a 之后推荐的导出函数,比saveas清晰;close(gcf)必须加,否则循环几十次后 MATLAB 会因图形句柄过多而卡死。参数'Resolution'设 300 适合论文和报告,屏幕预览用 150 就够。
5.2 验证图是否“画对了”:流量守恒检查
桑基图最容易出的错是流量对不上,图看着漂亮但数据是错的。我习惯在绘图前做一次守恒检查:对每个中间节点,流入总和应该等于流出总和(允许有损耗节点单独处理)。下面这个检查我每次都会跑:
inflow = sum(flow, 1)'; % 每个节点的总流入 outflow = sum(flow, 2); % 每个节点的总流出 balance = inflow - outflow; % 找出不平衡超过阈值的节点 tol = 1e-6; badIdx = find(abs(balance) > tol); if ~isempty(badIdx) fprintf('以下节点流量不平衡:\n'); for i = badIdx' fprintf(' %s: 流入 %.2f, 流出 %.2f\n', nodes{i}, inflow(i), outflow(i)); end end逻辑说明:inflow是列求和转置,outflow是行求和,两者相减就是每个节点的净流量。源节点净流量为负、汇节点为正,中间节点应该接近零。tol是容差,浮点运算下不要用== 0判断。这个检查帮我抓过好几次数据录入错误,比图出来后肉眼找靠谱得多。
5.3 一个我踩过的坑:别在循环里重复调用colormap
早期我为了每个年份颜色不同,在循环里反复调colormap,结果图是变了,但 MATLAB 警告刷屏,而且导出颜色和屏幕不一致。后来改成在循环外算好nodeColors,循环内只传参,问题消失。从那以后我每次批量出图都强制走一遍“先算颜色、再画图、最后导出”的顺序,不再在绘图函数里做任何全局状态修改。希望帮到你。
本文还有配套的精品资源,点击获取