简介:基于 MATLAB 实现的缩放-绘图交互工具包,面向工程师与科研人员,用于在数据可视化中快速定位局部区域、多尺度查看曲线或图像,尤其适合大数据集分析、工程仿真结果探索与教学演示。压缩包共 8 个文件,以 3 个 m 脚本为主体,分别承担核心逻辑实现、图像示例与普通图形示例;另附 JSON 参数配置文件、Markdown 说明文档、许可证及 Git 属性文件,整体仅 10KB,结构紧凑,便于逐行研读。核心逻辑通过鼠标按下、拖拽与滚轮等事件回调,动态更新坐标轴范围,实现缩放和平移;同时引入数据窗口管理,保证大数据量下仍能流畅操作。从源码中可完整学习到回调注册、事件处理、坐标范围计算和性能优化等关键写法,能直接应用于自定义绘图界面。目前已有 54 人学习下载,适合有一定 MATLAB 基础、希望掌握交互式绘图技巧的读者快速参考与复用。
1. ZoomPlot for MATLAB:论文插图里的局部放大刚需
做论文插图的人都知道,一张能同时讲清楚“整体趋势”和“局部细节”的图最难画。信号波形在某一秒有个毛刺,主图拉远了看不见,拉近了上下文又丢了。ZoomPlot 就是为解决这个需求而生的 MATLAB 工具包:你在主图上圈出关注区域,在空白处放一个放大的子坐标轴,它自动用连接线把两处对应起来。本文是围绕这个压缩包的完整拆解,从底层原理、调用方式到实际踩坑记录,一次说清楚。适合正在写论文、做实验报告、需要把分析结果交给导师复核的信号处理与图像处理从业者。
2. 原理与格式:Annotation、Axes 与坐标空间换算
拿到包之后,第一件事不是急着跑示例,而是先弄清楚它到底改了什么。ZoomPlot 没有改动你的数据,也没有重画你的曲线,它做的事情恰好是在 figure 的坐标空间里,把你已经创建好的两个坐标轴关联起来,然后画一条从主图框到放大图框的连接线。下面把这条链路拆开讲。
2.1 ZoomPlot 的底层逻辑:两个 Axes 加一条连接线
在 MATLAB 里,“图”这个东西本身就是一堆图形对象的组合。主图是一个 axes,放大图也是一个 axes,两者共享同一个 figure。ZoomPlot 的核心逻辑是这样的:它接收主坐标轴句柄 mainAxes 和放大坐标轴句柄 subAxes,然后利用 annotation 函数在 figure 上绘制直线或带箭头的直线,把主图里圈出的区域与放大图连接起来。
这里有个关键点:annotation 和 plot 不一样。plot 是在 axes 的数据坐标系里画线,而 annotation 是在 figure 的归一化坐标系里画线。数据坐标会随 xlim、ylim 变化,而归一化坐标只跟 figure 的大小和位置有关。ZoomPlot 之所以能把主图框和放大图框精确对齐,就是因为它把主图 axes 的 Position 和放大图 axes 的 Position 换算到了同一套坐标空间里,再在两端点上画线。
明白了这一点,后面所有参数调整都有据可依。例如你发现连接线位置对不上,先查两根轴的 Position 属性是不是都在同一个 Units 下;你嫌连接线太粗,改的是 annotation 对象的 LineWidth,而不是 axes 的 LineWidth。这个工具不神秘,黑匣子的盖子就薄薄一层。
2.2 函数签名与输入参数:真正需要管的三个输入
ZoomPlot 的调用形式并不复杂,核心是三个输入:主图句柄、放大图句柄、连接线样式。我实际使用时的习惯是先看包里 demo 脚本跑一遍,再对着参数表微调。下面这张表是我自己在项目中常用的参数集合,供你对照。
| 输入/参数 | 含义 | 常见取值 | 备注 |
|---|---|---|---|
| mainAxes | 主坐标轴句柄 | gca 或自己创建的 axes | 建议显式创建,别用 gca 碰运气 |
| subAxes | 放大坐标轴句柄 | axes('Position', ...) | 必须先设置好 Position |
| LineStyle | 连接线线型 | '--'、':'、'-'、'-.' | 论文里我习惯用虚线 |
| Color | 连接线颜色 | [0.85 0.33 0.10] 或 'k' | 深色更稳妥 |
| LineWidth | 连接线粗细 | 1.0 ~ 2.0 | 超过 2 会喧宾夺主 |
| Arrow | 是否带箭头 | true / false | 看期刊要求,有的不让用箭头 |
| Smooth | 连接线是否平滑过渡 | true / false | 新版本常见选项,以包里文档为准 |
参数这一栏要注意,不同小版本的 ZoomPlot 支持的名称可能略有差异。我一般会在 MATLAB 里用 help zoomPlot 看当前版本的具体说明,或者直接打开源码看 input parser 这一段,比看任何教程都准。调用顺序也很直接:先画好主图,再创建放大 axes 并画好局部数据,最后调用 zoomPlot 把两者连起来。
2.3 从零到一的调用顺序:先建坐标轴,再画数据,最后连线
很多新手翻车的地方不是参数,而是顺序。ZoomPlot 默认你传入的两个 axes 都已经存在,并且数据已经画好了。如果你先调用 zoomPlot,再往 axes 里画数据,连接线的位置信息取自函数调用那一刻的 axes Position,后面的绘制动作会破坏布局的预期。
figure('Units', 'centimeters', 'Position', [2 2 18 10]); % 第一步:创建主坐标轴并绘制数据 mainAxes = axes('Units', 'normalized', 'Position', [0.08 0.12 0.60 0.75]); plot(mainAxes, t, y, 'b-', 'LineWidth', 1.2); xlim(mainAxes, [0 5]); ylim(mainAxes, [-1.5 1.8]); grid on; % 第二步:创建放大坐标轴并绘制局部数据 subAxes = axes('Units', 'normalized', 'Position', [0.72 0.15 0.24 0.36]); idx = t >= 1.9 & t <= 2.2; plot(subAxes, t(idx), y(idx), 'b-', 'LineWidth', 1.2); xlim(subAxes, [1.9 2.2]); ylim(subAxes, [0.2 1.6]); grid on; % 第三步:调用 ZoomPlot 生成连接线 zoomPlot(mainAxes, subAxes, ... 'Color', [0.85 0.33 0.10], ... 'LineWidth', 1.5, ... 'LineStyle', '--');这段代码里,主图占 figure 左侧 60% 的宽度,放大图放在右上角,两者位置不重叠。连接线的两个端点分别来自 mainAxes 的 Position 边界和 subAxes 的 Position 边界,这个对应关系是工具自动计算的。逻辑上要注意,mainAxes 的宽度不建议超过 0.65,否则会让放大图被迫挤到边缘;放大图尺寸也不宜太小,宽度低于 0.2 时内部刻度字会叠在一起。
3. 实战:三种常见图的局部放大做法
原理清楚了,接下来就是实测。这一章我准备了三类最常见的使用场景:一维信号曲线的毛刺观察、二维图像局部区域的放大、以及同一张图里放置多个放大窗口。每一段都给出可以直接抄走的代码,并标注哪些参数要按自己的数据调整。
3.1 信号波形局部放大:最标准的调用流程
先做一个带随机噪声的正弦波,在 2 秒附近人为加一个尖峰,然后用 ZoomPlot 把尖峰区域拎出来看细节。这是信号处理方向最典型的场景。
rng(0); fs = 1000; t = 0:1/fs:5; y = sin(2*pi*t) + 0.05*randn(size(t)); y(2001:2010) = y(2001:2010) + 0.8; % 人为在 2 秒处加入尖峰 figure('Units', 'centimeters', 'Position', [2 2 18 10]); mainAxes = axes('Units', 'normalized', 'Position', [0.08 0.12 0.58 0.75]); plot(mainAxes, t, y, 'b-', 'LineWidth', 1.1); xlim(mainAxes, [0 5]); ylim(mainAxes, [-1.5 2.0]); xlabel(mainAxes, 'Time (s)'); ylabel(mainAxes, 'Amplitude'); grid on; subAxes = axes('Units', 'normalized', 'Position', [0.72 0.18 0.24 0.38]); idx = t >= 1.85 & t <= 2.25; plot(subAxes, t(idx), y(idx), 'b-', 'LineWidth', 1.1); xlim(subAxes, [1.85 2.25]); ylim(subAxes, [-0.2 1.8]); xlabel(subAxes, 'Time (s)'); ylabel(subAxes, 'Amplitude'); grid on; zoomPlot(mainAxes, subAxes, ... 'Color', [0.85 0.33 0.10], ... 'LineWidth', 1.4, ... 'LineStyle', '--');这段代码里 rng(0) 是让噪声序列可复现,方便你对比跑出来的效果。主图和放大图里的数据是同一条曲线,放大的只是 xlim 和 ylim 所圈出的局部区间。有一点我不太建议做:在 subAxes 里重新生成一次数据而不是用索引切片,这样会导致两条曲线不完全一致,给导师看的时候容易被挑刺。索引切片是最稳的。
参数层面,放大图的两个坐标范围决定了信息量。xlim 太宽,放大效果不明显;太窄,只有一两个采样点,看不出波形形态。我一般先运行一次,观察放大图里的曲线形状,再回来调整范围,反复两次就能定下来。
3.2 图像区域放大:图像中 ROI 的细节展示
做图像处理时,ZoomPlot 同样可以用,只是数据从曲线变成矩阵。下面的例子展示一张含噪灰度图里,把指定 ROI 放大并对比噪声纹理。
img = imread('cameraman.tif'); img = double(img); img(80:90, 120:130) = img(80:90, 120:130) + 40; % 局部增强痕迹 figure('Units', 'centimeters', 'Position', [2 2 18 10]); mainAxes = axes('Units', 'normalized', 'Position', [0.08 0.12 0.58 0.75]); imagesc(mainAxes, img); colormap(mainAxes, 'gray'); axis(mainAxes, 'image'); axis off; subAxes = axes('Units', 'normalized', 'Position', [0.72 0.15 0.24 0.38]); imagesc(subAxes, img(70:100, 110:140)); colormap(subAxes, 'gray'); axis(subAxes, 'image'); axis off; zoomPlot(mainAxes, subAxes, ... 'Color', [1 1 1], ... 'LineWidth', 1.5, ... 'LineStyle', '-');这段代码的关键点是放大图里直接用矩阵切片 img(70:100, 110:140),而不是用 xlim 和 ylim 去限制 imagesc 的显示范围。原因在于 imagesc 默认会按像素坐标伸展,如果只改 xlim,图像会按数据范围重新映射颜色,导致放大图的灰度分布和主图不一致。矩阵切片直接从源头保证了显示内容完全一致,颜色映射也不受影响。
连接线颜色这里用了白色,因为图像本身是深色背景,黑色线会淹没在图像纹理中。如果你的主图背景偏白,连接线就用深色;如果图像内容本身颜色复杂,还可以把连接线加粗到 2.0。这里不需要强求固定风格,以最终导出图片的清晰度为准。
3.3 多放大窗口与线型组合调整
一篇论文里极少只放大一个区域,更多情况是主图是全局结果,两三个关键局部各放一个窗口。多窗口时要注意 subAxes 之间的位置不能重叠,并且后创建的 axes 会覆盖先创建的 axes,需要手动调整层级顺序。
figure('Units', 'centimeters', 'Position', [2 2 20 10]); mainAxes = axes('Units', 'normalized', 'Position', [0.06 0.10 0.50 0.80]); plot(mainAxes, t, y, 'k-', 'LineWidth', 1.0); grid on; subAxes1 = axes('Units', 'normalized', 'Position', [0.62 0.55 0.32 0.30]); idx1 = t >= 1.9 & t <= 2.2; plot(subAxes1, t(idx1), y(idx1), 'k-', 'LineWidth', 1.0); grid on; subAxes2 = axes('Units', 'normalized', 'Position', [0.62 0.12 0.32 0.30]); idx2 = t >= 3.8 & t <= 4.2; plot(subAxes2, t(idx2), y(idx2), 'k-', 'LineWidth', 1.0); grid on; zoomPlot(mainAxes, subAxes1, ... 'Color', [0.85 0.33 0.10], 'LineWidth', 1.2, 'LineStyle', '--'); zoomPlot(mainAxes, subAxes2, ... 'Color', [0.00 0.45 0.74], 'LineWidth', 1.2, 'LineStyle', '--');两个放大窗口用不同颜色的连接线,读者一眼就能区分哪条线对应哪个局部图。这里有一点必须提醒:位置重叠时,后面创建的 subAxes 会盖住前面的,所以要么像上面这样把两个窗口上下错开,要么在创建后调用 uistack(subAxes1, 'top') 调整层级。我个人更倾向于错开位置,因为 uistack 调整后会改变视觉重心,有时候连接线会被窗口遮住,反而不美观。
4. 避坑:位置、单位和连接线的五个常见问题
用了一段时间 ZoomPlot,踩过的坑基本集中在坐标单位、图层顺序和窗口尺寸这三类问题。这一章把最常见的五种翻车情况整理出来,每条按照现象、原因、解决的顺序写,方便你对照排查。
4.1 连接线对不准:Position 单位被改过
现象:连接线的端点明显偏离主图框或放大图框,看起来像随手画的两条线。原因:ZoomPlot 内部按 normalized 单位读取 axes 的 Position,但你创建坐标轴时用了 'Units', 'pixels',或者中途用 set 改过 Units,导致换算错位。解决:创建主图、放大图时统一固定 Units 为 normalized。如果已经用像素单位创建了 axes,可以在调用前补一句 set(mainAxes, 'Units', 'normalized') 和 set(subAxes, 'Units', 'normalized')。
4.2 放大图被主图覆盖:图层顺序不对
现象:放大图创建后看不见,只看到主图的坐标轴盖在它上面。原因:figure 中后创建的图形对象默认在最上层,但你如果先创建放大图再创建主图,或者用 subplot 混用了 axes 创建顺序,主图就会盖住放大图。解决:在调用 zoomPlot 之前执行 uistack(subAxes, 'top')。如果多个窗口被遮挡,按需要的视觉顺序逐个调整。这个操作不会影响数据内容,也不触发重绘,是最快的修正方式。
4.3 拉伸 figure 窗口后连接线错位
现象:编辑状态下拖动拉大 figure,连接线还留在原来的位置,跟两个 axes 对不上。原因:annotation 的位置是基于调用那一刻的 figure 归一化坐标记录下来的,而 axes 的 Position 在 Resize 后会按比例变化,两者变化逻辑不一致。解决:我的习惯是先固定 figure 尺寸再连线,出图前不再拖动窗口。如果必须在编辑状态下反复调整,每次调整后都重新执行一次 zoomPlot。推荐做法是写一个重绘函数,把创建坐标轴和调用 zoomPlot 全部放进去,改完布局直接运行整个脚本。
4.4 放大图里空白太多
现象:放大图内容只占窗口一半,周围大片留白,曲线被挤在中间。原因:plot 默认的 xlim 和 ylim 会留 5% 至 10% 的余量,放大图范围本身小,余量就显得特别大。解决:在 subAxes 上显式设置 xlim 和 ylim,不要依赖自动范围。xlim 的首尾值最好刚好覆盖目标区间,例如 1.9 到 2.2,不要写 1.85 到 2.25 这种多留余量的范围。另一个小技巧是把 subAxes 的 Position 宽高比设置成和目标数据的宽高比一致,这样放大图内部不会出现拉伸变形。
4.5 导出 PDF 后连接线颜色变淡
现象:屏幕上连接线颜色清晰,用 exportgraphics 导出 PDF 后颜色浅了一截,细线几乎看不清。原因:annotation 默认的 LineWidth 通常只有 0.5,屏幕渲染时反锯齿让它看起来还行,导出到矢量格式后线条按实际尺寸绘制,细线在缩小显示时自然发虚。解决:把连接线 LineWidth 设置到 1.2 以上,颜色尽量用深色,不要用浅灰或亮橙。导出前在 figure 窗口里缩放一下,确认线条在 80% 显示比例下仍然清晰可见。这条经验帮我在投稿阶段少吃了很多哑巴亏。
5. 进阶用法:把 ZoomPlot 变成你的论文出图模板
如果每次画图都要从头写一遍创建坐标轴的代码,效率太低了。我运行过几十次后,把它封装成了一个带默认风格的模板函数,效果稳定,也方便统一整套论文图片风格。
5.1 封装一个通用的局部放大绘图函数
function myPlotWithZoom(t, y, zoomRange, fileName) fig = figure('Units', 'centimeters', 'Position', [2 2 18 10]); mainAxes = axes('Units', 'normalized', 'Position', [0.08 0.12 0.58 0.75]); plot(mainAxes, t, y, 'k-', 'LineWidth', 1.0); grid on; subAxes = axes('Units', 'normalized', 'Position', [0.72 0.18 0.24 0.38]); idx = t >= zoomRange(1) & t <= zoomRange(2); plot(subAxes, t(idx), y(idx), 'k-', 'LineWidth', 1.0); xlim(subAxes, [zoomRange(1) zoomRange(2)]); grid on; zoomPlot(mainAxes, subAxes, ... 'Color', [0 0 0], 'LineWidth', 1.2, 'LineStyle', '--'); end这个函数接受时间序列、信号序列、放大区间和文件名四个参数,内部完成所有布局和连线逻辑。以后画类似波形图时,一行命令就能完成主图加局部放大的整套流程。封装时要注意 zoomRange 必须是双元素向量,并且范围不能超过 t 的最小值和最大值,否则 idx 索引会得到空数组。
5.2 批量出图时统一尺寸和格式
批量处理多组实验数据时,我会把所有图导出为 PDF 和 PNG 双份。PDF 用于论文投稿,PNG 用于内部沟通。导出时固定 figure 尺寸是保证版面一致的前提。
exportgraphics(fig, [fileName '.pdf'], 'ContentType', 'vector'); exportgraphics(fig, [fileName '.png'], 'Resolution', 300);PDF 一定要用 vector 类型,这样连接线和刻度线都是矢量,文字清晰度不会打折扣。PNG 用 300 DPI 基本能满足大部分期刊的图片要求。如果期刊要求 600 DPI,把 Resolution 参数改掉重跑一遍即可。这套流程我已经用了快一年,中途没有因为图片清晰度被退修过。
5.3 统一字号和线宽的全局设置
为了让整套论文图视觉风格一致,我会在脚本开头设置一组默认值。默认字号、线宽、坐标轴框线都固定下来,ZoomPlot 的连接线风格也保持一致。这样即使隔了两周再回来画图,也不会出现字号不一致的问题。
set(groot, 'DefaultAxesFontName', 'Helvetica'); set(groot, 'DefaultAxesFontSize', 9); set(groot, 'DefaultLineLineWidth', 1.0);从那以后我养成了一个习惯:每张带局部放大的图,都在脚本里固定 figure 尺寸、固定两个 axes 的范围、固定连接线参数,导出前再缩放到 80% 检查一遍连接线是否清晰。这套流程看着不起眼,但帮我省掉了大量返工时间。希望帮到你。
本文还有配套的精品资源,点击获取