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 values
  • enter (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 to this.state.x
  • on subsequent renders property cx will transition to this.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 with start, enter, update and leave 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 be this.state.points
  • keyAccessor which is a function that given an individual data point in this.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 the keyAccessor attribute
  • data 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 milliseconds
  • delay 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 to d.value (i.e. the current data value) on enter and update
  • y will start at i * 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 chart
  • opacity 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 by props.state.value
  • the opacity of the rect and name label (driven by props.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!