Interactive volcano plots in JavaScript with CanvasXpress

JavaScript
CanvasXpress
bioinformatics
visualization
Build a publication-ready, interactive volcano plot in the browser with a few lines of JavaScript — one portable, reproducible CanvasXpress spec.
Author

Isaac Neuhaus

Published

September 22, 2026

A volcano plot puts log fold change on x, statistical significance on y, and pushes the interesting genes into the top corners. CanvasXpress is a JavaScript library at heart, so in the browser there’s no wrapper at all — you give a <canvas> a data object and a config, and you get an interactive, reproducible figure. The same spec renders identically from R and Python too.

Load the library

Drop one script tag (and the stylesheet) on your page. Here we pin to a released version on a CDN:

<link href="https://cdn.jsdelivr.net/gh/neuhausi/canvasXpress@69.6.1/inst/htmlwidgets/lib/canvasXpress/canvasXpress.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/gh/neuhausi/canvasXpress@69.6.1/inst/htmlwidgets/lib/canvasXpress/canvasXpress.min.js"></script>

The markup and the spec

Give the page a canvas, then build the data (a value matrix plus annotations) and a config, and call new CanvasXpress(...):

<canvas id="volcano" width="650" height="500"></canvas>
<script>
  // Normal-distribution helpers (no stats library needed in the browser)
  function gauss() {                        // standard normal via Box–Muller
    var u = 0, v = 0;
    while (u === 0) u = Math.random();
    while (v === 0) v = Math.random();
    return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
  }
  function normCdf(x) {                      // standard normal CDF via an erf approximation
    var s = x < 0 ? -1 : 1, ax = Math.abs(x) / Math.SQRT2;
    var t = 1 / (1 + 0.3275911 * ax);
    var y = 1 - (((((1.061405429 * t - 1.453152027) * t + 1.421413741) * t - 0.284496736) * t + 0.254829592) * t) * Math.exp(-ax * ax);
    return 0.5 * (1 + s * y);
  }

  // Simulate a realistic volcano: most genes unchanged, a minority differentially
  // expressed; significance follows effect size / standard error. Genes are the
  // variables (rows); each row holds [logFC, -log-pVal].
  var genes = [], rows = [], group = [], fc = [];
  for (var i = 0; i < 1000; i++) {
    var de = Math.random() < 0.12;                          // ~12% truly changed
    var sgn = Math.random() < 0.5 ? -1 : 1;
    var effect = de ? sgn * (0.8 + 0.9 * Math.random()) : 0;
    var se = 0.22 + 0.5 * Math.random();                    // per-gene standard error
    var f = effect + se * gauss();                          // observed log2FC
    var praw = 2 * (1 - normCdf(Math.abs(f / se)));         // z-test p-value
    var p = Math.min(-Math.log(Math.max(praw, 1e-16)) / Math.LN10, 16);
    genes.push("Gene" + (i + 1));
    rows.push([Math.round(f * 1000) / 1000, Math.round(p * 1000) / 1000]);
    group.push(f > 1 && p > 1.3 ? "Increased" : f < -1 && p > 1.3 ? "Decreased" : "NoChange");
    fc.push(Math.round(Math.pow(2, Math.abs(f)) * 100) / 100);
  }

  var data = {
    y: { vars: genes, smps: ["logFC", "-log-pVal"], data: rows },
    z: { Group: group, FC: fc }
  };

  var config = {
    graphType: "Scatter2D",
    title: "Publication-ready volcano plot",
    xAxis: ["logFC"], yAxis: ["-log-pVal"],
    xAxisTitle: "log2 fold change", yAxisTitle: "-log10 p-value",
    colorBy: "Group",
    colorKey: { Group: { Increased: "rgba(197,27,38,0.75)",
                         Decreased: "rgba(33,102,172,0.75)",
                         NoChange:  "rgba(150,150,150,0.35)" } },
    sizeBy: "FC", sizes: [3, 6, 9, 12, 15],
    showDecorations: true,
    decorations: { line: [
      { color: "rgba(120,120,120,0.8)", width: 1, x: 1 },
      { color: "rgba(120,120,120,0.8)", width: 1, x: -1 },
      { color: "rgba(120,120,120,0.8)", width: 1, y: 1.301 }
    ] }
  };

  new CanvasXpress("volcano", data, config);
</script>

Live result

Here’s exactly that code, running on this page:

Hover a point for its gene and values, drag to zoom, or use the toolbar to filter by regulation group. The dashed guides mark the ±1 log2FC and p = 0.05 cutoffs.

Why “reproducible”

The data and config above are the figure — one portable JSON specification. Save it and the identical plot renders from the R (canvasXpress) and Python (canvasxpress) packages, and it can be validated against a published schema and re-rendered later, so a figure can be regenerated and audited rather than just viewed.

Next