{"spec_id":"area-cumulative-flow","library":"d3","language":"javascript","code":"// anyplot.ai\n// area-cumulative-flow: Cumulative Flow Diagram for Workflow Analytics\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-08-18\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 100, right: 260, bottom: 90, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Support-ticket lifecycle over a 90-day window. Each stage's value is the\n// CUMULATIVE count of tickets that have entered or passed through that stage\n// by the given day — earliest stage (Opened) is always >= every later stage.\nlet seed = 20260818;\nfunction lcg() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\n\nconst stages = [\"Opened\", \"Triaged\", \"In Progress\", \"Waiting on Customer\", \"Resolved\"];\nconst advanceRate = [0.42, 0.38, 0.3, 0.24]; // fraction of the gap to the prior stage that advances per day\nconst days = 90;\nconst startDate = new Date(2026, 4, 1);\n\nconst dates = Array.from({ length: days }, (_, i) => {\n  const d = new Date(startDate);\n  d.setDate(d.getDate() + i);\n  return d;\n});\n\n// Deliberate bottleneck: the In-Progress -> Waiting-on-Customer transition\n// slows sharply for two weeks (reviewers overloaded), so the \"In Progress\"\n// band visibly widens. Deliberate drain: the Waiting-on-Customer -> Resolved\n// transition speeds up afterward (a backlog-clearing push), so the \"Waiting\n// on Customer\" band visibly narrows. This demonstrates the CFD's signature\n// bottleneck-spotting use case called out in the spec.\nfunction effectiveRate(idx, day) {\n  if (idx === 2 && day >= 35 && day < 49) return advanceRate[idx] * 0.28;\n  if (idx === 3 && day >= 60 && day < 72) return Math.min(0.85, advanceRate[idx] * 2.3);\n  return advanceRate[idx];\n}\n\n// Daily new-ticket arrivals: a base rate with a weekly dip (weekends) plus noise.\nconst cumulative = stages.map(() => new Array(days).fill(0));\nfor (let i = 0; i < days; i += 1) {\n  const dow = dates[i].getDay();\n  const weekendDamp = dow === 0 || dow === 6 ? 0.4 : 1;\n  const arrivals = Math.round((7 + 4 * Math.sin(i / 9)) * weekendDamp + lcg() * 4);\n  cumulative[0][i] = (i === 0 ? 0 : cumulative[0][i - 1]) + Math.max(0, arrivals);\n\n  for (let s = 1; s < stages.length; s += 1) {\n    const prevDayOwn = i === 0 ? 0 : cumulative[s][i - 1];\n    const gap = cumulative[s - 1][i] - prevDayOwn;\n    const advance = Math.floor(gap * effectiveRate(s - 1, i) + lcg() * 1.5);\n    cumulative[s][i] = prevDayOwn + Math.max(0, Math.min(gap, advance));\n  }\n}\n\n// --- Colors --------------------------------------------------------------------\n// Canonical ordinal assignment (positions 1-4) for the four in-flight stages,\n// but \"Resolved\" — the dominant, positive-outcome stage — is reassigned away\n// from position 5 (matte red, the reserved bad/error semantic anchor) to lime\n// (position 8), so a wall of the dominant color doesn't read as \"a pile of\n// problems\".\nconst colorIndex = { Opened: 0, Triaged: 1, \"In Progress\": 2, \"Waiting on Customer\": 3, Resolved: 7 };\nconst color = d3.scaleOrdinal().domain(stages).range(stages.map((s) => t.palette[colorIndex[s]]));\n\n// --- Stack (per-stage WIP, stacked bottom-to-top in reverse workflow order) ----\n// Each stage's plotted band value is its work-in-progress (WIP) — the count\n// currently sitting in that stage — not its raw cumulative count. WIP for an\n// in-flight stage is the gap between its cumulative count and the next\n// stage's; the terminal stage's WIP is its full cumulative count (tickets\n// that have finished and stay in the \"Resolved\" pool).\nconst stackData = dates.map((date, i) => ({\n  date,\n  Opened: cumulative[0][i] - cumulative[1][i],\n  Triaged: cumulative[1][i] - cumulative[2][i],\n  \"In Progress\": cumulative[2][i] - cumulative[3][i],\n  \"Waiting on Customer\": cumulative[3][i] - cumulative[4][i],\n  Resolved: cumulative[4][i],\n}));\n\n// Stage order top-to-bottom per spec: Opened (earliest) on top, Resolved\n// (latest) on bottom. d3.stack() lays keys bottom-first, so pass the reverse.\nconst stackKeys = [...stages].reverse();\nconst series = d3.stack().keys(stackKeys)(stackData);\n\n// --- SVG mount ---------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales --------------------------------------------------------------------\nconst x = d3.scaleTime().domain(d3.extent(dates)).range([0, iw]);\nconst y = d3\n  .scaleLinear()\n  .domain([0, d3.max(cumulative[0])])\n  .nice()\n  .range([ih, 0]);\n\n// --- Gridlines -----------------------------------------------------------------\ng.append(\"g\")\n  .attr(\"class\", \"grid\")\n  .call(d3.axisLeft(y).tickSize(-iw).tickFormat(\"\"))\n  .call((sel) => sel.select(\".domain\").remove())\n  .selectAll(\"line\")\n  .attr(\"stroke\", t.grid);\n\n// --- Bands ---------------------------------------------------------------------\nconst area = d3\n  .area()\n  .x((d, i) => x(dates[i]))\n  .y0((d) => y(d[0]))\n  .y1((d) => y(d[1]));\n\ng.selectAll(\"path.band\")\n  .data(series)\n  .join(\"path\")\n  .attr(\"class\", \"band\")\n  .attr(\"fill\", (d) => color(d.key))\n  .attr(\"d\", (d) => area(d));\n\n// --- Axes ------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(d3.timeWeek.every(2)).tickFormat(d3.timeFormat(\"%b %d\")));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(6));\n\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.grid);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\nxAxis.selectAll(\"text\").attr(\"dy\", \"1.4em\");\n\n// --- Axis labels -------------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 68)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .text(\"Date\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -80)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"16px\")\n  .text(\"Cumulative Ticket Count\");\n\n// --- Legend (workflow order, top stage first) ---------------------------------\nconst legend = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left + iw + 40}, ${margin.top + 20})`);\n\nconst legendRows = legend\n  .selectAll(\"g\")\n  .data(stages)\n  .join(\"g\")\n  .attr(\"transform\", (_, i) => `translate(0, ${i * 34})`);\n\nlegendRows\n  .append(\"rect\")\n  .attr(\"width\", 18)\n  .attr(\"height\", 18)\n  .attr(\"rx\", 3)\n  .attr(\"fill\", (d) => color(d));\n\nlegendRows\n  .append(\"text\")\n  .attr(\"x\", 26)\n  .attr(\"y\", 14)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"15px\")\n  .text((d) => d);\n\n// --- Title ---------------------------------------------------------------------\nconst title = \"Support Ticket Pipeline · area-cumulative-flow · javascript · d3 · anyplot.ai\";\nconst titleFontSize = Math.max(16, Math.round(22 * Math.min(1, 67 / title.length)));\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 50)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", `${titleFontSize}px`)\n  .style(\"font-weight\", \"600\")\n  .text(title);\n"}