
简介这份资源面向需要在MATLAB中绘制流向图的开发者与数据分析人员提供自定义函数drawSankey用于弥补MATLAB标准绘图库缺少Sankey图支持的空白。Sankey图以连接线宽度表示流量大小常用于能量转换、物质流动与信息传递等场景在环境科学、经济学和社会学领域均有应用。压缩包共2个文件包含1个m脚本与1个txt说明整体约4KB其中m文件承载函数定义、参数处理、数据计算与图形绘制逻辑txt文件则给出许可协议与使用条件。已有286人学习下载。通过阅读源码读者可掌握节点列表、流量矩阵等输入的组织方式理解如何借助plot、patch等图形函数构建自定义图表并据此调整颜色、线宽与布局完成从数据准备到图形输出的完整流程同时获得一个深入学习MATLAB自定义图形编程的实例。1. 用 MATLAB 画桑基图为什么自带函数不够用drawSankey 能补上哪块做能源流、物料平衡、用户路径分析的人迟早会撞上同一个需求把一堆「从哪来到哪去」的流量画成桑基图。MATLAB 里能画流向图的官方函数屈指可数plot系列没有原生桑基支持社区里流传的几套方案要么依赖第三方工具箱要么只能画固定层级的简单图。drawSankey这个方向解决的正是这个缺口——用纯 MATLAB 代码把节点、连线、流量宽度三件事拆开控制不装额外工具箱也能跑。它适合三类人一是做能量审计、碳流核算需要把多级转换画清楚的工程人员二是做用户行为漏斗、资金流向节点带权重的数据分析者三是想在自己的 MATLAB 脚本里嵌一张可调样式流向图而不是导出到别的软件重画的人。核心价值在于「可控」节点位置、连线曲率、颜色映射、标签格式都能在代码里改不用跟黑匣子函数较劲。2. 桑基图的几何本质节点、流量带与贝塞尔曲线怎么算桑基图看着复杂拆开只有三样东西节点矩形、连接两节点的流量带、以及流量带的宽度。宽度正比于流量值这是桑基图区别于普通有向图的关键。理解这一点后面调参数才不会瞎试。2.1 节点布局为什么手动指定坐标比自动布局更靠谱自动布局在节点少的时候好看节点一多就乱。drawSankey这类实现通常让你直接给每个节点一个[x, y]坐标层级靠 x 拉开同层节点靠 y 错开。这样做的好处是结果可复现——同一份数据每次跑出来一模一样方便写进报告。节点在代码里一般表示成一个结构体数组每个元素含名称、坐标、颜色。流量带则记录起点节点索引、终点节点索引、流量值。下面是一个最小数据结构% 节点定义名称、x 坐标、y 坐标、颜色 nodes struct( ... name, {原料A, 工序B, 工序C, 成品D}, ... x, {0, 1, 2, 3}, ... y, {0.5, 0.7, 0.3, 0.5}, ... color, {[0.2 0.4 0.8], [0.3 0.7 0.4], [0.9 0.6 0.2], [0.6 0.3 0.7]}); % 流量定义起点索引、终点索引、流量值 flows [1 2 80; 1 3 20; 2 4 60; 3 4 35];这里flows第一列是起点节点在nodes里的下标第二列是终点下标第三列是流量。注意流量值不需要归一化函数内部会按节点总进出量算比例。2.2 流量带宽度按节点总量归一化而不是按全局最大值一个容易翻车的点流量带宽度如果按全局最大流量归一化小流量的带子会细到看不见。正确做法是按每个节点的总流出量或总流入量归一化这样每个节点自己的带宽分配是满的视觉上更均衡。具体逻辑是对每个源节点把它所有出边的流量求和每条出边占该节点高度的比例就是flow / sum_out。节点矩形高度通常固定比如 0.1 个坐标单位那么这条边在源节点侧的厚度就是node_height * flow / sum_out。node_h 0.1; % 节点矩形高度 for i 1:size(flows,1) src flows(i,1); val flows(i,3); sum_out sum(flows(flows(:,1)src, 3)); band_h node_h * val / sum_out; % 该流量带在源侧的厚度 % band_h 后续用于生成上下边界曲线 end参数node_h是唯一需要手动定的几何量取 0.08 到 0.15 之间通常比较顺眼。太小带子挤在一起太大节点之间会重叠。2.3 贝塞尔曲线用三次曲线把上下边界画顺流量带的上下边界各是一条从源节点边缘到目标节点边缘的曲线。用三次贝塞尔曲线最省事控制点取在两点水平中点附近曲线就不会出现奇怪的折角。function [x, y] bezier_band(p0, p1, p2, p3, n) % p0,p3 为端点p1,p2 为控制点n 为采样点数 t linspace(0, 1, n); x (1-t).^3*p0(1) 3*(1-t).^2.*t*p1(1) 3*(1-t).*t.^2*p2(1) t.^3*p3(1); y (1-t).^3*p0(2) 3*(1-t).^2.*t*p1(2) 3*(1-t).*t.^2*p2(2) t.^3*p3(2); end控制点的 x 取(x0x3)/2y 分别取源侧上下边界和目标侧上下边界这样曲线在两端是水平的接节点边缘不会突兀。采样点数n取 50 到 100 足够平滑再多只是浪费渲染时间。3. 从零实现 drawSankey节点绘制、流量带填充与标签这一章把上一章的几何逻辑落成可运行的函数。整体思路是先画所有流量带用fill填充半透明多边形再画节点矩形盖在上面最后加文字标签。顺序不能反否则带子会遮住节点。3.1 主函数骨架与输入校验先定函数签名和基本校验。输入就是前面说的nodes和flows外加一个可选的样式参数结构体。function drawSankey(nodes, flows, opts) % 输入校验 if nargin 3, opts struct(); end if ~isfield(opts, nodeHeight), opts.nodeHeight 0.1; end if ~isfield(opts, alpha), opts.alpha 0.5; end if ~isfield(opts, fontSize), opts.fontSize 10; end nNode numel(nodes); if max(flows(:,1:2), [], all) nNode error(flows 中引用了不存在的节点索引); end hold on; axis equal; axis off; % 先画流量带后画节点 drawBands(nodes, flows, opts); drawNodes(nodes, opts); hold off; endopts用结构体传参是 MATLAB 里比较稳的做法新增参数不会破坏旧调用。axis equal必须加否则节点矩形会被拉成扁的。3.2 流量带绘制fill 的顶点顺序与透明度处理每条流量带是一个多边形源侧上边界点、目标侧上边界点、目标侧下边界点、源侧下边界点按顺序连起来。上下边界用贝塞尔采样注意目标侧的点要反向排列才能闭合。function drawBands(nodes, flows, opts) for i 1:size(flows,1) s flows(i,1); t flows(i,2); v flows(i,3); % 源节点侧厚度 sumS sum(flows(flows(:,1)s, 3)); hS opts.nodeHeight * v / sumS; % 目标节点侧厚度 sumT sum(flows(flows(:,2)t, 3)); hT opts.nodeHeight * v / sumT; % 源侧上下边界 y简化以节点 y 为中心 yS0 nodes(s).y - hS/2; yS1 nodes(s).y hS/2; yT0 nodes(t).y - hT/2; yT1 nodes(t).y hT/2; xS nodes(s).x 0.05; xT nodes(t).x - 0.05; [bx1, by1] bezier_band([xS yS1], [(xSxT)/2 yS1], [(xSxT)/2 yT1], [xT yT1], 60); [bx2, by2] bezier_band([xS yS0], [(xSxT)/2 yS0], [(xSxT)/2 yT0], [xT yT0], 60); X [bx1; flipud(bx2)]; Y [by1; flipud(by2)]; fill(X, Y, nodes(s).color, FaceAlpha, opts.alpha, EdgeColor, none); end endFaceAlpha设 0.5 左右能让重叠的带子透出层次全不透明会糊成一片。EdgeColor设none去掉多边形边框否则每条带子边缘会有细线密集时很脏。3.3 节点与标签矩形位置微调与文字避让节点用rectangle画宽度给 0.1 左右高度用opts.nodeHeight。标签放在节点上方或下方根据节点 y 坐标决定避免和带子重叠。function drawNodes(nodes, opts) w 0.1; for i 1:numel(nodes) rectangle(Position, [nodes(i).x - w/2, nodes(i).y - opts.nodeHeight/2, ... w, opts.nodeHeight], FaceColor, nodes(i).color, EdgeColor, k); % 标签y 偏高的放下方偏低的放上方 if nodes(i).y 0.5 ty nodes(i).y - opts.nodeHeight/2 - 0.04; va top; else ty nodes(i).y opts.nodeHeight/2 0.04; va bottom; end text(nodes(i).x, ty, nodes(i).name, HorizontalAlignment, center, ... VerticalAlignment, va, FontSize, opts.fontSize); end end标签避让是个经验活。如果节点密集可以给每个节点加一个labelOffset字段手动微调比自动算法省心。4. 参数调优与样式控制颜色映射、曲线张力、导出分辨率图能画出来只是第一步要放进报告或论文样式得能调。这一章讲几个最常改的参数以及它们背后的取舍。4.1 颜色映射按流量大小渐变还是按源节点固定两种策略各有场景。按源节点固定颜色读者能一眼看出「这条带子从哪来」按流量大小做渐变能突出主通道。实现上前者直接用nodes(s).color后者用colormap加interp1映射。% 按流量大小映射颜色从蓝到红 cmap parula(256); vNorm (v - min(flows(:,3))) / (max(flows(:,3)) - min(flows(:,3)) eps); cIdx max(1, round(vNorm * 255) 1); bandColor cmap(cIdx, :);eps是防止最大最小相等时除零。parula是 MATLAB 默认色图对比度适中打印成灰度也能区分。4.2 曲线张力控制点偏移量对视觉流畅度的影响前面贝塞尔控制点取的是水平中点曲线比较「平」。想让带子更「鼓」把控制点 x 往两端收一点比如取xS 0.3*(xT-xS)和xS 0.7*(xT-xS)。这个 0.3/0.7 就是张力参数。张力取值视觉效果适用场景0.5 / 0.5最平直接近直线节点间距小避免曲线交叉0.3 / 0.7适度弯曲流畅通用场景推荐默认0.1 / 0.9弯曲明显接近 S 形节点间距大需要强调流向改张力只需动bezier_band调用里的控制点坐标不影响其他逻辑。4.3 导出exportgraphics 的分辨率与矢量格式选择exportgraphics是较新版本 MATLAB 推荐的导出函数比print稳定。位图用 300 dpi矢量用 PDF 或 EPS。exportgraphics(gcf, sankey.png, Resolution, 300); exportgraphics(gcf, sankey.pdf, ContentType, vector);PDF 矢量格式在 LaTeX 里插入不会糊但带透明度FaceAlpha的图形导出矢量时部分渲染器可能不支持透明会变成实色。遇到这种情况要么导出高分辨率 PNG要么把FaceAlpha设成 1 再导 PDF。5. 避坑与排查坐标轴、透明度、大数据量下的五个翻车点这一章是我自己踩过的坑按「现象 → 原因 → 解决」写你遇到时可以直接对号入座。现象一节点被拉成扁矩形。原因是没加axis equalMATLAB 默认按窗口比例缩放坐标轴。解决在hold on之后立刻加axis equal并且axis off之前不要再改轴范围。现象二流量带边缘有细白线。原因是fill默认带边框相邻多边形之间露出背景。解决fill时显式设EdgeColor, none。如果还有缝把FaceAlpha提到 0.6 以上或者给带子加一个同色的细边框EdgeColor, nodes(s).color, LineWidth, 0.5。现象三导出 PDF 后透明带子变实色。原因是 PDF 矢量渲染对FaceAlpha支持不一致。解决导出前把opts.alpha临时设为 1或者改用 PNG 300 dpi。如果必须矢量且要透明试试导出 SVG部分版本支持更好。现象四节点一多标签全叠在一起。原因是自动避让没做所有标签按固定偏移放。解决给nodes加一个可选labelDy字段手动给每个节点不同的垂直偏移或者按 y 坐标排序后相邻节点标签交替上下放。现象五流量数据里有零值或负值图直接画崩。零值会让sum_out为 0 导致除零负值在几何上没有意义。解决画之前过滤掉flows(:,3) 0的行并检查每个节点的总流出是否大于 0。加一行flows flows(flows(:,3) 0, :);就能挡掉大部分问题。注意桑基图对数据完整性很敏感一条流量缺失可能导致某个节点总流出对不上总流入。画之前先用sum核对一下每个中间节点的进出是否平衡不平衡的图在专业场合会被质疑。6. 进阶技巧用子图嵌套做多层级桑基与交互式高亮基础版能画静态图但真实报告里往往需要更细的控制。这一章给两个进阶方向都是我在实际项目里用过、觉得值得投入的。第一个是多层级嵌套。当节点分属不同子系统时可以在主桑基图里把某个节点展开成子桑基图。做法是用axes在指定位置创建子坐标轴把子系统的nodes和flows传进去再跑一遍drawSankey。关键是子图的坐标范围要单独设不能继承主图否则比例会乱。% 在主图某个节点位置嵌入子桑基图 ax2 axes(Position, [0.6 0.15 0.25 0.25]); % [左 下 宽 高] drawSankey(subNodes, subFlows, struct(nodeHeight, 0.08, fontSize, 8));Position四个值都是归一化坐标相对整个 figure。子图字号要调小否则和主图不协调。第二个是交互式高亮。MATLAB 的ButtonDownFcn可以给每个流量带绑定点击回调点一下把该带子加粗、其他带子变淡。实现思路是给fill返回的句柄存起来回调里改FaceAlpha和LineWidth。h fill(X, Y, bandColor, FaceAlpha, 0.5, EdgeColor, none); set(h, ButtonDownFcn, (src,~) highlightBand(src, allHandles));highlightBand里遍历所有句柄把当前这个设成FaceAlpha0.9, LineWidth2其余设成FaceAlpha0.1。再点一次恢复。这个技巧在给非技术背景的人演示时特别管用对方能自己点着看每条流向。最后一个习惯每次改完样式先导出 300 dpi PNG 看一眼再决定要不要导 PDF。我早期图省事直接导 PDF结果透明层全丢返工重画。现在固定流程是 PNG 确认、PDF 交付省下不少后悔药。希望帮到你。本文还有配套的精品资源点击获取