Genome browser tracks in JavaScript with CanvasXpress

JavaScript
CanvasXpress
genome
bioinformatics
visualization
Stack coverage and gene-model tracks over genomic coordinates in the browser — an interactive, zoomable view from one portable spec.
Author

Isaac Neuhaus

Published

October 31, 2026

A genome browser view stacks tracks that share one coordinate axis: coverage, gene models, variants. Building one in the browser by hand is a lot of SVG and coordinate math. CanvasXpress is a JavaScript library at heart, so the whole view is a data object of tracks plus a config.

Load the 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>

The markup and the spec

Four tracks that share one coordinate axis: gene models (box, multi-exon, connected), coverage across three tissues (bar), an expression heatmap (heatmap), and variant markers (triangle).

<canvas id="genome" width="760" height="520"></canvas>
<script>
  // Gene models: each gene is a set of exon [start, end] intervals, drawn connected.
  var genes = [];
  for (var j = 0; j < 4; j++) {
    var start = 40 + j * 120, nex = 2 + Math.floor(Math.random() * 3), exons = [];
    for (var k = 0; k < nex; k++) exons.push([start + k * 30, start + k * 30 + 18]);
    genes.push({ id: "GENE" + (j + 1), dir: j % 2 === 0 ? "right" : "left",
                 fill: "rgb(70,130,220)", outline: "rgb(0,0,0)", data: exons });
  }

  // Coverage and expression: one entry per binned position, three tissues each.
  var coverage = [], express = [], rnd = function (a, b) { return a + Math.floor(Math.random() * (b - a + 1)); };
  for (var i = 0; i < 22; i++) {
    var o = 50 + i * 22;
    coverage.push({ id: "p" + i, offset: o, data: [rnd(8, 45), rnd(8, 45), rnd(8, 45)] });
    express.push({ id: "p" + i, offset: o,
                   data: [Math.round(Math.random() * 4 - 2), Math.round(Math.random() * 4 - 2),
                          Math.round(Math.random() * 4 - 2)] });
  }

  // Variants: point markers at genomic positions.
  var snps = [];
  for (var s = 0; s < 9; s++)
    snps.push({ id: "rs" + (1000 + s), offset: rnd(45, 520), fill: "rgb(200,40,40)", outline: "rgb(0,0,0)" });

  var data = { tracks: [
    { name: "Genes (exon models)", type: "box", connect: true, data: genes },
    { name: "Coverage — Heart / Liver / Kidney", type: "bar", height: 120,
      fill: "rgb(80,80,200)", outline: "rgb(60,60,60)", data: coverage },
    { name: "Expression (z-score)", type: "heatmap", height: 90, data: express },
    { name: "SNPs", type: "triangle", data: snps }
  ]};

  var config = { graphType: "Genome", background: "rgb(245,245,245)", featureStaggered: true,
                 title: "Multi-track genome browser: genes, coverage, expression and variants" };

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

Live result

Scroll/drag to pan and zoom into a region; every track stays locked to the shared coordinate axis, so the gene models, coverage, expression and variants line up position-for-position. Swap each track for your own pileup, annotation and VCF-derived data.

Why “reproducible”

The tracks and config are the figure — one portable JSON spec that renders the same from R, Python, or a web page.

Next