Sunburst Chart
A sunburst chart shows hierarchical composition as concentric rings: the inner ring breaks the total into categories, and the outer ring splits each category into its subcategories. Use it to answer "what makes up the total, and what makes up each part?" in one view, such as revenue by product line and product, or tickets by team and ticket type.
Two-level hierarchy
CustomChartDef sunburst_chart {
label: 'Sunburst Chart'
description: "A sunburst chart shows hierarchical composition as concentric rings, with each level of the hierarchy radiating outward."
fields {
field category {
type: 'dimension'
label: 'Category (inner ring)'
}
field subcategory {
type: 'dimension'
label: 'Subcategory (outer ring)'
}
field value {
type: 'measure'
label: 'Value'
}
}
options {
option color_scheme {
type: 'select'
label: 'Color scheme'
options: ['tableau10', 'category10', 'accent', 'dark2', 'paired', 'set2']
default_value: 'tableau10'
}
option donut_hole {
type: 'select'
label: 'Center hole size'
options: [0, 0.2, 0.35, 0.5]
default_value: 0.35
}
}
template: @vg {
"$schema": "https://vega.github.io/schema/vega/v5.json",
"description": "Sunburst chart of a two-level hierarchy, built from stacked angles.",
"autosize": "none",
"padding": 0,
"signals": [
{
"name": "width",
"init": "containerSize()[0] - 5",
"on": [{ "events": "window:resize", "update": "containerSize()[0] - 5" }]
},
{
"name": "height",
"init": "containerSize()[1] - 5",
"on": [{ "events": "window:resize", "update": "containerSize()[1] - 5" }]
},
{"name": "radius", "update": "min(width, height) / 2"},
{"name": "holeR", "update": "radius * @{options.donut_hole.value}"},
{"name": "ringSplit", "update": "holeR + (radius - holeR) * 0.55"},
{"name": "grandTotal", "update": "length(data('cats')) ? data('cats')[0].total : 0"},
{
"name": "hovered",
"value": null,
"on": [
{
"events": "@catArc:mouseover",
"update": "{kind: 'category', category: datum.category, label: datum.category, amount: datum.amount, share: datum.amount / datum.total}"
},
{
"events": "@leafArc:mouseover",
"update": "{kind: 'subcategory', category: datum.category, label: datum.subcategory, amount: datum.amount, share: datum.amount / datum.total}"
},
{"events": "@catArc:mouseout, @leafArc:mouseout", "update": "null"}
]
}
],
"data": [
{
"name": "leaves",
"values": @{values},
"transform": [
{"type": "formula", "expr": "datum['@{fields.category.name}']", "as": "category"},
{"type": "formula", "expr": "datum['@{fields.subcategory.name}']", "as": "subcategory"},
{"type": "formula", "expr": "datum['@{fields.value.name}']", "as": "amount"},
{"type": "filter", "expr": "datum.amount != null && datum.amount > 0"},
{
"type": "aggregate",
"groupby": ["category", "subcategory"],
"fields": ["amount"],
"ops": ["sum"],
"as": ["amount"]
},
{"type": "collect", "sort": {"field": ["category", "subcategory"]}},
{
"type": "stack",
"field": "amount",
"as": ["s0", "s1"]
},
{
"type": "joinaggregate",
"fields": ["amount"],
"ops": ["sum"],
"as": ["total"]
},
{
"type": "joinaggregate",
"fields": ["amount"],
"ops": ["sum"],
"as": ["cat_total"],
"groupby": ["category"]
},
{"type": "formula", "expr": "datum.s0 / datum.total * 2 * PI", "as": "a0"},
{"type": "formula", "expr": "datum.s1 / datum.total * 2 * PI", "as": "a1"}
]
},
{
"name": "cats",
"source": "leaves",
"transform": [
{
"type": "aggregate",
"groupby": ["category"],
"fields": ["amount"],
"ops": ["sum"],
"as": ["amount"]
},
{"type": "collect", "sort": {"field": "category"}},
{
"type": "stack",
"field": "amount",
"as": ["s0", "s1"]
},
{
"type": "joinaggregate",
"fields": ["amount"],
"ops": ["sum"],
"as": ["total"]
},
{"type": "formula", "expr": "datum.s0 / datum.total * 2 * PI", "as": "a0"},
{"type": "formula", "expr": "datum.s1 / datum.total * 2 * PI", "as": "a1"}
]
}
],
"scales": [
{
"name": "color",
"type": "ordinal",
"domain": {"data": "cats", "field": "category"},
"range": {"scheme": @{options.color_scheme.value}}
}
],
"marks": [
{
"type": "arc",
"name": "catArc",
"from": {"data": "cats"},
"encode": {
"update": {
"x": {"signal": "width / 2"},
"y": {"signal": "height / 2"},
"startAngle": {"field": "a0"},
"endAngle": {"field": "a1"},
"innerRadius": {"signal": "holeR"},
"outerRadius": {"signal": "ringSplit"},
"fill": {"scale": "color", "field": "category"},
"fillOpacity": {
"signal": "hovered === null || hovered.category === datum.category ? 1 : 0.3"
},
"stroke": {"value": "white"},
"strokeWidth": {"value": 1.5},
"tooltip": {
"signal": "{'Category': datum.category, 'Value': format(datum.amount, ','), 'Share of Total': format(datum.amount / datum.total, '.1%')}"
}
}
}
},
{
"type": "arc",
"name": "leafArc",
"from": {"data": "leaves"},
"encode": {
"update": {
"x": {"signal": "width / 2"},
"y": {"signal": "height / 2"},
"startAngle": {"field": "a0"},
"endAngle": {"field": "a1"},
"innerRadius": {"signal": "ringSplit + 1"},
"outerRadius": {"signal": "radius"},
"fill": {"scale": "color", "field": "category"},
"fillOpacity": {
"signal": "hovered === null ? 0.7 : (hovered.category !== datum.category ? 0.2 : (hovered.kind === 'subcategory' && hovered.label === datum.subcategory ? 1 : 0.75))"
},
"stroke": {"value": "white"},
"strokeWidth": {"value": 1},
"tooltip": {
"signal": "{'Subcategory': datum.subcategory, 'Category': datum.category, 'Value': format(datum.amount, ','), 'Share of Total': format(datum.amount / datum.total, '.1%'), 'Share of Category': format(datum.amount / datum.cat_total, '.1%')}"
}
}
}
},
{
"type": "text",
"interactive": false,
"encode": {
"update": {
"x": {"signal": "width / 2"},
"y": {"signal": "height / 2 - 14"},
"align": {"value": "center"},
"text": {"signal": "hovered === null ? 'Total' : hovered.label"},
"limit": {"signal": "holeR * 1.7"},
"fontSize": {"value": 13},
"fontWeight": {"value": 600},
"fill": {"value": "#374151"},
"opacity": {"signal": "holeR > 30 ? 1 : 0"}
}
}
},
{
"type": "text",
"interactive": false,
"encode": {
"update": {
"x": {"signal": "width / 2"},
"y": {"signal": "height / 2 + 8"},
"align": {"value": "center"},
"text": {"signal": "format(hovered === null ? grandTotal : hovered.amount, ',')"},
"fontSize": {"value": 16},
"fontWeight": {"value": 700},
"fill": {"value": "#111827"},
"opacity": {"signal": "holeR > 30 ? 1 : 0"}
}
}
},
{
"type": "text",
"interactive": false,
"encode": {
"update": {
"x": {"signal": "width / 2"},
"y": {"signal": "height / 2 + 28"},
"align": {"value": "center"},
"text": {"signal": "hovered === null ? '' : format(hovered.share, '.1%') + ' of total'"},
"fontSize": {"value": 11},
"fill": {"value": "#6b7280"},
"opacity": {"signal": "holeR > 30 ? 1 : 0"}
}
}
}
]
};;
}
Three-level hierarchy
The same chart with one more ring: category, subcategory, then item (for example, product line, product, then variant). Works best when the outer ring has a manageable number of slices; beyond a few dozen items the tooltips remain usable but the slices get thin.
CustomChartDef sunburst_chart_three_level {
label: 'Sunburst Chart (3 levels)'
description: "A sunburst chart with three rings showing a category, subcategory, and item hierarchy as nested composition."
fields {
field category {
type: 'dimension'
label: 'Category (inner ring)'
}
field subcategory {
type: 'dimension'
label: 'Subcategory (middle ring)'
}
field item {
type: 'dimension'
label: 'Item (outer ring)'
}
field value {
type: 'measure'
label: 'Value'
}
}
options {
option color_scheme {
type: 'select'
label: 'Color scheme'
options: ['tableau10', 'category10', 'accent', 'dark2', 'paired', 'set2']
default_value: 'tableau10'
}
option donut_hole {
type: 'select'
label: 'Center hole size'
options: [0, 0.2, 0.35, 0.5]
default_value: 0.35
}
}
template: @vg {
"$schema": "https://vega.github.io/schema/vega/v5.json",
"description": "Three-ring sunburst built from stacked angles.",
"autosize": "none",
"padding": 0,
"signals": [
{
"name": "width",
"init": "containerSize()[0] - 5",
"on": [{ "events": "window:resize", "update": "containerSize()[0] - 5" }]
},
{
"name": "height",
"init": "containerSize()[1] - 5",
"on": [{ "events": "window:resize", "update": "containerSize()[1] - 5" }]
},
{"name": "radius", "update": "min(width, height) / 2"},
{"name": "holeR", "update": "radius * @{options.donut_hole.value}"},
{"name": "ring1End", "update": "holeR + (radius - holeR) * 0.38"},
{"name": "ring2End", "update": "holeR + (radius - holeR) * 0.7"},
{"name": "grandTotal", "update": "length(data('cats')) ? data('cats')[0].total : 0"},
{
"name": "hovered",
"value": null,
"on": [
{
"events": "@catArc:mouseover",
"update": "{kind: 'category', category: datum.category, subcategory: null, label: datum.category, amount: datum.amount, share: datum.amount / datum.total}"
},
{
"events": "@subArc:mouseover",
"update": "{kind: 'subcategory', category: datum.category, subcategory: datum.subcategory, label: datum.subcategory, amount: datum.amount, share: datum.amount / datum.total}"
},
{
"events": "@leafArc:mouseover",
"update": "{kind: 'item', category: datum.category, subcategory: datum.subcategory, label: datum.item, amount: datum.amount, share: datum.amount / datum.total}"
},
{"events": "@catArc:mouseout, @subArc:mouseout, @leafArc:mouseout", "update": "null"}
]
}
],
"data": [
{
"name": "leaves",
"values": @{values},
"transform": [
{"type": "formula", "expr": "datum['@{fields.category.name}']", "as": "category"},
{"type": "formula", "expr": "datum['@{fields.subcategory.name}']", "as": "subcategory"},
{"type": "formula", "expr": "datum['@{fields.item.name}']", "as": "item"},
{"type": "formula", "expr": "datum['@{fields.value.name}']", "as": "amount"},
{"type": "filter", "expr": "datum.amount != null && datum.amount > 0"},
{
"type": "aggregate",
"groupby": ["category", "subcategory", "item"],
"fields": ["amount"],
"ops": ["sum"],
"as": ["amount"]
},
{"type": "collect", "sort": {"field": ["category", "subcategory", "item"]}},
{"type": "stack", "field": "amount", "as": ["s0", "s1"]},
{"type": "joinaggregate", "fields": ["amount"], "ops": ["sum"], "as": ["total"]},
{
"type": "joinaggregate",
"fields": ["amount"],
"ops": ["sum"],
"as": ["sub_total"],
"groupby": ["category", "subcategory"]
},
{"type": "formula", "expr": "datum.s0 / datum.total * 2 * PI", "as": "a0"},
{"type": "formula", "expr": "datum.s1 / datum.total * 2 * PI", "as": "a1"}
]
},
{
"name": "subs",
"source": "leaves",
"transform": [
{
"type": "aggregate",
"groupby": ["category", "subcategory"],
"fields": ["amount"],
"ops": ["sum"],
"as": ["amount"]
},
{"type": "collect", "sort": {"field": ["category", "subcategory"]}},
{"type": "stack", "field": "amount", "as": ["s0", "s1"]},
{"type": "joinaggregate", "fields": ["amount"], "ops": ["sum"], "as": ["total"]},
{
"type": "joinaggregate",
"fields": ["amount"],
"ops": ["sum"],
"as": ["cat_total"],
"groupby": ["category"]
},
{"type": "formula", "expr": "datum.s0 / datum.total * 2 * PI", "as": "a0"},
{"type": "formula", "expr": "datum.s1 / datum.total * 2 * PI", "as": "a1"}
]
},
{
"name": "cats",
"source": "leaves",
"transform": [
{
"type": "aggregate",
"groupby": ["category"],
"fields": ["amount"],
"ops": ["sum"],
"as": ["amount"]
},
{"type": "collect", "sort": {"field": "category"}},
{"type": "stack", "field": "amount", "as": ["s0", "s1"]},
{"type": "joinaggregate", "fields": ["amount"], "ops": ["sum"], "as": ["total"]},
{"type": "formula", "expr": "datum.s0 / datum.total * 2 * PI", "as": "a0"},
{"type": "formula", "expr": "datum.s1 / datum.total * 2 * PI", "as": "a1"}
]
}
],
"scales": [
{
"name": "color",
"type": "ordinal",
"domain": {"data": "cats", "field": "category"},
"range": {"scheme": @{options.color_scheme.value}}
}
],
"marks": [
{
"type": "arc",
"name": "catArc",
"from": {"data": "cats"},
"encode": {
"update": {
"x": {"signal": "width / 2"},
"y": {"signal": "height / 2"},
"startAngle": {"field": "a0"},
"endAngle": {"field": "a1"},
"innerRadius": {"signal": "holeR"},
"outerRadius": {"signal": "ring1End"},
"fill": {"scale": "color", "field": "category"},
"fillOpacity": {
"signal": "hovered === null || hovered.category === datum.category ? 1 : 0.3"
},
"stroke": {"value": "white"},
"strokeWidth": {"value": 1.5},
"tooltip": {
"signal": "{'Category': datum.category, 'Value': format(datum.amount, ','), 'Share of Total': format(datum.amount / datum.total, '.1%')}"
}
}
}
},
{
"type": "arc",
"name": "subArc",
"from": {"data": "subs"},
"encode": {
"update": {
"x": {"signal": "width / 2"},
"y": {"signal": "height / 2"},
"startAngle": {"field": "a0"},
"endAngle": {"field": "a1"},
"innerRadius": {"signal": "ring1End + 1"},
"outerRadius": {"signal": "ring2End"},
"fill": {"scale": "color", "field": "category"},
"fillOpacity": {
"signal": "hovered === null ? 0.85 : (hovered.category !== datum.category ? 0.2 : (hovered.subcategory === null ? 0.9 : (hovered.subcategory === datum.subcategory ? 1 : 0.5)))"
},
"stroke": {"value": "white"},
"strokeWidth": {"value": 1.2},
"tooltip": {
"signal": "{'Subcategory': datum.subcategory, 'Category': datum.category, 'Value': format(datum.amount, ','), 'Share of Total': format(datum.amount / datum.total, '.1%'), 'Share of Category': format(datum.amount / datum.cat_total, '.1%')}"
}
}
}
},
{
"type": "arc",
"name": "leafArc",
"from": {"data": "leaves"},
"encode": {
"update": {
"x": {"signal": "width / 2"},
"y": {"signal": "height / 2"},
"startAngle": {"field": "a0"},
"endAngle": {"field": "a1"},
"innerRadius": {"signal": "ring2End + 1"},
"outerRadius": {"signal": "radius"},
"fill": {"scale": "color", "field": "category"},
"fillOpacity": {
"signal": "hovered === null ? 0.65 : (hovered.category !== datum.category ? 0.15 : (hovered.kind === 'item' ? (hovered.label === datum.item ? 1 : 0.45) : (hovered.kind === 'subcategory' ? (hovered.subcategory === datum.subcategory ? 0.95 : 0.4) : 0.8)))"
},
"stroke": {"value": "white"},
"strokeWidth": {"value": 1},
"tooltip": {
"signal": "{'Item': datum.item, 'Subcategory': datum.subcategory, 'Category': datum.category, 'Value': format(datum.amount, ','), 'Share of Total': format(datum.amount / datum.total, '.1%'), 'Share of Subcategory': format(datum.amount / datum.sub_total, '.1%')}"
}
}
}
},
{
"type": "text",
"interactive": false,
"encode": {
"update": {
"x": {"signal": "width / 2"},
"y": {"signal": "height / 2 - 14"},
"align": {"value": "center"},
"text": {"signal": "hovered === null ? 'Total' : hovered.label"},
"limit": {"signal": "holeR * 1.7"},
"fontSize": {"value": 13},
"fontWeight": {"value": 600},
"fill": {"value": "#374151"},
"opacity": {"signal": "holeR > 30 ? 1 : 0"}
}
}
},
{
"type": "text",
"interactive": false,
"encode": {
"update": {
"x": {"signal": "width / 2"},
"y": {"signal": "height / 2 + 8"},
"align": {"value": "center"},
"text": {"signal": "format(hovered === null ? grandTotal : hovered.amount, ',')"},
"fontSize": {"value": 16},
"fontWeight": {"value": 700},
"fill": {"value": "#111827"},
"opacity": {"signal": "holeR > 30 ? 1 : 0"}
}
}
},
{
"type": "text",
"interactive": false,
"encode": {
"update": {
"x": {"signal": "width / 2"},
"y": {"signal": "height / 2 + 28"},
"align": {"value": "center"},
"text": {"signal": "hovered === null ? '' : format(hovered.share, '.1%') + ' of total'"},
"fontSize": {"value": 11},
"fill": {"value": "#6b7280"},
"opacity": {"signal": "holeR > 30 ? 1 : 0"}
}
}
}
]
};;
}