{"spec_id":"energy-level-atomic","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\nenergy-level-atomic: Atomic Energy Level Diagram\nLibrary: letsplot 4.10.1 | Python 3.13.13\nQuality: 87/100 | Updated: 2026-05-30\n\"\"\"\n\nimport os\n\nimport pandas as pd\nfrom lets_plot import (\n    LetsPlot,\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_rect,\n    geom_segment,\n    geom_text,\n    ggplot,\n    ggsize,\n    labs,\n    layer_tooltips,\n    scale_color_identity,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n)\nfrom lets_plot.export import ggsave\n\n\nLetsPlot.setup_html()\n\n# Theme tokens — Imprint palette, theme-adaptive chrome\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\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# Imprint categorical palette — canonical order\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nlyman_color = IMPRINT_PALETTE[0]  # brand green — Lyman series (UV)\nbalmer_color = IMPRINT_PALETTE[1]  # lavender — Balmer series (visible)\npaschen_color = IMPRINT_PALETTE[2]  # blue — Paschen series (IR)\n\n# Data — Hydrogen atom energy levels (E_n = -13.6/n² eV)\nlevel_names = [\"n=1\", \"n=2\", \"n=3\", \"n=4\", \"n=5\", \"n=6\"]\nenergies = [-13.6, -3.4, -1.51, -0.85, -0.54, -0.38]\n\n# Power transform on y to spread converging upper levels for legibility\ny_positions = [-(abs(e) ** 0.33) for e in energies]\nlevel_ypos = dict(zip(level_names, y_positions, strict=True))\nion_ypos = 0.0\n\nlevel_df = pd.DataFrame(\n    {\n        \"label\": level_names,\n        \"energy\": energies,\n        \"y\": y_positions,\n        \"x_start\": [0.08] * 6,\n        \"x_end\": [0.56] * 6,\n        \"x_label_right\": [0.60] * 6,\n        \"energy_label\": [f\"{e:.2f} eV\" for e in energies],\n    }\n)\n\n# Emission transitions with observed wavelength (nm)\ntransitions = [\n    (\"n=2\", \"n=1\", \"Lyman\", lyman_color, 121.6),\n    (\"n=3\", \"n=1\", \"Lyman\", lyman_color, 102.6),\n    (\"n=4\", \"n=1\", \"Lyman\", lyman_color, 97.2),\n    (\"n=3\", \"n=2\", \"Balmer\", balmer_color, 656.3),\n    (\"n=4\", \"n=2\", \"Balmer\", balmer_color, 486.1),\n    (\"n=5\", \"n=2\", \"Balmer\", balmer_color, 434.0),\n    (\"n=6\", \"n=2\", \"Balmer\", balmer_color, 410.2),\n    (\"n=4\", \"n=3\", \"Paschen\", paschen_color, 1875.1),\n    (\"n=5\", \"n=3\", \"Paschen\", paschen_color, 1282.0),\n    (\"n=6\", \"n=3\", \"Paschen\", paschen_color, 1093.8),\n]\n\n# Balmer arrows are thicker to emphasise the visible-spectrum series\nseries_cfg = {\n    \"Lyman\": {\"color\": lyman_color, \"size\": 1.4},\n    \"Balmer\": {\"color\": balmer_color, \"size\": 2.2},\n    \"Paschen\": {\"color\": paschen_color, \"size\": 1.4},\n}\n\n# Stagger arrows horizontally within each series to avoid overlap\nseries_base_x = {\"Lyman\": 0.14, \"Balmer\": 0.28, \"Paschen\": 0.43}\nwithin_series_gap = 0.05\n\narrow_rows = []\nseries_counter = {\"Lyman\": 0, \"Balmer\": 0, \"Paschen\": 0}\nfor from_lvl, to_lvl, series, color, wavelength in transitions:\n    idx = series_counter[series]\n    x_pos = series_base_x[series] + idx * within_series_gap\n    series_counter[series] += 1\n    y_top = level_ypos[from_lvl]\n    y_bot = level_ypos[to_lvl]\n    arrow_rows.append(\n        {\n            \"x\": x_pos,\n            \"y_from\": y_top + 0.06,\n            \"y_to\": y_bot - 0.06,\n            \"series\": series,\n            \"color\": color,\n            \"wavelength\": f\"{wavelength:.1f} nm\",\n            \"transition\": f\"{from_lvl} → {to_lvl}\",\n            \"is_alpha\": idx == 0,\n        }\n    )\n\narrow_df = pd.DataFrame(arrow_rows)\n\n# V-shape arrowheads at the bottom (emission direction)\nhead_len, head_width = 0.10, 0.014\nhead_df = pd.DataFrame(\n    {\n        \"x_left\": arrow_df[\"x\"] - head_width,\n        \"x_right\": arrow_df[\"x\"] + head_width,\n        \"x_tip\": arrow_df[\"x\"].values,\n        \"y_base\": arrow_df[\"y_to\"] + head_len,\n        \"y_tip\": arrow_df[\"y_to\"].values,\n        \"color\": arrow_df[\"color\"].values,\n        \"series\": arrow_df[\"series\"].values,\n    }\n)\n\n# Wavelength annotations on the first (α) transition of each series\nalpha_arrows = arrow_df[arrow_df[\"is_alpha\"]].copy()\nalpha_arrows[\"y_mid\"] = (alpha_arrows[\"y_from\"] + alpha_arrows[\"y_to\"]) / 2\nalpha_arrows[\"x_label\"] = alpha_arrows[\"x\"] + 0.025\n# Nudge Paschen α-annotation right to clear the Balmer/Paschen crossing zone near n=3\nalpha_arrows.loc[alpha_arrows[\"series\"] == \"Paschen\", \"x_label\"] = (\n    alpha_arrows.loc[alpha_arrows[\"series\"] == \"Paschen\", \"x\"] + 0.15\n)\n\n# Manual legend on the right side — anchored near upper energy levels to fill sparse area\nlegend_labels = [\"Lyman (UV)\", \"Balmer (Visible)\", \"Paschen (IR)\"]\nlegend_colors = [lyman_color, balmer_color, paschen_color]\nlegend_df = pd.DataFrame(\n    {\n        \"x_seg\": [0.68] * 3,\n        \"xend_seg\": [0.74] * 3,\n        \"x_text\": [0.755] * 3,\n        \"y\": [-0.35, -0.70, -1.05],\n        \"label\": legend_labels,\n        \"color\": legend_colors,\n    }\n)\n\n# Ionization limit line and label\nion_df = pd.DataFrame({\"x\": [0.08], \"xend\": [0.56], \"y\": [ion_ypos], \"yend\": [ion_ypos]})\nion_label_df = pd.DataFrame({\"x\": [0.60], \"y\": [ion_ypos], \"label\": [\"0 eV (ionization)\"]})\n\n# Subtle shaded continuum band above the ionization limit\nion_band_df = pd.DataFrame({\"xmin\": [0.08], \"xmax\": [0.56], \"ymin\": [ion_ypos], \"ymax\": [ion_ypos + 0.25]})\n\n# Y-axis ticks at actual energy values (transformed positions, eV labels)\ny_breaks = y_positions + [ion_ypos]\ny_labels = [f\"{e:.1f}\" for e in energies] + [\"0.0\"]\n\n# Theme — fully adaptive chrome, no hardcoded grays\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    axis_text_x=element_blank(),\n    axis_ticks_x=element_blank(),\n    axis_title_x=element_blank(),\n    axis_line_x=element_blank(),\n    axis_text_y=element_text(size=10, color=INK_SOFT),\n    axis_title_y=element_text(size=12, color=INK),\n    axis_line_y=element_line(color=INK_SOFT, size=0.8),\n    axis_ticks_y=element_line(color=INK_SOFT),\n    plot_title=element_text(size=16, hjust=0.5, color=INK),\n    plot_subtitle=element_text(size=12, hjust=0.5, color=INK_SOFT),\n    panel_grid_major_x=element_blank(),\n    panel_grid_minor_x=element_blank(),\n    panel_grid_major_y=element_line(color=INK_SOFT, size=0.3),\n    panel_grid_minor_y=element_blank(),\n    legend_position=\"none\",\n)\n\n# Build plot\nplot = (\n    ggplot()\n    # Ionization continuum — subtle band above 0 eV\n    + geom_rect(\n        data=ion_band_df,\n        mapping=aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\"),\n        fill=INK_SOFT,\n        alpha=0.10,\n        color=PAGE_BG,\n    )\n    # Energy level horizontal lines\n    + geom_segment(\n        data=level_df,\n        mapping=aes(x=\"x_start\", xend=\"x_end\", y=\"y\", yend=\"y\"),\n        size=2.0,\n        color=INK,\n        tooltips=layer_tooltips().line(\"@label\").line(\"Energy: @energy_label\"),\n    )\n    # Quantum state labels (left of each level line)\n    + geom_text(\n        data=level_df, mapping=aes(x=\"x_start\", y=\"y\", label=\"label\"), hjust=1.3, size=5, color=INK, fontface=\"bold\"\n    )\n    # Energy value labels (right of each level line)\n    + geom_text(\n        data=level_df, mapping=aes(x=\"x_label_right\", y=\"y\", label=\"energy_label\"), hjust=0, size=4, color=INK_SOFT\n    )\n    # Ionization limit dashed line\n    + geom_segment(\n        data=ion_df, mapping=aes(x=\"x\", xend=\"xend\", y=\"y\", yend=\"yend\"), size=1.2, color=INK_SOFT, linetype=\"dashed\"\n    )\n    + geom_text(data=ion_label_df, mapping=aes(x=\"x\", y=\"y\", label=\"label\"), hjust=-0.05, size=4, color=INK_MUTED)\n)\n\n# Add transition arrows and arrowheads per series\nfor series_name, cfg in series_cfg.items():\n    s_arrows = arrow_df[arrow_df[\"series\"] == series_name]\n    s_heads = head_df[head_df[\"series\"] == series_name]\n    plot = (\n        plot\n        + geom_segment(\n            data=s_arrows,\n            mapping=aes(x=\"x\", xend=\"x\", y=\"y_from\", yend=\"y_to\", color=\"color\"),\n            size=cfg[\"size\"],\n            tooltips=layer_tooltips().line(\"@series series\").line(\"@transition\").line(\"λ = @wavelength\"),\n        )\n        + geom_segment(\n            data=s_heads,\n            mapping=aes(x=\"x_left\", xend=\"x_tip\", y=\"y_base\", yend=\"y_tip\", color=\"color\"),\n            size=cfg[\"size\"],\n        )\n        + geom_segment(\n            data=s_heads,\n            mapping=aes(x=\"x_right\", xend=\"x_tip\", y=\"y_base\", yend=\"y_tip\", color=\"color\"),\n            size=cfg[\"size\"],\n        )\n    )\n\n# Wavelength labels on the α-line of each series\nplot = plot + geom_text(\n    data=alpha_arrows,\n    mapping=aes(x=\"x_label\", y=\"y_mid\", label=\"wavelength\"),\n    hjust=0,\n    size=3.5,\n    color=INK_MUTED,\n    fontface=\"italic\",\n)\n\n# Legend, scales, and final assembly\nplot = (\n    plot\n    + scale_color_identity()\n    + geom_segment(data=legend_df, mapping=aes(x=\"x_seg\", xend=\"xend_seg\", y=\"y\", yend=\"y\", color=\"color\"), size=2.5)\n    + geom_text(data=legend_df, mapping=aes(x=\"x_text\", y=\"y\", label=\"label\", color=\"color\"), hjust=0, size=4)\n    + scale_x_continuous(limits=[-0.05, 1.05], expand=[0, 0])\n    + scale_y_continuous(breaks=y_breaks, labels=y_labels)\n    + labs(\n        x=\"\",\n        y=\"Energy (eV)\",\n        title=\"energy-level-atomic · python · letsplot · anyplot.ai\",\n        subtitle=\"Hydrogen Atom: Lyman, Balmer & Paschen Series\",\n    )\n    + ggsize(800, 450)\n    + anyplot_theme\n)\n\n# Save — PNG at 3200×1800 (scale=4 × ggsize 800×450) + HTML\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}