codexwestern-blot-lane-control-interpretation.py
western-blot-lane-control-interpretation.py
"""Draw qualitative control lanes, not measured Western blot data.
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 Rectangle
BLUE = "#1764a0"
INK = "#203040"
GRAY = "#818b93"
def lane(ax, x, y, w, h):
ax.add_patch(Rectangle((x, y), w, h, facecolor="white",
edgecolor="#b8c5ce", lw=1.3))
def band(ax, x, y, w, color=BLUE, lw=6):
ax.plot([x + .12, x + w - .12], [y, y], color=color, lw=lw,
solid_capstyle="butt")
def main():
fig, ax = plt.subplots(figsize=(12, 7.6), dpi=110, facecolor="white")
ax.set(xlim=(0, 12), ylim=(0, 7.6))
ax.axis("off")
ax.text(6, 7.28, "Western blot controls ask different questions",
ha="center", fontsize=19, weight="bold", color=INK)
ax.text(6, 6.88, "Invented qualitative lane symbols • no measured positions, intensities or confirmed identity",
ha="center", fontsize=11.5, color=INK)
ax.text(.45, 6.34, "A Is binding consistent with the proposed target?",
fontsize=14, weight="bold", color=INK)
xs = [1.8, 3.65, 5.5, 7.35]
labels = ["Known positive", "Sample", "Target deleted", "No primary\n(matched sample)"]
for x, label in zip(xs, labels):
lane(ax, x, 3.7, 1.04, 1.75)
ax.text(x + .52, 5.72, label, ha="center", va="center", fontsize=11, color=INK)
for x in xs[:2]:
band(ax, x, 4.96, 1.04)
for x in xs[:3]:
band(ax, x, 4.09, 1.04, GRAY, 5)
ax.plot([.75, 8.6], [4.96, 4.96], color=BLUE, lw=.7, ls=":", zorder=0)
ax.text(.52, 4.96, "candidate\nposition", ha="center", va="center",
fontsize=9.5, color=BLUE)
ax.text(9, 4.93, "Candidate disappears\nafter target deletion:\nsupports assignment", va="center",
fontsize=11, color=BLUE)
ax.text(9, 4.02, "Lower band persists:\nnot explained by target", va="center",
fontsize=11, color=INK)
ax.text(4.65, 3.36, "No-primary control tests secondary/reagent background; it cannot validate primary specificity.",
ha="center", fontsize=10.3, color=INK)
ax.text(.45, 2.85, "B Did loading or transfer change with the target readout?",
fontsize=14, weight="bold", color=INK)
for x, label in [(2, "Sample A"), (4.1, "Sample B"), (6.2, "Patchy transfer")]:
ax.text(x + .5, 2.47, label, ha="center", fontsize=11, color=INK)
lane(ax, x, 1.97, 1, .24)
lane(ax, x, .58, 1, 1.02)
band(ax, 2, 2.09, 1, BLUE, 3)
band(ax, 4.1, 2.09, 1, BLUE, 6)
for y in [.74, .96, 1.18, 1.43]:
band(ax, 2, y, 1, GRAY, 2)
band(ax, 4.1, y, 1, GRAY, 4)
if y < 1.1:
band(ax, 6.2, y, 1, GRAY, 3)
ax.text(1.63, 2.09, "Target", ha="right", va="center", fontsize=11, color=BLUE)
ax.text(1.63, 1.09, "Total-protein\nstain", ha="right", va="center", fontsize=11, color=INK)
ax.text(8.06, 2.03, "A target difference can accompany\na difference in total transferred protein.",
va="center", fontsize=11, color=INK)
ax.text(8.06, 1.12, "Blank patches suggest transfer trouble.\nA target signal alone cannot diagnose why.",
va="center", fontsize=11, color=INK)
ax.text(6, .18, "Check proportional response before normalization. Symbol thickness is explanatory only.",
ha="center", fontsize=11, color=INK)
fig.subplots_adjust(left=.015, right=.985, bottom=.01, top=.99)
fig.savefig(Path(__file__).with_suffix(".png").name, facecolor="white", transparent=False)
plt.close(fig)
if __name__ == "__main__":
main()