{"spec_id":"histogram-cumulative","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// histogram-cumulative: Cumulative Histogram\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic fixed-seed LCG) -------------------------\n// Package delivery times (minutes) — a right-skewed distribution where the\n// cumulative view answers \"what share of packages arrive within X minutes?\"\nlet seed = 42;\nfunction lcgRandom() {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n}\nfunction randomNormal(mean, stdDev) {\n  const u1 = lcgRandom() || 1e-9;\n  const u2 = lcgRandom();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * stdDev;\n}\n\nconst sampleCount = 400;\nconst deliveryTimes = [];\nfor (let i = 0; i < sampleCount; i++) {\n  const base = Math.exp(randomNormal(Math.log(30), 0.35));\n  deliveryTimes.push(Math.max(5, base));\n}\n\n// Nearest-rank percentile helper on the raw (unbinned) sample.\nconst sortedTimes = [...deliveryTimes].sort((a, b) => a - b);\nconst percentile = (p) => sortedTimes[Math.min(sortedTimes.length - 1, Math.floor(p * sortedTimes.length))];\nconst p90Value = percentile(0.9);\n\n// Trim the long, near-featureless tail: bin only up to the 96th percentile\n// and collapse the sparse remainder into a single \"overflow\" bin, so the\n// informative rise of the S-curve gets most of the horizontal space.\nconst binWidth = 5;\nconst binMax = Math.ceil(percentile(0.96) / binWidth) * binWidth;\nconst binCount = binMax / binWidth;\nconst binCounts = new Array(binCount + 1).fill(0);\nfor (const value of deliveryTimes) {\n  const idx = value >= binMax ? binCount : Math.floor(value / binWidth);\n  binCounts[idx] += 1;\n}\n\nconst binLabels = binCounts.map((_, i) => (i < binCount ? `${i * binWidth}–${(i + 1) * binWidth}` : `${binMax}+`));\nlet running = 0;\nconst cumulativeProportion = binCounts.map((count) => {\n  running += count;\n  return running / sampleCount;\n});\nconst p90BinIndex = Math.min(binCount - 1, Math.floor(p90Value / binWidth));\n\n// --- Mount -------------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Custom plugin: dashed p90 guide line + label ---------------------------\n// Drawn with the canvas 2D API directly against the chart's own scales — a\n// plain inline Chart.js plugin, not a community/annotation package.\nconst p90MarkerPlugin = {\n  id: \"p90Marker\",\n  afterDraw(chart) {\n    const { ctx, chartArea, scales } = chart;\n    const x = scales.x.getPixelForValue(p90BinIndex);\n    const nearRightEdge = x > chartArea.left + (chartArea.right - chartArea.left) * 0.85;\n\n    ctx.save();\n    ctx.strokeStyle = t.amber;\n    ctx.lineWidth = 2;\n    ctx.setLineDash([6, 4]);\n    ctx.beginPath();\n    ctx.moveTo(x, chartArea.top);\n    ctx.lineTo(x, chartArea.bottom);\n    ctx.stroke();\n    ctx.setLineDash([]);\n\n    ctx.fillStyle = t.amber;\n    ctx.font = \"600 14px sans-serif\";\n    ctx.textAlign = nearRightEdge ? \"right\" : \"left\";\n    ctx.fillText(`p90 ≈ ${Math.round(p90Value)} min`, x + (nearRightEdge ? -8 : 8), chartArea.top + 18);\n    ctx.restore();\n  },\n};\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"line\",\n  data: {\n    labels: binLabels,\n    datasets: [\n      {\n        label: \"Cumulative proportion\",\n        data: cumulativeProportion,\n        borderColor: t.palette[0],\n        backgroundColor: `${t.palette[0]}26`,\n        borderWidth: 3,\n        pointRadius: 0,\n        pointHoverRadius: 0,\n        stepped: \"after\",\n        fill: \"origin\",\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"histogram-cumulative · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"500\" },\n        padding: { bottom: 20 },\n      },\n      legend: { display: false },\n      tooltip: { enabled: false },\n    },\n    scales: {\n      x: {\n        ticks: { color: t.inkSoft, font: { size: 14 }, maxRotation: 0, autoSkip: true },\n        grid: { display: false },\n        title: { display: true, text: \"Delivery Time (minutes)\", color: t.ink, font: { size: 16 } },\n      },\n      y: {\n        min: 0,\n        max: 1,\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          callback: (value) => `${Math.round(value * 100)}%`,\n        },\n        grid: { color: t.grid },\n        title: { display: true, text: \"Cumulative Share of Deliveries\", color: t.ink, font: { size: 16 } },\n      },\n    },\n  },\n  plugins: [p90MarkerPlugin],\n});\n"}