LiveLineChart
A reading that keeps arriving, against a window that keeps moving.
Use it for a number that keeps arriving — a metric off a socket, a sensor, a queue depth. Each reading is placed at the time it carries against a window running from window seconds ago to now, so the gaps between readings are part of what is drawn.
The window is tied to the wall clock, not to the data. The line drifts left whether or not anything is arriving, so a feed that stalls shows as a flat run reaching back from the tip. That costs a frame callback for as long as the chart is mounted; paused and status="loading" stop it, and it never starts when the platform asks for reduced motion — the window then advances as each reading lands instead.
For a fixed series that is not going to grow — a week of visits, twelve months of revenue — use LineChart. It places points by their position in the list, which is the right answer when the spacing between them is not the subject.
Installation
LiveLineChart ships with the library — no separate install.
import { LiveLineChart, type LiveLinePoint, Frame } from 'panelui-native';Or copy the source into your project, to own and edit it:
npx panelui-cli@latest add live-line-chartUsage
<LiveLineChart data={points} window={30}>
<LiveLineChart.Header title="Requests / sec" />
<LiveLineChart.Grid />
<LiveLineChart.Area />
<LiveLineChart.Line />
<LiveLineChart.Tip />
<LiveLineChart.XAxis />
</LiveLineChart>Composition
<LiveLineChart>
<LiveLineChart.Header /> {/* what is watched, and what it reads now */}
<LiveLineChart.Grid /> {/* the rules the readings are judged against */}
<LiveLineChart.Area /> {/* the gradient fill under the line */}
<LiveLineChart.Line /> {/* the line */}
<LiveLineChart.Skeleton /> {/* while status="loading" */}
<LiveLineChart.Tip /> {/* the dot at the leading end, and its badge */}
<LiveLineChart.XAxis /> {/* how far back the plot reaches */}
<LiveLineChart.YAxis /> {/* the value scale */}
<LiveLineChart.Tooltip /> {/* drag back to read a reading that has gone past */}
</LiveLineChart>LiveLineChart.Header— The strip above the plot. Its value falls back to the reading under the crosshair, then to the latest one, so a drag reads out here without wiring anything up.LiveLineChart.Grid— The horizontal rules the readings are judged against.LiveLineChart.Area— The gradient fill under the line. Its own part, so a chart that wants the shape without the weight of a filled band does not have one.LiveLineChart.Line— The line, rebuilt on the UI thread every frame the window moves.LiveLineChart.Tip— The dot at the leading end. It rides the newest reading rather than the right-hand edge. Passbadgeto write the current reading beside it — off by default, since a floating card nobody opened reads as a tooltip.LiveLineChart.XAxis— How far back the plot reaches, labelled as offsets from now.LiveLineChart.YAxis— The value scale. Declaring one widens the left gutter so its labels have somewhere to sit.LiveLineChart.Tooltip— Drag back through the window to read a reading that has already gone past.LiveLineChart.Skeleton— A flat line down the middle, shown whilestatus="loading".
Examples
A metric off a socket
Append to data as readings arrive and the chart does the rest. Times are milliseconds, as Date.now() gives them.
Old readings are dropped past maxPoints — they are off the window and cannot come back, and an unbounded feed otherwise grows an array for as long as the screen is open.
const [points, setPoints] = useState<LiveLinePoint[]>([]);
useEffect(() => {
const timer = setInterval(() => {
setPoints((current) => [...current, { time: Date.now(), value: read() }]);
}, 500);
return () => clearInterval(timer);
}, []);
<LiveLineChart data={points} window={30}>
<LiveLineChart.Header title="Requests / sec" />
<LiveLineChart.Grid />
<LiveLineChart.Area />
<LiveLineChart.Line />
<LiveLineChart.Tip />
<LiveLineChart.XAxis />
</LiveLineChart>Colour that follows the direction
momentumColors colours the line, the fill and the tip by where the reading has been going over the last few points rather than by a fixed hue. It is the one thing on the chart readable without looking at the axis, which is what a number watched out of the corner of an eye needs.
Left out, the chart draws in one colour throughout.
<LiveLineChart
data={points}
momentumColors={{ up: '#10b981', down: '#ef4444' }}
>
<LiveLineChart.Grid />
<LiveLineChart.Area />
<LiveLineChart.Line />
<LiveLineChart.Tip />
</LiveLineChart>Reading back through the window
Tooltip pins the crosshair to the moment it was put on rather than to the place on screen, so it travels left with the reading it named instead of sitting still while the line slides out from under it.
A reading can scroll off the window while it is being read. The crosshair goes with it.
<LiveLineChart data={points} onActivePointChange={setReading}>
<LiveLineChart.Header title="Throughput" />
<LiveLineChart.Grid />
<LiveLineChart.YAxis />
<LiveLineChart.Line />
<LiveLineChart.Tip />
<LiveLineChart.Tooltip />
<LiveLineChart.XAxis />
</LiveLineChart>Holding the window still
paused freezes the window where it is. Readings still arrive and are still kept — it is the window that stops, so what is on screen holds still long enough to be talked about, axis and tip included. Clearing it jumps the window back to the present.
<LiveLineChart data={points} paused={held}>
<LiveLineChart.Header title="Requests / sec">
<Button size="sm" variant="ghost" onPress={() => setHeld(!held)}>
{held ? 'Resume' : 'Hold'}
</Button>
</LiveLineChart.Header>
<LiveLineChart.Line />
<LiveLineChart.Tip />
</LiveLineChart>API Reference
LiveLineChart
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | |
data | LiveLinePoint[] | — | The readings so far, oldest first. Append to it as they arrive. |
window | number | — | How much time the plot spans, in seconds. |
paused | boolean | false | Freeze the window where it is. The readings still arrive; the clock stops. |
yDomain | [number, number] | — | Fix the y-axis instead of deriving it from what is visible. |
domainDuration | number | 420 | Milliseconds for the y-axis to settle after the range changes. |
curve | ChartCurve | 'monotone' | monotone never overshoots between readings; linear joins them straight. |
maxPoints | number | 500 | The most readings kept. Older ones are dropped, since they are off the window and cannot come back — an unbounded feed otherwise grows an array for as long as the screen is open. |
aspectRatio | number | 2 | Width ÷ height of the plot. |
status | LiveLineChartStatus | 'ready' | loading draws a flat placeholder and holds the clock. |
momentumColors | LiveLineMomentumColors | — | Colour per direction. Left out, the chart draws in one hue throughout. |
color | string | — | Overrides the --color-chart-1 token. Ignored when momentumColors is set. |
onActivePointChange | (point: LiveLinePoint | null) => void | — | The reading under the crosshair as it moves, and null when the finger lifts. |
LiveLineChart.Grid
| Prop | Type | Default | Description |
|---|---|---|---|
rows | number | — | How many bands the plot is divided into. |
color | string | — | |
dashArray | string | — |
LiveLineChart.Line
| Prop | Type | Default | Description |
|---|---|---|---|
strokeWidth | number | — | |
color | string | — | Overrides the chart's colour, momentum included. |
LiveLineChart.Area
| Prop | Type | Default | Description |
|---|---|---|---|
opacity | number | — | Opacity at the top of the fill, fading to nothing at the baseline. |
color | string | — | Overrides the chart's colour, momentum included. |
LiveLineChart.Tip
| Prop | Type | Default | Description |
|---|---|---|---|
badge | boolean | false | Show the current reading in a badge beside the dot. Off by default. The badge is a floating card, which is the shape a reader has learnt means "you touched something" — sitting there unasked it reads as a tooltip nobody opened. Turn it on where the chart has no header to put the reading in, and it becomes the only place the number is written. |
pulse | boolean | true | Ring the dot with a repeating pulse. |
formatValue | (value: number) => string | — | Format the badge. Defaults to a compact number. |
className | string | — |
LiveLineChart.XAxis
| Prop | Type | Default | Description |
|---|---|---|---|
ticks | number | — | How many labels along the bottom. |
formatTick | (secondsAgo: number) => string | — | Rewrites a label. Given how many seconds back the tick is. |
className | string | — |
LiveLineChart.YAxis
| Prop | Type | Default | Description |
|---|---|---|---|
ticks | number | — | How many labels up the side. |
formatValue | (value: number) => string | — | Format a value. Defaults to a compact number. |
className | string | — |
LiveLineChart.Tooltip
| Prop | Type | Default | Description |
|---|---|---|---|
formatValue | (value: number) => string | — | Format the value. Defaults to a compact number. |
className | string | — |
LiveLineChart.Skeleton
| Prop | Type | Default | Description |
|---|---|---|---|
color | string | — |
LiveLineChart.Header
| Prop | Type | Default | Description |
|---|---|---|---|
className | string | — | |
title | string | — | Small line above the value — what is being watched. |
value | string | — | The readout. Left out, it shows the current reading. |
caption | string | — | One muted line under the value. |
formatValue | (value: number) => string | — | Format the derived value. Defaults to a compact number. |
Every part also accepts the underlying React Native props (ViewProps or TextProps) and a className for Tailwind utilities.
Notes
The y-axis follows what is visible, not everything kept. A spike that has scrolled off the left edge stops holding the axis open, so a feed that settles is not left flat against the bottom of a plot scaled for something that happened a minute ago. Pass yDomain to fix it instead.
Tip draws a dot and nothing else unless badge is passed. The badge is a floating card, which is the shape a reader has learnt means they touched something — sitting there unasked it reads as a tooltip that opened by itself. Turn it on for a chart with no header to put the reading in. It hides itself while the crosshair is out either way, so there is never a second card answering a different question.
paused holds the whole picture, not only the clock. Readings keep arriving behind the frozen edge, and the y-axis, the tip and the momentum colour are all derived from the newest one — left live they would go on rescaling, chasing and recolouring under a line that has stopped, which is a held chart that is still moving.
The tip rides the newest reading rather than the right-hand edge. Pinning it to the edge would hold it still and steady, which is the picture of a feed that is working.
The x-axis is labelled in offsets from now — -30s, -15s, now — rather than clock times. A moving window labelled with wall-clock times rewrites every label on every frame.
The frame callback stops on unmount, on paused, on status="loading", and never starts under reduced motion. Nothing else in the library animates without an interaction or a change of data, so a chart left mounted off-screen is worth paused.