Create an animated bar chart with React and react-move
Requires solid experience with React, ES6 and a bit of SVG
14 August 2018
Covers version 2.8.0 of react-move.
When creating custom charts one of the most popular JavaScript libraries is D3, particularly when fine-grained control over animations/transitions is required. D3 is a powerful library but its enter/exit/update paradigm can be tricky to learn, particularly when dealing with nested elements.
An alternative approach is to use a library such as React or VueJS for managing the DOM and to use D3 just for data manipulation (geographic projections, tree layouts etc.). However, unlike D3, neither library has a built-in animation/transition system.
There's a large number of React animation libraries in existence but in this article we'll focus on react-move which uses a D3-esque approach to transitions (it uses D3 under the hood). (BTW I'm interested to see this example built with any of the other animation libraries.)
This article shows how to build a bar chart with enter, leave and update animations. When bars are created, they'll fade in and grow from zero. When they leave they'll fade out and when they update they'll grow or shrink.
We'll start with a simple example where a single element is animated between different states. We'll then expand this example to animate several elements. Finally we'll build a bar chart with enter, leave and update animations.
1. Single element animation with react-move
Let's start by building a simple component with a single state property x and a render function that outputs a circle. We'll also add a button which randomises this.state.x when clicked:
import React, { Component } from "react";
import ReactDOM from "react-dom";
let getRandom = () => 600 * Math.random();
class Dot extends Component {
constructor(props) {
super(props);
this.state = {
x: getRandom()
};
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.setState({ x: getRandom() });
}
render() {
return (
<div>
<div>
<button onClick={this.handleClick}>Update</button>
</div>
<svg width="800" height="100">
<circle cx={this.state.x} cy="50" r="20" fill="#368F8B" />
</svg>
</div>
);
}
}
ReactDOM.render(<Dot />, document.getElementById("root"));
When the update button is clicked, the dot jumps to a new position.
We'll now animate the circle by adding react-move:
import Animate from "react-move/Animate";
and adding an <Animate> component:
<Animate
start={ { cx: 0 } }
enter={ { cx: [this.state.x] } }
update={ { cx: [this.state.x] } } >
</Animate>
Start, enter, update and leave attributes
Animate has 4 attributes which specify the animation:
startwhich lets us set initial valuesenter(optional) which specifies the animation straight after the element has been created (e.g. fade-ins)update(optional) which specifies the animation for elements already in existence (e.g. state changes)leave(optional) which specifies the animation just before the element is removed (e.g. fade-outs)
Each of the 4 attributes is set to a JavaScript object e.g. {cx: 0}. This object can contain as many properties as we like. In our instance we just want to animate a single attribute cx hence our object just contains a single property cx. We can name these properties whatever we like by the way.
react-move has a convention whereby property values that should animate must be put into an array e.g. { cx: [this.state.x] }. This allows us fine grained control over which properties will be animated.
In our example,
start={ { cx: 0 } }
enter={ { cx: [this.state.x] } }
update={ { cx: [this.state.x] } }
we're saying:
- when the circle is first created, property
cxis set to 0 - once the circle has been created, property
cxwill transition tothis.state.x - on subsequent renders property
cxwill transition tothis.state.x
Note there's two curly braces: the outside pair are to break out of JSX into JavaScript and the inner pair belong to the object.
Render function
The child of <Animate> must be a function d => {...} where d is an object that has the same properties as in the start, enter etc. objects. Thus in our example, d will have a cx property. This function will be called at each time-step with each of d's properties being interpolated.
Any attributes or styles that we wish to animate should use d's properties e.g.
d => <circle cx={d.cx} cy="50" r="20" fill="#368F8B" />
To summarise we've:
- used the
Animatecomponent, specifying the what, when and how of the animation withstart,enter,updateandleaveobjects - the child of
Animateis a function whose input is an object containing interpolated properties and output is the animated element
The complete example is here on CodeSandbox.
2. Multiple element animation with react-move
Now we'll look at animating a group of elements using react-move. We'll be using a component called NodeGroup which is used in a similar manner to Animate but has some important differences.
Supposing we have an array of data in our state this.state.points we'll define two further attributes on NodeGroup:
datawhose value will bethis.state.pointskeyAccessorwhich is a function that given an individual data point inthis.state.pointsreturns a unique identifier
We also define start, enter, update and leave attributes but instead of setting these to objects, they are set to functions. Each function is called for each element in data. The element is passed into the function and the output is (in the simplest case) an object describing the properties we wish to interpolate.
<NodeGroup
data={this.state.points}
keyAccessor={d => d.id}
start={() => ({ cx: 350 })}
enter={d => ({ cx: [d.x] })}
update={d => ({ cx: [d.x] })}
></NodeGroup>
The start, enter and update attributes are set to functions which return an object with the properties we want to animate. Aside from the need use functions the start, enter etc. attributes behave as described in the previous section.
The child of NodeGroup must be a function:
nodes => (
<g>
{nodes.map(({ key, data, state }, i) => (
<circle
key={key}
cx={state.cx}
cy={data.y}
r={20}
fill={colours[i]}
/>
))}
</g>
)
The input of this function nodes is an array with elements corresponding to elements from this.state.points. Each element has properties key, data and state:
keyis the key we specified with thekeyAccessorattributedatais the original datum e.g.{id: 0, x: 123, y: 0}stateis the interpolated object e.g.{cx: 23.5}. This will change with time during the course of the transition.
We map nodes to <circle> elements, using the state object for animated properties (i.e. state.cx), and data for properties on our original data array (i.e. data.y) that don't need to be animated.
The complete multi-element animation example can be seen on CodeSandbox.
Fine tuning the animation
We can add a timing property to the object returned by the enter, update and leave functions:
enter={(d, i) => ({
cx: [d.x],
timing: { duration: 750, delay: i * 400, ease: easeBounce }
})}
>
timing has 3 (optional) properties:
durationwhich specifies the duration of the transition in millisecondsdelaywhich specifies how long to wait before the transition starts (in milliseconds)easewhich specifies an ease function (typically you'd use ones from d3-ease)
In the example above, the circles will, one-by-one, bounce into position.
3. Animated bar chart
Now we'll look at how to build our bar chart with the following animations:
- fade-in transition when bars are added
- bars grow from zero width when added
- bar width animates when state changes
- bars fade out when removed
We'll use the bar chart created in Create a bar chart using React (no other libraries) as our starting point.
Our data array will be of the form:
[
{
id: 1,
value: 123,
name: 'Item 1'
},
...
]
and we have 3 buttons for adding, removing and updating this array:
<div id="menu">
<button onClick={this.handleAdd}>Add item</button>
<button onClick={this.handleRemove}>Remove item</button>
<button onClick={this.handleUpdate}>Update values</button>
</div>
Our NodeGroup looks like:
<NodeGroup
data={this.state.data}
keyAccessor={d => d.name}
start={this.startTransition}
enter={this.enterTransition}
update={this.updateTransition}
leave={this.leaveTransition}
>
To keep things tidy we've added our transition functions as methods:
startTransition(d, i) {
return { value: 0, y: i * barHeight, opacity: 0 };
}
enterTransition(d) {
return { value: [d.value], opacity: [1], timing: { duration: 250 } };
}
updateTransition(d, i) {
return { value: [d.value], y: [i * barHeight], timing: { duration: 300 } };
}
leaveTransition(d) {
return { opacity: [0], y: [-barHeight], timing: { duration: 250 } };
}
This time we're interpolating 3 properties: value, y and opacity:
valuewill start at 0, transition tod.value(i.e. the current data value) on enter and updateywill start ati * barHeightand will transition on update. When the bar is removed,yis set to-barHeightso that the bar appears to move off the chartopacitywill start at 0, transition to 1 when the bar enters and transition back to 0 as the bar leaves
Hopefully this demonstrates the fine-grained control over the animations that react-move gives us.
Now let's look at the render function:
nodes => (
<g>
{nodes.map(({ key, data, state }) => (
<BarGroup key={key} data={data} state={state} />
))}
</g>
)
Here we pass key, data and state straight through to BarGroup which is our component for rendering a single bar and its labels:
function BarGroup(props) {
let width = widthScale(props.state.value);
let yMid = barHeight * 0.5;
return (
<g className="bar-group" transform={`translate(0, ${props.state.y})`}>
<rect
y={barPadding * 0.5}
width={width}
height={barHeight - barPadding}
style={ { fill: barColour, opacity: props.state.opacity } }
/>
<text
className="value-label"
x={width - 6}
y={yMid}
alignmentBaseline="middle"
>
{props.state.value.toFixed(0)}
</text>
<text
className="name-label"
x="-6"
y={yMid}
alignmentBaseline="middle"
style={ { opacity: props.state.opacity } }
>
{props.data.name}
</text>
</g>
);
}
The animated elements include:
- the transform of the bar group, driven by
props.state.y - the width of the
rectelement and value label position which are driven byprops.state.value - the opacity of the
rectand name label (driven byprops.state.opacity)
(Remember that props.state is the object with the interpolated properties value, y and opacity.)
The complete example is on CodeSandbox.
Summary
This article was the result of a search for a React animation library that could replicate the type of transitions we see in D3 visualisations. react-move certainly seems a good candidate: we've demonstrated fairly sophisticated control over transitions that seems to match what D3 can deliver. Maybe this isn't a surprise as react-move uses D3 under the hood.
The syntax can get a bit confusing, especially with the amount of nested brackets due to jumping in and out of JSX. Whether this is an impediment to react-move's adoption is yet to be seen. It's certainly going to be a close call deciding between sticking with D3 or using React & react-move!




