{"spec_id":"spectrum-nmr","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// spectrum-nmr: NMR Spectrum (Nuclear Magnetic Resonance)\n// Library: chartjs 4.4.7 | JavaScript 22.22.3\n// Quality: 88/100 | Created: 2026-06-03\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\n\n// Lorentzian lineshape: models sharp NMR resonance peaks\nconst lorentz = (x, c, hw, h) => h / (1 + ((x - c) / hw) ** 2);\n\n// Chemical shift axis: 4.5 to -0.3 ppm, 4000 points\nconst N = 4000;\nconst ppmMin = -0.3;\nconst ppmMax = 4.5;\nconst ppmStep = (ppmMax - ppmMin) / (N - 1);\n\n// 1H NMR of ethanol (400 MHz): J = 7 Hz = 0.0175 ppm coupling constant\nconst J = 0.0175;\nconst hw = 0.006; // Lorentzian half-width (~2.4 Hz at 400 MHz; J/hw ≈ 2.9 gives resolved multiplets)\n\nconst peakDefs = [\n    // TMS internal standard at 0.00 ppm — singlet, small reference peak\n    { c: 0.00,           hw,          h: 0.10, label: \"TMS\\n0.00 ppm\" },\n    // CH3 triplet at 1.25 ppm (3H, 1:2:1 intensity pattern)\n    { c: 1.25 - J,       hw,          h: 0.42 },\n    { c: 1.25,           hw,          h: 0.85, label: \"CH₃\\n1.25 ppm\" },\n    { c: 1.25 + J,       hw,          h: 0.42 },\n    // OH singlet at 2.61 ppm (1H, broad due to fast proton exchange)\n    { c: 2.61,           hw: hw * 2.5, h: 0.28, label: \"OH\\n2.61 ppm\" },\n    // CH2 quartet at 3.69 ppm (2H, 1:3:3:1 intensity pattern)\n    { c: 3.69 - 1.5 * J, hw,          h: 0.20 },\n    { c: 3.69 - 0.5 * J, hw,          h: 0.60, label: \"CH₂\\n3.69 ppm\" },\n    { c: 3.69 + 0.5 * J, hw,          h: 0.60 },\n    { c: 3.69 + 1.5 * J, hw,          h: 0.20 },\n];\n\n// Compute spectrum intensities across the chemical shift axis\nconst chartData = Array.from({ length: N }, (_, i) => {\n    const ppm = ppmMax - ppmStep * i;\n    const y = peakDefs.reduce((sum, p) => sum + lorentz(ppm, p.c, p.hw, p.h), 0);\n    return { x: ppm, y };\n});\n\n// Peaks that carry labels\nconst labeledPeaks = peakDefs.filter(p => p.label);\n\n// Title with font size scaled to length (baseline 67 chars → 22px)\nconst titleText =\n    \"1H NMR Spectrum of Ethanol · spectrum-nmr · javascript · chartjs · anyplot.ai\";\nconst titleFontSize = Math.max(15, Math.round(22 * 67 / titleText.length));\n\n// Fill the full canvas with the page background before Chart.js draws\nconst bgPlugin = {\n    id: \"bg\",\n    beforeDraw({ ctx, width, height }) {\n        ctx.save();\n        ctx.fillStyle = t.pageBg;\n        ctx.fillRect(0, 0, width, height);\n        ctx.restore();\n    },\n};\n\n// Draw only bottom and left axis spines (L-shaped frame, scientific style)\nconst spinePlugin = {\n    id: \"spines\",\n    afterDatasetsDraw({ ctx, chartArea: { top, right, bottom, left } }) {\n        ctx.save();\n        ctx.strokeStyle = t.inkSoft;\n        ctx.lineWidth = 1;\n        ctx.beginPath();\n        ctx.moveTo(left, top);\n        ctx.lineTo(left, bottom);\n        ctx.moveTo(left, bottom);\n        ctx.lineTo(right, bottom);\n        ctx.stroke();\n        ctx.restore();\n    },\n};\n\n// Draw chemical group labels above each key peak\nconst labelPlugin = {\n    id: \"peakLabels\",\n    afterDatasetsDraw({ ctx, scales: { x: xs, y: ys } }) {\n        ctx.save();\n        ctx.textAlign = \"center\";\n        ctx.fillStyle = t.ink;\n        ctx.font = \"600 13px sans-serif\";\n        labeledPeaks.forEach(({ c, h, label }) => {\n            const px = xs.getPixelForValue(c);\n            const lines = label.split(\"\\n\");\n            lines.forEach((line, i) => {\n                const py = ys.getPixelForValue(h + 0.09) - (lines.length - 1 - i) * 17;\n                ctx.fillText(line, px, py);\n            });\n        });\n        ctx.restore();\n    },\n};\n\n// Mount\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\nnew Chart(canvas, {\n    type: \"scatter\",\n    data: {\n        datasets: [{\n            data: chartData,\n            showLine: true,\n            borderColor: t.palette[0],\n            backgroundColor: t.palette[0] + \"1a\",\n            borderWidth: 1.8,\n            pointRadius: 0,\n            fill: \"origin\",\n            tension: 0,\n        }],\n    },\n    options: {\n        responsive: true,\n        maintainAspectRatio: false,\n        animation: false,\n        plugins: {\n            title: {\n                display: true,\n                text: titleText,\n                color: t.ink,\n                font: { size: titleFontSize, weight: \"600\" },\n                padding: { top: 8, bottom: 16 },\n            },\n            legend: { display: false },\n            tooltip: { enabled: false },\n        },\n        scales: {\n            x: {\n                type: \"linear\",\n                reverse: true,\n                min: ppmMin,\n                max: ppmMax,\n                border: { display: false },\n                ticks: {\n                    color: t.inkSoft,\n                    font: { size: 14 },\n                    stepSize: 0.5,\n                    callback: (v) => v.toFixed(1),\n                },\n                grid: { color: t.grid },\n                title: {\n                    display: true,\n                    text: \"Chemical Shift (ppm)\",\n                    color: t.ink,\n                    font: { size: 16 },\n                },\n            },\n            y: {\n                min: 0,\n                max: 1.05,\n                border: { display: false },\n                ticks: {\n                    color: t.inkSoft,\n                    font: { size: 14 },\n                    callback: (v) => v.toFixed(1),\n                },\n                grid: { color: t.grid },\n                title: {\n                    display: true,\n                    text: \"Intensity (a.u.)\",\n                    color: t.ink,\n                    font: { size: 16 },\n                },\n            },\n        },\n    },\n    plugins: [bgPlugin, spinePlugin, labelPlugin],\n});\n"}