# 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 |