Charts
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.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/chart-3d-surface.jsonPrompt
Build a React + TypeScript + Tailwind "Chart3dSurface" widget on three +
@react-three/fiber + @react-three/drei, with zod.
Contract
- A zod schema is the single source of truth:
{ status: "loading" | "empty" | "error" | "ready"; title: string;
items: { id; label?; values: number[] }[]; // rows of a z grid
xLabels?: string[]; unit?: string }.
- Component props = z.infer of the schema, plus onRetry?: () => void,
autoRotate?: boolean (default true) and the usual div props spread on the
bg-card root. No hand-written parallel interface.
Behavior
- Four first-class branches in one rounded-xl border bg-card panel; only
ready mounts a <Canvas>. SSR must never touch WebGL: gate the canvas on a
client-only signal (useSyncExternalStore whose server snapshot is null, or
a mounted flag) and render the loading skeleton until it resolves.
- Token resolution: THREE.Color cannot parse oklch(), so read each token via
getComputedStyle(document.documentElement).getPropertyValue(...), paint it
onto an offscreen 1×1 2D canvas and read the rasterised rgb pixel back.
Re-resolve when a MutationObserver on the <html> class attribute fires
(theme flip); disconnect the observer on unmount.
- Geometry: one BufferGeometry over the rows × cols grid. Positions span a
fixed world size regardless of grid density; heights normalise zMin→0 and
zMax→PEAK; each vertex colour lerps low→high by its normalised height.
Two triangles per cell wound so computed normals face up; dispose the
geometry (and any helper) on rebuild and unmount.
- Guard degenerate input: fewer than 2 rows or 2 columns renders an honest
"not enough grid" message instead of a broken mesh; ragged rows clamp to
the shortest row; a flat grid (zMin === zMax) sits at mid-height.
- Hover: onPointerMove on the surface mesh converts event.point back to the
nearest row/col; a small marker sphere plus a drei <Html> tooltip
(bg-popover token classes) name the cell and its value; onPointerOut
clears it. Commit hover state only when the cell actually changes.
- OrbitControls with damping; autoRotate only while the prop is true AND
prefers-reduced-motion does not match; frameloop drops to "demand" when
the idle orbit is off.
Rendering & styling
- Zero hardcoded colours: the surface ramps var(--chart-2) → var(--chart-1),
the wireframe overlay and base GridHelper use --border, the hover marker
--foreground — all rasterised at runtime; the DOM legend gradient uses the
same two tokens straight in CSS, so WebGL and DOM can never disagree.
- <Canvas dpr={[1,2]} flat> (flat = no tone mapping, so colours stay true to
the tokens) inside a w-full aspect-[16/10] wrapper with role="img" and a
descriptive aria-label; an sr-only paragraph summarises rows × cols, the
min–max range and where the peak sits.
- ambientLight + directionalLight; meshStandardMaterial with vertexColors
and polygonOffset so the wireframe never z-fights; axis tick labels are
drei <Html> nodes, capped and evenly sampled — muted-foreground text on a
translucent rounded bg-card/85 chip, so labels stay legible when the idle
orbit carries them in front of the mesh. Grazing camera angles make tick
labels collide on screen, so a useFrame pass projects every tick and
greedily hides (opacity, with a short transition) any chip that would
overlap one already kept, rows before columns; start the camera far enough
back that the ground-ring labels stay in frame through a full orbit.
- cn() merges className; the retry button carries focus-visible ring tokens;
skeleton pulses respect motion-reduce.
Customization levers
- Palette ramp: swap TOKEN_LOW/TOKEN_HIGH (--chart-2 → --chart-1 by default)
for any two chart tokens; the DOM legend gradient follows for free.
- Relief strength: PEAK, the world height zMax maps to — lower for subtle
fields, higher for dramatic terrain.
- Camera & motion: initial position/fov, autoRotateSpeed, min/max zoom
distance, or autoRotate={false} for a calmer dashboard.
- Wireframe: opacity ≈ 0.25 by default; delete that mesh for a smooth sheet
or raise it for a technical CAD look.
- Label density: LABEL_CAP ticks per axis, sampled with endpoints kept —
raise it for sparse grids, or omit xLabels for a pure shape read.
- Footprint: the wrapper's aspect-[16/10] plus the SPAN/depth clamp are the
size levers; the grid's own row/col ratio drives depth automatically.Concepts
- Client-only WebGL gate — the server and the hydration pass render a plain-DOM skeleton; the
<Canvas>mounts strictly after hydration, so SSR, crawlers and no-WebGL environments never crash on a GL context. - Runtime token rasterisation —
THREE.Colorcannot parseoklch(), so each semantic token is painted onto a 1×1 2D canvas and read back as an rgb pixel; the browser performs the colour-space conversion, and the same trick works for any future CSS colour syntax. - Theme flip as a subscription — a MutationObserver on the
<html>class attribute is the store the palette subscribes to: flip dark mode and the surface, wireframe and marker re-resolve without a remount. - Height-mapped vertex colours — every vertex encodes its value twice, as elevation and as a
chart-2 → chart-1lerp, so the shape survives flat viewing angles and the colour survives silhouette views; the DOM legend uses the identical two tokens in plain CSS. - Inverse-projection hover — instead of per-cell hit targets, one pointer handler converts the world-space hit point back to the nearest grid indices; a marker sphere and an
<Html>tooltip follow that vertex, and state only commits when the cell changes. - Motion as a preference, frames as a budget — the idle orbit runs only without
prefers-reduced-motion, and when it is off the frameloop drops to demand mode, so a settled chart stops burning animation frames.
3D Scatter Cloud
An orbitable WebGL scatter cloud with cluster-colored spheres, axis grids and DOM tooltips, chart tokens resolved to three.js at runtime.
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.