{"spec_id":"titration-curve","library":"letsplot","language":"python","code":"\"\"\" anyplot.ai\ntitration-curve: Acid-Base Titration Curve\nLibrary: letsplot 4.10.1 | Python 3.13.14\nQuality: 85/100 | Updated: 2026-06-24\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nfrom lets_plot import *\nfrom lets_plot import ggsave\n\n\nLetsPlot.setup_html()\n\n# Theme tokens — Imprint palette, theme-adaptive chrome\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\"\nRULE = \"#E0DFDA\" if THEME == \"light\" else \"#2E2E2B\"\n\n# Imprint categorical palette — theme-independent data colors\nBRAND = \"#009E73\"  # position 1: pH curve (first series, always brand green)\nCOLOR_DERIV = \"#C475FD\"  # position 2: derivative curve (lavender)\nCOLOR_EQUIV = \"#AE3030\"  # position 5: equivalence point (matte red — semantic: critical marker)\nCOLOR_BUFFER = \"#4467A3\"  # position 3: buffer region (blue — water/chemistry context)\n\n# Data — Strong acid/strong base: 25 mL of 0.1 M HCl titrated with 0.1 M NaOH\nconcentration_acid = 0.1\nvolume_acid = 25.0\nconcentration_base = 0.1\n\nvolume_naoh = np.linspace(0, 50, 200)\nph = np.zeros_like(volume_naoh)\n\nfor i, v in enumerate(volume_naoh):\n    moles_acid = concentration_acid * volume_acid / 1000\n    moles_base = concentration_base * v / 1000\n    total_volume = (volume_acid + v) / 1000\n\n    if moles_base < moles_acid:\n        excess_h = (moles_acid - moles_base) / total_volume\n        ph[i] = -np.log10(excess_h)\n    elif np.isclose(moles_base, moles_acid, atol=1e-8):\n        ph[i] = 7.0\n    else:\n        excess_oh = (moles_base - moles_acid) / total_volume\n        poh = -np.log10(excess_oh)\n        ph[i] = 14.0 - poh\n\n# Derivative (dpH/dV) — scaled to pH axis range for same-axis overlay\ndpH = np.gradient(ph, volume_naoh)\ndpH_max = np.max(dpH)\ndpH_scaled = dpH / dpH_max * 12.0\n\n# Equivalence point (strong acid/strong base → pH 7.0)\nequiv_volume = volume_acid * concentration_acid / concentration_base\nequiv_ph = 7.0\n\n# Buffer region: pre-equivalence low-slope area (~2–20 mL for this titration)\nbuffer_mask = (volume_naoh >= 2) & (volume_naoh <= 20)\ndf_buffer = pd.DataFrame({\"volume_ml\": volume_naoh[buffer_mask], \"ph\": ph[buffer_mask]})\n\n# Series data — color mapped by series name (alphabetical: \"dpH/dV…\" before \"pH Curve\")\ndf_ph = pd.DataFrame({\"volume_ml\": volume_naoh, \"y\": ph, \"series\": \"pH Curve\"})\ndf_deriv = pd.DataFrame({\"volume_ml\": volume_naoh, \"y\": dpH_scaled, \"series\": \"dpH/dV (scaled)\"})\n\nequiv_point = pd.DataFrame(\n    {\n        \"volume_ml\": [equiv_volume],\n        \"ph\": [equiv_ph],\n        \"label\": [f\"Equivalence Point\\n{equiv_volume:.0f} mL, pH {equiv_ph:.1f}\"],\n    }\n)\n\n# Theme-adaptive chrome — see default-style-guide.md \"Theme-adaptive Chrome\"\nanyplot_theme = theme(\n    plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n    panel_background=element_rect(fill=PAGE_BG),\n    panel_grid_major_y=element_line(color=RULE, size=0.5),\n    panel_grid_major_x=element_blank(),\n    panel_grid_minor=element_blank(),\n    panel_border=element_blank(),\n    axis_line=element_blank(),\n    axis_title=element_text(color=INK, size=12, face=\"bold\"),\n    axis_text=element_text(color=INK_SOFT, size=10),\n    plot_title=element_text(color=INK, size=16, hjust=0.5, face=\"bold\"),\n    plot_subtitle=element_text(color=INK_SOFT, size=12, hjust=0.5),\n    legend_text=element_text(color=INK_SOFT, size=10),\n    legend_title=element_text(color=INK),\n    legend_background=element_rect(fill=ELEVATED_BG),\n    legend_position=[0.85, 0.15],\n    legend_justification=[0.5, 0.5],\n    plot_margin=[20, 20, 10, 20],\n)\n\n# Plot\nplot = (\n    ggplot()\n    # Acidic region background (pH 0–7): subtle red tint\n    + geom_rect(\n        aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\"),\n        data=pd.DataFrame({\"xmin\": [0], \"xmax\": [50], \"ymin\": [0], \"ymax\": [7]}),\n        fill=COLOR_EQUIV,\n        alpha=0.10,\n        tooltips=\"none\",\n    )\n    # Basic region background (pH 7–14): subtle blue tint\n    + geom_rect(\n        aes(xmin=\"xmin\", xmax=\"xmax\", ymin=\"ymin\", ymax=\"ymax\"),\n        data=pd.DataFrame({\"xmin\": [0], \"xmax\": [50], \"ymin\": [7], \"ymax\": [14]}),\n        fill=COLOR_BUFFER,\n        alpha=0.10,\n        tooltips=\"none\",\n    )\n    # pH 7 neutral divider line\n    + geom_hline(yintercept=7.0, color=INK_MUTED, size=0.5, linetype=\"dotted\")\n    # Buffer region shading — alpha raised from 0.1 to 0.25 for clear visibility\n    + geom_area(\n        aes(x=\"volume_ml\", y=\"ph\"),\n        data=df_buffer,\n        fill=COLOR_BUFFER,\n        alpha=0.25,\n        tooltips=\"none\",\n    )\n    # Region labels\n    + geom_text(\n        aes(x=\"x\", y=\"y\", label=\"label\"),\n        data=pd.DataFrame({\"x\": [44], \"y\": [2.5], \"label\": [\"Acidic\"]}),\n        size=10,\n        color=COLOR_EQUIV,\n        fontface=\"bold\",\n        alpha=0.6,\n    )\n    + geom_text(\n        aes(x=\"x\", y=\"y\", label=\"label\"),\n        data=pd.DataFrame({\"x\": [44], \"y\": [12.0], \"label\": [\"Basic\"]}),\n        size=10,\n        color=COLOR_BUFFER,\n        fontface=\"bold\",\n        alpha=0.6,\n    )\n    + geom_text(\n        aes(x=\"x\", y=\"y\", label=\"label\"),\n        data=pd.DataFrame({\"x\": [11], \"y\": [2.5], \"label\": [\"Buffer Region\"]}),\n        size=8,\n        color=COLOR_BUFFER,\n        fontface=\"italic\",\n        alpha=0.7,\n    )\n    # Derivative curve (dashed — secondary series on same axis)\n    + geom_line(\n        aes(x=\"volume_ml\", y=\"y\", color=\"series\"),\n        data=df_deriv,\n        size=1.5,\n        linetype=\"dashed\",\n        alpha=0.9,\n        tooltips=layer_tooltips().line(\"dpH/dV (scaled)\"),\n    )\n    # Main pH titration curve\n    + geom_line(\n        aes(x=\"volume_ml\", y=\"y\", color=\"series\"),\n        data=df_ph,\n        size=2.0,\n        tooltips=layer_tooltips()\n        .title(\"pH Curve\")\n        .format(\"volume_ml\", \".1f\")\n        .format(\"y\", \".2f\")\n        .line(\"Volume: @volume_ml mL\")\n        .line(\"pH: @y\"),\n    )\n    # Equivalence point vertical marker and diamond\n    + geom_vline(xintercept=equiv_volume, color=COLOR_EQUIV, size=0.8, linetype=\"dashed\", alpha=0.7)\n    + geom_point(\n        aes(x=\"volume_ml\", y=\"ph\"),\n        data=equiv_point,\n        color=COLOR_EQUIV,\n        size=8,\n        shape=18,\n    )\n    + geom_label(\n        aes(x=\"volume_ml\", y=\"ph\", label=\"label\"),\n        data=equiv_point,\n        size=9,\n        color=COLOR_EQUIV,\n        fill=ELEVATED_BG,\n        label_padding=0.4,\n        label_r=0.15,\n        fontface=\"bold\",\n        nudge_x=8,\n        nudge_y=1.8,\n    )\n    # Color scale: alphabetical order → \"dpH/dV (scaled)\" → lavender, \"pH Curve\" → brand green\n    + scale_color_manual(values=[\"#C475FD\", \"#009E73\"], name=\"\")\n    + scale_x_continuous(limits=[0, 50], breaks=list(range(0, 55, 5)))\n    + scale_y_continuous(limits=[0, 14], breaks=list(range(0, 15, 2)))\n    + labs(\n        x=\"Volume of NaOH added (mL)\",\n        y=\"pH\",\n        title=\"titration-curve · python · letsplot · anyplot.ai\",\n        subtitle=\"Strong Acid–Base: 25 mL of 0.1 M HCl titrated with 0.1 M NaOH\",\n    )\n    + anyplot_theme\n    + ggsize(800, 450)\n)\n\n# Save — PNG (scale=4 → 3200×1800) and HTML (interactive)\nggsave(plot, f\"plot-{THEME}.png\", path=\".\", scale=4)\nggsave(plot, f\"plot-{THEME}.html\", path=\".\")\n"}