ROC Curve with AUC — MUI X Charts

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 MUI X Charts

Renders

JavaScript source (MUI X Charts)

// anyplot.ai
// roc-curve: ROC Curve with AUC
// Library: muix 7.29.1 | JavaScript 22.23.2
// Quality: 91/100 | Created: 2026-09-05
//# anyplot-orientation: square
// anyplot.ai
// roc-curve: ROC Curve with AUC
// Library: muix 7.29.1 | JavaScript 22.23.2
// License: @mui/x-charts — MIT (community). Pro/Premium are out of scope.
// Quality: pending | Created: 2026-09-05
import { LineChart } from "@mui/x-charts/LineChart";
import { ChartsReferenceLine } from "@mui/x-charts/ChartsReferenceLine";
import Box from "@mui/material/Box";
import Typography from "@mui/material/Typography";

const t = window.ANYPLOT_TOKENS;

// --- Data (in-memory, deterministic) ----------------------------------------
// The ROC pipeline: lcg/randNormal synthesize classifier scores,
// rocFromScores sweeps every threshold into an empirical (fpr, tpr, auc)
// curve, and onGrid resamples that step function onto a shared FPR grid so
// both models plot against one xAxis.

// Tiny fixed-seed LCG — the browser has no seeded RNG
function lcg(seed: number) {
  let s = seed >>> 0;
  return () => {
    s = (Math.imul(1664525, s) + 1013904223) >>> 0;
    return s / 4294967295;
  };
}
const rand = lcg(42);

// Standard normal deviate via Box-Muller, driven by the LCG above.
function randNormal(mean: number, std: number) {
  const u1 = Math.max(rand(), 1e-9);
  const u2 = rand();
  const z = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
  return mean + z * std;
}

// Simulate classifier scores for malignant (positive) vs. benign (negative)
// biopsy samples, then sweep every threshold to trace the empirical ROC
// curve — mirrors what sklearn.metrics.roc_curve produces from real
// predictions. AUC follows from the trapezoidal rule over the curve.
function rocFromScores(
  nPos: number,
  nNeg: number,
  meanPos: number,
  meanNeg: number,
  std: number,
) {
  const scored = [
    ...Array.from({ length: nPos }, () => ({
      s: randNormal(meanPos, std),
      label: 1,
    })),
    ...Array.from({ length: nNeg }, () => ({
      s: randNormal(meanNeg, std),
      label: 0,
    })),
  ].sort((a, b) => b.s - a.s);

  const fpr = [0];
  const tpr = [0];
  let tp = 0;
  let fp = 0;
  for (const { label } of scored) {
    if (label === 1) tp += 1;
    else fp += 1;
    fpr.push(fp / nNeg);
    tpr.push(tp / nPos);
  }

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

// Resample a step-function ROC curve onto a shared FPR grid so every series
// (both models plus the diagonal) can be plotted against one xAxis.
function onGrid(fpr: number[], tpr: number[], grid: number[]) {
  return grid.map((x) => {
    let i = 0;
    while (i < fpr.length - 1 && fpr[i + 1] < x) i += 1;
    const j = Math.min(i + 1, fpr.length - 1);
    if (fpr[j] === fpr[i]) return tpr[j];
    const frac = (x - fpr[i]) / (fpr[j] - fpr[i]);
    return tpr[i] + frac * (tpr[j] - tpr[i]);
  });
}

const N_SAMPLES = 500;
const GRID = Array.from({ length: 101 }, (_, i) => i / 100);

const forest = rocFromScores(N_SAMPLES, N_SAMPLES, 2.3, 0, 1);
const logistic = rocFromScores(N_SAMPLES, N_SAMPLES, 1.15, 0, 1);
const forestTpr = onGrid(forest.fpr, forest.tpr, GRID);
const logisticTpr = onGrid(logistic.fpr, logistic.tpr, GRID);

const TITLE = "roc-curve · javascript · muix · anyplot.ai";
const TITLE_H = 56;

// --- Chart (default-exported component — the harness mounts it) -----------

export default function Chart() {
  const { width, height } = window.ANYPLOT_SIZE;

  return (
    <Box
      sx={{
        width,
        height,
        bgcolor: t.pageBg,
        display: "flex",
        flexDirection: "column",
      }}
    >
      <Box
        sx={{
          height: TITLE_H,
          display: "flex",
          alignItems: "center",
          px: "40px",
          pt: "10px",
        }}
      >
        <Typography
          sx={{
            color: t.ink,
            fontSize: "25px",
            fontWeight: 600,
            lineHeight: 1,
          }}
        >
          {TITLE}
        </Typography>
      </Box>

      <LineChart
        width={width}
        height={height - TITLE_H}
        skipAnimation
        grid={{ horizontal: true }}
        xAxis={[
          {
            data: GRID,
            scaleType: "linear",
            min: 0,
            max: 1,
            label: "False Positive Rate",
            tickLabelStyle: { fontSize: 14 },
            labelStyle: { fontSize: 16 },
          },
        ]}
        yAxis={[
          {
            min: 0,
            max: 1,
            label: "True Positive Rate",
            // tickFontSize drives the auto-computed label offset (see MUI X
            // ChartsYAxis: labelRefPoint.x = -(tickFontSize + tickSize + 10));
            // set it wide enough to clear the "0.XX"-style tick text, while
            // tickLabelStyle.fontSize keeps the rendered tick size correct.
            tickFontSize: 40,
            tickLabelStyle: { fontSize: 14 },
            labelStyle: { fontSize: 16 },
          },
        ]}
        series={[
          {
            id: "forest",
            data: forestTpr,
            label: `Random Forest (AUC = ${forest.auc.toFixed(2)})`,
            color: t.palette[0],
            showMark: false,
            curve: "linear",
          },
          {
            id: "logistic",
            data: logisticTpr,
            label: `Logistic Regression (AUC = ${logistic.auc.toFixed(2)})`,
            color: t.palette[1],
            showMark: false,
            curve: "linear",
          },
          {
            // No `label`: this is the y=x reference, not a fitted model, so
            // it's excluded from the legend (see ChartsReferenceLine below,
            // which annotates it directly on the chart instead).
            id: "baseline",
            data: GRID,
            color: t.inkSoft,
            showMark: false,
            curve: "linear",
          },
        ]}
        margin={{ top: 20, bottom: 90, left: 130, right: 40 }}
        sx={{
          "& .MuiLineElement-series-forest": { strokeWidth: 3.5 },
          "& .MuiLineElement-series-logistic": { strokeWidth: 3 },
          "& .MuiLineElement-series-baseline": {
            strokeDasharray: "10 6",
            strokeWidth: 2,
            strokeOpacity: 0.6,
          },
          "& .MuiChartsGrid-line": { stroke: t.grid, strokeWidth: 1 },
        }}
        slotProps={{
          legend: {
            direction: "row",
            position: { vertical: "bottom", horizontal: "middle" },
          },
        }}
      >
        {/* Annotates the dashed "baseline" series in place of a legend
            entry — the reference line's own stroke is hidden (it would
            otherwise duplicate the horizontal gridline); only its label
            renders, horizontally centered above (FPR=0.5, TPR=0.5) where the
            diagonal data series crosses, clear of the line itself. */}
        <ChartsReferenceLine
          y={0.6}
          label="Random guess (AUC = 0.50)"
          lineStyle={{ stroke: "none" }}
          labelStyle={{ fill: t.inkSoft, fontSize: 13 }}
        />
      </LineChart>
    </Box>
  );
}

Retrieve this implementation

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