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

Usage

<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. Pass badge to 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 while status="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

PropTypeDefaultDescription
classNamestring
dataLiveLinePoint[]The readings so far, oldest first. Append to it as they arrive.
windownumberHow much time the plot spans, in seconds.
pausedbooleanfalseFreeze 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.
domainDurationnumber420Milliseconds for the y-axis to settle after the range changes.
curveChartCurve'monotone'monotone never overshoots between readings; linear joins them straight.
maxPointsnumber500The 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.
aspectRationumber2Width ÷ height of the plot.
statusLiveLineChartStatus'ready'loading draws a flat placeholder and holds the clock.
momentumColorsLiveLineMomentumColorsColour per direction. Left out, the chart draws in one hue throughout.
colorstringOverrides the --color-chart-1 token. Ignored when momentumColors is set.
onActivePointChange(point: LiveLinePoint | null) => voidThe reading under the crosshair as it moves, and null when the finger lifts.

LiveLineChart.Grid

PropTypeDefaultDescription
rowsnumberHow many bands the plot is divided into.
colorstring
dashArraystring

LiveLineChart.Line

PropTypeDefaultDescription
strokeWidthnumber
colorstringOverrides the chart's colour, momentum included.

LiveLineChart.Area

PropTypeDefaultDescription
opacitynumberOpacity at the top of the fill, fading to nothing at the baseline.
colorstringOverrides the chart's colour, momentum included.

LiveLineChart.Tip

PropTypeDefaultDescription
badgebooleanfalseShow 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.
pulsebooleantrueRing the dot with a repeating pulse.
formatValue(value: number) => stringFormat the badge. Defaults to a compact number.
classNamestring

LiveLineChart.XAxis

PropTypeDefaultDescription
ticksnumberHow many labels along the bottom.
formatTick(secondsAgo: number) => stringRewrites a label. Given how many seconds back the tick is.
classNamestring

LiveLineChart.YAxis

PropTypeDefaultDescription
ticksnumberHow many labels up the side.
formatValue(value: number) => stringFormat a value. Defaults to a compact number.
classNamestring

LiveLineChart.Tooltip

PropTypeDefaultDescription
formatValue(value: number) => stringFormat the value. Defaults to a compact number.
classNamestring

LiveLineChart.Skeleton

PropTypeDefaultDescription
colorstring

LiveLineChart.Header

PropTypeDefaultDescription
classNamestring
titlestringSmall line above the value — what is being watched.
valuestringThe readout. Left out, it shows the current reading.
captionstringOne muted line under the value.
formatValue(value: number) => stringFormat 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.

On this page