Reorderable Matrix Example

Matrix diagrams visualize a network by treating nodes as rows and columns of a table; cells are colored in if an edge exists between two nodes. This example depicts character co-occurrences in Victor Hugo’s Les Misérables. The underlying data is an undirected graph, and so the matrix is symmetric around the diagonal. The matrix is also reorderable: grab a node label to rearrange rows and columns!

Vega JSON Specification <>

{
  "$schema": "https://vega.github.io/schema/vega/v5.json",
  "description": "A re-orderable adjacency matrix depicting character co-occurrence in the novel Les Misérables.",
  "width": 770,
  "height": 770,
  "padding": 2,

  "signals": [
    { "name": "cellSize", "value": 10 },
    { "name": "count", "update": "length(data('nodes'))" },
    { "name": "width", "update": "span(range('position'))" },
    { "name": "height", "update": "width" },
    {
      "name": "src", "value": {},
      "on": [
        {"events": "text:pointerdown", "update": "datum"},
        {"events": "window:pointerup", "update": "{}"}
      ]
    },
    {
      "name": "dest", "value": -1,
      "on": [
        {
          "events": "[@columns:pointerdown, window:pointerup] > window:pointermove",
          "update": "src.name && datum !== src ? (0.5 + count * clamp(x(), 0, width) / width) : dest"
        },
        {
          "events": "[@rows:pointerdown, window:pointerup] > window:pointermove",
          "update": "src.name && datum !== src ? (0.5 + count * clamp(y(), 0, height) / height) : dest"
        },
        {"events": "window:pointerup", "update": "-1"}
      ]
    }
  ],

  "data": [
    {
      "name": "nodes",
      "url": "data/miserables.json",
      "format": {"type": "json", "property": "nodes"},
      "transform": [
        {
          "type": "formula", "as": "order",
          "expr": "datum.group"
        },
        {
          "type": "formula", "as": "score",
          "expr": "dest >= 0 && datum === src ? dest : datum.order"
        },
        {
          "type": "window", "sort": {"field": "score"},
          "ops": ["row_number"], "as": ["order"]
        }
      ]
    },
    {
      "name": "edges",
      "url": "data/miserables.json",
      "format": {"type": "json", "property": "links"},
      "transform": [
        {
          "type": "lookup", "from": "nodes", "key": "index",
          "fields": ["source", "target"], "as": ["sourceNode", "targetNode"]
        },
        {
          "type": "formula", "as": "group",
          "expr": "datum.sourceNode.group === datum.targetNode.group ? datum.sourceNode.group : count"
        }
      ]
    },
    {
      "name": "cross",
      "source": "nodes",
      "transform": [
        { "type": "cross" }
      ]
    }
  ],

  "scales": [
    {
      "name": "position",
      "type": "band",
      "domain": {"data": "nodes", "field": "order", "sort": true},
      "range": {"step": {"signal": "cellSize"}}
    },
    {
      "name": "color",
      "type": "ordinal",
      "range": "category",
      "domain": {
        "fields": [
          {"data": "nodes", "field": "group"},
          {"signal": "count"}
        ],
        "sort": true
      }
    }
  ],

  "marks": [
    {
      "type": "rect",
      "from": {"data": "cross"},
      "encode": {
        "update": {
          "x": {"scale": "position", "field": "a.order"},
          "y": {"scale": "position", "field": "b.order"},
          "width": {"scale": "position", "band": 1, "offset": -1},
          "height": {"scale": "position", "band": 1, "offset": -1},
          "fill": [
            {"test": "datum.a === src || datum.b === src", "value": "#ddd"},
            {"value": "#f5f5f5"}
          ]
        }
      }
    },
    {
      "type": "rect",
      "from": {"data": "edges"},
      "encode": {
        "update": {
          "x": {"scale": "position", "field": "sourceNode.order"},
          "y": {"scale": "position", "field": "targetNode.order"},
          "width": {"scale": "position", "band": 1, "offset": -1},
          "height": {"scale": "position", "band": 1, "offset": -1},
          "fill": {"scale": "color", "field": "group"}
        }
      }
    },
    {
      "type": "rect",
      "from": {"data": "edges"},
      "encode": {
        "update": {
          "x": {"scale": "position", "field": "targetNode.order"},
          "y": {"scale": "position", "field": "sourceNode.order"},
          "width": {"scale": "position", "band": 1, "offset": -1},
          "height": {"scale": "position", "band": 1, "offset": -1},
          "fill": {"scale": "color", "field": "group"}
        }
      }
    },
    {
      "type": "text",
      "name": "columns",
      "from": {"data": "nodes"},
      "encode": {
        "update": {
          "x": {"scale": "position", "field": "order", "band": 0.5},
          "y": {"offset": -2},
          "text": {"field": "name"},
          "fontSize": {"value": 10},
          "angle": {"value": -90},
          "align": {"value": "left"},
          "baseline": {"value": "middle"},
          "fill": [
            {"test": "datum === src", "value": "steelblue"},
            {"value": "black"}
          ]
        }
      }
    },
    {
      "type": "text",
      "name": "rows",
      "from": {"data": "nodes"},
      "encode": {
        "update": {
          "x": {"offset": -2},
          "y": {"scale": "position", "field": "order", "band": 0.5},
          "text": {"field": "name"},
          "fontSize": {"value": 10},
          "align": {"value": "right"},
          "baseline": {"value": "middle"},
          "fill": [
            {"test": "datum === src", "value": "steelblue"},
            {"value": "black"}
          ]
        }
      }
    }
  ]
}