Kolmogorov-Smirnov Plot for Distribution Comparison — Chart.js

A Kolmogorov-Smirnov (K-S) plot compares two empirical cumulative distribution functions (ECDFs) and visualizes the K-S statistic, which is the maximum vertical distance between the two distributions. The plot displays both ECDFs as step functions, highlights the point of maximum divergence, and typically reports the K-S statistic value and p-value for hypothesis testing. This visualization is essential for determining whether two samples come from the same underlying distribution.

Kolmogorov-Smirnov Plot for Distribution Comparison rendered with Chart.js

Renders

JavaScript source (Chart.js)

// anyplot.ai
// ks-test-comparison: Kolmogorov-Smirnov Plot for Distribution Comparison
// Library: chartjs 4.4.7 | JavaScript 22.23.2
// Quality: 89/100 | Created: 2026-08-26

const t = window.ANYPLOT_TOKENS;

// --- Data (in-memory, deterministic LCG + Box-Muller) -----------------------
function lcg(seed) {
  let state = seed >>> 0;
  return function () {
    state = (1103515245 * state + 12345) >>> 0;
    return state / 4294967296;
  };
}
const rand = lcg(42);
function randNormal() {
  const u1 = Math.max(rand(), 1e-12);
  const u2 = rand();
  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}
function clamp(v, lo, hi) {
  return Math.min(Math.max(v, lo), hi);
}

const n1 = 300;
const n2 = 250;
const goodScores = Array.from({ length: n1 }, () => clamp(680 + 45 * randNormal(), 300, 850)).sort((a, b) => a - b);
const badScores = Array.from({ length: n2 }, () => clamp(610 + 55 * randNormal(), 300, 850)).sort((a, b) => a - b);

// --- ECDF step coordinates ---------------------------------------------------
const xMin = Math.min(goodScores[0], badScores[0]);
const xMax = Math.max(goodScores[n1 - 1], badScores[n2 - 1]);
const pad = (xMax - xMin) * 0.03;

function ecdfSteps(sorted) {
  const n = sorted.length;
  const pts = [{ x: xMin - pad, y: 0 }];
  sorted.forEach((v, i) => pts.push({ x: v, y: (i + 1) / n }));
  pts.push({ x: xMax + pad, y: 1 });
  return pts;
}

function ecdfAt(sorted, x) {
  let lo = 0;
  let hi = sorted.length;
  while (lo < hi) {
    const mid = (lo + hi) >> 1;
    if (sorted[mid] <= x) lo = mid + 1;
    else hi = mid;
  }
  return lo / sorted.length;
}

// --- K-S statistic: max |ECDF1 - ECDF2|, evaluated at every sample value ----
let ksD = 0;
let ksX = goodScores[0];
goodScores.concat(badScores).forEach((x) => {
  const diff = Math.abs(ecdfAt(goodScores, x) - ecdfAt(badScores, x));
  if (diff > ksD) {
    ksD = diff;
    ksX = x;
  }
});
const ksYGood = ecdfAt(goodScores, ksX);
const ksYBad = ecdfAt(badScores, ksX);

// Two-sample K-S asymptotic p-value (Kolmogorov distribution series)
function ksPValue(d, sizeA, sizeB) {
  const nEff = (sizeA * sizeB) / (sizeA + sizeB);
  const en = Math.sqrt(nEff);
  const lambda = (en + 0.12 + 0.11 / en) * d;
  let sum = 0;
  for (let k = 1; k <= 100; k++) {
    sum += (k % 2 === 0 ? -1 : 1) * Math.exp(-2 * k * k * lambda * lambda);
  }
  return clamp(2 * sum, 0, 1);
}
const pValue = ksPValue(ksD, n1, n2);
const pLabel = pValue < 0.001 ? "< 0.001" : pValue.toFixed(3);

// --- Mount --------------------------------------------------------------
const canvas = document.createElement("canvas");
document.getElementById("container").appendChild(canvas);

// --- Chart ----------------------------------------------------------------
new Chart(canvas, {
  type: "line",
  data: {
    datasets: [
      {
        label: `Good customers (n=${n1})`,
        data: ecdfSteps(goodScores),
        stepped: "after",
        borderColor: t.palette[0],
        backgroundColor: "transparent",
        borderWidth: 3.5,
        pointRadius: 0,
        fill: false,
      },
      {
        label: `Bad customers (n=${n2})`,
        data: ecdfSteps(badScores),
        stepped: "after",
        borderColor: t.palette[4],
        backgroundColor: "transparent",
        borderWidth: 3.5,
        pointRadius: 0,
        fill: false,
      },
      {
        label: `Max distance D = ${ksD.toFixed(3)}`,
        data: [
          { x: ksX, y: ksYGood },
          { x: ksX, y: ksYBad },
        ],
        borderColor: t.amber,
        backgroundColor: t.amber,
        borderWidth: 3,
        borderDash: [8, 5],
        pointRadius: 6,
        pointBackgroundColor: t.amber,
        pointBorderColor: t.pageBg,
        pointBorderWidth: 2,
        fill: false,
      },
    ],
  },
  options: {
    responsive: true,
    maintainAspectRatio: false,
    animation: false,
    plugins: {
      title: {
        display: true,
        text: "ks-test-comparison · javascript · chartjs · anyplot.ai",
        color: t.ink,
        font: { size: 27, weight: "500" },
        padding: { bottom: 8 },
      },
      subtitle: {
        display: true,
        text: `K-S statistic D = ${ksD.toFixed(3)}  ·  p-value = ${pLabel}`,
        color: t.inkSoft,
        font: { size: 16 },
        padding: { bottom: 20 },
      },
      legend: {
        position: "bottom",
        labels: {
          color: t.ink,
          font: { size: 16 },
          usePointStyle: true,
          filter: (item) => item.datasetIndex < 2,
        },
      },
      tooltip: {
        callbacks: {
          title: (items) => `Credit score ${Math.round(items[0].parsed.x)}`,
          label: (item) =>
            item.datasetIndex === 2
              ? `Max divergence: D = ${ksD.toFixed(3)}`
              : `${item.dataset.label.replace(/\s*\(n=\d+\)/, "")}: ${(item.parsed.y * 100).toFixed(1)}% cumulative`,
        },
      },
    },
    scales: {
      x: {
        type: "linear",
        title: { display: true, text: "Credit Score", color: t.ink, font: { size: 16 } },
        ticks: { color: t.inkSoft, font: { size: 14 } },
        grid: { display: false },
      },
      y: {
        min: 0,
        max: 1,
        title: { display: true, text: "Cumulative Proportion", color: t.ink, font: { size: 16 } },
        ticks: { color: t.inkSoft, font: { size: 14 } },
        grid: { color: t.grid },
      },
    },
  },
});

Part of Kolmogorov-Smirnov Plot for Distribution Comparison on anyplot.ai.

Other implementations