codexillustrative-food-web.py
illustrative-food-web.py
"""Illustrative feeding routes, not measurements of a named ecosystem.
Tested with Python 3.14.4, NumPy 2.3.5 and Matplotlib 3.10.7.
Write an opaque white same-basename PNG to the working directory.
"""
from pathlib import Path
import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.patches import FancyArrowPatch, FancyBboxPatch
INK = "#243642"
BLUE = "#246a92"
BROWN = "#8b5939"
def arrow(ax, start, end, *, dashed=False, bend=0):
ax.add_patch(FancyArrowPatch(
start, end, arrowstyle="-|>", mutation_scale=18,
connectionstyle=f"arc3,rad={bend}", linewidth=2,
linestyle="--" if dashed else "-", color=BROWN if dashed else BLUE,
zorder=1,
))
def box(ax, x, y, name, role, color):
ax.add_patch(FancyBboxPatch(
(x - 0.93, y - 0.43), 1.86, 0.86,
boxstyle="round,pad=0.05", facecolor=color,
edgecolor=INK, linewidth=1.3, zorder=2,
))
ax.text(x, y + 0.11, name, ha="center", va="center",
fontsize=12, weight="bold", color=INK)
ax.text(x, y - 0.18, role, ha="center", va="center",
fontsize=9.5, color=INK)
def main():
fig, ax = plt.subplots(figsize=(9.0, 4.8))
fig.set_facecolor("white")
ax.set_facecolor("white")
ax.set(xlim=(0, 10.6), ylim=(0, 5.5))
ax.axis("off")
fig.subplots_adjust(left=0.02, right=0.98, top=0.96, bottom=0.04)
ax.text(5.3, 5.14, "An illustrative aquatic food web",
ha="center", fontsize=16, weight="bold", color=INK)
ax.text(5.3, 4.72, "Generic groups and assumed routes • not ecosystem measurements",
ha="center", fontsize=11, color=INK)
xs = [1.25, 3.95, 6.65, 9.35]
names = ["Algae", "Zooplankton", "Small fish", "Large fish"]
roles = ["primary producers", "primary consumers",
"predator AND prey", "predators of small fish"]
colors = ["#e7f2de", "#e5f1fb", "#e5f1fb", "#e5f1fb"]
for x, name, role, color in zip(xs, names, roles, colors):
box(ax, x, 3.65, name, role, color)
for x1, x2 in zip(xs[:-1], xs[1:]):
arrow(ax, (x1 + 1.0, 3.65), (x2 - 1.0, 3.65))
box(ax, 3.7, 1.75, "Detritus", "dead organic material", "#f6ecdf")
box(ax, 7.4, 1.75, "Decomposers", "e.g. bacteria and fungi", "#f6ecdf")
arrow(ax, (1.25, 3.17), (2.69, 1.9), dashed=True, bend=0.10)
arrow(ax, (6.65, 3.17), (4.71, 1.9), dashed=True, bend=-0.10)
arrow(ax, (4.72, 1.75), (6.39, 1.75))
ax.text(3.7, 2.72, "Selected dead-material transfers",
ha="center", fontsize=10.5, color=BROWN)
arrow(ax, (0.45, 0.8), (1.35, 0.8))
ax.text(1.55, 0.8, "RESOURCE → CONSUMER", va="center",
fontsize=11, color=BLUE, weight="bold")
arrow(ax, (6.0, 0.8), (6.9, 0.8), dashed=True)
ax.text(7.1, 0.8, "dead-material transfer", va="center",
fontsize=11, color=BROWN)
ax.text(5.3, 0.24, "Other routes omitted; box sizes and arrow widths carry no quantitative scale",
ha="center", fontsize=10, color=INK)
fig.savefig(Path(__file__).with_suffix(".png").name, dpi=130,
facecolor="white", transparent=False)
plt.close(fig)
print("Illustration: 4 feeding positions, 3 chain arrows, 2 dead-material arrows.")
if __name__ == "__main__":
main()