├── typing.gif
├── public
├── favicon.ico
├── manifest.json
└── index.html
├── src
├── index.js
├── components
│ ├── Alphabet.jsx
│ ├── FancyText.jsx
│ └── Letter.jsx
└── App.jsx
├── .gitignore
├── package.json
├── LICENSE
└── README.md
/typing.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Swizec/react-d3-enter-exit-transitions/HEAD/typing.gif
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Swizec/react-d3-enter-exit-transitions/HEAD/public/favicon.ico
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { render } from "react-dom";
3 | import App from "./App";
4 |
5 | render(, document.getElementById("root"));
6 |
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | }
10 | ],
11 | "start_url": "./index.html",
12 | "display": "standalone",
13 | "theme_color": "#000000",
14 | "background_color": "#ffffff"
15 | }
16 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | npm-debug.log
4 | dist# See https://help.github.com/ignore-files/ for more about ignoring files.
5 |
6 | # dependencies
7 | /node_modules
8 |
9 | # testing
10 | /coverage
11 |
12 | # production
13 | /build
14 |
15 | # misc
16 | .DS_Store
17 | .env.local
18 | .env.development.local
19 | .env.test.local
20 | .env.production.local
21 |
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "react-d3-enter-exit-transitions",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "d3": "^5.7.0",
7 | "gh-pages": "^2.0.1",
8 | "react": "^16.5.2",
9 | "react-dom": "^16.5.2",
10 | "react-transition-group": "^2.5.0"
11 | },
12 | "devDependencies": {
13 | "react-scripts": "1.0.17"
14 | },
15 | "scripts": {
16 | "start": "react-scripts start",
17 | "build": "react-scripts build",
18 | "test": "react-scripts test --env=jsdom",
19 | "eject": "react-scripts eject",
20 | "predeploy": "yarn run build",
21 | "deploy": "gh-pages -d build"
22 | },
23 | "homepage": "https://swizec.github.io/react-d3-enter-exit-transitions"
24 | }
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) [year] [fullname]
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/src/components/Alphabet.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import * as d3 from "d3";
3 | import { TransitionGroup } from "react-transition-group";
4 |
5 | import Letter from "./Letter";
6 |
7 | class Alphabet extends React.Component {
8 | static letters = "abcdefghijklmnopqrstuvwxyz".split("");
9 | state = { alphabet: [] };
10 |
11 | componentDidMount() {
12 | d3.interval(this.shuffleAlphabet, 1500);
13 | }
14 |
15 | shuffleAlphabet = () => {
16 | const alphabet = d3
17 | .shuffle(Alphabet.letters)
18 | .slice(0, Math.floor(Math.random() * Alphabet.letters.length))
19 | .sort();
20 |
21 | this.setState({
22 | alphabet
23 | });
24 | };
25 |
26 | render() {
27 | let transform = `translate(${this.props.x}, ${this.props.y})`;
28 |
29 | return (
30 |
31 |
32 | {this.state.alphabet.map((d, i) => (
33 |
34 | ))}
35 |
36 |
37 | );
38 | }
39 | }
40 |
41 | export default Alphabet;
42 |
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
13 |
22 | React App
23 |
24 |
25 |
28 |
29 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/src/App.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 |
3 | import Alphabet from "./components/Alphabet";
4 | import FancyText from "./components/FancyText";
5 |
6 | class App extends Component {
7 | state = { text: "" };
8 |
9 | changeText(event) {
10 | this.setState({ text: event.target.value });
11 | }
12 |
13 | render() {
14 | return (
15 |
16 |
17 | Animated typing built with React 16+, D3js v5, and
18 | react-transition-group v2
19 |
20 |
21 | Type some text. Delete stome stuff. Watch enter/update/exit
22 | transitions at play.
23 |
24 |
25 | Inspired by Bostock's block{" "}
26 |
27 | General Update Pattern 4.0
28 |
29 |
30 |
42 |
46 |
47 | );
48 | }
49 | }
50 |
51 | export default App;
52 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # React d3 enter/exit animations
3 |
4 | 
5 |
6 | This is a proof of concept implementation of declarative enter-update-exit
7 | transitions built with React and d3js.
8 |
9 | The main `FancyText` component's render method is completely
10 | declarative. It renders `Letter`s using a naive loop.
11 |
12 | ```javascript
13 | render() {
14 | let transform = `translate(${this.props.x}, ${this.props.y})`;
15 |
16 | return (
17 |
18 |
19 | {this.state.alphabet.map((d, i) => (
20 |
21 | ))}
22 |
23 |
24 | );
25 | }
26 | ```
27 |
28 | Each `Letter` component then uses lifecycle hooks from
29 | `ReactTransitionGroup` to do transitions.
30 |
31 | ```javascript
32 | class Letter extends Component {
33 | state = {
34 | y: -60,
35 | className: 'enter',
36 | fillOpacity: 1e-6
37 | }
38 |
39 | componentWillEnter(callback) {
40 | // start enter transition, then callback()
41 | }
42 |
43 | componentWillLeave(callback) {
44 | // start exit transition, then callback()
45 | }
46 |
47 | componentWillReceiveProps(nextProps) {
48 | if (this.props.i != nextProps.i) {
49 | // start update transition
50 | }
51 | }
52 |
53 | render() {
54 | return (
55 |
60 | {this.props.d}
61 |
62 | );
63 | }
64 | };
65 |
66 | ```
67 |
68 | Based on Mike Bostock's
69 | [General Update Pattern 4.0](https://bl.ocks.org/mbostock/a8a5baa4c4a470cda598)
70 | block.
71 |
72 | ## License
73 |
74 | CC0 (public domain)
75 |
--------------------------------------------------------------------------------
/src/components/FancyText.jsx:
--------------------------------------------------------------------------------
1 | import React, { Component } from "react";
2 | import { TransitionGroup } from "react-transition-group";
3 | import * as d3 from "d3";
4 |
5 | import Letter from "./Letter";
6 |
7 | class FancyText extends Component {
8 | state = {
9 | textWithIds: [],
10 | lastId: 0
11 | };
12 |
13 | componentDidUpdate(prevProps) {
14 | if (prevProps.text === this.props.text) return;
15 |
16 | const oldText = this.state.textWithIds;
17 | const newText = this.props.text.split("");
18 | let indexOfChange = 0,
19 | sizeOfChange = 0,
20 | newLastId = this.state.lastId;
21 |
22 | // find change
23 | for (
24 | ;
25 | newText[indexOfChange] ==
26 | (oldText[indexOfChange] && oldText[indexOfChange][0]);
27 | indexOfChange++
28 | );
29 |
30 | // calculate size of change
31 | if (newText.length > oldText.length) {
32 | while (
33 | newText[indexOfChange + sizeOfChange] !=
34 | (oldText[indexOfChange] && oldText[indexOfChange][0]) &&
35 | indexOfChange + sizeOfChange < newText.length
36 | ) {
37 | sizeOfChange = sizeOfChange + 1;
38 | }
39 | } else {
40 | while (
41 | newText[indexOfChange] !=
42 | (oldText[indexOfChange + sizeOfChange] &&
43 | oldText[indexOfChange + sizeOfChange][0]) &&
44 | indexOfChange + sizeOfChange < oldText.length
45 | ) {
46 | sizeOfChange = sizeOfChange + 1;
47 | }
48 | }
49 |
50 | // use existing ids up to point of change
51 | d3.range(0, indexOfChange).forEach(i => (newText[i] = oldText[i]));
52 |
53 | // use new ids for additions
54 | if (newText.length > oldText.length) {
55 | d3.range(indexOfChange, indexOfChange + sizeOfChange).forEach(i => {
56 | let letter = newText[i];
57 | newText[i] = [letter, newLastId++];
58 | });
59 |
60 | // use existing ids from change to end
61 | d3.range(indexOfChange + sizeOfChange, newText.length).forEach(
62 | i => (newText[i] = oldText[i - sizeOfChange])
63 | );
64 | } else {
65 | // use existing ids from change to end, but skip what's gone
66 | d3.range(indexOfChange, newText.length).forEach(
67 | i => (newText[i] = oldText[i + sizeOfChange])
68 | );
69 | }
70 |
71 | this.setState({
72 | textWithIds: newText,
73 | lastId: newLastId
74 | });
75 | }
76 |
77 | render() {
78 | let { x, y } = this.props;
79 |
80 | return (
81 |
82 |
83 | {this.state.textWithIds.map(([l, id], i) => (
84 |
85 | ))}
86 |
87 |
88 | );
89 | }
90 | }
91 |
92 | export default FancyText;
93 |
--------------------------------------------------------------------------------
/src/components/Letter.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import * as d3 from "d3";
3 | import Transition from "react-transition-group/Transition";
4 |
5 | const ExitColor = "brown",
6 | UpdateColor = "#333",
7 | EnterColor = "green";
8 |
9 | class Letter extends React.Component {
10 | defaultState = {
11 | y: -60,
12 | x: this.props.index * 32,
13 | color: EnterColor,
14 | fillOpacity: 1e-6
15 | };
16 | state = this.defaultState;
17 | letterRef = React.createRef();
18 |
19 | onEnter = () => {
20 | // Letter is entering the visualization
21 | let node = d3.select(this.letterRef.current);
22 |
23 | node.transition()
24 | .duration(750)
25 | .ease(d3.easeCubicInOut)
26 | .attr("y", 0)
27 | .style("fill-opacity", 1)
28 | .on("end", () => {
29 | this.setState({
30 | y: 0,
31 | fillOpacity: 1,
32 | color: UpdateColor
33 | });
34 | });
35 | };
36 |
37 | onExit = () => {
38 | // Letter is dropping out
39 | let node = d3.select(this.letterRef.current);
40 |
41 | node.style("fill", ExitColor)
42 | .transition(this.transition)
43 | .attr("y", 60)
44 | .style("fill-opacity", 1e-6)
45 | .on("end", () => this.setState(this.defaultState));
46 | };
47 |
48 | componentDidUpdate(prevProps, prevState) {
49 | if (prevProps.in !== this.props.in && this.props.in) {
50 | // A new enter transition has begun
51 | this.setState({
52 | x: this.props.index * 32
53 | });
54 | } else if (prevProps.index !== this.props.index) {
55 | // Letter is moving to a new location
56 | let node = d3.select(this.letterRef.current),
57 | targetX = this.props.index * 32;
58 |
59 | node.style("fill", UpdateColor)
60 | .transition()
61 | .duration(750)
62 | .ease(d3.easeCubicInOut)
63 | .attr("x", targetX)
64 | .on("end", () =>
65 | this.setState({
66 | x: targetX,
67 | color: UpdateColor
68 | })
69 | );
70 | }
71 | }
72 |
73 | render() {
74 | const { x, y, fillOpacity, color } = this.state,
75 | { letter } = this.props;
76 |
77 | return (
78 |
85 |
96 | {letter}
97 |
98 |
99 | );
100 | }
101 | }
102 |
103 | export default Letter;
104 |
--------------------------------------------------------------------------------