电阻-温度数据拟合工具(最小二乘法)

代码(html)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>电阻-温度数据拟合工具</title>
    <script src="https://cdn.bootcdn.net/ajax/libs/echarts/5.4.3/echarts.min.js"></script>
    <style>
        body {
            font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
            max-width: 1000px;
            margin: 0 auto;
            padding: 20px;
            background-color: #f5f7f9;
            color: #333;
        }
        h1 {
            color: #2c3e50;
            text-align: center;
            margin-bottom: 30px;
        }
        .container {
            display: flex;
            flex-wrap: wrap;
            gap: 20px;
            margin-bottom: 30px;
        }
        .data-section {
            flex: 1;
            min-width: 300px;
            background: white;
            border-radius: 8px;
            padding: 20px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        }
        .result-section {
            flex: 2;
            min-width: 300px;
            background: white;
            border-radius: 8px;
            padding: 20px;
            box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
        }
        table {
            width: 100%;
            border-collapse: collapse;
            margin-bottom: 15px;
        }
        th, td {
            border: 1px solid #ddd;
            padding: 10px;
            text-align: center;
        }
        th {
            background-color: #f2f2f2;
        }
        th:first-child, th:nth-child(2) {
            width: 35%;
        }
        th:last-child {
            width: 30%;
        }
        #data-table td.action-cell {
            display: flex;
            justify-content: center;
            align-items: center;
        }
        input {
            width: 100%;
            box-sizing: border-box;
            padding: 8px;
            border: 1px solid #ddd;
            border-radius: 4px;
        }
        .btn {
            padding: 10px 15px;
            margin: 5px;
            border: none;
            border-radius: 4px;
            cursor: pointer;
            font-weight: bold;
        }
        .btn-primary {
            background-color: #3498db;
            color: white;
        }
        .btn-danger {
            background-color: #e74c3c;
            color: white;
        }
        .btn-success {
            background-color: #2ecc71;
            color: white;
        }
        .buttons {
            display: flex;
            justify-content: center;
            gap: 10px;
            margin-top: 10px;
        }
        .equation {
            font-size: 18px;
            margin: 15px 0;
            padding: 15px;
            background-color: #f9f9f9;
            border-left: 4px solid #3498db;
            border-radius: 4px;
        }
        .r-squared {
            font-size: 16px;
            margin: 15px 0;
            padding: 15px;
            background-color: #f9f9f9;
            border-left: 4px solid #2ecc71;
            border-radius: 4px;
        }
        .kb-values {
            font-size: 16px;
            margin: 10px 0;
            padding: 10px;
            background-color: #fff7f0;
            border-left: 4px solid #6c5ce7;
            border-radius: 4px;
        }
        .chart-container {
            margin-top: 20px;
            position: relative;
            height: 300px;
            width: 100%;
        }
        .instructions {
            background-color: #f8f9fa;
            padding: 15px;
            border-radius: 8px;
            margin-bottom: 20px;
            border-left: 4px solid #ffc107;
        }
    </style>
</head>
<body>
    <h1>电阻-温度数据拟合工具</h1>
     
    <div class="container">
        <div class="data-section">
            <h2>数据输入</h2>
            <table id="data-table">
                <thead>
                    <tr>
                        <th>电阻值 (Ω)</th>
                        <th>温度值 (°C)</th>
                        <th>操作</th>
                    </tr>
                </thead>
                <tbody>
                    <tr>
                        <td><input type="number" class="resistance" step="0.1" value="100"></td>
                        <td><input type="number" class="temperature" step="0.1" value="20"></td>
                        <td class="action-cell"><button class="btn btn-danger" onclick="deleteRow(this)">删除</button></td>
                    </tr>
                    <tr>
                        <td><input type="number" class="resistance" step="0.1" value="120"></td>
                        <td><input type="number" class="temperature" step="0.1" value="25"></td>
                        <td class="action-cell"><button class="btn btn-danger" onclick="deleteRow(this)">删除</button></td>
                    </tr>
                    <tr>
                        <td><input type="number" class="resistance" step="0.1" value="140"></td>
                        <td><input type="number" class="temperature" step="0.1" value="30"></td>
                        <td class="action-cell"><button class="btn btn-danger" onclick="deleteRow(this)">删除</button></td>
                    </tr>
                </tbody>
            </table>
            <div class="buttons">
                <button class="btn btn-primary" onclick="addRow()">添加行</button>
                <button class="btn btn-success" onclick="calculateFit()">计算拟合</button>
            </div>
        </div>
        
        <div class="result-section">
            <h2>拟合结果</h2>
            <div class="equation" id="equation-result">拟合方程将显示在这里</div>
            <div class="kb-values" id="kb-values">k = - , b = -</div>
            <div class="r-squared" id="r-squared-result">决定系数将显示在这里</div>
            <div class="chart-container" id="fit-chart"></div>
        </div>
    </div>

    <script>
        // 全局变量存储图表实例
        let fitChart = null;
        
        // 添加新行
        function addRow() {
            const tbody = document.querySelector('#data-table tbody');
            const newRow = document.createElement('tr');
            newRow.innerHTML = `
                <td><input type="number" class="resistance" step="0.1" value="0"></td>
                <td><input type="number" class="temperature" step="0.1" value="0"></td>
                <td class="action-cell"><button class="btn btn-danger" onclick="deleteRow(this)">删除</button></td>
            `;
            tbody.appendChild(newRow);
        }
        
        // 删除行
        function deleteRow(button) {
            const tbody = document.querySelector('#data-table tbody');
            const row = button.parentNode.parentNode;
            if (tbody.rows.length > 1) { // 确保至少保留一行
                tbody.removeChild(row);
            } else {
                alert("至少需要保留一行数据");
            }
        }
        
        // 获取数据
        function getData() {
            const rows = document.querySelectorAll('#data-table tbody tr');
            const data = [];
            
            rows.forEach(row => {
                const resistance = parseFloat(row.querySelector('.resistance').value);
                const temperature = parseFloat(row.querySelector('.temperature').value);
                
                if (!isNaN(resistance) && !isNaN(temperature)) {
                    data.push([resistance, temperature]);
                }
            });
            
            return data;
        }
        
        // 最小二乘法拟合
        function linearRegression(data) {
            let sumX = 0, sumY = 0, sumXY = 0, sumXX = 0;
            const n = data.length;
            
            data.forEach(point => {
                sumX += point[0];
                sumY += point[1];
                sumXY += point[0] * point[1];
                sumXX += point[0] * point[0];
            });
            
            const slope = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);
            const intercept = (sumY - slope * sumX) / n;
            
            return { slope, intercept };
        }
        
        // 计算决定系数R²
        function calculateRSquared(data, slope, intercept) {
            // 计算y的平均值
            const yMean = data.reduce((sum, point) => sum + point[1], 0) / data.length;
            
            // 计算总平方和
            const totalSumOfSquares = data.reduce((sum, point) => {
                return sum + Math.pow(point[1] - yMean, 2);
            }, 0);
            
            // 计算残差平方和
            const residualSumOfSquares = data.reduce((sum, point) => {
                const predictedY = slope * point[0] + intercept;
                return sum + Math.pow(point[1] - predictedY, 2);
            }, 0);
            
            // 计算R²
            const rSquared = 1 - (residualSumOfSquares / totalSumOfSquares);
            
            return rSquared;
        }
        
        // 计算拟合
        function calculateFit() {
            const data = getData();
            
            if (data.length < 2) {
                alert("至少需要两个有效数据点才能进行拟合");
                return;
            }
            
            // 执行线性回归
            const { slope, intercept } = linearRegression(data);
            
            // 计算决定系数
            const rSquared = calculateRSquared(data, slope, intercept);
            
            // 显示结果:k、b单独一行
            document.getElementById('equation-result').textContent = 
                `拟合方程: y = ${slope.toFixed(6)} x + ${intercept.toFixed(6)}`;
            document.getElementById('kb-values').textContent = `k = ${slope.toFixed(6)} , b = ${intercept.toFixed(6)}`;
            document.getElementById('r-squared-result').textContent = 
                `决定系数 R² = ${rSquared.toFixed(6)}`;
            
            // 绘制图表
            drawChart(data, slope, intercept);
        }
        
        // 绘制图表
        function drawChart(data, slope, intercept) {
            const chartDom = document.getElementById('fit-chart');
            
            // 如果已有图表实例,先销毁
            if (fitChart) {
                fitChart.dispose();
            }
            
            // 创建新图表
            fitChart = echarts.init(chartDom);
            
            // 对x值进行排序,以便绘制平滑的拟合线
            const sortedData = [...data].sort((a, b) => a[0] - b[0]);
            const minX = Math.min(...sortedData.map(point => point[0]));
            const maxX = Math.max(...sortedData.map(point => point[0]));
            
            // 生成拟合线上的点
            const fitLine = [
                [minX, slope * minX + intercept],
                [maxX, slope * maxX + intercept]
            ];

            // 计算用于自动设置坐标轴范围的值(包含数据点与拟合线)
            const allX = sortedData.map(p => p[0]).concat(fitLine.map(p => p[0]));
            const allY = sortedData.map(p => p[1]).concat(fitLine.map(p => p[1]));

            const rawMinX = Math.min(...allX);
            const rawMaxX = Math.max(...allX);
            const rawMinY = Math.min(...allY);
            const rawMaxY = Math.max(...allY);

            // 添加小的边距(当范围为0时提供默认边距)
            const xRange = rawMaxX - rawMinX;
            const yRange = rawMaxY - rawMinY;
            const xPad = xRange === 0 ? Math.abs(rawMinX) * 0.1 + 1 : xRange * 0.06;
            const yPad = yRange === 0 ? Math.abs(rawMinY) * 0.1 + 1 : yRange * 0.06;

            const xMin = rawMinX - xPad;
            const xMax = rawMaxX + xPad;
            const yMin = rawMinY - yPad;
            const yMax = rawMaxY + yPad;

            // 配置选项
            const option = {
                title: {
                    text: '电阻-温度关系拟合',
                    left: 'center'
                },
                tooltip: {
                    trigger: 'axis',
                    axisPointer: { type: 'cross' },
                    formatter: function (params) {
                        // params 是一个数组,里面包含两个 series 的数据(scatter 和 line)
                        const parts = params.map(p => {
                            if (p.seriesType === 'scatter') {
                                return `${p.marker} ${p.seriesName}: 电阻 ${p.data[0]} Ω, 温度 ${p.data[1]} °C`;
                            } else if (p.seriesType === 'line') {
                                return `${p.marker} ${p.seriesName}: 拟合值 ${p.data[1].toFixed(2)} °C`;
                            } else {
                                return '';
                            }
                        });
                        return parts.join('<br/>');
                    }
                },
                legend: {
                    data: ['原始数据', '拟合直线'],
                    top: 30
                },
                xAxis: {
                    type: 'value',
                    name: '电阻值 (Ω)',
                    nameLocation: 'middle',
                    nameGap: 30,
                    min: xMin,
                    max: xMax
                },
                yAxis: {
                    type: 'value',
                    name: '温度值 (°C)',
                    nameLocation: 'middle',
                    nameGap: 30,
                    min: yMin,
                    max: yMax
                },
                series: [
                    {
                        name: '原始数据',
                        type: 'scatter',
                        data: sortedData,
                        symbolSize: 8
                    },
                    {
                        name: '拟合直线',
                        type: 'line',
                        data: fitLine,
                        lineStyle: {
                            width: 2
                        },
                        symbol: 'none'
                    }
                ]
            };
            
            // 使用配置项和数据显示图表
            fitChart.setOption(option);
        }
        
        // 页面加载时初始化图表
        window.onload = function() {
            calculateFit();
        };
        
        // 窗口大小变化时调整图表大小
        window.addEventListener('resize', function() {
            if (fitChart) {
                fitChart.resize();
            }
        });
    </script>
</body>
</html>
View Code

 

ScreenGif

 

posted @ 2025-09-23 10:28  阿坦  阅读(9)  评论(0)    收藏  举报