# 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 ``` ### 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 ``` ### Customization #### Label Style You can adjust the style of the label using the `labelProps` prop. ```jsx live ``` #### 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 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(); // 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 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` | `ComponentClass \| FunctionComponent` | No | `-` | Custom component for the scrubber beacon. | | `BeaconLabelComponent` | `ComponentClass \| FunctionComponent` | No | `-` | Custom component for the scrubber beacon label. | | `LineComponent` | `FunctionComponent \| ComponentClass` | 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 \| 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 |