ARTICLE DETAIL

资讯详情

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

基于Matlab绘制三元相图的完整实现:坐标变换、网格与数据叠加

基于Matlab绘制三元相图的完整实现:坐标变换、网格与数据叠加 简介本资源是一套基于MATLAB实现三元相图可视化绘制的完整代码实践包面向计算机、电子信息工程、应用数学等专业的本科生适用于课程设计、期末大作业及毕业设计中的材料科学或热力学相关可视化任务。压缩包共34个文件包含23张示例效果图PNG、10个核心MATLAB脚本.m及1份说明文档.md总大小2.18MB其中STernary.m为通用三元坐标绘图函数多个ternDemo*.m文件覆盖不同数据格式与标注风格的典型用例PNG图像直观展示各类相图效果便于快速理解输出形态与参数影响。已有630人学习下载资源结构清晰、案例丰富提供从原始数据输入、坐标变换、区域着色到标签注释的全流程实现逻辑特别适合作为MATLAB图形编程与科学可视化教学的参考范例。 做组分研究的朋友应该都遇到过这个场景三种原料按不同比例混合性能数据攒了一大堆最后想画一张三元相图把所有结果落在同一个等边三角形坐标里。用Origin试了一下模板没有现成的自己画网格线又很麻烦刻度标签和坐标轴关系的处理更是让人头疼。所以后来我把这套绘制流程整体搬到了Matlab里写成了带源码和示例图片的项目包也就是标题里那个“基于Matlab绘制三元相图源码图片.rar”。这篇内容就把整个实现思路、关键代码、封装结构和踩坑记录都摊开讲一遍给同样被三角坐标折磨的科研党一个可以直接参考的版本。我默认你手上已经有Matlab基础环境R2016b之后的版本都能跑代码里用到的主要是plot、patch、scatter、scatteredInterpolant、contourf这些常规功能不需要额外装工具箱这一点在材料、化工、冶金、地质相关的课题组里适应性很高。1. 三元相图到底在画什么坐标变换是绕不开的第一关1.1 三元组成为什么只有两个自由度先明确一个基础问题三元相图的横纵坐标到底是什么。直角坐标系用(x, y)描述平面位置而三元相图描述的是三个组分A、B、C在混合物中的含量百分比。表面看似乎需要三个变量但三个组分的含量之和恒定等于100%。一旦xA和xB确定了xC就自动等于1 - xA - xB没有独立变化的余地。这个约束关系决定了三元体系本质上只有两个自由度所以它才能落到一个二维平面上。常见的展示形式是等边三角形三个顶点分别对应纯A、纯B、纯C三角形三条边对应三种二元体系内部点代表三组分混合物。我们的目标就是把满足“和为1”的三元组成映射到平面坐标系上的一个具体位置。很多人在Origin里找不到三元相图模板本质就是因为Origin的常规绘图轴无法直接处理这类约束数据。而Matlab里我们可以自己写坐标变换把这个“非标准坐标”转换成标准的笛卡尔坐标接下来的绘图就全部变成常规操作了。1.2 用重心坐标把三元组成映射到平面位置三元组成到平面坐标的变换在数学上叫重心坐标变换。设三个顶点A、B、C在平面上的位置分别为posA、posB、posC那么任意三元组成(xA, xB, xC)对应的平面坐标就是xy xA * posA xB * posB xC * posC;这个式子本质上就是在做一次加权平均。可以理解为三角形内某个点的位置等于三个顶点位置的加权和权重就是三个组分的百分比。这个变换简单、直观而且能很容易地向量化处理多个数据点。我实际项目中用的函数是这样的function xy tern2xy(a, b, c, posA, posB, posC) % 将三元组成转换为平面坐标 % 输入a,b,c可为列向量函数内部自动归一到和为1 total a b c; a a ./ total; b b ./ total; c c ./ total; xy a(:) * posA b(:) * posB c(:) * posC; end注意这里我做了一步归一化调用的时候不要求数据严格加起来等于1比如你直接填重量百分比70、20、10也能计算函数里面会自动处理。这一点在实际数据分析中挺关键我经常从实验记录表里导出的数据是百分比数值不一定是0到1的小数有了这个归一化步骤就省去了手动预处理。1.3 两种顶点排列习惯与统一约定顶点排列顺序会影响相图的方向也不只是美观问题。同样一组数据A在顶部和A在左下角画出来的相图虽然“等价”但和文献对照时容易出问题。常见的约定有两种一种是A在顶部、B在左下、C在右下整体逆时针排列另一种是A在左下、B在右下、C在顶部也是逆时针。我个人的习惯是固定使用第一种即posA [0.5, sqrt(3)/2]顶部posB [0, 0]左下posC [1, 0]右下这样排列的好处是BC边在水平方向上和常见的三元相图文献展示形式一致而且读图时从底边开始看二元体系也很自然。一旦确定了这个约定坐标变换函数、刻度标签函数都不用再为坐标方向操心。2. 把三角坐标系画出来边框、网格和刻度标签的Matlab实现2.1 顶点定义和基础线条画相图框架的第一步当然是定义三个顶点坐标。上面已经给了具体数值直接定义变量posA [0.5, sqrt(3)/2]; posB [0, 0]; posC [1, 0]; % 画出三角形边框 figure(Color, w); hold on; line([posB(1), posC(1)], [posB(2), posC(2)], Color, k, LineWidth, 1.5); line([posC(1), posA(1)], [posC(2), posA(2)], Color, k, LineWidth, 1.5); line([posA(1), posB(1)], [posA(2), posB(2)], Color, k, LineWidth, 1.5);这里有个很容易忽略的问题画完三角形后Matlab默认的坐标轴范围可能不是正方形的会导致三角形看起来是扁的。一定要加上axis equal; axis off; xlim([-0.1, 1.1]); ylim([-0.1, sqrt(3)/2 0.1]);axis equal让x轴y轴的比例保持一致axis off把边框和标准坐标刻度去掉xlim和ylim稍微留出边距这样画出来的三角形才是正立的等边三角形顶点标签不会被图形边界截断。2.2 三类网格线的生成逻辑网格线是三元相图外部特征的很大一部分。常见的网格线是10%、20%、…、90%的组分等比例线分为三类xA为常数的线、xB为常数的线、xC为常数的线。以xA 0.3为例这条线代表所有xA含量为30%的组成点的集合。它是一条平行于BC边的直线段连接AB边和AC边上的两个点。这两个点的坐标可以这样确定与AB边的交点xA为0.3那么xB为0.7xC为0坐标是 0.3 * posA 0.7 * posB与AC边的交点xA为0.3那么xB为0xC为0.7坐标是 0.3 * posA 0.7 * posCB组分和C组分等比例线的逻辑完全同理。把这三个方向的网格线放到一个函数里循环生成function drawTernaryGrid(ax, posA, posB, posC) % 绘制10%~90%的三元网格线 gridVals 0.1:0.1:0.9; lineStyle {:, [0.6 0.6 0.6]}; % xA k 的线平行于BC边 for k gridVals p1 k * posA (1 - k) * posB; p2 k * posA (1 - k) * posC; plot(ax, [p1(1), p2(1)], [p1(2), p2(2)], LineStyle, lineStyle{1}, Color, lineStyle{2}); end % xB k 的线平行于AC边 for k gridVals p1 k * posB (1 - k) * posA; p2 k * posB (1 - k) * posC; plot(ax, [p1(1), p2(1)], [p1(2), p2(2)], LineStyle, lineStyle{1}, Color, lineStyle{2}); end % xC k 的线平行于AB边 for k gridVals p1 k * posC (1 - k) * posA; p2 k * posC (1 - k) * posB; plot(ax, [p1(1), p2(1)], [p1(2), p2(2)], LineStyle, lineStyle{1}, Color, lineStyle{2}); end end这里的关键在于理解“xA为常数”对应的两个端点始终落在两条边上。不要试图在三角形内部找网格线的起点和终点那样既容易算错代码也复杂。用顶点坐标线性组合来表示边上的点是处理所有网格线问题最快的套路。2.3 刻度标签的摆放策略刻度标签放在三角形的三条边上每一条边标注一个组分的含量。比如BC边只标注xA的刻度CA边标注xB的刻度AB边标注xC的刻度。摆放标签时要避开网格线端点和顶点标签通常的做法是沿边方向取样再做一个小的偏移。下面这个例子是在BC边上放xA的刻度标签function drawTickLabels(ax, posA, posB, posC) gridVals 0.1:0.1:0.9; labels {10,20,30,40,50,60,70,80,90}; % BC边上的xA标签 for i 1:length(gridVals) k gridVals(i); pos (1 - k) * posB k * posC; % 向下偏移 text(ax, pos(1), pos(2) - 0.045, labels{i}, ... HorizontalAlignment, center, FontSize, 8); end % CA边上的xB标签沿边方向偏移 for i 1:length(gridVals) k gridVals(i); pos (1 - k) * posC k * posA; text(ax, pos(1) 0.045, pos(2), labels{i}, ... HorizontalAlignment, center, FontSize, 8); end % AB边上的xC标签沿边方向偏移 for i 1:length(gridVals) k gridVals(i); pos (1 - k) * posA k * posB; text(ax, pos(1) - 0.045, pos(2), labels{i}, ... HorizontalAlignment, center, FontSize, 8); end end偏移量0.045是根据等边三角形边长1.0的情况来定的实际项目里如果调整了边长这个偏移量要跟着缩放。也可以在函数里加一个offset参数运行时根据图纸尺寸手动微调这比写死更实用。给顶点添加字母标签也很直接text(posA(1), posA(2) 0.08, A, FontSize, 14, HorizontalAlignment, center); text(posB(1) - 0.08, posB(2) - 0.02, B, FontSize, 14, HorizontalAlignment, center); text(posC(1) 0.08, posC(2) - 0.02, C, FontSize, 14, HorizontalAlignment, center);在正式出图时可以把A、B、C替换成实际的组分名称比如Al₂O₃、SiO₂、CaO这样图面信息更完整。3. 往相图上叠数据散点、等值线和区域着色三种常见需求3.1 散点标注直接投影即可框架搭好以后往里面叠数据就变得非常简单。散点是最基本的数据展示形式比如你有20组不同配方的样品每个样品有对应的性能值可以直接用scatter画出来。% 假设data是Nx3矩阵三列分别为xA, xB, xC % values是Nx1向量如烧结温度/硬度/电导率等 xy tern2xy(data(:,1), data(:,2), data(:,3), posA, posB, posC); scatter(xy(:,1), xy(:,2), 50, values, filled); colorbar; colormap(parula);如果不需要按数值着色只做组成点的位置标注那就把values去掉全部用同一种颜色scatter(xy(:,1), xy(:,2), 40, o, MarkerFaceColor, [0.2 0.5 0.8], MarkerEdgeColor, k);这里我建议MarkerEdgeColor设为黑色细边因为不同颜色的点在浅色网格线上叠放时容易被“吞”加个深色描边会清晰很多。实测效果在黑白打印时尤其明显。3.2 等值线平面网格插值加三角形裁剪散点只能表达离散位置如果数据点足够多或者你希望表达温度、压力、性能等连续变化就要用到等值线。我推荐的流程是这样的在平面坐标上生成一个覆盖三角形区域的矩形网格用逆变换把网格点转换成三元组成坐标判断网格点是否在三角形内部用scatteredInterpolant在组成空间做插值三角形外的值置为NaN用contourf或contour画等值线逆变换函数也很简单用矩阵求逆实现function abc xy2tern(x, y, posA, posB, posC) % 将平面坐标转换为三元组成坐标 % 返回Nx3矩阵每行为(xA, xB, xC) T [posA(1), posB(1), posC(1); posA(2), posB(2), posC(2); 1, 1, 1]; pts [x(:); y(:); ones(1, numel(x))]; abc (T \ pts); end这个逆变换的思路本质上是解一个三元的线性方程组。因为平面坐标只有两个维度所以额外加入“三个组成之和为1”这个约束条件从而保证方程可解。主流程代码% 生成平面网格 [xg, yg] meshgrid(linspace(-0.05, 1.05, 300), linspace(-0.05, 0.95, 300)); abc xy2tern(xg, yg, posA, posB, posC); % 判断是否在三角形内 inside all(abc -1e-6 abc 1 1e-6, 2); inside reshape(inside, size(xg)); % 插值 F scatteredInterpolant(data(:,1), data(:,2), data(:,3), values, natural, none); z nan(size(xg)); z(inside) F(abc(inside, 1), abc(inside, 2), abc(inside, 3)); % 画等值线 contourf(xg, yg, z, 12, LineColor, none);scatteredInterpolant的插值方法我推荐使用natural它对散布不均匀的数据适应性最好不会像linear那样在某些稀疏区域产生明显的折痕也不会像spline那样出现过冲。如果你手头数据密度很高用linear速度更快结果差别不大。3.3 相区填充和边界线相图里经常需要表达不同的相区比如液相区、固相区、两相共存区。有几种常见做法第一种手头只有相区边界线数据直接把边界线连起来用patch填充内部。比如某个两相区的边界是A-B-C三个转折点围成的多边形% 边界点组成按顺时针或逆时针排列 bx [0.1 0.4 0.7 0.1]; by [0.05 0.3 0.1 0.05]; patch(bx, by, [0.9 0.8 0.9], EdgeColor, k, FaceAlpha, 0.3);第二种相区边界是从热力学计算软件导出的离散点序列直接用plot按顺序连线闭合后配合patch填充plot(boundary_x, boundary_y, r-, LineWidth, 2);第三种如果数据量足以描述相区也可以先网格插值得到一个0/1分类变量再用contourf在level0.5处画分界线。本质上就是等值线做法的一个特例把等值线设置为0.5即可。区域着色的时候注意FaceAlpha不要设置得太低透明度太低会盖住网格线透明度太高相区又看不清我一般取0.2到0.4之间。4. 源码包怎么组织函数拆分、参数设计和调用示例4.1 项目目录结构与文件分工这份源码包我并不是把所有代码堆在一个文件里而是按功能拆分成几个文件这样后续在不同项目里复用会方便很多。压缩包里的目录结构是这样的ternary_plot_demo/ ├── main_ternary_demo.m ├── funcs/ │ ├── tern2xy.m │ ├── xy2tern.m │ ├── draw_ternary_frame.m │ └── add_ternary_data.m ├── data/ │ └── sample_compositions.xlsx └── output/ ├── fig_ternary_scatter.png ├── fig_ternary_contour.png └── fig_ternary_phase.pngmain_ternary_demo.m是主脚本演示从读数据到出图的完整流程。funcs目录放函数文件data目录放示例数据output目录放生成好的图片。这样组织的好处是主脚本可以很短只做串联函数各自独立调试和复用都很方便。4.2 核心函数接口设计draw_ternary_frame.m的设计我参考了Matlab官方绘图函数常见的名称-值对参数风格接口如下function ax draw_ternary_frame(posA, posB, posC, varargin) % 可选参数 % Grid : on/off是否显示网格线默认on % TickLabels : on/off是否显示刻度标签默认on % VertexLabels: {A,B,C}顶点标签内容 % FontSize : 刻度字号默认8 % LineWidth : 边框线宽默认1.5内部实现为先画边框再根据参数决定是否画网格线和刻度标签。用varargin配inputParser可以很容易地实现这种可选参数风格。数据叠加函数add_ternary_data.m也类似function h add_ternary_data(data, values, posA, posB, posC, varargin) % 在已有三角坐标系上叠加数据 % data: Nx3矩阵三列组成 % values: Nx1向量可选用于散点着色或等值线这样封装之后主脚本就变得非常清爽% 主脚本示例 posA [0.5, sqrt(3)/2]; posB [0, 0]; posC [1, 0]; data readmatrix(data/sample_compositions.xlsx); temp data(:, 4); % 假设第四列是温度 figure; draw_ternary_frame(posA, posB, posC, ... VertexLabels, {Al2O3, SiO2, CaO}, ... FontSize, 9); add_ternary_data(data(:,1:3), temp, posA, posB, posC, ... Type, contour); exportgraphics(gca, output/fig_ternary_contour.png, Resolution, 300);4.3 两种常见调用示例根据数据形态不同通常有两种调用场景。散点场景数据是几十个离散实验点要表达的是每个点的组成位置最多用颜色区分性能高低。add_ternary_data(data(:,1:3), hardness, posA, posB, posC, ... Type, scatter, MarkerSize, 60);等值线场景数据量较大或希望用插值结果预测未测试区域的性能趋势。add_ternary_data(data(:,1:3), temp, posA, posB, posC, ... Type, contour, Levels, 15);这两种模式基本覆盖了日常90%以上的三元相图绘制需求。实际项目里我做过的聚合物-水-表面活性剂体系、陶瓷三组分烧结体系、合金成分优化体系都是在这套框架上完成的。5. 我踩过的坑三角画扁、等值线越界、标签被截断5.1 坐标轴变形三角形总是扁的这是最经典的问题。很多第一次尝试的人都会发现自己的等边三角形画出来变成等腰三角形甚至看上去像普通三角形。原因很简单Matlab默认的坐标轴纵横比例不是1:1会按照窗口尺寸自动伸缩。对策就是上面提到的axis equal必须在画边框之后立即加上。这个命令保证x轴和y轴的单位长度一致是等边三角形的基础。另外还有一个细节即使加了axis equal如果后面又调用了xlim或ylim并且两个轴的范围比例不合适仍然可能变形。所以我的建议是axis equal; xlim([-0.1, 1.1]); ylim([-0.1, sqrt(3)/2 0.1]); axis off;只要x轴和y轴的范围比例与三角形本身宽高比一致等边三角形就不会走样。5.2 等值线溢出三角形外用contourf画等值线时Matlab会默认对矩形网格区域做填充。如果不加处理会在三角形的三个角之外也画出颜色填充视觉上非常奇怪。根本原因是插值区域没有限制在三角形内。解决思路有两个第一个思路是在生成网格时直接用三角形内的点。可以在组成空间里用meshgrid生成xA和xB网格然后计算xC 1 - xA - xB把xC小于0的点全部去掉。这样得到的插值网格本身就是三角形区域的。第二个思路是前面第3.2节的方法先生成矩形网格再通过逆变换判断网格点的重心坐标是否都在0和1之间然后把三角形外的插值结果设为NaN。NaN在contourf中不会显示等于自动裁剪了。我实测下来第二种思路更通用因为第一步的矩形网格在平面坐标中是均匀的三角内部的点分布也更均匀画出的等值线平滑度更好。5.3 标签重叠、中文乱码和图片导出标签重叠经常发生。网格线端点和刻度标签的位置非常接近尤其是10%和90%这种靠近顶点的位置几乎和顶点字母标签挤在一起。我的处理方式是把刻度标签的字体缩小到8号或9号并适当增加偏移量。如果还是重叠就手动调整个别标签的位置这种“特例处理”在出论文图时很常见不用担心破坏代码结构。另外一个高频问题是中文标签乱码。Matlab在老版本里对中文支持不好我处理的办法有两种一种是图面全部用英文字段中文放到图注里另一种是设置字体set(gca, FontName, Microsoft YaHei);如果你用的是新版本Matlab默认字体支持中文一般不会出问题。但如果要投期刊英文标签永远是更稳妥的选择。图片导出方面exportgraphics是个好工具exportgraphics(gcf, output/fig_ternary_scatter.png, Resolution, 300);要提交矢量图给编辑的话用print导出pdf或eps格式print(gcf, output/fig_ternary_scatter, -dpdf, -painters);.rar包里output目录下放的示例图片就是用这两种方式导出的一份位图用于日常查看一份矢量图用于后续排版。6. 常用的进阶玩法伪三元、热力配色和批量出图6.1 伪三元体系的数据归一化实际研究很多用到的并不是严格的三组分体系而是“伪三元”。比如聚合物/水/表面活性剂体系中往往把聚合物和表面活性剂的混合物当成一个组分或者把某两种原料按固定比例合并成一个“拟组分”。这种数据如果直接送入绘图函数可能导致三个组分之和不为1画出的点跑出三角形。解决方案很简单在tern2xy函数里已经做了归一化处理所以只要你把“拟组分”的百分比填成一个数值函数内部会自动归一到100%。这算是一个意外收获——当初为省事写的归一化后来发现恰好适合伪三元数据。唯一需要注意是伪三元体系中组分边界线的热力学意义和真三元不同体现在图上时要在图注中明确说明避免读者误解。6.2 用颜色表达相区或性能指标颜色是三元相图里最有力的表现手段。两个方向一个是连续变量着色比如液相线温度、硬度、电导率等连续变化的物理量用scatter的cdata加colorbar或者用contourf的填充色表达。配色建议用parula它是Matlab默认的感知均匀色图而且对红色绿色色弱人群更友好。不要用老式的jet颜色过渡不均匀在黑白打印时也分不出层次。另一个是离散相区着色比如“单相区”“双相区”“三相区”用patch填充不同的颜色配合图例说明。相区颜色尽量选择低饱和度色避免和网格线、数据点抢视觉重点。6.3 批量出图与论文配图如果你的实验包含多组条件比如不同烧结温度下的相组成或者不同保温时间下的成分分布批量出图就很有必要了。我的做法是写一个循环把每个工况条件对应的数据文件读取后调用一次绘图流程输出一张png全部放到output目录里。生成的文件名加上条件编号for i 1:length(fileLists) data readmatrix(fileLists(i)); figure; draw_ternary_frame(posA, posB, posC, ...); add_ternary_data(data(:,1:3), data(:,4), posA, posB, posC, ...); exportgraphics(gcf, sprintf(output/fig_case_%02d.png, i), Resolution, 300); close(gcf); end这里每张图用一个独立的figure并在导出后关闭可以避免内存占用过大。图刷多了之后Matlab的图形窗口会非常吃内存特别是在数据量大的情况下这个习惯能省不少事。论文配图还要注意统一的字体字号。我一般会在一开始就建立一个绘图风格基线比如字体统一用Arial、字号8-10、线条粗细1.5、坐标轴离屏边距3mm这样一组图放在论文里风格统一看起来专业很多。最后分享一个小技巧这套代码里我最常改动的参数就是顶点位置posA/posB/posC。虽然直接给死也够用但我后来把所有函数都加了一个顶点参数接口这样换一种顶点排列顺序时只需要在调用处改一个参数坐标变换、刻度标签、网格线全都不用动。三元相图这种重复劳动量很大的工作封装设计里留出这个灵活性后面能少走很多弯路。本文还有配套的精品资源点击获取
返回列表