Live example:
app/demo/markers-and-trades.tsx
in the example app.Trade-fill markers and a scrolling buy/sell stream
Markers
Draw glyphs into the canvas at a(time, value) position. Markers are read on the UI thread, so
pass a SharedValue<Marker[]> and update it with .set(...) / .modify(...).
onMarkerPress fires on tap — there is no hover on touch. (It replaces the
old onMarkerHover.)Attaching trade details
Usemarker.data for your trade metadata, or use marker.id to look up a full
record in your application. onMarkerPress runs on the JS thread and receives
the marker plus its chart-local position at tap time. A collapsed group’s
event.members contains all its markers, so a detail sheet can list them:
Inspecting markers while scrubbing
Enablescrub.markers to include trades in the existing tooltip and scrub
callback. This is off by default and does not change marker taps or the built-in
tooltip’s content.
radius), inside the drawn time window. The
tolerance follows zoom and layout changes. In candle mode, it selects all
trades in the hovered candle’s [time, time + candleWidth) bucket. A trade
exactly at the end belongs to the next candle. Gaps select none.
With matching enabled, onScrub sends null when a valid point becomes
unavailable (for example an unpriced gap), clearing an external selection.
Results preserve marker input order and data. They include co-located trades
and members hidden by stacking, grouping, or maxVisible; glyph Y positions and
offsets do not affect a time-based scrub. This is independent of
markerHitRadius, which continues to control taps.
TooltipRenderProps.markers is a shared value. Bind it inside an animated
component so the count/details update on the UI thread:
[] while disabled, inactive, or without a match. Its
array stays stable while movement/price ticks leave the selection unchanged;
marker input changes refresh the details. Read metadata in the same worklet
(for example (marker.data as { quantity: number }).quantity). For a React list,
use onScrub or bridge a useAnimatedReaction only when the selection changes;
reading markers.get() during React render does not subscribe the component.
LiveChartSeries supports scrub.markers through its UI-thread onScrub
worklet; it does not expose renderTooltip. All supplied marker timestamps are
eligible, including those associated with hidden series.
The Markers & trades demo has Scrub details and Candles switches.
Hold briefly (150 ms), then drag across the plot to inspect trades while keeping
quick marker taps available. Try a burst with stacking enabled to inspect every
trade in the tooltip.
Stacking co-located markers
By default markers draw at their anchor and overlap. SetmarkerCluster="stacked" to fan
co-located markers apart horizontally (overlapping, left-over-right — a stacked-coins look) at
their side ("above" / "below" / "center") — e.g. buys below the line and sells above it. Once a
cluster exceeds maxBeforeGroup (default 5) it collapses into a single count-badge marker; tapping it
fires onMarkerPress with isGrouped: true and the full members list. Grouping is recomputed every
frame, so a cluster fans back out as you zoom in.
overlap (0–1, default 0.75) and the collapse threshold:
direction: "vertical" to pile co-located markers
into a column instead — each glyph keeps its time (x) and stacks along the value axis, growing
away from the line in its side direction ("above" climbs up, "below" descends, "center" climbs
up from the line). This is the “transactions piled on the candle” look; raise maxBeforeGroup so a
tall column doesn’t collapse to a count badge early.
maxVisible
below.
Cap a vertical column
maxVisible limits a vertical column to its oldest glyphs, keeping the
markers nearest the line and hiding newer overflow. It defaults to unbounded.
Grouping still takes precedence, so maxBeforeGroup must be higher than the
burst you want to cap; otherwise the bucket collapses to its group badge first.
Unbounded / 4 / 8
maxVisible control. Turn on vertical column, choose a cap, then tap
Burst ×12 to see the newest markers disappear.
Custom collapsed-group badge
A collapsed group draws a round count badge by default (groupBadge: "count"). Two ways to give
it your own Skia look:
The built-in count uses the measured width of each digit, so proportional fonts such as System
remain readable. Pass an object with letterSpacing to add or remove space between count digits:
groupBadge: "marker" draws the representative marker’s own glyph
(the newest marker in the run: its image → icon/pill → kind). So a run of green + buy pills
collapses to a single buy pill, not a generic count.
MarkerGroupBadge object to draw a
glyph that’s independent of the members, for when the collapse should look different from the
individual markers (e.g. tiny dots that collapse into a distinct “Buy 5” image). It takes your own
Skia image, or an icon/pill — the same options as a single marker.
showGroupCount to stamp the member count in the glyph’s top-right corner (the
“Buy 5” look). All of this is drawn in Skia and batched with every other marker (one draw call) —
unlike renderMarker, which floats a React Native view. Tapping the group still fires
onMarkerPress with isGrouped: true and the members list, regardless of the badge style.
Built-in kind glyphs are "trade" | "boost" | "graduation" | "winner" | "clawback". Override
the glyph with icon (text/emoji) or image (a Skia SkImage). Set pill to wrap the icon in a
filled circular badge in the marker color (icon rendered white) — e.g. a green + buy / red −
sell tag:
y: pass an absolute value, or omit it to pin the marker to the line. On a
single-series chart, omitting value anchors the marker to the line at time (interpolated from
data); in multi-series, set seriesId to anchor to that series’ line. So for a marker that should
always sit on the line, just give it a time and kind.
Plot edges: markers disappear when their time anchor leaves the plot’s left or right
edge, with half a pixel of tolerance for the live clock. This applies to built-in and custom
markers, clustering, and onMarkerPress. A glyph anchored inside the plot may still extend
past an edge; vertical anchors keep a 24 px allowance above and below the plot.
Custom marker rendering
Built-in glyphs are drawn into the Skia canvas. When you need something the canvas can’t draw — a nativeBlurView glass badge, a gradient pill, an animated component — pass renderMarker. Return
a React Native element to float it, auto-centered, at the marker’s live (time, value) position
(tracked on the UI thread); return null/undefined to keep the built-in glyph for that marker.
onMarkerPress via the marker hit-test. renderMarker receives a second ctx argument
({ index, isGrouped, groupCount, side }) so a representative can render a distinct collapsed look.
expo-blur is the consumer’s choice — renderMarker accepts any element, and the library takes no
extra dependency.
Use
renderMarker for a handful of special markers, not hundreds of high-frequency ones: each
custom marker is its own animated view + projection, whereas built-in glyphs batch into a single
draw call. Works the same on LiveChart and LiveChartSeries.Trade stream
For trade-fill markers specifically, feed aSharedValue<TradeEvent[]>:
Need a static line that doesn’t track the data — a price alert, target, or working order? See
Reference lines & bands.