codex/western-blot-transfer-antibody-workflow.py

western-blot-transfer-antibody-workflow.py
"""Draw a schematic Western blot workflow; no experimental measurements.

Tested with Python 3.14, matplotlib 3.10.7 and numpy 2.3.5.
Run from _media; the matching PNG is written to the current directory.
"""

from pathlib import Path

import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch, Rectangle, Circle


BLUE = "#1764a0"
INK = "#203040"
ORANGE = "#c65a16"


def box(ax, x, y, w, h, color="white", edge=INK, lw=1.4):
    ax.add_patch(Rectangle((x, y), w, h, facecolor=color, edgecolor=edge, lw=lw))


def arrow(ax, a, b, color=INK, lw=1.8):
    ax.add_patch(FancyArrowPatch(a, b, arrowstyle="-|>", mutation_scale=15,
                               color=color, lw=lw))


def antibody(ax, x, y, color, scale=1):
    ax.plot([x, x, x - .14 * scale], [y + .39 * scale, y + .17 * scale, y],
            color=color, lw=3)
    ax.plot([x, x + .14 * scale], [y + .17 * scale, y],
            color=color, lw=3)


def main():
    fig, ax = plt.subplots(figsize=(12, 7.4), dpi=110, facecolor="white")
    ax.set(xlim=(0, 12), ylim=(0, 7.4))
    ax.axis("off")
    ax.text(6, 7.08, "Western blot: separation → transfer → detection",
            ha="center", fontsize=19, weight="bold", color=INK)
    ax.text(6, 6.68, "SDS-PAGE workflow • schematic symbols, no measured bands",
            ha="center", fontsize=12, color=INK)

    for x, title in [(0.2, "1  Separate"), (4.35, "2  Transfer"), (8.45, "3  Detect")]:
        box(ax, x, 2.35, 3.35, 3.92, edge="#b8c5ce")
        ax.text(x + 1.675, 5.9, title, ha="center", fontsize=16,
                weight="bold", color=INK)

    box(ax, .72, 3.18, 2.18, 2.18, "#edf5fa")
    for x in [1.08, 1.67, 2.25]:
        box(ax, x, 5.16, .25, .14, "white", BLUE)
    for x, ys in [(1.08, [4.7, 4.1, 3.58]), (1.67, [4.7, 3.77]),
                  (2.25, [4.4, 4.1, 3.58])]:
        for y in ys:
            ax.plot([x - .02, x + .3], [y, y], color=BLUE, lw=4)
    arrow(ax, (3.12, 5.1), (3.12, 3.5), BLUE)
    ax.text(1.8, 2.8, "Anionic protein complexes\nmigrate through a gel", ha="center",
            fontsize=11, color=INK)

    # Section through the stack: negatively charged SDS-protein complexes
    # travel from the gel towards the membrane on the anode side.
    box(ax, 4.62, 3.43, .13, 1.72, "#dbe4ea")
    box(ax, 5.08, 3.43, .47, 1.72, "#edf5fa", BLUE)
    box(ax, 6.3, 3.43, .12, 1.72, "#f2dbc9", ORANGE)
    box(ax, 7.3, 3.43, .13, 1.72, "#dbe4ea")
    for y in [3.78, 4.27, 4.76]:
        ax.add_patch(Circle((5.31, y), .07, color=BLUE))
        arrow(ax, (5.61, y), (6.19, y), BLUE, 1.4)
        ax.add_patch(Circle((6.36, y), .06, color=BLUE))
    ax.text(4.68, 5.42, "−", ha="center", fontsize=20, color=INK)
    ax.text(7.36, 5.42, "+", ha="center", fontsize=20, color=INK)
    ax.text(4.68, 3.1, "cathode", ha="center", fontsize=10, color=INK)
    ax.text(5.3, 5.24, "gel", ha="center", fontsize=11, color=BLUE)
    ax.text(6.36, 5.25, "membrane", ha="center", fontsize=10, color=ORANGE)
    ax.text(7.36, 3.1, "anode", ha="center", fontsize=10, color=INK)
    ax.text(6.025, 2.65, "Transfer across gel thickness\nClose contact preserves positions",
            ha="center", fontsize=10.5, color=INK)

    box(ax, 8.92, 3.65, 2.44, .15, "#f2dbc9", ORANGE)
    ax.add_patch(Circle((9.52, 3.85), .13, facecolor=BLUE, edgecolor=BLUE))
    antibody(ax, 9.7, 3.96, BLUE, 1.3)
    antibody(ax, 9.7, 4.4, ORANGE, 1.15)
    ax.add_patch(Circle((9.85, 4.88), .12, facecolor="#f0b429", edgecolor=ORANGE))
    ax.text(10.13, 5.12, "label", fontsize=11, color=ORANGE)
    ax.text(10.16, 4.67, "secondary", fontsize=11, color=ORANGE)
    ax.text(10.16, 4.15, "primary", fontsize=11, color=BLUE)
    ax.text(9.5, 3.27, "retained target", fontsize=11, color=BLUE)
    ax.text(10.12, 2.67, "Block → probe → wash\nLabel produces the readout",
            ha="center", fontsize=11, color=INK)

    arrow(ax, (3.62, 4.25), (4.27, 4.25))
    arrow(ax, (7.79, 4.25), (8.38, 4.25))
    for x, title, detail in [(.2, "Position", "Reference lanes estimate apparent mass"),
                           (4.35, "Retention", "Check the receiving sheet and residual gel"),
                           (8.45, "Recognition", "Positive, negative and no-primary controls")]:
        ax.text(x + 1.675, 1.85, title, ha="center", fontsize=13,
                weight="bold", color=INK)
        ax.text(x + 1.675, 1.35, detail.replace(" estimate ", "\nestimate ")
                .replace(" and residual", "\nand residual")
                .replace(" and no-primary", "\nand no-primary"),
                ha="center", fontsize=10.5, color=INK)
    ax.text(6, .53, "Band identity and amount require controls; darkness alone is insufficient.",
            ha="center", fontsize=12, color=INK)
    fig.subplots_adjust(left=.015, right=.985, bottom=.025, top=.985)
    fig.savefig(Path(__file__).with_suffix(".png").name, facecolor="white", transparent=False)
    plt.close(fig)


if __name__ == "__main__":
    main()