Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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>
like image 325
user1987289 Avatar asked Jan 17 '13 13:01

user1987289


2 Answers

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
like image 182
chad Avatar answered Oct 13 '22 01:10

chad


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; });
}
like image 45
Superboggly Avatar answered Oct 12 '22 23:10

Superboggly