这一章在干嘛?

第 3 章你学会了 plot,第 11 章你拿到了句柄这把「万能钥匙」——本章就是把两者合体:用句柄图形系统精确指挥每一种图。我们会过一遍 MATLAB 的二维图全家桶(柱图、面积图、火柴杆图、直方图、饼图、对数坐标),进入三维世界(plot3、mesh、surf、contour 思路),然后拆开图形的「底层积木」line/text/rectangle/patch,最后用文件数据 + 函数句柄做几个完整的实战绘图程序,顺带学 comet/movie 两种动画。学完本章,你能把任何数据画成任何想要的样子。

12.1 一份数据,四种画法:plot / bar / area / stem

12.2 直方图与饼图

12.3 对数坐标

12.4 动画基础:comet 与 movie

12.5 图形属性定制:柱宽与爆炸饼图

12.6 三维绘图

12.7 核心图形对象:line / text / rectangle / patch

12.8 绘图实战:函数绘图与文件数据绘图

12.9 保存与打印绘图

12.10 通关标准与自测

12.1 一份数据,四种画法:plot / bar / area / stem

到目前为止你用熟了 plot(折线)和 bar(柱状图),其实它们是一家人:同一份 - 数据,不同视觉编码bar 画竖直柱状图,barh 画水平柱状图,area 画连续曲线并填充曲线下方区域,stem 画火柴杆图(stem plot,每根杆顶一个圆圈)。什么时候用哪个?看你想强调什么:趋势用 plot,比较离散量用 bar,强调累积量用 area,强调单点位置用 stem。这不是审美问题而是信息设计问题——同一份数据,换一种图型,读者的注意力就被引向不同的结论:柱状图诱导观众「比较相邻柱子的高低」,面积图诱导观众「感受总量随时间的累积」,火柴杆图诱导观众「逐点检查数值」。选错图型,等于把结论埋在了噪音里,这也是教材编程规范里「按需挑选最能突出关键信息的图型」的含义。

下面这个脚本用 subplot 布局,把同一份人口数据画成四种图(subplot 第三个参数是矩阵里的行优先编号——注意这和 MATLAB 矩阵「按列展开」的习惯相反,是个易错点):

% subplot_types.m
year = 2016:2020;
pop  = [0.9 1.4 1.7 1.3 1.8];
subplot(2,2,1)
plot(year, pop)
title('plot');   xlabel('Year'); ylabel('Population (mil)')
subplot(2,2,2)
bar(year, pop)
title('bar');    xlabel('Year'); ylabel('Population (mil)')
subplot(2,2,3)
area(year, pop)
title('area');   xlabel('Year'); ylabel('Population (mil)')
subplot(2,2,4)
stem(year, pop)
title('stem');   xlabel('Year'); ylabel('Population (mil)')

plot 折线图:2016–2020 年人口变化趋势,适合看走势

bar 柱状图:同样数据画成五根竖柱,适合逐年比较大小

area 面积图:曲线下方的区域被填充,强调累计规模

stem 火柴杆图:每个数据点是一根杆加一个圆圈,突出单个数据点的位置

用循环 + cell 数组消灭重复代码

上面四段几乎一样的代码可以压缩成一个循环——把函数名存进 cell 数组,用 eval 拼调用(教材示范写法,生产代码更推荐 str2func,见 12.8 节):

titles = {'plot', 'bar', 'area', 'stem'};
for i = 1:4
    subplot(2,2,i)
    feval(titles{i}, year, pop)
    title(titles{i}); xlabel('Year'); ylabel('Population (mil)')
end

顺便回顾第 7 章的教训:eval 能跑但难调试,str2func + 函数句柄是更安全的等价物。

12.1.1 矩阵数据画柱状图:分组与堆叠

bar 传一个矩阵,它会按行分组画柱,自动循环颜色区分每列。比如两行四列的年龄数据:

>> groupages = [8 19 43 25; 35 44 30 45];
>> bar(groupages)
>> xlabel('Group'); ylabel('Ages')

矩阵数据的分组柱状图:第 1 组和第 2 组各有四根柱(四列数据),颜色循环区分

'stacked' 选项则变成堆叠柱状图——每列的值摞起来,柱顶高度等于该行所有值之和:

>> bar(groupages, 'stacked')
>> xlabel('Group'); ylabel('Ages')

堆叠柱状图:第 1 组总和约 95、第 2 组总和约 154,每段颜色代表矩阵的一列

常见坑:barh 的轴标签是「横着」的

barh 里数据的角色互换:数据值沿 x 轴、类别沿 y 轴。所以 xlabel/ylabel 要跟 bar 写反,初学者照抄模板就会标错轴。

12.2 直方图与饼图

12.2.1 histogram:数据分布一眼看穿

直方图(histogram)是特殊的柱状图:把向量里的值按范围装进(bin),柱高表示落在每个箱里的数的个数——它回答的是「数据怎么分布」,而不是「每个点是多少」。(R2014b 起新函数 histogram 取代了旧的 hist。)

>> quizzes = [10 8 5 10 10 6 9 7 8 10 1 8];
>> histogram(quizzes)
>> xlabel('Grade'); ylabel('#'); title('Quiz Grades')

12 个测验成绩的直方图:自动分成 10 个箱,10 分出现 4 次、8 分出现 3 次,分布一目了然

histogram 返回的句柄同样可以存进变量查看和修改属性——句柄思想在每种图上都通用:

注意直方图和普通柱状图的本质区别:柱状图的 x 轴是类别(哪一年、哪个部门),每根柱的高度来自一个独立的数;直方图的 x 轴是数值本身,数据被切箱统计,柱高是频数。把「成绩列表」画成 bar 会得到 12 根杂乱无章的柱(每根是一次测验),画成 histogram 才能看到「分数集中在 8~10 分」这个真正有用的结论。分析数据分布时,先想 histogram。

>> hhan = histogram(quizzes)
hhan =
  Histogram with properties:
             Data: [10 8 5 10 10 6 9 7 8 10 1 8]
           Values: [1 0 0 0 1 1 1 3 1 4]
         NumBins: 10
        BinEdges: [0.5 1.5 2.5 3.5 4.5 5.5 6.5 7.5 8.5 9.5 10.5]
        BinWidth: 1
       BinLimits: [0.5 10.5]
    Normalization: 'count'
       FaceColor: 'auto'
       EdgeColor: [0 0 0]
 
     Show all properties

Values 直接告诉你每个箱的频数,做统计描述时非常好用(更多统计内容在第 14 章)。想指定箱数用 histogram(vec, n)

12.2.2 pie:占比与标签

pie(vec) 画饼图:每个元素占总和的百分比,从圆顶开始逆时针排布。向量 [11 14 8 3 1] 总和 37,所以 11 占 30%、14 占 38%……:

>> pie([11 14 8 3 1])

饼图自动标注百分比:11→30%、14→38%、8→22%、3→8%、1→3%,从顶部逆时针排布

再传一个字符串 cell 数组当第二参数,百分比就被替换成你的标签:

>> pie([11 14 8 3 1], {'A', 'B', 'C', 'D', 'F'})

带标签的饼图:cell 数组里的 A/B/C/D/F 替换了百分比标签

饼图的百分比是「自动算」的

你传给 pie 的永远是原始数量,不是百分比——MATLAB 替你除以总和。所以别先把数据归一化再传进去,那样等于除了两次。

12.3 对数坐标

plot 的两根轴都是线性刻度。但信号处理、频率响应这类场景里,数据的数量级横跨 ,线性轴上小数值会被压成一条贴地的直线。MATLAB 备了三个函数:loglog(双对数)、semilogy(y 轴对数)、semilogx(x 轴对数)。看对比:

>> subplot(1,2,1)
>> plot(logspace(1,10))
>> title('plot')
>> subplot(1,2,2)
>> semilogy(logspace(1,10))
>> title('semilogy')

同一组跨 9 个数量级的数据:左图线性轴上几乎全被顶部压扁,右图 semilogy 的对数 y 轴把它拉成一条近似直线

logspace(1,10) 生成从 的对数均匀间隔向量。线性轴下曲线的「前半生」完全看不清;换 semilogy 后,指数增长被「拉直」——对数坐标的价值就在这。

三个函数怎么选?口诀是「哪根轴跨数量级,就把哪根轴换成对数」:只有 y 跨数量级(比如电路的幅频响应)用 semilogy;只有 x 跨(比如频率从 1 Hz 扫到 1 MHz)用 semilogx;两边都跨(比如 Bode 图、幂律拟合)用 loglog。它们与 plot 的参数用法完全一致,换函数就行,零学习成本。

指数增长在对数轴上是直线

这不只是好看:如果 semilogy 画出来近似一条直线,就说明数据服从指数规律 ,因为两边取对数后 成线性关系。判断「数据是不是指数型」最快的方法就是对数轴一画。

12.4 动画基础:comet 与 movie

动画效果没法印在书页上,务必亲手敲进 MATLAB 跑一遍。

方式一:comet——彗星轨迹。对采样足够密的 ,comet 会从第一个点开始,逐点画出轨迹,后面拖着一条「彗尾」:

>> x = -2*pi : 1/100 : 2*pi;
>> y = sin(x);
>> comet(x, y)

跑完后静止画面与 plot(x,y) 相同,但过程是动画——演示函数曲线「如何被画出来」特别直观。

方式二:getframe + movie——帧录制。在循环里每帧画一点,用 getframe 抓屏存进矩阵,最后 movie 回放:

% sinmovie.m —— 播放 sin 函数的「电影」
clear
x = -2*pi: 1/5 : 2*pi;
y = sin(x);
n = length(x);
 
for i = 1:n
    plot(x(i), y(i), 'r*')
    axis([min(x)-1 max(x)+1 min(y)-1 max(y)+1])  % 固定坐标轴!
    M(i) = getframe;
end
movie(M)

常见坑:动画坐标轴「呼吸抖动」

录帧时如果不固定 axis,MATLAB 每帧都会根据当前数据点自动调整坐标范围,画面就会缩缩放放像抽风。循环前用 axis 把坐标范围钉死(如上例用 min/max 外扩一点),所有帧共用同一坐标系,动画才平滑。

12.5 图形属性定制:柱宽与爆炸饼图

12.5.1 柱宽:bar/barh 的第三参数

bar(x, y) 默认柱宽 0.8。把宽度当第三参数传进去:小于 1 柱间留缝,等于 1 柱子贴在一起,大于 1 柱子互相重叠

year = 2016:2020;
pop  = [0.9 1.4 1.7 1.3 1.8];
for i = 1:4
    subplot(1,4,i)
    barh(year, pop, 0.4 + i*0.2)   % 宽度依次 0.6/0.8/1.0/1.2
    title(sprintf('Width=%.1f', 0.4 + i*0.2))
    xlabel('Population (mil)'); ylabel('Year')
end

四个不同柱宽的 barh 对比:0.6 有明显间隙,1.0 柱子相连,1.2 开始互相重叠

12.5.2 爆炸饼图:把最抢眼的扇区「拉出来」

pie 的第二参数也可以是逻辑向量:为 true 的扇区会从饼里「炸」出来分离。结合 max 可以自动突出占比最大的扇区:

>> gradenums = [11 14 8 3 1];
>> letgrades = {'A', 'B', 'C', 'D', 'F'};
>> which = gradenums == max(gradenums)
which =
     0     1     0     0     0
>> pie(gradenums, which, letgrades)
>> title(strcat('Largest Fraction of Grades: ', letgrades(which)))

爆炸饼图:B 扇区(占比最大)被分离出来,标题由 strcat 自动拼出「Largest Fraction of Grades: B」

这个例子还展示了标题里动态生成内容的套路:letgrades(which) 是逻辑索引取出 'B'strcat 把它拼进标题——数据变了标题自动跟着变。

12.6 三维绘图

三维函数大多和二维版同名,只是加个「3」:plot3bar3bar3hpie3comet3stem3

12.6.1 plot3:三维空间中的点与线

plot3 传三个坐标向量即可:

>> x = 1:5;
>> y = [0 2 4 11 3];
>> z = 2:2:10;
>> plot3(x, y, z, 'k*')
>> grid
>> xlabel('x'); ylabel('y'); zlabel('z')
>> title('3D plot')

plot3 画出的三维散点:五个黑色 * 悬浮在带网格的三维空间中,z 轴从 2 到 10

画完点工具栏的旋转图标(rotate 3D),按住鼠标可以任意转视角——三维图不转着看等于白画。zlabel 负责给 z 轴加标签,grid 加的三维网格能帮你建立空间感。想用代码固定视角,用 view(az, el)az 是方位角(绕 z 轴转多少度)、el 是仰角(从水平面抬高多少度),默认 view(-37.5, 30)view(2) 则切到正上方俯视,三维曲面立刻变成一张平面「地图」——这正是等高线图的视角。光照也能玩:surf 对象有 FaceLighting 属性,配合 camlight 给曲面打一盏灯,凸起和凹陷的立体感会强很多,这些属性都可以用第 11 章的句柄语法随时调整。

12.6.2 bar3 与 pie3:三维版的柱和饼

>> y = 1:6;
>> z = [33 11 5 9 22 30];
>> bar3(y, z)
>> xlabel('x'); ylabel('y'); zlabel('z'); title('3D bar')

bar3 三维柱状图:六根深蓝色立方柱,33 和 30 两根格外高

bar3 也能直接吃矩阵——每行一组三维柱。经典的 spiral(5)(把 1~25 螺旋填进 矩阵)可视化效果很有意思:

>> mat = spiral(5)
mat =
    21    22    23    24    25
    20     7     8     9    10
    19     6     1     2    11
    18     5     4     3    12
    17    16    15    14    13
>> bar3(mat)
>> title('3D spiral')

spiral(5) 矩阵的三维柱状图:柱高从中心 1 螺旋式向外递增到 25,颜色也随值变化

>> pie3([3 10 5 2])

pie3 三维饼图:同一个圆盘带上厚度,占比 50%/25%/15%/10%

三维动画版彗星就是 comet3(x, y, z),玩法同 comet

12.6.3 mesh 与 surf:从「线框」到「曲面」

真正的高维数据()要用 mesh(网眼线框图)和 surf(曲面图,用颜色表现参数曲面)。MATLAB 还内置了形状生成器,sphere(n) 一次生成 阶的 x、y、z 三个方阵:

>> [x, y, z] = sphere(15);
>> size(x)
ans =
    16    16
>> mesh(x, y, z)
>> title('Mesh of sphere')

球的 mesh 线框图:经纬线勾勒出球面,线色随高度由蓝渐变到黄

>> [x, y, z] = sphere(15);
>> sh = surf(x, y, z);
>> title('Surf of sphere')
>> colorbar     % 右侧色标,显示颜色对应的数值范围

球的 surf 曲面图:颜色填充整个球面,右侧 colorbar 显示颜色到数值的映射

曲面返回的句柄 sh 是 Surface 对象——第 11 章的句柄功夫在这里直接兑现。它的 FaceAlpha 属性控制透明度(0 全透、1 不透),改成 0.5 球变半透明,能看见背面:

>> sh
sh =
  Surface with properties:
     EdgeColor: [0 0 0]
     LineStyle: '-'
     FaceColor: 'flat'
    FaceLighting: 'flat'
       FaceAlpha: 1
    ...
>> sh.FaceAlpha = 0.5;

FaceAlpha 改为 0.5 后:球面半透明,背面网格线透出来

12.6.4 meshgrid:给 z = f(x,y) 造网格

自己的二元函数没有现成形状生成器,用 meshgrid 网格点,再逐点算

>> [x, y] = meshgrid(-2*pi: 0.1: 2*pi);
>> z = cos(x) + sin(y);
>> surf(x, y, z)
>> title('cos(x) + sin(y)')
>> xlabel('x'); ylabel('y'); zlabel('z')

meshgrid + surf 画出的 z = cos(x)+sin(y) 曲面:周期性的山脊与沟壑交织成波纹面

meshgrid 在干嘛?

x 是行向量、y 是列向量,meshgrid 把它们「织」成两个同尺寸矩阵:一个矩阵每行都复制 x,另一个每列都复制 y。这样 这种逐元素运算就能一次性对整张网格向量化求值——你写的公式和数学式一模一样,不用写双重循环。想看等高线,用 contour(x,y,z),或用 meshc/surfc 把等高线垫在三维图底下(见教材「探索更多」)。

双 y 轴与更多玩法

plotyy 可以给图同时配左右两根 y 轴,适合画两个量纲不同的量(比如温度和电流);xlim/ylim/zlim 手动钉轴范围;errorbar 画置信区间;datetime 类型直接当 x 轴能自动排日期刻度。这些都在文档里等着你。

12.7 核心图形对象:line / text / rectangle / patch

第 11 章我们说 MATLAB 的图是对象树上的对象,最底层的「积木」叫核心图形对象(core graphics objects):linetextrectanglepatchimage(image 在第 13 章讲)。plot、bar 这些高级函数,内部全是用这些积木拼出来的。直接操作积木,你能画出任何高级函数不直接支持的东西。

什么时候需要动用这些底层函数?三个典型信号:① 要画的几何形状没有现成函数——比如在数据图上圈一个异常区域、画一个传感器覆盖范围的圆;② 要精细控制某个元素的样式——plot 的一揽子参数不够用,需要单独给某条线某个标注上属性;③ 要做可复用的绘图组件——比如习题里反复出现的圆角矩形、冰球场、多面体,用积木搭一次,以后改几个属性就能换形状。掌握积木层之后,你与图形系统的关系就从「用户」变成了「作者」。

12.7.1 line:比 plot 更裸的画线

line(x, y, '属性名', 值, ...) 直接创建 Line 对象:

>> x = -2*pi: 1/5 : 2*pi;
>> y = sin(x);
>> hl = line(x, y, 'LineWidth', 6, 'Color', [0.5 0.5 0.5])
hl =
  Line with properties:
        Color: [0.5 0.5 0.5]
    LineStyle: '-'
    LineWidth: 6
    ...

用 line 基元画的 sin 曲线:6 磅粗的灰色线条,比默认 plot 粗得多

一个「不靠 plot 画圆」的例子——先开白底窗口,用参数方程生成圆上的点,line 画成红色点线,再用 axis square 强制坐标框为正方形(否则「圆」会被拉成椭圆),最后 axis off 藏掉坐标轴:

>> figure('Color', [1 1 1])
>> pts = 0:0.1:2*pi;
>> line(cos(pts), sin(pts), 'LineStyle', ':', ...
        'LineWidth', 4, 'Color', 'r')
>> axis square
>> axis off

用 line + axis square + axis off 画出的红色虚线圆:没有坐标轴,就是干净的一个圈

12.7.2 text:在图上任意位置写字

text(x, y, '字符串') 在坐标 处(文本框左下角)写字。字符串里用 \命令 打特殊字符——希腊字母、箭头、数学符号:

>> x = -4:0.2:4;
>> y = sin(x);
>> hp = line(x, y, 'LineWidth', 3);
>> thand = text(2, 0, 'Sin(\pi)\rightarrow')
thand =
  Text (Sin(\pi)\rightarrow) with properties:
    String: 'Sin(\pi)\rightarrow'
    FontSize: 10
    ...
    Position: [2 0 0]
    Units: 'data'

text 在曲线旁写下 Sin(π)→:反斜杠命令 \pi 和 \rightarrow 被渲染成希腊字母 π 和右箭头

文本框的 Extent 属性记录实际占用的范围([x y 宽 高]),BackgroundColorEdgeColor 默认都是 none(看不见)。改一改就有「标签框」效果:

>> thand.Extent
ans =
    2.0000   -0.0321    0.6912    0.0613
>> set(thand, 'BackgroundColor', [0.8 0.8 0.8], ...
             'EdgeColor', [1 0 0])

同一个文本框加了浅灰底色和红边框:Sin(π)→ 变成醒目的标签框

交互式放字的兄弟函数 gtext('字符串'):鼠标变成十字线,点哪字落哪(内部是 text + ginput 的组合,ginput 能把鼠标点击处的坐标存下来)。

12.7.3 rectangle:能带圆角的矩形

rectangle 不带参数时创建一个默认矩形:Position[0 0 1 1](左下角在原点、宽高各 1),Curvature[0 0](直角)——乍看窗口里好像啥都没有:

>> recthand = rectangle
recthand =
  Rectangle with properties:
    FaceColor: 'none'
    EdgeColor: [0 0 0]
    LineWidth: 0.5000
    LineStyle: '-'
    Curvature: [0 0]
     Position: [0 0 1 1]

默认 rectangle:只有一条细黑边框贴着 [0,1]×[0,1] 区域,FaceColor 为 none 所以内部透明

Curvature 是 rectangle 的灵魂:从 [0 0](直角)到 [1 1](正圆/椭圆)连续可调,介于中间就是圆角矩形。创建时可直接带属性,之后也能 set:

>> rh = rectangle('Position', [0.2 0.2 0.5 0.8], ...
                  'Curvature', [0.5 0.5]);
>> axis([0 1.2 0 1.2])
>> set(rh, 'LineWidth', 3, 'LineStyle', ':')

带曲率的圆角矩形:Position 定位在 (0.2,0.2),Curvature 0.5 让四角变圆,点线加粗后像个肥皂盒

曲率从 0.2 涨到 0.8 的渐变过程(教材习题的运行结果):

圆角矩形单个示例:Curvature 较大时接近胶囊形状

2×2 子图对比曲率 0.2/0.4/0.6/0.8 的圆角矩形:圆角一个比一个明显,0.8 已接近椭圆

循环里不断挪 Position、录帧回放,就是第 12.4 节 movie 技术与 rectangle 的合体(教材习题 22 的效果):

循环移动并放大的圆角矩形录成的「电影」帧叠影:10 个点线框沿对角线生长

曲率 + rectangle 还能画冰球场——长方形两端带半圆角,中间一条红线:

用 rectangle(Curvature 使两端为圆弧)加一条 line 画出的冰球场示意图「Let's play hockey!」

12.7.4 patch:多边形积木

patch 创建多边形贴片。最简单:传顶点坐标向量 + 颜色,三个点画一个红三角:

>> x = [0 1 0.5];
>> y = [0 0 1];
>> hp = patch(x, y, 'r')
hp =
  Patch with properties:
    FaceColor: [1 0 0]
     FaceAlpha: 1
     EdgeColor: [0 0 0]
     LineStyle: '-'
        Faces: [1 2 3]
     Vertices: [3x2 double]

patch 画的红色三角形:三个顶点 (0,0)、(1,0)、(0.5,1),黑边红面

注意两个关键属性:Vertices 存顶点坐标;Faces 描述顶点怎么连线成面——顶点自动编号 1、2、3,Faces: [1 2 3] 表示按 1→2→3 连(默认再回到 1)。

更强大的姿势:用结构体传 Vertices + Faces,就能拼复杂多边形。五个顶点连三个三角形:

mypatch.Vertices = [ ...
    0   0      % 顶点1
    2   0      % 顶点2
    1   2      % 顶点3
    1  -2      % 顶点4
    3   1];    % 顶点5
mypatch.Faces = [ ...
    1 2 3      % 面1:顶点1-2-3
    2 3 5      % 面2:顶点2-3-5
    1 2 4];    % 面3:顶点1-2-4
patchhan = patch(mypatch, 'FaceColor', 'r', 'EdgeColor', 'k');

五个顶点、三个三角面拼成的红色 patch:整体像一个不规则箭头

给每个面单独上色FaceColor'flat',再用 FaceVertexCData 提供每面的 RGB 行:

>> mycolors = [0 0 1; 0 1 1; 1 1 0];   % 蓝 / 青 / 黄
>> patchhan = patch(mypatch, 'FaceVertexCData', mycolors, ...
                    'FaceColor', 'flat');

三个面分别染成蓝色、青色和黄色的 patch:同一个几何体,FaceColor=flat + FaceVertexCData 换装

patch 也能上三维——四个顶点四个面拼一个四面体,转着看:

polyhedron.Vertices = [ ...
    0    0    0
    1    0    0
    0    1    0
    0.5  0.5  1];
polyhedron.Faces = [ ...
    1 2 3
    1 2 4
    1 3 4
    2 3 4];
pobj = patch(polyhedron, ...
    'FaceColor', [0.8 0.8 0.8], 'EdgeColor', 'black');

三维 patch 拼成的四面体:初始视角只看到两个面,旋转后可见全部棱边

随机顶点的三角 patch 也是习题常客(黑边灰面):

教材习题 24 的随机三角 patch:三个 0~1 随机顶点,灰色面、宽度为 3 的黑边

常见坑:Vertices 的顺序就是 Faces 的编号依据

Faces 里写的 1、2、3……指的是 Vertices 矩阵的行号。先给哪个点、后给哪个点决定了连线路径;写错行号,多边形会「拧麻花」。改几何形状时先在纸上把顶点编号标清楚。

12.8 绘图实战:函数绘图与文件数据绘图

前面零件都认识了,现在组装成真正的程序。本节的例子都有共同的两段式结构:前半段是第 9 章的文件 I/O(fopen/fgetl/textscan/fclose),后半段是本章的绘图函数加第 11 章的句柄属性。三块知识拼在一起,才是一个能交付的数据可视化程序——从原始文件到最终图表,中间不借人手。

12.8.1 函数句柄 + cell 数组:一劳永逸的多视图绘图

这个函数接收三个参数: 向量、生成 函数句柄、存着图类型的 cell 数组,自动为每种图类型开一个子图:

function plotxywithcell(x, fnhan, rca)
% plotxywithcell 接收 x 向量、函数句柄和图类型名 cell 数组
% 用 subplot 把所有图类型画在同一个窗口
    lenrca = length(rca);
    y = fnhan(x);                    % 用句柄算出 y
    for i = 1:lenrca
        subplot(1, lenrca, i)
        funh = str2func(rca{i});     % 字符串 → 函数句柄
        funh(x, y)                   % 调用对应的绘图函数
        title(upper(rca{i}))
        xlabel('x')
        ylabel(func2str(fnhan))      % 句柄 → 函数名字符串
    end
end
>> anfn = @(x) x.^3;
>> x = 1:2:9;
>> rca = {'bar', 'area', 'plot'};
>> plotxywithcell(x, anfn, rca)

plotxywithcell 生成的三联子图之一:BAR 柱状图显示 x 的三次方,标题大写自动生成

同一份 x³ 数据的 AREA 面积图:曲线下方填充,强调累计

同一份 x³ 数据的 PLOT 折线图:右上扬的立方曲线

这里 str2func(字符串转句柄)和 func2str(句柄转字符串)是一对反向工具,加上 cell 数组循环,函数就「通用」了——换任何 、加任何图类型,都不用改函数本体。这正是第 11 章函数句柄思想的实战兑现。

12.8.2 读文件画图实战一:按行解析 A#B# 格式

文件 ab16.dat 每行长这样(A、B 两个部门四个季度的销售额):

A5.2B6.4
A3.2B5.5
A4.4B4.3
A4.5B2.2

脚本逐行读入字符串,用第 7 章的 strtok 切分、str2double 转数字,每个季度画一个子图,并用 set(gca, 'XTickLabel', ...) 把 x 轴刻度标签从数字 1/2 换成 A/B

fid = fopen('ab16.dat');
if fid == -1
    disp('File open not successful')
else
    for i = 1:4
        aline = fgetl(fid);              % 读一行字符串
        aline = aline(2:end);            % 去掉开头的 'A'
        [compa, rest] = strtok(aline, 'B');  % 按'B'切开
        compa = str2double(compa);
        compb = str2double(rest(2:end)); % rest 以 'B' 开头,去掉它
        subplot(1,4,i)
        bar([compa, compb])
        set(gca, 'XTickLabel', {'A', 'B'})   % 刻度标签换成 A、B
        axis([0 3 0 8])
        ylabel('Sales (millions)')
        title(sprintf('Quarter %d', i))
    end
    closeresult = fclose(fid);
    if closeresult ~= 0
        disp('File close not successful')
    end
end

四个季度销售额子图:每张图 A、B 两根柱,x 轴刻度已被 XTickLabel 换成部门代号

Quarter 2:A 部门 3.2、B 部门 5.5,B 明显领先

Quarter 3:A 4.4、B 4.3,几乎打平

Quarter 4:A 4.5、B 2.2,A 反超

12.8.3 读文件画图实战二:textscan + 双图联排

文件 compsales.dat 每行是「数值 + 部门缩写」:

5.2 X
3.3 A
5.8 P
2.9 Q

textscan 用格式串 '%f %s' 一次性把数字读进一个 cell、缩写读进另一个 cell,然后左柱图右饼图各来一份:

fid = fopen('compsales.dat');
if fid == -1
    disp('File open not successful')
else
    filecell = textscan(fid, '%f %s');   % {数字列, 缩写列}
    subplot(1,2,1)
    bar(filecell{1})
    xlabel('Division'); ylabel('Sales (millions)')
    set(gca, 'XTickLabel', filecell{2})  % 柱图 x 轴用缩写
    subplot(1,2,2)
    pie(filecell{1}, filecell{2})        % 饼图用缩写当标签
    title('Sales in millions by division')
    closeresult = fclose(fid);
    if closeresult ~= 0
        disp('File close not successful')
    end
end

compsales 的柱状图:X/A/P/Q 四个部门,x 轴刻度直接显示部门缩写

同一份数据的饼图:P 占比最大,各扇区用部门缩写做标签

12.8.4 更多实战:面积图、参数化绘图、堆叠水平柱

教材习题里还有三个典型:从文件读 “x 0 y 1” 格式的坐标对画面积图;给绘图函数做可选参数(nargin 判断传没传线宽/颜色);三年份收入与房价的堆叠水平柱图:

习题 2:从文件解析出四个 (x,y) 点后画的面积图,标题自动写「Four data points」

习题 16 的 plotexvar:传入线宽 12、颜色 'r' 后的粗红 sin 曲线,标题自动显示「Four arguments」

习题 17:2004–2011 年收入(蓝)与房价(红)的堆叠水平柱图,年份用 XData 属性放到 y 轴上

套路总结:文件 → 字符串 → 数字 → 图

所有「读文件画图」都是同一套流水线:fopen 检查 fid 是否为 -1 → 逐行 fgetl(或整体 textscan)→ 字符串切片 + str2double → 向量化画图 → fclose 检查返回值是否为 0。第 9 章的文件 I/O 加上本章的绘图函数,就是你以后处理任何实验数据、日志数据的标准姿势。

12.9 保存与打印绘图

图画好了,怎么带走?

  • 复制:Figure 窗口菜单 Edit → Copy Figure,然后到 Word 里粘贴。
  • 另存为:File → Save As,支持 .jpg、.tif、.png 等常见图像格式,也支持 MATLAB 专用的 .fig 格式——.fig 保存的是完整图形对象,下次在 MATLAB 里打开还能继续用句柄改属性。
  • Generate Code:如果图不是纯代码画的(比如你在 Property Editor 里手动改过),File → Generate Code 能生成一个重建这张图的脚本——手调出来的效果从此可复现。
  • 程序化保存/打印print 命令。脚本里光写一个 print 就用默认设置打印当前窗口;指定文件名则保存为文件:
print -dtiff -r400 plot.tif   % 存成 400 dpi 的 tif

-dtiff 指定 tif 格式,-r400 指定分辨率 400 dpi。写报告批量出图时全靠它。

12.10 通关标准与自测

本章通关标准:

  1. 给你一份未知向量,能在 30 秒内决定用 histogram(看分布)、pie(看占比)、bar/barh(比大小)、stem(看点位)还是 semilogy(看指数规律),并说明理由——选图本身就是分析。
  2. 能用 meshgrid + surf 画出任意 ,并用返回的句柄改出半透明效果(FaceAlpha)。
  3. 能徒手用 line/text/rectangle/patch 组合画出一个「plot 函数画不出来」的图形(比如带圆角的框 + 标注 + 填充三角),并解释 Vertices/Faces 的关系。

会得到什么?[0.5 0.5] 呢?

[1 1] 是最大曲率——宽高不相等时得到椭圆,宽高相等(配合 axis square)就是正圆[0.5 0.5] 是中间状态:四个角变成半径为边长一半的圆弧,即圆角矩形。Curvature 两个分量分别控制水平/垂直方向的弯曲程度,从 0 到 1 连续可调。

具体在描述什么?

五个顶点按 Vertices 矩阵的行号编号为 1~5,这个 Faces 矩阵声明了三个三角面:面 1 连接顶点 1→2→3,面 2 连接顶点 2→3→5,面 3 连接顶点 1→2→4(每个面默认闭合)。想给不同面上色,把 FaceColor 设为 ‘flat’ 并给 FaceVertexCData 传入「每面一行 RGB」的矩阵即可。