{"spec_id":"spirometry-flow-volume","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\nspirometry-flow-volume: Spirometry Flow-Volume Loop\nLibrary: bokeh 3.9.1 | Python 3.13.14\nQuality: 91/100 | Updated: 2026-06-17\n\"\"\"\n\nimport os\nimport sys\n\n\n# Prevent self-import: this file is named bokeh.py, which shadows the installed\n# bokeh package when its directory sits at the front of sys.path.\n_this_dir = os.path.dirname(os.path.abspath(__file__))\nsys.path = [p for p in sys.path if os.path.abspath(p or \".\") != _this_dir]\n\nimport time\nfrom pathlib import Path\n\nimport numpy as np\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, HoverTool, Label, Legend, LegendItem\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens (see prompts/default-style-guide.md \"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 — first series ALWAYS #009E73\nBRAND = \"#009E73\"  # measured loop (the patient)\nPREDICTED = \"#4467A3\"  # predicted normal reference loop\nACCENT_PEF = \"#AE3030\"  # matte-red focal anchor for Peak Expiratory Flow\nACCENT_FEV1 = \"#BD8233\"  # ochre focal for FEV1 landmark\n\n# Data — Spirometry flow-volume loop (measured vs predicted normal)\nnp.random.seed(42)\nn_points = 160\n\n# Clinical landmarks for a mildly-obstructed adult\nfvc_measured = 4.2  # forced vital capacity (L)\npef_measured = 9.5  # peak expiratory flow (L/s)\nfev1_measured = 3.3  # volume exhaled in 1 s (L)\n\nfvc_predicted = 4.8\npef_predicted = 10.8\n\n# Expiratory limb (measured): sharp rise to PEF then concave decline\nvolume_exp = np.linspace(0, fvc_measured, n_points)\nt_exp = volume_exp / fvc_measured\npef_fraction = 0.12  # PEF occurs early in the forced exhalation\nrise = pef_measured * np.sin(np.pi / 2 * t_exp / pef_fraction)\ndecline = pef_measured * (1 - (t_exp - pef_fraction) / (1 - pef_fraction))\nflow_exp = np.where(t_exp <= pef_fraction, rise, decline)\nflow_exp = np.maximum(flow_exp, 0.0)\nflow_exp[t_exp > pef_fraction] *= 1 - 0.15 * ((t_exp[t_exp > pef_fraction] - pef_fraction) / (1 - pef_fraction)) ** 2\n\n# Inspiratory limb (measured): symmetric U-shape below zero\nvolume_insp = np.linspace(fvc_measured, 0, n_points)\nt_insp = np.linspace(0, 1, n_points)\npeak_insp_flow = -6.5\nflow_insp = peak_insp_flow * np.sin(np.pi * t_insp)\n\n# Predicted normal — expiratory limb\nvolume_pred_exp = np.linspace(0, fvc_predicted, n_points)\nt_pred_exp = volume_pred_exp / fvc_predicted\npef_frac_pred = 0.10\nrise_pred = pef_predicted * np.sin(np.pi / 2 * t_pred_exp / pef_frac_pred)\ndecline_pred = pef_predicted * (1 - (t_pred_exp - pef_frac_pred) / (1 - pef_frac_pred))\nflow_pred_exp = np.where(t_pred_exp <= pef_frac_pred, rise_pred, decline_pred)\nflow_pred_exp = np.maximum(flow_pred_exp, 0.0)\nflow_pred_exp[t_pred_exp > pef_frac_pred] *= (\n    1 - 0.12 * ((t_pred_exp[t_pred_exp > pef_frac_pred] - pef_frac_pred) / (1 - pef_frac_pred)) ** 2\n)\n\n# Predicted normal — inspiratory limb\nvolume_pred_insp = np.linspace(fvc_predicted, 0, n_points)\npeak_insp_pred = -7.5\nflow_pred_insp = peak_insp_pred * np.sin(np.pi * t_insp)\n\n# Close each loop into a single connected path\nvolume_measured = np.concatenate([volume_exp, volume_insp])\nflow_measured = np.concatenate([flow_exp, flow_insp])\nvolume_predicted = np.concatenate([volume_pred_exp, volume_pred_insp])\nflow_predicted = np.concatenate([flow_pred_exp, flow_pred_insp])\n\nsource_measured = ColumnDataSource(data={\"volume\": volume_measured, \"flow\": flow_measured})\nsource_predicted = ColumnDataSource(data={\"volume\": volume_predicted, \"flow\": flow_predicted})\n\n# Landmark points on the measured curve\npef_idx = int(np.argmax(flow_exp))\npef_volume = volume_exp[pef_idx]\npef_flow = flow_exp[pef_idx]\nfev1_idx = int(np.argmin(np.abs(volume_exp - fev1_measured)))\nfev1_flow = flow_exp[fev1_idx]\n\n# Plot\np = figure(\n    width=3200,\n    height=1800,\n    title=\"spirometry-flow-volume · python · bokeh · anyplot.ai\",\n    x_axis_label=\"Volume (L)\",\n    y_axis_label=\"Flow (L/s)\",\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=60,\n)\n\n# Diagnostic gap: shade between measured and predicted expiratory limbs\npred_exp_interp = np.interp(volume_exp, volume_pred_exp, flow_pred_exp)\npatch_vol = np.concatenate([volume_exp, volume_exp[::-1]])\npatch_flow = np.concatenate([flow_exp, pred_exp_interp[::-1]])\np.patch(x=patch_vol, y=patch_flow, fill_color=BRAND, fill_alpha=0.10, line_color=None)\n\n# Zero-flow reference line\np.line(\n    x=[-0.2, max(fvc_measured, fvc_predicted) + 0.3], y=[0, 0], line_color=INK_MUTED, line_width=2, line_dash=\"dotted\"\n)\n\n# Predicted normal loop (dashed, background)\nr_pred = p.line(\n    x=\"volume\",\n    y=\"flow\",\n    source=source_predicted,\n    line_color=PREDICTED,\n    line_width=5,\n    line_dash=\"dashed\",\n    line_alpha=0.85,\n)\n\n# Measured loop (solid, foreground)\nr_meas = p.line(x=\"volume\", y=\"flow\", source=source_measured, line_color=BRAND, line_width=6)\n\n# PEF focal marker + label\np.scatter(x=[pef_volume], y=[pef_flow], size=26, fill_color=ACCENT_PEF, line_color=PAGE_BG, line_width=4)\np.add_layout(\n    Label(\n        x=pef_volume,\n        y=pef_flow,\n        text=f\"PEF = {pef_measured:.1f} L/s\",\n        text_font_size=\"30pt\",\n        text_color=ACCENT_PEF,\n        text_font_style=\"bold\",\n        x_offset=24,\n        y_offset=18,\n    )\n)\n\n# FEV1 marker + label\np.scatter(x=[fev1_measured], y=[fev1_flow], size=22, fill_color=ACCENT_FEV1, line_color=PAGE_BG, line_width=4)\np.add_layout(\n    Label(\n        x=fev1_measured,\n        y=fev1_flow,\n        text=f\"FEV1 = {fev1_measured:.1f} L\",\n        text_font_size=\"30pt\",\n        text_color=ACCENT_FEV1,\n        text_font_style=\"bold\",\n        x_offset=22,\n        y_offset=-44,\n    )\n)\n\n# Clinical values box — placed in the empty upper region, clear of both limbs\nclinical_text = (\n    f\"FVC = {fvc_measured:.1f} L\\n\"\n    f\"FEV1 = {fev1_measured:.1f} L\\n\"\n    f\"FEV1/FVC = {fev1_measured / fvc_measured:.0%}\\n\"\n    f\"PEF = {pef_measured:.1f} L/s\"\n)\np.add_layout(\n    Label(\n        x=3.0,\n        y=9.6,\n        text=clinical_text,\n        text_font_size=\"28pt\",\n        text_color=INK,\n        text_font_style=\"bold\",\n        text_baseline=\"top\",\n        background_fill_color=ELEVATED_BG,\n        background_fill_alpha=0.92,\n        border_line_color=INK_SOFT,\n        border_line_alpha=0.35,\n        padding=18,\n    )\n)\n\n# Legend\nlegend = Legend(\n    items=[LegendItem(label=\"Measured\", renderers=[r_meas]), LegendItem(label=\"Predicted Normal\", renderers=[r_pred])],\n    location=\"bottom_right\",\n    label_text_font_size=\"34pt\",\n    label_text_color=INK_SOFT,\n    glyph_width=70,\n    glyph_height=34,\n    spacing=16,\n    padding=22,\n    background_fill_color=ELEVATED_BG,\n    background_fill_alpha=0.9,\n    border_line_color=INK_SOFT,\n    border_line_alpha=0.35,\n)\np.add_layout(legend)\n\n# Hover tool — Bokeh's interactive inspection (retained in the HTML artifact)\np.add_tools(HoverTool(tooltips=[(\"Volume\", \"@volume{0.2f} L\"), (\"Flow\", \"@flow{0.2f} L/s\")], mode=\"mouse\"))\n\n# Style — theme-adaptive chrome\np.title.text_font_size = \"50pt\"\np.title.text_color = INK\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\np.xaxis.axis_line_color = INK_SOFT\np.yaxis.axis_line_color = INK_SOFT\np.xaxis.major_tick_line_color = INK_SOFT\np.yaxis.major_tick_line_color = INK_SOFT\np.xaxis.axis_line_width = 2\np.yaxis.axis_line_width = 2\n\n# Grid — subtle y-grid only for reading flow values\np.xgrid.grid_line_color = None\np.ygrid.grid_line_color = INK\np.ygrid.grid_line_alpha = 0.15\n\n# Backgrounds\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = None\n\n# Save — write interactive HTML, then screenshot via headless Chrome\noutput_file(f\"plot-{THEME}.html\")\nsave(p)\n\nW, H = 3200, 1800\nopts = Options()\nfor arg in (\n    \"--headless=new\",\n    \"--no-sandbox\",\n    \"--disable-dev-shm-usage\",\n    \"--disable-gpu\",\n    f\"--window-size={W},{H}\",\n    \"--hide-scrollbars\",\n):\n    opts.add_argument(arg)\ndriver = webdriver.Chrome(options=opts)\n# CDP override forces an exact W×H viewport regardless of outer window chrome\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}