Charts

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.

Preview in your theme

Loading preview…

"use client"

import * as React from "react"
import * as THREE from "three"
import { Canvas } from "@react-three/fiber"
import { Html, Line, OrbitControls } from "@react-three/drei"
import { AlertCircle, Globe2, RefreshCcw } from "lucide-react"

import { cn } from "@/lib/utils"
import type { Chart3dGlobeData } from "./chart-3d-globe.contract"

export interface Chart3dGlobeProps
  extends Omit<React.HTMLAttributes<HTMLDivElement>, "title">,
    Chart3dGlobeData {

Installation

npx shadcn@latest add https://ui.zyeon.ai/r/chart-3d-globe.json

Prompt

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.Color can't parse oklch, 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.

On This Page