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
- This period
- Previous
| Day | This period | Previous |
|---|---|---|
| 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).
Show data table
| Day | This period | Previous |
|---|---|---|
| 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).
- This period
- Previous
- This period
- Previous
- This period
- Previous
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).
| Channel | Orders |
|---|---|
| Online | 412 |
| Retail | 268 |
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.
| Name | Type | Default | Description |
|---|---|---|---|
axes | ChartAxes"frame""none" | 'frame' | From |
children | ReactNode | – | The renderer: an From |
empty | ReactNode | – | Replaces the default empty message. From |
error | ReactNode | – | Replaces the default error message, for example an InlineError with a retry action. From |
interaction | ChartInteractionMode"nizivo""none" | 'nizivo' | From |
labels | Partial<ChartFrameLabels> | – | From |
legend | ReactNode | – | A From |
pending | boolean | – | A refresh is in flight while the chart shows data (§16.5): From |
render | RenderProp<ChartFrameState> | – | From |
status | DataStatus"ready""loading""empty""error" | 'ready'; a ready chart with no table rows renders as `'empty'`. | From |
summary | ReactNode | – | One line under the title, for example "Daily sales, Oct 1–30, vs previous period". From |
tablerequired | ChartTable | – | From |
tableView | ChartTableView"hidden""details" | 'hidden' | From |
ticks | ChartTicks | – | From |
titlerequired | ReactNode | – | The figure's caption and its accessible name. From |
titleVisibility | ChartTitleVisibility"visible""hidden" | 'visible' |
From |
xDirection | ChartXDirection"ltr""rtl""auto" | 'ltr' | From |
- Also takes every
<figure>attribute exceptchildren,title.
render takes an element, or, in client code, a function of the props and this state (ChartFrameState):
| Name | Type | Default | Description |
|---|---|---|---|
interactionrequired | ChartInteractionMode"nizivo""none" | – | As passed: whether the child claims keyboard access to its points. |
pendingrequired | boolean | – | |
statusrequired | DataStatus"ready""loading""empty""error" | – | The status as rendered: |
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:
| Key | Default | Where |
|---|---|---|
loading | Loading chart | Visually hidden text in the plot while loading |
empty | No data to show | The plot's message when empty, unless empty is passed |
error | This chart could not be loaded | The plot's message on error, unless error is passed |
table | Show data table | The summary of the tableView="details" disclosure |
Other exports
ChartAxestypeimport 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"ChartFrameAttributestypeimport type { ChartFrameAttributes } from '@nizivo/charts/chart-frame'ChartFrame's HTML side: the
<figure>'s attributes and therenderprop.Name Type Default Description renderRenderProp<ChartFrameState>– ChartFrameElementPropstypeimport type { ChartFrameElementProps } from '@nizivo/charts/chart-frame'type ChartFrameElementProps = ChartFrameProps & ChartFrameAttributesThe props of
ChartFrame, listed above.ChartFrameLabelstypeimport type { ChartFrameLabels } from '@nizivo/charts/chart-frame'ChartFrame's own strings (§16.4): serialisable, English defaults in
chartFrameLabels.Name Type Default Description emptyrequiredstring– Shown in the plot when there is no data and no
emptyslot.errorrequiredstring– Shown in the plot when loading failed and there is no
errorslot.loadingrequiredstring– Visually hidden text in the plot while
status="loading"(§16.5: no live region).tablerequiredstring– The summary of the
tableView="details"disclosure.ChartFramePropstypeimport type { ChartFrameProps } from '@nizivo/charts/chart-frame'The props every renderer may rely on (§14.1). The component also takes
ChartFrameAttributes.Name Type Default Description 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-busyanddata-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'ChartFrameStatetypeimport type { ChartFrameState } from '@nizivo/charts/chart-frame'What a
renderfunction receives.The render state of
ChartFrame, listed above.ChartInteractionModetypeimport 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"ChartTabletypeimport type { ChartTable } from '@nizivo/charts/chart-frame'Name Type Default Description 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.ChartTableViewtypeimport 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"ChartTickAligntypeimport 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"ChartTickstypeimport 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.
Name Type Default Description 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.ChartTitleVisibilitytypeimport 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, neverdisplay: 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"ChartXDirectiontypeimport 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'sdiris 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"DataStatustypeimport 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
statusof 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"chartFrameLabelsconstantimport { chartFrameLabels } from '@nizivo/charts/chart-frame'English defaults of ChartFrame's strings (§16.4); pass
labelsto translate per instance.const chartFrameLabels: ChartFrameLabels
Data attributes and slots
| Element | Attributes |
|---|---|
figure.nz-chart-frame | data-slot="chart-frame", data-x-direction, data-loading, data-empty, data-error, data-pending, aria-busy |
figcaption.nz-chart-frame-caption | data-slot="chart-frame-caption": .nz-chart-frame-title, .nz-chart-frame-summary; .nz-chart-visually-hidden with titleVisibility="hidden" |
div.nz-chart-frame-legend | data-slot="chart-frame-legend", when legend is passed |
div.nz-chart-frame-body | data-slot="chart-frame-body": the y labels, the plot and the x labels |
div.nz-chart-frame-plot | data-slot="chart-frame-plot": the child while ready, else the skeleton or the message |
div.nz-chart-frame-y-ticks, -x-ticks | aria-hidden="true", with axes="frame" |
table.nz-chart-frame-table | aria-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
| Hook | Default | Read as |
|---|---|---|
--nz-chart-frame-aspect | 18 / 7 |
|
--nz-chart-frame-gutter | 2.75rem |
|
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 arearia-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.tsxchecks 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'. WithtableView="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-busyand 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.tsxinpackages/charts/test/conformancechecks every framed part for both claims, in all four statuses.- The default table sits in a visually hidden wrapper, not hidden itself: a
<table>ignoresinline-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.
Charts
@nizivo/charts: server-rendered SVG charts with one small client island, a data table as the accessible alternative, and a frame that keeps Nizivo's caption, table, legend and status around any third-party renderer.
AreaChart
A time series drawn on the server, this period in brand green with a 10% wash and an optional dashed comparison, with one interactive layer for the crosshair, the tooltip and keyboard access to every point.