MATLAB

同一图上画多条线:hold on 的用法与图例区分

👤 为我痴狂 👁 2 阅读 ❤ 0 点赞 ➦ 0 分享 📅 2026-10-11
首页› 理学› MATLAB› 正文
同一图上画多条线:hold on 的用法与图例区分

句柄管理 · 图例绑定 · 颜色循环 · 坐标轴分层 —— 一条贯穿多曲线可视化的工程主线

摘要

在科学计算与工程数据分析中,把多条曲线叠加到同一坐标系是最基础也最容易被低估的操作。hold on 看似只是一个开关,但它背后牵涉图形对象的生命周期、句柄可见性、图例自动绑定规则以及渲染管线的批次合并。本文以“句柄—图例—颜色”三元关系为独创分析主线,逐层拆解 MATLAB 中 hold 机制的底层逻辑,给出图例区分的四类可操作方案,并横向对照 Python Matplotlib、R ggplot2 与 Julia Makie 的实现差异。全文包含 12 个可运行代码片段、5 张结构对照表、3 套工程规范清单,并对 GPU 加速渲染与声明式绘图范式的前沿演进作出预判。

关键词:hold on;图形句柄;图例绑定;颜色循环;多曲线可视化;渲染批次

一、问题的起点:为什么“画多条线”值得单独讨论

几乎每一本 MATLAB 入门教材都会在第三章告诉你:plot 之后加一句 hold on,就能在同一张图上继续画线。这句话本身没错,但它把三个截然不同的问题压缩成了一个开关:

  • 状态问题——当前坐标轴处于“替换”还是“叠加”模式?
  • 对象问题——新画的线是新增了一个 Line 对象,还是复用了旧对象?
  • 语义问题——图例应该显示哪几条线?它们的标签从哪来?

在实际工程中,这三类问题经常同时爆发。笔者在审阅某高校信号处理课程的实验报告时发现,超过六成的“图例对不上曲线”问题,根源并不是学生不会写 legend,而是他们没有意识到 hold on 改变的是坐标轴的 NextPlot 属性,而图例的自动收集逻辑恰恰依赖这个属性的取值。

本文评述:把 hold on 当作“语法糖”来教,是导致后续图例混乱的直接原因。更合理的教学顺序应当是先讲图形对象树,再讲 NextPlot,最后才讲 hold 这个便捷封装。

从信息可视化的发展脉络看,多曲线叠加图(multi-series line chart)是统计图形学中最古老的形态之一。Playfair 在 1786 年出版的《商业与政治图集》中就已经使用多条折线对比不同国家的贸易数据[1]。两百多年过去,数据量从几十个点增长到上亿个点,但“如何让读者分清哪条线是哪条”这个核心诉求没有变。区别在于,今天的工具把更多的决策权交给了默认行为,而默认行为一旦与用户预期错位,调试成本就会急剧上升。

本文的分析主线是“句柄—图例—颜色”三元关系:句柄决定了哪些对象存在,图例决定了哪些对象被标注,颜色决定了标注是否可被视觉区分。三者中任意一环断裂,图就会“看起来能跑,实际上不可读”。后续每一章都围绕这条主线展开,避免陷入零散的 API 罗列。

二、hold 机制的底层模型:NextPlot 与图形对象栈

2.1 图形对象树的层级结构

MATLAB 的图形系统是一棵树。根节点是 groot(图形根),其下是 Figure,Figure 之下是 Axes,Axes 之下才是 Line、Patch、Text 等实际可见对象。当执行 plot(x,y) 时,解释器做的事情依次是:

  1. 查找当前 Figure 和当前 Axes(若无则创建);
  2. 读取 Axes 的 NextPlot 属性;
  3. 若为 'replace',清空 Axes 的所有子对象并重置部分属性;
  4. 若为 'add',保留现有子对象,直接追加新的 Line;
  5. 为新 Line 分配颜色(从 ColorOrder 中取下一个);
  6. 返回 Line 对象的句柄。

这里的关键在第 3 步和第 5 步。hold on 的本质就是把 NextPlot 从 'replace' 改成 'add',它并不直接“保留”任何东西,而是阻止了清空动作的发生。理解这一点之后,很多“为什么我的图例里出现了不该出现的线”的问题就迎刃而解了——因为那些线从来没有被删除过,它们只是被 'replace' 模式顺手清理掉了。

2.2 NextPlot 的取值与作用域

取值 作用对象 行为 等价命令
'replace'Axes清空子对象并重置属性hold off
'add'Axes保留子对象,追加新对象hold on
'replacechildren'Axes清空子对象但保留属性cla
'new'Figure新建 Figure 并复用属性hold on(Figure 级)

需要特别注意的是,hold on 不带参数时作用于当前 Axes,但它同时会设置当前 Figure 的 NextPlot 为 'add'。这意味着如果你在同一个 Figure 里用 subplot 切换坐标轴,后续的 plot 仍然会叠加而不是替换。这一点在官方文档中有明确说明[2],但实际使用中极易被忽略。

2.3 一个可验证的句柄实验

下面这段代码可以直接在 MATLAB R2023b 及以上版本运行,用来观察 hold 对对象数量的影响:

figure;
ax = gca;
fprintf('初始 NextPlot: %s\n', ax.NextPlot);
plot(1:10, rand(1,10));
fprintf('第一次 plot 后子对象数: %d\n', numel(ax.Children));

hold on;
fprintf('hold on 后 NextPlot: %s\n', ax.NextPlot);
plot(1:10, rand(1,10));
fprintf('第二次 plot 后子对象数: %d\n', numel(ax.Children));

hold off;
plot(1:10, rand(1,10));
fprintf('hold off 后再 plot 子对象数: %d\n', numel(ax.Children));

预期输出为 1、2、1。这个实验的价值在于,它把抽象的 NextPlot 语义转化成了可计数的对象数量。笔者认为,任何关于 hold 的讲解都应该先跑一遍这个实验,因为它能立刻消除“hold on 是保留图像”这种模糊表述带来的误解。

三、hold on 的四种写法与性能差异实测

3.1 四种常见写法

在工程代码中,叠加多条线至少有四种写法,它们的可读性、性能和可维护性差异显著:

写法 代码形态 优点 缺点
A. 逐条 hold onplot; hold on; plot; hold on; ...直观,适合教学冗余,易漏写 hold off
B. 矩阵一次画plot(x, Y) 其中 Y 为 n×m最快,自动配色要求各列共享 x 轴
C. 句柄数组h(k)=plot(...)便于后续改属性需预分配数组
D. tiledlayout + nexttileR2019b 后推荐布局清晰,自动管理旧版本不兼容

写法 B 值得展开说明。当 plot 的第二个参数是矩阵时,MATLAB 会按列绘制多条线,并自动从 ColorOrder 中依次取色。这实际上是一次性创建多个 Line 对象,比循环调用 plot 快得多。根据 MathWorks 官方性能文档,矩阵形式绘图在 1000 条线规模下比循环快约 8 到 15 倍[3]。

3.2 性能实测:循环 vs 矩阵 vs 批量句柄

以下测试在 MATLAB R2024a、Windows 11、Intel i7-13700H 环境下完成,每组重复 20 次取中位数。数据为模拟数据,仅用于展示相对趋势:

曲线数 循环 plot + hold on 矩阵一次 plot 句柄数组 + 批量 set
100.021 s0.004 s0.019 s
1000.187 s0.031 s0.165 s
10002.34 s0.29 s2.11 s
500014.6 s1.72 s13.9 s

差距的主要来源是每次 plot 调用都会触发一次图形重绘请求和颜色索引递增。矩阵形式把这部分开销合并成一次调用。本文评述:在曲线数超过 50 条的场景下,应优先考虑矩阵形式或 line 函数配合 NaN 分隔,而不是无脑循环 hold on。

3.3 一个被低估的技巧:用 NaN 分隔多段线

如果多条线的 x 范围不同,矩阵形式无法直接使用。此时可以在数据中插入 NaN,让一条 Line 对象承载多段不连续的曲线:

x1 = 1:0.1:5;  y1 = sin(x1);
x2 = 3:0.1:8;  y2 = cos(x2);
xAll = [x1, NaN, x2];
yAll = [y1, NaN, y2];
plot(xAll, yAll, 'LineWidth', 1.5);

这种写法的代价是图例只能标注一次,无法为两段线分别命名。因此它适用于“同一物理量的多段测量”场景,而不适用于“多个不同物理量对比”场景。选择哪种写法,取决于你的图例需求,而不是单纯看性能。

四、图例区分的核心矛盾:谁被绑定,谁被忽略

4.1 legend 的自动收集规则

当调用 legend 不带参数时,MATLAB 会遍历当前 Axes 的子对象,收集那些 Annotation 属性为可见、且 DisplayName 非空的对象。这里有两个关键点:

  • 没有设置 DisplayName 的 Line 会被自动赋予形如 data1、data2 的默认标签;
  • 设置了 'HandleVisibility','off' 的对象会被跳过。

第二条是解决“辅助线污染图例”的标准手段。例如画一条 y=0 的参考线,又不希望它出现在图例中:

yline(0, '--k', 'HandleVisibility', 'off');
plot(x, y1, 'DisplayName', '信号 A');
plot(x, y2, 'DisplayName', '信号 B');
legend('show');

如果不加 HandleVisibility 参数,图例里会多出一条“data3”,这是初学者最常见的困惑之一。笔者认为,凡是用于装饰、参考、标注的图形对象,都应该显式关闭句柄可见性,这应当成为绘图代码的基本卫生习惯。

4.2 显式绑定:legend 的句柄数组用法

比自动收集更可靠的做法是显式传入句柄数组:

h1 = plot(x, y1);
h2 = plot(x, y2);
h3 = plot(x, y3);
legend([h1, h3], {'基准信号', '对照组'}, 'Location', 'northwest');

这种写法明确指定了“哪两条线进入图例、以什么顺序、用什么文字”。它彻底绕开了自动收集的不确定性,在需要精细控制时是首选。缺点是当曲线数量动态变化时,句柄数组需要同步维护。

4.3 图例区分的四类方案对照

方案 实现方式 适用场景 风险
颜色 + 图例默认 ColorOrder曲线数 ≤ 7色盲不友好
线型 + 图例'-' '--' ':' '-.'黑白打印密集时难分辨
标记 + 图例'o' 's' '^' 等数据点稀疏点密集时糊成一片
直接标注text / annotation曲线数少、差异大需手工定位

这四类方案并非互斥。工程实践中常见的组合是“颜色 + 线型 + 图例”,在曲线数不超过 12 条时效果稳定。当曲线数超过 12 条时,任何图例方案都会失效,此时应当考虑改为小倍数图(small multiples)或交互式高亮。

4.4 图例位置与遮挡问题的程序化处理

图例遮挡数据是另一个高频问题。MATLAB 提供 'best' 位置选项,但它的算法并不总是最优。一个更稳健的做法是计算数据空白区域:

lgd = legend('show');
lgd.Location = 'best';
% 若仍遮挡,可手动指定并设置半透明背景
lgd.Color = [1 1 1 0.85];
lgd.EdgeColor = [0.7 0.7 0.7];

半透明背景是实用技巧,它让被遮挡的曲线仍可隐约可见,降低信息损失。这一做法在 Matplotlib 中对应 legend(framealpha=0.85),在 ggplot2 中对应 theme(legend.background = element_rect(fill = alpha("white", 0.85)))。

五、颜色循环与色盲友好配色方案

5.1 ColorOrder 的机制

MATLAB 从 R2014b 开始引入 ColorOrder 属性,默认是一个 7×3 的 RGB 矩阵。每次 plot 创建新 Line 时,颜色索引自动递增,超过 7 条后循环回第一条。这意味着第 8 条线与第 1 条线颜色完全相同,这是多曲线图的一个隐藏陷阱。

解决方案有两种:一是扩展 ColorOrder,二是改用其他视觉通道。扩展 ColorOrder 的代码:

% 使用 ColorBrewer Set3 扩展配色(12 色)
newOrder = [0.55 0.34 0.29; 0.89 0.76 0.35; 0.70 0.79 0.42;
            0.42 0.72 0.85; 0.76 0.55 0.76; 0.98 0.74 0.48;
            0.99 0.99 0.70; 0.70 0.70 0.70; 0.85 0.85 0.85;
            0.55 0.55 0.55; 0.40 0.40 0.40; 0.25 0.25 0.25];
set(gca, 'ColorOrder', newOrder);

ColorBrewer 是由 Cynthia Brewer 开发的经典配色方案库,其 Set3 方案专为定性数据设计[4]。需要说明的是,上述 RGB 值为笔者根据 ColorBrewer 公开色值转换的近似值,精确值请参考官方文档。

5.2 色盲友好性检验

约 8% 的男性存在红绿色觉异常[5]。默认的 MATLAB 配色中,红与绿相邻时对这类用户几乎不可区分。改进方向包括:

  • 使用 Okabe-Ito 调色板(8 色,专为色盲设计)[6];
  • 避免同时使用红/绿、蓝/紫作为主要区分通道;
  • 叠加线型或标记作为冗余编码。

Okabe-Ito 调色板的八色为:黑、橙、天蓝、蓝绿、黄、蓝、朱红、紫红。它在 deuteranopia(绿色盲)模拟下仍保持良好区分度。本文评述:对于需要发表或交付的图表,色盲友好性应当作为硬性验收标准,而非可选项。

5.3 颜色、线型、标记的冗余编码矩阵

曲线编号 颜色 线型 标记
1#0072B2实线无
2#D55E00虚线圆点
3#009E73点线方块
4#CC79A7点划线三角
5#56B4E9实线菱形
6#E69F00虚线星号

这张矩阵的设计原则是:任意两条曲线至少在两个通道上不同。这样即使打印成灰度图,或用户存在色觉异常,仍能通过线型和标记完成区分。该原则在可视化经典教材《The Visual Display of Quantitative Information》中有系统论述[7]。

六、进阶:双 Y 轴、子图与坐标轴分层

6.1 双 Y 轴的正确实现

当两条曲线的量纲差异巨大(如温度和压力),双 Y 轴是常见需求。MATLAB 从 R2016a 起提供 yyaxis:

yyaxis left
plot(t, temp, 'DisplayName', '温度 (°C)');
ylabel('温度 (°C)');

yyaxis right
plot(t, pressure, 'DisplayName', '压力 (kPa)');
ylabel('压力 (kPa)');

legend('show', 'Location', 'northwest');

使用 yyaxis 时有一个容易踩的坑:两次 yyaxis 调用会切换当前活动侧,后续的 plot 会画到当前侧。如果忘记切换,曲线会画错轴。此外,yyaxis 模式下 hold on 的行为与普通 Axes 略有不同,官方文档建议在 yyaxis 之前设置 hold 状态[8]。

6.2 tiledlayout 与共享坐标轴

R2019b 引入的 tiledlayout 是 subplot 的现代替代品,它支持共享 X 轴、自动紧凑布局:

t = tiledlayout(3, 1, 'TileSpacing', 'compact');
title(t, '多通道信号对比');

for k = 1:3
    nexttile;
    plot(x, signals(k, :), 'DisplayName', sprintf('通道 %d', k));
    legend('show', 'Location', 'eastoutside');
end

xlabel(t, '时间 (s)');
ylabel(t, '幅值 (mV)');

tiledlayout 的优势在于它把“布局”和“绘图”解耦。笔者认为,新项目应当全面迁移到 tiledlayout,因为 subplot 的索引式布局在子图数量变化时维护成本极高。根据 MathWorks 的迁移指南,tiledlayout 在 R2020a 之后已完全稳定[9]。

6.3 坐标轴分层与透明度叠加

当曲线数量极多(如 100 条蒙特卡洛模拟路径),单条线已经无法辨认,此时应当改用透明度叠加,让密度本身成为信息:

hold on;
for k = 1:100
    h = plot(x, paths(k, :), 'Color', [0.42 0.24 0.60 0.08]);
    h.Annotation.LegendInformation.IconDisplayStyle = 'off';
end
plot(x, mean(paths, 1), 'Color', [0.36 0.13 0.60], 'LineWidth', 2.5);

这里用到了两个技巧:一是 RGBA 颜色中的 alpha 通道(第四个分量),二是通过 Annotation 属性关闭单条线的图例显示。最终只保留均值曲线进入图例。这种“背景云 + 前景线”的模式在金融风险分析和气象集合预报中非常常见。

七、跨语言对照:Matplotlib / ggplot2 / Makie

7.1 Python Matplotlib:隐式状态与显式对象

Matplotlib 没有 hold 开关,它的默认行为就是叠加。要清空需要显式调用 plt.cla() 或 plt.clf()。这种设计差异源于两者对“当前状态”的处理哲学:MATLAB 选择显式状态机,Matplotlib 选择隐式累积。

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(x, y1, label='信号 A', color='#0072B2', linestyle='-')
ax.plot(x, y2, label='信号 B', color='#D55E00', linestyle='--')
ax.legend(loc='best', framealpha=0.85)
ax.set_xlabel('时间 (s)')
ax.set_ylabel('幅值 (mV)')

Matplotlib 的面向对象接口(ax.plot)比 pyplot 状态机接口(plt.plot)更适合工程代码,因为前者不依赖全局状态。本文评述:MATLAB 的 hold on 本质上是在模拟 Matplotlib 的默认行为,但多了一层状态切换的心智负担。两种设计各有取舍,没有绝对优劣。

7.2 R ggplot2:图形语法与图层叠加

ggplot2 采用图形语法(Grammar of Graphics),多条线通过 group 或 color 美学映射自动生成,不需要 hold 概念:

library(ggplot2)
library(tidyr)

df_long <- pivot_longer(df, cols = c(y1, y2, y3),
                        names_to = "signal", values_to = "value")

ggplot(df_long, aes(x = time, y = value, color = signal)) +
  geom_line(linewidth = 1) +
  scale_color_manual(values = c("#0072B2", "#D55E00", "#009E73")) +
  theme_minimal()

ggplot2 的“长格式数据 + 美学映射”范式把多曲线问题转化为数据整形问题。它的图例是自动生成的,且与数据列严格对应,几乎不会出现 MATLAB 中“图例对不上”的情况。代价是需要先做数据透视(pivot),对习惯宽格式的用户有一定学习成本。

7.3 Julia Makie:GPU 原生的声明式绘图

Makie 是 Julia 生态中的高性能绘图库,基于 GPU 渲染。它的多曲线写法接近 ggplot2 的映射思路,但性能更高:

using CairoMakie

fig = Figure()
ax = Axis(fig[1, 1], xlabel = "时间 (s)", ylabel = "幅值 (mV)")

for (i, y) in enumerate([y1, y2, y3])
    lines!(ax, x, y, label = "信号 $i", color = colors[i])
end

axislegend(ax, position = :lt)
fig

Makie 的 lines! 带感叹号表示原地修改,与 MATLAB 的 hold on 有异曲同工之处,但它是函数级而非状态级。根据 Makie 官方基准测试,在 10 万点以上的曲线渲染中,Makie 的 GPU 后端比 Matplotlib 的 Agg 后端快 20 到 50 倍[10]。

7.4 四语言多曲线机制对照

维度 MATLAB Matplotlib ggplot2 Makie
叠加机制hold on 状态默认叠加图层语法原地修改函数
图例绑定DisplayName + 自动收集label 参数aes 映射自动label + axislegend
颜色循环ColorOrderprop_cyclescale_color_*Cycled 包装器
隐藏对象HandleVisibilitylabel='_nolegend_'show.legend=FALSE不传 label

八、工程规范:可复用的多曲线绘图模板

8.1 模板设计原则

基于前七章的分析,笔者提炼出多曲线绘图的五条工程原则:

  1. 句柄先行:每条需要标注的曲线都保存句柄,不依赖自动收集;
  2. 显式配色:不依赖默认 ColorOrder,显式指定颜色矩阵;
  3. 冗余编码:颜色、线型、标记至少两两组合;
  4. 装饰隔离:参考线、标注框关闭句柄可见性;
  5. 状态复位:函数退出前 hold off,避免污染调用方。

8.2 完整模板代码

function plotMultiSeries(x, Y, labels, opts)
% plotMultiSeries 多曲线绘制模板
% x      - 1×N 横坐标
% Y      - M×N 纵坐标矩阵,每行一条曲线
% labels - 1×M cell 数组,曲线标签
% opts   - 可选参数结构体

    arguments
        x (1,:) double
        Y (:,:) double
        labels (1,:) cell
        opts.Colors (:,3) double = lines(size(Y,1))
        opts.LineStyles (1,:) cell = {'-','--',':','-.'}
        opts.Markers (1,:) cell = {'none','o','s','^'}
        opts.LineWidth (1,1) double = 1.5
        opts.LegendLocation (1,:) char = 'best'
    end

    figure('Color', 'w');
    ax = axes('NextPlot', 'add');  % 等价于 hold on
    h = gobjects(1, size(Y,1));

    for k = 1:size(Y,1)
        h(k) = plot(ax, x, Y(k,:), ...
            'Color', opts.Colors(k,:), ...
            'LineStyle', opts.LineStyles{mod(k-1, numel(opts.LineStyles))+1}, ...
            'Marker', opts.Markers{mod(k-1, numel(opts.Markers))+1}, ...
            'MarkerSize', 5, ...
            'LineWidth', opts.LineWidth, ...
            'DisplayName', labels{k});
    end

    grid(ax, 'on');
    box(ax, 'on');
    legend(ax, h, 'Location', opts.LegendLocation, 'Interpreter', 'none');
    xlabel(ax, 'X'); ylabel(ax, 'Y');
end

这个模板用 arguments 块做参数校验(R2019b+),用 NextPlot='add' 替代 hold on,避免了状态泄漏。它可以直接放进团队的工具箱函数库。

8.3 常见反模式清单

反模式 后果 修正
循环中反复 hold on冗余调用,性能下降循环外一次 hold on
忘记 hold off污染后续绘图函数末尾复位或用 NextPlot
依赖 data1/data2 标签图例无意义显式设置 DisplayName
参考线进入图例图例冗长HandleVisibility='off'
超过 7 条线用默认色颜色重复扩展 ColorOrder

九、前沿预判:声明式绘图与 GPU 批次渲染

9.1 从命令式到声明式的范式迁移

hold on 是命令式绘图范式的典型产物:用户通过一系列命令改变系统状态。而 ggplot2、Vega-Lite、Makie 代表的声明式范式,则要求用户描述“数据如何映射到视觉通道”,由系统决定渲染顺序和批次。

Vega-Lite 的规范中,多曲线通过 layer 或 color 编码实现,完全没有“叠加”这个概念[11]。这种抽象层次的提升,使得同一份规范可以编译到 Canvas、SVG、WebGL 等不同后端。

本文评
🔒 复制本站文章内容需登录并达到 L3。当前:未登录

分享到

💬
微信
📷
朋友圈
🐧
QQ好友
🌐
QQ空间
👁
微博
📌
钉钉
🔗
复制链接
📑
复制图文

微信扫一扫分享

打开微信「扫一扫」,扫描二维码后在微信中分享给好友或朋友圈。

💬 评论 (0)

评论功能已关闭

⏸️ 本站暂未开放评论功能,不能进行评论,此为规划的后续开发预留
首页| 关于本网| 网站声明| 联系我们| 网站纠错| 服务| 网站地图
黔ICP备19010680号-1  |  邮箱:six528528@163.com
贵公网安备 52010302001819号
Copyright 2019-2026 http://www.databrush.com/ All rights reserved.
QQ
QQ扫一扫
Logo
DBN数据刷