Model Lift Chart — D3.js

A lift curve visualizes how much better a predictive model performs compared to random selection, showing the cumulative lift ratio as you target increasing percentages of the population. It answers the question: "If I target the top X% of predictions, how many times more responders will I capture than random targeting?" This plot is essential for evaluating and comparing classification models in scenarios where targeting efficiency matters.

Model Lift Chart rendered with D3.js

Renders

JavaScript source (D3.js)

// anyplot.ai
// lift-curve: Model Lift Chart
// Library: d3 7.9.0 | JavaScript 22.23.2
// Quality: 91/100 | Created: 2026-09-05

const t = window.ANYPLOT_TOKENS;
const { width, height } = window.ANYPLOT_SIZE;
const margin = { top: 90, right: 260, bottom: 90, left: 110 };
const iw = width - margin.left - margin.right;
const ih = height - margin.top - margin.bottom;

// --- Data (in-memory, deterministic) ----------------------------------------
// Simulates a churn-prediction model: each customer has a latent quality score
// `q` driving the true churn probability, and the model's predicted score is a
// noisy observation of `q` — an imperfect but informative ranking signal.
function lcg(seed) {
  let s = seed >>> 0;
  return () => {
    s = (Math.imul(s, 1664525) + 1013904223) >>> 0;
    return s / 4294967296;
  };
}
const rand = lcg(42);
function randNormal() {
  const u1 = Math.max(rand(), 1e-9);
  const u2 = rand();
  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}

const n = 3000;
const customers = [];
for (let i = 0; i < n; i++) {
  const q = randNormal();
  const churnProb = 1 / (1 + Math.exp(-(1.8 * q - 1.75)));
  const churned = rand() < churnProb ? 1 : 0;
  const score = q + 0.65 * randNormal();
  customers.push({ churned, score });
}
customers.sort((a, b) => b.score - a.score);

const overallRate = customers.reduce((s, c) => s + c.churned, 0) / n;
let cumChurned = 0;
const curve = customers.map((c, i) => {
  cumChurned += c.churned;
  return { pct: ((i + 1) / n) * 100, lift: cumChurned / (i + 1) / overallRate };
});

const bisectPct = d3.bisector((d) => d.pct).left;
const deciles = d3.range(1, 11).map((k) => {
  const idx = Math.min(bisectPct(curve, k * 10), n - 1);
  return { pct: curve[idx].pct, lift: curve[idx].lift, decile: k * 10 };
});
const labeledDeciles = new Set([20, 40, 60, 80, 100]);

// --- SVG mount ---------------------------------------------------------------
const svg = d3
  .select("#container")
  .append("svg")
  .attr("width", width)
  .attr("height", height);
const g = svg
  .append("g")
  .attr("transform", `translate(${margin.left},${margin.top})`);

// --- Scales -------------------------------------------------------------------
const x = d3.scaleLinear().domain([0, 100]).range([0, iw]);
const y = d3
  .scaleLinear()
  .domain([0, d3.max(curve, (d) => d.lift) * 1.08])
  .nice()
  .range([ih, 0]);

// --- Gridlines (y-axis only) --------------------------------------------------
g.append("g")
  .selectAll("line")
  .data(y.ticks(6))
  .join("line")
  .attr("x1", 0)
  .attr("x2", iw)
  .attr("y1", (d) => y(d))
  .attr("y2", (d) => y(d))
  .attr("stroke", t.grid);

// --- Reference line: y = 1 (random selection, no lift) -----------------------
g.append("line")
  .attr("x1", 0)
  .attr("x2", iw)
  .attr("y1", y(1))
  .attr("y2", y(1))
  .attr("stroke", t.inkSoft)
  .attr("stroke-width", 2)
  .attr("stroke-dasharray", "9,6");

g.append("text")
  .attr("x", iw + 14)
  .attr("y", y(1))
  .attr("dy", "0.32em")
  .attr("fill", t.inkSoft)
  .style("font-size", "15px")
  .text("Random selection (lift = 1)");

// --- Lift curve ---------------------------------------------------------------
const line = d3
  .line()
  .x((d) => x(d.pct))
  .y((d) => y(d.lift));

g.append("path")
  .datum(curve)
  .attr("fill", "none")
  .attr("stroke", t.palette[0])
  .attr("stroke-width", 3.5)
  .attr("d", line);

// --- Decile markers ------------------------------------------------------------
g.selectAll("circle.decile")
  .data(deciles)
  .join("circle")
  .attr("class", "decile")
  .attr("cx", (d) => x(d.pct))
  .attr("cy", (d) => y(d.lift))
  .attr("r", 7)
  .attr("fill", t.palette[0])
  .attr("stroke", t.pageBg)
  .attr("stroke-width", 2);

g.selectAll("text.decile-label")
  .data(deciles.filter((d) => labeledDeciles.has(d.decile)))
  .join("text")
  .attr("class", "decile-label")
  .attr("x", (d) => x(d.pct))
  .attr("y", (d) => y(d.lift) - 18)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "15px")
  .style("font-weight", "600")
  .text((d) => `${d.lift.toFixed(1)}×`);

// --- Model curve label ---------------------------------------------------------
g.append("text")
  .attr("x", x(curve[0].pct) + 14)
  .attr("y", y(curve[0].lift))
  .attr("dy", "0.32em")
  .attr("fill", t.palette[0])
  .style("font-size", "15px")
  .style("font-weight", "600")
  .text("Model");

// --- Axes -----------------------------------------------------------------------
const xAxis = g
  .append("g")
  .attr("transform", `translate(0,${ih})`)
  .call(
    d3
      .axisBottom(x)
      .ticks(10)
      .tickFormat((d) => `${d}%`),
  );
const yAxis = g.append("g").call(
  d3
    .axisLeft(y)
    .ticks(6)
    .tickFormat((d) => `${d.toFixed(1)}×`),
);
for (const ax of [xAxis, yAxis]) {
  ax.selectAll("text").attr("fill", t.inkSoft).style("font-size", "14px");
  ax.selectAll("line").attr("stroke", t.inkSoft);
  ax.select(".domain").attr("stroke", t.inkSoft);
}

// --- Axis labels ------------------------------------------------------------
g.append("text")
  .attr("x", iw / 2)
  .attr("y", ih + 60)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "17px")
  .text("Population Targeted (%)");

g.append("text")
  .attr("transform", "rotate(-90)")
  .attr("x", -ih / 2)
  .attr("y", -78)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "17px")
  .text("Cumulative Lift");

// --- Title --------------------------------------------------------------------
svg
  .append("text")
  .attr("x", width / 2)
  .attr("y", 46)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "22px")
  .style("font-weight", "600")
  .text("Model Lift Chart · lift-curve · javascript · d3 · anyplot.ai");

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/lift-curve/d3/code. Any spec id and library id listed in llms-full.txt fit the same URL shape; every URL below is complete and callable.

{
  "spec_id": "lift-curve",
  "language": "javascript",
  "library": "d3",
  "page": "https://anyplot.ai/lift-curve/javascript/d3",
  "hub": "https://anyplot.ai/lift-curve",
  "code_json": "https://api.anyplot.ai/specs/lift-curve/d3/code",
  "spec_json": "https://api.anyplot.ai/specs/lift-curve",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/lift-curve/javascript/d3/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/lift-curve/javascript/d3/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/lift-curve/javascript/d3/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/lift-curve/javascript/d3/plot-dark.html",
  "quality_score": 91.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of Model Lift Chart on anyplot.ai.

Other implementations