{"spec_id":"titration-curve","library":"seaborn","language":"python","code":"\"\"\" anyplot.ai\ntitration-curve: Acid-Base Titration Curve\nLibrary: seaborn 0.13.2 | Python 3.13.14\nQuality: 90/100 | Created: 2026-06-24\n\"\"\"\n\nimport os\nimport sys\n\n\n# Remove the script's own directory from sys.path so sibling files (e.g. matplotlib.py)\n# do not shadow installed packages when running from the implementations directory.\n_here = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p) != _here]\ndel _here\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nimport pandas as pd\nimport seaborn as sns\n\n\n# Theme tokens\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\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 — canonical order\nIMPRINT_PALETTE = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\", \"#2ABCCD\", \"#954477\", \"#99B314\"]\nBRAND = IMPRINT_PALETTE[0]  # #009E73 — pH curve (first series)\nDERIV_COLOR = IMPRINT_PALETTE[1]  # #C475FD — derivative dpH/dV (second series)\nINDICATOR_COLOR = IMPRINT_PALETTE[3]  # #BD8233 — phenolphthalein indicator range\n\n# Combined context + theme setup (seaborn-idiomatic: context scales all elements globally)\nsns.set_theme(\n    context=\"notebook\",\n    font_scale=0.85,\n    style=\"ticks\",\n    rc={\n        \"figure.facecolor\": PAGE_BG,\n        \"axes.facecolor\": PAGE_BG,\n        \"axes.edgecolor\": INK_SOFT,\n        \"axes.labelcolor\": INK,\n        \"text.color\": INK,\n        \"xtick.color\": INK_SOFT,\n        \"ytick.color\": INK_SOFT,\n        \"grid.color\": INK,\n        \"grid.alpha\": 0.15,\n        \"legend.facecolor\": ELEVATED_BG,\n        \"legend.edgecolor\": INK_SOFT,\n    },\n)\n\n# Data — 0.1 M HCl (25 mL) titrated with 0.1 M NaOH\n# Extra density near the equivalence point for a smooth inflection\nvol_naoh = np.unique(np.concatenate([np.linspace(0.0, 50.0, 150), np.linspace(24.2, 25.8, 200)]))\n\nn_hcl = 2.5  # mmol  (25 mL × 0.1 mmol/mL)\nn_naoh = vol_naoh * 0.1\ntotal_ml = 25.0 + vol_naoh\n\nconc_h = np.maximum((n_hcl - n_naoh) / total_ml, 1e-15)\nconc_oh = np.maximum((n_naoh - n_hcl) / total_ml, 1e-15)\n\nph = np.where(n_naoh < n_hcl, -np.log10(conc_h), np.where(n_naoh > n_hcl, 14.0 + np.log10(conc_oh), 7.0))\nph = np.clip(ph, 0.0, 14.0)\n\ndph_dv = np.gradient(ph, vol_naoh)\n\neq_vol = 25.0\neq_ph = 7.0\n\n# Simulate replicate titration measurements (fixed seed → fully deterministic)\n# Enables seaborn's native errorbar aggregation — not available in plain matplotlib\nrng = np.random.default_rng(42)\nn_reps = 8\nph_noise = rng.normal(0, 0.15, (n_reps, len(vol_naoh)))\nph_df = pd.DataFrame(\n    {\"volume\": np.tile(vol_naoh, n_reps), \"pH\": np.clip((ph[np.newaxis, :] + ph_noise).ravel(), 0.0, 14.0)}\n)\n\n# Plot\nfig, ax1 = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax1.set_facecolor(PAGE_BG)\n\n# Shaded transition zone (±3 mL around equivalence point)\nax1.axvspan(eq_vol - 3.0, eq_vol + 3.0, alpha=0.07, color=BRAND, zorder=1, label=\"Transition zone\")\n\n# Phenolphthalein indicator range (pH 8.2–10.0) — guides endpoint indicator selection\nax1.axhspan(8.2, 10.0, alpha=0.08, color=INDICATOR_COLOR, zorder=1, label=\"Phenolphthalein range\")\n\n# Primary curve — seaborn lineplot with SD confidence band from replicate data\n# errorbar='sd' is seaborn-native statistical aggregation over the replicate DataFrame\nsns.lineplot(\n    data=ph_df,\n    x=\"volume\",\n    y=\"pH\",\n    ax=ax1,\n    color=BRAND,\n    linewidth=2.5,\n    errorbar=\"sd\",\n    err_kws={\"alpha\": 0.22, \"linewidth\": 0},\n    label=\"pH\",\n    zorder=3,\n)\n\nax1.set_xlim(0, 50)\nax1.set_ylim(0, 14)\nax1.set_xlabel(\"Volume of NaOH added (mL)\", fontsize=10, color=INK)\nax1.set_ylabel(\"pH\", fontsize=10, color=INK)\nax1.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT)\nax1.yaxis.grid(True, alpha=0.15, linewidth=0.8, color=INK, zorder=0)\n\n# Equivalence point — dashed vertical marker\nax1.axvline(x=eq_vol, color=INK_MUTED, linestyle=\"--\", linewidth=1.2, alpha=0.85, zorder=2)\nax1.annotate(\n    f\"Equiv. point\\n{eq_vol:.0f} mL · pH {eq_ph:.0f}\",\n    xy=(eq_vol, eq_ph),\n    xytext=(33.0, 3.0),\n    fontsize=8,\n    color=INK_SOFT,\n    arrowprops={\"arrowstyle\": \"->\", \"color\": INK_MUTED, \"lw\": 0.9},\n    zorder=5,\n)\n\n# Secondary axis — derivative dpH/dV locates the equivalence point precisely\nax2 = ax1.twinx()\nax2.patch.set_visible(False)\n\n# Fill under spike for strong visual prominence, then overlay the seaborn line\nax2.fill_between(vol_naoh, 0, dph_dv, alpha=0.20, color=DERIV_COLOR, zorder=2)\nsns.lineplot(x=vol_naoh, y=dph_dv, ax=ax2, color=DERIV_COLOR, linewidth=2.0, label=\"dpH/dV\", zorder=3)\n\nax2.set_ylabel(\"dpH / dV  (pH / mL)\", fontsize=10, color=DERIV_COLOR)\nax2.tick_params(axis=\"y\", labelsize=8, colors=DERIV_COLOR)\nax2.set_ylim(0, np.max(dph_dv) * 1.5)\n\n# Spine styling — sns.despine for ax1 (seaborn-idiomatic), manual for ax2\nsns.despine(ax=ax1, right=True, top=True)\nax1.spines[\"left\"].set_color(INK_SOFT)\nax1.spines[\"bottom\"].set_color(INK_SOFT)\nax2.spines[\"top\"].set_visible(False)\nax2.spines[\"left\"].set_visible(False)\nax2.spines[\"bottom\"].set_visible(False)\nax2.spines[\"right\"].set_visible(True)\nax2.spines[\"right\"].set_color(DERIV_COLOR)\nax2.spines[\"right\"].set_alpha(0.6)\n\n# Combined legend (primary + secondary axes) — remove ax2 auto-legend first\nh1, l1 = ax1.get_legend_handles_labels()\nh2, l2 = ax2.get_legend_handles_labels()\nif ax2.get_legend():\n    ax2.get_legend().remove()\nax1.legend(h1 + h2, l1 + l2, fontsize=8, loc=\"upper left\", facecolor=ELEVATED_BG, edgecolor=INK_SOFT, framealpha=0.9)\n\n# Title\ntitle = \"titration-curve · python · seaborn · anyplot.ai\"\nax1.set_title(title, fontsize=12, fontweight=\"medium\", color=INK)\n\nfig.subplots_adjust(left=0.08, right=0.88, top=0.92, bottom=0.12)\n\n# Save — no bbox_inches='tight' per seaborn canvas rule (figsize × dpi = exact target)\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\nplt.close()\n"}