Cumulative Histogram — D3.js

A cumulative histogram (also known as an ogive or cumulative frequency histogram) displays the running total of observations up to each bin boundary. The y-axis shows cumulative count or proportion, creating a monotonically increasing step function that reaches the total sample size (or 1.0 for normalized).

Cumulative Histogram rendered with D3.js

Renders

JavaScript source (D3.js)

// anyplot.ai
// histogram-cumulative: Cumulative Histogram
// Library: d3 7.9.0 | JavaScript 22.23.2
// Quality: 89/100 | Created: 2026-09-05

const t = window.ANYPLOT_TOKENS;
const theme = window.ANYPLOT_THEME;
const { width, height } = window.ANYPLOT_SIZE;
const margin = { top: 100, right: 70, bottom: 100, left: 120 };
const iw = width - margin.left - margin.right;
const ih = height - margin.top - margin.bottom;

// The "muted / other / rest" semantic anchor from the style guide isn't part
// of window.ANYPLOT_TOKENS for JS, so it's reproduced here directly.
const muted = theme === "dark" ? "#A8A79F" : "#6B6A63";
const mutedOpacity = theme === "dark" ? 0.6 : 0.45;

// --- Data (in-memory, deterministic) ----------------------------------------
// Deterministic LCG so the sample is reproducible without a seeded Math.random.
const lcg = (seed) => {
  let s = seed % 2147483647;
  return () => (s = (s * 48271) % 2147483647) / 2147483647;
};
const rand = lcg(42);

// Call-center wait times (seconds) — right-skewed, as most calls answer quickly
// and a long tail waits far longer. Exponential draws model that shape well.
const MEAN_WAIT = 32;
const waitTimes = Array.from({ length: 650 }, () => -MEAN_WAIT * Math.log(1 - rand()));

// Cap the visible domain at the 99th percentile rather than the raw sample
// max: a single slow outlier call would otherwise stretch the x-axis with a
// long, information-free flat tail. Values beyond the cap are clamped into
// the final bin so the cumulative curve still reaches exactly 100%.
const sortedWait = waitTimes.slice().sort(d3.ascending);
const p99Wait = d3.quantile(sortedWait, 0.99);
const maxWait = Math.ceil(p99Wait / 10) * 10;
const clampedWaitTimes = waitTimes.map((w) => Math.min(w, maxWait));
const binCount = 20;
const bins = d3.bin().domain([0, maxWait]).thresholds(binCount)(clampedWaitTimes);

let running = 0;
const total = waitTimes.length;
const steps = [{ x: bins[0].x0, y: 0 }];
for (const bin of bins) {
  running += bin.length;
  steps.push({ x: bin.x1, y: (running / total) * 100 });
}
const maxBinCount = d3.max(bins, (b) => b.length);

// --- SVG mount ---------------------------------------------------------------
const svg = d3.select("#container").append("svg").attr("width", width).attr("height", height);
const g = svg.append("g").attr("transform", `translate(${margin.left},${margin.top})`);

// --- Scales --------------------------------------------------------------
const x = d3.scaleLinear().domain([0, maxWait]).range([0, iw]);
const y = d3.scaleLinear().domain([0, 100]).range([ih, 0]);
const barHeight = d3.scaleLinear().domain([0, maxBinCount]).range([0, ih * 0.4]);

// --- Gridlines (y-axis only, subtle) -----------------------------------------
g.append("g")
  .selectAll("line")
  .data(y.ticks(5))
  .join("line")
  .attr("x1", 0)
  .attr("x2", iw)
  .attr("y1", (d) => y(d))
  .attr("y2", (d) => y(d))
  .attr("stroke", t.grid)
  .attr("stroke-width", 1);

// --- Underlying histogram (secondary encoding, muted) ------------------------
g.selectAll("rect.freq")
  .data(bins)
  .join("rect")
  .attr("class", "freq")
  .attr("x", (d) => x(d.x0) + 1)
  .attr("width", (d) => Math.max(0, x(d.x1) - x(d.x0) - 2))
  .attr("y", (d) => ih - barHeight(d.length))
  .attr("height", (d) => barHeight(d.length))
  .attr("fill", muted)
  .attr("opacity", mutedOpacity);

// --- Cumulative step curve (primary series) -----------------------------------
const areaGen = d3
  .area()
  .x((d) => x(d.x))
  .y0(ih)
  .y1((d) => y(d.y))
  .curve(d3.curveStepAfter);

const lineGen = d3
  .line()
  .x((d) => x(d.x))
  .y((d) => y(d.y))
  .curve(d3.curveStepAfter);

g.append("path").datum(steps).attr("d", areaGen).attr("fill", t.palette[0]).attr("opacity", 0.16);
g.append("path")
  .datum(steps)
  .attr("d", lineGen)
  .attr("fill", "none")
  .attr("stroke", t.palette[0])
  .attr("stroke-width", 3.5)
  .attr("stroke-linejoin", "round");

// --- Axes ----------------------------------------------------------------
const xAxis = g
  .append("g")
  .attr("transform", `translate(0,${ih})`)
  .call(d3.axisBottom(x).ticks(8).tickFormat((d) => `${d}s`));
const yAxis = g.append("g").call(d3.axisLeft(y).ticks(5).tickFormat((d) => `${d}%`));
for (const ax of [xAxis, yAxis]) {
  ax.selectAll("text").attr("fill", t.inkSoft).style("font-size", "14px");
  ax.selectAll("line").attr("stroke", t.inkSoft);
  ax.select(".domain").attr("stroke", t.inkSoft);
}
g.selectAll(".tick line").attr("stroke", t.inkSoft);

// --- Axis labels -----------------------------------------------------------
g.append("text")
  .attr("x", iw / 2)
  .attr("y", ih + 64)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "18px")
  .text("Call Wait Time (seconds)");

g.append("text")
  .attr("transform", "rotate(-90)")
  .attr("x", -ih / 2)
  .attr("y", -88)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "18px")
  .text("Cumulative Share of Calls");

// --- Legend ------------------------------------------------------------------
const legend = svg.append("g").attr("transform", `translate(${width - margin.right - 260}, ${margin.top - 56})`);
const legendItems = [
  { label: "Cumulative %", color: t.palette[0], opacity: 1 },
  { label: "Calls per bin", color: muted, opacity: mutedOpacity },
];
legendItems.forEach((item, i) => {
  const row = legend.append("g").attr("transform", `translate(${i * 150}, 0)`);
  row.append("rect").attr("width", 16).attr("height", 16).attr("fill", item.color).attr("opacity", item.opacity);
  row
    .append("text")
    .attr("x", 24)
    .attr("y", 13)
    .attr("fill", t.inkSoft)
    .style("font-size", "14px")
    .text(item.label);
});

// --- Title -------------------------------------------------------------------
const TITLE = "Call Center Wait Times · histogram-cumulative · javascript · d3 · anyplot.ai";
const titleFontSize = Math.max(15, Math.round(22 * Math.min(1, 67 / TITLE.length)));
svg
  .append("text")
  .attr("x", width / 2)
  .attr("y", 48)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", `${titleFontSize}px`)
  .style("font-weight", "600")
  .text(TITLE);

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/histogram-cumulative/d3/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "histogram-cumulative",
  "language": "javascript",
  "library": "d3",
  "page": "https://anyplot.ai/histogram-cumulative/javascript/d3",
  "hub": "https://anyplot.ai/histogram-cumulative",
  "code_json": "https://api.anyplot.ai/specs/histogram-cumulative/d3/code",
  "spec_json": "https://api.anyplot.ai/specs/histogram-cumulative",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/histogram-cumulative/javascript/d3/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/histogram-cumulative/javascript/d3/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/histogram-cumulative/javascript/d3/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/histogram-cumulative/javascript/d3/plot-dark.html",
  "quality_score": 89.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Cumulative Histogram on anyplot.ai.

Other implementations