An interactive NGS sequencing-QC dashboard in JavaScript with CanvasXpress
After a sequencing run you get a QC table — one row per sample, a dozen metrics — and you need to answer three questions fast: which samples fail, is a whole batch off, and are the failures correlated? A single scatter can’t do it; you want several views that talk to each other — select a suspicious sample in one and see it light up in the rest. In the browser that usually means wiring event handlers between chart libraries by hand. CanvasXpress does it for you: give every panel the same broadcast group and a selection broadcasts to all of them. Here we build a real QC dashboard for 96 RNA/exome samples.
Load the library and the data
The 96-sample QC table is published next to this post as a small JS object, qc-data.js — QC_DATA is a sample-centric CanvasXpress data object (each sample is a point; z holds its lane/batch/library).
<link href="https://cdn.jsdelivr.net/gh/neuhausi/canvasXpress@73.3.0/inst/htmlwidgets/lib/canvasXpress/canvasXpress.css" rel="stylesheet">
<script src="https://cdn.jsdelivr.net/gh/neuhausi/canvasXpress@73.3.0/inst/htmlwidgets/lib/canvasXpress/canvasXpress.min.js"></script>
<script src="qc-data.js"></script>The panels
The overview heatmap transposes the data to metrics × samples and z-scores each metric. The four triage panels read QC_DATA directly and all share broadcastGroup: "qc" — select points in one and the same samples re-mark in the rest.
<canvas id="qcHeat" width="820" height="420"></canvas>
<canvas id="qcMap" width="640" height="360"></canvas>
<canvas id="qcContam" width="640" height="360"></canvas>
<canvas id="qcQ30" width="640" height="360"></canvas>
<canvas id="qcReads" width="820" height="300"></canvas>
<script>
var metrics = QC_DATA.y.smps, samples = QC_DATA.y.vars, rows = QC_DATA.y.data;
// Overview: transpose to metrics x samples and z-score each metric.
var hmData = metrics.map(function (_, c) {
var col = rows.map(function (r) { return r[c]; });
var mean = col.reduce(function (a, b) { return a + b; }, 0) / col.length;
var sd = Math.sqrt(col.reduce(function (a, b) { return a + (b - mean) * (b - mean); }, 0) / col.length) || 1;
return col.map(function (v) { return Math.round((v - mean) / sd * 100) / 100; });
});
new CanvasXpress("qcHeat",
{ y: { vars: metrics, smps: samples, data: hmData },
x: { Batch: QC_DATA.z.Batch, Library: QC_DATA.z.Library } },
{ graphType: "Heatmap", samplesClustered: true, variablesClustered: true,
colorSpectrum: ["navy", "white", "firebrick3"],
title: "QC metrics (z-scored) — 96 samples clustered" });
// Linked triage panels (same QC_DATA, shared broadcast group).
function panel(id, cfg) {
new CanvasXpress(id, QC_DATA, Object.assign({ broadcast: true, broadcastGroup: "qc" }, cfg));
}
panel("qcMap", { graphType: "Scatter2D", xAxis: ["pct_duplication"], yAxis: ["pct_mapped"],
colorBy: "Batch", sizeBy: "total_reads", title: "Mapping rate vs duplication — select bottom-right" });
panel("qcContam", { graphType: "Scatter2D", xAxis: ["pct_mapped"], yAxis: ["contamination_freemix"],
colorBy: "Batch", title: "Contamination (freemix) vs mapping rate" });
panel("qcQ30", { graphType: "Scatter2D", xAxis: ["gc_content"], yAxis: ["pct_q30"],
colorBy: "Library", title: "Q30 vs GC content, coloured by library" });
panel("qcReads", { graphType: "Bar", graphOrientation: "vertical", xAxis: ["total_reads"],
colorBy: "Batch", title: "Total reads per sample (flag the short ones)" });
</script>Live result
Drag a selection box across the bad cluster in the mapping-vs-duplication panel — the other panels restrict to the same samples so you can read their contamination, library and depth.
The cross-filter is one shared broadcastGroup — no framework, no event wiring. Each panel stays a portable data + config spec.
Why “reproducible”
The same specs reproduce the dashboard from R, Python, or a web page, against the same QC table.