Coordinate Systems in Recharts

When working with Recharts, you'll encounter three different coordinate systems. Understanding when to use each one is essential for creating custom annotations, shapes, and interactive features.

Different Rechart components accept, or provide, coordinate from different systems.

Overview

Coordinate SystemDescriptionExample components
Domain CoordinatesValues in your data domain (e.g., x="March", y=5000) can be provided directly to some components and are converted to pixel positions automatically.ReferenceDot, ReferenceLine, ReferenceArea
Pixel/Chart-Range CoordinatesPixel positions relative to the chart's viewBox (e.g., x=100, y=50). The top-left corner of the chart area is (0, 0).Dot, Rectangle, Cross, custom SVG shapes
Browser eventsMouse and touch events have several of their own coordinate systems: viewport, page, client, screen. Recharts provides helper methods to convert to chart-based coordinates.Mouse and touch handlers

1. Domain Coordinates

Domain coordinates are the most intuitive—they use values from your actual data. When you specify x="March" on a ReferenceLine, Recharts automatically converts that to the correct pixel position based on your XAxis and YAxis scale.

You can also optionally pass position prop. This is important for Bar charts, where the axis scale typically has bandwidth: meaning that a single data point spans multiple pixels. The position prop controls where within that bandwidth the element is placed (e.g., start, middle, end).

Other charts, like Line charts, have no bandwidth (each data point maps to a single pixel). In those cases, the position prop is ignored.

Components that use domain coordinates:

Advantages:

  • Automatically adjusts when chart size changes
  • Responds to axis domain changes (zoom, brush filtering)
  • Works with both categorical and numerical axes
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ReferenceLine, ReferenceDot, Label } from 'recharts';
import { RechartsDevtools } from '@recharts/devtools';

const data = [
  { name: 'Jan', sales: 4000, target: 3500 },
  { name: 'Feb', sales: 3000, target: 3500 },
  { name: 'Mar', sales: 5000, target: 3500 },
  { name: 'Apr', sales: 4500, target: 3500 },
  { name: 'May', sales: 6000, target: 3500 },
  { name: 'Jun', sales: 5500, target: 3500 },
];

/**
 * This example demonstrates data-based coordinates.
 * The ReferenceLine at y={3500} uses a data value, not pixels.
 * The ReferenceDot at x="Mar" and y={5000} also uses data values.
 * Recharts automatically converts these to the correct pixel positions.
 */
export default function DataCoordinatesExample() {
  return (
    <LineChart width={500} height={300} data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
      <CartesianGrid />
      <XAxis dataKey="name" />
      <YAxis domain={[0, 7000]} />
      <Tooltip />
      <Line type="monotone" dataKey="sales" strokeWidth={2} />

      {/* Data-based horizontal line at y=3500 (the target) */}
      <ReferenceLine y={3500} stroke="red" strokeDasharray="5 5">
        <Label value="Target: 3500" position="insideTopRight" fill="red" />
      </ReferenceLine>

      {/* Data-based dot at the peak */}
      <ReferenceDot x="May" y={6000} r={8} fill="green" stroke="none">
        <Label value="Peak" position="top" fill="green" />
      </ReferenceDot>

      <RechartsDevtools />
    </LineChart>
  );
}

2. Pixel/Chart-Range Coordinates

Pixel coordinates are positions measured in pixels from the top-left corner of the chart. Use the useOffset hook to get the plot area dimensions, then position elements relative to that area.

Key hooks:

When to use:

  • Annotations that should stay at fixed positions regardless of data
  • Decorative elements like watermarks or logos
  • Custom legend, tooltip, or label positioning
import { LineChart, Line, XAxis, YAxis, CartesianGrid, usePlotArea } from 'recharts';

const data = [
  { name: 'Jan', sales: 4000 },
  { name: 'Feb', sales: 3000 },
  { name: 'Mar', sales: 5000 },
  { name: 'Apr', sales: 4500 },
  { name: 'May', sales: 6000 },
  { name: 'Jun', sales: 5500 },
];

/**
 * Custom component that renders annotations using pixel coordinates
 * relative to the chart's plot area (offset-based).
 */
function PixelAnnotations() {
  const plotArea = usePlotArea();

  if (!plotArea) return null;

  // Draw a rectangle at specific pixel coordinates within the plot area
  const rectX = plotArea.x + 50;
  const rectY = plotArea.y + 30;

  // Draw a circle in the center of the plot area
  const centerX = plotArea.x + plotArea.width / 2;
  const centerY = plotArea.y + plotArea.height / 2;

  return (
    <g>
      {/* Rectangle at fixed pixel position */}
      <rect x={rectX} y={rectY} width={80} height={40} fill="rgba(255, 165, 0, 0.3)" stroke="orange" strokeWidth={2} />
      <text x={rectX + 40} y={rectY + 25} textAnchor="middle" fill="orange" fontSize={12}>
        50px, 30px
      </text>

      {/* Circle at center of plot area */}
      <circle cx={centerX} cy={centerY} r={20} fill="rgba(0, 128, 255, 0.3)" stroke="blue" strokeWidth={2} />
      <text x={centerX} y={centerY + 40} textAnchor="middle" fill="blue" fontSize={12}>
        Center
      </text>
    </g>
  );
}

/**
 * This example demonstrates pixel/chart-range based coordinates.
 * The annotations use usePlotArea() to get the chart's plot area dimensions
 * and position elements at specific pixel offsets within that area.
 */
export default function PixelCoordinatesExample() {
  return (
    <LineChart width={500} height={300} data={data} margin={{ top: 20, right: 30, left: 20, bottom: 5 }}>
      <CartesianGrid />
      <XAxis dataKey="name" />
      <YAxis />
      <Line type="monotone" dataKey="sales" strokeWidth={2} />
      <PixelAnnotations />
    </LineChart>
  );
}

3. Mouse Event Coordinates

Mouse events from the browser provide coordinates in the browser's viewport. The getRelativeCoordinate function converts these to chart-relative coordinates, accounting for:

  • Chart position on the page
  • Scroll offset
  • CSS transforms (scale, rotate)
  • Browser zoom in or zoom out

Key function:

This function works with both HTML elements (like <div>) and SVG elements. The returned coordinates are relative to the top-left corner of the element that received the event.

import {
  RelativePointer,
  MouseHandlerDataParam,
  getRelativeCoordinate,
  Area,
  AreaChart,
  XAxis,
  YAxis,
  Cross,
  useChartWidth,
  useChartHeight,
  Legend,
  Text,
  TextProps,
  ZIndexLayer,
  DefaultZIndexes,
} from 'recharts';
import { generateMockData, RechartsDevtools } from '@recharts/devtools';
import { useState, MouseEvent, TouchEvent, useCallback } from 'react';

const data = generateMockData(30, 123);

const TextWithOutline = (textProps: TextProps) => (
  <Text stroke="white" strokeWidth={3} fill="black" paintOrder="stroke" {...textProps} />
);

const PixelCrosshair = ({ pointer }: { pointer: RelativePointer | null }) => {
  const width = useChartWidth();
  const height = useChartHeight();
  if (pointer == null || width == null || height == null) {
    return null;
  }
  return (
    <ZIndexLayer zIndex={DefaultZIndexes.cursorLine}>
      <TextWithOutline
        x={pointer.relativeX + 5}
        y={0}
        verticalAnchor="start"
      >{`x: ${pointer.relativeX}`}</TextWithOutline>
      <TextWithOutline
        y={pointer.relativeY + 5}
        x={width}
        verticalAnchor="start"
        textAnchor="end"
      >{`y: ${pointer.relativeY}`}</TextWithOutline>
      <Cross
        /*
         * pointerEvents: none is necessary because without it, browser will detect hovering over the Cross itself
         * which makes it trigger mouseLeave on the chart which makes it erase the Cross which looks like it's blinking.
         * If we skip pointer events on the cross then we can skip the mouseLeave and the movement is smooth.
         */
        style={{ pointerEvents: 'none' }}
        x={pointer.relativeX}
        y={pointer.relativeY}
        top={0}
        left={0}
        width={width}
        height={height}
        stroke="green"
        strokeWidth={1}
        strokeDasharray="4"
      />
    </ZIndexLayer>
  );
};

export default function CrosshairExample({
  initialPointers = [],
}: {
  initialPointers?: ReadonlyArray<RelativePointer>;
}) {
  const [pointers, setPointers] = useState<ReadonlyArray<RelativePointer>>(initialPointers);

  const handleMouseMove = useCallback(
    (_data: MouseHandlerDataParam, event: MouseEvent<SVGGraphicsElement>) => {
      /*
       * Here you have three coordinates available to your use:
       * 1. MouseHandlerDataParam.activeCoordinate
       *    - this is the coordinate where Recharts decided to display the Tooltip
       *    - may or may not be close to the mouse position
       *    - relative to chart position
       * 2. RelativePointer resolved by getRelativeCoordinate
       *    - mouse pointer position
       *    - relative to the element position
       *    - because we have registered this event on the chart, the coordinates here are also relative to the chart
       * 3. event.clientX (and pageX and screenX)
       *    - these are the standard browser event coordinates
       *    - some absolute some relative, depending on scroll and viewport, see the docs for details
       *    - https://developer.mozilla.org/en-US/docs/Web/API/MouseEvent
       *
       * This particular example demonstrates the use of (2.)
       */
      const chartPointer: RelativePointer = getRelativeCoordinate(event);
      setPointers([chartPointer]);
    },
    [setPointers],
  );

  const handleTouchMove = useCallback(
    (_data: unknown, event: TouchEvent<SVGGraphicsElement>) => {
      const chartPointers: RelativePointer[] = getRelativeCoordinate(event);
      setPointers(chartPointers);
    },
    [setPointers],
  );

  const handleLeave = useCallback(() => {
    setPointers([]);
  }, [setPointers]);

  return (
    <AreaChart
      style={{
        width: '100%',
        maxWidth: '500px',
        maxHeight: '200px',
        aspectRatio: 1,
        touchAction: 'none',
      }}
      responsive
      data={data}
      onMouseMove={handleMouseMove}
      onMouseLeave={handleLeave}
      onTouchMove={handleTouchMove}
      onTouchEnd={handleLeave}
    >
      <XAxis dataKey="label" />
      <YAxis width="auto" />
      <Area type="monotone" dataKey="x" />
      {pointers.map((pointer, index) => (
        <PixelCrosshair key={index} pointer={pointer} />
      ))}
      <Legend />
      <RechartsDevtools />
    </AreaChart>
  );
}

Converting Between Coordinate Systems

Often you need to convert between coordinate systems. For example, you might want to position a custom marker at a specific data point, or determine which data point the user clicked on.

Data → Pixels:

  • useXAxisScale - returns a function to convert X data values to pixel positions
  • useYAxisScale - returns a function to convert Y data values to pixel positions
  • useCartesianScale - convenience hook for converting both at once

Pixels → Data:

Accessing Ticks:

// #region imports and mock data
import {
  CartesianGrid,
  DefaultZIndexes,
  getRelativeCoordinate,
  InverseScaleFunction,
  Line,
  LineChart,
  MouseHandlerDataParam,
  ReferenceLine,
  RelativePointer,
  useXAxisInverseDataSnapScale,
  useYAxisInverseDataSnapScale,
  XAxis,
  YAxis,
  ZIndexLayer,
} from 'recharts';
import { MouseEvent, TouchEvent, useCallback, useState } from 'react';
import { RechartsDevtools } from '@recharts/devtools';

const data = [
  { name: 'Jan', sales: 400 },
  { name: 'Feb', sales: 3000 },
  { name: 'Mar', sales: 50000 },
  { name: 'Apr', sales: 24500 },
  { name: 'May', sales: 6000 },
  { name: 'Jun', sales: 45500 },
];
// #endregion

/**
 * This Crosshair component shows how to convert pixel coordinates to data values.
 * In effect it still follows the mouse pointer, but instead of showing pixel coordinates,
 * it shows the closest data values on both axes.
 *
 * It "snaps" to the closest data points using useXAxisInverseDataSnapScale and useYAxisInverseDataSnapScale hooks.
 *
 */
const DomainCrosshair = ({ pointer }: { pointer: RelativePointer | null }) => {
  // Convert pixel coordinates to the closest data values
  const xAxisInverseScale: InverseScaleFunction | undefined = useXAxisInverseDataSnapScale();
  const yAxisInverseScale: InverseScaleFunction | undefined = useYAxisInverseDataSnapScale();

  if (pointer == null || xAxisInverseScale == null || yAxisInverseScale == null) {
    return null;
  }
  const xDataLabel = String(xAxisInverseScale(pointer.relativeX));
  const yDataLabel = String(yAxisInverseScale(pointer.relativeY));
  return (
    <ZIndexLayer zIndex={DefaultZIndexes.cursorLine}>
      <ReferenceLine
        x={xDataLabel}
        /*
         * pointerEvents: none is necessary because without it, browser will detect hovering over the Cross itself
         * which makes it trigger mouseLeave on the chart which makes it erase the Cross which looks like it's blinking.
         * If we skip pointer events on the cross then we can skip the mouseLeave and the movement is smooth.
         */
        style={{ pointerEvents: 'none' }}
        stroke="green"
        strokeWidth={1}
        strokeDasharray="4"
        label={{
          value: xDataLabel,
          position: 'top',
          offset: 5,
        }}
      />
      <ReferenceLine
        y={yDataLabel}
        /*
         * pointerEvents: none is necessary because without it, browser will detect hovering over the Cross itself
         * which makes it trigger mouseLeave on the chart which makes it erase the Cross which looks like it's blinking.
         * If we skip pointer events on the cross then we can skip the mouseLeave and the movement is smooth.
         */
        style={{ pointerEvents: 'none' }}
        stroke="green"
        strokeWidth={1}
        strokeDasharray="4"
        label={{
          value: yDataLabel,
          position: 'right',
          offset: 5,
        }}
      />
    </ZIndexLayer>
  );
};

/**
 * This example demonstrates how to use getRelativeCoordinate to get pointer position in pixels,
 * and then convert those pixel coordinates to the closest data values using useXAxisInverseDataSnapScale and useYAxisInverseDataSnapScale hooks.
 */
export default function DataSnapExample({
  initialPointers = [],
}: {
  initialPointers?: ReadonlyArray<RelativePointer>;
}) {
  const [pointers, setPointers] = useState<ReadonlyArray<RelativePointer>>(initialPointers);

  const handleMouseMove = useCallback(
    (_data: MouseHandlerDataParam, event: MouseEvent<SVGGraphicsElement>) => {
      const chartPointer: RelativePointer = getRelativeCoordinate(event);
      setPointers([chartPointer]);
    },
    [setPointers],
  );

  const handleTouchMove = useCallback(
    (_data: unknown, event: TouchEvent<SVGGraphicsElement>) => {
      const chartPointers = getRelativeCoordinate(event);
      setPointers(chartPointers);
    },
    [setPointers],
  );

  const handleLeave = useCallback(() => {
    setPointers([]);
  }, [setPointers]);

  return (
    <LineChart
      style={{
        width: '100%',
        maxWidth: '500px',
        maxHeight: '200px',
        aspectRatio: 1,
        touchAction: 'none',
      }}
      responsive
      data={data}
      margin={{ top: 20, right: 50, left: 20, bottom: 30 }}
      onMouseMove={handleMouseMove}
      onMouseLeave={handleLeave}
      onTouchMove={handleTouchMove}
      onTouchEnd={handleLeave}
    >
      <CartesianGrid />
      <XAxis dataKey="name" />
      <YAxis width="auto" />
      <Line type="monotone" dataKey="sales" strokeWidth={2} />

      {pointers.map(pointer => (
        <DomainCrosshair key={`crosshair-${pointer.relativeX}-${pointer.relativeY}`} pointer={pointer} />
      ))}

      <RechartsDevtools />
    </LineChart>
  );
}
// #region imports and mock data
import {
  CartesianGrid,
  DefaultZIndexes,
  getRelativeCoordinate,
  InverseScaleFunction,
  Line,
  LineChart,
  MouseHandlerDataParam,
  ReferenceLine,
  RelativePointer,
  useXAxisInverseTickSnapScale,
  useYAxisInverseTickSnapScale,
  XAxis,
  YAxis,
  ZIndexLayer,
} from 'recharts';
import { MouseEvent, TouchEvent, useCallback, useState } from 'react';
import { RechartsDevtools } from '@recharts/devtools';

const data = [
  { name: 'Jan', sales: 400 },
  { name: 'Feb', sales: 3000 },
  { name: 'Mar', sales: 50000 },
  { name: 'Apr', sales: 24500 },
  { name: 'May', sales: 6000 },
  { name: 'Jun', sales: 45500 },
];
// #endregion

/**
 * This Crosshair component shows how to convert pixel coordinates to data values.
 * In effect it still follows the mouse pointer, but instead of showing pixel coordinates,
 * it shows the closest data values on both axes.
 *
 * It "snaps" to the closest data points using useXAxisInverseTickSnapScale and useYAxisInverseTickSnapScale hooks.
 *
 */
const AxisTickCrosshair = ({ pointer }: { pointer: RelativePointer | null }) => {
  // Convert pixel coordinates to the closest data values
  const xAxisInverseScale: InverseScaleFunction | undefined = useXAxisInverseTickSnapScale();
  const yAxisInverseScale: InverseScaleFunction | undefined = useYAxisInverseTickSnapScale();

  if (pointer == null || xAxisInverseScale == null || yAxisInverseScale == null) {
    return null;
  }
  const xDataLabel = String(xAxisInverseScale(pointer.relativeX));
  const yDataLabel = String(yAxisInverseScale(pointer.relativeY));
  return (
    <ZIndexLayer zIndex={DefaultZIndexes.cursorLine}>
      <ReferenceLine
        x={xDataLabel}
        /*
         * pointerEvents: none is necessary because without it, browser will detect hovering over the Cross itself
         * which makes it trigger mouseLeave on the chart which makes it erase the Cross which looks like it's blinking.
         * If we skip pointer events on the cross then we can skip the mouseLeave and the movement is smooth.
         */
        style={{ pointerEvents: 'none' }}
        stroke="green"
        strokeWidth={1}
        strokeDasharray="4"
        label={{
          value: xDataLabel,
          position: 'top',
          offset: 5,
        }}
      />
      <ReferenceLine
        y={yDataLabel}
        /*
         * pointerEvents: none is necessary because without it, browser will detect hovering over the Cross itself
         * which makes it trigger mouseLeave on the chart which makes it erase the Cross which looks like it's blinking.
         * If we skip pointer events on the cross then we can skip the mouseLeave and the movement is smooth.
         */
        style={{ pointerEvents: 'none' }}
        stroke="green"
        strokeWidth={1}
        strokeDasharray="4"
        label={{
          value: yDataLabel,
          position: 'right',
          offset: 5,
        }}
      />
    </ZIndexLayer>
  );
};

/**
 * This example demonstrates how to use getRelativeCoordinate to get pointer position in pixels,
 * and then convert those pixel coordinates to the closest axis ticks using useXAxisInverseTickSnapScale and useYAxisInverseTickSnapScale hooks.
 */
export default function AxisTickSnapExample({
  initialPointers = [],
}: {
  initialPointers?: ReadonlyArray<RelativePointer>;
}) {
  const [pointers, setPointers] = useState<ReadonlyArray<RelativePointer>>(initialPointers);

  const handleMouseMove = useCallback(
    (_data: MouseHandlerDataParam, event: MouseEvent<SVGGraphicsElement>) => {
      const chartPointer: RelativePointer = getRelativeCoordinate(event);
      setPointers([chartPointer]);
    },
    [setPointers],
  );

  const handleTouchMove = useCallback(
    (_data: unknown, event: TouchEvent<SVGGraphicsElement>) => {
      const chartPointers = getRelativeCoordinate(event);
      setPointers(chartPointers);
    },
    [setPointers],
  );

  const handleLeave = useCallback(() => {
    setPointers([]);
  }, [setPointers]);

  return (
    <LineChart
      style={{
        width: '100%',
        maxWidth: '500px',
        maxHeight: '200px',
        aspectRatio: 1,
        touchAction: 'none',
      }}
      responsive
      data={data}
      margin={{ top: 20, right: 50, left: 20, bottom: 30 }}
      onMouseMove={handleMouseMove}
      onMouseLeave={handleLeave}
      onTouchMove={handleTouchMove}
      onTouchEnd={handleLeave}
    >
      <CartesianGrid />
      <XAxis dataKey="name" />
      <YAxis width="auto" />
      <Line type="monotone" dataKey="sales" strokeWidth={2} />

      {pointers.map(pointer => (
        <AxisTickCrosshair key={`crosshair-${pointer.relativeX}-${pointer.relativeY}`} pointer={pointer} />
      ))}

      <RechartsDevtools />
    </LineChart>
  );
}

Choosing the Right Coordinate System

I want to...Use
Mark a specific data valueData coordinates (ReferenceArea,ReferenceLine, ReferenceDot)
Show a target line that moves with zoom/brushData coordinates (ReferenceLine)
Add a watermark at a fixed positionPixel coordinates (useOffset + custom SVG)
Create a crosshair that follows the mouseMouse coordinates (getRelativeCoordinate)
Click to add an annotation at a data pointMouse → Data conversion (getRelativeCoordinate + useXAxisInverseScale)
Draw custom shapes at data positionsData → Pixel conversion (useXAxisScale + useYAxisScale)