Dendrogram¶
The "dendrogram" transform turns a precomputed clustering tree into
parent–child edges for a link mark. It preserves merge heights
and computes node positions along an index or categorical band scale.
{
"description": [
"Unbalanced dendrogram",
"A precomputed tree rendered with one orthogonal link mark. Pan and zoom vertically to explore the discrete leaf positions."
],
"data": {
"values": [
{ "id": "A", "parent": "AB", "height": 0, "row": 0 },
{ "id": "B", "parent": "AB", "height": 0, "row": 1 },
{ "id": "C", "parent": "ABC", "height": 0, "row": 2 },
{ "id": "D", "parent": "root", "height": 0, "row": 3 },
{ "id": "AB", "parent": "ABC", "height": 0.8 },
{ "id": "ABC", "parent": "root", "height": 1.6 },
{ "id": "root", "parent": null, "height": 3 }
]
},
"transform": [
{
"type": "dendrogram",
"key": "id",
"parentKey": "parent",
"height": "height",
"leafIndex": "row"
}
],
"scales": {
"y": {
"type": "index",
"domain": [0, 3],
"paddingInner": 0.08,
"paddingOuter": 0.15
}
},
"mark": {
"type": "link",
"linkShape": "orthogonal",
"orient": "horizontal",
"clip": "y",
"size": 1.5
},
"encoding": {
"x": {
"field": "sourceHeight",
"type": "quantitative",
"scale": { "domain": [0, 3], "reverse": true, "zoom": false },
"axis": { "title": "Merge height" }
},
"x2": { "field": "targetHeight" },
"y": {
"field": "sourceIndex",
"type": "index",
"band": 0.5,
"axis": { "orient": "right", "title": "Leaf index" }
},
"y2": { "field": "targetIndex", "band": 0.5 },
"yOffset": {
"field": "sourceOffset",
"type": "quantitative",
"scale": {
"domain": [0, 1],
"range": [0, { "expr": "-height * bandstep('y', height)" }]
}
},
"y2Offset": { "field": "targetOffset" },
"color": { "value": "#34495e" }
}
}
This index-scale example uses an offset scale to keep branches aligned with rows while zooming.
Choosing a scale¶
| Index scale | Band scale | |
|---|---|---|
| Leaf values | Integer positions, typically dense: 0, 1, 2, … |
Category IDs, such as "Sample A" or "TP53" |
| Order | Determined by the numeric indices | Determined by the order of categories in the domain |
| Sample/gene labels | Require separate metadata or a mapping from the indices | Can use the category values directly |
| Navigation | Zooming and panning | Currently unsupported |
| Interval brushing | Supported | Currently unsupported |
In encodings, "nominal" and "ordinal" describe categorical data; "band" is the
positional scale type used to place those categories.
Input tree¶
Each input row describes a node. The root's parent is null; every other parent
identifies another node. For an index scale, leaves have explicit integer
positions along the scale. Internal nodes need no row position.
[
{ "id": "A", "parent": "AB", "height": 0, "row": 0 },
{ "id": "B", "parent": "AB", "height": 0, "row": 1 },
{ "id": "C", "parent": "root", "height": 0, "row": 2 },
{ "id": "AB", "parent": "root", "height": 1 },
{ "id": "root", "parent": null, "height": 3 }
]
{
"type": "dendrogram",
"key": "id",
"parentKey": "parent",
"height": "height",
"leafIndex": "row"
}
Each subtree's leaves must form a contiguous run in the ordered leaf sequence; indices may have gaps. Input row order does not determine leaf order.
With faceted data, each facet is processed as a separate tree. Otherwise, each input file is processed as a separate tree.
For a data-preparation workflow, see the clustered heatmap example.
Categorical band scales¶
To generate a category order from the tree, use leaf for sample or gene IDs
and siblingOrder for the order of children under each parent.
{
"type": "dendrogram",
"key": "id",
"parentKey": "parent",
"height": "height",
"leaf": "sample",
"siblingOrder": "childOrder",
"domainParam": { "name": "sampleOrder", "push": "outer" }
}
The transform can publish its ordered band domain with domainParam.
A string declares a local output. To share the domain with a sibling heatmap,
declare { "name": "sampleOrder" } in their common ancestor's
params, then push as above. Set the shared band scale's
domain to { "expr": "sampleOrder" }. Each axis can have its own tree and
output; no numeric heatmap positions or separate domain arrays are needed.
The example below shows imaginary z scores for four samples and four genes, with a precomputed tree on each axis.
{
"description": [
"Categorical Clustered Heatmap",
"Two supplied trees publish sample and gene orders to shared band scales. The z scores and trees are imaginary."
],
"name": "clusteredHeatmap",
"params": [{ "name": "sampleOrder" }, { "name": "geneOrder" }],
"resolve": {
"scale": { "x": "excluded", "y": "excluded" },
"legend": { "color": "collected" }
},
"scales": {
"x": {
"type": "band",
"domain": { "expr": "geneOrder" },
"paddingInner": 0.08,
"paddingOuter": 0.12
},
"y": {
"type": "band",
"domain": { "expr": "sampleOrder" },
"paddingInner": 0.08,
"paddingOuter": 0.12,
"reverse": true
}
},
"spacing": 10,
"spec": {
"name": "matrix",
"data": {
"values": [
{ "sample": "Sample C", "gene": "TP53", "z": 1.6 },
{ "sample": "Sample C", "gene": "MYC", "z": 1.1 },
{ "sample": "Sample C", "gene": "EGFR", "z": -0.9 },
{ "sample": "Sample C", "gene": "BRCA1", "z": -1.4 },
{ "sample": "Sample A", "gene": "TP53", "z": 1.2 },
{ "sample": "Sample A", "gene": "MYC", "z": 0.8 },
{ "sample": "Sample A", "gene": "EGFR", "z": -1.1 },
{ "sample": "Sample A", "gene": "BRCA1", "z": -0.8 },
{ "sample": "Sample D", "gene": "TP53", "z": -0.4 },
{ "sample": "Sample D", "gene": "MYC", "z": -0.7 },
{ "sample": "Sample D", "gene": "EGFR", "z": 1.3 },
{ "sample": "Sample D", "gene": "BRCA1", "z": 0.9 },
{ "sample": "Sample B", "gene": "TP53", "z": -1.5 },
{ "sample": "Sample B", "gene": "MYC", "z": -1.2 },
{ "sample": "Sample B", "gene": "EGFR", "z": 0.8 },
{ "sample": "Sample B", "gene": "BRCA1", "z": 1.5 }
]
},
"mark": "rect",
"encoding": {
"x": {
"field": "gene",
"type": "nominal",
"axis": { "title": "Gene", "labelAngle": 0 }
},
"y": {
"field": "sample",
"type": "nominal",
"axis": { "title": "Sample", "orient": "right" }
},
"color": {
"field": "z",
"type": "quantitative",
"scale": { "domain": [-2, 2], "scheme": "blueorange" },
"legend": {
"title": "Imaginary z score",
"orient": "right"
}
}
}
},
"surround": {
"top": [
{
"name": "geneTree",
"height": 90,
"data": {
"values": [
{
"id": "gEGFR",
"parent": "gEB",
"height": 0,
"gene": "EGFR",
"childOrder": 0
},
{ "id": "geneRoot", "parent": null, "height": 2.2 },
{
"id": "gMYC",
"parent": "gTM",
"height": 0,
"gene": "MYC",
"childOrder": 1
},
{
"id": "gEB",
"parent": "geneRoot",
"height": 0.9,
"childOrder": 1
},
{
"id": "gBRCA1",
"parent": "gEB",
"height": 0,
"gene": "BRCA1",
"childOrder": 1
},
{
"id": "gTM",
"parent": "geneRoot",
"height": 0.7,
"childOrder": 0
},
{
"id": "gTP53",
"parent": "gTM",
"height": 0,
"gene": "TP53",
"childOrder": 0
}
]
},
"transform": [
{
"type": "dendrogram",
"key": "id",
"parentKey": "parent",
"height": "height",
"leaf": "gene",
"siblingOrder": "childOrder",
"domainParam": { "name": "geneOrder", "push": "outer" }
}
],
"mark": {
"type": "link",
"linkShape": "orthogonal",
"orient": "vertical",
"size": 1.3,
"minPickingSize": 6,
"color": "#334155"
},
"encoding": {
"x": {
"field": "sourceValue",
"type": "nominal",
"band": 0.5,
"axis": null
},
"x2": { "field": "targetValue", "band": 0.5 },
"xOffset": {
"field": "sourceOffset",
"type": "quantitative",
"scale": {
"domain": [0, 1],
"range": [
0,
{
"expr": "width * bandstep('x', width)"
}
]
}
},
"x2Offset": { "field": "targetOffset" },
"y": {
"field": "sourceHeight",
"type": "quantitative",
"scale": {
"domain": [0, 2.4],
"reverse": false,
"zoom": false,
"nice": false
},
"axis": null
},
"y2": { "field": "targetHeight" }
}
}
],
"left": [
{
"name": "sampleTree",
"width": 90,
"data": {
"values": [
{
"id": "sD",
"parent": "sABC",
"height": 0,
"sample": "Sample D",
"childOrder": 1
},
{
"id": "sampleRoot",
"parent": null,
"height": 2.6
},
{
"id": "sA",
"parent": "sCA",
"height": 0,
"sample": "Sample A",
"childOrder": 1
},
{
"id": "sCA",
"parent": "sABC",
"height": 0.6,
"childOrder": 0
},
{
"id": "sB",
"parent": "sampleRoot",
"height": 0,
"sample": "Sample B",
"childOrder": 1
},
{
"id": "sC",
"parent": "sCA",
"height": 0,
"sample": "Sample C",
"childOrder": 0
},
{
"id": "sABC",
"parent": "sampleRoot",
"height": 1.4,
"childOrder": 0
}
]
},
"transform": [
{
"type": "dendrogram",
"key": "id",
"parentKey": "parent",
"height": "height",
"leaf": "sample",
"siblingOrder": "childOrder",
"domainParam": { "name": "sampleOrder", "push": "outer" }
}
],
"mark": {
"type": "link",
"linkShape": "orthogonal",
"orient": "horizontal",
"size": 1.3,
"minPickingSize": 6,
"color": "#334155"
},
"encoding": {
"y": {
"field": "sourceValue",
"type": "nominal",
"band": 0.5,
"axis": null
},
"y2": { "field": "targetValue", "band": 0.5 },
"yOffset": {
"field": "sourceOffset",
"type": "quantitative",
"scale": {
"domain": [0, 1],
"range": [
0,
{
"expr": "-height * bandstep('y', height)"
}
]
}
},
"y2Offset": { "field": "targetOffset" },
"x": {
"field": "sourceHeight",
"type": "quantitative",
"scale": {
"domain": [0, 2.8],
"reverse": true,
"zoom": false,
"nice": false
},
"axis": null
},
"x2": { "field": "targetHeight" }
}
}
]
}
}
To align the tree with an existing band domain, use leafOrder instead of
siblingOrder. Each subtree must remain contiguous among the tree's leaves,
even if the domain includes extra categories that reserve band slots.
Publishing a domain with domainParam requires a single tree and does not
support facets. See output parameters for scope and
lifecycle rules.
Output and placement¶
One output row represents each non-root node's incoming edge. Child metadata,
such as labels or colors, is copied to that row. Generated fields overwrite
same-named metadata. The optional as array changes the output field names.
| Fields | Meaning |
|---|---|
sourceId, targetId |
Parent and child node IDs |
sourceHeight, targetHeight |
Their supplied merge heights |
sourceIndex, targetIndex |
Integer row anchors |
sourceValue, targetValue |
Category anchors in categorical mode |
sourceOffset, targetOffset |
Relative displacements from the row anchors |
For the input tree above, the generated fields are shown below. Each column is one output row; copied child metadata is omitted.
| Field | AB → A |
AB → B |
root → C |
root → AB |
|---|---|---|---|---|
sourceId |
"AB" |
"AB" |
"root" |
"root" |
targetId |
"A" |
"B" |
"C" |
"AB" |
sourceHeight |
1 | 1 | 3 | 3 |
targetHeight |
0 | 0 | 0 | 1 |
sourceIndex |
0 | 0 | 1 | 1 |
targetIndex |
0 | 1 | 2 | 0 |
sourceOffset |
0.5 | 0.5 | 0.25 | 0.25 |
targetOffset |
0 | 0 | 0 | 0.5 |
Internal positions are midpoints of the outermost child positions, split into
an anchor and a fractional offset. Band scales use sourceValue and targetValue
category anchors instead of sourceIndex and targetIndex.
For a horizontal dendrogram, encode heights on x/x2, anchors on y/y2
with band: 0.5, and fractions on yOffset/y2Offset. Give the primary offset
scale an explicit domain [0, 1] and range
[0, { "expr": "-height * bandstep('y', height)" }]. Both offset channels then use
the full padded row step. See offset channels
and the bandstep() helper.
Empty input or a tree with a single node produces no edges.
Parameters¶
Common options¶
keyRequired-
Type: string (field name)
Unique string or finite numeric node identifier.
parentKeyRequired-
Type: string (field name)
Parent node identifier. Exactly one root must contain null.
heightRequired-
Type: string (field name)
Finite, nonnegative merge height, including leaf heights.
as-
Type: array
Output field names for source/target IDs, heights, anchors, and fractional offsets. Generated values overwrite copied child metadata. Names must be distinct and cannot use the reserved internal __uniqueId. Categorical mode defaults to sourceValue/targetValue for its anchor fields.
Default value: ["sourceId", "targetId", "sourceHeight", "targetHeight", "sourceIndex", "targetIndex", "sourceOffset", "targetOffset"]
description-
Type: string
A description of the transform step. Can be used for documentation and agent context.
Index scales¶
leafIndexRequired-
Type: string (field name)
Integer row position for each leaf, matching the heatmap order. Indices must be distinct and in [0, 2^44); each subtree's leaves must be contiguous in the ordered leaf sequence. Internal nodes need no value in this field.
Band scales¶
leafRequired-
Type: string (field name)
Leaf category field. Values must be distinct strings, finite numbers, or booleans.
domainParam-
Type: string | object
Publishes the ordered band domain as a read-only array parameter.
A string such as
"sampleOrder"creates the output in this view. Use{ "name": "sampleOrder", "push": "outer" }to publish into the nearest ancestor that declares{ "name": "sampleOrder" }in itsparams.
Choose exactly one ordering option:
Order from the tree¶
siblingOrderRequired-
Type: string (field name)
Finite numeric order on each non-root node, unique among its siblings.