A brief course on D3
One of the most striking features of the D3 framework is the use of selections, which allows you to add, update and remove elements in one single chained call. It does this by maintaining an inner-join between the data and the render-elements. This is a pretty powerful feature and greatly aids in building visualizations.
Every render element has an __data__ property that contains the bound data. New render elements will not have the __data__ property and will be created. Existing render elements will get updated and render-elements that have __data__ but no corresponding presence in the new data set will be removed. With that context, the call chain below should look more meaningful.
1 2 3 4 5 6 7
This selects all SVG Path elements with a class name of “link” from the parent element: layoutRoot. We enter the selection with a binding to data(), followed by enter(). By entering the selection, we create the inner-join which will automatically know which elements to create and which ones to update. These calls are part of the Selection API. D3 also has APIs for doing animations (Transitions), data processing, computing a varieties of layouts and generating shapes using SVG.
Other powerful features of D3 include:
- Easy mapping of data to Html elements and vice versa
- Flexibility to choose your own render elements (be it html or svg)
- Simple API to configure the layouts and styling
- Animation support with the transition API
Building the Tree
For this blog post, I am going to use sample tree data that looks like so:
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
d3.layout.tree() is the starting point for tree layouts in D3. The call to this function returns an object that contains a bunch of methods to configure the layout and also provides methods to compute the layout.
1 2 3 4 5 6 7 8 9 10 11
You can ignore the call to the size()method, it is just setting the width and height of the tree, passing in as a 2-element array ([w,h]). If you are curious why I have the size.height as my first element, its because I am rotating the tree by 90 degrees. We will see more about his further down the post. The children()function is more interesting, as it tells the tree layout about the child nodes of the data-item. In my example, the contents property represents the children, as seen in the treeData variable above.
The next two methods, nodes() and links()does what you expect. It recurses through the data (by calling children() on each item) and computes the layout info for each data item. The links() method takes the output of nodes() and computes the edges between the different nodes. We are storing this computed layout in the nodes and links variables, each of which is a flat array of elements. Each element in the nodes array is a wrapper around the original data item with augmented layout info, whereas each element in the links array is a wrapper around the node item. Below you can see these wrapper objects:
The node item is augmented with the children, parent, x and y properties. The link item just has the source and target properties, which represents the edge from the parent to the child respectively. You can see that the source and target properties point to the node objects.
Rendering the tree
Now that we have the layout computed, the next step is to setup the UI elements that render the data and layout. For the tree diagram we are going to use SVG, although the same layout can also be achieved using plain Html,CSS with absolute positioning. To create our UI elements, we will use D3’s selection API, as mentioned above, to generate the SVG elements and set properties based on the computed layout.
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
The code above creates the root container (SVG with a Group (<g>) element) and adds the edges first followed by the nodes and text labels. This order ensures that the nodes and labels always stay on top of the edges. You can also see the use of the .data().enter() method chain to setup the inner-join for binding data to UI. The attr() method is used to set properties on the SVG elements. One thing to note here is that we are creating a group (<g>) element to hold both the node (<circle>) and the label (<text>). Also this group element is translated by [y, x], ie. by interchanging the original layout co-ordinates. We do this to rotate the tree by 90 degrees. This rotation makes the labels more readable.
You can take a look at the tree demo here.