Skip to content

Commit 4769418

Browse files
feat(highcharts): implement line-arrhenius (#5176)
## Implementation: `line-arrhenius` - highcharts Implements the **highcharts** version of `line-arrhenius`. **File:** `plots/line-arrhenius/implementations/highcharts.py` **Parent Issue:** #4408 --- :robot: *[impl-generate workflow](https://github.com/MarkusNeusinger/pyplots/actions/runs/23389777938)* --------- 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 feeacdd commit 4769418

2 files changed

Lines changed: 474 additions & 0 deletions

File tree

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
""" pyplots.ai
2+
line-arrhenius: Arrhenius Plot for Reaction Kinetics
3+
Library: highcharts unknown | Python 3.14.3
4+
Quality: 87/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.scatter import ScatterSeries
17+
from highcharts_core.options.series.spline import SplineSeries
18+
from scipy import stats
19+
from selenium import webdriver
20+
from selenium.webdriver.chrome.options import Options
21+
22+
23+
# Data - First-order decomposition reaction rate constants at various temperatures
24+
R_gas = 8.314 # Gas constant J/(mol·K)
25+
Ea_true = 75000 # Activation energy in J/mol (75 kJ/mol)
26+
A_prefactor = 1e13 # Pre-exponential factor (s⁻¹)
27+
28+
np.random.seed(42)
29+
temperature_K = np.array([300, 325, 350, 375, 400, 425, 450, 475, 500, 550, 600])
30+
inv_T = 1.0 / temperature_K
31+
ln_k_true = np.log(A_prefactor) - Ea_true / (R_gas * temperature_K)
32+
ln_k = ln_k_true + np.random.normal(0, 0.15, len(temperature_K))
33+
34+
# Linear regression
35+
slope, intercept, r_value, p_value, std_err = stats.linregress(inv_T, ln_k)
36+
r_squared = r_value**2
37+
Ea_fitted = -slope * R_gas / 1000 # kJ/mol
38+
39+
# Regression line points
40+
inv_T_fit = np.linspace(inv_T.min() * 0.97, inv_T.max() * 1.03, 100)
41+
ln_k_fit = slope * inv_T_fit + intercept
42+
43+
# Scale 1/T by 1000 for readability (units: 10⁻³ K⁻¹)
44+
inv_T_scaled = inv_T * 1000
45+
inv_T_fit_scaled = inv_T_fit * 1000
46+
47+
# Chart
48+
chart = Chart(container="container")
49+
chart.options = HighchartsOptions()
50+
51+
chart.options.chart = {
52+
"width": 4800,
53+
"height": 2700,
54+
"backgroundColor": "#ffffff",
55+
"spacingTop": 60,
56+
"spacingBottom": 60,
57+
"marginBottom": 160,
58+
"spacingLeft": 100,
59+
"spacingRight": 100,
60+
"style": {"fontFamily": "'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif"},
61+
}
62+
63+
chart.options.title = {
64+
"text": "line-arrhenius \u00b7 highcharts \u00b7 pyplots.ai",
65+
"style": {"fontSize": "48px", "fontWeight": "600", "color": "#1a1a2e"},
66+
"margin": 50,
67+
}
68+
69+
# Temperature reference marks as plotLines on x-axis
70+
ref_temps = [300, 350, 400, 450, 500, 600]
71+
plot_lines = []
72+
for t in ref_temps:
73+
plot_lines.append(
74+
{
75+
"value": 1000.0 / t,
76+
"color": "rgba(0,0,0,0.12)",
77+
"width": 1,
78+
"dashStyle": "Dot",
79+
"label": {"text": f"{t} K", "style": {"fontSize": "22px", "color": "#888888"}, "rotation": 0, "y": -10},
80+
}
81+
)
82+
83+
chart.options.x_axis = {
84+
"title": {
85+
"text": "1000 / T (K\u207b\u00b9)",
86+
"style": {"fontSize": "36px", "fontWeight": "600", "color": "#333333"},
87+
"margin": 24,
88+
},
89+
"labels": {"style": {"fontSize": "26px", "color": "#444444"}, "step": 2},
90+
"reversed": True,
91+
"tickInterval": 0.2,
92+
"lineColor": "#666666",
93+
"lineWidth": 2,
94+
"tickLength": 0,
95+
"gridLineWidth": 0,
96+
"plotLines": plot_lines,
97+
}
98+
99+
chart.options.y_axis = {
100+
"title": {"text": "ln(k)", "style": {"fontSize": "36px", "fontWeight": "600", "color": "#333333"}, "margin": 24},
101+
"labels": {"style": {"fontSize": "26px", "color": "#444444"}},
102+
"lineColor": "#666666",
103+
"lineWidth": 2,
104+
"gridLineWidth": 1,
105+
"gridLineColor": "rgba(0,0,0,0.08)",
106+
}
107+
108+
chart.options.legend = {
109+
"enabled": True,
110+
"align": "right",
111+
"verticalAlign": "top",
112+
"layout": "vertical",
113+
"x": -60,
114+
"y": 80,
115+
"itemStyle": {"fontSize": "28px", "fontWeight": "500", "color": "#333333"},
116+
"backgroundColor": "rgba(255,255,255,0.85)",
117+
"borderColor": "#cccccc",
118+
"borderWidth": 1,
119+
"borderRadius": 6,
120+
"padding": 14,
121+
"symbolRadius": 6,
122+
}
123+
chart.options.credits = {"enabled": False}
124+
125+
chart.options.plot_options = {
126+
"scatter": {"marker": {"radius": 14, "lineWidth": 3, "lineColor": "#ffffff"}},
127+
"spline": {"marker": {"enabled": False}, "lineWidth": 4},
128+
}
129+
130+
# Regression line series
131+
fit_data = [[round(float(x), 4), round(float(y), 3)] for x, y in zip(inv_T_fit_scaled, ln_k_fit, strict=False)]
132+
regression_series = SplineSeries()
133+
regression_series.data = fit_data
134+
regression_series.name = "Linear Fit"
135+
regression_series.color = "#c0392b"
136+
regression_series.line_width = 4
137+
regression_series.dash_style = "Dash"
138+
chart.add_series(regression_series)
139+
140+
# Data points series (on top of regression line)
141+
scatter_data = [[round(float(x), 4), round(float(y), 3)] for x, y in zip(inv_T_scaled, ln_k, strict=False)]
142+
data_series = ScatterSeries()
143+
data_series.data = scatter_data
144+
data_series.name = "Experimental Data"
145+
data_series.color = "#306998"
146+
data_series.z_index = 5
147+
chart.add_series(data_series)
148+
149+
# Annotations for R², Ea, and slope - positioned near the data midpoint
150+
anno_x = round(float(inv_T_scaled[3]), 4) # Near 375K data point region
151+
y_mid = float(slope * inv_T[3] + intercept) # On regression line at that x
152+
153+
chart.options.annotations = [
154+
Annotation.from_dict(
155+
{
156+
"draggable": "",
157+
"labelOptions": {"allowOverlap": True, "overflow": "none", "crop": False},
158+
"labels": [
159+
{
160+
"point": {"xAxis": 0, "yAxis": 0, "x": anno_x, "y": round(y_mid + 2.5, 2)},
161+
"text": f"R\u00b2 = {r_squared:.4f}",
162+
"style": {"fontSize": "34px", "fontWeight": "700", "color": "#c0392b"},
163+
"backgroundColor": "rgba(255,255,255,0.92)",
164+
"borderColor": "#c0392b",
165+
"borderWidth": 2,
166+
"borderRadius": 8,
167+
"padding": 16,
168+
},
169+
{
170+
"point": {"xAxis": 0, "yAxis": 0, "x": anno_x, "y": round(y_mid + 1.2, 2)},
171+
"text": f"E\u2090 = {Ea_fitted:.1f} kJ/mol",
172+
"style": {"fontSize": "34px", "fontWeight": "700", "color": "#d35400"},
173+
"backgroundColor": "rgba(255,255,255,0.92)",
174+
"borderColor": "#d35400",
175+
"borderWidth": 2,
176+
"borderRadius": 8,
177+
"padding": 16,
178+
},
179+
{
180+
"point": {"xAxis": 0, "yAxis": 0, "x": anno_x, "y": round(y_mid - 0.1, 2)},
181+
"text": f"Slope = \u2212E\u2090/R = {slope:.0f} K",
182+
"style": {"fontSize": "30px", "fontWeight": "600", "color": "#555555"},
183+
"backgroundColor": "rgba(255,255,255,0.92)",
184+
"borderColor": "#888888",
185+
"borderWidth": 1,
186+
"borderRadius": 8,
187+
"padding": 14,
188+
},
189+
],
190+
}
191+
)
192+
]
193+
194+
# Download Highcharts JS and annotations module
195+
highcharts_url = "https://cdn.jsdelivr.net/npm/highcharts@11/highcharts.js"
196+
hc_req = urllib.request.Request(highcharts_url, headers={"User-Agent": "Mozilla/5.0"})
197+
with urllib.request.urlopen(hc_req, timeout=30) as resp:
198+
highcharts_js = resp.read().decode("utf-8")
199+
200+
annotations_url = "https://cdn.jsdelivr.net/npm/highcharts@11/modules/annotations.js"
201+
ann_req = urllib.request.Request(annotations_url, headers={"User-Agent": "Mozilla/5.0"})
202+
with urllib.request.urlopen(ann_req, timeout=30) as resp:
203+
annotations_module_js = resp.read().decode("utf-8")
204+
205+
# Generate JS and build HTML
206+
html_str = chart.to_js_literal()
207+
208+
html_content = f"""<!DOCTYPE html>
209+
<html>
210+
<head>
211+
<meta charset="utf-8">
212+
<script>{highcharts_js}</script>
213+
<script>{annotations_module_js}</script>
214+
</head>
215+
<body style="margin:0;">
216+
<div id="container" style="width: 4800px; height: 2700px;"></div>
217+
<script>{html_str}</script>
218+
</body>
219+
</html>"""
220+
221+
# Save HTML
222+
with open("plot.html", "w", encoding="utf-8") as f:
223+
f.write(html_content)
224+
225+
# Screenshot with headless Chrome
226+
with tempfile.NamedTemporaryFile(mode="w", suffix=".html", delete=False, encoding="utf-8") as f:
227+
f.write(html_content)
228+
temp_path = f.name
229+
230+
chrome_options = Options()
231+
chrome_options.add_argument("--headless")
232+
chrome_options.add_argument("--no-sandbox")
233+
chrome_options.add_argument("--disable-dev-shm-usage")
234+
chrome_options.add_argument("--disable-gpu")
235+
chrome_options.add_argument("--window-size=4800,2700")
236+
237+
driver = webdriver.Chrome(options=chrome_options)
238+
driver.get(f"file://{temp_path}")
239+
time.sleep(5)
240+
driver.save_screenshot("plot.png")
241+
driver.quit()
242+
243+
Path(temp_path).unlink()

0 commit comments

Comments
 (0)