Rainfall plot¶
Inter-mutation distance across the genome for a single tumor sample. Point color encodes substitution class and arrow annotations call out compact kataegis-like clusters.
Data use and provenance
The mutation calls are the TCGA BRCA sample distributed with the MIT-licensed
maftools rainfall example. During
data loading, the package selects the most mutated sample, calculates
inter-event distances, and detects compact six-mutation windows. The trimmed
MAF omits NCBI_Build; its coordinates and gene-symbol overlaps identify it
as hg19. The displayed
hg19 RefSeq gene bodies come from the assembly-wide resource independently
prepared from the official UCSC refGene table. The track design follows the
MutGlyph gene-annotation pattern.
GenomeSpy performs the lane packing and scored-label filtering while rendering.
What to notice¶
The focal chr8 view contains three nearby compact mutation clusters. The gene track places CPQ, TSPYL5, MTDH, LAPTM4B, and neighboring RefSeq genes on the same zoomable locus scale, providing context without implying that a nearby gene caused a cluster.
Python implementation¶
The rainfall points, cluster arrows, and full hg19 gene table are vertically concatenated. Their x scale and genome axis are shared, while their unrelated y scales remain independent. The domain opens on chr8 but does not pre-filter the gene table, so annotation bodies remain available when panning. GenomeSpy packs overlapping genes into three lanes and retains non-overlapping labels according to their display score.
Code¶
"""Rainfall plot.
Inter-mutation distance across the genome for a single tumor sample. Point
color encodes substitution class and arrow annotations call out compact
kataegis-like clusters.
"""
from __future__ import annotations
import genome_spy as gs
from genome_spy.datasets._annotations import refseq_gene_bodies
from genome_spy.datasets._mutation import brca_rainfall_data
from genome_spy.schema import GenomeAxis, Legend, Scale
CONVERSION_ORDER = ["C>T", "C>G", "C>A", "T>C", "T>A", "T>G"]
CONVERSION_COLORS = ["#f64b3c", "#4f63c9", "#2891e8", "#f6b617", "#4caf50", "#f7931a"]
GENOME_DOMAIN = [{"chrom": "chr1"}, {"chrom": "chrY"}]
# Load mutation distances and cluster annotations for one tumor sample.
data = brca_rainfall_data()
points = data["points"]
change_points = data["change_points"]
y_domain = [0.0, data["y_max"]]
sample_name = data["sample"]
assembly = data["reference_build"]
# Load gene annotations for the same genome version.
genes = refseq_gene_bodies(assembly)
# Label chromosomes and give neighboring chromosomes alternating backgrounds.
axis = (
GenomeAxis()
.title("Genomic position")
.chromGrid(True)
.chromGridOpacity(0.16)
.chromGridFillEven("#f5f7fb")
.chromGridFillOdd("#ffffff")
.chromLabels(True)
.chromLabelFontSize(11)
.chromTicks(True)
.grid(False)
)
mutation_scale = Scale().domain(CONVERSION_ORDER).range(CONVERSION_COLORS)
mutation_legend = (
Legend()
.orient("top-right")
.direction("horizontal")
.columns(3)
.symbolSize(80)
.title("Substitution class")
)
# Plot the distance from each mutation to the previous one; low points are close
# together. offset=1 accounts for mutation positions being numbered from one.
rainfall_points = (
gs.Chart(points)
.mark_point(size=18, filled=True, opacity=0.95)
.encode(
x=gs.Locus("chrom", "pos", offset=1),
y=gs.Y("log10_distance:Q")
.scale(reverse=False, domain=y_domain)
.title("log10 inter-event distance"),
color=gs.Color("con_class:N").scale(mutation_scale).legend(mutation_legend),
)
)
# Draw pointers to the annotated mutation clusters.
change_point_stems = (
gs.Chart(change_points)
.mark_rule(color="#111111", size=1.2)
.encode(
x=gs.Locus("chrom", "start", offset=1),
y=gs.Y(gs.datum(0), type="quantitative")
.scale(reverse=False, domain=y_domain)
.title("log10 inter-event distance"),
y2=gs.Y2("arrow_y"),
)
)
# Add an arrowhead at the tip of each pointer.
change_point_heads = (
gs.Chart(change_points)
.mark_point(shape="triangle-up", size=60, filled=True, color="#111111")
.encode(
x=gs.Locus("chrom", "start", offset=1),
y=gs.Y("arrow_y:Q").scale(reverse=False, domain=y_domain),
)
)
# Combine the mutation points and cluster pointers.
rainfall_track = (rainfall_points + change_point_stems + change_point_heads).properties(
name="rainfall-track",
title=sample_name,
height=300,
)
# Choose the gene details to show on hover.
gene_tooltip = [
gs.Tooltip("symbol:N").title("Gene"),
gs.Tooltip("identifier:N").title("RefSeq locus"),
gs.Tooltip("chrom:N").title("Chromosome"),
gs.Tooltip("start:Q").title("Start").format(",d"),
gs.Tooltip("end:Q").title("End").format(",d"),
gs.Tooltip("strand:N").title("Strand"),
]
# Draw genes as arrows showing their reading direction; reveal them on zoom.
gene_bodies = (
gs.Chart()
.mark_arrow(
style="arrow-block",
fill="#d5d9de",
stroke="#59636e",
strokeWidth=1,
yOffset=8,
size=7,
tooltip=gs.HandledTooltip(handler="default"),
)
.encode(
x=gs.Locus("chrom", "start"),
x2=gs.Locus("chrom", "end"),
direction=gs.Direction("strand:N").scale(
domain=["+", "-"], range=["forward", "reverse"]
),
tooltip=gene_tooltip,
)
.properties(
opacity=gs.dynamic_opacity(unitsPerPixel=[100000, 40000], values=[0, 1])
)
)
# Hide overlapping names, using the supplied scores to choose which to keep.
gene_labels = (
gs.Chart()
.transform_measure_text(field="symbol", as_="label_width", fontSize=11)
.transform_filter_scored_labels(
pos="linear_start",
pos2="linear_end",
asMidpoint="label_position",
score="score",
width="label_width",
lane="lane",
padding=5,
)
.mark_text(
baseline="middle",
align="center",
# Keep names inside the left and right edges of the track.
clip="x",
yOffset=-2,
size=11,
color="#20262d",
tooltip=gs.HandledTooltip(handler="default"),
)
.encode(
x=gs.X("label_position:L"),
text="symbol:N",
tooltip=gene_tooltip,
)
)
# Combine gene shapes and names, leaving room above each row for the text.
gene_track = (
(gene_bodies + gene_labels)
.properties(
name="refseq-genes",
data=genes,
title=gs.title("RefSeq genes", orient="left", offset=8),
height=gs.step(24),
padding=gs.Paddings(top=10),
)
.encode(
y=gs.Y("lane:O")
.scale(
type="index",
domain=[0, 3],
reverse=True,
align=0,
paddingInner=0.4,
paddingOuter=0.5,
zoom=False,
)
.axis(None)
)
.transform_linearize_genomic_coordinate(
chrom="chrom",
pos=["start", "end"],
as_=["linear_start", "linear_end"],
)
# Put overlapping genes on separate rows, showing up to three rows.
.transform_collect(sort=gs.compare(field=["linear_start", "linear_end"]))
.transform_pileup(
start="linear_start",
end="linear_end",
as_="lane",
preference="strand",
preferredOrder=["-", "+"],
)
.transform_filter(gs.datum.lane < 3)
)
# Put genes below the mutation plot so both tracks move together when zooming.
chart = (
(rainfall_track & gene_track)
.properties(
assembly=assembly,
width="container",
scales=gs.scales(x=gs.Scale(domain=GENOME_DOMAIN)),
axes=gs.axes(x=axis),
spacing=8,
description=(
"A genome-wide rainfall plot with substitution classes, arrowed "
"change-point annotations, and aligned RefSeq gene bodies."
),
)
.resolve_scale(x="shared", y="independent")
.resolve_axis(x="shared", y="independent")
.configure_view(stroke="lightgray")
)