{"spec_id":"titration-curve","library":"plotnine","language":"python","code":"\"\"\" anyplot.ai\ntitration-curve: Acid-Base Titration Curve\nLibrary: plotnine 0.15.7 | Python 3.13.14\nQuality: 85/100 | Updated: 2026-06-24\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent this file from shadowing the installed plotnine package\nsys.path = [p for p in sys.path if p not in (\"\", \".\") and not p.endswith(\"/python\")]\n\nimport numpy as np\nimport pandas as pd\nfrom plotnine import (\n    aes,\n    element_blank,\n    element_line,\n    element_rect,\n    element_text,\n    geom_area,\n    geom_line,\n    geom_point,\n    geom_ribbon,\n    geom_segment,\n    geom_text,\n    ggplot,\n    guide_legend,\n    labs,\n    scale_color_manual,\n    scale_x_continuous,\n    scale_y_continuous,\n    theme,\n    theme_minimal,\n)\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 positions used in this chart\nCOLOR_PH = \"#009E73\"  # position 1: brand green — pH curve (first series)\nCOLOR_DERIV = \"#C475FD\"  # position 2: lavender — derivative curve\n\n# Data — 25 mL of 0.1 M HCl titrated with 0.1 M NaOH (vectorized analytical calculation)\nvolume_hcl = 25.0\nconc_hcl = 0.1\nconc_naoh = 0.1\nmoles_hcl = volume_hcl * conc_hcl / 1000\n\nvolume_ml = np.concatenate([np.linspace(0, 24, 80), np.linspace(24, 26, 40), np.linspace(26, 50, 80)])\n\nmoles_naoh = conc_naoh * volume_ml / 1000\ntotal_volume_L = (volume_hcl + volume_ml) / 1000\nexcess_h = np.clip(moles_hcl - moles_naoh, 1e-14, None) / total_volume_L\nexcess_oh = np.clip(moles_naoh - moles_hcl, 1e-14, None) / total_volume_L\nph_acid = -np.log10(excess_h)\nph_base = 14.0 + np.log10(excess_oh)\nph = np.where(moles_naoh < moles_hcl - 1e-10, ph_acid, np.where(moles_naoh > moles_hcl + 1e-10, ph_base, 7.0))\n\n# Derivative dpH/dV — scaled to pH axis range for overlay display\n_, unique_idx = np.unique(volume_ml, return_index=True)\nunique_idx = np.sort(unique_idx)\nvol_unique = volume_ml[unique_idx]\nph_unique = ph[unique_idx]\ndph_dv_unique = np.gradient(ph_unique, vol_unique)\ndph_dv = np.interp(volume_ml, vol_unique, dph_dv_unique)\ndph_dv = np.nan_to_num(dph_dv, nan=0.0, posinf=0.0, neginf=0.0)\ndph_max = dph_dv.max()\ndph_scaled = dph_dv / dph_max * 12\n\n# Long-format DataFrame for grammar-of-graphics layering\ndf_ph = pd.DataFrame({\"volume_ml\": volume_ml, \"value\": ph, \"series\": \"pH\"})\ndf_deriv = pd.DataFrame({\"volume_ml\": volume_ml, \"value\": dph_scaled, \"series\": \"dpH/dV (scaled)\"})\ndf = pd.concat([df_ph, df_deriv], ignore_index=True)\n\ndf_area = pd.DataFrame({\"volume_ml\": volume_ml, \"value\": dph_scaled})\n\n# Transition region ribbon (±3 mL around equivalence point)\neq_volume = 25.0\neq_ph = 7.0\nmask = (volume_ml >= 22) & (volume_ml <= 28)\ndf_ribbon = pd.DataFrame(\n    {\"volume_ml\": volume_ml[mask], \"ymin\": np.clip(ph[mask] - 1.2, 0, 14), \"ymax\": np.clip(ph[mask] + 1.2, 0, 14)}\n)\n\n# Equivalence point marker and annotation DataFrames\ndf_eq = pd.DataFrame({\"volume_ml\": [eq_volume], \"value\": [eq_ph], \"y_start\": [0.0]})\ndf_eq_label = pd.DataFrame(\n    {\n        \"volume_ml\": [eq_volume + 2.5],\n        \"value\": [eq_ph + 1.8],\n        \"label\": [f\"Equivalence Point\\n({eq_volume:.0f} mL, pH {eq_ph:.0f})\"],\n    }\n)\ndf_peak_label = pd.DataFrame(\n    {\"volume_ml\": [38.0], \"value\": [11.0], \"label\": [f\"Peak dpH/dV = {dph_max:.1f}\\nat {eq_volume:.0f} mL\"]}\n)\n\npalette = {\"pH\": COLOR_PH, \"dpH/dV (scaled)\": COLOR_DERIV}\ntitle = \"titration-curve · python · plotnine · anyplot.ai\"\n\n# Plot\nplot = (\n    ggplot()\n    # Transition region shading around equivalence point\n    + geom_ribbon(aes(x=\"volume_ml\", ymin=\"ymin\", ymax=\"ymax\"), data=df_ribbon, fill=COLOR_PH, alpha=0.25)\n    # Derivative area fill — more visible than default\n    + geom_area(aes(x=\"volume_ml\", y=\"value\"), data=df_area, fill=COLOR_DERIV, alpha=0.18)\n    # Equivalence point vertical dashed reference line\n    + geom_segment(\n        aes(x=\"volume_ml\", xend=\"volume_ml\", y=\"y_start\", yend=\"value\"),\n        data=df_eq,\n        linetype=\"dashed\",\n        color=INK_MUTED,\n        size=0.6,\n    )\n    # Main curves via color aesthetic mapping\n    + geom_line(aes(x=\"volume_ml\", y=\"value\", color=\"series\"), data=df, size=1.5)\n    # Equivalence point diamond marker — Imprint matte red for semantic emphasis\n    + geom_point(aes(x=\"volume_ml\", y=\"value\"), data=df_eq, color=\"#AE3030\", size=4, shape=\"D\", stroke=0.5)\n    # Annotations (idiomatic plotnine geom_text, not matplotlib annotate)\n    + geom_text(\n        aes(x=\"volume_ml\", y=\"value\", label=\"label\"), data=df_eq_label, size=3, ha=\"left\", color=INK, fontstyle=\"italic\"\n    )\n    + geom_text(\n        aes(x=\"volume_ml\", y=\"value\", label=\"label\"),\n        data=df_peak_label,\n        size=3.5,\n        ha=\"left\",\n        color=COLOR_DERIV,\n        fontweight=\"bold\",\n    )\n    # Scales\n    + scale_color_manual(values=palette, name=\" \", guide=guide_legend(override_aes={\"size\": 3}))\n    + scale_x_continuous(breaks=range(0, 55, 5), limits=(0, 50))\n    + scale_y_continuous(breaks=range(0, 15, 2), limits=(0, 14))\n    + labs(x=\"Volume of NaOH added (mL)\", y=\"pH / dpH/dV (scaled)\", title=title)\n    # Theme — Imprint-compliant, theme-adaptive chrome\n    + theme_minimal()\n    + theme(\n        figure_size=(8, 4.5),\n        plot_title=element_text(size=12, weight=\"bold\", color=INK, margin={\"b\": 10}),\n        axis_title=element_text(size=10, color=INK),\n        axis_text=element_text(size=8, color=INK_SOFT),\n        legend_text=element_text(size=8, color=INK_SOFT),\n        legend_title=element_text(size=8, color=INK),\n        legend_position=(0.15, 0.85),\n        legend_background=element_rect(fill=ELEVATED_BG, color=INK_SOFT, size=0.3),\n        panel_grid_minor=element_blank(),\n        panel_grid_major_x=element_blank(),\n        panel_grid_major_y=element_line(color=INK, size=0.3, alpha=0.15),\n        axis_line_x=element_line(color=INK_SOFT, size=0.5),\n        axis_line_y=element_line(color=INK_SOFT, size=0.5),\n        plot_background=element_rect(fill=PAGE_BG, color=PAGE_BG),\n        panel_background=element_rect(fill=PAGE_BG),\n    )\n)\n\n# Save\nplot.save(f\"plot-{THEME}.png\", dpi=400, width=8, height=4.5, units=\"in\")\n"}