Skip to main content
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

Use marker.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

Enable scrub.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.
In line mode, the crosshair selects timestamps within 16 canvas pixels on either side (configurable through 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:
The tooltip selection is [] 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. Set markerCluster="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.
Pass the object form to tune the fan — overlap (0–1, default 0.75) and the collapse threshold:
By default the stack fans horizontally. Pass 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.
A column whose anchor sits near the top or bottom of the canvas is clamped at the canvas edge automatically: glyphs that would land off the plot are hidden (the base glyph always draws), the same overflow treatment as 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.
The example app’s Markers & trades screen includes an 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:
1. Reuse the members’ look — 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.
2. A dedicated group badge — pass a 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.
Either way, add 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:
Anchoring 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 native BlurView 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.
Custom-rendered markers are floated as RN views over the canvas, so they render crisp at native resolution (and skip the marker atlas entirely — no glyph drawn behind them). Taps still fire 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 a SharedValue<TradeEvent[]>:
Need a static line that doesn’t track the data — a price alert, target, or working order? See Reference lines & bands.