{"spec_id":"ks-test-comparison","library":"d3","language":"javascript","code":"// anyplot.ai\n// ks-test-comparison: Kolmogorov-Smirnov Plot for Distribution Comparison\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 120, right: 70, bottom: 90, left: 100 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Deterministic PRNG (LCG) + Box-Muller normal sampler -------------------\nlet seed = 42;\nfunction nextUniform() {\n  seed = (seed * 1103515245 + 12345) % 2147483648;\n  return seed / 2147483648;\n}\nfunction nextNormal(mean, std) {\n  const u1 = Math.max(nextUniform(), 1e-9);\n  const u2 = nextUniform();\n  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n  return mean + z * std;\n}\n\n// --- Data: credit scores for Good vs Bad loan customers ---------------------\nconst n1 = 260;\nconst n2 = 220;\nconst goodScores = Array.from({ length: n1 }, () => nextNormal(680, 55));\nconst badScores = Array.from({ length: n2 }, () => nextNormal(590, 65));\n\nconst sorted1 = goodScores.slice().sort((a, b) => a - b);\nconst sorted2 = badScores.slice().sort((a, b) => a - b);\n\n// --- Two-sample K-S statistic (merge sweep over both sorted samples) -------\nlet i = 0;\nlet j = 0;\nlet ksD = 0;\nlet ksX = sorted1[0];\nlet ksD1 = 0;\nlet ksD2 = 0;\nwhile (i < n1 && j < n2) {\n  const x = sorted1[i] <= sorted2[j] ? sorted1[i] : sorted2[j];\n  if (sorted1[i] <= sorted2[j]) i += 1;\n  else j += 1;\n  const d1 = i / n1;\n  const d2 = j / n2;\n  const diff = Math.abs(d1 - d2);\n  if (diff > ksD) {\n    ksD = diff;\n    ksX = x;\n    ksD1 = d1;\n    ksD2 = d2;\n  }\n}\n\n// Asymptotic two-sample K-S p-value (Kolmogorov distribution series)\nconst nEff = (n1 * n2) / (n1 + n2);\nconst lambda = (Math.sqrt(nEff) + 0.12 + 0.11 / Math.sqrt(nEff)) * ksD;\nlet pValue = 0;\nfor (let k = 1; k <= 100; k += 1) {\n  pValue += 2 * (-1) ** (k - 1) * Math.exp(-2 * k * k * lambda * lambda);\n}\npValue = Math.min(Math.max(pValue, 0), 1);\nconst pLabel = pValue < 0.001 ? \"p < 0.001\" : `p = ${pValue.toFixed(3)}`;\n\n// --- Scales -------------------------------------------------------------------\nconst allScores = sorted1.concat(sorted2);\nconst scoreMin = d3.min(allScores);\nconst scoreMax = d3.max(allScores);\nconst scorePad = (scoreMax - scoreMin) * 0.05;\nconst xDomain = [scoreMin - scorePad, scoreMax + scorePad];\n\nconst x = d3.scaleLinear().domain(xDomain).range([0, iw]);\nconst y = d3.scaleLinear().domain([0, 1]).range([ih, 0]);\n\n// --- ECDF step-function point series -----------------------------------------\nfunction ecdfPoints(sorted, n) {\n  const pts = [[xDomain[0], 0]];\n  sorted.forEach((value, idx) => pts.push([value, (idx + 1) / n]));\n  pts.push([xDomain[1], 1]);\n  return pts;\n}\nconst goodEcdf = ecdfPoints(sorted1, n1);\nconst badEcdf = ecdfPoints(sorted2, n2);\nconst stepLine = d3\n  .line()\n  .x((d) => x(d[0]))\n  .y((d) => y(d[1]))\n  .curve(d3.curveStepAfter);\n\n// --- SVG mount ----------------------------------------------------------------\nconst svg = d3.select(\"#container\").append(\"svg\").attr(\"width\", width).attr(\"height\", height);\nconst g = svg.append(\"g\").attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Gridlines (y-axis only) ---------------------------------------------------\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(y.ticks(5))\n  .join(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", (d) => y(d))\n  .attr(\"y2\", (d) => y(d))\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\n// --- Axes -----------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(d3.axisBottom(x).ticks(8).tickFormat(d3.format(\"d\")));\nconst yAxis = g.append(\"g\").call(d3.axisLeft(y).ticks(5).tickFormat(d3.format(\".1f\")));\nfor (const axisGroup of [xAxis, yAxis]) {\n  axisGroup.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  axisGroup.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  axisGroup.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\nxAxis.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\nyAxis.selectAll(\"line\").remove();\n\ng.append(\"text\")\n  .attr(\"x\", iw / 2)\n  .attr(\"y\", ih + 60)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .text(\"Credit Score\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -70)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .text(\"Cumulative Proportion\");\n\n// --- ECDF step curves -----------------------------------------------------------\ng.append(\"path\").datum(badEcdf).attr(\"fill\", \"none\").attr(\"stroke\", t.palette[4]).attr(\"stroke-width\", 3).attr(\"d\", stepLine);\ng.append(\"path\").datum(goodEcdf).attr(\"fill\", \"none\").attr(\"stroke\", t.palette[0]).attr(\"stroke-width\", 3).attr(\"d\", stepLine);\n\n// --- Maximum divergence marker ---------------------------------------------------\ng.append(\"line\")\n  .attr(\"x1\", x(ksX))\n  .attr(\"x2\", x(ksX))\n  .attr(\"y1\", y(ksD1))\n  .attr(\"y2\", y(ksD2))\n  .attr(\"stroke\", t.ink)\n  .attr(\"stroke-width\", 2.5)\n  .attr(\"stroke-dasharray\", \"7,5\");\ng.append(\"circle\").attr(\"cx\", x(ksX)).attr(\"cy\", y(ksD1)).attr(\"r\", 5.5).attr(\"fill\", t.ink);\ng.append(\"circle\").attr(\"cx\", x(ksX)).attr(\"cy\", y(ksD2)).attr(\"r\", 5.5).attr(\"fill\", t.ink);\n\n// --- Info panel: legend + K-S statistic -----------------------------------------\nconst panelWidth = 340;\nconst panelHeight = 138;\nconst panelX = iw - panelWidth - 16;\nconst panelY = ih - panelHeight - 16;\nconst panel = g.append(\"g\").attr(\"transform\", `translate(${panelX},${panelY})`);\npanel\n  .append(\"rect\")\n  .attr(\"width\", panelWidth)\n  .attr(\"height\", panelHeight)\n  .attr(\"rx\", 8)\n  .attr(\"fill\", t.elevatedBg)\n  .attr(\"stroke\", t.grid)\n  .attr(\"stroke-width\", 1);\n\nconst legendRows = [\n  { label: `Good customers (n=${n1})`, color: t.palette[0] },\n  { label: `Bad customers (n=${n2})`, color: t.palette[4] },\n];\nlegendRows.forEach((row, idx) => {\n  const rowY = 32 + idx * 32;\n  panel.append(\"line\").attr(\"x1\", 20).attr(\"x2\", 50).attr(\"y1\", rowY).attr(\"y2\", rowY).attr(\"stroke\", row.color).attr(\"stroke-width\", 4);\n  panel\n    .append(\"text\")\n    .attr(\"x\", 60)\n    .attr(\"y\", rowY + 5)\n    .attr(\"fill\", t.inkSoft)\n    .style(\"font-size\", \"14px\")\n    .text(row.label);\n});\n\npanel\n  .append(\"text\")\n  .attr(\"x\", 20)\n  .attr(\"y\", panelHeight - 24)\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"16px\")\n  .style(\"font-weight\", \"600\")\n  .text(`D = ${ksD.toFixed(3)}, ${pLabel}`);\n\n// --- Title -----------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 54)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"26px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"ks-test-comparison · javascript · d3 · anyplot.ai\");\n"}