GFF3 gene annotations¶
A browser-style gene-annotation track built from a GFF3 source. Layered marks separate transcript bodies, exon structure, UTRs, and transcript labels within one shared locus view.
Data use and provenance
The visualization uses a sorted and bgzip-compressed copy of the GENCODE human release 43 (GRCh38.p13) comprehensive gene annotation GFF3. GENCODE states that all project data are open access.
What to notice¶
Transcript bodies, exon and UTR blocks, and transcript labels share packed
lanes. The labels include < or > to indicate strand and are shown only when
they fit within the transcript interval.
Python implementation¶
The lazy GFF3 source is flattened and projected into transcript records, packed into lanes, and expanded again to draw exon, CDS, and UTR features.
See the official GenomeSpy example for the feature hierarchy and transform details.
Code¶
"""GFF3 gene annotations.
A browser-style gene-annotation track built from a GFF3 source. Layered marks
separate transcript bodies, exon structure, UTRs, and transcript labels within
one shared locus view.
"""
from __future__ import annotations
import genome_spy as gs
DOMAIN = [
{"chrom": "chr5", "pos": 177482500},
{"chrom": "chr5", "pos": 177518000},
]
# Add an invisible, wider line to make each transcript easier to hover over.
transcript_tooltip_trap = (
gs.Chart()
.mark_rule(color="#b0b0b0", opacity=0, size=7)
.properties(name="gencode-tooltip-trap", title="GENCODE transcript")
)
# Draw a thin line spanning each transcript.
transcript_body = (
gs.Chart()
.mark_rule(color="#b0b0b0", tooltip=None)
.properties(name="gencode-transcript-body")
)
transcript_layer = gs.layer(transcript_tooltip_trap, transcript_body).properties(
name="gencode-transcript"
)
# Give each exon and its parts a separate row of data for drawing.
exon_base = (
gs.Chart()
.transform_flatten(fields=["_child_features"])
.transform_flatten(fields=["_child_features"], as_=["child_feature"])
.transform_project(
fields=[
"gene_name",
"_lane",
"child_feature.type",
"child_feature.seq_id",
"child_feature.start",
"child_feature.end",
"child_feature.attributes.exon_number",
"child_feature.attributes.exon_id",
],
as_=[
"gene_name",
"_lane",
"type",
"seq_id",
"start",
"end",
"exon_number",
"exon_id",
],
)
)
# Outline each exon.
exon_rects = (
exon_base.mark_rect(
minWidth=0.5,
minOpacity=0.5,
stroke="#505050",
fill="#fafafa",
strokeWidth=1,
)
.transform_filter(gs.datum.type == "exon")
.properties(title="GENCODE exon")
)
# Color coding regions (CDS) and untranslated regions (UTRs) differently.
utr_cds_rects = (
exon_base.mark_rect(
minWidth=0.5,
minOpacity=0,
strokeWidth=1,
strokeOpacity=0,
stroke="gray",
)
.transform_filter(
(gs.datum.type != "exon")
& (gs.datum.type != "start_codon")
& (gs.datum.type != "stop_codon")
)
.encode(
fill=gs.Fill("type:N").scale(
domain=["five_prime_UTR", "CDS", "three_prime_UTR"],
range=["#83bcb6", "#ffbf79", "#d6a5c9"],
)
)
.properties(title="GENCODE exon")
)
# Label the untranslated regions as 5' or 3'.
utr_labels = (
exon_base.mark_text(
color="black",
size=11,
opacity=0.7,
paddingX=2,
paddingY=1.5,
tooltip=None,
)
.transform_filter(
(gs.datum.type == "three_prime_UTR") | (gs.datum.type == "five_prime_UTR")
)
.transform_formula(
expr=gs.expr.if_(gs.datum.type == "three_prime_UTR", "3'", "5'"),
as_="label",
)
.encode(text=gs.Text("label:N"))
)
exon_layer = gs.layer(exon_rects, utr_cds_rects, utr_labels).properties(
name="gencode-exons"
)
# Label each transcript with its name, ID, and reading direction.
transcript_labels = (
gs.Chart()
.mark_text(size=10, yOffset=12, tooltip=None, color="#505050")
.transform_formula(
expr=gs.expr.if_(gs.datum.strand == "-", "< ", "")
+ gs.datum.transcript_name
+ " - "
+ gs.datum.transcript_id
+ gs.expr.if_(gs.datum.strand == "+", " >", ""),
as_="label",
)
.encode(text=gs.Text("label:N"))
.properties(name="gencode-transcript-labels")
)
# Load gene annotations for the visible region and combine their shapes and labels.
chart = (
gs.layer(transcript_layer, exon_layer, transcript_labels)
.properties(
title="GENCODE GFF3 gene annotations",
description=(
"A packed gene-annotation track showing transcript bodies, exons, "
"UTRs, and labels within one locus view."
),
assembly="hg38",
height=gs.step(28),
viewportHeight="container",
data=gs.lazy.gff3(
"https://data.genomespy.app/sample-data/gencode.v43.annotation.sorted.gff3.gz",
windowSize=2_000_000,
debounceDomainChange=300,
),
scales=gs.scales(x=gs.Scale(domain=DOMAIN)),
)
.encode(
x=gs.Locus("seq_id", "start", offset=1),
x2=gs.Locus("seq_id", "end"),
y=gs.Y("_lane", type="index")
.scale(zoom=False, reverse=True, domain=[0, 40], padding=0.5)
.axis(None),
)
# Extract the transcript names, positions, and exon details from the GFF3 data.
.transform_flatten()
.transform_formula(expr=gs.datum.attributes.gene_name, as_="gene_name")
.transform_flatten(fields=["child_features"])
.transform_flatten(fields=["child_features"], as_=["child_feature"])
.transform_project(
fields=[
"gene_name",
"child_feature.type",
"child_feature.strand",
"child_feature.seq_id",
"child_feature.start",
"child_feature.end",
"child_feature.attributes.gene_type",
"child_feature.attributes.transcript_type",
"child_feature.attributes.gene_id",
"child_feature.attributes.transcript_id",
"child_feature.attributes.transcript_name",
"child_feature.attributes.tag",
"source",
"child_feature.child_features",
],
as_=[
"gene_name",
"type",
"strand",
"seq_id",
"start",
"end",
"gene_type",
"transcript_type",
"gene_id",
"transcript_id",
"transcript_name",
"tag",
"source",
"_child_features",
],
)
# Put overlapping transcripts on separate rows.
.transform_collect(sort=gs.compare(["seq_id", "start", "transcript_id"]))
.transform_pileup(start="start", end="end", as_="_lane")
.configure_view(stroke="lightgray")
)