{"spec_id":"spectrum-nmr","library":"d3","language":"javascript","code":"// anyplot.ai\n// spectrum-nmr: NMR Spectrum (Nuclear Magnetic Resonance)\n// Library: d3 7.9.0 | JavaScript 22.22.3\n// Quality: 91/100 | Created: 2026-06-03\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\n\nconst margin = { top: 100, right: 80, bottom: 95, left: 90 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// Ethanol ¹H NMR at 300 MHz — Lorentzian line shapes\n// J-coupling ~7 Hz → Δppm = 7/300 ≈ 0.0233 ppm\nconst J = 7 / 300;\nconst gamma = 0.005; // Lorentzian HWHM in ppm\n\nfunction lorentzian(x, x0, amp) {\n  const d = x - x0;\n  return amp * gamma * gamma / (d * d + gamma * gamma);\n}\n\n// [ppm_position, amplitude] for each multiplet line\nconst peakLines = [\n  [0.00, 0.15],                                                // TMS singlet\n  [1.2 - J, 0.50], [1.2, 1.00], [1.2 + J, 0.50],             // CH₃ triplet 1:2:1\n  [2.60, 0.40],                                                // OH singlet\n  [3.7 - 1.5 * J, 0.22], [3.7 - 0.5 * J, 0.66],              // CH₂ quartet 1:3:3:1\n  [3.7 + 0.5 * J, 0.66], [3.7 + 1.5 * J, 0.22],\n];\n\nconst nPoints = 4000;\nconst ppmMin = -0.5;\nconst ppmMax = 5.2;\n\nconst spectrum = [];\nfor (let i = 0; i < nPoints; i++) {\n  const ppm = ppmMin + (ppmMax - ppmMin) * i / (nPoints - 1);\n  // Deterministic micro-noise for realism; amplitude << peak heights\n  let intensity = 0.002 * Math.sin(ppm * 127.3) * Math.sin(ppm * 43.7 + 0.5);\n  for (const [pos, amp] of peakLines) {\n    intensity += lorentzian(ppm, pos, amp);\n  }\n  spectrum.push({ ppm, intensity });\n}\n\n// SVG\nconst svg = d3.select(\"#container\")\n  .append(\"svg\").attr(\"width\", width).attr(\"height\", height);\n\nconst g = svg.append(\"g\")\n  .attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// Scales — x reversed per NMR convention (high ppm on left)\nconst x = d3.scaleLinear().domain([ppmMax, ppmMin]).range([0, iw]);\nconst maxInt = d3.max(spectrum, d => d.intensity);\nconst y = d3.scaleLinear().domain([-0.04, maxInt * 1.18]).range([ih, 0]);\n\n// Horizontal gridlines\n[0.25, 0.5, 0.75].forEach(val => {\n  g.append(\"line\")\n    .attr(\"x1\", 0).attr(\"x2\", iw)\n    .attr(\"y1\", y(val)).attr(\"y2\", y(val))\n    .attr(\"stroke\", t.grid).attr(\"stroke-width\", 0.8);\n});\n\n// Baseline reference (dashed)\ng.append(\"line\")\n  .attr(\"x1\", 0).attr(\"x2\", iw)\n  .attr(\"y1\", y(0)).attr(\"y2\", y(0))\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1)\n  .attr(\"stroke-dasharray\", \"5,4\")\n  .attr(\"opacity\", 0.35);\n\n// Spectrum line\nconst line = d3.line().x(d => x(d.ppm)).y(d => y(d.intensity));\n\ng.append(\"path\")\n  .datum(spectrum)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 1.8)\n  .attr(\"d\", line);\n\n// Axes\nconst xAxis = g.append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(11).tickSize(6));\n\nconst yAxis = g.append(\"g\")\n  .call(d3.axisLeft(y).tickValues([0, 0.25, 0.5, 0.75, 1.0]).tickSize(6));\n\n[xAxis, yAxis].forEach(ax => {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n});\n\n// Axis labels\ng.append(\"text\")\n  .attr(\"x\", iw / 2).attr(\"y\", ih + 68)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"18px\")\n  .text(\"Chemical Shift (ppm)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2).attr(\"y\", -68)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft).style(\"font-size\", \"18px\")\n  .text(\"Intensity (a.u.)\");\n\n// Peak labels: name + chemical shift value\nconst peakLabels = [\n  { ppm: 0.00, amp: 0.15, name: \"TMS\",  ppmStr: \"0.00\" },\n  { ppm: 1.20, amp: 1.00, name: \"CH₃\", ppmStr: \"1.20\" },\n  { ppm: 2.60, amp: 0.40, name: \"OH\",   ppmStr: \"2.60\" },\n  { ppm: 3.70, amp: 0.66, name: \"CH₂\", ppmStr: \"3.70\" },\n];\n\npeakLabels.forEach(({ ppm, amp, name, ppmStr }) => {\n  const lx = x(ppm);\n  const peakY = y(amp);\n  const nameY = peakY - 46;\n  const shiftY = peakY - 28;\n\n  // Short tick from peak top up to label\n  g.append(\"line\")\n    .attr(\"x1\", lx).attr(\"x2\", lx)\n    .attr(\"y1\", peakY - 8).attr(\"y2\", shiftY + 6)\n    .attr(\"stroke\", t.inkSoft).attr(\"stroke-width\", 0.8).attr(\"opacity\", 0.5);\n\n  g.append(\"text\")\n    .attr(\"x\", lx).attr(\"y\", nameY)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.ink).style(\"font-size\", \"14px\").style(\"font-weight\", \"600\")\n    .text(name);\n\n  g.append(\"text\")\n    .attr(\"x\", lx).attr(\"y\", shiftY)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.inkSoft).style(\"font-size\", \"13px\")\n    .text(ppmStr + \" ppm\");\n});\n\n// Title\nsvg.append(\"text\")\n  .attr(\"x\", width / 2).attr(\"y\", 58)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\").style(\"font-weight\", \"600\")\n  .text(\"Ethanol ¹H NMR · spectrum-nmr · javascript · d3 · anyplot.ai\");\n"}