{"spec_id":"spirometry-flow-volume","library":"ggplot2","language":"r","code":"#' anyplot.ai\n#' spirometry-flow-volume: Spirometry Flow-Volume Loop\n#' Library: ggplot2 3.5.1 | R 4.4.1\n#' Quality: 91/100 | Created: 2026-06-17\n\nlibrary(ggplot2)\nlibrary(dplyr)\nlibrary(ragg)\n\nset.seed(42)\n\n# --- Theme tokens -----------------------------------------------------------\nTHEME       <- Sys.getenv(\"ANYPLOT_THEME\", \"light\")\nPAGE_BG     <- if (THEME == \"light\") \"#FAF8F1\" else \"#1A1A17\"\nELEVATED_BG <- if (THEME == \"light\") \"#FFFDF6\" else \"#242420\"\nINK         <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\nINK_SOFT    <- if (THEME == \"light\") \"#4A4A44\" else \"#B8B7B0\"\nINK_MUTED   <- if (THEME == \"light\") \"#6B6A63\" else \"#A8A79F\"\nGRID        <- if (THEME == \"light\") \"#1A1A17\" else \"#F0EFE8\"\n\nBRAND <- \"#009E73\"  # Imprint palette position 1 — measured loop (first series)\nMUTED <- INK_MUTED  # Imprint semantic anchor \"muted\" — predicted normal reference\n\n# --- Data -------------------------------------------------------------------\n# Forced spirometry maneuver: airflow (L/s) vs exhaled lung volume (L).\n# Expiratory limb rises sharply to Peak Expiratory Flow (PEF) then declines\n# roughly linearly; inspiratory limb is a symmetric U below the zero-flow line.\nflow_volume_loop <- function(fvc, pef, pif, v_peak) {\n    # Expiratory limb: 0 -> FVC, sharp rise to PEF then near-linear decline.\n    v_rise <- seq(0, v_peak, length.out = 50)\n    f_rise <- pef * (v_rise / v_peak)^0.55\n    v_fall <- seq(v_peak, fvc, length.out = 150)[-1]\n    f_fall <- pef * (1 - (v_fall - v_peak) / (fvc - v_peak))^1.15\n\n    # Inspiratory limb: FVC -> 0, symmetric U-shaped curve (negative flow).\n    v_insp <- seq(fvc, 0, length.out = 200)\n    f_insp <- -pif * sin(pi * v_insp / fvc)^0.9\n\n    data.frame(\n        volume = c(v_rise, v_fall, v_insp),\n        flow   = c(f_rise, f_fall, f_insp)\n    )\n}\n\n# Predicted normal loop (reference) and the patient's measured loop.\npredicted <- flow_volume_loop(fvc = 5.1, pef = 10.4, pif = 6.0, v_peak = 0.55) %>%\n    mutate(series = \"Predicted normal\")\nmeasured <- flow_volume_loop(fvc = 4.6, pef = 9.1, pif = 5.2, v_peak = 0.65) %>%\n    mutate(series = \"Measured\")\n\nloop_df <- bind_rows(measured, predicted) %>%\n    mutate(series = factor(series, levels = c(\"Measured\", \"Predicted normal\")))\n\n# Peak Expiratory Flow marker (highest flow on the measured loop).\npef_point <- measured %>% slice_max(flow, n = 1)\n\n# Clinical summary values for the measured maneuver.\nclinical <- paste(\n    \"FVC  = 4.60 L\",\n    \"FEV1 = 3.55 L\",\n    \"PEF  = 9.10 L/s\",\n    sep = \"\\n\"\n)\n\n# --- Plot -------------------------------------------------------------------\np <- ggplot(loop_df, aes(volume, flow, color = series, linetype = series)) +\n    geom_hline(yintercept = 0, color = INK_SOFT, linewidth = 0.4) +\n    geom_path(linewidth = 1.3, lineend = \"round\") +\n    geom_point(\n        data = pef_point, aes(volume, flow),\n        inherit.aes = FALSE, color = BRAND, size = 3.6\n    ) +\n    annotate(\n        \"text\", x = pef_point$volume + 0.18, y = pef_point$flow + 0.05,\n        label = \"PEF\", hjust = 0, vjust = 0.5,\n        color = INK, size = 4.2, fontface = \"bold\"\n    ) +\n    annotate(\n        \"label\", x = 4.55, y = 8.6, label = clinical,\n        hjust = 1, vjust = 1, color = INK, fill = ELEVATED_BG,\n        label.size = 0.3, size = 4.0, lineheight = 1.25, family = \"mono\"\n    ) +\n    scale_color_manual(values = c(\"Measured\" = BRAND, \"Predicted normal\" = MUTED)) +\n    scale_linetype_manual(values = c(\"Measured\" = \"solid\", \"Predicted normal\" = \"dashed\")) +\n    scale_x_continuous(breaks = seq(0, 5, 1), expand = expansion(mult = 0.02)) +\n    scale_y_continuous(breaks = seq(-6, 10, 2)) +\n    labs(\n        title = \"spirometry-flow-volume · r · ggplot2 · anyplot.ai\",\n        x = \"Volume (L)\", y = \"Flow (L/s)\",\n        color = NULL, linetype = NULL\n    ) +\n    theme_minimal(base_size = 8) +\n    theme(\n        plot.background  = element_rect(fill = PAGE_BG, color = PAGE_BG),\n        panel.background = element_rect(fill = PAGE_BG, color = NA),\n        panel.grid.major = element_line(color = GRID, linewidth = 0.25),\n        panel.grid.minor = element_blank(),\n        axis.title       = element_text(color = INK, size = 11),\n        axis.title.x     = element_text(margin = margin(t = 8)),\n        axis.title.y     = element_text(margin = margin(r = 8)),\n        axis.text        = element_text(color = INK_SOFT, size = 9),\n        axis.line        = element_line(color = INK_SOFT, linewidth = 0.4),\n        plot.title       = element_text(color = INK, size = 12, margin = margin(b = 12)),\n        plot.margin      = margin(18, 22, 14, 16),\n        legend.position  = \"inside\",\n        legend.position.inside = c(0.99, 0.02),\n        legend.justification = c(1, 0),\n        legend.background = element_rect(fill = ELEVATED_BG, color = INK_SOFT, linewidth = 0.3),\n        legend.margin    = margin(6, 10, 6, 10),\n        legend.text      = element_text(color = INK_SOFT, size = 10),\n        legend.key       = element_blank()\n    )\n\n# --- Save -------------------------------------------------------------------\nggsave(\n    filename = sprintf(\"plot-%s.png\", THEME),\n    plot     = p,\n    device   = ragg::agg_png,\n    width    = 8,\n    height   = 4.5,\n    units    = \"in\",\n    dpi      = 400\n)\n"}