{"spec_id":"line-reaction-coordinate","library":"altair","language":"python","code":"\"\"\" anyplot.ai\nline-reaction-coordinate: Reaction Coordinate Energy Diagram\nLibrary: altair 6.2.2 | Python 3.13.14\nQuality: 93/100 | Updated: 2026-06-24\n\"\"\"\n\nimport os\n\nimport altair as alt\nimport numpy as np\nimport pandas as pd\nfrom PIL import Image\n\n\n# Theme tokens\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 palette\ncurve_color = \"#009E73\"  # brand green — first Imprint position\nea_color = \"#AE3030\"  # matte red — semantic for activation barrier/cost\ndh_color = \"#4467A3\"  # blue — third Imprint position\n\n# Data — single-step exothermic reaction\nreactant_energy = 50.0  # kJ/mol\ntransition_energy = 120.0  # kJ/mol\nproduct_energy = 20.0  # kJ/mol\n\nreaction_coord = np.linspace(0, 1, 300)\npeak_pos = 0.40\nsigma = 0.11\n\nbaseline = reactant_energy + (product_energy - reactant_energy) / (1 + np.exp(-14 * (reaction_coord - 0.55)))\ngaussian = np.exp(-((reaction_coord - peak_pos) ** 2) / (2 * sigma**2))\nbaseline_at_peak = reactant_energy + (product_energy - reactant_energy) / (1 + np.exp(-14 * (peak_pos - 0.55)))\nbump_height = transition_energy - baseline_at_peak\nenergy = baseline + bump_height * gaussian\n\ncurve_df = pd.DataFrame({\"x\": reaction_coord, \"y\": energy})\npeak_idx = int(np.argmax(energy))\nactual_peak_energy = float(energy[peak_idx])\npeak_x = float(reaction_coord[peak_idx])\nea_value = actual_peak_energy - reactant_energy\ndh_value = reactant_energy - product_energy\n\nea_x = 0.16\ndh_x = 0.82\n\n# Reference line data (dashed horizontal lines at each energy level)\nhlines_df = pd.DataFrame(\n    {\n        \"x\": [0.0, ea_x, ea_x, peak_x + 0.06, 0.68, 1.0],\n        \"y\": [reactant_energy, reactant_energy, actual_peak_energy, actual_peak_energy, product_energy, product_energy],\n        \"group\": [\"reactant\", \"reactant\", \"ts\", \"ts\", \"product\", \"product\"],\n    }\n)\n\n# Shared axis scales\nx_scale = alt.Scale(domain=[-0.04, 1.06], nice=False)\ny_scale = alt.Scale(domain=[0, 135])\n\n# Energy curve\ncurve = (\n    alt.Chart(curve_df)\n    .mark_line(strokeWidth=4, color=curve_color)\n    .encode(\n        x=alt.X(\"x:Q\", scale=x_scale, title=\"Reaction Coordinate\"),\n        y=alt.Y(\"y:Q\", scale=y_scale, title=\"Energy (kJ/mol)\"),\n        tooltip=[\n            alt.Tooltip(\"x:Q\", title=\"Reaction Coordinate\", format=\".2f\"),\n            alt.Tooltip(\"y:Q\", title=\"Energy (kJ/mol)\", format=\".1f\"),\n        ],\n    )\n)\n\n# Dashed reference lines at each energy level\nhlines = (\n    alt.Chart(hlines_df)\n    .mark_line(strokeWidth=1.2, strokeDash=[8, 6], color=INK_SOFT, opacity=0.45)\n    .encode(x=alt.X(\"x:Q\", scale=x_scale), y=alt.Y(\"y:Q\", scale=y_scale), detail=\"group:N\")\n)\n\n# Activation energy (Ea) double-headed arrow\nea_line = (\n    alt.Chart(pd.DataFrame({\"x\": [ea_x, ea_x], \"y\": [reactant_energy, actual_peak_energy]}))\n    .mark_line(strokeWidth=2.5, color=ea_color)\n    .encode(x=alt.X(\"x:Q\", scale=x_scale), y=alt.Y(\"y:Q\", scale=y_scale))\n)\nea_heads = (\n    alt.Chart(\n        pd.DataFrame(\n            {\"x\": [ea_x, ea_x], \"y\": [actual_peak_energy, reactant_energy], \"shape\": [\"triangle-up\", \"triangle-down\"]}\n        )\n    )\n    .mark_point(filled=True, size=250, color=ea_color)\n    .encode(\n        x=alt.X(\"x:Q\", scale=x_scale),\n        y=alt.Y(\"y:Q\", scale=y_scale),\n        shape=alt.Shape(\"shape:N\", scale=alt.Scale(range=[\"triangle-up\", \"triangle-down\"]), legend=None),\n    )\n)\n\n# Enthalpy change (ΔH) double-headed arrow\ndh_line = (\n    alt.Chart(pd.DataFrame({\"x\": [dh_x, dh_x], \"y\": [product_energy, reactant_energy]}))\n    .mark_line(strokeWidth=2.5, color=dh_color)\n    .encode(x=alt.X(\"x:Q\", scale=x_scale), y=alt.Y(\"y:Q\", scale=y_scale))\n)\ndh_heads = (\n    alt.Chart(\n        pd.DataFrame(\n            {\"x\": [dh_x, dh_x], \"y\": [reactant_energy, product_energy], \"shape\": [\"triangle-up\", \"triangle-down\"]}\n        )\n    )\n    .mark_point(filled=True, size=250, color=dh_color)\n    .encode(\n        x=alt.X(\"x:Q\", scale=x_scale),\n        y=alt.Y(\"y:Q\", scale=y_scale),\n        shape=alt.Shape(\"shape:N\", scale=alt.Scale(range=[\"triangle-up\", \"triangle-down\"]), legend=None),\n    )\n)\n\n# Species labels\nspecies_df = pd.DataFrame(\n    {\n        \"x\": [0.07, peak_x, 0.93],\n        \"y\": [reactant_energy - 9, actual_peak_energy + 5, product_energy - 9],\n        \"text\": [\"Reactants\", \"Transition State ‡\", \"Products\"],\n    }\n)\nspecies_labels = (\n    alt.Chart(species_df)\n    .mark_text(fontSize=15, fontWeight=\"bold\", color=INK)\n    .encode(x=alt.X(\"x:Q\", scale=x_scale), y=alt.Y(\"y:Q\", scale=y_scale), text=\"text:N\")\n)\n\n# Arrow value labels (Ea and ΔH magnitudes)\narrow_labels_df = pd.DataFrame(\n    {\n        \"x\": [ea_x + 0.035, dh_x + 0.04],\n        \"y\": [(reactant_energy + actual_peak_energy) / 2, (reactant_energy + product_energy) / 2],\n        \"text\": [f\"Ea = {ea_value:.0f} kJ/mol\", f\"ΔH = −{dh_value:.0f} kJ/mol\"],\n        \"color\": [ea_color, dh_color],\n    }\n)\narrow_labels = (\n    alt.Chart(arrow_labels_df)\n    .mark_text(fontSize=13, fontWeight=\"bold\", align=\"left\")\n    .encode(\n        x=alt.X(\"x:Q\", scale=x_scale),\n        y=alt.Y(\"y:Q\", scale=y_scale),\n        text=\"text:N\",\n        color=alt.Color(\"color:N\", scale=None),\n    )\n)\n\n# Energy level value annotations (muted, italic)\nenergy_vals_df = pd.DataFrame(\n    {\n        \"x\": [0.02, 0.97, peak_x + 0.22],\n        \"y\": [reactant_energy + 6, product_energy + 6, actual_peak_energy + 4],\n        \"text\": [f\"{reactant_energy:.0f} kJ/mol\", f\"{product_energy:.0f} kJ/mol\", f\"{actual_peak_energy:.0f} kJ/mol\"],\n    }\n)\nenergy_vals = (\n    alt.Chart(energy_vals_df)\n    .mark_text(fontSize=12, fontStyle=\"italic\", color=INK_MUTED)\n    .encode(x=alt.X(\"x:Q\", scale=x_scale), y=alt.Y(\"y:Q\", scale=y_scale), text=\"text:N\")\n)\n\n# Title (55 chars < 67 baseline, no scaling needed)\ntitle_str = \"line-reaction-coordinate · python · altair · anyplot.ai\"\n\n# Combine all layers and apply theme-adaptive chrome\nchart = (\n    alt.layer(hlines, curve, ea_line, ea_heads, dh_line, dh_heads, species_labels, arrow_labels, energy_vals)\n    .properties(\n        background=PAGE_BG,\n        width=620,\n        height=320,\n        title=alt.Title(\n            title_str,\n            fontSize=16,\n            anchor=\"middle\",\n            color=INK,\n            subtitle=\"Exothermic Reaction · Single-Step Energy Profile\",\n            subtitleFontSize=12,\n            subtitleColor=INK_SOFT,\n            subtitlePadding=6,\n        ),\n    )\n    .configure_view(fill=PAGE_BG, strokeWidth=0)\n    .configure_axis(\n        labelFontSize=10,\n        titleFontSize=12,\n        titleColor=INK,\n        labelColor=INK_SOFT,\n        domainColor=INK_SOFT,\n        tickColor=INK_SOFT,\n        tickSize=4,\n        titlePadding=10,\n        grid=False,\n    )\n    .configure_title(color=INK)\n    .interactive()\n)\n\n# Save — pad PNG to exact 3200×1800 target\nTW, TH = 3200, 1800\n\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\nchart.save(f\"plot-{THEME}.html\")\n"}