Introduction
An ego graph puts one node, the customer, account, or person the view is about, at the centre. ego-graph lays its neighbourhood around it, with branch folding and sensible treatment for relationships that should not control the hierarchy.
ego-graph is deliberately small:
- The core accepts plain
{ id, width, height }nodes and{ source, target }edges. - It is synchronous, renderer-agnostic, and returns top-left positions in a
Map. - The
react-flowentry point contains only pure adapters; it does not import React.
See it work
Section titled “See it work”This is the canonical executable sample at examples/polar-petal-playground.tsx; the page imports its source rather than maintaining a second copy. Edit the code and the preview reruns.
import { customLayout } from 'ego-graph'; import './styles.css'; const nodes = [ { id: 'ada', label: 'Ada', width: 120, height: 44 }, { id: 'lin', label: 'Lin', width: 120, height: 44 }, { id: 'maria', label: 'Maria', width: 120, height: 44 }, { id: 'kofi', label: 'Kofi', width: 120, height: 44 }, { id: 'noor', label: 'Noor', width: 120, height: 44 }, ]; const edges = [ { source: 'ada', target: 'lin' }, { source: 'ada', target: 'maria' }, { source: 'ada', target: 'kofi' }, { source: 'maria', target: 'noor' }, ]; const positions = customLayout( { nodes, edges, root: 'ada' }, { firstLayer: { mode: 'radial' }, nextLayers: { mode: 'radial' } }, ); const byId = new Map(nodes.map((node) => [node.id, node])); const center = (id: string) => { const position = positions.get(id)!; const node = byId.get(id)!; return { x: position.x + node.width / 2 + 280, y: position.y + node.height / 2 + 190 }; }; export default function App() { return ( <svg viewBox="0 0 560 1000" role="img" aria-label="An ego graph laid out with customLayout"> <rect width="560" height="1000" fill="#f8fafc" /> {edges.map((edge) => { const from = center(edge.source); const to = center(edge.target); return <line key={`${edge.source}-${edge.target}`} x1={from.x} y1={from.y} x2={to.x} y2={to.y} stroke="#94a3b8" strokeWidth="2" />; })} {nodes.map((node) => { const position = positions.get(node.id)!; return ( <g key={node.id} transform={`translate(${position.x + 280}, ${position.y + 190})`}> <rect width={node.width} height={node.height} rx="10" fill={node.id === 'ada' ? '#3157b7' : 'white'} stroke="#3157b7" /> <text x={node.width / 2} y="28" textAnchor="middle" fill={node.id === 'ada' ? 'white' : '#172554'} fontSize="14">{node.label}</text> </g> ); })} </svg> ); }
First layer, then the rest
Section titled “First layer, then the rest”The sample above is customLayout with radial then radial: every parent spreads its own children radially.
That is one combination. firstLayer and nextLayers each take one spec or a list of count bands. First-layer modes are radial, dagre, bubble, or dagreSubtree. nextLayers may also be cloud.
import { customLayout } from 'ego-graph';
customLayout(graph, { firstLayer: { mode: 'bubble' }, nextLayers: { mode: 'radial' },});bubble packs siblings into concentric rings. innerRingCapacity (T, default 6) is the innermost ring, not a fold cutoff and not a cap on how many children a node may have. Ring 0 holds up to T children, ring 1 holds up to 2T, ring k holds up to (k + 1) × T. Children fill from the inside out. Around the root the rings are centred on the ego. Deeper in the tree the whole disk sits in front of the parent, along the ray that parent was placed on.
Dagre modes (dagre, dagreSubtree, sectorThreshold) need import { customLayout } from 'ego-graph/dagre' and the optional @dagrejs/dagre peer.
Layouts & options has the ring rule. Layer bands picks a mode from how many children a parent has. The React Flow complete example exposes both layers as band controls.