Skip to content

Commit 9735799

Browse files
feat(pygal): implement line-reaction-coordinate (#5148)
## Implementation: `line-reaction-coordinate` - pygal Implements the **pygal** version of `line-reaction-coordinate`. **File:** `plots/line-reaction-coordinate/implementations/pygal.py` **Parent Issue:** #4409 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/23388404498)* --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com> Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent 4518025 commit 9735799

2 files changed

Lines changed: 400 additions & 0 deletions

File tree

Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
""" pyplots.ai
2+
line-reaction-coordinate: Reaction Coordinate Energy Diagram
3+
Library: pygal 3.1.0 | Python 3.14.3
4+
Quality: 85/100 | Created: 2026-03-21
5+
"""
6+
7+
import numpy as np
8+
import pygal
9+
from pygal.style import Style
10+
11+
12+
# Data - Single-step exothermic reaction
13+
reactant_energy = 50.0 # kJ/mol
14+
transition_energy = 120.0 # kJ/mol
15+
product_energy = 20.0 # kJ/mol
16+
activation_energy = transition_energy - reactant_energy # Ea = 70 kJ/mol
17+
enthalpy_change = product_energy - reactant_energy # ΔH = -30 kJ/mol
18+
19+
# Generate smooth energy profile
20+
n_points = 300
21+
reaction_coord = np.linspace(0, 10, n_points)
22+
23+
# Gaussian barrier centered at transition state
24+
sigma = 1.2
25+
peak_pos = 5.0
26+
base_curve = reactant_energy + (transition_energy - reactant_energy) * np.exp(
27+
-0.5 * ((reaction_coord - peak_pos) / sigma) ** 2
28+
)
29+
30+
# Vectorized Hermite smoothstep transition to product energy
31+
t_raw = np.clip((reaction_coord - 6.5) / 2.0, 0.0, 1.0)
32+
smooth_t = t_raw * t_raw * (3 - 2 * t_raw)
33+
base_curve = base_curve * (1 - smooth_t) + product_energy * smooth_t
34+
35+
# Vectorized tail flattening
36+
base_curve = np.where(reaction_coord < 1.5, reactant_energy, base_curve)
37+
base_curve = np.where(reaction_coord > 8.5, product_energy, base_curve)
38+
39+
# Smooth join regions with repeated convolution
40+
kernel = np.ones(17) / 17
41+
energy_curve = base_curve.copy()
42+
for _ in range(3):
43+
padded = np.pad(energy_curve, 17, mode="edge")
44+
energy_curve = np.convolve(padded, kernel, mode="same")[17:-17]
45+
46+
curve_points = list(zip(reaction_coord.tolist(), energy_curve.tolist(), strict=True))
47+
48+
# Colorblind-safe palette
49+
BLUE = "#306998"
50+
DARK_BLUE = "#1A4971"
51+
TEAL = "#2980B9"
52+
ORANGE = "#E67E22"
53+
GRAY = "#AAAAAA"
54+
55+
# Style — y-guides at key energies serve as reference lines
56+
custom_style = Style(
57+
background="white",
58+
plot_background="#FAFBFC",
59+
foreground="#2C3E50",
60+
foreground_strong="#2C3E50",
61+
foreground_subtle="#CCCCCC",
62+
colors=(BLUE, GRAY, GRAY, TEAL, ORANGE, DARK_BLUE, BLUE, BLUE),
63+
title_font_size=60,
64+
label_font_size=40,
65+
major_label_font_size=36,
66+
legend_font_size=32,
67+
value_font_size=28,
68+
stroke_width=5,
69+
)
70+
71+
# Chart
72+
chart = pygal.XY(
73+
width=4800,
74+
height=2700,
75+
style=custom_style,
76+
title="line-reaction-coordinate \u00b7 pygal \u00b7 pyplots.ai",
77+
x_title="Reaction Coordinate",
78+
y_title="Potential Energy (kJ/mol)",
79+
show_legend=True,
80+
legend_at_bottom=True,
81+
legend_at_bottom_columns=5,
82+
show_x_guides=False,
83+
show_y_guides=True,
84+
show_x_labels=False,
85+
dots_size=0,
86+
stroke=True,
87+
margin=80,
88+
margin_left=260,
89+
margin_right=160,
90+
margin_bottom=200,
91+
margin_top=120,
92+
range=(8, 140),
93+
xrange=(-0.2, 10.2),
94+
truncate_legend=-1,
95+
tooltip_border_radius=8,
96+
)
97+
98+
# Main energy curve
99+
chart.add("Energy Profile", curve_points, stroke_style={"width": 6}, show_dots=False, fill=False)
100+
101+
# Horizontal reference lines at reactant and product energy levels
102+
chart.add(
103+
None,
104+
[(0.0, reactant_energy), (10.0, reactant_energy)],
105+
stroke_style={"width": 3, "dasharray": "16, 8"},
106+
show_dots=False,
107+
)
108+
chart.add(
109+
None,
110+
[(0.0, product_energy), (10.0, product_energy)],
111+
stroke_style={"width": 3, "dasharray": "16, 8"},
112+
show_dots=False,
113+
)
114+
115+
# Ea vertical indicator at transition state peak (teal — colorblind-safe)
116+
ea_x = peak_pos
117+
chart.add(
118+
f"Ea = {activation_energy:.0f} kJ/mol",
119+
[{"value": (ea_x, reactant_energy), "node": {"r": 14}}, {"value": (ea_x, transition_energy), "node": {"r": 14}}],
120+
stroke_style={"width": 5, "dasharray": "8, 5"},
121+
)
122+
123+
# ΔH vertical indicator (orange — colorblind-safe)
124+
dh_x = 8.5
125+
chart.add(
126+
f"\u0394H = {enthalpy_change:.0f} kJ/mol",
127+
[{"value": (dh_x, reactant_energy), "node": {"r": 14}}, {"value": (dh_x, product_energy), "node": {"r": 14}}],
128+
stroke_style={"width": 5, "dasharray": "8, 5"},
129+
)
130+
131+
# Transition state marker (larger, dark blue)
132+
chart.add(
133+
"Transition State (\u2021)",
134+
[{"value": (peak_pos, transition_energy), "node": {"r": 22}}],
135+
stroke_style={"width": 0},
136+
dots_size=22,
137+
)
138+
139+
# Reactant marker
140+
chart.add(
141+
f"Reactants ({reactant_energy:.0f} kJ/mol)",
142+
[{"value": (1.0, reactant_energy), "node": {"r": 16}}],
143+
stroke_style={"width": 0},
144+
dots_size=16,
145+
)
146+
147+
# Product marker
148+
chart.add(
149+
f"Products ({product_energy:.0f} kJ/mol)",
150+
[{"value": (9.5, product_energy), "node": {"r": 16}}],
151+
stroke_style={"width": 0},
152+
dots_size=16,
153+
)
154+
155+
# Custom y-axis labels at chemically meaningful values
156+
chart.y_labels = [
157+
{"label": f"{product_energy:.0f}", "value": product_energy},
158+
{"label": f"{reactant_energy:.0f}", "value": reactant_energy},
159+
{"label": "80", "value": 80},
160+
{"label": "100", "value": 100},
161+
{"label": f"{transition_energy:.0f}", "value": transition_energy},
162+
]
163+
164+
# Save
165+
chart.render_to_file("plot.html")
166+
chart.render_to_png("plot.png")

0 commit comments

Comments
 (0)