6
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.
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;
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;
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.
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"}; // ....
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.
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:
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" ) } // ...
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> ); }
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.
6