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.

A screen reader receives one read-only chart snapshot rather than a new node for every arriving point: name, loading or empty state, current value and direction, time window, and whether it is paused. accessibilityLabel names it, falling back to the Header title and then “Live line chart”.

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.

If the chart you need is not one of these, Plot is the same drawing machinery with nothing decided: you compose the marks — columns, lines, fills, dots, and marks you write yourself — over one measured box and one shared scale.

Installation

LiveLineChart ships with the library — no separate install.

import { LiveLineChart, type LiveLinePoint, Frame, Button } 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>

Versions

Live

The window slides against the clock and the tip rides the newest reading. window is how many seconds are on screen; readings older than that leave on the left.

<Frame>
  <Frame.Header>
    <Frame.Title>Requests / sec</Frame.Title>
    <Frame.Action>Live</Frame.Action>
  </Frame.Header>
  <Frame.Panel>
    <LiveLineChart data={points} window={30}>
      <LiveLineChart.Header title="Right now" />
      <LiveLineChart.Grid />
      <LiveLineChart.Area />
      <LiveLineChart.Line />
      <LiveLineChart.Tip />
      <LiveLineChart.XAxis />
    </LiveLineChart>
  </Frame.Panel>
</Frame>

Momentum

Colour taken from the direction of travel rather than from one fixed hue. Passing an empty momentumColors takes the up, down and flat colours from the theme; give it your own to override them.

<LiveLineChart data={points} window={24} momentumColors={{}}>
  <LiveLineChart.Header
    title="Throughput"
    caption="The colour is the last few readings, not the value"
  />
  <LiveLineChart.Grid />
  <LiveLineChart.YAxis />
  <LiveLineChart.Area />
  <LiveLineChart.Line />
  <LiveLineChart.Tip />
  <LiveLineChart.XAxis />
</LiveLineChart>

Read back

Drag across the plot to read a moment that has already gone past, and hold the window still to talk about it. The two work together: dragging a window that is still moving means chasing the reading you are trying to name.

const [held, setHeld] = useState(false);

<Frame>
  <Frame.Header>
    <Frame.Title>Read it back</Frame.Title>
    <Frame.Action>
      <Button size="sm" variant="ghost" onPress={() => setHeld(!held)}>
        {held ? 'Resume' : 'Hold'}
      </Button>
    </Frame.Action>
  </Frame.Header>
  <Frame.Panel>
    <LiveLineChart data={points} window={30} paused={held}>
      <LiveLineChart.Header title="Requests / sec" caption="Drag across the plot" />
      <LiveLineChart.Grid />
      <LiveLineChart.YAxis />
      <LiveLineChart.Line />
      <LiveLineChart.Tip />
      <LiveLineChart.Tooltip />
      <LiveLineChart.XAxis />
    </LiveLineChart>
  </Frame.Panel>
</Frame>

Lifecycle contract

CaseContract
default initializationAn empty input starts with a finite empty domain and no active reading.
controlled acceptanceThe data prop is authoritative; normalized finite readings are rendered.
controlled rejectionNot applicable: the chart emits no data mutation requests.
external resetReplacing data reconciles or clears the selected timestamp.
disabled pathpaused and inactive AppState both stop frame ownership.
prop replacementdata, maxPoints, window, motion, pause, and tooltip changes are reconciled.
unmount cleanupAnimation frames and AppState listeners are released; Tooltip removal clears selection.
reduced motionThe live clock owns no frames while reduced motion is active.
callback countsSelection callbacks follow only accepted finite readings.

Executable evidence: packages/panelui/test/live-line-lifecycle.test.mjs (4 tests).

API Reference

LiveLineChart

PropTypeDefaultDescription
classNamestring—
accessibilityLabelstring—Names the chart's single screen-reader snapshot. Falls back to the Header title, then to "Live line chart".
accessibilityHintstring—Additional guidance after the snapshot. No gesture is invented for it.
dataLiveLinePoint[]—The readings so far. Invalid values are dropped and timestamps are ordered.
windownumber30How much time the plot spans, in seconds. Invalid values use 30.
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. Must be positive and finite.
aspectRationumber2Width ÷ height of the plot.
statusLiveLineChartStatus'ready'loading draws a flat placeholder and holds the clock.
momentumColorsLiveLineMomentumColors—Colour per direction. Left out, the chart draws in one hue throughout.
colorstring—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

PropTypeDefaultDescription
rowsnumber—How many bands the plot is divided into.
colorstring—
dashArraystring—

LiveLineChart.Line

PropTypeDefaultDescription
strokeWidthnumber—
colorstring—Overrides the chart's colour, momentum included.

LiveLineChart.Area

PropTypeDefaultDescription
opacitynumber—Opacity at the top of the fill, fading to nothing at the baseline.
colorstring—Overrides 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) => string—Format the badge. Defaults to a compact number.
classNamestring—

LiveLineChart.XAxis

PropTypeDefaultDescription
ticksnumber—How many labels along the bottom.
formatTick(secondsAgo: number) => string—Rewrites a label. Given how many seconds back the tick is.
classNamestring—

LiveLineChart.YAxis

PropTypeDefaultDescription
ticksnumber—How many labels up the side.
formatValue(value: number) => string—Format a value. Defaults to a compact number.
classNamestring—

LiveLineChart.Tooltip

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

LiveLineChart.Skeleton

PropTypeDefaultDescription
colorstring—

LiveLineChart.Header

PropTypeDefaultDescription
classNamestring—
titlestring—Small line above the value — what is being watched.
valuestring—The readout. Left out, it shows the current reading.
captionstring—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.

The chart owns one canonical buffer: non-finite readings are ignored, out-of-order timestamps are sorted, and the last reading at a duplicate timestamp replaces the earlier one. maxPoints is applied after that normalization, so a controlled replacement or a smaller limit cannot leave a stale point selected. 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.

The visual Header value, axes, Tip badge, crosshair and Tooltip are hidden from the accessibility tree because the chart snapshot already carries their reading. Header action children remain ordinary accessible controls. Existing Header, Tip and Tooltip formatters are reused for the snapshot. It updates when data or state renders, but it is not a live region and does not announce every frame. Tooltip read-back remains a touch interaction; onActivePointChange reports that interaction but is not an action, so the chart does not claim adjustable controls. Render application errors beside or instead of the chart — status models only loading and ready.

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, while the app is backgrounded, on paused, on status="loading", and never starts under reduced motion. Returning to the app synchronizes the window before drawing resumes. Nothing else in the library animates without an interaction or a change of data, so a chart left mounted off-screen is worth paused.

Public exports

Values: LiveLineChart, useLiveLineChart

Types: LiveLineChartProps, LiveLineChartHandle, LiveLineChartHeaderProps, LiveLineChartGridProps, LiveLineChartAreaProps, LiveLineChartLineProps, LiveLineChartTipProps, LiveLineChartXAxisProps, LiveLineChartYAxisProps, LiveLineChartTooltipProps, LiveLineChartSkeletonProps, LiveLineChartStatus, LiveLineMomentum, LiveLineMomentumColors, LiveLinePoint

On this page