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