ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

Matlab绘制三元相图全攻略:从坐标转换到等值线填充

Matlab绘制三元相图全攻略:从坐标转换到等值线填充 简介本资源是一套基于MATLAB实现三元相图可视化绘制的完整代码实践包面向计算机、电子信息工程、应用数学等专业的本科生与研究生适用于课程设计、期末大作业及毕业设计中的材料科学或热力学可视化需求。压缩包共34个文件包含10个核心MATLAB脚本.m、23张示例输出图像.png及1份说明文档.md总大小2.18MB其中STernary.m为通用三元坐标绘图函数多个ternDemo*.m文件覆盖不同数据格式与标注风格的典型用例PNG图像直观展示各类相图效果便于快速验证与参考。已有630人学习下载资源结构清晰、即开即用提供从原始数据输入、坐标变换、区域着色到标签标注的全流程实现逻辑特别适合具备MATLAB基础、需自主调试与拓展功能的学习者作为技术参照。 打开那份挂着“源码图片”字样的压缩包之前我一直觉得用Matlab画三元相图应该跟普通散点图差不多找个自带函数直接画就完事。真正上手才发现Matlab并没有内置“三元坐标轴”这样现成的图形类型所有三角形边框、刻度线、网格线都要自己一笔一画算出来。这也是为什么网上能找到一堆零散代码但很少有人把完整逻辑讲清楚。这个项目标题很典型说明很多做材料、化工、地质、食品、制药相关实验的人都会遇到同一个需求手里有A、B、C三个组分的比例数据想把它们展示在一个等边三角形里顺便把相边界、等温线这类信息也叠上去。这篇文章我就把整个方案拆开揉碎从数学原理讲到完整可运行的代码再讲我实际调试时踩过的坑。适合那些刚接触三元相图或者已经会画散点但想进一步画等值线、填充相区的读者。1. 为什么说画三元相图的关键是坐标转换1.1 三元相图在表达什么从成分点到相区边界三元相图的本质是把三组分体系的成分信息映射到一个等边三角形平面里。比如一个体系包含A、B、C三种物质三角形三个顶点分别代表纯A、纯B、纯C三角形内部任意一个点就代表一组具体的三元比例比如40% A、30% B、30% C。在实际科研和工程里这种图最常见的用途是看“相区”也就是在什么样的成分组合下体系处于单相区、两相区还是三相区。材料科学里的合金设计、物理化学里的液液相平衡、制药领域的辅料配比筛选都会用到。很多人一开始以为三元相图很复杂其实它跟普通二维图最大的区别只有一个普通图用两个正交坐标轴表达两个变量而三元相图用三个“非正交”的方向表达三个组分只不过这三个组分之间有一个天然的约束——加起来等于100%。因为有这个约束三元体系真正的自由度只有两个所以它在二维平面上完全可以表达。这个“压缩到二维平面”的过程就是坐标转换。1.2 等边三角形坐标系的几何直觉我习惯先画一个等边三角形来找感觉。假设三个顶点分别记作顶点A、顶点B、顶点C那么三角形内任意一点P它距离三条边的相对位置就可以对应三组分的含量。越靠近顶点AA的占比越高越靠近对边BCA的占比越低。这种“距离定比例”的直觉真正转换到计算机坐标里却有个麻烦我们知道的是三组分比例比如A0.4、B0.3、C0.3这是一个三维空间里的点在平面上的投影。计算机画图时只知道x轴和y轴没有“三角形坐标”这种原生概念所以必须先找到一个映射关系把三维的比例坐标映射到二维的屏幕坐标。等边三角形的好处是三个方向对称刻度均匀。很多人问为什么不用等腰直角三角形用当然可以用但三个轴的比例尺会变得不均匀刻度间隔也难统一不利于阅读。所以绝大多数三元相图都默认画成等边三角形。1.3 Matlab没有原生三元坐标轴所以要自己做变换Matlab的plot、scatter、contourf这类函数全部建立在直角坐标系上。你给它们一个三元比例它们并不知道这个比例应该画在哪。我们需要的是先把三元比例换算成直角坐标再调用普通的绘图函数。这就是整个项目里最核心、也最容易被忽略的技术点。很多网上的半成品代码问题都出在这一步有人直接把A含量当x、B含量当y画出来是一个被压缩变形的直角坐标图根本不是三元相图有人转换公式写错了数据全落在三角形外还有人算对了坐标但忘了归一化导致ABC不等于1时整个图形比例混乱。所以这篇文章所有代码都围绕一个核心函数展开——ternary2cartesian它负责把任意三元比例映射到二维坐标。只要这个函数是对的后面所有画边框、画网格、画等值线都只是沿用它。2. 坐标转换公式与刻度网格的完整推导2.1 从三元比例到二维坐标的矩阵表达我先给出最常用的等边三角形坐标定义。设三个顶点的二维坐标分别为顶点A代表100% A在 (1, 0)顶点B代表100% B在 (0.5, sqrt(3)/2)顶点C代表100% C在 (0, 0)这个三角形是一个边长1的等边三角形底边是C到A右斜边是A到B左斜边是B到C看起来比较舒服。现在给定任意一组三元比例 a、b、c它们可以是0到1之间的小数也可以是0到100之间的百分数。只要先做归一化得到三者比例归一化后的值a_norm a / (a b c) b_norm b / (a b c) c_norm c / (a b c)那么该点在二维直角坐标系里的坐标就是x a_norm * 1 b_norm * 0.5 c_norm * 0 y a_norm * 0 b_norm * sqrt(3)/2 c_norm * 0翻译成代码非常简洁function [x, y] ternary2cartesian(a, b, c) % 输入三组分比例归一化后转换为二维直角坐标 total a b c; if any(total(:) 0) error(三元比例之和不能为0); end a a ./ total; b b ./ total; x a 0.5 * b; y (sqrt(3) / 2) * b; end注意这里c没有直接出现在x、y表达式里因为c被约束为1 - a - b只要a和b确定c自然确定。这也再次说明三元体系只有两个自由度。很多初学者会坚持把c也写进公式最后反而把坐标搞乱。如果你不喜欢这个顶点位置想把三角形摆成正三角形且三个顶点均匀分布也可以换一套坐标比如把顶点A放正上方、顶点B放右下、顶点C放左下。但代码逻辑完全一样只是三个顶点的二维坐标不同。我建议统一用上面这套因为x坐标在0到1之间y坐标在0到0.866之间后续做插值、裁剪都很方便。2.2 刻度位置为什么用线性插值三角形边框画好后需要加刻度线。三元相图的刻度跟普通坐标轴不太一样它沿三条边分布C-A边反映的是C和A的比例变化B为0A-B边反映的是A和B的比例变化C为0B-C边反映的是B和C的比例变化A为0每条边上的刻度位置本质上就是“在两个顶点之间做线性插值”。这个思想很关键因为它不需要重新推导任何几何公式只要知道两个端点的坐标就能算出任意比例对应的位置。以A-B边为例假设要从A点走向B点步长参数t从0变到1当t0时在A点当t1时在B点。如果要在A-B边上标出“B含量为20%”的刻度因为B顶点本身代表100% BA顶点代表0% B所以这个刻度点的参数就是t 0.2坐标就是P (1 - t) * 顶点A坐标 t * 顶点B坐标写成代码p (1 - t) * v(1, :) t * v(2, :);其中v矩阵保存三个顶点的坐标。同理B-C边上的“C含量”刻度也是从B点向C点插值C-A边上的“A含量”刻度从C点向A点插值。这里面有个容易搞混的细节不同边上的插值方向决定了t对应的是哪一个组分。比如A-B边上t对应B含量B-C边上t对应C含量C-A边上t对应A含量。方向反了刻度标签的位置就会错位画出来特别别扭。我自己的做法是固定按“前一个顶点到后一个顶点”的方向插值并在注释里写清楚每一段对应哪个组分。2.3 网格线的参数化算法三元相图里的网格线通常是一组平行于三角形三条边的虚线分别表示A、B、C某一组分的恒定比例线。例如“A20%”的网格线是所有A含量为20%的点连成的线段它平行于B-C边。画这些线的参数化算法也很直观。以“固定A含量”为例假设A比例固定为a那么B含量b可以从0扫到1-a对应的C含量就是c 1 - a - b。每取一个b就得到一个三元比例(a, b, c)调用ternary2cartesian得到二维坐标把所有点连起来就是一条平行于B-C边的线段。代码大概是这样的思路function plot_ternary_grid(step) vals step:step:(100-step); % 固定A for a vals bVec linspace(0, 100 - a, 50); cVec 100 - a - bVec; [x, y] ternary2cartesian(a * ones(size(bVec)), bVec, cVec); plot(x, y, --, Color, [0.8 0.8 0.8]); end % 固定B、固定C同理 end注意这一步不需要特别精确到50个点因为三元比例到二维坐标是线性变换连线段只需要两个端点就够了。用linspace(0, 100-a, 2)完全可以但多取几个点也不会出错反而在某些边缘情况下看起来更平滑。网格线要不要画取决于你的图用途。如果只是展示散点成分网格会显得拥挤如果是做相图分析网格线能帮助读者快速读取某个点的大致成分建议保留。网格线的密度用步长10%比较合适我用过5%的整张图会糊成一片。3. 完整源码实现从0到1搭一个三元相图绘制函数3.1 第一个函数坐标转换与基础画布我建议把这套代码拆成几个独立函数放在同一个文件夹里这样以后画任何三元图都能复用。第一个函数就是前面写的ternary2cartesian它只做坐标转换不画任何图形保证单纯可靠。第二个函数负责画三角形边框、刻度和顶点标签我叫它ternary_axes。这个函数的关键点是画完边框后要用axis equal保证横纵比例一致否则等边三角形会被拉成歪三角形。然后用axis off把默认坐标刻度隐藏否则会在图旁边出现一堆多余的x、y数值。顶点标签我习惯用英文大写字母A、B、C放在顶点外侧稍远的位置避免跟网格线重叠。function h ternary_axes(tickStep) % 绘制等边三角形边框、刻度和顶点标签 if nargin 1 tickStep 10; end v [1, 0; 0.5, sqrt(3)/2; 0, 0]; % 顶点A、B、C hold on; axis equal; axis off; % 边框 plot(v([1 2 3 1], 1), v([1 2 3 1], 2), k-, LineWidth, 1.5); % 刻度 labels tickStep:tickStep:(100-tickStep); for k 1:length(labels) t labels(k) / 100; % AB边从A到Bt对应B含量 pAB (1 - t) * v(1, :) t * v(2, :); % BC边从B到Ct对应C含量 pBC (1 - t) * v(2, :) t * v(3, :); % CA边从C到At对应A含量 pCA (1 - t) * v(3, :) t * v(1, :); % 画短刻度线这里简化处理用小圆点代替 plot(pAB(1), pAB(2), k., MarkerSize, 8); plot(pBC(1), pBC(2), k., MarkerSize, 8); plot(pCA(1), pCA(2), k., MarkerSize, 8); % 刻度标签 text(pAB(1), pAB(2) - 0.02, num2str(labels(k)), FontSize, 8, ... HorizontalAlignment, center); text(pBC(1) - 0.02, pBC(2), num2str(labels(k)), FontSize, 8, ... HorizontalAlignment, right); text(pCA(1), pCA(2) - 0.02, num2str(labels(k)), FontSize, 8, ... HorizontalAlignment, center); end % 顶点标签 text(v(1,1) 0.04, v(1,2) - 0.04, A, FontSize, 14, FontWeight, bold); text(v(2,1), v(2,2) 0.04, B, FontSize, 14, FontWeight, bold); text(v(3,1) - 0.04, v(3,2) - 0.04, C, FontSize, 14, FontWeight, bold); end这里刻度线我用了小圆点而不是真正的短线段严谨一点应该画垂直于边的短线但圆点在大多数情况下已经能看得清楚。如果你要发表在期刊上建议改成真正的刻度短线垂直于边向外延伸大约0.02个单位这样更符合期刊制图规范。3.2 第二个函数散点、网格、自定义标签基础画布有了接下来是往图上添加核心内容。我把网格和散点分开写这样用户可以自由选择叠加哪些元素。网格函数plot_ternary_grid负责画三组平行虚线。散点函数ternary_scatter则是对scatter的封装输入三组分比例内部自动转换坐标再画点。function h ternary_scatter(a, b, c, varargin) % 三元散点图绘制 [x, y] ternary2cartesian(a, b, c); h scatter(x, y, varargin{:}); end如果你需要在每个点旁边标上样品编号或名称可以再写一个ternary_textfunction ternary_text(a, b, c, labels) % 在三元散点旁添加文本标签 [x, y] ternary2cartesian(a, b, c); text(x, y, labels, FontSize, 9, HorizontalAlignment, left, ... VerticalAlignment, bottom, Clipping, on); end注意Clipping设置为on防止某些标签跑到三角形外面去。标签重叠是常见问题尤其当样品点密集时我会先做一个简单的偏移或抖动或者用Matlab的text函数逐点微调位置。更高级的方案是用开源布局算法做标签避让但大多数时候手动微调够用了。3.3 运行结果和图形说明把所有函数组合起来一个最基本的调用脚本长这样% 模拟一组样品成分数据 A [10, 20, 30, 40, 50, 60, 70, 80, 55, 65]; B [30, 40, 20, 10, 40, 20, 10, 10, 25, 15]; C 100 - A - B; figure; ternary_axes(10); plot_ternary_grid(10); hold on; ternary_scatter(A, B, C, 60, filled, ... MarkerFaceColor, [0.8500 0.3250 0.0980], ... MarkerEdgeColor, k);运行后你会看到一张标准的等边三角形图底边C-A、右斜边A-B、左斜边B-C网格线交叉样品点落在对应成分位置。这就是三元相图最基础、最常用的样式。有两点经验供参考。第一scatter的MarkerFaceColor可以选择带透明的颜色比如[0.8500 0.3250 0.0980]这样多个点重叠也能看出密集程度。第二如果样品点很多比如上千个建议把MarkerEdgeColor改成none否则全部描边会让图看起来很脏。4. 进阶把温度、浓度等第四维信息画进三元相图4.1 用scatteredInterpolant做三角形区域插值单纯画散点只能展示成分分布但很多相图真正的价值在于展示某个性质随成分的变化比如液相线温度、黏度、界面张力、析出率。这类数据通常只在有限几个成分点上测得要画成平滑的等值线图必须先做插值。我推荐用Matlab自带的scatteredInterpolant。它接受散点的x、y坐标和对应的z值生成一个插值函数之后可以在这个函数上取任意位置的值。F scatteredInterpolant(xData, yData, zData);其中xData、yData是三元数据转换后的二维坐标zData是对应点的性质值。默认插值方法是线性如果想要平滑曲面可以换成natural。但要注意如果原始数据信噪比很低插值曲面会有一堆毛刺我一般建议先用二次多项式拟合一个趋势面再画插值结果。4.2 等温线、等值线的绘制代码绘制等值线的基本流程是在三角形覆盖的矩形区域内生成规则网格点调用插值函数计算这些网格点上的z值裁剪掉三角形外部区域用contourf或contour绘制等值线我直接给一个能跑的完整例子。这个例子里我用了一个模拟的z值函数实际使用时替换成你的实验数据即可。% 生成模拟散点数据 A rand(200,1) * 100; B rand(200,1) * (100 - A); C 100 - A - B; [x, y] ternary2cartesian(A, B, C); % 模拟性质值例如液相线温度 Z 30 0.4*A 0.6*B 0.3*C 0.005*A.*B - 0.002*C.^2 randn(size(A))*2; % 构造插值函数 F scatteredInterpolant(x, y, Z, natural); % 生成三角形区域内的网格点 [xq, yq] meshgrid(0:0.005:1, 0:0.005:0.866); in inpolygon(xq, yq, [1 0.5 0], [0 0.866 0]); zq F(xq, yq); zq(~in) NaN; % 绘图 figure; hold on; ternary_axes(10); contourf(xq, yq, zq, 15, LineWidth, 0.5); % 再把散点叠上去 ternary_scatter(A, B, C, 20, filled, MarkerFaceColor, k);这里有几个细节值得说。第一meshgrid生成的是直角坐标系下覆盖三角形的规则网格但插值函数只能保证在原有数据凸包范围内有效三角形边界附近可能因为缺少数据点出现明显畸变这需要通过合理设计实验点来避免。第二inpolygon裁剪很重要如果不裁剪图会出现一个矩形的彩色背景而不是三角形区域。第三contourf的等值线数量要适当15层左右对于大多数相图来说已经足够太多容易显得杂乱。4.3 相区填充与图例标注的小技巧等值线只是第四维信息的一种表达方式。另一种常见需求是给不同相区填充不同颜色比如把单相区涂成浅蓝色把两相区涂成浅黄色。这本质上是一个多边形填充问题只要知道相区边界上的点坐标按顺序连成一个封闭多边形用patch或fill填充即可。patch(xPolygon, yPolygon, [0.8 0.9 1], EdgeColor, k, LineWidth, 1);这里最容易犯的错误是顶点顺序错乱。patch要求多边形顶点按顺时针或逆时针连续排列如果顺序乱了会出现填充区域互相交叉的乱象。我在实际画相区时会先在一个临时图上画边界散点确认走向后再生成封闭多边形。图例方面我一般不用Matlab自带legend。三元相图的图例经常要放在三角形外面但三角形图本身信息密度高旁边的空白区域有限。我习惯手动用text加注释或者用annotation在figure级别放置这样灵活性更高。5. 踩坑与排查为了这组图我交过的学费5.1 图全跑到三角形外面去了这是新手最容易遇到的问题。数据点画出来后全都落在三角形外面或者有一部分在三角形外面。通常原因有两个一个是忘了归一化比如ABC的和等于200但你的输入没有除以总和另一个是三元比例里混入了非法值比如某些样品本来就是二元体系C0那么点会落在三角形边上这不算错误但看惯了内部散点的人会误以为数据有问题。排查方法很简单在调用ternary2cartesian之前先检查输入数据的最小值和最大值并打印总和范围。如果总和不是100也不是1说明数据不干净。还可以对转换后的x、y坐标做一次范围检查正常情况下x的范围是0到1y的范围是0到0.866超出这个范围一定有问题。5.2 网格线把图搞得没法看网格线本身不会出错但参数选不好会让图很难看。我试过把网格步长设成2%结果整张图被密密麻麻的虚线盖住数据点完全淹没在网格里。后来我调整了策略网格步长设为10%而且把网格线颜色设成浅灰色线型用点线而不是虚线这样既有参考价值又不抢主数据的注意力。另外如果图形要投期刊很多杂志要求网格线不可见或者只能有少量刻度辅助线。这时候可以做一个总的开关参数发布前把网格关掉只保留边框和刻度。5.3 中文标签变成方框Matlab默认字体在Windows下对中文支持不够好经常出现标签全是空心方框的情况。这个问题在绘制三元相图时特别突出因为相图里的组分名常常是化学式或材料名如果不是纯英文很容易踩雷。最简单粗暴的解决办法是图内标签全部用英文或希腊字母比如A、B、C组分名用元素符号替代。如果一定要显示中文用set(gca, FontName, SimHei)或text(..., FontName, SimHei)指定中文字体。我常用的是SimHei和Microsoft YaHei前者是黑体后者是微软雅黑在大多数Windows机器上都有。5.4 万级散点数据画不动三元相图不总是只有几十个点有时候我们会在一个体系里做几千甚至上万个成分点的计算比如用热力学计算软件扫出整个相图区域。这时候scatter的渲染速度会明显下降图片缩放也会卡顿。我的优化方案有三个方向第一如果数据点不需要精确到每个像素就用先抽样再绘图的方法比如从一万个点里随机抽2000个画出来趋势基本不变第二用密度图的思路把三角形区域划分成小格子统计每个格子里有多少个点然后用patch或pcolor画密度色块观感比裸散点更好第三把图形导出为矢量图时不要直接导出散点图先缩小Marker尺寸再导出不然生成的文件会非常大。5.5 等值线插值结果在边界上异常突起用scatteredInterpolant做插值靠近三角形边界的区域经常会出现不自然的突起或凹陷原因是边界附近缺少数据点插值函数在数据稀疏区域会表现得很不稳定。我常用的缓解办法是在实验设计阶段就确保边界上有足够的采样点比如在每条边上等间隔多测几个点。如果数据已经固定无法补测可以尝试用带约束的插值方法比如先用fit做一个二项式曲面拟合再在拟合结果上画等值线。或者干脆将边界处的数据点做一次镜像补充在三角形外对称地补几个虚拟点插值结果会更平滑。这个方法比较“野”但我在实际中确实用过效果不错。提示补充虚拟点时建议保持z值不变或者按照插值趋势外推不要随意编造数值否则可能引入错误信息。6. 最后说点个人习惯做这套代码的过程中我最大的体会是三元相图的难点不在绘图本身而在坐标转换以及对数据规范性的理解。只要把ternary2cartesian这个函数想透了其他所有环节都是锦上添花。我现在拿到一份三元数据第一件事不是画图而是先做数据检查算一遍各组分的总和、范围看看有没有NaN和负值。这些脏数据如果不处理后面画出来的图怎么修饰都没用。代码组织上我习惯把所有三元绘图相关的函数放在一个独立的文件夹里比如叫ternary_plot_tools里面只放函数文件。每个脚本通过addpath引入这个文件夹这样多个项目可以共用同一套绘图逻辑改一个刻度样式所有图全部同步。最后再分享一个小技巧当你需要生成多张三元相图批量输出时可以写一个简单的循环把数据文件逐个读进来然后调用同一套绘图函数最后用exportgraphics导出PNG。这样一天出几十张图也不用手工改图了。导出时分辨率建议至少300dpi期刊够用屏幕上看也清晰。本文还有配套的精品资源点击获取
返回列表