尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

MATLAB三元相图绘制全攻略:从坐标换算到高级可视化

MATLAB三元相图绘制全攻略:从坐标换算到高级可视化 做材料、化工或者制药配方研究的人对三元相图应该都不陌生。三个组分加在一起正好是100%落在同一个等边三角形里每个点代表一组完整配方能在一张图上看清楚相区范围、性能边界、混合规律这些关键信息。但真到了动手用MATLAB画的时候很多人会卡在同一个位置翻遍帮助文档也找不到一个叫ternplot或ternaryplot的原生函数。我最早也被这个问题折腾过后来从坐标换算开始把整个绘制流程完整趟了一遍。这篇文章就把从数据准备到高级可视化的全部要点整理出来给需要画三元相图的人做一份可以直接照着操作的完整参考。这篇文章不会丢给你一个现成函数就完事而是从数据应该长什么样、坐标怎么换算、三角形框架怎么搭、散点怎么落、相区怎么填、等值线怎么画一步一步讲清楚。涉及的代码基于MATLAB R2019b之后版本都能直接运行个别函数在R2018上需要做小替换我会在对应位置单独说明。适合刚接触三元相图的研究生也适合被“画一张像文献里那样规范的三元图”折磨过几天的工程师。1. 三元相图的核心概念与绘制思路拆解1.1 三元相图到底在表达什么三元相图是一个三组分体系的“配方地图”。A、B、C三个组分的含量百分比之和为100%于是任何一组配方都可以对应到等边三角形内部唯一的一个点上。三角形的三个顶点代表纯组分三条边分别代表两组分的二元混合体系内部任意一点则代表三组分同时存在的混合配方。生活里最直接的类比是调配一杯果汁饮料橙汁、苹果汁、水三样东西按不同比例混合比例始终是总和100%。每种比例在三角形里就是一个点如果某种比例喝起来口感好就可以把这个点标成“优质区”口感差的标成“劣质区”。三元相图本质上就是这个思路的工程化版本只是把“口感”换成了“相态”“强度”“电导率”“溶解度”这些工程关注的响应值。在材料、化工、能源领域三元相图的应用极其广泛。陶瓷行业常画CaO-Al₂O₃-SiO₂系相图来设计烧结配方冶金领域用渣系三元图判断液相线温度聚合物领域用三组分相图做共混膜和微胶囊的配方筛选制药行业还会用三元相图筛选自微乳制剂的辅料组合。不管是哪类背景画图的底层需求都一致把三组分数据和对应的响应值放进同一张图里快速识别趋势与边界。1.2 用MATLAB画三元图的三个真实痛点痛点第一坐标系问题。MATLAB原生绘图函数围绕直角坐标设计plot、scatter、fill、contour全都是二维直角坐标体系。三元相图用的是三角坐标输入是三组分的比例输出却要落在等边三角形平面里这一步必须自己完成坐标映射。痛点第二数据形态不统一。有人手里的数据是几十个离散配方点的测试结果有人拿到的是相区边界的边界点列还有人需要在一堆散点基础上插值画出性能等值线。三种数据形态的处理方式完全不同如果不先想清楚就把数据丢进绘图函数画出来的图大概率是错的。痛点第三细节装饰全靠手搓。三角形边框、网格线、三轴刻度、顶点标签在MATLAB里没有现成组件全部要自己写代码生成。加上text标注容易重叠、中文容易乱码、导出大图容易模糊这些老问题三元相图的“最后一公里”往往比坐标换算还费时。所以我的绘制思路可以归结为三步先把数据清洗成统一的三列比例格式再做三角坐标到直角坐标的映射最后用一套自建的绘图函数叠加三角形框架、散点、相区、等值线和标注。这套流程一旦跑通后面换任何三元体系都只是换数据的问题。2. 数据准备决定出图质量的第一步2.1 三种常见数据形态的处理方法我在实际项目中遇到的三元相图数据基本逃不出下面三类形态第一类是离散配方点。这是最常见的结构每一行代表一次试验包含A、B、C三个组分的比例以及一个或多个响应值。比如聚合物共混实验里A、B是两种聚合物C是增容剂响应值是膜的拉伸强度。对这种数据最终画出来的通常是带颜色或类别区分的散点图。第二类是相区边界点列。这类数据描述了某个相区比如单相区、两相区、三相区在三角坐标下的轮廓线一般是一圈有序的边界点。画图时需要把边界点连成闭合多边形再用半透明颜色填充。相图采集软件导出回来的boundary数据大多属于这一类。第三类是等值线数据。这种数据在成分空间内分布比较密集每个点都带一个连续变化的响应值需要通过插值生成整个三角形区域内的性能场再画等值线或者彩色云图。比如三元体系电导率随组分的变化扫描几百个点铺满整个三角区域就属于典型的需要插值画等值线的场景。拿到数据第一步不要直接画图先判断自己属于哪一类。判断错了后面全盘皆输。最典型的反面例子是把几十个稀疏离散点直接做插值结果等值线穿过了根本没有数据支撑的区域看起来精确实际上全是假象。2.2 比例归一化与常见脏数据三元相图对输入数据有一个硬性前提A、B、C三组分之和必须严格等于1或者说100%。但实验记录里经常出现比例之和不是1的情况。原因很多比如配比表中混入了第四组分助剂或者质量百分比四舍五入后累积误差超过千分之一。我处理这种事情的原则很简单加一步归一化永远不要跳过。% 归一化确保 ABC1 total A B C; A A ./ total; B B ./ total; C C ./ total;如果原始比例中有个别组分是零比如许多配方点落在二元边上归一化本身不会出问题。真正容易出问题的是全零行一旦某一行A、B、C全是0归一化会出现0/0得到NaN画图时这个点会消失还会污染之后的插值。处理办法是在归一化前先检查总和把全零行直接剔除或标记出来。另一个容易踩的坑是精度问题。归一化之后我习惯保到小数点后4到6位因为后面做插值和等值线时小数点后的小差异都可能导致网格点被错误地归到三角形外部。坐标边界上的点差一个1e-6就可能从三角形内侧飘到外侧。2.3 我常用的数据组织格式参考我通常在MATLAB里把离散点数据整理成一个table或者至少一个列整齐的矩阵格式固定成四列A、B、C、response。比如一组样例数据A比例B比例C比例响应值(拉伸强度MPa)0.800.100.1034.20.600.200.2028.70.400.300.3022.50.300.400.3019.80.200.500.3017.10.100.800.1015.6只要数据整理成这种结构后续所有绘图函数都可以直接对接。等值线数据也应用同样的格式只是点数更多、分布更密集。相区边界数据则要额外保证点列是有序的闭合环否则连线会交叉乱飞。3. 坐标换算与三角形构图框架实现3.1 重心坐标一步到位的数学原理三元相图画图最核心的一步是把三组分比例映射到平面直角坐标。这一步看起来复杂实际只需要用一个等边三角形的重心坐标公式。我把三个顶点放在如下位置A顶点放在(0, 0)B顶点放在(1, 0)C顶点放在(0.5, sqrt(3)/2)也就是边长为1的等边三角形。因为ABC1任意配方点P的平面坐标就是三组分在这个三角形三个顶点上的加权平均x A0 B1 C*0.5y A0 B0 C*sqrt(3)/2化简一下就变成非常清爽的两行x B 0.5*Cy C*sqrt(3)/2这个映射完全不需要旋转矩阵或者复杂的仿射变换直接线性加权就能把三角空间里的点一一映射到等边三角形平面内。这也是我对比过好几套方案之后觉得最稳的一种理解成本低也不容易出现边界漂移。3.2 坐标转换函数与反向转换我把这个转换封装成了一个通用函数tern2cart后续所有画图都调用它function [x, y] tern2cart(a, b, c) % TERN2CART 三元组成转换为三角平面直角坐标 % 顶点A(0,0)顶点B(1,0)顶点C(0.5, sqrt(3)/2) % 输入a、b、c为三个组分比例要求 abc1 x b 0.5 * c; y c * sqrt(3) / 2; end有反算需求的时候也需要一个cart2tern函数。比如你在图上点击了一个点想读出对应的三组分比例就需要反向计算function [a, b, c] cart2tern(x, y) % CART2TERN 三角平面直角坐标反算三元组成 c 2 * y / sqrt(3); b x - 0.5 * c; a 1 - b - c; end反向转换里a用1-b-c计算而不是直接再去取一个坐标分量。这样做的原因是数值稳定性更好即使输入的点稍微偏离三角形平面也能强制满足总和为1。3.3 三角形框架与网格线绘制坐标转换函数就绪后先把三角形框架画出来。我这里给出了一个可以反复调用的网格绘制函数plotTernaryGridfunction plotTernaryGrid(step) % PLOTTERNARYGRID 在三角坐标中绘制背景网格 % 默认按0.1步长绘制10%间隔网格线 if nargin 1 step 0.1; end hold on; % 绘制等边三角形边框 patch([0 1 0.5], [0 0 sqrt(3)/2], [0.97 0.97 0.97], ... EdgeColor, [0.3 0.3 0.3], LineWidth, 1.2); % 固定A组分画平行于底边的线 for a0 step:step:1-step bv linspace(0, 1-a0, 100); cv 1 - a0 - bv; [xv, yv] tern2cart(a0*ones(size(bv)), bv, cv); plot(xv, yv, Color, [0.75 0.75 0.75], LineWidth, 0.5); end % 固定B组分画平行于左边斜边的线 for b0 step:step:1-step av linspace(0, 1-b0, 100); cv 1 - b0 - av; [xv, yv] tern2cart(av, b0*ones(size(av)), cv); plot(xv, yv, Color, [0.75 0.75 0.75], LineWidth, 0.5); end % 固定C组分画平行于右边斜边的线 for c0 step:step:1-step av linspace(0, 1-c0, 100); bv 1 - c0 - av; [xv, yv] tern2cart(av, bv, c0*ones(size(av))); plot(xv, yv, Color, [0.75 0.75 0.75], LineWidth, 0.5); end end这个函数在第一次调用时可能会多花一两分钟理解但之后几乎每次画三元图都用得上。画完网格线后所有散点和填充都会叠在统一的灰色网格背景上图的专业感一下就上来了。有一个细节值得单独提醒绘图后务必加上axis equal否则MATLAB自动调整纵横比会让等边三角形被拉伸成不等边的样子刻度空间也会失真。如果你发现自己的三角形越看越扁八成就差这一行。4. 基础绘制散点分布、相区填充与轴标签4.1 离散配方点的散点绘制散点图是三元相图里最常用的表达方式。把配方点的A/B/C比例转成x/y坐标后可以直接用scatter画点用颜色或点型承载响应值。如果响应值是连续数值比如拉伸强度、电导率我的做法是用颜色映射表达figure(Color, w); hold on; plotTernaryGrid(0.1); [x, y] tern2cart(data.A, data.B, data.C); scatter(x, y, 60, data.response, filled); colormap(turbo); colorbar; axis equal; axis off;这里scatter的第三个参数60是点大小第四个参数是颜色数据配合colormap和colorbar就能做出一张带性能标尺的三元散点图。turbo色带在R2020b之后可用如果用老版本MATLAB改成jet或者parula都行。如果响应值是离散类别比如相态是“透明均一”“微乳液”“凝胶”这时候更适合用gscatter函数按类别分组建模或者手动循环画不同颜色和marker的散点figure(Color, w); hold on; [idx1, idx2, idx3] deal(data.type 1, data.type 2, data.type 3); [x, y] tern2cart(data.A, data.B, data.C); scatter(x(idx1), y(idx1), 60, [0.2 0.5 0.8], o, filled); scatter(x(idx2), y(idx2), 60, [0.9 0.4 0.2], s, filled); scatter(x(idx3), y(idx3), 60, [0.3 0.7 0.3], ^, filled);离散类别的散点图在相图类文章里出现频率极高。三个相区如果颜色对比拉得开图的内容哪怕不加等值线也已经很能说明问题。4.2 用半透明patch填充相区如果手头有相区边界点列下一步就是把相区画成填充色块。MATLAB里填充多边形用fill或者patch都行我习惯用patch因为FaceAlpha透明度控制起来更顺手。% phase1边界点n×3矩阵列为A/B/C比例 [xb, yb] tern2cart(phase1(:,1), phase1(:,2), phase1(:,3)); patch(xb, yb, [0.85 0.90 0.85], ... FaceAlpha, 0.35, ... EdgeColor, [0.2 0.4 0.2], ... LineWidth, 1.2);多个相区叠加时透明度尤其重要。相邻相区用不同色相、同样较低的FaceAlpha叠出来的交叉区颜色自然加深读者一眼就能看出两个相区可能重叠。我一般把FaceAlpha控制在0.25到0.4之间低于0.2颜色太淡高于0.5则会把底层散点完全盖住。有一个小坑是这个循环体必须手动闭合。如果边界点列不是首尾相连的填充区域会出现一条不自然的缺口。处理办法是在patch之前给坐标向量首位补一个重复点或者直接用fill函数fill会自动处理闭合。4.3 三轴刻度与顶点标签标注三元相图没有现成的坐标轴刻度标注全得自己写。三个顶点的标签比较简单直接在顶点附近放text文本text(0, -0.04, A, FontSize, 14, FontWeight, bold, ... HorizontalAlignment, center); text(1, -0.04, B, FontSize, 14, FontWeight, bold, ... HorizontalAlignment, center); text(0.5, sqrt(3)/2 0.04, C, FontSize, 14, ... FontWeight, bold, HorizontalAlignment, center);轴上刻度的实现则要沿着三条边循环放置。比如底边AB的刻度点坐标就是(t, 0)其中t从0.1变化到0.9左边斜边AC的刻度沿边取点再往左下方向偏移一点右边斜边BC的刻度则往右下方向偏移。注意文本的方向要顺着边的方向旋转否则在两条斜边上的数字看起来会东倒西歪。% 底边AB刻度标注该点对应的B组分含量 for k 1:9 t k / 10; text(t, -0.03, sprintf(%d%%, k*10), ... FontSize, 8, HorizontalAlignment, center); end这一步纯属磨细节但这恰恰是自绘图和专业软件出图差距最明显的地方。很多跑得通的代码画出来的三角形看起来“差点意思”往往就是刻度标注和文本位置没调好。5. 高级可视化等值线、三角插值填充与多图对比5.1 在三角空间里画等值线的双平面插值法散点图只能表达离散点上的信息等值线才能把连续变化的响应场完整展示出来。但MATLAB的contour函数是基于矩形网格的不能直接用在三角坐标上这是三元相图可视化里最大的一个坎。我的解决方案是“双平面插值法”。既然A1-B-C那B和C就可以看作两个独立变量原来散点数据在b-c平面上其实落在左下三角区域。我先生成b-c平面的矩形网格再筛掉网格中bc1的部分用scatteredInterpolant插值得到响应场最后用contour提取等值线点再转换回三角坐标绘制。代码实现如下% 数据准备B、C为自变量A 1 - B - C B data.B; C data.C; resp data.response; % 生成b-c平面网格限制在三角区域内 bq linspace(0, 1, 200); cq linspace(0, 1, 200); [Bg, Cg] meshgrid(bq, cq); mask (Bg Cg) 1; % 只保留三角形内部 % 插值 F scatteredInterpolant(B(:), C(:), resp(:), natural); Z F(Bg, Cg); Z(~mask) NaN; % 三角形外置为NaN防止contour画出去 % 使用contour提取等值线边提取边转换坐标 figure(Color, w); hold on; plotTernaryGrid(0.1); [Cc, ~] contour(Bg, Cg, Z, [20 24 28 32], k-); k 1; while k size(Cc, 2) level Cc(1, k); npts Cc(2, k); qb Cc(1, k1:knpts); qc Cc(2, k1:knpts); % 转换回三角平面坐标 xline qb 0.5 * qc; yline 0.5 * sqrt(3) * qc; plot(xline, yline, LineWidth, 1.8); k k npts 1; end axis equal; axis off;插值时我特意用了natural方法而不是默认的linear。原因在边界位置线性插值在数据点覆盖不足的三角形边缘会出现外推生成不自然的等值线弯折natural插值对不规则分布的数据更稳健适合三元体系这类边界附近采样稀疏的场景。5.2 用Delaunay三角化直接画彩色填充场如果你想画的是“热力图”式的连续场不是等值线那比转网格更高效的方法是直接在散点上做Delaunay三角化然后用patch按插值颜色填充。tri delaunay(data.B, data.C); [x, y] tern2cart(data.A, data.B, data.C); figure(Color, w); hold on; patch(Faces, tri, ... Vertices, [x(:), y(:)], ... FaceVertexCData, data.response(:), ... FaceColor, interp, ... EdgeColor, none); colorbar; colormap(turbo); axis equal; axis off;这行代码的含义是把离散点连成三角网每个三角形顶点处的颜色由该点响应值确定三角形内部颜色按顶点值渐变插值最终形成一块连续的颜色表面。相比网格插值方案这个方法不需要先生成密集矩形网格直接在原始点上工作计算量小得多。但这个方法有一个限制条件数据点必须足够密集且不能跨相区乱连。我见过把这个方法用到稀疏数据上的情况三角片直接跨越整个三角形把本来不相邻的区域染成同一颜色误导性极强。所以如果数据点少于几十个或者明显存在相区边界我会先按相区拆分数据再对每个相区分别做三角化最后叠加到同一个坐标系里。5.3 多子图对比与出版级导出设置做完单张三元相图工程上经常要对比不同批次或不同温度条件下的相图。多子图并排时最需要注意的是色标统一不然两幅图同一颜色代表不同的值读者稍不留意就被误导。我的习惯是先用subplot布局再统一调色标范围figure(Color, w); for i 1:4 subplot(2, 2, i); % 画第i组数据的散点/等值线... xlabel(sprintf(Case %d, i)); caxis([minVal, maxVal]); % 老版本用法 % R2022a之后可用 clim([minVal, maxVal]); endcaxis是历史版本写法R2022a开始推荐clim但caxis仍然能用。为了保证老用户复制代码不出错我用了caxis。导出图片时R2020a以上版本推荐exportgraphics自带抗锯齿导出的图片质感比saveas好太多exportgraphics(gcf, ternary_phase.png, Resolution, 300);老版本没有exportgraphics就退回到printprint(gcf, -dpng, -r300, ternary_phase.png);300dpi是期刊和报告的基本要求。如果只是日常看效果150dpi足够文件体积也小很多。6. 常见问题速查表与避坑心得6.1 高频问题与解决方案对照把平时回复同事和学员问得最多的几个问题整理成了一张表按图索骥能省不少时间现象可能原因推荐解法等值线延伸到三角形外部插值网格未做三角形区域maskbc1的网格置为NaN散点全部挤在底边附近某个组分占比极低点本身接近二元边确认数据无误后可忽略或剔除该组分重新归一化相区填充色盖住散点FaceAlpha设置过高降到0.3左右三角形显示成不等边形状缺少axis equal统一加上axis equal中文标签显示为方框MATLAB默认字体不含中文字形图内用英文标注或者单独指定FontName为支持中文的字体等值线在梯度平缓处莫名其妙断开插值网格分辨率不足把linspace点数从200提高到500patch颜色不渐变FaceColor设置成了flat而非interp改成FaceColor, interp表格之外还有一个频率极高的坑Delaunay三角化跨相区连线。某个配方点在相区I另一个在相区II直接把所有点拿去delaunay生成的三角片会把两个相区之间的相区III区域“染”成过渡色完全不符合物理事实。我处理这种问题的标准流程是先聚类或按已知相区分组然后每个相区单独三角化。6.2 边界数据和稀疏数据的特殊处理等值线插值对边界点非常敏感。如果数据只在三角形内部密集三条边附近缺少采样点scatteredInterpolant会在边界区域产生明显的外推畸变等值线沿着边线乱卷。处理办法是在边界处补充虚拟点每个边上的端点、中点按实际相邻数据的响应值粗略赋值参与插值但不参与最终显示。这样等值线在靠近边缘时会被这些边界约束点“拉住”不会飞出三角形。稀疏数据要格外克制。拿到只有二三十个点的实验数据时我个人建议最多画散点图加相区边界填充不要硬画等值线。二三十个点插出来的等值线看起来连续美观但信息量撑不起这种视觉密度很容易误导读者以为那些区域真的被数据覆盖过。宁可用散点相区划分表达也好过强行插值。6.3 我的个人习惯与工具沉淀建议经验积累下来我画三元相图前都会执行一遍固定流程先归一化检查再用tern2cart做坐标转换然后plotTernaryGrid画网格再叠加散点或填充最后统一调字号、色标和导出分辨率。流程固化成习惯之后一张规范的三元相图从拿到数据到导出成稿基本十分钟内能完成。我建议你也把这两个基础函数存成自己的工具文件一份tern2cart、一份plotTernaryGrid。之后不管画什么体系的三元图都从这两个工具开始叠图。根据我的经验这么做有三个好处减少重复输入造成的低级错误统一了所有出图风格后续想加新功能也只要在一个地方改。最后再分享一个小技巧。如果数据里偶尔出现个别异常点先别急着画图用最大值最小值扫一遍各个组分和响应值列通常能在十秒内发现单位写错、数量级差几位这类低级问题。三元相图因为坐标系特殊异常点不像直角坐标图那么容易被眼睛捕捉到一旦混进去污染整个插值结果排查起来非常耗时。数据清洗环节多花一分钟后面少花一小时。
返回列表