6

I'm trying to use the D3 Tree Layout to create a family tree of sorts and one of the things I noticed is that when I have many children nodes, it would stretch out horizontally across the screen. Ideally I would like a more vertical layout for these nodes so that people don't have to scroll across the screen and can just keep looking down the tree.

Here is what I currently see:

enter image description here

Now it might not be that bad, but if I had say 20 children, it would span across the whole screen and that is something I kind of want to avoid.

I have seen questions like this but this doesn't help me because I want a specific layout and not simply a resize... I have large nodes and they begin to collide with one another if I try to dynamically resize the tree -- shrinking the tree does not do me any good. I specifically need a different layout for situations where there are more than a certain number of children.

Here is kind of what I was envisioning/hoping for. Notice the root does not make this format because it has only 4 children. Ideally I want it so that if a parent has 5 or more children, it would result in the layout below. If the root had 5 children, it would result in this layout and the layout should simply stretch out vertically if users wanted to see the root's grandchildren (the A, B, C... nodes). If necessary I can get a diagram of that going: enter image description here

I found a semi-similar question regarding custom children layouts and he said he had to play around with the actual d3js code. I kind of want to avoid this so I am hoping to find out if this is possible with d3js as it is right now and, if so, how to go about it? I don't need a complete answer, but a snippet of code proving that this is possible would be very helpful.

If necessary I can upload a JSFiddle for people to play around with.

Community
  • 1
  • 1
aug
  • 11,138
  • 9
  • 72
  • 93

2 Answers2

7

Check out this fiddle:

http://jsfiddle.net/dyXzu/

I took the sample code from http://bl.ocks.org/mbostock/4339083 and made some modifications. Note that in the example, x and y are switched when drawing so the layout appears as a vertical tree.

The important thing I did was modifying the depth calculator:

Original:

// Normalize for fixed-depth.
nodes.forEach(function(d) { d.y = d.depth * 180; });

Fixed:

// Normalize for fixed-depth.
nodes.forEach(function (d) {
    d.y = d.depth * 180;
    if (d.parent != null) {
        d.x =  d.parent.x - (d.parent.children.length-1)*30/2
        + (d.parent.children.indexOf(d))*30;
    }
    // if the node has too many children, go in and fix their positions to two columns.
    if (d.children != null && d.children.length > 4) {
        d.children.forEach(function (d, i) {
            d.y = (d.depth * 180 + i % 2 * 100);
            d.x =  d.parent.x - (d.parent.children.length-1)*30/4
            + (d.parent.children.indexOf(d))*30/2 - i % 2 * 15;
        });
    }
});

Basically, I manually calculate the position of each node, overriding d3's default node positioning. Note that now there's no auto-scaling for x. You could probably figure this out manually by first going through and counting open nodes (d.children is not null if they exist, d._children stores the nodes when they are closed), and then adding up the total x.

Nodes with children in the two-column layout look a little funky, but changing the line-drawing method should improve things.

lion
  • 378
  • 1
  • 6
  • Thanks a lot for the example. I actually also realized yesterday about how I could change the x and y's so I'm playing around with that. One thing I am not entirely sure about this method is that even though I am changing the x and y's of the nodes, the tree still "thinks" treats the layout as if it were spread out (this is mainly apparent if you have nodeSize set). I upvoted your answer and will let you know how it turns out after further testing. – aug Aug 09 '13 at 18:23
  • Yeah, I did some tinkering and it seems that changing the x and y manually breaks everything pretty badly. The function I wrote traverses nodes the wrong way actually, it should be nodes.reverse().forEach, but this breaks everything since it assumes only one branch is expanded. If you're not OK with the positioning, you may have to reimplement the Reingold-Tilford algorithm d3 uses with multi-row support, or possibly override the tree.separation method to fool d3 into thinking the doubled rows need spacing half as wide. – lion Aug 09 '13 at 19:31
  • Hmmm I didn't think about changing the separation function (which is totally allowable in d3). I might play around with that too. Thank you so much for the advice. I'll get back to you on that when I can. – aug Aug 09 '13 at 21:26
  • I got the layout to work! Right now I just need to fix the separation but this definitely a terrific answer. Thank you so much :) if I figure out how to fix the separation between the nodes, I'll edit and add it to your answer but for now I'm going to accept this because it definitely has the right idea. – aug Aug 12 '13 at 15:58
  • so I've been playing around with the paths and while I did get the paths to work, the transitions are killing me. The way the current tree layout works is it selects all the links and transitions all the paths. I use a different pathing algorithm for vertical layout but the transitions are making all the paths conform to it. I might make a separate question for this but if you have any advice on this issue, that would be awesome. – aug Aug 12 '13 at 18:18
  • what you could do is alter `var link = svg.selectAll("path.link") .data(links, function (d) { return d.target.id; });` by checking d.source.children.length and returning null if it's over some number. Then make another variable link2 that contains your different kind of link with a different transition, for all the links with source nodes with too many children. – lion Aug 12 '13 at 18:42
  • 2
    Actually it seems that adding null elements into data is a bad idea. I updated the fiddle at http://jsfiddle.net/dyXzu/3/. What I did instead was make two arrays and throw the vertical layout paths into a separate d3 selection. Also added in a height calculator and the separation (which is kind of hard to calculate for multi-row layouts). – lion Aug 12 '13 at 19:59
  • Thank you so much for the example once again. I'll play around with it some more and get back to you. Might later add a bounty and award you karma for all of your help. – aug Aug 12 '13 at 21:31
  • just wanted to get back to you and I tried out what you did in the jsFiddle (finally) and it worked like a charm. Thank you so much. – aug Sep 03 '13 at 22:36
0

You could dynamically count the children and adjust the width/height of the DOM element accordingly. I would recommend counting the maximum number of children in any level, which can be calculated using the depth property provided by d3.tree.nodes(), or just by recursively going down the tree. Modifying some code which was in a previous SO answer, you could have something like this:

    var levelWidth = [1];
    var childCount = function (level, n) {
            if (n.children && n.children.length > 0) {
            if (levelWidth.length <= level + 1) levelWidth.push(0);
                 levelWidth[level + 1] += n.children.length;
            n.children.forEach(function (d) {
                childCount(level + 1, d);
            });
        }
    };
    childCount(0, root);
    var newHeight = d3.max(levelWidth) * 40;  
    tree = tree.size([Math.max(newHeight, 660), w]);
        document.getElementById("#divID").setAttribute("height", Math.max(newHeight, 660) + 50);    

Or using the nodes() method.

var nodes = tree.nodes(root), arr = [];
for(i = 0; i < nodes.length; i++)
    arr[nodes[i].depth]++;
var max = 0;
for(i = 0; i <nodes.length; i++)
    if(arr[i] > max)
        max = arr[i];
$('#elementOfChoice').css("height", max*40);
var newHeight = max * 40;  
tree = tree.size([Math.max(newHeight, 660), w]);
user4815162342
  • 1,638
  • 2
  • 17
  • 23
  • Just from a glance... I'm fairly sure this won't work... D3 draws via SVG and the SVG elements aren't going to change their layout depending on the width or height of the DOM elements that are surrounding them. They are enclosed within an SVG element and if I shrink the SVG element, the drawing will simply get cut off. I need a solution that actually changes the layout and placement of the nodes. – aug Aug 07 '13 at 17:40
  • In the code, both the DOM element and `tree` are adjusted. `tree` is a `d3.layout.tree()`, and `tree.size` is properly adjusted. – user4815162342 Aug 07 '13 at 17:46
  • Hmm I think I see what you are saying. Alright I'll give it a whirl and see what I can come up with. I appreciate you taking the time to help me with my question btw :) – aug Aug 07 '13 at 17:55
  • So I played around with it and unfortunately this does not help my situation. Like I said, I want to change the _layout_ of the nodes. I don't simply want to shrink it and make it fit. The tree does get smaller and fit a little nicer, but I specifically want a different layout for situations where I have much larger nodes and I want them to be spaced out (in these situations you can only shrink the tree so much before they begin to collide with one another)... I have updated my question. – aug Aug 07 '13 at 21:41