{"spec_id":"spirometry-flow-volume","library":"makie","language":"julia","code":"# anyplot.ai\n# spirometry-flow-volume: Spirometry Flow-Volume Loop\n# Library: makie 0.22.10 | Julia 1.11.9\n# Quality: 90/100 | Created: 2026-06-17\n\nusing CairoMakie\nusing Colors\nusing Random\n\nRandom.seed!(42)\n\n# --- Theme tokens -----------------------------------------------------------\nconst THEME       = get(ENV, \"ANYPLOT_THEME\", \"light\")\nconst PAGE_BG     = THEME == \"light\" ? colorant\"#FAF8F1\" : colorant\"#1A1A17\"\nconst ELEVATED_BG = THEME == \"light\" ? colorant\"#FFFDF6\" : colorant\"#242420\"\nconst INK         = THEME == \"light\" ? colorant\"#1A1A17\" : colorant\"#F0EFE8\"\nconst INK_SOFT    = THEME == \"light\" ? colorant\"#4A4A44\" : colorant\"#B8B7B0\"\nconst INK_MUTED   = THEME == \"light\" ? colorant\"#6B6A63\" : colorant\"#A8A79F\"\n\nconst BRAND     = colorant\"#009E73\"  # Imprint palette position 1 — measured loop\nconst REFERENCE = INK_MUTED          # Imprint muted anchor — predicted normal overlay\n\n# --- Data: forced expiratory + inspiratory flow-volume loops ----------------\n# Volume axis runs 0 (full inflation, TLC) -> FVC (full exhalation, RV).\n# Expiratory limb rises sharply to PEF then declines toward zero at FVC;\n# inspiratory limb is a symmetric U below the zero-flow line.\nn = 160\n\n# Measured loop (mild obstructive pattern: slightly scooped expiratory decline)\nfvc_meas   = 4.8\npef_meas   = 9.6\nv_pef_meas = 0.45\npif_meas   = 5.4\n\nv_exp_meas = collect(range(0, fvc_meas; length = n))\nflow_exp_meas = [v <= v_pef_meas ?\n    pef_meas * sqrt(v / v_pef_meas) :\n    pef_meas * ((fvc_meas - v) / (fvc_meas - v_pef_meas))^1.25\n    for v in v_exp_meas]\n\nv_insp_meas = collect(range(fvc_meas, 0; length = n))\nflow_insp_meas = [-pif_meas * sqrt(max(0.0, 1 - ((v - fvc_meas / 2) / (fvc_meas / 2))^2))\n                  for v in v_insp_meas]\n\nvolume_meas = vcat(v_exp_meas, v_insp_meas)\nflow_meas   = vcat(flow_exp_meas, flow_insp_meas)\n\n# Predicted normal loop (reference overlay — larger, near-linear decline)\nfvc_pred   = 5.3\npef_pred   = 10.6\nv_pef_pred = 0.5\npif_pred   = 6.2\n\nv_exp_pred = collect(range(0, fvc_pred; length = n))\nflow_exp_pred = [v <= v_pef_pred ?\n    pef_pred * sqrt(v / v_pef_pred) :\n    pef_pred * ((fvc_pred - v) / (fvc_pred - v_pef_pred))\n    for v in v_exp_pred]\n\nv_insp_pred = collect(range(fvc_pred, 0; length = n))\nflow_insp_pred = [-pif_pred * sqrt(max(0.0, 1 - ((v - fvc_pred / 2) / (fvc_pred / 2))^2))\n                  for v in v_insp_pred]\n\nvolume_pred = vcat(v_exp_pred, v_insp_pred)\nflow_pred   = vcat(flow_exp_pred, flow_insp_pred)\n\n# --- Plot -------------------------------------------------------------------\nfig = Figure(\n    size            = (1600, 900),\n    fontsize        = 14,\n    backgroundcolor = PAGE_BG,\n)\n\nax = Axis(\n    fig[1, 1];\n    title             = \"spirometry-flow-volume · julia · makie · anyplot.ai\",\n    titlesize         = 22,\n    titlecolor        = INK,\n    xlabel            = \"Volume (L)\",\n    ylabel            = \"Flow (L/s)\",\n    xlabelsize        = 16,\n    ylabelsize        = 16,\n    xlabelcolor       = INK,\n    ylabelcolor       = INK,\n    xticklabelsize    = 14,\n    yticklabelsize    = 14,\n    xticklabelcolor   = INK_SOFT,\n    yticklabelcolor   = INK_SOFT,\n    xtickcolor        = INK_SOFT,\n    ytickcolor        = INK_SOFT,\n    backgroundcolor   = PAGE_BG,\n    topspinevisible   = false,\n    rightspinevisible = false,\n    leftspinecolor    = INK_SOFT,\n    bottomspinecolor  = INK_SOFT,\n    xgridcolor        = RGBAf(INK.r, INK.g, INK.b, 0.12),\n    ygridcolor        = RGBAf(INK.r, INK.g, INK.b, 0.12),\n    xminorgridvisible = false,\n    yminorgridvisible = false,\n)\n\nxlims!(ax, -0.35, 5.85)\nylims!(ax, -7.8, 12.2)\n\n# Zero-flow reference line separating expiration (above) from inspiration (below)\nhlines!(ax, 0; color = RGBAf(INK.r, INK.g, INK.b, 0.35), linewidth = 1.2)\n\n# Predicted normal loop (dashed reference)\nlines!(ax, volume_pred, flow_pred;\n    color = REFERENCE, linewidth = 2.5, linestyle = :dash, label = \"Predicted normal\")\n\n# Measured loop (solid brand green)\nlines!(ax, volume_meas, flow_meas;\n    color = BRAND, linewidth = 3.2, label = \"Measured\")\n\n# Peak Expiratory Flow marker\nscatter!(ax, [v_pef_meas], [pef_meas];\n    color = BRAND, markersize = 15, strokecolor = PAGE_BG, strokewidth = 2,\n    label = \"PEF\")\ntext!(ax, v_pef_meas + 0.18, pef_meas;\n    text = \"PEF\", color = INK, fontsize = 15, align = (:left, :center))\n\n# Limb annotations\ntext!(ax, 2.15, 7.4; text = \"Expiratory limb\", color = INK_MUTED,\n    fontsize = 14, font = :italic, align = (:center, :bottom))\ntext!(ax, 2.5, -6.9; text = \"Inspiratory limb\", color = INK_MUTED,\n    fontsize = 14, font = :italic, align = (:center, :top))\n\n# Clinical values box (upper-right empty region)\npoly!(ax, Point2f[(3.55, 6.4), (5.7, 6.4), (5.7, 11.6), (3.55, 11.6)];\n    color = ELEVATED_BG, strokecolor = INK_SOFT, strokewidth = 1)\ntext!(ax, 3.72, 11.0; text = \"Spirometry values\", color = INK,\n    fontsize = 16, font = :bold, align = (:left, :center))\ntext!(ax, 3.72, 9.7; text = \"FVC\\nFEV1\\nFEV1/FVC\\nPEF\",\n    color = INK_SOFT, fontsize = 15, align = (:left, :top))\ntext!(ax, 5.62, 9.7; text = \"4.80 L\\n3.88 L\\n81%\\n9.6 L/s\",\n    color = INK, fontsize = 15, align = (:right, :top))\n\naxislegend(ax; position = :lt, framecolor = INK_SOFT, framevisible = true,\n    backgroundcolor = ELEVATED_BG, labelcolor = INK, labelsize = 14,\n    padding = (12, 12, 10, 10))\n\n# --- Save -------------------------------------------------------------------\nsave(\"plot-$(THEME).png\", fig; px_per_unit = 2)\n"}