Skip to content

Events

Every payload is plain data: your own items, headings, foci and numbers, never a PixiJS object. An item comes back exactly as your source wrote it, including its data, typed by the source’s second type parameter (createSceneSource<Focus, Data>).

Running the code below: events.tsx
events.tsx
import { useState } from "react";
import { createSceneSource, DataExplorer, type ExplorerItem } from "@data-xplorer/core";
// Your own data rides along on each item as `data`, and comes back with it in the events.
type Planet = { moons: number };
const planets: [string, number][] = [["Mercury", 0], ["Venus", 0], ["Earth", 1], ["Mars", 2], ["Jupiter", 95]];
const source = createSceneSource<string, Planet>({
load(focus) {
if (focus !== null) {
const moons = planets.find(([name]) => name === focus)![1];
return {
id: focus,
heading: focus,
clusters: [],
nodes: Array.from({ length: Math.min(moons, 12) }, (_, i) => ({
id: `${focus}-${i}`,
label: `Moon ${i + 1}`,
weight: 1,
href: `#${focus}-${i}`,
drillsInto: null,
shape: "disc" as const,
})),
edges: [],
};
}
return {
id: "sun",
heading: "Planets",
nodes: [],
edges: [],
clusters: planets.map(([name, moons]) => ({
id: name,
label: name,
weight: 1 + moons,
href: `#${name}`,
drillsInto: moons > 0 ? name : null,
shape: "cloud" as const,
data: { moons },
})),
};
},
parentOf: (planet) => ({ kind: "anchored", focus: null, anchorId: planet }),
});
export default function Events() {
const [focus, setFocus] = useState<string | null>(null);
const [log, setLog] = useState<string[]>([]);
const [openInstead, setOpenInstead] = useState(false);
const say = (line: string) => setLog((lines) => [line, ...lines].slice(0, 30));
const name = (item: ExplorerItem<string, Planet> | null) => (item ? item.label : "nothing");
return (
<>
<div className="demo-toolbar">
<label>
<input type="checkbox" checked={openInstead} onChange={(e) => setOpenInstead(e.target.checked)} /> Double-click
opens details instead of entering
</label>
</div>
<DataExplorer
source={source}
focus={focus}
onSettle={(next) => {
setFocus(next);
say(`onSettle: ${next ?? "top"}`);
}}
onNodeSelect={(item) => say(`onNodeSelect: ${name(item)}${item?.data ? `, ${item.data.moons} moons` : ""}`)}
onNodeHover={(item) => item && say(`onNodeHover: ${name(item)}`)}
onEnter={({ heading, direction }) => say(`onEnter: ${heading} (${direction})`)}
onLeave={({ heading, direction }) => say(`onLeave: ${heading} (${direction})`)}
onBackgroundClick={({ world }) => say(`onBackgroundClick at ${world.x.toFixed(0)}, ${world.y.toFixed(0)}`)}
onNodeActivate={(event) => {
say(`onNodeActivate: ${event.item.label} by ${event.source}`);
if (openInstead) {
event.preventDefault(); // the explorer will not enter it
say(` ...details for ${event.item.label} would open here`);
}
}}
/>
<pre className="demo-log" aria-live="polite">
{log.join("\n") || "Click, hover or double-click an item."}
</pre>
</>
);
}
Event Fires Payload
onSettle The camera came to rest on a scene, once per burst of motion. the focus
onSceneChange Every scene swap, immediately, including a scene filling in as it streams. Good for a breadcrumb. { heading, focus, scene }
onNodeSelect A confirmed single click or tap on an item, or null on empty space. the item, or null
onNodeHover The item under the mouse changes. Mouse only. the item, or null
onNodeActivate A double-click or double-tap on an item, or Enter on its outline entry. { item, source, preventDefault() }
onLeave, onEnter The viewer left a scene, then arrived at one, by any route. { heading, focus, scene, direction }
onCameraChange The camera moved: at most once per drawn frame. { x, y, scale }
onBackgroundClick A confirmed single click or tap on empty space. { screen, world }
onTextureError An item image could not be drawn; once per URL. { url }

direction is "in" (entering an item), "out" (zooming out) or "jump" (a focus change from outside, such as Back). A scene filling in as it streams, or refreshing after invalidate, is not a navigation and fires neither.

onNodeActivate is cancellable. By default the explorer enters the item if it can, and for a pointer otherwise zooms in at that point. Call preventDefault() to do something else, such as opening a panel: tick the box in the demo and double-click a planet.