ROC Curve with AUC — D3.js

A Receiver Operating Characteristic (ROC) curve visualizes the performance of a binary classifier by plotting the True Positive Rate (TPR) against the False Positive Rate (FPR) at various classification thresholds. The Area Under the Curve (AUC) provides a single metric summarizing model performance, where 1.0 indicates perfect classification and 0.5 represents random guessing.

ROC Curve with AUC rendered with D3.js

Renders

JavaScript source (D3.js)

// anyplot.ai
// roc-curve: ROC Curve with AUC
// Library: d3 7.9.0 | JavaScript 22.23.2
// Quality: 91/100 | Created: 2026-09-05
//# anyplot-orientation: square

const t = window.ANYPLOT_TOKENS;
const isDark = window.ANYPLOT_THEME === "dark";
const muted = isDark ? "#A8A79F" : "#6B6A63"; // Imprint semantic anchor: muted
const { width, height } = window.ANYPLOT_SIZE;
const margin = { top: 110, right: 90, bottom: 100, left: 120 };
const iw = width - margin.left - margin.right;
const ih = height - margin.top - margin.bottom;

// --- Data: three synthetic diagnostic-test classifiers of varying skill, each
// built from a deterministic LCG (own seed per model, so runs stay reproducible
// and independent of each other) ---------------------------------------------
function makeLcg(seed) {
  let state = seed >>> 0;
  return () => {
    state = (state * 1664525 + 1013904223) >>> 0;
    return state / 4294967296;
  };
}
const clamp01 = (v) => Math.min(1, Math.max(0, v));

function buildRoc({ seed, muDiseased, muHealthy, sd }) {
  const rand = makeLcg(seed);
  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 nDiseased = 150;
  const nHealthy = 150;
  const diseasedScores = Array.from({ length: nDiseased }, () => clamp01(muDiseased + sd * randNormal()));
  const healthyScores = Array.from({ length: nHealthy }, () => clamp01(muHealthy + sd * randNormal()));

  const labeledScores = [
    ...diseasedScores.map((score) => ({ score, isDiseased: true })),
    ...healthyScores.map((score) => ({ score, isDiseased: false })),
  ].sort((a, b) => b.score - a.score);

  // Sweep the decision threshold from high to low, accumulating hits/misses —
  // the same construction sklearn.metrics.roc_curve uses on predicted scores.
  let truePositives = 0;
  let falsePositives = 0;
  const points = [{ fpr: 0, tpr: 0 }];
  for (const { isDiseased } of labeledScores) {
    if (isDiseased) truePositives += 1;
    else falsePositives += 1;
    points.push({ fpr: falsePositives / nHealthy, tpr: truePositives / nDiseased });
  }

  let auc = 0;
  for (let i = 1; i < points.length; i++) {
    const a = points[i - 1];
    const b = points[i];
    auc += ((b.fpr - a.fpr) * (a.tpr + b.tpr)) / 2;
  }
  return { points, auc };
}

const models = [
  { name: "Strong classifier", seed: 42, muDiseased: 0.66, muHealthy: 0.34, sd: 0.16 },
  { name: "Moderate classifier", seed: 7, muDiseased: 0.6, muHealthy: 0.4, sd: 0.2 },
  { name: "Weak classifier", seed: 99, muDiseased: 0.56, muHealthy: 0.44, sd: 0.24 },
].map((spec) => ({ ...spec, ...buildRoc(spec) }));

const color = d3
  .scaleOrdinal()
  .domain(models.map((m) => m.name))
  .range(t.palette);

// --- 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 — equal-aspect: iw === ih so an FPR unit spans the same pixels
// as a TPR unit, per the spec's "equal aspect ratio preferred" note ---------
const x = d3.scaleLinear().domain([0, 1]).range([0, iw]);
const y = d3.scaleLinear().domain([0, 1]).range([ih, 0]);

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

// --- Diagonal reference line (random classifier, y = x) ---------------------
g.append("line")
  .attr("x1", x(0))
  .attr("y1", y(0))
  .attr("x2", x(1))
  .attr("y2", y(1))
  .attr("stroke", muted)
  .attr("stroke-width", 2.5)
  .attr("stroke-dasharray", "10,8");

// --- Area fill under the strongest curve only, to keep a single focal point,
// then the ROC curve for each model in its own Imprint color -----------------
const area = d3
  .area()
  .x((d) => x(d.fpr))
  .y0(ih)
  .y1((d) => y(d.tpr));
g.append("path").datum(models[0].points).attr("fill", color(models[0].name)).attr("opacity", 0.1).attr("d", area);

const line = d3
  .line()
  .x((d) => x(d.fpr))
  .y((d) => y(d.tpr));
g.selectAll(".roc-line")
  .data(models)
  .join("path")
  .attr("class", "roc-line")
  .attr("fill", "none")
  .attr("stroke", (d) => color(d.name))
  .attr("stroke-width", 4)
  .attr("stroke-linejoin", "round")
  .attr("stroke-linecap", "round")
  .attr("d", (d) => line(d.points));

// --- Axes -------------------------------------------------------------------
const xAxis = g
  .append("g")
  .attr("transform", `translate(0,${ih})`)
  .call(d3.axisBottom(x).ticks(5).tickFormat(d3.format(".1f")));
const yAxis = g.append("g").call(d3.axisLeft(y).ticks(5).tickFormat(d3.format(".1f")));
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 + 65)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "18px")
  .text("False Positive Rate");
g.append("text")
  .attr("transform", "rotate(-90)")
  .attr("x", -ih / 2)
  .attr("y", -90)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "18px")
  .text("True Positive Rate");

// --- Legend (bottom-right — every ROC curve stays at/above the diagonal, so
// the low-TPR/high-FPR corner below it stays clear of the data) --------------
const legendEntries = [
  ...models.map((m) => ({ label: `${m.name} (AUC = ${m.auc.toFixed(2)})`, stroke: color(m.name), dash: null })),
  { label: "Random classifier (AUC = 0.50)", stroke: muted, dash: "8,6" },
];
const legend = g.append("g").attr("transform", `translate(${iw - 460}, ${ih - 160})`);
const rows = legend
  .selectAll(".legend-row")
  .data(legendEntries)
  .join("g")
  .attr("class", "legend-row")
  .attr("transform", (_, i) => `translate(0, ${i * 34})`);
rows
  .append("line")
  .attr("x1", 0)
  .attr("x2", 36)
  .attr("y1", 0)
  .attr("y2", 0)
  .attr("stroke", (d) => d.stroke)
  .attr("stroke-width", (d) => (d.dash ? 2.5 : 4))
  .attr("stroke-dasharray", (d) => d.dash);
rows
  .append("text")
  .attr("x", 48)
  .attr("y", 5)
  .attr("fill", (d, i) => (i === legendEntries.length - 1 ? t.inkSoft : t.ink))
  .style("font-size", "15px")
  .text((d) => d.label);

// --- Title --------------------------------------------------------------------
svg
  .append("text")
  .attr("x", width / 2)
  .attr("y", 55)
  .attr("text-anchor", "middle")
  .attr("fill", t.ink)
  .style("font-size", "24px")
  .style("font-weight", "600")
  .text("roc-curve · javascript · d3 · anyplot.ai");

Retrieve this implementation

Runnable source as JSON, for any HTTP client: https://api.anyplot.ai/specs/roc-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": "roc-curve",
  "language": "javascript",
  "library": "d3",
  "page": "https://anyplot.ai/roc-curve/javascript/d3",
  "hub": "https://anyplot.ai/roc-curve",
  "code_json": "https://api.anyplot.ai/specs/roc-curve/d3/code",
  "spec_json": "https://api.anyplot.ai/specs/roc-curve",
  "render_light_png": "https://storage.googleapis.com/anyplot-images/plots/roc-curve/javascript/d3/plot-light.png",
  "render_dark_png": "https://storage.googleapis.com/anyplot-images/plots/roc-curve/javascript/d3/plot-dark.png",
  "interactive_light_html": "https://storage.googleapis.com/anyplot-images/plots/roc-curve/javascript/d3/plot-light.html",
  "interactive_dark_html": "https://storage.googleapis.com/anyplot-images/plots/roc-curve/javascript/d3/plot-dark.html",
  "quality_score": 91.0,
  "license": "MIT",
  "guide": "https://anyplot.ai/llms.txt"
}

Part of ROC Curve with AUC on anyplot.ai.

Other implementations