Volcano Plot for Statistical Significance — Chart.js

A volcano plot displays statistical significance (-log10 p-value) on the y-axis versus effect size (log2 fold change) on the x-axis. Points are typically color-coded to highlight features that are both statistically significant and have large effect sizes. This visualization is essential for quickly identifying the most important changes in differential expression, proteomics, and genomics studies.

Volcano Plot for Statistical Significance rendered with Chart.js

Renders

JavaScript source (Chart.js)

// anyplot.ai
// volcano-basic: Volcano Plot for Statistical Significance
// Library: chartjs 4.4.7 | JavaScript 22.23.2
// Quality: 93/100 | Created: 2026-09-09

const t = window.ANYPLOT_TOKENS;
const INK_MUTED = window.ANYPLOT_THEME === "dark" ? "#A8A79F" : "#6B6A63";

// --- Data (in-memory, deterministic LCG — proteomics case study) -----------
// Differential protein abundance, tumor vs. healthy tissue, mass-spec proteomics.
function makeLcg(seed) {
  let state = seed >>> 0;
  return () => {
    state = (state * 1664525 + 1013904223) >>> 0;
    return state / 4294967296;
  };
}
const rand = makeLcg(42);

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

const PVAL_THRESHOLD = 1.3; // -log10(0.05)
const FC_THRESHOLD = 1; // log2(2)

const nonSigPoints = [];
const downPoints = [];
const upPoints = [];

const nProteins = 1400;
for (let i = 0; i < nProteins; i++) {
  const log2FoldChange = randNormal() * 1.6;
  const signalBoost = Math.abs(log2FoldChange) / 2.4;
  const rawPValue = Math.exp(-rand() * 7 - signalBoost * 6);
  const negLog10Pvalue = Math.min(-Math.log10(Math.max(rawPValue, 1e-30)), 26);

  const point = { x: log2FoldChange, y: negLog10Pvalue };
  const isSignificant =
    negLog10Pvalue > PVAL_THRESHOLD && Math.abs(log2FoldChange) > FC_THRESHOLD;
  if (!isSignificant) {
    nonSigPoints.push(point);
  } else if (log2FoldChange > 0) {
    upPoints.push(point);
  } else {
    downPoints.push(point);
  }
}

const allY = [...nonSigPoints, ...downPoints, ...upPoints].map((p) => p.y);
const allX = [...nonSigPoints, ...downPoints, ...upPoints].map((p) => p.x);
const xLimit = Math.ceil(Math.max(...allX.map(Math.abs)) * 1.08 * 2) / 2;
const yLimit = Math.ceil(Math.max(...allY) * 1.08);

const horizontalThreshold = [
  { x: -xLimit, y: PVAL_THRESHOLD },
  { x: xLimit, y: PVAL_THRESHOLD },
];
const verticalThresholdDown = [
  { x: -FC_THRESHOLD, y: 0 },
  { x: -FC_THRESHOLD, y: yLimit },
];
const verticalThresholdUp = [
  { x: FC_THRESHOLD, y: 0 },
  { x: FC_THRESHOLD, y: yLimit },
];

// --- Top-hit labels (spec's "consider labeling top significant features") --
const GENE_POOL = ["TP53", "EGFR", "KRAS", "BRCA1", "PTEN"];
const topUp = [...upPoints].sort((a, b) => b.y - a.y).slice(0, 3);
const topDown = [...downPoints].sort((a, b) => b.y - a.y).slice(0, 2);
const topFeatures = [...topUp, ...topDown].map((p, i) => ({
  ...p,
  name: GENE_POOL[i % GENE_POOL.length],
}));

const topFeatureLabels = {
  id: "topFeatureLabels",
  afterDatasetsDraw(chart) {
    const { ctx, scales } = chart;
    ctx.save();
    ctx.font = "600 13px sans-serif";
    ctx.fillStyle = t.ink;
    ctx.textBaseline = "bottom";
    for (const feature of topFeatures) {
      const px = scales.x.getPixelForValue(feature.x);
      const py = scales.y.getPixelForValue(feature.y);
      ctx.fillText(feature.name, px + 7, py - 3);
    }
    ctx.restore();
  },
};

function withAlpha(hex, alpha) {
  const r = parseInt(hex.slice(1, 3), 16);
  const g = parseInt(hex.slice(3, 5), 16);
  const b = parseInt(hex.slice(5, 7), 16);
  return `rgba(${r}, ${g}, ${b}, ${alpha})`;
}

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

// --- Chart ---------------------------------------------------------------
new Chart(canvas, {
  type: "scatter",
  plugins: [topFeatureLabels],
  data: {
    datasets: [
      {
        type: "line",
        label: "p = 0.05 cutoff",
        data: horizontalThreshold,
        borderColor: t.ink,
        borderDash: [8, 5],
        borderWidth: 1.5,
        pointRadius: 0,
        fill: false,
      },
      {
        type: "line",
        label: "±2-fold cutoff",
        data: verticalThresholdDown,
        borderColor: t.ink,
        borderDash: [8, 5],
        borderWidth: 1.5,
        pointRadius: 0,
        fill: false,
      },
      {
        type: "line",
        label: "±2-fold cutoff",
        data: verticalThresholdUp,
        borderColor: t.ink,
        borderDash: [8, 5],
        borderWidth: 1.5,
        pointRadius: 0,
        fill: false,
      },
      {
        label: "Not significant",
        data: nonSigPoints,
        backgroundColor: withAlpha(INK_MUTED, 0.4),
        pointRadius: 2.5,
        pointHoverRadius: 2.5,
      },
      {
        label: "Down-regulated",
        data: downPoints,
        backgroundColor: withAlpha(t.palette[2], 0.65),
        pointRadius: 3.5,
        pointHoverRadius: 3.5,
      },
      {
        label: "Up-regulated",
        data: upPoints,
        backgroundColor: withAlpha(t.palette[4], 0.65),
        pointRadius: 3.5,
        pointHoverRadius: 3.5,
      },
    ],
  },
  options: {
    responsive: true,
    maintainAspectRatio: false,
    animation: false,
    plugins: {
      title: {
        display: true,
        text: "volcano-basic · javascript · chartjs · anyplot.ai",
        color: t.ink,
        font: { size: 22 },
      },
      legend: {
        position: "top",
        labels: {
          color: t.ink,
          font: { size: 16 },
          filter: (item) => item.datasetIndex >= 3,
        },
      },
    },
    scales: {
      x: {
        min: -xLimit,
        max: xLimit,
        ticks: { color: t.inkSoft, font: { size: 14 } },
        grid: { display: false },
        title: {
          display: true,
          text: "log2(Fold Change)",
          color: t.ink,
          font: { size: 18 },
        },
      },
      y: {
        min: 0,
        max: yLimit,
        ticks: { color: t.inkSoft, font: { size: 14 } },
        grid: { color: t.grid },
        title: {
          display: true,
          text: "-log10(p-value)",
          color: t.ink,
          font: { size: 18 },
        },
      },
    },
  },
});

Retrieve this implementation

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

Part of Volcano Plot for Statistical Significance on anyplot.ai.

Other implementations