# 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
### 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`.
#### Simple Reference Line
A minimal reference line without labels, useful for marking key thresholds:
```jsx live
}
dataY={10}
stroke="var(--color-fg)"
/>
```
#### With Labels
You can add text labels to reference lines and position them using alignment and offset props:
```jsx live
```
### 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
```
### Labels
#### Customization
You can customize label appearance using `labelFont`, `labelDx`, `labelDy`, `labelHorizontalAlignment`, and `labelVerticalAlignment` props.
```jsx live
```
#### Bounds
Use `labelBoundsInset` to prevent labels from getting too close to chart edges.
```jsx live
```
#### Custom Component
You can adjust the style of the label using a custom `LabelComponent`.
```jsx live
function LabelStyleExample() {
const LiquidationLabel = useMemo(
() =>
memo((props) => (
)),
[],
);
const PriceLabel = useMemo(
() =>
memo((props) => (
)),
[],
);
return (
);
}
```
### 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) => (
);
return (
);
};
const TrendArrowIcon = ({
x,
y,
isPositive,
color,
}: {
x: number;
y: number;
isPositive: boolean;
color: string;
}) => {
return (
);
};
const DynamicPriceLabel = memo(
({ color, ...props }: React.ComponentProps & { color: string }) => (
),
);
const DraggableReferenceLine = memo(
({
baselineAmount,
startAmount,
chartRef,
}: {
baselineAmount: number;
startAmount: number;
chartRef: RefObject;
}) => {
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();
const labelComponent = useCallback(
(props: React.ComponentProps) => (
),
[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: 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 (
<>
setTextDimensions(dimensions)}
verticalAlignment="middle"
x={drawingArea.x + padding + dragIconSize + iconGap + trendArrowIconSize}
y={yPixel + 1}
>
{percentageLabel}
>
);
},
);
const BaselinePriceLabel = useMemo(() => memo((props) => (
)), []);
const PriceTargetChart = () => {
const priceData = useMemo(() => sparklineInteractiveData.year.map((d) => d.value), []);
const { isPhone } = useBreakpoints();
const chartRef = useRef(null);
const formatPrice = useCallback((value: number) => {
return `$${value.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
}, []);
return (
({ min: min * 0.7, max: max * 1.3 }) }}
>
{!isPhone && (
)}
);
};
return
}
```
## 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. |
| `beaconStroke` | `string` | No | `'var(--color-bg)'` | Stroke color of the scrubber beacon circle. |
| `beaconTransitions` | `{ update?: Transition$1; pulse?: Transition$1 \| undefined; pulseRepeatDelay?: number \| undefined; } \| undefined` | No | `-` | Transition configuration for the scrubber beacon. |
| `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 | `-` | 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 | `{ 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; 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 |