Skip to main content

Overview

Hierarchical graphs (also called compound graphs or nested graphs) allow nodes to contain other nodes, creating a parent-child tree structure. This is useful for:
  • Statecharts with nested states
  • Organizational charts
  • File system hierarchies
  • Grouped diagrams

Parent-Child Relationships

Nodes can specify a parentId to create hierarchy:
import { createGraph } from '@statelyai/graph';

const graph = createGraph({
  nodes: [
    { id: 'parent' },
    { id: 'child1', parentId: 'parent' },
    { id: 'child2', parentId: 'parent' },
    { id: 'grandchild', parentId: 'child1' }
  ]
});

// Tree structure:
// parent
//   ├─ child1
//   │   └─ grandchild
//   └─ child2
The parentId field creates a hierarchy tree that is separate from the edge graph. Edges connect nodes at any level, while parentId defines containment.

Root Nodes

Nodes without a parentId (or with parentId: null) are root-level nodes:
import { getRoots } from '@statelyai/graph';

const graph = createGraph({
  nodes: [
    { id: 'root1' },
    { id: 'root2' },
    { id: 'child', parentId: 'root1' }
  ]
});

const roots = getRoots(graph);
// => [node root1, node root2]

Compound Nodes

A compound node (also called a group node) is a node that has children. Use isCompound() to check:
import { isCompound, isLeaf } from '@statelyai/graph';

const graph = createGraph({
  nodes: [
    { id: 'parent' },
    { id: 'child', parentId: 'parent' }
  ]
});

isCompound(graph, 'parent'); // true
isLeaf(graph, 'parent');     // false

isCompound(graph, 'child');  // false
isLeaf(graph, 'child');      // true

Querying Hierarchy

The library provides several functions to navigate the hierarchy tree:

Get Children

import { getChildren } from '@statelyai/graph';

const graph = createGraph({
  nodes: [
    { id: 'parent' },
    { id: 'child1', parentId: 'parent' },
    { id: 'child2', parentId: 'parent' }
  ]
});

// Get direct children
const children = getChildren(graph, 'parent');
// => [node child1, node child2]

// Get root-level nodes
const roots = getChildren(graph, null);
// => [node parent]

Get Parent

import { getParent } from '@statelyai/graph';

const parent = getParent(graph, 'child1');
// => node parent

const noParent = getParent(graph, 'parent');
// => undefined (root node)

Get Ancestors

Returns all ancestors from the node up to the root:
import { getAncestors } from '@statelyai/graph';

const graph = createGraph({
  nodes: [
    { id: 'root' },
    { id: 'mid', parentId: 'root' },
    { id: 'leaf', parentId: 'mid' }
  ]
});

const ancestors = getAncestors(graph, 'leaf');
// => [node mid, node root]
// Nearest parent first

Get Descendants

Returns all descendants recursively:
import { getDescendants } from '@statelyai/graph';

const graph = createGraph({
  nodes: [
    { id: 'root' },
    { id: 'child', parentId: 'root' },
    { id: 'grandchild', parentId: 'child' }
  ]
});

const descendants = getDescendants(graph, 'root');
// => [node child, node grandchild]
// Depth-first order

Get Siblings

Nodes with the same parentId:
import { getSiblings } from '@statelyai/graph';

const graph = createGraph({
  nodes: [
    { id: 'parent' },
    { id: 'a', parentId: 'parent' },
    { id: 'b', parentId: 'parent' },
    { id: 'c', parentId: 'parent' }
  ]
});

const siblings = getSiblings(graph, 'a');
// => [node b, node c]
// Excludes 'a' itself

Get Depth

Depth in the hierarchy tree (root = 0):
import { getDepth } from '@statelyai/graph';

const graph = createGraph({
  nodes: [
    { id: 'root' },
    { id: 'child', parentId: 'root' },
    { id: 'grandchild', parentId: 'child' }
  ]
});

getDepth(graph, 'root');       // => 0
getDepth(graph, 'child');      // => 1
getDepth(graph, 'grandchild'); // => 2
getDepth(graph, 'missing');    // => -1

Least Common Ancestor (LCA)

Find the deepest proper ancestor shared by multiple nodes:
import { getLCA } from '@statelyai/graph';

const graph = createGraph({
  nodes: [
    { id: 'root' },
    { id: 'a', parentId: 'root' },
    { id: 'b', parentId: 'root' },
    { id: 'a1', parentId: 'a' },
    { id: 'a2', parentId: 'a' }
  ]
});

getLCA(graph, 'a1', 'a2');
// => node a

getLCA(graph, 'a1', 'b');
// => node root

getLCA(graph, 'a', 'b');
// => node root
The LCA must be a proper ancestor, meaning it excludes the input nodes themselves.

Initial Node ID

Compound nodes can specify an initialNodeId to indicate which child is the entry point:
const graph = createGraph({
  nodes: [
    {
      id: 'parent',
      initialNodeId: 'start' // Entry point for this compound node
    },
    { id: 'start', parentId: 'parent' },
    { id: 'end', parentId: 'parent' }
  ],
  edges: [
    { id: 'e1', sourceId: 'start', targetId: 'end' }
  ]
});
This is useful for statecharts where entering a compound state activates a specific child state.

Relative Distance

Measure distance from a parent’s initialNodeId to child nodes:
import { getRelativeDistance, getRelativeDistanceMap } from '@statelyai/graph';

const graph = createGraph({
  nodes: [
    { id: 'parent', initialNodeId: 's1' },
    { id: 's1', parentId: 'parent' },
    { id: 's2', parentId: 'parent' },
    { id: 's3', parentId: 'parent' }
  ],
  edges: [
    { id: 'e1', sourceId: 's1', targetId: 's2' },
    { id: 'e2', sourceId: 's2', targetId: 's3' }
  ]
});

// Distance from parent's initialNodeId (s1)
getRelativeDistance(graph, 's1'); // => 0
getRelativeDistance(graph, 's2'); // => 1
getRelativeDistance(graph, 's3'); // => 2

// Get all distances at once
const distMap = getRelativeDistanceMap(graph, 'parent');
// => { s1: 0, s2: 1, s3: 2 }
getRelativeDistance() only follows edges between siblings (nodes with the same parentId). This keeps the distance calculation scoped to a single hierarchy level.

Deleting Nodes with Children

When you delete a compound node, you can choose what happens to its children:

Cascade Delete (Default)

Delete the node and all descendants:
import { deleteNode } from '@statelyai/graph';

const graph = createGraph({
  nodes: [
    { id: 'root' },
    { id: 'child', parentId: 'root' },
    { id: 'grandchild', parentId: 'child' }
  ]
});

deleteNode(graph, 'root');
// Removes: root, child, grandchild

Reparent Children

Re-parent children to the deleted node’s parent:
import { deleteNode } from '@statelyai/graph';

const graph = createGraph({
  nodes: [
    { id: 'root' },
    { id: 'mid', parentId: 'root' },
    { id: 'leaf', parentId: 'mid' }
  ]
});

deleteNode(graph, 'mid', { reparent: true });
// After deletion:
// - 'mid' is removed
// - 'leaf' becomes child of 'root'

Hierarchy + Edges

Hierarchy and edges are independent structures:
  • parentId defines containment (which nodes are inside other nodes)
  • Edges define connections/transitions (how nodes relate to each other)
Edges can connect nodes at different hierarchy levels:
const graph = createGraph({
  nodes: [
    { id: 'group1' },
    { id: 'a', parentId: 'group1' },
    { id: 'group2' },
    { id: 'b', parentId: 'group2' }
  ],
  edges: [
    // Edge crosses hierarchy levels
    { id: 'e1', sourceId: 'a', targetId: 'b' }
  ]
});

// Tree structure:
// group1
//   └─ a ───edge───> b
//                     ↑
// group2 ─────────────┘
When exporting to formats like DOT or GraphML, some renderers may have special rules for edges that cross hierarchy boundaries. Check the format documentation for details.

Example: Statechart

Hierarchical graphs are perfect for statecharts:
import { createGraph } from '@statelyai/graph';

const statechart = createGraph({
  id: 'authentication',
  initialNodeId: 'loggedOut',
  nodes: [
    // Root-level states
    { id: 'loggedOut' },
    {
      id: 'loggedIn',
      initialNodeId: 'profile' // Default child when entering
    },
    // Nested states inside 'loggedIn'
    { id: 'profile', parentId: 'loggedIn' },
    { id: 'settings', parentId: 'loggedIn' },
    { id: 'admin', parentId: 'loggedIn' }
  ],
  edges: [
    // Login transition
    { id: 'login', sourceId: 'loggedOut', targetId: 'loggedIn' },
    // Logout transition
    { id: 'logout', sourceId: 'loggedIn', targetId: 'loggedOut' },
    // Navigate between child states
    { id: 'toProfile', sourceId: 'settings', targetId: 'profile' },
    { id: 'toSettings', sourceId: 'profile', targetId: 'settings' },
    { id: 'toAdmin', sourceId: 'settings', targetId: 'admin' }
  ]
});

Example: Organization Chart

interface EmployeeData {
  name: string;
  title: string;
  department: string;
}

const orgChart = createGraph<EmployeeData>({
  id: 'org-chart',
  nodes: [
    {
      id: 'ceo',
      label: 'CEO',
      data: { name: 'Alice', title: 'Chief Executive Officer', department: 'Executive' }
    },
    {
      id: 'cto',
      parentId: 'ceo',
      label: 'CTO',
      data: { name: 'Bob', title: 'Chief Technology Officer', department: 'Engineering' }
    },
    {
      id: 'dev1',
      parentId: 'cto',
      label: 'Senior Dev',
      data: { name: 'Charlie', title: 'Senior Developer', department: 'Engineering' }
    },
    {
      id: 'dev2',
      parentId: 'cto',
      label: 'Junior Dev',
      data: { name: 'Dana', title: 'Junior Developer', department: 'Engineering' }
    }
  ],
  edges: [] // No edges needed - hierarchy defines relationships
});

Next Steps

Queries

Explore all hierarchy query functions

Visual Graphs

Position and render hierarchical layouts

Operations

Add, update, and delete hierarchical nodes

Algorithms

Traverse and analyze hierarchical graphs

Build docs developers (and LLMs) love