Oncoplot

A cohort-level alteration matrix with per-sample burden bars above, recurrently altered genes in the center, and per-gene summary bars at the side.

Data use and provenance

The packaged mutation and annotation inputs are the TCGA LAML example files distributed with maftools, which is MIT-licensed. TCGA data are open-access. During data loading, the package excludes silent and non-coding calls, selects the ten most recurrently altered genes, orders samples by alteration pattern, and collapses repeated sample-gene calls to Multi_Hit. GenomeSpy then renders the prepared tables.

Code

"""Oncoplot.

A cohort-level alteration matrix with per-sample burden bars above, recurrently
altered genes in the center, and per-gene summary bars at the side.
"""

from __future__ import annotations

import genome_spy as gs
from genome_spy.datasets._oncoprint import laml_oncoplot_data
from genome_spy.schema import Legend, RulerMarkConfig, Scale


CLASS_ORDER = [
    "In_Frame_Ins",
    "Frame_Shift_Del",
    "Missense_Mutation",
    "In_Frame_Del",
    "Frame_Shift_Ins",
    "Nonsense_Mutation",
    "Splice_Site",
    "Multi_Hit",
]

# Load the prepared mutation matrix and sample and gene summaries.
data = laml_oncoplot_data()
# Follow the mouse with a line to help identify the current sample column.
sample_ruler = gs.ruler(
    "sampleRuler",
    persist=False,
    encodings=["x"],
    snap=False,
    mark=RulerMarkConfig(opacity=0.3),
)

class_colors = (
    Scale()
    .domain(CLASS_ORDER)
    .range(
        [
            "#d53e4f",
            "#377eb8",
            "#33a02c",
            "#fff176",
            "#6a3d9a",
            "#ff1f1f",
            "#ff9800",
            "#111111",
        ]
    )
)

# Set the space reserved for the matrix and its surrounding summaries.
matrix_width = 400
percent_width = 52
counts_width = 120
tmb_height = 70
matrix_height = 352
tmb_limit = data["tmb_limit"]
count_limit = data["count_limit"]
sample_domain = data["sample_domain"]
gene_order = data["genes"].sort_values("gene_order")["gene"].tolist()
gene_scale = {"domain": gene_order, "reverse": True, "padding": 0.08}

mutation_legend = (
    Legend()
    .title("Mutation class")
    .orient("bottom")
    .direction("horizontal")
    .columns(3)
    .symbolSize(90)
)

# Stack mutation counts by class above each sample.
tmb = (
    gs.Chart(data["sample_tmb"])
    .transform_stack(
        field="count",
        groupby=["sample_order"],
        as_=["_y0", "_y1"],
    )
    .mark_rect()
    .encode(
        x=gs.X("sample_order:I").axis(None).title(None),
        y=gs.Y("_y0:Q").scale(reverse=False, domain=[0, tmb_limit]).title("TMB"),
        y2=gs.Y2("_y1"),
        color=gs.Color("class:N").scale(class_colors).legend(None),
    )
    .properties(width=matrix_width, height=tmb_height)
)

# Give every sample–gene cell a pale background.
grid = (
    gs.Chart(data["grid"])
    .mark_rect(color="#f1f3f5", stroke="white", strokeWidth=0.5)
    .encode(
        x=gs.X("sample_order:I").axis(None).title(None),
        y=gs.Y("gene:N").title(None),
    )
)

# Fill altered cells with their mutation-class colors.
matrix = (
    gs.Chart(data["events"])
    .mark_rect(stroke="white", strokeWidth=0.5)
    .encode(
        x=gs.X("sample_order:I").axis(None).title(None),
        y=gs.Y("gene:N").title(None),
        color=gs.Color("class:N").scale(class_colors).legend(mutation_legend),
    )
)

# Combine the background and mutations, then add the hover line.
matrix_panel = (
    (grid + matrix)
    .properties(
        width=matrix_width,
        height=matrix_height,
        scales={"y": gene_scale},
    )
    .add_params(sample_ruler)
)

# Show the percentage of samples with an alteration in each gene.
percent_panel = (
    gs.Chart(data["genes"])
    .mark_text(align="right", dx=-2, size=11)
    .encode(
        x=gs.value(1),
        y=gs.Y("gene:N").axis(None).title(None),
        text=gs.Text("label:N"),
    )
    .properties(width=percent_width, height=matrix_height, scales={"y": gene_scale})
)

# Leave space above the percentages to line them up with the matrix rows.
percent_header = (
    gs.Chart([{}])
    .mark_text(opacity=0)
    .properties(width=percent_width, height=tmb_height)
)

# Label the gene-count bars on the right.
count_title = (
    gs.Chart([{"label": "No. of samples"}])
    .mark_text(size=11)
    .encode(
        x=gs.value(0.5),
        text=gs.Text("label:N"),
    )
    .properties(width=counts_width, height=tmb_height)
)

# Stack the number of affected samples by mutation class for each gene.
count_bars = (
    gs.Chart(data["gene_counts"])
    .transform_stack(field="count", groupby=["gene"], as_=["_x0", "_x1"])
    .mark_rect()
    .encode(
        x=gs.X("_x0:Q")
        .scale(reverse=False, domain=[0, count_limit], zero=True)
        .title(None),
        x2=gs.X2("_x1"),
        y=gs.Y("gene:N").axis(None).title(None),
        color=gs.Color("class:N").scale(class_colors).legend(None),
    )
    .properties(width=counts_width, height=matrix_height)
)

# Add pale row backgrounds behind the gene-count bars.
count_grid = (
    gs.Chart(data["genes"][["gene"]])
    .mark_rect(color="#f1f3f5", stroke="white", strokeWidth=0.5)
    .encode(
        y=gs.Y("gene:N").axis(None).title(None),
    )
    .properties(width=counts_width, height=matrix_height)
)

counts_panel = (count_grid + count_bars).properties(
    width=counts_width, height=matrix_height, scales={"y": gene_scale}
)

# Keep the top bars and matrix columns aligned while zooming through samples.
sample_column = (
    gs.concat(tmb, matrix_panel, columns=1, spacing=4)
    .properties(
        scales={
            "x": {
                "domain": sample_domain,
                "paddingInner": 0,
                "paddingOuter": 0,
                "zoom": True,
            }
        }
    )
    .resolve_scale(x="shared", y="independent")
)
percent_column = gs.concat(percent_header, percent_panel, columns=1, spacing=4)
counts_column = gs.concat(count_title, counts_panel, columns=1, spacing=4)

summary = f"Altered in {data['altered_samples']} ({data['altered_samples'] / data['total_samples']:.2%}) of {data['total_samples']} samples."

# Place the matrix beside its percentages and counts, and add the cohort total.
chart = (
    gs.concat(sample_column, percent_column, counts_column, columns=3, spacing=4)
    .resolve_scale(x="independent", y="independent")
    .resolve_axis(y="independent")
    .properties(
        title=summary,
        description="A TCGA LAML oncoplot styled after the canonical maftools example with top mutation-burden bars, recurrently altered genes, percent labels, and right-side per-gene sample counts.",
    )
)