Trees and Dendrograms

How to customize charts of trees and graphs in W&B. Made by Stacey Svetlichnaya using Weights & Biases
Stacey Svetlichnaya

Circular dendrogram

Log a circular dendrogram natively to W&B (super quick Colab example).

Input format: at minimum, each node needs an id. Most nodes except the root should have parent ids. Names/labels are optional.

This example is taken directly from the Vega documentation, and this chart renders a code/variable type tree. See the end of this report for the full Vega spec.

Display configuration

Click on the gear icon in the top right to adjust the chart settings interactively.

Section 2

Full code

data = [[a["id"], a["name"], a["parent"], a["size"]] for a in raw_data]
table = wandb.Table(columns=["node_id", "node_name", "parent", "size"], data=data)
fields = {"node_name" : "node_name", "node_id" : "node_id", "parent" : "parent", "size" : "size"}
dendro = wandb.plot_table(vega_spec_name="stacey/dendrogram", data_table=table, fields=fields)
wandb.log({"custom_tree" : dendro})

where raw_data has the form:

raw_data = [
{
    "id": 126,
    "name": "Variable",
    "parent": 67,
    "size": 1124
  },
  {
    "id": 127,
    "name": "Variance",
    "parent": 67,
    "size": 1876
  },
  {
    "id": 128,
    "name": "Xor",
    "parent": 67,
    "size": 1101
  },
  {
    "id": 129,
    "name": "scale",
    "parent": 1
  }, ...
]

Tree (regular dendrogram)

Here the same sample data is rendered as a regular tree to show the space of possibilities (see the original in Vega). The only change to the plotting code above requires changing the vega_spec_name.

Section 3

Next steps

Custom chart features

Datasets/research to showcase

Useful references

Circular Dendrogram Full Vega Spec

{
  "$schema": "https://vega.github.io/schema/vega/v5.json",
  "description": "An example of a radial layout for a node-link diagram of hierarchical data.",
  "width": 720,
  "height": 720,
  "padding": 5,
  "autosize": "none",

  "signals": [
    {
      "name": "labels", "value": true,
      "bind": {"input": "checkbox"}
    },
    {
      "name": "radius", "value": 280,
      "bind": {"input": "range", "min": 20, "max": 600}
    },
    {
      "name": "extent", "value": 360,
      "bind": {"input": "range", "min": 0, "max": 360, "step": 1}
    },
    {
      "name": "rotate", "value": 0,
      "bind": {"input": "range", "min": 0, "max": 360, "step": 1}
    },
    {
      "name": "layout", "value": "tidy",
      "bind": {"input": "radio", "options": ["tidy", "cluster"]}
    },
    {
      "name": "links", "value": "line",
      "bind": {
        "input": "select",
        "options": ["line", "curve", "diagonal", "orthogonal"]
      }
    },
    { "name": "originX", "update": "width / 2" },
    { "name": "originY", "update": "height / 2" }
  ],

  "data": [
    {
      "name": "wandb",
      "transform": [
        {
          "type": "stratify",
          "key": "${field:node_id}",
          "parentKey": "${field:parent}"
        },
        {
          "type": "tree",
          "sort" : {"field": "${field:size}", "order": "descending"},                 
          "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": "wandb",
      "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": "magma"},
      "domain": {"data": "wandb", "field": "depth"},
      "zero": true
    }
  ],

  "marks": [
    {
      "type": "path",
      "from": {"data": "links"},
      "encode": {
        "update": {
          "x": {"signal": "originX"},
          "y": {"signal": "originY"},
          "path": {"field": "path"},
          "stroke": {"value": "#ccc"}
        }
      }
    },
    {
      "type": "symbol",
      "from": {"data": "wandb"},
      "encode": {
        "enter": {
          "size": {"value": 100},
          "stroke": {"value": "#fff"}
        },
        "update": {
          "x": {"field": "x"},
          "y": {"field": "y"},
          "fill": {"scale": "color", "field": "depth"}
        }
      }
    },
    {
      "type": "text",
      "from": {"data": "wandb"},
      "encode": {
        "enter": {
          "text": {"field": "${field:node_name}"},
          "fontSize": {"value": 9},
          "baseline": {"value": "middle"}
        },
        "update": {
          "x": {"field": "x"},
          "y": {"field": "y"},
          "dx": {"signal": "(datum.leftside ? -1 : 1) * 6"},
          "angle": {"signal": "datum.leftside ? datum.angle - 180 : datum.angle"},
          "align": {"signal": "datum.leftside ? 'right' : 'left'"},
          "opacity": {"signal": "labels ? 1 : 0"}
        }
      }
    }
  ]