MT磁性数据快速分析程序

这是一个非常实用的HTML文件,可以脱离复杂的构建环境直接在任何浏览器中双击运行。

请将以下代码保存为 MagneticAnalyzer.html,然后双击在浏览器中打开即可:

使用时请查看左侧专门的「数据格式与使用说明」面板及输入数据的格式(用空格、制表符分隔均可,可直接从 Origin 或 TXT 中复制数据)。 1/M 可以是由用户提供(第三列),或者程序自动计算(仅有两列时)。

有效磁矩 (mu_eff) 计算的量纲前提:如果需要绝对数值准确,必须采用摩尔磁化率(emu/mol/Oe)。

示意数据及图例

源代码如下

复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>磁性数据全谱分析仪</title>
    <!-- 引入 Tailwind CSS 用于界面排版 -->
    <script src="https://cdn.tailwindcss.com"></script>
    <!-- 引入 ECharts 用于数据绘图 -->
    <script src="https://cdn.jsdelivr.net/npm/echarts@5.5.0/dist/echarts.min.js"></script>
    <style>
        /* 简单自定义滚动条 */
        ::-webkit-scrollbar { width: 6px; height: 6px; }
        ::-webkit-scrollbar-track { background: #f1f5f9; }
        ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; }
        ::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
    </style>
</head>
<body class="bg-slate-50 text-slate-800 font-sans h-screen flex flex-col overflow-hidden">

    <!-- 顶部导航栏 -->
    <header class="bg-white border-b border-slate-200 px-6 py-4 flex items-center justify-between shadow-sm shrink-0 z-20">
        <div class="flex items-center gap-2">
            <svg class="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6"></path></svg>
            <h1 class="text-xl font-bold text-slate-800">磁性数据全谱分析仪 (HTML独立版)</h1>
        </div>
        <div class="text-sm text-slate-500">
            支持 M-T 曲线导数分析与居里-外斯 (Curie-Weiss) 拟合
        </div>
    </header>

    <div class="flex flex-1 overflow-hidden">
        <!-- 左侧控制面板 -->
        <div class="w-96 bg-white border-r border-slate-200 flex flex-col shadow-sm z-10 shrink-0 overflow-y-auto">
            
            <!-- 使用说明区块 -->
            <div class="p-5 bg-blue-50/50 border-b border-blue-100">
                <h2 class="font-bold text-blue-800 mb-3 flex items-center gap-2">
                    <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path></svg>
                    数据格式与使用说明
                </h2>
                <div class="text-xs text-slate-600 space-y-3 leading-relaxed">
                    <div>
                        <strong class="text-slate-800 block mb-1">【数据要求】</strong>
                        请提供 2 列或 3 列数值数据,各列之间用<span class="text-blue-600 font-semibold">空格、Tab键或逗号</span>分隔:<br/>
                        <ul class="list-disc pl-4 mt-1">
                            <li>第 1 列:<b>温度 T</b> (K)</li>
                            <li>第 2 列:<b>磁化强度 M</b> 或 <b>磁化率 χ</b></li>
                            <li>第 3 列:<b>1/M</b> (可选,程序默认自动计算 1/M)</li>
                        </ul>
                    </div>
                    <div>
                        <strong class="text-slate-800 block mb-1">【物理单位提示】</strong>
                        要获得准确的<b>有效磁矩 (μ_eff)</b>,输入的 M 必须是<b>摩尔磁化率 (emu/mol·Oe)</b>。<br/>
                        若输入为质量磁化率 (emu/g) 或原始磁矩,需自行转换后方可使 μ_eff 的绝对值具有物理意义 (相变点与外斯温度的判定不受影响)。
                    </div>
                    <div>
                        <strong class="text-slate-800 block mb-1">【操作步骤】</strong>
                        1. 在下方粘贴数据。<br/>
                        2. 在右侧图表中观察顺磁区(高温直线区)。<br/>
                        3. 调整下方的"拟合温区"滑块,使拟合虚线与实际的高温直线部分完美重合。
                    </div>
                </div>
            </div>

            <!-- 数据输入区块 -->
            <div class="flex-1 p-5 flex flex-col min-h-[300px]">
                <h2 class="font-semibold text-sm mb-2 flex items-center gap-2 text-slate-700">
                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path></svg>
                    粘贴原始数据
                </h2>
                <textarea id="dataInput" class="flex-1 w-full p-3 text-xs font-mono border border-slate-300 rounded-md focus:ring-2 focus:ring-blue-500 focus:border-blue-500 outline-none resize-none whitespace-pre overflow-x-auto shadow-inner bg-slate-50" placeholder="例如:
3.0002	0.5686
4.2090	0.5694
5.1382	0.5717
..."></textarea>
            </div>

            <!-- 拟合控制区块 -->
            <div class="p-5 border-t border-slate-200 bg-slate-50">
                <h2 class="font-semibold text-sm mb-4 flex items-center gap-2 text-slate-700">
                    <svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 6V4m0 2a2 2 0 100 4m0-4a2 2 0 110 4m-6 8a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4m6 6v10m6-2a2 2 0 100-4m0 4a2 2 0 110-4m0 4v2m0-6V4"></path></svg>
                    居里-外斯拟合温区 (K)
                </h2>
                <div class="flex items-center gap-3">
                    <input type="number" id="fitMin" value="150" class="w-full px-3 py-2 text-sm border border-slate-300 rounded shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 text-center" />
                    <span class="text-slate-400 font-bold">~</span>
                    <input type="number" id="fitMax" value="300" class="w-full px-3 py-2 text-sm border border-slate-300 rounded shadow-sm focus:outline-none focus:ring-1 focus:ring-blue-500 text-center" />
                </div>
            </div>
        </div>

        <!-- 右侧图表与结果展示区 -->
        <div class="flex-1 flex flex-col bg-slate-100 overflow-y-auto relative">
            
            <!-- 结果指标卡片 -->
            <div class="grid grid-cols-3 gap-5 p-6 pb-2 shrink-0">
                <div class="bg-white rounded-xl p-5 shadow-sm border border-slate-200 flex flex-col relative overflow-hidden">
                    <div class="absolute top-0 left-0 w-1 h-full bg-blue-500"></div>
                    <span class="text-sm text-slate-500 font-medium mb-1">相变温度 (Tc)</span>
                    <div class="flex items-baseline gap-1 mt-1">
                        <span id="tcVal" class="text-3xl font-bold text-slate-800">--</span>
                        <span class="text-sm font-medium text-slate-400">K</span>
                    </div>
                    <span class="text-xs text-blue-600/80 mt-2 bg-blue-50 px-2 py-1 rounded w-max">基于 dM/dT 极小值</span>
                </div>
                
                <div class="bg-white rounded-xl p-5 shadow-sm border border-slate-200 flex flex-col relative overflow-hidden">
                    <div class="absolute top-0 left-0 w-1 h-full bg-indigo-500"></div>
                    <span class="text-sm text-slate-500 font-medium mb-1">峰值/阻挡温度 (Tp)</span>
                    <div class="flex items-baseline gap-1 mt-1">
                        <span id="tpVal" class="text-3xl font-bold text-slate-800">--</span>
                        <span class="text-sm font-medium text-slate-400">K</span>
                    </div>
                    <span class="text-xs text-indigo-600/80 mt-2 bg-indigo-50 px-2 py-1 rounded w-max">基于 M 极大值</span>
                </div>

                <div class="bg-white rounded-xl p-5 shadow-sm border border-slate-200 flex flex-col relative overflow-hidden">
                    <div class="absolute top-0 left-0 w-1 h-full bg-teal-500"></div>
                    <span class="text-sm text-slate-500 font-medium mb-1">外斯温度 (θ_CW)</span>
                    <div class="flex items-baseline gap-1 mt-1">
                        <span id="thetaVal" class="text-3xl font-bold text-slate-800">--</span>
                        <span class="text-sm font-medium text-slate-400">K</span>
                    </div>
                    <span class="text-xs text-teal-600/80 mt-2 bg-teal-50 px-2 py-1 rounded w-max">基于设定的高温区拟合</span>
                </div>
            </div>

            <!-- 图表容器 -->
            <div class="p-6 flex-1 flex flex-col gap-6 min-h-[700px]">
                
                <!-- 图表 1: M 和 dM/dT -->
                <div class="bg-white p-5 rounded-xl shadow-sm border border-slate-200 flex-1 min-h-[300px] flex flex-col">
                    <h3 class="text-base font-bold text-slate-700 mb-2 border-l-4 border-slate-400 pl-2">磁化强度 M & 微商 dM/dT 随温度演化</h3>
                    <div id="chart1" class="flex-1 w-full h-full"></div>
                </div>

                <!-- 图表 2: 1/M 居里外斯拟合 -->
                <div class="bg-white p-5 rounded-xl shadow-sm border border-slate-200 flex-1 min-h-[300px] flex flex-col relative">
                    <h3 class="text-base font-bold text-slate-700 mb-3 border-l-4 border-teal-500 pl-2 flex justify-between items-center">
                        居里-外斯定律拟合 (1/M vs T)
                        <span id="fitRangeDisplay" class="text-xs font-normal text-slate-500 bg-slate-100 px-3 py-1 rounded-full border border-slate-200"></span>
                    </h3>
                    
                    <!-- 拟合详细参数展示框 -->
                    <div class="mb-3 p-3 bg-teal-50/50 border border-teal-100 rounded-lg text-sm text-teal-900 grid grid-cols-2 gap-y-2">
                        <div><strong class="opacity-75">拟合方程:</strong> <span id="fitEquation" class="font-mono">--</span></div>
                        <div><strong class="opacity-75">决定系数 R²:</strong> <span id="r2Val" class="font-mono">--</span></div>
                        <div><strong class="opacity-75">居里常数 C:</strong> <span id="cVal" class="font-mono">--</span></div>
                        <div><strong class="opacity-75">有效磁矩 μ_eff:</strong> <span id="muEffVal" class="font-mono text-teal-700 font-bold">--</span> μB <span class="text-xs opacity-50">(需确保M为摩尔量)</span></div>
                    </div>

                    <div id="chart2" class="flex-1 w-full h-full"></div>
                </div>
            </div>
        </div>
    </div>

    <script>
        // DOM 元素引用
        const dataInput = document.getElementById('dataInput');
        const fitMinInput = document.getElementById('fitMin');
        const fitMaxInput = document.getElementById('fitMax');
        
        const tcVal = document.getElementById('tcVal');
        const tpVal = document.getElementById('tpVal');
        const thetaVal = document.getElementById('thetaVal');
        
        const fitEquation = document.getElementById('fitEquation');
        const r2Val = document.getElementById('r2Val');
        const cVal = document.getElementById('cVal');
        const muEffVal = document.getElementById('muEffVal');
        const fitRangeDisplay = document.getElementById('fitRangeDisplay');

        // ECharts 实例
        let chart1, chart2;
        let parsedData = [];

        // 初始化图表
        function initCharts() {
            chart1 = echarts.init(document.getElementById('chart1'));
            chart2 = echarts.init(document.getElementById('chart2'));
            
            // 监听窗口大小变化
            window.addEventListener('resize', () => {
                chart1.resize();
                chart2.resize();
            });
        }

        // 解析输入文本
        function parseData(text) {
            const lines = text.trim().split('\n');
            let dataObj = [];
            
            for (let i = 0; i < lines.length; i++) {
                const line = lines[i].trim();
                // 忽略空行和表头
                if (!line || !/^[0-9.-]/.test(line)) continue;
                
                // 支持空格、制表符、逗号分割
                const parts = line.split(/[\t\s,]+/).filter(Boolean);
                if (parts.length >= 2) {
                    const t = parseFloat(parts[0]);
                    const m = parseFloat(parts[1]);
                    // 如果存在第三列则使用第三列作为1/M,否则自动计算
                    const invM = parts.length >= 3 ? parseFloat(parts[2]) : (1 / m);
                    
                    if (!isNaN(t) && !isNaN(m)) {
                        dataObj.push({ t, m, invM });
                    }
                }
            }

            // 按温度升序排序
            dataObj.sort((a, b) => a.t - b.t);

            // 中心差分法计算 dM/dT
            for (let i = 0; i < dataObj.length; i++) {
                if (i === 0 || i === dataObj.length - 1) {
                    dataObj[i].dmdt = 0;
                } else {
                    const dt = dataObj[i + 1].t - dataObj[i - 1].t;
                    const dm = dataObj[i + 1].m - dataObj[i - 1].m;
                    dataObj[i].dmdt = dt !== 0 ? dm / dt : 0;
                }
            }

            parsedData = dataObj;
            updateDashboard();
        }

        // 核心计算与UI更新
        function updateDashboard() {
            if (parsedData.length === 0) {
                clearDashboard();
                return;
            }

            // 1. 寻找 T_peak 和 T_c
            let maxM = -Infinity;
            let peakT = null;
            let minDmdt = Infinity;
            let tc = null;

            parsedData.forEach(d => {
                if (d.m > maxM) {
                    maxM = d.m;
                    peakT = d.t;
                }
                // 限定区间寻找 dM/dT 的极小值以避免边缘噪声
                if (d.t > 10 && d.t < 150 && d.dmdt < minDmdt) {
                    minDmdt = d.dmdt;
                    tc = d.t;
                }
            });

            // 2. 居里-外斯线性拟合
            const minT = parseFloat(fitMinInput.value) || 0;
            const maxT = parseFloat(fitMaxInput.value) || 300;
            const fitData = parsedData.filter(d => d.t >= minT && d.t <= maxT);
            
            let theta = null, c = null, slope = 0, intercept = 0, r2 = null, mu_eff = null;

            if (fitData.length > 3) {
                const n = fitData.length;
                let sum_t = 0, sum_invM = 0, sum_t_invM = 0, sum_t_t = 0;
                
                fitData.forEach(d => {
                    sum_t += d.t;
                    sum_invM += d.invM;
                    sum_t_invM += d.t * d.invM;
                    sum_t_t += d.t * d.t;
                });

                // 最小二乘法
                slope = (n * sum_t_invM - sum_t * sum_invM) / (n * sum_t_t - sum_t * sum_t);
                intercept = (sum_invM - slope * sum_t) / n;

                c = 1 / slope;
                theta = -intercept / slope;
                mu_eff = c > 0 ? 2.827 * Math.sqrt(c) : null;

                // 计算 R²
                const mean_invM = sum_invM / n;
                let ssTot = 0, ssRes = 0;
                fitData.forEach(d => {
                    const predicted = slope * d.t + intercept;
                    ssTot += Math.pow(d.invM - mean_invM, 2);
                    ssRes += Math.pow(d.invM - predicted, 2);
                });
                r2 = ssTot !== 0 ? 1 - (ssRes / ssTot) : 0;
            }

            // 更新文本显示
            tcVal.innerText = tc ? tc.toFixed(1) : '--';
            tpVal.innerText = peakT ? peakT.toFixed(1) : '--';
            thetaVal.innerText = theta ? theta.toFixed(1) : '--';
            
            fitRangeDisplay.innerText = `区间: ${minT} K - ${maxT} K`;
            if (slope) {
                const sign = intercept > 0 ? '+' : '-';
                fitEquation.innerText = `1/M = ${slope.toFixed(5)}T ${sign} ${Math.abs(intercept).toFixed(5)}`;
                r2Val.innerText = r2 ? r2.toFixed(5) : '--';
                cVal.innerText = c ? c.toFixed(4) : '--';
                muEffVal.innerText = mu_eff ? mu_eff.toFixed(2) : '--';
            } else {
                fitEquation.innerText = '--';
                r2Val.innerText = '--';
                cVal.innerText = '--';
                muEffVal.innerText = '--';
            }

            // 更新 ECharts
            renderCharts(tc, theta, slope, intercept);
        }

        function clearDashboard() {
            tcVal.innerText = '--';
            tpVal.innerText = '--';
            thetaVal.innerText = '--';
            fitEquation.innerText = '--';
            r2Val.innerText = '--';
            cVal.innerText = '--';
            muEffVal.innerText = '--';
            chart1.clear();
            chart2.clear();
        }

        // 渲染图表
        function renderCharts(tc, theta, slope, intercept) {
            // 构造 Chart 1 数据系列
            const mSeries = parsedData.map(d => [d.t, d.m]);
            const dmdtSeries = parsedData.map(d => [d.t, d.dmdt]);

            const option1 = {
                tooltip: { trigger: 'axis', textStyle: { fontSize: 12 } },
                legend: { data: ['M (磁化强度)', 'dM/dT (微商)'], top: 0 },
                grid: { left: '8%', right: '8%', bottom: '10%', top: '15%' },
                xAxis: { 
                    type: 'value', name: 'T (K)', nameLocation: 'middle', nameGap: 25,
                    scale: true, splitLine: { lineStyle: { type: 'dashed', color: '#e2e8f0' } }
                },
                yAxis: [
                    { type: 'value', name: 'M', position: 'left', splitLine: { show: false } },
                    { type: 'value', name: 'dM/dT', position: 'right', splitLine: { show: false } }
                ],
                series: [
                    {
                        name: 'M (磁化强度)', type: 'line', data: mSeries, 
                        showSymbol: false, itemStyle: { color: '#1e40af' }, lineStyle: { width: 2 },
                        markLine: tc ? {
                            symbol: ['none', 'none'],
                            data: [{ xAxis: tc, name: 'Tc', lineStyle: { color: '#3b82f6', type: 'dashed' }, label: { formatter: 'Tc = '+tc.toFixed(1)+'K' } }]
                        } : null
                    },
                    {
                        name: 'dM/dT (微商)', type: 'line', yAxisIndex: 1, data: dmdtSeries, 
                        showSymbol: false, itemStyle: { color: '#ef4444' }, lineStyle: { width: 1.5, opacity: 0.8 }
                    }
                ]
            };
            chart1.setOption(option1);

            // 构造 Chart 2 数据系列 (仅在 T > 外斯温度 时画拟合线)
            const invMSeries = parsedData.map(d => [d.t, d.invM]);
            let fitLineData = [];
            if (slope && intercept) {
                const maxT = parsedData[parsedData.length - 1].t;
                // 让拟合线画到横坐标相交点附近
                const startT = theta ? Math.max(0, theta - 20) : 0; 
                fitLineData = [
                    [startT, slope * startT + intercept],
                    [maxT, slope * maxT + intercept]
                ];
            }

            const option2 = {
                tooltip: { trigger: 'axis', textStyle: { fontSize: 12 } },
                legend: { data: ['1/M (实验数据)', 'Curie-Weiss 拟合'], top: 0 },
                grid: { left: '8%', right: '8%', bottom: '10%', top: '15%' },
                xAxis: { 
                    type: 'value', name: 'T (K)', nameLocation: 'middle', nameGap: 25,
                    scale: true, splitLine: { lineStyle: { type: 'dashed', color: '#e2e8f0' } }
                },
                yAxis: { 
                    type: 'value', name: '1/M', splitLine: { show: false } 
                },
                series: [
                    {
                        name: '1/M (实验数据)', type: 'scatter', symbolSize: 4, data: invMSeries, 
                        itemStyle: { color: '#64748b' },
                        markLine: theta ? {
                            symbol: ['none', 'none'],
                            data: [
                                { xAxis: theta, name: 'θ', lineStyle: { color: '#0d9488', type: 'dashed' }, label: { formatter: 'θ = '+theta.toFixed(1)+'K' } },
                                { yAxis: 0, lineStyle: { color: '#cbd5e1', type: 'solid' }, label: {show:false} }
                            ]
                        } : null
                    },
                    {
                        name: 'Curie-Weiss 拟合', type: 'line', data: fitLineData,
                        showSymbol: false, itemStyle: { color: '#0d9488' }, lineStyle: { width: 2, type: 'dashed' }
                    }
                ]
            };
            chart2.setOption(option2);
        }

        // 绑定事件监听
        dataInput.addEventListener('input', (e) => parseData(e.target.value));
        fitMinInput.addEventListener('change', updateDashboard);
        fitMaxInput.addEventListener('change', updateDashboard);

        // 初始化
        window.onload = () => {
            initCharts();
            // 如果输入框有默认值,执行一次解析
            if(dataInput.value.trim() !== '') {
                parseData(dataInput.value);
            }
        };

    </script>
</body>
</html>