{"spec_id":"energy-level-atomic","library":"d3","language":"javascript","code":"// anyplot.ai\n// energy-level-atomic: Atomic Energy Level Diagram\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 93/100 | Created: 2026-08-24\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 120, right: 200, bottom: 90, left: 120 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data: hydrogen atom energy levels (in-memory, deterministic) ----------\n// E(n) = -13.606 / n^2 eV — Rydberg formula for the hydrogen atom\nconst RYDBERG_EV = 13.606;\nconst levels = d3.range(1, 7).map((n) => ({ n, energy: -RYDBERG_EV / (n * n) }));\nconst energyOf = (n) => levels.find((l) => l.n === n).energy;\n\n// Transitions: emission (downward, upper -> lower) and one absorption (upward)\nconst transitions = [\n  { from: 1, to: 3, kind: \"absorption\", series: \"Absorption\" },\n  { from: 2, to: 1, kind: \"emission\", series: \"Lyman series\" },\n  { from: 3, to: 1, kind: \"emission\", series: \"Lyman series\" },\n  { from: 4, to: 1, kind: \"emission\", series: \"Lyman series\" },\n  { from: 3, to: 2, kind: \"emission\", series: \"Balmer series\" },\n  { from: 4, to: 2, kind: \"emission\", series: \"Balmer series\" },\n].map((tr) => {\n  const deltaE = Math.abs(energyOf(tr.to) - energyOf(tr.from));\n  const wavelengthNm = 1239.84 / deltaE; // E = hc / lambda, hc = 1239.84 eV*nm\n  return { ...tr, deltaE, wavelengthNm };\n});\n\n// --- SVG mount ---------------------------------------------------------------\nconst svg = d3\n  .select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height)\n  .style(\"font-family\", \"system-ui, -apple-system, sans-serif\");\n\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales -------------------------------------------------------------------\n// Square-root transform on the (negative) energy axis spreads out the levels\n// that converge near the ionization limit, per the spec's nonlinear-scale note.\nconst minEnergy = d3.min(levels, (d) => d.energy);\nconst y = d3.scalePow().exponent(0.5).domain([minEnergy, 0]).range([ih, 0]);\n\nconst levelLineWidth = iw * 0.54;\nconst arrowX = d3\n  .scalePoint()\n  .domain(transitions.map((_, i) => i))\n  .range([50, levelLineWidth - 50])\n  .padding(1);\n\nconst wavelengths = transitions.map((tr) => tr.wavelengthNm);\nconst [minWl, maxWl] = d3.extent(wavelengths);\nconst color = d3.scaleSequential(d3.interpolateRgbBasis(t.seq)).domain([minWl, maxWl]);\n\n// --- Y axis (energy) -----------------------------------------------------------\nconst tickValues = [...levels.map((d) => d.energy), 0];\nconst yAxis = g\n  .append(\"g\")\n  .call(\n    d3\n      .axisLeft(y)\n      .tickValues(tickValues)\n      .tickFormat((d) => (d === 0 ? \"0 eV\" : `${d.toFixed(2)} eV`))\n      .tickSize(6)\n  );\nyAxis.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"15px\");\nyAxis.selectAll(\"line\").attr(\"stroke\", t.grid);\nyAxis.select(\".domain\").attr(\"stroke\", t.inkSoft);\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -88)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .text(\"Energy (eV)\");\n\n// --- Ionization limit reference line -------------------------------------------\ng.append(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", levelLineWidth)\n  .attr(\"y1\", y(0))\n  .attr(\"y2\", y(0))\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 2)\n  .attr(\"stroke-dasharray\", \"8,6\");\n\ng.append(\"text\")\n  .attr(\"x\", levelLineWidth + 14)\n  .attr(\"y\", y(0) + 5)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .style(\"font-style\", \"italic\")\n  .text(\"Ionization limit\");\n\n// --- Energy level lines ----------------------------------------------------\ng.selectAll(\".level-line\")\n  .data(levels)\n  .join(\"line\")\n  .attr(\"class\", \"level-line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", levelLineWidth)\n  .attr(\"y1\", (d) => y(d.energy))\n  .attr(\"y2\", (d) => y(d.energy))\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 4)\n  .attr(\"stroke-linecap\", \"round\");\n\ng.selectAll(\".level-label\")\n  .data(levels)\n  .join(\"text\")\n  .attr(\"class\", \"level-label\")\n  .attr(\"x\", levelLineWidth + 14)\n  .attr(\"y\", (d) => y(d.energy) + 5)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .style(\"font-weight\", \"500\")\n  .text((d) => `n = ${d.n}`);\n\n// --- Transition arrows, colored by transition wavelength -----------------------\nconst headLen = 14;\ntransitions.forEach((tr, i) => {\n  const x = arrowX(i);\n  const yFrom = y(energyOf(tr.from));\n  const yTo = y(energyOf(tr.to));\n  const dir = yTo > yFrom ? 1 : -1; // +1 = pointing down (emission), -1 = up (absorption)\n  const c = color(tr.wavelengthNm);\n\n  g.append(\"line\")\n    .attr(\"x1\", x)\n    .attr(\"y1\", yFrom)\n    .attr(\"x2\", x)\n    .attr(\"y2\", yTo - dir * headLen)\n    .attr(\"stroke\", c)\n    .attr(\"stroke-width\", 3.5);\n\n  g.append(\"path\")\n    .attr(\n      \"d\",\n      `M ${x - 7} ${yTo - dir * headLen} L ${x + 7} ${yTo - dir * headLen} L ${x} ${yTo} Z`\n    )\n    .attr(\"fill\", c);\n\n  g.append(\"text\")\n    .attr(\"x\", x + 10)\n    .attr(\"y\", (yFrom + yTo) / 2)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"13px\")\n    .text(`${tr.wavelengthNm.toFixed(1)} nm`);\n});\n\n// --- Series group labels beneath the plot ---------------------------------------\nconst seriesGroups = d3.rollup(\n  transitions.map((tr, i) => ({ ...tr, x: arrowX(i) })),\n  (v) => d3.mean(v, (d) => d.x),\n  (d) => d.series\n);\nfor (const [series, cx] of seriesGroups) {\n  g.append(\"text\")\n    .attr(\"x\", cx)\n    .attr(\"y\", ih + 40)\n    .attr(\"text-anchor\", \"middle\")\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .style(\"font-style\", \"italic\")\n    .text(series);\n}\n\n// --- Direction legend ------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", 0)\n  .attr(\"y\", 22)\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"13px\")\n  .text(\"↓ emission   ↑ absorption\");\n\n// --- Wavelength colorbar legend (vertical, in the right margin) ----------------\nconst barX = iw + 44;\nconst barWidth = 22;\nconst barHeight = 300;\nconst barY = (ih - barHeight) / 2;\n\nconst gradientId = \"wavelength-gradient\";\nconst defs = svg.append(\"defs\");\nconst gradient = defs\n  .append(\"linearGradient\")\n  .attr(\"id\", gradientId)\n  .attr(\"x1\", \"0\")\n  .attr(\"x2\", \"0\")\n  .attr(\"y1\", \"0\")\n  .attr(\"y2\", \"1\");\ngradient.append(\"stop\").attr(\"offset\", \"0%\").attr(\"stop-color\", t.seq[0]);\ngradient.append(\"stop\").attr(\"offset\", \"100%\").attr(\"stop-color\", t.seq[1]);\n\ng.append(\"rect\")\n  .attr(\"x\", barX)\n  .attr(\"y\", barY)\n  .attr(\"width\", barWidth)\n  .attr(\"height\", barHeight)\n  .attr(\"fill\", `url(#${gradientId})`)\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 1);\n\nconst wlScale = d3.scaleLinear().domain([minWl, maxWl]).range([0, barHeight]);\n[minWl, (minWl + maxWl) / 2, maxWl].forEach((wl) => {\n  g.append(\"text\")\n    .attr(\"x\", barX + barWidth + 8)\n    .attr(\"y\", barY + wlScale(wl) + 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"13px\")\n    .text(`${Math.round(wl)} nm`);\n});\n\ng.append(\"text\")\n  .attr(\"x\", barX + barWidth / 2)\n  .attr(\"y\", barY - 14)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"14px\")\n  .text(\"Wavelength\");\n\n// --- Title ------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 48)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"energy-level-atomic · javascript · d3 · anyplot.ai\");\n"}