{"spec_id":"calibration-beer-lambert","library":"muix","language":"javascript","code":"// anyplot.ai\n// calibration-beer-lambert: Beer-Lambert Calibration Curve\n// Library: muix 7.29.1 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-08-20\nimport { ChartContainer } from \"@mui/x-charts/ChartContainer\";\nimport { ScatterPlot } from \"@mui/x-charts/ScatterChart\";\nimport { LinePlot } from \"@mui/x-charts/LineChart\";\nimport { ChartsXAxis } from \"@mui/x-charts/ChartsXAxis\";\nimport { ChartsYAxis } from \"@mui/x-charts/ChartsYAxis\";\nimport { ChartsGrid } from \"@mui/x-charts/ChartsGrid\";\nimport { ChartsLegend } from \"@mui/x-charts/ChartsLegend\";\nimport { useXScale, useYScale, useDrawingArea } from \"@mui/x-charts/hooks\";\n\nconst t = window.ANYPLOT_TOKENS;\nconst THEME = window.ANYPLOT_THEME === \"dark\" ? \"dark\" : \"light\";\n// ANYPLOT_TOKENS has no \"muted\" anchor — derive it from default-style-guide.md\n// \"Theme-adaptive Chrome\" (tertiary text / confidence-band fill token).\nconst INK_MUTED = THEME === \"dark\" ? \"#A8A79F\" : \"#6B6A63\";\n\n// --- Data: UV-Vis calibration standards for a drug-substance assay (243 nm) -\n// Blank plus six standards spanning the assay's working range.\nconst concentration = [0, 2, 4, 6, 8, 10, 12]; // mg/L\nconst absorbance = [0.008, 0.178, 0.325, 0.512, 0.671, 0.845, 1.029]; // dimensionless\n\n// --- Ordinary least-squares fit (Beer-Lambert: A = εlc, i.e. y = mx + b) ----\nconst n = concentration.length;\nconst xMean = concentration.reduce((a, b) => a + b, 0) / n;\nconst yMean = absorbance.reduce((a, b) => a + b, 0) / n;\nlet sxy = 0;\nlet sxx = 0;\nfor (let i = 0; i < n; i += 1) {\n  sxy += (concentration[i] - xMean) * (absorbance[i] - yMean);\n  sxx += (concentration[i] - xMean) ** 2;\n}\nconst slope = sxy / sxx;\nconst intercept = yMean - slope * xMean;\n\nlet ssRes = 0;\nlet ssTot = 0;\nfor (let i = 0; i < n; i += 1) {\n  const fitted = intercept + slope * concentration[i];\n  ssRes += (absorbance[i] - fitted) ** 2;\n  ssTot += (absorbance[i] - yMean) ** 2;\n}\nconst rSquared = 1 - ssRes / ssTot;\nconst residualStdErr = Math.sqrt(ssRes / (n - 2));\nconst T_CRIT_95 = 2.571; // two-tailed 95% critical value, t-distribution df=5\n\n// --- 95% prediction interval band (single future observation, not the mean\n// response — includes the extra \"+1\" term that widens it beyond a CI band) --\nconst GRID_POINTS = 40;\nconst xMin = Math.min(...concentration);\nconst xMax = Math.max(...concentration);\nconst gridX = Array.from(\n  { length: GRID_POINTS },\n  (_, i) => xMin + ((xMax - xMin) * i) / (GRID_POINTS - 1),\n);\nconst fittedY = gridX.map((x) => intercept + slope * x);\nconst halfWidth = gridX.map(\n  (x) =>\n    T_CRIT_95 *\n    residualStdErr *\n    Math.sqrt(1 + 1 / n + (x - xMean) ** 2 / sxx),\n);\nconst upperY = fittedY.map((y, i) => y + halfWidth[i]);\n// Clamp the lower bound at zero — absorbance has no physical negative range,\n// even though the statistical interval dips slightly below it near the blank.\nconst lowerY = fittedY.map((y, i) => Math.max(0, y - halfWidth[i]));\n\nconst scatterData = concentration.map((x, i) => ({\n  x,\n  y: absorbance[i],\n  id: `std-${i}`,\n}));\n\n// --- Unknown sample: measured absorbance -> concentration read off the fit -\nconst unknownAbsorbance = 0.6;\nconst unknownConcentration = (unknownAbsorbance - intercept) / slope;\n\nconst yAllValues = [...absorbance, ...upperY, unknownAbsorbance, 0];\nconst yPad = (Math.max(...yAllValues) - Math.min(...yAllValues)) * 0.08;\nconst yDomainMin = Math.min(...yAllValues) - yPad;\nconst yDomainMax = Math.max(...yAllValues) + yPad;\n\nconst unknownColor = t.palette[1]; // lavender — distinct category from the standards\nconst equationLabel = `y = ${slope.toFixed(4)}x + ${intercept.toFixed(4)}   ·   R² = ${rSquared.toFixed(4)}`;\nconst unknownLabel = `Unknown: A = ${unknownAbsorbance.toFixed(2)} → c ≈ ${unknownConcentration.toFixed(2)} mg/L`;\n\nconst title = \"calibration-beer-lambert · javascript · muix · anyplot.ai\";\n\nfunction PredictionBand() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const top = gridX.map((x, i) => `${xScale(x)},${yScale(upperY[i])}`).join(\" L \");\n  const bottomIndices = [...gridX.keys()].reverse();\n  const bottom = bottomIndices\n    .map((i) => `${xScale(gridX[i])},${yScale(lowerY[i])}`)\n    .join(\" L \");\n  return <path d={`M ${top} L ${bottom} Z`} fill={INK_MUTED} opacity={0.2} stroke=\"none\" />;\n}\n\n// Dashed guide lines from the unknown sample down to the x-axis and across to\n// the y-axis, illustrating how the fit converts a measured absorbance into a\n// concentration — not a fake tooltip, just a static geometric annotation.\nfunction UnknownGuides() {\n  const xScale = useXScale();\n  const yScale = useYScale();\n  const area = useDrawingArea();\n  const px = xScale(unknownConcentration);\n  const py = yScale(unknownAbsorbance);\n  const style = { stroke: INK_MUTED, strokeWidth: 2, strokeDasharray: \"8 6\" };\n  return (\n    <g>\n      <line x1={px} y1={py} x2={px} y2={area.top + area.height} style={style} />\n      <line x1={area.left} y1={py} x2={px} y2={py} style={style} />\n    </g>\n  );\n}\n\n// --- Chart (default-exported component — the harness mounts it) -----------\nexport default function Chart() {\n  const { width, height } = window.ANYPLOT_SIZE;\n\n  return (\n    <ChartContainer\n      width={width}\n      height={height}\n      margin={{ top: 92, right: 64, bottom: 76, left: 132 }}\n      series={[\n        {\n          type: \"scatter\",\n          id: \"standards\",\n          label: \"Calibration standards\",\n          data: scatterData,\n          markerSize: 13,\n          color: t.palette[0],\n        },\n        {\n          type: \"scatter\",\n          id: \"unknown\",\n          label: \"Unknown sample\",\n          data: [{ x: unknownConcentration, y: unknownAbsorbance, id: \"unknown\" }],\n          markerSize: 15,\n          color: unknownColor,\n        },\n        {\n          type: \"line\",\n          id: \"fit\",\n          data: fittedY,\n          curve: \"linear\",\n          color: t.ink,\n          showMark: false,\n          disableHighlight: true,\n        },\n      ]}\n      xAxis={[\n        {\n          data: gridX,\n          scaleType: \"linear\",\n          min: xMin,\n          max: xMax,\n          label: \"Concentration (mg/L)\",\n          tickLabelStyle: { fontSize: 14 },\n          labelStyle: { fontSize: 16 },\n        },\n      ]}\n      yAxis={[\n        {\n          scaleType: \"linear\",\n          min: yDomainMin,\n          max: yDomainMax,\n          label: \"Absorbance\",\n          tickLabelStyle: { fontSize: 14 },\n          labelStyle: { fontSize: 16 },\n        },\n      ]}\n      skipAnimation\n    >\n      <ChartsGrid horizontal />\n      <PredictionBand />\n      <LinePlot skipAnimation slotProps={{ line: { style: { strokeWidth: 3 } } }} />\n      <UnknownGuides />\n      <ChartsXAxis />\n      <ChartsYAxis />\n      <ScatterPlot />\n      <ChartsLegend\n        direction=\"row\"\n        position={{ horizontal: \"right\", vertical: \"top\" }}\n        itemMarkWidth={14}\n        itemMarkHeight={14}\n        labelStyle={{ fontSize: 13, fill: t.inkSoft }}\n      />\n      <text x={width / 2} y={44} textAnchor=\"middle\" fontSize={30} fontWeight={600} fill={t.ink}>\n        {title}\n      </text>\n      <text x={140} y={78} fontSize={15} fill={t.inkSoft}>\n        {equationLabel}\n      </text>\n      <text x={140} y={98} fontSize={13} fill={INK_MUTED}>\n        Shaded band: 95% prediction interval · {unknownLabel}\n      </text>\n      <text\n        x={width - 64}\n        y={height - 30}\n        textAnchor=\"end\"\n        fontSize={12}\n        fill={INK_MUTED}\n      >\n        n = {n} standards (incl. blank)\n      </text>\n    </ChartContainer>\n  );\n}\n"}