dcsimg
October 17, 2019
Hot Topics:

Creating a Tree Diagram with D3.js

  • April 12, 2019
  • By Robert Gravelle
  • Send Email »
  • More Articles »

D3—short for Data Driven Documents—is an incredibly successful JavaScript-powered data visualization library that supports a myriad of features including animations, DOM events, analysis, calculations, and more. It has a variety of objects to represent just about any data structure you can imagine. These include the usual suspects, such as line and bar charts, as well as more esoteric data representations like scatterplots, dendrograms, hexbin maps, and the ever popular sankey. In today's article, we'll be using D3.js to show a data set using a tree layout.

The Tree Layout Explained

The "Tree layout" is not a distinct type of diagram per se. Rather, it's one type of D3's family of hierarchical layouts. Other layout types include cluster and treemap. The tree layout produces a "node-link" diagram that lays out the connections among nodes in a way that displays the relationship of one node to another in a parent-child fashion.

A sample tree layout diagram
Figure 1: A sample tree layout diagram

To achieve that end, your data has to be structured in a certain way so that children are arrays of the parent object. Here's the basic data set for our demo:

const treeData = {
   "name": "Eve",
   "children": [
      {
         "name": "Cain"
      },
      {
         "name": "Seth",
         "children": [
            {
               "name": "Enos"
            },
            {
               "name": "Noam"
            }
         ]
      },
      {
         "name": "Abel"
      },
      {
         "name": "Awan",
         "children": [
            {
               "name": "Enoch"
            }
         ]
      },
      {
        "name": "Azura"
      }
   ]
};

Although not strictly required, we can assign additional attributes to each node to determine its color, size, shape, as well as those of connecting lines:

const treeData = {
   "name": "Eve",
   "value": 15,
   "type": "black",
   "level": "yellow",
   "children": [
      {
         "name": "Cain",
         "value": 10,
         "type": "grey",
         "level": "red"
      },
      {
         "name": "Seth",
         "value": 10,
         "type": "grey",
         "level": "red",
         "children": [
            {
               "name": "Enos",
               "value": 7.5,
               "type": "grey",
               "level": "purple"
            },
            {
               "name": "Noam",
               "value": 7.5,
               "type": "grey",
               "level": "purple"
            }
         ]
      },
      {
         "name": "Abel",
         "value": 10,
         "type": "grey",
         "level": "blue"
      },
      {
         "name": "Awan",
         "value": 10,
         "type": "grey",
         "level": "green",
         "children": [
            {
               "name": "Enoch",
               "value": 7.5,
               "type": "grey",
               "level": "orange"
            }
         ]
      },
      {
         "name": "Azura",
         "value": 10,
         "type": "grey",
         "level": "green"
      }
   ]
};

We can then call the d3.hierarchy() method to assign the data to a hierarchy using parent-child relationships, and then map the node data to the tree layout from there:

// Declares a tree layout and assigns the size
const treemap = d3.tree().size([height, width]);
let nodes = d3.hierarchy(treeData, d => d.children);
nodes = treemap(nodes);

Adding the Nodes

It's a good idea to add each node as a group so that they can later be referenced as such. The following code also assigns some classes to each node—one for the root and another for child nodes:

const node = g.selectAll(".node")
      .data(nodes.descendants())
   .enter().append("g")
      .attr("class", d => "node" + (d.children ? " node--internal"
         : " node--leaf"))
      .attr("transform", d => "translate(" + d.y + "," +
         d.x + ")");

Linking the Nodes

Here's the code that links parent and child nodes together. The last function modifies the d attribute, which defines a path to be drawn, so that it is curved, rather than a straight line:

const link = g.selectAll(".link")
      .data(nodes.descendants().slice(1))
      .enter().append("path")
      .attr("class", "link")
      .style("stroke", d => d.data.level)
      .attr("d", d => {
          return "M" + d.y + "," + d.x
             + "C" + (d.y + d.parent.y) / 2 + "," + d.x
             + " " + (d.y + d.parent.y) / 2 + "," + d.parent.x
             + " " + d.parent.y + "," + d.parent.x;
      });

Adding the Circle to Each Node

As mentioned earlier, we can use various data attributes to determine the appearance and behavior of each node.

node.append("circle")
   .attr("r", d => d.data.value)
   .style("stroke", d => d.data.type)
   .style("fill", d => d.data.level);

Adding Node Labels

Each data point's value is displayed as a node label. Special care must be taken to position the label so that it clears the connecting links.

node.append("text")
   .attr("dy", ".35em")
   .attr("x", d => d.children ? (d.data.value + 5) * -1 :
      d.data.value + 5)
   .attr("y", d => d.children && d.depth !== 0 ?
      -(d.data.value + 5) : d)
   .style("text-anchor", d => d.children ? "end" : "start")
   .text(d => d.data.name);

Conclusion

Here is the final diagram in all its colorful glory:

The final diagram
Figure 2: The final diagram

All the source code is up on Codepen for your viewing pleasure. Enjoy!







Enterprise Development Update

Don't miss an article. Subscribe to our newsletter below.


Thanks for your registration, follow us on our social networks to keep up-to-date