How to make a dendrogram using the D3 library (part 2)

This is the second of a series of articles which illustrates how to develop a dendrogram using the JavaScript D3 library. These dendrograms will be built according to a particular data structure defined in a JSON file.

See  How to make a dendrogram using the D3 library (part 1)

In this article, first we will develop the JavaScript code that will display the tree structure as shown in Figure 1.

dendrogram_es01
Fig.1: tree structure

As I mentioned in the previous article, when a dendrogram does not show the ultrametric distances, it is actually a tree structure.

The following listing is the HTML page displaying the dendrogram. Copy and save it as dendrogram01.html.

<!doctype html></html>
<meta charset="utf-8" />
<style>
.node circle {     
  fill: #fff;    
  stroke: steelblue;    
  stroke-width: 1.5px; 
} 
.node {    
  font: 20px sans-serif; 
} 
.link {    
  fill: none;    
  stroke: #ccc;    
  stroke-width: 1.5px; 
}
</style> 
<script type="text/javascript" src="http://d3js.org/d3.v3.min.js">/script>
<script type="text/javascript"> 
var width = 600; 
var height = 500; 
var cluster = d3.layout.cluster()    
   .size([height, width-200]); 
var diagonal = d3.svg.diagonal()    
   .projection (function(d) { return [d.y, d.x];}); 
var svg = d3.select("body").append("svg")    
   .attr("width",width)    
   .attr("height",height)    
   .append("g")    
   .attr("transform","translate(100,0)"); 
d3.json("dendrogram01.json", function(error, root){    
   var nodes = cluster.nodes(root);    
   var links = cluster.links(nodes);    
   var link = svg.selectAll(".link")       
      .data(links)       
      .enter().append("path")       
      .attr("class","link")       
      .attr("d", diagonal);     
   var node = svg.selectAll(".node")       
      .data(nodes)       
      .enter().append("g")       
      .attr("class","node")       
      .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });    
   node.append("circle")       
      .attr("r", 4.5);    
   node.append("text")       
      .attr("dx", function(d) { return d.children ? -8 : 8; })       
      .attr("dy", 3)       
      .style("text-anchor", function(d) { return d.children ? "end" : "start"; })      
      .text( function(d){ return d.name;}); 
}); 
</script>

If you load this HTML page on your browser, you obtain the dendrogram as shown in Fig.1. Warning: the dendrogram01.json file must reside in the same directory of the HTML page, otherwise you need to modify the path within the d3.json() function.

Now let’s start analyzing the JavaScript code within the HTML page.

var width = 600;
var height = 500;

First we need to define the size of the drawing area within the web page. For example, we can define a 600×500 (pixels) area totally dedicated to the visualization of our dendrogram.

var cluster = d3.layout.cluster()
  .size([height, width-200]);
var diagonal = d3.svg.diagonal()
  .projection (function(d) { return [d.y, d.x];});

Moreover, deciding to work with a tree structure, accordingly we need to manage hierarchically structured data. The D3 library provides us with a set of tools in order to more easily manage this type of structures (layouts). As regards dendrograms, we are going to use the cluster layout (d3.layout.cluster).

The cluster layout is an object internally tree-structured set of node objects, each of which is characterized by the following attributes:

  • parent : the parent node, the root node has null as value.
  • children : it is an array containing the children nodes, the leaves nodes have null as value.
  • depth : the level of the node in the tree-structure.
  • x : the x coordinate of the node.
  • y : the y coordinate of the node.
cluster_node
Fig.2: node object

Thus, let’s define the cluster layout, assigning a portion of the drawing area (500×400 pixels) to it.

Another class of objects, useful for representing our tree structure, are the diagonal objects. These elements serve specifically to draw the links which connect the various nodes to each other.

The diagonal objects (d3.svg.diagonal) are graphic elements which draw a cubic Bezier curves between two specific points. This type of object is widely used with the D3 library especially when we need to draw a connection between two different elements in the drawing area.

The Bezier curves are built by specifying a particular set of parameters ( in SVG they are expressed by the paths), and they are widely used in computer graphics. Depending on the number of parameters, you will get gradually more complex curves.

The following figures (from WikiPedia) show, respectively, the geometric construction of the quadratic Bezier curves( 1 point defined in addition to the origin and destination), cubic Bezier curves( 2 points defined in addition to the origin and destination) and quartic Bezier curves(3 points defined in addition to the origin and destination).

240px-Bezier_2_big
Fig. 3: quadratic Bezier curve
240px-Bezier_3_big
Fig.4: cubic Bezier curve
240px-Bezier_4_big
Fig.5: quartic Bezier curve

Once we have defined this kind of curves, we assign the x and y values of each node (d.x and d.y) to them using the projection() method. Now we can define the code for set up the <svg> root tag.

var svg = d3.select("body").append("svg")
    .attr("width",width)
    .attr("height",height)
    .append("g")
    .attr("transform","translate(100,0)");

Now it is time to read and parse the data contained in the dendrogram01.json file (this file has been discussed in the previous article). The anonymous iterative function starts reading from the root element of the data structure. In order to do that, we have to pass this root element to the cluster.nodes() function. This function, belonging to the cluster layout, returns an array of node objects. The returned value is assigne to the nodes variable. Subsequently, we use this variable as argument of the cluster.links() function. Also this function belongs to the cluster layout, and it calculates the links from the tree-structure passed as argument. The returned value is an array of links objects, that we assign to the links variable.

  d3.json("dendrogram01.json", function(error, root){

    var nodes = cluster.nodes(root);
    var links = cluster.links(nodes);

With this two lines of code we have built up all the data structure that lies beneath a dendrogram ( this is very handy).

Now let’s start to draw the graphic elements. We can start with drawing the links of the dendrogram. The following listing draws a SVG path (that is a Bezier curve) for each link stored in the links array (previously generated during the reading of the JSON file).

    var link = svg.selectAll(".link")
      .data(links)
      .enter().append("path")
      .attr("class","link")
      .attr("d", diagonal);

Now let’s draw the nodes. Each node will be represented with a small circle (SVG circle) with radius of 4.5 pixels. The circle will be drawn at (x,y) position, that are the x and y values defined within the node object. In this example (a tree structure) the x and y values are automatically calculated by the cluster.nodes() function.

    var node = svg.selectAll(".node")
      .data(nodes)
      .enter().append("g")
      .attr("class","node")
      .attr("transform", function(d) { return "translate(" + d.y + "," + d.x + ")"; });

    node.append("circle")
      .attr("r", 4.5);

Then we need to add the name to each node. These names are defined in the JSON file as well.

    node.append("text")
      .attr("dx", function(d) { return d.children ? -8 : 8; })
      .attr("dy", 3)
      .style("text-anchor", function(d) { return d.children ? "end" : "start"; })
      .text( function(d){ return d.name;});

And with this, we have finished writing the code for this first example.

In the next article we will develop a real dendrogram, in which the distances between nodes are quantitatively evaluated.

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.