# 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-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`. ```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 Components You can adjust the style of the label using a custom `LabelComponent`. ```jsx live function CustomLabelExample() { const PriceLabel = memo((props) => ( )); 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 ( d.value), color: seriesColor, }, ]} xAxis={{ range: ({ min, max }) => ({ min, max: max - 24 }), }} > ( )} dataY={startPrice} label={formattedStartPrice} stroke="var(--color-fgMuted)" labelDx={-12} labelHorizontalAlignment="right" /> ); } return ; } ``` 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 ( ); }); 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 ( d.value), color: seriesColor, }, ]} xAxis={{ range: ({ min, max }) => ({ min, max: max - 24 }), }} > ( )} dataY={startPrice} label={formattedStartPrice} stroke="var(--color-fgMuted)" labelDx={-12} labelHorizontalAlignment="right" /> ); } return ; } ``` ### 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) => ; return ( ); }; const TrendArrowIcon = ({ x, y, isPositive, color }) => { return ( ); }; const DynamicPriceLabel = memo(({ color, ...props }) => ( )); 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) => , [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 ( <> 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) => { 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. | | `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` | 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; 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. |