{"spec_id":"ks-test-comparison","library":"echarts","language":"javascript","code":"// anyplot.ai\n// ks-test-comparison: Kolmogorov-Smirnov Plot for Distribution Comparison\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 95/100 | Created: 2026-08-26\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic LCG + Box-Muller) -----------------------\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return function () {\n    state = (Math.imul(state, 1664525) + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\n\nfunction normalSamples(n, mean, std, rng) {\n  const out = [];\n  while (out.length < n) {\n    const u1 = Math.max(rng(), 1e-12);\n    const u2 = rng();\n    const mag = Math.sqrt(-2 * Math.log(u1));\n    out.push(mean + std * mag * Math.cos(2 * Math.PI * u2));\n    if (out.length < n) out.push(mean + std * mag * Math.sin(2 * Math.PI * u2));\n  }\n  return out.slice(0, n);\n}\n\nconst rng = makeLcg(42);\nconst goodScores = normalSamples(400, 680, 55, rng)\n  .map((v) => Math.min(850, Math.max(300, v)))\n  .sort((a, b) => a - b);\nconst badScores = normalSamples(400, 605, 68, rng)\n  .map((v) => Math.min(850, Math.max(300, v)))\n  .sort((a, b) => a - b);\n\n// --- ECDF construction --------------------------------------------------\nfunction ecdfPoints(sorted) {\n  const n = sorted.length;\n  const pad = (sorted[n - 1] - sorted[0]) * 0.03;\n  const points = [[sorted[0] - pad, 0]];\n  sorted.forEach((value, i) => points.push([value, (i + 1) / n]));\n  points.push([sorted[n - 1] + pad, 1]);\n  return points;\n}\n\nfunction ecdfValue(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 |F_good(x) - F_bad(x)| over all sample values -------\nconst combined = goodScores.concat(badScores).sort((a, b) => a - b);\nlet ksStat = 0;\nlet ksX = combined[0];\ncombined.forEach((x) => {\n  const diff = Math.abs(ecdfValue(goodScores, x) - ecdfValue(badScores, x));\n  if (diff > ksStat) {\n    ksStat = diff;\n    ksX = x;\n  }\n});\nconst f1AtKs = ecdfValue(goodScores, ksX);\nconst f2AtKs = ecdfValue(badScores, ksX);\nconst bandHalfWidth = (850 - 300) * 0.012;\n\n// Asymptotic two-sample K-S p-value (Kolmogorov distribution)\nconst n1 = goodScores.length;\nconst n2 = badScores.length;\nconst nEff = (n1 * n2) / (n1 + n2);\nconst lambda = (Math.sqrt(nEff) + 0.12 + 0.11 / Math.sqrt(nEff)) * ksStat;\nlet pValue = 0;\nfor (let k = 1; k <= 100; k++) {\n  pValue += 2 * (k % 2 === 0 ? -1 : 1) * Math.exp(-2 * k * k * lambda * lambda);\n}\npValue = Math.min(1, Math.max(0, pValue));\nconst pLabel = pValue < 0.001 ? \"p < 0.001\" : `p = ${pValue.toFixed(3)}`;\n\n// --- Init ---------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option ---------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: [t.palette[0], t.palette[4]],\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"ks-test-comparison · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    textStyle: { color: t.ink, fontSize: 27, fontWeight: 500 },\n  },\n  legend: {\n    data: [\"Good customers (n=400)\", \"Bad customers (n=400)\"],\n    top: 56,\n    textStyle: { color: t.ink, fontSize: 16 },\n  },\n  grid: { left: 100, right: 70, top: 130, bottom: 90 },\n  xAxis: {\n    type: \"value\",\n    name: \"Credit score\",\n    nameLocation: \"middle\",\n    nameGap: 40,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    min: 300,\n    max: 850,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Cumulative probability\",\n    nameLocation: \"middle\",\n    nameGap: 60,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    min: 0,\n    max: 1,\n    axisLabel: { color: t.inkSoft, fontSize: 14 },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { lineStyle: { color: t.grid } },\n  },\n  series: [\n    {\n      name: \"Good customers (n=400)\",\n      type: \"line\",\n      step: \"end\",\n      showSymbol: false,\n      lineStyle: { width: 3, color: t.palette[0] },\n      data: ecdfPoints(goodScores),\n    },\n    {\n      name: \"Bad customers (n=400)\",\n      type: \"line\",\n      step: \"end\",\n      showSymbol: false,\n      lineStyle: { width: 3, color: t.palette[4] },\n      data: ecdfPoints(badScores),\n    },\n    {\n      name: \"Max divergence\",\n      type: \"line\",\n      showSymbol: true,\n      symbolSize: 9,\n      silent: true,\n      legendHoverLink: false,\n      lineStyle: { width: 2, type: \"dashed\", color: t.ink },\n      itemStyle: { color: t.ink },\n      data: [\n        [ksX, f1AtKs],\n        [ksX, f2AtKs],\n      ],\n      markArea: {\n        silent: true,\n        itemStyle: { color: t.amber, opacity: 0.22 },\n        data: [\n          [\n            { xAxis: ksX - bandHalfWidth, yAxis: Math.min(f1AtKs, f2AtKs) },\n            { xAxis: ksX + bandHalfWidth, yAxis: Math.max(f1AtKs, f2AtKs) },\n          ],\n        ],\n      },\n      markPoint: {\n        symbol: \"circle\",\n        symbolSize: 0,\n        label: {\n          show: true,\n          position: ksX > 575 ? \"left\" : \"right\",\n          distance: 16,\n          color: t.ink,\n          fontSize: 18,\n          fontWeight: \"bold\",\n          lineHeight: 24,\n          padding: [8, 12],\n          backgroundColor: t.elevatedBg,\n          borderColor: t.ink,\n          borderWidth: 1,\n          borderRadius: 6,\n          formatter: `D = ${ksStat.toFixed(3)}\\n${pLabel}`,\n        },\n        data: [{ coord: [ksX, Math.max(f1AtKs, f2AtKs) + 0.02] }],\n      },\n    },\n  ],\n});\n"}