Cursor and scrub
A drag that solves the curve on the UI thread and reports nothing but numbers.
import {
CartesianChart,
ChartCursorDot,
ChartCursorLine,
ChartLine,
useChartScrub,
} from "@delacour/react-native-charts";
function Revenue() {
const scrub = useChartScrub(["revenue"]);
return (
<CartesianChart data={rows} scrub={scrub} xKey="day" yKeys={["revenue"]}>
<ChartLine color="#0A84FF" curve="monotone" yKey="revenue" />
<ChartCursorLine axis="x" color="#8E8E93" dash={[4, 4]} />
<ChartCursorDot borderColor="#FFFFFF" color="#0A84FF" yKey="revenue" />
</CartesianChart>
);
}useChartScrub allocates a set of shared values, one group per key you name; the root's scrub
prop is what makes the chart take touches at all. Hold and drag: the pan clamps the touch to the
plot, inverts it through the scale, finds the nearest datum and solves the drawn curve — all on
the UI thread, with no hop to JavaScript, which is what lets the dot keep up with a fast drag.
ChartCursorLine and ChartCursorDot read those values and draw. Both are invisible until the
scrub is active and both are parked off-canvas when there is nothing to point at, so a chart with
no touch yet has no stray mark at the origin.
Behaviour
<CartesianChart scrub={scrub} scrubConfig={{ behaviour: "claim" }} />
<CartesianChart scrub={scrub} scrubConfig={{ behaviour: "hold", holdDuration: 150 }} />
<CartesianChart scrub={scrub} scrubConfig={{ enabled: false }} />hold is the default: the scrub activates after a long press, so a chart inside a scrolling feed
takes nothing by accident. claim gives horizontal drags to the scrub and vertical ones to the
scroll — for a chart that owns its width. block wins outright, and needs the scrollable's own
gesture passed to useScrubGesture to block.
Reading the scrub from a view
import type { ChartScrubState } from "@delacour/react-native-charts";
import Animated, { useAnimatedStyle } from "react-native-reanimated";
function Readout({ scrub }: { scrub: ChartScrubState }) {
const style = useAnimatedStyle(() => {
const x = scrub.snappedX.value;
return {
opacity: scrub.isActive.value ? 1 : 0,
transform: [{ translateX: Number.isFinite(x) ? x : 0 }],
};
});
return <Animated.View style={style} />;
}Every field on ChartScrubState holds a number or a boolean — never an SkPath, never an
object. That is what lets a tooltip be an ordinary React Native view, positioned from a plain
useAnimatedStyle, with no Skia import in sight. series[key].value is the nearest datum's value
in domain units, which is what a label prints.
The per-series values start at NaN and x, y and snappedX go to NaN inside a gap. A NaN
written into a style freezes that view with no error — guard with Number.isFinite, as above.
y versus snappedY
<ChartCursorDot color="#0A84FF" yKey="revenue" />
<ChartCursorDot color="#0A84FF" snap={false} yKey="revenue" />Each series carries two points. snappedX/snappedY are the nearest datum's canvas point: a dot
bound to them lands on real measurements, which is what the readout beside it names. x/y are
the position on the drawn curve at the touched x, solved against the same cubics the renderer
drew: a dot bound to them glides continuously along the line. Both come out of one binary search.
The marks snap by default; snap={false} glides, and the dot will then disagree with any label
showing a measured value.
On a horizontal chart there is no curve to glide along, so the glide values equal the snapped
ones and the category lands in the root's snappedY.
Stacked series
For a key in stackKeys the dot rides the top of the visible segment, while series[key].value
stays the raw measurement — so the mark sits where the eye expects and the readout prints what
was measured.
API Reference
useChartScrub
function useChartScrub(keys: readonly string[]): ChartScrubState;Stable for the life of the component unless the set of keys changes. A fresh array of the same keys does not reallocate.
ChartCursorLine
Prop
Type
ChartCursorDot
Prop
Type
ScrubConfig
Prop
Type
ChartScrubState
Prop
Type
ChartScrubSeriesState
Prop
Type