Charts
Five figures — LineChart, AreaChart, BarChart, PieChart, RadarChart — and the plot the first three share.
Overview
Five figures — LineChart, AreaChart, BarChart, PieChart, RadarChart — and the plot
the first three share.
First example
import { Chart } from '@xaui/native/chart'export function Example() {return <Chart />}
Anatomy
<Chart>
<Chart.Frame />
<Chart.LegendItem />
<Chart.Legend />
<Chart.Plot />
<Chart.Series />
<Chart.TextSlot />
<Chart.ViewSlot />
</Chart>
Usage
import { Chart } from '@xaui/native/chart' import { LineChart } from '@xaui/native/line-chart' import { AreaChart } from '@xaui/native/area-chart' import { BarChart } from '@xaui/native/bar-chart' import { PieChart } from '@xaui/native/pie-chart' import { RadarChart } from '@xaui/native/radar-chart'
Root props
ChartFrameProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| variant | ChartVariant | undefined | No | — |
| size | ChartSize | undefined | No | The figure's height, its type, and the type in the words around it. |
| color | string | undefined | No | A raw tint (R7). The palette the figure and the legend both read. |
| seriesCount | number | undefined | No | How many series the figure inside draws. The frame cannot count them — it has not rendered the figure, and the keys are that figure's props — and a legend needs the palette walked to the right length or its third dot is the wrong colour. It is the one number a frame asks for. |
| isDisabled | boolean | undefined | No | — |
| style | StyleProp<ViewStyle> | No | — |
| children | ReactNode | No | — |
Slots
ChartLegendItemProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| index | number | undefined | No | Which series it stands for — the index the figure walked its `yKeys` in. |
| color | string | undefined | No | A colour of its own, for an entry the palette does not cover. |
| children | ReactNode | No | — |
ChartLegendProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| labels | readonly string[] | undefined | No | The short form: one entry per label, coloured in palette order. |
| variant | ChartVariant | undefined | No | The appearance to walk the palette from, for a legend that is **not** inside a `<Chart>` — a `Widget`'s header, typically. They have to be the ones the figure was given, or the legend names the wrong colours. Inside a frame the frame wins. |
| size | ChartSize | undefined | No | — |
| color | string | undefined | No | — |
| count | number | undefined | No | How many series the ramp has. Defaults to the number of labels, or of children — pass it when a legend prints fewer entries than the figure draws. |
| children | ReactNode | No | — |
ChartPlotProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| hasGrid | boolean | undefined | No | Whether the horizontal rules are drawn behind the series. |
| hasXAxis | boolean | undefined | No | Whether the x labels are drawn under the plot. |
| hasYAxis | boolean | undefined | No | Whether the y labels are drawn beside it. |
| tickCount | number | undefined | No | How many labels the y axis aims for. A **target**, not a count: the step lands on a number a reader recognises and how many of those fit is then arithmetic. |
| xLabelCount | number | undefined | No | How many x labels to draw. A dozen months under a phone-wide plot is a smear, so every nth row is labelled and the rest are not. |
| formatX | ((value: unknown, index: number) => string) | undefined | No | How an x value reads under the plot. Unset, it is written as it comes. |
| formatY | ((value: number) => string) | undefined | No | How a y value reads beside it — `$10k`, `60 %`. |
| hasZeroBaseline | boolean | undefined | No | Whether the y axis starts at zero rather than at the lowest value. |
| data | readonly Data[] | Yes | The rows. An empty list draws the axes and nothing else, rather than nothing at all. |
| xKey | XK | Yes | Which key runs along the bottom. |
| yKeys | readonly YK[] | Yes | Which keys are the series, in the order the palette walks them. |
| variant | ChartVariant | undefined | No | — |
| size | ChartSize | undefined | No | The plot's height and the axis type. Never its width. |
| color | string | undefined | No | A raw tint (R7), never a token. The palette is walked out of it. |
| spacing | "point" | "band" | undefined | No | Where a reading sits along the bottom. `point` puts the first and last on the plot's edges, which is what a line connecting readings does. `band` gives each one a slot and centres it, which is what a bar occupies. A line inset by half a slot reads as cut off; a bar on the edge is half outside the plot. |
| bandPadding | number | undefined | No | The share of a slot left empty either side of a bar. Only read under `band`. |
| isDisabled | boolean | undefined | No | Dims the plot. There is nothing to press unless a caller adds it. |
| children | (plot: ChartPlot) => ReactNode | Yes | The scaled series, the frame and the scales. |
| style | StyleProp<ViewStyle> | No | — |
ChartSeriesProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| hasGrid | boolean | undefined | No | Whether the horizontal rules are drawn behind the series. |
| hasXAxis | boolean | undefined | No | Whether the x labels are drawn under the plot. |
| hasYAxis | boolean | undefined | No | Whether the y labels are drawn beside it. |
| tickCount | number | undefined | No | How many labels the y axis aims for. A **target**, not a count: the step lands on a number a reader recognises and how many of those fit is then arithmetic. |
| xLabelCount | number | undefined | No | How many x labels to draw. A dozen months under a phone-wide plot is a smear, so every nth row is labelled and the rest are not. |
| formatX | ((value: unknown, index: number) => string) | undefined | No | How an x value reads under the plot. Unset, it is written as it comes. |
| formatY | ((value: number) => string) | undefined | No | How a y value reads beside it — `$10k`, `60 %`. |
| hasZeroBaseline | boolean | undefined | No | Whether the y axis starts at zero rather than at the lowest value. |
| data | readonly Data[] | Yes | The rows. An empty list draws the axes and nothing else, rather than nothing at all. |
| xKey | XK | Yes | Which key runs along the bottom. |
| yKeys | readonly YK[] | Yes | Which keys are the series, in the order the palette walks them. |
| variant | ChartVariant | undefined | No | — |
| size | ChartSize | undefined | No | The plot's height and the axis type. Never its width. |
| color | string | undefined | No | A raw tint (R7), never a token. The palette is walked out of it. |
| isDisabled | boolean | undefined | No | Dims the plot. There is nothing to press unless a caller adds it. |
| style | StyleProp<ViewStyle> | No | — |
ChartTextSlotProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
ChartViewSlotProps
The node's inherited React Native props apply too, and so do its style props — padding, margin, width and the rest.
| Prop | Type | Required | Description |
|---|---|---|---|
| children | ReactNode | No | — |
Variants, sizes and colour
A shade per series, not a colour per series
The palette is walked out of one colour — the variant's ink, or color when a caller
gives one — in OKLab lightness.
A chart's series are usually one quantity split: organic and paid traffic, mobile and desktop. Shades of one colour say "parts of a whole" where a rainbow says "unrelated things", and it is the only scheme that survives a caller changing the accent, because there is nothing to change but the seed.
The walk reduces chroma to stay in gamut rather than clamping channels. Clamping gives away the hue — the channel that overflowed stops moving while the others keep going, and a blue drifts several degrees towards cyan across a ramp — where pulling chroma in keeps the angle exactly and gives away only saturation, which is what a lighter shade of one colour is anyway.
Variants and colour
variant | The series' ink |
|---|---|
primary | accent |
secondary | foreground |
success | success |
warning | warning |
danger | danger |
The ProgressBar's five, for the ProgressBar's reasons: a chart reports a quantity, so
primary and secondary are its two emphases and the three intents are for when the number
itself is the news. tertiary and ghost are gone — a series with no ink is not a series —
and the *-soft pairs with them, since a chart's fills are already soft.
The ink is bgSelected, a role rather than a token named in an axis: the tint pass
re-runs paint and never the axes, so a series colour written as an axis would snap back to
the accent the moment a caller set color, and the palette walked out of it would go too.
No figure paints its own ground. One that did would be a card inside the Chart frame,
or inside the caller's own Card where there is no frame.
Sizes
size | Plot height | Ring / web |
|---|---|---|
sm | 140 | 140 / 180 |
md | 200 | 200 / 240 |
lg | 280 | 260 / 300 |
size is the plot's height, never its width. A chart spans its parent — RN's own
behaviour, and the reason there is no fullWidth — and how tall it is is the only thing a
caller cannot infer from the column it sits in. A pie and a radar are square, so for those it
is a diameter.
Accessibility
Migration from legacy
Implementation notes
The frame
A figure on a screen is a card with words around it, and those words are a title, a
subtitle, a number and a legend. Chart is that card:
<Chart seriesCount={2}>
<Chart.Header>
<Chart.Heading>
<Chart.Title>Source du trafic</Chart.Title>
<Chart.Description>Sessions</Chart.Description>
</Chart.Heading>
<Chart.Legend labels={['Organique', 'Payant']} />
</Chart.Header>
<LineChart data={rows} xKey="month" yKeys={['organic', 'paid']} />
<Chart.Footer>
<Chart.Description>Douze derniers mois</Chart.Description>
</Chart.Footer>
</Chart>
| slot | what it is |
|---|---|
Chart | The card, and the palette everything in it shares |
Chart.Header | The row above the figure, pushed apart |
Chart.Heading | The title, its description and a value, as one block |
Chart.Title | What the figure is. A header to a screen reader |
Chart.Description | What it is measuring — the unit, the period, the caveat |
Chart.Value | The one number it is about, when it is about one |
Chart.Legend | Which colour is which series |
Chart.LegendItem | One entry of it |
Chart.Footer | The row under the figure. It wraps |
It is optional in both directions. A figure on its own is the shape all five shipped as,
and it draws no ground — so <LineChart /> in a caller's own Card is a complete use. And a
frame with no figure in it is a complete use too: a card with a title, a value and a
footer is what a chart looks like while its data is loading, or when there is none.
<Chart>
<Chart.Header>
<Chart.Heading>
<Chart.Title>Sessions</Chart.Title>
<Chart.Description>Aucune donnée sur la période</Chart.Description>
</Chart.Heading>
</Chart.Header>
<Skeleton height={200} />
</Chart>
Neither half asks for the other, which is what makes the pair worth having.
The frame owns the appearance and the figure takes it. variant, size and color are
handed down, so the legend's dots and the figure's series are the same colours in the same
order without either being told twice — which is the whole reason Chart.Legend can exist
rather than being a prop on a figure. A figure that names its own still wins.
seriesCount is the one number the frame asks for: it cannot count the figure's series,
because it has not rendered the figure and the keys are that figure's props, and a legend
needs the palette walked to the right length or its third dot is the wrong colour.
The labels stay the caller's. What a series is called is a sentence in their language;
which colour it got is arithmetic the palette already did. labels is the short form and
children are the long one — a legend carrying a value and a share beside each name, which is
what a donut usually wants under it.
Chart.Heading exists for ProgressBar.Header's reason: the gap between a title and its
subtitle is a different gap from the one between that block and the legend beside it, and two
gaps belong to two roots.
They are drawn here, not wrapped
Nothing is imported to draw them. react-native-svg is already an optional peer — the
Select's check and the Icon's chevron use it — and everything above it is this library's:
the scales, the paths, the palette.
That is not an aesthetic preference. A chart library is a second design system: it has its
own idea of a colour, its own appearance blob, its own units, and its API is the one place
variant and color cannot reach. Wrapping one means either exposing that blob — which is
customAppearance, the thing R2 removed — or fighting it. The alternative was a native
graphics engine as a peer dependency, which is a lot of install for four figures.
And the maths becomes tests. chart-scale, chart-path and chart-palette are 106
cases. A curve that must never dip below its data, an axis that must never label a value
above the tallest bar, a bar whose corner must not exceed its own height — all of that is a
test rather than a screenshot someone has to remember to look at.
The API is chartkit's shape
<LineChart data={rows} xKey="month" yKeys={['revenue']} />
Rows of objects, a key for the axis, a key per series. The one difference is yKeys
plural, because a chart with two series is the common case and not an escape hatch.
The series are props, not children, and that is the single place this family parts
company with the rest of the library. A line is not a component a caller composes — it is a
column of their data. What composition there is lives on ChartPlot, which takes a render
function for the case a caller needs a mark the five do not draw.
The plot
LineChart, AreaChart and BarChart are three files over one ChartPlot, the way the
Autocomplete is a few files over selectRecipe. Three charts with three axis tables would
be three to keep in step, and the drift would show as a line chart and a bar chart on one
dashboard with labels at two sizes.
It owns the frame, the grid, the axes, both scales and the palette. PieChart and
RadarChart are square rather than framed, so they take the palette and the ink and draw
their own geometry.
The axis is honest
niceScale picks a step from 1, 2, 2.5, 5 or 10 times a power of ten, then widens the
domain to a multiple of it. The other way round — squeezing the ticks into the data's own
range — is what produces an axis labelled 3.33 and 6.67, and an axis whose top tick sits
below the tallest bar is an axis that lies about the data.
tickCount is a target, not a count.
Point spacing and band spacing
A bar occupies a slot, so it is centred in one and the ends of the plot are empty. A line connects readings, so the first and the last sit on the plot's edges. A line inset by half a slot at either end reads as a chart that has been cut off; a bar on the edge is half outside the plot.
Two scales rather than one with a flag, because they are two shapes. Each chart asks for the one it is.
The labels are React Native
<Text> beside the canvas rather than inside it, so they take the theme's font, its scaling
and its colour. <Text> inside an <Svg> needs a font file loaded and ignores the
platform's text size.
The plot reserves 40 points for the y labels and 20 for the x. A label of "1 200 000" is the
case that loses to, and formatY is the answer — which is why it exists.
The curve never leaves its data
monotone is Fritsch–Carlson: the tangent at each point starts as the average of the slopes
either side of it, is flattened to zero wherever the data turns, and is capped at three
times the neighbouring slope elsewhere. That cap is the condition under which a cubic Hermite
segment cannot leave the interval its endpoints set.
The alternative — a control point at the x-midpoint — is four lines and overshoots: two high readings either side of a low one bow the curve below the low one, and on an area chart that is ink under the axis.
curve="linear" for straight edges.
Shared props
Every cartesian chart takes these, plus everything View accepts and the ViewStyle keys as
props (R14):
| Prop | Type | Default | Notes |
|---|---|---|---|
data | Data[] | — | An empty list draws the axes |
xKey | keyof Data | — | What runs along the bottom |
yKeys | (keyof Data)[] | — | The series, in palette order |
variant | ChartVariant | 'primary' | |
size | 'sm' | 'md' | 'lg' | 'md' | The plot's height |
color | string | — | The palette's seed |
hasGrid | boolean | true | |
hasXAxis | boolean | true | |
hasYAxis | boolean | true | |
tickCount | number | 4 | A target, not a count |
xLabelCount | number | what fits | Every nth row is labelled |
formatX | (value, index) => string | — | |
formatY | (value: number) => string | — | |
hasZeroBaseline | boolean | true | An axis from the data's floor lies about proportions |
isDisabled | boolean | false |
Extending them
ChartPlot is exported. It takes the same props and a render function that receives the
scaled series, the frame, both scales and the ticks — enough to draw a mark none of the five
does, on the same axes as the ones that do.
<ChartPlot data={rows} xKey="month" yKeys={['revenue']}>
{({ series, frame }) => (
<Path d={linePath(series[0].points)} stroke={series[0].color} />
)}
</ChartPlot>
chart-scale and chart-path are utils/ — private, and deliberately: they are the
family's own maths, not an API. A chart of your own reaches for ChartPlot.