Skip to content

Commit 164cd6b

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

2 files changed

Lines changed: 382 additions & 0 deletions

File tree

Lines changed: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
""" pyplots.ai
2+
line-arrhenius: Arrhenius Plot for Reaction Kinetics
3+
Library: altair 6.0.0 | Python 3.14.3
4+
Quality: 90/100 | Created: 2026-03-21
5+
"""
6+
7+
import altair as alt
8+
import numpy as np
9+
import pandas as pd
10+
11+
12+
# Data - First-order decomposition reaction rate constants at various temperatures
13+
np.random.seed(42)
14+
temperature_K = np.array([300, 325, 350, 375, 400, 425, 450, 475, 500, 525, 550, 600])
15+
R = 8.314 # J/(mol·K)
16+
Ea_true = 75000 # J/mol (75 kJ/mol)
17+
A_true = 1.5e12 # Pre-exponential factor (s⁻¹)
18+
19+
# Arrhenius equation: k = A * exp(-Ea / (R*T))
20+
ln_k_true = np.log(A_true) - Ea_true / (R * temperature_K)
21+
ln_k_measured = ln_k_true + np.random.normal(0, 0.15, len(temperature_K))
22+
23+
inv_T = 1.0 / temperature_K # 1/T in K⁻¹
24+
25+
# Compute regression parameters for annotations
26+
coeffs = np.polyfit(inv_T, ln_k_measured, 1)
27+
slope_fit, intercept_fit = coeffs
28+
y_pred = slope_fit * inv_T + intercept_fit
29+
ss_res = np.sum((ln_k_measured - y_pred) ** 2)
30+
ss_tot = np.sum((ln_k_measured - np.mean(ln_k_measured)) ** 2)
31+
r_squared = 1 - ss_res / ss_tot
32+
Ea_fit = -slope_fit * R # Activation energy in J/mol
33+
34+
# DataFrame
35+
data_df = pd.DataFrame({"inv_T": inv_T, "ln_k": ln_k_measured, "T_K": temperature_K})
36+
37+
# Shared scales
38+
x_scale = alt.Scale(domain=[inv_T.min() - 0.0001, inv_T.max() + 0.0001], nice=False)
39+
y_scale = alt.Scale(domain=[ln_k_measured.min() - 1.2, ln_k_measured.max() + 1.2])
40+
41+
# Regression line using Altair's native transform_regression
42+
reg_line = (
43+
alt.Chart(data_df)
44+
.mark_line(strokeWidth=3, color="#306998")
45+
.transform_regression("inv_T", "ln_k", extent=[inv_T.min() - 0.00005, inv_T.max() + 0.00005])
46+
.encode(x=alt.X("inv_T:Q", scale=x_scale), y=alt.Y("ln_k:Q", scale=y_scale))
47+
)
48+
49+
# Data points with interactive highlight
50+
highlight = alt.selection_point(on="pointerover", nearest=True, empty=False)
51+
52+
points = (
53+
alt.Chart(data_df)
54+
.mark_point(filled=True, color="#306998", stroke="white", strokeWidth=1.5)
55+
.encode(
56+
x=alt.X("inv_T:Q", scale=x_scale, title="1/T (K⁻¹)"),
57+
y=alt.Y("ln_k:Q", scale=y_scale, title="ln(k)"),
58+
size=alt.condition(highlight, alt.value(500), alt.value(350)),
59+
tooltip=[
60+
alt.Tooltip("T_K:Q", title="Temperature", format=".0f"),
61+
alt.Tooltip("inv_T:Q", title="1/T", format=".5f"),
62+
alt.Tooltip("ln_k:Q", title="ln(k)", format=".2f"),
63+
],
64+
)
65+
.add_params(highlight)
66+
)
67+
68+
# Annotation: Ea and R² value
69+
ea_kj = Ea_fit / 1000
70+
annotation_text = f"Eₐ = {ea_kj:.1f} kJ/mol R² = {r_squared:.4f}"
71+
slope_text = f"slope = −Eₐ/R = {slope_fit:.0f} K"
72+
73+
annotation_df = pd.DataFrame(
74+
{"inv_T": [inv_T.min() + 0.0002], "ln_k": [ln_k_measured.max() + 0.7], "text": [annotation_text]}
75+
)
76+
77+
slope_ann_df = pd.DataFrame(
78+
{"inv_T": [inv_T.min() + 0.0002], "ln_k": [ln_k_measured.max() + 0.15], "text": [slope_text]}
79+
)
80+
81+
ea_label = (
82+
alt.Chart(annotation_df)
83+
.mark_text(fontSize=20, align="left", fontWeight="bold", color="#306998")
84+
.encode(x=alt.X("inv_T:Q", scale=x_scale), y=alt.Y("ln_k:Q", scale=y_scale), text="text:N")
85+
)
86+
87+
slope_label = (
88+
alt.Chart(slope_ann_df)
89+
.mark_text(fontSize=17, align="left", fontStyle="italic", color="#666666")
90+
.encode(x=alt.X("inv_T:Q", scale=x_scale), y=alt.Y("ln_k:Q", scale=y_scale), text="text:N")
91+
)
92+
93+
# Secondary x-axis: temperature labels at data point positions
94+
temp_labels_df = pd.DataFrame(
95+
{
96+
"inv_T": inv_T[::2],
97+
"ln_k": [ln_k_measured.min() - 0.5] * len(inv_T[::2]),
98+
"text": [f"{int(t)} K" for t in temperature_K[::2]],
99+
}
100+
)
101+
102+
temp_tick_labels = (
103+
alt.Chart(temp_labels_df)
104+
.mark_text(fontSize=14, color="#888888", angle=0)
105+
.encode(x=alt.X("inv_T:Q", scale=x_scale), y=alt.Y("ln_k:Q", scale=y_scale), text="text:N")
106+
)
107+
108+
# Temperature axis label
109+
temp_axis_label_df = pd.DataFrame(
110+
{"inv_T": [(inv_T.min() + inv_T.max()) / 2], "ln_k": [ln_k_measured.min() - 0.9], "text": ["Temperature (K)"]}
111+
)
112+
113+
temp_axis_label = (
114+
alt.Chart(temp_axis_label_df)
115+
.mark_text(fontSize=16, color="#888888", fontStyle="italic")
116+
.encode(x=alt.X("inv_T:Q", scale=x_scale), y=alt.Y("ln_k:Q", scale=y_scale), text="text:N")
117+
)
118+
119+
# Combine all layers
120+
chart = (
121+
alt.layer(reg_line, points, ea_label, slope_label, temp_tick_labels, temp_axis_label)
122+
.properties(
123+
width=1600,
124+
height=900,
125+
title=alt.Title(
126+
"line-arrhenius · altair · pyplots.ai",
127+
fontSize=28,
128+
anchor="middle",
129+
color="#2C3E50",
130+
subtitle="First-Order Decomposition · Rate Constants vs Inverse Temperature",
131+
subtitleFontSize=18,
132+
subtitleColor="#7f8c8d",
133+
subtitlePadding=8,
134+
),
135+
)
136+
.configure_axis(
137+
labelFontSize=18,
138+
titleFontSize=22,
139+
titleFont="Helvetica Neue, Arial, sans-serif",
140+
labelFont="Helvetica Neue, Arial, sans-serif",
141+
titleColor="#333333",
142+
labelColor="#555555",
143+
grid=False,
144+
domain=False,
145+
tickColor="#aaaaaa",
146+
tickSize=5,
147+
tickWidth=0.6,
148+
)
149+
.configure_title(font="Helvetica Neue, Arial, sans-serif", color="#222222")
150+
.configure_view(strokeWidth=0)
151+
.interactive()
152+
)
153+
154+
# Save
155+
chart.save("plot.png", scale_factor=3.0)
156+
chart.save("plot.html")
Lines changed: 226 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,226 @@
1+
library: altair
2+
specification_id: line-arrhenius
3+
created: '2026-03-21T22:07:37Z'
4+
updated: '2026-03-21T22:20:06Z'
5+
generated_by: claude-opus-4-5-20251101
6+
workflow_run: 23389778092
7+
issue: 4408
8+
python_version: 3.14.3
9+
library_version: 6.0.0
10+
preview_url: https://storage.googleapis.com/pyplots-images/plots/line-arrhenius/altair/plot.png
11+
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/line-arrhenius/altair/plot_thumb.png
12+
preview_html: https://storage.googleapis.com/pyplots-images/plots/line-arrhenius/altair/plot.html
13+
quality_score: 90
14+
review:
15+
strengths:
16+
- Complete spec compliance with all required features (regression line, R², Ea annotation,
17+
temperature labels)
18+
- Strong design with removed spines/domain, clean typography, and effective annotation
19+
hierarchy
20+
- Idiomatic Altair usage with native transform_regression and interactive features
21+
- Realistic physical chemistry data with appropriate parameters
22+
- Clean, well-organized code structure
23+
weaknesses:
24+
- Temperature reference labels (14pt) could be slightly larger for better readability
25+
- Minor whitespace imbalance on the right edge of the canvas
26+
image_description: The plot displays an Arrhenius plot with "line-arrhenius · altair
27+
· pyplots.ai" as the title and a subtitle "First-Order Decomposition · Rate Constants
28+
vs Inverse Temperature". The x-axis shows "1/T (K⁻¹)" ranging from 0.00160 to
29+
0.00340, and the y-axis shows "ln(k)" ranging from approximately -3 to 14. A blue
30+
regression line (#306998 Python Blue) extends across the full data range with
31+
12 filled blue circular data points (white stroke) plotted on top. In the upper-left
32+
area, a bold blue annotation reads "Eₐ = 74.5 kJ/mol R² = 0.9995" and below it
33+
in gray italic "slope = −Eₐ/R = -8963 K". Along the bottom of the plot area, gray
34+
temperature labels (550 K, 500 K, 450 K, 400 K, 350 K, 300 K) provide secondary
35+
x-axis reference with a centered "Temperature (K)" label beneath them. The design
36+
is clean with no grid lines, no spines/domain lines, and a white background. Layout
37+
fills the canvas well in landscape format.
38+
criteria_checklist:
39+
visual_quality:
40+
score: 28
41+
max: 30
42+
items:
43+
- id: VQ-01
44+
name: Text Legibility
45+
score: 7
46+
max: 8
47+
passed: true
48+
comment: All font sizes explicitly set (title 28pt, subtitle 18pt, axis titles
49+
22pt, ticks 18pt, annotations 20/17pt). Temperature labels at 14pt slightly
50+
small.
51+
- id: VQ-02
52+
name: No Overlap
53+
score: 6
54+
max: 6
55+
passed: true
56+
comment: No overlapping elements anywhere in the plot.
57+
- id: VQ-03
58+
name: Element Visibility
59+
score: 6
60+
max: 6
61+
passed: true
62+
comment: 12 data points with size 350/500, clearly visible with white stroke
63+
separation.
64+
- id: VQ-04
65+
name: Color Accessibility
66+
score: 4
67+
max: 4
68+
passed: true
69+
comment: Single-color scheme (Python Blue), no colorblind concerns.
70+
- id: VQ-05
71+
name: Layout & Canvas
72+
score: 3
73+
max: 4
74+
passed: true
75+
comment: Good landscape layout. Minor whitespace imbalance on right side.
76+
- id: VQ-06
77+
name: Axis Labels & Title
78+
score: 2
79+
max: 2
80+
passed: true
81+
comment: 'Descriptive with units: 1/T (K⁻¹) and ln(k).'
82+
design_excellence:
83+
score: 15
84+
max: 20
85+
items:
86+
- id: DE-01
87+
name: Aesthetic Sophistication
88+
score: 6
89+
max: 8
90+
passed: true
91+
comment: Thoughtful design with Python Blue, Helvetica Neue, removed spines/domain.
92+
Clearly above defaults.
93+
- id: DE-02
94+
name: Visual Refinement
95+
score: 5
96+
max: 6
97+
passed: true
98+
comment: Grid disabled, domain/spines removed, view stroke removed, custom
99+
tick colors. Well-polished.
100+
- id: DE-03
101+
name: Data Storytelling
102+
score: 4
103+
max: 6
104+
passed: true
105+
comment: Annotations display key insight (Ea and R²), slope annotation provides
106+
interpretation, subtitle frames experiment.
107+
spec_compliance:
108+
score: 15
109+
max: 15
110+
items:
111+
- id: SC-01
112+
name: Plot Type
113+
score: 5
114+
max: 5
115+
passed: true
116+
comment: 'Correct Arrhenius plot: ln(k) vs 1/T with scatter and regression.'
117+
- id: SC-02
118+
name: Required Features
119+
score: 4
120+
max: 4
121+
passed: true
122+
comment: 'All spec features present: regression line, R², Ea/R annotation,
123+
temperature labels, visible data points.'
124+
- id: SC-03
125+
name: Data Mapping
126+
score: 3
127+
max: 3
128+
passed: true
129+
comment: X=1/T (K⁻¹), Y=ln(k) correctly mapped.
130+
- id: SC-04
131+
name: Title & Legend
132+
score: 3
133+
max: 3
134+
passed: true
135+
comment: Title format correct. No legend needed for single-series.
136+
data_quality:
137+
score: 14
138+
max: 15
139+
items:
140+
- id: DQ-01
141+
name: Feature Coverage
142+
score: 5
143+
max: 6
144+
passed: true
145+
comment: 12 data points spanning 300-600K with realistic scatter. Good coverage
146+
but uniform scatter.
147+
- id: DQ-02
148+
name: Realistic Context
149+
score: 5
150+
max: 5
151+
passed: true
152+
comment: First-order decomposition with Ea=75 kJ/mol, realistic chemistry
153+
scenario.
154+
- id: DQ-03
155+
name: Appropriate Scale
156+
score: 4
157+
max: 4
158+
passed: true
159+
comment: Temperature 300-600K and Ea=75 kJ/mol are realistic for reaction
160+
kinetics.
161+
code_quality:
162+
score: 10
163+
max: 10
164+
items:
165+
- id: CQ-01
166+
name: KISS Structure
167+
score: 3
168+
max: 3
169+
passed: true
170+
comment: 'Clean linear flow: imports, data, plot layers, combine, save.'
171+
- id: CQ-02
172+
name: Reproducibility
173+
score: 2
174+
max: 2
175+
passed: true
176+
comment: np.random.seed(42) set.
177+
- id: CQ-03
178+
name: Clean Imports
179+
score: 2
180+
max: 2
181+
passed: true
182+
comment: All imports used.
183+
- id: CQ-04
184+
name: Code Elegance
185+
score: 2
186+
max: 2
187+
passed: true
188+
comment: Appropriate complexity, well-organized layer composition.
189+
- id: CQ-05
190+
name: Output & API
191+
score: 1
192+
max: 1
193+
passed: true
194+
comment: Saves as plot.png and plot.html with current API.
195+
library_mastery:
196+
score: 8
197+
max: 10
198+
items:
199+
- id: LM-01
200+
name: Idiomatic Usage
201+
score: 5
202+
max: 5
203+
passed: true
204+
comment: 'Expert Altair patterns: layered composition, encoding types, alt.Title,
205+
configure methods.'
206+
- id: LM-02
207+
name: Distinctive Features
208+
score: 3
209+
max: 5
210+
passed: true
211+
comment: Uses transform_regression, selection_point, tooltips, .interactive(),
212+
HTML export.
213+
verdict: APPROVED
214+
impl_tags:
215+
dependencies: []
216+
techniques:
217+
- annotations
218+
- layer-composition
219+
- hover-tooltips
220+
- html-export
221+
patterns:
222+
- data-generation
223+
dataprep:
224+
- regression
225+
styling:
226+
- edge-highlighting

0 commit comments

Comments
 (0)