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:
start
which 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
cx
is set to 0 - once the circle has been created, property
cx
will transition tothis.state.x
- on subsequent renders property
cx
will 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
Animate
component, specifying the what, when and how of the animation withstart
,enter
,update
andleave
objects - the child of
Animate
is 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
:
data
whose value will bethis.state.points
keyAccessor
which is a function that given an individual data point inthis.state.points
returns 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
:
key
is the key we specified with thekeyAccessor
attributedata
is the original datum e.g.{id: 0, x: 123, y: 0}
state
is 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:
duration
which specifies the duration of the transition in millisecondsdelay
which specifies how long to wait before the transition starts (in milliseconds)ease
which 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
:
value
will start at 0, transition tod.value
(i.e. the current data value) on enter and updatey
will start ati * barHeight
and will transition on update. When the bar is removed,y
is set to-barHeight
so that the bar appears to move off the chartopacity
will 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
rect
element and value label position which are driven byprops.state.value
- the opacity of the
rect
and 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
!