Skip to content

BAM Read Alignments

This example shows an IGV-like BAM alignment view built from GenomeSpy grammar building blocks. It combines depth coverage, mismatch and insertion support, read pileup, read direction, CIGAR operation overlays, per-read mismatch bases, and zoom-dependent lazy loading.

{
  "description": [
    "BAM read alignment example using a downsampled GIAB HG002 chr20 slice.",
    "Shows an IGV-like BAM alignment view with depth coverage, insertion and mismatch summaries, directional read pileups, CIGAR overlays, and base-quality filtering."
  ],

  "assembly": "hg38",

  "scales": {
    "x": {
      "domain": [
        { "chrom": "chr20", "pos": 10031817 },
        { "chrom": "chr20", "pos": 10031936 }
      ]
    }
  },

  "params": [
    {
      "name": "minMapq",
      "value": 0,
      "bind": {
        "input": "range",
        "min": 0,
        "max": 60,
        "step": 1,
        "debounce": 100
      }
    },
    {
      "name": "minBaseQuality",
      "value": 0,
      "bind": {
        "input": "range",
        "min": 0,
        "max": 40,
        "step": 1,
        "debounce": 100
      }
    },
    {
      "name": "windowSize",
      "value": 15000
    }
  ],

  "data": {
    "lazy": {
      "type": "bam",
      "url": "https://data.genomespy.app/sample-data/NIST-HG002/HG002.GRCh38.chr20_9950000_10100000.downsample33pct.bam",
      "windowSize": { "expr": "windowSize" }
    }
  },

  "transform": [
    {
      "type": "filter",
      "expr": "datum.mapq == null || datum.mapq >= minMapq"
    },
    {
      "type": "formula",
      "expr": "datum.mapq == null ? 0 : datum.mapq",
      "as": "_mapqOrZero"
    },
    { "type": "pileup", "start": "start", "end": "end", "as": "_lane" }
  ],

  "resolve": { "axis": { "x": "shared" } },

  "spacing": 5,

  "vconcat": [
    {
      "name": "coverage",
      "title": {
        "text": "Depth and mismatch support",
        "style": "overlay-title",
        "fontSize": 11
      },
      "height": 40,
      "resolve": { "scale": { "color": "independent" } },
      "layer": [
        {
          "name": "depth",
          "title": "Depth",
          "transform": [
            { "type": "flattenCigar", "copyFields": ["chrom"] },
            { "type": "filter", "expr": "datum.cigarType == 'aligned'" },
            {
              "type": "collect",
              "sort": { "field": ["chrom", "cigarStart"] }
            },
            {
              "type": "coverage",
              "chrom": "chrom",
              "start": "cigarStart",
              "end": "cigarEnd",
              "as": "coverage",
              "asStart": "start",
              "asEnd": "end"
            }
          ],
          "mark": "rect",
          "encoding": {
            "x": {
              "chrom": "chrom",
              "pos": "start",
              "type": "locus",
              "band": 0
            },
            "x2": { "chrom": "chrom", "pos": "end", "band": 0 },
            "y": {
              "field": "coverage",
              "type": "quantitative",
              "axis": { "tickCount": 2, "title": "Depth" }
            },
            "color": { "value": "#d0d0d0" }
          }
        },
        {
          "name": "mismatch-summary",
          "title": "Mismatch support",
          "transform": [
            { "type": "filter", "expr": "datum.md != null" },
            { "type": "alignmentMismatches", "copyFields": ["chrom"] },
            {
              "type": "filter",
              "expr": "datum.baseQuality == null || datum.baseQuality >= minBaseQuality"
            },
            {
              "type": "aggregate",
              "groupby": ["chrom", "mismatchStart", "base"]
            },
            {
              "type": "stack",
              "field": "count",
              "groupby": ["chrom", "mismatchStart"],
              "sort": { "field": "base", "order": "ascending" },
              "as": ["mismatchCount0", "mismatchCount1"]
            },
            {
              "type": "formula",
              "expr": "datum.mismatchStart + 1",
              "as": "mismatchEnd"
            }
          ],
          "mark": "rect",
          "encoding": {
            "x": {
              "chrom": "chrom",
              "pos": "mismatchStart",
              "type": "locus",
              "band": 0
            },
            "x2": { "chrom": "chrom", "pos": "mismatchEnd", "band": 0 },
            "y": { "field": "mismatchCount0", "type": "quantitative" },
            "y2": { "field": "mismatchCount1" },
            "color": {
              "field": "base",
              "type": "nominal",
              "scale": {
                "domain": ["A", "C", "T", "G", "N"],
                "range": ["#4FBF45", "#4D96E8", "#E85F78", "#E8B322", "#BDBDBD"]
              },
              "legend": null
            }
          }
        },
        {
          "name": "insertion-summary",
          "title": "Insertion support",
          "transform": [
            { "type": "flattenCigar", "copyFields": ["chrom"] },
            { "type": "filter", "expr": "datum.cigarType == 'insertion'" },
            {
              "type": "aggregate",
              "groupby": ["chrom", "cigarStart"]
            }
          ],
          "mark": {
            "type": "rule",
            "color": "black",
            "size": 1
          },
          "encoding": {
            "x": {
              "chrom": "chrom",
              "pos": "cigarStart",
              "type": "locus",
              "band": 0
            },
            "y": { "datum": 0, "type": "quantitative" },
            "y2": { "field": "count" }
          }
        }
      ]
    },
    {
      "name": "read-alignments",
      "title": { "text": "Read alignments", "orient": "none" },

      "height": { "step": { "expr": "laneHeight" } },
      "viewportHeight": "container",

      "params": [
        {
          "name": "laneHeight",
          "value": 12,
          "bind": { "input": "range", "min": 1, "max": 30, "step": 1 },
          "transition": { "type": "lerp", "halfLife": 30, "epsilon": 0.02 }
        }
      ],

      "resolve": {
        "scale": { "color": "independent", "opacity": "independent" }
      },

      "layer": [
        {
          "name": "read-layers",
          "title": "Read alignments",
          "resolve": {
            "scale": { "opacity": "independent" }
          },
          "encoding": {
            "y": {
              "field": "_lane",
              "type": "index",
              "scale": {
                "padding": 0.25,
                "reverse": true,
                "zoom": false
              }
            }
          },
          "layer": [
            {
              "name": "read-backbone",
              "title": "Read alignment",
              "mark": {
                "type": "arrow",
                "style": "arrow-block",
                "minStemLength": 10,
                "fill": "#e0e0e0",
                "stroke": "#c4c4c4",
                "strokeWidth": { "expr": "linearstep(3, 8, laneHeight)" }
              },
              "encoding": {
                "x": {
                  "chrom": "chrom",
                  "pos": "start",
                  "type": "locus",
                  "band": 0
                },
                "x2": { "chrom": "chrom", "pos": "end", "band": 0 },
                "direction": {
                  "field": "strand",
                  "type": "nominal",
                  "scale": {
                    "domain": ["+", "-"],
                    "range": ["forward", "reverse"]
                  }
                },
                "opacity": {
                  "field": "_mapqOrZero",
                  "type": "quantitative",
                  "scale": { "domain": [0, 60], "range": [0.25, 1] },
                  "legend": {
                    "title": "Mapping quality",
                    "values": [0, 20, 40, 60]
                  }
                },
                "tooltip": [
                  { "field": "name", "title": "Read" },
                  { "field": "strand", "title": "Strand" },
                  { "field": "isPaired", "title": "Paired" },
                  { "field": "isProperPair", "title": "Proper pair" },
                  { "field": "isDuplicate", "title": "Duplicate" },
                  { "field": "isQcFail", "title": "QC fail" },
                  { "field": "isSecondary", "title": "Secondary" },
                  { "field": "isSupplementary", "title": "Supplementary" },
                  { "field": "mapq", "title": "Mapping quality" },
                  { "field": "cigar", "title": "CIGAR" }
                ]
              }
            },
            {
              "name": "cigar-overlays",
              "title": "CIGAR operation",
              "transform": [
                {
                  "type": "formula",
                  "expr": "datum.seq",
                  "as": "_seq"
                },
                {
                  "type": "flattenCigar",
                  "copyFields": ["chrom", "_lane", "name", "cigar", "_seq"]
                }
              ],
              "encoding": {
                "x": {
                  "chrom": "chrom",
                  "pos": "cigarStart",
                  "type": "locus",
                  "band": 0
                },
                "x2": { "chrom": "chrom", "pos": "cigarEnd", "band": 0 }
              },
              "layer": [
                {
                  "name": "deletions",
                  "title": "Deletion",
                  "transform": [
                    {
                      "type": "filter",
                      "expr": "datum.cigarType == 'deletion'"
                    }
                  ],
                  "layer": [
                    {
                      "title": "Deletion",
                      "mark": {
                        "type": "rect",
                        "color": "white",
                        "minWidth": 1
                      }
                    },
                    {
                      "title": "Deletion",
                      "mark": {
                        "type": "rule",
                        "color": "#222",
                        "minLength": 1
                      }
                    }
                  ]
                },
                {
                  "name": "skips",
                  "title": "Skipped region",
                  "transform": [
                    { "type": "filter", "expr": "datum.cigarType == 'skip'" }
                  ],
                  "mark": {
                    "type": "rule",
                    "color": "#6b6b6b",
                    "strokeDash": [2, 2],
                    "minLength": 1
                  }
                },
                {
                  "name": "insertions",
                  "title": "Insertion",
                  "transform": [
                    {
                      "type": "filter",
                      "expr": "datum.cigarType == 'insertion'"
                    }
                  ],
                  "encoding": {
                    "x2": null,
                    "tooltip": [
                      {
                        "expr": "slice(datum._seq, datum.readStart, datum.readEnd)",
                        "title": "Inserted sequence"
                      }
                    ]
                  },
                  "mark": {
                    "type": "text",
                    "text": "I",
                    "color": "black",
                    "size": { "expr": "laneHeight * 0.90" },
                    "font": "Radley"
                  }
                },
                {
                  "name": "soft-clips",
                  "title": "Soft-clipped bases",
                  "transform": [
                    {
                      "type": "filter",
                      "expr": "datum.cigarType == 'softClip'"
                    }
                  ],
                  "encoding": { "x2": null },
                  "mark": {
                    "type": "text",
                    "text": "S",
                    "color": "#555",
                    "size": { "expr": "laneHeight * 0.90" },
                    "font": "Radley"
                  }
                }
              ]
            },
            {
              "name": "mismatches",
              "title": "Mismatch",
              "transform": [
                { "type": "filter", "expr": "datum.md != null" },
                {
                  "type": "alignmentMismatches",
                  "copyFields": [
                    "chrom",
                    "_lane",
                    "name",
                    "cigar",
                    "mapq",
                    "strand"
                  ]
                },
                {
                  "type": "formula",
                  "expr": "datum.baseQuality == null ? 20 : datum.baseQuality",
                  "as": "_baseQualityForOpacity"
                },
                {
                  "type": "filter",
                  "expr": "datum.baseQuality == null || datum.baseQuality >= minBaseQuality"
                }
              ],
              "encoding": {
                "x": {
                  "chrom": "chrom",
                  "pos": "mismatchStart",
                  "type": "locus",
                  "band": 0
                },
                "x2": { "chrom": "chrom", "pos": "mismatchEnd", "band": 0 }
              },
              "layer": [
                {
                  "title": "Mismatch",
                  "mark": {
                    "type": "rect",
                    "minWidth": 1
                  },
                  "encoding": {
                    "color": {
                      "field": "base",
                      "type": "nominal",
                      "scale": {
                        "domain": ["A", "C", "T", "G", "N"],
                        "range": [
                          "#4FBF45",
                          "#4D96E8",
                          "#E85F78",
                          "#E8B322",
                          "#BDBDBD"
                        ]
                      },
                      "legend": { "title": "Base" }
                    },
                    "opacity": {
                      "field": "_baseQualityForOpacity",
                      "type": "quantitative",
                      "scale": {
                        "domain": [5, 20],
                        "range": [0.1, 1],
                        "clamp": true,
                        "nice": false
                      },
                      "legend": {
                        "title": "Base quality",
                        "values": [6, 10, 15, 20]
                      }
                    }
                  }
                },
                {
                  "title": "Mismatch base",
                  "mark": {
                    "type": "text",
                    "color": "black",
                    "size": { "expr": "laneHeight * 0.75" },
                    "tooltip": null
                  },
                  "encoding": {
                    "text": { "field": "base", "type": "nominal" }
                  }
                }
              ]
            }
          ]
        },
        {
          "name": "zoom-message",
          "data": { "values": [{}] },
          "params": [
            {
              "name": "zoomMessageState",
              "expr": "abs(span(domain('x'))) > windowSize ? 1 : 0",
              "transition": { "type": "lerp", "halfLife": 60 }
            }
          ],
          "opacity": { "expr": "zoomMessageState" },
          "layer": [
            {
              "mark": {
                "type": "rect",
                "fill": "white",
                "opacity": 0.7
              }
            },
            {
              "mark": {
                "type": "text",
                "text": "Zoom in closer to load data.",
                "color": "#333",
                "size": 13,
                "yOffset": 20
              },
              "encoding": {
                "x": { "value": 0.5 },
                "y": { "value": 1 }
              }
            }
          ]
        }
      ]
    }
  ],

  "config": {
    "view": { "stroke": "lightgray" },
    "legend": {}
  }
}

The example uses a small BAM slice derived from public Genome in a Bottle / NIST HG002 (NA24385) Illumina 300x whole-genome alignments. The slice covers chr20:9950000-10100000 on GRCh38 and is downsampled for browser-based visualization. It is intended only as a visualization demo, not for clinical interpretation, diagnostic decisions, variant calling, biological inference, benchmarking, genealogy, or re-identification.

Source attribution: Genome in a Bottle / NIST HG002 (NA24385) data, and Zook, J.M. et al. Extensive sequencing of seven human genomes to characterize benchmark reference materials. Scientific Data 3, 160025 (2016). https://doi.org/10.1038/sdata.2016.25

What to notice

The top track summarizes read depth from aligned CIGAR blocks and stacks MD-derived mismatch support by alternate base. Insertions are summarized with vertical rules because they are anchored between reference bases and have zero reference width.

The read pileup uses arrow marks for strand direction and opacity for mapping quality. The mapping-quality slider filters low-confidence alignments. CIGAR-derived overlays mark deletions, skipped regions, insertions, and soft-clipped ends.

Mismatching bases are extracted from MD tags and rendered as colored ranged rectangles with base letters on top. Their opacity follows base quality, and the base-quality slider can hide low-quality mismatch evidence. When the visible region is wider than the configured BAM loading window, the read track fades in a zoom-in message.

GenomeSpy Features

This example combines several GenomeSpy capabilities in one alignment view:

  • Lazy data sources load BAM alignments only for the visible region. The BAM loading window is controlled by an ExprRef so the same parameter can drive the zoom message.
  • Parameters bind mapping-quality and base-quality thresholds to sliders.
  • pileup assigns reads to non-overlapping lanes.
  • flattenCigar expands CIGAR strings into operation rows for coverage and read overlays.
  • alignmentMismatches extracts sparse mismatch rows from the read sequence, CIGAR string, and MD tag.
  • coverage, aggregate, and stack summarize depth and mismatch support.
  • formula derives helper fields for mapping-quality and base-quality opacity.
  • layer composes coverage, insertion summaries, read bodies, CIGAR annotations, and mismatch labels while keeping color and opacity scales independent where needed.