Skip to content

Commit b080aec

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

File tree

2 files changed

+392
-0
lines changed

2 files changed

+392
-0
lines changed
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
""" pyplots.ai
2+
line-arrhenius: Arrhenius Plot for Reaction Kinetics
3+
Library: pygal 3.1.0 | Python 3.14.3
4+
Quality: 90/100 | Created: 2026-03-21
5+
"""
6+
7+
import numpy as np
8+
import pygal
9+
from pygal.style import Style
10+
from scipy import stats
11+
12+
13+
# Data — First-order decomposition reaction rate constants at various temperatures
14+
temperature_K = np.array([300, 330, 360, 400, 440, 480, 520, 560, 600])
15+
np.random.seed(42)
16+
activation_energy = 75000 # J/mol (75 kJ/mol)
17+
R = 8.314 # Gas constant J/(mol·K)
18+
pre_exponential = 1.0e12 # s⁻¹
19+
rate_constant_k = pre_exponential * np.exp(-activation_energy / (R * temperature_K))
20+
# Add realistic experimental scatter (larger noise for visible deviation from fit)
21+
rate_constant_k *= np.exp(np.random.normal(0, 0.25, len(temperature_K)))
22+
23+
# Transformed coordinates for Arrhenius plot
24+
inv_T = 1000.0 / temperature_K # 1000/T for readable axis values (×10⁻³ K⁻¹)
25+
ln_k = np.log(rate_constant_k)
26+
27+
# Linear regression: ln(k) = ln(A) - Ea/R × (1/T)
28+
slope, intercept, r_value, p_value, std_err = stats.linregress(inv_T, ln_k)
29+
r_squared = r_value**2
30+
Ea_extracted = -slope * R * 1000 # Convert back (factor of 1000 from 1000/T scaling)
31+
32+
# Regression line — extend slightly beyond data for visual clarity
33+
x_pad = 0.04
34+
inv_T_fit = np.linspace(float(min(inv_T)) - x_pad, float(max(inv_T)) + x_pad, 80)
35+
ln_k_fit = slope * inv_T_fit + intercept
36+
37+
# Style — colorblind-safe palette: deep blue fit line, amber/orange data points
38+
custom_style = Style(
39+
background="white",
40+
plot_background="#f8f9fa",
41+
foreground="#2c3e50",
42+
foreground_strong="#1a252f",
43+
foreground_subtle="#dde1e4",
44+
colors=("#306998", "#d4760a", "#8b5cf6"),
45+
guide_stroke_color="#e8ecef",
46+
major_guide_stroke_color="#cfd4d8",
47+
guide_stroke_dasharray="6,3",
48+
title_font_size=68,
49+
label_font_size=44,
50+
major_label_font_size=40,
51+
legend_font_size=38,
52+
value_font_size=32,
53+
tooltip_font_size=34,
54+
stroke_width=4,
55+
opacity=0.92,
56+
opacity_hover=1.0,
57+
title_font_family="sans-serif",
58+
label_font_family="sans-serif",
59+
major_label_font_family="sans-serif",
60+
legend_font_family="sans-serif",
61+
value_font_family="sans-serif",
62+
)
63+
64+
# Y-axis labels — tight to data range with minimal padding
65+
y_min_data, y_max_data = float(min(ln_k)), float(max(ln_k))
66+
y_floor = int(np.floor(y_min_data))
67+
y_ceil = int(np.ceil(y_max_data))
68+
y_labels_list = list(range(y_floor, y_ceil + 1, 2))
69+
if y_labels_list[-1] < y_ceil:
70+
y_labels_list.append(y_ceil)
71+
72+
# Chart — pygal XY with tight axis range and polished config
73+
chart = pygal.XY(
74+
style=custom_style,
75+
width=4800,
76+
height=2700,
77+
title="line-arrhenius · pygal · pyplots.ai",
78+
x_title="1000/T (K⁻¹)",
79+
y_title="ln(k)",
80+
show_dots=True,
81+
dots_size=16,
82+
show_x_guides=False,
83+
show_y_guides=True,
84+
legend_at_bottom=True,
85+
legend_at_bottom_columns=3,
86+
legend_box_size=28,
87+
truncate_legend=-1,
88+
margin=40,
89+
margin_top=70,
90+
margin_bottom=160,
91+
margin_left=170,
92+
margin_right=80,
93+
tooltip_fancy_mode=True,
94+
tooltip_border_radius=10,
95+
x_value_formatter=lambda x: f"{x:.2f}",
96+
y_value_formatter=lambda y: f"{y:.1f}",
97+
range=(y_floor - 0.5, y_ceil + 0.5),
98+
xrange=(float(min(inv_T) - 0.1), float(max(inv_T) + 0.1)),
99+
y_labels=y_labels_list,
100+
y_labels_major_every=1,
101+
print_values=False,
102+
show_minor_x_labels=False,
103+
interpolate="cubic",
104+
css=[
105+
"file://style.css",
106+
"file://graph.css",
107+
"inline:"
108+
".axis > .line { stroke: transparent !important; } "
109+
".plot .background { rx: 14; ry: 14; } "
110+
".legends .legend text { font-weight: 500; } "
111+
".title { font-weight: 600; letter-spacing: 1px; }",
112+
],
113+
)
114+
115+
# X-axis labels: 1000/T with temperature in parentheses
116+
x_label_temps = np.array([300, 360, 440, 520, 600])
117+
x_label_positions = sorted(1000.0 / x_label_temps)
118+
chart.x_labels = [float(x) for x in x_label_positions]
119+
chart.x_labels_major = [float(x) for x in x_label_positions]
120+
chart.x_label_rotation = 0
121+
chart.x_value_formatter = lambda x: f"{x:.2f} ({int(round(1000.0 / x))} K)"
122+
123+
# Regression fit line — smooth blue line
124+
fit_points = [
125+
{"value": (float(x), float(y)), "label": f"Fit: ln(k) = {slope:.2f} × (1000/T) + {intercept:.2f}"}
126+
for x, y in zip(inv_T_fit, ln_k_fit, strict=False)
127+
]
128+
chart.add(
129+
f"Linear Fit (R² = {r_squared:.3f})",
130+
fit_points,
131+
show_dots=False,
132+
stroke_style={"width": 6, "linecap": "round", "linejoin": "round"},
133+
)
134+
135+
# Experimental data points — amber markers with rich tooltips
136+
data_points = [
137+
{"value": (float(x), float(y)), "label": f"T = {int(t)} K\nk = {k:.3e} s⁻¹\nln(k) = {y:.2f}\n1000/T = {x:.3f}"}
138+
for x, y, t, k in zip(inv_T, ln_k, temperature_K, rate_constant_k, strict=False)
139+
]
140+
chart.add("Experimental Data", data_points, stroke=False, dots_size=20)
141+
142+
# Activation energy annotation — dedicated legend entry for Eₐ result
143+
# Uses a zero-opacity point on the regression line to anchor the tooltip
144+
mid_x = float(np.median(inv_T))
145+
mid_y = float(slope * mid_x + intercept)
146+
chart.add(
147+
f"Eₐ = {Ea_extracted / 1000:.1f} kJ/mol (−Eₐ/R = {slope:.1f} K⁻¹)",
148+
[
149+
{
150+
"value": (mid_x, mid_y),
151+
"label": f"Activation Energy: Eₐ = {Ea_extracted / 1000:.1f} kJ/mol\n"
152+
f"Slope = {slope:.2f} · −Eₐ/R = {slope * 1000:.0f} K",
153+
}
154+
],
155+
dots_size=0,
156+
stroke=False,
157+
)
158+
159+
# Save — PNG for static output, HTML with pygal's interactive SVG tooltips
160+
chart.render_to_png("plot.png")
161+
chart.render_to_file("plot.html")
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
library: pygal
2+
specification_id: line-arrhenius
3+
created: '2026-03-21T22:08:23Z'
4+
updated: '2026-03-21T22:42:22Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 23389777964
7+
issue: 4408
8+
python_version: 3.14.3
9+
library_version: 3.1.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/line-arrhenius/pygal/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/line-arrhenius/pygal/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/line-arrhenius/pygal/plot.html
13+
quality_score: 90
14+
review:
15+
strengths:
16+
- Excellent visual quality with all font sizes explicitly set and colorblind-safe
17+
blue/amber palette
18+
- Strong CSS customization leveraging pygal unique SVG styling capabilities (transparent
19+
axes, rounded corners, custom font weights)
20+
- Realistic chemistry data with proper Arrhenius equation parameters and experimental
21+
noise
22+
- Clean code structure with good reproducibility (seed=42)
23+
- Dual x-axis labels showing both 1000/T and temperature in K aid interpretation
24+
weaknesses:
25+
- Activation energy annotation conveyed through legend entry rather than on-plot
26+
annotation (pygal limitation)
27+
- Data storytelling could be enhanced with more emphasis on the key insight
28+
image_description: 'The plot displays an Arrhenius plot with "1000/T (K⁻¹)" on the
29+
x-axis and "ln(k)" on the y-axis. A blue linear regression line runs from the
30+
upper-left (~12.5 at 1.67 K⁻¹) to lower-right (~-2.5 at 3.33 K⁻¹). Nine amber/orange
31+
circular data points are scattered along the fit line showing realistic experimental
32+
scatter. The x-axis labels include both 1000/T values and corresponding temperatures
33+
in parentheses (e.g., "1.67 (600 K)", "3.33 (300 K)"). The y-axis shows integer
34+
values from -3 to 13 with subtle dashed horizontal grid lines. The plot background
35+
is light gray with a white page background, and the plot area has rounded corners.
36+
The title "line-arrhenius · pygal · pyplots.ai" appears at the top in bold. A
37+
bottom legend shows three entries: "Linear Fit (R² = 0.999)" (blue), "Experimental
38+
Data" (amber), and "Eₐ = 74.9 kJ/mol (−Eₐ/R = -9.0 K⁻¹)" (purple). Axis lines
39+
are hidden, giving a clean appearance.'
40+
criteria_checklist:
41+
visual_quality:
42+
score: 30
43+
max: 30
44+
items:
45+
- id: VQ-01
46+
name: Text Legibility
47+
score: 8
48+
max: 8
49+
passed: true
50+
comment: All font sizes explicitly set (title=68, label=44, major_label=40,
51+
legend=38). Clearly readable at full resolution.
52+
- id: VQ-02
53+
name: No Overlap
54+
score: 6
55+
max: 6
56+
passed: true
57+
comment: No overlapping elements. X-axis labels well-spaced. Legend fits cleanly
58+
at bottom.
59+
- id: VQ-03
60+
name: Element Visibility
61+
score: 6
62+
max: 6
63+
passed: true
64+
comment: Amber data points at dots_size=20 are prominent. Well-suited for
65+
9 data points.
66+
- id: VQ-04
67+
name: Color Accessibility
68+
score: 4
69+
max: 4
70+
passed: true
71+
comment: Blue and amber is an excellent colorblind-safe combination with strong
72+
contrast.
73+
- id: VQ-05
74+
name: Layout & Canvas
75+
score: 4
76+
max: 4
77+
passed: true
78+
comment: Plot fills canvas well with explicit margins. Good proportions at
79+
4800x2700.
80+
- id: VQ-06
81+
name: Axis Labels & Title
82+
score: 2
83+
max: 2
84+
passed: true
85+
comment: 'Descriptive labels with units: 1000/T (K⁻¹) and ln(k).'
86+
design_excellence:
87+
score: 15
88+
max: 20
89+
items:
90+
- id: DE-01
91+
name: Aesthetic Sophistication
92+
score: 6
93+
max: 8
94+
passed: true
95+
comment: 'Strong custom design: blue/amber palette, custom font weights, letter-spacing,
96+
distinct backgrounds. Above configured defaults.'
97+
- id: DE-02
98+
name: Visual Refinement
99+
score: 5
100+
max: 6
101+
passed: true
102+
comment: Axis lines removed via CSS, dashed guide lines, rounded corners,
103+
custom guide stroke colors.
104+
- id: DE-03
105+
name: Data Storytelling
106+
score: 4
107+
max: 6
108+
passed: true
109+
comment: 'Clear hierarchy: blue line as primary, amber scatter shows experimental
110+
noise, R² and Eₐ in legend. Dual x-axis labels aid interpretation.'
111+
spec_compliance:
112+
score: 14
113+
max: 15
114+
items:
115+
- id: SC-01
116+
name: Plot Type
117+
score: 5
118+
max: 5
119+
passed: true
120+
comment: 'Correct Arrhenius plot: ln(k) vs 1/T with linear regression.'
121+
- id: SC-02
122+
name: Required Features
123+
score: 3
124+
max: 4
125+
passed: true
126+
comment: 'All features present. Minor: Eₐ annotation in legend rather than
127+
on-plot (pygal limitation).'
128+
- id: SC-03
129+
name: Data Mapping
130+
score: 3
131+
max: 3
132+
passed: true
133+
comment: X=1000/T, Y=ln(k) correctly mapped. Full data range displayed.
134+
- id: SC-04
135+
name: Title & Legend
136+
score: 3
137+
max: 3
138+
passed: true
139+
comment: Title matches format. Legend labels are descriptive and accurate.
140+
data_quality:
141+
score: 14
142+
max: 15
143+
items:
144+
- id: DQ-01
145+
name: Feature Coverage
146+
score: 5
147+
max: 6
148+
passed: true
149+
comment: 9 data points spanning 300-600K, regression fit, scatter, R², and
150+
Eₐ extraction shown.
151+
- id: DQ-02
152+
name: Realistic Context
153+
score: 5
154+
max: 5
155+
passed: true
156+
comment: First-order decomposition with Eₐ=75 kJ/mol, realistic physical chemistry
157+
scenario.
158+
- id: DQ-03
159+
name: Appropriate Scale
160+
score: 4
161+
max: 4
162+
passed: true
163+
comment: Temperature 300-600K, Eₐ=75 kJ/mol, rate constants spanning orders
164+
of magnitude — all physically realistic.
165+
code_quality:
166+
score: 10
167+
max: 10
168+
items:
169+
- id: CQ-01
170+
name: KISS Structure
171+
score: 3
172+
max: 3
173+
passed: true
174+
comment: Clean Imports → Data → Transform → Style → Chart → Save flow.
175+
- id: CQ-02
176+
name: Reproducibility
177+
score: 2
178+
max: 2
179+
passed: true
180+
comment: np.random.seed(42) set before noise generation.
181+
- id: CQ-03
182+
name: Clean Imports
183+
score: 2
184+
max: 2
185+
passed: true
186+
comment: numpy, pygal, Style, scipy.stats all used.
187+
- id: CQ-04
188+
name: Code Elegance
189+
score: 2
190+
max: 2
191+
passed: true
192+
comment: Well-organized with clear comments. Clever zero-opacity legend entry
193+
for Eₐ.
194+
- id: CQ-05
195+
name: Output & API
196+
score: 1
197+
max: 1
198+
passed: true
199+
comment: Saves plot.png and plot.html. Current pygal API.
200+
library_mastery:
201+
score: 7
202+
max: 10
203+
items:
204+
- id: LM-01
205+
name: Idiomatic Usage
206+
score: 4
207+
max: 5
208+
passed: true
209+
comment: Good use of pygal.XY, Style, CSS injection, tooltip config, value
210+
formatters, legend positioning.
211+
- id: LM-02
212+
name: Distinctive Features
213+
score: 3
214+
max: 5
215+
passed: true
216+
comment: CSS injection for axis removal and rounded corners, tooltip_fancy_mode,
217+
HTML export with interactive SVG tooltips.
218+
verdict: APPROVED
219+
impl_tags:
220+
dependencies:
221+
- scipy
222+
techniques:
223+
- custom-legend
224+
- hover-tooltips
225+
- html-export
226+
patterns:
227+
- data-generation
228+
dataprep:
229+
- regression
230+
styling:
231+
- grid-styling

0 commit comments

Comments
 (0)