Justin Wilson
Justin Wilson

Reputation: 340

Drawing a collapsible indented tree with d3.xml instead of d3.json

I am able to get the collapsible indented tree to work properly with a JSON sample file; however, I cannot modify the code to use an XML file. Below are my sample JSON file, sample XML file, and attempt to modify .js file to use XML instead of JSON.

I think that these are the key areas of code that I have to modify but not sure:

d3.xml("d3/simple-flare.xml", "application/xml", function (error, flare) {
        flare.x0 = 0;
        flare.y0 = 0;
        update(root = flare);
    });

...

function update(source) {

        // Compute the flattened node list. TODO use d3.layout.hierarchy.
        var nodes = tree.nodes(root);

...

// Update the links…
        var link = svg.selectAll("path.link")
            .data(tree.links(nodes), function (d) { return d.target.id; });

simple-flare.json

{
 "name": "flare",
 "children": [
  {
   "name": "analytics",
   "children": [
    {
     "name": "cluster",
     "children": [
      {"name": "AgglomerativeCluster", "size": 3938},
      {"name": "CommunityStructure", "size": 3812},
      {"name": "MergeEdge", "size": 743}
     ]
    },
    {
     "name": "graph",
     "children": [
      {"name": "BetweennessCentrality", "size": 3534},
      {"name": "LinkDistance", "size": 5731}
     ]
    },
    {
     "name": "optimization",
     "children": [
      {"name": "AspectRatioBanker", "size": 7074}
     ]
    }
   ]
  },
  {
   "name": "animate",
   "children": [
    {"name": "Easing", "size": 17010},
    {"name": "FunctionSequence", "size": 5842},
    {
     "name": "interpolate",
     "children": [
      {"name": "ArrayInterpolator", "size": 1983},
      {"name": "ColorInterpolator", "size": 2047},
      {"name": "DateInterpolator", "size": 1375},
      {"name": "Interpolator", "size": 8746},
      {"name": "MatrixInterpolator", "size": 2202},
      {"name": "NumberInterpolator", "size": 1382},
      {"name": "ObjectInterpolator", "size": 1629},
      {"name": "PointInterpolator", "size": 1675},
      {"name": "RectangleInterpolator", "size": 2042}
     ]
    },
    {"name": "ISchedulable", "size": 1041},
    {"name": "Parallel", "size": 5176},
    {"name": "Pause", "size": 449},
    {"name": "Scheduler", "size": 5593},
    {"name": "Sequence", "size": 5534},
    {"name": "Transition", "size": 9201},
    {"name": "Transitioner", "size": 19975},
    {"name": "TransitionEvent", "size": 1116},
    {"name": "Tween", "size": 6006}
   ]
  }
 ]
}

simple-flare.xml

<?xml version="1.0" encoding="UTF-8" ?>
<flare>
  <analytics>
     <cluster>
        <agglomerativeCluster>3938</agglomerativeCluster>
        <communityStructure>3812</communityStructure>
        <mergeEdge>743</mergeEdge>
     </cluster>
     <graph>
        <test>3343</test>
        <mmmm>3353</mmmm>
        <lalala>454</lalala>
     </graph>
     <optimization>
        <AspectRatio>7074</AspectRatio>
     </optimization>
  </analytics>
</flare>

collapseIndentTree.js

// Changes XML to JSON
function xmlToJson(xml) {

    // Create the return object
    var obj = {};

    if (xml.nodeType == 1) { // element
        // do attributes
        if (xml.attributes.length > 0) {
            obj["@attributes"] = {};
            for (var j = 0; j < xml.attributes.length; j++) {
                var attribute = xml.attributes.item(j);
                obj["@attributes"][attribute.nodeName] = attribute.nodeValue;
            }
        }
    } else if (xml.nodeType == 3) { // text
        obj = xml.nodeValue;
    }

    // do children
    if (xml.hasChildNodes()) {
        for (var i = 0; i < xml.childNodes.length; i++) {
            var item = xml.childNodes.item(i);
            var nodeName = item.nodeName;
            if (typeof (obj[nodeName]) == "undefined") {
                obj[nodeName] = xmlToJson(item);
            } else {
                if (typeof (obj[nodeName].push) == "undefined") {
                    var old = obj[nodeName];
                    obj[nodeName] = [];
                    obj[nodeName].push(old);
                }
                obj[nodeName].push(xmlToJson(item));
            }
        }
    }
    return obj;
};

function indenttree() {
    var margin = { top: 30, right: 20, bottom: 30, left: 20 },
    width = 960 - margin.left - margin.right,
    barHeight = 20,
    barWidth = width * .8;

    var i = 0,
        duration = 400,
        root;

    var tree = d3.layout.tree()
        .nodeSize([0, 20]);

    var diagonal = d3.svg.diagonal()
        .projection(function (d) { return [d.y, d.x]; });

    var svg = d3.select(".nester_wrap").append("svg")
        .attr("width", width + margin.left + margin.right)
      .append("g")
        .attr("transform", "translate(" + margin.left + "," + margin.top + ")");

    d3.xml("d3/simple-flare.xml", "application/xml", function (error, flare) {
        var flareJSON = xmlToJson(flare)
        flareJSON.x0 = 0;
        flareJSON.y0 = 0;

        var xmlText = new XMLSerializer().serializeToString(flare);
        var xmlTextNode = document.createTextNode(xmlText);
        var parentDiv = document.getElementById('footerArea');
        parentDiv.appendChild(xmlTextNode);

        alert(JSON.stringify(flareJSON));

        update(root = flare);
        //update(root = flareJSON);
        //update(root = d3.select(flare).selectAll("*")[0]);
        //update(root = flare.selectNodes("//*")[0]);
    });

    /*
    d3.json("d3/simple-flare.json", function (error, flare) {
        flare.x0 = 0;
        flare.y0 = 0;
        update(root = flare);
    });

    //notes
    d3.json("flare.json", function(root) {
    var nodes = flatten(root),
        links = d3.layout.tree().links(nodes);

    d3.xml("flare.xml", "application/xml", function(xml) {
        var nodes = self.nodes = d3.select(xml).selectAll("*")[0],
            links = self.links = nodes.slice(1).map(function(d) {
                return {source: d, target: d.parentNode};
            });
    */

    function update(source) {

        // Compute the flattened node list. TODO use d3.layout.hierarchy.
        var nodes = tree.nodes(root);

        var height = Math.max(500, nodes.length * barHeight + margin.top + margin.bottom);

        d3.select("svg").transition()
            .duration(duration)
            .attr("height", height);

        d3.select(self.frameElement).transition()
            .duration(duration)
            .style("height", height + "px");

        // Compute the "layout".
        nodes.forEach(function (n, i) {
            n.x = i * barHeight;
        });

        // Update the nodes…
        var node = svg.selectAll("g.node")
            .data(nodes, function (d) { return d.id || (d.id = ++i); });

        var nodeEnter = node.enter().append("g")
            .attr("class", "node")
            .attr("transform", function (d) { return "translate(" + source.y0 + "," + source.x0 + ")"; })
            .style("opacity", 1e-6);

        // Enter any new nodes at the parent's previous position.
        nodeEnter.append("rect")
            .attr("class", "indent")
            .attr("y", -barHeight / 2)
            .attr("height", barHeight)
            .attr("width", barWidth)
            .style("fill", color)
            .on("click", click);

        nodeEnter.append("text")
            .attr("class", "indent")
            .attr("dy", 3.5)
            .attr("dx", 5.5)
            .text(function (d) { return d.name; });

        // Transition nodes to their new position.
        nodeEnter.transition()
            .duration(duration)
            .attr("transform", function (d) { return "translate(" + d.y + "," + d.x + ")"; })
            .style("opacity", 1);

        node.transition()
            .duration(duration)
            .attr("transform", function (d) { return "translate(" + d.y + "," + d.x + ")"; })
            .style("opacity", 1)
          .select("rect")
            .style("fill", color);

        // Transition exiting nodes to the parent's new position.
        node.exit().transition()
            .duration(duration)
            .attr("transform", function (d) { return "translate(" + source.y + "," + source.x + ")"; })
            .style("opacity", 1e-6)
            .remove();

        // Update the links…
        var link = svg.selectAll("path.link")
            .data(tree.links(nodes), function (d) { return d.target.id; });

        // Enter any new links at the parent's previous position.
        link.enter().insert("path", "g")
            .attr("class", "link")
            .attr("d", function (d) {
                var o = { x: source.x0, y: source.y0 };
                return diagonal({ source: o, target: o });
            })
          .transition()
            .duration(duration)
            .attr("d", diagonal);

        // Transition links to their new position.
        link.transition()
            .duration(duration)
            .attr("d", diagonal);

        // Transition exiting nodes to the parent's new position.
        link.exit().transition()
            .duration(duration)
            .attr("d", function (d) {
                var o = { x: source.x, y: source.y };
                return diagonal({ source: o, target: o });
            })
            .remove();

        // Stash the old positions for transition.
        nodes.forEach(function (d) {
            d.x0 = d.x;
            d.y0 = d.y;
        });
    }

    // Toggle children on click.
    function click(d) {
        if (d.children) {
            d._children = d.children;
            d.children = null;
        } else {
            d.children = d._children;
            d._children = null;
        }
        update(d);
    }

    function color(d) {
        return d._children ? "#3182bd" : d.children ? "#c6dbef" : "#fd8d3c";
    }
}
var chart = indenttree();

Upvotes: 1

Views: 1907

Answers (1)

tge
tge

Reputation: 91

I was just about to do something similar, and, since new to D3, first searched on the web and stumbled over this posting. I got it to work and here are my findings (most likely optimizable).

Based on your original code, first I was able to display the text of the nodes quite easily:

nodeEnter.append("text")
         .attr("class", "indent")
         .attr("dy", 3.5)
         .attr("dx", 5.5)
         .text(function (d) { return d.tagName + " = " +
                               d.firstChild.nodeValue; });
//       .text(function (d) { return d.name; });

Then, getting the links between the nodes was a bit more complicated (I used firebug and its JS debugger to see what's going on). Once, the following was missing:

var nodes = tree.nodes(root);
var links = d3.layout.tree().links(nodes);  // missing

however just doing this throws an exception "[].map not a function" which turned out to be a bit sad because I really like your original idea to iterate directly over the XML structure instead of first converting it to JSON.

In the JS debugger I found that the tree.nodes() function nicely augments the XML nodes hierarchy by depth and parent attributes (as explained here https://github.com/mbostock/d3/wiki/Tree-Layout), however it does NOT create an additional attribute children (probably because in the DOM representing the XML, there IS already a member named children - which however is of type HTMLCollection (here is something related: Difference between HTMLCollection, NodeLists, and arrays of objects) and unfortunately that seems to be the reason for the exception above: D3 expects an array to call map() but finds an HTMLcollection).

So, it seems as if there is no other way than first converting XML to JSON, which you originally did, however commented it out. This method however required a few other changes:

First, now use the JSON data:

// update(root = flare);
update(root = flareJSON);

Then, undo my change above:

nodeEnter.append("text")
        .attr("class", "indent")
        .attr("dy", 3.5)
        .attr("dx", 5.5)
        .text(function (d) { return d.name; });

Now, flareJSON contains neither parent nor children, therefore tree.nodes() does not generate anything useful. I entirely changed this function (I ignored the attributes XML stuff):

// Changes XML to JSON
function xmlToJson(xml)
{
  // ignore text leaves
  if(xml.hasChildNodes())
  {
    // Produce a node with a name
    var obj = { name: (xml.tagName || "root") + (xml.firstChild.nodeValue ? (" = " + xml.firstChild.nodeValue) : "") };

    // iterate over children
    for (var i = 0; i < xml.childNodes.length; i++)
    {
      // if recursive call returned a node, append it to children
      var child = xmlToJson(xml.childNodes.item(i));
      if(child)
      {
        (obj.children || (obj.children = [])).push(child);
      }
    }

    return obj;
  }

  return undefined;
};

Now it works as I guess it is supposed to :-)

It still would be really nice if the original idea (directly draw XML) worked but that probably requires changes in D3 (see above) ...

EDIT: Hehe, just discovered this: How to have forEach available on pseudo-arrays returned by querySelectorAll? If I insert this into the JS code, then the pure XML-based approach works too (well, almost, some warnings appear in the JS console, but I'll keep trying ...

Upvotes: 1

Related Questions