LineChart
Animated time series, drawn on the UI thread.
A time series, drawn and animated on the UI thread.
The chart is composed rather than configured: the grid, each series, the axis and the crosshair are separate children, so a chart that wants no grid simply does not have one. A single component with twenty booleans is how charts end up unreadable at the call site.
Colours come from the --color-chart-1 … --color-chart-5 tokens, so a chart follows the active theme out of the box and is put on brand by overriding those five. Nothing in it hardcodes a colour.
Installation
LineChart ships with the library — no separate install.
import { LineChart, Frame, Text, Button } from 'panelui-native';Or copy the source into your project, to own and edit it:
npx panelui-cli@latest add line-chartUsage
<LineChart data={visits} xDataKey="month">
<LineChart.Grid />
<LineChart.Area dataKey="visits" />
<LineChart.Line dataKey="visits" />
<LineChart.XAxis />
<LineChart.Tooltip />
</LineChart>Composition
<LineChart>
<LineChart.Grid>…</LineChart.Grid>
<LineChart.Area>…</LineChart.Area>
<LineChart.Line>…</LineChart.Line>
<LineChart.Skeleton>…</LineChart.Skeleton>
<LineChart.XAxis>…</LineChart.XAxis>
<LineChart.Tooltip>…</LineChart.Tooltip>
<LineChart.Legend>…</LineChart.Legend>
</LineChart>LineChart.Grid— Horizontal reference lines, under everything and outside the reveal.LineChart.Area— The gradient fill under a series. Separate from the line, because a two-series chart usually wants it on only one of them.LineChart.Line— One series. Takes its colour from a--color-chart-*token unless given one.LineChart.Skeleton— The flat rule with a sweep along it, shown whilestatus="loading".LineChart.XAxis— The x labels, as real text under the plot.LineChart.Tooltip— The crosshair and the drag that drives it.LineChart.Legend— A swatch and a name per registered series.
Examples
A chart card with a crosshair
Pair it with Frame, and add LineChart.Tooltip. Dragging across the chart moves a crosshair and floats a small label at the point with the value — the minimal readout a drag wants. The label tracks the finger on the UI thread; only its text crosses back into JS, and only when the active index changes.
<Frame>
<Frame.Header className="flex-col items-start gap-0.5">
<Frame.Title>Monthly Revenue</Frame.Title>
<Text size="sm" muted>Press and drag across the chart to inspect values</Text>
</Frame.Header>
<Frame.Panel className="p-0">
<LineChart data={revenue} xDataKey="month">
<LineChart.Grid />
<LineChart.Area dataKey="revenue" />
<LineChart.Line dataKey="revenue" />
<LineChart.XAxis ticks={5} />
<LineChart.Tooltip formatValue={(v) => `$${v.toLocaleString()}`} />
</LineChart>
</Frame.Panel>
</Frame>Replay the reveal
Take a ref and call replay() to run the entrance again — for a control that lets the reader watch the line draw itself once more.
const chart = useRef<LineChartHandle>(null);
<Button onPress={() => chart.current?.replay()}>Replay</Button>
<LineChart ref={chart} data={revenue} xDataKey="month">
<LineChart.Grid />
<LineChart.Line dataKey="revenue" />
</LineChart>A sparkline
Set compact and drop the grid, axis and crosshair: the line reaches the edges and the shape is the whole point. Sits beside a number in a KPI card.
<View className="h-14 w-32">
<LineChart data={points} xDataKey="t" compact aspectRatio={2.3}>
<LineChart.Line dataKey="v" strokeWidth={2} />
</LineChart>
</View>Two series and a dashed comparison
Both series share one y-axis so they stay comparable. Give a target line the next colour token and a dash pattern, so actual and target are told apart by shape as well as by colour.
<LineChart data={revenue} xDataKey="month">
<LineChart.Grid />
<LineChart.Line dataKey="revenue" colorIndex={1} />
<LineChart.Line dataKey="target" colorIndex={2} dashArray="6,5" />
<LineChart.XAxis ticks={5} />
<LineChart.Tooltip formatValue={(v) => `$${(v / 1000).toFixed(1)}k`} />
</LineChart>Loading, and then data
Add LineChart.Skeleton and drive status. While loading, a flat rule sits where the series will be with a highlight travelling along it; on ready that same line grows into the data — one component the whole way through.
<LineChart data={data} status={status} xDataKey="month">
<LineChart.Grid />
<LineChart.Skeleton />
<LineChart.Area dataKey="revenue" />
<LineChart.Line dataKey="revenue" />
<LineChart.XAxis />
<LineChart.Tooltip />
</LineChart>API Reference
LineChart
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | |
data | LineChartDatum[] | — | The rows. Each one is a point along the x-axis. |
xDataKey | string | 'date' | Key holding the x label. Used by the axis and the crosshair readout. |
status | LineChartStatus | 'ready' | loading draws a flat skeleton with a sweep running along it, and morphs into the real series when it turns ready. One component throughout, rather than a spinner swapped for a chart — swapping loses the transition. |
aspectRatio | number | 2 | Width ÷ height. 2 is the wide card shape; 1.6 suits a narrow column. |
animationDuration | number | 1100 | Milliseconds for the reveal on mount. |
domainDuration | number | 500 | Milliseconds for the y-axis to settle after the data changes. |
yDomain | [number, number] | — | Fix the y-axis instead of deriving it from the data. |
curve | LineChartCurve | 'monotone' | monotone never overshoots between points; linear joins them straight. |
onActiveIndexChange | (index: number, datum: LineChartDatum | null) => void | — | The point under the crosshair as it moves, and -1/null when the finger lifts. This is how a readout in the card's header gets its value — that header is outside the chart, so it cannot use useLineChart. Fires when the index changes, not per frame. |
compact | boolean | false | Drop the axis padding so the line reaches the edges — for a sparkline with no grid, axis or crosshair, where the shape is the whole point. |
LineChart.Grid
| Prop | Type | Default | Description |
|---|---|---|---|
rows | number | — | Horizontal rules across the plot. |
color | string | — | |
dashArray | string | — | Dash pattern, e.g. "4,6". Omit for a solid rule. |
opacity | number | — |
LineChart.Line
| Prop | Type | Default | Description |
|---|---|---|---|
dataKey | string | — | Key holding this series' y values. |
color | string | — | Stroke colour. Defaults to the --color-chart-* token at colorIndex, so a series follows the theme without the call site naming a colour. |
colorIndex | 1 | 2 | 3 | 4 | 5 | 1 | Which --color-chart-* token to take when color is not given. |
strokeWidth | number | 2.5 | |
dashArray | string | — | Dash pattern, e.g. "6,4" — for a projection or a secondary series. |
showMarkers | boolean | false | A dot at every point. Best kept for short series. |
LineChart.Area
| Prop | Type | Default | Description |
|---|---|---|---|
dataKey | string | — | |
color | string | — | |
colorIndex | 1 | 2 | 3 | 4 | 5 | 1 | |
opacity | number | — | Opacity at the line. Fades to nothing at the baseline. |
LineChart.Skeleton
| Prop | Type | Default | Description |
|---|---|---|---|
duration | number | — | Milliseconds for one pass of the sweep. |
color | string | — |
LineChart.XAxis
| Prop | Type | Default | Description |
|---|---|---|---|
ticks | number | — | How many labels to show. The rest are dropped, evenly. |
format | (datum: LineChartDatum, index: number) => string | — | Turn a row into its label. Defaults to the value at xDataKey. |
className | string | — |
LineChart.Tooltip
| Prop | Type | Default | Description |
|---|---|---|---|
color | string | — | |
showLabel | boolean | — | Float a small label at the crosshair showing the x-value and each series' value at that point — the minimal readout a drag wants. On by default. |
formatValue | (value: number, key: string) => string | — | Format one series' value for the label. Defaults to a compact number. |
formatX | (datum: LineChartDatum) => string | — | Format the label's heading from the row. Defaults to the value at xDataKey. |
LineChart.Legend
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | |
labels | Record<string, string> | — | Label per series key. A key with no label falls back to the key itself. |
Every part also accepts the underlying React Native props (ViewProps or TextProps) and a className for Tailwind utilities.
Notes
The two layers
The geometry is SVG and anything with text or a gesture on it is a React Native view laid over the top. The parts sort themselves into the right layer, so composition stays a flat list of children — but it is why LineChart.XAxis renders real text rather than SVG text. SVG text ignores the platform's text scaling and the theme's font, and a gesture handler cannot be attached to an SVG node at all.
Three animations, three reasons
The reveal uncovers the plot left to right on mount, with everything inside sharing one clip — so the line, its fill and its markers arrive together rather than as three separate effects.
The y-domain is tweened when the data changes, rather than the path being swapped. A series that grows is redrawn against a moving axis instead of jumping to a new shape. The reveal deliberately does not replay: it happened once, and repeating it on every refresh turns a data update into an animation.
The crosshair resolves the nearest index on the UI thread. Only that index crosses back into JS, and only when it changes, so a drag across a hundred points costs a hundred re-renders at most rather than one per frame.
All three respect the system's reduce-motion setting: the chart renders at its final state instead.
Reading out the active point
onActiveIndexChange fires as the crosshair moves between points, and with -1/null when the finger lifts. That is the one to use for a readout in the card header, because the header is outside the chart and a hook cannot reach up out of the subtree it is called in. useLineChart is for something rendered inside the chart, which is the rarer case.
The crosshair label
LineChart.Tooltip floats a small label at the crosshair by default, showing the x-value and each series' value at that point. Turn it off with showLabel={false}, or format its numbers with formatValue. For a readout in the card header instead — outside the plot, where it does not cover the line — use onActiveIndexChange on the root.
The curve
monotone is the default and is the right one for a time series. A plain cubic spline overshoots between points, so a series that never goes below zero draws a dip under the axis between two low values — a shape that is not in the data. Use curve="linear" when the points are exact readings and the eye should not be given anything between them.
Sizing
The chart fills its parent's width and takes its height from aspectRatio. Give it a wider ratio for a card and a squarer one for a narrow column; do not set a height directly, as the aspect ratio is what keeps the plot geometry stable while the layout settles.