Skip to content
Nizivo UI

ChartFrame

The <figure> every chart renders in, around any renderer, Nizivo's or a third party's. It owns the caption, the legend slot, the HTML tick labels, the server-rendered data table and the loading, empty and error statuses.

Import

import { ChartFrame } from '@nizivo/charts/chart-frame';

The contract every renderer may rely on is chart-frame.contract.ts in packages/charts/src/chart-frame (seam K9, §14.1).

Static or interactive

Server. ChartFrame has no 'use client' and imports no client module, so it ships no client JavaScript when a Server Component renders it. The tableView="details" disclosure is a native <details>, with no JavaScript. Its keyboard claim (interaction) is a statement about the child, not code: ChartFrame itself adds no Tab stop except the disclosure's summary. How to choose.

Examples

Basic

SalesDaily sales, Oct 1–7, vs previous period
  • This period
  • Previous
DayThis periodPrevious
Oct 1$2,140$1,980
Oct 2$3,020$2,210
Oct 3$2,760$2,450
Oct 4$4,310$2,900
Oct 5$3,880$3,120
Oct 6$5,240$3,060
Oct 7$6,280$3,410
import type { JSX } from 'react';
import { Legend } from '@nizivo/charts/legend';
import { ChartFrame, type ChartTable } from '@nizivo/charts/chart-frame';

// A week of daily sales, already formatted, as a Server Component would pass it.
const TABLE: ChartTable = {
  columns: ['Day', 'This period', 'Previous'],
  rows: [
    ['Oct 1', '$2,140', '$1,980'],
    ['Oct 2', '$3,020', '$2,210'],
    ['Oct 3', '$2,760', '$2,450'],
    ['Oct 4', '$4,310', '$2,900'],
    ['Oct 5', '$3,880', '$3,120'],
    ['Oct 6', '$5,240', '$3,060'],
    ['Oct 7', '$6,280', '$3,410'],
  ],
};

const X = ['Oct 1', '', '', 'Oct 4', '', '', 'Oct 7'];

const Y = ['$8k', '$6k', '$4k', '$2k', '$0'];

/** Stands in for a renderer: a percent-space SVG, as AreaChart draws it (viewBox 0 0 100 100). */
function Plot(): JSX.Element {
  return (
    <svg
      className="nz-chart-svg"
      viewBox="0 0 100 100"
      preserveAspectRatio="none"
      aria-hidden="true"
    >
      {[0, 25, 50, 75].map((y) => (
        <line key={y} className="nz-chart-grid" x1="0" x2="100" y1={y} y2={y} />
      ))}
      <line className="nz-chart-baseline" x1="0" x2="100" y1="100" y2="100" />
      <path
        className="nz-chart-area"
        data-series="current"
        d="M0,73.25L16.667,62.25L33.333,65.5L50,46.125L66.667,51.5L83.333,34.5L100,21.5V100H0Z"
      />
      <path
        className="nz-chart-line"
        data-series="previous"
        d="M0,75.25L16.667,72.375L33.333,69.375L50,63.75L66.667,61L83.333,61.75L100,57.375"
      />
      <path
        className="nz-chart-line"
        data-series="current"
        d="M0,73.25L16.667,62.25L33.333,65.5L50,46.125L66.667,51.5L83.333,34.5L100,21.5"
      />
    </svg>
  );
}

const LEGEND = (
  <Legend
    items={[
      { label: 'This period', series: 'current' },
      { label: 'Previous', series: 'previous' },
    ]}
  />
);

export function ChartFrameExample(): JSX.Element {
  return (
    <ChartFrame
      title="Sales"
      summary="Daily sales, Oct 1–7, vs previous period"
      table={TABLE}
      ticks={{ x: X, y: Y }}
      legend={LEGEND}
    >
      <Plot />
    </ChartFrame>
  );
}

Details

The data table as a native disclosure under the plot (zero JS).

Sales
Show data table
DayThis periodPrevious
Oct 1$2,140$1,980
Oct 2$3,020$2,210
Oct 3$2,760$2,450
Oct 4$4,310$2,900
Oct 5$3,880$3,120
Oct 6$5,240$3,060
Oct 7$6,280$3,410
import type { JSX } from 'react';
import { ChartFrame, type ChartTable } from '@nizivo/charts/chart-frame';

// A week of daily sales, already formatted, as a Server Component would pass it.
const TABLE: ChartTable = {
  columns: ['Day', 'This period', 'Previous'],
  rows: [
    ['Oct 1', '$2,140', '$1,980'],
    ['Oct 2', '$3,020', '$2,210'],
    ['Oct 3', '$2,760', '$2,450'],
    ['Oct 4', '$4,310', '$2,900'],
    ['Oct 5', '$3,880', '$3,120'],
    ['Oct 6', '$5,240', '$3,060'],
    ['Oct 7', '$6,280', '$3,410'],
  ],
};

const X = ['Oct 1', '', '', 'Oct 4', '', '', 'Oct 7'];

const Y = ['$8k', '$6k', '$4k', '$2k', '$0'];

/** Stands in for a renderer: a percent-space SVG, as AreaChart draws it (viewBox 0 0 100 100). */
function Plot(): JSX.Element {
  return (
    <svg
      className="nz-chart-svg"
      viewBox="0 0 100 100"
      preserveAspectRatio="none"
      aria-hidden="true"
    >
      {[0, 25, 50, 75].map((y) => (
        <line key={y} className="nz-chart-grid" x1="0" x2="100" y1={y} y2={y} />
      ))}
      <line className="nz-chart-baseline" x1="0" x2="100" y1="100" y2="100" />
      <path
        className="nz-chart-area"
        data-series="current"
        d="M0,73.25L16.667,62.25L33.333,65.5L50,46.125L66.667,51.5L83.333,34.5L100,21.5V100H0Z"
      />
      <path
        className="nz-chart-line"
        data-series="previous"
        d="M0,75.25L16.667,72.375L33.333,69.375L50,63.75L66.667,61L83.333,61.75L100,57.375"
      />
      <path
        className="nz-chart-line"
        data-series="current"
        d="M0,73.25L16.667,62.25L33.333,65.5L50,46.125L66.667,51.5L83.333,34.5L100,21.5"
      />
    </svg>
  );
}

export function ChartFrameDetailsExample(): JSX.Element {
  return (
    <ChartFrame title="Sales" table={TABLE} ticks={{ x: X, y: Y }} tableView="details">
      <Plot />
    </ChartFrame>
  );
}

Status

Loading, empty and error keep the frame's box (§16.5).

Sales
  • This period
  • Previous
Loading chart
Sales
  • This period
  • Previous

No data to show

Sales
  • This period
  • Previous

This chart could not be loaded

import type { JSX } from 'react';
import { Legend } from '@nizivo/charts/legend';
import { ChartFrame, type ChartTable } from '@nizivo/charts/chart-frame';

// A week of daily sales, already formatted, as a Server Component would pass it.
const TABLE: ChartTable = {
  columns: ['Day', 'This period', 'Previous'],
  rows: [
    ['Oct 1', '$2,140', '$1,980'],
    ['Oct 2', '$3,020', '$2,210'],
    ['Oct 3', '$2,760', '$2,450'],
    ['Oct 4', '$4,310', '$2,900'],
    ['Oct 5', '$3,880', '$3,120'],
    ['Oct 6', '$5,240', '$3,060'],
    ['Oct 7', '$6,280', '$3,410'],
  ],
};

const LEGEND = (
  <Legend
    items={[
      { label: 'This period', series: 'current' },
      { label: 'Previous', series: 'previous' },
    ]}
  />
);

export function ChartFrameStatusExample(): JSX.Element {
  return (
    <div>
      <ChartFrame title="Sales" status="loading" table={TABLE} legend={LEGEND} />
      <ChartFrame title="Sales" status="empty" table={TABLE} legend={LEGEND} />
      <ChartFrame title="Sales" status="error" table={TABLE} legend={LEGEND} />
    </div>
  );
}

Third party

A third-party renderer (any <svg>): the caller passes table and interaction="none", and the frame still gives the caption, the data table, the legend and the status (§14.1).

Orders
ChannelOrders
Online412
Retail268
import type { JSX } from 'react';
import { ChartFrame } from '@nizivo/charts/chart-frame';

export function ChartFrameThirdPartyExample(): JSX.Element {
  return (
    <ChartFrame
      title="Orders"
      table={{
        columns: ['Channel', 'Orders'],
        rows: [
          ['Online', '412'],
          ['Retail', '268'],
        ],
      }}
      axes="none"
      interaction="none"
    >
      <svg viewBox="0 0 200 80" width="100%" height="100%" role="presentation">
        <rect x="10" y="10" width="120" height="24" fill="#1a573a" />
        <rect x="10" y="46" width="78" height="24" fill="#6b8a1e" />
      </svg>
    </ChartFrame>
  );
}

The Charts overview has the same recipe with a client renderer such as Recharts.

Props

Server component. Props: ChartFrameElementProps. Source: src/chart-frame/chart-frame.tsx.

NameTypeDefaultDescription
axesChartAxes
"frame""none"
'frame'

From ChartFrameProps.

childrenReactNode–

The renderer: an <svg> (or anything) that fills the plot. Rendered only while ready.

From ChartFrameProps.

emptyReactNode–

Replaces the default empty message.

From ChartFrameProps.

errorReactNode–

Replaces the default error message, for example an InlineError with a retry action.

From ChartFrameProps.

interactionChartInteractionMode
"nizivo""none"
'nizivo'

From ChartFrameProps.

labelsPartial<ChartFrameLabels>–

From ChartFrameProps.

legendReactNode–

A <Legend>, or any node; rendered between the caption and the plot in every status.

From ChartFrameProps.

pendingboolean–

A refresh is in flight while the chart shows data (§16.5): aria-busy and data-pending.

From ChartFrameProps.

renderRenderProp<ChartFrameState>–

From ChartFrameAttributes.

statusDataStatus
"ready""loading""empty""error"
'ready'; a ready chart with no table rows renders as `'empty'`.

From ChartFrameProps.

summaryReactNode–

One line under the title, for example "Daily sales, Oct 1–30, vs previous period".

From ChartFrameProps.

tablerequiredChartTable–

From ChartFrameProps.

tableViewChartTableView
"hidden""details"
'hidden'

From ChartFrameProps.

ticksChartTicks–

From ChartFrameProps.

titlerequiredReactNode–

The figure's caption and its accessible name.

From ChartFrameProps.

titleVisibilityChartTitleVisibility
"visible""hidden"
'visible'

'hidden' hides the title and summary visually; they still name the figure.

From ChartFrameProps.

xDirectionChartXDirection
"ltr""rtl""auto"
'ltr'

From ChartFrameProps.

  • Also takes every <figure> attribute except children, title.

render takes an element, or, in client code, a function of the props and this state (ChartFrameState):

NameTypeDefaultDescription
interactionrequiredChartInteractionMode
"nizivo""none"
–

As passed: whether the child claims keyboard access to its points.

pendingrequiredboolean–
statusrequiredDataStatus
"ready""loading""empty""error"
–

The status as rendered: 'empty' when a ready chart has no rows.

ChartFrameProps in chart-frame.contract.ts (packages/charts/src/chart-frame) is the renderer seam's contract; ChartFrameElementProps adds the <figure>'s attributes, and every chart's own props include them too (one API shape across the charts, M40). With point-aligned labels (xAlign="point"), an x position whose label is '' renders an empty grid cell with no tick element, except the last cell, which holds both edge labels: a long daily series sends hundreds of empty positions. xDirection is explained under Right to left.

Hiding the caption

titleVisibility="hidden" hides the title and summary visually, for a chart whose container already titles it, such as a card header (Figma's Revenue Card draws no chart caption). The default is "visible". The caption is not removed: the <figcaption> gets nz-chart-visually-hidden (a 1 × 1 clipped box out of flow, never display: none), so it takes no layout space, stays in the server HTML, and still names the figure and its data table. Every chart takes the same prop (ChartFramePassthrough, M40's one API shape):

<AreaChart
  title="Revenue"
  summary="Daily, Oct 1–30"
  titleVisibility="hidden"
  data={days}
  x="date"
  y="sales"
/>

Keep title meaningful when it is hidden: it is still what a screen reader announces for the chart.

The English defaults of labels (ChartFrameLabels), in chartFrameLabels:

KeyDefaultWhere
loadingLoading chartVisually hidden text in the plot while loading
emptyNo data to showThe plot's message when empty, unless empty is passed
errorThis chart could not be loadedThe plot's message on error, unless error is passed
tableShow data tableThe summary of the tableView="details" disclosure

Other exports

ChartAxes type

import type { ChartAxes } from '@nizivo/charts/chart-frame'

'frame': ChartFrame draws the HTML tick labels (ticks). 'none': the child draws its own.

type ChartAxes = "frame" | "none"

ChartFrameAttributes type

import type { ChartFrameAttributes } from '@nizivo/charts/chart-frame'

ChartFrame's HTML side: the <figure>'s attributes and the render prop.

NameTypeDefaultDescription
renderRenderProp<ChartFrameState>–
ChartFrameElementProps type

import type { ChartFrameElementProps } from '@nizivo/charts/chart-frame'

type ChartFrameElementProps = ChartFrameProps & ChartFrameAttributes

The props of ChartFrame, listed above.

ChartFrameLabels type

import type { ChartFrameLabels } from '@nizivo/charts/chart-frame'

ChartFrame's own strings (§16.4): serialisable, English defaults in chartFrameLabels.

NameTypeDefaultDescription
emptyrequiredstring–

Shown in the plot when there is no data and no empty slot.

errorrequiredstring–

Shown in the plot when loading failed and there is no error slot.

loadingrequiredstring–

Visually hidden text in the plot while status="loading" (§16.5: no live region).

tablerequiredstring–

The summary of the tableView="details" disclosure.

ChartFrameProps type

import type { ChartFrameProps } from '@nizivo/charts/chart-frame'

The props every renderer may rely on (§14.1). The component also takes ChartFrameAttributes.

NameTypeDefaultDescription
axesChartAxes
"frame""none"
'frame'
childrenReactNode–

The renderer: an <svg> (or anything) that fills the plot. Rendered only while ready.

emptyReactNode–

Replaces the default empty message.

errorReactNode–

Replaces the default error message, for example an InlineError with a retry action.

interactionChartInteractionMode
"nizivo""none"
'nizivo'
labelsPartial<ChartFrameLabels>–
legendReactNode–

A <Legend>, or any node; rendered between the caption and the plot in every status.

pendingboolean–

A refresh is in flight while the chart shows data (§16.5): aria-busy and data-pending.

statusDataStatus
"ready""loading""empty""error"
'ready'; a ready chart with no table rows renders as `'empty'`.
summaryReactNode–

One line under the title, for example "Daily sales, Oct 1–30, vs previous period".

tablerequiredChartTable–
tableViewChartTableView
"hidden""details"
'hidden'
ticksChartTicks–
titlerequiredReactNode–

The figure's caption and its accessible name.

titleVisibilityChartTitleVisibility
"visible""hidden"
'visible'

'hidden' hides the title and summary visually; they still name the figure.

xDirectionChartXDirection
"ltr""rtl""auto"
'ltr'
ChartFrameState type

import type { ChartFrameState } from '@nizivo/charts/chart-frame'

What a render function receives.

The render state of ChartFrame, listed above.

ChartInteractionMode type

import type { ChartInteractionMode } from '@nizivo/charts/chart-frame'

'nizivo': the child is a Nizivo renderer, whose ChartInteraction island gives keyboard access to the points. 'none': a third-party child; the frame then makes no keyboard claim for the points, but still guarantees the caption, the data table, the legend and the status.

type ChartInteractionMode = "nizivo" | "none"

ChartTable type

import type { ChartTable } from '@nizivo/charts/chart-frame'

The accessible alternative to the picture (§14.1, §16): pre-formatted strings only.

NameTypeDefaultDescription
columnsrequiredreadonly string[]–

Column headers. columns[0] heads the row-header column (the x value or the category).

rowsrequiredReadonlyArray<readonly string[]>–

One row per datum, row[0] its header (<th scope="row">), the rest its formatted values.

ChartTableView type

import type { ChartTableView } from '@nizivo/charts/chart-frame'

How the data table shows. 'hidden': visually hidden, read by assistive technology (no chrome, as Figma 03 draws charts). 'details': a native <details> disclosure under the plot, so anyone can open the values (zero JS).

type ChartTableView = "hidden" | "details"

ChartTickAlign type

import type { ChartTickAlign } from '@nizivo/charts/chart-frame'

How x labels line up with the plot. 'point': one label per point of a point scale (lines and areas): the first sits at the plot's start edge, the last at its end edge, both clamped inward (Figma 03). 'band': one label centred in each band of a band scale (bars).

type ChartTickAlign = "point" | "band"

ChartTicks type

import type { ChartTicks } from '@nizivo/charts/chart-frame'

HTML tick labels on CSS grid tracks: one track per position, so no label needs a position of its own.

NameTypeDefaultDescription
xreadonly string[]–

One label per x position, '' where no tick falls (tickLabels() builds it).

xAlignChartTickAlign
"point""band"
'point'
yreadonly string[]–

Value labels from the top tick down to the baseline, evenly spaced: the ticks of a niced domain (valueDomain()), whose first and last ticks are the plot's top and bottom edges.

ChartTitleVisibility type

import type { ChartTitleVisibility } from '@nizivo/charts/chart-frame'

Whether the caption shows. 'visible': the title and summary above the plot. 'hidden': both visually hidden (clipped to a 1 × 1 box out of flow, never display: none), for a chart whose container already titles it (a card header, Figma's Revenue Card): the caption takes no layout space, stays in the HTML and still names the figure and its data table.

type ChartTitleVisibility = "visible" | "hidden"

ChartXDirection type

import type { ChartXDirection } from '@nizivo/charts/chart-frame'

The direction of the x axis. Time and value axes do not mirror by default (§14.1, §16.4). 'rtl' mirrors the plot and its x labels; 'auto' mirrors them when the page's dir is right-to-left (HTML directionality, CSS :dir(rtl)), in the server HTML already, and the ChartInteraction island reads the resolved direction after hydration.

type ChartXDirection = "ltr" | "rtl" | "auto"

DataStatus type

import type { DataStatus } from '@nizivo/charts/chart-frame'

The data lifecycle of every data-display part (§16.5). Declared structurally here because charts imports nothing from @nizivo/react; it mirrors the status of KpiTile's contract (§10.7, §16.5), which @nizivo/react 0.1.0 does not export yet. Keep the two identical.

type DataStatus = "ready" | "loading" | "empty" | "error"

chartFrameLabels constant

import { chartFrameLabels } from '@nizivo/charts/chart-frame'

English defaults of ChartFrame's strings (§16.4); pass labels to translate per instance.

const chartFrameLabels: ChartFrameLabels

Data attributes and slots

ElementAttributes
figure.nz-chart-framedata-slot="chart-frame", data-x-direction, data-loading, data-empty, data-error, data-pending, aria-busy
figcaption.nz-chart-frame-captiondata-slot="chart-frame-caption": .nz-chart-frame-title, .nz-chart-frame-summary; .nz-chart-visually-hidden with titleVisibility="hidden"
div.nz-chart-frame-legenddata-slot="chart-frame-legend", when legend is passed
div.nz-chart-frame-bodydata-slot="chart-frame-body": the y labels, the plot and the x labels
div.nz-chart-frame-plotdata-slot="chart-frame-plot": the child while ready, else the skeleton or the message
div.nz-chart-frame-y-ticks, -x-ticksaria-hidden="true", with axes="frame"
table.nz-chart-frame-tablearia-labelledby the title; inside details.nz-chart-frame-details with tableView="details"

Classes a renderer draws with: nz-chart-svg (the percent-space SVG that fills the plot), nz-chart-grid, nz-chart-baseline, nz-chart-line, nz-chart-area and nz-chart-visually-hidden, with data-series on a mark (current, previous, cat-1, cat-2, cat-3, other).

CSS hooks

HookDefaultRead as
--nz-chart-frame-aspect18 / 7
  • aspect-ratio on .nz-chart-frame-body
--nz-chart-frame-gutter2.75rem
  • min-inline-size on .nz-chart-frame-y-ticks

Layers: nz.components. Classes: .nz-chart-area, .nz-chart-baseline, .nz-chart-frame, .nz-chart-frame-body, .nz-chart-frame-caption, .nz-chart-frame-cell, .nz-chart-frame-details, .nz-chart-frame-details-summary, .nz-chart-frame-message, .nz-chart-frame-plot, .nz-chart-frame-skeleton, .nz-chart-frame-summary, .nz-chart-frame-table, .nz-chart-frame-table-scroll, .nz-chart-frame-tick, .nz-chart-frame-title, .nz-chart-frame-x-ticks, .nz-chart-frame-x-ticks-band, .nz-chart-frame-y-ticks, .nz-chart-grid, .nz-chart-line, .nz-chart-svg, .nz-chart-visually-hidden, .nz-legend.

Tokens this stylesheet reads (25)

--nz-bg-subtle, --nz-border-default, --nz-border-focus, --nz-border-subtle, --nz-border-width-default, --nz-border-width-focus, --nz-chart-grid, --nz-chart-neutral, --nz-chart-primary, --nz-duration-chart-draw, --nz-easing-standard, --nz-font-body-m, --nz-font-body-m-font-size, --nz-font-body-m-line-height, --nz-font-label-m, --nz-font-label-m-strong, --nz-radius-md, --nz-radius-sm, --nz-space-1, --nz-space-2, --nz-space-3, --nz-text-disabled, --nz-text-primary, --nz-text-secondary, --nz-text-tertiary

--nz-chart-frame-aspect is the body's aspect ratio (18 / 7, Figma 03's 720 × 280 plot), and --nz-chart-frame-gutter the y labels' column's minimum width (2.75rem). The column keeps it in every status, so the plot does not move while the labels are absent, and widens for longer labels (ar-EG's digits, say) instead of letting them overflow.

The frame is a size container (container-type: inline-size): a narrow chart adapts to its own width, never the viewport's. Under 31rem, a body with axes is at least 12rem tall, so the plot keeps about 10rem and the y labels their room (above that width the aspect ratio alone sizes it, a short one of yours included). Under 25rem, the x axis shows every other label, counting only the positions that have one, and always keeps the first and the last; the labels it hides are still in the data table.

Accessibility

  • A <figure> named by its <figcaption>. The tick labels are aria-hidden: the data table carries the values.
  • titleVisibility="hidden" keeps that name: the caption is visually hidden, not removed, so the figure is named the same and the table is still labelled by the title. The browser lane checks that the hidden caption takes no layout space and that the name does not change, in Chromium, WebKit and Firefox (caption.spec.tsx); chart.conformance.tsx checks that every framed part forwards the prop.
  • The data table is rendered on the server. With tableView="hidden" it is in the HTML only while the status is 'ready'. With tableView="details" the disclosure renders in every status (an inert line until ready), so the frame's box does not move between statuses (§16.5).
  • Loading sets aria-busy and renders visually hidden text (labels.loading). No status uses a live region (§16.5).
  • interaction="nizivo" promises a keyboard path to the points, which a Nizivo renderer's island provides; interaction="none" promises none. chart.conformance.tsx in packages/charts/test/conformance checks every framed part for both claims, in all four statuses.
  • The default table sits in a visually hidden wrapper, not hidden itself: a <table> ignores inline-size: 1px, so the wrapper's 1 × 1 clipped box is what keeps a wide table from adding horizontal overflow on a narrow screen (the charts-only fixture checks the 1 × 1 box).
  • Focus: the charts stylesheet draws its own focus outline (--nz-border-width-focus, solid --nz-border-focus, 2 px offset) on any link, button, summary or [tabindex] inside a frame or a legend, so it does not need @nizivo/react's base CSS.

Figma

Figma page 03, Data Visualization (7275:13284). The frame has no component of its own: it is the chrome the Area Chart (7275:13336, 720 × 280), Bar Chart (7275:13387, 640 × 260) and Donut Chart (7275:13455) sets share. Figma draws no data table; the hidden table is code's accessible alternative. Notes: docs/figma/03-data-viz.md.

On this page