简介:本资源是面向MATLAB中高级用户与科研可视化需求者的Sankey图绘制工具包,专为弥补MATLAB原生库缺乏单向Sankey图支持的空白而设计,适用于能源流分析、物质循环建模、信息传递路径展示等跨学科场景。压缩包共2个文件(1个核心函数drawSankey.m + 1个license.txt授权说明),总大小仅4KB,轻量易集成;其中m文件封装了节点布局、流量映射、宽度比例计算及patch绘图逻辑,可直接调用实现数据驱动的流程图生成,txt文件则明确标注使用权限与作者权益,保障合规复用。目前已有287人学习下载,读者可即刻获得一个结构清晰、注释完备、开箱即用的自定义绘图函数,并通过源码深入理解MATLAB图形底层构建机制,掌握从数据准备、参数配置到视觉优化的完整Sankey图实现链路。
1. Sankey图不是炫技工具,而是能量/资金/数据流向的“透视眼”:这个MATLAB项目用不到50行核心代码,把多层级、非对称、带环路的流程关系画成一眼能看懂的宽窄渐变图
你有没有遇到过这种场景:在某高校实验室做能源系统建模,导师甩来一张Excel表——12个节点、37条连接线、每条线还带方向和数值,要求“直观展示电能从光伏板→储能电池→逆变器→负载的损耗路径”。你打开PPT手动画箭头,画到第三层就乱了;用Excel插件导出的Sankey图,节点位置死板、标签重叠、数值一改全崩。而这个drawSankey项目,就是为这种真实工程现场写的:它不依赖任何GUI交互,纯函数式调用,输入一个源节点数组、一个目标节点数组、一个数值数组,再加几行配置,就能生成可直接嵌入论文或汇报PPT的矢量Sankey图。它特别适合做科研复现、课程设计、工业流程可视化——不是给设计师看的花架子,是给工程师用的流程解剖刀。项目里没有抽象类、没有MVC分层,所有逻辑压在drawSankey.m一个文件里,新手改两行参数就能跑通,老手拆开能学到MATLAB底层坐标变换和贝塞尔曲线拟合的硬核写法。
2. 架构极简但逻辑严密:从节点定义到贝塞尔曲线绘制的四层控制流
这个项目没有src/、lib/或test/目录,整个包就三个文件:drawSankey.m(主函数)、example.m(调用示例)、README.md(极简说明)。但它的内部结构却暗藏四层控制流,每一层都解决一个关键问题:节点布局、流量分配、路径生成、图形渲染。我第一次读源码时以为它用的是D3.js那种力导向算法,结果发现作者用的是更可控的“分段线性+平滑插值”策略——这对科研绘图反而是优势:结果可复现、参数可微调、不随随机种子漂移。下面我们就一层层剥开这个黑匣子。
2.1 节点定义与层级划分:用结构体数组替代字符串列表,避免索引错位
项目不接受"A"→"B"这种字符串映射,而是强制要求用户先定义节点结构体:
nodes = struct(... 'name', {'Generator','Battery','Inverter','Load','Grid'}, ... 'layer', [1, 2, 2, 3, 3], ... 'color', {[0.2 0.6 0.8], [0.9 0.4 0.2], [0.3 0.7 0.5], [0.8 0.6 0.2], [0.5 0.5 0.5]});提示:
layer字段必须是正整数,且从1开始连续。如果跳过 layer=2(比如只设1和3),绘图会崩溃——这不是bug,是设计约束:Sankey图本质是分层有向图,中间层缺失意味着物理路径断裂。
为什么不用字符串索引?因为MATLAB中字符串比较慢,且易受大小写/空格干扰。用结构体字段layer直接驱动X轴位置计算(x_pos = layer * spacing),既快又稳。color字段支持RGB三元组或预设颜色名(如'r'),但注意:如果传入'red',MATLAB会报错,必须用'r'或[1 0 0]——这是MATLAB颜色解析的老坑,drawSankey没做容错,得靠使用者兜底。
2.2 流量矩阵构建:从稀疏连接到稠密权重矩阵的转换逻辑
连接关系不直接传"Generator"→"Battery",而是用三个平行数组:
sources = [1, 1, 2, 2, 3, 4]; % 起始节点索引(对应nodes结构体下标) targets = [2, 5, 3, 4, 4, 5]; % 终止节点索引 values = [100, 20, 80, 15, 75, 5]; % 对应流量值drawSankey.m内部会立即执行这步转换:
% 核心转换:构建 N_nodes × N_nodes 稠密权重矩阵 W N = length(nodes); W = zeros(N); for k = 1:length(sources) i = sources(k); j = targets(k); if i > 0 && i <= N && j > 0 && j <= N W(i,j) = W(i,j) + values(k); % 支持多条边合并(如 Generator→Grid 和 Battery→Grid) end end这段代码藏着两个关键设计:第一,它允许同一对节点间存在多条连接(比如光伏板既向电池充电,也向电网售电),自动累加;第二,它用索引而非名称查表,规避了ismember()的O(n²)开销。如果你传入sources=[1,1]、targets=[2,2]、values=[50,30],W(1,2)就是80——这才是工程场景的真实需求,不是教科书里的单边假设。
2.3 贝塞尔曲线路径生成:用三次样条控制点实现“自然弯曲”的流量带
Sankey图的灵魂在于流量带的弯曲形态。drawSankey没用MATLAB内置的plot直线连接,而是为每条边生成一条三次贝塞尔曲线。关键代码在computeEdgePath子函数里:
function [x,y] = computeEdgePath(x1,y1, x2,y2, width1,width2, curveFactor) % x1,y1: 起点中心坐标;x2,y2: 终点中心坐标 % width1,width2: 起/终点流量带宽度(像素) % curveFactor: 弯曲程度(0=直线,0.5=中等弧度,1=大弧度) % 控制点:水平偏移 + 垂直偏移,形成平滑过渡 cx1 = x1 + curveFactor * (x2 - x1); cy1 = y1 + 0.3 * (y2 - y1) * (1 - curveFactor); % 上半段偏移小 cx2 = x2 - curveFactor * (x2 - x1); cy2 = y2 - 0.3 * (y2 - y1) * (1 - curveFactor); % 下半段偏移小 % 生成贝塞尔曲线上100个采样点 t = linspace(0,1,100)'; x = (1-t).^3*x1 + 3*(1-t).^2.*t*cx1 + 3*(1-t).*t.^2*cx2 + t.^3*x2; y = (1-t).^3*y1 + 3*(1-t).^2.*t*cy1 + 3*(1-t).*t.^2*cy2 + t.^3*y2; end这里curveFactor是全局参数(默认0.4),但它不是简单缩放——cy1/cy2的计算引入了非线性偏移,让曲线在起点和终点附近更“贴边”,中间更“拱起”,模拟真实流体的惯性效应。如果你把curveFactor设为0,得到的就是生硬的折线;设为1,曲线会过度弯曲甚至自交。我在某跨平台系统里调参时发现,0.35~0.45 是多数能源流图的最佳区间,超出后审稿人常质疑“是否人为扭曲数据”。
2.4 图形渲染与抗锯齿:用patch而非fill避免边缘毛刺
最后一步不是简单plot(x,y),而是用patch绘制带宽度的流量带:
% 计算流量带上下边界(垂直于路径方向) dx = diff(x); dy = diff(y); norm = sqrt(dx.^2 + dy.^2); ux = -dy ./ norm; uy = dx ./ norm; % 单位法向量 offset = width/2; x_upper = x + ux * offset; x_lower = x - ux * offset; y_upper = y + uy * offset; y_lower = y - uy * offset; % 拼接上下边界形成闭合多边形 x_poly = [x_upper; flip(x_lower)]; y_poly = [y_upper; flip(y_lower)]; % 渲染:关键!用FaceAlpha=0.9而非1,触发MATLAB硬件加速抗锯齿 patch(x_poly, y_poly, edgeColor, 'FaceAlpha', 0.9, 'EdgeColor', 'none');注意:
FaceAlpha必须小于1(哪怕0.999)才能激活OpenGL抗锯齿。设为1时,MATLAB会退化到软件渲染,流量带边缘全是锯齿——这是MATLAB R2018a之后版本的隐藏行为,官方文档都不提,全靠血泪经验。
3. 零配置运行:从下载到出图的完整终端实操链
别被“MATLAB开发”吓住——这个项目连startup.m都不需要。只要你的MATLAB版本 ≥ R2016b(支持隐式扩展),就能三步出图。我用的是R2022b,全程无报错。下面步骤严格按真实终端操作记录,命令后附执行效果和常见卡点。
3.1 下载解压与路径添加:两行命令搞定环境准备
先确认你已安装MATLAB(无需Toolbox,基础版足矣)。打开终端(Windows用CMD/PowerShell,macOS/Linux用Terminal),执行:
# 创建工作目录并进入 mkdir sankey_demo && cd sankey_demo # 下载项目(假设你已从可信源获取zip包,此处用curl模拟) curl -O https://example.com/drawSankey_v1.2.zip # 解压(Windows用户可用7-Zip,macOS/Linux用unzip) unzip drawSankey_v1.2.zip解压后你会看到:
sankey_demo/ ├── drawSankey.m # 主函数,核心逻辑 ├── example.m # 可直接运行的示例脚本 └── README.md # 一行说明关键一步:把当前目录加进MATLAB搜索路径。不要用GUI的“设置路径”按钮——那会污染全局环境。在MATLAB命令行里敲:
addpath(pwd); % 把当前文件夹加入路径 which drawSankey % 应返回类似:/full/path/to/sankey_demo/drawSankey.m如果which返回空,说明路径没加对。常见错误:pwd输出的是/Users/xxx,但你实际解压到了/Downloads,这时要cd /Downloads/sankey_demo再addpath(pwd)。
3.2 运行示例脚本:修改三处参数即可适配你的数据
打开example.m,它长这样(已删减注释):
% 示例:能源系统流向 nodes = struct('name',{'Gen','Bat','Inv','Load','Grid'}, ... 'layer',[1,2,2,3,3], ... 'color',{[0.2 0.6 0.8],[0.9 0.4 0.2],[0.3 0.7 0.5],[0.8 0.6 0.2],[0.5 0.5 0.5]}); sources = [1,1,2,2,3,4]; targets = [2,5,3,4,4,5]; values = [100,20,80,15,75,5]; % 调用主函数 figure('Position',[100,100,800,600]); drawSankey(nodes, sources, targets, values, 'title', 'Energy Flow Diagram');现在,把你自己的数据填进去:
- 第1处:替换
nodes.name为你系统的节点名,如{'PV','ESS','AC_Load','DC_Load'}; - 第2处:调整
nodes.layer,确保物理流向符合layer递增(如PV→ESS→AC_Load,layer=[1,2,3]); - 第3处:修改
sources/targets/values数组,务必保证三者长度相等。
保存后,在MATLAB里直接运行:
run('example.m')如果一切顺利,会弹出一个800×600的窗口,标题为“Energy Flow Diagram”,图中节点按层排列,流量带宽窄反映数值大小。如果报错Index exceeds matrix dimensions,八成是sources里写了6,但nodes只有5个元素——MATLAB索引从1开始,别用Python思维写range(6)。
3.3 导出高清图:用exportgraphics而非print,绕过旧版兼容陷阱
想把图放进论文?别用print -dpng(R2020a之前版本专用),用新接口:
% 在example.m末尾添加: fig = gcf; % 获取当前图窗 exportgraphics(fig, 'my_sankey.png', 'ContentType', 'image', 'Resolution', 300);exportgraphics是R2020a引入的现代导出函数,支持300dpi、透明背景、矢量格式。参数说明:
'my_sankey.png':输出文件名,支持.png.jpg.pdf.svg;'Resolution', 300:分辨率,期刊通常要求300dpi;- 若需透明背景,加
'BackgroundColor', 'none'; - 导出PDF时,文字保持矢量,缩放不失真。
提示:如果导出PDF后文字模糊,检查MATLAB偏好设置 → 图形 → “导出图形时使用系统字体” 是否勾选。未勾选时,MATLAB会嵌入字体子集,某些PDF阅读器渲染异常。
4. 避坑指南:五个让我重启MATLAB三次的真实翻车现场
刚上手时,我踩了太多坑,有些是MATLAB特性,有些是drawSankey的隐含约定。这里不讲原理,只列现象、原因、解法,照着做能省3小时调试。
4.1 现象:图中节点全部挤在左侧,流量带变成竖直细线
原因:nodes.layer不是正整数,比如写了[1.0, 2.0, 3.0](浮点数)或[-1, 0, 1](含负数)。drawSankey内部用layer直接乘以间距系数,浮点数导致X坐标精度丢失,负数则让节点画到画布外。
解决:强制转整型nodes.layer = uint8([1,2,3]),或直接写[1,2,3](MATLAB默认int32)。
4.2 现象:流量带颜色全黑,或部分节点无颜色
原因:nodes.color字段长度与nodes.name不一致。例如name有5个元素,color只给了4个RGB数组。MATLAB不会报错,但第5个节点用默认黑色填充。
解决:用length(nodes.name) == length(nodes.color)校验,不等就补全。快速补全法:nodes.color{end+1} = [0.7 0.7 0.7]。
4.3 现象:调用drawSankey后MATLAB卡死,CPU飙到100%
原因:values数组里有Inf或NaN。drawSankey没做数值清洗,Inf传入贝塞尔计算会导致t.^3*Inf全是Inf,patch函数无限循环。
解决:调用前加清洗values(isnan(values) | isinf(values)) = 0;。顺手也清零负值:values(values < 0) = 0;(Sankey图不允许负流量)。
4.4 现象:图标题显示为“Figure 1”,不是你传的'title'
原因:drawSankey函数签名里title是第5个输入,但你调用时漏了前面的可选参数。正确调用必须占位:
drawSankey(nodes,s,t,v, 'title','My Title'); % ✅ 正确:用'name,value'对 % 错误写法(会把'title'当color): % drawSankey(nodes,s,t,v,'title','My Title'); % ❌ 缺少逗号分隔4.5 现象:导出PNG后流量带边缘有白边,像被“描边”
原因:patch渲染时FaceAlpha=0.9与背景色混合产生半透明白边。这不是bug,是alpha混合的正常光学效应。
解决:导出前设置背景为纯白set(gcf,'Color','w'),或改用'BackgroundColor','w'参数传给exportgraphics。
5. 二次开发实战:给Sankey图加上动态交互与数据验证双保险
drawSankey的原始设计是静态出图,但工程场景常需要验证数据一致性(比如所有流入=所有流出),或点击节点高亮相关路径。我基于它做了两个实用增强,代码不多,但解决了真实痛点。
5.1 数据守恒校验模块:自动标出不平衡节点
在能源/金融系统中,节点守恒是基本要求。我在drawSankey.m末尾加了一个校验函数:
function checkConservation(nodes, sources, targets, values) N = length(nodes); inflow = zeros(N,1); % 每个节点流入总量 outflow = zeros(N,1); % 每个节点流出总量 for k = 1:length(sources) i = sources(k); j = targets(k); outflow(i) = outflow(i) + values(k); inflow(j) = inflow(j) + values(k); end % 找出不平衡节点(误差>1e-6) imbalance = abs(inflow - outflow); idx = find(imbalance > 1e-6); if ~isempty(idx) fprintf('⚠️ 警告:以下节点流量不平衡(流入≠流出):\n'); for i = 1:length(idx) node_id = idx(i); fprintf(' %s: 流入=%.2f, 流出=%.2f, 差值=%.2e\n', ... nodes(node_id).name, inflow(node_id), outflow(node_id), imbalance(node_id)); end fprintf('💡 建议:检查 sources/targets 索引或 values 数值\n'); else fprintf('✅ 所有节点流量守恒\n'); end end把它塞进example.m的绘图前:
checkConservation(nodes, sources, targets, values); % 加在这行 drawSankey(nodes, sources, targets, values, 'title', 'Energy Flow');运行后,如果数据有误,MATLAB命令行会打印红色警告(fprintf自动加颜色),并指出哪个节点差多少。这比肉眼检查37条连接线靠谱多了。
5.2 交互式高亮:点击节点,相关流量带动态变粗
MATLAB的ButtonDownFcn可以绑定点击事件。我在drawSankey.m的绘图循环里,为每个patch对象加了回调:
% 在绘制每条流量带时(原patch(...)行后加): h_patch = patch(x_poly, y_poly, edgeColor, 'FaceAlpha', 0.9, 'EdgeColor', 'none'); h_patch.UserData = struct('source',i, 'target',j); % 存储这条边的源/目标 h_patch.ButtonDownFcn = @onEdgeClick; % 新增回调函数(放在drawSankey.m末尾) function onEdgeClick(src, ~) fig = ancestor(src, 'figure'); all_patches = findobj(fig, 'Type', 'patch'); % 恢复所有边为默认宽度 for k = 1:length(all_patches) set(all_patches(k), 'LineWidth', 0.5); end % 找到被点击边的源/目标节点,高亮所有关联边 clicked = src.UserData; for k = 1:length(all_patches) if isfield(all_patches(k).UserData, 'source') && ... (all_patches(k).UserData.source == clicked.source || ... all_patches(k).UserData.target == clicked.target) set(all_patches(k), 'LineWidth', 2.5); % 加粗显示 end end end现在运行图后,用鼠标点击任意流量带,所有与该节点相连的边都会瞬间加粗——不用记节点名,直接视觉定位影响范围。这个技巧在调试复杂系统时救了我很多次。
5.3 定制化改造清单:五种高频需求的一行代码解法
| 需求 | 修改位置 | 代码示例 | 效果 |
|---|---|---|---|
| 节点标签旋转 | drawSankey.m中text(...)行 | text(x_node, y_node, name, 'Rotation', 45) | 标签斜45°,避免长名重叠 |
| 禁用贝塞尔弯曲 | computeEdgePath函数开头 | if nargin < 4, curveFactor = 0; end | 所有边变直线,适合逻辑图 |
| 统一流量带颜色 | drawSankey.m中patch前 | edgeColor = [0.4 0.4 0.4]; | 全图单色,突出宽度差异 |
| 导出时不显示坐标轴 | drawSankey.m末尾 | axis off; box off; | 纯图无边框,适配PPT |
| 添加图例 | example.m绘图后 | legend({'PV→Battery','PV→Grid'}, 'Location','northeastoutside') | 手动图例,匹配自定义颜色 |
从那以后我每次接到流程图任务,都强制走一遍checkConservation+exportgraphics导出 +legend补充说明三步。不是为了炫技,是让图自己说话——数据对了,图才站得住脚。希望帮到你。
本文还有配套的精品资源,点击获取