import { LineChart } from '@coinbase/cds-web-visualization'
- framer-motion: ^10.18.0
LineChart is a wrapper for CartesianChart that makes it easy to create standard line charts, supporting a single x/y axis pair. Charts are built using SVGs.
Basics
The only prop required is series, which takes an array of series objects. Each series object needs an id and a data array of numbers.
<LineChart showArea height={{ base: 200, tablet: 225, desktop: 250 }} series={[ { id: 'prices', data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58], }, ]} />
LineChart also supports multiple lines, interaction, and axes.
Other props, such as areaType can be applied to the chart as a whole or per series.
function MultipleLine() { const pages = useMemo( () => ['Page A', 'Page B', 'Page C', 'Page D', 'Page E', 'Page F', 'Page G'], [], ); const pageViews = useMemo(() => [2400, 1398, 9800, 3908, 4800, 3800, 4300], []); const uniqueVisitors = useMemo(() => [4000, 3000, 2000, 2780, 1890, 2390, 3490], []); const chartAccessibilityLabel = `Website visitors across ${pageViews.length} pages.`; const scrubberAccessibilityLabel = useCallback( (index: number) => { return `${pages[index]} has ${pageViews[index]} views and ${uniqueVisitors[index]} unique visitors.`; }, [pages, pageViews, uniqueVisitors], ); const numberFormatter = useCallback( (value: number) => new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(value), [], ); return ( <LineChart enableScrubbing showArea showXAxis showYAxis accessibilityLabel={chartAccessibilityLabel} height={{ base: 200, tablet: 225, desktop: 250 }} series={[ { id: 'pageViews', data: pageViews, color: 'var(--color-accentBoldGreen)', // Label will render next to scrubber beacon label: 'Page Views', }, { id: 'uniqueVisitors', data: uniqueVisitors, color: 'var(--color-accentBoldPurple)', label: 'Unique Visitors', // Default area is gradient areaType: 'dotted', }, ]} xAxis={{ // Used on the x-axis to provide context for each index from the series data array data: pages, }} yAxis={{ showGrid: true, tickLabelFormatter: numberFormatter, }} > <Scrubber accessibilityLabel={scrubberAccessibilityLabel} /> </LineChart> ); }
Data
The data array for each series defines the y values for that series. You can adjust the y values for a series of data by setting the data prop on the xAxis.
function DataFormat() { const yData = useMemo(() => [2, 5.5, 2, 8.5, 1.5, 5], []); const xData = useMemo(() => [1, 2, 3, 5, 8, 10], []); const chartAccessibilityLabel = `Chart with custom X and Y data. ${yData.length} data points`; const scrubberAccessibilityLabel = useCallback( (index: number) => { return `Point ${index + 1}: X value ${xData[index]}, Y value ${yData[index]}`; }, [xData, yData], ); return ( <LineChart enableScrubbing showArea showXAxis showYAxis accessibilityLabel={chartAccessibilityLabel} curve="natural" height={{ base: 200, tablet: 225, desktop: 250 }} inset={{ top: 16, right: 16, bottom: 0, left: 0 }} points series={[ { id: 'line', data: yData, }, ]} xAxis={{ data: xData, showLine: true, showTickMarks: true, showGrid: true }} yAxis={{ domain: { min: 0 }, position: 'left', showLine: true, showTickMarks: true, showGrid: true, }} > <Scrubber hideOverlay accessibilityLabel={scrubberAccessibilityLabel} /> </LineChart> ); }
Live Updates
You can change the data passed in via series prop to update the chart.
You can also use the useRef hook to reference the scrubber and pulse it on each update.
function LiveUpdates() { const scrubberRef = useRef<ScrubberRef>(null); const initialData = useMemo(() => { return sparklineInteractiveData.hour.map((d) => d.value); }, []); const [priceData, setPriceData] = useState(initialData); const lastDataPointTimeRef = useRef(Date.now()); const updateCountRef = useRef(0); const intervalSeconds = 3600 / initialData.length; const maxPercentChange = Math.abs(initialData[initialData.length - 1] - initialData[0]) * 0.05; useEffect(() => { const priceUpdateInterval = setInterval( () => { setPriceData((currentData) => { const newData = [...currentData]; const lastPrice = newData[newData.length - 1]; const priceChange = (Math.random() - 0.5) * maxPercentChange; const newPrice = Math.round((lastPrice + priceChange) * 100) / 100; // Check if we should roll over to a new data point const currentTime = Date.now(); const timeSinceLastPoint = (currentTime - lastDataPointTimeRef.current) / 1000; if (timeSinceLastPoint >= intervalSeconds) { // Time for a new data point - remove first, add new at end lastDataPointTimeRef.current = currentTime; newData.shift(); // Remove oldest data point newData.push(newPrice); // Add new data point updateCountRef.current = 0; } else { // Just update the last data point newData[newData.length - 1] = newPrice; updateCountRef.current++; } return newData; }); // Pulse the scrubber on each update scrubberRef.current?.pulse(); }, 2000 + Math.random() * 1000, ); return () => clearInterval(priceUpdateInterval); }, [intervalSeconds, maxPercentChange]); const chartAccessibilityLabel = useMemo(() => { return `Live Bitcoin price chart. Current price: $${priceData[priceData.length - 1].toFixed(2)}`; }, [priceData]); const scrubberAccessibilityLabel = useCallback( (index: number) => { const price = priceData[index]; return `Bitcoin price at position ${index + 1}: $${price.toFixed(2)}`; }, [priceData], ); return ( <LineChart enableScrubbing showArea accessibilityLabel={chartAccessibilityLabel} height={{ base: 200, tablet: 225, desktop: 250 }} inset={{ right: 64 }} series={[ { id: 'btc', data: priceData, color: assets.btc.color, }, ]} > <Scrubber ref={scrubberRef} accessibilityLabel={scrubberAccessibilityLabel} /> </LineChart> ); }
Missing Data
By default, null values in data create gaps in a line. Use connectNulls to skip null values and draw a continuous line.
Note that scrubber beacons and points are still only shown at non-null data values.
function MissingData() { const pages = ['Page A', 'Page B', 'Page C', 'Page D', 'Page E', 'Page F', 'Page G']; const pageViews = [2400, 1398, null, 3908, 4800, 3800, 4300]; const uniqueVisitors = [4000, 3000, null, 2780, 1890, 2390, 3490]; const numberFormatter = useCallback( (value: number) => new Intl.NumberFormat('en-US', { maximumFractionDigits: 0 }).format(value), [], ); return ( <LineChart enableScrubbing showArea showXAxis showYAxis height={{ base: 200, tablet: 225, desktop: 250 }} // You can render points at every valid data point by always returning true points series={[ { id: 'pageViews', data: pageViews, color: 'var(--color-accentBoldGreen)', // Label will render next to scrubber beacon label: 'Page Views', connectNulls: true, }, { id: 'uniqueVisitors', data: uniqueVisitors, color: 'var(--color-accentBoldPurple)', label: 'Unique Visitors', }, ]} xAxis={{ // Used on the x-axis to provide context for each index from the series data array data: pages, }} yAxis={{ showGrid: true, tickLabelFormatter: numberFormatter, }} > {/* We can offset the overlay to account for the points being drawn on the lines */} <Scrubber overlayOffset={6} /> </LineChart> ); }
Empty State
<LineChart height={{ base: 200, tablet: 225, desktop: 250 }} series={[ { id: 'line', color: 'rgb(var(--gray50))', data: [1, 1], showArea: true, }, ]} yAxis={{ domain: { min: -1, max: 3 } }} />
Scales
LineChart uses linear scaling on axes by default, but you can also use other types, such as log. See XAxis and YAxis for more information.
<LineChart showArea showYAxis height={{ base: 200, tablet: 225, desktop: 250 }} series={[ { id: 'prices', data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58], }, ]} yAxis={{ scaleType: 'log', showGrid: true, ticks: [1, 10, 100], }} />
Interaction
Charts have built in functionality enabled through scrubbing, which can be used by setting enableScrubbing to true. You can listen to value changes through onScrubberPositionChange. Adding Scrubber to LineChart showcases the current scrubber position.
function Interaction() { const [scrubberPosition, setScrubberPosition] = useState<number | undefined>(); return ( <VStack gap={2}> <Text font="label1"> {scrubberPosition !== undefined ? `Scrubber position: ${scrubberPosition}` : 'Not scrubbing'} </Text> <LineChart enableScrubbing showArea height={{ base: 200, tablet: 225, desktop: 250 }} onScrubberPositionChange={setScrubberPosition} series={[ { id: 'prices', data: [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58], }, ]} > <Scrubber /> </LineChart> </VStack> ); }
Points
You can use points from LineChart with onClick listeners to render instances of Point that are interactable.
function Points() { const keyMarketShiftIndices = [4, 6, 7, 9, 10]; const data = [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58]; return ( <CartesianChart height={{ base: 200, tablet: 225, desktop: 250 }} series={[ { id: 'prices', data: data, }, ]} > <Area fill="rgb(var(--blue5))" seriesId="prices" /> <Line points={({ dataX, dataY, ...props }) => keyMarketShiftIndices.includes(dataX) ? { ...props, strokeWidth: 2, stroke: 'var(--color-bg)', radius: 5, onClick: () => alert( `You have clicked a key market shift at position ${dataX + 1} with value ${dataY}!`, ), accessibilityLabel: `Key market shift point at position ${dataX + 1}, value ${dataY}. Click to view details.`, } : false } seriesId="prices" /> </CartesianChart> ); }
Animations
You can configure chart transitions using transition on LineChart and beaconTransitions on Scrubber. You can also disable animations by setting the animate on LineChart to false.
function Transitions() { const dataCount = 20; const maxDataOffset = 15000; const minStepOffset = 2500; const maxStepOffset = 10000; const domainLimit = 20000; const updateInterval = 500; const myTransitionConfig = { type: 'spring', stiffness: 700, damping: 20 }; const negativeColor = 'rgb(var(--gray15))'; const positiveColor = 'var(--color-fgPositive)'; function generateNextValue(previousValue: number) { const range = maxStepOffset - minStepOffset; const offset = Math.random() * range + minStepOffset; let direction; if (previousValue >= maxDataOffset) { direction = -1; } else if (previousValue <= -maxDataOffset) { direction = 1; } else { direction = Math.random() < 0.5 ? -1 : 1; } let newValue = previousValue + offset * direction; newValue = Math.max(-maxDataOffset, Math.min(maxDataOffset, newValue)); return newValue; } function generateInitialData() { const data = []; let previousValue = Math.random() * 2 * maxDataOffset - maxDataOffset; data.push(previousValue); for (let i = 1; i < dataCount; i++) { const newValue = generateNextValue(previousValue); data.push(newValue); previousValue = newValue; } return data; } const MyGradient = memo((props: DottedAreaProps) => { const areaGradient = { stops: ({ min, max }: AxisBounds) => [ { offset: min, color: negativeColor, opacity: 1 }, { offset: 0, color: negativeColor, opacity: 0 }, { offset: 0, color: positiveColor, opacity: 0 }, { offset: max, color: positiveColor, opacity: 1 }, ], }; return <DottedArea {...props} gradient={areaGradient} />; }); function CustomTransitionsChart() { const [data, setData] = useState(generateInitialData); useEffect(() => { const intervalId = setInterval(() => { setData((currentData) => { const lastValue = currentData[currentData.length - 1] ?? 0; const newValue = generateNextValue(lastValue); return [...currentData.slice(1), newValue]; }); }, updateInterval); return () => clearInterval(intervalId); }, []); const tickLabelFormatter = useCallback( (value: number) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', maximumFractionDigits: 0, }).format(value), [], ); const valueAtIndexFormatter = useCallback( (dataIndex: number) => new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD', }).format(data[dataIndex]), [data], ); const lineGradient = { stops: [ { offset: 0, color: negativeColor }, { offset: 0, color: positiveColor }, ], }; return ( <CartesianChart enableScrubbing height={{ base: 200, tablet: 250, desktop: 300 }} inset={{ top: 32, bottom: 32, left: 16, right: 16 }} series={[ { id: 'prices', data: data, gradient: lineGradient, }, ]} yAxis={{ domain: { min: -domainLimit, max: domainLimit } }} > <YAxis showGrid requestedTickCount={2} tickLabelFormatter={tickLabelFormatter} /> <Line showArea AreaComponent={MyGradient} seriesId="prices" strokeWidth={3} transition={myTransitionConfig} /> <Scrubber hideOverlay beaconTransitions={{ update: myTransitionConfig }} label={valueAtIndexFormatter} /> </CartesianChart> ); } return <CustomTransitionsChart />; }
Accessibility
You can use accessibilityLabel on both the chart and the scrubber to provide descriptive labels. The chart's label gives an overview, while the scrubber's label provides specific information about the current data point being viewed.
function BasicAccessible() { const data = useMemo(() => [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58], []); // Chart-level accessibility label provides overview const chartAccessibilityLabel = useMemo(() => { const currentPrice = data[data.length - 1]; return `Price chart showing trend over ${data.length} data points. Current value: ${currentPrice}. Use arrow keys to adjust view`; }, [data]); // Scrubber-level accessibility label provides specific position info const scrubberAccessibilityLabel = useCallback( (index: number) => { return `Price at position ${index + 1} of ${data.length}: ${data[index]}`; }, [data], ); return ( <LineChart enableScrubbing showArea showYAxis accessibilityLabel={chartAccessibilityLabel} height={{ base: 200, tablet: 225, desktop: 250 }} series={[ { id: 'prices', data: data, }, ]} yAxis={{ showGrid: true, }} > <Scrubber accessibilityLabel={scrubberAccessibilityLabel} /> </LineChart> ); }
When a chart has a visible header or title, you can use aria-labelledby to reference it, and still provide a dynamic scrubber accessibility label.
function AccessibleWithHeader() { const headerId = useId(); const data = useMemo(() => [10, 22, 29, 45, 98, 45, 22, 52, 21, 4, 68, 20, 21, 58], []); // Display label provides overview const displayLabel = useMemo( () => `Revenue chart showing trend. Current value: ${data[data.length - 1]}`, [data], ); // Scrubber-specific accessibility label const scrubberAccessibilityLabel = useCallback( (index: number) => { return `Viewing position ${index + 1} of ${data.length}, value: ${data[index]}`; }, [data], ); return ( <VStack gap={2}> <Text font="label1" id={headerId}> {displayLabel} </Text> <LineChart enableScrubbing showArea showYAxis aria-labelledby={headerId} height={{ base: 200, tablet: 225, desktop: 250 }} series={[ { id: 'revenue', data: data, }, ]} yAxis={{ showGrid: true, }} > <Scrubber accessibilityLabel={scrubberAccessibilityLabel} /> </LineChart> </VStack> ); }
Styling
Axes
Using showXAxis and showYAxis allows you to display the axes. For more information, such as adjusting domain and range, see XAxis and YAxis.