Radial Tree
A radial tree draws hierarchical data as a node-link diagram fanning out from a central root, with depth shown by distance from the center.
- Good for: category taxonomies (subcategory under category under department), org structures, or any nested grouping shown as a branching shape across 1-3 levels.
- Not great for: part-to-whole sizing (use a Treemap or Sunburst Chart, which size nodes by value), flat (non-hierarchical) data, or deep trees with more than three levels.

Syntax
Use the following AML definition to add the Radial Tree to your custom chart library.
CustomChartDef radial_tree {
label: 'Radial Tree'
description: 'To draw a hierarchy of level columns as a radial node-link tree.'
fields {
field level_1 {
label: 'Level 1'
type: 'dimension'
sort {
apply_order: 1
direction: 'asc'
}
}
field level_2 {
label: 'Level 2'
type: 'dimension'
sort {
apply_order: 2
direction: 'asc'
}
}
field level_3 {
label: 'Level 3'
type: 'dimension'
sort {
apply_order: 3
direction: 'asc'
}
}
}
options {
option root_label {
label: 'Root label'
type: 'input'
default_value: 'All'
}
option layout {
label: 'Layout'
type: 'select'
options: ['tidy', 'cluster']
default_value: 'tidy'
}
option link_shape {
label: 'Link style'
type: 'select'
options: ['line', 'curve', 'diagonal', 'orthogonal']
default_value: 'curve'
}
option spread {
label: 'Angular spread (degrees)'
type: 'select'
options: [180, 270, 360]
default_value: 360
}
option show_labels {
label: 'Show labels'
type: 'toggle'
default_value: true
}
option color_scheme {
label: 'Color scheme (by depth)'
type: 'select'
options: ['blues', 'teals', 'greens', 'purples', 'viridis', 'magma']
default_value: 'blues'
}
}
template: @vg {
"$schema": "https://vega.github.io/schema/vega/v5.json",
"signals": [
{
"name": "width",
"init": "containerSize()[0] - 10",
"on": [{ "events": "window:resize", "update": "containerSize()[0] - 10" }]
},
{
"name": "height",
"init": "containerSize()[1] - 10",
"on": [{ "events": "window:resize", "update": "containerSize()[1] - 10" }]
},
{"name": "originX", "update": "width / 2"},
{"name": "originY", "update": "height / 2"},
{"name": "radius", "update": "max(20, min(width, height) / 2 - 70)"},
{"name": "extent", "update": "@{options.spread.value}"},
{"name": "rotate", "value": 0},
{"name": "labels", "update": "@{options.show_labels.value}"},
{"name": "layout", "update": "'@{options.layout.value}'"},
{"name": "links", "update": "'@{options.link_shape.value}'"},
{
"name": "hovered",
"value": null,
"on": [
{"events": "@nodes:mouseover", "update": "datum.node_id"},
{"events": "@nodes:mouseout", "update": "null"}
]
},
{
"name": "normalPointSelection",
"value": null,
"on": [
{"events": "@nodes:click", "update": "datum.depth === 1 ? {'@{fields.level_1.name}': [datum.label]} : datum.depth === 2 ? {'@{fields.level_2.name}': [datum.label]} : datum.depth === 3 ? {'@{fields.level_3.name}': [datum.label]} : null"},
{"events": "click[!event.item]", "update": "null"}
]
},
{
"name": "hoverPointSelection",
"value": null,
"on": [
{"events": "@nodes:mouseover", "update": "datum.depth === 1 ? {'@{fields.level_1.name}': [datum.label]} : datum.depth === 2 ? {'@{fields.level_2.name}': [datum.label]} : datum.depth === 3 ? {'@{fields.level_3.name}': [datum.label]} : null"},
{"events": "@nodes:mouseout", "update": "null"}
]
}
],
"holisticsConfig": {
"crossFilterSignals": ["normalPointSelection"],
"contextMenuSignals": ["hoverPointSelection"]
},
"data": [
{
"name": "tree",
"values": @{values},
"transform": [
{"type": "formula", "expr": "datum['@{fields.level_1.name}'] == null ? '' : '' + datum['@{fields.level_1.name}']", "as": "l1"},
{"type": "formula", "expr": "datum['@{fields.level_2.name}'] == null ? '' : '' + datum['@{fields.level_2.name}']", "as": "l2"},
{"type": "formula", "expr": "datum['@{fields.level_3.name}'] == null ? '' : '' + datum['@{fields.level_3.name}']", "as": "l3"},
{"type": "filter", "expr": "datum.l1 !== ''"},
{
"type": "formula",
"as": "node_arr",
"expr": "[{id: '__root__', parent: null, label: '@{options.root_label.value}'}, {id: '1§' + datum.l1, parent: '__root__', label: datum.l1}, (datum.l2 !== '' ? {id: '2§' + datum.l1 + '§' + datum.l2, parent: '1§' + datum.l1, label: datum.l2} : null), (datum.l2 !== '' && datum.l3 !== '' ? {id: '3§' + datum.l1 + '§' + datum.l2 + '§' + datum.l3, parent: '2§' + datum.l1 + '§' + datum.l2, label: datum.l3} : null)]"
},
{"type": "flatten", "fields": ["node_arr"], "as": ["node"]},
{"type": "filter", "expr": "datum.node != null"},
{"type": "formula", "expr": "datum.node.id", "as": "node_id"},
{"type": "formula", "expr": "datum.node.parent", "as": "parent_id"},
{"type": "formula", "expr": "datum.node.label", "as": "label"},
{"type": "aggregate", "groupby": ["node_id", "parent_id", "label"]},
{"type": "stratify", "key": "node_id", "parentKey": "parent_id"},
{
"type": "tree",
"method": {"signal": "layout"},
"size": [1, {"signal": "radius"}],
"as": ["alpha", "radius", "depth", "children"]
},
{"type": "formula", "expr": "(rotate + extent * datum.alpha + 270) % 360", "as": "angle"},
{"type": "formula", "expr": "PI * datum.angle / 180", "as": "radians"},
{"type": "formula", "expr": "inrange(datum.angle, [90, 270])", "as": "leftside"},
{"type": "formula", "expr": "originX + datum.radius * cos(datum.radians)", "as": "x"},
{"type": "formula", "expr": "originY + datum.radius * sin(datum.radians)", "as": "y"}
]
},
{
"name": "links",
"source": "tree",
"transform": [
{"type": "treelinks"},
{
"type": "linkpath",
"shape": {"signal": "links"},
"orient": "radial",
"sourceX": "source.radians",
"sourceY": "source.radius",
"targetX": "target.radians",
"targetY": "target.radius"
}
]
}
],
"scales": [
{
"name": "color",
"type": "linear",
"range": {"scheme": @{options.color_scheme.value}},
"domain": {"data": "tree", "field": "depth"},
"zero": true
}
],
"marks": [
{
"type": "path",
"from": {"data": "links"},
"encode": {
"update": {
"x": {"signal": "originX"},
"y": {"signal": "originY"},
"path": {"field": "path"},
"stroke": {"value": "#d0d5dd"},
"strokeWidth": {"value": 1}
}
}
},
{
"name": "nodes",
"type": "symbol",
"from": {"data": "tree"},
"encode": {
"enter": {
"size": {"value": 90},
"stroke": {"value": "white"},
"strokeWidth": {"value": 1}
},
"update": {
"x": {"field": "x"},
"y": {"field": "y"},
"fill": {"scale": "color", "field": "depth"},
"size": {"signal": "datum.node_id === hovered ? 200 : 90"},
"tooltip": {
"signal": "{'Label': datum.label, 'Depth': datum.depth, 'Children': length(datum.children || [])}"
}
}
}
},
{
"type": "text",
"from": {"data": "tree"},
"interactive": false,
"encode": {
"enter": {
"text": {"field": "label"},
"fontSize": {"value": 10},
"baseline": {"value": "middle"},
"fill": {"value": "#374151"}
},
"update": {
"x": {"field": "x"},
"y": {"field": "y"},
"dx": {"signal": "(datum.leftside ? -1 : 1) * 7"},
"angle": {"signal": "datum.leftside ? datum.angle - 180 : datum.angle"},
"align": {"signal": "datum.leftside ? 'right' : 'left'"},
"opacity": {"signal": "labels ? 1 : 0"}
}
}
}
]
};;
}
Required fields
A Radial Tree expects up to three fields, all dimensions. level_1 is required; level_2 and level_3 are optional, so a branch can stop early when a deeper level is null. Each row contributes one path from the root down through its non-null levels.
| Field | Label | Type | Role |
|---|---|---|---|
level_1 | Level 1 | dimension | First branch under the root (required). Sorted ascending (apply_order: 1). |
level_2 | Level 2 | dimension | Second branch under Level 1 (optional). Sorted ascending (apply_order: 2). |
level_3 | Level 3 | dimension | Outermost branch under Level 2 (optional). Sorted ascending (apply_order: 3). |
Data requirements: No measure is needed; the template builds the tree from the level columns alone, and node positions do not depend on any value. The template drops rows with a null or empty level_1 and collapses duplicate node paths, so you don't need to pre-aggregate. Each child should roll up to a single parent across the columns, otherwise the same label appears under multiple branches.
Sample data:
| level_1 | level_2 | level_3 |
|---|---|---|
| Electronics | Phones | iPhone |
| Electronics | Phones | Android |
| Electronics | Laptops | MacBook |
| Apparel | Shirts | T-shirt |
| Apparel | Shirts | Polo |
| Services |
Options
Set these options to adjust the layout without editing the Vega template. The CustomChartDef block above declares each option's type and allowed values.
| Option | Default | Effect |
|---|---|---|
root_label | All | Text shown on the central root node that all Level 1 branches connect to. |
layout | tidy | Tree layout algorithm. tidy spaces nodes by structure; cluster aligns all leaf nodes at the same outer radius. |
link_shape | curve | Style of the connecting links between nodes. |
spread | 360 | Angular span the tree fans across, in degrees. 360 is a full circle; lower values open a wedge. |
show_labels | true | Toggles the text label next to each node. |
color_scheme | blues | Sequential color scheme used to shade nodes by their depth from the root. |
Known limitations
-
Three levels maximum. The template reads only
level_1throughlevel_3. Deeper hierarchies need extra level columns and matching node logic added to the template. -
Nodes are not sized by value. Every node is the same size and color encodes depth, not magnitude. Use a Treemap or Sunburst Chart when arc or area should reflect a measure.
-
Each child must roll up to one parent. A label that appears under more than one parent renders as separate nodes, which misrepresents the hierarchy. Make sure the level columns form a clean tree.