{"spec_id":"spirometry-flow-volume","library":"matplotlib","language":"python","code":"\"\"\" anyplot.ai\nspirometry-flow-volume: Spirometry Flow-Volume Loop\nLibrary: matplotlib 3.11.0 | Python 3.13.13\nQuality: 93/100 | Updated: 2026-06-17\n\"\"\"\n\nimport os\n\nimport matplotlib.pyplot as plt\nimport numpy as np\nfrom matplotlib.patches import FancyArrowPatch\n\n\n# Theme-adaptive chrome\nTHEME = os.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG = \"#FAF8F1\" if THEME == \"light\" else \"#1A1A17\"\nELEVATED_BG = \"#FFFDF6\" if THEME == \"light\" else \"#242420\"\nINK = \"#1A1A17\" if THEME == \"light\" else \"#F0EFE8\"\nINK_SOFT = \"#4A4A44\" if THEME == \"light\" else \"#B8B7B0\"\nINK_MUTED = \"#6B6A63\" if THEME == \"light\" else \"#A8A79F\"\n\n# Imprint palette\nBRAND = \"#009E73\"  # measured loop — always first series\nMATTE_RED = \"#AE3030\"  # semantic anchor: obstruction / loss\n\n# Data — simulate a realistic flow-volume loop (mild obstruction pattern)\nnp.random.seed(42)\n\n# Measured values (typical adult, mild obstruction)\nfvc = 4.2  # Forced Vital Capacity (L)\npef = 8.5  # Peak Expiratory Flow (L/s)\nfev1 = 3.1  # FEV1 (L)\n\n# Predicted normal values\nfvc_pred = 4.8\npef_pred = 10.2\nfev1_pred = 4.0\n\nN = 200\n\n\ndef expiratory_limb(capacity, peak, n=N):\n    \"\"\"Sharp rise to PEF then a roughly linear decline (ascending volume).\"\"\"\n    vol = np.linspace(0, capacity, n)\n    t = vol / capacity\n    raw = (1 - np.exp(-30 * t)) * (1 - t) ** 0.5\n    return vol, raw / raw.max() * peak\n\n\ndef inspiratory_limb(capacity, peak, n=N):\n    \"\"\"Symmetric U-shaped curve below zero flow (ascending volume).\"\"\"\n    vol = np.linspace(0, capacity, n)\n    t = vol / capacity\n    return vol, -peak * np.sin(np.pi * t) ** 0.8\n\n\nvol_exp_m, flow_exp_m = expiratory_limb(fvc, pef)\nvol_insp_m, flow_insp_m = inspiratory_limb(fvc, 5.5)\nvol_exp_p, flow_exp_p = expiratory_limb(fvc_pred, pef_pred)\nvol_insp_p, flow_insp_p = inspiratory_limb(fvc_pred, 6.5)\n\n# Closed measured loop: expiration (0→FVC) then inspiration back (FVC→0)\nvol_loop_m = np.concatenate([vol_exp_m, vol_insp_m[::-1]])\nflow_loop_m = np.concatenate([flow_exp_m, flow_insp_m[::-1]])\nvol_loop_p = np.concatenate([vol_exp_p, vol_insp_p[::-1]])\nflow_loop_p = np.concatenate([flow_exp_p, flow_insp_p[::-1]])\n\n# Peak Expiratory Flow point on the measured curve\npef_idx = int(np.argmax(flow_exp_m))\npef_volume, pef_flow = vol_exp_m[pef_idx], flow_exp_m[pef_idx]\n\n# Common grid for obstruction-gap fills — single interpolation per limb\ngrid = np.linspace(0, fvc_pred, N)\nexp_m_g = np.interp(grid, vol_exp_m, flow_exp_m, right=0.0)\nexp_p_g = np.interp(grid, vol_exp_p, flow_exp_p)\ninsp_m_g = np.interp(grid, vol_insp_m, flow_insp_m, right=0.0)\ninsp_p_g = np.interp(grid, vol_insp_p, flow_insp_p)\n\n# Plot — landscape 3200×1800\nfig, ax = plt.subplots(figsize=(8, 4.5), dpi=400, facecolor=PAGE_BG)\nax.set_facecolor(PAGE_BG)\n\n# Obstruction gap between predicted and measured limbs\nax.fill_between(grid, exp_m_g, exp_p_g, color=MATTE_RED, alpha=0.13, zorder=1, label=\"Obstruction gap\")\nax.fill_between(grid, insp_m_g, insp_p_g, color=MATTE_RED, alpha=0.09, zorder=1)\n\n# Zero-flow reference line\nax.axhline(y=0, color=INK_SOFT, linewidth=0.8, alpha=0.5, zorder=1)\n\n# Predicted normal loop (dashed muted reference)\nax.plot(vol_loop_p, flow_loop_p, color=INK_MUTED, linewidth=1.6, linestyle=\"--\", zorder=2, label=\"Predicted Normal\")\n\n# Measured loop (solid brand green)\nax.plot(vol_loop_m, flow_loop_m, color=BRAND, linewidth=2.5, solid_capstyle=\"round\", zorder=3, label=\"Measured\")\n\n# Directional arrows along the measured loop\nfor vol, flow, idx in (\n    (vol_exp_m, flow_exp_m, N // 5),\n    (vol_exp_m, flow_exp_m, int(N * 0.65)),\n    (vol_insp_m[::-1], flow_insp_m[::-1], N // 2),\n):\n    ax.add_patch(\n        FancyArrowPatch(\n            (vol[idx], flow[idx]),\n            (vol[idx + 4], flow[idx + 4]),\n            arrowstyle=\"-|>\",\n            mutation_scale=11,\n            color=BRAND,\n            linewidth=2.0,\n            zorder=4,\n        )\n    )\n\n# Mark PEF\nax.scatter(pef_volume, pef_flow, s=90, color=BRAND, edgecolors=PAGE_BG, linewidth=1.2, zorder=5)\nax.annotate(\n    f\"PEF = {pef_flow:.1f} L/s\",\n    xy=(pef_volume, pef_flow),\n    xytext=(pef_volume + 0.55, pef_flow - 0.7),\n    fontsize=8,\n    fontweight=\"bold\",\n    color=INK,\n    arrowprops={\"arrowstyle\": \"->\", \"color\": INK_SOFT, \"lw\": 1.2, \"connectionstyle\": \"arc3,rad=0.2\"},\n)\n\n# Breathing-direction labels\nax.text(2.1, pef * 0.82, \"EXPIRATION →\", fontsize=8, color=INK_MUTED, fontweight=\"bold\", ha=\"center\")\nax.text(2.1, -5.5 * 0.72, \"← INSPIRATION\", fontsize=8, color=INK_MUTED, fontweight=\"bold\", ha=\"center\")\n\n# Clinical values text box\ntextstr = (\n    f\"FVC      = {fvc:.1f} L   (pred {fvc_pred:.1f})\\n\"\n    f\"FEV₁     = {fev1:.1f} L   (pred {fev1_pred:.1f})\\n\"\n    f\"FEV₁/FVC = {fev1 / fvc:.0%}\\n\"\n    f\"PEF      = {pef_flow:.1f} L/s (pred {pef_pred:.1f})\"\n)\nax.text(\n    0.975,\n    0.96,\n    textstr,\n    transform=ax.transAxes,\n    fontsize=8,\n    color=INK,\n    va=\"top\",\n    ha=\"right\",\n    family=\"monospace\",\n    bbox={\n        \"boxstyle\": \"round,pad=0.5\",\n        \"facecolor\": ELEVATED_BG,\n        \"edgecolor\": INK_SOFT,\n        \"alpha\": 0.95,\n        \"linewidth\": 0.8,\n    },\n)\n\n# Axes & chrome\nax.set_xlabel(\"Volume (L)\", fontsize=10, color=INK)\nax.set_ylabel(\"Flow (L/s)\", fontsize=10, color=INK)\nax.set_title(\"spirometry-flow-volume · python · matplotlib · anyplot.ai\", fontsize=12, fontweight=\"medium\", color=INK)\nax.tick_params(axis=\"both\", labelsize=8, colors=INK_SOFT, labelcolor=INK_SOFT)\nax.spines[\"top\"].set_visible(False)\nax.spines[\"right\"].set_visible(False)\nfor s in (\"left\", \"bottom\"):\n    ax.spines[s].set_color(INK_SOFT)\nax.yaxis.grid(True, alpha=0.15, color=INK, linewidth=0.8)\n\nleg = ax.legend(fontsize=8, loc=\"lower left\", framealpha=0.95)\nleg.get_frame().set_facecolor(ELEVATED_BG)\nleg.get_frame().set_edgecolor(INK_SOFT)\nplt.setp(leg.get_texts(), color=INK_SOFT)\n\nfig.subplots_adjust(left=0.07, right=0.97, top=0.91, bottom=0.11)\nplt.savefig(f\"plot-{THEME}.png\", dpi=400, facecolor=PAGE_BG)\n"}