Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
156 changes: 156 additions & 0 deletions plots/titration-curve/implementations/plotnine.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
""" pyplots.ai
titration-curve: Acid-Base Titration Curve
Library: plotnine 0.15.3 | Python 3.14.3
Quality: 93/100 | Created: 2026-03-21
"""

import numpy as np
import pandas as pd
from plotnine import (
aes,
element_blank,
element_line,
element_rect,
element_text,
geom_area,
geom_line,
geom_point,
geom_ribbon,
geom_segment,
geom_text,
ggplot,
guide_legend,
labs,
scale_color_manual,
scale_x_continuous,
scale_y_continuous,
theme,
theme_minimal,
)


# Data — 25 mL of 0.1 M HCl titrated with 0.1 M NaOH (vectorized)
volume_hcl = 25.0
conc_hcl = 0.1
conc_naoh = 0.1
moles_hcl = volume_hcl * conc_hcl / 1000

volume_ml = np.concatenate([np.linspace(0, 24, 80), np.linspace(24, 26, 40), np.linspace(26, 50, 80)])

# Vectorized pH calculation
moles_naoh = conc_naoh * volume_ml / 1000
total_volume_L = (volume_hcl + volume_ml) / 1000
excess_h = np.clip(moles_hcl - moles_naoh, 1e-14, None) / total_volume_L
excess_oh = np.clip(moles_naoh - moles_hcl, 1e-14, None) / total_volume_L
ph_acid = -np.log10(excess_h)
ph_base = 14.0 + np.log10(excess_oh)
ph = np.where(moles_naoh < moles_hcl - 1e-10, ph_acid, np.where(moles_naoh > moles_hcl + 1e-10, ph_base, 7.0))

# Compute derivative dpH/dV using unique-volume spacing to avoid division by zero
_, unique_idx = np.unique(volume_ml, return_index=True)
unique_idx = np.sort(unique_idx)
vol_unique = volume_ml[unique_idx]
ph_unique = ph[unique_idx]
dph_dv_unique = np.gradient(ph_unique, vol_unique)
dph_dv = np.interp(volume_ml, vol_unique, dph_dv_unique)
dph_dv = np.nan_to_num(dph_dv, nan=0.0, posinf=0.0, neginf=0.0)
dph_max = dph_dv.max()
dph_scaled = dph_dv / dph_max * 12

# Build long-format dataframe for grammar-of-graphics layering
df_ph = pd.DataFrame({"volume_ml": volume_ml, "value": ph, "series": "pH"})
df_deriv = pd.DataFrame({"volume_ml": volume_ml, "value": dph_scaled, "series": "dpH/dV (scaled)"})
df = pd.concat([df_ph, df_deriv], ignore_index=True)

# Derivative area fill — uses geom_area for distinctive plotnine layering
df_area = pd.DataFrame({"volume_ml": volume_ml, "value": dph_scaled})

# Transition region ribbon (±2 mL around equivalence) with stronger visibility
eq_volume = 25.0
eq_ph = 7.0
mask = (volume_ml >= 22) & (volume_ml <= 28)
df_ribbon = pd.DataFrame(
{"volume_ml": volume_ml[mask], "ymin": np.clip(ph[mask] - 1.2, 0, 14), "ymax": np.clip(ph[mask] + 1.2, 0, 14)}
)

# Equivalence point marker and label dataframes for geom_point/geom_text
df_eq = pd.DataFrame({"volume_ml": [eq_volume], "value": [eq_ph]})
df_eq_label = pd.DataFrame(
{
"volume_ml": [eq_volume + 2.5],
"value": [eq_ph + 1.8],
"label": [f"Equivalence Point\n({eq_volume:.0f} mL, pH {eq_ph:.0f})"],
}
)
df_peak_label = pd.DataFrame(
{"volume_ml": [38.0], "value": [11.0], "label": [f"Peak dpH/dV = {dph_max:.1f}\nat {eq_volume:.0f} mL"]}
)

# Color palette
palette = {"pH": "#306998", "dpH/dV (scaled)": "#E8A838"}

plot = (
ggplot()
# Transition region shading
+ geom_ribbon(aes(x="volume_ml", ymin="ymin", ymax="ymax"), data=df_ribbon, fill="#306998", alpha=0.18)
# Derivative area fill — distinctive plotnine geom_area usage
+ geom_area(aes(x="volume_ml", y="value"), data=df_area, fill="#E8A838", alpha=0.12)
# Equivalence point vertical reference
+ geom_segment(
aes(x="volume_ml", xend="volume_ml", y=0, yend="value"),
data=df_eq,
linetype="dashed",
color="#999999",
size=0.6,
)
# Main curves via color aesthetic mapping
+ geom_line(aes(x="volume_ml", y="value", color="series"), data=df, size=1.5)
# Equivalence point diamond marker
+ geom_point(
aes(x="volume_ml", y="value"), data=df_eq, color="#C0392B", fill="#E74C3C", size=5, shape="D", stroke=0.5
)
# Annotations via geom_text (idiomatic plotnine, not matplotlib annotate)
+ geom_text(
aes(x="volume_ml", y="value", label="label"),
data=df_eq_label,
size=11,
ha="left",
color="#333333",
fontstyle="italic",
)
+ geom_text(
aes(x="volume_ml", y="value", label="label"),
data=df_peak_label,
size=9,
ha="left",
color="#E8A838",
fontweight="bold",
)
# Scales
+ scale_color_manual(values=palette, name=" ", guide=guide_legend(override_aes={"size": 3}))
+ scale_x_continuous(breaks=range(0, 55, 5), limits=(0, 50))
+ scale_y_continuous(breaks=range(0, 15, 2), limits=(0, 14))
+ labs(x="Volume of NaOH added (mL)", y="pH / dpH/dV (scaled)", title="titration-curve · plotnine · pyplots.ai")
# Theme — refined minimal with polished details
+ theme_minimal()
+ theme(
figure_size=(16, 9),
plot_title=element_text(size=24, weight="bold", margin={"b": 15}),
axis_title=element_text(size=20, color="#444444"),
axis_text=element_text(size=16, color="#555555"),
legend_text=element_text(size=16),
legend_title=element_text(size=14),
legend_position=(0.15, 0.85),
legend_background=element_rect(fill="white", alpha=0.85, color="#DDDDDD", size=0.3),
panel_grid_minor=element_blank(),
panel_grid_major_x=element_blank(),
panel_grid_major_y=element_line(color="#E8E8E8", size=0.3),
axis_line_x=element_line(color="#888888", size=0.5),
axis_line_y=element_line(color="#888888", size=0.5),
plot_background=element_rect(fill="white", color="white"),
panel_background=element_rect(fill="#FAFAFA", color="white"),
)
)

# Save
plot.save("plot.png", dpi=300)
224 changes: 224 additions & 0 deletions plots/titration-curve/metadata/plotnine.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
library: plotnine
specification_id: titration-curve
created: '2026-03-21T22:11:31Z'
updated: '2026-03-21T22:38:37Z'
generated_by: claude-opus-4-5-20251101
workflow_run: 23389866067
issue: 4407
python_version: 3.14.3
library_version: 0.15.3
preview_url: https://storage.googleapis.com/pyplots-images/plots/titration-curve/plotnine/plot.png
preview_thumb: https://storage.googleapis.com/pyplots-images/plots/titration-curve/plotnine/plot_thumb.png
preview_html: null
quality_score: 93
review:
strengths:
- 'Excellent chemistry: vectorized analytical pH calculation produces a physically
accurate titration curve'
- Strong idiomatic plotnine usage with grammar-of-graphics layering (multiple data
frames, geom_area, geom_ribbon)
- Clear visual hierarchy with red diamond equivalence point marker as focal point
- Polished theme with subtle grid, refined panel background, and styled legend
- Custom blue/amber palette with good colorblind accessibility
weaknesses:
- Derivative area fill at alpha=0.12 is nearly invisible — could be slightly more
prominent
- No true secondary y-axis for the derivative (plotnine limitation, handled gracefully
with scaling)
image_description: 'The plot displays an acid-base titration curve with a bold blue
S-shaped pH line rising from ~1 at 0 mL to ~13 at 50 mL, with a sharp vertical
transition around 25 mL. An amber/gold derivative curve (dpH/dV scaled) spikes
sharply at 25 mL, with a faint amber area fill beneath it. A light blue semi-transparent
ribbon shades the transition region (~22–28 mL). A red diamond marker sits at
the equivalence point (25 mL, pH 7) with an italic annotation reading "Equivalence
Point (25 mL, pH 7)." A bold amber label in the upper right reads "Peak dpH/dV
= 57.5 at 25 mL." The legend in the upper left shows "pH" (blue) and "dpH/dV (scaled)"
(amber) with a white background box. The title reads "titration-curve · plotnine
· pyplots.ai" in bold. X-axis: "Volume of NaOH added (mL)" (0–50), Y-axis: "pH
/ dpH/dV (scaled)" (0–14). Background is a subtle off-white (#FAFAFA) with faint
horizontal grid lines only. Clean, polished appearance.'
criteria_checklist:
visual_quality:
score: 29
max: 30
items:
- id: VQ-01
name: Text Legibility
score: 8
max: 8
passed: true
comment: 'All font sizes explicitly set: title=24, axis_title=20, axis_text=16,
legend_text=16'
- id: VQ-02
name: No Overlap
score: 6
max: 6
passed: true
comment: No overlapping text elements, annotations well-separated
- id: VQ-03
name: Element Visibility
score: 5
max: 6
passed: true
comment: Lines and markers clearly visible; derivative area fill at alpha=0.12
is very faint
- id: VQ-04
name: Color Accessibility
score: 4
max: 4
passed: true
comment: Blue and amber palette is colorblind-safe with good contrast
- id: VQ-05
name: Layout & Canvas
score: 4
max: 4
passed: true
comment: Good 16:9 layout with balanced margins
- id: VQ-06
name: Axis Labels & Title
score: 2
max: 2
passed: true
comment: Descriptive labels with units on both axes
design_excellence:
score: 16
max: 20
items:
- id: DE-01
name: Aesthetic Sophistication
score: 6
max: 8
passed: true
comment: Custom blue/amber palette, styled legend, refined panel background,
clearly above defaults
- id: DE-02
name: Visual Refinement
score: 5
max: 6
passed: true
comment: Subtle y-only grid, minor grid removed, styled axis lines, refined
panel background
- id: DE-03
name: Data Storytelling
score: 5
max: 6
passed: true
comment: Clear focal point at equivalence point with diamond marker, derivative
peak labeled, transition shaded
spec_compliance:
score: 14
max: 15
items:
- id: SC-01
name: Plot Type
score: 5
max: 5
passed: true
comment: Correct S-shaped titration curve
- id: SC-02
name: Required Features
score: 3
max: 4
passed: true
comment: Equivalence point marked, derivative overlay, transition shading.
Derivative scaled on primary axis (plotnine lacks twin axes)
- id: SC-03
name: Data Mapping
score: 3
max: 3
passed: true
comment: X=volume 0-50 mL, Y=pH 0-14, all data visible
- id: SC-04
name: Title & Legend
score: 3
max: 3
passed: true
comment: Title format correct, legend labels match data series
data_quality:
score: 15
max: 15
items:
- id: DQ-01
name: Feature Coverage
score: 6
max: 6
passed: true
comment: Full S-curve with plateaus, sharp transition, derivative peak, and
equivalence point
- id: DQ-02
name: Realistic Context
score: 5
max: 5
passed: true
comment: 'Real chemistry: 25 mL of 0.1 M HCl titrated with 0.1 M NaOH'
- id: DQ-03
name: Appropriate Scale
score: 4
max: 4
passed: true
comment: Realistic concentrations, correct equivalence at 25 mL and pH 7
code_quality:
score: 10
max: 10
items:
- id: CQ-01
name: KISS Structure
score: 3
max: 3
passed: true
comment: 'Linear flow: imports, data, plot, save. No functions or classes'
- id: CQ-02
name: Reproducibility
score: 2
max: 2
passed: true
comment: Fully deterministic analytical calculation
- id: CQ-03
name: Clean Imports
score: 2
max: 2
passed: true
comment: All imports are used
- id: CQ-04
name: Code Elegance
score: 2
max: 2
passed: true
comment: Clean vectorized NumPy calculations, appropriate complexity
- id: CQ-05
name: Output & API
score: 1
max: 1
passed: true
comment: Saves as plot.png with dpi=300, current API
library_mastery:
score: 9
max: 10
items:
- id: LM-01
name: Idiomatic Usage
score: 5
max: 5
passed: true
comment: 'Expert grammar-of-graphics: ggplot + geom layers, aes mappings,
long-format data, guide_legend with override_aes'
- id: LM-02
name: Distinctive Features
score: 4
max: 5
passed: true
comment: Uses geom_area, geom_ribbon, multiple data frames, guide_legend with
override_aes — distinctively plotnine patterns
verdict: APPROVED
impl_tags:
dependencies: []
techniques:
- annotations
- layer-composition
patterns:
- data-generation
dataprep:
- normalization
styling:
- alpha-blending
- grid-styling
Loading