Skip to main content
LiveChartSeries draws multiple line series that share an axis, time window, and crosshair. Pass a SharedValue<SeriesConfig[]>.
Live example: app/demo/multi-series.tsx in the example app.

Multiple live series with a toggleable legend

Update the series the same way you feed a single chart — append points in place with series.modify(...) (mutating each entry’s data array and value), or replace the whole array with series.set(...). Avoid series.value = .... Ordinary live price updates use smoothing, including feeds that append a point and set value together. The dot and newest drawn sample ease to the new value together, so the line does not bend back from a snapped sample to a lagging dot. This affects presentation only: source history and recorded scrub values stay unchanged. Earlier samples retain their recorded geometry. smoothing={1} and snapKey remain immediate; scrolling back retains its historical edge behavior. With an explicit presentationTime, an already-recorded value at or before the head is presented immediately because that clock/value pair already defines the presentation. Y-range contraction uses smoothing by default; outward bounds snap unless you pass rangeAnimation={{ animateExpansion: true }}. Enable it to ease the chart into a wider range when a series is revealed or a new extreme arrives. The first visible data fits immediately; subsequent expansion follows this setting. For a dataset or unit change (for example Price → Market Cap), replace each series’ history array and live value together. Replacement entries reach drawing on the next measured engine frame, alongside that frame’s Y range and tips. History arrays remain shared, so this does not isolate in-place edits to existing points. Normal live appends with series.modify(...) remain supported. Use snapKey when the replacement should settle immediately instead of easing with the configured smoothing.

Initial rendering

When history is already available, initialize series with that history before mounting the chart. This lets its lines, legend, and value-label layout initialize together. Its first measured, visible data fits the Y-range immediately, even with animateExpansion: true, so lines do not wait for a placeholder range to reach their prices. Later live updates, series toggles, and replacements retain the configured easing. Empty and hidden-only feeds wait for visible history before this initial fit. For asynchronous history, keep the loading state active until the feed is ready. Heavy controls or other surrounding UI can mount after the chart’s initial frame, as the Multi-series demo does.

Replay clock

Drive reconstruction or replay with a SharedValue rather than updating a numeric nowOverride prop every frame:
Supply each series’ current presented value alongside the clock. A head of 1050 produces viewport edge 1060. On a 320-point plot, the line and live dot end at x=288, leaving 32 points of breathing room. Advancing the head to 1070 moves the edge to 1080. Paths and range fitting ignore samples after an explicit head, and scrubbing uses the same time limit. A historical viewport ends at its own edge when that is earlier than the head. Panning into or past the buffer keeps the presented value and range. Scrub callbacks and tooltip values interpolate only through presented history and the current tip, with the selection dot clamped to that head. Degen bursts use the same dot position and ignore queued future momentum; an offscreen tip cannot spawn a burst.

All history

Set the authoritative start once to keep the complete history in a growing live window without updating the timeWindow prop on every frame:
At current time 3760, the visible window is 2875 seconds and its edge is 3875, so the left edge remains 1000. The growing span includes breathing room; use a buffer from 0 up to (but excluding) 1. timeWindow remains the fallback and the anchor for the default minimum zoom window. Pinch starts from the actual painted window. Default maximum zoom includes the full anchored span even when retained samples begin later; explicit zoom.minTimeWindow/maxTimeWindow take precedence. Pan and pinch can park a narrower or historical view; call ref.current?.resetZoom() to restore the growing live view.

Transforming the presentation range

For an overlay that needs temporary breathing room, transform the fitted range using a UI-thread reveal value instead of changing recorded prices:
Animate reveal from 0 to 1 with Reanimated when the overlay appears, then back to 0 on release. The callback runs after normal fitting/easing and can also read from and to, the visible Unix-second interval. Its output never feeds back into fitting: bounds 17.6–42.4 become 7.6–52.4 at full reveal on every frame. Releasing or removing the callback restores the original fit. Invalid ranges are ignored. Recorded prices and scrub values stay unchanged; hard bounds apply to the underlying fit before the presentation transform.

Series options

Each SeriesConfig supports: Set line={{ simplify: 1 }} on LiveChartSeries to denoise every rendered path by roughly one screen pixel. A series can choose a different tolerance, or set simplify: 0 to keep its exact point path. The shared axis, scrub values, and live dots continue to use the original series data.

Dimming replacement data

For a timeframe change, retain the current series while the next range loads, then set one UI-thread opacity multiplier for every plotted series:
The multiplier covers strokes, value lines, and inline value labels. It does not dim axes, the legend, reference lines, or scrub UI. Live dots and pulse rings fade completely out while the multiplier is below 1, then fade back in after the plotted series returns to full opacity.

Legend

The legend renders toggle chips by default. Configure it with the legend prop, and listen for taps with onSeriesToggle:

Per-series dots

The dot prop controls the live dots, their pulse, optional static glow, value lines, and inline labels. Each dot has a crisp contrasting outer ring (in the chart background color) by default, matching the single-series live dot so overlapping series stay legible. The soft glow remains opt-in:
Tune or turn off the backing ring, tune the independent glow, hide the dots, or override the fill color:
See the full prop list in the LiveChartSeries reference.

Per-series scrub tooltip

LiveChartSeries keeps its historical guide-only scrub by default. Opt into the Morfi market-detail readout with scrub.seriesTooltip: a dashed vertical guide, a top-centred bucket range, and one pill plus intersection dot for every visible series. Close values stack instead of overlapping, wide pills flip to the guide’s left near the right edge, and hidden legend series are omitted.
This feature is opt-in and backward compatible. scrub={true}, scrub={{}}, and scrub={{ seriesTooltip: false }} all keep the existing guide-only multi-series behavior. The existing scrub.tooltip property is still the master switch; setting it to false also suppresses the per-series pills.
Enable the default tooltip treatment with:
Pass an object instead of true to customize formatting and appearance:
formatSeriesValue and formatTimeRange run on the UI thread; keep them worklet-safe, just like the chart’s formatValue and formatTime. Omit bucketSeconds to infer the range width from the first visible series. The range end is always clamped to the chart’s current-time anchor, so an in-progress live bucket never displays a future end time. Set alwaysShow to keep the value pills at the visible endpoints while the user is not touching the chart. Idle mode deliberately hides the guide and time pill; scrubbing restores the full readout.
The multi-series demo has live toggles for the pills, pinned mode, and styling.