{"spec_id":"line-arrhenius","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\nline-arrhenius: Arrhenius Plot for Reaction Kinetics\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 87/100 | Updated: 2026-06-24\n\"\"\"\n\nimport os\nimport re\nimport sys\n\n\n# Script filename shadows the installed 'pygal' package when run as 'python pygal.py';\n# dropping the script directory from sys.path lets the real package resolve.\nsys.path.pop(0)\n\nimport cairosvg\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\nfrom scipy import stats\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette — canonical positions 1→8, first series always #009E73\nIMPRINT_PALETTE = (\n    \"#009E73\",  # brand green  — position 1\n    \"#C475FD\",  # lavender     — position 2\n    \"#4467A3\",  # blue         — position 3\n    \"#BD8233\",  # ochre        — position 4\n    \"#AE3030\",  # matte red    — position 5\n    \"#2ABCCD\",  # cyan         — position 6\n    \"#954477\",  # rose         — position 7\n    \"#99B314\",  # lime         — position 8\n)\n\n# Data — first-order decomposition reaction rate constants spanning 300–600 K\ntemperature_K = np.array([300, 330, 360, 400, 440, 480, 520, 560, 600])\nnp.random.seed(42)\nactivation_energy = 75000  # J/mol (75 kJ/mol)\nR = 8.314  # gas constant J/(mol·K)\npre_exponential = 1.0e12  # s⁻¹\nrate_constant_k = pre_exponential * np.exp(-activation_energy / (R * temperature_K))\nrate_constant_k *= np.exp(np.random.normal(0, 0.25, len(temperature_K)))\n\n# Arrhenius transformed coordinates\ninv_T = 1000.0 / temperature_K  # 1000/T (×10⁻³ K⁻¹) for readable x-axis\nln_k = np.log(rate_constant_k)\n\n# Linear regression: ln(k) = ln(A) − Ea/R × (1/T)\nslope, intercept, r_value, p_value, std_err = stats.linregress(inv_T, ln_k)\nr_squared = r_value**2\nEa_extracted = -slope * R * 1000  # factor of 1000 accounts for the 1000/T scaling\n\n# Smooth regression line — 80 points, slightly extended beyond data\nx_pad = 0.04\ninv_T_fit = np.linspace(float(min(inv_T)) - x_pad, float(max(inv_T)) + x_pad, 80)\nln_k_fit = slope * inv_T_fit + intercept\n\n# Y-axis range tight to data\ny_floor = int(np.floor(float(min(ln_k))))\ny_ceil = int(np.ceil(float(max(ln_k))))\ny_labels_list = list(range(y_floor, y_ceil + 1, 2))\nif y_labels_list[-1] < y_ceil:\n    y_labels_list.append(y_ceil)\n\n# Title fontsize — scaled from 67-char baseline (44 chars here, so ratio=1.0)\ntitle = \"line-arrhenius · python · pygal · anyplot.ai\"\nn = len(title)\nratio = 67 / n if n > 67 else 1.0\ntitle_font_size = max(44, round(66 * ratio))\n\n# Style — Imprint palette + theme-adaptive chrome\ncustom_style = Style(\n    background=PAGE_BG,\n    plot_background=PAGE_BG,\n    foreground=INK,\n    foreground_strong=INK,\n    foreground_subtle=INK_MUTED,\n    colors=IMPRINT_PALETTE,\n    title_font_size=title_font_size,\n    label_font_size=56,\n    major_label_font_size=44,\n    legend_font_size=44,\n    value_font_size=36,\n    stroke_width=2.5,\n    opacity=0.92,\n    opacity_hover=1.0,\n    title_font_family=\"sans-serif\",\n    label_font_family=\"sans-serif\",\n    major_label_font_family=\"sans-serif\",\n    legend_font_family=\"sans-serif\",\n    value_font_family=\"sans-serif\",\n)\n\n# Chart — canonical 3200×1800 landscape canvas\nchart = pygal.XY(\n    style=custom_style,\n    width=3200,\n    height=1800,\n    title=title,\n    x_title=\"1000/T (K⁻¹)\",\n    y_title=\"ln(k)\",\n    show_dots=True,\n    dots_size=12,\n    show_x_guides=False,\n    show_y_guides=True,\n    legend_at_bottom=True,\n    legend_at_bottom_columns=3,\n    legend_box_size=24,\n    truncate_legend=-1,\n    margin=40,\n    margin_top=70,\n    margin_bottom=140,\n    margin_left=140,\n    margin_right=60,\n    tooltip_fancy_mode=True,\n    tooltip_border_radius=8,\n    x_value_formatter=lambda x: f\"{x:.2f}\",\n    y_value_formatter=lambda y: f\"{y:.1f}\",\n    range=(y_floor - 0.5, y_ceil + 0.5),\n    xrange=(float(min(inv_T) - 0.1), float(max(inv_T) + 0.1)),\n    y_labels=y_labels_list,\n    y_labels_major_every=1,\n    print_values=False,\n    show_minor_x_labels=False,\n    css=[\n        \"file://style.css\",\n        \"file://graph.css\",\n        \"inline:\"\n        \".axis > .line { stroke: transparent !important; } \"\n        \".plot .background { rx: 10; ry: 10; } \"\n        \".legends .legend text { font-weight: 500; } \"\n        \".title { font-weight: 600; letter-spacing: 1px; }\",\n    ],\n)\n\n# X-axis labels: 1000/T value with corresponding temperature in parentheses\nx_label_temps = np.array([300, 360, 440, 520, 600])\nx_label_positions = sorted(1000.0 / x_label_temps)\nchart.x_labels = [float(x) for x in x_label_positions]\nchart.x_labels_major = [float(x) for x in x_label_positions]\nchart.x_label_rotation = 0\nchart.x_value_formatter = lambda x: f\"{x:.2f} ({int(round(1000.0 / x))} K)\"\n\n# Regression fit line — smooth, no dots\nfit_points = [\n    {\"value\": (float(x), float(y)), \"label\": f\"Fit: ln(k) = {slope:.2f} × (1000/T) + {intercept:.2f}\"}\n    for x, y in zip(inv_T_fit, ln_k_fit, strict=False)\n]\nchart.add(\n    f\"Linear Fit (R² = {r_squared:.3f})\",\n    fit_points,\n    show_dots=False,\n    stroke_style={\"width\": 4, \"linecap\": \"round\", \"linejoin\": \"round\"},\n)\n\n# Experimental data points — ochre markers with rich tooltips\ndata_points = [\n    {\"value\": (float(x), float(y)), \"label\": f\"T = {int(t)} K\\nk = {k:.3e} s⁻¹\\nln(k) = {y:.2f}\\n1000/T = {x:.3f}\"}\n    for x, y, t, k in zip(inv_T, ln_k, temperature_K, rate_constant_k, strict=False)\n]\nchart.add(\"Experimental Data\", data_points, stroke=False, dots_size=16)\n\n# Activation energy — visible dot + label at regression line midpoint\nmid_x = float(np.median(inv_T))\nmid_y = float(slope * mid_x + intercept)\nea_r = -slope * 1000  # Ea/R in K (accounts for 1000/T axis scaling)\nchart.add(\n    f\"Eₐ = {Ea_extracted / 1000:.1f} kJ/mol  (Eₐ/R = {ea_r:.0f} K)\",\n    [{\"value\": (mid_x, mid_y), \"label\": f\"Eₐ/R = {ea_r:.0f} K\"}],\n    dots_size=8,\n    stroke=False,\n    print_labels=True,\n)\n\n# Render SVG and inject Ea/R slope annotation as a text element on the chart\nsvg_bytes = chart.render()\nsvg_str = svg_bytes.decode(\"utf-8\")\n\n# Locate the annotation dot (dots_size=8 → r=\"8\") and place label next to it\ndot_match = re.search(r'<circle cx=\"([^\"]+)\" cy=\"([^\"]+)\" r=\"8\"', svg_str)\nif dot_match:\n    cx, cy = float(dot_match.group(1)), float(dot_match.group(2))\n    annotation = (\n        f'<text x=\"{cx + 70:.0f}\" y=\"{cy - 90:.0f}\" '\n        f'font-family=\"sans-serif\" font-size=\"44\" '\n        f'fill=\"{INK}\" font-weight=\"500\">'\n        f\"Eₐ/R = {ea_r:.0f} K</text>\"\n    )\n    svg_str = svg_str.replace(\"</svg>\", annotation + \"\\n</svg>\")\n\nannotated_svg = svg_str.encode(\"utf-8\")\ncairosvg.svg2png(bytestring=annotated_svg, write_to=f\"plot-{THEME}.png\")\nchart.render_to_file(f\"plot-{THEME}.html\")\n"}