# 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-visualization'
```

## Examples

### Basic Example

ReferenceLine can be used to add important details to a chart, such as a reference price or date.

```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}
  curve="monotone"
  showArea
>
  <ReferenceLine
    dataX={5}
    label="Vertical Reference Line"
    labelProps={{ horizontalAlignment: 'left', dx: 8 }}
  />
  <ReferenceLine
    dataY={50}
    label="Horizontal Reference Line"
    labelProps={{ verticalAlignment: 'bottom', dy: -8, horizontalAlignment: 'right' }}
  />
</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"
    labelPosition="left"
    labelProps={{ verticalAlignment: 'bottom', dy: -4 }}
  />
  <ReferenceLine
    dataY={100000}
    label="100,000"
    labelPosition="left"
    labelProps={{ verticalAlignment: 'bottom', dy: -4 }}
  />
</LineChart>
```

### Customization

#### Label Style

You can adjust the style of the label using the `labelProps` prop.

```jsx live
<LineChart
  curve="monotone"
  height={{ base: 150, tablet: 200, desktop: 250 }}
  inset={{ right: 4 }}
  series={[
    {
      id: 'prices',
      data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58],
    },
  ]}
>
  <ReferenceLine
    dataY={25}
    label="Liquidation"
    labelPosition="left"
    labelProps={{
      horizontalAlignment: 'left',
      dx: 12,
      borderRadius: 4,
      inset: { top: 4, bottom: 4, left: 8, right: 8 },
      color: 'rgb(var(--yellow70))',
      background: 'var(--color-accentSubtleYellow)',
      font: 'label1',
    }}
    stroke="var(--color-bgWarning)"
  />
  <ReferenceLine
    dataY={25}
    label="$25"
    labelPosition="right"
    labelProps={{
      horizontalAlignment: 'right',
      dx: -12,
      borderRadius: 4,
      inset: { top: 2, bottom: 2, left: 4, right: 4 },
      color: 'rgb(var(--yellow70))',
      background: 'var(--color-bg)',
      font: 'label1',
    }}
    stroke="transparent"
  />
</LineChart>
```

#### Draggable Price Target

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

```jsx live
function DraggablePriceTarget() {
  const DragIcon = ({ x, y }: { x: number; y: number }) => {
    const DragCircle = (props: React.SVGProps<SVGCircleElement>) => (
      <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,
  }: {
    x: number;
    y: number;
    isPositive: boolean;
    color: string;
  }) => {
    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 DraggableReferenceLine = memo(
    ({
      baselineAmount,
      startAmount,
      chartRef,
    }: {
      baselineAmount: number;
      startAmount: number;
      chartRef: RefObject<SVGSVGElement>;
    }) => {
      const theme = useTheme();
      const { isPhone } = useBreakpoints();

      const formatPrice = useCallback((value: number) => {
        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();

      // 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: number, clientY: number) => {
          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: React.MouseEvent) => {
        e.preventDefault();
        setIsDragging(true);
      };

      const handleTouchStart = (e: React.TouchEvent) => {
        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
            dataY={amount}
            label={dollarLabel}
            labelPosition="right"
            labelProps={{
              background: color,
              borderRadius: 4,
              color: 'white',
              dx: -12,
              font: 'label1',
              horizontalAlignment: 'right',
              inset: { top: 5, bottom: 5, left: 10, right: 10 },
            }}
          />
          <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 PriceTargetChart = () => {
    const priceData = useMemo(() => sparklineInteractiveData.year.map((d) => d.value), []);
    const { isPhone } = useBreakpoints();

    const chartRef = useRef<SVGSVGElement>(null);

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

    return (
      <LineChart
        ref={chartRef}
        showArea
        animate={false}
        curve="monotone"
        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
            LineComponent={SolidLine}
            dataY={priceData[priceData.length - 1]}
            label={formatPrice(priceData[priceData.length - 1])}
            labelProps={{ dx: 8, horizontalAlignment: 'left' }}
          />
        )}
        <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` | `ComponentClass<ScrubberBeaconProps, any> \| FunctionComponent<ScrubberBeaconProps>` | No | `-` | Custom component for the scrubber beacon. |
| `BeaconLabelComponent` | `ComponentClass<ChartTextProps, any> \| FunctionComponent<ChartTextProps>` | No | `-` | Custom component for the scrubber beacon label. |
| `LineComponent` | `FunctionComponent<ReferenceLineProps> \| ComponentClass<ReferenceLineProps, any>` | No | `-` | Custom component for the scrubber line. |
| `classNames` | `{ overlay?: string; beacon?: string \| undefined; line?: string \| undefined; beaconLabel?: string \| undefined; } \| undefined` | No | `-` | Custom class names for scrubber elements. |
| `hideLine` | `boolean` | No | `-` | Hides the scrubber line |
| `hideOverlay` | `boolean` | No | `-` | Whether to hide the overlay rect which obscures future data. |
| `idlePulse` | `boolean` | No | `-` | Pulse the scrubber beacon while it is at rest. |
| `key` | `Key \| null` | No | `-` | - |
| `label` | `ChartTextChildren \| ((dataIndex: number) => ChartTextChildren)` | No | `-` | Label text displayed above the scrubber line. |
| `labelProps` | `ReferenceLineLabelProps` | No | `-` | Props passed to the scrubber lines 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: ScrubberBeaconRef \| null) => void) \| RefObject<ScrubberBeaconRef> \| null` | No | `-` | - |
| `seriesIds` | `string[]` | No | `-` | An array of series IDs that will receive visual emphasis as the user scrubs through the chart. Use this prop to restrict the scrubbing visual behavior to specific series. By default, all series will be highlighted by the Scrubber. |
| `styles` | `{ overlay?: CSSProperties; beacon?: CSSProperties \| undefined; line?: CSSProperties \| undefined; beaconLabel?: CSSProperties \| undefined; } \| undefined` | No | `-` | Custom styles for scrubber elements. |
| `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 |


