Mutation Landscape Oncoplot

Authors

[Editor] Shixiang Wang;

[Contributors] Claude Code — AI-generated, independently AI-reviewed;

Modified

2026-10-19

🤖 AI Skill — Download the Bizard unified skill for your AI assistant ⬇️ Download Skill ZIP
🔌 MCP Server — Let AI agents (Claude Code, Cursor, …) query Bizard live: claude mcp add bizard -- npx @openbiox/bizard-mcp — or the free hosted instance (no install, faster from mainland China): claude mcp add --transport http bizard http://biotree.top:38123/bizard-mcp/mcp 📖 MCP Docs
📖 Cite Bizard — if Bizard helps your research, please cite: Li, K., Zheng, H., Huang, K., et al. (2026). Bizard: A Community-Driven Platform for Accelerating and Enhancing Biomedical Data Visualization. iMetaMed, e70038. 🔗 DOI / BibTeX

A mutation landscape plot (oncoplot, also called a mutation waterfall plot) displays the somatic alteration profile of a cohort gene by gene and sample by sample: rows are the most frequently mutated genes, columns are the samples, and each filled cell is a mutation, colored by its consequence (missense, nonsense, frameshift, splice site, …). Mutation frequency labels on the left, burden bars on top and the option to split samples by molecular subtype or cohort make the oncoplot the standard summary figure of cancer genomics studies, where it reveals which genes drive which subgroup. This tutorial reproduces the chart type covered by the 顶刊图社 series (issue 008); see the Reference section below.

Example

MutationOncoplot DEMO

This oncoplot summarizes a simulated 60-sample cohort across 20 cancer driver genes. Samples are grouped by two molecular subtypes (Subtype A and Subtype B), genes are grouped by signaling pathway (RTK/RAS, PI3K, p53, …), and the top barplot shows each sample’s total mutation count. Subtype A is dominated by TP53 (35% of all samples) and EGFR mutations, while Subtype B carries most of the STK11, KEAP1 and KRAS alterations — exactly the kind of subgroup-specific pattern an oncoplot is meant to expose. Mutation types are color-coded and TP53 occasionally shows Multi_Hit (more than one event). The same cohort is drawn again with the maftools engine in sections 4–5.

Setup

  • System Requirements: Cross-platform (Linux/MacOS/Windows)

  • Programming Language: R

  • Dependencies: ComplexHeatmap; tidyverse; maftools

# Installing necessary packages
if (!requireNamespace("ComplexHeatmap", quietly = TRUE)) {
  install.packages("ComplexHeatmap")
}
if (!requireNamespace("tidyverse", quietly = TRUE)) {
  install.packages("tidyverse")
}
if (!requireNamespace("maftools", quietly = TRUE)) {
  BiocManager::install("maftools")
}

# Load packages
library(ComplexHeatmap)
library(tidyverse)
library(maftools)

Data Preparation

The example simulates a small cohort so the tutorial is fully reproducible without downloading protected TCGA data: 60 samples, two molecular subtypes, and 20 common cancer driver genes mutated with subtype-specific probabilities. Multi_Hit marks genes carrying more than one event in a sample.

set.seed(2026)

genes <- c("TP53", "EGFR", "KRAS", "STK11", "KEAP1", "PIK3CA", "PTEN", "RB1",
           "APC", "ARID1A", "NF1", "NOTCH1", "BRAF", "CDKN2A", "SMAD4", "FBXW7",
           "ATM", "BRCA2", "ERBB2", "MET")
cohort <- rep(c("Subtype A", "Subtype B"), c(35, 25))
samples <- sprintf("S%02d", 1:60)

# Subtype-specific mutation probabilities per gene
p_a <- c(.52, .34, .10, .14, .10, .10, .12, .10, .06, .22, .08, .06, .04, .10, .05, .06, .06, .05, .05, .04)
p_b <- c(.22, .08, .42, .40, .36, .14, .10, .10, .10, .06, .10, .06, .06, .10, .10, .08, .05, .05, .06, .08)
mut_types <- c("Missense", "Nonsense", "Frameshift", "Splice_Site", "In_Frame")
type_p <- c(.58, .14, .12, .08, .08)

mutations <- map_dfr(seq_along(samples), function(i) {
  p <- if (cohort[i] == "Subtype A") p_a else p_b
  hit <- genes[runif(length(genes)) < p]
  if (length(hit) == 0) return(NULL)
  type <- sample(mut_types, length(hit), replace = TRUE, prob = type_p)
  # Tumor suppressors such as TP53 can receive a second hit, shown as Multi_Hit
  if (runif(1) < .15 && "TP53" %in% hit) {
    type[hit == "TP53"] <- "Multi_Hit"
  }
  tibble(sample = samples[i], gene = hit, type = type)
})

head(mutations)
# A tibble: 6 × 3
  sample gene   type       
  <chr>  <chr>  <chr>      
1 S01    PIK3CA Missense   
2 S01    NF1    Missense   
3 S01    BRCA2  Missense   
4 S01    MET    Missense   
5 S02    TP53   Missense   
6 S02    STK11  Splice_Site

Note: In your own analysis the input is a long table with one row per mutation, containing at least the sample name, the gene name and the mutation type (from a MAF file, maftools, or your variant caller). The code below reshapes it into the alternating matrix used by oncoPrint().

# Reshape the mutation table into a gene x sample matrix; a cell holds one or
# several alteration types separated by ";"
gene_levels <- names(sort(table(factor(mutations$gene, levels = genes)), decreasing = TRUE))
mat <- matrix("", nrow = length(genes), ncol = length(samples),
              dimnames = list(genes, samples))
for (i in seq_len(nrow(mutations))) {
  cell <- mat[mutations$gene[i], mutations$sample[i]]
  mat[mutations$gene[i], mutations$sample[i]] <-
    if (cell == "") mutations$type[i] else paste(cell, mutations$type[i], sep = ";")
}

# Order samples: by subtype, then by mutation count (waterfall look)
mut_count <- colSums(mat != "")
ord <- order(factor(cohort, levels = c("Subtype A", "Subtype B")), -mut_count)
mat <- mat[gene_levels, ord]

# Annotation vectors aligned with the reordered matrix
cohort_ordered <- factor(cohort[ord], levels = c("Subtype A", "Subtype B"))
tmb <- mut_count[ord]

# Colors for alteration types
mutation_colors <- c(
  Missense = "#3690C0", Nonsense = "#A50F15", Frameshift = "#F16913",
  Splice_Site = "#6A51A3", In_Frame = "#31A354", Multi_Hit = "#252525"
)

# One drawing function per alteration type
rect_fun <- function(color) {
  function(x, y, w, h) grid.rect(x, y, w * 0.9, h * 0.9, gp = gpar(fill = color, col = NA))
}
alter_fun <- list(
  background = rect_fun("#F2F2F2"),
  Missense = rect_fun(mutation_colors["Missense"]),
  Nonsense = rect_fun(mutation_colors["Nonsense"]),
  Frameshift = rect_fun(mutation_colors["Frameshift"]),
  Splice_Site = rect_fun(mutation_colors["Splice_Site"]),
  In_Frame = rect_fun(mutation_colors["In_Frame"]),
  Multi_Hit = rect_fun(mutation_colors["Multi_Hit"])
)

The matrix above is the native ComplexHeatmap input. The maftools workflow instead consumes a MAF-style table — one row per variant, with the gene, the sample barcode and the Variant_Classification (e.g. Missense_Mutation, Nonsense_Mutation, Frame_Shift_Del). Two details matter when converting:

  • Multi_Hit is derived, not stored. Our simulated table marks the five gene-sample cells that carry two events with the explicit class Multi_Hit, but read.maf() does not know it as a mutation class: rows whose Variant_Classification is outside its vocabulary are moved to the silent set (maf@maf.silent) and excluded from the plots — and a file consisting only of such rows fails with “No non-synonymous mutations found”. We therefore expand every Multi_Hit cell back into two individual events (Missense + Nonsense); maftools then re-derives the multi-hit cells itself — which is also how real MAF files behave (one row per variant).
  • Coordinates and alleles are required by the MAF schema. For plotting they only need to be well-formed; real files carry the actual genomic positions.
# Expand explicit Multi_Hit cells into two individual events
mutations_long <- bind_rows(
  mutations |> filter(type != "Multi_Hit"),
  mutations |> filter(type == "Multi_Hit") |> mutate(type = "Missense"),
  mutations |> filter(type == "Multi_Hit") |> mutate(type = "Nonsense"))

# Our short type names -> the MAF Variant_Classification vocabulary
maf_class <- c(Missense = "Missense_Mutation", Nonsense = "Nonsense_Mutation",
               Frameshift = "Frame_Shift_Del", Splice_Site = "Splice_Site",
               In_Frame = "In_Frame_Del")
variant_type <- c(Missense = "SNP", Nonsense = "SNP", Frameshift = "DEL",
                  Splice_Site = "SNP", In_Frame = "DEL")

set.seed(7)
maf_data <- mutations_long |>
  mutate(
    Hugo_Symbol = gene,
    Variant_Classification = maf_class[type],
    Variant_Type = variant_type[type],
    Chromosome = paste0("chr", (match(gene, genes) %% 22) + 1),
    Start_Position = 100000L + as.integer(match(gene, genes)) * 54321L +
      row_number() * 37L + sample(1:99L, n(), replace = TRUE),
    End_Position = Start_Position,
    Reference_Allele = "A",
    Tumor_Seq_Allele2 = "G",
    Tumor_Sample_Barcode = sample) |>
  select(Hugo_Symbol, Chromosome, Start_Position, End_Position,
         Reference_Allele, Tumor_Seq_Allele2, Variant_Classification,
         Variant_Type, Tumor_Sample_Barcode)

# Sample-level annotation (kept alongside the MAF)
clin_data <- tibble(Tumor_Sample_Barcode = samples, Subtype = cohort)

# Read everything into a maftools MAF object
maf <- read.maf(maf = as.data.frame(maf_data),
                clinicalData = as.data.frame(clin_data), verbose = FALSE)

head(maf_data)
# A tibble: 6 × 9
  Hugo_Symbol Chromosome Start_Position End_Position Reference_Allele
  <chr>       <chr>               <int>        <int> <chr>           
1 PIK3CA      chr7               426005       426005 A               
2 NF1         chr12              697688       697688 A               
3 BRCA2       chr19             1077920      1077920 A               
4 MET         chr21             1186660      1186660 A               
5 TP53        chr2               154572       154572 A               
6 STK11       chr5               317521       317521 A               
# ℹ 4 more variables: Tumor_Seq_Allele2 <chr>, Variant_Classification <chr>,
#   Variant_Type <chr>, Tumor_Sample_Barcode <chr>

Visualization

The same cohort is drawn twice: sections 1–3 build the landscape on ComplexHeatmap::oncoPrint() — full control over annotation tracks and cell rendering — while sections 4–5 use the maftools engine on the MAF representation prepared above, the standard route for real mutation files.

1. Basic Plot

The basic oncoplot shows the alteration matrix with one colored block per mutation; the left labels give each gene’s mutation frequency and the right barplot counts alterations per gene.

p1 <- oncoPrint(mat,
  alter_fun = alter_fun, alter_fun_is_vectorized = FALSE, col = mutation_colors,
  column_order = samples[ord], show_column_names = FALSE,
  row_names_gp = gpar(fontsize = 9, fontface = "italic"), pct_gp = gpar(fontsize = 8),
  heatmap_legend_param = list(title = "Mutation type"))
draw(p1)
Figure 1: Basic Mutation Landscape Oncoplot

2. Add Cohort Split and Mutation Burden

Splitting the columns by molecular subtype and adding a top barplot of each sample’s mutation count turns the matrix into the classic landscape figure: subgroup-specific genes become visually obvious.

top_annotation <- HeatmapAnnotation(
  TMB = anno_barplot(tmb, height = unit(1.1, "cm"),
                     gp = gpar(fill = "#6A51A3", col = NA)),
  annotation_name_gp = gpar(fontsize = 9))

p2 <- oncoPrint(mat,
  alter_fun = alter_fun, alter_fun_is_vectorized = FALSE, col = mutation_colors,
  column_order = samples[ord], show_column_names = FALSE,
  column_split = cohort_ordered, column_title_gp = gpar(fontsize = 10, fontface = "bold"),
  top_annotation = top_annotation,
  bottom_annotation = HeatmapAnnotation(
    Subtype = as.character(cohort_ordered),
    col = list(Subtype = c("Subtype A" = "#E08214", "Subtype B" = "#5E3C99")),
    annotation_name_side = "left", annotation_name_gp = gpar(fontsize = 9)),
  row_names_gp = gpar(fontsize = 9, fontface = "italic"), pct_gp = gpar(fontsize = 8),
  heatmap_legend_param = list(title = "Mutation type"))
draw(p2)
Figure 2: Oncoplot split by molecular subtype with mutation burden bars
Tip

Key Parameters:

  • column_split: splits the columns into panels, here by molecular subtype; row_split works the same way for genes (used in Figure 3).
  • top_annotation: any HeatmapAnnotation — the mutation burden barplot (anno_barplot) is the conventional choice.
  • alter_fun_is_vectorized = FALSE: draw alteration blocks one cell at a time; required when the functions above are not vectorized.

3. Group Genes by Pathway

Grouping the rows by signaling pathway (RTK/RAS, PI3K, p53, …) organizes the waterfall into functional blocks, which is how oncoplots are usually laid out in publications.

pathway <- c(
  TP53 = "p53", RB1 = "p53", CDKN2A = "p53",
  EGFR = "RTK/RAS", ERBB2 = "RTK/RAS", MET = "RTK/RAS", KRAS = "RTK/RAS",
  BRAF = "RTK/RAS", NF1 = "RTK/RAS",
  PIK3CA = "PI3K", PTEN = "PI3K", STK11 = "PI3K", KEAP1 = "PI3K",
  APC = "WNT/Notch", NOTCH1 = "WNT/Notch", FBXW7 = "WNT/Notch",
  ARID1A = "Chromatin", SMAD4 = "TGF-beta", ATM = "DDR", BRCA2 = "DDR")
pathway_levels <- c("RTK/RAS", "PI3K", "p53", "WNT/Notch", "Chromatin", "TGF-beta", "DDR")

p3 <- oncoPrint(mat,
  alter_fun = alter_fun, alter_fun_is_vectorized = FALSE, col = mutation_colors,
  column_order = samples[ord], show_column_names = FALSE,
  row_split = factor(pathway[gene_levels], levels = pathway_levels),
  row_title_gp = gpar(fontsize = 9, fontface = "bold"), row_title_rot = 0,
  column_split = cohort_ordered, column_title_gp = gpar(fontsize = 10, fontface = "bold"),
  top_annotation = top_annotation,
  row_names_gp = gpar(fontsize = 9, fontface = "italic"), pct_gp = gpar(fontsize = 8),
  heatmap_legend_param = list(title = "Mutation type"))
draw(p3)
Figure 3: Oncoplot with genes grouped by signaling pathway

Figure 3 reveals the same cohort structure with a functional reading: Subtype A accumulates TP53 events and RTK/RAS-pathway EGFR mutations, while Subtype B concentrates STK11/KEAP1 (PI3K pathway) and KRAS alterations.

4. maftools Oncoplot

With the cohort available as a MAF object (maf), maftools::oncoplot() draws the classic cancer-genomics landscape: per-sample mutation-burden bars on top, per-gene frequency and variant-class breakdown on the right, automatic percentage labels and its own colour vocabulary. sortByMutation = TRUE arranges the samples into the familiar waterfall order (they are ordered on the binarised mutation matrix).

oncoplot(maf = maf, top = 20, sortByMutation = TRUE)
Figure 4: Basic maftools oncoplot of the same cohort
Tip

Key Parameters:

  • top: how many of the most frequently mutated genes to draw (here all 20).
  • sortByMutation: order samples by mutation count (TRUE) or keep file order (FALSE).
  • Samples without a single alteration are not shown: maftools only knows the samples present in the MAF, so the subtitle reads “Altered in 53 (100%) of 53 samples” — the 53 of the 60 cohort samples that carry at least one mutation.

5. maftools with Clinical Annotation

clinicalFeatures adds annotation tracks from the clinical table — the direct counterpart of the column_split view in Figure 2. sortByAnnotation = TRUE groups samples by the annotation, so the subtype-specific genes line up in blocks again (compare the TP53 block on the left with the STK11/KEAP1 block on the right).

oncoplot(maf = maf, top = 20,
         clinicalFeatures = "Subtype", sortByAnnotation = TRUE)
Figure 5: maftools oncoplot annotated and sorted by molecular subtype
Tip

Key Parameters:

  • clinicalFeatures: column(s) of the clinical table drawn as annotation tracks below the matrix.
  • sortByAnnotation: sort samples within the annotation groups (TRUE).

6. Choosing an Engine

Both routes describe the same cohort — TP53 altered in 40% of the mutated samples (35% of all 60), STK11/KEAP1/KRAS concentrated in Subtype B — but they optimise for different workflows:

  • ComplexHeatmap::oncoPrint() (sections 1–3): full control — custom cell renderers, arbitrary annotation tracks from any data, arbitrary row/column grouping, integration with the rest of the heatmap ecosystem. Best when the oncoplot must compose with other heatmap panels or needs bespoke styling.
  • maftools::oncoplot() (sections 4–5): the standard intake for real MAF files — read a cohort’s variants in one call and get publication-ready summaries (burden bars, variant-class breakdown, clinical annotations, derived multi-hit cells). Best for routine cohort figures and for anything downstream in the maftools toolkit (mutual exclusivity, TiTv, driver analysis).

Applications

  • Cohort characterization: first-line summary of a cancer genomics study, usually split by subtype, stage or treatment arm.
  • Driver discovery: recurrently altered genes and their co-occurrence or mutual exclusivity patterns.
  • Mutation burden: the top barplot summarises the per-sample burden — the ComplexHeatmap route counts mutated genes, the maftools route counts individual variants (they differ only for multi-hit samples).
  • Both engines shown above accept real cohorts: ComplexHeatmap composes with any heatmap panel, while maftools::read.maf() ingests MAF files from your variant caller directly — it adds clinicalData annotation tracks (sections 4–5) and unlocks the wider maftools toolkit (mutual exclusivity, TiTv, driver analysis) downstream.

Reference

[1] 顶刊图社 | 第008期:基因突变全景瀑布图. WeChat article. http://mp.weixin.qq.com/s?__biz=MzU5NjE2ODU0OQ==&mid=2247483816&idx=1&sn=73bb796a166fb501934ab4fab2aaf28a

[2] Mayakonda A, Lin DC, Assenov Y, Plass C, Koeffler HP. Maftools: efficient and comprehensive analysis of somatic variants in cancer. Genome Res. 2018;28(11):1747-1756. doi:10.1101/gr.239244.118

[3] Gu Z, Eils R, Schlesner M. Complex heatmaps reveal patterns and correlations in multidimensional genomic data. Bioinformatics. 2016;32(18):2847-2849. doi:10.1093/bioinformatics/btw313