# CS465 - Lab 8 Tree layout

# Objectives

  • Learn about D3 hierarchies
  • Learn how to draw a tree

# Starting the lab

Go to Github Classroom and grab the starter code for the lab.

# Building a tree

In this lab. we will explore some of the tools provided by D3 to help us lay out our visualization. Specifically we will be looking at making trees.

# D3 Hierarchies

d3.hierarchy is a tool to help us rearrange our data, much like the nest tool that you used earlier. The assumption is that we have hierarchical data stored in a JavaScript object that looks like:

const sample_data = {
        name:"/",
        children:[
            {
                name: 'A',
                children: [
                {
                    name: 'D',
                    size: 5
                },
                {
                    name: 'E',
                    size: 15
                }]
            },
            {
                name: 'B',
                children: [
                {
                    name: 'F',
                    size: 20
                },
                ]
            },
            {
                name: 'C',
                size: 10
            },

        ]
    };

Note that in this hierarchy, descendants are represented by a property called children that contains an array of other nodes (if we don't want to call this property children, we have an accessor function that can be used to return a different array, but by default, D3 will look for children). The name and size properties here are just arbitrary data associated with our nodes.

As per the documentation, after we run our data through d3.hierarchy(), our root object and each of its descendants will be transformed into new objects with the following fields (from the documentation, edited for clarity):

  • node.data - this is the original data of the node
  • node.depth - zero for the root node, and increasing by one for each descendant generation.
  • node.height - zero for leaf nodes, and the greatest distance from any descendant leaf for internal nodes.
  • node.parent - the parent node, or null for the root node.
  • node.children - an array of child nodes, if any; undefined for leaf nodes.
  • node.value - an optional property that is created by sum() or count()

The value property allows us to weight the nodes. Running the count() function sets value to the number of leaves under this node (1 if the node is a leaf). The sum function is more complex. We pass in a value function, which is used to compute the value of an arbitrary node using its data property. However, since this function sums, so the value of any parent node is the value returned by the value function, plus the combined values of all of its children. While we don't need to have values for our nodes in general, some hierarchy visualizations (e.g. treemaps) require them.

You should also take a moment to look at the other hierarchy functions. There are some helpful functions there for doing tree traversals in various orders.

Let's take a look at what this does with our data. Our data, incidentally, is the file hierarchy for one of my python projects. Because it is not tabular, it is packaged as a JSON file. Note that d3.json works just like d3.csv.

Step one Call d3.hierarchy on the data. This should just look like const root = d3.hierarchy(data);.

Step two Examine the data structures. Use console.log to examine both data and root.

# Laying out the nodes

Once the data is in the hierarchical form, D3 offers us a collection of different ways to lay it out. We are going to start with the tree layout, which provides a nice basic tree.

An important facet of most of the D3 layout tools is that they aren't as straightforward as "here is a data, draw me a tree". Primarily, the layout functions handle the algorithmic side and then get out of our way when it comes to the actual drawing.

With the tree layout, we will provide the layout tool with a size, and after we run our hierarchy through the tree() function, each node of our hierarchy will be updated with x and y values. If the size we provided corresponds to the size of our display area, we could just use the x and y values to place the nodes. However, we don't need to interpret the values that literally. As you will see in the documentation, you could get a radial layout if you interpret the x and y as angle and radius. You can re-scale, flip the graph on its side, etc.

Using the tree function is similar to other D3 tools like scales; we start by calling d3.tree() to create the layout function, and then we can configure it before ultimately calling it on our data.

Step one Create the tree function. Call d3.tree() to create the layout function, and then configure it with the size() function, which takes in a length two array of the dimensions. Pass this [WIDTH, HEIGHT]. It should look much like you were creating a scale function. Save this in a variable called tree.

Step two Call the new tree() function on the root. This alters root, so you don't need to assign the return value to a new variable.

Step three Time to draw! I've already given you a selection for the SVG region. Use this add a circle for every node on the tree. Of course, our data is all stored in a hierarchical structure, and we need an array for our data binding step. Use .data(root.descendants()). This function flattens the tree into an array. Each node now has an x and y -- use these to set the cx and cy attributes. Just worry about enter(), we won't be editing the tree.

Step four Test. The result is a bit disappointing. It is just a bunch of circles. Admittedly, that is all we asked it to draw -- we'll return to this in a minute. While it is not much to look at, you can see a rough structure of a tree in the pattern of the dots.

Step five Flip the tree. The tree would be more interesting if we could see the filenames. Aesthetically, it will be better if the tree flows left to right, so the labels don't overlap. We can flip the tree simply by reversing the WIDTH and HEIGHT in the size() function and then swapping the x and y values when we draw. Try this.

Step six Add text. Duplicate what you did to add text. I think the text looks best if it is to the right of the node for leaves and on the left for parents. You can do this by looking to see if d.height is equal to 0 (if it is, it is a leaf), and then use this to set the text-anchor to either "start" or "end" depending on the answer. I also add or subtract a little from the x value accordingly to move the text a little bit away from the circle. You don't have to be as fussy as this, but do try to make sure the text doesn't write on top of the circle or other labels. I also recommend a little styling to make the text small (style font-size).

Step seven Test, test, test.

# Edges

There are a couple of ways we could draw edges, but D3 has a useful shape tool called Link, which will do nicely for us.

We again need to follow the same pattern of a) get a generator, b) configure it, and then c) call it.

The link generator works like the line tool. We pass it some data and it returns instructions appropriate for the d attribute of a path object. However, it expects data in the form of:

{
    source: node1,
    target: node2
}

Fortunately for us, the hierarchy object has a links() function that returns an array of just these objects for all of the edges in the tree.

The link generator needs to know how to extract the x and y position data from a node. So, again like the line generator, we will provide accessor functions.

Do these following steps before the code that draws the nodes and labels so that the lines will be on the bottom.

Step one Make a link generator. We want a d3.linkHorizontal(). We will configure it as suggested in the documentation.

var link = d3.linkHorizontal()
    .x(function(d) { return d.y; })
    .y(function(d) { return d.x; });

Note the flip of x and y again.

Step two Draw the paths. Do the select/data/enter/append dance again with path objects. For the data, pass in root.links(). For the d attribute of the path, call the link generator on your data object like we did for the lines in the line chart.

Step three Test and style.

# [Optional]: Experiment with other layouts

One simple experiment is to swap out d3.tree for d3.cluster. This is just a different layout that puts all of the leaves on the same level.

A more advanced experiment would be to build a treemap. If you want to play with this, I recommend a second file.

Step one The treemap starts in the same way: by building a hierarchy.

Step two The treemap needs values, so you need to call sum or count on the hierarchy. This line will compute the value based on the file size: root.sum((d)=> d.children ? 0 : d.size);. The conditional in there makes sure that directories have no inherent size of their own.

Step three Build the treemap tool. This works just like the tree tool. Set the size of the final treemap at the very least. There are other good configuration options like padding and round as well.

Step four Call the function you created on root. Just like the tree function, it operates directly on your hierarchical data. This time it adds four properties: x0, x1, y0, and y1, which provide the boundaries of the rectangles.

Step five Add some rects to the SVG. Pass roots.descendants into the data() function and use the x0, x1, y0, and y1to set the x,y, width and height attributes.

Step six Add some color. There are lots of ways to do this. Here are two.

You could color by file type. The color_scale is just a categorical color scale. This just takes a fairly loose approach to file typing -- it just extracts the last three letters. Note that directories are just white to provide padding.

 .style("fill", (d) => d.children? "white" : color_scale(d.data.name.slice(d.data.name.length - 3)));`

Another more radical approach is to color everything based on the first level directory they belong to. This can be done with a pre-order traversal that picks unique colors for nodes with a depth of 1 and then propagates these down to the children:

 root.eachBefore((d)=>{
    if (! d.parent){
        //root, skip
        return;
    }else if (! d.parent.parent){
        // first level directories
        d.color = color_scale(d.data.name);
    } else{
        // propagating colors down
        d.color = d.parent.color;
    }

});
Last Updated: 11/16/2018, 11:46:12 AM