Sunburst Chart

Authors

[Editor] Shixiang Wang;

[Contributors] ZCode (GLM) — AI-generated, independently AI-reviewed.

Modified

2026-10-31

🤖 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 sunburst chart is a multi-level donut for hierarchical composition: each ring is one level of the hierarchy, angular width is proportional to a value (counts, cost), and segments nest radially from root to leaves [1]. Compared with a treemap it emphasizes the levels of the hierarchy; compared with stacked pies it keeps every level quantitative and interactive (hover to inspect, click to zoom into a subtree). In clinical data it shines for cohorts that decompose naturally — cancer type → treatment line → response, or department → diagnosis group → procedure.

This tutorial builds sunbursts with plotly [2] using the ids/labels/parents/values node table. Because the figure is an interactive HTML widget, the rendered page supports hover tooltips and click-to-zoom.

This tutorial covers two workflows:

  1. A basic three-level cohort sunburst;
  2. A color-coded variant with per-branch colors and hover text.

Example

Sunburst DEMO

Setup

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

  • Programming language: R

  • Dependent packages: plotly; dplyr

# Load packages
library(plotly)
library(dplyr)
sessioninfo::session_info("attached")
─ Session info ───────────────────────────────────────────────────────────────
 setting  value
 version  R version 4.6.2 (2026-10-29)
 os       Ubuntu 24.04.5 LTS
 system   x86_64, linux-gnu
 ui       X11
 language (EN)
 collate  C.UTF-8
 ctype    C.UTF-8
 tz       UTC
 date     2026-10-31
 pandoc   3.1.3 @ /usr/bin/ (via rmarkdown)
 quarto   1.10.20 @ /usr/local/bin/quarto

─ Packages ───────────────────────────────────────────────────────────────────
 ! package * version    date (UTC) lib source
 P dplyr   * 1.2.1      2026-04-03 [?] RSPM
 P ggplot2 * 4.0.3.9000 2026-10-30 [?] Github (tidyverse/ggplot2@b638d65)
 P plotly  * 4.12.1     2026-07-22 [?] RSPM

 [1] /home/runner/.cache/R/renv/library/Bizard-5140bdc6/linux-ubuntu-noble/R-4.6/x86_64-pc-linux-gnu
 [2] /home/runner/work/_temp/Library
 [3] /opt/R/4.6.2/lib/R/library

 * ── Packages attached to the search path.
 P ── Loaded and on-disk path mismatch.

──────────────────────────────────────────────────────────────────────────────

Data Preparation

We simulate an oncology cohort of 270 patients across three cancer types, two treatment lines, and three response categories (PR = partial response, SD = stable disease, PD = progressive disease). The node table needs exactly four columns: id (unique path), label, parent (empty for roots), and value (the leaf count; with branchvalues = "total" every parent’s value must equal the sum of its children).

set.seed(251)

coh <- data.frame(
  cancer = rep(c("Breast", "Lung (NSCLC)", "Colorectal"), each = 90),
  line   = rep(c("1st line", "2nd line+"), each = 45, times = 3),
  response = c(rep(c("PR", "SD", "PD"), times = c(28, 12, 5)),
               rep(c("PR", "SD", "PD"), times = c(15, 18, 12)),
               rep(c("PR", "SD", "PD"), times = c(30, 10, 5)),
               rep(c("PR", "SD", "PD"), times = c(10, 20, 15)),
               rep(c("PR", "SD", "PD"), times = c(22, 15, 8)),
               rep(c("PR", "SD", "PD"), times = c(8, 22, 15)))
)
cnt <- coh |> count(cancer, line, response, name = "n")

nodes <- bind_rows(
  # roots: one per cancer type
  data.frame(id = unique(cnt$cancer), label = unique(cnt$cancer), parent = "",
             value = sapply(unique(cnt$cancer), function(x) sum(cnt$n[cnt$cancer == x]))),
  # level 2: treatment lines
  cnt |>
    distinct(cancer, line) |>
    mutate(id = paste(cancer, line), label = line, parent = cancer,
           value = sapply(paste(cancer, line), function(x) sum(cnt$n[paste(cnt$cancer, cnt$line) == x]))) |>
    select(id, label, parent, value),
  # level 3: responses
  cnt |>
    mutate(id = paste(cancer, line, response), label = response,
           parent = paste(cancer, line), value = n) |>
    select(id, label, parent, value)
)

# parent values must equal the sum of their children (branchvalues = "total")
stopifnot(all(sapply(unique(nodes$parent[nodes$parent != ""]),
                     function(p) sum(nodes$value[nodes$parent == p]) ==
                       nodes$value[nodes$id == p])))

Visualization

1. Basic Cohort Sunburst

fig1 <- plot_ly(
  type = "sunburst",
  ids = nodes$id, labels = nodes$label,
  parents = nodes$parent, values = nodes$value,
  branchvalues = "total",
  hovertemplate = "%{id}<br>n = %{value}<extra></extra>"
) |>
  layout(title = "Oncology cohort: type → line → response")
fig1
Figure 1: Three-level sunburst of the oncology cohort: inner ring = cancer type, middle = treatment line, outer = response. Hover shows counts; click a segment to zoom into its subtree.

2. Color-Coded Sunburst

cols <- setNames(c("#4E79A7", "#F28E2B", "#E15759"), unique(cnt$cancer))

resp_rate <- cnt |>
  group_by(cancer, line) |>
  mutate(rate = scales::percent(n / sum(n), accuracy = 1)) |>
  ungroup()
lbls <- resp_rate |>
  mutate(id = paste(cancer, line, response),
         label = paste0(response, " · ", rate))

fig2 <- plot_ly(
  type = "sunburst",
  ids = nodes$id, labels = lbls$label[match(nodes$id, lbls$id)],
  parents = nodes$parent, values = nodes$value,
  branchvalues = "total",
  marker = list(colors = ifelse(nodes$parent == "", "#8C8C8C",
                                cols[substr(nodes$parent, 1, nchar(nodes$parent))])),
  textinfo = "label",
  insidetextorientation = "radial",
  hovertemplate = "%{id}<br>n = %{value}<extra></extra>"
) |>
  layout(title = "Response rates within each treatment line")
fig2
Figure 2: The same hierarchy with per-branch colors inherited down the tree; response segments carry hover text with the response rate within their parent branch.
TipPractice tips
  • With branchvalues = "total", parent values must equal the sum of their children — validate the node table programmatically (as in Data Preparation) or plotly draws overlapping wedges. Alternatively use branchvalues = "remainder" and give parents only their own value.
  • Keep ids unique full paths (paste(parent, child)) — duplicated ids silently merge subtrees.
  • Sunbursts read badly beyond ~3 levels and ~30 leaves; collapse rare categories into an “Other” segment first.
  • Static export for print figures needs a screenshot of the widget — the webshot2 package or a browser screenshot; hover interactivity is the web version’s advantage.

Application

Sunbursts are used for cohort composition (registry papers), cost decomposition (diagnosis → procedure), and taxonomic abundance (metagenomics: phylum → genus → species). The panel below mirrors a typical publication layout: (A) the full hierarchy and (B) the response-rate variant.

Sunburst Application

Reference

[1] Data-to-Viz. Sunburst. https://www.data-to-viz.com/graph/sunburst.html

[2] Sievert C. Interactive Web-Based Data Visualization with R, plotly, and shiny. Chapman and Hall/CRC; 2020. doi:10.1201/9780429447273