Skip to content

Commit 2d627f1

Browse files
feat(highcharts): implement line-reaction-coordinate (#5157)
## Implementation: `line-reaction-coordinate` - highcharts Implements the **highcharts** version of `line-reaction-coordinate`. **File:** `plots/line-reaction-coordinate/implementations/highcharts.py` **Parent Issue:** #4409 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/23388404577)* --------- 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 6ed8da5 commit 2d627f1

2 files changed

Lines changed: 504 additions & 0 deletions

File tree

Lines changed: 280 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,280 @@
1+
""" pyplots.ai
2+
line-reaction-coordinate: Reaction Coordinate Energy Diagram
3+
Library: highcharts unknown | Python 3.14.3
4+
Quality: 90/100 | Created: 2026-03-21
5+
"""
6+
7+
import tempfile
8+
import time
9+
import urllib.request
10+
from pathlib import Path
11+
12+
import numpy as np
13+
from highcharts_core.chart import Chart
14+
from highcharts_core.options import HighchartsOptions
15+
from highcharts_core.options.annotations import Annotation
16+
from highcharts_core.options.series.spline import SplineSeries
17+
from selenium import webdriver
18+
from selenium.webdriver.chrome.options import Options
19+
20+
21+
# Data - Single-step exothermic reaction energy profile
22+
reactant_energy = 50.0 # kJ/mol
23+
product_energy = 20.0 # kJ/mol
24+
transition_state_energy = 120.0 # kJ/mol
25+
26+
activation_energy = transition_state_energy - reactant_energy # Ea = 70 kJ/mol
27+
delta_h = product_energy - reactant_energy # ΔH = -30 kJ/mol
28+
29+
# Generate smooth reaction coordinate curve using Hermite interpolation
30+
reaction_coord = np.linspace(0, 1, 300)
31+
energy = np.full_like(reaction_coord, reactant_energy)
32+
33+
plateau_end = 0.15
34+
rise_end = 0.45
35+
fall_end = 0.85
36+
37+
for i, rc in enumerate(reaction_coord):
38+
if rc <= plateau_end:
39+
energy[i] = reactant_energy
40+
elif rc <= rise_end:
41+
t = (rc - plateau_end) / (rise_end - plateau_end)
42+
t_smooth = t * t * (3 - 2 * t)
43+
energy[i] = reactant_energy + (transition_state_energy - reactant_energy) * t_smooth
44+
elif rc <= fall_end:
45+
t = (rc - rise_end) / (fall_end - rise_end)
46+
t_smooth = t * t * (3 - 2 * t)
47+
energy[i] = transition_state_energy - (transition_state_energy - product_energy) * t_smooth
48+
else:
49+
energy[i] = product_energy
50+
51+
ts_idx = np.argmax(energy)
52+
ts_rc = float(reaction_coord[ts_idx])
53+
ts_energy = float(energy[ts_idx])
54+
55+
ea_arrow_x = 0.20
56+
dh_arrow_x = 0.80
57+
ea_mid_y = (reactant_energy + ts_energy) / 2
58+
dh_mid_y = (reactant_energy + product_energy) / 2
59+
60+
# Chart configuration
61+
chart = Chart(container="container")
62+
chart.options = HighchartsOptions()
63+
64+
chart.options.chart = {
65+
"width": 4800,
66+
"height": 2700,
67+
"backgroundColor": "#fafbfc",
68+
"spacingTop": 60,
69+
"spacingBottom": 60,
70+
"spacingLeft": 80,
71+
"spacingRight": 100,
72+
"style": {"fontFamily": "'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"},
73+
}
74+
75+
chart.options.title = {
76+
"text": "line-reaction-coordinate \u00b7 highcharts \u00b7 pyplots.ai",
77+
"style": {"fontSize": "48px", "fontWeight": "600", "color": "#1a1a2e"},
78+
"margin": 40,
79+
}
80+
81+
chart.options.x_axis = {
82+
"title": {
83+
"text": "Reaction Coordinate",
84+
"style": {"fontSize": "38px", "fontWeight": "600", "color": "#333333"},
85+
"margin": 20,
86+
},
87+
"labels": {"enabled": False},
88+
"min": -0.05,
89+
"max": 1.05,
90+
"lineColor": "#666666",
91+
"lineWidth": 2,
92+
"tickLength": 0,
93+
"gridLineWidth": 0,
94+
}
95+
96+
chart.options.y_axis = {
97+
"title": {
98+
"text": "Potential Energy (kJ/mol)",
99+
"style": {"fontSize": "36px", "fontWeight": "600", "color": "#333333"},
100+
"margin": 20,
101+
},
102+
"labels": {"style": {"fontSize": "28px", "color": "#444444"}},
103+
"min": 0,
104+
"max": 140,
105+
"startOnTick": False,
106+
"endOnTick": False,
107+
"gridLineWidth": 0,
108+
"lineColor": "#666666",
109+
"lineWidth": 2,
110+
"tickColor": "#666666",
111+
"plotLines": [
112+
{"value": reactant_energy, "color": "#bbbbbb", "width": 2, "dashStyle": "Dash", "zIndex": 1},
113+
{"value": product_energy, "color": "#bbbbbb", "width": 2, "dashStyle": "Dash", "zIndex": 1},
114+
],
115+
}
116+
117+
chart.options.legend = {"enabled": False}
118+
chart.options.credits = {"enabled": False}
119+
chart.options.tooltip = {"enabled": False}
120+
121+
chart.options.plot_options = {
122+
"spline": {"lineWidth": 5, "marker": {"enabled": False}, "states": {"hover": {"lineWidth": 6}}},
123+
"line": {"marker": {"enabled": False}, "enableMouseTracking": False, "states": {"hover": {"lineWidth": 5}}},
124+
}
125+
126+
# Energy curve series
127+
curve_data = [[float(rc), float(e)] for rc, e in zip(reaction_coord, energy, strict=False)]
128+
energy_series = SplineSeries()
129+
energy_series.data = curve_data
130+
energy_series.name = "Energy Profile"
131+
energy_series.color = "#306998"
132+
energy_series.line_width = 5
133+
chart.add_series(energy_series)
134+
135+
# Ea arrow as a spline series with triangle markers for arrowheads
136+
ea_arrow = SplineSeries()
137+
ea_arrow.data = [
138+
{
139+
"x": ea_arrow_x,
140+
"y": reactant_energy,
141+
"marker": {"enabled": True, "symbol": "triangle", "radius": 10, "rotation": 180},
142+
},
143+
{"x": ea_arrow_x, "y": ts_energy, "marker": {"enabled": True, "symbol": "triangle", "radius": 10}},
144+
]
145+
ea_arrow.name = "Ea Arrow"
146+
ea_arrow.color = "#d35400"
147+
ea_arrow.line_width = 4
148+
ea_arrow.show_in_legend = False
149+
chart.add_series(ea_arrow)
150+
151+
# ΔH arrow as a spline series with triangle markers for arrowheads
152+
dh_arrow = SplineSeries()
153+
dh_arrow.data = [
154+
{
155+
"x": dh_arrow_x,
156+
"y": product_energy,
157+
"marker": {"enabled": True, "symbol": "triangle", "radius": 10, "rotation": 180},
158+
},
159+
{"x": dh_arrow_x, "y": reactant_energy, "marker": {"enabled": True, "symbol": "triangle", "radius": 10}},
160+
]
161+
dh_arrow.name = "dH Arrow"
162+
dh_arrow.color = "#2980b9"
163+
dh_arrow.line_width = 4
164+
dh_arrow.show_in_legend = False
165+
chart.add_series(dh_arrow)
166+
167+
# Annotations for labels via proper Highcharts-core API
168+
chart.options.annotations = [
169+
Annotation.from_dict(
170+
{
171+
"draggable": "",
172+
"labelOptions": {"allowOverlap": True, "overflow": "none", "crop": False},
173+
"labels": [
174+
{
175+
"point": {"xAxis": 0, "yAxis": 0, "x": ts_rc, "y": ts_energy},
176+
"text": "Transition State (\u2021)",
177+
"style": {"fontSize": "32px", "fontWeight": "700", "color": "#c0392b"},
178+
"backgroundColor": "rgba(255,255,255,0.95)",
179+
"borderColor": "#c0392b",
180+
"borderWidth": 2,
181+
"borderRadius": 8,
182+
"padding": 14,
183+
"y": -55,
184+
},
185+
{
186+
"point": {"xAxis": 0, "yAxis": 0, "x": 0.06, "y": reactant_energy},
187+
"text": "Reactants",
188+
"style": {"fontSize": "34px", "fontWeight": "700", "color": "#306998"},
189+
"backgroundColor": "rgba(255,255,255,0)",
190+
"borderWidth": 0,
191+
"y": -40,
192+
},
193+
{
194+
"point": {"xAxis": 0, "yAxis": 0, "x": 0.94, "y": product_energy},
195+
"text": "Products",
196+
"style": {"fontSize": "34px", "fontWeight": "700", "color": "#306998"},
197+
"backgroundColor": "rgba(255,255,255,0)",
198+
"borderWidth": 0,
199+
"y": -40,
200+
},
201+
{
202+
"point": {"xAxis": 0, "yAxis": 0, "x": ea_arrow_x, "y": ea_mid_y},
203+
"text": f"E\u2090 = {int(activation_energy)} kJ/mol",
204+
"style": {"fontSize": "30px", "fontWeight": "700", "color": "#d35400"},
205+
"backgroundColor": "rgba(255,255,255,0.95)",
206+
"borderColor": "#d35400",
207+
"borderWidth": 2,
208+
"borderRadius": 8,
209+
"padding": 12,
210+
"x": -200,
211+
"y": 0,
212+
},
213+
{
214+
"point": {"xAxis": 0, "yAxis": 0, "x": dh_arrow_x, "y": dh_mid_y},
215+
"text": f"\u0394H = {int(delta_h)} kJ/mol",
216+
"style": {"fontSize": "30px", "fontWeight": "700", "color": "#2980b9"},
217+
"backgroundColor": "rgba(255,255,255,0.95)",
218+
"borderColor": "#2980b9",
219+
"borderWidth": 2,
220+
"borderRadius": 8,
221+
"padding": 12,
222+
"x": 140,
223+
"y": 0,
224+
},
225+
],
226+
}
227+
)
228+
]
229+
230+
# Download Highcharts JS and annotations module
231+
highcharts_url = "https://cdn.jsdelivr.net/npm/highcharts@11/highcharts.js"
232+
hc_req = urllib.request.Request(highcharts_url, headers={"User-Agent": "Mozilla/5.0"})
233+
with urllib.request.urlopen(hc_req, timeout=30) as resp:
234+
highcharts_js = resp.read().decode("utf-8")
235+
236+
annotations_url = "https://cdn.jsdelivr.net/npm/highcharts@11/modules/annotations.js"
237+
ann_req = urllib.request.Request(annotations_url, headers={"User-Agent": "Mozilla/5.0"})
238+
with urllib.request.urlopen(ann_req, timeout=30) as resp:
239+
annotations_module_js = resp.read().decode("utf-8")
240+
241+
# Generate JS literal with annotations included via proper API
242+
html_str = chart.to_js_literal()
243+
244+
# Build HTML with inline scripts
245+
html_content = f"""<!DOCTYPE html>
246+
<html>
247+
<head>
248+
<meta charset="utf-8">
249+
<script>{highcharts_js}</script>
250+
<script>{annotations_module_js}</script>
251+
</head>
252+
<body style="margin:0;">
253+
<div id="container" style="width: 4800px; height: 2700px;"></div>
254+
<script>{html_str}</script>
255+
</body>
256+
</html>"""
257+
258+
# Save HTML
259+
with open("plot.html", "w", encoding="utf-8") as f:
260+
f.write(html_content)
261+
262+
# Screenshot with headless Chrome
263+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
264+
f.write(html_content)
265+
temp_path = f.name
266+
267+
chrome_options = Options()
268+
chrome_options.add_argument("--headless")
269+
chrome_options.add_argument("--no-sandbox")
270+
chrome_options.add_argument("--disable-dev-shm-usage")
271+
chrome_options.add_argument("--disable-gpu")
272+
chrome_options.add_argument("--window-size=4800,2700")
273+
274+
driver = webdriver.Chrome(options=chrome_options)
275+
driver.get(f"file://{temp_path}")
276+
time.sleep(5)
277+
driver.save_screenshot("plot.png")
278+
driver.quit()
279+
280+
Path(temp_path).unlink()

0 commit comments

Comments
 (0)