{"spec_id":"lift-curve","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// lift-curve: Model Lift Chart\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 92/100 | Created: 2026-09-05\n\nconst t = window.ANYPLOT_TOKENS;\n\n// --- Data (in-memory, deterministic fraud-detection scenario) --------------\n// Tiny fixed-seed LCG — the browser has no seeded Math.random().\nfunction makeLcg(seed) {\n  let state = seed >>> 0;\n  return function lcg() {\n    state = (Math.imul(state, 1664525) + 1013904223) >>> 0;\n    return state / 4294967296;\n  };\n}\nconst rng = makeLcg(42);\n\nconst SAMPLE_COUNT = 3000;\nconst transactions = [];\nfor (let i = 0; i < SAMPLE_COUNT; i++) {\n  // Latent risk, skewed toward zero — most transactions are low risk.\n  const risk = Math.pow(rng(), 3);\n  const fraudProbability = Math.min(0.95, risk * 0.9 + 0.02);\n  const isFraud = rng() < fraudProbability ? 1 : 0;\n  // Model score correlates with risk but includes noise (imperfect model).\n  const noise = (rng() - 0.5) * 0.3;\n  const score = Math.max(0, Math.min(1, risk + noise));\n  transactions.push({ isFraud, score });\n}\n\nconst rankedByScore = transactions.slice().sort((a, b) => b.score - a.score);\nconst cumulativeFraud = new Array(SAMPLE_COUNT + 1).fill(0);\nfor (let i = 0; i < SAMPLE_COUNT; i++) {\n  cumulativeFraud[i + 1] = cumulativeFraud[i] + rankedByScore[i].isFraud;\n}\nconst totalFraud = cumulativeFraud[SAMPLE_COUNT];\nconst baselineRate = totalFraud / SAMPLE_COUNT;\n\nconst liftPoints = [];\nfor (let percent = 1; percent <= 100; percent++) {\n  const targeted = Math.max(1, Math.round((percent / 100) * SAMPLE_COUNT));\n  const responseRate = cumulativeFraud[targeted] / targeted;\n  liftPoints.push({ x: percent, y: responseRate / baselineRate });\n}\n\nconst DECILE_STEP = 10;\n// Deciles called out with an explicit numeric label (spec: \"actual values at\n// key percentiles\"); FOCUS_DECILE also gets a larger marker as the chart's\n// single focal point.\nconst CALLOUT_DECILES = [10, 30];\nconst FOCUS_DECILE = 10;\n\n// --- Mount -------------------------------------------------------------\nconst canvas = document.createElement(\"canvas\");\ndocument.getElementById(\"container\").appendChild(canvas);\n\n// --- Plugins ---------------------------------------------------------------\n// Draws \"3.1x at 10%\" callouts above the CALLOUT_DECILES markers.\nconst decileCalloutPlugin = {\n  id: \"decileCallout\",\n  afterDatasetsDraw(chart) {\n    const meta = chart.getDatasetMeta(0);\n    const { ctx, chartArea } = chart;\n    ctx.save();\n    ctx.font = \"bold 15px sans-serif\";\n    ctx.fillStyle = t.ink;\n    ctx.textAlign = \"left\";\n    // Offset up-and-right of the marker: the falling curve approaches from\n    // the upper-left and departs to the lower-right, so that quadrant stays clear.\n    for (const percent of CALLOUT_DECILES) {\n      const point = meta.data[percent - 1];\n      if (!point) continue;\n      const value = liftPoints[percent - 1].y;\n      const labelY = Math.max(point.y - 22, chartArea.top + 14);\n      ctx.fillText(`${value.toFixed(1)}x at ${percent}%`, point.x + 14, labelY);\n    }\n    ctx.restore();\n  },\n};\n\n// --- Chart ---------------------------------------------------------------\nnew Chart(canvas, {\n  type: \"line\",\n  plugins: [decileCalloutPlugin],\n  data: {\n    datasets: [\n      {\n        label: \"Model lift\",\n        data: liftPoints,\n        borderColor: t.palette[0],\n        backgroundColor: t.palette[0],\n        borderWidth: 3,\n        pointBackgroundColor: t.palette[0],\n        pointBorderColor: t.pageBg,\n        pointBorderWidth: 2,\n        pointRadius: (ctx) => {\n          const percent = ctx.dataIndex + 1;\n          if (percent === FOCUS_DECILE) return 8;\n          return percent % DECILE_STEP === 0 ? 6 : 0;\n        },\n        pointHoverRadius: 7,\n        tension: 0.15,\n        fill: false,\n      },\n      {\n        label: \"Random baseline (no lift)\",\n        data: [\n          { x: 0, y: 1 },\n          { x: 100, y: 1 },\n        ],\n        borderColor: t.ink,\n        backgroundColor: t.pageBg,\n        borderWidth: 2,\n        borderDash: [8, 4],\n        pointBackgroundColor: t.pageBg,\n        pointBorderColor: t.ink,\n        pointBorderWidth: 2,\n        pointRadius: 0,\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: \"lift-curve · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22 },\n        padding: { bottom: 20 },\n      },\n      legend: {\n        position: \"top\",\n        align: \"end\",\n        labels: { color: t.ink, font: { size: 16 }, boxWidth: 24, usePointStyle: true },\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: 0,\n        max: 100,\n        title: { display: true, text: \"Population Targeted (%)\", color: t.ink, font: { size: 18 } },\n        ticks: { color: t.inkSoft, font: { size: 14 }, callback: (v) => `${v}%` },\n        grid: { display: false },\n      },\n      y: {\n        beginAtZero: true,\n        title: { display: true, text: \"Cumulative Lift Ratio\", color: t.ink, font: { size: 18 } },\n        ticks: { color: t.inkSoft, font: { size: 14 }, callback: (v) => `${v}x` },\n        grid: { color: t.grid },\n      },\n    },\n  },\n});\n"}