{"spec_id":"line-reaction-coordinate","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nline-reaction-coordinate: Reaction Coordinate Energy Diagram\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 87/100 | Updated: 2026-06-24\n\"\"\"\n\nimport os\nimport re\nimport sys\n\n\n# Script filename shadows the installed 'pygal' package when run as 'python pygal.py';\n# dropping the script directory from sys.path lets the real package resolve.\nsys.path.pop(0)\n\nimport cairosvg\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\n\n\n# Theme tokens — Imprint palette chrome\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Data — single-step exothermic reaction\nreactant_energy = 50.0\ntransition_energy = 120.0\nproduct_energy = 20.0\nactivation_energy = transition_energy - reactant_energy  # Ea = 70 kJ/mol\nenthalpy_change = product_energy - reactant_energy  # ΔH = -30 kJ/mol\n\n# Generate smooth energy profile\nn_points = 300\nreaction_coord = np.linspace(0, 10, n_points)\n\nsigma = 1.2\npeak_pos = 5.0\nbase_curve = reactant_energy + (transition_energy - reactant_energy) * np.exp(\n    -0.5 * ((reaction_coord - peak_pos) / sigma) ** 2\n)\n\nt_raw = np.clip((reaction_coord - 6.5) / 2.0, 0.0, 1.0)\nsmooth_t = t_raw * t_raw * (3 - 2 * t_raw)\nbase_curve = base_curve * (1 - smooth_t) + product_energy * smooth_t\n\nbase_curve = np.where(reaction_coord < 1.5, reactant_energy, base_curve)\nbase_curve = np.where(reaction_coord > 8.5, product_energy, base_curve)\n\nkernel = np.ones(17) / 17\nenergy_curve = base_curve.copy()\nfor _ in range(3):\n    padded = np.pad(energy_curve, 17, mode=\"edge\")\n    energy_curve = np.convolve(padded, kernel, mode=\"same\")[17:-17]\n\ncurve_points = list(zip(reaction_coord.tolist(), energy_curve.tolist(), strict=True))\n\n# Series colors: Imprint palette positions assigned by role\nEA_COLOR = \"#2ABCCD\"  # Ea indicator — Imprint pos 6 (cyan/teal)\nDH_COLOR = \"#BD8233\"  # ΔH indicator — Imprint pos 4 (ochre/warm)\nSERIES_COLORS = (\n    \"#009E73\",  # Energy Profile — Imprint pos 1 (brand green)\n    INK_MUTED,  # Reference line at reactant level (theme-adaptive muted)\n    INK_MUTED,  # Reference line at product level (theme-adaptive muted)\n    EA_COLOR,  # Ea indicator\n    DH_COLOR,  # ΔH indicator\n    \"#C475FD\",  # Transition State — Imprint pos 2 (lavender)\n    \"#4467A3\",  # Reactants — Imprint pos 3 (blue)\n    \"#AE3030\",  # Products — Imprint pos 5 (matte red)\n)\n\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=SERIES_COLORS,\n    title_font_size=66,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    stroke_width=2.5,\n)\n\nchart = pygal.XY(\n    width=3200,\n    height=1800,\n    style=custom_style,\n    title=\"line-reaction-coordinate · python · pygal · anyplot.ai\",\n    x_title=\"Reaction Coordinate\",\n    y_title=\"Potential Energy (kJ/mol)\",\n    show_legend=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=3,\n    show_x_guides=False,\n    show_y_guides=True,\n    show_x_labels=False,\n    dots_size=0,\n    stroke=True,\n    margin=60,\n    margin_left=200,\n    margin_right=120,\n    margin_bottom=200,\n    margin_top=80,\n    range=(5, 130),\n    xrange=(-0.2, 10.2),\n    truncate_legend=-1,\n    tooltip_border_radius=8,\n)\n\n# Main energy curve — Imprint pos 1 (brand green)\nchart.add(\"Energy Profile\", curve_points, stroke_style={\"width\": 6}, show_dots=False, fill=False)\n\n# Horizontal reference lines at key energy levels\nchart.add(\n    None,\n    [(0.0, reactant_energy), (10.0, reactant_energy)],\n    stroke_style={\"width\": 3, \"dasharray\": \"16, 8\"},\n    show_dots=False,\n)\nchart.add(\n    None,\n    [(0.0, product_energy), (10.0, product_energy)],\n    stroke_style={\"width\": 3, \"dasharray\": \"16, 8\"},\n    show_dots=False,\n)\n\n# Ea and ΔH vertical indicators — no endpoint circles; arrowheads injected via SVG\nea_x = peak_pos\nchart.add(\n    f\"Ea = {activation_energy:.0f} kJ/mol\",\n    [(ea_x, reactant_energy), (ea_x, transition_energy)],\n    stroke_style={\"width\": 5, \"dasharray\": \"8, 5\"},\n    show_dots=False,\n)\n\ndh_x = 8.5\nchart.add(\n    f\"ΔH = {enthalpy_change:.0f} kJ/mol\",\n    [(dh_x, reactant_energy), (dh_x, product_energy)],\n    stroke_style={\"width\": 5, \"dasharray\": \"8, 5\"},\n    show_dots=False,\n)\n\n# Key markers\nchart.add(\n    \"Transition State (‡)\",\n    [{\"value\": (peak_pos, transition_energy), \"node\": {\"r\": 22}}],\n    stroke_style={\"width\": 0},\n    dots_size=22,\n)\nchart.add(\n    f\"Reactants ({reactant_energy:.0f} kJ/mol)\",\n    [{\"value\": (1.0, reactant_energy), \"node\": {\"r\": 16}}],\n    stroke_style={\"width\": 0},\n    dots_size=16,\n)\nchart.add(\n    f\"Products ({product_energy:.0f} kJ/mol)\",\n    [{\"value\": (9.5, product_energy), \"node\": {\"r\": 16}}],\n    stroke_style={\"width\": 0},\n    dots_size=16,\n)\n\n# Custom y-axis labels at chemically meaningful energy values\nchart.y_labels = [\n    {\"label\": f\"{product_energy:.0f}\", \"value\": product_energy},\n    {\"label\": f\"{reactant_energy:.0f}\", \"value\": reactant_energy},\n    {\"label\": \"80\", \"value\": 80},\n    {\"label\": \"100\", \"value\": 100},\n    {\"label\": f\"{transition_energy:.0f}\", \"value\": transition_energy},\n]\n\n# --- SVG post-processing: inject text labels and arrowheads ---\nsvg_bytes = chart.render()\nsvg_str = svg_bytes.decode(\"utf-8\")\n\n\ndef find_plot_transform(svg):\n    \"\"\"Return (tx, ty) translation of the main plot <g> element.\"\"\"\n    m = re.search(r'transform=\"translate\\(([\\d.]+),\\s*([\\d.]+)\\)\"\\s+class=\"plot\"', svg)\n    if m:\n        return float(m.group(1)), float(m.group(2))\n    return 0.0, 0.0\n\n\ndef find_circle_with_r(svg, r_str, skip=0):\n    \"\"\"Return (cx, cy) of the (skip+1)-th circle with r=r_str (local plot coords).\n    Handles any attribute order inside the <circle> element.\"\"\"\n    count = 0\n    for m in re.finditer(r\"<circle\\b[^>]*/>\", svg):\n        elem = m.group(0)\n        if not re.search(rf'\\br=\"{re.escape(r_str)}\"', elem):\n            continue\n        cx_m = re.search(r'\\bcx=\"([\\d.eE+-]+)\"', elem)\n        cy_m = re.search(r'\\bcy=\"([\\d.eE+-]+)\"', elem)\n        if cx_m and cy_m:\n            if count == skip:\n                return float(cx_m.group(1)), float(cy_m.group(1))\n            count += 1\n    return None, None\n\n\n# Circles are in the plot's local coordinate system (inside a translate group).\n# Find the plot transform so we can convert to absolute SVG coordinates.\nplot_tx, plot_ty = find_plot_transform(svg_str)\n\n# TS marker has r=22 (unique); Reactants/Products both use r=16\n# Reactants (serie-6) comes before Products (serie-7) in the SVG, so skip=0 → Reactants.\nts_lx, ts_ly = find_circle_with_r(svg_str, \"22\")\nre_lx, re_ly = find_circle_with_r(svg_str, \"16\", skip=0)\n\n# Convert local plot coords → absolute SVG coords\nts_sx = ts_lx + plot_tx if ts_lx is not None else None\nts_sy = ts_ly + plot_ty if ts_ly is not None else None\nre_sx = re_lx + plot_tx if re_lx is not None else None\nre_sy = re_ly + plot_ty if re_ly is not None else None\n\nextra_svg = \"\"\n\nif ts_sx is not None and re_sx is not None:\n    # Derive linear data → SVG coordinate transform from two known points\n    scale_x = (ts_sx - re_sx) / (5.0 - 1.0)\n    offset_x = ts_sx - scale_x * 5.0\n    scale_y = (ts_sy - re_sy) / (120.0 - 50.0)\n    offset_y = ts_sy - scale_y * 120.0\n\n    def xy(dx, dy):\n        return scale_x * dx + offset_x, scale_y * dy + offset_y\n\n    # Direct on-plot text labels\n    ts_ax, ts_ay = xy(peak_pos, transition_energy)\n    re_ax, re_ay = xy(1.0, reactant_energy)\n    pr_ax, pr_ay = xy(9.5, product_energy)\n    fs = 46\n\n    extra_svg += (\n        f'<g class=\"text-annotations\" font-family=\"sans-serif\" font-size=\"{fs}\"'\n        f' font-weight=\"bold\" fill=\"{INK}\">'\n        f'<text x=\"{ts_ax:.1f}\" y=\"{ts_ay - 35:.1f}\" text-anchor=\"middle\">'\n        f\"Transition State (‡)</text>\"\n        f'<text x=\"{re_ax + 30:.1f}\" y=\"{re_ay - 30:.1f}\" text-anchor=\"start\">'\n        f\"Reactants</text>\"\n        f'<text x=\"{pr_ax - 30:.1f}\" y=\"{pr_ay - 30:.1f}\" text-anchor=\"end\">'\n        f\"Products</text>\"\n        f\"</g>\"\n    )\n\n    # Arrowhead triangles for Ea and ΔH double-headed arrows\n    aw, ah = 18, 32  # half-width and height of each triangle\n\n    def tri_up(x, y, color):\n        \"\"\"Triangle tip pointing up (screen-up = decreasing SVG y).\"\"\"\n        return f'<polygon points=\"{x:.1f},{y:.1f} {x - aw:.1f},{y + ah:.1f} {x + aw:.1f},{y + ah:.1f}\" fill=\"{color}\"/>'\n\n    def tri_down(x, y, color):\n        \"\"\"Triangle tip pointing down (screen-down = increasing SVG y).\"\"\"\n        return f'<polygon points=\"{x:.1f},{y:.1f} {x - aw:.1f},{y - ah:.1f} {x + aw:.1f},{y - ah:.1f}\" fill=\"{color}\"/>'\n\n    # Ea: from reactant level (lower on screen) up to TS (higher on screen)\n    ea_bx, ea_by = xy(ea_x, reactant_energy)\n    ea_tx, ea_ty = xy(ea_x, transition_energy)\n    # ΔH: from product level (lower on screen) up to reactant level (higher on screen)\n    dh_bx, dh_by = xy(dh_x, product_energy)\n    dh_tx, dh_ty = xy(dh_x, reactant_energy)\n\n    extra_svg += (\n        f'<g class=\"arrowheads\">'\n        f\"{tri_up(ea_tx, ea_ty, EA_COLOR)}\"\n        f\"{tri_down(ea_bx, ea_by, EA_COLOR)}\"\n        f\"{tri_up(dh_tx, dh_ty, DH_COLOR)}\"\n        f\"{tri_down(dh_bx, dh_by, DH_COLOR)}\"\n        f\"</g>\"\n    )\n\nmodified_svg_str = svg_str.replace(\"</svg>\", extra_svg + \"\\n</svg>\", 1)\nmodified_svg_bytes = modified_svg_str.encode(\"utf-8\")\n\n# Save\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(modified_svg_bytes)\n\ncairosvg.svg2png(bytestring=modified_svg_bytes, write_to=f\"plot-{THEME}.png\")\n"}