# CartesianChart
**📖 Live documentation:** https://cds.coinbase.com/components/charts/CartesianChart/
A flexible, low-level chart component for displaying data in an x/y coordinate space. Provides a foundation for building custom chart visualizations with full control over rendering.
## Import
```tsx
import { CartesianChart } from '@coinbase/cds-web-visualization'
```
## Examples
CartesianChart is a customizable, SVG based component that can be used to display a variety of data in a x/y coordinate space. The underlying logic is handled by D3.
### Basic Example
[AreaChart](/components/charts/AreaChart/), [BarChart](/components/charts/BarChart/), and [LineChart](/components/charts/LineChart/) are built on top of CartesianChart and have default functionality for your chart.
```jsx live
```
### Series
Series are the data that will be displayed on the chart. Each series must have a defined `id`.
#### Series Data
You can pass in an array of numbers or an array of tuples for the `data` prop. Passing in null values is equivalent to no data at that index.
```jsx live
function ForecastedPrice() {
const ForecastRect = memo(({ startIndex, endIndex }) => {
const { drawingArea, getXScale } = useCartesianChartContext();
const xScale = getXScale();
if (!xScale) return;
const startX = xScale(startIndex);
const endX = xScale(endIndex);
return (
);
});
return (
);
}
```
#### Series Axis IDs
Each series can have a different `yAxisId`, allowing you to compare data from different contexts.
```jsx live
`$${value}k`}
width={60}
/>
`$${value}k`}
/>
```
#### Series Stacks
You can provide a `stackId` to stack series together.
```jsx live
```
### Axes
You can configure your x and y axes with the `xAxis` and `yAxis` props. `xAxis` accepts an object or array, while `yAxis` accepts an object or array.
When `layout="horizontal"`, you can define multiple x-axes (for multiple value scales) but only one y-axis.
```jsx live
```
For more info, learn about [XAxis](/components/charts/XAxis/#axis-config) and [YAxis](/components/charts/YAxis/#axis-config) configuration.
### Inset
You can adjust the inset around the entire chart (outside the axes) with the `inset` prop. This is useful for when you want to have components that are outside of the drawing area of the data but still within the chart svg.
You can also remove the default inset, such as to have a compact line chart.
```jsx live
function Insets() {
const data = [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58];
const formatPrice = useCallback((dataIndex) => {
const price = data[dataIndex];
return `$${price.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
}, []);
return (
No inset
Custom inset
Default inset
);
}
```
### Scrubbing
CartesianChart has built-in scrubbing functionality that can be enabled with the `enableScrubbing` prop. This will then enable the usage of `onScrubberPositionChange` to get the current position of the scrubber as the user interacts with the chart.
```jsx live
function Scrubbing() {
const [scrubIndex, setScrubIndex] = useState(undefined);
const onScrubberPositionChange = useCallback((index) => {
setScrubIndex(index);
}, []);
return (
Scrubber index: {scrubIndex ?? 'none'}
);
}
```
### Animations
CartesianChart delegates transition control to its child components. You can also disable all animations chart-wide by passing `animate={false}` on CartesianChart.
#### Enter Only
Disable the update morph animation while keeping a slow enter reveal. Data changes snap instantly but the initial chart appearance animates. Useful when new data arrives frequently and morphing would be distracting.
```tsx live
function EnterAnimationOnly() {
const dataCount = 15;
const updateInterval = 2500;
function generateNextValue(prev: number) {
const step = Math.random() * 30 - 15;
return Math.max(0, Math.min(100, prev + step));
}
function generateInitialData() {
const data = [50];
for (let i = 1; i < dataCount; i++) {
data.push(generateNextValue(data[i - 1]));
}
return data;
}
function Chart() {
const [data, setData] = useState(generateInitialData);
useEffect(() => {
const intervalId = setInterval(() => {
setData((current) => {
const last = current[current.length - 1];
return [...current.slice(1), generateNextValue(last)];
});
}, updateInterval);
return () => clearInterval(intervalId);
}, []);
return (
);
}
return ;
}
```
#### Update Only
Disable the enter reveal animation while keeping a slow update morph. The chart appears instantly but data changes animate smoothly. Useful when the chart is embedded in content that should not animate on load.
```tsx live
function UpdateAnimationOnly() {
const dataCount = 15;
const updateInterval = 2500;
function generateNextValue(prev: number) {
const step = Math.random() * 30 - 15;
return Math.max(0, Math.min(100, prev + step));
}
function generateInitialData() {
const data = [50];
for (let i = 1; i < dataCount; i++) {
data.push(generateNextValue(data[i - 1]));
}
return data;
}
function Chart() {
const [data, setData] = useState(generateInitialData);
useEffect(() => {
const intervalId = setInterval(() => {
setData((current) => {
const last = current[current.length - 1];
return [...current.slice(1), generateNextValue(last)];
});
}, updateInterval);
return () => clearInterval(intervalId);
}, []);
return (
);
}
return ;
}
```
#### Mixed Transitions Per Child
Each child component can define its own transitions independently. Here, the `Line` uses a spring morph while the bars snap with no update animation. This lets you fine-tune each visual layer within a single chart.
```tsx live
function MixedTransitions() {
const dataCount = 10;
const updateInterval = 2000;
function generateNextValue(prev: number) {
const step = Math.random() * 20 - 10;
return Math.max(10, Math.min(100, prev + step));
}
function generateInitialData() {
const data = [50];
for (let i = 1; i < dataCount; i++) {
data.push(generateNextValue(data[i - 1]));
}
return data;
}
function Chart() {
const [data, setData] = useState(generateInitialData);
useEffect(() => {
const intervalId = setInterval(() => {
setData((current) => {
const last = current[current.length - 1];
return [...current.slice(1), generateNextValue(last)];
});
}, updateInterval);
return () => clearInterval(intervalId);
}, []);
return (
d * 0.3),
color: 'var(--color-accentBoldPurple)',
yAxisId: 'bars',
},
]}
xAxis={{ scaleType: 'band' }}
yAxis={[
{ id: 'default' },
{ id: 'bars', range: ({ min, max }) => ({ min: max - 48, max }) },
]}
aria-hidden="true"
>
);
}
return ;
}
```
#### No Animations
You can disable all animations chart-wide by setting `animate` to `false` on CartesianChart. This is useful for static snapshots or when performance is a concern. Compare this to the animated examples above — data still updates, but changes snap instantly without any transition.
```tsx live
function DisableAnimations() {
const dataCount = 15;
const updateInterval = 2500;
function generateNextValue(prev: number) {
const step = Math.random() * 30 - 15;
return Math.max(0, Math.min(100, prev + step));
}
function generateInitialData() {
const data = [50];
for (let i = 1; i < dataCount; i++) {
data.push(generateNextValue(data[i - 1]));
}
return data;
}
function Chart() {
const [data, setData] = useState(generateInitialData);
useEffect(() => {
const intervalId = setInterval(() => {
setData((current) => {
const last = current[current.length - 1];
return [...current.slice(1), generateNextValue(last)];
});
}, updateInterval);
return () => clearInterval(intervalId);
}, []);
return (
);
}
return ;
}
```
### Customization
#### Price with Volume
You can showcase the price and volume of an asset over time within one chart.
```jsx live
function PriceWithVolume() {
const [scrubIndex, setScrubIndex] = useState(null);
const btcData = btcCandles.slice(0, 180).reverse();
const btcPrices = btcData.map((candle) => parseFloat(candle.close));
const btcVolumes = btcData.map((candle) => parseFloat(candle.volume));
const btcDates = btcData.map((candle) => new Date(parseInt(candle.start) * 1000));
const formatPrice = useCallback((price) => {
return `$${price.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
}, []);
const formatPriceInThousands = useCallback((price) => {
return `$${(price / 1000).toLocaleString('en-US', {
minimumFractionDigits: 0,
maximumFractionDigits: 2,
})}k`;
}, []);
const formatVolume = useCallback((volume) => {
return `${(volume / 1000).toFixed(2)}K`;
}, []);
const formatDate = useCallback((date) => {
return date.toLocaleDateString('en-US', {
month: 'short',
day: 'numeric',
});
}, []);
const displayIndex = scrubIndex ?? btcPrices.length - 1;
const currentPrice = btcPrices[displayIndex];
const currentVolume = btcVolumes[displayIndex];
const currentDate = btcDates[displayIndex];
const priceChange =
displayIndex > 0
? (currentPrice - btcPrices[displayIndex - 1]) / btcPrices[displayIndex - 1]
: 0;
const accessibilityLabel = useMemo(() => {
if (scrubIndex === null)
return `Current Bitcoin price: ${formatPrice(currentPrice)}, Volume: ${formatVolume(currentVolume)}`;
return `Bitcoin price at ${formatDate(currentDate)}: ${formatPrice(currentPrice)}, Volume: ${formatVolume(currentVolume)}`;
}, [scrubIndex, currentPrice, currentVolume, currentDate, formatPrice, formatVolume, formatDate]);
const ThinSolidLine = memo((props) => );
const headerId = useId();
return (
);
}
```
#### Earnings History
You can also create your own type of cartesian chart by using `getSeriesData`, `getXScale`, and `getYScale` directly.
```jsx live
function EarningsHistory() {
const CirclePlot = memo(({ seriesId, opacity = 1 }) => {
const { drawingArea, getSeries, getSeriesData, getXScale, getYScale } =
useCartesianChartContext();
const series = getSeries(seriesId);
const data = getSeriesData(seriesId);
const xScale = getXScale();
const yScale = getYScale(series?.yAxisId);
if (!xScale || !yScale || !data || !isCategoricalScale(xScale)) return null;
const yScaleSize = Math.abs(yScale.range()[1] - yScale.range()[0]);
// Have circle diameter be the smaller of the x scale bandwidth or 10% of the y space available
const diameter = Math.min(xScale.bandwidth(), yScaleSize / 10);
return (
{data.map((value, index) => {
if (value === null || value === undefined) return null;
// Get x position from band scale - center of the band
const xPos = xScale(index);
if (xPos === undefined) return null;
const centerX = xPos + xScale.bandwidth() / 2;
// Get y position from value
const yValue = Array.isArray(value) ? value[1] : value;
const centerY = yScale(yValue);
if (centerY === undefined) return null;
return (
);
})}
);
});
const quarters = useMemo(() => ['Q1', 'Q2', 'Q3', 'Q4'], []);
const estimatedEPS = useMemo(() => [1.71, 1.82, 1.93, 2.34], []);
const actualEPS = useMemo(() => [1.68, 1.83, 2.01, 2.24], []);
const formatEarningAmount = useCallback((value) => {
return `$${value.toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
})}`;
}, []);
const surprisePercentage = useCallback(
(index) => {
const percentage = (actualEPS[index] - estimatedEPS[index]) / estimatedEPS[index];
const percentageString = percentage.toLocaleString('en-US', {
style: 'percent',
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
return (
0 ? 'var(--color-fgPositive)' : 'var(--color-fgNegative)',
fontWeight: 'bold',
}}
>
{percentage > 0 ? '+' : ''}
{percentageString}
);
},
[actualEPS, estimatedEPS],
);
const LegendEntry = memo(({ opacity = 1, label }) => {
return (
{label}
);
});
const LegendDot = memo((props) => {
return ;
});
return (
quarters[index]} />
);
}
```
#### Trading Trends
You can have multiple axes with different domains and ranges to showcase different pieces of data over the time time period.
```jsx live
function TradingTrends() {
const profitData = [34, 24, 28, -4, 8, -16, -3, 12, 24, 18, 20, 28];
const gains = profitData.map((value) => (value > 0 ? value : 0));
const losses = profitData.map((value) => (value < 0 ? value : 0));
const renderProfit = useCallback((value) => {
return `$${value}M`;
}, []);
const ThinSolidLine = memo((props) => (
));
const ThickSolidLine = memo((props) => (
));
return (
({ min: min, max: max - 64 }),
domain: { min: -40, max: 40 },
},
{ id: 'revenue', range: ({ min, max }) => ({ min: max - 64, max }), domain: { min: 100 } },
]}
>
);
}
```
## Props
| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `alignContent` | `ResponsiveProp` | No | `-` | - |
| `alignItems` | `ResponsiveProp` | No | `-` | - |
| `alignSelf` | `ResponsiveProp` | No | `-` | - |
| `animate` | `boolean` | No | `true` | Whether to animate the chart. |
| `as` | `div` | No | `-` | The underlying element or component the polymorphic component will render. Changing as also changes the inherited native props (e.g. href for as=a) and the expected ref type. |
| `aspectRatio` | `ResponsiveProp` | No | `-` | - |
| `background` | `ResponsiveProp` | No | `-` | - |
| `borderBottomLeftRadius` | `ResponsiveProp` | No | `-` | - |
| `borderBottomRightRadius` | `ResponsiveProp` | No | `-` | - |
| `borderBottomWidth` | `ResponsiveProp` | No | `-` | - |
| `borderColor` | `ResponsiveProp` | No | `-` | - |
| `borderEndWidth` | `ResponsiveProp` | No | `-` | - |
| `borderRadius` | `ResponsiveProp` | No | `-` | - |
| `borderStartWidth` | `ResponsiveProp` | No | `-` | - |
| `borderTopLeftRadius` | `ResponsiveProp` | No | `-` | - |
| `borderTopRightRadius` | `ResponsiveProp` | No | `-` | - |
| `borderTopWidth` | `ResponsiveProp` | No | `-` | - |
| `borderWidth` | `ResponsiveProp` | No | `-` | - |
| `bordered` | `boolean` | No | `-` | Add a border around all sides of the box. |
| `borderedBottom` | `boolean` | No | `-` | Add a border to the bottom side of the box. |
| `borderedEnd` | `boolean` | No | `-` | Add a border to the trailing side of the box. |
| `borderedHorizontal` | `boolean` | No | `-` | Add a border to the leading and trailing sides of the box. |
| `borderedStart` | `boolean` | No | `-` | Add a border to the leading side of the box. |
| `borderedTop` | `boolean` | No | `-` | Add a border to the top side of the box. |
| `borderedVertical` | `boolean` | No | `-` | Add a border to the top and bottom sides of the box. |
| `bottom` | `ResponsiveProp>` | No | `-` | - |
| `classNames` | `{ root?: string; chart?: string \| undefined; } \| undefined` | No | `-` | Custom class names for the component. |
| `color` | `ResponsiveProp` | No | `-` | - |
| `columnGap` | `ResponsiveProp` | No | `-` | - |
| `dangerouslySetBackground` | `string` | No | `-` | - |
| `display` | `ResponsiveProp` | No | `-` | - |
| `elevation` | `ResponsiveProp` | No | `-` | - |
| `enableScrubbing` | `boolean` | No | `-` | Enables scrubbing interactions. When true, allows scrubbing and makes scrubber components interactive. |
| `flexBasis` | `ResponsiveProp>` | No | `-` | - |
| `flexDirection` | `ResponsiveProp` | No | `-` | - |
| `flexGrow` | `ResponsiveProp` | No | `-` | - |
| `flexShrink` | `ResponsiveProp` | No | `-` | - |
| `flexWrap` | `ResponsiveProp` | No | `-` | - |
| `font` | `ResponsiveProp` | No | `-` | - |
| `fontFamily` | `ResponsiveProp` | No | `-` | - |
| `fontSize` | `ResponsiveProp` | No | `-` | - |
| `fontWeight` | `ResponsiveProp` | No | `-` | - |
| `gap` | `ResponsiveProp` | No | `-` | - |
| `grid` | `ResponsiveProp` | No | `-` | - |
| `gridArea` | `ResponsiveProp` | No | `-` | - |
| `gridAutoColumns` | `ResponsiveProp>` | No | `-` | - |
| `gridAutoFlow` | `ResponsiveProp` | No | `-` | - |
| `gridAutoRows` | `ResponsiveProp>` | No | `-` | - |
| `gridColumn` | `ResponsiveProp` | No | `-` | - |
| `gridColumnEnd` | `ResponsiveProp` | No | `-` | - |
| `gridColumnStart` | `ResponsiveProp` | No | `-` | - |
| `gridRow` | `ResponsiveProp` | No | `-` | - |
| `gridRowEnd` | `ResponsiveProp` | No | `-` | - |
| `gridRowStart` | `ResponsiveProp` | No | `-` | - |
| `gridTemplate` | `ResponsiveProp` | No | `-` | - |
| `gridTemplateAreas` | `ResponsiveProp` | No | `-` | - |
| `gridTemplateColumns` | `ResponsiveProp>` | No | `-` | - |
| `gridTemplateRows` | `ResponsiveProp>` | No | `-` | - |
| `height` | `ResponsiveProp>` | No | `-` | - |
| `inset` | `number \| Partial` | No | `-` | Inset around the entire chart (outside the axes). |
| `justifyContent` | `ResponsiveProp` | No | `-` | - |
| `key` | `Key \| null` | No | `-` | - |
| `layout` | `horizontal \| vertical` | No | `'vertical'` | Chart layout - describes the direction bars/areas grow. - vertical (default): Bars grow vertically. X is category axis, Y is value axis. - horizontal: Bars grow horizontally. Y is category axis, X is value axis. |
| `left` | `ResponsiveProp>` | No | `-` | - |
| `legend` | `ReactNode` | No | `-` | Whether to show the legend or a custom legend element. - true renders the default Legend component - A React element renders that element as the legend - false or omitted hides the legend |
| `legendAccessibilityLabel` | `string` | No | `'Legend'` | Accessibility label for the legend group. |
| `legendPosition` | `top \| bottom \| left \| right` | No | `'bottom'` | Position of the legend relative to the chart. |
| `lineHeight` | `ResponsiveProp` | No | `-` | - |
| `margin` | `ResponsiveProp<0 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10>` | No | `-` | - |
| `marginBottom` | `ResponsiveProp<0 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10>` | No | `-` | - |
| `marginEnd` | `ResponsiveProp<0 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10>` | No | `-` | - |
| `marginStart` | `ResponsiveProp<0 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10>` | No | `-` | - |
| `marginTop` | `ResponsiveProp<0 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10>` | No | `-` | - |
| `marginX` | `ResponsiveProp<0 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10>` | No | `-` | - |
| `marginY` | `ResponsiveProp<0 \| -1 \| -2 \| -0.25 \| -0.5 \| -0.75 \| -1.5 \| -3 \| -4 \| -5 \| -6 \| -7 \| -8 \| -9 \| -10>` | No | `-` | - |
| `maxHeight` | `ResponsiveProp>` | No | `-` | - |
| `maxWidth` | `ResponsiveProp>` | No | `-` | - |
| `minHeight` | `ResponsiveProp>` | No | `-` | - |
| `minWidth` | `ResponsiveProp>` | No | `-` | - |
| `onChange` | `FormEventHandler` | No | `-` | - |
| `onScrubberPositionChange` | `((index: number) => void) \| undefined` | No | `-` | Callback fired when the scrubber position changes. Receives the dataIndex of the scrubber or undefined when not scrubbing. |
| `opacity` | `ResponsiveProp` | No | `-` | - |
| `overflow` | `ResponsiveProp` | No | `-` | - |
| `padding` | `ResponsiveProp` | No | `-` | - |
| `paddingBottom` | `ResponsiveProp` | No | `-` | - |
| `paddingEnd` | `ResponsiveProp` | No | `-` | - |
| `paddingStart` | `ResponsiveProp` | No | `-` | - |
| `paddingTop` | `ResponsiveProp` | No | `-` | - |
| `paddingX` | `ResponsiveProp` | No | `-` | - |
| `paddingY` | `ResponsiveProp` | No | `-` | - |
| `pin` | `PinningDirection` | No | `-` | Direction in which to absolutely pin the box. |
| `position` | `ResponsiveProp` | No | `-` | - |
| `ref` | `((instance: SVGSVGElement \| null) => void) \| RefObject \| null` | No | `-` | - |
| `right` | `ResponsiveProp>` | No | `-` | - |
| `rowGap` | `ResponsiveProp` | No | `-` | - |
| `series` | `Series[]` | No | `-` | Configuration objects that define how to visualize the data. Each series contains its own data array. |
| `style` | `CSSProperties` | No | `-` | Custom styles for the root element. |
| `styles` | `{ root?: CSSProperties; chart?: CSSProperties \| undefined; } \| undefined` | No | `-` | Custom styles for the 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 |
| `textAlign` | `ResponsiveProp` | No | `-` | - |
| `textDecoration` | `ResponsiveProp` | No | `-` | - |
| `textTransform` | `ResponsiveProp` | No | `-` | - |
| `top` | `ResponsiveProp>` | No | `-` | - |
| `transform` | `ResponsiveProp` | No | `-` | - |
| `userSelect` | `ResponsiveProp` | No | `-` | - |
| `visibility` | `ResponsiveProp` | No | `-` | - |
| `width` | `ResponsiveProp>` | No | `-` | - |
| `xAxis` | `Partial \| Partial[]` | No | `-` | Configuration for x-axis(es). Can be a single config or array of configs. |
| `yAxis` | `Partial> \| Partial>[]` | No | `-` | Configuration for y-axis(es). Can be a single config or array of configs. |
| `zIndex` | `ResponsiveProp` | No | `-` | - |