Charts
3D Bar Grid
A four-state WebGL bar grid — categories along X, series lanes along Z, orbitable boxes on a ground grid with hover tooltips, driven by one contract.
Preview in your theme
Loading preview…
Installation
npx shadcn@latest add https://ui.zyeon.ai/r/chart-3d-bars.jsonPrompt
Build a React + TypeScript + Tailwind "Chart3dBars" widget on three.js via
@react-three/fiber and @react-three/drei (Canvas, Html, OrbitControls), with zod.
Contract
- A zod schema is the single source of truth:
{ status: "loading" | "empty" | "error" | "ready"; title: string;
categories: string[]; series: { label: string; values: number[] }[];
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,
forwardRef to the card).
- values align with categories by index: missing entries render as zero,
extras are ignored — normalize once in a memo, never in the render loop.
Behavior
- Four first-class branches in one bg-card panel: loading (pulsing bar-row
skeleton, aria-hidden, sr-only status text), empty (icon + zero-data copy,
also used when categories or series are empty), error (message + a
"Try again" button only when onRetry exists), ready (the 3D scene).
- SSR must never touch WebGL: gate the Canvas behind a mounted flag that is
false on the server and during hydration (useSyncExternalStore with a
never-firing subscribe, or an equivalent effect-set flag); until it flips,
the ready branch renders the same skeleton as loading.
- Scene: one box per (category, series) cell — categories along X, series
lanes along Z, height = value / max × a fixed world height, with a small
floor so a positive value never vanishes and a flat pad marking an exact
zero. Boxes stand on a line-segment ground grid, one cell per column/lane,
with axis tick labels at the grid edges.
- Hover: R3F pointer events set a { series, category } highlight (emissive
bump on that bar) and show one drei <Html> tooltip above it with
"series · category" and the formatted value; pointer-out clears it,
guarded so crossing between neighbouring bars doesn't blink.
- OrbitControls with damping, pan disabled, polar angle clamped above the
floor; autoRotate only while prefers-reduced-motion does not match AND no
bar is hovered (listen to the media query and clean the listener up).
Rendering & styling
- Never hardcode a material color. Resolve tokens at runtime: read
--chart-1..5 and --border with getComputedStyle, paint each value into a
1×1 offscreen 2D canvas, read the pixel back and build a THREE.Color from
the channels via setRGB with the sRGB color-space flag — THREE.Color
cannot parse the modern CSS color functions design tokens are written in.
Re-resolve when a MutationObserver sees the html element's class attribute
change (theme flip); disconnect the observer on unmount.
- Lane i's meshStandardMaterial uses resolved chart token (i mod 5) + 1; the
ground grid's lineBasicMaterial uses the border token; axis tick labels
are drei <Html> spans styled text-muted-foreground so they re-theme free.
- ambientLight + one key directionalLight + a soft fill light; transparent
canvas (default) so bg-card shows through; <Canvas dpr={[1,2]}> inside a
fixed-aspect w-full wrapper.
- The canvas wrapper is role="img" with a descriptive aria-label; an sr-only
block lists every series with its per-category values, and a visible
caption states the peak value plus the orbit/zoom/hover affordances.
Customization levers
- Footprint & drama: BAR_W/BAR_D inside the 1-unit cell (city blocks vs thin
columns) and MAX_H for the tallest bar's height.
- Camera: fov and the position formula scale with category/lane counts —
tighten for a hero embed, widen for dense dashboards; clamp min/max
distance to taste.
- Motion: autoRotateSpeed, damping factor, or drop autoRotate entirely; the
reduced-motion gate must stay.
- Palette: lanes cycle the five chart tokens — remap specific series to
fixed tokens (e.g. brand vs competitor) inside the resolver.
- Tooltip density: the drei Html card is plain token-styled DOM — add
share-of-total or a category rank row without touching the scene.Concepts
- Runtime token resolution — WebGL materials can't read CSS custom properties, so the chart paints each token into an offscreen 2D pixel and hands THREE the channels; one MutationObserver re-runs it on theme flip, keeping the scene in lock-step with light/dark.
- SSR gate — the server and the hydration pass render a plain DOM skeleton; the Canvas mounts one paint later on the client, so prerendering never touches WebGL and hydration never mismatches.
- Two encode axes, one height channel — position encodes the two dimensions (category × series) and height alone encodes the value, so the picture stays honest: no volume or color-ramp double-encoding.
- Hover raycast tooltip — R3F's pointer events raycast into the scene; the hit bar brightens via emissive and a single drei Html card projects above it, styled entirely with popover tokens.
- Motion respect — auto-rotation is the decorative part and it yields twice: to prefers-reduced-motion and to an active hover; orbiting by hand keeps working either way.
- Parallel text channel — role="img" with a data-bearing label plus an sr-only per-series value list means the scene is skippable, not silent, for screen readers.
Vector Field
A four-state quiver plot on a derived lattice: one centred arrow per node carrying direction, length and colour, evenly spaced streamlines traced through the same data, holes that stay holes, and a keyboard-reachable readout on every vector.
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.