- framer-motion: ^10.18.0
Basics
Scrubber can be used to provide horizontal interaction with a chart. As your mouse hovers over the chart, you will see a line and scrubber beacon following.
<LineChart enableScrubbing showArea showYAxis height={{ base: 150, tablet: 200, desktop: 250 }} series={[ { id: 'prices', data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58], }, ]} xAxis={{ /* Give space between the scrubber and the axis */ range: ({ min, max }) => ({ min, max: max - 8 }), }} yAxis={{ showGrid: true, }} > <Scrubber idlePulse /> </LineChart>
All series will be scrubbed by default. You can set seriesIds to show only specific series.
<LineChart enableScrubbing height={{ base: 150, tablet: 200, desktop: 250 }} series={[ { id: 'top', data: [15, 28, 32, 44, 46, 36, 40, 45, 48, 38], }, { id: 'upperMiddle', data: [12, 23, 21, 29, 34, 28, 31, 38, 42, 35], color: '#ef4444', type: 'dotted', }, { id: 'lowerMiddle', data: [8, 15, 14, 25, 20, 18, 22, 28, 24, 30], color: '#f59e0b', curve: 'natural', gradient: { axis: 'y', stops: [ { offset: 0, color: '#E3D74D' }, { offset: 100, color: '#F7931A' }, ], }, LineComponent: (props) => <SolidLine {...props} strokeWidth={4} />, }, { id: 'bottom', data: [4, 8, 11, 15, 16, 14, 16, 10, 12, 14], color: '#800080', curve: 'step', AreaComponent: DottedArea, showArea: true, }, ]} > <Scrubber seriesIds={['top', 'lowerMiddle']} /> </LineChart>
Labels
Setting label on a series will display a label to the side of the scrubber beacon, and
setting label on Scrubber displays a label above the scrubber line.
<LineChart enableScrubbing height={{ base: 150, tablet: 200, desktop: 250 }} series={[ { id: 'prices', data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58], label: 'Price', }, ]} showArea > <Scrubber label={(dataIndex: number) => `Day ${dataIndex + 1}`} /> </LineChart>
Pulsing
Setting idlePulse to true will cause the scrubber beacons to pulse when the user is not actively scrubbing.
<LineChart enableScrubbing showArea height={{ base: 150, tablet: 200, desktop: 250 }} series={[ { id: 'prices', data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58], color: 'var(--color-fgPositive)', }, ]} > <ReferenceLine LineComponent={(props) => <DottedLine {...props} strokeDasharray="0 16" strokeWidth={3} />} dataY={10} stroke="var(--color-fg)" /> <Scrubber idlePulse /> </LineChart>
You can also use the imperative handle to pulse the scrubber beacons programmatically.
function ImperativeHandle() { const scrubberRef = useRef(null); return ( <VStack gap={2}> <LineChart enableScrubbing height={{ base: 150, tablet: 200, desktop: 250 }} series={[ { id: 'prices', data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58], }, ]} showArea > <Scrubber ref={scrubberRef} /> </LineChart> <Button onClick={() => scrubberRef.current?.pulse()}>Pulse</Button> </VStack> ); }
Styling
Beacons
You can use BeaconComponent to customize the visual appearance of scrubber beacons.
function OutlineBeacon() { // Simple outline beacon with no pulse animation const OutlineBeaconComponent = memo( forwardRef(({ dataX, dataY, seriesId, color, isIdle }: ScrubberBeaconProps, ref) => { const { getSeries, getXScale, getYScale } = useCartesianChartContext(); const targetSeries = getSeries(seriesId); const xScale = getXScale(); const yScale = getYScale(targetSeries?.yAxisId); const pixelCoordinate = useMemo(() => { if (!xScale || !yScale) return; return projectPoint({ x: dataX, y: dataY, xScale, yScale }); }, [dataX, dataY, xScale, yScale]); // Provide a no-op pulse implementation for simple beacons useImperativeHandle(ref, () => ({ pulse: () => {} }), []); if (!pixelCoordinate) return; if (isIdle) { return ( <> <m.circle animate={{ cx: pixelCoordinate.x, cy: pixelCoordinate.y }} cx={pixelCoordinate.x} cy={pixelCoordinate.y} transition={defaultTransition} r={6} fill={color} /> <m.circle animate={{ cx: pixelCoordinate.x, cy: pixelCoordinate.y }} cx={pixelCoordinate.x} cy={pixelCoordinate.y} transition={defaultTransition} r={3} fill="var(--color-bg)" /> </> ); } return ( <> <circle cx={pixelCoordinate.x} cy={pixelCoordinate.y} r={6} fill={color} /> <circle cx={pixelCoordinate.x} cy={pixelCoordinate.y} r={3} fill="var(--color-bg)" /> </> ); }), ); const dataCount = 14; const minDataValue = 0; const maxDataValue = 100; const minStepOffset = 5; const maxStepOffset = 20; const updateInterval = 2000; function generateNextValue(previousValue) { const range = maxStepOffset - minStepOffset; const offset = Math.random() * range + minStepOffset; let direction; if (previousValue >= maxDataValue) { direction = -1; } else if (previousValue <= minDataValue) { direction = 1; } else { direction = Math.random() < 0.5 ? -1 : 1; } let newValue = previousValue + offset * direction; return Math.max(minDataValue, Math.min(maxDataValue, newValue)); } function generateInitialData() { const data = []; let previousValue = Math.random() * (maxDataValue - minDataValue) + minDataValue; data.push(previousValue); for (let i = 1; i < dataCount; i++) { const newValue = generateNextValue(previousValue); data.push(newValue); previousValue = newValue; } return data; } const OutlineBeaconChart = memo(() => { const [data, setData] = useState(generateInitialData); useEffect(() => { const intervalId = setInterval(() => { setData((currentData) => { const lastValue = currentData[currentData.length - 1] ?? 50; const newValue = generateNextValue(lastValue); return [...currentData.slice(1), newValue]; }); }, updateInterval); return () => clearInterval(intervalId); }, []); return ( <LineChart enableScrubbing showArea showYAxis height={{ base: 150, tablet: 200, desktop: 250 }} series={[ { id: 'prices', data, color: 'var(--color-fg)', }, ]} xAxis={{ range: ({ min, max }) => ({ min, max: max - 16 }), }} yAxis={{ showGrid: true, domain: { min: 0, max: 100 } }} > <Scrubber BeaconComponent={OutlineBeaconComponent} /> </LineChart> ); }); return <OutlineBeaconChart />; }
Labels
You can use BeaconLabelComponent to customize the labels for each scrubber beacon.
function CustomBeaconLabel() { // This custom component label shows the percentage value of the data at the scrubber position. const MyScrubberBeaconLabel = memo(({ seriesId, color, label, ...props}: ScrubberBeaconLabelProps) => { const { getSeriesData, dataLength } = useCartesianChartContext(); const { scrubberPosition } = useScrubberContext(); const seriesData = useMemo(() => getLineData(getSeriesData(seriesId)), [getSeriesData, seriesId]); const dataIndex = useMemo(() => { return scrubberPosition ?? Math.max(0, dataLength - 1); }, [scrubberPosition, dataLength]); const percentageLabel = useMemo(() => { if (seriesData !== undefined) { const dataAtPosition = seriesData[dataIndex]; return `${label} · ${dataAtPosition}%`; } return label; }, [label, seriesData, dataIndex]) return ( <DefaultScrubberBeaconLabel {...props} seriesId={seriesId} color="rgb(var(--gray0))" background={color} label={percentageLabel} /> ); }); return ( <LineChart enableScrubbing height={{ base: 150, tablet: 200, desktop: 250 }} series={[ { id: 'Boston', data: [25, 30, 35, 45, 60, 100], color: 'rgb(var(--green40))', label: 'Boston', }, { id: 'Miami', data: [20, 25, 30, 35, 20, 0], color: 'rgb(var(--blue40))', label: 'Miami', }, { id: 'Denver', data: [10, 15, 20, 25, 40, 0], color: 'rgb(var(--orange40))', label: 'Denver', }, { id: 'Phoenix', data: [15, 10, 5, 0, 0, 0], color: 'rgb(var(--red40))', label: 'Phoenix', }, ]} showYAxis showArea areaType="dotted" yAxis={{ showGrid: true, }} > <Scrubber BeaconLabelComponent={MyScrubberBeaconLabel} /> </LineChart> ); }
Using labelElevated will elevate the Scrubber's reference line label with a shadow.
<LineChart enableScrubbing height={{ base: 150, tablet: 200, desktop: 250 }} series={[ { id: 'prices', data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58], }, ]} showArea inset={{ top: 60 }} > <Scrubber label={(dataIndex: number) => `Day ${dataIndex + 1}`} labelElevated /> </LineChart>
You can use LabelComponent to customize this label even further.
function CustomLabelComponent() { const CustomLabelComponent = memo((props: ScrubberLabelProps) => { const { drawingArea } = useCartesianChartContext(); if (!drawingArea) return; return ( <DefaultScrubberLabel {...props} background="var(--color-bgPrimary)" color="var(--color-bgPrimaryWash)" dy={32} elevated fontWeight="label1" y={drawingArea.y + drawingArea.height} /> ); }); return ( <LineChart enableScrubbing showArea height={{ base: 150, tablet: 200, desktop: 250 }} inset={{ top: 16, bottom: 64 }} series={[ { id: 'prices', data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58], }, ]} > <Scrubber LabelComponent={CustomLabelComponent} label={(dataIndex: number) => `Day ${dataIndex + 1}`} /> </LineChart> ); }
Fonts
You can use labelFont to customize the font of the scrubber line label and beaconLabelFont to customize the font of the beacon labels.
<LineChart enableScrubbing showArea showYAxis height={{ base: 150, tablet: 200, desktop: 250 }} series={[ { id: 'btc', data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58], label: 'BTC', color: assets.btc.color, }, { id: 'eth', data: [5, 15, 18, 30, 65, 30, 15, 35, 15, 2, 45, 12, 15, 40], label: 'ETH', color: assets.eth.color, }, ]} yAxis={{ showGrid: true, }} > <Scrubber label={(dataIndex: number) => `Day ${dataIndex + 1}`} labelFont="legal" beaconLabelFont="legal" /> </LineChart>
Bounds
Use labelBoundsInset to prevent the scrubber line label from getting too close to chart edges.
<Box marginX={-3}> <LineChart enableScrubbing showArea height={{ base: 150, tablet: 200, desktop: 250 }} inset={{ left: 0, right: 0 }} series={[ { id: 'prices', data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58], }, ]} > <Scrubber label="Without bounds - text touches edge" labelBoundsInset={0} /> </LineChart> </Box>
<Box marginX={-3}> <LineChart enableScrubbing showArea height={{ base: 150, tablet: 200, desktop: 250 }} inset={{ left: 0, right: 0 }} series={[ { id: 'prices', data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58], }, ]} > <Scrubber label="With bounds inset - text has space" labelBoundsInset={{ left: 12, right: 12 }} /> </LineChart> </Box>
Line
You can use LineComponent to customize Scrubber's line. In this case, as a user scrubs, they will see a solid line instead of dotted.
<LineChart enableScrubbing height={{ base: 150, tablet: 200, desktop: 250 }} series={[ { id: 'prices', data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58], }, ]} showArea > <Scrubber LineComponent={SolidLine} /> </LineChart>
Opacity
You can use BeaconComponent and BeaconLabelComponent with the opacity prop to hide scrubber beacons and labels when idle.
function HiddenScrubberWhenIdle() { const MyScrubberBeacon = memo( forwardRef((props: ScrubberBeaconProps, ref) => { const { scrubberPosition } = useScrubberContext(); const isScrubbing = scrubberPosition !== undefined; return <DefaultScrubberBeacon ref={ref} {...props} opacity={isScrubbing ? 1 : 0} />; }), ); const MyScrubberBeaconLabel = memo((props: ScrubberBeaconLabelProps) => { const { scrubberPosition } = useScrubberContext(); const isScrubbing = scrubberPosition !== undefined; return <DefaultScrubberBeaconLabel {...props} opacity={isScrubbing ? 1 : 0} />; }); return ( <LineChart enableScrubbing showArea height={{ base: 150, tablet: 200, desktop: 250 }} series={[ { id: 'prices', data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58], label: 'Price', }, ]} > <Scrubber BeaconComponent={MyScrubberBeacon} BeaconLabelComponent={MyScrubberBeaconLabel} /> </LineChart> ); }
Overlay
By default, Scrubber will show an overlay to de-emphasize future data. You can hide this by setting hideOverlay to true.
<LineChart enableScrubbing height={{ base: 150, tablet: 200, desktop: 250 }} series={[ { id: 'prices', data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58], }, ]} showArea > <Scrubber hideOverlay /> </LineChart>