Basic ECDF Plot — D3.js

An ECDF (Empirical Cumulative Distribution Function) plot displays a step function that shows the proportion of observations less than or equal to each value. Unlike histograms, ECDF plots require no binning or smoothing, providing a non-parametric estimate of the cumulative distribution. The y-axis ranges from 0 to 1, allowing direct reading of percentiles and quantiles from the visualization.

Basic ECDF Plot rendered with D3.js

Renders

JavaScript source (D3.js)

// anyplot.ai
// ecdf-basic: Basic ECDF Plot
// Library: d3 7.9.0 | JavaScript 22.22.3
// Quality: 90/100 | Created: 2026-06-25

//# anyplot-orientation: landscape

const t = window.ANYPLOT_TOKENS;
const { width, height } = window.ANYPLOT_SIZE;
const margin = { top: 80, right: 130, bottom: 80, left: 90 };
const iw = width - margin.left - margin.right;
const ih = height - margin.top - margin.bottom;

// Deterministic LCG + Box-Muller for reproducible normal samples
function makeLCG(seed) {
  let s = seed >>> 0;
  return () => { s = (Math.imul(1664525, s) + 1013904223) >>> 0; return s / 0x100000000; };
}

function normalSamples(n, mean, std, rng) {
  const out = [];
  for (let i = 0; i < n; i++) {
    const u1 = Math.max(1e-12, rng());
    const u2 = rng();
    out.push(mean + std * Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2));
  }
  return out.sort((a, b) => a - b);
}

const expressDelivery = normalSamples(160, 3.2, 0.6, makeLCG(42));
const standardDelivery = normalSamples(160, 4.8, 1.3, makeLCG(137));

// Build ECDF step points: prepend (xStart, 0) so the line starts from 0 at left
function ecdfPoints(sorted, xStart, xEnd) {
  const n = sorted.length;
  return [
    { x: xStart, y: 0 },
    ...sorted.map((v, i) => ({ x: v, y: (i + 1) / n })),
    { x: xEnd, y: 1 },
  ];
}

const allValues = [...expressDelivery, ...standardDelivery];
const pad = (d3.max(allValues) - d3.min(allValues)) * 0.04;
const xDomMin = d3.min(allValues) - pad;
const xDomMax = d3.max(allValues) + pad;

const expressECDF = ecdfPoints(expressDelivery, xDomMin, xDomMax);
const standardECDF = ecdfPoints(standardDelivery, xDomMin, xDomMax);

// 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([xDomMin, xDomMax]).range([0, iw]);
const y = d3.scaleLinear().domain([0, 1]).range([ih, 0]);

// Horizontal grid lines at quartile proportions
g.selectAll(".hgrid").data([0.25, 0.5, 0.75]).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);

// Axes
const xAxis = g.append("g").attr("transform", `translate(0,${ih})`)
  .call(d3.axisBottom(x).ticks(8).tickSize(5));
const yAxis = g.append("g")
  .call(d3.axisLeft(y).tickValues([0, 0.25, 0.5, 0.75, 1]).tickFormat(d3.format(".0%")).tickSize(5));

for (const ax of [xAxis, yAxis]) {
  ax.selectAll("text").attr("fill", t.inkSoft).style("font-size", "14px");
  ax.selectAll(".tick line").attr("stroke", t.inkSoft);
  ax.select(".domain").attr("stroke", t.inkSoft);
}

// ECDF step lines — curveStepAfter matches ECDF semantics: step up AT each observation
const line = d3.line().x(d => x(d.x)).y(d => y(d.y)).curve(d3.curveStepAfter);

g.append("path").datum(expressECDF)
  .attr("fill", "none").attr("stroke", t.palette[0]).attr("stroke-width", 3).attr("d", line);

g.append("path").datum(standardECDF)
  .attr("fill", "none").attr("stroke", t.palette[1]).attr("stroke-width", 3).attr("d", line);

// Axis labels
svg.append("text")
  .attr("x", margin.left + iw / 2).attr("y", height - 18)
  .attr("text-anchor", "middle").attr("fill", t.inkSoft).style("font-size", "16px")
  .text("Delivery Time (days)");

svg.append("text")
  .attr("transform", "rotate(-90)")
  .attr("x", -(margin.top + ih / 2)).attr("y", 22)
  .attr("text-anchor", "middle").attr("fill", t.inkSoft).style("font-size", "16px")
  .text("Cumulative Proportion");

// Legend (top-left inside plot area — region near (xMin, y=1) is empty for ECDF)
[
  { label: "Express Shipping (n=160)", color: t.palette[0] },
  { label: "Standard Shipping (n=160)", color: t.palette[1] },
].forEach(({ label, color }, i) => {
  const ly = 24 + i * 32;
  g.append("line").attr("x1", 20).attr("x2", 58).attr("y1", ly).attr("y2", ly)
    .attr("stroke", color).attr("stroke-width", 3);
  g.append("text").attr("x", 68).attr("y", ly + 5)
    .attr("fill", t.inkSoft).style("font-size", "14px").text(label);
});

// Title
svg.append("text")
  .attr("x", width / 2).attr("y", 44).attr("text-anchor", "middle")
  .attr("fill", t.ink).style("font-size", "22px").style("font-weight", "600")
  .text("ecdf-basic · javascript · d3 · anyplot.ai");

Part of Basic ECDF Plot on anyplot.ai.

Other implementations