Animating a whole chart
The previous lesson showed how to animate a single shape with the motion library.
But real charts are made of dozens of elements. Time to apply what we know to a full chart and trigger our first real dataviz transitions.
Most basic example
In the previous lesson we saw that a motion.circle with an animate prop transitions smoothly between two values. A chart is just many shapes, so the natural question is: how do we scale that up?
Rendering many animated circles works exactly the same way as rendering many regular ones: it all comes down to mapping over a data array.
Everything starts from a dataset, which is an array. A .map() loops over it and renders one circle per item. When the dataset changes, the loop runs again, each motion.circle receives a new animate target, and the spring takes over from there:
import { useState } from 'react'; import { motion } from 'motion/react'; import { data1, data2, COLORS } from './data'; export default function App() { const [data, setData] = useState(data1); return ( <div style={{ padding: 16 }}> <button onClick={() => setData(data === data1 ? data2 : data1)}> Switch dataset </button> <svg width={500} height={220}> {data.map((d) => ( <motion.circle key={d.id} r={28} fill={COLORS[d.id]} fillOpacity={0.5} stroke={COLORS[d.id]} strokeWidth={2} animate={{ cx: d.x, cy: d.y }} transition={{ type: 'spring', stiffness: 100, damping: 18 }} /> ))} </svg> </div> ); }
The key is key
In the sandbox above, the three circles animate independently and in parallel. Each motion.circle tracks its own spring.
But how does that work? When the dataset changes, how does React know which circle is which?
The answer is the key prop we attached to every motion.circle.
React uses that key to match each rendered element with its previous DOM node. If the key is already on screen, React reuses the same node and Motion smoothly transitions it from its old position to the new one.
Use the wrong key (the array index, for instance) and React re-mounts a fresh element on every dataset change. Motion has no previous value to transition from, so the chart appears to teleport rather than animate.
Let me show you in a video:
Barplot & props
Let's swap the circle for a rectangle and build a first animated barplot.
The recipe is the same: loop over the dataset, render a motion.rect for each item. When the dataset changes, the loop runs again and each rectangle animates to its new width.
import { useState } from 'react'; import { Barplot } from './Barplot'; import { data1, data2 } from './data'; // The parent owns the data state. The Barplot component just renders. export default function App() { const [data, setData] = useState(data1); return ( <div style={{ padding: 16 }}> <button onClick={() => setData(data === data1 ? data2 : data1)}> Switch dataset </button> <Barplot data={data} width={500} height={260} /> </div> ); }
One small but important shift in this example: the data state no longer lives inside the chart. The App component owns the state and passes the current data down as a prop.
The Barplot component is a pure renderer that animates whenever its data prop changes. That's how dataviz components are wired in real applications: business logic and state live higher up, the chart only renders what it's given.
Graphs with many shapes per item
Some chart types use several SVG elements to represent a single data point.
A lollipop chart, for instance, draws each item as a segment and a circle. A dumbbell chart goes further: two circles plus a connecting segment, one per group, to show two values per category.
This doesn't change the recipe.
We still loop over the dataset and render one g per item with a unique key. Inside that g, we put several motion.* elements (a motion.line, a motion.circle, a motion.text, and so on). Each one animates independently when the data changes.
import { useState } from 'react'; import { Dumbbell } from './Dumbbell'; import { data1, data2 } from './data'; export default function App() { const [data, setData] = useState(data1); return ( <div style={{ padding: 16 }}> <button onClick={() => setData(data === data1 ? data2 : data1)}> Switch dataset </button> <Dumbbell data={data} width={500} height={360} /> </div> ); }
Push the animation into a leaf component. Even as charts get more complex, the animation complexity does not have to grow with them. In the sandbox above, the Dumbbell component handles scales, margins and the loop over data — but knows nothing about Motion. All the motion.* elements live one level down, in a tiny DumbbellItem leaf component.
Apply this pattern everywhere: keep your chart components free of animation logic, and isolate the motion.* calls in the smallest component you can write. The chart logic stays unchanged whether you animate it or not.
Exercises
Up next: enter and exit
Everything we've animated so far assumes the same data points exist before and after the update — only their values change.
But what about points that enter the dataset for the first time, or ones that leave it?
That's the topic of the next lesson.
Enjoyed this lesson?
It comes from the D3 ❤️ React course. Enrollment is closed for now: join the waitlist to hear first when doors reopen for cohort 3.