user1987289
user1987289

Reputation: 71

How do I write recursive d3.js code to deal with nested data structures?

I have a background in functional programming and understand recursion in principle, but I can't seem to translate this knowledge into the D3.js environment.

I have a hello world script below which attempts to simply print the contents of a nested data structure. Following advice on other threads, I can use .filter to return just the nodes, but how do I continue this example to recursively print the nested items?

<!DOCYPE html>
<html>
    <head>
        <meta charset="utf-8">
        <script src="d3.v3.js"></script>

        <script>
            function draw(data)
            {
                "use strict";

                d3.select("body")
                    .selectAll("p")
                    .data(data)
                    .enter()
                    .append("p")
                    .text(function(d) {
                            if (d instanceof Array) {
                                return "WHAT DO I PUT HERE?";
                            }
                            else {
                                return d;
                            };
                        });
            }
        </script>
    </head>

    <body>
        Hello world

        <script>
            draw([1, [2, [1, 2, 3, 4] ], 3, 4, 5]);
        </script>
    </body>
</html>

Upvotes: 7

Views: 5538

Answers (2)

chad
chad

Reputation: 1369

You need a root feature and then a recursive function that fills it.

function makeNestedListItems (parentLists) {
    var item = parentLists.append('li')
        .text(function (d) { return d.txt; });
    var children = parentLists.selectAll('ul')
        .data(function (d) {
            return d.children
        })
      .enter().append('ul');
    if (!children.empty()) {
        makeNestedListItems(children);
    }
}
var data = {
    txt: 'root',
    children: [{
            txt: "a",
            children: [{
                    txt: "aa",
                    children: [{
                            txt: "aaa",
                            children: []
                        }, {
                            txt: "aab",
                            children: []
                        }
                    ]
                }, {
                    txt: "ab",
                    children: []
                }
            ]
        }, {
            txt: "b",
            children: [{
                    txt: "ba",
                    children: []
                }, {
                    txt: "bb",
                    children: []
                }, {
                    txt: "bc",
                    children: []
                }
            ]
        }, {
            txt: "c",
            children: []
        }
    ]
};
var rootList = d3.select('body').selectAll('ul').data([data])
        .enter().append('ul');
makeNestedListItems(rootList);

Which should produce

  • root
    • a
      • aa
        • aaa
        • aab
      • ab
    • b
      • ba
      • bb
      • bc
    • c

Upvotes: 7

Superboggly
Superboggly

Reputation: 5834

The easy way to do this is to avoid recursion! The typical D3.js approach is to recurse your data and determine the information you need for layout (for example, total size of children, total depth of nesting, depth of each node) and then flatten the structure and use the computed values for layout.

An excellent example of this can be found in this tree example where the calculation and flattening are taken care of with the built-in function:

var tree = d3.layout.tree()...

That said, if you would really like to try to wrap your head around the kind of selection gymnastics required to do the recursion directly in the layout you can. The key is that you have to make selections and then set their data based on the parent's data.

In the example below I hardcoded maxLevels for convenience, but you could calculate it from your data before entering the loop.

Also note that I was exceedingly lazy about layout, because to do it properly you need a recursive pass on your data first to compute at least how many children each element has before you start. You can play with the fiddle here.

var data = { children: [{
        txt: "a", children: [{
            txt: "aa", children: [{
                txt: "aaa"}, {
                txt: "aab"}]}, {
            txt: "ab"}]}, {
        txt: "b", children: [{
            txt: "ba"}, {
            txt: "bb"}, {
            txt: "bc"}]}, {
        txt: "c"}]};

var svg = d3.selectAll("svg");

svg.attr({ width: 500, height: 500});

var recurse = svg.selectAll("g.level0").data([data]).enter()
    .append("g").classed("level0", true);

var maxLevels = 4;
for (var level = 0; level < maxLevels; level++) {

    var nextLevel = level + 1;

    var next = svg.selectAll("g.level" + level).filter(function (d) {
        return d.children !== undefined;
    });

    next.selectAll("g.level" + nextLevel)
        .data(function (d) { return d.children; })
        .enter().append("g")
        .attr("class", function (d) {
            return "level" + nextLevel + " " + d.txt;
        })
        .attr("transform", function (d, i) {
            return "translate(" + (nextLevel * 25) + "," + (i * 10 * (5 - level) + 15) + ")";
        });

    next.selectAll("text.level" + nextLevel)
        .data(function (d) {  return d.children; })
        .enter().append("text")
        .classed("level" + level, true)
        .attr("x", function (d, i, j) {  return nextLevel * 25;  })
        .attr("y", function (d, i, j) {
            return j * (10 * (10 - level)) + (i+1) * 15;
        })
        .attr("fill", "black")
        .text(function (d) { return d.txt; });
}

Upvotes: 2

Related Questions