Using d3's attrTween to Animate Bezier Curves in a Family Tree Visualization

I have been playing with a d3.js-based family tree visualization tool (available at with part of the British monarchy tree loaded; you can also load your own GEDCOM file or view other samples).

When viewing the whole tree, and focusing on a single person in the tree with its path winding its way to the root person, I was wanting to be able to "collapse" the path to a straight path and show all of the people in that path.

I implemented this using d3's attrTween for the relevant svg path attribute of the (cubic) Bezier curves, and thought it seemed pretty cool.

A short video demonstrating this feature is linked to below (video is cued to where the feature is used). A few screen shots of the before and after are also shown.

Using d3 attribute transitions/tweens to
"collapse" a family tree path
(video is cued to where the feature is used)

Before and after the svg path transitions to
"collapse" a family tree path
(visualization is available here)

The implementation uses transitions and attrTween to animate the svg path parameters for the curves used for the links between nodes (these are cubic Bezier curves in this case).

As a point of reference, a screenshot of a typical path and the associated equation is shown below.

${\bf P_0}$ is the center of the circle for the parent, and ${\bf P_3}$ is the center of the circle for the child. The C stuff in there for the "d" attribute contains the cubic B├ęzier commands. The equation for the thicker blue curve is given by
$${\bf{r}}(t) = {(1-t)}^3 {\bf P_0} + 3{(1-t)}^2 t {\bf P_1} + 3{(1-t)} t^2 {\bf P_2} + t^3 {\bf P_3}, \ t \in [0,1]$$

The idea here is to keep the $y$ values for the points the same, while transitioning the $x$ values to the $x$ value for the root person at the bottom of the screen. This will result in the Bezier curve actually being a line. Undoing the transition just means moving the values back to their original values.

The basic idea with the attrTween function is that you create a function that is given a $u$ in $[0,1]$, and it returns the desired value for the path element at that $u$. When $u=0$ you are at the beginning of the transition, and at $u=1$ you are at the end (the length of the transition is handled via the duration function call for the transition itself).

In this case, I am storing a reference to the path for the link between a parent a child in parentAndLink.linkD3, which is a d3-based selector (i.e., defined a la'selector')), points contains the values for the points at the beginning of the transition and desiredPs contains the values for the points at the end of the transition. currentPs is initialized to the points at the beginning of the transition (this will be different depending on if we are collapsing or undoing the collapse - although I just realized that I could probably skip that and simply use $1-u$ in the function call when reversing it). The basic code that then does the transition for the link curves is as follows (transitioning of the nodes/circles is handled via standard d3 transition stuff of the $cx$ and $cy$ attributes of the circles):

                     .duration(durationMainLine_ms )
                     .attrTween('d', function() {

                       return function(u) {

                         currentPs.P0.x = points.P0.x + 
                                                    u * (desiredPs.P0.x - points.P0.x);
                         currentPs.P1.x = points.P1.x + 
                                                    u * (desiredPs.P1.x - points.P1.x);
                         currentPs.P2.x = points.P2.x + 
                                                    u * (desiredPs.P2.x - points.P2.x);
                         currentPs.P3.x = points.P3.x + 
                                                    u * (desiredPs.P3.x - points.P3.x);

                         return getDFromBezierPoints(currentPoints);



Note that getDFromBezierPoints is a simple helper function that creates the string for the path given the four points.

I'm still playing with the durations/delays of the transitions, and there are likely some optimizations that still need to be implemented, but it seems to work ok for moderately sized trees. In terms of its utility within the context of a family tree visualization, it allows a sort of "deep dive" into a specific pedigree thread from a high-level view of the entire tree.

No comments:

Post a Comment

Popular Posts