Probability-Probability (P-P) Plot — Chart.js

A diagnostic plot comparing the cumulative distribution function (CDF) of observed data against a theoretical distribution by plotting empirical CDF values against theoretical CDF values. Unlike Q-Q plots which compare quantiles, P-P plots compare cumulative probabilities on both axes (0 to 1), making them more sensitive to deviations in the center of the distribution. Points falling along the 45-degree diagonal indicate a good fit.

Probability-Probability (P-P) Plot rendered with Chart.js

Renders

JavaScript source (Chart.js)

// anyplot.ai
// pp-basic: Probability-Probability (P-P) Plot
// Library: chartjs 4.4.7 | JavaScript 22.22.3
// Quality: 89/100 | Created: 2026-06-09
//# anyplot-orientation: square
const t = window.ANYPLOT_TOKENS;

// --- Data: 200 samples from a right-skewed distribution vs normal reference ---
// Deterministic LCG for reproducibility (no seeded Math.random in browser)
let seed = 987654321;
function lcg() {
  seed = (seed * 1664525 + 1013904223) >>> 0;
  return seed / 0x100000000;
}

function randn() {
  const u1 = Math.max(lcg(), 1e-10);
  const u2 = lcg();
  return Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
}

// 200 samples with slight right skew: z + 0.3*z^2 shifts mass rightward
const n = 200;
const rawData = Array.from({ length: n }, () => {
  const z = randn();
  return z + 0.3 * z * z;
});

// Sort ascending for empirical CDF
const sorted = [...rawData].sort((a, b) => a - b);

// Fit normal: sample mean and standard deviation
const mean = sorted.reduce((s, v) => s + v, 0) / n;
const std = Math.sqrt(sorted.reduce((s, v) => s + (v - mean) ** 2, 0) / n);

// Error function (Abramowitz & Stegun, max error ~1.5e-7)
function erf(x) {
  const sign = x >= 0 ? 1 : -1;
  const a = Math.abs(x);
  const t1 = 1 / (1 + 0.3275911 * a);
  const poly =
    (((1.061405429 * t1 - 1.453152027) * t1 + 1.421413741) * t1 - 0.284496736) * t1 + 0.254829592;
  return sign * (1 - poly * t1 * Math.exp(-a * a));
}

function normCDF(x, mu, sigma) {
  return 0.5 * (1 + erf((x - mu) / (sigma * Math.SQRT2)));
}

// P-P coordinates: (theoretical CDF, empirical CDF) for each sorted observation
const ppPoints = sorted.map((val, i) => ({
  x: normCDF(val, mean, std),
  y: (i + 1) / (n + 1), // Hazen plotting position
}));

// 45-degree reference line: perfect distributional fit
const refLine = [
  { x: 0, y: 0 },
  { x: 1, y: 1 },
];

// KS 95% confidence band: ±1.36/sqrt(n) from the diagonal
// Highlights whether S-curve deviation is statistically significant
const ksEps = 1.36 / Math.sqrt(n);
const xs = Array.from({ length: 101 }, (_, i) => i / 100);
const upperBand = xs.map((x) => ({ x, y: Math.min(1, x + ksEps) }));
const lowerBand = xs.map((x) => ({ x, y: Math.max(0, x - ksEps) }));

// --- Mount -----------------------------------------------------------------
const canvas = document.createElement("canvas");
document.getElementById("container").appendChild(canvas);

// Suppress grid lines at axis boundaries (value=0 and value=1) to achieve
// an L-shaped frame — removes the top and right box spines that Chart.js
// draws by default when tick grid lines coincide with the chart edge.
const innerGrid = (ctx) =>
  ctx.tick.value <= 0 || ctx.tick.value >= 1 ? "transparent" : t.grid;

// --- Chart -----------------------------------------------------------------
new Chart(canvas, {
  type: "scatter",
  data: {
    datasets: [
      {
        // Fill from upperBand to lowerBand (index +1) for the confidence envelope
        label: "95% confidence band",
        data: upperBand,
        type: "line",
        backgroundColor: t.palette[0] + "20",
        borderColor: "transparent",
        borderWidth: 0,
        pointRadius: 0,
        fill: "+1",
        tension: 0,
        order: 3,
      },
      {
        label: "_lower",
        data: lowerBand,
        type: "line",
        borderColor: "transparent",
        borderWidth: 0,
        pointRadius: 0,
        fill: false,
        tension: 0,
        order: 3,
      },
      {
        label: "Reference (perfect fit)",
        data: refLine,
        type: "line",
        borderColor: t.inkSoft,
        borderWidth: 2,
        borderDash: [8, 5],
        pointRadius: 0,
        fill: false,
        tension: 0,
        order: 2,
      },
      {
        label: "Empirical vs Theoretical CDF",
        data: ppPoints,
        backgroundColor: t.palette[0] + "a6",
        borderColor: t.pageBg,
        borderWidth: 1,
        pointRadius: 5,
        pointHoverRadius: 7,
        order: 1,
      },
    ],
  },
  options: {
    responsive: true,
    maintainAspectRatio: false,
    animation: false,
    plugins: {
      title: {
        display: true,
        text: "pp-basic · javascript · chartjs · anyplot.ai",
        color: t.ink,
        font: { size: 22, weight: "600" },
        padding: { top: 10, bottom: 20 },
      },
      legend: {
        labels: {
          color: t.ink,
          font: { size: 14 },
          boxWidth: 14,
          padding: 16,
          filter: (item) => !item.text.startsWith("_"),
        },
      },
    },
    scales: {
      x: {
        type: "linear",
        min: 0,
        max: 1,
        border: { display: true, color: t.inkSoft },
        ticks: {
          color: t.inkSoft,
          font: { size: 14 },
          maxTicksLimit: 6,
        },
        grid: { color: innerGrid },
        title: {
          display: true,
          text: "Theoretical CDF (Normal)",
          color: t.ink,
          font: { size: 16 },
          padding: { top: 12 },
        },
      },
      y: {
        type: "linear",
        min: 0,
        max: 1,
        border: { display: true, color: t.inkSoft },
        ticks: {
          color: t.inkSoft,
          font: { size: 14 },
          maxTicksLimit: 6,
        },
        grid: { color: innerGrid },
        title: {
          display: true,
          text: "Empirical CDF",
          color: t.ink,
          font: { size: 16 },
          padding: { bottom: 12 },
        },
      },
    },
  },
});

Retrieve this implementation

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

Part of Probability-Probability (P-P) Plot on anyplot.ai.

Other implementations