# ReferenceLine

**📖 Live documentation:** https://cds.coinbase.com/components/charts/ReferenceLine/

A horizontal or vertical reference line to mark important values on a chart, such as targets, thresholds, or baseline values.

## Import

```tsx
import { ReferenceLine } from '@coinbase/cds-web/visualizations/chart'
```

## Examples

### Basics

ReferenceLine can be used to add important details to a chart, such as a reference price or date. You can create horizontal lines using `dataY` or vertical lines using `dataX`.

```jsx live
<LineChart
  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)"
  />
</LineChart>
```

#### With Labels

You can add text labels to reference lines and position them using alignment and offset props:

```jsx live
<LineChart
  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],
    },
  ]}
  inset={0}
  showArea
>
  <ReferenceLine
    dataX={5}
    label="Vertical Reference Line"
    labelDx={8}
    labelHorizontalAlignment="left"
  />
  <ReferenceLine
    dataY={50}
    label="Horizontal Reference Line"
    labelDy={-8}
    labelHorizontalAlignment="right"
    labelVerticalAlignment="bottom"
  />
</LineChart>
```

### Data Values

ReferenceLine relies on `dataX` or `dataY` to position the line. Passing in `dataY` will create a horizontal line across the y axis at that value, and passing in `dataX` will do the same along the x axis.

```jsx live
<LineChart
  showArea
  curve="natural"
  height={{ base: 150, tablet: 200, desktop: 250 }}
  series={[
    {
      id: 'growth',
      data: [
        2, 4, 8, 15, 30, 65, 140, 280, 580, 1200, 2400, 4800, 9500, 19000, 38000, 75000, 150000,
      ],
      color: 'var(--color-fgPositive)',
    },
  ]}
>
  <ReferenceLine
    dataY={10000}
    label="10,000"
    labelDy={-4}
    labelPosition="left"
    labelVerticalAlignment="bottom"
  />
  <ReferenceLine
    dataY={100000}
    label="100,000"
    labelDy={-4}
    labelPosition="left"
    labelVerticalAlignment="bottom"
  />
</LineChart>
```

### Labels

#### Customization

You can customize label appearance using `labelFont`, `labelDx`, `labelDy`, `labelHorizontalAlignment`, and `labelVerticalAlignment` props.

```jsx live
<LineChart
  height={150}
  series={[
    {
      id: 'prices',
      data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58],
    },
  ]}
  showArea
>
  <ReferenceLine
    dataY={50}
    label="Target Price"
    labelDy={-8}
    labelFont="legal"
    labelHorizontalAlignment="right"
    labelPosition="right"
    labelVerticalAlignment="bottom"
  />
  <ReferenceLine
    dataX={7}
    label="Midpoint"
    labelDx={8}
    labelFont="label1"
    labelHorizontalAlignment="left"
    labelPosition="top"
  />
</LineChart>
```

#### Bounds

Use `labelBoundsInset` to prevent labels from getting too close to chart edges.

```jsx live
<Box marginX={-3}>
  <LineChart
    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],
      },
    ]}
    showArea
  >
    <ReferenceLine
      dataX={0}
      label="No Bounds Inset"
      labelBoundsInset={0}
      labelDy={0}
      labelPosition="top"
    />
    <ReferenceLine
      dataX={13}
      label="12px Bounds Inset"
      labelBoundsInset={{ left: 12, right: 12 }}
      labelDy={0}
      labelPosition="top"
    />
  </LineChart>
</Box>
```

#### Custom Components

You can adjust the style of the label using a custom `LabelComponent`.

```jsx live
function CustomLabelExample() {
  const PriceLabel = memo((props) => (
    <DefaultReferenceLineLabel
      {...props}
      background="var(--color-bgSecondary)"
      borderRadius={12.5}
      color="var(--color-fg)"
      inset={{ top: 4, bottom: 4, left: 8, right: 8 }}
      font="label1"
    />
  ));

  function Example() {
    const hourData = useMemo(() => sparklineInteractiveData.hour, []);
    const startPrice = hourData[0].value;
    const endPrice = hourData[hourData.length - 1].value;
    const isPositive = endPrice >= startPrice;
    const seriesColor = isPositive ? 'var(--color-fgPositive)' : 'var(--color-fgNegative)';

    const formattedStartPrice = useMemo(
      () =>
        startPrice.toLocaleString('en-US', {
          minimumFractionDigits: 2,
          maximumFractionDigits: 2,
        }),
      [startPrice],
    );

    return (
      <LineChart
        enableScrubbing
        showArea
        areaType="dotted"
        height={{ base: 200, tablet: 250, desktop: 300 }}
        series={[
          {
            id: 'hourly-prices',
            data: hourData.map((d) => d.value),
            color: seriesColor,
          },
        ]}
        xAxis={{
          range: ({ min, max }) => ({ min, max: max - 24 }),
        }}
      >
        <Scrubber />
        <ReferenceLine
          LabelComponent={PriceLabel}
          LineComponent={(props) => (
            <DottedLine {...props} strokeDasharray="0 16" strokeWidth={3} />
          )}
          dataY={startPrice}
          label={formattedStartPrice}
          stroke="var(--color-fgMuted)"
          labelDx={-12}
          labelHorizontalAlignment="right"
        />
      </LineChart>
    );
  }

  return <Example />;
}
```

You can also optionally hide the label based on user scrubbing.

```jsx live
function StartPriceReferenceLine() {
  const PriceLabel = memo((props) => {
    const { scrubberPosition } = useScrubberContext();
    const { getXScale, drawingArea } = useCartesianChartContext();
    const isScrubbing = scrubberPosition !== undefined;

    const fadeZone = 128;

    const opacity = useMemo(() => {
      if (!isScrubbing) return 0;
      const xScale = getXScale();
      if (!xScale) return 1;
      const scrubX = xScale(scrubberPosition) ?? 0;
      const rightEdge = drawingArea.x + drawingArea.width;
      return rightEdge - scrubX >= fadeZone ? 1 : 0;
    }, [isScrubbing, scrubberPosition, getXScale, drawingArea]);

    return (
      <DefaultReferenceLineLabel
        {...props}
        background="var(--color-bgSecondary)"
        borderRadius={12.5}
        color="var(--color-fg)"
        inset={{ top: 4, bottom: 4, left: 8, right: 8 }}
        font="label1"
        styles={{ root: { opacity: opacity, transition: 'opacity 0.25s ease' } }}
      />
    );
  });

  function Example() {
    const hourData = useMemo(() => sparklineInteractiveData.hour, []);
    const startPrice = hourData[0].value;
    const endPrice = hourData[hourData.length - 1].value;
    const isPositive = endPrice >= startPrice;
    const seriesColor = isPositive ? 'var(--color-fgPositive)' : 'var(--color-fgNegative)';

    const formattedStartPrice = useMemo(
      () =>
        startPrice.toLocaleString('en-US', {
          minimumFractionDigits: 2,
          maximumFractionDigits: 2,
        }),
      [startPrice],
    );

    return (
      <LineChart
        enableScrubbing
        showArea
        areaType="dotted"
        height={{ base: 200, tablet: 250, desktop: 300 }}
        series={[
          {
            id: 'hourly-prices',
            data: hourData.map((d) => d.value),
            color: seriesColor,
          },
        ]}
        xAxis={{
          range: ({ min, max }) => ({ min, max: max - 24 }),
        }}
      >
        <Scrubber />
        <ReferenceLine
          LabelComponent={PriceLabel}
          LineComponent={(props) => (
            <DottedLine {...props} strokeDasharray="0 16" strokeWidth={3} />
          )}
          dataY={startPrice}
          label={formattedStartPrice}
          stroke="var(--color-fgMuted)"
          labelDx={-12}
          labelHorizontalAlignment="right"
        />
      </LineChart>
    );
  }

  return <Example />;
}
```

### Draggable Price Target

You can pair a ReferenceLine with a custom drag component to create a draggable price target.

```tsx live
function DraggablePriceTarget() {
  const DragIcon = ({ x, y }) => {
    const DragCircle = (props) => <circle {...props} fill="var(--color-fg)" r="1.5" />;

    return (
      <g transform={`translate(${x}, ${y})`}>
        <g transform="translate(0, -8)">
          <DragCircle cx="2" cy="2" />
          <DragCircle cx="2" cy="8" />
          <DragCircle cx="2" cy="14" />
          <DragCircle cx="9" cy="2" />
          <DragCircle cx="9" cy="8" />
          <DragCircle cx="9" cy="14" />
        </g>
      </g>
    );
  };

  const TrendArrowIcon = ({ x, y, isPositive, color }) => {
    return (
      <g transform={`translate(${x - 8}, ${y - 8})`}>
        <g
          style={{
            // Flip horizontally and vertically for positive trend (pointing top-right)
            transform: isPositive ? 'scale(-1, -1)' : 'scale(-1, 1)',
            transformOrigin: '8px 8px',
          }}
        >
          <path
            d="M4.88574 12.7952L14.9887 2.69223L13.2916 0.995178L3.18883 11.098V4.84898L0.988831 7.04898V14.9952H8.99974L11.1997 12.7952H4.88574Z"
            fill={color}
          />
        </g>
      </g>
    );
  };

  const DynamicPriceLabel = memo(({ color, ...props }) => (
    <DefaultReferenceLineLabel
      {...props}
      background={color}
      borderRadius={4}
      color="white"
      dx={-12}
      font="label1"
      horizontalAlignment="right"
      inset={{ top: 5, bottom: 5, left: 10, right: 10 }}
    />
  ));

  const DraggableReferenceLine = memo(({ baselineAmount, startAmount, chartRef }) => {
    const theme = useTheme();
    const { isPhone } = useBreakpoints();

    const formatPrice = useCallback((value) => {
      return `$${value.toLocaleString('en-US', {
        minimumFractionDigits: 2,
        maximumFractionDigits: 2,
      })}`;
    }, []);

    const { getYScale, drawingArea } = useCartesianChartContext();
    const [amount, setAmount] = useState(startAmount);
    const [isDragging, setIsDragging] = useState(false);
    const [textDimensions, setTextDimensions] = useState({ width: 0, height: 0 });
    const color = amount >= baselineAmount ? 'var(--color-bgPositive)' : 'var(--color-bgNegative)';

    const yScale = getYScale();

    const labelComponent = useCallback(
      (props) => <DynamicPriceLabel {...props} color={color} />,
      [color],
    );

    // Set up persistent event listeners on the chart SVG element
    useEffect(() => {
      const element = chartRef.current;

      if (!element || !yScale || !('invert' in yScale && typeof yScale.invert === 'function')) {
        return;
      }

      const updatePosition = (clientX, clientY) => {
        const point = element.createSVGPoint();
        point.x = clientX;
        point.y = clientY;

        const svgPoint = point.matrixTransform(element.getScreenCTM()?.inverse());

        // Clamp the Y position to the chart area
        const clampedY = Math.max(
          drawingArea.y,
          Math.min(drawingArea.y + drawingArea.height, svgPoint.y),
        );

        const rawAmount = yScale.invert(clampedY);

        const rawPercentage = ((rawAmount - baselineAmount) / baselineAmount) * 100;

        let targetPercentage = Math.round(rawPercentage);

        if (targetPercentage === 0) {
          targetPercentage = rawPercentage >= 0 ? 1 : -1;
        }

        const newAmount = baselineAmount * (1 + targetPercentage / 100);
        setAmount(newAmount);
      };

      const handleMouseMove = (event: MouseEvent) => {
        if (!isDragging) {
          return;
        }
        updatePosition(event.clientX, event.clientY);
      };

      const handleTouchMove = (event: TouchEvent) => {
        if (!isDragging || event.touches.length === 0) {
          return;
        }
        const touch = event.touches[0];
        updatePosition(touch.clientX, touch.clientY);
      };

      const handleMouseUp = () => {
        setIsDragging(false);
      };

      const handleTouchEnd = () => {
        setIsDragging(false);
      };

      const handleMouseLeave = () => {
        setIsDragging(false);
      };

      element.addEventListener('mousemove', handleMouseMove);
      element.addEventListener('mouseup', handleMouseUp);
      element.addEventListener('mouseleave', handleMouseLeave);
      element.addEventListener('touchmove', handleTouchMove);
      element.addEventListener('touchend', handleTouchEnd);
      element.addEventListener('touchcancel', handleTouchEnd);

      return () => {
        element.removeEventListener('mousemove', handleMouseMove);
        element.removeEventListener('mouseup', handleMouseUp);
        element.removeEventListener('mouseleave', handleMouseLeave);
        element.removeEventListener('touchmove', handleTouchMove);
        element.removeEventListener('touchend', handleTouchEnd);
        element.removeEventListener('touchcancel', handleTouchEnd);
      };
    }, [isDragging, yScale, chartRef, baselineAmount, drawingArea.y, drawingArea.height]);

    if (!yScale) return null;

    const yPixel = yScale(amount);

    if (yPixel === undefined || yPixel === null) return null;

    const difference = amount - baselineAmount;
    const percentageChange = Math.round((difference / baselineAmount) * 100);
    const isPositive = difference > 0;

    const percentageLabel = isPhone
      ? `${Math.abs(percentageChange)}%`
      : `${Math.abs(percentageChange)}% (${formatPrice(Math.abs(difference))})`;
    const dollarLabel = formatPrice(amount);

    const handleMouseDown = (e) => {
      e.preventDefault();
      setIsDragging(true);
    };

    const handleTouchStart = (e) => {
      e.preventDefault();
      setIsDragging(true);
    };

    const padding = 16;
    const dragIconSize = 16;
    const trendArrowIconSize = 16;
    const iconGap = 8;
    const totalPadding = padding * 2 + iconGap;

    const rectWidth = textDimensions.width + totalPadding + dragIconSize + trendArrowIconSize;

    return (
      <>
        <ReferenceLine
          LabelComponent={labelComponent}
          dataY={amount}
          label={dollarLabel}
          labelPosition="right"
        />
        <g
          onMouseDown={handleMouseDown}
          onTouchStart={handleTouchStart}
          style={{
            cursor: isDragging ? 'grabbing' : 'grab',
            opacity: textDimensions.width === 0 ? 0 : 1,
          }}
        >
          <rect
            fill="var(--color-bgSecondary)"
            height={32}
            rx={theme.borderRadius['400']}
            ry={theme.borderRadius['400']}
            width={rectWidth}
            x={drawingArea.x}
            y={yPixel - 16}
          />
          <DragIcon x={drawingArea.x + padding} y={yPixel} />
          <TrendArrowIcon
            color={color}
            isPositive={isPositive}
            x={drawingArea.x + padding + dragIconSize + iconGap}
            y={yPixel}
          />
          <ChartText
            disableRepositioning
            color={color}
            font="label1"
            horizontalAlignment="left"
            onDimensionsChange={(dimensions) => setTextDimensions(dimensions)}
            verticalAlignment="middle"
            x={drawingArea.x + padding + dragIconSize + iconGap + trendArrowIconSize}
            y={yPixel + 1}
          >
            {percentageLabel}
          </ChartText>
        </g>
      </>
    );
  });

  const BaselinePriceLabel = useMemo(
    () =>
      memo((props) => <DefaultReferenceLineLabel {...props} dx={8} horizontalAlignment="left" />),
    [],
  );

  const PriceTargetChart = () => {
    const priceData = useMemo(() => sparklineInteractiveData.year.map((d) => d.value), []);
    const { isPhone } = useBreakpoints();

    const chartRef = useRef<SVGSVGElement>(null);

    const formatPrice = useCallback((value) => {
      return `$${value.toLocaleString('en-US', {
        minimumFractionDigits: 2,
        maximumFractionDigits: 2,
      })}`;
    }, []);

    return (
      <LineChart
        ref={chartRef}
        showArea
        animate={false}
        height={250}
        inset={
          isPhone
            ? { top: 16, bottom: 16, left: 0, right: 0 }
            : { top: 16, bottom: 16, left: 8, right: 80 }
        }
        series={[
          {
            id: 'prices',
            data: priceData,
            color: assets.btc.color,
          },
        ]}
        yAxis={{ domain: ({ min, max }) => ({ min: min * 0.7, max: max * 1.3 }) }}
      >
        {!isPhone && (
          <ReferenceLine
            LabelComponent={BaselinePriceLabel}
            LineComponent={SolidLine}
            dataY={priceData[priceData.length - 1]}
            label={formatPrice(priceData[priceData.length - 1])}
          />
        )}
        <DraggableReferenceLine
          baselineAmount={priceData[priceData.length - 1]}
          chartRef={chartRef}
          startAmount={priceData[priceData.length - 1] * 1.3}
        />
      </LineChart>
    );
  };
  return <PriceTargetChart />;
}
```

## 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<FontFamily \| inherit>` | 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<FontFamily \| inherit>` | 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<HTMLButtonElement \| null> \| (instance: HTMLButtonElement \| 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. |
| `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. |


