Interactive volcano plots in JavaScript with CanvasXpress
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
- Gallery of examples: https://canvasxpress.org/examples.html
- Documentation: https://canvasxpress.org/documentation.html
- Source & npm: https://github.com/neuhausi/canvasXpress