# Scrubber

**📖 Live documentation:** https://cds.coinbase.com/components/charts/Scrubber/?platform=mobile

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-mobile/visualizations/chart'
```

## Examples

### Basics

Scrubber can be used to provide horizontal interaction with a chart. As you drag over the chart, you will see a line and scrubber beacon following.

The Scrubber component is optional. Charts like [BarChart](/components/charts/BarChart) can use `enableScrubbing` with `getScrubberAccessibilityLabel` for screen reader accessibility without adding Scrubber—invisible tap targets allow users to navigate segments. Add Scrubber when you want the visual beacon, overlay, and labels for touch users.

```jsx
<LineChart
  enableScrubbing
  height={150}
  series={[
    {
      id: 'prices',
      data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58],
    },
  ]}
  showYAxis
  showArea
  yAxis={{
    showGrid: true,
  }}
>
  <Scrubber idlePulse />
</LineChart>
```

All series will be scrubbed by default. You can set `seriesIds` to show only specific series.
In `layout="horizontal"`, beacon labels are intentionally hidden to avoid overlap with scrubber beacons.

```jsx
<LineChart
  enableScrubbing
  height={150}
  series={[
    {
      id: 'top',
      data: [15, 28, 32, 44, 46, 36, 40, 45, 48, 38],
    },
    {
      id: 'bottom',
      data: [4, 8, 11, 15, 16, 14, 16, 10, 12, 14],
    },
  ]}
>
  <Scrubber seriesIds={['top']} />
</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.

```tsx
<LineChart
  enableScrubbing
  height={150}
  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

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
<LineChart
  enableScrubbing
  height={250}
  series={[
    {
      id: 'prices',
      data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58],
      color: 'var(--color-fgPositive)',
    },
  ]}
  showArea
>
  <ReferenceLine
    LineComponent={(props) => <DottedLine {...props} dashIntervals={[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.

```jsx
function ImperativeHandle() {
  const scrubberRef = useRef(null);
  return (
    <VStack gap={2}>
      <LineChart
        enableScrubbing
        height={150}
        series={[
          {
            id: 'prices',
            data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58],
          },
        ]}
        showYAxis
        showArea
        xAxis={{
          /* Give space between the scrubber and the axis */
          range: ({ min, max }) => ({ min, max: max - 8 }),
        }}
        yAxis={{
          showGrid: true,
        }}
      >
        <Scrubber ref={scrubberRef} />
      </LineChart>
      <Button onPress={() => scrubberRef.current?.pulse()}>Pulse</Button>
    </VStack>
  );
}
```

### Styling

#### Beacons

You can use the `beaconStroke` prop to customize the stroke color of the scrubber beacon.

```jsx
function CustomStrokeColor() {
  const theme = useTheme();
  const backgroundColor = `rgb(${theme.spectrum.red40})`;
  const foregroundColor = `rgb(${theme.spectrum.gray0})`;

  return (
    <Box background={backgroundColor} borderRadius="lg" padding={4}>
      <LineChart
        enableScrubbing
        showArea
        height={150}
        series={[
          {
            id: 'prices',
            data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58],
            color: foregroundColor,
          },
        ]}
      >
        <Scrubber
          idlePulse
          hideOverlay
          lineStroke={foregroundColor}
          beaconStroke={backgroundColor}
        />
      </LineChart>
    </Box>
  );
}
```

For more advanced customizations, you can pass a custom component to `BeaconComponent`.

```tsx
function OutlineBeacon() {
  const theme = useTheme();

  const dataCount = 14;
  const minDataValue = 0;
  const maxDataValue = 100;
  const minStepOffset = 5;
  const maxStepOffset = 20;
  const updateInterval = 2000;

  function generateNextValue(previousValue: number) {
    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;
    }

    const 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) => (
      <DefaultScrubberBeacon
        {...props}
        stroke={theme.color.fg}
        color={theme.color.bg}
        radius={5}
        strokeWidth={3}
      />
    ),
    [theme.color.fg, theme.color.bg],
  );

  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={150}
        series={[
          {
            id: 'prices',
            data,
            color: theme.color.fg,
          },
        ]}
        xAxis={{
          range: ({ min, max }) => ({ min, max: max - 16 }),
        }}
        yAxis={{
          showGrid: true,
          domain: { min: 0, max: 100 },
        }}
      >
        <Scrubber BeaconComponent={InvertedBeacon} />
      </LineChart>
    );
  });

  return <OutlineBeaconChart />;
}
```

#### Labels

You can use `BeaconLabelComponent` to customize the labels for each scrubber beacon.

```tsx
function CustomBeaconLabel() {
  const theme = useTheme();
  // 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 = useDerivedValue(() => {
        return scrubberPosition.value ?? Math.max(0, dataLength - 1);
      }, [scrubberPosition, dataLength]);

      const percentageLabel = useDerivedValue(() => {
        if (seriesData !== undefined) {
          const dataAtPosition = seriesData[dataIndex.value];
          return `${unwrapAnimatedValue(label)} · ${dataAtPosition}%`;
        }
        return unwrapAnimatedValue(label);
      }, [label, seriesData, dataIndex]);

      return (
        <DefaultScrubberBeaconLabel
          {...props}
          background={color}
          color={theme.color.bg}
          label={percentageLabel}
          seriesId={seriesId}
        />
      );
    },
  );

  return (
    <LineChart
      enableScrubbing
      showArea
      showYAxis
      areaType="dotted"
      height={150}
      series={[
        {
          id: 'Boston',
          data: [25, 30, 35, 45, 60, 100],
          color: `rgb(${theme.spectrum.green40})`,
          label: 'Boston',
        },
        {
          id: 'Miami',
          data: [20, 25, 30, 35, 20, 0],
          color: `rgb(${theme.spectrum.blue40})`,
          label: 'Miami',
        },
        {
          id: 'Denver',
          data: [10, 15, 20, 25, 40, 0],
          color: `rgb(${theme.spectrum.orange40})`,
          label: 'Denver',
        },
        {
          id: 'Phoenix',
          data: [15, 10, 5, 0, 0, 0],
          color: `rgb(${theme.spectrum.red40})`,
          label: 'Phoenix',
        },
      ]}
      yAxis={{
        showGrid: true,
      }}
    >
      <Scrubber BeaconLabelComponent={MyScrubberBeaconLabel} />
    </LineChart>
  );
}
```

You can use `hideBeaconLabels` to hide beacon labels, while still being able to provide a label for a series.

```tsx
<LineChart
  enableScrubbing
  legend
  height={200}
  series={[
    {
      id: 'pageViews',
      data: [2400, 1398, 9800, 3908, 4800, 3800, 4300],
      color: theme.color.accentBoldGreen,
      label: 'Page Views',
    },
    {
      id: 'uniqueVisitors',
      data: [4000, 3000, 2000, 2780, 1890, 2390, 3490],
      color: theme.color.accentBoldPurple,
      label: 'Unique Visitors',
    },
  ]}
  showArea
  inset={{ top: 60 }}
>
  <Scrubber hideBeaconLabels label={(dataIndex: number) => `Day ${dataIndex + 1}`} labelElevated />
</LineChart>
```

Using `labelElevated` will elevate the Scrubber's reference line label with a shadow.

```tsx
<LineChart
  enableScrubbing
  height={200}
  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.

```tsx
function CustomLabelComponent() {
  const CustomLabelComponent = memo((props: ScrubberLabelProps) => {
    const theme = useTheme();
    const { drawingArea } = useCartesianChartContext();

    if (!drawingArea) return;

    return (
      <DefaultScrubberLabel
        {...props}
        background={theme.color.bgPrimary}
        color={theme.color.bgPrimaryWash}
        dy={32}
        elevated
        fontWeight={FontWeight.Bold}
        y={drawingArea.y + drawingArea.height}
      />
    );
  });
  return (
    <LineChart
      enableScrubbing
      showArea
      height={200}
      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>
  );
}
```

##### Multi-line Centered Text

You can create custom multi-line centered labels using Skia's `ParagraphBuilder` with `TextAlign.Center`. Set `paragraphAlignment={TextAlign.Center}` on your custom label component to ensure proper positioning.

```tsx
function TwoLineCenteredLabel() {
  const theme = useTheme();
  const data = useMemo(() => [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58], []);

  const fontMgr = useMemo(() => Skia.TypefaceFontProvider.Make(), []);

  const formatPrice = useCallback((price: number) => {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD',
      minimumFractionDigits: 2,
      maximumFractionDigits: 2,
    }).format(price);
  }, []);

  const scrubberLabel = useCallback(
    (index: number) => {
      const price = formatPrice(data[index]);
      const day = `Day ${index + 1}`;

      const priceStyle: SkTextStyle = {
        fontFamilies: ['Inter'],
        fontSize: 16,
        fontStyle: { weight: FontWeight.Bold },
        color: Skia.Color(theme.color.fg),
      };

      const dayStyle: SkTextStyle = {
        fontFamilies: ['Inter'],
        fontSize: 14,
        fontStyle: { weight: FontWeight.Normal },
        color: Skia.Color(theme.color.fgMuted),
      };

      const builder = Skia.ParagraphBuilder.Make({ textAlign: TextAlign.Center }, fontMgr);

      builder.pushStyle(priceStyle);
      builder.addText(price);
      builder.addText('\n');

      builder.pushStyle(dayStyle);
      builder.addText(day);

      const para = builder.build();
      para.layout(384);
      return para;
    },
    [data, formatPrice, theme.color.fg, theme.color.fgMuted, fontMgr],
  );

  // Custom label component that sets paragraphAlignment to center
  const CenteredScrubberLabel = memo((props: ScrubberLabelProps) => (
    <DefaultScrubberLabel {...props} paragraphAlignment={TextAlign.Center} />
  ));

  return (
    <LineChart
      enableScrubbing
      showArea
      height={200}
      inset={{ top: 64 }}
      series={[
        {
          id: 'prices',
          data: data,
          color: theme.color.accentBoldBlue,
        },
      ]}
    >
      <Scrubber
        idlePulse
        labelElevated
        LabelComponent={CenteredScrubberLabel}
        label={scrubberLabel}
      />
    </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.

```tsx
function CustomLabelFonts() {
  const theme = useTheme();

  return (
    <LineChart
      enableScrubbing
      showArea
      showYAxis
      height={200}
      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.

```jsx
function WithoutBoundsExample() {
  return (
    <LineChart
      enableScrubbing
      showArea
      height={150}
      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>
  );
}
```

```jsx
function WithBoundsExample() {
  return (
    <LineChart
      enableScrubbing
      showArea
      height={150}
      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>
  );
}
```

#### 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
<LineChart
  enableScrubbing
  height={150}
  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.

```tsx
function HiddenScrubberWhenIdle() {
  const MyScrubberBeacon = memo(
    forwardRef((props: ScrubberBeaconProps, ref) => {
      const { scrubberPosition } = useScrubberContext();
      const beaconOpacity = useDerivedValue(
        () => (scrubberPosition.value !== undefined ? 1 : 0),
        [scrubberPosition],
      );

      return <DefaultScrubberBeacon ref={ref} {...props} opacity={beaconOpacity} />;
    }),
  );

  const MyScrubberBeaconLabel = memo((props: ScrubberBeaconLabelProps) => {
    const { scrubberPosition } = useScrubberContext();
    const labelOpacity = useDerivedValue(
      () => (scrubberPosition.value !== undefined ? 1 : 0),
      [scrubberPosition],
    );

    return <DefaultScrubberBeaconLabel {...props} opacity={labelOpacity} />;
  });

  return (
    <LineChart
      enableScrubbing
      showArea
      height={150}
      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`.

```jsx
<LineChart
  enableScrubbing
  ...
>
  <Scrubber hideOverlay />
</LineChart>
```

### Composed Examples

#### Percentage Beacon Labels

You can use `BeaconLabelComponent` to display a label with the percentage value of the data at the scrubber position.

```tsx
function PercentageBeaconLabels() {
  const theme = useTheme();

  const PercentageScrubberBeaconLabel = memo(
    ({ seriesId, color, label, ...props }: ScrubberBeaconLabelProps) => {
      const { getSeriesData, series, fontProvider } = useCartesianChartContext();
      const { scrubberPosition } = useScrubberContext();

      const seriesData = useMemo(
        () => getLineData(getSeriesData(seriesId)),
        [getSeriesData, seriesId],
      );

      const dataLength = useMemo(
        () =>
          series?.reduce((max, s) => {
            const data = getSeriesData(s.id);
            return Math.max(max, data?.length ?? 0);
          }, 0) ?? 0,
        [series, getSeriesData],
      );

      const dataIndex = useDerivedValue(() => {
        return scrubberPosition.value ?? Math.max(0, dataLength - 1);
      }, [scrubberPosition, dataLength]);

      const labelColor = `rgb(${theme.spectrum.gray0})`;

      const regularStyle: SkTextStyle = useMemo(
        () => ({
          fontFamilies: ['Inter'],
          fontSize: 14,
          fontStyle: {
            weight: FontWeight.Normal,
          },
          color: Skia.Color(labelColor),
        }),
        [labelColor],
      );

      const boldStyle: SkTextStyle = useMemo(
        () => ({
          ...regularStyle,
          fontStyle: {
            weight: FontWeight.Bold,
          },
        }),
        [regularStyle],
      );

      const percentageLabel = useDerivedValue(() => {
        const labelValue = unwrapAnimatedValue(label);

        if (seriesData !== undefined) {
          const dataAtPosition = seriesData[dataIndex.value];

          const builder = Skia.ParagraphBuilder.Make({ textAlign: TextAlign.Left }, fontProvider);

          builder.pushStyle(boldStyle);
          builder.addText(`${dataAtPosition}%`);
          builder.pushStyle(regularStyle);
          builder.addText(` ${labelValue}`);

          const para = builder.build();
          para.layout(512);
          return para;
        }

        return labelValue;
      }, [label, seriesData, dataIndex, fontProvider, boldStyle, regularStyle]);

      return (
        <DefaultScrubberBeaconLabel
          {...props}
          background={color}
          label={percentageLabel}
          seriesId={seriesId}
        />
      );
    },
  );

  const isLightTheme = theme.activeColorScheme === 'light';
  const background = isLightTheme
    ? `rgb(${theme.spectrum.gray90})`
    : `rgb(${theme.spectrum.gray0})`;
  const scrubberLineStroke = isLightTheme
    ? `rgb(${theme.spectrum.gray0})`
    : `rgb(${theme.spectrum.gray90})`;

  return (
    <Box borderRadius={300} padding={2} style={{ backgroundColor: background }}>
      <LineChart
        enableScrubbing
        showArea
        areaType="dotted"
        height={200}
        inset={{ bottom: 8, left: 8, top: 8, right: 0 }}
        series={[
          {
            id: 'prices2',
            data: [90, 78, 71, 55, 2, 55, 78, 48, 79, 96, 32, 80, 79, 42],
            color: `rgb(${theme.spectrum.blue40})`,
            label: 'ATL',
          },
          {
            id: 'prices',
            data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58],
            color: `rgb(${theme.spectrum.chartreuse40})`,
            label: 'NYC',
          },
        ]}
        xAxis={{
          range: ({ min, max }) => ({ min, max: max - 92 }),
        }}
      >
        <Scrubber
          hideOverlay
          idlePulse
          BeaconLabelComponent={PercentageScrubberBeaconLabel}
          beaconStroke={background}
          lineStroke={scrubberLineStroke}
        />
      </LineChart>
    </Box>
  );
}
```

#### Multi Line Beacon Label

You can render two-line beacon labels by returning a custom Skia paragraph from `BeaconLabelComponent`.

```tsx
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: Record<string, string> = {
  blue: 'BLUE',
  red: 'RED',
};

function MatchupBeaconLabels() {
  const theme = useTheme();

  const MatchupScrubberBeaconLabel = memo(
    ({ seriesId, color, ...props }: ScrubberBeaconLabelProps) => {
      const { getSeriesData, series, fontProvider } = useCartesianChartContext();
      const { scrubberPosition } = useScrubberContext();

      const seriesData = useMemo(
        () => getLineData(getSeriesData(seriesId)),
        [getSeriesData, seriesId],
      );

      const dataLength = useMemo(
        () =>
          series?.reduce((max, currentSeries) => {
            const data = getSeriesData(currentSeries.id);
            return Math.max(max, data?.length ?? 0);
          }, 0) ?? 0,
        [series, getSeriesData],
      );

      const dataIndex = useDerivedValue(() => {
        return scrubberPosition.value ?? Math.max(0, dataLength - 1);
      }, [scrubberPosition, dataLength]);

      const teamLabel = matchupTeamLabels[seriesId] ?? String(seriesId).toUpperCase();
      const labelColor = color ?? theme.color.fgPrimary;
      const legalFontSize = theme.fontSize.legal;
      const title3FontSize = theme.fontSize.title3;

      const teamStyle: SkTextStyle = useMemo(
        () => ({
          fontFamilies: ['Inter'],
          fontSize: legalFontSize,
          fontStyle: {
            weight: FontWeight.Normal,
          },
          color: Skia.Color(labelColor),
        }),
        [labelColor, legalFontSize],
      );

      const percentageStyle: SkTextStyle = useMemo(
        () => ({
          fontFamilies: ['Inter'],
          fontSize: title3FontSize,
          fontStyle: {
            weight: FontWeight.Bold,
          },
          color: Skia.Color(labelColor),
        }),
        [title3FontSize, labelColor],
      );

      const matchupLabel = useDerivedValue(() => {
        if (seriesData === undefined) {
          return teamLabel;
        }

        const value = seriesData[dataIndex.value];
        const builder = Skia.ParagraphBuilder.Make({ textAlign: TextAlign.Left }, fontProvider);

        builder.pushStyle(teamStyle);
        builder.addText(teamLabel);
        builder.addText('\n');
        builder.pushStyle(percentageStyle);
        builder.addText(`${value}%`);

        const paragraph = builder.build();
        paragraph.layout(240);
        return paragraph;
      }, [dataIndex, fontProvider, percentageStyle, seriesData, teamLabel, teamStyle]);

      return (
        <DefaultScrubberBeaconLabel
          {...props}
          background="transparent"
          color={labelColor}
          elevated={false}
          inset={0}
          label={matchupLabel}
          seriesId={seriesId}
        />
      );
    },
  );

  return (
    <LineChart
      enableScrubbing
      showArea
      areaType="dotted"
      height={150}
      inset={{ bottom: 8, left: 8, top: 8, right: 0 }}
      series={[
        {
          id: 'blue',
          data: matchupBlueData,
          color: `rgb(${theme.spectrum.blue50})`,
          label: 'BLUE',
        },
        {
          id: 'red',
          data: matchupRedData,
          color: `rgb(${theme.spectrum.red50})`,
          label: 'RED',
        },
      ]}
      xAxis={{
        range: ({ min, max }) => ({ min, max: max - 72 }),
      }}
      yAxis={{
        domain: { min: 0, max: 100 },
      }}
    >
      <Scrubber
        idlePulse
        BeaconLabelComponent={MatchupScrubberBeaconLabel}
        beaconLabelHorizontalOffset={16}
        beaconLabelPreferredSide="right"
      />
    </LineChart>
  );
}
```

## 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. |
| `beaconLabelFont` | `display1 \| display2 \| display3 \| title1 \| title2 \| title3 \| title4 \| headline \| body \| label1 \| label2 \| caption \| legal` | 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 | `theme.color.bg` | Stroke color of the scrubber beacon circle. |
| `beaconTransitions` | `{ enter?: Transition \| null; update?: Transition \| null \| undefined; pulse?: Transition \| undefined; pulseRepeatDelay?: number \| undefined; } \| undefined` | No | `-` | Transition configuration for the scrubber beacon. |
| `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` | `string \| SkParagraph \| ((dataIndex: number) => string \| SkParagraph)` | 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 | `{ 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` | `display1 \| display2 \| display3 \| title1 \| title2 \| title3 \| title4 \| headline \| body \| label1 \| label2 \| caption \| legal` | 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` | `null \| RefObject<View \| null> \| (instance: View \| null) => void \| (() => VoidOrUndefinedOnly)` | No | `-` | Allows getting a ref to the component instance. Once the component unmounts, React will set ref.current to null (or call the ref with null if you passed a callback ref). |
| `seriesIds` | `string[]` | No | `-` | Array of series IDs to highlight when scrubbing with scrubber beacons. By default, all series will be highlighted. |
| `transitions` | `{ enter?: Transition \| null; update?: Transition \| null \| undefined; pulse?: Transition \| undefined; pulseRepeatDelay?: number \| undefined; } \| undefined` | No | `-` | Transition configuration for the scrubber. Controls enter, update, and pulse animations for beacons and beacon labels. |


