{"spec_id":"lift-curve","library":"d3","language":"javascript","code":"// anyplot.ai\n// lift-curve: Model Lift Chart\n// Library: d3 7.9.0 | JavaScript 22.23.2\n// Quality: 91/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\nconst { width, height } = window.ANYPLOT_SIZE;\nconst margin = { top: 90, right: 260, bottom: 90, left: 110 };\nconst iw = width - margin.left - margin.right;\nconst ih = height - margin.top - margin.bottom;\n\n// --- Data (in-memory, deterministic) ----------------------------------------\n// Simulates a churn-prediction model: each customer has a latent quality score\n// `q` driving the true churn probability, and the model's predicted score is a\n// noisy observation of `q` — an imperfect but informative ranking signal.\nfunction lcg(seed) {\n  let s = seed >>> 0;\n  return () => {\n    s = (Math.imul(s, 1664525) + 1013904223) >>> 0;\n    return s / 4294967296;\n  };\n}\nconst rand = lcg(42);\nfunction randNormal() {\n  const u1 = Math.max(rand(), 1e-9);\n  const u2 = rand();\n  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);\n}\n\nconst n = 3000;\nconst customers = [];\nfor (let i = 0; i < n; i++) {\n  const q = randNormal();\n  const churnProb = 1 / (1 + Math.exp(-(1.8 * q - 1.75)));\n  const churned = rand() < churnProb ? 1 : 0;\n  const score = q + 0.65 * randNormal();\n  customers.push({ churned, score });\n}\ncustomers.sort((a, b) => b.score - a.score);\n\nconst overallRate = customers.reduce((s, c) => s + c.churned, 0) / n;\nlet cumChurned = 0;\nconst curve = customers.map((c, i) => {\n  cumChurned += c.churned;\n  return { pct: ((i + 1) / n) * 100, lift: cumChurned / (i + 1) / overallRate };\n});\n\nconst bisectPct = d3.bisector((d) => d.pct).left;\nconst deciles = d3.range(1, 11).map((k) => {\n  const idx = Math.min(bisectPct(curve, k * 10), n - 1);\n  return { pct: curve[idx].pct, lift: curve[idx].lift, decile: k * 10 };\n});\nconst labeledDeciles = new Set([20, 40, 60, 80, 100]);\n\n// --- SVG mount ---------------------------------------------------------------\nconst svg = d3\n  .select(\"#container\")\n  .append(\"svg\")\n  .attr(\"width\", width)\n  .attr(\"height\", height);\nconst g = svg\n  .append(\"g\")\n  .attr(\"transform\", `translate(${margin.left},${margin.top})`);\n\n// --- Scales -------------------------------------------------------------------\nconst x = d3.scaleLinear().domain([0, 100]).range([0, iw]);\nconst y = d3\n  .scaleLinear()\n  .domain([0, d3.max(curve, (d) => d.lift) * 1.08])\n  .nice()\n  .range([ih, 0]);\n\n// --- Gridlines (y-axis only) --------------------------------------------------\ng.append(\"g\")\n  .selectAll(\"line\")\n  .data(y.ticks(6))\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\n// --- Reference line: y = 1 (random selection, no lift) -----------------------\ng.append(\"line\")\n  .attr(\"x1\", 0)\n  .attr(\"x2\", iw)\n  .attr(\"y1\", y(1))\n  .attr(\"y2\", y(1))\n  .attr(\"stroke\", t.inkSoft)\n  .attr(\"stroke-width\", 2)\n  .attr(\"stroke-dasharray\", \"9,6\");\n\ng.append(\"text\")\n  .attr(\"x\", iw + 14)\n  .attr(\"y\", y(1))\n  .attr(\"dy\", \"0.32em\")\n  .attr(\"fill\", t.inkSoft)\n  .style(\"font-size\", \"15px\")\n  .text(\"Random selection (lift = 1)\");\n\n// --- Lift curve ---------------------------------------------------------------\nconst line = d3\n  .line()\n  .x((d) => x(d.pct))\n  .y((d) => y(d.lift));\n\ng.append(\"path\")\n  .datum(curve)\n  .attr(\"fill\", \"none\")\n  .attr(\"stroke\", t.palette[0])\n  .attr(\"stroke-width\", 3.5)\n  .attr(\"d\", line);\n\n// --- Decile markers ------------------------------------------------------------\ng.selectAll(\"circle.decile\")\n  .data(deciles)\n  .join(\"circle\")\n  .attr(\"class\", \"decile\")\n  .attr(\"cx\", (d) => x(d.pct))\n  .attr(\"cy\", (d) => y(d.lift))\n  .attr(\"r\", 7)\n  .attr(\"fill\", t.palette[0])\n  .attr(\"stroke\", t.pageBg)\n  .attr(\"stroke-width\", 2);\n\ng.selectAll(\"text.decile-label\")\n  .data(deciles.filter((d) => labeledDeciles.has(d.decile)))\n  .join(\"text\")\n  .attr(\"class\", \"decile-label\")\n  .attr(\"x\", (d) => x(d.pct))\n  .attr(\"y\", (d) => y(d.lift) - 18)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"15px\")\n  .style(\"font-weight\", \"600\")\n  .text((d) => `${d.lift.toFixed(1)}×`);\n\n// --- Model curve label ---------------------------------------------------------\ng.append(\"text\")\n  .attr(\"x\", x(curve[0].pct) + 14)\n  .attr(\"y\", y(curve[0].lift))\n  .attr(\"dy\", \"0.32em\")\n  .attr(\"fill\", t.palette[0])\n  .style(\"font-size\", \"15px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Model\");\n\n// --- Axes -----------------------------------------------------------------------\nconst xAxis = g\n  .append(\"g\")\n  .attr(\"transform\", `translate(0,${ih})`)\n  .call(\n    d3\n      .axisBottom(x)\n      .ticks(10)\n      .tickFormat((d) => `${d}%`),\n  );\nconst yAxis = g.append(\"g\").call(\n  d3\n    .axisLeft(y)\n    .ticks(6)\n    .tickFormat((d) => `${d.toFixed(1)}×`),\n);\nfor (const ax of [xAxis, yAxis]) {\n  ax.selectAll(\"text\").attr(\"fill\", t.inkSoft).style(\"font-size\", \"14px\");\n  ax.selectAll(\"line\").attr(\"stroke\", t.inkSoft);\n  ax.select(\".domain\").attr(\"stroke\", t.inkSoft);\n}\n\n// --- Axis labels ------------------------------------------------------------\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(\"Population Targeted (%)\");\n\ng.append(\"text\")\n  .attr(\"transform\", \"rotate(-90)\")\n  .attr(\"x\", -ih / 2)\n  .attr(\"y\", -78)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"17px\")\n  .text(\"Cumulative Lift\");\n\n// --- Title --------------------------------------------------------------------\nsvg\n  .append(\"text\")\n  .attr(\"x\", width / 2)\n  .attr(\"y\", 46)\n  .attr(\"text-anchor\", \"middle\")\n  .attr(\"fill\", t.ink)\n  .style(\"font-size\", \"22px\")\n  .style(\"font-weight\", \"600\")\n  .text(\"Model Lift Chart · lift-curve · javascript · d3 · anyplot.ai\");\n"}