Skip to content

Commit b323d0d

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

2 files changed

Lines changed: 343 additions & 0 deletions

File tree

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
""" pyplots.ai
2+
line-arrhenius: Arrhenius Plot for Reaction Kinetics
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 as export_ggsave
11+
from scipy import stats
12+
13+
14+
LetsPlot.setup_html() # noqa: F405
15+
16+
# Data - First-order decomposition reaction rate constants at various temperatures
17+
np.random.seed(42)
18+
temperature_K = np.array([300, 325, 350, 375, 400, 425, 450, 475, 500, 550, 600])
19+
R = 8.314 # Gas constant (J/(mol·K))
20+
Ea_true = 75000 # Activation energy (J/mol) ~75 kJ/mol
21+
A_true = 1e12 # Pre-exponential factor (s⁻¹)
22+
23+
rate_constant_k = A_true * np.exp(-Ea_true / (R * temperature_K))
24+
noise = np.random.normal(0, 0.15, len(temperature_K))
25+
ln_k = np.log(rate_constant_k) + noise
26+
27+
inv_T = 1.0 / temperature_K
28+
29+
# Linear regression for annotation values only
30+
slope, intercept, r_value, p_value, std_err = stats.linregress(inv_T, ln_k)
31+
r_squared = r_value**2
32+
Ea_extracted = -slope * R / 1000 # Convert to kJ/mol
33+
34+
# Main DataFrame
35+
df_points = pd.DataFrame(
36+
{
37+
"inv_T": inv_T,
38+
"ln_k": ln_k,
39+
"temp_K": [f"{t} K" for t in temperature_K],
40+
"k_val": [f"{np.exp(lk):.2e}" for lk in ln_k],
41+
}
42+
)
43+
44+
# Annotation
45+
eq_text = f"Slope = \u2212Ea/R = {slope:.0f} K\nEa = {Ea_extracted:.1f} kJ/mol\nR\u00b2 = {r_squared:.4f}"
46+
y_range = ln_k.max() - ln_k.min()
47+
annot_x = inv_T.max() - (inv_T.max() - inv_T.min()) * 0.02
48+
annot_y = ln_k.min() + y_range * 0.25
49+
50+
# Secondary x-axis: temperature labels at top of plot
51+
temp_ticks = np.array([600, 500, 450, 400, 350, 300])
52+
inv_T_ticks = 1.0 / temp_ticks
53+
y_top = ln_k.max() + y_range * 0.08
54+
df_sec_axis = pd.DataFrame(
55+
{
56+
"inv_T": np.concatenate([inv_T_ticks, [np.mean(inv_T_ticks)]]),
57+
"y_label": np.concatenate([[y_top] * len(temp_ticks), [y_top + y_range * 0.06]]),
58+
"y_tick_start": np.concatenate([[y_top - y_range * 0.02] * len(temp_ticks), [np.nan]]),
59+
"y_tick_end": np.concatenate([[y_top - y_range * 0.04] * len(temp_ticks), [np.nan]]),
60+
"label": [f"{t} K" for t in temp_ticks] + ["Temperature (K)"],
61+
"is_title": [False] * len(temp_ticks) + [True],
62+
}
63+
)
64+
df_ticks = df_sec_axis[~df_sec_axis["is_title"]].copy()
65+
df_title = df_sec_axis[df_sec_axis["is_title"]].copy()
66+
67+
# Plot with lets-plot specific features: geom_smooth, tooltips, flavor
68+
plot = (
69+
ggplot()
70+
# Regression line using lets-plot's geom_smooth
71+
+ geom_smooth(
72+
aes(x="inv_T", y="ln_k"), data=df_points, method="lm", color="#306998", size=1.8, alpha=0.4, se=True, level=0.95
73+
)
74+
+ geom_point(
75+
aes(x="inv_T", y="ln_k"),
76+
data=df_points,
77+
fill="#306998",
78+
color="white",
79+
size=8,
80+
shape=21,
81+
stroke=1.2,
82+
tooltips=layer_tooltips().line("@temp_K").line("1/T = @inv_T").line("ln(k) = @ln_k").line("k = @k_val"),
83+
)
84+
# Annotation with regression parameters
85+
+ geom_text(
86+
aes(x="x", y="y", label="label"),
87+
data=pd.DataFrame({"x": [annot_x], "y": [annot_y], "label": [eq_text]}),
88+
size=12,
89+
color="#333333",
90+
hjust=1,
91+
)
92+
# Secondary x-axis: temperature labels at top
93+
+ geom_text(aes(x="inv_T", y="y_label", label="label"), data=df_ticks, size=11, color="#777777")
94+
+ geom_segment(
95+
aes(x="inv_T", y="y_tick_start", xend="inv_T", yend="y_tick_end"), data=df_ticks, color="#BBBBBB", size=0.5
96+
)
97+
+ geom_text(aes(x="inv_T", y="y_label", label="label"), data=df_title, size=13, color="#555555", fontface="italic")
98+
+ labs(x="1/T (K\u207b\u00b9)", y="ln(k)", title="line-arrhenius \u00b7 letsplot \u00b7 pyplots.ai")
99+
+ scale_x_continuous(breaks=inv_T_ticks.tolist(), labels=[f"{v:.2e}" for v in inv_T_ticks])
100+
+ scale_y_continuous(limits=[ln_k.min() - y_range * 0.08, y_top + y_range * 0.10])
101+
+ coord_cartesian(xlim=[inv_T.min() * 0.95, inv_T.max() * 1.05])
102+
+ ggsize(1600, 900)
103+
+ flavor_solarized_light()
104+
+ theme(
105+
axis_text=element_text(size=16, color="#555555"),
106+
axis_title=element_text(size=20, color="#333333"),
107+
plot_title=element_text(size=24, color="#222222", face="bold"),
108+
panel_grid_major_x=element_blank(),
109+
panel_grid_major_y=element_line(color="#D5D0C8", size=0.4),
110+
panel_grid_minor=element_blank(),
111+
axis_ticks=element_blank(),
112+
axis_ticks_length=0,
113+
plot_margin=[40, 40, 20, 20],
114+
)
115+
)
116+
117+
# Save
118+
export_ggsave(plot, filename="plot.png", path=".", scale=3)
119+
export_ggsave(plot, filename="plot.html", path=".")
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
library: letsplot
2+
specification_id: line-arrhenius
3+
created: '2026-03-21T22:07:01Z'
4+
updated: '2026-03-21T22:35:07Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 23389777945
7+
issue: 4408
8+
python_version: 3.14.3
9+
library_version: 4.9.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/line-arrhenius/letsplot/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/line-arrhenius/letsplot/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/line-arrhenius/letsplot/plot.html
13+
quality_score: 91
14+
review:
15+
strengths:
16+
- Full spec compliance with all required features (regression line, R², Ea annotation,
17+
secondary axis)
18+
- Strong design with solarized light flavor creating a distinctive warm aesthetic
19+
- Effective use of lets-plot distinctive features (geom_smooth, layer_tooltips,
20+
flavor)
21+
- Clean well-structured code with proper reproducibility
22+
weaknesses:
23+
- Secondary axis constructed via manual geom_text/geom_segment rather than native
24+
support (lets-plot limitation)
25+
- Slight extra vertical space to accommodate secondary axis labels
26+
image_description: The plot displays an Arrhenius plot on a warm solarized light
27+
background. The title "line-arrhenius · letsplot · pyplots.ai" appears bold in
28+
the top-left. The x-axis shows "1/T (K⁻¹)" with scientific notation tick labels
29+
(1.67e-03 to 3.33e-03), and the y-axis shows "ln(k)" ranging from about -4 to
30+
18. A secondary temperature axis at the top displays temperature values from 600
31+
K down to 300 K with small tick marks and an italic "Temperature (K)" header.
32+
Eleven dark blue filled circles with white edge strokes are plotted along a blue
33+
regression line. A light gray 95% confidence band surrounds the regression line.
34+
An annotation box in the lower-right region displays "Slope = −Ea/R = -8983 K",
35+
"Ea = 74.7 kJ/mol", and "R² = 0.9995". The plot uses subtle horizontal grid lines
36+
only, with no vertical grid, and the overall composition is clean and polished.
37+
criteria_checklist:
38+
visual_quality:
39+
score: 29
40+
max: 30
41+
items:
42+
- id: VQ-01
43+
name: Text Legibility
44+
score: 8
45+
max: 8
46+
passed: true
47+
comment: 'All font sizes explicitly set: title=24, axis_title=20, axis_text=16,
48+
annotation=12'
49+
- id: VQ-02
50+
name: No Overlap
51+
score: 6
52+
max: 6
53+
passed: true
54+
comment: No overlapping text elements
55+
- id: VQ-03
56+
name: Element Visibility
57+
score: 6
58+
max: 6
59+
passed: true
60+
comment: Points size=8 with white stroke, perfectly visible for 11 data points
61+
- id: VQ-04
62+
name: Color Accessibility
63+
score: 4
64+
max: 4
65+
passed: true
66+
comment: Single-color scheme with good contrast against solarized light background
67+
- id: VQ-05
68+
name: Layout & Canvas
69+
score: 3
70+
max: 4
71+
passed: true
72+
comment: Good layout with minor extra vertical space for secondary axis accommodation
73+
- id: VQ-06
74+
name: Axis Labels & Title
75+
score: 2
76+
max: 2
77+
passed: true
78+
comment: 'Descriptive labels with units: 1/T (K⁻¹) and ln(k)'
79+
design_excellence:
80+
score: 15
81+
max: 20
82+
items:
83+
- id: DE-01
84+
name: Aesthetic Sophistication
85+
score: 6
86+
max: 8
87+
passed: true
88+
comment: Strong design with solarized light flavor, Python Blue with white-stroked
89+
points, intentional hierarchy
90+
- id: DE-02
91+
name: Visual Refinement
92+
score: 5
93+
max: 6
94+
passed: true
95+
comment: Horizontal grid only, ticks removed, generous margins, clean composition
96+
- id: DE-03
97+
name: Data Storytelling
98+
score: 4
99+
max: 6
100+
passed: true
101+
comment: Annotation communicates activation energy and fit quality, secondary
102+
axis provides physical context
103+
spec_compliance:
104+
score: 15
105+
max: 15
106+
items:
107+
- id: SC-01
108+
name: Plot Type
109+
score: 5
110+
max: 5
111+
passed: true
112+
comment: 'Correct Arrhenius plot: ln(k) vs 1/T'
113+
- id: SC-02
114+
name: Required Features
115+
score: 4
116+
max: 4
117+
passed: true
118+
comment: 'All spec features present: regression line, R², Ea annotation, secondary
119+
axis, visible data points'
120+
- id: SC-03
121+
name: Data Mapping
122+
score: 3
123+
max: 3
124+
passed: true
125+
comment: X=1/T, Y=ln(k), correct mapping
126+
- id: SC-04
127+
name: Title & Legend
128+
score: 3
129+
max: 3
130+
passed: true
131+
comment: Title matches required format, no legend needed for single series
132+
data_quality:
133+
score: 14
134+
max: 15
135+
items:
136+
- id: DQ-01
137+
name: Feature Coverage
138+
score: 5
139+
max: 6
140+
passed: true
141+
comment: Clear linear relationship with realistic scatter, 11 points spanning
142+
300-600 K
143+
- id: DQ-02
144+
name: Realistic Context
145+
score: 5
146+
max: 5
147+
passed: true
148+
comment: First-order decomposition reaction with Ea ≈ 75 kJ/mol, realistic
149+
chemistry
150+
- id: DQ-03
151+
name: Appropriate Scale
152+
score: 4
153+
max: 4
154+
passed: true
155+
comment: Temperature range and activation energy are physically realistic
156+
code_quality:
157+
score: 10
158+
max: 10
159+
items:
160+
- id: CQ-01
161+
name: KISS Structure
162+
score: 3
163+
max: 3
164+
passed: true
165+
comment: Clean imports → data → plot → save structure
166+
- id: CQ-02
167+
name: Reproducibility
168+
score: 2
169+
max: 2
170+
passed: true
171+
comment: np.random.seed(42) set
172+
- id: CQ-03
173+
name: Clean Imports
174+
score: 2
175+
max: 2
176+
passed: true
177+
comment: All imports used
178+
- id: CQ-04
179+
name: Code Elegance
180+
score: 2
181+
max: 2
182+
passed: true
183+
comment: Clean, well-organized code
184+
- id: CQ-05
185+
name: Output & API
186+
score: 1
187+
max: 1
188+
passed: true
189+
comment: Saves via export_ggsave with scale=3
190+
library_mastery:
191+
score: 8
192+
max: 10
193+
items:
194+
- id: LM-01
195+
name: Idiomatic Usage
196+
score: 4
197+
max: 5
198+
passed: true
199+
comment: 'Good grammar of graphics usage: geom_smooth, layer_tooltips, flavor
200+
themes'
201+
- id: LM-02
202+
name: Distinctive Features
203+
score: 4
204+
max: 5
205+
passed: true
206+
comment: Uses flavor_solarized_light, layer_tooltips, geom_smooth with confidence
207+
band, HTML export
208+
verdict: APPROVED
209+
impl_tags:
210+
dependencies:
211+
- scipy
212+
techniques:
213+
- annotations
214+
- layer-composition
215+
- hover-tooltips
216+
- html-export
217+
patterns:
218+
- data-generation
219+
dataprep:
220+
- regression
221+
styling:
222+
- edge-highlighting
223+
- grid-styling
224+
- alpha-blending

0 commit comments

Comments
 (0)