Charts
3D Heightmap Matrix
A four-state 3D lego-plot matrix — rows × columns of WebGL columns whose height and colour intensity both encode one value, with orbit controls and hover tooltips.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/chart-3d-heightmap.jsonPrompt
Build a React + TypeScript + Tailwind "Chart3dHeightmap" dashboard card on
three.js via @react-three/fiber + @react-three/drei, with zod. It renders a
rows × columns value matrix as a lego plot: one square column per cell, whose
height AND colour intensity encode the same value.
Contract
- A zod schema is the single source of truth:
{ status: "loading" | "empty" | "error" | "ready"; title: string;
rowLabels: string[]; colLabels: string[];
values: number[][] (row-major, nonnegative — values[r][c] pairs with
rowLabels[r] × colLabels[c]); unit?: string }.
- Component props = z.infer of the schema, plus onRetry?: () => void and the
usual div props (className merged with cn(), rest spread on the root).
- The label lists define the grid: missing matrix entries render as flat zero
pads, extras beyond the label counts are ignored — normalize once in a memo
so the render loop never guards again.
Behavior
- Four first-class branches inside one bg-card shell: loading (pulsing DOM
bar skeleton, aria-hidden, motion-reduce:animate-none), empty (icon + copy,
also taken when either label list is empty), error (message + "Try again"
rendered only when onRetry exists), ready (the WebGL scene).
- SSR must never touch WebGL: gate <Canvas> behind a client-mounted flag
(useSyncExternalStore with a server snapshot of false) and keep showing the
loading skeleton until the client has mounted AND tokens are resolved.
- Token → pixel → material: THREE.Color cannot parse modern CSS color
functions, so resolve each token with getComputedStyle, paint it onto a 1×1
offscreen 2D canvas, read the pixel channels back, and setRGB with the
SRGBColorSpace flag. Keep the alpha byte and multiply it into the floor
materials' opacity (dark themes often define --border as low-alpha white,
and dropping alpha would render it as opaque white); skip the paint when a
token resolves to an empty string and treat a fully transparent read-back
as a missing token — stand in a neutral mid-grey instead of returning
whatever colour the shared probe held last. Re-resolve through a
MutationObserver on the html element's class attribute (that is where the
theme flips) and disconnect the observer on unmount.
- Columns: footprint ~0.8 of a 1-unit floor cell; height = value / max × 2.4
world units, with a small floor for tiny values and a flat pad for exact
zeros; tint = lerp(muted → chart-1) by the same ratio — two channels, one
number.
- Hover via R3F pointer events on each mesh: outline the column with an
edges-geometry wireframe in the muted-foreground colour (never brighten the
face — that would fake a different value on the intensity ramp) and float a
drei <Html> tooltip above it: "rowLabel · colLabel" plus the value + unit.
Guard pointer-out against out-of-order events between neighbouring columns,
and clear the hover when the pointer leaves the wrapper.
- OrbitControls with damping; autoRotate only while prefers-reduced-motion
does not match AND nothing is hovered; pan disabled and the polar angle
clamped so the floor can never flip over.
- Cleanup: the MutationObserver, the matchMedia listener and the one owned
geometry are all released on unmount; fiber disposes the declarative rest.
Rendering & styling
- Semantic tokens only, resolved at runtime: columns lerp var(--muted) →
var(--chart-1); inner floor grid lines wear --border, the outer frame and
the hover outline wear --muted-foreground. Tick labels and the tooltip are
DOM (drei <Html>) styled with token classes (text-muted-foreground on a
translucent bg-card chip so a label stays legible whenever a column swings
behind it; bg-popover + border for the tooltip) — no hex, no raw palette
values anywhere in source.
- <Canvas dpr={[1,2]}> inside a w-full aspect-[16/10] overflow-hidden
role="img" wrapper with a descriptive aria-label; ambientLight plus a key
and a fill directionalLight; meshStandardMaterial, roughness ~0.45.
- Below the canvas: a caption explaining the encoding and the gestures, and a
0 → max gradient scale strip built from the same two tokens.
- Accessibility: an sr-only summary sentence (dimensions, peak cell, total)
plus a full <table> of the matrix with row and column headers — the exact
numbers never depend on WebGL or a pointer.
Customization levers
- Ramp endpoints: swap the lerp pair (muted → chart-1) for any two tokens,
e.g. chart-2 → chart-5 for a two-hue ramp; keep one perceptual direction so
intensity still reads as magnitude.
- Density and drama: the column footprint (0.6 airy … 0.9 lego-dense) and the
max world height (1.6 flat terrain … 3.5 dramatic) are single constants.
- Motion: autoRotateSpeed (0 kills the idle spin entirely), dampingFactor,
and the polar-angle clamps set how far the reader can tilt.
- Camera: the position formula scales with rows/cols, and the default pitch
is deliberately steep enough that the floor-edge row labels clear the
column silhouette — bias it higher for a map-like read, but not so low
that tall columns swallow the row axis again.
- Chrome: drop the caption or the scale strip for a bare embed; keep the
sr-only table — it is the accessible chart.Concepts
- Mount-gated WebGL — the
<Canvas>renders only after a client-only mounted flag flips, so the server and the hydration pass see nothing but the loading skeleton: SSR never touches a WebGL context, and the card keeps one silhouette while the scene arrives. - Token → pixel → material — THREE cannot parse the modern CSS color functions design tokens are written in, so each token is painted onto a one-pixel 2D canvas and read back as channels; a MutationObserver on the html class attribute re-runs the pipeline the moment the theme flips, so the 3D materials re-theme like any Tailwind class.
- Dual encoding — the same value/max ratio drives both the column's height and its tint along a single muted → chart-1 ramp; when perspective occludes a back row, colour still carries the reading, and in greyscale the height still does.
- Honest hover — the hovered column gets a wireframe collar instead of a brightness shift, because brightening the face would fake a different position on the intensity ramp; the exact number rides in a DOM tooltip (drei Html) styled with popover tokens, not a texture.
- Motion etiquette — the idle auto-rotate is decoration, so it stops for
prefers-reduced-motionand pauses while a column is hovered; orbiting stays available either way, because reading the data must not depend on the animation. - The table is the chart — a screen reader gets the summary sentence plus the full row × column table, so every cell is reachable without WebGL, a pointer, or colour perception.
3D Funnel
A four-state pipeline funnel of stacked 3D truncated cones — orbitable with damped controls, per-stage conversion labels, and theme tokens resolved into WebGL materials at runtime.
3D Radial Bars
A four-state 3D radial bar chart on react-three-fiber — bars ring a center, height encodes value, theme tokens resolved at runtime for WebGL, orbit camera and hover tooltips.