A streaming atlas
The optional members: loadStreaming with expectedItemCount (countries arrive in pages, into room reserved for them), invalidate() (mark a country visited and its ring appears in place), a custom ramp for population density, and live palette and background switches: plain, gradient and image backdrops, and the grid and halo toggled.
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 ExplorerNode, type ExplorerScene, type RampColours, type SceneSource,} from "@data-xplorer/core";import { delay } from "../shared/delay";import { CONTINENTS, continentNamed, wiki, type Country, type VisitedStore } from "./data";
/** * The optional half of the contract: * * - `loadSceneStreaming`: a continent's countries arrive a few at a time, as a paged API would * return them. The explorer draws the first page at once and folds the rest in, and * `expectedItemCount` reserves room for all of them so the layout never has to grow. * - `subscribeToInvalidation` + `refreshScene`: marking a country visited changes data behind a * scene that is already on screen, and the explorer redraws it in place, camera untouched. */type ContinentName = string;
const PAGE_SIZE = 4;const PAGE_MS = 350;
/** Sparse places blue, crowded places orange, by way of purple and red rather than back through * green: the middle stops steer the hue, and `hsl` turns it the shorter way between each pair. */export const DENSITY_RAMP: RampColours = { stops: ["hsl(215 75% 58%)", "hsl(272 75% 58%)", "hsl(328 75% 58%)", "hsl(25 75% 58%)"], interpolation: "hsl",};const DENSEST = 1500; // people per km², just above the densest country listed
function countryNode(country: Country, visited: VisitedStore): ExplorerNode<ContinentName> { const density = (country.population * 1000) / country.area; // people per km² const been = visited.has(country.name); return { id: country.name, label: country.name, sublabel: `${country.population} million`, weight: country.population, marks: [ `${country.population} million people`, `${Math.round(density)} per km²`, ...(been ? ["visited"] : []), ], href: wiki(country.name), drillsInto: null, shape: "disc", ramp: Math.log(Math.max(density, 1)) / Math.log(DENSEST), // log scale: 2 per km² and 1,000 per km² both read emphasis: been ? ["ring"] : [], // US071 — a visited country asks for a pulse; the view's `effects` decide how it shows. effects: been ? ["pulse"] : [], };}
function continentCluster(name: string, visited: VisitedStore): ExplorerCluster<ContinentName> { const countries = continentNamed(name)!.countries; const population = Math.round(countries.reduce((sum, country) => sum + country.population, 0)); const been = countries.filter((country) => visited.has(country.name)).length; return { id: name, label: name, sublabel: `${countries.length} countries`, weight: population, marks: [ `${population} million people (listed countries)`, `visited ${been} of ${countries.length}`, ], href: wiki(name), drillsInto: name, shape: "cloud", truncated: null, emphasis: been > 0 ? ["ring"] : [], };}
function sceneFor( name: ContinentName | null, visited: VisitedStore, countryLimit = Infinity,): ExplorerScene<ContinentName> { if (name === null) { return { id: "world", heading: "World", clusters: CONTINENTS.map((continent) => continentCluster(continent.name, visited)), nodes: [], edges: [], }; } const countries = continentNamed(name)!.countries; return { id: name, heading: name, clusters: [], nodes: countries.slice(0, countryLimit).map((country) => countryNode(country, visited)), edges: [], expectedItemCount: countries.length, };}
export function atlasSource(visited: VisitedStore): SceneSource<ContinentName> { const source = createSceneSource<ContinentName>({ load: async (name) => { await delay(400); return sceneFor(name, visited); },
// Resolve with the first page, then keep paging; each update is the fuller scene so far, and // the source caches the fullest it has seen, so a later visit comes straight from the cache. async loadStreaming(name, onUpdate) { const total = continentNamed(name)!.countries.length; await delay(400); void (async () => { for (let shown = PAGE_SIZE * 2; shown < total + PAGE_SIZE; shown += PAGE_SIZE) { await delay(PAGE_MS); onUpdate(sceneFor(name, visited, shown)); } })(); return sceneFor(name, visited, PAGE_SIZE); },
parentOf: (name) => ({ kind: "anchored", focus: null, anchorId: name }), }); // Marking a country visited changes every scene that shows it or counts it. visited.subscribe(() => source.invalidate()); return source;}
export const decodeContinent = (hash: string) => (continentNamed(hash) ? hash : null);import { useState, useSyncExternalStore } from "react";import { createRoot } from "react-dom/client";import { FALLBACK_PALETTE, type ExplorerBackground, type ExplorerGridOption, type ExplorerEffects, type ExplorerPalette } from "@data-xplorer/core";import { ExampleShell } from "../shared/ExampleShell";import { visitedStore, type VisitedStore } from "./data";import { atlasSource, decodeContinent, DENSITY_RAMP } from "./source";import contoursUrl from "./contours.svg";
const LIGHT_PALETTE: ExplorerPalette = { background: 0xf7f8fb, grid: 0x526079, label: 0x0f172a, labelStroke: 0xffffff, selection: 0x0f172a, ownership: 0x1d4ed8, aggregate: 0x526079, hover: 0x0f172a,};
/** US071 — three backdrops, in the forms `background` accepts. The image is a same-origin SVG: * a canvas can only draw pixels its page is allowed to read. */const BACKDROPS: { name: string; dark: ExplorerBackground; light: ExplorerBackground }[] = [ { name: "plain", dark: {}, light: {} }, { name: "gradient", dark: { gradient: { from: "#0b1a33", to: "#020409", angle: 160 } }, light: { gradient: { from: "#eef4ff", to: "#fdf6ec", angle: 160 } }, }, { name: "image", dark: { color: "#06070c", image: { url: contoursUrl, alpha: 0.5 } }, light: { color: "#fbfaf6", image: { url: contoursUrl, alpha: 0.25 } }, },];
/** US071 — the same `pulse` request, three ways: breathing, held still, or switched off. */const GLOWS: { name: string; effects: ExplorerEffects }[] = [ { name: "pulse", effects: {} }, { name: "still glow", effects: { pulse: false } }, { name: "ring only", effects: { glow: false } },];
const visited = visitedStore(["France", "Japan", "Peru"]);const source = atlasSource(visited);
function VisitedToggle({ store, name }: { store: VisitedStore; name: string }) { useSyncExternalStore(store.subscribe, store.version); return ( <button type="button" onClick={() => store.toggle(name)}> {store.has(name) ? "Unmark visited" : "Mark visited"} </button> );}
/** The grid, in the forms `background.grid` accepts: the built-in dots, a host-drawn tile, or none. */const STUD_TILE = () => import("./stud-tile");const GRIDS: { name: string; option: ExplorerGridOption }[] = [ { name: "dots", option: true }, { name: "studs", option: { tile: STUD_TILE, step: 80, zoom: "continuous", alpha: 0.5 } }, { name: "off", option: false },];
function Atlas() { const [mode, setMode] = useState<"dark" | "light">("dark"); // US071 — declarative options, applied live: no new scene, no camera reset. const [grid, setGrid] = useState(0); const [backdrop, setBackdrop] = useState(0); const [glow, setGlow] = useState(0); const [halo, setHalo] = useState(true);
function switchMode() { const next = mode === "dark" ? "light" : "dark"; document.documentElement.dataset.mode = next; // the page's own colours setMode(next); // and the canvas's: a new palette object re-tints it live }
return ( <ExampleShell title="A streaming atlas" intro={ <p> Continents and a few of their countries, sized by population and coloured by density on a custom ramp (blue sparse, orange crowded). A continent's countries stream in a few at a time, the way a paged API delivers them. Select a country and mark it visited: its ring and pulse appear in place, with no reload and no camera jump. With reduced motion turned on in your system settings, the pulse holds still. The theme button re-tints the live canvas, and the backdrop, grid and halo buttons switch the{" "} <code>background</code> and <code>effects</code> options live. </p> } source={source} encode={(name) => name} decode={decodeContinent} trailOf={(name) => (name === null ? ["World"] : ["World", name])} palette={mode === "dark" ? FALLBACK_PALETTE : LIGHT_PALETTE} ramp={DENSITY_RAMP} background={{ ...BACKDROPS[backdrop]![mode], grid: GRIDS[grid]!.option }} effects={{ halo, ...GLOWS[glow]!.effects }} toolbar={ <> <button type="button" onClick={switchMode}> Switch to {mode === "dark" ? "light" : "dark"} </button> <button type="button" onClick={() => setBackdrop((backdrop + 1) % BACKDROPS.length)}> Backdrop: {BACKDROPS[backdrop]!.name} </button> <button type="button" onClick={() => setGrid((grid + 1) % GRIDS.length)}> Grid: {GRIDS[grid]!.name} </button> <button type="button" onClick={() => setGlow((glow + 1) % GLOWS.length)}> Visited: {GLOWS[glow]!.name} </button> <button type="button" aria-pressed={halo} onClick={() => setHalo(!halo)}> Visited halo {halo ? "on" : "off"} </button> </> } renderSelection={(item) => item.drillsInto !== null ? null : <VisitedToggle store={visited} name={item.id} /> } /> );}
createRoot(document.getElementById("root")!).render(<Atlas />);/** * A small atlas: a few countries per continent, with population (millions) and land area * (thousand km²). Figures are rounded and approximate; they are here to give the explorer * something real-looking to size and colour, not as a reference. */export interface Country { name: string; population: number; // millions area: number; // thousand km²}
export interface Continent { name: string; countries: Country[];}
const c = (name: string, population: number, area: number): Country => ({ name, population, area });
export const CONTINENTS: Continent[] = [ { name: "Asia", countries: [ c("India", 1430, 3287), c("China", 1410, 9597), c("Indonesia", 278, 1905), c("Pakistan", 240, 882), c("Bangladesh", 173, 148), c("Japan", 124, 378), c("Philippines", 117, 300), c("Vietnam", 99, 331), c("Thailand", 72, 513), c("South Korea", 52, 100), c("Nepal", 30, 147), c("Mongolia", 3.4, 1564), ], }, { name: "Africa", countries: [ c("Nigeria", 224, 924), c("Ethiopia", 127, 1104), c("Egypt", 113, 1002), c("Democratic Republic of the Congo", 102, 2345), c("Tanzania", 67, 945), c("South Africa", 62, 1221), c("Kenya", 55, 580), c("Morocco", 37, 447), c("Ghana", 34, 239), c("Namibia", 2.6, 824), ], }, { name: "Europe", countries: [ c("Germany", 84, 357), c("France", 68, 552), c("United Kingdom", 68, 243), c("Italy", 59, 301), c("Spain", 48, 506), c("Poland", 37, 313), c("Netherlands", 18, 42), c("Sweden", 10.5, 450), c("Portugal", 10.4, 92), c("Greece", 10.4, 132), c("Norway", 5.5, 385), c("Ireland", 5.3, 70), ], }, { name: "North America", countries: [ c("United States", 335, 9834), c("Mexico", 129, 1964), c("Canada", 40, 9985), c("Guatemala", 18, 109), c("Cuba", 11, 110), c("Costa Rica", 5.2, 51), c("Jamaica", 2.8, 11), ], }, { name: "South America", countries: [ c("Brazil", 216, 8516), c("Colombia", 52, 1142), c("Argentina", 46, 2780), c("Peru", 34, 1285), c("Venezuela", 28, 916), c("Chile", 19.6, 756), c("Ecuador", 18, 284), c("Bolivia", 12, 1099), c("Uruguay", 3.4, 176), ], }, { name: "Oceania", countries: [ c("Australia", 27, 7692), c("Papua New Guinea", 10, 463), c("New Zealand", 5.2, 268), c("Fiji", 0.9, 18), c("Samoa", 0.2, 2.8), ], },];
export const continentNamed = (name: string) => CONTINENTS.find((continent) => continent.name === name) ?? null;
export const wiki = (title: string) => `https://en.wikipedia.org/wiki/${title.replaceAll(" ", "_")}`;
/** * Which countries the viewer has visited: personal data, changed from the page. A real app would * keep it on a server; what matters here is that it changes underneath scenes already on screen. */export function visitedStore(initial: string[]) { const visited = new Set(initial); const listeners = new Set<() => void>(); let version = 0; return { has: (name: string) => visited.has(name), toggle(name: string) { if (!visited.delete(name)) visited.add(name); version++; listeners.forEach((listener) => listener()); }, subscribe(listener: () => void) { listeners.add(listener); return () => listeners.delete(listener); }, version: () => version, };}
export type VisitedStore = ReturnType<typeof visitedStore>;