{"spec_id":"titration-curve","library":"pygal","language":"python","code":"\"\"\" anyplot.ai\ntitration-curve: Acid-Base Titration Curve\nLibrary: pygal 3.1.3 | Python 3.13.14\nQuality: 84/100 | Updated: 2026-06-24\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent the local pygal.py from shadowing the installed pygal package\nsys.path = [p for p in sys.path if p and os.path.abspath(p) != os.path.abspath(os.path.dirname(__file__))]\n\nimport numpy as np\nimport pygal\nfrom pygal.style import Style\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 — semantic positions for this chart\n# Series add() order: pH (1) → dpH/dV (2) → equivalence (3) → pH-7 ref (4) → annotation (5)\nCHART_PALETTE = (\n    \"#009E73\",  # pH curve — Imprint brand green (position 1)\n    \"#C475FD\",  # dpH/dV derivative — Imprint lavender (position 2)\n    \"#AE3030\",  # equivalence point — matte red (semantic: critical threshold)\n    INK_MUTED,  # pH 7 reference — muted neutral (structural reference)\n    \"#AE3030\",  # equivalence annotation text (matches line color)\n)\n\n# Data — 25 mL of 0.1 M HCl titrated with 0.1 M NaOH\nca, va = 0.1, 25.0\ncb = 0.1\nequivalence_vol = va * ca / cb  # 25.0 mL\n\nvolume_ml = np.linspace(0.01, 50.0, 500)\nph = np.empty_like(volume_ml)\nfor i, v in enumerate(volume_ml):\n    moles_h = ca * va - cb * v\n    total_vol = va + v\n    if moles_h > 1e-10:\n        ph[i] = -np.log10(moles_h / total_vol)\n    elif moles_h < -1e-10:\n        oh_conc = -moles_h / total_vol\n        ph[i] = 14.0 + np.log10(oh_conc)\n    else:\n        ph[i] = 7.0\n\n# Derivative dpH/dV — normalised 0-1 so the right axis has clean labels\ndpH_dV = np.gradient(ph, volume_ml)\ndpH_dV = np.clip(dpH_dV, 0, None)\ndpH_dV = dpH_dV / dpH_dV.max()  # peak = 1.0\n\neq_vol = equivalence_vol\neq_ph = 7.0\n\n# Title — 45 chars < 67 baseline → full title_font_size=66\ntitle_str = \"titration-curve · python · pygal · anyplot.ai\"\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=CHART_PALETTE,\n    title_font_size=66,\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    font_family=\"'Helvetica Neue', 'Helvetica', 'Arial', sans-serif\",\n)\n\n# Subsample — 100 points gives a smooth curve with fast render\nstep = 5\ncurve_pts = [(float(volume_ml[i]), float(ph[i])) for i in range(0, len(volume_ml), step)]\nderiv_pts = [(float(volume_ml[i]), float(dpH_dV[i])) for i in range(0, len(volume_ml), step)]\neq_line_pts = [(float(eq_vol), 0.0), (float(eq_vol), 14.0)]\nref_ph7_pts = [(0.0, 7.0), (50.0, 7.0)]\n\n# Chart — single XY with secondary y-axis for dpH/dV overlay\nchart = pygal.XY(\n    style=custom_style,\n    width=3200,\n    height=1800,\n    title=title_str,\n    x_title=\"Volume of NaOH added (mL)\",\n    y_title=\"pH\",\n    secondary_range=(0.0, 0.7),\n    show_dots=False,\n    show_x_guides=False,\n    show_y_guides=True,\n    range=(0.0, 14.0),\n    xrange=(0.0, 50.0),\n    legend_at_bottom=True,\n    legend_at_bottom_columns=2,\n    legend_box_size=30,\n    truncate_legend=-1,\n    margin=30,\n    margin_top=100,\n    margin_left=180,\n    margin_right=200,\n    margin_bottom=160,\n    tooltip_fancy_mode=True,\n    x_value_formatter=lambda x: f\"{x:.1f}\",\n    y_value_formatter=lambda y: f\"{y:.1f}\",\n)\n\n# Series — color order follows CHART_PALETTE positions\nchart.add(\n    \"pH (0.1 M HCl + NaOH)\",\n    curve_pts,\n    show_dots=False,\n    stroke_style={\"width\": 8, \"linecap\": \"round\", \"linejoin\": \"round\"},\n)\n\nchart.add(\n    \"dpH/dV (normalised, right axis)\",\n    deriv_pts,\n    secondary=True,\n    show_dots=False,\n    stroke_style={\"width\": 6, \"linecap\": \"round\", \"linejoin\": \"round\"},\n)\n\nchart.add(\n    f\"Equivalence ({eq_vol:.0f} mL, pH {eq_ph:.0f})\",\n    eq_line_pts,\n    show_dots=False,\n    stroke_style={\"width\": 3, \"dasharray\": \"14,8\"},\n)\n\nchart.add(\"pH 7 Reference\", ref_ph7_pts, show_dots=False, stroke_style={\"width\": 2, \"dasharray\": \"6,6\"})\n\n# Direct text annotation at equivalence point — positioned to the right of the vertical line\nann_label = f\"{eq_vol:.0f} mL, pH {eq_ph:.0f}\"\nchart.add(\n    f\"Eq. point: {ann_label}\",\n    [(float(eq_vol + 2.5), float(eq_ph + 2.0))],\n    show_dots=True,\n    dots_size=10,\n    print_values=True,\n    value_formatter=lambda _: ann_label,\n    stroke=False,\n)\n\n# Save\nchart.render_to_png(f\"plot-{THEME}.png\")\n\nwith open(f\"plot-{THEME}.html\", \"wb\") as f:\n    f.write(chart.render())\n"}