Progressive reveal animations in SVG using a <svg:clipPath>

In the previous post on “Building a tree diagram in D3.js” we saw how the D3.js library could be used to render tree diagrams. If you haven’t read that post yet, I would encourage reading it as we will be expanding on it in this post.

image

Now that we have a nice tree diagram of a hierarchy, it would be good to build in some interactions. After all, given a tree, one would certainly like to click on the nodes and see something happening. Lets do that by animating the path from the clicked node all the way to the root. To pull this off, we need three things:

  1. A set of all nodes from the clicked node to the root
  2. A set of all links (paths) from the clicked node back to the root
  3. A revealing animation that progressively draws the paths from the clicked node to the root

The first two parts are easy and we have already seen that in the previous post. d3.layout.tree has a method called nodes() that gives us the collection of the nodes with their (x, y) locations and links() gives all links between the nodes. We will use the nodes collection to traverse the parent chain from the clicked node all the way to the root. Once we have the nodes along the parent chain, we use that to select the links that lie between each of these nodes. With the links between the nodes, the last part is to animate them into view. The animation part is the most interesting and also the topic of this post.

Animating the path to root

Now that we have the links between the nodes, the next step is to simply draw the SVG Path segments connecting these nodes. We will keep these path segments in its own SVG Group for easier manipulation. To animate these paths, there are two different approaches we can take:

  • We can draw the path progressively by setting the SVG Path data in each time interval of the animation OR
  • We can use a clipping path for the SVG Group and grow the bounds of the path from right to left. As the clipping path grows in size, it reveals more of the underlying group (which contains the paths), thus animating the path from the clicked node to root. This technique can even be used for progressively revealing complicated graphics.

I am adopting the later approach for this post. To set this up, we first need to add a node to the . We will also give it an id, so we can reference it later. This clip-path is going to be a since that is all we need. The code below sets up the clipping path for the that contains the paths to root. The variable *animGroup*holds the reference to this group.

 1var svgRoot = d3.select(containerName)
 2    .append("svg:svg").attr("width", size.width).attr("height", size.height);
 3
 4// Add the clipping path
 5svgRoot.append("svg:clipPath").attr("id", "clipper")
 6    .append("svg:rect")
 7    .attr('id', 'clip-rect');
 8
 9var layoutRoot = svgRoot
10    .append("svg:g")
11    .attr("class", "container")
12    .attr("transform", "translate(" + maxLabelLength + ",0)");
13
14// set the clipping path
15var animGroup = layoutRoot.append("svg:g")
16    .attr("clip-path", "url(#clipper)");
Note that we have also set up an “id” for the (#clip-rect), which we will use later for animating the clipping path. The “click” handler To kick off the animation, we will detect a click happening on a node and then draw the path from the clicked node to the root. In the code below we do this using d3.js’ event API: the on(“event”) method.

 1function setupMouseEvents()
 2{
 3    ui.nodeGroup.on('mouseover', function(d, i)
 4    {
 5        d3.select(this).select("circle").classed("hover", true);
 6    })
 7        .on('mouseout', function(d, i)
 8        {
 9            d3.select(this).select("circle").classed("hover", false);
10        })
11        .on('click', function(nd, i)
12        {
13            // Walk parent chain
14            var ancestors = [];
15            var parent = nd;
16            while (!_.isUndefined(parent)) {
17                ancestors.push(parent);
18                parent = parent.parent;
19            }
20
21            // Get the matched links
22            var matchedLinks = [];
23            ui.linkGroup.selectAll('path.link')
24                .filter(function(d, i)
25                {
26                    return _.any(ancestors, function(p)
27                    {
28                        return p === d.target;
29                    });
30                })
31                .each(function(d)
32                {
33                    matchedLinks.push(d);
34                });
35
36            animateParentChain(matchedLinks);
37        });
38}
In the the “click” event handler, we traverse the parent links back to the root and record all the ancestors in the path. We then filter all the rendered links () by picking only the ones that contain the nodes in the ancestors list. This becomes our list of matched links. Using these links we can now finally handle the animation that progressively reveals the emboldened path back to the root.

Back to my Mac root

Since we have all the links that take us from the clicked node to the root, we apply the same method that we used earlier to render the links. Using d3.diagonal(), we can generate the data between the ancestor nodes. Only this time, the path will be rendered with a class set to “selected”, which emboldens the route back to the root. image

 1function animateParentChain(links)
 2{
 3    var linkRenderer = d3.svg.diagonal()
 4        .projection(function(d)
 5        {
 6            return [d.y, d.x];
 7        });
 8
 9    // Links
10    ui.animGroup.selectAll("path.selected")
11        .data([])
12        .exit().remove();
13
14    ui.animGroup
15        .selectAll("path.selected")
16        .data(links)
17        .enter().append("svg:path")
18        .attr("class", "selected")
19        .attr("d", linkRenderer);
20
21    // Animate the clipping path
22    var overlayBox = ui.svgRoot.node().getBBox();
23
24    ui.svgRoot.select("#clip-rect")
25        .attr("x", overlayBox.x + overlayBox.width)
26        .attr("y", overlayBox.y)
27        .attr("width", 0)
28        .attr("height", overlayBox.height)
29        .transition().duration(500)
30        .attr("x", overlayBox.x)
31        .attr("width", overlayBox.width);
32}
33	
Once the paths have been rendered, we set the clipping path starting with a zero-width , which is then animated to the full width of the tree-diagram. It is in this animation of the clipping-path where you will see the path drawing itself out, giving us the final effect we were looking for!

Demo

You can see this code in action here.