Skip to content

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

key Required

Type: string (field name)

Unique string or finite numeric node identifier.

parentKey Required

Type: string (field name)

Parent node identifier. Exactly one root must contain null.

height Required

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

leafIndex Required

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

leaf Required

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 its params.

Choose exactly one ordering option:

Order from the tree

siblingOrder Required

Type: string (field name)

Finite numeric order on each non-root node, unique among its siblings.

Explicit leaf order

leafOrder Required

Type: Scalar[] | ExprRef

Complete ordered band domain. Include every leaf category exactly once; extra categories reserve slots.