{"id":10443,"date":"2026-08-06T08:32:26","date_gmt":"2026-08-06T08:32:26","guid":{"rendered":"https:\/\/voyawave.com\/%e6%9f%af%e8%a5%bf%e6%8b%9f%e5%90%88\/"},"modified":"2026-08-11T07:44:41","modified_gmt":"2026-08-11T07:44:41","slug":"%e6%9f%af%e8%a5%bf%e6%8b%9f%e5%90%88","status":"publish","type":"page","link":"https:\/\/voyawave.com\/zh-hans\/%e6%9f%af%e8%a5%bf%e6%8b%9f%e5%90%88\/","title":{"rendered":"\u67ef\u897f\u62df\u5408"},"content":{"rendered":"\t\t<div data-elementor-type=\"wp-page\" data-elementor-id=\"10443\" class=\"elementor elementor-10443 elementor-10441\">\n\t\t\t\t<div class=\"elementor-element elementor-element-bc0ddab e-con-full wd-section-stretch-content e-flex e-con e-parent\" data-id=\"bc0ddab\" data-element_type=\"container\" data-e-type=\"container\">\n\t\t\t\t<div class=\"elementor-element elementor-element-86fda32 elementor-widget elementor-widget-html\" data-id=\"86fda32\" data-element_type=\"widget\" data-e-type=\"widget\" data-widget_type=\"html.default\">\n\t\t\t\t<div class=\"elementor-widget-container\">\n\t\t\t\t\t<!DOCTYPE html>\n<html lang=\"zh-CN\">\n<head>\n  <meta charset=\"UTF-8\" \/>\n  <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0\" \/>\n  <title>\u8272\u6563\u66f2\u7ebf\u62df\u5408\uff5c\u67ef\u897f\u62df\u5408\u5de5\u5177 v7<\/title>\n  <style>\n    :root{\n      --bg:#ffffff;\n      --page:#f7f8fa;\n      --card:#ffffff;\n      --text:#101114;\n      --muted:#6b7280;\n      --line:#e5e7eb;\n      --soft:#f3f4f6;\n      --red:#b5121b;\n      --red2:#e0242e;\n      --redSoft:#fff0f1;\n      --black:#0b0b0d;\n      --shadow:0 12px 30px rgba(15,23,42,.08);\n      --radius:16px;\n    }\n    *{box-sizing:border-box}\n    body{\n      margin:0;\n      background:var(--page);\n      color:var(--text);\n      font-family:\"Microsoft YaHei\", \"PingFang SC\", \"Segoe UI\", Arial, sans-serif;\n    }\n    .app{min-height:100vh;padding:24px;background:\n      linear-gradient(90deg, rgba(181,18,27,.03) 1px, transparent 1px),\n      linear-gradient(rgba(181,18,27,.03) 1px, transparent 1px),#fafafa;\n      background-size:58px 58px;\n    }\n    .topbar{\n      height:64px;display:flex;align-items:center;justify-content:space-between;\n      background:var(--card);border:1px solid var(--line);border-radius:18px;\n      padding:0 20px;box-shadow:var(--shadow);margin-bottom:20px;\n    }\n    .brand{display:flex;align-items:center;gap:12px;font-weight:800;font-size:22px;letter-spacing:.3px}\n    .logo{width:34px;height:34px;border-radius:9px;background:linear-gradient(135deg,var(--black),var(--red));position:relative;box-shadow:0 5px 12px rgba(181,18,27,.28)}\n    .logo:after{content:\"\";position:absolute;inset:8px;border-left:3px solid #fff;border-bottom:3px solid #fff;transform:rotate(-45deg);border-radius:2px}\n    .top-actions{display:flex;gap:10px}.top-actions button{height:38px;padding:0 14px;border-radius:10px;border:1px solid #f0b7bb;background:#fff;color:var(--red);font-weight:700;cursor:pointer}.top-actions button:hover{background:var(--redSoft)}\n    .layout{display:grid;grid-template-columns:420px 1fr;gap:20px;max-width:1500px;margin:0 auto}\n    .card{background:var(--card);border:1px solid var(--line);border-radius:var(--radius);box-shadow:var(--shadow)}\n    .left{padding:18px}.right{padding:18px;min-width:0}\n    .section-title{display:flex;align-items:center;justify-content:space-between;margin:0 0 14px;font-size:16px;font-weight:800;color:var(--black)}\n    .section-title span:first-child:before{content:\"\";display:inline-block;width:5px;height:18px;border-radius:4px;background:var(--red);vertical-align:-4px;margin-right:9px}\n    .hint{font-size:12px;color:var(--muted);line-height:1.6;margin-top:8px}\n    .field-grid{display:grid;grid-template-columns:1fr 1fr;gap:12px}.field{display:flex;flex-direction:column;gap:6px}.field label{font-size:13px;color:#4b5563;font-weight:700}.field input,.field select,textarea{width:100%;border:1px solid #d7dce3;background:#fff;border-radius:10px;padding:10px 12px;min-height:42px;font-size:14px;color:var(--text);outline:none}.field input:focus,.field select:focus,textarea:focus{border-color:var(--red);box-shadow:0 0 0 3px rgba(181,18,27,.12)}\n    .btn{height:42px;border:none;border-radius:10px;background:var(--red);color:white;font-weight:800;cursor:pointer;padding:0 14px}.btn:hover{background:#9d0f17}.btn.secondary{background:#171717}.btn.secondary:hover{background:#000}.btn.ghost{background:#fff;color:var(--red);border:1px solid #f0b7bb}.btn.ghost:hover{background:var(--redSoft)}\n    .btn-row{display:flex;gap:10px;flex-wrap:wrap;margin-top:12px}.btn-row .btn{flex:1;min-width:92px}\n    .divider{height:1px;background:var(--line);margin:18px 0}\n    textarea{height:135px;resize:vertical;font-family:Consolas,\"Microsoft YaHei\",monospace;line-height:1.45;background:#fff}\n    .data-list{height:190px;overflow:auto;border:1px solid #e2e8f0;border-radius:12px;background:#fff}.data-item{display:grid;grid-template-columns:72px 1fr 1fr 28px;gap:8px;align-items:center;padding:9px 10px;border-bottom:1px solid #f1f5f9;font-size:13px}.data-item:last-child{border-bottom:none}.data-item .g{font-weight:800;color:var(--red)}.data-item button{border:none;background:#fff;color:#9ca3af;font-size:18px;cursor:pointer}.data-item button:hover{color:var(--red)}\n    .chart-head{display:grid;grid-template-columns:1fr auto;gap:16px;align-items:start;margin-bottom:12px}.info-block{display:grid;grid-template-columns:repeat(4, minmax(120px, 1fr));gap:10px}.stat{background:#fff;border:1px solid var(--line);border-radius:13px;padding:10px 12px}.stat .label{font-size:12px;color:var(--muted);font-weight:700}.stat .value{font-size:16px;font-weight:800;margin-top:4px;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}.stat .value.red{color:var(--red)}\n    .chart-wrap{height:500px;border:1px solid var(--line);border-radius:15px;background:#fff;position:relative;overflow:hidden}.chart-wrap canvas{width:100%;height:100%;display:block;cursor:crosshair}.chart-menu{position:fixed;z-index:9999;display:none;min-width:150px;padding:8px;border:1px solid #f0b7bb;background:#fff;border-radius:12px;box-shadow:0 14px 35px rgba(15,23,42,.18)}.chart-menu button{width:100%;height:36px;border:none;border-radius:9px;background:#fff;color:var(--red);font-weight:800;cursor:pointer;text-align:left;padding:0 10px}.chart-menu button:hover{background:var(--redSoft)}.chart-tip{position:absolute;right:12px;top:12px;background:rgba(255,255,255,.92);border:1px solid #f0b7bb;color:#7f1d1d;border-radius:10px;padding:6px 9px;font-size:12px;pointer-events:none}.legend{display:flex;gap:12px;flex-wrap:wrap;margin:10px 0 0}.legend-item{display:flex;align-items:center;gap:6px;color:#4b5563;font-size:13px}.swatch{width:16px;height:4px;border-radius:4px}\n    .result-grid{display:grid;grid-template-columns:1fr;gap:10px;max-height:250px;overflow:auto}.result-card{border:1px solid var(--line);border-radius:12px;padding:10px 12px;background:#fff}.result-card.active{border-color:var(--red);box-shadow:0 0 0 3px rgba(181,18,27,.10)}.result-card h4{margin:0 0 8px;font-size:14px}.result-card .params{display:grid;grid-template-columns:repeat(2,1fr);gap:6px;font-family:Consolas,monospace;font-size:12px;color:#374151}.result-card .full-formula{margin:8px 0;padding:8px 10px;border-radius:10px;background:#fff0f1;border:1px solid #fecdd3;color:#7f1d1d;font-family:Consolas,\"Microsoft YaHei\",monospace;font-size:12px;line-height:1.55;word-break:break-all}.result-card .unit-note{font-size:11px;color:#6b7280;margin-top:4px}\n    .bottom{display:grid;grid-template-columns:1fr 1.2fr;gap:18px;margin-top:18px}.calc-box{padding:16px}.calc-row{display:flex;gap:10px;align-items:end}.calc-row .field{flex:1}.output{margin-top:12px;border:1px solid var(--line);border-radius:12px;background:#fff;min-height:110px;padding:12px;font-family:Consolas,\"Microsoft YaHei\",monospace;font-size:13px;white-space:pre-wrap;color:#111827}.message{margin-top:12px;border-radius:10px;padding:10px 12px;font-size:13px;line-height:1.6;display:none}.message.ok{background:#eefdf3;color:#166534;border:1px solid #bbf7d0}.message.err{background:#fff0f1;color:#991b1b;border:1px solid #fecdd3}.message.show{display:block}\n    .formula{font-family:Consolas,monospace;background:#111;color:#fff;border-radius:12px;padding:12px;margin-top:10px;font-size:13px;line-height:1.6}.mini-title{font-size:15px;font-weight:800;margin:0 0 12px;color:var(--black)}\n    @media(max-width:1100px){.layout{grid-template-columns:1fr}.bottom{grid-template-columns:1fr}.info-block{grid-template-columns:repeat(2,1fr)}}\n  <\/style>\n<\/head>\n<body>\n<div class=\"app\">\n  <div class=\"topbar\">\n    <div class=\"brand\"><div class=\"logo\"><\/div><span>\u67ef\u897f\u62df\u5408<\/span><\/div>\n    <div class=\"top-actions\"><button id=\"exportPngBtn\">\u5bfc\u51fa PNG<\/button><button id=\"exportCsvBtn\">\u5bfc\u51fa CSV<\/button><\/div>\n  <\/div>\n\n  <div class=\"layout\">\n    <div class=\"card left\">\n      <div class=\"section-title\"><span>\u8f93\u5165\u53c2\u6570<\/span><\/div>\n      <div class=\"field-grid\">\n        <div class=\"field\"><label>\u6ce2\u957f \u03bb (nm)<\/label><input id=\"lambdaInput\" type=\"number\" step=\"0.001\" value=\"531.951\"><\/div>\n        <div class=\"field\"><label>\u6298\u5c04\u7387 n<\/label><input id=\"nInput\" type=\"number\" step=\"0.000001\" value=\"1.515\"><\/div>\n        <div class=\"field\"><label>\u7ec4\u522b<\/label><input id=\"groupInput\" type=\"text\" value=\"1\u7ec4\"><\/div>\n        <div class=\"field\"><label>&nbsp;<\/label><button class=\"btn\" id=\"addBtn\">\u6dfb\u52a0\u6570\u636e\u70b9<\/button><\/div>\n      <\/div>\n      <div class=\"divider\"><\/div>\n      <div class=\"section-title\"><span>\u66f2\u7ebf\u62df\u5408\u6570\u636e<\/span><small id=\"pointCount\" style=\"color:#6b7280\">0 \u70b9<\/small><\/div>\n      <div class=\"data-list\" id=\"dataList\"><\/div>\n      <div class=\"btn-row\">\n        <button class=\"btn ghost\" id=\"deleteBtn\">\u5220\u9664\u9009\u4e2d<\/button>\n        <button class=\"btn ghost\" id=\"clearBtn\">\u6e05\u7a7a<\/button>\n      <\/div>\n      <div class=\"divider\"><\/div>\n      <div class=\"section-title\"><span>\u6279\u91cf\u7c98\u8d34<\/span><\/div>\n      <textarea id=\"bulkInput\" placeholder=\"\u652f\u6301\u683c\u5f0f\uff1a&#10;[1\u7ec4]&#10;531.951, 1.515&#10;635, 1.485&#10;980, 1.465&#10;&#10;[2\u7ec4]&#10;...\">[1\u7ec4]\n531.951, 1.515\n635, 1.485\n980, 1.465<\/textarea>\n      <div class=\"btn-row\">\n        <button class=\"btn secondary\" id=\"importBtn\">\u5bfc\u5165\u6587\u672c<\/button>\n        <button class=\"btn\" id=\"fitBtn\">\u67ef\u897f\u62df\u5408<\/button>\n      <\/div>\n      <div id=\"msg\" class=\"message\"><\/div>\n      <div class=\"formula\">\u62df\u5408\u516c\u5f0f\uff1an = A + B \/ \u03bb\u00b2 + C \/ \u03bb\u2074<br>\u8bf4\u660e\uff1a\u8f93\u5165\u5355\u4f4d\u4e3a nm\uff0c\u5185\u90e8\u81ea\u52a8\u6362\u7b97\u4e3a \u03bcm \u8fdb\u884c\u6570\u503c\u7a33\u5b9a\u62df\u5408\uff1b\u62df\u5408\u7ed3\u679c\u4f1a\u663e\u793a\u5b8c\u6574\u516c\u5f0f\u3002<\/div>\n    <\/div>\n\n    <div class=\"card right\">\n      <div class=\"chart-head\">\n        <div class=\"info-block\">\n          <div class=\"stat\"><div class=\"label\">\u516c\u5f0f<\/div><div class=\"value\">Cauchy<\/div><\/div>\n          <div class=\"stat\"><div class=\"label\">\u5f53\u524d \u03bb (nm)<\/div><div class=\"value red\" id=\"cursorX\">0.000<\/div><\/div>\n          <div class=\"stat\"><div class=\"label\">\u5f53\u524d n<\/div><div class=\"value red\" id=\"cursorY\">0.00000<\/div><\/div>\n          <div class=\"stat\"><div class=\"label\">\u9009\u4e2d\u66f2\u7ebf<\/div><div class=\"value\" id=\"selectedGroup\">\u672a\u9009\u62e9<\/div><\/div>\n        <\/div>\n        <div style=\"min-width:290px\">\n          <div class=\"mini-title\">\u62df\u5408\u7ed3\u679c<\/div>\n          <div class=\"result-grid\" id=\"resultGrid\"><\/div>\n        <\/div>\n      <\/div>\n      <div class=\"chart-wrap\"><canvas id=\"chart\"><\/canvas><div class=\"chart-tip\">\u6eda\u8f6e\u7f29\u653e\uff5c\u5de6\u952e\u6846\u9009\uff5c\u53f3\u952e\u8fd8\u539f\uff5c\u66f2\u7ebf\u70b9\u8ddf\u968f<\/div><\/div><div class=\"chart-menu\" id=\"chartMenu\"><button id=\"resetZoomBtn\">\u8fd8\u539f\u56fe\u50cf\u5927\u5c0f<\/button><\/div>\n      <div class=\"legend\" id=\"legend\"><\/div>\n      <div class=\"bottom\">\n        <div class=\"card calc-box\">\n          <div class=\"mini-title\">\u6298\u5c04\u7387\u8ba1\u7b97<\/div>\n          <div class=\"calc-row\">\n            <div class=\"field\"><label>\u8f93\u5165\u6ce2\u957f \u03bb (nm)<\/label><input id=\"calcLambda\" type=\"number\" step=\"0.001\" value=\"589.3\"><\/div>\n            <button class=\"btn\" id=\"calcBtn\">\u8ba1\u7b97<\/button>\n          <\/div>\n          <div class=\"output\" id=\"calcOutput\">\u70b9\u51fb\u201c\u8ba1\u7b97\u201d\u540e\u663e\u793a\u5404\u7ec4\u6298\u5c04\u7387\u3002<\/div>\n        <\/div>\n        <div class=\"card calc-box\">\n          <div class=\"mini-title\">\u4f7f\u7528\u63d0\u793a<\/div>\n          <div class=\"hint\">1. \u67ef\u897f\u4e09\u53c2\u6570\u62df\u5408\u81f3\u5c11\u9700\u8981 3 \u4e2a\u6ce2\u957f\/\u6298\u5c04\u7387\u6570\u636e\u70b9\u3002<br>2. \u4e09\u70b9\u62df\u5408\u65f6\u66f2\u7ebf\u4f1a\u7cbe\u786e\u901a\u8fc7\u4e09\u70b9\uff0cR\u00b2 \u901a\u5e38\u4e3a 1\uff1b\u66f4\u591a\u70b9\u65f6\u4e3a\u6700\u5c0f\u4e8c\u4e58\u62df\u5408\u3002<br>3. \u963f\u8d1d\u6570\u9ed8\u8ba4\u4f7f\u7528 F=486.1327 nm\u3001d=587.5618 nm\u3001C=656.2725 nm\u3002<br>4. \u9f20\u6807\u79fb\u52a8\u663e\u793a\u5750\u6807\u548c\u7ad6\u7ebf\uff1b\u5de6\u952e\u70b9\u51fb\u6700\u8fd1\u66f2\u7ebf\uff1b\u6eda\u8f6e\u7f29\u653e\u56fe\u8868\uff1b\u53f3\u952e\u53ef\u8fd8\u539f\u56fe\u50cf\u5927\u5c0f\u3002<\/div>\n        <\/div>\n      <\/div>\n    <\/div>\n  <\/div>\n<\/div>\n\n<script>\n(function(){\n  const data = [];\n  const fits = new Map();\n  let selectedPointIndex = -1;\n  let selectedFitGroup = null;\n  const colors = ['#b5121b','#111111','#e0242e','#6b7280','#8b0000','#ef4444','#374151','#dc2626','#7f1d1d','#000000'];\n\n  const $ = id => document.getElementById(id);\n  const canvas = $('chart');\n  const ctx = canvas.getContext('2d');\n  let plot = {xMin:300,xMax:1000,yMin:1.4,yMax:1.55,left:70,right:28,top:28,bottom:58,w:0,h:0};\n  let view = null; \/\/ null \u8868\u793a\u81ea\u52a8\u9002\u914d\u5168\u90e8\u6570\u636e\uff1b\u6eda\u8f6e\u7f29\u653e\u540e\u4fdd\u5b58\u5f53\u524d\u89c6\u7a97\n  let mouse = {x:null,y:null,inside:false, lambdaSnap:null};\n  let hoverPointIndex = -1;\n  let hoverCurvePoint = null; \/\/ \u9f20\u6807\u5f53\u524d\u4f4d\u7f6e\u5728\u62df\u5408\u66f2\u7ebf\u4e0a\u7684\u6295\u5f71\u70b9\n  let drag = {active:false, moved:false, startX:0, startY:0, x:0, y:0};\n  const xSnapStepNm = 0.5;\n\n  function showMsg(text,type='ok'){\n    const el=$('msg'); el.textContent=text; el.className='message show '+type;\n  }\n  function clearMsg(){ $('msg').className='message'; $('msg').textContent=''; }\n  function fmt(v,d=6){ return Number.isFinite(v) ? v.toFixed(d) : '--'; }\n  function signedTerm(value, decimals, unitTerm){\n    if(!Number.isFinite(value)) return '';\n    const sign = value >= 0 ? ' + ' : ' - ';\n    return sign + Math.abs(value).toFixed(decimals) + unitTerm;\n  }\n  function fullCauchyFormula(f, decimals=10){\n    return 'n(\u03bb) = ' + f.A.toFixed(decimals) + signedTerm(f.B, decimals, ' \/ \u03bb\u00b2') + signedTerm(f.C, decimals, ' \/ \u03bb\u2074');\n  }\n  function plainCauchyFormula(f, decimals=10){\n    return 'n(lambda_um) = ' + f.A.toFixed(decimals) + signedTerm(f.B, decimals, ' \/ lambda_um^2') + signedTerm(f.C, decimals, ' \/ lambda_um^4');\n  }\n  function lambdaNmToT(lamNm){ const u = lamNm \/ 1000.0; return 1\/(u*u); }\n  function cauchyEval(coef, lamNm){ const t=lambdaNmToT(lamNm); return coef.A + coef.B*t + coef.C*t*t; }\n\n  function addPoint(lam,n,g){\n    lam=Number(lam); n=Number(n); g=String(g||'1\u7ec4').trim()||'1\u7ec4';\n    if(!Number.isFinite(lam)||lam<=0||!Number.isFinite(n)||n<=0){ showMsg('\u8bf7\u8f93\u5165\u6709\u6548\u7684\u6ce2\u957f\u548c\u6298\u5c04\u7387\u3002','err'); return false; }\n    data.push({lambda:lam,n:n,group:g});\n    selectedPointIndex=data.length-1; renderAll(); return true;\n  }\n\n  function parseBulk(text){\n    const rows=text.split(\/\\r?\\n\/); let group='1\u7ec4'; const pts=[];\n    for(const raw of rows){\n      const line=raw.trim(); if(!line) continue;\n      const m=line.match(\/^\\[(.+?)\\]$\/); if(m){ group=m[1].trim(); continue; }\n      const parts=line.split(\/[\uff0c,\\s\\t]+\/).filter(Boolean);\n      if(parts.length>=2){\n        const lam=Number(parts[0]); const n=Number(parts[1]); const g=parts[2] ? parts[2] : group;\n        if(Number.isFinite(lam)&&Number.isFinite(n)&&lam>0&&n>0) pts.push({lambda:lam,n:n,group:g});\n      }\n    }\n    return pts;\n  }\n\n  \/\/ Robust least-squares for n = A + B*t + C*t^2, where t=1\/lambda_um^2.\n  \/\/ To avoid nm-scale conditioning problems, fit in normalized variable z=(t-mean)\/std,\n  \/\/ then convert back to A\/B\/C. This fixes the previous \u201c\u5171\u7ebf\u6216\u6570\u503c\u6761\u4ef6\u5dee\u201d\u8bef\u5224.\n  function fitCauchy(points){\n    if(points.length<3) throw new Error('\u81f3\u5c11\u9700\u89813\u4e2a\u6570\u636e\u70b9');\n    const t = points.map(p=>lambdaNmToT(p.lambda));\n    const y = points.map(p=>p.n);\n    const mean = t.reduce((a,b)=>a+b,0)\/t.length;\n    let variance = t.reduce((s,v)=>s+(v-mean)*(v-mean),0)\/t.length;\n    let scale = Math.sqrt(variance);\n    if(!Number.isFinite(scale) || scale < 1e-14) scale = 1;\n    const z = t.map(v=>(v-mean)\/scale);\n\n    const M = [[0,0,0],[0,0,0],[0,0,0]];\n    const b = [0,0,0];\n    for(let i=0;i<z.length;i++){\n      const r=[1,z[i],z[i]*z[i]];\n      for(let a=0;a<3;a++){\n        b[a]+=r[a]*y[i];\n        for(let c=0;c<3;c++) M[a][c]+=r[a]*r[c];\n      }\n    }\n    const abc = solve3x3(M,b);\n    if(!abc) throw new Error('\u77e9\u9635\u6c42\u89e3\u5931\u8d25');\n    const [a0,a1,a2]=abc;\n    const C = a2\/(scale*scale);\n    const B = a1\/scale - 2*a2*mean\/(scale*scale);\n    const A = a0 - a1*mean\/scale + a2*mean*mean\/(scale*scale);\n    const coef={A,B,C};\n\n    let sse=0, sst=0; const avg=y.reduce((p,q)=>p+q,0)\/y.length;\n    for(const p of points){ const yp=cauchyEval(coef,p.lambda); sse += (p.n-yp)*(p.n-yp); sst += (p.n-avg)*(p.n-avg); }\n    const r2 = sst<1e-24 ? (sse<1e-20 ? 1 : 0) : 1 - sse\/sst;\n    const nF=cauchyEval(coef,486.1327), nd=cauchyEval(coef,587.5618), nC=cauchyEval(coef,656.2725);\n    const abbe = Math.abs(nF-nC)>1e-12 ? (nd-1)\/(nF-nC) : NaN;\n    return {A,B,C,r2,abbe,nF,nd,nC,points:points.slice(), note: points.length===3?'\u4e09\u70b9\u7cbe\u786e\u62df\u5408':'\u6700\u5c0f\u4e8c\u4e58\u62df\u5408'};\n  }\n\n  function solve3x3(A,b){\n    const m = A.map((row,i)=>row.slice().concat([b[i]]));\n    for(let col=0; col<3; col++){\n      let pivot=col;\n      for(let r=col+1;r<3;r++) if(Math.abs(m[r][col])>Math.abs(m[pivot][col])) pivot=r;\n      if(Math.abs(m[pivot][col])<1e-18) return null;\n      if(pivot!==col){ const tmp=m[col]; m[col]=m[pivot]; m[pivot]=tmp; }\n      const div=m[col][col];\n      for(let j=col;j<4;j++) m[col][j]\/=div;\n      for(let r=0;r<3;r++){\n        if(r===col) continue;\n        const factor=m[r][col];\n        for(let j=col;j<4;j++) m[r][j]-=factor*m[col][j];\n      }\n    }\n    return [m[0][3],m[1][3],m[2][3]];\n  }\n\n  function runFit(){\n    fits.clear(); selectedFitGroup=null;\n    const groups = groupData(); const failed=[];\n    for(const [g,pts] of groups.entries()){\n      try{ fits.set(g, fitCauchy(pts)); }\n      catch(e){ failed.push(`${g}: ${e.message}`); }\n    }\n    renderAll();\n    view=null;\n    if(fits.size){ showMsg(`\u5df2\u5b8c\u6210 ${fits.size} \u7ec4\u67ef\u897f\u62df\u5408\u3002${failed.length?' \u672a\u62df\u5408\uff1a'+failed.join('\uff1b'):''}`,'ok'); }\n    else showMsg('\u672a\u5b8c\u6210\u62df\u5408\uff1a'+(failed.join('\uff1b')||'\u8bf7\u81f3\u5c11\u8f93\u5165\u4e00\u7ec4\u4e09\u4e2a\u6570\u636e\u70b9\u3002'),'err');\n  }\n\n  function groupData(){\n    const m=new Map();\n    for(const p of data){ if(!m.has(p.group)) m.set(p.group,[]); m.get(p.group).push(p); }\n    return m;\n  }\n\n  function renderDataList(){\n    $('pointCount').textContent = `${data.length} \u70b9`;\n    const box=$('dataList'); box.innerHTML='';\n    data.forEach((p,i)=>{\n      const div=document.createElement('div'); div.className='data-item'; if(i===selectedPointIndex) div.style.background='#fff0f1';\n      div.innerHTML=`<span class=\"g\">${escapeHtml(p.group)}<\/span><span>${p.lambda.toFixed(3)} nm<\/span><span>n=${p.n.toFixed(6)}<\/span><button title=\"\u5220\u9664\">\u00d7<\/button>`;\n      div.addEventListener('click',()=>{selectedPointIndex=i;renderAll();});\n      div.querySelector('button').addEventListener('click',(ev)=>{ev.stopPropagation();data.splice(i,1);if(selectedPointIndex>=data.length)selectedPointIndex=data.length-1;renderAll();});\n      box.appendChild(div);\n    });\n  }\n  function escapeHtml(s){return String(s).replace(\/[&<>\"]\/g,c=>({'&':'&amp;','<':'&lt;','>':'&gt;','\"':'&quot;'}[c]));}\n\n  function renderResults(){\n    const box=$('resultGrid'); box.innerHTML='';\n    let k=0;\n    for(const [g,f] of fits.entries()){\n      const div=document.createElement('div'); div.className='result-card'+(selectedFitGroup===g?' active':'');\n      const color=colors[k%colors.length]; k++;\n      const formulaText = fullCauchyFormula(f, 10);\n      div.innerHTML=`<h4 style=\"color:${color}\">${escapeHtml(g)}\uff5c${f.note}<\/h4>\n        <div class=\"full-formula\">${escapeHtml(formulaText)}<\/div>\n        <div class=\"unit-note\">\u516c\u5f0f\u4e2d \u03bb \u7684\u5355\u4f4d\u4e3a \u03bcm\uff1b\u8f93\u5165\/\u663e\u793a\u6ce2\u957f\u4ecd\u4e3a nm\u3002<\/div>\n        <div class=\"params\">\n          <span>A = ${fmt(f.A,10)}<\/span><span>R\u00b2 = ${fmt(f.r2,8)}<\/span>\n          <span>B = ${fmt(f.B,10)} \u03bcm\u00b2<\/span><span>Vd = ${fmt(f.abbe,4)}<\/span>\n          <span>C = ${fmt(f.C,10)} \u03bcm\u2074<\/span><span>\u70b9\u6570 = ${f.points.length}<\/span>\n        <\/div>`;\n      div.addEventListener('click',()=>{selectedFitGroup=g; renderAll();});\n      box.appendChild(div);\n    }\n    $('selectedGroup').textContent = selectedFitGroup || '\u672a\u9009\u62e9';\n  }\n\n  function resizeCanvas(){\n    const rect=canvas.parentElement.getBoundingClientRect();\n    const dpr=window.devicePixelRatio||1;\n    canvas.width=Math.max(300,Math.floor(rect.width*dpr)); canvas.height=Math.max(240,Math.floor(rect.height*dpr));\n    ctx.setTransform(dpr,0,0,dpr,0,0);\n    plot.w=rect.width; plot.h=rect.height;\n  }\n  function getAllExtents(){\n    const lambdas=data.map(p=>p.lambda); const ns=data.map(p=>p.n);\n    for(const [g,f] of fits.entries()){\n      const lMin=Math.min(...f.points.map(p=>p.lambda)), lMax=Math.max(...f.points.map(p=>p.lambda));\n      for(let i=0;i<80;i++){ const lam=lMin+(lMax-lMin)*i\/79; lambdas.push(lam); ns.push(cauchyEval(f,lam)); }\n    }\n    let xMin=lambdas.length?Math.min(...lambdas):300, xMax=lambdas.length?Math.max(...lambdas):1200;\n    let yMin=ns.length?Math.min(...ns):1.4, yMax=ns.length?Math.max(...ns):1.6;\n    if(xMin===xMax){xMin*=0.9;xMax*=1.1;} if(yMin===yMax){yMin-=0.05;yMax+=0.05;}\n    const xp=(xMax-xMin)*0.1, yp=(yMax-yMin)*0.2;\n    return {xMin:xMin-xp,xMax:xMax+xp,yMin:yMin-yp,yMax:yMax+yp};\n  }\n  function X(lam){return plot.left+(lam-plot.xMin)\/(plot.xMax-plot.xMin)*(plot.w-plot.left-plot.right)}\n  function Y(n){return plot.top+(plot.yMax-n)\/(plot.yMax-plot.yMin)*(plot.h-plot.top-plot.bottom)}\n  function invX(px){return plot.xMin+(px-plot.left)\/(plot.w-plot.left-plot.right)*(plot.xMax-plot.xMin)}\n  function invY(py){return plot.yMax-(py-plot.top)\/(plot.h-plot.top-plot.bottom)*(plot.yMax-plot.yMin)}\n  function isInsidePlot(px,py){\n    return px>=plot.left && px<=plot.w-plot.right && py>=plot.top && py<=plot.h-plot.bottom;\n  }\n  function resetView(){\n    view = null;\n    drawChart();\n  }\n  function setViewFromWheel(px,py,deltaY){\n    if(!view) view = {...getAllExtents()};\n    const cx = invX(px);\n    const cy = invY(py);\n    if(!Number.isFinite(cx) || !Number.isFinite(cy)) return;\n    const factor = deltaY < 0 ? 0.82 : 1.22; \/\/ \u4e0a\u6eda\u653e\u5927\uff0c\u4e0b\u6eda\u7f29\u5c0f\n    const xSpan = Math.max((view.xMax-view.xMin)*factor, 0.5);\n    const ySpan = Math.max((view.yMax-view.yMin)*factor, 1e-9);\n    const xRatio = (cx-view.xMin)\/(view.xMax-view.xMin);\n    const yRatio = (cy-view.yMin)\/(view.yMax-view.yMin);\n    view.xMin = cx - xSpan*xRatio;\n    view.xMax = view.xMin + xSpan;\n    view.yMin = cy - ySpan*yRatio;\n    view.yMax = view.yMin + ySpan;\n    drawChart();\n  }\n  function snapXValue(v){ return Math.round(v\/xSnapStepNm)*xSnapStepNm; }\n  function niceXStep(span, target=8){\n    const raw = Math.max(span\/target, xSnapStepNm);\n    const pow = Math.pow(10, Math.floor(Math.log10(raw)));\n    const bases = [0.5,1,2,2.5,5,10];\n    let best = xSnapStepNm;\n    for(const m of [0.1,1,10,100,1000]){\n      for(const b of bases){\n        const step = b*pow*m;\n        const step05 = Math.ceil(step\/xSnapStepNm)*xSnapStepNm;\n        if(step05 >= raw){ return step05; }\n        best = step05;\n      }\n    }\n    return best;\n  }\n  function formatXTick(val, step){\n    if(step < 1) return val.toFixed(1);\n    if(Math.abs(step-Math.round(step))>1e-9) return val.toFixed(1);\n    return val.toFixed(0);\n  }\n  function nearestDataPoint(px,py){\n    let best=-1, dist=Infinity;\n    data.forEach((p,i)=>{\n      const dx=X(p.lambda)-px, dy=Y(p.n)-py;\n      const d=dx*dx+dy*dy;\n      if(d<dist){ dist=d; best=i; }\n    });\n    return dist <= 144 ? best : -1;\n  }\n  function groupColor(group){\n    const keys = Array.from(groupData().keys());\n    const idx = Math.max(0, keys.indexOf(group));\n    return colors[idx % colors.length];\n  }\n  function getHoverCurvePoint(lam, px, py){\n    if(!fits.size || !Number.isFinite(lam)) return null;\n    let entries;\n    \/\/ \u5df2\u7ecf\u5de6\u952e\u9009\u4e2d\u66f2\u7ebf\u65f6\uff0c\u9f20\u6807\u8ddf\u968f\u70b9\u56fa\u5b9a\u663e\u793a\u5728\u8be5\u66f2\u7ebf\u4e0a\uff1b\u672a\u9009\u4e2d\u65f6\uff0c\u53d6\u9f20\u6807\u5f53\u524d\u4f4d\u7f6e\u6700\u8fd1\u7684\u66f2\u7ebf\u3002\n    if(selectedFitGroup && fits.has(selectedFitGroup)) entries = [[selectedFitGroup, fits.get(selectedFitGroup)]];\n    else entries = Array.from(fits.entries());\n    let best = null, dist = Infinity;\n    for(const [g,f] of entries){\n      const n = cauchyEval(f, lam);\n      const cx = X(lam), cy = Y(n);\n      if(!Number.isFinite(n) || !Number.isFinite(cx) || !Number.isFinite(cy)) continue;\n      const d = Math.abs(cy - py);\n      if(d < dist){ dist = d; best = {group:g, fit:f, lambda:lam, n, x:cx, y:cy, color:groupColor(g)}; }\n    }\n    return best;\n  }\n  function setViewFromSelection(x1,y1,x2,y2){\n    const gx0=plot.left, gy0=plot.top, gx1=plot.w-plot.right, gy1=plot.h-plot.bottom;\n    const sx0=Math.max(gx0,Math.min(gx1,Math.min(x1,x2)));\n    const sx1=Math.max(gx0,Math.min(gx1,Math.max(x1,x2)));\n    const sy0=Math.max(gy0,Math.min(gy1,Math.min(y1,y2)));\n    const sy1=Math.max(gy0,Math.min(gy1,Math.max(y1,y2)));\n    if(Math.abs(sx1-sx0)<8 || Math.abs(sy1-sy0)<8) return false;\n    const nx0=invX(sx0), nx1=invX(sx1);\n    const ny0=invY(sy1), ny1=invY(sy0);\n    view = {xMin:Math.min(nx0,nx1), xMax:Math.max(nx0,nx1), yMin:Math.min(ny0,ny1), yMax:Math.max(ny0,ny1)};\n    return true;\n  }\n\n  function drawChart(){\n    resizeCanvas(); Object.assign(plot, view || getAllExtents());\n    ctx.clearRect(0,0,plot.w,plot.h);\n    ctx.fillStyle='#fff'; ctx.fillRect(0,0,plot.w,plot.h);\n    const gx0=plot.left, gy0=plot.top, gw=plot.w-plot.left-plot.right, gh=plot.h-plot.top-plot.bottom;\n    ctx.strokeStyle='#e5e7eb'; ctx.lineWidth=1; ctx.beginPath(); ctx.rect(gx0,gy0,gw,gh); ctx.stroke();\n    ctx.font='12px Microsoft YaHei'; ctx.fillStyle='#6b7280'; ctx.textAlign='center'; ctx.textBaseline='top';\n    const xStep = niceXStep(plot.xMax-plot.xMin, 8);\n    const xStart = Math.ceil(plot.xMin\/xStep)*xStep;\n    for(let val=xStart, guard=0; val<=plot.xMax+1e-9 && guard<200; val+=xStep, guard++){\n      const x=X(val);\n      ctx.strokeStyle='#eef2f7';\n      ctx.beginPath(); ctx.moveTo(x,gy0); ctx.lineTo(x,gy0+gh); ctx.stroke();\n      ctx.fillText(formatXTick(val,xStep),x,gy0+gh+10);\n    }\n    \/\/ \u7f29\u653e\u5230\u8f83\u5c0f\u8303\u56f4\u65f6\u663e\u793a0.5 nm\u7ec6\u5206\u523b\u5ea6\uff0c\u4fbf\u4e8e\u7cbe\u786e\u8bfb\u6570\u3002\n    if((plot.xMax-plot.xMin) <= 80){\n      const minorStart = Math.ceil(plot.xMin\/xSnapStepNm)*xSnapStepNm;\n      ctx.strokeStyle='rgba(181,18,27,.09)';\n      for(let val=minorStart, guard=0; val<=plot.xMax+1e-9 && guard<300; val+=xSnapStepNm, guard++){\n        const x=X(val);\n        ctx.beginPath(); ctx.moveTo(x,gy0+gh); ctx.lineTo(x,gy0+gh-5); ctx.stroke();\n      }\n    }\n    ctx.textAlign='right'; ctx.textBaseline='middle';\n    for(let i=0;i<=6;i++){ const y=gy0+gh*i\/6; ctx.strokeStyle='#eef2f7'; ctx.beginPath(); ctx.moveTo(gx0,y); ctx.lineTo(gx0+gw,y); ctx.stroke(); const val=plot.yMax-(plot.yMax-plot.yMin)*i\/6; ctx.fillText(val.toFixed(3),gx0-8,y); }\n    ctx.fillStyle='#111'; ctx.font='14px Microsoft YaHei'; ctx.textAlign='center'; ctx.textBaseline='top'; ctx.fillText('\u03bb (nm)', gx0+gw\/2, plot.h-28);\n    ctx.save(); ctx.translate(10,gy0+gh\/2); ctx.rotate(-Math.PI\/2); ctx.fillText('Index n',0,0); ctx.restore();\n\n    const groups=Array.from(groupData().keys());\n    groups.forEach((g,idx)=>{\n      const color=colors[idx%colors.length]; const pts=groupData().get(g);\n      const f=fits.get(g);\n      if(f){\n        const lmin=Math.max(plot.xMin,Math.min(...pts.map(p=>p.lambda))*0.92); const lmax=Math.min(plot.xMax,Math.max(...pts.map(p=>p.lambda))*1.08);\n        ctx.strokeStyle=color; ctx.lineWidth=(selectedFitGroup===g)?3:2; ctx.beginPath();\n        for(let i=0;i<=240;i++){ const lam=lmin+(lmax-lmin)*i\/240; const py=Y(cauchyEval(f,lam)); const px=X(lam); if(i===0) ctx.moveTo(px,py); else ctx.lineTo(px,py); }\n        ctx.stroke();\n      }\n      pts.forEach(p=>{\n        const pointIndex=data.indexOf(p);\n        const r = pointIndex===hoverPointIndex ? 8 : 5;\n        ctx.fillStyle=color; ctx.strokeStyle=pointIndex===hoverPointIndex ? '#111' : '#fff'; ctx.lineWidth=pointIndex===hoverPointIndex ? 2.5 : 2;\n        ctx.beginPath(); ctx.arc(X(p.lambda),Y(p.n),r,0,Math.PI*2); ctx.fill(); ctx.stroke();\n      });\n    });\n    drawHoverOverlay();\n    drawSelectionOverlay();\n    renderLegend();\n  }\n\n  function drawHoverOverlay(){\n    if(!mouse.inside || mouse.x===null || mouse.y===null) return;\n    const gx0=plot.left, gy0=plot.top, gw=plot.w-plot.left-plot.right, gh=plot.h-plot.top-plot.bottom;\n    const lam = Number.isFinite(mouse.lambdaSnap) ? mouse.lambdaSnap : snapXValue(invX(mouse.x));\n    const x=Math.max(gx0, Math.min(gx0+gw, X(lam)));\n    ctx.save();\n    ctx.beginPath();\n    ctx.rect(gx0,gy0,gw,gh);\n    ctx.clip();\n\n    \/\/ \u7ad6\u7ebf\u53ea\u4ee3\u8868\u5f53\u524d\u9f20\u6807\u5bf9\u5e94\u7684\u6ce2\u957f\u4f4d\u7f6e\u3002\n    ctx.strokeStyle='rgba(181,18,27,.72)';\n    ctx.lineWidth=1.2;\n    ctx.setLineDash([5,5]);\n    ctx.beginPath();\n    ctx.moveTo(x,gy0);\n    ctx.lineTo(x,gy0+gh);\n    ctx.stroke();\n    ctx.setLineDash([]);\n\n    \/\/ \u8ddf\u968f\u9f20\u6807\u79fb\u52a8\u7684\u653e\u5927\u70b9\uff1ax \u662f\u9f20\u6807\u5bf9\u5e94\u6ce2\u957f\uff0cy \u662f\u8be5\u6ce2\u957f\u4e0b\u62df\u5408\u66f2\u7ebf\u7684 n \u503c\uff0c\u800c\u4e0d\u662f\u9f20\u6807\u81ea\u8eab\u7684 y \u5750\u6807\u3002\n    if(hoverCurvePoint && hoverCurvePoint.x>=gx0 && hoverCurvePoint.x<=gx0+gw && hoverCurvePoint.y>=gy0 && hoverCurvePoint.y<=gy0+gh){\n      ctx.fillStyle='rgba(255,255,255,.98)';\n      ctx.strokeStyle=hoverCurvePoint.color;\n      ctx.lineWidth=3;\n      ctx.beginPath();\n      ctx.arc(hoverCurvePoint.x, hoverCurvePoint.y, 7.5, 0, Math.PI*2);\n      ctx.fill();\n      ctx.stroke();\n      ctx.fillStyle=hoverCurvePoint.color;\n      ctx.beginPath();\n      ctx.arc(hoverCurvePoint.x, hoverCurvePoint.y, 3.2, 0, Math.PI*2);\n      ctx.fill();\n\n      const text=`${hoverCurvePoint.group} \u66f2\u7ebf  \u03bb=${hoverCurvePoint.lambda.toFixed(1)} nm, n=${hoverCurvePoint.n.toFixed(6)}`;\n      ctx.font='12px Microsoft YaHei';\n      const tw=ctx.measureText(text).width+18;\n      const tx=Math.min(hoverCurvePoint.x+12, gx0+gw-tw-6);\n      const ty=Math.max(gy0+8, hoverCurvePoint.y-38);\n      ctx.fillStyle='rgba(255,255,255,.96)';\n      ctx.strokeStyle='rgba(181,18,27,.55)';\n      ctx.lineWidth=1;\n      roundRect(ctx,tx,ty,tw,28,8); ctx.fill(); ctx.stroke();\n      ctx.fillStyle='#111'; ctx.textAlign='left'; ctx.textBaseline='middle'; ctx.fillText(text,tx+9,ty+14);\n    }\n\n    \/\/ \u82e5\u9f20\u6807\u63a5\u8fd1\u539f\u59cb\u5b9e\u6d4b\u70b9\uff0c\u4ecd\u7136\u663e\u793a\u8be5\u70b9\u4fe1\u606f\uff0c\u5e76\u4e14\u5b9e\u6d4b\u70b9\u672c\u8eab\u4f1a\u88ab\u653e\u5927\u3002\n    if(hoverPointIndex>=0){\n      const p=data[hoverPointIndex];\n      const px=X(p.lambda), py=Y(p.n);\n      ctx.fillStyle='rgba(255,255,255,.96)';\n      ctx.strokeStyle='rgba(17,17,17,.45)';\n      ctx.lineWidth=1;\n      const text=`\u5b9e\u6d4b\u70b9 ${p.group}  \u03bb=${p.lambda.toFixed(3)} nm, n=${p.n.toFixed(6)}`;\n      ctx.font='12px Microsoft YaHei';\n      const tw=ctx.measureText(text).width+18;\n      const tx=Math.min(px+12, gx0+gw-tw-6);\n      const ty=Math.max(gy0+8, py+14);\n      roundRect(ctx,tx,ty,tw,28,8); ctx.fill(); ctx.stroke();\n      ctx.fillStyle='#111'; ctx.textAlign='left'; ctx.textBaseline='middle'; ctx.fillText(text,tx+9,ty+14);\n    }\n    ctx.restore();\n  }\n  function drawSelectionOverlay(){\n    if(!drag.active || !drag.moved) return;\n    const gx0=plot.left, gy0=plot.top, gx1=plot.w-plot.right, gy1=plot.h-plot.bottom;\n    const x0=Math.max(gx0,Math.min(gx1,drag.startX));\n    const y0=Math.max(gy0,Math.min(gy1,drag.startY));\n    const x1=Math.max(gx0,Math.min(gx1,drag.x));\n    const y1=Math.max(gy0,Math.min(gy1,drag.y));\n    ctx.save();\n    ctx.fillStyle='rgba(181,18,27,.10)';\n    ctx.strokeStyle='rgba(181,18,27,.85)';\n    ctx.lineWidth=1.4;\n    ctx.setLineDash([6,4]);\n    ctx.fillRect(Math.min(x0,x1),Math.min(y0,y1),Math.abs(x1-x0),Math.abs(y1-y0));\n    ctx.strokeRect(Math.min(x0,x1),Math.min(y0,y1),Math.abs(x1-x0),Math.abs(y1-y0));\n    ctx.restore();\n  }\n  function roundRect(ctx,x,y,w,h,r){\n    ctx.beginPath();\n    ctx.moveTo(x+r,y); ctx.arcTo(x+w,y,x+w,y+h,r); ctx.arcTo(x+w,y+h,x,y+h,r);\n    ctx.arcTo(x,y+h,x,y,r); ctx.arcTo(x,y,x+w,y,r); ctx.closePath();\n  }\n\n  function renderLegend(){\n    const lg=$('legend'); lg.innerHTML=''; let i=0;\n    for(const g of groupData().keys()){\n      const div=document.createElement('div'); div.className='legend-item';\n      div.innerHTML=`<span class=\"swatch\" style=\"background:${colors[i%colors.length]}\"><\/span>${escapeHtml(g)}`; i++;\n      lg.appendChild(div);\n    }\n  }\n\n  function nearestFit(px,py){\n    let best=null, dist=Infinity;\n    for(const [g,f] of fits.entries()){\n      const pts=f.points; const lmin=Math.max(plot.xMin,Math.min(...pts.map(p=>p.lambda))*0.92); const lmax=Math.min(plot.xMax,Math.max(...pts.map(p=>p.lambda))*1.08);\n      for(let i=0;i<=240;i++){ const lam=lmin+(lmax-lmin)*i\/240; const dx=X(lam)-px, dy=Y(cauchyEval(f,lam))-py; const d=dx*dx+dy*dy; if(d<dist){dist=d; best=g;} }\n    }\n    return dist<900 ? best : null;\n  }\n\n  function calculateAtLambda(){\n    const lam=Number($('calcLambda').value);\n    if(!Number.isFinite(lam)||lam<=0){ $('calcOutput').textContent='\u8bf7\u8f93\u5165\u6709\u6548\u6ce2\u957f\u3002'; return; }\n    if(!fits.size){ $('calcOutput').textContent='\u8bf7\u5148\u8fdb\u884c\u67ef\u897f\u62df\u5408\u3002'; return; }\n    let out=`\u03bb = ${lam.toFixed(3)} nm\\n`;\n    for(const [g,f] of fits.entries()) out += `${g}: n = ${cauchyEval(f,lam).toFixed(8)}\\n`;\n    $('calcOutput').textContent=out.trim();\n  }\n\n  function exportCSV(){\n    let csv='type,group,lambda_nm,n,A,B_um2,C_um4,R2,Abbe_Vd,formula_lambda_um\\n';\n    for(const p of data) csv += `data,${p.group},${p.lambda},${p.n},,,,,,\\n`;\n    for(const [g,f] of fits.entries()) csv += `fit,${g},,,${f.A},${f.B},${f.C},${f.r2},${f.abbe},\"${plainCauchyFormula(f,10)}\"\\n`;\n    const blob=new Blob([csv],{type:'text\/csv;charset=utf-8'}); const url=URL.createObjectURL(blob); const a=document.createElement('a'); a.href=url; a.download='cauchy_fit_results.csv'; a.click(); URL.revokeObjectURL(url);\n  }\n  function exportPNG(){\n    const a=document.createElement('a'); a.href=canvas.toDataURL('image\/png'); a.download='cauchy_fit_chart.png'; a.click();\n  }\n\n  function renderAll(){renderDataList(); renderResults(); drawChart();}\n\n  $('addBtn').onclick=()=>{clearMsg(); if(addPoint($('lambdaInput').value,$('nInput').value,$('groupInput').value)) showMsg('\u5df2\u6dfb\u52a0\u6570\u636e\u70b9\u3002','ok');};\n  $('importBtn').onclick=()=>{clearMsg(); const pts=parseBulk($('bulkInput').value); if(!pts.length){showMsg('\u6ca1\u6709\u8bc6\u522b\u5230\u6709\u6548\u6570\u636e\u3002','err');return;} data.length=0; data.push(...pts); selectedPointIndex=data.length-1; fits.clear(); selectedFitGroup=null; view=null; renderAll(); showMsg(`\u5df2\u5bfc\u5165 ${pts.length} \u4e2a\u6570\u636e\u70b9\u3002`,'ok');};\n  $('fitBtn').onclick=runFit;\n  $('clearBtn').onclick=()=>{data.length=0; fits.clear(); selectedPointIndex=-1; selectedFitGroup=null; view=null; clearMsg(); renderAll();};\n  $('deleteBtn').onclick=()=>{if(selectedPointIndex>=0){data.splice(selectedPointIndex,1); selectedPointIndex=Math.min(selectedPointIndex,data.length-1); fits.clear(); view=null; renderAll();}};\n  $('calcBtn').onclick=calculateAtLambda;\n  $('exportCsvBtn').onclick=exportCSV; $('exportPngBtn').onclick=exportPNG;\n  canvas.addEventListener('mousedown',ev=>{\n    if(ev.button!==0) return;\n    hideChartMenu();\n    const rect=canvas.getBoundingClientRect();\n    const x=ev.clientX-rect.left, y=ev.clientY-rect.top;\n    if(!isInsidePlot(x,y)) return;\n    drag.active=true; drag.moved=false; drag.startX=x; drag.startY=y; drag.x=x; drag.y=y;\n  });\n  canvas.addEventListener('mousemove',ev=>{\n    const rect=canvas.getBoundingClientRect();\n    const x=ev.clientX-rect.left, y=ev.clientY-rect.top;\n    mouse.x=x; mouse.y=y; mouse.inside=isInsidePlot(x,y);\n    mouse.lambdaSnap = mouse.inside ? snapXValue(invX(x)) : null;\n    hoverPointIndex = mouse.inside ? nearestDataPoint(x,y) : -1;\n    hoverCurvePoint = mouse.inside ? getHoverCurvePoint(mouse.lambdaSnap, x, y) : null;\n    if(drag.active){\n      drag.x=x; drag.y=y;\n      if(Math.hypot(drag.x-drag.startX, drag.y-drag.startY)>4) drag.moved=true;\n    }\n    $('cursorX').textContent=mouse.inside ? fmt(mouse.lambdaSnap,1) : fmt(invX(x),3);\n    $('cursorY').textContent=hoverCurvePoint ? fmt(hoverCurvePoint.n,5) : fmt(invY(y),5);\n    drawChart();\n  });\n  canvas.addEventListener('mouseup',ev=>{\n    if(ev.button!==0 || !drag.active) return;\n    const rect=canvas.getBoundingClientRect();\n    const x=ev.clientX-rect.left, y=ev.clientY-rect.top;\n    drag.x=x; drag.y=y;\n    if(drag.moved){\n      const changed=setViewFromSelection(drag.startX, drag.startY, drag.x, drag.y);\n      drag.active=false; drag.moved=false;\n      if(changed) drawChart();\n      return;\n    }\n    drag.active=false; drag.moved=false;\n    const g=nearestFit(x,y);\n    if(g){selectedFitGroup=g; renderAll();}\n    else drawChart();\n  });\n  canvas.addEventListener('mouseleave',()=>{ mouse.inside=false; mouse.x=null; mouse.y=null; mouse.lambdaSnap=null; hoverPointIndex=-1; hoverCurvePoint=null; if(drag.active){drag.active=false;drag.moved=false;} drawChart(); });\n  canvas.addEventListener('wheel',ev=>{\n    ev.preventDefault();\n    hideChartMenu();\n    const rect=canvas.getBoundingClientRect();\n    const x=ev.clientX-rect.left, y=ev.clientY-rect.top;\n    mouse.x=x; mouse.y=y; mouse.inside=isInsidePlot(x,y); mouse.lambdaSnap = mouse.inside ? snapXValue(invX(x)) : null; hoverPointIndex = mouse.inside ? nearestDataPoint(x,y) : -1; hoverCurvePoint = mouse.inside ? getHoverCurvePoint(mouse.lambdaSnap, x, y) : null;\n    if(mouse.inside) setViewFromWheel(x,y,ev.deltaY);\n  }, {passive:false});\n  canvas.addEventListener('contextmenu',ev=>{\n    ev.preventDefault();\n    showChartMenu(ev.clientX, ev.clientY);\n  });\n  function showChartMenu(clientX,clientY){\n    const menu=$('chartMenu');\n    menu.style.left=clientX+'px';\n    menu.style.top=clientY+'px';\n    menu.style.display='block';\n  }\n  function hideChartMenu(){ $('chartMenu').style.display='none'; }\n  $('resetZoomBtn').onclick=()=>{ hideChartMenu(); resetView(); };\n  document.addEventListener('click',ev=>{ if(!$('chartMenu').contains(ev.target)) hideChartMenu(); });\n  window.addEventListener('resize',drawChart);\n\n  \/\/ Load the user's example by default and fit immediately.\n  data.push({lambda:531.951,n:1.515,group:'1\u7ec4'},{lambda:635,n:1.485,group:'1\u7ec4'},{lambda:980,n:1.465,group:'1\u7ec4'});\n  try{ fits.set('1\u7ec4', fitCauchy(data)); selectedFitGroup='1\u7ec4'; }catch(e){}\n  renderAll(); calculateAtLambda();\n})();\n<\/script>\n<\/body>\n<\/html>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t\t\t<\/div>\n\t\t","protected":false},"excerpt":{"rendered":"<p>\u8272\u6563\u66f2\u7ebf\u62df\u5408\uff5c\u67ef\u897f\u62df\u5408\u5de5\u5177 v7 \u67ef\u897f\u62df<\/p>\n","protected":false},"author":1,"featured_media":0,"parent":0,"menu_order":0,"comment_status":"closed","ping_status":"closed","template":"","meta":{"footnotes":""},"class_list":["post-10443","page","type-page","status-publish","hentry"],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v28.3 - https:\/\/yoast.com\/product\/yoast-seo-wordpress\/ -->\n<title>\u67ef\u897f\u62df\u5408 - VoyaWave<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/voyawave.com\/zh-hans\/%e6%9f%af%e8%a5%bf%e6%8b%9f%e5%90%88\/\" \/>\n<meta property=\"og:locale\" content=\"zh_CN\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"\u67ef\u897f\u62df\u5408 - VoyaWave\" \/>\n<meta property=\"og:description\" content=\"\u8272\u6563\u66f2\u7ebf\u62df\u5408\uff5c\u67ef\u897f\u62df\u5408\u5de5\u5177 v7 \u67ef\u897f\u62df\" \/>\n<meta property=\"og:url\" content=\"https:\/\/voyawave.com\/zh-hans\/%e6%9f%af%e8%a5%bf%e6%8b%9f%e5%90%88\/\" \/>\n<meta property=\"og:site_name\" content=\"VoyaWave\" \/>\n<meta property=\"article:modified_time\" content=\"2026-08-11T07:44:41+00:00\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"\u9884\u8ba1\u9605\u8bfb\u65f6\u95f4\" \/>\n\t<meta name=\"twitter:data1\" content=\"1 \u5206\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\\\/\\\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\\\/\\\/voyawave.com\\\/zh-hans\\\/%e6%9f%af%e8%a5%bf%e6%8b%9f%e5%90%88\\\/\",\"url\":\"https:\\\/\\\/voyawave.com\\\/zh-hans\\\/%e6%9f%af%e8%a5%bf%e6%8b%9f%e5%90%88\\\/\",\"name\":\"\u67ef\u897f\u62df\u5408 - VoyaWave\",\"isPartOf\":{\"@id\":\"https:\\\/\\\/springgreen-quail-104003.hostingersite.com\\\/#website\"},\"datePublished\":\"2026-08-06T08:32:26+00:00\",\"dateModified\":\"2026-08-11T07:44:41+00:00\",\"breadcrumb\":{\"@id\":\"https:\\\/\\\/voyawave.com\\\/zh-hans\\\/%e6%9f%af%e8%a5%bf%e6%8b%9f%e5%90%88\\\/#breadcrumb\"},\"inLanguage\":\"zh-Hans\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\\\/\\\/voyawave.com\\\/zh-hans\\\/%e6%9f%af%e8%a5%bf%e6%8b%9f%e5%90%88\\\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\\\/\\\/voyawave.com\\\/zh-hans\\\/%e6%9f%af%e8%a5%bf%e6%8b%9f%e5%90%88\\\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\\\/\\\/voyawave.com\\\/zh-hans\\\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"\u67ef\u897f\u62df\u5408\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\\\/\\\/springgreen-quail-104003.hostingersite.com\\\/#website\",\"url\":\"https:\\\/\\\/springgreen-quail-104003.hostingersite.com\\\/\",\"name\":\"Shenzhen VoyaWave Optics Technology Co., Ltd.\",\"description\":\"\",\"publisher\":{\"@id\":\"https:\\\/\\\/springgreen-quail-104003.hostingersite.com\\\/#organization\"},\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\\\/\\\/springgreen-quail-104003.hostingersite.com\\\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"zh-Hans\"},{\"@type\":\"Organization\",\"@id\":\"https:\\\/\\\/springgreen-quail-104003.hostingersite.com\\\/#organization\",\"name\":\"\u542f\u626c\u5149\u5b66\",\"url\":\"https:\\\/\\\/springgreen-quail-104003.hostingersite.com\\\/\",\"logo\":{\"@type\":\"ImageObject\",\"inLanguage\":\"zh-Hans\",\"@id\":\"https:\\\/\\\/springgreen-quail-104003.hostingersite.com\\\/#\\\/schema\\\/logo\\\/image\\\/\",\"url\":\"https:\\\/\\\/voyawave.com\\\/wp-content\\\/uploads\\\/2025\\\/05\\\/2-240920110542592-scaled.png\",\"contentUrl\":\"https:\\\/\\\/voyawave.com\\\/wp-content\\\/uploads\\\/2025\\\/05\\\/2-240920110542592-scaled.png\",\"width\":2560,\"height\":766,\"caption\":\"\u542f\u626c\u5149\u5b66\"},\"image\":{\"@id\":\"https:\\\/\\\/springgreen-quail-104003.hostingersite.com\\\/#\\\/schema\\\/logo\\\/image\\\/\"}}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"\u67ef\u897f\u62df\u5408 - VoyaWave","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/voyawave.com\/zh-hans\/%e6%9f%af%e8%a5%bf%e6%8b%9f%e5%90%88\/","og_locale":"zh_CN","og_type":"article","og_title":"\u67ef\u897f\u62df\u5408 - VoyaWave","og_description":"\u8272\u6563\u66f2\u7ebf\u62df\u5408\uff5c\u67ef\u897f\u62df\u5408\u5de5\u5177 v7 \u67ef\u897f\u62df","og_url":"https:\/\/voyawave.com\/zh-hans\/%e6%9f%af%e8%a5%bf%e6%8b%9f%e5%90%88\/","og_site_name":"VoyaWave","article_modified_time":"2026-08-11T07:44:41+00:00","twitter_card":"summary_large_image","twitter_misc":{"\u9884\u8ba1\u9605\u8bfb\u65f6\u95f4":"1 \u5206"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/voyawave.com\/zh-hans\/%e6%9f%af%e8%a5%bf%e6%8b%9f%e5%90%88\/","url":"https:\/\/voyawave.com\/zh-hans\/%e6%9f%af%e8%a5%bf%e6%8b%9f%e5%90%88\/","name":"\u67ef\u897f\u62df\u5408 - VoyaWave","isPartOf":{"@id":"https:\/\/springgreen-quail-104003.hostingersite.com\/#website"},"datePublished":"2026-08-06T08:32:26+00:00","dateModified":"2026-08-11T07:44:41+00:00","breadcrumb":{"@id":"https:\/\/voyawave.com\/zh-hans\/%e6%9f%af%e8%a5%bf%e6%8b%9f%e5%90%88\/#breadcrumb"},"inLanguage":"zh-Hans","potentialAction":[{"@type":"ReadAction","target":["https:\/\/voyawave.com\/zh-hans\/%e6%9f%af%e8%a5%bf%e6%8b%9f%e5%90%88\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/voyawave.com\/zh-hans\/%e6%9f%af%e8%a5%bf%e6%8b%9f%e5%90%88\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/voyawave.com\/zh-hans\/"},{"@type":"ListItem","position":2,"name":"\u67ef\u897f\u62df\u5408"}]},{"@type":"WebSite","@id":"https:\/\/springgreen-quail-104003.hostingersite.com\/#website","url":"https:\/\/springgreen-quail-104003.hostingersite.com\/","name":"Shenzhen VoyaWave Optics Technology Co., Ltd.","description":"","publisher":{"@id":"https:\/\/springgreen-quail-104003.hostingersite.com\/#organization"},"potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/springgreen-quail-104003.hostingersite.com\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"zh-Hans"},{"@type":"Organization","@id":"https:\/\/springgreen-quail-104003.hostingersite.com\/#organization","name":"\u542f\u626c\u5149\u5b66","url":"https:\/\/springgreen-quail-104003.hostingersite.com\/","logo":{"@type":"ImageObject","inLanguage":"zh-Hans","@id":"https:\/\/springgreen-quail-104003.hostingersite.com\/#\/schema\/logo\/image\/","url":"https:\/\/voyawave.com\/wp-content\/uploads\/2025\/05\/2-240920110542592-scaled.png","contentUrl":"https:\/\/voyawave.com\/wp-content\/uploads\/2025\/05\/2-240920110542592-scaled.png","width":2560,"height":766,"caption":"\u542f\u626c\u5149\u5b66"},"image":{"@id":"https:\/\/springgreen-quail-104003.hostingersite.com\/#\/schema\/logo\/image\/"}}]}},"_links":{"self":[{"href":"https:\/\/voyawave.com\/zh-hans\/wp-json\/wp\/v2\/pages\/10443","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/voyawave.com\/zh-hans\/wp-json\/wp\/v2\/pages"}],"about":[{"href":"https:\/\/voyawave.com\/zh-hans\/wp-json\/wp\/v2\/types\/page"}],"author":[{"embeddable":true,"href":"https:\/\/voyawave.com\/zh-hans\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/voyawave.com\/zh-hans\/wp-json\/wp\/v2\/comments?post=10443"}],"version-history":[{"count":16,"href":"https:\/\/voyawave.com\/zh-hans\/wp-json\/wp\/v2\/pages\/10443\/revisions"}],"predecessor-version":[{"id":10610,"href":"https:\/\/voyawave.com\/zh-hans\/wp-json\/wp\/v2\/pages\/10443\/revisions\/10610"}],"wp:attachment":[{"href":"https:\/\/voyawave.com\/zh-hans\/wp-json\/wp\/v2\/media?parent=10443"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}