写论文或者做汇报图的时候,Matlab里最耗时间的环节往往不是跑仿真计算,而是子图排版。我最早用subplot画多子图,每次都要手动调间距,后来换了社区的tight_subplot,R2019b之后又开始用官方的tiledlayout。这三条路线我都完整走过,处理的图从简单的2x2到需要统一坐标轴、共享图例的复杂组合都有,踩过的坑也不少。这篇就专门聊一聊Matlab子图排版这件事,把tight_subplot和tiledlayout的实战效果、适用边界、代码写法放在一起对比,给正在纠结选哪个方案的读者一个可以直接参考的结论。
和你先交个底:这篇文章不是简单的API科普,而是我从实际出图流程里总结出来的经验。尤其是最近做一批需要“多子图 + 统一配色 + 直接导出PNG/PDF”的实验图,全程体验非常有代表性,踩过的坑也很有价值。无论是刚入门Matlab画图的新手,还是被subplot折磨已久的老用户,这篇应该都能给你一些有用的东西。
1. 为什么subplot总是“差一点”:默认间距背后的排版逻辑
1.1 subplot的网格逻辑和默认留白
subplot的本质是在figure上按m行n列建立一个虚拟网格,每次调用subplot(m,n,p)就在对应位置创建一个axes对象。很多入门教程都会告诉你“subplot很方便,一行代码就能生成子图”,但它从设计之初就没有把“紧凑排版”当成核心目标,更没有提供语义化的留白控制。默认状态下,每个axes四周都保留了大量空白,相邻子图之间的间隙尤其明显。
看一下这个最常见的demo:
figure; for k = 1:4 subplot(2, 2, k); plot(1:100, cumsum(randn(100, 1))); title(sprintf('Subplot %d', k)); xlabel('x'); ylabel('y'); end运行之后你就能感受到问题:左右两列子图之间的间隙非常大,顶部和底部的留白也多得莫名其妙。每个子图自带title、xlabel、ylabel之后,相邻子图的y轴刻度数字会和上一个子图的x轴标签挤在一起,视觉上非常难受。这种图如果直接截图放进论文,你会明显觉得版面利用率低,信息密度不够。
那有人会说,subplot不是有Position属性可以调吗?确实可以,但那是另一套麻烦。单独看subplot本身,它只能控制“子图在网格中的位置”,无法直接控制“子图之间的间隙”和“整张画布的边距”。所以默认效果才总是显得很松散。这也是后面tight_subplot和tiledlayout能取代它的根本原因。
1.2 手动调Position为什么是个无底洞
为什么用过subplot的人,后来几乎都逃不掉手动调Position?因为想要紧凑布局,只能拿到axes句柄之后逐个用set(gca, 'Position', [...])去改坐标。这里的Position是相对于figure的归一化坐标,四元组分别表示左下角的x、y坐标以及宽度和高度。一旦子图数量多了,每个子图的x、y、width、height都得自己按比例算。
figure; hold on; h(1) = axes('Position', [0.10 0.55 0.35 0.35]); h(2) = axes('Position', [0.55 0.55 0.35 0.35]); h(3) = axes('Position', [0.10 0.10 0.35 0.35]); h(4) = axes('Position', [0.55 0.10 0.35 0.35]);你可以看到,凡是手写Position方案的人,都在做同一件事:把figure的整个宽度拆成“左边距 + 子图宽度 + 中间间隙 + 子图宽度 + 右边距”。这个比例一旦要改,比如从双栏图改成单栏图,所有坐标都得重算。更烦的是,最后用set(gcf, 'PaperPosition', ...)或者print导出时,figure尺寸变化后这些归一化位置并不一定按预期效果缩放,又是一轮重新调整。
我个人的体会是,subplot适合“快速浏览”,比如脚本调试时临时看看几条曲线长什么样,但完全不适合“定稿出图”。如果你要做论文图、实验报告图、作业图,用subplot默认排版基本等于给自己挖坑。明白了这一点,再去看tight_subplot和tiledlayout的定位就会清晰很多——它们都是为了解决subplot这个“最后差一点”的问题而出现的。
2. tight_subplot:社区方案为什么能流传十几年
2.1 一个函数解决80%的排版需求
tight_subplot是MathWorks File Exchange上的经典函数,作者是Pekka Kumpulainen,最早发布于很多年前,至今仍在广泛使用。它的作用是生成一组位置统一、间隙可控的axes,让你不用再手动计算坐标比例。
为什么这个社区函数能流传这么久?因为它本质上是把“手动算Position”这套重复劳动做成了参数化封装。你只需要告诉它行列数、子图间隙、四周留白,它就能一次性算好所有axes的Position。虽然底层仍然是设Position,但你不必再手工算比例,这就是最大的价值。
很多老版本的Matlab没有官方紧凑布局方案,而tight_subplot只有一个.m文件,没有任何依赖,复制到搜索路径里就能用。这一点让它在高校和工业界都流传很广。我做图像处理大作业的时候用过它,做深度学习训练曲线可视化的时候也用过它,稳定性和可移植性都经得起考验。
2.2 参数拆解与代码模板
tight_subplot的调用格式是这样的:
[ha, pos] = tight_subplot(rows, cols, gap, marg_h, marg_w)参数含义如下:
rows、cols:几行几列,和subplot的行列概念一致。gap:2元素向量[gap_v gap_h],表示子图之间垂直和水平间隙,单位是figure宽度的比例。marg_h:2元素向量[marg_bottom marg_top],底部和顶部留白。marg_w:2元素向量[marg_left marg_right],左侧和右侧留白。
返回值里,ha是所有axes句柄组成的向量,pos是对应的位置矩阵。这里最容易混淆的是gap和margin的顺序,尤其是gap,第一个值管垂直方向,第二个值管水平方向,和“垂直优先”的直觉有点冲突。
一个常用的模板:
[ha, pos] = tight_subplot(2, 3, [0.03 0.03], [0.08 0.05], [0.10 0.05]); for k = 1:6 axes(ha(k)); plot(1:50, sin((1:50) / k), 'LineWidth', 1.2); xlabel('t (ms)'); ylabel(sprintf('ch %d', k)); endgap的值怎么给?我的习惯是:gap的单位是figure短边的比例。比如figure默认尺寸是560x420,那0.03大约就是13个像素左右的间隙,视觉上属于“紧凑但又不挤”的程度。论文图我一般给[0.02 0.02],marg_h给[0.07 0.05],marg_w给[0.08 0.02]。但这没有绝对标准,因为xlabel和ylabel的字体、字号会影响实际效果,需要每套图微调一两次。
2.3 容易出问题的几个实际操作点
用tight_subplot看起来很省事,但有几个坑必须提前知道。
第一个坑是不要和subplot混用。在同一张figure里,用tight_subplot创建axes之后再调用subplot,新生成的axes会覆盖或干扰原有布局,造成排版错乱。建议一个figure里只用一种方案。
第二个坑是colorbar会破坏对齐。tight_subplot给每个子图分配的是固定位置,如果某个子图需要单独显示colorbar,colorbar会自动压缩当前axes的绘图区,导致这个子图和其他子图不再对齐。如果你需要多个子图共享一个色标,tight_subplot本身没有很好的解决方案,得手动把colorbar挪到figure右侧再调整位置,非常痛苦。
第三个坑是figure尺寸缩放。tight_subplot算好的位置虽然是比例化的,但如果你把figure拉得太扁,或者从屏幕尺寸改成PaperPosition指定的尺寸,xlabel和ylabel仍然可能出现重叠。导出前最好固定figure尺寸,不要随便拉伸。
第四个坑是切换axes的性能问题。axes(ha(k))这种方式在循环里每画一个子图都要做一次“把某个axes设为当前axes”的操作。如果循环里只是画几张图,完全不用在意,但如果你在循环里生成几千张图,这个开销会积累起来。实际项目中我一般会把tight_subplot创建axes的部分抽出来,把后续画图逻辑写成一个函数,减少重复切换操作。
3. tiledlayout:官方方案如何把“布局”当成一个对象来管理
3.1 官方引入tiledlayout的动机
R2019b版本开始,MathWorks官方推出了tiledlayout,随后在很多官方文档里都推荐用它替代subplot。它和subplot最大的区别在于:subplot只是“生成一个axes”,而tiledlayout把布局本身变成了一个对象。
你可以把它理解成,以前的subplot相当于手动把若干张纸贴在一面墙上,位置自己算;而tiledlayout相当于给你一块白板和一根网格尺,你只管往格子里放图,空隙和边距由白板统一管理。这种设计带来的最直接好处是:调整间距时不需要动任何axes的坐标,只需要设置布局对象的属性。
对使用者来说,最大的变化是写代码时不再需要关心每个子图的“物理坐标”是什么,只需要关心行列和顺序。这一点在你需要频繁调整图的结构时会非常明显。
3.2 两种布局模式:grid模式和flow模式
tiledlayout有两种布局模式。默认是'flow'模式,tile会根据figure的空间自动决定每排放几个子图。另一种是'grid'模式,指定行数和列数后严格按网格排。
对于论文图、实验报告图这种需要稳定结构的场景,我强烈建议手动指定grid模式,因为flow模式在你调整figure尺寸时,布局可能会自动重排,导致同一个脚本在不同的figure尺寸下生成的图结构不一样。这对出图来说是个不稳定因素。
grid模式的标准写法:
tl = tiledlayout(2, 3, 'TileSpacing', 'compact', 'Padding', 'compact'); for k = 1:6 ax = nexttile; plot(1:50, sin((1:50) / k), 'LineWidth', 1.2); xlabel('t (ms)'); ylabel(sprintf('ch %d', k)); end这里TileSpacing控制子图之间的间隙,Padding控制布局与figure边界之间的留白。两者可选值都包括'loose'、'compact'、'tight',后面还可以跟数值,比如'TileSpacing', 8表示以像素为单位指定间距。不过我不太建议用'tight',它会让两个子图之间的坐标轴刻度数字在视觉上非常靠近,尤其是xlabel和ylabel同时存在时,看起来会有点拥挤。论文图我一般用'compact'。
3.3 跨子图共享:colorbar、图例、坐标轴
tiledlayout最大的竞争优势,在于它原生支持跨子图共享colorbar、图例和坐标轴联动。这是tight_subplot和subplot都很难优雅实现的事情。
共享colorbar的写法很直接:
tl = tiledlayout(2, 2, 'TileSpacing', 'compact', 'Padding', 'compact'); for k = 1:4 ax = nexttile; imagesc(rand(20, 20)); end cb = colorbar(tl);这样只会在整个布局的右侧生成一个colorbar,所有子图共用同一个色标,而且不会挤压任何单个子图的绘图区。以前用subplot + colorbar的时候,要么每个子图都挂一个自己的colorbar,要么手动创建colorbar再挪动位置,非常繁琐。
共享图例的实现更简单:
lgd = legend(tl, {'Data A', 'Data B'});这一行会在布局底部或指定位置生成一个跨子图的图例,不需要在每个子图里重复添加图例。对于多子图对比实验,这能显著减少重复信息,让图面更干净。
坐标轴联动可以和tiledlayout混用。你可以取出不同tile的axes句柄,然后用linkaxes实现同步缩放:
ax1 = nexttile(1); plot(randn(50, 1)); ax2 = nexttile(2); plot(randn(50, 1)); linkaxes([ax1 ax2], 'x');这在subplot下也能做,但tiledlayout拿句柄的方式更直观,而且布局稳定后不会因为手动调整位置而破坏联动关系。
3.4 非均匀布局:span能力
R2021a开始,tiledlayout支持用span合并相邻的tile,实现“上面两个子图、下面一个大子图”这种非均匀布局。这在subplot里需要手动算Position才能实现,在tiledlayout里只需要一个参数。
tl = tiledlayout(2, 2); ax1 = nexttile(1); plot(1:10); ax2 = nexttile(2); plot(rand(10, 1)); ax3 = nexttile(3, [1 2]); % 占第3行的所有列 plot(1:20, cumsum(randn(20, 1)));第一个子图和第二个子图占据第一行的两列,第三个子图横跨第二行的两列。这种组合方式在做“整体趋势图 + 局部放大图”的时候特别常用。如果你需要在老版本Matlab里实现同样的效果,那只能用subplot + 手写Position,代码量和维护成本都会高很多。
4. 三套方案实测对比:代码量、出图效果与扩展性
4.1 同一张目标图的三种写法
为了让你直观看到差别,我拿一个具体场景做例子:2x2子图,每个子图画一条正弦曲线,带x和y轴标签,整体保持紧凑排版,最后导出300 dpi的PNG。
先看subplot + 手调Position的写法:
figure('Units', 'normalized', 'Position', [0.1 0.1 0.5 0.65]); pos = {[0.08 0.55 0.40 0.35], [0.55 0.55 0.40 0.35], ... [0.08 0.10 0.40 0.35], [0.55 0.10 0.40 0.35]}; for k = 1:4 ax = axes('Position', pos{k}); plot(0:0.01:2*pi, sin((0:0.01:2*pi) * k)); xlabel('rad'); ylabel('amp'); end这种写法最大的问题是,一旦你想调整间隙或边距,所有Position都要重新算一遍。而且这个示例还只处理了4个子图,如果子图数量增加到6个、9个,代码会迅速恶化成一团数字地狱。
再看tight_subplot的写法:
[ha, ~] = tight_subplot(2, 2, [0.08 0.08], [0.10 0.06], [0.10 0.04]); for k = 1:4 axes(ha(k)); plot(0:0.01:2*pi, sin((0:0.01:2*pi) * k)); xlabel('rad'); ylabel('amp'); end参数清晰,代码量明显减少。
最后看tiledlayout的写法:
tl = tiledlayout(2, 2, 'TileSpacing', 'compact', 'Padding', 'compact'); for k = 1:4 ax = nexttile; plot(0:0.01:2*pi, sin((0:0.01:2*pi) * k)); xlabel('rad'); ylabel('amp'); endcode量其实和tight_subplot差不多。差距不在第一层,而在于后续扩展。如果要在tight_subplot上加共享colorbar、共享图例,你需要手动处理很多事情;而tiledlayout只需要一两行代码。可以这么说:简单场景下三者差距不大,但越复杂的图,tiledlayout的优势越明显。
4.2 三种方案能力对比表
我把对比项列成一张表,方便你保存和查阅:
| 对比项 | subplot | tight_subplot | tiledlayout |
|---|---|---|---|
| 是否需要外部下载 | 否 | 是(File Exchange) | 否 |
| 支持版本 | 所有版本 | 所有版本 | R2019b及以上 |
| 统一间隙/边距 | 不支持 | 支持 | 支持 |
| 调整行列顺序 | 按索引,较麻烦 | 按索引,较麻烦 | nexttile可指定,方便 |
| 跨行跨列布局 | 需手写Position | 需手写Position | 原生支持span |
| 共享colorbar | 难 | 难 | 原生支持 |
| 共享图例 | 难 | 难 | 原生支持 |
| 导出兼容性 | 一般 | 一般 | 最顺滑 |
| 适用场景 | 快速调试 | 老版本/固定布局批量出图 | 新版本正式图表 |
这张表我建议重点看最后三行。如果你只是临时看一眼结果,subplot完全够用。但如果你要出正式图,尤其是投稿或提交报告用的图,tiledlayout的共享布局能力真的能节省大量时间。
4.3 导出质量的实测对比
导出图的质量决定着前面所有排版工作的最终呈现。实际操作中,我一般统一用exportgraphics导出,而不是老的saveas:
exportgraphics(gcf, 'demo.png', 'Resolution', 300);在这条命令下,tight_subplot和tiledlayout导出的边距分布都比较均匀,subplot默认留白的图则往往需要二次裁剪。原因其实不复杂:exportgraphics会按figure当前的布局和尺寸进行渲染,而subplot默认情况下四周留白太多,导出的PNG会出现大量无意义的空白区域。
还有个容易被忽略的点是PDF矢量导出。tiledlayout导出PDF时,文字和坐标轴的矢量信息保留得比较干净,放到论文里放大也不会糊。tight_subplot如果布局比例稍有偏差,打印时可能出现某个子图的轴线超出figure边缘的情况。此时你看到的屏幕效果和导出效果不完全一致,原因通常出在figure原来的LogicalScreenSize和导出的PaperSize不完全匹配。解决方法是导出前先固定figure的PaperSize:
set(gcf, 'PaperUnits', 'inches', 'PaperSize', [6 4]);这样导出的尺寸就是可控的,不会出现边角被裁掉的情况。
5. 选型建议与避坑清单
5.1 直接给结论:什么场景用哪个
根据我这几年画图的经验,选型可以按下面几条来:
- 如果Matlab版本低于R2019b,而且暂时不方便升级,那就直接用tight_subplot。它无依赖、易搬运,是老版本环境里最可靠的紧凑排版方案。
- 如果是R2019b及以上版本,并且做的是论文图、汇报图、图像处理作业、深度学习可视化这类正式内容,直接上tiledlayout。花十几分钟熟悉nexttile的用法,之后所有出图流程都会省力。
- 如果只是在命令行窗口想快速看几个变量之间的大致关系,subplot完全够了,没必要为了临时调试去做紧凑排版。
- 如果手里有一套基于subplot的旧代码,不建议一次性重写。可以先把画图核心逻辑封装成函数,内部换用tiledlayout或tight_subplot,这样逐步替换,风险可控。
5.2 实战里最容易被忽略的排版坑
以下坑都是我在实际出图过程中踩过的,列出来供你参考。
第一,中文字体问题。Matlab默认字体对中文支持不好,直接在图里写中文,导出后可能变成方框。解决方法是在导出前统一设置字体:
set(findall(gcf, '-property', 'FontName'), 'FontName', '微软雅黑');这句会把当前figure里所有对象的FontName统一替换掉,比一个个set省事得多。
第二,多个子图都加了legend,导致整个图面拥堵。如果你的子图很多,建议优先使用tiledlayout跨子图图例,而不是每个子图内放一个legend。每个legend都会吃掉一部分绘图区,图面会变得很挤。
第三,nexttile和subplot不要混用。在同一个figure里,如果先用subplot创建了几个axes,再用nexttile添加tile,生成的布局会非常混乱,甚至出现多余的空框。我的建议是一个figure里自始至终只用一种接口。
第四,TileSpacing设置得太紧,同时xlabel文字又很长,刻度标签很容易互相挤压。解决办法不是把子图继续往中间挤,而是增大底部留白,比如调大Padding或marg_h的值。
第五,使用tight_subplot时删除某个axes会导致句柄错位。比如你删掉了ha(2),后续循环再访问ha(3)时,就会遇到一个已经失效的句柄。最好用新的索引重新建立列表,或者只在全部画完后再删。
第六,导出时不要用saveas。saveas保存的是当前屏幕所见的位置关系和视图状态,输出质量不稳定。用exportgraphics会更干净,导出参数也更丰富,比如'Resolution'、'ContentType'、'BackgroundColor'等都能控制。
5.3 一个提升效率的封装思路
如果你经常要生成同一套样式的图,可以考虑自己写一个简单封装函数:
function tl = myGrid(rows, cols) tl = tiledlayout(rows, cols, 'TileSpacing', 'compact', 'Padding', 'compact'); set(gcf, 'Color', 'w'); end这样每次新建figure只需要:
tl = myGrid(2, 2); for k = 1:4 ax = nexttile; plot(1:50, randn(50, 1), 'LineWidth', 1.2); grid on; xlabel('time (s)'); ylabel(sprintf('trial %d', k)); end后续如果要调整整体样式,比如统一字体、统一坐标轴粗细,只需要改封装函数内部,所有调用它的脚本都会自动生效。这是对老项目改造时风险最小的切入方式,也是批量出图场景下维护成本最低的做法。
最后再分享一点个人体会
做完这轮对比之后,我对子图排版的思路基本固化成了一条:新代码无脑用tiledlayout,老代码在不重写的前提下优先用tight_subplot,subplot只用来临时看效果,不再拿它出正式图。另外有个小建议,无论用哪种方案,正式导出前都先放大figure窗口看一眼坐标轴有没有被挤出画布,尤其是脚本里设置了figure的Position时。很多排版问题在屏幕上不容易发现,一旦导出成PDF或者PNG,边角异常就藏不住了。排版这种事,表面上是代码问题,本质上是在平衡信息密度和审阅体验。花一晚上把tiledlayout的核心用法搞清楚,之后每一次出图都会把这部分时间省回来。