{"spec_id":"histogram-cumulative","library":"d3","language":"javascript","code":"// anyplot.ai\n// histogram-cumulative: Cumulative Histogram\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst theme = window.ANYPLOT_THEME;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 100, right: 70, bottom: 100, left: 120 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// The \"muted / other / rest\" semantic anchor from the style guide isn't part\n// of window.ANYPLOT_TOKENS for JS, so it's reproduced here directly.\nconst muted = theme === \"dark\" ? \"#A8A79F\" : \"#6B6A63\";\nconst mutedOpacity = theme === \"dark\" ? 0.6 : 0.45;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Deterministic LCG so the sample is reproducible without a seeded Math.random.\nconst lcg = (seed) => {\n  let s = seed % 2147483647;\n  return () => (s = (s * 48271) % 2147483647) / 2147483647;\n};\nconst rand = lcg(42);\n\n// Call-center wait times (seconds) — right-skewed, as most calls answer quickly\n// and a long tail waits far longer. Exponential draws model that shape well.\nconst MEAN_WAIT = 32;\nconst waitTimes = Array.from({ length: 650 }, () => -MEAN_WAIT * Math.log(1 - rand()));\n\n// Cap the visible domain at the 99th percentile rather than the raw sample\n// max: a single slow outlier call would otherwise stretch the x-axis with a\n// long, information-free flat tail. Values beyond the cap are clamped into\n// the final bin so the cumulative curve still reaches exactly 100%.\nconst sortedWait = waitTimes.slice().sort(d3.ascending);\nconst p99Wait = d3.quantile(sortedWait, 0.99);\nconst maxWait = Math.ceil(p99Wait / 10) * 10;\nconst clampedWaitTimes = waitTimes.map((w) => Math.min(w, maxWait));\nconst binCount = 20;\nconst bins = d3.bin().domain([0, maxWait]).thresholds(binCount)(clampedWaitTimes);\n\nlet running = 0;\nconst total = waitTimes.length;\nconst steps = [{ x: bins[0].x0, y: 0 }];\nfor (const bin of bins) {\n  running += bin.length;\n  steps.push({ x: bin.x1, y: (running / total) * 100 });\n}\nconst maxBinCount = d3.max(bins, (b) => b.length);\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.scaleLinear().domain([0, maxWait]).range([0, iw]);\nconst y = d3.scaleLinear().domain([0, 100]).range([ih, 0]);\nconst barHeight = d3.scaleLinear().domain([0, maxBinCount]).range([0, ih * 0.4]);\n\n// --- Gridlines (y-axis only, subtle) -----------------------------------------\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(y.ticks(5))\n  .join(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", (d) => y(d))\n  .attr(\"y2\", (d) => y(d))\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// --- Underlying histogram (secondary encoding, muted) ------------------------\ng.selectAll(\"rect.freq\")\n  .data(bins)\n  .join(\"rect\")\n  .attr(\"class\", \"freq\")\n  .attr(\"x\", (d) => x(d.x0) + 1)\n  .attr(\"width\", (d) => Math.max(0, x(d.x1) - x(d.x0) - 2))\n  .attr(\"y\", (d) => ih - barHeight(d.length))\n  .attr(\"height\", (d) => barHeight(d.length))\n  .attr(\"fill\", muted)\n  .attr(\"opacity\", mutedOpacity);\n\n// --- Cumulative step curve (primary series) -----------------------------------\nconst areaGen = d3\n  .area()\n  .x((d) => x(d.x))\n  .y0(ih)\n  .y1((d) => y(d.y))\n  .curve(d3.curveStepAfter);\n\nconst lineGen = d3\n  .line()\n  .x((d) => x(d.x))\n  .y((d) => y(d.y))\n  .curve(d3.curveStepAfter);\n\ng.append(\"path\").datum(steps).attr(\"d\", areaGen).attr(\"fill\", t.palette[0]).attr(\"opacity\", 0.16);\ng.append(\"path\")\n  .datum(steps)\n  .attr(\"d\", lineGen)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 3.5)\n  .attr(\"stroke-linejoin\", \"round\");\n\n// --- Axes ----------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(8).tickFormat((d) => `${d}s`));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(5).tickFormat((d) => `${d}%`));\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.inkSoft);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\ng.selectAll(\".tick line\").attr(\"stroke\", t.inkSoft);\n\n// --- Axis labels -----------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 64)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Call Wait Time (seconds)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -88)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"18px\")\n  .text(\"Cumulative Share of Calls\");\n\n// --- Legend ------------------------------------------------------------------\nconst legend = svg.append(\"g\").attr(\"transform\", `translate(${width - margin.right - 260}, ${margin.top - 56})`);\nconst legendItems = [\n  { label: \"Cumulative %\", color: t.palette[0], opacity: 1 },\n  { label: \"Calls per bin\", color: muted, opacity: mutedOpacity },\n];\nlegendItems.forEach((item, i) => {\n  const row = legend.append(\"g\").attr(\"transform\", `translate(${i * 150}, 0)`);\n  row.append(\"rect\").attr(\"width\", 16).attr(\"height\", 16).attr(\"fill\", item.color).attr(\"opacity\", item.opacity);\n  row\n    .append(\"text\")\n    .attr(\"x\", 24)\n    .attr(\"y\", 13)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .text(item.label);\n});\n\n// --- Title -------------------------------------------------------------------\nconst TITLE = \"Call Center Wait Times · histogram-cumulative · javascript · d3 · anyplot.ai\";\nconst titleFontSize = Math.max(15, Math.round(22 * Math.min(1, 67 / TITLE.length)));\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 48)\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"}