{"spec_id":"lift-curve","library":"echarts","language":"javascript","code":"// anyplot.ai\n// lift-curve: Model Lift Chart\n// Library: echarts 6.1.0 | JavaScript 22.23.2\n// Quality: 95/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 so the ranking of predicted scores is reproducible.\nlet seed = 42;\nfunction rand() {\n  seed = (seed * 1103515245 + 12345) & 0x7fffffff;\n  return seed / 0x7fffffff;\n}\n\nconst nTransactions = 2000;\nconst fraudRate = 0.06;\n\n// Model score correlates with true fraud label but with realistic noise\n// (wide overlap between the two distributions), so the resulting lift curve\n// starts well below the theoretical maximum and decays toward 1 - the shape\n// of a genuinely good, but imperfect, classifier.\nconst records = [];\nfor (let i = 0; i < nTransactions; i++) {\n  const isFraud = rand() < fraudRate ? 1 : 0;\n  const score = isFraud\n    ? Math.min(1, 0.35 + rand() * 0.55)\n    : Math.max(0, rand() * 0.7);\n  records.push({ isFraud, score });\n}\nrecords.sort((a, b) => b.score - a.score);\n\nconst totalFraud = records.reduce((sum, r) => sum + r.isFraud, 0);\nconst baselineRate = totalFraud / nTransactions;\n\n// Cumulative lift at each decile (10%, 20%, ..., 100%) of targeted population.\nconst steps = 20;\nconst pctTargeted = [];\nconst liftValues = [];\nfor (let s = 1; s <= steps; s++) {\n  const cutoff = Math.round((s / steps) * nTransactions);\n  const capturedFraud = records\n    .slice(0, cutoff)\n    .reduce((sum, r) => sum + r.isFraud, 0);\n  const targetedRate = capturedFraud / cutoff;\n  pctTargeted.push(Math.round((s / steps) * 100));\n  liftValues.push(Number((targetedRate / baselineRate).toFixed(2)));\n}\n\n// Decile markers (every other step = every 10%) for emphasis.\nconst decileIndices = pctTargeted\n  .map((pct, idx) => (pct % 10 === 0 ? idx : -1))\n  .filter((idx) => idx >= 0);\n\n// --- Init --------------------------------------------------------------------\nconst chart = echarts.init(document.getElementById(\"container\"));\n\n// --- Option --------------------------------------------------------------------\nchart.setOption({\n  animation: false,\n  color: t.palette,\n  backgroundColor: \"transparent\",\n  title: {\n    text: \"Fraud Detection Model · lift-curve · javascript · echarts · anyplot.ai\",\n    left: \"center\",\n    top: 20,\n    textStyle: { color: t.ink, fontSize: 19, fontWeight: 500 },\n  },\n  legend: {\n    data: [\"Model\"],\n    top: 70,\n    textStyle: { color: t.inkSoft, fontSize: 15 },\n  },\n  grid: { left: 90, right: 70, top: 130, bottom: 90 },\n  xAxis: {\n    type: \"value\",\n    name: \"Population Targeted (%)\",\n    nameLocation: \"middle\",\n    nameGap: 45,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\n    min: 0,\n    max: 100,\n    axisLabel: {\n      color: t.inkSoft,\n      fontSize: 14,\n      formatter: \"{value}%\",\n    },\n    axisLine: { lineStyle: { color: t.inkSoft } },\n    splitLine: { show: false },\n  },\n  yAxis: {\n    type: \"value\",\n    name: \"Cumulative Lift\",\n    nameLocation: \"middle\",\n    nameGap: 60,\n    nameTextStyle: { color: t.ink, fontSize: 16 },\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: \"Model\",\n      type: \"line\",\n      data: pctTargeted.map((pct, idx) => [pct, liftValues[idx]]),\n      smooth: false,\n      symbol: \"circle\",\n      symbolSize: (val, params) =>\n        decileIndices.includes(params.dataIndex) ? 12 : 0,\n      lineStyle: { color: t.palette[0], width: 4 },\n      itemStyle: { color: t.palette[0] },\n      areaStyle: {\n        color: {\n          type: \"linear\",\n          x: 0,\n          y: 0,\n          x2: 0,\n          y2: 1,\n          colorStops: [\n            { offset: 0, color: `${t.palette[0]}33` },\n            { offset: 1, color: `${t.palette[0]}00` },\n          ],\n        },\n      },\n      // Reference line for random selection (y=1) - a markLine keeps the\n      // \"no lift\" baseline attached to the Model series instead of a second\n      // full series, avoiding legend/series boilerplate for a constant line.\n      markLine: {\n        silent: true,\n        symbol: \"none\",\n        lineStyle: { color: t.inkSoft, width: 2, type: \"dashed\" },\n        label: {\n          show: true,\n          formatter: \"Random selection (no lift)\",\n          position: \"insideMiddleTop\",\n          color: t.inkSoft,\n          fontSize: 13,\n        },\n        data: [{ yAxis: 1 }],\n      },\n      // Callout annotating the headline lift value at the first decile.\n      markPoint: {\n        symbol: \"pin\",\n        symbolSize: 56,\n        itemStyle: { color: t.palette[0] },\n        label: {\n          formatter: `${liftValues[1].toFixed(1)}x`,\n          color: t.pageBg,\n          fontSize: 13,\n          fontWeight: 600,\n        },\n        data: [\n          {\n            name: \"Top decile lift\",\n            coord: [pctTargeted[1], liftValues[1]],\n          },\n        ],\n      },\n    },\n  ],\n});\n"}