Chromosomes and locus scales

A genomic position has two parts: a chromosome or other contig, and a position within that contig. GenomeSpy uses an assembly’s contig sizes and order to concatenate those coordinates into one continuous visual axis.

Use Locus() for this chromosome-aware encoding. The resulting channel has the locus data type and automatically uses a locus scale.

For a single unit view, keep a small domain close to its encoding:

gs.Locus("chrom", "pos").scale(domain=REGION)

When several views share a genomic scale, put the domain on their common parent:

tracks.properties(
    scales=gs.scales(x=gs.Scale(domain=REGION)),
)

View-level domains are easier to reason about than domains buried in encodings deep in a view hierarchy, and they make the owner of a shared domain explicit.

Point features

Single nucleotide variants and other point features need one locus:

variants = [
    {"chrom": "chr17", "pos": 43_044_295, "impact": "moderate"},
    {"chrom": "chr17", "pos": 43_057_481, "impact": "high"},
    {"chrom": "chr17", "pos": 43_070_977, "impact": "low"},
    {"chrom": "chr17", "pos": 43_082_144, "impact": "high"},
]

point_chart = (
    gs.Chart(variants)
    .mark_point(filled=True, size=110)
    .encode(
        x=gs.Locus("chrom", "pos")
        .scale(domain=BRCA1_DOMAIN)
        .axis(title="Genomic position", chromGrid=True),
        y=gs.Y("impact:N").title("Impact"),
        color=gs.Color("impact:N").legend(None),
    )
    .properties(assembly="hg38", title="Point variants on chr17")
)

The root assembly="hg38" supplies chromosome sizes and order. The scale domain focuses the initial view on a region of chromosome 17.

GenomeAxis options are available through .axis(...). In the example, chromGrid=True enables chromosome-aware grid styling and the title identifies the coordinate channel. The remaining options are listed in genome axis for loci.

Genomic intervals

Features such as genes, exons, peaks, and copy-number segments cover a range. Pair x with x2 to provide the two interval boundaries:

features = [
    {"chrom": "chr17", "start": 43_044_000, "end": 43_050_000, "kind": "enhancer"},
    {"chrom": "chr17", "start": 43_057_000, "end": 43_061_000, "kind": "exon"},
    {"chrom": "chr17", "start": 43_068_000, "end": 43_075_000, "kind": "enhancer"},
]

interval_chart = (
    gs.Chart(features)
    .mark_rect()
    .encode(
        x=gs.Locus("chrom", "start").scale(domain=BRCA1_DOMAIN),
        x2=gs.Locus("chrom", "end"),
        y=gs.Y("kind:N").title("Feature kind"),
        color=gs.Color("kind:N").legend(None),
    )
    .properties(assembly="hg38", title="Half-open genomic intervals")
)

The primary locus establishes the x scale and assembly. The secondary x2 channel supplies only the other endpoint and inherits that scale. The same pattern works with rules, links, and arrows.

How chromosome positions become one axis

The assembly lists contigs in their preferred order and gives each contig’s length. GenomeSpy places the first contig at the start of a continuous axis, then places the next contig after it, and so on. Locus() inserts the required coordinate-linearization step automatically.

Usually, keep chromosome and position in separate fields and let GenomeSpy linearize them. A pre-linearized field can be encoded with a regular channel of type L, but it is then your responsibility to ensure that it matches the selected assembly. The GenomeSpy documentation covers both forms in encoding genomic coordinates.

Coordinate counting and offsets

GenomeSpy’s internal genomic intervals are zero-based and half-open:

  • zero-based means the first base starts at position 0;

  • half-open [start, end) includes start but excludes end.

For example, [100, 103) covers three bases: 100, 101, and 102. This convention makes interval length simply end - start.

Formats do not all use the same convention. VCF positions and GFF3 starts are one-based. Pass offset=1 to subtract one from such a position during locus encoding:

gff_features = [
    {"seqid": "chr17", "start1": 43_044_001, "end1": 43_050_000, "name": "A"},
    {"seqid": "chr17", "start1": 43_057_001, "end1": 43_061_000, "name": "B"},
]

offset_chart = (
    gs.Chart(gff_features)
    .mark_rect(color="#4c78a8")
    .encode(
        x=gs.Locus("seqid", "start1", offset=1).scale(domain=BRCA1_DOMAIN),
        x2=gs.Locus("seqid", "end1"),
        y=gs.Y("name:N").title("Feature"),
    )
    .properties(assembly="hg38", title="One-based inclusive input")
)

For a one-based inclusive interval such as GFF3 [1, 10], subtract one from the start and leave the inclusive end unchanged. It then becomes the equivalent zero-based half-open interval [0, 10). That is why the example sets offset=1 only on start1.

For a one-based point such as a VCF POS, set offset=1 on its single locus channel. Do not apply an offset merely because labels should begin at one; an offset changes data coordinates, while axis label numbering is a presentation choice. The same convention is described in coordinate counting.

Built-in assemblies

GenomeSpy includes hg38, hg19, hg18, mm10, mm9, and dm6; the current list is in supported genomes. Set one as the root default when all locus scales use it:

chart.properties(assembly="hg38")

An assembly can instead be set on a specific scale with .scale(assembly="hg38"). Prefer the root property when several tracks share the same coordinate system; it avoids repetition and keeps their relationship clear.

Custom assemblies

A custom assembly needs contig names, sizes, and an intentional order. Define a reusable assembly in the root genomes mapping and select it by name:

toy_genome = {
    "contigs": [
        {"name": "chrA", "size": 1_000},
        {"name": "chrB", "size": 600},
        {"name": "plasmid", "size": 250},
    ]
}
toy_features = [
    {"chrom": "chrA", "start": 100, "end": 300, "label": "alpha"},
    {"chrom": "chrB", "start": 50, "end": 220, "label": "beta"},
    {"chrom": "plasmid", "start": 20, "end": 180, "label": "gamma"},
]

custom_assembly_chart = (
    gs.Chart(toy_features)
    .mark_rect()
    .encode(
        x=gs.Locus("chrom", "start"),
        x2=gs.Locus("chrom", "end"),
        y=gs.Y("label:N").axis(None),
        color=gs.Color("label:N").legend(None),
    )
    .properties(
        genomes={"toy": toy_genome},
        assembly="toy",
        title="A custom three-contig assembly",
    )
)

The contig array determines the axis order: chrA, then chrB, then plasmid. A custom definition may instead contain a url pointing to a two-column chrom.sizes file. Define an assembly once under genomes when several scales reuse it; a one-off inline definition can be placed in scale.assembly.

Contig names in the data must match the assembly. For example, 1 and chr1 are different names unless the data loader or preparation step normalizes them. The accepted definition forms are described in custom genomes.

Multiple assemblies

Most charts use one root assembly. Cross-species and coordinate-comparison views can assign a different assembly to each locus scale:

# Orthologous gene starts from the UCSC hg38 and mm10 RefSeq annotations.
homologs = [
    {
        "gene": "BRCA1",
        "hsChrom": "chr17",
        "hsPos": 43_044_295,
        "mmChrom": "chr11",
        "mmPos": 101_488_764,
    },
    {
        "gene": "TP53",
        "hsChrom": "chr17",
        "hsPos": 7_668_421,
        "mmChrom": "chr11",
        "mmPos": 69_580_359,
    },
    {
        "gene": "MYC",
        "hsChrom": "chr8",
        "hsPos": 127_736_231,
        "mmChrom": "chr15",
        "mmPos": 61_985_341,
    },
]

multiple_assembly_chart = (
    gs.Chart(homologs)
    .mark_point(filled=True, size=120)
    .encode(
        # A chrom/pos domain is linearized without the scale's assembly, so it
        # only resolves under a root assembly. These scales carry their own.
        x=gs.Locus("hsChrom", "hsPos")
        .scale(type="locus", assembly="hg38")
        .axis(title="Human hg38", chromGrid=True),
        y=gs.Locus("mmChrom", "mmPos")
        .scale(type="locus", assembly="mm10")
        .axis(title="Mouse mm10", chromGrid=True),
        color=gs.Color("gene:N").legend(title="Gene"),
        tooltip="gene:N",
    )
    .properties(title="Orthologous genes in two assemblies")
)

Here, x uses human hg38 coordinates and y uses mouse mm10 coordinates. A root assembly is unnecessary because every locus scale identifies its own assembly. Keep this scale-local form for genuinely different coordinate systems; ordinary aligned genome-browser tracks should share one root assembly.

The cytoband ideogram and Manhattan plot examples put whole-genome locus scales to work.