Label points and genomic intervals¶
Labels and leader lines make the important parts of a chart easier to find. In this tutorial we combine ordinary text, rule, and interval marks with your main visualization to showcase how datapoints or intervals can be annotated.
Label selected points¶
Give each label annotation the coordinates of its target and a pair of pixel offsets for the label endpoint. A rule connects the offset endpoint to the point, and a text mark draws the label:
points = [
{"gene": "A", "effect": -1.8, "significance": 2.1},
{"gene": "B", "effect": -0.7, "significance": 1.2},
{"gene": "C", "effect": 0.2, "significance": 0.7},
{"gene": "D", "effect": 1.1, "significance": 1.5},
{"gene": "E", "effect": 2.0, "significance": 2.8},
]
annotations = [
{
"gene": "A",
"effect": -1.8,
"significance": 2.1,
"label_x_offset": -16,
"label_y_offset": -16,
},
{
"gene": "E",
"effect": 2.0,
"significance": 2.8,
"label_x_offset": -16,
"label_y_offset": 16,
},
]
point_tooltip = [
gs.Tooltip("gene:N").title("Gene"),
gs.Tooltip("effect:Q").title("Effect"),
gs.Tooltip("significance:Q").title("Significance"),
]
point_marks = (
gs.Chart(points)
.mark_point(filled=True, size=80)
.encode(
x=gs.X("effect:Q").title("Effect"),
y=gs.Y("significance:Q").title("Significance"),
tooltip=point_tooltip,
)
)
# Use pixel offsets for the label endpoint so zooming the data scales does not
# move the label. The primary x/y endpoint is shifted; x2/y2 stays on the point.
leader_lines = (
gs.Chart(annotations)
.mark_rule(color="#555", size=1, tooltip=None)
.encode(
x=gs.X("effect:Q").title("Effect"),
xOffset=gs.XOffset("label_x_offset:Q").scale(None),
x2="effect",
y=gs.Y("significance:Q").title("Significance"),
yOffset=gs.YOffset("label_y_offset:Q").scale(None),
y2="significance",
)
)
point_labels = (
gs.Chart(annotations)
.mark_text(
baseline="bottom",
yOffset=-3,
fontWeight="bold",
tooltip=None,
)
.encode(
x=gs.X("effect:Q").title("Effect"),
xOffset=gs.XOffset("label_x_offset:Q").scale(None),
y=gs.Y("significance:Q").title("Significance"),
yOffset=gs.YOffset("label_y_offset:Q").scale(None),
text="gene:N",
)
)
point_annotation_chart = (point_marks + leader_lines + point_labels).properties(
title="Selected observations with leader-line labels"
)
The label table contains only the points you want to call out. This is usually clearer than labelling every observation. Pixel offsets keep label spacing steady while readers zoom.
Leader lines are optional when a label can sit directly above its point. When several labels are close together, adjust their offsets rather than moving the points, and keep the number of callouts small. The airway volcano plot and airway MA plot apply this pattern to selected genes.
Add a gene context track¶
A gene track is simply an interval track. Each row needs a chromosome, start and end coordinates, and a label. Strand and a label-priority score are useful additions. The coordinates must use the same assembly and counting convention as the tracks they accompany.
The example below uses the full packaged hg19 RefSeq gene-body resource and opens around three mutation clusters from the rainfall gallery. Each gene is represented by one body for each overlapping symbol, chromosome, and strand locus. The label score counts contributing RefSeq transcripts:
GENE_REGION = [
{"chrom": "chr8", "pos": 97_850_000},
{"chrom": "chr8", "pos": 99_000_000},
]
genes = refseq_gene_bodies("hg19")
gene_tooltip = [
gs.Tooltip("symbol:N").title("Gene"),
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"),
]
gene_bodies = (
gs.Chart()
.mark_arrow(
style="arrow-block",
fill="#d5d9de",
stroke="#59636e",
strokeWidth=1,
yOffset=5,
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])
)
)
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",
clip=False,
yOffset=-5,
size=11,
color="#20262d",
tooltip=gs.HandledTooltip(handler="default"),
)
.encode(
x=gs.X("label_position:L"),
text="symbol:N",
tooltip=gene_tooltip,
)
)
gene_annotation_track = (
(gene_bodies + gene_labels)
.properties(
assembly="hg19",
data=genes,
scales=gs.scales(x=gs.Scale(domain=GENE_REGION)),
title=gs.title("RefSeq gene annotations", orient="left"),
height=gs.step(24),
axes=gs.axes(x=gs.GenomeAxis(title="Genomic position")),
)
.encode(
y=gs.Y("lane:O")
.scale(
type="index",
domain=[0, 3],
reverse=True,
align=0,
paddingInner=0.4,
paddingOuter=0.2,
zoom=False,
)
.axis(None)
)
.transform_linearize_genomic_coordinate(
chrom="chrom",
pos=["start", "end"],
as_=["linear_start", "linear_end"],
)
.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)
)
Arrange overlapping genes¶
Nearby genes can overlap, so
transform_linearize_genomic_coordinate()
first places their coordinates on one shared axis.
transform_collect() puts them in a predictable
order, and transform_pileup() assigns
overlapping genes to separate lanes. The final filter shows up to three lanes
to keep the track compact.
Arrow-shaped gene bodies show their direction. For intervals without a direction, use a rectangle or rule instead.
Keep labels readable¶
transform_measure_text() measures each gene
name. transform_filter_scored_labels() then
shows only labels that fit without overlapping. When there is not enough room
for every label, genes with a higher score are preferred.
Here, score is only a layout preference: it helps choose which names to show.
It is not a measure of biological importance. Labels are checked again as the
reader zooms, so more names can appear when there is room.
Align the track with another visualization¶
Place the completed annotation track below a signal track with &, then put
the assembly, locus domain, genome axis, and shared x resolution on their
common parent. Keep y scales independent because the primary measurement and
the packed gene lanes have unrelated units.
The rainfall plot aligns hg19 genes with mutation distances around a cluster-rich region. The TCGA OV GISTIC landscape uses the same track dataflow with hg19 genes around the 19p13.3 deletion peak. In both charts, zooming either view updates the primary plot and gene context together.
The RefSeq genes with scored labels example shows the full pattern with transcript bodies and semantic zoom.