Titration curve
Chemistry — acetic acid + NaOH
Example from the compendium of canonical charts
Python Code
"""Chemistry — Titration curve (acetic acid + NaOH)."""
import numpy as np
import plotly.graph_objects as go
PKA = 4.76
CA = 0.100 # M acetic acid
VA = 50.0 # mL
CB = 0.100 # M NaOH
def ph_at_volume(v_naoh_ml):
"""Compute pH when v_naoh_ml of 0.100 M NaOH has been added to 50 mL of 0.100 M acetic acid."""
moles_acid = VA / 1000.0 * CA # 5.0e-3 mol
moles_base = v_naoh_ml / 1000.0 * CB
if v_naoh_ml == 0.0:
# Pure weak acid: pH = ½(pKa - log(Ca))
return 0.5 * (PKA - np.log10(CA))
if moles_base < moles_acid:
# Buffer region (Henderson-Hasselbalch)
ratio = moles_base / (moles_acid - moles_base)
return PKA + np.log10(ratio)
if abs(moles_base - moles_acid) < 1e-9:
# Equivalence point: sodium acetate hydrolysis
# pH = 7 + (pKa + log(C)) / 2 where C = concentration of acetate at eq pt
V_total = (VA + v_naoh_ml) / 1000.0
C_acetate = moles_acid / V_total
return 7.0 + (PKA + np.log10(C_acetate)) / 2.0
# Past equivalence point: excess NaOH
excess_moles_base = moles_base - moles_acid
V_total = (VA + v_naoh_ml) / 1000.0
c_oh = excess_moles_base / V_total
pOH = -np.log10(c_oh)
return 14.0 - pOH
def generate():
# Dense grid with extra resolution near equivalence
v_pre = np.linspace(0, 49.0, 300)
v_near = np.linspace(49.0, 51.0, 200)
v_post = np.linspace(51.0, 100.0, 150)
volumes = np.concatenate([v_pre, v_near, v_post])
volumes = np.unique(volumes)
ph = np.array([ph_at_volume(v) for v in volumes])
ph = np.clip(ph, 0, 14)
# Key points
v_half_eq = 25.0
ph_half_eq = PKA
v_eq = 50.0
ph_eq = ph_at_volume(v_eq)
print(f" pH at half-equivalence ({v_half_eq} mL): {ph_half_eq:.2f}")
print(f" pH at equivalence ({v_eq} mL): {ph_eq:.2f}")
fig = go.Figure()
# Main curve
fig.add_trace(go.Scatter(
x=volumes,
y=ph,
mode="lines",
name="Titration curve",
line=dict(color=VIOLET, width=2.5),
hovertemplate="V(NaOH)=%{x:.1f} mL<br>pH=%{y:.2f}<extra></extra>",
))
# Half-equivalence marker
fig.add_trace(go.Scatter(
x=[v_half_eq],
y=[ph_half_eq],
mode="markers",
name="Half-equivalence",
marker=dict(color=PINK, size=12, symbol="diamond"),
hovertemplate="Half-eq: V=%{x} mL, pH=%{y:.2f}<extra></extra>",
))
# Equivalence point marker
fig.add_trace(go.Scatter(
x=[v_eq],
y=[ph_eq],
mode="markers",
name="Equivalence point",
marker=dict(color=TEAL, size=12, symbol="star"),
hovertemplate="Eq. point: V=%{x} mL, pH=%{y:.2f}<extra></extra>",
))
fig.update_layout(
xaxis=dict(title="Volume NaOH Added (mL)", range=[-1, 102]),
yaxis=dict(title="pH", range=[1.5, 13.5], dtick=2),
legend=dict(orientation="h", y=1.05, x=0),
annotations=[
dict(
x=v_half_eq,
y=ph_half_eq,
text=f"Half-equivalence<br>V = {v_half_eq} mL, pH = pKa = {PKA}",
showarrow=True,
arrowhead=2,
ax=-90,
ay=30,
font=dict(size=11),
bgcolor="rgba(255,255,255,0.8)",
),
dict(
x=v_eq,
y=ph_eq,
text=f"Equivalence point<br>V = {v_eq} mL, pH ≈ {ph_eq:.2f}",
showarrow=True,
arrowhead=2,
ax=70,
ay=-40,
font=dict(size=11),
bgcolor="rgba(255,255,255,0.8)",
),
],
margin=dict(t=50, b=60, l=70, r=60),
)
return fig
if __name__ == "__main__":
generate()
Made with Plotly