DEV Community

Cover image for Treemaps with D3.js
Hajar | هاجر
Hajar | هاجر

Posted on

Treemaps with D3.js

I think the first thing to say about treemaps is that they're just a way to visualize your data in a nice, more structured way. And that the first question to ask is: How should I structure my data in a way that d3.treemap can work with?

Before writing about how to structure data for d3.treemap to use, you should know that there are two different input formats to use when building a treemap with d3.js:

  • .csv files.
  • .json files.

And since I've only worked with the .json format, that's what I am writing about.

So let's fetch do data. (I am using the freeCodeCamp top 100 most sold video games data.)

document.addEventListener('DOMContentLoaded', () =>{
    fetch("https://cdn.freecodecamp.org/testable-projects-fcc/data/tree_map/video-game-sales-data.json")
        .then(res=>res.json())
        .then(res=>{
            drawTreeMap(res);   
        });
  });
const drawTreeMap = (dataset)=>{
    // pass for now
}
Enter fullscreen mode Exit fullscreen mode

Now that we have our json data, let's work on how we should structure our data in a way that d3.treemap can work with. And to do so we should pass our data to d3.hierarchy.

const drawTreeMap = (dataset)=>{
   const hierarchy = d3.hierarchy(dataset);
}
Enter fullscreen mode Exit fullscreen mode

What d3. hierarchy does is take the data and add to it: depth, height, and parent.

  • depth: counts how many parents every node has.
  • height: counts how many levels of children every node has.
  • parent: the parent of the node or null for the root node.

The data we've fetched has a height of 2 because it consists of 18 children (first level of children). And every child of the first level has its own children (second level of children).
And each of the first-level children has a height of 1 and a depth of 1 (they have children and a parent). And each child of the second-level has a depth of 2 and a height of 0 (two higher parents and no children).

We now have a new version of the data but still, it feels something is missing here. I mean, how would d3.treemap know each child's value so that it'd make room for that child depending on that value?

So we need to use sum and sort methods with d3.hierarchy to calculate that value and sort the children according to it.

 const drawTreeMap = (dataset)=>{
    const hierarchy = d3.hierarchy(dataset)
                        .sum(d=>d.value)  //sum every child's values
                        .sort((a,b)=>b.value-a.value) // and sort them in descending order 
}
Enter fullscreen mode Exit fullscreen mode

Now, this new version of the data (which has a total value for each child) is ready to be placed on a treemap.

So let's create a treemap.

const treemap = d3.treemap()
                  .size([400, 450]) // width: 400px, height:450px
                  .padding(1);      // set padding to 1

Enter fullscreen mode Exit fullscreen mode

Finally, we can pass the data to the treemap.

const root = treemap(hierarchy);
Enter fullscreen mode Exit fullscreen mode

treemap now knows the worth of every node and the hierarchy of the data --which node is a parent and which is a child. And with that knowledge it's able to structure the data, it's able to determine the x and y attributes for each node.

If you inspect the root variable now, you'll notice that treemap did you a huge favor and added x0, x1, y0, and y attributes to every node of the data. And with those attributes, you can make rect elements of these nodes and append them to an svg element to see them on your screen.

To make an array of these nodes and to access them we use root.leaves().

const svg = d3.select("svg"); //make sure there's a svg element in your html file.

              svg.selectAll("rect")
                 .data(root.leaves())
                 .enter()
                 .append("rect")
                 .attr("x", d=>d.x0)   
                 .attr("y", d=>d.y0)
                 .attr("width",  d=>d.x1 - d.x0)
                 .attr("height", d=>d.y1 - d.y0)
                 .attr("fill", "#5AB7A9")

Enter fullscreen mode Exit fullscreen mode

Now the treemap should be like this:
A treemap with single color

It looks nice, but specifying a different color to each category would make it more helpful, right? So let's add more colors.

d3.js has a lot of color schemes to choose from but I am choosing different colors.

  const colors = ['#1C1832', '#9E999D', '#F2259C', '#347EB4', 
                  '#08ACB6', '#91BB91', '#BCD32F', '#75EDB8',
                  "#89EE4B", '#AD4FE8', '#D5AB61', '#BC3B3A',
                  '#F6A1F9', '#87ABBB', '#412433', '#56B870', 
                  '#FDAB41', '#64624F']
Enter fullscreen mode Exit fullscreen mode

To use these colors on our nodes we need to scale them first. And to scale something in d3.js, we need to use a scaling function and to provide a domain and range to it.

I think the simplest explanation for the domain and range methods is that the domain is the data we have and that the range is the form we need that data to be shown in.

For example, we here need to use colors to scale the data categories. So our data is the categories and the form we need these categories to be shown in is colors. Every category should be colored with color from colors.
Let's see how this looks in code.

const categories = dataset.children.map(d=>d.name); 
const colorScale = d3.scaleOrdinal() // the scale function
                     .domain(categories) // the data
                     .range(colors)    // the way the data should be shown

Enter fullscreen mode Exit fullscreen mode

So now we should change the fill attribute we used earlier and use it with colorScale instead.

  svg.selectAll("rect")
     .data(root.leaves())
     .enter()
     .append("rect")
     .attr("x", d=>d.x0)
     .attr("y", d=>d.y0)
     .attr("width",  d=>d.x1 - d.x0)
     .attr("height", d=>d.y1 - d.y0)
     .attr("fill", d=>colorScale(d.data.category)) //new
Enter fullscreen mode Exit fullscreen mode

Here's how it should look now:
A treemap with multiple colors

Note: You can add text on the rectangles to make the treemap more informative. I am not adding text here but this stackoverflow answer helped me a lot when I needed to add wrapped text.

Final Code

document.addEventListener('DOMContentLoaded', () =>{
  fetch("https://cdn.freecodecamp.org/testable-projects-fcc/data/tree_map/video-game-sales-data.json")
      .then(res=>res.json())
      .then(res=>{
          drawTreeMap(res);   
      });
});

const drawTreeMap = (dataset)=>{
    const hierarchy = d3.hierarchy(dataset)
                        .sum(d=>d.value)  //sums every child values
                        .sort((a,b)=>b.value-a.value), // and sort them in descending order 

          treemap = d3.treemap()
                      .size([500, 450])
                      .padding(1),

          root = treemap(hierarchy);

    const categories = dataset.children.map(d=>d.name),      

          colors = ['#1C1832', '#9E999D', '#F2259C', '#347EB4', 
                      '#08ACB6', '#91BB91', '#BCD32F', '#75EDB8',
                      "#89EE4B", '#AD4FE8', '#D5AB61', '#BC3B3A',
                      '#F6A1F9', '#87ABBB', '#412433', '#56B870', 
                      '#FDAB41', '#64624F'],

          colorScale = d3.scaleOrdinal() // the scale function
                        .domain(categories) // the data
                        .range(colors);    // the way the data should be shown             

    const svg = d3.select("svg"); //make sure there's a svg element in your html file

              svg.selectAll("rect")
                 .data(root.leaves())
                 .enter()
                 .append("rect")
                 .attr("x", d=>d.x0)
                 .attr("y", d=>d.y0)
                 .attr("width",  d=>d.x1 - d.x0)
                 .attr("height", d=>d.y1 - d.y0)
                 .attr("fill", d=>colorScale(d.data.category));
}                
Enter fullscreen mode Exit fullscreen mode

Top comments (0)