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-chart

Usage

<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 while status="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

PropTypeDefaultDescription
classNamestring
dataLineChartDatum[]The rows. Each one is a point along the x-axis.
xDataKeystring'date'Key holding the x label. Used by the axis and the crosshair readout.
statusLineChartStatus'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.
aspectRationumber2Width ÷ height. 2 is the wide card shape; 1.6 suits a narrow column.
animationDurationnumber1100Milliseconds for the reveal on mount.
domainDurationnumber500Milliseconds for the y-axis to settle after the data changes.
yDomain[number, number]Fix the y-axis instead of deriving it from the data.
curveLineChartCurve'monotone'monotone never overshoots between points; linear joins them straight.
onActiveIndexChange(index: number, datum: LineChartDatum | null) => voidThe 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.
compactbooleanfalseDrop 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

PropTypeDefaultDescription
rowsnumberHorizontal rules across the plot.
colorstring
dashArraystringDash pattern, e.g. "4,6". Omit for a solid rule.
opacitynumber

LineChart.Line

PropTypeDefaultDescription
dataKeystringKey holding this series' y values.
colorstringStroke colour. Defaults to the --color-chart-* token at colorIndex, so a series follows the theme without the call site naming a colour.
colorIndex1 | 2 | 3 | 4 | 51Which --color-chart-* token to take when color is not given.
strokeWidthnumber2.5
dashArraystringDash pattern, e.g. "6,4" — for a projection or a secondary series.
showMarkersbooleanfalseA dot at every point. Best kept for short series.

LineChart.Area

PropTypeDefaultDescription
dataKeystring
colorstring
colorIndex1 | 2 | 3 | 4 | 51
opacitynumberOpacity at the line. Fades to nothing at the baseline.

LineChart.Skeleton

PropTypeDefaultDescription
durationnumberMilliseconds for one pass of the sweep.
colorstring

LineChart.XAxis

PropTypeDefaultDescription
ticksnumberHow many labels to show. The rest are dropped, evenly.
format(datum: LineChartDatum, index: number) => stringTurn a row into its label. Defaults to the value at xDataKey.
classNamestring

LineChart.Tooltip

PropTypeDefaultDescription
colorstring
showLabelbooleanFloat 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) => stringFormat one series' value for the label. Defaults to a compact number.
formatX(datum: LineChartDatum) => stringFormat the label's heading from the row. Defaults to the value at xDataKey.

LineChart.Legend

PropTypeDefaultDescription
classNamestring
labelsRecord<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.

On this page