{"spec_id":"gain-curve","library":"chartjs","language":"javascript","code":"// anyplot.ai\n// gain-curve: Cumulative Gains Chart\n// Library: chartjs 4.4.7 | JavaScript 22.23.2\n// Quality: 90/100 | Created: 2026-09-05\n\n//# anyplot-orientation: landscape\n\nconst t = window.ANYPLOT_TOKENS;\nconst INK_MUTED = t.theme === \"light\" ? \"#6B6A63\" : \"#A8A79F\";\n\nfunction hexToRgba(hex, alpha) {\n  const r = parseInt(hex.slice(1, 3), 16);\n  const g = parseInt(hex.slice(3, 5), 16);\n  const b = parseInt(hex.slice(5, 7), 16);\n  return `rgba(${r}, ${g}, ${b}, ${alpha})`;\n}\n\n// --- Data: simulated marketing-campaign response model --------------------\n// Fixed-seed LCG (no seeded Math.random in the browser)\nlet seed = 42;\nconst rand = () => {\n  seed = (seed * 1664525 + 1013904223) % 4294967296;\n  return seed / 4294967296;\n};\n\nconst nCustomers = 2000;\nconst customers = [];\nfor (let i = 0; i < nCustomers; i++) {\n  const affinity = rand(); // latent propensity to respond, uniform 0..1\n  const responded = rand() < affinity * affinity * 0.4 ? 1 : 0; // ~13% base rate, concentrated at high affinity\n  const score = Math.max(0, Math.min(1, affinity + (rand() - 0.5) * 0.35)); // noisy model prediction of affinity\n  customers.push({ responded, score });\n}\n\n// Rank by predicted probability, descending — the gains-chart targeting order\ncustomers.sort((a, b) => b.score - a.score);\nconst totalPositives = customers.reduce((sum, c) => sum + c.responded, 0);\nconst positiveRatePct = (totalPositives / nCustomers) * 100;\n\n// Model curve: cumulative % of positives captured vs. % of population targeted\nconst modelCurve = [{ x: 0, y: 0 }];\nconst stride = Math.ceil(nCustomers / 200); // finer stride than population/100 keeps the curve smooth, not stair-stepped\nlet cumulativePositives = 0;\nfor (let i = 0; i < nCustomers; i++) {\n  cumulativePositives += customers[i].responded;\n  if ((i + 1) % stride === 0 || i === nCustomers - 1) {\n    modelCurve.push({\n      x: ((i + 1) / nCustomers) * 100,\n      y: (cumulativePositives / totalPositives) * 100,\n    });\n  }\n}\n\n// Reference curves\nconst randomBaseline = [\n  { x: 0, y: 0 },\n  { x: 100, y: 100 },\n];\nconst perfectModel = [\n  { x: 0, y: 0 },\n  { x: positiveRatePct, y: 100 },\n  { x: 100, y: 100 },\n];\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: \"Model\",\n        data: modelCurve,\n        borderColor: t.palette[0], // Imprint pos 1 — brand green\n        backgroundColor: hexToRgba(t.palette[0], 0.15), // low-alpha fill reinforces the \"gain\" area under the curve\n        borderWidth: 3.5,\n        pointRadius: 0,\n        tension: 0.2,\n        fill: \"origin\",\n      },\n      {\n        label: \"Perfect model\",\n        data: perfectModel,\n        borderColor: INK_MUTED,\n        backgroundColor: INK_MUTED, // legend swatch fill; line itself has no area fill\n        borderWidth: 2,\n        borderDash: [3, 3],\n        pointRadius: 0,\n        pointStyle: \"line\", // legend swatch reads as a dashed line, matching the on-chart style\n        tension: 0,\n        fill: false,\n      },\n      {\n        label: \"Random selection\",\n        data: randomBaseline,\n        borderColor: t.ink,\n        backgroundColor: t.ink, // legend swatch fill; line itself has no area fill\n        borderWidth: 2,\n        borderDash: [8, 6],\n        pointRadius: 0,\n        pointStyle: \"line\", // legend swatch reads as a dashed line, matching the on-chart style\n        tension: 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: \"gain-curve · javascript · chartjs · anyplot.ai\",\n        color: t.ink,\n        font: { size: 22, weight: \"500\" },\n        padding: { top: 12, bottom: 6 },\n      },\n      subtitle: {\n        display: true,\n        text: \"Marketing campaign response model — customers ranked by predicted probability\",\n        color: t.inkSoft,\n        font: { size: 14, style: \"italic\" },\n        padding: { bottom: 14 },\n      },\n      legend: {\n        position: \"top\",\n        labels: {\n          color: t.ink,\n          font: { size: 16 },\n          padding: 24,\n          usePointStyle: true,\n          pointStyleWidth: 40,\n        },\n      },\n    },\n    scales: {\n      x: {\n        type: \"linear\",\n        min: 0,\n        max: 100,\n        title: {\n          display: true,\n          text: \"Population Targeted (%)\",\n          color: t.ink,\n          font: { size: 16, weight: \"500\" },\n          padding: { top: 8 },\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          stepSize: 20,\n          callback: (val) => val + \"%\",\n        },\n        grid: { display: false },\n      },\n      y: {\n        type: \"linear\",\n        min: 0,\n        max: 100,\n        title: {\n          display: true,\n          text: \"Positive Cases Captured (%)\",\n          color: t.ink,\n          font: { size: 16, weight: \"500\" },\n          padding: { bottom: 8 },\n        },\n        ticks: {\n          color: t.inkSoft,\n          font: { size: 14 },\n          stepSize: 20,\n          callback: (val) => val + \"%\",\n        },\n        grid: { color: t.grid },\n      },\n    },\n  },\n});\n"}