Skip to main content
ReferenceLine
@coinbase/cds-web-visualization@3.4.0-beta.1
A horizontal or vertical reference line to mark important values on a chart.
Import
import { ReferenceLine } from '@coinbase/cds-web-visualization'
SourceView source code

Basic Example

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

Loading...
Live Code
<LineChart
  height={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={4}
    label="Vertical Reference Line"
    labelProps={{ horizontalAlignment: 'left', dx: 8 }}
  />
  <ReferenceLine
    dataY={70}
    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.

Loading...
Live Code
<LineChart
  showArea
  curve="natural"
  height={400}
  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={100}
    label="100"
    labelPosition="left"
    labelProps={{ verticalAlignment: 'bottom', dy: -4 }}
  />
  <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 }}
  />
  <ReferenceLine
    dataY={1000000}
    label="1,000,000"
    labelPosition="left"
    labelProps={{ verticalAlignment: 'bottom', dy: -4 }}
  />
</LineChart>

Customization

Label Style

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

Loading...
Live Code
<LineChart
  curve="monotone"
  height={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.

Loading...
Live Code
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 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 handleMouseMove = (event: MouseEvent) => {
          if (!isDragging) {
            return;
          }

          const point = element.createSVGPoint();
          point.x = event.clientX;
          point.y = event.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 handleMouseUp = () => {
          setIsDragging(false);
        };

        const handleMouseLeave = () => {
          setIsDragging(false);
        };

        element.addEventListener('mousemove', handleMouseMove);
        element.addEventListener('mouseup', handleMouseUp);
        element.addEventListener('mouseleave', handleMouseLeave);

        return () => {
          element.removeEventListener('mousemove', handleMouseMove);
          element.removeEventListener('mouseup', handleMouseUp);
          element.removeEventListener('mouseleave', handleMouseLeave);
        };
      }, [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 = `${Math.abs(percentageChange)}% (${formatPrice(Math.abs(difference))})`;
      const dollarLabel = formatPrice(amount);

      const handleMouseDown = (e: React.MouseEvent) => {
        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}
            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 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={{ 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 }) }}
      >
        <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 />
}

Is this page useful?

Coinbase Design is an open-source, adaptable system of guidelines, components, and tools that aid the best practices of user interface design for crypto products.