import dagre from "@dagrejs/dagre"; import type { ChartNode, ChartNodeKind } from "./types"; // Must match what OrgChartNode actually renders: Dagre reserves exactly this // much space per node, so a box that grows past it overlaps its neighbour. export const NODE_DIMENSIONS: Record = { person: { width: 268, height: 80 }, role: { width: 268, height: 80 }, group: { width: 244, height: 62 }, vacancy: { width: 244, height: 62 }, }; export type VisibleEdge = { id: string; source: string; target: string }; // Collapsing a node means excluding its descendants here, not CSS-hiding a // layout computed for the full tree — Dagre only ever lays out what's // actually visible, which is what keeps this usable at ~800 employees. export function collectVisible(tree: ChartNode[], isExpanded: (id: string) => boolean): { visibleNodes: ChartNode[]; visibleEdges: VisibleEdge[] } { const visibleNodes: ChartNode[] = []; const visibleEdges: VisibleEdge[] = []; function walk(n: ChartNode) { visibleNodes.push(n); if (n.children.length > 0 && isExpanded(n.id)) { for (const c of n.children) { visibleEdges.push({ id: `${n.id}->${c.id}`, source: n.id, target: c.id }); walk(c); } } } for (const root of tree) walk(root); return { visibleNodes, visibleEdges }; } // Dagre returns center points; React Flow positions nodes by their top-left // corner, hence the width/height/2 offset below. export function layoutWithDagre(visibleNodes: ChartNode[], visibleEdges: VisibleEdge[]): Map { const g = new dagre.graphlib.Graph(); // ranksep leaves room for the expand button that overhangs each node's // bottom edge; nodesep keeps sibling cards from visually merging. g.setGraph({ rankdir: "TB", nodesep: 40, ranksep: 88 }); g.setDefaultEdgeLabel(() => ({})); for (const n of visibleNodes) { const { width, height } = NODE_DIMENSIONS[n.kind]; g.setNode(n.id, { width, height }); } for (const e of visibleEdges) g.setEdge(e.source, e.target); dagre.layout(g); const positions = new Map(); for (const n of visibleNodes) { const { width, height } = NODE_DIMENSIONS[n.kind]; const { x, y } = g.node(n.id); positions.set(n.id, { x: x - width / 2, y: y - height / 2 }); } return positions; }