React for Data Visualization
Student Login

Enter/update/exit animation

Now that you know how to use transitions, it's time to take it up a notch. Enter/exit animations.

Enter/exit animations are the most common use of transitions. They're what happens when a new element enters or exits the picture. For example: in visualizations like this famous Nuclear Detonation Timeline by Isao Hashimoto. Each new Boom! flashes to look like an explosion.

I don't know how Hashimoto did it, but with React & D3, you'd do it with enter/exit transitions.

Another favorite of mine is an old animated alphabet example by Mike Bostock, the creator of D3, that's no longer online.

That's what we're going to build: An animated alphabet. New letters fall down and are green, updated letters move right or left, deleted letters are red and fall down.

You can play with a more advanced version here. Same principle as the alphabet, but it animates what you type.

Typing animation screenshot

We're building the alphabet version because the string diffing algorithm is a pain to explain. I learned that the hard way when giving workshops on React and D3…

String diffing algorithm sketch

See?

Easy on paper, but the code is long and weird. That, or I'm bad at implementing it. Either way, it's too tangential to explain here. You can read the article about it.

Animated alphabet

Our goal is to render a random subset of the alphabet. Every time the set updates, old letters transition out, new letters transition in, and updated letters transition into a new position.

We need two components:

  • Alphabet, which creates random lists of letters every 1.5 seconds, then maps through them to render Letter components
  • Letter, which renders an SVG text element and takes care of its own enter/update/exit transitions

You can see the full code on GitHub here.

Project setup

To get started you'll need a project. Start one with create-react-app or in CodeSandbox. Either works.

You'll need a base App component that renders an SVG with an <Alphabet> child. Our component is self-contained so that's all you need.

Something like this 👇

import Alphabet from './components/Alphabet`;
const App = () => (
<svg width="100%" height="600">
<Alphabet x={32} y={300} />
</svg>
)

I follow the convention of putting components in a src/components directory. You don't have to.

Remember to install dependencies: d3 and react-transition-group

The Alphabet component

The Alphabet component holds a list of letters in component state and renders a collection of Letter components in a loop.

We start with a skeleton like this:

// src/components/Alphabet.js
import React from "react";
import * as d3 from "d3";
import { TransitionGroup } from "react-transition-group";
import Letter from "./Letter";
class Alphabet extends React.Component {
static letters = "abcdefghijklmnopqrstuvwxyz".split("");
state = { alphabet: [] };
componentDidMount() {
// start interval
}
shuffleAlphabet = () => {
// generate new alphabet
};
render() {
let transform = `translate(${this.props.x}, ${this.props.y})`;
return (
// spit out letters
);
}
}
export default Alphabet;

We import dependencies and define the Alphabet component. It keeps a list of available letters in a static letters property and an empty alphabet in component state.

We'll start a d3.interval on componentDidMount and use shuffleAlphabet to generate alphabet subsets.

To showcase enter-update-exit transitions, we create a new alphabet every second and a half. Using d3.interval lets us do that in a browser friendly way.

// src/components/Alphabet/index.js
componentDidMount() {
d3.interval(this.shuffleAlphabet, 1500);
}
shuffleAlphabet = () => {
const alphabet = d3
.shuffle(Alphabet.letters)
.slice(0, Math.floor(Math.random() * Alphabet.letters.length))
.sort();
this.setState({
alphabet
});
};

Think of this as our game loop: Change alphabet state in consistent time intervals.

We use d3.interval( //.., 1500) to call shuffleAlphabet every 1.5 seconds. Same as setInterval, but friendlier to batteries and CPUs because it pegs to requestAnimationFrame. On each period, we use shuffleAlphabet to shuffle available letters, slice out a random amount, sort them, and update component state with setState.

This process ensures our alphabet is both random and in alphabetical order.

Starting the interval in componentDidMount ensures it only runs when our Alphabet is on the page. In real life you should stop it on componentWillUnmount. Since this is a tiny experiment and we know <Alphabet> never unmounts without a page refresh, it's okay to skip that step.

Declarative render for enter/exit transitions

Our declarative enter/exit transitions start in the render method.

// src/components/Alphabet/index.js
render() {
let transform = `translate(${this.props.x}, ${this.props.y})`;
return (
<g transform={transform}>
<TransitionGroup enter={true} exit={true} component="g">
{this.state.alphabet.map((d, i) => (
<Letter letter={d} index={i} key={d} />
))}
</TransitionGroup>
</g>
);
}

An SVG transformation moves our alphabet into the specified (x, y) position. We map through this.state.alphabet inside a <TransitionGroup> component and render a <Letter> component for every letter. Each Letter gets a letter prop for the text, an index prop to know where it stands, and a key so React can tell them apart.

The key property

The key property is how React identifies components. Pick wrong, and you're gonna have a bad time. I spent many hours debugging and writing workarounds before I realized that basing my key on the index was a Bad Move™. Obviously, you want the letter to stay constant in each component and the index to change.

That's how x-axis transitions work.

You move the letter into a specific place in the alphabet. You'll see what I mean when we look at the Letter component.

TransitionGroup

React TransitionGroup gives us coordinated control over a set of transitionable components. Each Letter is going to be a <Transition>, you'll see.

We need TransitionGroup to gain declarative control over the enter/exit cycle. Transition components can handle transitions themselves, but they need an in prop to say whether they're in or out of the visualization.

Flip from false to true, run an enter transition.

true to false, run an exit transition.

We can make this change from within our component, of course. When responding to user events for example. In our case we need that control to come from outside based on which letters exist in the alphabet array.

TransitionGroup handles that for us. It automatically passes the correct in prop to its children based on who is and isn't being rendered.

As an added bonus, we can use TransitionGroup to set a bunch of default parameters for child Transitions. Whether to use enter animations, exit animations, stuff like that. You can read a full list in the docs.

The Letter component

We're ready for the component that can transition itself into and out of a visualization. Without consumers having to worry about what's going on behind the scenes 👌

The skeleton for our Letter component looks like this:

// src/components/Letter.js
import React from "react";
import * as d3 from "d3";
import Transition from "react-transition-group/Transition";
const ExitColor = "brown",
UpdateColor = "#333",
EnterColor = "green";
class Letter extends React.Component {
defaultState = {
y: -60,
x: this.props.index * 32,
color: EnterColor,
fillOpacity: 1e-6
};
state = this.defaultState;
letterRef = React.createRef();
onEnter = () => {
// Letter enters the visualization
};
onExit = () => {
// Letter drops out transition
};
componentDidUpdate(prevProps, prevState) {
// update transition
}
render() {
const { x, y, fillOpacity, color } = this.state,
{ letter } = this.props;
return (
// render Transition with text
);
}
}
export default Letter;

We start with some imports and define a Letter component with a default state. We keep defaultState in a separate value because we're going to manually reset state in some cases.

A letterRef helps us hand over control to D3 during transitions, the onEnter callback handles enter transitions, onExit exit transitions, and componentDidUpdate update transitions. Render is where it call comes together.

Each of these transition methods is going to follow the same approach you learned about in the swipe transition example. Render from state, transition with D3, update state to match.

You can make this component more flexible by moving the various magic numbers we use into props. Default y offset, transition duration, colors, stuff like that. The world is your oyster my friend.

onEnter

We start with the enter transition in the onEnter callback.

// src/components/Letter.js
onEnter = () => {
// Letter is entering the visualization
let node = d3.select(this.letterRef.current)
node
.transition()
.duration(750)
.ease(d3.easeCubicInOut)
.attr("y", 0)
.style("fill-opacity", 1)
.on("end", () => {
this.setState({
y: 0,
fillOpacity: 1,
color: UpdateColor,
})
})
}

We use d3.select to grab our DOM node and take control with D3. Start a new transition with .transition(), specify a duration, an easing function, and specify the changes. Vertical position moves to 0, opacity changes to 1.

This creates a drop-in fade-in effect.

When our transition ends, we update state with the new y coordinate, fillOpacity, and color.

The result is an invisible letter that starts at -60px and moves into 0px and full visibility over 750 milliseconds.

onExit

Our exit transition goes in the onExit callback.

// src/components/Alphabet/
onExit = () => {
// Letter is dropping out
let node = d3.select(this.letterRef.current)
node
.style("fill", ExitColor)
.transition(this.transition)
.attr("y", 60)
.style("fill-opacity", 1e-6)
.on("end", () => this.setState(this.defaultState))
}

Same as before, we take control of the DOM, run a transition, and update state when we're done. We start with forcing our letter into a new color, then move it 60px down, transition to invisible, and reset state.

But why are we resetting state instead of updating to current reality?

Our components never unmount.

We avoid unmounts to keep transitions smoother. Instead of unmounting, we have to reset state back to its default values.

That moves the letter back into its enter state and ensures even re-used letters drop down from the top. Took me a while to tinker that one out.

render

Hard work is done. Here's how you render:

// src/components/Alphabet/Letter.js
render() {
const { x, y, fillOpacity, color } = this.state,
{ letter } = this.props;
return (
<Transition
in={this.props.in}
unmountOnExit={false}
timeout={750}
onEnter={this.onEnter}
onExit={this.onExit}
>
<text
dy=".35em"
x={x}
y={y}
style={{
fillOpacity: fillOpacity,
fill: color,
font: "bold 48px monospace"
}}
ref={this.letterRef}
>
{letter}
</text>
</Transition>
);
}

We render a Transition element, which gives us the transition super powers we need to run enter/exit transitions. Update transitions work on all React components.

The outside TransitionGroup gives us the correct in prop value, we just have to pass it into Transition. We disable unmountOnExit to make transitions smoother, define a timeout which has to match what we're using in our transitions, and define onEnter and onExit callbacks.

There's a lot more to the API that we can use and you should check that out in the docs. Docs don't go into detail on everything, but if you experiment I'm sure you'll figure it out.

Inside the transition we render an SVG <text> element rendered at an (x, y) position with a color and fillOpacity style. It shows a single letter taken from the letter prop.

That's it 👍

Boom. We're done.

Alphabet transitions are hard to screenshot

You can play with a more complex version of this example here: https://swizec.github.io/react-d3-enter-exit-transitions/. Try typing different strings and see how the visualization reacts.

The typing example uses the same Letter components to declaratively render its string, but it drives the input through your typing instead of an automatic shuffler.

Key takeaways for transitions are:

  • use d3 for transitions
  • use React to manage SVG elements
  • use TransitionGroup and Transition for the enter/update/exit pattern
Previous:
Powerful animation with transitions and game loops combined (24:40)
Next:
Animating with React, Redux, and D3
Created by Swizec with ❤️