Author avatar

Manujith Pallewatte

D3 Treemap in React

Manujith Pallewatte

  • May 19, 2020
  • 15 Min read
  • 5,919 Views
  • May 19, 2020
  • 15 Min read
  • 5,919 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// Treemap.js
2import * as d3 from 'd3';
3import React, { useRef, useEffect } from 'react';
4
5function Treemap({ width, height, data }){
6    const ref = useRef();
7
8    useEffect(() => {
9        const svg = d3.select(ref.current)
10            .attr("width", width)
11            .attr("height", height)
12            .style("border", "1px solid black")
13    }, []);
14
15    useEffect(() => {
16        draw();
17    }, [data]);
18
19    const draw = () => {
20        
21    }
22
23
24    return (
25        <div className="chart">
26            <svg ref={ref}>
27            </svg>
28        </div>
29        
30    )
31
32}
33
34export default Treemap;
javascript
1// App.js
2import React from 'react';
3import './App.css';
4import Treemap from './Treemap';
5
6const dataset = [];
7
8function App() {
9    return (
10        <div className="App">
11            <h2>Graphs with React</h2>
12            <Treemap width={600} height={400} data={dataset} />
13        </div>
14    );
15}
16
17export 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// Treemap.js
2
3// ...
4    const draw = () => {
5        const svg = d3.select(ref.current);
6
7        // Give the data to this cluster layout:
8        var root = d3.hierarchy(data).sum(function(d){ return d.value});
9
10        // initialize treemap
11        d3.treemap()
12            .size([width, height])
13            .paddingTop(28)
14            .paddingRight(7)
15            .paddingInner(3)
16            (root);
17        
18        const color = d3.scaleOrdinal()
19            .domain(["boss1", "boss2", "boss3"])
20            .range([ "#402D54", "#D18975", "#8FD175"]);
21
22        const opacity = d3.scaleLinear()
23            .domain([10, 30])
24            .range([.5,1]);
25
26
27        // Select the nodes
28        var nodes = svg
29                    .selectAll("rect")
30                    .data(root.leaves())
31
32        // draw rectangles
33        nodes.enter()
34            .append("rect")
35            .attr('x', function (d) { return d.x0; })
36            .attr('y', function (d) { return d.y0; })
37            .attr('width', function (d) { return d.x1 - d.x0; })
38            .attr('height', function (d) { return d.y1 - d.y0; })
39            .style("stroke", "black")
40            .style("fill", function(d){ return color(d.parent.data.name)} )
41            .style("opacity", function(d){ return opacity(d.data.value)})
42
43        nodes.exit().remove()
44
45        // select node titles
46        var nodeText = svg
47            .selectAll("text")
48            .data(root.leaves())
49
50        // add the text
51        nodeText.enter()
52            .append("text")
53            .attr("x", function(d){ return d.x0+5})    // +10 to adjust position (more right)
54            .attr("y", function(d){ return d.y0+20})    // +20 to adjust position (lower)
55            .text(function(d){ return d.data.name.replace('mister_','') })
56            .attr("font-size", "19px")
57            .attr("fill", "white")
58        
59        // select node titles
60        var nodeVals = svg
61            .selectAll("vals")
62            .data(root.leaves())  
63
64        // add the values
65        nodeVals.enter()
66            .append("text")
67            .attr("x", function(d){ return d.x0+5})    // +10 to adjust position (more right)
68            .attr("y", function(d){ return d.y0+35})    // +20 to adjust position (lower)
69            .text(function(d){ return d.data.value })
70            .attr("font-size", "11px")
71            .attr("fill", "white")
72    
73        // add the parent node titles
74        svg
75        .selectAll("titles")
76        .data(root.descendants().filter(function(d){return d.depth==1}))
77        .enter()
78        .append("text")
79            .attr("x", function(d){ return d.x0})
80            .attr("y", function(d){ return d.y0+21})
81            .text(function(d){ return d.data.name })
82            .attr("font-size", "19px")
83            .attr("fill",  function(d){ return color(d.data.name)} )
84    
85        // Add the chart heading
86        svg
87        .append("text")
88            .attr("x", 0)
89            .attr("y", 14)    // +20 to adjust position (lower)
90            .text("Three group leaders and 14 employees")
91            .attr("font-size", "19px")
92            .attr("fill",  "grey" )
93    }
94// ...
95
96// App.js
97
98// ....
99const 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"};
100// ....
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    const draw = () => {
3        const svg = d3.select(ref.current);
4
5        // Give the data to this cluster layout:
6        var root = d3.hierarchy(data).sum(function(d){ return d.value});
7
8        // initialize treemap
9        d3.treemap()
10            .size([width, height])
11            .paddingTop(28)
12            .paddingRight(7)
13            .paddingInner(3)
14            (root);
15        
16        const color = d3.scaleOrdinal()
17            .domain(["boss1", "boss2", "boss3", "boss4"])
18            .range([ "#402D54", "#D18975", "#8FD175", "#3182bd"]);
19
20        const opacity = d3.scaleLinear()
21            .domain([10, 30])
22            .range([.5,1]);
23
24
25        // Select the nodes
26        var nodes = svg
27                    .selectAll("rect")
28                    .data(root.leaves())
29
30        // animate new additions
31        nodes
32            .transition().duration(300)
33                .attr('x', function (d) { return d.x0; })
34                .attr('y', function (d) { return d.y0; })
35                .attr('width', function (d) { return d.x1 - d.x0; })
36                .attr('height', function (d) { return d.y1 - d.y0; })
37                .style("opacity", function(d){ return opacity(d.data.value)})
38                .style("fill", function(d){ return color(d.parent.data.name)} )
39        
40        // draw rectangles
41        nodes.enter()
42            .append("rect")
43            .attr('x', function (d) { return d.x0; })
44            .attr('y', function (d) { return d.y0; })
45            .attr('width', function (d) { return d.x1 - d.x0; })
46            .attr('height', function (d) { return d.y1 - d.y0; })
47            .style("stroke", "black")
48            .style("fill", function(d){ return color(d.parent.data.name)} )
49            .style("opacity", function(d){ return opacity(d.data.value)})
50
51        nodes.exit().remove()
52
53        // select node titles
54        var nodeText = svg
55            .selectAll("text")
56            .data(root.leaves())
57
58        // animate new additions
59        nodeText
60            .transition().duration(300)
61                .attr("x", function(d){ return d.x0+5})
62                .attr("y", function(d){ return d.y0+20})
63                .text(function(d){ return d.data.name.replace('mister_','') })
64
65        // add the text
66        nodeText.enter()
67            .append("text")
68            .attr("x", function(d){ return d.x0+5})
69            .attr("y", function(d){ return d.y0+20})
70            .text(function(d){ return d.data.name.replace('mister_','') })
71            .attr("font-size", "19px")
72            .attr("fill", "white")
73
74        nodeText.exit().remove()
75        
76        //select node values
77        var nodeVals = svg
78            .selectAll("vals")
79            .data(root.leaves())  
80        
81        nodeVals
82            .transition().duration(300)
83                .attr("x", function(d){ return d.x0+5})
84                .attr("y", function(d){ return d.y0+35})
85                .text(function(d){ return d.data.value })
86
87        // add the values
88        nodeVals.enter()
89            .append("text")
90            .attr("x", function(d){ return d.x0+5})    
91            .attr("y", function(d){ return d.y0+35}) 
92            .text(function(d){ return d.data.value })
93            .attr("font-size", "11px")
94            .attr("fill", "white")
95
96        nodeVals.exit().remove()
97    
98        // add the parent node titles
99        svg
100            .selectAll("titles")
101            .data(root.descendants().filter(function(d){return d.depth==1}))
102            .enter()
103            .append("text")
104                .attr("x", function(d){ return d.x0})
105                .attr("y", function(d){ return d.y0+21})
106                .text(function(d){ return d.data.name })
107                .attr("font-size", "19px")
108                .attr("fill",  function(d){ return color(d.data.name)} )
109    
110        // Add the chart heading
111        svg
112        .append("text")
113            .attr("x", 0)
114            .attr("y", 14)    
115            .text("Three group leaders and 14 employees")
116            .attr("font-size", "19px")
117            .attr("fill",  "grey" )
118    }
119    // ...
javascript
1// App.js
2function App() {
3    const [data, setData] = useState(null);
4
5    useEffect(() => {
6        setData(dataset);
7    }, []);
8
9    function deepCopy(obj){
10        return JSON.parse(JSON.stringify(obj));
11    }
12
13    const updateData1 = () => {
14        var _data = deepCopy(data);
15        
16        _data["children"][0]["children"][0]["value"] = 50;
17        _data["children"][0]["children"][1]["value"] = 10;
18        _data["children"][0]["children"][2]["value"] = 30;
19
20        _data["children"][1]["children"][0]["value"] = 4;
21        _data["children"][1]["children"][1]["value"] = 8;
22
23        setData(_data);
24    }
25
26    const updateData2 = () => {
27        var _data = deepCopy(data);
28        
29        _data["children"][0]["children"].push({
30            "name":"mister_p",
31            "group":"C",
32            "value":20,
33            "colname":"level3"
34        })
35
36        _data["children"][2]["children"].splice(2,1);
37
38        setData(_data);
39    }
40
41    const updateData3 = () => {
42        var _data = deepCopy(data);
43
44        _data["children"].push({
45            "name": "boss4",
46            "children": [{
47                "name":"mister_z",
48                "group":"E",
49                "value":40,
50                "colname":"level3"
51            }]
52        });
53
54        setData(_data);
55    }
56
57    const updateData4 = () => {
58        var _data = deepCopy(data);
59
60        _data["children"].splice(1,1);
61
62        setData(_data);
63    }
64
65    const resetData = () => {
66        setData(dataset);
67    }
68
69    if(data === null) return <></>;
70
71    return (
72        <div className="App">
73            <h2>Graphs with React</h2>
74            <div className="btns">
75                <button onClick={updateData1}>Change Child Data Values</button>
76                <button onClick={updateData2}>Add/Remove Child Nodes</button>
77                <button onClick={updateData3}>Add Parent Nodes</button>
78                <button onClick={updateData4}>Remove Parent Nodes</button>
79                <button onClick={resetData}>Reset</button>
80            </div>
81            <Treemap width={600} height={400} data={data} />
82        </div>
83    );
84}
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.