Author avatar

Manujith Pallewatte

D3 Treemap in React

Manujith Pallewatte

  • May 19, 2020
  • 15 Min read
  • 233 Views
  • May 19, 2020
  • 15 Min read
  • 233 Views
Web Development
Front End Web Development
Client-side Framework
React

Introduction

Treemap is a useful chart that provides a holistic visualization of hierarchical data. It also provides an idea of the composition of the hierarchy apart from the structure, which makes it even more valuable. Treemap can be used for analyzing stocks, the performance of different departments of an organization, visualizing COVID spread in countries, etc.

Implementing a Treemap chart with D3 is trivial, and we can find many sample codes for it online. But replicating the same in a React-D3 integration can be a bit tricky. In the guide, we will explore how to replicate one such sample with moderate complexity. Then we will add animations to the updating of the data to simulate a real-life dataset. The codefrom this guide is available in the Github repo.

Initializing the D3 Chart Component

First, create the component skeleton to add D3 support to React and integrate with it. Check out Pluralsight's guide Drawing Charts in React with D3 to learn how to create a simple D3 chart. Using the BarChart structure demonstrated in that guide, the Treemap chart can be initialized as follows.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// Treemap.js
import * as d3 from 'd3';
import React, { useRef, useEffect } from 'react';

function Treemap({ width, height, data }){
    const ref = useRef();

    useEffect(() => {
        const svg = d3.select(ref.current)
            .attr("width", width)
            .attr("height", height)
            .style("border", "1px solid black")
    }, []);

    useEffect(() => {
        draw();
    }, [data]);

    const draw = () => {
        
    }


    return (
        <div className="chart">
            <svg ref={ref}>
            </svg>
        </div>
        
    )

}

export default Treemap;
javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// App.js
import React from 'react';
import './App.css';
import Treemap from './Treemap';

const dataset = [];

function App() {
    return (
        <div className="App">
            <h2>Graphs with React</h2>
            <Treemap width={600} height={400} data={dataset} />
        </div>
    );
}

export default App;
javascript

Make sure that D3 is installed using npm install --save d3. You can start the app and verify that the component and D3 are functioning as expected.

Creating Treemap with Multiple Hierarchies

The visualization power of a Treemap chart can be better demonstrated with a dataset that has multiple hierarchies. This Treemap example provides an excellent introduction into the Treemap concept with a dataset of three simple hierarchies, and without extra complexities such as transitions and zooming.

You can import the dataset to React by copying the JSON data straight into the data variable. This avoids the hassle of data fetching through React for the moment. The rest of the D3 code shown can be imported to React with minimal changes.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
// Treemap.js

// ...
    const draw = () => {
        const svg = d3.select(ref.current);

        // Give the data to this cluster layout:
        var root = d3.hierarchy(data).sum(function(d){ return d.value});

        // initialize treemap
        d3.treemap()
            .size([width, height])
            .paddingTop(28)
            .paddingRight(7)
            .paddingInner(3)
            (root);
        
        const color = d3.scaleOrdinal()
            .domain(["boss1", "boss2", "boss3"])
            .range([ "#402D54", "#D18975", "#8FD175"]);

        const opacity = d3.scaleLinear()
            .domain([10, 30])
            .range([.5,1]);


        // Select the nodes
        var nodes = svg
                    .selectAll("rect")
                    .data(root.leaves())

        // draw rectangles
        nodes.enter()
            .append("rect")
            .attr('x', function (d) { return d.x0; })
            .attr('y', function (d) { return d.y0; })
            .attr('width', function (d) { return d.x1 - d.x0; })
            .attr('height', function (d) { return d.y1 - d.y0; })
            .style("stroke", "black")
            .style("fill", function(d){ return color(d.parent.data.name)} )
            .style("opacity", function(d){ return opacity(d.data.value)})

        nodes.exit().remove()

        // select node titles
        var nodeText = svg
            .selectAll("text")
            .data(root.leaves())

        // add the text
        nodeText.enter()
            .append("text")
            .attr("x", function(d){ return d.x0+5})    // +10 to adjust position (more right)
            .attr("y", function(d){ return d.y0+20})    // +20 to adjust position (lower)
            .text(function(d){ return d.data.name.replace('mister_','') })
            .attr("font-size", "19px")
            .attr("fill", "white")
        
        // select node titles
        var nodeVals = svg
            .selectAll("vals")
            .data(root.leaves())  

        // add the values
        nodeVals.enter()
            .append("text")
            .attr("x", function(d){ return d.x0+5})    // +10 to adjust position (more right)
            .attr("y", function(d){ return d.y0+35})    // +20 to adjust position (lower)
            .text(function(d){ return d.data.value })
            .attr("font-size", "11px")
            .attr("fill", "white")
    
        // add the parent node titles
        svg
        .selectAll("titles")
        .data(root.descendants().filter(function(d){return d.depth==1}))
        .enter()
        .append("text")
            .attr("x", function(d){ return d.x0})
            .attr("y", function(d){ return d.y0+21})
            .text(function(d){ return d.data.name })
            .attr("font-size", "19px")
            .attr("fill",  function(d){ return color(d.data.name)} )
    
        // Add the chart heading
        svg
        .append("text")
            .attr("x", 0)
            .attr("y", 14)    // +20 to adjust position (lower)
            .text("Three group leaders and 14 employees")
            .attr("font-size", "19px")
            .attr("fill",  "grey" )
    }
// ...

// App.js

// ....
const dataset = {"children":[{"name":"boss1","children":[{"name":"mister_a","group":"A","value":28,"colname":"level3"},{"name":"mister_b","group":"A","value":19,"colname":"level3"},{"name":"mister_c","group":"C","value":18,"colname":"level3"},{"name":"mister_d","group":"C","value":19,"colname":"level3"}],"colname":"level2"},{"name":"boss2","children":[{"name":"mister_e","group":"C","value":14,"colname":"level3"},{"name":"mister_f","group":"A","value":11,"colname":"level3"},{"name":"mister_g","group":"B","value":15,"colname":"level3"},{"name":"mister_h","group":"B","value":16,"colname":"level3"}],"colname":"level2"},{"name":"boss3","children":[{"name":"mister_i","group":"B","value":10,"colname":"level3"},{"name":"mister_j","group":"A","value":13,"colname":"level3"},{"name":"mister_k","group":"A","value":13,"colname":"level3"},{"name":"mister_l","group":"D","value":25,"colname":"level3"},{"name":"mister_m","group":"D","value":16,"colname":"level3"},{"name":"mister_n","group":"D","value":28,"colname":"level3"}],"colname":"level2"}],"name":"CEO"};
// ....
javascript

With the above changes, you can now see the Treemap chart materializing as expected. Note that the sizes of the chart rectangles may vary from the sizes shown in the original example due to the differences in the chart's height and width. But the gist is that the sizes should express the relative value differences of the nodes.

Adding Transitions

While the above code gets the work done, in real-world scenarios you are often required to work with dynamic data. Hence, the Treemap must be adjusted for it. Ideally, the transitions should occur for both of the following scenarios:

  1. Animate rectangle sizes when node values are updated
  2. Animate addition/removal of rectangles when nodes are added/removed (nodes are the employees or group leaders in this example)

Following the transition steps of the previous guide, this can be easily accomplished. The following code modifies the draw() method to add the required animations. Then you can add a few more methods to the App component to do data updates and visually observe the animations.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
    // ...
    const draw = () => {
        const svg = d3.select(ref.current);

        // Give the data to this cluster layout:
        var root = d3.hierarchy(data).sum(function(d){ return d.value});

        // initialize treemap
        d3.treemap()
            .size([width, height])
            .paddingTop(28)
            .paddingRight(7)
            .paddingInner(3)
            (root);
        
        const color = d3.scaleOrdinal()
            .domain(["boss1", "boss2", "boss3", "boss4"])
            .range([ "#402D54", "#D18975", "#8FD175", "#3182bd"]);

        const opacity = d3.scaleLinear()
            .domain([10, 30])
            .range([.5,1]);


        // Select the nodes
        var nodes = svg
                    .selectAll("rect")
                    .data(root.leaves())

        // animate new additions
        nodes
            .transition().duration(300)
                .attr('x', function (d) { return d.x0; })
                .attr('y', function (d) { return d.y0; })
                .attr('width', function (d) { return d.x1 - d.x0; })
                .attr('height', function (d) { return d.y1 - d.y0; })
                .style("opacity", function(d){ return opacity(d.data.value)})
                .style("fill", function(d){ return color(d.parent.data.name)} )
        
        // draw rectangles
        nodes.enter()
            .append("rect")
            .attr('x', function (d) { return d.x0; })
            .attr('y', function (d) { return d.y0; })
            .attr('width', function (d) { return d.x1 - d.x0; })
            .attr('height', function (d) { return d.y1 - d.y0; })
            .style("stroke", "black")
            .style("fill", function(d){ return color(d.parent.data.name)} )
            .style("opacity", function(d){ return opacity(d.data.value)})

        nodes.exit().remove()

        // select node titles
        var nodeText = svg
            .selectAll("text")
            .data(root.leaves())

        // animate new additions
        nodeText
            .transition().duration(300)
                .attr("x", function(d){ return d.x0+5})
                .attr("y", function(d){ return d.y0+20})
                .text(function(d){ return d.data.name.replace('mister_','') })

        // add the text
        nodeText.enter()
            .append("text")
            .attr("x", function(d){ return d.x0+5})
            .attr("y", function(d){ return d.y0+20})
            .text(function(d){ return d.data.name.replace('mister_','') })
            .attr("font-size", "19px")
            .attr("fill", "white")

        nodeText.exit().remove()
        
        //select node values
        var nodeVals = svg
            .selectAll("vals")
            .data(root.leaves())  
        
        nodeVals
            .transition().duration(300)
                .attr("x", function(d){ return d.x0+5})
                .attr("y", function(d){ return d.y0+35})
                .text(function(d){ return d.data.value })

        // add the values
        nodeVals.enter()
            .append("text")
            .attr("x", function(d){ return d.x0+5})    
            .attr("y", function(d){ return d.y0+35}) 
            .text(function(d){ return d.data.value })
            .attr("font-size", "11px")
            .attr("fill", "white")

        nodeVals.exit().remove()
    
        // add the parent node titles
        svg
            .selectAll("titles")
            .data(root.descendants().filter(function(d){return d.depth==1}))
            .enter()
            .append("text")
                .attr("x", function(d){ return d.x0})
                .attr("y", function(d){ return d.y0+21})
                .text(function(d){ return d.data.name })
                .attr("font-size", "19px")
                .attr("fill",  function(d){ return color(d.data.name)} )
    
        // Add the chart heading
        svg
        .append("text")
            .attr("x", 0)
            .attr("y", 14)    
            .text("Three group leaders and 14 employees")
            .attr("font-size", "19px")
            .attr("fill",  "grey" )
    }
    // ...
javascript
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
// App.js
function App() {
    const [data, setData] = useState(null);

    useEffect(() => {
        setData(dataset);
    }, []);

    function deepCopy(obj){
        return JSON.parse(JSON.stringify(obj));
    }

    const updateData1 = () => {
        var _data = deepCopy(data);
        
        _data["children"][0]["children"][0]["value"] = 50;
        _data["children"][0]["children"][1]["value"] = 10;
        _data["children"][0]["children"][2]["value"] = 30;

        _data["children"][1]["children"][0]["value"] = 4;
        _data["children"][1]["children"][1]["value"] = 8;

        setData(_data);
    }

    const updateData2 = () => {
        var _data = deepCopy(data);
        
        _data["children"][0]["children"].push({
            "name":"mister_p",
            "group":"C",
            "value":20,
            "colname":"level3"
        })

        _data["children"][2]["children"].splice(2,1);

        setData(_data);
    }

    const updateData3 = () => {
        var _data = deepCopy(data);

        _data["children"].push({
            "name": "boss4",
            "children": [{
                "name":"mister_z",
                "group":"E",
                "value":40,
                "colname":"level3"
            }]
        });

        setData(_data);
    }

    const updateData4 = () => {
        var _data = deepCopy(data);

        _data["children"].splice(1,1);

        setData(_data);
    }

    const resetData = () => {
        setData(dataset);
    }

    if(data === null) return <></>;

    return (
        <div className="App">
            <h2>Graphs with React</h2>
            <div className="btns">
                <button onClick={updateData1}>Change Child Data Values</button>
                <button onClick={updateData2}>Add/Remove Child Nodes</button>
                <button onClick={updateData3}>Add Parent Nodes</button>
                <button onClick={updateData4}>Remove Parent Nodes</button>
                <button onClick={resetData}>Reset</button>
            </div>
            <Treemap width={600} height={400} data={data} />
        </div>
    );
}
javascript

Conclusion

In a previous guide we explored how we can integrate D3 with React in a way that minimizes rework in adapting future D3 samples. In this guide, we built upon that to implement a Treemap chart with multiple hierarchies. Then we explored how we could animate the data updates. While this provides a sufficient introduction to the Treemap space, I highly suggest that you go through the following samples and try replicating them to React yourself, as an exercise.

0