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