An org chart
Departments, teams and people as a tree, into a radial person view whose manager and reports are drawn edges you can walk. It was written from the documentation alone, as a test of whether the documentation is enough, and its smoke.test.ts drives it headless with createTestStack.
The frame below runs the example built from exactly these files. Start with source.ts. Every
example shares one small page shell (packages/core/examples/shared/ExampleShell.tsx): it keeps the focus in the
URL hash, builds a breadcrumb from onSceneChange, and shows the selected item from onNodeSelect.
import { createSceneSource, type ExplorerCluster, type ExplorerEdge, type ExplorerNode, type ExplorerScene, type SceneSource,} from "@data-xplorer/core";import { delay } from "../shared/delay";import { CEO_ID, DEPARTMENTS, headcountOf, PEOPLE, reportsOf, TEAMS, type Department, type Person, type Team,} from "./data";
/** * A tree with a graph hanging off every leaf: * * - `null`: the departments as clusters, and the CEO as a node (packed); * - a department: its teams as clusters, and its head as a node (packed); * - a team: its people (packed), the lead as a square; * - a person: radial, with the person in the middle, their manager and their direct reports around * them, joined by "reports-to" and "manages" edges. Each neighbour is a person too, so you can * walk up and down the reporting line. * * Each item carries its own record in `data` (a `Selected`), which the selection events hand back, * so the details panel never has to find it again. */export type Focus = | { kind: "department"; id: string } | { kind: "team"; id: string } | { kind: "person"; id: string };
export const CEO_COLOUR = "#eab308";
export const encodeFocus = (focus: Focus) => `${focus.kind}:${focus.id}`;
export function decodeFocus(hash: string): Focus | null { const colon = hash.indexOf(":"); const kind = hash.slice(0, colon); const id = hash.slice(colon + 1); if (kind === "department" && DEPARTMENTS.has(id)) return { kind, id }; if (kind === "team" && TEAMS.has(id)) return { kind, id }; if (kind === "person" && PEOPLE.has(id)) return { kind, id }; return null;}
const href = (focus: Focus) => `#${encodeFocus(focus)}`;
export const itemId = { department: (id: string) => `dept:${id}`, team: (id: string) => `team:${id}`, person: (id: string) => `person:${id}`,};
/** What an item stands for: carried in its `data`. */export type Selected = | { kind: "department"; department: Department } | { kind: "team"; team: Team } | { kind: "person"; person: Person };
const colourOf = (person: Person) => person.departmentId === null ? CEO_COLOUR : DEPARTMENTS.get(person.departmentId)!.colour;
function personNode(person: Person): ExplorerNode<Focus, Selected> { const reports = reportsOf(person.id).length; return { id: itemId.person(person.id), label: person.name, sublabel: person.role, weight: reports + 1, // the person and the people who report to them marks: [person.role, ...(reports > 0 ? [`${reports} direct reports`] : [])], href: href({ kind: "person", id: person.id }), drillsInto: { kind: "person", id: person.id }, // Anyone who manages someone is a square; everyone else a disc. shape: reports > 0 ? "square" : "disc", color: colourOf(person), effects: person.id === CEO_ID ? ["pulse"] : [], data: { kind: "person", person }, };}
function departmentCluster(department: Department): ExplorerCluster<Focus, Selected> { const headcount = headcountOf(department); return { id: itemId.department(department.id), label: department.name, sublabel: `${headcount} people`, weight: headcount, marks: [`${headcount} people`, `${department.teamIds.length} teams`], href: href({ kind: "department", id: department.id }), drillsInto: { kind: "department", id: department.id }, shape: "cloud", truncated: null, color: department.colour, data: { kind: "department", department }, };}
function teamCluster(team: Team): ExplorerCluster<Focus, Selected> { const headcount = team.memberIds.length; return { id: itemId.team(team.id), label: team.name, sublabel: `${headcount} people`, weight: headcount, marks: [`${headcount} people`, `led by ${PEOPLE.get(team.leadId)!.name}`], href: href({ kind: "team", id: team.id }), drillsInto: { kind: "team", id: team.id }, shape: "cloud", truncated: null, color: DEPARTMENTS.get(team.departmentId)!.colour, data: { kind: "team", team }, };}
/** "manages" from a manager to someone who reports to them, drawn in the packed scenes too. */const manages = (manager: Person, itemTargetId: string): ExplorerEdge => ({ id: `${manager.id}->${itemTargetId}`, sourceId: itemId.person(manager.id), targetId: itemTargetId, kind: "manages",});
function buildScene(focus: Focus | null): ExplorerScene<Focus, Selected> { if (focus === null) { const ceo = PEOPLE.get(CEO_ID)!; const departments = [...DEPARTMENTS.values()]; return { id: "root", heading: "Rosa & Co.", clusters: departments.map(departmentCluster), nodes: [personNode(ceo)], edges: departments.map((d) => manages(ceo, itemId.department(d.id))), }; }
if (focus.kind === "department") { const department = DEPARTMENTS.get(focus.id)!; const head = PEOPLE.get(department.headId)!; const teams = department.teamIds.map((id) => TEAMS.get(id)!); return { id: encodeFocus(focus), heading: department.name, clusters: teams.map(teamCluster), nodes: [personNode(head)], edges: teams.map((t) => manages(head, itemId.team(t.id))), }; }
if (focus.kind === "team") { const team = TEAMS.get(focus.id)!; const lead = PEOPLE.get(team.leadId)!; const people = team.memberIds.map((id) => PEOPLE.get(id)!); return { id: encodeFocus(focus), heading: team.name, clusters: [], nodes: people.map(personNode), edges: people.filter((p) => p !== lead).map((p) => manages(lead, itemId.person(p.id))), }; }
// A person: radial, because the scene sets `focusId`. const centre = PEOPLE.get(focus.id)!; const manager = centre.managerId === null ? null : PEOPLE.get(centre.managerId)!; const reports = reportsOf(centre.id); const centreId = itemId.person(centre.id); const edges: ExplorerEdge[] = [ ...(manager ? [{ id: `${centre.id}->${manager.id}`, sourceId: centreId, targetId: itemId.person(manager.id), kind: "reports-to" }] : []), ...reports.map((r) => manages(centre, itemId.person(r.id))), ]; return { id: encodeFocus(focus), heading: centre.name, focusId: centreId, clusters: [], nodes: [centre, ...(manager ? [manager] : []), ...reports].map(personNode), edges, };}
/** The scene a person normally lives in, and so where zooming out of them lands. */function homeOf(person: Person): Focus | null { if (person.teamId !== null) return { kind: "team", id: person.teamId }; if (person.departmentId !== null) return { kind: "department", id: person.departmentId }; return null; // the CEO sits in the top scene}
export function orgChartSource(): SceneSource<Focus, Selected> { return createSceneSource<Focus, Selected>({ keyOf: (focus) => (focus === null ? "root" : encodeFocus(focus)), load: async (focus) => { await delay(80); return buildScene(focus); }, parentOf(focus) { if (focus.kind === "department") { return { kind: "anchored", focus: null, anchorId: itemId.department(focus.id) }; } if (focus.kind === "team") { const departmentId = TEAMS.get(focus.id)!.departmentId; return { kind: "anchored", focus: { kind: "department", id: departmentId }, anchorId: itemId.team(focus.id) }; } // A person zooms out to their own item in their team (or department, or the top scene). // Walking sideways first retraces the walk instead: the explorer remembers. const person = PEOPLE.get(focus.id)!; return { kind: "anchored", focus: homeOf(person), anchorId: itemId.person(person.id) }; }, });}import { createRoot } from "react-dom/client";import type { ExplorerEdges, ExplorerItem } from "@data-xplorer/core";import { ExampleShell } from "../shared/ExampleShell";import { DEPARTMENTS, PEOPLE, TEAMS, headcountOf } from "./data";import { decodeFocus, encodeFocus, orgChartSource, type Focus, type Selected } from "./source";
const source = orgChartSource();
/** "manages" runs down the chart with an arrow; "reports-to" runs up it, dashed. */const EDGES: ExplorerEdges = { visible: true, style: { width: 1.5, alpha: 0.5 }, styles: { manages: { arrow: "end" }, "reports-to": { arrow: "end", dash: [5, 4], curve: 0.12 }, },};
// Where a scene sits in the company, not the path walked to reach it.function trailOf(focus: Focus | null): string[] { const top = "Rosa & Co."; if (focus === null) return [top]; if (focus.kind === "department") return [top, DEPARTMENTS.get(focus.id)!.name]; if (focus.kind === "team") { const team = TEAMS.get(focus.id)!; return [top, DEPARTMENTS.get(team.departmentId)!.name, team.name]; } const person = PEOPLE.get(focus.id)!; return [ top, ...(person.departmentId ? [DEPARTMENTS.get(person.departmentId)!.name] : []), ...(person.teamId ? [TEAMS.get(person.teamId)!.name] : []), person.name, ];}
/** The details of *our* object behind the selected item: its `data`, handed back with it. */function Details({ item }: { item: ExplorerItem<Focus, Selected> }) { const selected = item.data; if (!selected) return null;
if (selected.kind === "person") { const { person } = selected; const manager = person.managerId ? PEOPLE.get(person.managerId)! : null; const team = person.teamId ? TEAMS.get(person.teamId)! : null; const department = person.departmentId ? DEPARTMENTS.get(person.departmentId)! : null; return ( <dl className="example__details"> <dt>Role</dt> <dd>{person.role}</dd> <dt>Email</dt> <dd> <a href={`mailto:${person.email}`}>{person.email}</a> </dd> <dt>Team</dt> <dd>{team?.name ?? "—"}</dd> <dt>Department</dt> <dd>{department?.name ?? "—"}</dd> <dt>Manager</dt> <dd>{manager ? <a href={`#person:${manager.id}`}>{manager.name}</a> : "Nobody: the top of the chart"}</dd> </dl> ); }
if (selected.kind === "team") { const { team } = selected; return ( <dl className="example__details"> <dt>Department</dt> <dd>{DEPARTMENTS.get(team.departmentId)!.name}</dd> <dt>Lead</dt> <dd>{PEOPLE.get(team.leadId)!.name}</dd> <dt>People</dt> <dd>{team.memberIds.map((id) => PEOPLE.get(id)!.name).join(", ")}</dd> </dl> ); }
const { department } = selected; return ( <dl className="example__details"> <dt>Head</dt> <dd>{PEOPLE.get(department.headId)!.name}</dd> <dt>Headcount</dt> <dd>{headcountOf(department)}</dd> <dt>Teams</dt> <dd>{department.teamIds.map((id) => TEAMS.get(id)!.name).join(", ")}</dd> </dl> );}
createRoot(document.getElementById("root")!).render( <ExampleShell title="An org chart" intro={ <p> A small company. Enter a department to see its teams, a team to see its people, and a person to see who they report to and who reports to them. From a person you can walk up to their manager or down to a report. Colours follow the department; the CEO pulses. Click anything for its details. </p> } source={source} encode={encodeFocus} decode={decodeFocus} trailOf={trailOf} edges={EDGES} renderSelection={(item) => <Details item={item} />} />,);/** * A made-up company: a CEO, four departments each with a head, two or three teams per department, * each team with a lead and some members. Every person but the CEO has a manager: * * - the CEO manages each department head; * - a department head manages the lead of each of their teams; * - a team lead manages the rest of their team. * * This file knows nothing about the explorer. */export interface Person { id: string; name: string; role: string; email: string; /** null for the CEO and department heads, who sit above any one team. */ teamId: string | null; /** null only for the CEO. */ departmentId: string | null; managerId: string | null;}
export interface Team { id: string; name: string; departmentId: string; leadId: string; memberIds: string[]; // lead first}
export interface Department { id: string; name: string; headId: string; teamIds: string[]; colour: string;}
const slug = (name: string) => name.toLowerCase().replace(/[^a-z]+/g, ".");const email = (name: string) => `${slug(name)}@example.com`;
// [department, colour, head, [team, lead, ...members][]]const raw: [string, string, string, [string, string, ...string[]][]][] = [ [ "Engineering", "#3b82f6", "Ada Brooks", [ ["Platform", "Ben Ortiz", "Cara Lin", "Dev Patel", "Eli Novak", "Fay Wu"], ["Product", "Gus Hale", "Hana Ito", "Ivo Petrov"], ["Data", "Jun Park", "Kai Moss", "Lea Roth", "Max Ford"], ], ], [ "Design", "#ec4899", "Nia Cole", [ ["Brand", "Oli Grant", "Pia Shah", "Quin Doyle"], ["UX Research", "Rae Kim", "Sam Hart", "Tia Bell", "Uma Reid"], ], ], [ "Sales", "#f59e0b", "Vic Lowe", [ ["Enterprise", "Wes Clark", "Xia Chen", "Yan Ruiz", "Zoe Price", "Abe Fox"], ["SMB", "Bea Lane", "Cal West", "Dot Snow"], ["Partnerships", "Eve Marsh", "Finn Day", "Gia Rossi"], ], ], [ "Operations", "#10b981", "Hal Stone", [ ["People", "Ida Knox", "Jay Vance", "Kim Lowry"], ["Finance", "Lou Baker", "Mae Quinn", "Ned Frost", "Ora Blake"], ], ],];
export const CEO_ID = "ceo";
export const PEOPLE = new Map<string, Person>();export const TEAMS = new Map<string, Team>();export const DEPARTMENTS = new Map<string, Department>();
PEOPLE.set(CEO_ID, { id: CEO_ID, name: "Rosa Vega", role: "Chief Executive Officer", email: email("Rosa Vega"), teamId: null, departmentId: null, managerId: null,});
for (const [deptName, colour, headName, teams] of raw) { const departmentId = slug(deptName); const headId = slug(headName); PEOPLE.set(headId, { id: headId, name: headName, role: `Head of ${deptName}`, email: email(headName), teamId: null, departmentId, managerId: CEO_ID, }); const teamIds: string[] = []; for (const [teamName, leadName, ...memberNames] of teams) { const teamId = `${departmentId}/${slug(teamName)}`; const leadId = slug(leadName); teamIds.push(teamId); PEOPLE.set(leadId, { id: leadId, name: leadName, role: `${teamName} Lead`, email: email(leadName), teamId, departmentId, managerId: headId, }); for (const name of memberNames) { PEOPLE.set(slug(name), { id: slug(name), name, role: `${teamName} Specialist`, email: email(name), teamId, departmentId, managerId: leadId, }); } TEAMS.set(teamId, { id: teamId, name: teamName, departmentId, leadId, memberIds: [leadId, ...memberNames.map(slug)], }); } DEPARTMENTS.set(departmentId, { id: departmentId, name: deptName, headId, teamIds, colour });}
export const reportsOf = (personId: string) => [...PEOPLE.values()].filter((p) => p.managerId === personId);
/** Everyone in a department, its head included. */export const headcountOf = (department: Department) => 1 + department.teamIds.reduce((sum, id) => sum + TEAMS.get(id)!.memberIds.length, 0);import { describe, expect, test } from "bun:test";import { createTestStack } from "@data-xplorer/core/headless";import { CEO_ID, DEPARTMENTS, TEAMS } from "./data";import { itemId, orgChartSource } from "./source";
/** * Drives the explorer's scene stack without a canvas: enter a department, a team, then a person, * and check where the stack is each time. Written first, in US072's cold-start test, against * `createSceneStack` with a hand-made `requestAnimationFrame` polyfill; `createTestStack` (US072 * change 4) made both unnecessary. */const mount = () => createTestStack(orgChartSource(), { viewport: { width: 1000, height: 700 } });
describe("org chart, headless", () => { test("enters a department, a team, then a person", async () => { const stack = await mount(); expect(stack.current.focus).toBeNull(); expect(stack.current.heading).toBe("Rosa & Co.");
const engineering = DEPARTMENTS.get("engineering")!; await stack.enter(itemId.department(engineering.id)); expect(stack.current.focus).toEqual({ kind: "department", id: "engineering" }); expect(stack.current.heading).toBe("Engineering"); expect(stack.depth).toBe(1);
const platform = TEAMS.get(engineering.teamIds[0]!)!; await stack.enter(itemId.team(platform.id)); expect(stack.current.focus).toEqual({ kind: "team", id: platform.id }); expect(stack.current.heading).toBe("Platform");
await stack.enter(itemId.person(platform.leadId)); expect(stack.current.focus).toEqual({ kind: "person", id: platform.leadId }); // Radial: the lead in the middle, their head above and their team below. expect(stack.current.scene.focusId).toBe(itemId.person(platform.leadId)); expect(stack.current.scene.edges.map((e) => e.kind).sort()).toEqual([ ...Array(platform.memberIds.length - 1).fill("manages"), "reports-to", ]);
// Walk up to the manager, then zoom out: back the way we came. await stack.enter(itemId.person(engineering.headId)); expect(stack.current.focus).toEqual({ kind: "person", id: engineering.headId }); await stack.zoomOut(); expect(stack.current.focus).toEqual({ kind: "person", id: platform.leadId }); });
test("the CEO has no manager edge", async () => { const stack = await mount(); await stack.enter(itemId.person(CEO_ID)); expect(stack.current.scene.edges.every((e) => e.kind === "manages")).toBe(true); });});