如果你已经会用 plot、subplot 画出一张像样的二维曲线,说明 MATLAB 绘图的第一层功力算是练成了。但真到了做数据分析、写论文插图、给甲方汇报的阶段,你会发现光会“画出来”远远不够,还得会“控制它”。这篇咱们接着上一篇的基础,把 MATLAB 图形可视化再往深挖一层,重点聊图形句柄、颜色映射、三维曲面、图形标注与导出这几个绕不开的核心点。适合三种人看:刚学完基础绘图但一调格式就头疼的初学者、想把手工作图流程改成自动化产图脚本的人、以及想弄明白为什么自己画出来的三维图总比别人“灰蒙蒙”一层的使用者。
1. 图形句柄:从“会画图”到“完全控制图形”
1.1 为什么必须掌握句柄
很多人学 MATLAB 绘图,停在了“plot 完就结束”的阶段。这倒不是态度问题,而是基础教程通常只教怎么把图画出来,没教怎么把图“拆开”看。
我的理解是,MATLAB 里每一条线、每一个坐标轴、每一个标题,本质上都是一个对象。这些对象像一间间房间,句柄就是房间的门牌号。你有了门牌号,才能进去改陈设、换家具。没有门牌号,你就只能站门外看着默认装修。
实际项目中什么场景需要门牌号?举个例子。你画了一组折线图,默认线宽是 0.5,但你要投期刊,编辑部要求线宽至少 1.0。你要是重新运行一遍代码,在 plot 里加 LineWidth 参数,那还行。但如果你画的是几十条曲线,散落在脚本各个角落,甚至有些图是用循环动态生成的,修改所有 plot 调用就非常痛苦。这时候你把当前坐标轴和线对象统一存下来,用 set 批量改,几行就搞定。
更关键的是交互式改造。别人发来的 .fig 文件,你能用 openfig 打开,通过 findobj 找到特定曲线,直接改数据或者改格式。这种“事后修改”能力在职场上比重新画一遍重要得多。
1.2 获取句柄的几种常规方式
说白了就五个常用函数:gcf 拿当前图窗、gca 拿当前坐标轴、gco 拿当前点击对象、findobj 按属性找对象、绘图函数本身返回对象句柄。
我实际用得最多的是绘图函数返回值。比如:
fig = figure('Position', [100 100 800 600]); ax = axes('Parent', fig); hLine = plot(ax, t, y, 'LineWidth', 1.2);这一小段代码把图窗、坐标轴、曲线三个层级的句柄全握在手里了。后面想改颜色、改线型、改坐标范围,直接:
set(hLine, 'Color', [0.8500 0.3250 0.0980], 'LineStyle', '--'); set(ax, 'FontName', 'Cambria', 'FontSize', 11); set(ax, 'XLim', [0 10], 'YLim', [0 1]);有些老读者习惯用xlabel('time')这种“命令行式”写法,这没问题,但如果你需要对 xlabel 对象本身改字号、改位置,就得把返回句柄存下来:
hx = xlabel('时间 t (s)'); set(hx, 'FontSize', 14, 'FontWeight', 'bold');理解句柄层级还有一个好处:不会把坐标轴对象和图形对象搞混。有人 set(gca, 'Color', 'red'),发现坐标轴区域变红了,以为是曲线变红了,其实是把坐标轴背景色改了。这种小误会很常见,本质就是没分清父对象和子对象。
1.3 子图与坐标轴精确操控
subplot 大家都用过,但它有个明显短板:默认的间距和位置是自动计算的,不够灵活。你想做一个 2×2 子图矩阵,希望第一行两张图大一点、第二行两张图小一点,subplot 就力不从心了。
这时候直接用 axes 指定位置更痛快。坐标轴位置是一个四元素向量 [left bottom width height],单位是归一化的图窗比例,取值范围 0 到 1。
fig = figure('Position', [100 100 900 700]); ax1 = axes('Parent', fig, 'Position', [0.06 0.55 0.40 0.36]); ax2 = axes('Parent', fig, 'Position', [0.55 0.55 0.40 0.36]); ax3 = axes('Parent', fig, 'Position', [0.06 0.08 0.85 0.36]);这种写法把第三张图拉得很宽,适合画需要横向延展的时序波形或者频谱图。
还有一种常见需求:两个数据量纲不同,要共用横轴但纵轴分别显示,典型做法是用 yyaxis。这个函数本质上也返回两个坐标轴句柄,比如:
ax = gca; ax.YAxis(1).Color = 'k'; ax.YAxis(2).Color = [0 0.4470 0.7410];如果你在写复杂 app,需要动态增删子图,务必记得把创建出来的坐标轴句柄保存到数组里,否则后续很难找到它们。
实操建议:作图脚本里尽量给 figure、axes、每次 plot 都赋句柄变量。一开始觉得啰嗦,等你回头改第三稿、第四稿时,才知道这几个变量能救命。
1.4 常用对象属性速查
这部分我整理了一个自用属性表,不一定全,但覆盖了我日常项目的八成需求。
| 对象 | 常用属性 | 典型用途 |
|---|---|---|
| Figure | Color, Position, Resize, Toolbar | 控制窗底色、位置、是否显示工具栏 |
| Axes | XLim, YLim, FontSize, Box, TickDir | 设置坐标范围、字体、边框方向 |
| Line | Color, LineWidth, LineStyle, Marker, MarkerSize | 曲线样式与标记 |
| Surfaces | FaceAlpha, EdgeAlpha, FaceColor, EdgeColor | 透明度与颜色填充方式 |
| Text | String, Position, HorizontalAlignment, FontSize | 文本位置与对齐方式 |
| Legend | Location, NumColumns, FontSize, TextColor | 图例位置与列数排版 |
记这些属性不用死背,关键是要看得懂帮助文档。遇到不清楚的属性,直接在命令行敲properties(对象)或者get(gca),就能看全当前对象所有可设置的属性名称和当前值。
2. 颜色映射与数据到色彩的转换
2.1 colormap 不只是换颜色
MATLAB 默认的 parula 色带我认为设计得相当不错,兼顾了视觉均匀性和色盲友好。但实际工作中,很多时候需要用特定色带表达语义,比如热力图的温度梯度、医学图像的灰度、降雨量图的蓝绿渐进,这时候 colormap 相关函数就派上用场。
colormap 本质是建立数据值和颜色之间的映射关系。默认情况下,坐标轴会根据当前所有图形对象的数据范围自动决定颜色映射的上下限。问题也出在这个“自动”上。你有两张子图,左侧数据范围是 0 到 100,右侧数据范围是 0 到 10000,如果各自自动映射,颜色就不可比。读者会以为两个图颜色相同就代表数值相同,这是很大的误导。
解决办法是用 clim 函数统一颜色范围。R2022a 之前的版本里,这个功能叫 caxis,现在官方推荐用 clim。
tiledlayout(1,2); nexttile; imagesc(data1); clim([0 10000]); nexttile; imagesc(data2); clim([0 10000]);这样两张图就采用同一个颜色标尺。
2.2 自定义色带与 colorbar 美化
MATLAB 自带几十种 colormap,但在品牌色、期刊指定色面前,往往还得自己调。
自建色带最直观的方法是用 colormap 接受一个矩阵,矩阵每行是 RGB 三元组。我常用[0 0.4470 0.7410; 1 1 1; 0.8500 0.3250 0.0980]这种三色渐变。如果你想要平滑渐变,可以写个小函数插值:
colorList = [0 0.4470 0.7410; 0.9290 0.6940 0.1250; 0.8500 0.3250 0.0980]; numColors = 256; cmap = interp1(linspace(0,1,size(colorList,1)), colorList, linspace(0,1,numColors)); colormap(gca, cmap);colorbar 的可用性也很高。常见调整包括:
cb = colorbar; cb.Label.String = '温度 (°C)'; cb.FontSize = 12; cb.Ticks = [0 25 50 75 100]; cb.TickLabels = {'0','25','50','75','100'};一个容易被忽视的问题是 colorbar 对应的是当前坐标轴还是图窗。在 tiledlayout 布局里,colorbar如果不带第二个参数,默认只给当前子图。想给整张图统一一个 colorbar,得用colorbar('Layout', 'north')或者手动指定位置,具体取决于你用的布局函数。
2.3 用伪彩色图展示矩阵数据
提到颜色映射,就绕不开 imagesc 和 pcolor。我的经验是:矩阵型数据要看大面趋势,用 imagesc 最省事;如果像素之间还需要叠加等高线或者矢量场,pcolor 更合适。
[X, Y] = meshgrid(-2:0.1:2); Z = X .* exp(-X.^2 - Y.^2); imagesc(X(1,:), Y(:,1), Z); axis xy; colormap('turbo'); clim([-0.5 0.5]); colorbar;注意axis xy这行。imagesc 默认把数据从上往下渲染,坐标轴的 Y 轴方向是反的,加上 axis xy 能让 Y 轴方向恢复为数学上习惯的“从下往上”。
半透明叠加也是可视化中常用手段。比如一个热度图上要叠加标记点,透明效果会好很多。可以用alpha调整对象透明度:
s = scatter(x, y, 30, z, 'filled'); alpha(s, 0.6);这个操作在散点数量多、重叠严重时,能明显改善图的阅读体验。
3. 三维图形可视化:从网格到曲面再到切片
3.1 meshgrid 是三维绘图的根基
很多初学者对 meshgrid 有误解,以为它是在生成“三维网格”,其实它只是把一维坐标转换成了二维网格矩阵。举个例子,你有一个时间轴 t 从 0 到 10,取 20 个点;还有一个纵轴 s 从 0 到 5,取 10 个点。你想画出平面上每个网格点的函数值,就必须把 t 和 s 组合成网格坐标。meshgrid 干的就是这件事:
t = linspace(0, 10, 20); s = linspace(0, 5, 10); [T, S] = meshgrid(t, s); Z = sin(T) .* cos(S);生成的 T 是横坐标矩阵,S 是纵坐标矩阵,Z 是函数值矩阵。三维绘图的本质就是在一张二维网格上,用高度和颜色表达第三个维度。
这里有个网上教程讲得不多的细节:meshgrid 默认的输出维度顺序是[X, Y],其中 X 按行变化,Y 按列变化。如果你熟悉ndgrid,会发现两者维度顺序正好相反。写代码时如果总是不对,怀疑一下是不是两个函数混用了。
3.2 mesh、surf、surfl 三种渲染方式的选型
同样是画三维曲面,mesh 画出来是线框,surf 画出来是填充曲面,surfl 则是在 surf 基础上加了光照效果。
mesh 适合看网格结构,学术论文里偶尔用来展示数值模型的离散节点。surf 是最常用的,适合看趋势。surfl 适合做展示图,但用不好容易让颜色失真,我一般慎用。
[X, Y] = meshgrid(-3:0.25:3); Z = peaks(X, Y); figure; subplot(1,3,1); mesh(X, Y, Z); title('mesh 线框'); subplot(1,3,2); surf(X, Y, Z); title('surf 填充曲面'); subplot(1,3,3); surfl(X, Y, Z); shading interp; colormap('parula'); title('surfl 光照着色');如果你发现 surf 出来的曲面有“马赛克感”,那不是数据问题,是网格太粗或者着色方式是 flat。可以调成shading interp,让颜色在相邻网格顶点之间平滑过渡。
3.3 等高线图与等高线填充图
等高线本质上是三维信息压缩到二维的一种表达。MATLAB 里有 contour 和 contourf,前者只画线,后者在线的间隙填充颜色。两者我都常用。画的时候要注意,如果 Z 的范围跨度过大,默认的等高线数量可能不够,可以自己指定contour(Z, [0.2 0.4 0.6 0.8]),写在第二个参数里的是你要精确画的等高线值。
还可以把二维等高线和三维曲面画在一起。比如用 contour3 在三维曲面上投影等高线:
surf(X, Y, Z, 'FaceAlpha', 0.7, 'EdgeColor', 'none'); hold on; contour3(X, Y, Z, 15, 'k');这样曲面和等高线同时呈现,既能看到高低趋势,又能看到具体数值位置。
3.4 三维数据的切片与局部观察
当你拿到一个体数据(比如流场数据、医学影像三维矩阵),直接画全景三维图往往很乱,更合理的做法是观察某个截面。
slice 函数可以很方便地在自定义平面上显示数值切片:
[x, y, z] = meshgrid(-2:0.2:2, -2:0.2:2, -2:0.2:2); v = x .* exp(-x.^2 - y.^2 - z.^2); xslice = [-1 0 1]; yslice = []; zslice = 0; slice(x, y, z, v, xslice, yslice, zslice);这个命令会画出 x 方向三个切面和 z=0 的一个切面,配色自动。切片图在项目汇报时特别有用,尤其是你要展示一个三维现象在不同截面的取值时,比一上来就甩一个完整三维立体图要清楚得多。
3.5 三维视角的调节
很多人画的图不好看,不是数据不好,而是视角不对。视点默认在很多场景下会挡住关键区域。
调整视角用 view 函数,它接受两个参数,第一个是方位角(azimuth),第二个是仰角(elevation)。方位角是观察点在 x-y 平面上的角度,0 度表示从 x 轴正方向看,90 度表示从 y 轴正方向看;仰角表示观察点相对水平面的高度,正值是俯视图。
view(45, 30); % 常见三维视角 view(0, 90); % 等效于二维俯视图 view(180, 0); % 从背面看配合axis equal也很重要。三维数据三个轴的实际刻度范围可能差异很大,不加 axis equal,球体画出来可能像橄榄球。
4. 图形标注、注释与排版细节
4.1 text 和 annotation,两个坐标系两种逻辑
图形里添加文字有两种主要方式。text 的坐标是数据坐标,也就是说你指定什么坐标,文字就放在什么数据值对应的位置。annotation 的坐标是图窗归一化坐标,0 到 1 之间,不考虑当前坐标轴的范围,直接定在画布的特定位置。
我用 text 比较多,因为文字能跟着数据走,缩放坐标轴时文字还贴在同一个数据点旁边。但 text 也有一个问题:如果数据范围变化,文字位置可能跑出可视区域。要避免这种情况,可以把位置绑定到坐标轴范围上:
ax = gca; xr = xlim(ax); yr = ylim(ax); text(ax, xr(1)+0.02*diff(xr), yr(2)-0.05*diff(yr), '左上角标注', 'Units', 'data');annotation 适合做图内注释框、箭头、引用线。因为它是按图窗比例定位的,无论坐标轴怎么动,注释都待在图纸的固定位置。
4.2 图例排版与多列图例
图例太挤的时候,很多人只会调字号、调字体,其实还有几个参数很实用。
NumColumns 可以设置图列数。四条曲线并排很长,可以改成两列,图例就方方正正的了。Location 还可以改成方位的英文缩写,比如 'northoutside' 表示在图上方外侧。
图例框背景色和边框也很容易顺手改掉,让图更干净:
lgd = legend({'信号A','信号B'}, 'NumColumns', 2, 'Location', 'northoutside'); lgd.Box = 'off'; lgd.BackgroundColor = 'none';4.3 数据探查与动态交互
如果只是自己看数据,不一定要导图,直接在图上用数据游标更高效。开启数据游标的命令是 datatip。交互式点击后,会弹出一个信息框,显示点的坐标和索引。
如果想自动在所有波峰处放置数据提示,可以配合 findpeaks 或逻辑索引来定位点。还有一个常用于动态图的技巧:用datacursormode打开游标模式,再设置光标回调函数,实现鼠标划过曲线时显示自定义文本。这个技能我是在做信号分析工具时学会的,写论文插图时也很实用。
dcm = datacursormode(gcf); dcm.UpdateFcn = @(src, evt) customDataTip(evt); function out = customDataTip(evt) pos = evt.Position; out = sprintf('波峰值: %.3f\n时间点: %.3f s', pos(2), pos(1)); end这样鼠标点哪里,就会显示哪里的解析值,省去了反复在命令行看坐标的麻烦。
5. 图形输出:从截图到一键生产
5.1 导出图片的工具选择
早些年我也用过截图工具直接抠图,但很快就放弃了,因为分辨率不足、字体发虚、白边难处理。现在 MATLAB 推荐用 exportgraphics,这是 R2020a 之后非常好用的函数。
exportgraphics(gcf, 'output.pdf', 'ContentType', 'vector'); exportgraphics(gcf, 'output.png', 'Resolution', 300);导出 PDF 的好处是矢量图,文字和线条放大不会糊。要是投期刊要求 tiif 或 eps,那还是先把图调到合适尺寸,再用print输出。print是老牌函数,功能强大,但坑也比较多。
print常用的格式大概是:
print(gcf, '-dpng', '-r300', 'output.png'); print(gcf, '-dpdf', '-r300', 'output.pdf');-r300代表 300 DPI,打印出来清晰。如果导出的是矢量化 PDF,分辨率这个参数影响不大。
5.2 一键导出多尺寸图片
实际汇报场景中,同一个图往往需要多种尺寸:PPT 里放一张宽图,Word 里放一张窄图,印刷件又要高分辨率 PNG。手动调一次还行,天天调就烦了。我习惯写一个小函数,把常用尺寸和格式包装进去。
function exportFig(fig, baseName) exportgraphics(fig, [baseName '.png'], 'Resolution', 300); exportgraphics(fig, [baseName '_wide.png'], 'Resolution', 150); end这样每次画完图,直接一句 exportFig(gcf, 'result'),连同宽窄版一起导出来。
5.3 导出图片的常见问题
导出过程中最容易翻车的三个地方:
第一是中文字体缺失或者变成方块。MATLAB 默认字体里某些中文字符显示不了,这时可以先设置坐标轴的 FontName 为中文字体,比如set(gca, 'FontName', 'SimHei'),或者干脆在导图前临时切换到支持中文的字体。
第二是图片白边过大。exportgraphics 相比 print 已经自动裁剪了大部分白边,但偶尔也会保留一圈空白。如果发现还有边距,可以在导出前手动设置图窗的 PaperOrientation 和 PaperPosition,或者先把图形界面调整紧凑些。
第三是图层顺序问题。有些标记和文字会被曲面挡住,导出的图和屏幕显示不完全一致。处理办法是调整绘制顺序,或者用 uistack 手动调上下层关系。
6. 常见问题速查与避坑指南
6.1 图窗空白或显示异常
最常见的现象是执行完脚本,什么都没弹出来。原因八成是图窗句柄被覆盖或者屏幕外位置导致看不到。比如代码设置了 figure 的 Position,把 y 坐标设置为负数,图窗就跑到了任务栏下面。解决方法是执行figure('Position', [100 100 800 600])强制把图窗放回可见区域。
还有一种情况是用了close all后马上画图,但图窗没有刷新,尤其是循环画图时。可以在循环末尾加drawnow强制刷新。
6.2 大数据量绘图卡顿与优化
画十万个点,MATLAB 还能扛;画上百万个点,交互旋转时就会明显卡顿。这时可以用三个思路优化。
一是降采样。用downsample或自行隔点取值,但要注意不能漏掉尖峰特征。二是用 scatter 和 plot 替代过于复杂的 surface 对象,减少图形对象数量。三是利用reducepatch减少三角网格量:
patchObj = patch('Faces', F_orig, 'Vertices', V_orig, 'FaceColor', 'flat'); reducepatch(patchObj, 0.2);这种优化对三维网格特别有效,从 100 万个三角面片压到 20 万,视觉差异很小,性能提升明显。
6.3 统一绘图风格
团队协作时,最怕十个人交过来十种风格的图。写一个 style 函数统一设置默认图形参数,是性价比最高的动作。
function applyMyStyle() set(groot, 'DefaultAxesFontSize', 12); set(groot, 'DefaultAxesFontName', 'Helvetica'); set(groot, 'DefaultLineLineWidth', 1.5); set(groot, 'DefaultSurfaceEdgeColor', 'none'); set(groot, 'DefaultFigureColor', 'w'); end在脚本开头调用一次,所有后续创建的图形都会继承这些默认值。我自己的项目里还会顺便设置颜色矩阵,让同类数据在不同图里始终用同一种颜色。
6.4 排查图形问题的通用思路
图形问题现场排查,我的顺序是这样:先看数据范围有没有异常,再查是否误用了坐标轴对象,最后检查颜色映射是否被上次的操作残留影响。很多时候图“不对”,并不是代码逻辑问题,而是上一次脚本运行残留的对象或变量污染。
执行cla; close all; clear;这个组合拳,可以解决八成莫名其妙的问题。注意 clear 会把工作区变量都清掉,如果后面还要用数据,就只执行close all和cla。
最后分享一个个人习惯:每一个复杂可视化项目里,我会单独建一个plot_helpers函数文件夹,把风格设置、色带生成、多尺寸导出、图例排版封装成几个小函数。后续新项目只需要复制这个文件夹改参数,就能快速得到风格统一、质量达标的图。你可以把这种做法当成自己工具箱的起点,慢慢扩充。图最终是给人看的,图的背后是数据逻辑和审美判断,MATLAB 只是把这两者表达出来的工具。