Skip to content

data-explorer

Zoom into a group to enter it, zoom out to leave, pan across to its neighbours. Your data, any shape of tree or graph, as a space you move through.

Any data

You describe each scene through a small contract: a heading, groups, items and the links between them. The explorer never walks your data, so cycles cost nothing.

Continuous

No page loads between levels: the next scene loads while the viewer zooms towards it and fades in inside the item they are entering.

Accessible

Beside the canvas, an outline of real links and buttons, read aloud and reachable by keyboard. Every item has a page of its own.

Small and lazy

React 18 or 19 and PixiJS 8. PixiJS loads only when the explorer first mounts. Typed throughout, ESM only.

Teams, then people, then one person’s neighbourhood. Scroll or pinch to zoom, drag to pan, double-click to enter.

Running the code below: concepts-graph.tsx
concepts-graph.tsx
import { useState } from "react";
import { createSceneSource, DataExplorer, type ExplorerEdge } from "@data-xplorer/core";
// A tiny organisation: teams of people, and who works with whom. The focus says where you are:
// the top (null), a team, or one person's neighbourhood.
type Focus = { team: string } | { person: string };
const teams: Record<string, string[]> = {
Design: ["Ada", "Ben", "Cleo"],
Platform: ["Dev", "Eli", "Fay", "Gus"],
Data: ["Hal", "Ivy"],
};
const teamOf = (person: string) => Object.keys(teams).find((t) => teams[t]!.includes(person))!;
// Pairs who work together across teams; teammates are joined too (below).
const pairs: [string, string][] = [["Ada", "Dev"], ["Cleo", "Hal"], ["Fay", "Ivy"], ["Ben", "Eli"]];
const person = (name: string) => ({
id: name,
label: name,
weight: 1,
href: `#${name}`,
drillsInto: { person: name }, // entering a person opens their neighbourhood
shape: "disc" as const,
});
const source = createSceneSource<Focus>({
load(focus) {
// The top: a packed scene of teams, each a cloud you can enter.
if (focus === null) {
return {
id: "top",
heading: "Organisation",
clusters: Object.keys(teams).map((team) => ({
id: team,
label: team,
weight: teams[team]!.length,
href: `#${team}`,
drillsInto: { team },
shape: "cloud" as const,
})),
nodes: [],
edges: [],
};
}
// A team: a packed scene of its people.
if ("team" in focus) {
return { id: focus.team, heading: focus.team, clusters: [], nodes: teams[focus.team]!.map(person), edges: [] };
}
// A person: a radial scene. `focusId` puts them in the middle; edges choose the neighbours,
// grouped round them by the edge's `kind`.
const me = focus.person;
const mates = teams[teamOf(me)]!.filter((p) => p !== me);
const partners = pairs.flatMap(([a, b]) => (a === me ? [b] : b === me ? [a] : []));
const edges: ExplorerEdge[] = [
...mates.map((p) => ({ id: `team-${p}`, sourceId: me, targetId: p, kind: "teammate" })),
...partners.map((p) => ({ id: `pair-${p}`, sourceId: me, targetId: p, kind: "works-with" })),
];
return { id: me, heading: me, focusId: me, clusters: [], nodes: [me, ...mates, ...partners].map(person), edges };
},
// Zooming out: a person returns to their dot in their team; a team to its cloud at the top.
parentOf(focus) {
if ("team" in focus) return { kind: "anchored", focus: null, anchorId: focus.team };
return { kind: "anchored", focus: { team: teamOf(focus.person) }, anchorId: focus.person };
},
});
export default function ConceptsGraph() {
const [focus, setFocus] = useState<Focus | null>(null);
return <DataExplorer source={source} focus={focus} onSettle={setFocus} edges={{ visible: true }} />;
}