An Arrhenius plot displays ln(k) versus 1/T to determine the activation energy of a chemical reaction from experimental rate constant data. The Arrhenius equation predicts a linear relationship on this transformed scale, where the slope equals -Ea/R (activation energy divided by the gas constant). This visualization is fundamental in physical chemistry and chemical engineering for characterizing reaction kinetics and comparing catalytic performance.

""" anyplot.ai
line-arrhenius: Arrhenius Plot for Reaction Kinetics
Library: altair 6.2.2 | Python 3.13.14
Quality: 90/100 | Updated: 2026-06-24
"""
import os
import altair as alt
import numpy as np
import pandas as pd
from PIL import Image
THEME = os.getenv("ANYPLOT_THEME", "light")
# Theme-adaptive chrome (Imprint palette)
PAGE_BG = "#FAF8F1" if THEME == "light" else "#1A1A17"
ELEVATED_BG = "#FFFDF6" if THEME == "light" else "#242420"
INK = "#1A1A17" if THEME == "light" else "#F0EFE8"
INK_SOFT = "#4A4A44" if THEME == "light" else "#B8B7B0"
INK_MUTED = "#6B6A63" if THEME == "light" else "#A8A79F"
# Imprint palette — first series always #009E73
BRAND = "#009E73"
# Data — first-order decomposition reaction rate constants at various temperatures
np.random.seed(42)
temperature_K = np.array([300, 325, 350, 375, 400, 425, 450, 475, 500, 525, 550, 600])
R = 8.314 # J/(mol·K)
Ea_true = 75000 # J/mol (75 kJ/mol)
A_true = 1.5e12 # Pre-exponential factor (s⁻¹)
# Arrhenius equation: k = A * exp(-Ea / (R*T))
ln_k_true = np.log(A_true) - Ea_true / (R * temperature_K)
ln_k_measured = ln_k_true + np.random.normal(0, 0.15, len(temperature_K))
inv_T = 1.0 / temperature_K # 1/T in K⁻¹
# Regression parameters for annotations
coeffs = np.polyfit(inv_T, ln_k_measured, 1)
slope_fit, intercept_fit = coeffs
y_pred = slope_fit * inv_T + intercept_fit
ss_res = np.sum((ln_k_measured - y_pred) ** 2)
ss_tot = np.sum((ln_k_measured - np.mean(ln_k_measured)) ** 2)
r_squared = 1 - ss_res / ss_tot
Ea_fit = -slope_fit * R # Activation energy in J/mol
# DataFrame
data_df = pd.DataFrame({"inv_T": inv_T, "ln_k": ln_k_measured, "T_K": temperature_K})
# Shared scales
x_scale = alt.Scale(domain=[inv_T.min() - 0.0001, inv_T.max() + 0.0001], nice=False)
y_scale = alt.Scale(domain=[ln_k_measured.min() - 1.2, ln_k_measured.max() + 1.2])
# Regression line via native transform_regression
reg_line = (
alt.Chart(data_df)
.mark_line(strokeWidth=2.5, color=BRAND)
.transform_regression("inv_T", "ln_k", extent=[inv_T.min() - 0.00005, inv_T.max() + 0.00005])
.encode(x=alt.X("inv_T:Q", scale=x_scale), y=alt.Y("ln_k:Q", scale=y_scale))
)
# Data points with interactive hover highlight
highlight = alt.selection_point(on="pointerover", nearest=True, empty=False)
points = (
alt.Chart(data_df)
.mark_point(filled=True, color=BRAND, stroke="white", strokeWidth=1.5)
.encode(
x=alt.X("inv_T:Q", scale=x_scale, title="1/T (K⁻¹)"),
y=alt.Y("ln_k:Q", scale=y_scale, title="ln(k)"),
size=alt.condition(highlight, alt.value(350), alt.value(200)),
tooltip=[
alt.Tooltip("T_K:Q", title="Temperature", format=".0f"),
alt.Tooltip("inv_T:Q", title="1/T", format=".5f"),
alt.Tooltip("ln_k:Q", title="ln(k)", format=".2f"),
],
)
.add_params(highlight)
)
# Annotations: Ea and R²
ea_kj = Ea_fit / 1000
annotation_text = f"Eₐ = {ea_kj:.1f} kJ/mol · R² = {r_squared:.4f}"
slope_text = f"slope = −Eₐ/R = {slope_fit:.0f} K"
annotation_df = pd.DataFrame(
{"inv_T": [inv_T.min() + 0.0002], "ln_k": [ln_k_measured.max() + 0.7], "text": [annotation_text]}
)
slope_ann_df = pd.DataFrame(
{"inv_T": [inv_T.min() + 0.0002], "ln_k": [ln_k_measured.max() + 0.15], "text": [slope_text]}
)
ea_label = (
alt.Chart(annotation_df)
.mark_text(fontSize=12, align="left", fontWeight="bold", color=BRAND)
.encode(x=alt.X("inv_T:Q", scale=x_scale), y=alt.Y("ln_k:Q", scale=y_scale), text="text:N")
)
slope_label = (
alt.Chart(slope_ann_df)
.mark_text(fontSize=11, align="left", fontStyle="italic", color=INK_SOFT)
.encode(x=alt.X("inv_T:Q", scale=x_scale), y=alt.Y("ln_k:Q", scale=y_scale), text="text:N")
)
# Secondary x-axis: temperature reference labels at data point positions
temp_labels_df = pd.DataFrame(
{
"inv_T": inv_T[::2],
"ln_k": [ln_k_measured.min() - 0.5] * len(inv_T[::2]),
"text": [f"{int(t)} K" for t in temperature_K[::2]],
}
)
temp_tick_labels = (
alt.Chart(temp_labels_df)
.mark_text(fontSize=11, color=INK_MUTED, angle=0)
.encode(x=alt.X("inv_T:Q", scale=x_scale), y=alt.Y("ln_k:Q", scale=y_scale), text="text:N")
)
temp_axis_label_df = pd.DataFrame(
{"inv_T": [(inv_T.min() + inv_T.max()) / 2], "ln_k": [ln_k_measured.min() - 0.9], "text": ["Temperature (K)"]}
)
temp_axis_label = (
alt.Chart(temp_axis_label_df)
.mark_text(fontSize=11, color=INK_MUTED, fontStyle="italic")
.encode(x=alt.X("inv_T:Q", scale=x_scale), y=alt.Y("ln_k:Q", scale=y_scale), text="text:N")
)
# Reference rule at ln(k) = 0 (rate constant = 1 s⁻¹ boundary)
zero_rule = (
alt.Chart(pd.DataFrame({"y": [0]}))
.mark_rule(strokeDash=[4, 4], color=INK_MUTED, opacity=0.35, strokeWidth=0.8)
.encode(y=alt.Y("y:Q", scale=y_scale))
)
# Combine all layers
chart = (
alt.layer(zero_rule, reg_line, points, ea_label, slope_label, temp_tick_labels, temp_axis_label)
.properties(
width=620,
height=320,
background=PAGE_BG,
title=alt.Title(
"line-arrhenius · python · altair · anyplot.ai",
fontSize=16,
anchor="middle",
color=INK,
subtitle="First-Order Decomposition · Rate Constants vs Inverse Temperature",
subtitleFontSize=12,
subtitleColor=INK_SOFT,
subtitlePadding=6,
),
)
.configure_view(fill=PAGE_BG, strokeWidth=0)
.configure_axis(
labelFontSize=10,
titleFontSize=12,
titleFont="Helvetica Neue, Arial, sans-serif",
labelFont="Helvetica Neue, Arial, sans-serif",
titleColor=INK,
labelColor=INK_SOFT,
grid=False,
domain=False,
tickColor=INK_MUTED,
tickSize=5,
tickWidth=0.6,
)
.configure_axisY(grid=True, gridColor=INK_MUTED, gridOpacity=0.15, gridWidth=0.5)
.configure_title(font="Helvetica Neue, Arial, sans-serif", color=INK)
.interactive()
)
# Save PNG (scale_factor=4.0) then pad to exact 3200×1800 target
chart.save(f"plot-{THEME}.png", scale_factor=4.0)
TW, TH = 3200, 1800
_img = Image.open(f"plot-{THEME}.png").convert("RGB")
_w, _h = _img.size
if _w > TW or _h > TH:
raise SystemExit(
f"altair vl-convert produced {_w}×{_h}, exceeds target {TW}×{TH}. "
f"Shrink chart .properties(width=, height=) values and re-render."
)
if _w < TW or _h < TH:
_bg = tuple(int(PAGE_BG.lstrip("#")[i : i + 2], 16) for i in (0, 2, 4))
_canvas = Image.new("RGB", (TW, TH), _bg)
_canvas.paste(_img, ((TW - _w) // 2, (TH - _h) // 2))
_canvas.save(f"plot-{THEME}.png")
chart.save(f"plot-{THEME}.html")
Part of Arrhenius Plot for Reaction Kinetics on anyplot.ai.