# Scrubber **๐Ÿ“– Live documentation:** https://cds.coinbase.com/components/charts/Scrubber/ An interactive scrubber component for exploring individual data points in charts. Displays values on hover or drag and supports custom labels and formatting. ## Import ```tsx import { Scrubber } from '@coinbase/cds-web-visualization' ``` ## Examples ### 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. ```jsx live ({ min, max: max - 8 }), }} yAxis={{ showGrid: true, }} > ``` All series will be scrubbed by default. You can set `seriesIds` to show only specific series. ```jsx live , }, { id: 'bottom', data: [4, 8, 11, 15, 16, 14, 16, 10, 12, 14], color: '#800080', curve: 'step', AreaComponent: DottedArea, showArea: true, }, ]} > ``` ### 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. In `layout="horizontal"`, beacon labels are intentionally hidden to avoid overlap with scrubber beacons. ```jsx live `Day ${dataIndex + 1}`} /> ``` ### Pulsing Pulses will show even when animation is disabled for the chart or scrubber. Set `idlePulse` to cause scrubber beacons to pulse when the user is not actively scrubbing. ```jsx live } dataY={10} stroke="var(--color-fg)" /> ``` You can also use the imperative handle to pulse the scrubber beacons programmatically. ```jsx live function ImperativeHandle() { const scrubberRef = useRef(null); return ( ); } ``` ### Styling #### Beacons You can use the `beaconStroke` prop to customize the stroke color of the scrubber beacon. ```jsx live ``` For more advanced customizations, you can pass a custom component to `BeaconComponent`. ```jsx live function OutlineBeacon() { 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 InvertedBeacon = useMemo( () => (props) => ( ), [], ); 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 ( ({ min, max: max - 16 }), }} yAxis={{ showGrid: true, domain: { min: 0, max: 100 }, }} > ); }); return ; } ``` #### Labels You can use `BeaconLabelComponent` to customize the labels for each scrubber beacon. ```jsx live function CustomBeaconLabel() { // This custom component label shows the percentage value of the data at the scrubber position. const MyScrubberBeaconLabel = memo(({ seriesId, color, label, ...props }) => { 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 ( ); }); return ( ); } ``` You can use `hideBeaconLabels` to hide beacon labels, while still being able to provide a label for a series. ```jsx live `Day ${dataIndex + 1}`} labelElevated /> ``` Using `labelElevated` will elevate the Scrubber's reference line label with a shadow. ```jsx live `Day ${dataIndex + 1}`} labelElevated /> ``` You can use `LabelComponent` to customize this label even further. ```jsx live function CustomLabelComponent() { const CustomLabelComponent = memo((props) => { const { drawingArea } = useCartesianChartContext(); if (!drawingArea) return; return ( ); }); return ( `Day ${dataIndex + 1}`} /> ); } ``` ##### Fonts You can use `labelFont` to customize the font of the scrubber line label and `beaconLabelFont` to customize the font of the beacon labels. ```jsx live `Day ${dataIndex + 1}`} labelFont="legal" beaconLabelFont="legal" /> ``` ##### Bounds Use `labelBoundsInset` to prevent the scrubber line label from getting too close to chart edges. ```jsx live ``` ```jsx live ``` #### 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. ```jsx live ``` #### Opacity You can use `BeaconComponent` and `BeaconLabelComponent` with the `opacity` prop to hide scrubber beacons and labels when idle. ```jsx live function HiddenScrubberWhenIdle() { const MyScrubberBeacon = memo( forwardRef((props, ref) => { const { scrubberPosition } = useScrubberContext(); const isScrubbing = scrubberPosition !== undefined; return ; }), ); const MyScrubberBeaconLabel = memo((props) => { const { scrubberPosition } = useScrubberContext(); const isScrubbing = scrubberPosition !== undefined; return ; }); return ( ); } ``` #### Overlay By default, Scrubber will show an overlay to de-emphasize future data. You can hide this by setting `hideOverlay` to `true`. ```jsx live ``` ### Composed Examples #### Percentage Beacon Labels You can use `BeaconLabelComponent` to display a label with the percentage value of the data at the scrubber position. ```jsx live function PercentageBeaconLabels() { const PercentageScrubberBeaconLabel = memo(({ seriesId, color, label, ...props }) => { 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 ( <> {dataAtPosition}% {label} ); } return label; }, [label, seriesData, dataIndex]); return ( ); }); const PercentageBeaconLabelChart = ({ background, scrubberLineStroke, ...props }) => { return ( ); }; function Example() { const theme = useTheme(); const isLightTheme = theme.activeColorScheme === 'light'; const background = isLightTheme ? 'rgb(var(--gray90))' : 'rgb(var(--gray0))'; const scrubberLineStroke = isLightTheme ? 'rgb(var(--gray0))' : 'rgb(var(--gray90))'; return ( ({ min, max: max - 92 }), }} background={background} scrubberLineStroke={scrubberLineStroke} /> ); } return ; } ``` #### Multi Line Beacon Label You can use a custom `BeaconLabelComponent` to render each beacon label as two lines (team name + percentage). ```jsx live function MatchupBeaconLabels() { const matchupBlueData = [ 47, 50, 51, 52, 53, 53, 53, 53, 52, 51, 51, 52, 53, 55, 57, 58, 59, 61, 63, 65, 64, 64, 64, 64, 64, 63, 63, 63, 64, 66, 68, 70, 71, 72, 74, 76, 76, 75, 74, 73, 74, 75, 75, 78, ]; const matchupRedData = matchupBlueData.map((value) => 100 - value); const matchupTeamLabels = { blue: 'BLUE', red: 'RED', }; const TeamBeaconLabel = memo( ({ color = 'var(--color-fgPrimary)', teamLabel, percentageLabel, transition, x, y, dx, horizontalAlignment, onDimensionsChange, ...chartTextProps }) => { const teamLabelDimensionsRef = useRef(null); const percentageLabelDimensionsRef = useRef(null); const emitCombinedDimensions = useCallback(() => { if (!onDimensionsChange) { return; } const teamRect = teamLabelDimensionsRef.current; const percentageRect = percentageLabelDimensionsRef.current; if (!teamRect || !percentageRect) { return; } const minX = Math.min(teamRect.x, percentageRect.x); const minY = Math.min(teamRect.y, percentageRect.y); const maxX = Math.max(teamRect.x + teamRect.width, percentageRect.x + percentageRect.width); const maxY = Math.max( teamRect.y + teamRect.height, percentageRect.y + percentageRect.height, ); onDimensionsChange({ x: minX, y: minY, width: maxX - minX, height: maxY - minY, }); }, [onDimensionsChange]); const handleTeamLabelDimensionsChange = useCallback( (rect) => { teamLabelDimensionsRef.current = rect; emitCombinedDimensions(); }, [emitCombinedDimensions], ); const handlePercentageLabelDimensionsChange = useCallback( (rect) => { percentageLabelDimensionsRef.current = rect; emitCombinedDimensions(); }, [emitCombinedDimensions], ); return ( {teamLabel} {percentageLabel} ); }, ); const MatchupScrubberBeaconLabel = memo(({ seriesId, color, ...props }) => { 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 teamLabel = matchupTeamLabels[seriesId] ?? String(seriesId).toUpperCase(); const value = useMemo(() => { if (seriesData === undefined) { return null; } return seriesData[dataIndex]; }, [dataIndex, seriesData]); return ( ); }); return ( ({ min, max: max - 64 }), }} yAxis={{ domain: { min: 0, max: 100 }, }} > ); } ``` ## Props | Prop | Type | Required | Default | Description | | --- | --- | --- | --- | --- | | `BeaconComponent` | `ScrubberBeaconComponent` | No | `DefaultScrubberBeacon` | Custom component for the scrubber beacon. | | `BeaconLabelComponent` | `ScrubberBeaconLabelComponent` | No | `DefaultScrubberBeaconLabel` | Custom component to render as a scrubber beacon label. | | `LabelComponent` | `ReferenceLineLabelComponent` | No | `DefaultReferenceLineLabel` | Component to render the label. | | `LineComponent` | `LineComponent` | No | `DottedLine` | Component to render the line. | | `accessibilityLabel` | `string \| ((dataIndex: number) => string)` | No | `-` | Accessibility label for the scrubber. Can be a static string or a function that receives the current dataIndex. If not provided, label will be used if it resolves to a string. | | `beaconLabelFont` | `ResponsiveProp` | No | `-` | Font style for the beacon labels. | | `beaconLabelHorizontalOffset` | `number` | No | `-` | Horizontal offset for beacon labels from their beacon position. Measured in pixels. | | `beaconLabelMinGap` | `number` | No | `-` | Minimum gap between beacon labels to prevent overlap. Measured in pixels. | | `beaconLabelPreferredSide` | `left \| right` | No | `'right'` | Preferred side for beacon labels. | | `beaconStroke` | `string` | No | `'var(--color-bg)'` | Stroke color of the scrubber beacon circle. | | `beaconTransitions` | `{ enter?: Transition$1 \| null; update?: Transition$1 \| null \| undefined; pulse?: Transition$1 \| undefined; pulseRepeatDelay?: number \| undefined; } \| undefined` | No | `-` | Transition configuration for the scrubber beacon. | | `classNames` | `{ overlay?: string; beacon?: string \| undefined; line?: string \| undefined; label?: string \| undefined; beaconLabel?: string \| undefined; } \| undefined` | No | `-` | Custom class names for individual elements of the Scrubber component | | `hideBeaconLabels` | `boolean` | No | `true in horizontal layout, false in vertical layout.` | Hides the beacon labels while keeping the line label visible (if provided). | | `hideLine` | `boolean` | No | `-` | Hides the scrubber line. | | `hideOverlay` | `boolean` | No | `-` | Hides the overlay rect which obscures data beyond the scrubber position. | | `idlePulse` | `boolean` | No | `-` | Pulse the beacons while at rest. | | `key` | `Key \| null` | No | `-` | - | | `label` | `ChartTextChildren \| ((dataIndex: number) => ChartTextChildren)` | No | `-` | Label text displayed above the scrubber line. Can be a static string or a function that receives the current dataIndex. | | `labelBoundsInset` | `number \| ChartInset` | No | `inset { top: 4, bottom: 20, left: 12, right: 12 } when labelElevated is true, otherwise none` | Bounds inset for the scrubber line label to prevent cutoff at chart edges. | | `labelElevated` | `boolean` | No | `-` | Whether to elevate the label with a shadow. When true, applies elevation and automatically adds bounds to keep label within chart area. | | `labelFont` | `ResponsiveProp` | No | `-` | Font style for the scrubber line label. | | `lineStroke` | `string` | No | `-` | Stroke color for the scrubber line. | | `overlayOffset` | `number` | No | `2` | Offset of the overlay rect relative to the drawing area. Useful for when scrubbing over lines, where the stroke width would cause part of the line to be visible. | | `ref` | `((instance: ScrubberBeaconGroupRef \| null) => void) \| RefObject \| null` | No | `-` | - | | `seriesIds` | `string[]` | No | `-` | Array of series IDs to highlight when scrubbing with scrubber beacons. By default, all series will be highlighted. | | `styles` | `{ overlay?: CSSProperties; beacon?: CSSProperties \| undefined; line?: CSSProperties \| undefined; label?: CSSProperties \| undefined; beaconLabel?: CSSProperties \| undefined; } \| undefined` | No | `-` | Custom styles for individual elements of the Scrubber component | | `testID` | `string` | No | `-` | Used to locate this element in unit and end-to-end tests. Under the hood, testID translates to data-testid on Web. On Mobile, testID stays the same - testID | | `transitions` | `{ enter?: Transition$1 \| null; update?: Transition$1 \| null \| undefined; pulse?: Transition$1 \| undefined; pulseRepeatDelay?: number \| undefined; } \| undefined` | No | `-` | Transition configuration for the scrubber. Controls enter, update, and pulse animations for beacons and beacon labels. | ## Styles | Selector | Static class name | Description | | --- | --- | --- | | `overlay` | `-` | Overlay element | | `beacon` | `-` | Beacon circle element | | `line` | `-` | Scrubber line element | | `label` | `-` | Scrubber line label element | | `beaconLabel` | `-` | Beacon label element |