Skip to content
UI

Chart

A Recharts wrapper that colors series from the chart tokens and adds a matching tooltip and legend.

@zenoxai/ui/components/chart Markdown
function ChartDemo() {
  const data = [
    { day: "Mon", opened: 42, resolved: 38 },
    { day: "Tue", opened: 57, resolved: 44 },
    { day: "Wed", opened: 35, resolved: 49 },
    { day: "Thu", opened: 61, resolved: 52 },
    { day: "Fri", opened: 48, resolved: 55 },
    { day: "Sat", opened: 19, resolved: 21 },
    { day: "Sun", opened: 14, resolved: 12 },
  ]
  const config = {
    opened: { label: "Opened", color: "var(--chart-1)" },
    resolved: { label: "Resolved", color: "var(--chart-2)" },
  } satisfies ChartConfig

  return (
    <ChartContainer config={config} className="aspect-auto h-64 w-full">
      <BarChart data={data}>
        <CartesianGrid vertical={false} />
        <XAxis dataKey="day" tickLine={false} axisLine={false} tickMargin={8} />
        <YAxis tickLine={false} axisLine={false} width={32} />
        <ChartTooltip content={<ChartTooltipContent />} />
        <ChartLegend content={<ChartLegendContent />} />
        <Bar dataKey="opened" fill="var(--color-opened)" radius={2} />
        <Bar dataKey="resolved" fill="var(--color-resolved)" radius={2} />
      </BarChart>
    </ChartContainer>
  )
}

Usage

import { Bar, BarChart, XAxis } from "recharts"

import {
  ChartContainer,
  ChartTooltip,
  ChartTooltipContent,
  type ChartConfig,
} from "@zenoxai/ui/components/chart"

const config = {
  opened: { label: "Opened", color: "var(--chart-1)" },
} satisfies ChartConfig

export function AlertsPerDay({ data }: { data: AlertsPerDayRow[] }) {
  return (
    <ChartContainer config={config} className="h-64 aspect-auto w-full">
      <BarChart data={data}>
        <XAxis dataKey="day" />
        <ChartTooltip content={<ChartTooltipContent />} />
        <Bar dataKey="opened" fill="var(--color-opened)" />
      </BarChart>
    </ChartContainer>
  )
}

Recharts 3 is an optional peer dependency of @zenoxai/ui. Install it in the app that renders charts:

pnpm add recharts

Import the chart parts from @zenoxai/ui/components/chart. No other subpath imports Recharts, so apps without charts do not need it. The root entry, @zenoxai/ui, re-exports this module: an app that imports from the root needs Recharts installed even if it renders no chart.

You write the chart with Recharts components; the library adds three things:

  • ChartContainer makes the chart responsive, styles the axes, grid and cursor with the theme tokens, and turns each key of config into a CSS variable, --color-<key>, scoped to the chart. Series reference it: fill="var(--color-opened)".
  • ChartTooltipContent and ChartLegendContent render the tooltip and legend with the labels from config, so the data can keep short keys.
  • ChartConfig types the config.

Give ChartContainer a height. It defaults to a 16:9 aspect ratio; replace it with aspect-auto and a height class for a fixed-height chart, or aspect-square for a donut.

Colors

Set color to one of the five chart tokens. They have light and dark values, so the chart follows the theme with no extra config. Assign them in order, --chart-1 first, and keep the same color for the same series across the charts of a page.

Token Color
--chart-1 Blue, the brand primary
--chart-2 Green
--chart-3 Orange
--chart-4 Gray
--chart-5 Light blue, a deeper step of the brand accent on light
--chart-line Red, for a line drawn over the series, such as a threshold

Every chart color reaches 3:1 against the surface in both themes, as WCAG asks of a data mark.

For a series that needs different colors per theme outside the tokens, pass theme: { light, dark } instead of color. Do not use the chart tokens for severity: severity has its own scale, used by SeverityBadge.

Examples

Area chart

An area chart suits one series over a long period, such as alerts per day over two weeks. minTickGap on the axis drops labels that would collide.

function ChartArea() {
  const data = [
    { date: "Oct 10", alerts: 132 },
    { date: "Oct 11", alerts: 98 },
    { date: "Oct 12", alerts: 87 },
    { date: "Oct 13", alerts: 154 },
    { date: "Oct 14", alerts: 171 },
    { date: "Oct 15", alerts: 149 },
    { date: "Oct 16", alerts: 238 },
    { date: "Oct 17", alerts: 196 },
    { date: "Oct 18", alerts: 104 },
    { date: "Oct 19", alerts: 91 },
    { date: "Oct 20", alerts: 162 },
    { date: "Oct 21", alerts: 177 },
    { date: "Oct 22", alerts: 158 },
    { date: "Oct 23", alerts: 143 },
  ]
  const config = {
    alerts: { label: "Alerts", color: "var(--chart-1)" },
  } satisfies ChartConfig

  return (
    <ChartContainer config={config} className="aspect-auto h-56 w-full">
      <AreaChart data={data} margin={{ left: 8, right: 8 }}>
        <CartesianGrid vertical={false} />
        <XAxis
          dataKey="date"
          tickLine={false}
          axisLine={false}
          tickMargin={8}
          minTickGap={24}
        />
        <ChartTooltip
          cursor={false}
          content={<ChartTooltipContent indicator="line" />}
        />
        <Area
          dataKey="alerts"
          type="monotone"
          fill="var(--color-alerts)"
          fillOpacity={0.2}
          stroke="var(--color-alerts)"
        />
      </AreaChart>
    </ChartContainer>
  )
}

Donut

A pie chart reads colors from the data: give each row a fill that points at its config key, and pass nameKey to the tooltip and legend so they find the labels. A Label inside Pie draws the total in the hole. Keep a donut to five slices or fewer.

function ChartDonut() {
  const data = [
    { source: "endpoint", alerts: 412, fill: "var(--color-endpoint)" },
    { source: "identity", alerts: 167, fill: "var(--color-identity)" },
    { source: "email", alerts: 89, fill: "var(--color-email)" },
    { source: "cloud", alerts: 64, fill: "var(--color-cloud)" },
    { source: "network", alerts: 38, fill: "var(--color-network)" },
  ]
  const config = {
    alerts: { label: "Alerts" },
    endpoint: { label: "Endpoint", color: "var(--chart-1)" },
    identity: { label: "Identity", color: "var(--chart-2)" },
    email: { label: "Email", color: "var(--chart-3)" },
    cloud: { label: "Cloud", color: "var(--chart-4)" },
    network: { label: "Network", color: "var(--chart-5)" },
  } satisfies ChartConfig
  const total = data.reduce((sum, item) => sum + item.alerts, 0)

  return (
    <ChartContainer config={config} className="aspect-square h-72">
      <PieChart>
        <ChartTooltip
          content={<ChartTooltipContent nameKey="source" hideLabel />}
        />
        <Pie data={data} dataKey="alerts" nameKey="source" innerRadius={60}>
          <Label
            content={({ viewBox }) => {
              if (!viewBox || !("cx" in viewBox)) return null
              const { cx = 0, cy = 0 } = viewBox
              return (
                <text x={cx} y={cy} textAnchor="middle">
                  <tspan
                    x={cx}
                    y={cy}
                    className="fill-foreground text-2xl font-semibold"
                  >
                    {total.toLocaleString("en-US")}
                  </tspan>
                  <tspan x={cx} y={cy + 20} className="fill-text-secondary">
                    Alerts
                  </tspan>
                </text>
              )
            }}
          />
        </Pie>
        <ChartLegend content={<ChartLegendContent nameKey="source" />} />
      </PieChart>
    </ChartContainer>
  )
}

Accessibility

  • Recharts 3 turns on its accessibility layer by default: the chart is focusable, and in bar, line and area charts the arrow keys move the tooltip between data points.
  • A chart is not readable by every user. Put the key figure in text next to it, and offer the data as a Table or an export where the numbers matter.
  • Color tells series apart only with a legend or a tooltip. Show the legend whenever there is more than one series.

API reference

Generated from the TypeScript source. className, style and children are accepted everywhere and omitted below.

ChartContainer

ChartContainer wraps one Recharts chart: it makes the chart responsive, styles it with the theme tokens and turns `config` into the `--color-<key>` variables its series use. Size it with a height or an aspect class; it defaults to aspect-video. For a single figure, use the StatTile pattern.

PropTypeDefault
config*ChartConfig

Label, icon and color of each data key of the chart.

-
initialDimension{ width: number; height: number; }

Size used until the container measures itself, as on the server.

{ width: 320, height: 200 } as const

ChartTooltip

The Tooltip component displays a floating box with data values when hovering over or clicking on chart elements.

No props of its own. Accepts the props of the element it renders.

ChartTooltipContent

ChartTooltipContent is the themed tooltip body: a heading, then a swatch, label and value per series, labeled from the chart config. Values are formatted with toLocaleString() in the browser's locale; pass `formatter` to format them in the app's locale.

PropTypeDefault
hideIndicatorboolean

Hides the color swatch beside each series.

false
hideLabelboolean

Hides the heading, the label of the hovered point.

false
indicator"line" | "dot" | "dashed"

Shape of the swatch: `dot` (default), `line` or `dashed`.

dot
labelKeystring

Config key whose label becomes the heading, or the payload field holding it. Defaults to the label of the hovered point.

-
nameKeystring

Config key that names each series, or the payload field holding it. Defaults to each item's name, then its data key.

-

ChartLegend

ChartLegend is Recharts' Legend; pass `content={<ChartLegendContent />}` to render it in the theme.

No props of its own. Accepts the props of the element it renders.

ChartLegendContent

ChartLegendContent is the themed legend: a swatch and label per series.

PropTypeDefault
hideIconboolean

Shows the color swatch even where the config sets an icon.

false
nameKeystring

Config key that names each series, or the payload field holding it.

-

ChartStyle

ChartStyle writes the `--color-<key>` variables of a config for one chart. ChartContainer renders it already; use it only in a custom container.

PropTypeDefault
config*ChartConfig-
id*string-