{"spec_id":"spirometry-flow-volume","library":"echarts","language":"javascript","code":"// anyplot.ai\n// spirometry-flow-volume: Spirometry Flow-Volume Loop\n// Library: echarts 5.5.1 | JavaScript 22.22.3\n// Quality: 94/100 | Created: 2026-06-17\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst theme = window.ANYPLOT_THEME;\n\n// Imprint palette + theme-adaptive chrome\nconst BRAND = t.palette[0]; // #009E73 — measured loop (first series)\nconst MUTED = theme === \"dark\" ? \"#A8A79F\" : \"#6B6A63\"; // predicted reference (muted anchor)\n\n// --- Data: pulmonary function test, healthy adult ---------------------------\n// Flow-volume loop = airflow (L/s) vs exhaled lung volume (L). The expiratory\n// limb rises sharply to Peak Expiratory Flow (PEF) then declines; the\n// inspiratory limb is a symmetric U below the zero-flow line. A tiny fixed-seed\n// LCG adds breath-to-breath jitter so the measured trace reads as real data.\nlet seed = 20260617;\nconst rand = () => {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff - 0.5;\n};\n\nconst N = 140;\nconst X_PEF = 0.5; // exhaled volume (L) at which PEF occurs\n\n// Expiratory airflow as a function of exhaled volume (rise then linear-ish fall)\nconst expFlow = (x, fvc, pef) =>\n  x <= X_PEF\n    ? pef * Math.sin((Math.PI / 2) * (x / X_PEF))\n    : pef * Math.pow(Math.max((fvc - x) / (fvc - X_PEF), 0), 1.15);\n\n// Inspiratory airflow (negative, symmetric U) as a function of exhaled volume\nconst inspFlow = (x, fvc, pif) => -pif * Math.sin(Math.PI * (x / fvc));\n\n// Measured (solid) — closed loop: expiratory 0→FVC, then inspiratory FVC→0\nconst FVC = 4.8,\n  PEF = 9.6,\n  PIF = 5.4,\n  FEV1 = 3.95;\nconst measured = [];\nfor (let i = 0; i <= N; i++) {\n  const x = (FVC * i) / N;\n  measured.push([x, expFlow(x, FVC, PEF) + rand() * 0.18]);\n}\nfor (let i = N; i >= 0; i--) {\n  const x = (FVC * i) / N;\n  measured.push([x, inspFlow(x, FVC, PIF) + rand() * 0.15]);\n}\n\n// Predicted normal (dashed) — smooth reference loop, slightly larger\nconst FVC_P = 5.1,\n  PEF_P = 10.2,\n  PIF_P = 5.9;\nconst predicted = [];\nfor (let i = 0; i <= N; i++) {\n  const x = (FVC_P * i) / N;\n  predicted.push([x, expFlow(x, FVC_P, PEF_P)]);\n}\nfor (let i = N; i >= 0; i--) {\n  const x = (FVC_P * i) / N;\n  predicted.push([x, inspFlow(x, FVC_P, PIF_P)]);\n}\n\n// --- Init -------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option -----------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  backgroundColor: \"transparent\",\n  color: t.palette,\n  title: {\n    text: \"spirometry-flow-volume · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 16,\n    textStyle: { color: t.ink, fontSize: 22, fontWeight: 600 },\n  },\n  legend: {\n    data: [\"Measured\", \"Predicted normal\"],\n    top: 52,\n    left: \"center\",\n    textStyle: { color: t.inkSoft, fontSize: 16 },\n    itemWidth: 34,\n    itemGap: 28,\n  },\n  grid: { left: 92, right: 64, top: 104, bottom: 78 },\n  xAxis: {\n    type: \"value\",\n    name: \"Volume (L)\",\n    nameLocation: \"center\",\n    nameGap: 42,\n    nameTextStyle: { color: t.ink, fontSize: 18 },\n    min: 0,\n    max: 5.4,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Flow (L/s)\",\n    nameLocation: \"center\",\n    nameGap: 56,\n    nameTextStyle: { color: t.ink, fontSize: 18 },\n    min: -8,\n    max: 12,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      name: \"Measured\",\n      type: \"line\",\n      data: measured,\n      showSymbol: false,\n      smooth: 0.2,\n      lineStyle: { color: BRAND, width: 3.5 },\n      // Zero-flow divider between expiratory (up) and inspiratory (down) limbs\n      markLine: {\n        silent: true,\n        symbol: \"none\",\n        lineStyle: { color: t.inkSoft, type: \"solid\", width: 1, opacity: 0.45 },\n        label: { show: false },\n        data: [{ yAxis: 0 }],\n      },\n      // Peak Expiratory Flow marker\n      markPoint: {\n        symbol: \"circle\",\n        symbolSize: 13,\n        data: [{ coord: [X_PEF, PEF], name: \"PEF\" }],\n        itemStyle: { color: BRAND, borderColor: t.pageBg, borderWidth: 2 },\n        label: {\n          formatter: \"PEF\",\n          color: t.ink,\n          fontSize: 14,\n          fontWeight: 600,\n          position: \"top\",\n          distance: 8,\n        },\n      },\n    },\n    {\n      name: \"Predicted normal\",\n      type: \"line\",\n      data: predicted,\n      showSymbol: false,\n      smooth: 0.2,\n      lineStyle: { color: MUTED, width: 2.5, type: \"dashed\" },\n    },\n  ],\n  // Clinical values callout (text box, per spec)\n  graphic: {\n    type: \"text\",\n    right: 70,\n    top: 128,\n    style: {\n      text: `FVC   = ${FVC.toFixed(2)} L\\nFEV1  = ${FEV1.toFixed(2)} L\\nFEV1/FVC = ${(FEV1 / FVC).toFixed(2)}\\nPEF   = ${PEF.toFixed(1)} L/s`,\n      fill: t.ink,\n      font: '500 16px sans-serif',\n      lineHeight: 25,\n      backgroundColor: t.elevatedBg,\n      borderColor: t.grid,\n      borderWidth: 1,\n      borderRadius: 8,\n      padding: [14, 18],\n    },\n  },\n});\n"}