Skip to main content

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.

Use it for category taxonomies (subcategory under category under department), org structures, or any nested grouping you want to see as a branching shape rather than nested rings.

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"}
]
}
],
"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"}
}
}
}
]
};;
}

Open Markdown
Let us know what you think about this document :)