三亩地 三亩地SAN MU DI · CODE DIARY
ARTICLE DETAIL

日记详情

真实记录编程学习的某一天,欢迎挑你感兴趣的翻一翻。

MATLAB双y轴绘图:从yyaxis函数到混合图表实战

MATLAB双y轴绘图:从yyaxis函数到混合图表实战

1. 项目概述:为什么我们需要双y轴图?

在数据可视化和科研绘图领域,我们常常会遇到一个棘手的问题:两组数据序列,它们的数值范围天差地别,但你又想将它们放在同一张图上进行对比分析。比如,你正在分析一个电子设备的性能,一组数据是电压,范围在0到5伏之间;另一组数据是电流,可能高达几百毫安。如果强行用同一个y轴来绘制,电流的微小波动会把电压曲线“压”成一条几乎看不见的直线,完全失去了对比的意义。又或者,在气象研究中,你想同时展示温度和降水量随时间的变化,一个单位是摄氏度,另一个是毫米,量纲和尺度都不同。这时候,MATLAB中的“双y轴”绘图功能就成了你的救星。

简单来说,双y轴图就是在同一个图形窗口(Figure)中,创建两个独立的、刻度不同的y轴(通常左右各一个),让两组差异巨大的数据能够“同台竞技”,各自清晰地展示自己的变化趋势和细节。这不仅仅是把两条线画在一起,而是通过巧妙的坐标轴管理,实现了数据的“分层”可视化,极大地提升了图表的表达能力和信息密度。对于工程师、科研人员和数据分析师而言,掌握这项技能,意味着你能更高效、更专业地呈现复杂的数据关系,让报告和论文中的图表更具说服力。

2. 核心思路与MATLAB工具选型

在MATLAB中实现双y轴,历史上和现在有几种不同的方法,了解它们的演变和适用场景,能帮助我们在不同情况下做出最佳选择。

2.1 经典方法:plotyy函数(已过时但需了解)

在老版本的MATLAB(R2016a之前)中,plotyy函数是绘制双y轴图的标准方法。它的语法相对直观:

[ax, h1, h2] = plotyy(x1, y1, x2, y2, fun1, fun2)

其中,x1, y1对应左侧y轴的数据,x2, y2对应右侧y轴的数据,fun1fun2是绘图函数,默认为@plot

为什么它被淘汰了?尽管plotyy能完成任务,但它存在几个明显的短板:

  1. 对象句柄管理复杂:它返回的ax是一个包含左右两个坐标轴对象的向量,h1h2是两个数据序列的句柄。后续要对某个坐标轴或某条曲线进行精细定制(比如改颜色、调刻度、加标签),你需要非常小心地指明操作对象,代码容易冗长且易错。
  2. 与现代图形系统兼容性一般:MATLAB在R2014b引入了全新的图形系统(HG2),带来了更强大的对象属性和更一致的编程接口。plotyy生成的对象树在和新系统的一些高级特性(如透明度、复杂的颜色映射)配合时,有时会显得不那么“和谐”。
  3. 功能扩展性差:如果你想在双y轴的基础上再添加第三个数据序列,或者进行更复杂的布局,plotyy就显得力不从心了。

注意:虽然plotyy在后续版本中仍然可用(出于向后兼容考虑),但MathWorks官方已明确标注其为“不推荐”状态。在新项目中,尤其是考虑到代码的长期维护和可读性,强烈建议使用更现代的方法。

2.2 现代标准:yyaxis函数(推荐首选)

自MATLAB R2016a起,yyaxis函数被引入,它代表了双y轴绘图的现代解决方案。与plotyy一次性生成整个图表不同,yyaxis采用了一种“状态切换”的模式,更符合面向对象的思想,操作起来也更为灵活和清晰。

它的核心思想是:激活当前图形中的左侧或右侧y轴,然后所有后续的绘图命令(如plot,scatter,bar)都会自动绑定到当前激活的y轴上。你可以把它想象成有两个独立的画板(坐标轴)重叠在一起,yyaxis leftyyaxis right就是切换画笔到不同画板的指令。

为什么推荐yyaxis

  1. 语法简洁直观:逻辑清晰,易于理解和记忆。
  2. 对象管理统一:它直接操作当前坐标轴对象(gca),所有图形对象的父级都是同一个坐标轴,这使得后续的属性设置(如标题、图例、网格)更加统一和简单。
  3. 完美的兼容性:完全基于新的HG2图形系统,可以无缝使用所有最新的图形特性。
  4. 极高的灵活性:可以轻松地在左右轴之间切换,添加任意数量的图形对象(理论上,你可以在左侧轴画多条线,在右侧轴也画多条线),也更容易与其他图形定制命令结合。

因此,对于绝大多数新项目和学习者,我将以yyaxis函数作为核心进行详细讲解。这是目前最主流、最推荐的方法。

3.yyaxis函数核心细节与实操要点

掌握了yyaxis的基本思想,我们来深入其核心细节。理解这些细节,是画出精美、专业双y轴图的关键。

3.1 基础绘图流程与坐标轴激活

让我们从一个最简单的例子开始,绘制正弦波和指数衰减曲线。

% 示例1:基础双y轴图 x = linspace(0, 10, 100); y1 = sin(x); % 数据1, 范围[-1, 1] y2 = 100 * exp(-0.5*x); % 数据2, 范围约[100, 0.67], 量级差异大 figure; % 创建一个新的图形窗口 % 激活左侧y轴并绘图 yyaxis left; plot(x, y1, ‘b-‘, ‘LineWidth’, 2); ylabel(‘正弦值’); ylim([-1.5, 1.5]); % 手动设置左侧y轴范围,让图形更美观 % 激活右侧y轴并绘图 yyaxis right; plot(x, y2, ‘r–‘, ‘LineWidth’, 2); ylabel(‘指数衰减值’); % 共用设置 xlabel(‘时间 / s’); title(‘正弦与指数衰减对比’); grid on;

代码解读与实操要点:

  • yyaxis left;:这条命令将当前坐标轴的“活动侧”切换到左边。执行后,gca(获取当前坐标轴)返回的对象的YAxisLocation属性变为‘left’,并且其YColor等属性会影响后续绘图的颜色映射。
  • plot(x, y1, …);:紧接着的plot命令会将数据绘制在左侧y轴上。线条的颜色、样式等属性设置与普通绘图无异。
  • ylabel(‘正弦值’);:为当前激活的y轴(此处为左侧)设置标签。这是一个关键点:所有坐标轴相关的设置命令(如ylabel,ylim,yticks)只作用于当前激活的y轴。
  • 同理,yyaxis right;切换到右侧轴进行绘图和设置。
  • xlabel,title,grid on:这些作用于整个坐标轴(或图形)的命令,只需要设置一次,它们会被左右y轴共享。

3.2 图形对象属性精细控制

yyaxis的强大之处在于,它返回的是一个单一的坐标轴对象,但该对象内部管理着两套独立的y轴属性。我们可以通过坐标轴对象的属性来精细控制每一侧。

% 示例2:精细控制属性 x = 0:0.1:4*pi; y1 = sin(x); y2 = 10 * cos(x) + 50; figure; yyaxis left; h1 = plot(x, y1, ‘Color’, [0, 0.4470, 0.7410], ‘LineWidth’, 2.5, ‘Marker’, ‘o’, ‘MarkerSize’, 6); ylabel(‘电压 (V)’, ‘FontSize’, 12, ‘FontWeight’, ‘bold’); ax = gca; % 获取坐标轴句柄 ax.YColor = [0, 0.4470, 0.7410]; % 将左侧y轴刻度、标签的颜色设置为与曲线一致 ax.YLim = [-1.2, 1.2]; yyaxis right; h2 = plot(x, y2, ‘Color’, [0.8500, 0.3250, 0.0980], ‘LineWidth’, 2.5, ‘LineStyle’, ‘-.’); ylabel(‘电流 (mA)’, ‘FontSize’, 12, ‘FontWeight’, ‘bold’); ax.YColor = [0.8500, 0.3250, 0.0980]; % 将右侧y轴刻度、标签的颜色设置为与曲线一致 % 注意:此时ax仍是同一个句柄,但属性已关联到右侧轴 % 设置共用属性 xlabel(‘时间 (ms)’); title(‘电路信号分析’, ‘FontSize’, 14); grid on; grid minor; % 打开次要网格线,使图表更精细 ax.FontSize = 11; % 设置全局字体大小 ax.Box = ‘on’; % 显示坐标轴盒子

关键技巧:

  • 获取句柄:在第一次yyaxis left绘图后,使用ax = gca;获取坐标轴句柄。这个句柄是操作整个双y轴图形的“总开关”。
  • 分别设置颜色:通过设置ax.YColor属性,可以分别改变左右y轴刻度线、刻度标签以及y轴标签的颜色。通常将其设置为与对应数据曲线相同的颜色,能极大增强图表的可读性。这是一个让图表瞬间变专业的细节。
  • 注意属性作用域:像ax.YLim(y轴范围)、ax.YTick(刻度位置)这样的属性,在调用yyaxis left/right后,会自动指向当前激活的那一侧y轴。因此,在示例中,我们在切换yyaxis上下文后分别设置了ax.YColor,它们分别生效于左右轴。

3.3 图例的创建与常见陷阱处理

为双y轴图添加图例,是另一个容易踩坑的地方。因为两条曲线分属不同的“层”,直接使用legend可能会遇到问题。

正确的方法:收集所有需要出现在图例中的图形对象句柄,然后一次性创建图例。

% 示例3:正确添加图例 x = linspace(0, 20, 200); temperature = 15 + 10 * sin(2*pi*x/24); % 模拟日温度变化 rainfall = 5 * randn(size(x)); % 模拟随机降雨量,有正有负 rainfall(rainfall < 0) = 0; % 将负值置零,降雨量不为负 figure; yyaxis left; h_temp = plot(x, temperature, ‘b-‘, ‘DisplayName’, ‘日间温度’); % 关键:设置’DisplayName’ ylabel(‘温度 (°C)’); ax = gca; ax.YColor = ‘b’; yyaxis right; h_rain = plot(x, rainfall, ‘g:’, ‘LineWidth’, 2, ‘DisplayName’, ‘降雨量’); % 关键:设置’DisplayName’ ylabel(‘降雨量 (mm)’); ax.YColor = ‘g’; xlabel(‘时间 (小时)’); title(‘气象数据日变化’); grid on; % 创建图例:传入两个曲线句柄 legend([h_temp, h_rain], ‘Location’, ‘northwest’);

为什么这样做?

  • plot命令中直接使用‘DisplayName’, ‘图例名’参数,是为图形对象分配图例文本最清晰的方式。
  • legend([h1, h2], …)通过显式传递图形对象句柄数组来创建图例,确保了MATLAB能准确找到并识别来自不同y轴的数据序列。这是最可靠的方法,避免了自动检测可能带来的混乱。

常见陷阱:

  • 陷阱1:图例顺序错乱。如果不指定句柄,legend可能按图形对象的创建顺序生成图例,而这个顺序可能不符合你的预期。显式指定句柄可以完全控制顺序。
  • 陷阱2:图例框覆盖曲线。使用‘Location’参数(如‘northoutside’,‘eastoutside’)可以将图例放在坐标轴外部,或者用鼠标拖动调整位置。

4. 高级应用与复杂场景实现

掌握了基础之后,我们可以挑战一些更复杂的应用场景,这些在实际科研和工程中非常常见。

4.1 混合图表类型:曲线与柱状图组合

有时我们需要用折线表示趋势,用柱状图表示分布或总量,双y轴完美支持这种混合。

% 示例4:折线图与柱状图组合 months = 1:12; sales = [120, 135, 160, 190, 210, 300, 280, 260, 230, 200, 180, 150]; % 销售额 growth_rate = [0, 12.5, 18.5, 18.8, 10.5, 42.9, -6.7, -7.1, -11.5, -13.0, -10.0, -16.7]; % 增长率百分比 figure(‘Position’, [100, 100, 800, 500]); % 设置图形窗口大小 yyaxis left; hb = bar(months, sales, ‘FaceColor’, [0.7 0.7 0.9], ‘EdgeColor’, ‘k’); ylabel(‘销售额 (万元)’, ‘FontSize’, 12); ax = gca; ax.YColor = ‘k’; ylim([0, 350]); yyaxis right; hl = plot(months, growth_rate, ‘ro-‘, ‘LineWidth’, 2.5, ‘MarkerSize’, 10, ‘MarkerFaceColor’, ‘r’); ylabel(‘月度增长率 (%)’, ‘FontSize’, 12); ax.YColor = ‘r’; ylim([-20, 50]); hline = refline(0, 0); % 添加一条y=0的参考线 hline.Color = [.5 .5 .5]; hline.LineStyle = ‘–‘; % 添加数据标签到柱状图 for i = 1:length(sales) text(months(i), sales(i)+10, num2str(sales(i)), … ‘HorizontalAlignment’, ‘center’, ‘FontSize’, 9); end xlabel(‘月份’); title(‘年度销售额与增长率分析’, ‘FontSize’, 14); xticks(1:12); xticklabels({‘Jan’,‘Feb’,‘Mar’,‘Apr’,‘May’,‘Jun’,‘Jul’,‘Aug’,‘Sep’,‘Oct’,‘Nov’,‘Dec’}); grid on; legend([hb, hl], {‘销售额’, ‘增长率’}, ‘Location’, ‘northwest’);

实操心得:

  • 图形叠加顺序:默认情况下,后绘制的图形对象会覆盖在先绘制的对象之上。这里我们先画bar(柱状图),再画plot(折线图),确保了折线图和其数据点标记完整显示,不会被柱状图遮挡。
  • 坐标轴范围:左右y轴的范围需要根据数据精心调整,以确保两个数据序列都能清晰展示,且比例协调。例如,左侧销售额从0开始,右侧增长率则包含负值,我们通过ylim设置了对称的范围[-20, 50],并将零线标出,使增长和下降一目了然。
  • 数据标签:使用text函数为柱状图添加数值标签,能进一步提升图表的可读性。计算标签的y坐标时,用了sales(i)+10,是为了让标签显示在柱顶上方一点,避免重叠。

4.2 多数据序列与复杂图例管理

一个坐标轴侧完全可以绘制多条曲线,图例需要管理更多的项目。

% 示例5:单侧多曲线与复杂图例 t = 0:0.01:2; % 左侧轴:两组电压信号 V1 = sin(2*pi*5*t); V2 = 0.5 * cos(2*pi*3*t + pi/4); % 右侧轴:两组电流信号 I1 = 50 * exp(-2*t) .* sin(2*pi*10*t); I2 = 30 * (1 - exp(-t)) .* randn(size(t))*0.3; % 带噪声的上升信号 figure; yyaxis left; hV1 = plot(t, V1, ‘b-‘, ‘LineWidth’, 1.5, ‘DisplayName’, ‘V1 (5Hz)’); hold on; % 关键:在左侧轴上开启图形保持,以添加第二条曲线 hV2 = plot(t, V2, ‘c–‘, ‘LineWidth’, 2, ‘DisplayName’, ‘V2 (3Hz)’); hold off; ylabel(‘电压 (V)’); ax = gca; ax.YColor = [0, 0.5, 1]; % 设置左侧轴颜色为蓝绿色系 yyaxis right; hI1 = plot(t, I1, ‘r-‘, ‘LineWidth’, 1.5, ‘DisplayName’, ‘I1 (衰减振荡)’); hold on; hI2 = plot(t, I2, ‘m:’, ‘LineWidth’, 1.5, ‘DisplayName’, ‘I2 (噪声上升)’); hold off; ylabel(‘电流 (mA)’); ax.YColor = [1, 0.2, 0.2]; % 设置右侧轴颜色为红紫色系 xlabel(‘时间 (s)’); title(‘多信号混合分析’); grid on; % 创建图例,按顺序传入所有四个曲线句柄 legend([hV1, hV2, hI1, hI2], ‘Location’, ‘best’, ‘NumColumns’, 2); % 使用两列布局图例

注意事项:

  • hold on的使用:在同一个yyaxis侧绘制多条曲线时,必须使用hold on来保留当前轴上的现有图形,否则后一条plot命令会清除前一条。绘制完成后,使用hold off关闭保持状态是一个好习惯。
  • 图例布局:当图例项较多时,‘NumColumns’参数非常有用,可以将图例排列成多列,节省空间,使图表更紧凑。
  • 颜色协调:为同侧的多条曲线选择在色系上相近但可区分的颜色(如左侧用蓝、青,右侧用红、品红),有助于读者快速将曲线与对应的y轴关联起来。

4.3 导出与发布:确保印刷和展示质量

辛辛苦苦画好的图,最终要插入论文、报告或PPT中。导出设置不当,可能导致字体模糊、尺寸错误或背景问题。

1. 设置图形属性:在绘图代码的最后、导出之前,可以通过set(gcf, …)set(gca, …)对图形窗口和坐标轴进行全局美化。

% 图形美化设置示例 ax = gca; % 坐标轴线条加粗 ax.LineWidth = 1.5; % 网格线设置为浅灰色虚线 ax.GridLineStyle = ‘-‘; ax.GridColor = [0.8, 0.8, 0.8]; ax.GridAlpha = 0.7; % 设置图形背景为白色(非默认的浅灰) set(gcf, ‘Color’, ‘w’); % 设置坐标区域背景为白色 ax.Color = ‘w’;

2. 导出为矢量图(推荐用于出版):矢量格式(如PDF, EPS, SVG)无限缩放不失真,是学术出版的首选。

% 导出为PDF exportgraphics(gcf, ‘my_dual_axis_plot.pdf’, ‘ContentType’, ‘vector’, ‘Resolution’, 300); % 导出为EPS (某些期刊要求) % print(gcf, ‘-depsc’, ‘-r300’, ‘my_plot.eps’);
  • ‘ContentType’, ‘vector’:指定输出为矢量图形。
  • ‘Resolution’, 300:对于混合了栅格元素(如图片)的图形,此参数设置其分辨率。纯矢量图形此参数不影响。

3. 导出为高分辨率栅格图(用于网页或PPT):

% 导出为PNG exportgraphics(gcf, ‘my_plot.png’, ‘Resolution’, 600); % 导出为TIFF (印刷常用) exportgraphics(gcf, ‘my_plot.tiff’, ‘Resolution’, 600);
  • ‘Resolution’, 600:设置输出分辨率为600 DPI,确保印刷清晰。

4. 调整图形尺寸和边距:有时默认的图形尺寸或边距不适合你的文档。

% 在创建图形时或导出前设置 figure(‘Units’, ‘inches’, ‘Position’, [1 1 6 4]); % 6英寸宽,4英寸高 % 或者调整坐标轴在图形中的位置,留出更多边距给标签 ax = gca; ax.Position = [0.15 0.15 0.75 0.75]; % [左, 下, 宽, 高], 比例值

5. 常见问题、排查技巧与进阶思考

即使按照步骤操作,你也可能会遇到一些意想不到的问题。下面是我在长期使用中总结的一些“坑”和解决方案。

5.1 常见问题速查表

问题现象可能原因解决方案
右侧y轴标签或刻度不显示/错位1. 在yyaxis right后忘记执行ylabel或相关设置。
2. 图形窗口或坐标轴尺寸太小,标签被挤出可视区域。
1. 检查代码,确保在yyaxis right后正确设置了ylabel
2. 增大图形窗口(figure(‘Position’, [x,y,width,height]))或调整坐标轴位置(ax.Position)。
图例只显示一条曲线使用legend()时未显式指定所有需要显示的图形对象句柄。使用legend([h1, h2, …])语法,将所有曲线句柄放入一个数组中传入。确保在plot时使用‘DisplayName’参数或后续用set(h, ‘DisplayName’, …)指定名称。
左右y轴刻度线颜色未随曲线改变未手动设置坐标轴的YColor属性。yyaxis只会自动关联一次颜色,后续手动修改曲线颜色后,轴颜色不会自动更新。plot命令后,显式设置ax.YColor为与曲线一致的颜色。例如:ax.YColor = h1.Color;
hold on命令无效或导致图形混乱hold on的状态是针对当前激活的y轴侧的。在切换yyaxis后,hold状态可能会被重置或产生混淆。最佳实践:在同一个yyaxis侧连续绘制多条曲线时,集中处理。即:yyaxis left; hold on; plot(…); plot(…); hold off;然后再yyaxis right; hold on; …。避免在两侧来回切换时依赖hold状态。
导出图片字体改变或元素缺失1. 导出为栅格图时分辨率不足。
2. 系统中缺少图形使用的字体。
3. 使用了exportgraphics不支持的图形特性(极少数)。
1. 提高导出分辨率(如600 DPI)。
2. 将图形中的字体设置为常用字体(如set(gca, ‘FontName’, ‘Arial’)),或导出为PDF/EPS矢量图。
3. 尝试使用print函数或另存为(File -> Save As)功能。
想删除某一侧的某条曲线,但误删其他直接对图形对象进行操作时,未正确指定句柄。在创建图形对象时,务必保存其句柄(如h1 = plot(…))。删除时使用delete(h1)。使用cla reset会清除当前激活侧的所有图形,需谨慎。

5.2 性能优化与大数据量处理

当需要绘制成千上万个数据点时,双y轴图可能会变得响应缓慢。这里有几个优化技巧:

  1. 数据降采样:如果数据点密度远高于屏幕像素密度,绘制所有点就是浪费。可以在绘图前对数据进行均匀降采样。
    % 假设原始数据 x, y 很长 decim_factor = 10; % 降采样因子 x_down = x(1:decim_factor:end); y_down = y(1:decim_factor:end); % 使用 x_down, y_down 进行绘图
  2. 简化图形对象:避免使用复杂的标记(如‘o’,‘s’),尤其是对于大数据系列。使用简单的线条(‘-‘,‘–‘)能显著提升渲染速度。
  3. 关闭实时渲染:在批量生成大量图形或进行复杂交互式更新前,可以暂时关闭图形渲染。
    set(gcf, ‘Visible’, ‘off’); % 隐藏图形窗口,后台操作 % … 执行一系列绘图操作 … set(gcf, ‘Visible’, ‘on’); % 操作完成后再显示 % 或者使用更底层的控制 ax = gca; ax.NextPlot = ‘replacechildren’; % 优化绘图模式

5.3 超越yyaxis:更复杂的多轴需求

yyaxis解决了两个y轴的问题。但如果需要三个或更多个y轴,或者需要更灵活地排列坐标轴(比如上下排列),就需要用到更底层的axes函数手动创建和定位坐标轴对象。这属于更高级的图形定制,其核心思路是:

  1. 使用axes(‘Position’, [left, bottom, width, height])在指定位置创建一个新的坐标轴。
  2. 分别在不同的坐标轴对象上绘图。
  3. 使用linkaxes函数将它们的x轴链接起来,以实现缩放和滚动的同步。
  4. 手动设置每个坐标轴的YColor,YTickLabel等属性,并可能需隐藏某些坐标轴的框线以避免视觉混乱。

这种方法给了你最大的自由度,但代码复杂度也急剧上升。对于绝大多数双y轴需求,yyaxis已经是功能、易用性和性能的最佳平衡点。

画出一张专业的双y轴图,工具的使用只是第一步,更重要的是对数据本身的理解和想要传达的信息的思考。永远记住,图表是为叙事服务的。在动手之前,先问自己:我为什么要将这两组数据放在一起?是为了对比趋势、揭示相关性、还是展示因果?左侧和右侧的变量哪个是主,哪个是次?刻度的范围如何设定才能最公平地展示两者关系,又不会误导读者?颜色、线型、标记的选择是否考虑了色盲友好性和黑白打印时的可区分性?

在我自己的工程报告中,我习惯于先用yyaxis快速生成草图,审视数据关系。然后,我会花更多时间在调整视觉元素上:让主要关注的曲线更粗、颜色更醒目;让辅助的曲线用虚线或细线;确保坐标轴标签包含完整的单位和物理意义;图例放在不遮挡关键数据的位置。这些细节的打磨,往往比使用一个酷炫的功能更能体现工作的严谨和专业。最后,在导出前,务必在最终要使用的媒介(如PDF阅读器、PPT幻灯片)上预览一下效果,确保万无一失。

← 返回列表