ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

MATLAB三元相图绘制实战:从坐标变换到高级可视化

MATLAB三元相图绘制实战:从坐标变换到高级可视化 去年处理一批三元合金样品的硬度数据成分是三种元素的配比想把“成分—性能”关系画成经典的三元相图。第一反应是在MATLAB里搜索ternary结果发现基础工具箱里根本没有现成函数。网上找来的代码要么是十多年前的脚本要么画出来坐标错乱、标注歪七扭八折腾了一周才终于搞定。这篇文章就围绕“MATLAB三元相图实战”展开从数据准备讲到高级可视化技巧覆盖坐标变换原理、散点绘图、性能云图、等值线叠加和各类报错排查。材料、化工、地学、食品配方这些凡是“三组分体系占比数据”的场景都能直接套用只要会MATLAB基础语法跟着操作就能画出能进论文、能进汇报PPT的三元图。1. 为什么MATLAB不给你现成的三元图坐标本质与实现思路1.1 三元坐标到底在表达什么三元相图表达的是三组分体系的配比关系。假设体系由A、B、C三种组分构成常见的数据形态是质量分数、摩尔分数或体积分数三者满足约束条件 A B C 100%归一化以后就是 A B C 1。这个约束条件非常关键——它意味着表面上看起来有三个变量实际自由度只有两个。打个比方二元组分图是一条线段一端是A、一端是B中间每个点表示两者的比例两个变量加起来等于1所以只需要一维坐标就能表达。三元组分图则是一个等边三角形三个顶点各代表100%的纯组分三条边代表“第三种组分含量为0”的二元混合物而三角形内部的每个点都对应一个唯一的(A, B, C)组合。MATLAB没有提供直接画三元图的官方函数本质原因是它不属于通用绘图需求而是材料、化工、地质这些专业领域的高频工具。官方绘图体系里plot、scatter、contour、pcolor这些底层函数已经足够强大缺的只是“把三元组分坐标换算成二维平面坐标”这一步。我的建议是别急着找大而全的工具包先把坐标变换吃透后面所有高级需求都能自己拼出来。1.2 从三角形坐标到平面坐标的数学变换画三元图的第一步是映射。选择一个等边三角形让A、B、C三个顶点分别对应三个纯组分。常用约定是A 100% 位于左下角 (0, 0)B 100% 位于右下角 (1, 0)C 100% 位于顶部 (0.5, sqrt(3)/2)从三元组分 (A, B, C) 到平面坐标 (x, y) 的变换公式为s A B C x (B 0.5 * C) / s y (sqrt(3) / 2 * C) / s注意这里内部先做了归一化所以原始数据即使总和不是严格等于1也能处理。验证几个特殊点当 A1, B0, C0 时x0, y0对应左下角当 B1 时x1, y0对应右下角当 C1 时x0.5, ysqrt(3)/2对应顶部。再看一个内部点比如 A0.5, B0, C0.5它应该位于左边斜边的中点代入公式得到 x0.25, ysqrt(3)/4确实是AC边中点。这个验证过程强烈建议自己做一遍能极大减少后面坐标写错的可能性。如果想把三角形整体放大或挪位置只需要把 (x, y) 乘以边长L再加上平移向量即可。本文统一用边长为1的三角形避免缩放干扰。1.3 三条技术路线对比我在调研过程中整理过三条路线各有优劣。方案上手难度可控性适用场景手动坐标变换 plot/scatter/contour中极高论文出图、定制化需求File Exchange上的现成ternary工具低中快速查看数据趋势App Designer自制应用高高做成工具给团队反复使用File Exchange上的工具包比如ternplot确实能省时间但代码质量参差不齐有的十几年没更新在新版MATLAB里运行会报错。最稳妥的还是自己写一个变换函数几十行代码就能完全掌控所有细节。本文主路线就是第一种同时会在第6节给出封装为可复用函数的建议。2. 数据准备从实验记录到能直接扔进绘图函数的表2.1 先统一成分的计量口径这个坑我见过太多次了。实验测试结果出来的数据有的是质量分数(wt%)有的是摩尔分数(at%)有的直接给元素浓度值。如果不统一口径就直接画图点的分布趋势可能会完全失真。实际操作中我一般先用一行命令检查表格里各组分的数量级summary(tbl)重点看A、B、C三列的最小值、最大值和均值。如果三列都是百分比数值比如5~90这样那有可能已经统一如果一列在0~1之间、另一列在0~100之间那肯定没统一必须换算。常见的换算包含质量分数转摩尔分数、原子分数转质量分数公式不复杂但要注意换算后重新归一化。归一化之后每个样品的 ABC 应该等于1或100%如果偏差超过2%建议标出来好好查一下原始记录。2.2 用table组织数据别用裸矩阵我早期习惯把实验数据存成普通矩阵列少的时候还行列一多就经常忘记第5列到底是硬度还是伸长率。后来统一改用MATLAB的table类型代码可读性提升一大截。一个典型的数据表结构长这样tbl table(sampleID, A, B, C, Hardness, ... VariableNames, {SampleID, A, B, C, Hardness});后续绘图时直接写 tbl.A、tbl.Hardness而不是 data(:,2)、data(:,5)。这不仅是代码风格问题更是安全性问题——减少了列错位导致数据串用的概率尤其在脚本越来越长的时候收益非常明显。2.3 三类常见的“脏数据”必须提前处理第一类是负值。成分数据出现负值基本只有两种可能要么录入错误要么数据清洗时算错。无论哪种都应该直接报错终止而不是带着负值画图。第二类是缺失值。实验里偶尔会有某个样品测坏了得到NaN。插值计算时NaN会传染导致一大片区域都变成NaN。我通常的做法是如果样品量足够多就剔除如果样品量少就明确标注“该点缺失”不要让绘图函数静默处理。第三类是零值。端点和边线上的样品在三元图里是有意义的比如二元合金 AB 就是C0的边线点不能因为“某个组分为0”就把数据删掉。坐标变换函数内部会基于总和归一化零值样品通常能正确落在边上。2.4 数据验证函数一劳永逸的检查脚本我写了一个简单的验证函数每次换新数据前先跑一遍能省下很多排查时间function validateTernaryData(tbl, comps) % comps {A, B, C} M [tbl.(comps{1}), tbl.(comps{2}), tbl.(comps{3})]; if any(M 0, all) error(成分数据出现负值请检查原始记录); end if any(isnan(M), all) error(成分数据包含NaN请填充或剔除); end total sum(M, 2); dev max(abs(total - 1)); if dev 0.02 warning(最大总和偏差为 %.3f超过2%%请确认计量口径是否统一, dev); end fprintf(数据验证通过。共 %d 个样品。\n, height(tbl)); end这个函数会在数据准备阶段拦截大部分低级错误。画图出问题的时候先跑一遍验证能排除掉一整类“数据本身有问题”的原因。3. 手写坐标变换三元图骨架、网格与散点的完整实现3.1 核心变换函数 abctoXY坐标变换是整个方案的基石。以下代码可以直接复制保存为 abctoXY.mfunction [x, y] abctoXY(A, B, C) % 三组分(A,B,C)转换为二维坐标(x,y) % 输入不需要归一化函数内部自动处理 s A B C; s(s 0) eps; % 防止全零导致除零 x (B 0.5 * C) ./ s; y (sqrt(3) / 2 * C) ./ s; end这段代码支持向量输入也就是说可以一次转换一整批样品点。调用方式很简单[x, y] abctoXY(tbl.A, tbl.B, tbl.C);我建议把逆变换函数也写出来用来调试和验证function [A, B, C] xyToabc(x, y) % 从平面坐标反推三组分浓度 C 2 * y / sqrt(3); B x - 0.5 * C; A 1 - B - C; end有了这两个函数调试时只需要随机抽几个点正变换再逆变换看能不能回到原始组分能回去就说明坐标系统没问题。3.2 绘制三角形骨架和顶点标签有了坐标系统就可以画图了。先用线画出三角形的三条边vertices [0 0; 1 0; 0.5 sqrt(3)/2; 0 0]; plot(vertices(:,1), vertices(:,2), k-, LineWidth, 1.5); axis equal; % 这是必须的否则三角形会被拉伸变形 axis off; % 三元图通常不显示普通坐标轴 hold on;标签摆放也有讲究。三个顶点名称如果直接写在顶点上很容易被数据点遮挡最好往外偏移一点text(-0.05, -0.03, A, FontSize, 14, FontWeight, bold, ... HorizontalAlignment, right); text(1.05, -0.03, B, FontSize, 14, FontWeight, bold, ... HorizontalAlignment, left); text(0.5, 0.87, C, FontSize, 14, FontWeight, bold, ... HorizontalAlignment, center);这里 HorizontalAlignment 参数很容易被忽略但它直接决定文字会不会跑出绘图区。写完之后用 xlim 和 ylim 给标签留出呼吸空间xlim([-0.15, 1.15]); ylim([-0.1, 0.95]);3.3 网格线的推导与实现网格线能帮助读者快速读出图上点的成分比例。三元图的网格有三个方向平行于三条边的线族。每条网格线对应的数学约束是“某个组分的含量固定”。我不建议手算每个端点坐标因为非常容易出错。更稳的做法是用 abctoXY 函数算出两个端点然后连线。详细代码如下function drawTernaryGrid(step) % step为网格间距默认0.1表示10%一条 if nargin 1, step 0.1; end hold on; % 平行于底边C组分为常数 for c step:step:(1-step) [x1, y1] abctoXY(1-c, 0, c); [x2, y2] abctoXY(0, 1-c, c); plot([x1 x2], [y1 y2], :, Color, [0.6 0.6 0.6]); end % 平行于左边B组分为常数 for b step:step:(1-step) [x1, y1] abctoXY(1-b, b, 0); [x2, y2] abctoXY(0, b, 1-b); plot([x1 x2], [y1 y2], :, Color, [0.6 0.6 0.6]); end % 平行于右边A组分为常数 for a step:step:(1-step) [x1, y1] abctoXY(a, 1-a, 0); [x2, y2] abctoXY(a, 0, 1-a); plot([x1 x2], [y1 y2], :, Color, [0.6 0.6 0.6]); end end这里有个细节值得说固定某个组分后比如固定 Cc剩余的 A 和 B 要满足 AB1-c所以一个端点是 (A1-c, B0)另一个端点是 (A0, B1-c)。直接把这些端点喂给 abctoXY函数会自动归一化不用自己算坐标。如果想加沿边的百分比刻度也可以类似地调用 abctoXY 算出刻度位置再用 text 写上去。3.4 散点叠加数据点加第四维着色骨架和网格画完后最关键的一步是把实验数据点放上去。最简单的做法[x, y] abctoXY(tbl.A, tbl.B, tbl.C); scatter(x, y, 60, tbl.Hardness, filled, MarkerEdgeColor, k);这里的第四个参数 tbl.Hardness 就是第四维性能数据比如硬度、强度、响应值等。点的大小设为60边缘加黑色描边这样即使两个点靠得很近也能分辨开来。接着补充颜色映射和色条colormap(parula); colorbar; clim([min(tbl.Hardness), max(tbl.Hardness)]);关于颜色映射我有一个使用习惯论文里我优先用 parula 或 turbo因为它们的色觉友好度比较高黑白打印时也有层次感。老式的 jet 虽然鲜艳但在某些色段会有视觉误导不够推荐。clim 函数在新版MATLAB中替代了老版的 caxis如果你的版本比较老用 caxis 也可以。散点标签方面样品一多千万别给每个点都加文字那会让图面变成一团乱麻。我通常只标注极值点、目标点或特殊设计点用 text 函数手动指定位置或者用 datatip 做交互式标注。4. 进阶可视化性能云图、等值线叠加与三角形边界裁剪4.1 为什么要做性能云图散点图能看出“哪些成分点性能高”但很难精确表达“性能随成分变化的趋势”。更好的方式是先生成性能云图把整个三角形区域填充成连续变化的颜色再叠加等值线这样成分-性能的“地形图”就非常直观了。举个例子假设有一批实验样品记录了硬度数据。我们希望知道“成分点为 (A0.3, B0.4, C0.3) 附近的硬度大概是多少”散点图只能靠周围最近的点去估而云图可以直接给出插值后的连续分布。4.2 用 griddata 生成规则网格思路是先把实验数据点到平面坐标 (x, y) 和性能值 z 取出来然后在三角形覆盖的矩形区域内生成细密网格再调用 griddata 插值[x, y] abctoXY(tbl.A, tbl.B, tbl.C); z tbl.Hardness; % 生成细网格200x180分辨率足够大多数场景使用 [Xq, Yq] meshgrid(linspace(0, 1, 200), linspace(0, sqrt(3)/2, 180)); Zq griddata(x, y, z, Xq, Yq, linear);这里有个很容易出现的坑Xq 的取值范围是 [0, 1]Yq 的取值范围是 [0, sqrt(3)/2]正好覆盖三角形。但 griddata 默认会在整个矩形范围内插值也就是说它会“脑补”出三角形外面大片区域的数据。如果不处理画出来的图上会有两个角甚至更多区域出现莫名其妙的颜色这就是异常填充。4.3 三角形边界掩膜与裁剪解决办法是用 inpolygon 函数判断哪些点真正落在三角形内部把外部点置为 NaN% 三角形顶点 xv [0 1 0.5]; yv [0 0 sqrt(3)/2]; mask inpolygon(Xq, Yq, xv, yv); Zq(~mask) NaN;原理很简单inpolygon 逐个判断网格点是否在多边形内部返回逻辑索引外部点赋值为 NaN 之后pcolor 和 contourf 会自动跳过这些点不绘制任何颜色和等高线。这一步是整个云图效果是否干净的关键。掩膜之后还有一个小细节由于插值算法本身存在数值误差边界附近的个别网格点可能被判定为内部但插值出来的值明显失真。如果发现边界有毛刺可以对 mask 做一次腐蚀处理比如把离三角形边界距离小于两个网格步长的点也置为 NaN。4.4 等高线叠加与出图美化云图基础上叠加等值线能让数值读取更精确% 先画填充云图 contourf(Xq, Yq, Zq, 20, LineStyle, none); colormap(turbo); clim([min(tbl.Hardness), max(tbl.Hardness)]); hold on; % 再叠加等值线 [C, h] contour(Xq, Yq, Zq, 10, k-, LineWidth, 0.8); clabel(C, h, FontSize, 9, LabelSpacing, 200); % 最后画回三角形边框保证边界清晰 plot(vertices(:,1), vertices(:,2), k-, LineWidth, 1.8); hold off;这里 clabel 的 LabelSpacing 控制等高线标注之间的间距数值越大标注越稀疏免得密密麻麻看不清。我在实际使用中通常设成 200~300再根据图幅大小微调。这套流程做完基本上就是一张能直接放进论文里的“成分-性能”三元图了。如果再配合第3节的散点叠加实验原始点也能保留在图上审稿人一眼就能看出哪些位置有真实数据、哪些位置是插值出来的可信度更高。5. 踩坑实录从点挤死角到等高线出界的排查链路5.1 所有点都堆在左下角坐标变换写错还是数据没归一化这是最经典的故障。症状是数据点全部挤在三角形的左下角附近整个图基本废掉。排查分两步走。第一步检查数据总和在命令行执行total tbl.A tbl.B tbl.C; min(total), max(total)如果 max 超过 1.5那大概率是数据没归一化比如直接把百分比数值当成小数用了。abctoXY 虽然内部做了归一化但如果在调用前你手动做了一次错误处理比如不小心乘以某个系数就会出问题。第二步抽查一个已知点。比如某个样品 A0.1, B0.2, C0.7按公式手算一下预期坐标x (0.2 0.35) / 1 0.55y sqrt(3)/2 * 0.7 ≈ 0.606。然后用 MATLAB 算一遍对比结果。不一致就说明变换公式有问题。这个排查链路的核心思想是先用最简单的已知点验证基础函数再往上排查数据。不要一上来就在几百个点的图里找原因效率太低。5.2 三角形被拉伸变形axis equal与坐标轴范围的锅很多人画完三角形发现它变成了瘦高的或扁平的形状三条边不再等长。原因是MATLAB默认会根据坐标轴范围自动拉伸坐标系。解决办法就是那句必须记住的话axis equal;这句话放在 plot 三角形骨架之后。加了它x轴和y轴的单位长度才会保持一致三角形才不变形。但注意axis equal 之后如果 xlim 和 ylim 设置不匹配仍然可能出现显示问题。我的做法是先 axis equal再 axis off然后设置一个稍微宽裕的 xlim 和 ylim最后用 axis manual 锁死显示范围。5.3 网格线与三角形边线对不上端点计算习惯性出错画出网格线后发现有些线没有“接住”三角形的边导致网格在边缘处断裂。原因几乎都是端点坐标手算错了。我的建议是绝对不要手算网格线的端点。每个端点都用 abctoXY 去算因为你只需要提供“该端点是哪两个组分、第三个组分为0”这样的语义信息剩下交给函数。比如底边网格线固定 Cc 之后一个端点肯定是 (A1-c, B0, Cc)另一个是 (A0, B1-c, Cc)。这样写出来的代码逻辑和数学定义一一对应几乎不可能错。5.4 等高线跑到三角形外部mask没做彻底或插值边界失真云图和等高线画出来后发现三角形外面也有颜色或等高线这是 griddata 在矩形区域插值的结果。解决办法就是第4节写的 inpolygon 加 NaN 掩膜。但掩膜之后还可能遇到一个情况三角形内部靠近边界的区域出现怪异的颜色斑块。这是插值算法在边界处“外推”导致的失真。我遇到时一般做两件事一是把 mask 往里缩一两个网格点把边界附近疑似失真的点剔除二是检查数据点是否在边界附近过于稀疏如果是就需要补充实验数据或者把插值方法从 linear 换成 natural自然邻近插值后者在边界处的表现通常更稳定。5.5 导出的图片模糊或文字发虚出图参数配置用默认的 saveas 或 print 导出的图放到Word或PPT里经常发虚。我现在统一使用 exportgraphicsexportgraphics(gca, ternary_plot.png, Resolution, 600);600dpi对论文来说足够对汇报PPT使用300dpi就够。另外推荐在绘图前统一字体set(gca, FontName, Times New Roman, FontSize, 11);这样导出的图片在排版时不会因为字体缺失而出现替换、走位问题。6. 多场景扩展多子图对比、动画演示与出图收尾6.1 多子图对比多个性能指标实际项目中往往不只测一个性能指标。硬度、抗拉强度、伸长率、导电率可能都有数据。用 tiledlayout 把它们并排列出来对比效果非常好tiledlayout(1, 3, Padding, compact, TileSpacing, compact); metrics {Hardness, Stress, Elongation}; for k 1:3 nexttile; drawTernaryGrid(0.1); hold on; scatter(x, y, 40, tbl.(metrics{k}), filled, MarkerEdgeColor, k); colormap(turbo); clim([min(tbl.(metrics{k})), max(tbl.(metrics{k}))]); colorbar; title(metrics{k}); axis equal; axis off; end这里有一个小技巧三张子图最好共用同一个颜色范围否则视觉上会误导读者以为同一颜色代表相同数值。如果三列性能数据的量纲差异很大就分别设置颜色范围但要在每张图下醒目地标出数值范围。6.2 动态演示成分扫描的动画可视化如果你想展示“某一组元含量逐渐增加时数据分布如何变化”用动画比静态图更有说服力。核心思路是循环drawnowF(numel(0.1:0.1:1)) struct(cdata, [], colormap, []); idx 1; for c 0.1:0.1:1.0 % 筛选当前C含量范围的样品 cIdx tbl.C c - 0.05 tbl.C c 0.05; cla; drawTernaryGrid(0.1); hold on; % 其他点灰显 scatter(x(~cIdx), y(~cIdx), 20, [0.75 0.75 0.75], filled); % 当前区间点亮 scatter(x(cIdx), y(cIdx), 80, r, filled, MarkerEdgeColor, k); axis equal; axis off; title(sprintf(C含量区间: %.0f%% ± 5%%, c*100)); drawnow; F(idx) getframe(gcf); idx idx 1; end % 导出GIF imwrite(F, ternary_scan.gif, LoopCount, Inf, DelayTime, 0.5);这个动画在做项目汇报时特别有用审阅的人能直观看到“C元素含量升高时数据点从右向左迁移”这样的动态变化。注意 getframe 获取的帧对象包含 cdata 和 colormap直接给 imwrite 用需要把每个帧转成 RGB 或索引图像实际使用时可以用 rgb2ind imwrite 的组合但上面的写法已经能覆盖大部分存档需求。6.3 数据量很大的时候散点让位给密度热力图当样品数量上千甚至上万时散点图会严重重叠几乎看不出分布规律。此时有两种替代方案一是把整个三角区域划分为小格子统计每个格子里的样品数量用 pcolor 画密度热力图二是对两个变量做二维直方图再用颜色表示频数。我经历过一个合金成分筛选项目三千多个配比点散点图完全糊成一片。改用密度热力图之后成分集中区域和稀疏区域一目了然对后续配方优化方向的选择帮助很大。三元相图的优势就在于它能同时展示三个变量的联合分布这个信息用普通二维散点图是表达不出来的。6.4 最后分享一点我的实际体会踩过这么多坑之后我现在做三组分体系可视化的流程非常固定先跑一遍数据验证函数再用 abctoXY 转换坐标最后按需要叠加散点、云图和等值线。这套方法在铝合金成分设计、锂电材料配比、聚合物共混体系上都验证过胜在可控、可复现、出图效率高。如果你也是做三组分体系的数据分析建议别急着下载各种大而全的工具包先把坐标变换原理吃透。一旦理解了“三元组分在数学上就是二维投影”这个本质后面无论遇到什么需求比如加一条特定成分线、标注一个目标区、把新数据映射到旧图上都能用相同的思路自行扩展。用到最后你会发现真正费时间的从来不是绘图本身而是想清楚你要表达什么、用什么方式表达最准确。这恰恰是高级可视化技巧的核心价值所在。
返回列表