Skip to content

Commit 5248cb5

Browse files
feat(letsplot): implement line-reaction-coordinate (#5149)
## Implementation: `line-reaction-coordinate` - letsplot Implements the **letsplot** version of `line-reaction-coordinate`. **File:** `plots/line-reaction-coordinate/implementations/letsplot.py` **Parent Issue:** #4409 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/23388404622)* --------- 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 bf63401 commit 5248cb5

2 files changed

Lines changed: 436 additions & 0 deletions

File tree

Lines changed: 214 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,214 @@
1+
""" pyplots.ai
2+
line-reaction-coordinate: Reaction Coordinate Energy Diagram
3+
Library: letsplot 4.9.0 | Python 3.14.3
4+
Quality: 91/100 | Created: 2026-03-21
5+
"""
6+
7+
import numpy as np
8+
import pandas as pd
9+
from lets_plot import * # noqa: F403
10+
from lets_plot.export import ggsave
11+
12+
13+
LetsPlot.setup_html() # noqa: F405
14+
15+
# Data - Single-step exothermic reaction energy profile
16+
reactant_energy = 50.0
17+
transition_energy = 120.0
18+
product_energy = 20.0
19+
20+
# Build smooth energy curve using Gaussian-based profile
21+
n_points = 300
22+
reaction_coord = np.linspace(0, 1, n_points)
23+
24+
# Piecewise smooth curve: Gaussian peak on sigmoid transition
25+
peak_pos = 0.45
26+
sigma = 0.12
27+
gaussian_peak = np.exp(-0.5 * ((reaction_coord - peak_pos) / sigma) ** 2)
28+
29+
# Smooth sigmoid transition from reactant to product level
30+
transition = 1 / (1 + np.exp(-20 * (reaction_coord - 0.6)))
31+
base_energy = reactant_energy * (1 - transition) + product_energy * transition
32+
33+
# Add the activation barrier
34+
barrier_height = transition_energy - (
35+
reactant_energy * (1 - 1 / (1 + np.exp(-20 * (peak_pos - 0.6))))
36+
+ product_energy * (1 / (1 + np.exp(-20 * (peak_pos - 0.6))))
37+
)
38+
energy = base_energy + barrier_height * gaussian_peak
39+
40+
# Flatten plateaus at start and end
41+
energy[reaction_coord < 0.1] = reactant_energy
42+
energy[reaction_coord > 0.9] = product_energy
43+
44+
df = pd.DataFrame({"reaction_coordinate": reaction_coord, "energy": energy})
45+
46+
# Key values
47+
ea = transition_energy - reactant_energy
48+
delta_h = product_energy - reactant_energy
49+
50+
# Shaded region under curve for visual richness
51+
area_df = df.copy()
52+
53+
# Colorblind-safe palette: blue (#306998) and orange (#E67E22) instead of red-green
54+
ea_color = "#D35400" # deep orange for Ea
55+
dh_color = "#2471A3" # steel blue for ΔH
56+
curve_color = "#306998" # Python blue
57+
label_color = "#2C3E50" # dark slate
58+
59+
# Horizontal reference lines at energy levels
60+
hline_df = pd.DataFrame(
61+
{
62+
"x": [0.0, 0.0],
63+
"xend": [1.0, 1.0],
64+
"y": [reactant_energy, product_energy],
65+
"yend": [reactant_energy, product_energy],
66+
}
67+
)
68+
69+
# Ea arrow segments (double-headed)
70+
ea_arrow_df = pd.DataFrame(
71+
{
72+
"x": [0.20, 0.20],
73+
"y": [reactant_energy + 2, transition_energy - 2],
74+
"xend": [0.20, 0.20],
75+
"yend": [transition_energy - 2, reactant_energy + 2],
76+
}
77+
)
78+
79+
# ΔH arrow segments (double-headed)
80+
dh_arrow_df = pd.DataFrame(
81+
{
82+
"x": [0.80, 0.80],
83+
"y": [reactant_energy - 2, product_energy + 2],
84+
"xend": [0.80, 0.80],
85+
"yend": [product_energy + 2, reactant_energy - 2],
86+
}
87+
)
88+
89+
# Build plot with lets-plot distinctive features
90+
plot = (
91+
ggplot(df, aes(x="reaction_coordinate", y="energy")) # noqa: F405
92+
# Shaded area under the energy curve using geom_area
93+
+ geom_area(fill=curve_color, alpha=0.08) # noqa: F405
94+
# Horizontal dashed reference lines
95+
+ geom_segment( # noqa: F405
96+
data=hline_df,
97+
mapping=aes(x="x", xend="xend", y="y", yend="yend"), # noqa: F405
98+
linetype="dashed",
99+
color="#B0B0B0",
100+
size=0.7,
101+
)
102+
# Main energy curve - prominent
103+
+ geom_line(color=curve_color, size=2.5) # noqa: F405
104+
# Ea double-headed arrow (orange - colorblind safe)
105+
+ geom_segment( # noqa: F405
106+
data=ea_arrow_df,
107+
mapping=aes(x="x", xend="xend", y="y", yend="yend"), # noqa: F405
108+
color=ea_color,
109+
size=1.3,
110+
arrow=arrow(length=10, type="open"), # noqa: F405
111+
)
112+
# ΔH double-headed arrow (steel blue - colorblind safe)
113+
+ geom_segment( # noqa: F405
114+
data=dh_arrow_df,
115+
mapping=aes(x="x", xend="xend", y="y", yend="yend"), # noqa: F405
116+
color=dh_color,
117+
size=1.3,
118+
arrow=arrow(length=10, type="open"), # noqa: F405
119+
)
120+
# Labels using geom_label (lets-plot distinctive: label_padding, label_r)
121+
+ geom_label( # noqa: F405
122+
data=pd.DataFrame({"x": [0.08], "y": [reactant_energy - 8], "label": ["Reactants\n50 kJ/mol"]}),
123+
mapping=aes(x="x", y="y", label="label"), # noqa: F405
124+
size=15,
125+
color=label_color,
126+
fill="#F8F9FA",
127+
alpha=0.85,
128+
label_padding=0.4,
129+
label_r=0.3,
130+
label_size=0,
131+
)
132+
+ geom_label( # noqa: F405
133+
data=pd.DataFrame({"x": [peak_pos], "y": [transition_energy + 8], "label": ["Transition State\n120 kJ/mol"]}),
134+
mapping=aes(x="x", y="y", label="label"), # noqa: F405
135+
size=15,
136+
color=label_color,
137+
fill="#F8F9FA",
138+
alpha=0.85,
139+
label_padding=0.4,
140+
label_r=0.3,
141+
label_size=0,
142+
)
143+
+ geom_label( # noqa: F405
144+
data=pd.DataFrame({"x": [0.92], "y": [product_energy - 8], "label": ["Products\n20 kJ/mol"]}),
145+
mapping=aes(x="x", y="y", label="label"), # noqa: F405
146+
size=15,
147+
color=label_color,
148+
fill="#F8F9FA",
149+
alpha=0.85,
150+
label_padding=0.4,
151+
label_r=0.3,
152+
label_size=0,
153+
)
154+
# Energy annotation labels with colored backgrounds matching their arrows
155+
+ geom_label( # noqa: F405
156+
data=pd.DataFrame(
157+
{"x": [0.20], "y": [(reactant_energy + transition_energy) / 2], "label": [f"Ea = {ea:.0f} kJ/mol"]}
158+
),
159+
mapping=aes(x="x", y="y", label="label"), # noqa: F405
160+
size=16,
161+
color="#FFFFFF",
162+
fill=ea_color,
163+
alpha=0.9,
164+
label_padding=0.5,
165+
label_r=0.3,
166+
label_size=0,
167+
fontface="bold",
168+
)
169+
+ geom_label( # noqa: F405
170+
data=pd.DataFrame(
171+
{"x": [0.80], "y": [(reactant_energy + product_energy) / 2], "label": [f"ΔH = {delta_h:.0f} kJ/mol"]}
172+
),
173+
mapping=aes(x="x", y="y", label="label"), # noqa: F405
174+
size=16,
175+
color="#FFFFFF",
176+
fill=dh_color,
177+
alpha=0.9,
178+
label_padding=0.5,
179+
label_r=0.3,
180+
label_size=0,
181+
fontface="bold",
182+
)
183+
# Scales
184+
+ scale_x_continuous( # noqa: F405
185+
name="Reaction Coordinate", breaks=[], expand=[0.02, 0.02]
186+
)
187+
+ scale_y_continuous( # noqa: F405
188+
name="Potential Energy (kJ/mol)", limits=[0, 145]
189+
)
190+
+ labs(title="line-reaction-coordinate · letsplot · pyplots.ai") # noqa: F405
191+
+ coord_cartesian(ylim=[0, 145]) # noqa: F405
192+
+ ggsize(1600, 900) # noqa: F405
193+
# Lets-plot distinctive: flavor for base styling + element_geom for global defaults
194+
+ flavor_high_contrast_light() # noqa: F405
195+
+ theme( # noqa: F405
196+
axis_text=element_text(size=16, color="#555555"), # noqa: F405
197+
axis_title=element_text(size=20, color="#333333"), # noqa: F405
198+
plot_title=element_text(size=24, hjust=0.5, color="#2C3E50", face="bold"), # noqa: F405
199+
axis_text_x=element_blank(), # noqa: F405
200+
axis_ticks_x=element_blank(), # noqa: F405
201+
axis_line_x=element_line(color="#CCCCCC", size=0.6), # noqa: F405
202+
axis_line_y=element_line(color="#CCCCCC", size=0.6), # noqa: F405
203+
panel_grid_major_y=element_line(color="#EEEEEE", size=0.3), # noqa: F405
204+
panel_grid_minor=element_blank(), # noqa: F405
205+
panel_grid_major_x=element_blank(), # noqa: F405
206+
legend_position="none",
207+
plot_background=element_rect(fill="#FFFFFF", color="#FFFFFF"), # noqa: F405
208+
panel_background=element_rect(fill="#FAFBFC", color="#FAFBFC"), # noqa: F405
209+
)
210+
)
211+
212+
# Save
213+
ggsave(plot, filename="plot.png", path=".", scale=3)
214+
ggsave(plot, filename="plot.html", path=".")

0 commit comments

Comments
 (0)