Skip to main content

Charts

Package: @zuilib/charts · 0.4.0

Charts for analytics platforms, built on Recharts and styled only through ZUI tokens. The package has two layers:

  • A serializable spec. One <Chart spec={spec} rows={rows} /> component renders any of ten chart types from a plain-JSON ChartSpec — type, series, axes, formats, stacking, reference lines, brush. Because the spec is JSON, it can live in a database, come from a server, or be produced by an end-user chart builder. Formatting is named by token ('currency:USD', 'compact', 'percent', 'date:short'); custom formatter functions travel as React props, never inside the spec.
  • Styled primitives. ChartFrame, ChartTooltipPanel, ChartLegendList and themed default prop bags for hand-written Recharts composition when a spec is not enough.

Charts cross-filter data tables: inside a CrossFilterProvider, clicking a bar, slice or legend chip filters a linked @zuilib/data-grid (and any other chart), and grid selection echoes back as a highlight — without the charts ever importing the grid. See cross-filtering.

Loading example
Loading example

Install

pnpm add @zuilib/charts

React and React DOM are the required peers. Recharts, primitives and tokens install transitively. Install recharts directly only if your code imports it for hand-written composition. Load the component and chart Tailwind entries once; see the components getting started.

@import "tailwindcss";
@import "@zuilib/primitives/tailwind.css";
@import "@zuilib/charts/tailwind.css";

Chart types

Line, area, bar (grouped, stacked, percent, horizontal), pie/donut, scatter/bubble, composed (bar + line + area), radar, radial bar, funnel and treemap — the whole gallery with live specs is on the spec page.

Theming

Series colors come from eight new tokens, --chart-1--chart-8 — a categorical palette validated for color-vision-deficiency separation in both light and dark mode. Slots are assigned to series in fixed declaration order and never cycled; the order is the CVD mechanism, so a ninth series should be folded into "Other", not given a new hue.

A brand theme may override --chart-1 to its primary hue. Overriding more slots requires re-validating the full order (adjacent pairs must keep ΔE ≥ 8 under CVD simulation); an unvalidated reshuffle can silently make two neighboring series indistinguishable for a colorblind reader.

Everything else — grid lines (--border), tick labels (--muted-foreground), the tooltip panel (--popover), skeleton and empty states — reads the same tokens as the rest of ZUI, so themes and dark mode apply with no chart-specific work.

Small screens and touch

A chart fills its container. Leave width and height out and the height follows aspect (16 / 9) with a 200px floor, so a phone-width chart keeps a readable plot; pass height to fix the height and keep the width fluid. ChartFrame is a CSS size container (@container, min-w-0): it never holds a flex or grid parent open at the width of a previous render, so it can sit in any column without a min-w-0 of your own.

Under 384px the value axis sizes to its labels and the category axis keeps only its end ticks; the legend wraps and, on touch pointers, its buttons grow to 44px hit areas. Desktop rendering is unchanged.

Accessibility

  • The container is a figure with an accessible name (spec.title or ariaLabel); Recharts' keyboard accessibilityLayer stays on.
  • The legend is real buttons (aria-pressed, focus ring): click isolates a series, click again restores all, shift-click toggles one. Hidden entries dim but never disappear, so color stays attached to the entity.
  • Values in tooltips and labels wear text tokens, never the series color.
  • Mark entrance animation is disabled automatically under prefers-reduced-motion (and controllable via the animate prop).
  • Three of the light-mode palette slots sit below 3:1 against white by design (they must stay distinguishable from each other first); tooltips, the interactive legend and a paired table view are the relief. When a chart carries load-bearing values, keep a table view nearby — which is exactly what the cross-filter wiring to @zuilib/data-grid gives you.