3D Globe Arcs
A four-state 3D globe with graticule, value-sized location markers and elevated great-circle arcs between origin/destination pairs, coloured from the live chart tokens.
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/chart-3d-globe.jsonPrompt
Build a React + TypeScript + Tailwind "Chart3dGlobe" widget on three.js via
@react-three/fiber and @react-three/drei (Html, Line, OrbitControls), with zod.
Contract
- A zod schema is the single source of truth:
{ status: "loading" | "empty" | "error" | "ready"; title: string;
points: { id, label, lat -90..90, lng -180..180, value >= 0 }[];
arcs: { from, to, value >= 0 }[] referencing point ids; unit?: string }.
- Component props = z.infer of the schema, plus spin?: boolean (idle
auto-rotate, default true), onRetry?: () => void and the usual div props
spread on the root. No hand-written parallel interface.
Behavior
- Four first-class branches in one bg-card panel. loading: an aria-hidden
pulsing ring in the canvas's aspect box + two text bars, with an sr-only
role="status". empty (or zero points): icon + zero-data copy. error:
role="alert" + "Try again" only when onRetry exists. ready: the globe.
- SSR/WebGL gate: the <Canvas> mounts only behind a "mounted" client flag
(useSyncExternalStore with a server snapshot of false); before that the
ready branch renders the same skeleton, so server HTML never touches WebGL.
- Colors for materials are CSS tokens resolved at runtime: read each custom
property off document.documentElement, normalize it through a 1×1 offscreen
2D canvas (fillStyle → fillRect → getImageData) because THREE.Color cannot
parse oklch, then THREE.Color().setRGB(r,g,b, SRGBColorSpace). Re-resolve
when the theme flips via a MutationObserver on the <html> class attribute;
disconnect it — and the matchMedia listener — on unmount.
- Geometry: lat/lng → unit vector (phi/theta sphere mapping). Markers are
small spheres sized on a square-root scale against the max value. Arcs are
great circles: slerp between the two unit vectors, lifted by a half-sine
bump whose height grows with the angular distance. Graticule = one
lineSegments soup of parallels + meridians built once at module load.
- Arcs whose from/to don't resolve to a point id (or resolve to the same one)
are skipped and counted in a visible role="status" line — never swallowed.
- Hover: R3F pointer events on markers set a hovered id; a drei <Html> tooltip
(token classes: bg-popover, border, text-popover-foreground) follows the
marker. The ocean sphere calls stopPropagation on pointerover/move so
markers on the far side never receive hover through the planet.
- OrbitControls: enableDamping, no pan, clamped zoom; autoRotate only when
spin && !prefers-reduced-motion, checked live via matchMedia.
Rendering & styling
- Zero hardcoded colors: markers and arcs use the resolved --chart-1..5
(arc inherits its origin marker's slot), ocean --muted, graticule --border;
the DOM legend dots use var(--chart-N) directly. Panel is rounded-xl border
bg-card p-6; cn() merges className.
- Lights: one ambientLight plus a key and a weak fill directionalLight;
meshStandardMaterial everywhere (matte ocean, roughness ≈ 0.95).
- The canvas wrapper is a fixed aspect-[4/3] div with role="img" and a
descriptive aria-label; <Canvas dpr={[1,2]}> fills it. After the globe: a
visible per-location legend (dot + label + value), a one-line how-to-read
caption, and an sr-only summary listing every location and every route with
its value — the figures are never locked inside WebGL.
Customization levers
- Motion: spin prop and the autoRotateSpeed constant (0.55) for idle drift;
reduced motion always wins. Damping factor 0.08 for gesture inertia.
- Arc shape: the lift formula (0.08 + 0.35 · angle/π) — raise the second term
for dramatic flight paths, drop it near zero for surface-hugging routes;
ARC_SEGMENTS (48) for smoothness.
- Marker and arc scale: MIN/MAX_MARKER radii and MIN/MAX_ARC_WIDTH map value
ranges to size; swap the square-root marker scale for linear if the values
are close together.
- Graticule density: GRID_STEP (20°) and GRID_SEGMENTS; drop the opacity or
the parallels entirely for a cleaner hero look.
- Palette: slots cycle the five chart tokens by point index; pin a token per
region instead by replacing the slot assignment. Camera: fov 45 at
[0, 0.35, 3.1] frames the sphere with arc headroom — pull back for wide
hero embeds.Concepts
- WebGL mount gate — the server renders inert DOM (card, stats, skeleton, sr-only figures) and the
<Canvas>appears only after a client flag flips, so SSR and hydration never construct a renderer; the loading state and the pre-mount gap share one skeleton, so nothing jumps. - Token → pixel → THREE.Color — WebGL can't read a CSS class and
THREE.Colorcan't parseoklch, so each token is painted onto a 1×1 2D canvas and read back as channels; a MutationObserver on the<html>class re-runs the resolve, which is how the globe re-themes on a dark-mode flip without a reload. - Elevated great circles — an arc is a spherical interpolation between two unit vectors plus a half-sine lift that grows with angular distance: neighbours hug the surface, transcontinental routes fly high — height encodes distance, never a value, and the caption says so.
- Occlusion by stopPropagation — the ocean sphere stops pointer propagation, so a marker on the far side never gets hover events through the planet; the tooltip can only name what you can actually see.
- Honest degradation — arcs referencing unknown or identical points are counted in a visible status line; reduced motion kills the idle rotation but keeps drag, zoom and hover; every figure is repeated as sr-only text because a WebGL canvas is a bitmap to assistive tech.
- Origin-coloured routes — an arc inherits its origin marker's chart-token slot, so "everything leaving São Paulo" reads as one colour family across markers, arcs and the DOM legend — one slot assignment, three consumers.
3D Surface Plot
A four-state rotatable 3D surface from a z-value grid — vertex colors lerped between chart tokens by height, a subtle wireframe overlay and hover tooltips.
3D Ribbon Series
A four-state multi-series 3D ribbon chart on react-three-fiber — one extruded lane per series, theme tokens resolved into WebGL materials, hover isolates a ribbon.