Skip to content

Layouts & options

type LayoutNode = { id: string; width: number; height: number };
type LayoutEdge = { source: string; target: string };
type EgoGraph<N, E> = { nodes: N[]; edges: E[]; root: string };
type Positions = Map<string, { x: number; y: number }>;

Your own fields travel through the generic types, so classification callbacks can inspect them.

customLayout(graph, options?) walks the spanning tree and places each parent’s direct children according to a layer spec. Import it from ego-graph for radial, bubble, and cloud. Import from ego-graph/dagre when a spec uses dagre, dagreSubtree, or sectorThreshold.

buildSpanningTree(graph, options?) and reachableNodeIds(nodeIds, edges, root) expose the underlying tree utilities.

type FirstLayerSpec = { mode: 'radial' | 'dagre' | 'bubble' | 'dagreSubtree'; sectorThreshold?: number };
type CloudLayerSpec = { mode: 'cloud'; threshold?: 3 | 5 | 8 | 13 | 21 };
type FirstLayerBand = FirstLayerSpec & { upTo?: number };
type FirstLayersSpec = FirstLayerSpec | readonly FirstLayerBand[];
customLayout(graph, {
firstLayer: { mode: 'radial' },
nextLayers: { mode: 'cloud', threshold: 8 },
});

Both firstLayer and nextLayers may be one spec or an array of bands. Omit firstLayer and it defaults to { mode: 'radial' }. cloud is next-layers only: the root has no outbound ray. Bubble ring capacity is innerRingCapacity on spacing. Cloud’s cap is threshold on the spec (3, 5, 8, 13, or 21; default 8). dagre may set sectorThreshold to split a bushy parent into several shallow pockets.

Mode Placement
radial Full circle around the root; a hemisphere centred on the outbound ray for deeper parents
dagre One shallow Dagre pocket along the outbound ray (straight down at the root)
bubble Direct children packed into concentric rings so the group stays round
dagreSubtree Remaining descendants as one Dagre tree; the walk does not recurse
cloud Fibonacci-sized arcs along the outbound ray (deeper parents only)

A mode picker should use FIRST_LAYER_NAMES for first-layer bands and DISTRIBUTION_NAMES for next layers.

import { DISTRIBUTION_NAMES, FIRST_LAYER_NAMES } from 'ego-graph';

The React Flow complete example does this for both layers as band editors.

Both firstLayer and nextLayers can be one spec, or an array of bands. Each band uses its spec while a parent has at most upTo children. Omit upTo on the last band to catch everything remaining.

import { customLayout } from 'ego-graph/dagre';
customLayout(graph, {
firstLayer: [
{ upTo: 5, mode: 'dagre' },
{ upTo: 20, mode: 'radial' },
{ mode: 'bubble' },
],
nextLayers: [
{ upTo: 5, mode: 'dagre' },
{ upTo: 20, mode: 'radial' },
{ mode: 'cloud', threshold: 8 },
],
});

Layer bands is the executable sample.

Former named functions as specs:

customLayout(graph, { firstLayer: { mode: 'radial' }, nextLayers: { mode: 'radial' } });
customLayout(graph, {
firstLayer: { mode: 'dagre', sectorThreshold: 5 },
nextLayers: { mode: 'dagre', sectorThreshold: 5 },
});
customLayout(graph, { firstLayer: { mode: 'radial' }, nextLayers: { mode: 'dagreSubtree' } });

The last two must be imported from ego-graph/dagre.

innerRingCapacity (T, default 6) is the capacity of the innermost ring. It is not a maximum child count. Extra children spill into outer rings.

Number rings from the inside, starting at 0:

Ring Capacity
0 (innermost) T
1 2T
k (k + 1) × T

Children are ordered with heavier subtrees first, then poured into ring 0 until it is full, then ring 1, and so on. A leftover partial ring is allowed. Fourteen children with T = 6 occupy 6 + 8, not a third ring.

Each ring is a full circle of equally spaced slots. Its radius is large enough that those siblings do not overlap. Each outer ring sits outside the previous one plus rankSep.

At the root, every ring is centred on the ego. When the parent is not the root, the whole disk is translated along the parent’s outbound ray so it sits in front of that parent: the parent’s radial half-extent, plus ringPadding, plus the bubble’s own radius.

Do not confuse a bubble ring with the first ring. The first ring is the circle of the root’s children under radial. A bubble ring is one concentric circle inside a bubble packing of a single parent’s children. A band is a child-count range, not a circle.

cloud packs a deeper parent’s children onto tight arcs centred on the outbound ray (grandparent → parent). It is not valid as firstLayer.

threshold (T, default 8) is one of 3, 5, 8, 13, 21. Layer sizes climb the Fibonacci sequence from 1 up to T, hold at T for leftover nodes, then taper. One non-Fibonacci remainder layer, if needed, sits in the descending run. Heavier children sit inward and on the ray. Adjacent layers are separated by rankSep; parent clearance and arc packing use ringPadding.

Every layout accepts these optional dials. DEFAULT_SPACING exports the length defaults. Bubble ring capacity is innerRingCapacity, a spacing field. Cloud’s cap is threshold on the layer spec.

Option Default Layouts Meaning
nodeSep 80 dagre, dagreSubtree Gap between siblings inside a Dagre pocket or subtree.
rankSep 100 dagre, dagreSubtree, bubble, cloud Gap between Dagre ranks, bubble rings, and cloud layers.
minRingRadius 220 radial, bubble Floor on ring radius so children clear the parent.
ringPadding 60 radial, bubble, cloud Extra space between adjacent branches, and parent clearance for a cloud.
innerRingCapacity 6 bubble Nodes on the innermost bubble ring. Further rings hold 2×, 3×, … that many.
satelliteGap π / 3 satellites Angular proximity that triggers satellite fanning.
minSatelliteGap π / 12 satellites Minimum separation for fanned satellites.