{"spec_id":"calibration-beer-lambert","library":"echarts","language":"javascript","code":"// anyplot.ai\n// calibration-beer-lambert: Beer-Lambert Calibration Curve\n// Library: echarts 5.5.1 | JavaScript 22.22.3\n// Quality: 92/100 | Created: 2026-06-03\n\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (hardcoded, deterministic) ---\n// UV-Vis spectrophotometry standards for a dye solution at 520 nm\nconst stdConc = [0,     0.5,   1.0,   1.5,   2.0,   2.5,   3.0,   3.5];\nconst stdAbs  = [0.002, 0.124, 0.253, 0.371, 0.496, 0.628, 0.742, 0.881];\nconst n = stdConc.length;\n\n// Linear regression: A = m·c + b (Beer-Lambert law)\nlet sumX = 0, sumY = 0, sumXX = 0, sumXY = 0;\nfor (let i = 0; i < n; i++) {\n  sumX  += stdConc[i];\n  sumY  += stdAbs[i];\n  sumXX += stdConc[i] * stdConc[i];\n  sumXY += stdConc[i] * stdAbs[i];\n}\nconst slope     = (n * sumXY - sumX * sumY) / (n * sumXX - sumX * sumX);\nconst intercept = (sumY - slope * sumX) / n;\nconst xMean     = sumX / n;\nconst Sxx       = sumXX - sumX * sumX / n;\nconst yMean     = sumY / n;\nconst SStot     = stdAbs.reduce((s, y) => s + (y - yMean) ** 2, 0);\nconst SSres     = stdConc.reduce((s, x, i) => s + (stdAbs[i] - (slope * x + intercept)) ** 2, 0);\nconst r2        = 1 - SSres / SStot;\nconst se        = Math.sqrt(SSres / (n - 2));\nconst tCrit     = 2.447; // t_{0.975}, 6 df\n\n// Regression line + 95% prediction interval over the concentration range\nconst nFit   = 71;\nconst xFit   = Array.from({ length: nFit }, (_, i) => i * 3.5 / (nFit - 1));\nconst piHalf = xFit.map(x => tCrit * se * Math.sqrt(1 + 1 / n + (x - xMean) ** 2 / Sxx));\nconst regLine = xFit.map(x => [x, slope * x + intercept]);\nconst piUpper = xFit.map((x, i) => [x, slope * x + intercept + piHalf[i]]);\nconst piLower = xFit.map((x, i) => [x, slope * x + intercept - piHalf[i]]);\n\n// Unknown sample: measured absorbance → inferred concentration\nconst unknownAbs  = 0.580;\nconst unknownConc = (unknownAbs - intercept) / slope;\n\n// Equation annotation strings\nconst signStr = intercept >= 0 ? `+ ${intercept.toFixed(4)}` : `− ${Math.abs(intercept).toFixed(4)}`;\nconst eqText  = `A = ${slope.toFixed(4)} · c  ${signStr}`;\nconst r2Text  = `R² = ${r2.toFixed(4)}`;\n\n// --- Init ---\nconst chart = echarts.init(document.getElementById(\"container\"));\n\nconst bandColor = 'rgba(0,158,115,0.18)';\n\n// Base option (PI dashed boundary lines + data series)\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: 'transparent',\n  title: {\n    text: 'calibration-beer-lambert · javascript · echarts · anyplot.ai',\n    left: 'center',\n    top: 22,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 'bold' }\n  },\n  legend: {\n    data: ['Calibration Standards', 'Linear Fit', '95% Prediction Interval', 'Unknown Sample'],\n    bottom: 22,\n    itemGap: 32,\n    textStyle: { color: t.inkSoft, fontSize: 14 }\n  },\n  grid: { left: 110, right: 70, top: 90, bottom: 140 },\n  xAxis: {\n    type: 'value',\n    name: 'Concentration (mg/L)',\n    nameLocation: 'middle',\n    nameGap: 46,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    min: -0.1,\n    max: 3.7,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine:  { show: true, lineStyle: { color: t.inkSoft } },\n    axisTick:  { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } }\n  },\n  yAxis: {\n    type: 'value',\n    name: 'Absorbance',\n    nameLocation: 'middle',\n    nameGap: 62,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    min: -0.02,\n    max: 1.02,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine:  { show: true, lineStyle: { color: t.inkSoft } },\n    axisTick:  { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } }\n  },\n  series: [\n    // PI upper boundary (dashed line, defines the legend entry)\n    {\n      name: '95% Prediction Interval',\n      type: 'line',\n      data: piUpper,\n      lineStyle: { color: t.palette[0], width: 1.5, type: 'dashed', opacity: 0.55 },\n      symbol: 'none',\n      silent: true,\n      z: 2\n    },\n    // PI lower boundary (dashed line, shares legend entry via same name)\n    {\n      name: '95% Prediction Interval',\n      type: 'line',\n      data: piLower,\n      lineStyle: { color: t.palette[0], width: 1.5, type: 'dashed', opacity: 0.55 },\n      symbol: 'none',\n      silent: true,\n      z: 2\n    },\n    // Regression line\n    {\n      name: 'Linear Fit',\n      type: 'line',\n      data: regLine,\n      color: t.palette[0],\n      lineStyle: { color: t.palette[0], width: 3 },\n      symbol: 'none',\n      z: 3\n    },\n    // Calibration standard points\n    {\n      name: 'Calibration Standards',\n      type: 'scatter',\n      data: stdConc.map((c, i) => [c, stdAbs[i]]),\n      symbolSize: 18,\n      itemStyle: { color: t.palette[0], borderColor: t.pageBg, borderWidth: 2 },\n      z: 4\n    },\n    // Unknown sample with dashed guide lines to axes\n    {\n      name: 'Unknown Sample',\n      type: 'scatter',\n      data: [[unknownConc, unknownAbs]],\n      symbol: 'diamond',\n      symbolSize: 22,\n      itemStyle: { color: t.palette[3] },\n      z: 5,\n      markLine: {\n        symbol: ['none', 'none'],\n        animation: false,\n        lineStyle: { type: 'dashed', color: t.inkSoft, width: 1.5, opacity: 0.75 },\n        label: { show: false },\n        data: [\n          [{ coord: [0, unknownAbs] },  { coord: [unknownConc, unknownAbs] }],\n          [{ coord: [unknownConc, 0] }, { coord: [unknownConc, unknownAbs] }]\n        ]\n      }\n    }\n  ]\n});\n\n// Add the PI filled polygon + equation box via convertToPixel (runs after layout is set)\nconst upperPx = piUpper.map(pt => chart.convertToPixel('grid', pt));\nconst lowerPx = piLower.map(pt => chart.convertToPixel('grid', pt));\nconst bandPoly = [...upperPx, ...[...lowerPx].reverse()];\n\nchart.setOption({\n  graphic: [\n    // PI filled band polygon\n    {\n      type: 'polygon',\n      shape: { points: bandPoly },\n      style: { fill: bandColor, stroke: 'none' },\n      silent: true,\n      z: 1\n    },\n    // Equation annotation box\n    {\n      type: 'group',\n      left: 122,\n      top: 108,\n      children: [\n        {\n          type: 'rect',\n          shape: { x: 0, y: 0, width: 248, height: 70, r: 5 },\n          style: { fill: t.elevatedBg, stroke: t.grid, lineWidth: 1 }\n        },\n        {\n          type: 'text',\n          x: 14,\n          y: 14,\n          style: { text: eqText, fill: t.ink, fontSize: 15, fontFamily: 'monospace' }\n        },\n        {\n          type: 'text',\n          x: 14,\n          y: 40,\n          style: { text: r2Text, fill: t.ink, fontSize: 15, fontFamily: 'monospace' }\n        }\n      ]\n    }\n  ]\n});\n"}