{"spec_id":"area-cumulative-flow","library":"plotly","language":"python","code":"\"\"\" anyplot.ai\narea-cumulative-flow: Cumulative Flow Diagram for Workflow Analytics\nLibrary: plotly 6.9.0 | Python 3.13.15\nQuality: 94/100 | Updated: 2026-08-18\n\"\"\"\n\nimport os\n\nimport numpy as np\nimport pandas as pd\nimport plotly.graph_objects as go\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\"\nGRID = \"rgba(26, 26, 23, 0.10)\" if THEME == \"light\" else \"rgba(240, 239, 232, 0.10)\"\n\n# Imprint palette — first series is always #009E73\nFILL_COLORS = [\n    \"rgba(0, 158, 115, 0.80)\",  # #009E73 Done\n    \"rgba(196, 117, 253, 0.80)\",  # #C475FD Testing\n    \"rgba(68, 103, 163, 0.80)\",  # #4467A3 Development\n    \"rgba(189, 130, 51, 0.80)\",  # #BD8233 Analysis\n    \"rgba(174, 48, 48, 0.80)\",  # #AE3030 Backlog\n]\nLINE_COLORS = [\"#009E73\", \"#C475FD\", \"#4467A3\", \"#BD8233\", \"#AE3030\"]\n\n# Data — Kanban board for a software team over 90 days\nnp.random.seed(42)\nn_days = 90\ndates = pd.date_range(\"2024-01-15\", periods=n_days, freq=\"D\")\nt = np.arange(n_days)\n\n# Cumulative counts per stage (monotonically non-decreasing)\n# Backlog: total items entered the system (~1.5 new items/day)\nbacklog = np.maximum.accumulate((20 + 1.5 * t + np.cumsum(np.random.normal(0, 0.8, n_days))).astype(int))\n\n# Each downstream stage lags the prior stage and has a slightly lower count\nbacklog_s = pd.Series(backlog.astype(float))\nanalysis_noise = np.random.normal(0, 0.9, n_days)\nanalysis = np.minimum(\n    np.maximum.accumulate((backlog_s.shift(3, fill_value=0) * 0.94 + analysis_noise).astype(int).values), backlog\n)\n\ndev_noise = np.random.normal(0, 0.8, n_days)\nanalysis_s = pd.Series(analysis.astype(float))\ndevelopment = np.minimum(\n    np.maximum.accumulate((analysis_s.shift(5, fill_value=0) * 0.91 + dev_noise).astype(int).values), analysis\n)\n\ntest_noise = np.random.normal(0, 0.7, n_days)\ndev_s = pd.Series(development.astype(float))\ntesting = np.minimum(\n    np.maximum.accumulate((dev_s.shift(7, fill_value=0) * 0.88 + test_noise).astype(int).values), development\n)\n\n# QA/release capacity constrained mid-project (ramps down into days 35-64): completion\n# rate drops, widening the Testing WIP band. A ramped, above-baseline catch-up burst\n# (days 65-84) then drains the backlog and narrows the band again — the bottleneck\n# dynamic a CFD exists to reveal.\ndone_rate = np.full(n_days, 0.85)\ndone_rate[30:35] = np.linspace(0.85, 0.55, 5)\ndone_rate[35:65] = 0.55\ndone_rate[65:75] = np.linspace(0.55, 0.95, 10)\ndone_rate[75:85] = 0.95\ndone_rate[85:90] = np.linspace(0.95, 0.85, 5)\n\ndone_noise = np.random.normal(0, 0.6, n_days)\ntest_s = pd.Series(testing.astype(float))\ndone = np.minimum(\n    np.maximum.accumulate((test_s.shift(5, fill_value=0) * done_rate + done_noise).astype(int).values), testing\n)\n\n# WIP per stage = difference between adjacent cumulative bounds\n# Stack order: Done (bottom) → Testing → Development → Analysis → Backlog (top)\nstage_names = [\"Done\", \"Testing\", \"Development\", \"Analysis\", \"Backlog\"]\nstage_wip = [\n    np.maximum(done, 0),\n    np.maximum(testing - done, 0),\n    np.maximum(development - testing, 0),\n    np.maximum(analysis - development, 0),\n    np.maximum(backlog - analysis, 0),\n]\n\n# Plot\nfig = go.Figure()\n\nfor name, wip, fill, line_color in zip(stage_names, stage_wip, FILL_COLORS, LINE_COLORS, strict=False):\n    fig.add_trace(\n        go.Scatter(\n            x=dates,\n            y=wip,\n            name=name,\n            stackgroup=\"one\",\n            mode=\"none\",\n            fillcolor=fill,\n            line={\"width\": 1.5, \"color\": line_color},\n            hovertemplate=f\"<b>{name}</b><br>%{{x|%b %d, %Y}}<br>WIP: %{{y:,.0f}} items<extra></extra>\",\n        )\n    )\n\n# Bi-weekly tick marks\ntick_dates = pd.date_range(\"2024-01-15\", \"2024-04-13\", freq=\"14D\")\n\nfig.update_layout(\n    autosize=False,\n    title={\n        \"text\": \"area-cumulative-flow · python · plotly · anyplot.ai\",\n        \"font\": {\"size\": 16, \"color\": INK},\n        \"x\": 0.02,\n        \"xanchor\": \"left\",\n        \"y\": 0.97,\n        \"yanchor\": \"top\",\n    },\n    xaxis={\n        \"title\": {\"text\": \"Date\", \"font\": {\"size\": 12, \"color\": INK}},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"linecolor\": INK_SOFT,\n        \"showgrid\": False,\n        \"tickvals\": tick_dates,\n        \"ticktext\": [d.strftime(\"%b %d\") for d in tick_dates],\n        \"tickangle\": 0,\n    },\n    yaxis={\n        \"title\": {\"text\": \"Cumulative Items\", \"font\": {\"size\": 12, \"color\": INK}},\n        \"tickfont\": {\"size\": 10, \"color\": INK_SOFT},\n        \"gridcolor\": GRID,\n        \"linecolor\": INK_SOFT,\n        \"showgrid\": True,\n        \"zeroline\": False,\n    },\n    legend={\n        \"bgcolor\": ELEVATED_BG,\n        \"bordercolor\": INK_SOFT,\n        \"borderwidth\": 1,\n        \"font\": {\"size\": 10, \"color\": INK_SOFT},\n        \"traceorder\": \"reversed\",\n        \"x\": 0.02,\n        \"y\": 0.88,\n        \"xanchor\": \"left\",\n        \"yanchor\": \"top\",\n    },\n    paper_bgcolor=PAGE_BG,\n    plot_bgcolor=PAGE_BG,\n    font={\"color\": INK},\n    hovermode=\"x unified\",\n    margin={\"l\": 80, \"r\": 40, \"t\": 80, \"b\": 60},\n)\n\n# Save\nfig.write_image(f\"plot-{THEME}.png\", width=800, height=450, scale=4)\nfig.write_html(f\"plot-{THEME}.html\", include_plotlyjs=\"cdn\")\n"}