{"spec_id":"ks-test-comparison","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// ks-test-comparison: Kolmogorov-Smirnov Plot for Distribution Comparison\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 89/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic LCG + Box-Muller) -----------------------\nfunction lcg(seed) {\n  let state = seed >>> 0;\n  return function () {\n    state = (1103515245 * state + 12345) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rand = lcg(42);\nfunction randNormal() {\n  const u1 = Math.max(rand(), 1e-12);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\nfunction clamp(v, lo, hi) {\n  return Math.min(Math.max(v, lo), hi);\n}\n\nconst n1 = 300;\nconst n2 = 250;\nconst goodScores = Array.from({ length: n1 }, () => clamp(680 + 45 * randNormal(), 300, 850)).sort((a, b) => a - b);\nconst badScores = Array.from({ length: n2 }, () => clamp(610 + 55 * randNormal(), 300, 850)).sort((a, b) => a - b);\n\n// --- ECDF step coordinates ---------------------------------------------------\nconst xMin = Math.min(goodScores[0], badScores[0]);\nconst xMax = Math.max(goodScores[n1 - 1], badScores[n2 - 1]);\nconst pad = (xMax - xMin) * 0.03;\n\nfunction ecdfSteps(sorted) {\n  const n = sorted.length;\n  const pts = [{ x: xMin - pad, y: 0 }];\n  sorted.forEach((v, i) => pts.push({ x: v, y: (i + 1) / n }));\n  pts.push({ x: xMax + pad, y: 1 });\n  return pts;\n}\n\nfunction ecdfAt(sorted, x) {\n  let lo = 0;\n  let hi = sorted.length;\n  while (lo < hi) {\n    const mid = (lo + hi) >> 1;\n    if (sorted[mid] <= x) lo = mid + 1;\n    else hi = mid;\n  }\n  return lo / sorted.length;\n}\n\n// --- K-S statistic: max |ECDF1 - ECDF2|, evaluated at every sample value ----\nlet ksD = 0;\nlet ksX = goodScores[0];\ngoodScores.concat(badScores).forEach((x) => {\n  const diff = Math.abs(ecdfAt(goodScores, x) - ecdfAt(badScores, x));\n  if (diff > ksD) {\n    ksD = diff;\n    ksX = x;\n  }\n});\nconst ksYGood = ecdfAt(goodScores, ksX);\nconst ksYBad = ecdfAt(badScores, ksX);\n\n// Two-sample K-S asymptotic p-value (Kolmogorov distribution series)\nfunction ksPValue(d, sizeA, sizeB) {\n  const nEff = (sizeA * sizeB) / (sizeA + sizeB);\n  const en = Math.sqrt(nEff);\n  const lambda = (en + 0.12 + 0.11 / en) * d;\n  let sum = 0;\n  for (let k = 1; k <= 100; k++) {\n    sum += (k % 2 === 0 ? -1 : 1) * Math.exp(-2 * k * k * lambda * lambda);\n  }\n  return clamp(2 * sum, 0, 1);\n}\nconst pValue = ksPValue(ksD, n1, n2);\nconst pLabel = pValue < 0.001 ? \"< 0.001\" : pValue.toFixed(3);\n\n// --- Mount --------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Chart ----------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"line\",\n  data: {\n    datasets: [\n      {\n        label: `Good customers (n=${n1})`,\n        data: ecdfSteps(goodScores),\n        stepped: \"after\",\n        borderColor: t.palette[0],\n        backgroundColor: \"transparent\",\n        borderWidth: 3.5,\n        pointRadius: 0,\n        fill: false,\n      },\n      {\n        label: `Bad customers (n=${n2})`,\n        data: ecdfSteps(badScores),\n        stepped: \"after\",\n        borderColor: t.palette[4],\n        backgroundColor: \"transparent\",\n        borderWidth: 3.5,\n        pointRadius: 0,\n        fill: false,\n      },\n      {\n        label: `Max distance D = ${ksD.toFixed(3)}`,\n        data: [\n          { x: ksX, y: ksYGood },\n          { x: ksX, y: ksYBad },\n        ],\n        borderColor: t.amber,\n        backgroundColor: t.amber,\n        borderWidth: 3,\n        borderDash: [8, 5],\n        pointRadius: 6,\n        pointBackgroundColor: t.amber,\n        pointBorderColor: t.pageBg,\n        pointBorderWidth: 2,\n        fill: false,\n      },\n    ],\n  },\n  options: {\n    responsive: true,\n    maintainAspectRatio: false,\n    animation: false,\n    plugins: {\n      title: {\n        display: true,\n        text: \"ks-test-comparison · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 27, weight: \"500\" },\n        padding: { bottom: 8 },\n      },\n      subtitle: {\n        display: true,\n        text: `K-S statistic D = ${ksD.toFixed(3)}  ·  p-value = ${pLabel}`,\n        color: t.inkSoft,\n        font: { size: 16 },\n        padding: { bottom: 20 },\n      },\n      legend: {\n        position: \"bottom\",\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          usePointStyle: true,\n          filter: (item) => item.datasetIndex < 2,\n        },\n      },\n      tooltip: {\n        callbacks: {\n          title: (items) => `Credit score ${Math.round(items[0].parsed.x)}`,\n          label: (item) =>\n            item.datasetIndex === 2\n              ? `Max divergence: D = ${ksD.toFixed(3)}`\n              : `${item.dataset.label.replace(/\\s*\\(n=\\d+\\)/, \"\")}: ${(item.parsed.y * 100).toFixed(1)}% cumulative`,\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        title: { display: true, text: \"Credit Score\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { display: false },\n      },\n      y: {\n        min: 0,\n        max: 1,\n        title: { display: true, text: \"Cumulative Proportion\", color: t.ink, font: { size: 16 } },\n        ticks: { color: t.inkSoft, font: { size: 14 } },\n        grid: { color: t.grid },\n      },\n    },\n  },\n});\n"}