codex/saponification-atom-charge-ledger.py

saponification-atom-charge-ledger.py
"""Draw an original ester hydrolysis ledger; Python 3.14, matplotlib 3.10."""
from pathlib import Path
import matplotlib.pyplot as plt
from matplotlib.patches import FancyBboxPatch


def main():
    plt.rcParams.update({'font.family': 'DejaVu Sans', 'font.size': 12})
    fig = plt.figure(figsize=(10, 6.2), dpi=100, facecolor='white')
    ax = fig.add_axes([0.03, 0.02, 0.94, 0.96])
    ax.set(xlim=(0, 10), ylim=(0, 6.2))
    ax.axis('off')
    ax.text(5, 5.96, 'One ester + one hydroxide → one carboxylate + one alcohol',
            ha='center', va='top', fontsize=17, weight='bold')
    ax.text(5, 5.37,
            r'$\mathrm{CH_3COOC_2H_5 + OH^- \longrightarrow CH_3COO^- + C_2H_5OH}$',
            ha='center', va='center', fontsize=19)
    cols = [0.4, 4.0, 5.25, 6.5, 7.75, 9.0]
    headers = ['Species / side', 'C atoms', 'H atoms', 'O atoms', 'Charge', 'Role']
    rows = [
        ['Ethyl acetate', '4', '8', '2', '0', 'ester'],
        ['Hydroxide', '0', '1', '1', '−1', 'base'],
        ['Reactant total', '4', '9', '3', '−1', ''],
        ['Acetate', '2', '3', '2', '−1', 'anion'],
        ['Ethanol', '2', '6', '1', '0', 'alcohol'],
        ['Product total', '4', '9', '3', '−1', ''],
    ]
    for x, label in zip(cols, headers):
        ax.text(x, 4.60, label, ha='left' if x == cols[0] else 'center',
                va='center', weight='bold', fontsize=12)
    for j, row in enumerate(rows):
        y = 4.06 - j * 0.49
        total = j in (2, 5)
        if total:
            ax.add_patch(FancyBboxPatch((0.18, y - 0.20), 9.64, 0.40,
                         boxstyle='round,pad=0.03', facecolor='#e7f2f8', edgecolor='none'))
        for i, (x, label) in enumerate(zip(cols, row)):
            ax.text(x, y, label, ha='left' if i == 0 else 'center', va='center',
                    weight='bold' if total else 'normal', color='#17344a')
        if j == 2:
            ax.plot([0.18, 9.82], [y - 0.26, y - 0.26], color='#96a7b2', linewidth=1)
    ax.text(5, 0.98, 'Atoms balance separately; the negative charge remains on acetate.',
            ha='center', fontsize=13, weight='bold')
    ax.text(5, 0.49, 'Hydroxide is consumed. Sodium, if present, is a spectator ion.',
            ha='center', fontsize=12)
    ax.text(5, 0.14, 'Formula accounting, not a molecular collision or kinetic trace.',
            ha='center', fontsize=11, color='#53616a')
    fig.savefig(Path(__file__).stem + '.png', dpi=100, facecolor='white', transparent=False)
    plt.close(fig)


if __name__ == '__main__':
    main()