{"spec_id":"area-cumulative-flow","library":"bokeh","language":"python","code":"\"\"\" anyplot.ai\narea-cumulative-flow: Cumulative Flow Diagram for Workflow Analytics\nLibrary: bokeh 3.9.2 | Python 3.13.15\nQuality: 94/100 | Updated: 2026-08-18\n\"\"\"\n\nimport os\nimport sys\nimport time\nfrom pathlib import Path\n\n\n# Remove the script's own directory from sys.path so \"bokeh\" resolves to the\n# installed package, not this file.\n_this_dir = str(Path(__file__).parent.resolve())\nsys.path = [p for p in sys.path if os.path.abspath(p) != _this_dir and p != \"\"]\n\nimport numpy as np\nimport pandas as pd\nfrom bokeh.io import output_file, save\nfrom bokeh.models import ColumnDataSource, DatetimeTickFormatter, Legend, LegendItem, Title\nfrom bokeh.plotting import figure\nfrom selenium import webdriver\nfrom selenium.webdriver.chrome.options import Options\n\n\n# Theme tokens\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\"\n\n# Imprint palette — first series always #009E73\nIMPRINT = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\"]\n\n# Data — 90-day Kanban board for a software delivery team\nnp.random.seed(42)\ndays = 90\ndates = pd.date_range(\"2024-01-15\", periods=days, freq=\"D\")\n\n# Items enter the backlog at a Poisson rate (~3/day) starting from 15 committed items\ndaily_intake = np.random.poisson(3, days)\nbacklog_cum = np.maximum.accumulate(np.cumsum(daily_intake) + 15)\n\n# Each downstream stage lags the upstream by a delay and has a throughput rate.\n# Development is deliberately throttled (lower rate, longer lag into Testing) so\n# the diagram tells a genuine bottleneck story through the data itself.\nanalysis_cum = np.zeros(days, dtype=int)\nanalysis_cum[5:] = (backlog_cum[:-5] * 0.88).astype(int)\nanalysis_cum = np.maximum.accumulate(analysis_cum)\n\ndev_cum = np.zeros(days, dtype=int)\ndev_cum[7:] = (analysis_cum[:-7] * 0.92).astype(int)\ndev_cum = np.maximum.accumulate(dev_cum)\n\ntesting_cum = np.zeros(days, dtype=int)\ntesting_cum[10:] = (dev_cum[:-10] * 0.75).astype(int)\ntesting_cum = np.maximum.accumulate(testing_cum)\n\ndone_cum = np.zeros(days, dtype=int)\ndone_cum[4:] = (testing_cum[:-4] * 0.97).astype(int)\ndone_cum = np.maximum.accumulate(done_cum)\n\n# Band heights = WIP in each stage (differences between consecutive cumulative boundaries)\nsource = ColumnDataSource(\n    data={\n        \"date\": dates,\n        \"done\": done_cum.astype(float),\n        \"testing\": np.maximum(0, testing_cum - done_cum).astype(float),\n        \"development\": np.maximum(0, dev_cum - testing_cum).astype(float),\n        \"analysis\": np.maximum(0, analysis_cum - dev_cum).astype(float),\n        \"backlog\": np.maximum(0, backlog_cum - analysis_cum).astype(float),\n    }\n)\n\nstages = [\"done\", \"testing\", \"development\", \"analysis\", \"backlog\"]\nlabels = [\"Done\", \"Testing\", \"Development\", \"Analysis\", \"Backlog\"]\n\n# Plot — `width`/`height` are the total canvas; min_border_* reserve room for\n# the native-pixel tick + axis-label stack at this size.\np = figure(\n    width=3200,\n    height=1800,\n    tools=\"\",\n    toolbar_location=None,\n    min_border_bottom=160,\n    min_border_left=180,\n    min_border_top=110,\n    min_border_right=50,\n)\n\n# Stacked areas — each color corresponds to the matching stacker\nrenderers = p.varea_stack(stackers=stages, x=\"date\", color=IMPRINT, alpha=0.70, source=source)\n\n# Development is the throttled stage (see data comment above) — render it near-opaque\n# while the other bands stay dimmer, so the bottleneck reads as a focal point at a glance.\nfor stage, renderer in zip(stages, renderers, strict=True):\n    renderer.glyph.fill_alpha = 0.95 if stage == \"development\" else 0.68\n\n# Subtle boundary lines at each stage transition\ncum_vals = np.zeros(days)\nfor stage in stages:\n    cum_vals = cum_vals + source.data[stage]\n    p.line(x=dates, y=cum_vals, line_width=1.5, line_color=INK_SOFT, line_alpha=0.40)\n\n# Legend — top to bottom: Backlog → Done (matches visual band order)\nlegend_items = [LegendItem(label=labels[i], renderers=[renderers[i]]) for i in range(len(stages) - 1, -1, -1)]\nlegend = Legend(\n    items=legend_items,\n    location=\"top_left\",\n    background_fill_color=ELEVATED_BG,\n    border_line_color=INK_SOFT,\n    label_text_color=INK_SOFT,\n    label_text_font_size=\"34pt\",\n    padding=20,\n    spacing=10,\n)\np.add_layout(legend)\n\n# Title + subtitle — typographic hierarchy via a bold headline over a lighter,\n# smaller descriptive line (no data annotations — this is a \"basic\" spec variant).\np.add_layout(\n    Title(\n        text=\"Five delivery stages, 90 days of cumulative Kanban flow\",\n        text_font_size=\"26pt\",\n        text_font_style=\"normal\",\n        text_color=INK_SOFT,\n    ),\n    \"above\",\n)\np.add_layout(\n    Title(\n        text=\"area-cumulative-flow · python · bokeh · anyplot.ai\",\n        text_font_size=\"50pt\",\n        text_font_style=\"bold\",\n        text_color=INK,\n    ),\n    \"above\",\n)\n\n# Theme-adaptive chrome\np.background_fill_color = PAGE_BG\np.border_fill_color = PAGE_BG\np.outline_line_color = INK_SOFT\n\np.xaxis.axis_label = \"Date\"\np.yaxis.axis_label = \"Cumulative Items\"\np.xaxis.axis_label_text_color = INK\np.yaxis.axis_label_text_color = INK\np.xaxis.axis_label_text_font_size = \"42pt\"\np.yaxis.axis_label_text_font_size = \"42pt\"\np.xaxis.major_label_text_color = INK_SOFT\np.yaxis.major_label_text_color = INK_SOFT\np.xaxis.major_label_text_font_size = \"34pt\"\np.yaxis.major_label_text_font_size = \"34pt\"\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\n\np.xgrid.grid_line_color = INK\np.ygrid.grid_line_color = INK\np.xgrid.grid_line_alpha = 0.10\np.ygrid.grid_line_alpha = 0.10\n\np.xaxis.formatter = DatetimeTickFormatter(days=\"%b %d\", months=\"%b %Y\")\n\n# Save HTML then screenshot with 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)\ndriver.set_window_size(W, H)\ndriver.get(f\"file://{Path(f'plot-{THEME}.html').resolve()}\")\n# Headless Chrome's --window-size sets the OUTER window, which still reserves a\n# phantom title-bar height even headless — pin the viewport exactly via CDP.\ndriver.execute_cdp_cmd(\n    \"Emulation.setDeviceMetricsOverride\", {\"width\": W, \"height\": H, \"deviceScaleFactor\": 1, \"mobile\": False}\n)\ntime.sleep(3)\ndriver.save_screenshot(f\"plot-{THEME}.png\")\ndriver.quit()\n"}