{"spec_id":"energy-level-atomic","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nenergy-level-atomic: Atomic Energy Level Diagram\nLibrary: altair 6.1.0 | Python 3.13.13\nQuality: 86/100 | Updated: 2026-05-30\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport pandas as pd\nfrom PIL import Image\n\n\n# Imprint palette — canonical order, theme-independent\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\n\n# Theme-adaptive chrome tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# --- Data: Hydrogen atom energy levels E_n = -13.6 / n² eV ---\nlevels = {n: -13.6 / n**2 for n in range(1, 7)}\nR_H = 1.097e7  # Rydberg constant, m⁻¹\n\n# Stagger line endpoints so labels stay clear at converging upper levels\nline_ends = {1: 9.0, 2: 9.0, 3: 9.0, 4: 8.0, 5: 9.0, 6: 8.0}\n\nlevel_df = pd.DataFrame(\n    [{\"energy\": levels[n], \"x_start\": 1.5, \"x_end\": line_ends[n], \"label\": f\"n = {n}\"} for n in range(1, 7)]\n)\n\nionization_df = pd.DataFrame([{\"energy\": 0.0, \"x_start\": 1.5, \"x_end\": 9.0, \"label\": \"n = ∞\"}])\nall_labels_df = pd.concat([level_df, ionization_df], ignore_index=True)\n\n# Spectral series transitions (emission: upper → lower)\n# label_dx: alternating offsets for Paschen labels to prevent crowding at converging levels\ntransition_data = [\n    # Lyman series (UV) — transitions to n=1\n    (2, 1, \"Lyman (UV)\", 2.5, 10),\n    (3, 1, \"Lyman (UV)\", 3.1, 10),\n    (4, 1, \"Lyman (UV)\", 3.7, 10),\n    # Balmer series (Visible) — transitions to n=2\n    (3, 2, \"Balmer (Visible)\", 4.8, 10),\n    (4, 2, \"Balmer (Visible)\", 5.4, 10),\n    (5, 2, \"Balmer (Visible)\", 6.0, 10),\n    # Paschen series (IR) — wider x-spacing + graduated dx so labels fan out clearly\n    (4, 3, \"Paschen (IR)\", 6.3, -18),\n    (5, 3, \"Paschen (IR)\", 7.2, 10),\n    (6, 3, \"Paschen (IR)\", 7.9, 25),\n]\n\narrow_df = pd.DataFrame(\n    [\n        {\n            \"x\": x,\n            \"y_upper\": levels[up],\n            \"y_lower\": levels[lo],\n            \"y_mid\": (levels[up] + levels[lo]) / 2,\n            \"series\": s,\n            \"transition\": f\"n={up} → n={lo}\",\n            \"wl_label\": f\"{1e9 / (R_H * (1 / lo**2 - 1 / up**2)):.0f} nm\",\n            \"label_dx\": dx,\n        }\n        for up, lo, s, x, dx in transition_data\n    ]\n)\n\nhead_df = pd.DataFrame([{\"x\": x, \"y\": levels[lo], \"series\": s} for up, lo, s, x, dx in transition_data])\n\nseries_order = [\"Lyman (UV)\", \"Balmer (Visible)\", \"Paschen (IR)\"]\n# Imprint palette positions 1–3: green (#009E73), lavender (#C475FD), blue (#4467A3)\nseries_colors = [IMPRINT[0], IMPRINT[1], IMPRINT[2]]\n\n# Symlog scale distributes energy levels more evenly than linear,\n# compressing the large n=1-to-n=2 gap while expanding upper levels\ny_scale = alt.Scale(type=\"symlog\", constant=1, domain=[-15, 1])\ny_axis = alt.Axis(\n    titleFontSize=12,\n    labelFontSize=10,\n    titlePadding=10,\n    values=[-13.6, -3.4, -1.51, -0.85, -0.54, -0.38, 0],\n    format=\".2f\",\n    gridOpacity=0.12,\n)\n\n# Layer 1: Energy level lines — use INK_SOFT + thin stroke so colored arrows read as primary story\nenergy_lines = (\n    alt.Chart(level_df)\n    .mark_rule(strokeWidth=1.5, color=INK_SOFT)\n    .encode(\n        x=alt.X(\"x_start:Q\", scale=alt.Scale(domain=[0, 11.5]), axis=None),\n        x2=\"x_end:Q\",\n        y=alt.Y(\"energy:Q\", title=\"Energy (eV)\", scale=y_scale, axis=y_axis),\n        tooltip=[alt.Tooltip(\"label:N\", title=\"Level\"), alt.Tooltip(\"energy:Q\", title=\"Energy (eV)\", format=\".2f\")],\n    )\n)\n\n# Layer 2: Ionization limit (dashed reference line)\nion_line = (\n    alt.Chart(ionization_df)\n    .mark_rule(strokeWidth=1.5, strokeDash=[8, 5], color=INK_SOFT)\n    .encode(x=\"x_start:Q\", x2=\"x_end:Q\", y=\"energy:Q\")\n)\n\n# Layer 3: Quantum number labels at line endpoints\nlevel_labels = (\n    alt.Chart(all_labels_df)\n    .mark_text(align=\"left\", baseline=\"middle\", fontSize=11, dx=8, fontWeight=\"bold\", color=INK)\n    .encode(x=\"x_end:Q\", y=\"energy:Q\", text=\"label:N\")\n)\n\n# Layer 4: Transition arrow shafts — thicker than level lines to assert visual priority\narrow_shafts = (\n    alt.Chart(arrow_df)\n    .mark_rule(strokeWidth=3.0, opacity=0.9)\n    .encode(\n        x=\"x:Q\",\n        y=\"y_upper:Q\",\n        y2=\"y_lower:Q\",\n        color=alt.Color(\n            \"series:N\",\n            scale=alt.Scale(domain=series_order, range=series_colors),\n            legend=alt.Legend(title=\"Spectral Series\", titleFontSize=10, labelFontSize=10, symbolSize=150),\n        ),\n        tooltip=[\"transition:N\", \"series:N\", alt.Tooltip(\"wl_label:N\", title=\"Wavelength\")],\n    )\n)\n\n# Layer 5: Arrowheads (triangle-down = emission pointing downward)\narrowheads = (\n    alt.Chart(head_df)\n    .mark_point(shape=\"triangle-down\", filled=True, size=200, opacity=0.9)\n    .encode(\n        x=\"x:Q\",\n        y=\"y:Q\",\n        color=alt.Color(\"series:N\", scale=alt.Scale(domain=series_order, range=series_colors), legend=None),\n    )\n)\n\n# Layer 6: Wavelength annotations — three sub-layers with distinct dx so each\n# Paschen label fans out to a different horizontal position, preventing overlap\n_wl_left = arrow_df[arrow_df[\"label_dx\"] == -18]\n_wl_mid = arrow_df[arrow_df[\"label_dx\"] == 10]\n_wl_right = arrow_df[arrow_df[\"label_dx\"] == 25]\n\nwl_labels_left = (\n    alt.Chart(_wl_left)\n    .mark_text(fontSize=8, angle=90, dx=-18, color=INK_MUTED, fontStyle=\"italic\")\n    .encode(x=\"x:Q\", y=\"y_mid:Q\", text=\"wl_label:N\")\n)\nwl_labels_mid = (\n    alt.Chart(_wl_mid)\n    .mark_text(fontSize=8, angle=90, dx=10, color=INK_MUTED, fontStyle=\"italic\")\n    .encode(x=\"x:Q\", y=\"y_mid:Q\", text=\"wl_label:N\")\n)\nwl_labels_right = (\n    alt.Chart(_wl_right)\n    .mark_text(fontSize=8, angle=90, dx=25, color=INK_MUTED, fontStyle=\"italic\")\n    .encode(x=\"x:Q\", y=\"y_mid:Q\", text=\"wl_label:N\")\n)\n\n# Combine all layers\nchart = (\n    alt.layer(\n        energy_lines, ion_line, level_labels, arrow_shafts, arrowheads, wl_labels_left, wl_labels_mid, wl_labels_right\n    )\n    .properties(\n        width=620,\n        height=320,\n        background=PAGE_BG,\n        title=alt.Title(\n            \"energy-level-atomic · python · altair · anyplot.ai\",\n            fontSize=16,\n            anchor=\"middle\",\n            color=INK,\n            subtitle=\"Hydrogen atom emission lines · energy levels: −13.6/n² eV\",\n            subtitleFontSize=11,\n            subtitleColor=INK_SOFT,\n        ),\n    )\n    .configure_view(strokeWidth=0, fill=PAGE_BG)\n    .configure_title(color=INK)\n    .configure_axis(\n        domainColor=INK_SOFT, tickColor=INK_SOFT, gridColor=INK, gridOpacity=0.12, labelColor=INK_SOFT, titleColor=INK\n    )\n    .configure_legend(fillColor=ELEVATED_BG, strokeColor=INK_SOFT, labelColor=INK_SOFT, titleColor=INK)\n)\n\n# Save PNG then pad to exact 3200×1800 target (landscape)\nTW, TH = 3200, 1800\nchart.save(f\"plot-{THEME}.png\", scale_factor=4.0)\n\n_img = Image.open(f\"plot-{THEME}.png\").convert(\"RGB\")\n_w, _h = _img.size\nif _w > TW or _h > TH:\n    raise SystemExit(\n        f\"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. \"\n        f\"Shrink chart .properties(width=, height=) values and re-render.\"\n    )\nif _w < TW or _h < TH:\n    _canvas = Image.new(\"RGB\", (TW, TH), PAGE_BG)\n    _canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))\n    _canvas.save(f\"plot-{THEME}.png\")\n\n# Save interactive HTML\nchart.save(f\"plot-{THEME}.html\")\n"}