Search code examples
javascriptjsoncsvd3.jshierarchy

Hierarchical JSON into flare.json format for use in Bilevel Partition


I have a CSV that I want to convert to hierarchical JSON for use with a Bilevel Partition.

The Bilevel Partition wants its JSON data in format similar to this flare.json file. Basically the leaf nodes have a name and size properties and everything in between has a name and children properties.

Here is my code for attempting to convert the CSV file into Hierarchical JSON.

CODE

var root = { "key": "Leeds CCGs", "values": d3.nest()
    .key(function(d) { return d.ccgname; })
    .key(function(d) { return d.practicename; })
    .key(function(d) { return d.diagnosisname; })
    .rollup(function(leaves) { return d3.sum(leaves, function(d) { return d.numpatientswithdiagnosis; }) })
    .entries(data)
}

The above code works as far as giving the data the required hierarchical structure, but the labels are wrong. Instead of name, children and size it gives me key and values only, all the way to the leaf nodes, similar to this file.

So I read around, and found this question on SO, it isn't to do with a Bilevel Partition, but I thought the same principle would apply, since both layouts need hierarchical JSON data.

So I set about doing the same, but I just can't get it to work. First of all in my code I do not have a size() function as mentioned in the SO question. Here is my whole code which is pretty much copied from the official example:

CODE

d3.csv('data/partition.csv', function (data) {

  var root = { "key": "Leeds CCGs", "values": d3.nest()
    .key(function(d) { return d.ccgname; })
    .key(function(d) { return d.practicename; })
    .key(function(d) { return d.diagnosisname; })
    .rollup(function(leaves) { return d3.sum(leaves, function(d) { return d.numpatientswithdiagnosis; }) })
    .entries(data)
  }

  // Compute the initial layout on the entire tree to sum sizes.
  // Also compute the full name and fill color for each node,
  // and stash the children so they can be restored as we descend.
  partition
    .value(function(d) { return d.values; })
    .nodes(root)
    .forEach(function(d) {
      d._children = d.values;
      d.sum       = d.value;
      d.key       = key(d);
      d.fill      = fill(d);
    });

  // Now redefine the value function to use the previously-computed sum.
  partition.children(function(d, depth) {
    return depth < 2 ? d._children : null;
  }).value(function(d) {
    return d.sum;
  });

  var center = svg.append("circle")
    .attr("r", radius / 4)
    .style('fill-opacity', '.2')
    .style('cursor', 'pointer')
    .on("click", zoomOut);

  center.append("title")
    .text("zoom out");

  var path = svg.selectAll("path")
    .data(partition.nodes(root).slice(1))
  .enter().append("path")
    .attr("d", arc)
    .style("fill", function(d) { return d.fill; })
    .style('cursor', 'help')
    .each(function(d) { this._current = updateArc(d); })
    .on("mouseover", update_legend)
    .on("mouseout", remove_legend)
    .on("click", zoomIn);

  function zoomIn(p) {
    if (p.depth > 1) p = p.parent;
    if (!p.children) return;
    zoom(p, p);
  }

  function zoomOut(p) {
    if (!p.parent) return;
    zoom(p.parent, p);
  }

  // Zoom to the specified new root.
  function zoom(root, p) {
    if (document.documentElement.__transition__) return;

    // Rescale outside angles to match the new layout.
    var enterArc,
        exitArc,
        outsideAngle = d3.scale.linear().domain([0, 2 * Math.PI]);

    function insideArc(d) {
      return p.key > d.key
          ? {depth: d.depth - 1, x: 0, dx: 0} : p.key < d.key
          ? {depth: d.depth - 1, x: 2 * Math.PI, dx: 0}
          : {depth: 0, x: 0, dx: 2 * Math.PI};
    }

    function outsideArc(d) {
      return {depth: d.depth + 1, x: outsideAngle(d.x), dx: outsideAngle(d.x + d.dx) - outsideAngle(d.x)};
    }

    center.datum(root);

    // When zooming in, arcs enter from the outside and exit to the inside.
    // Entering outside arcs start from the old layout.
    if (root === p) enterArc = outsideArc, exitArc = insideArc, outsideAngle.range([p.x, p.x + p.dx]);

    path = path.data(partition.nodes(root).slice(1), function(d) { return d.key; });

    // When zooming out, arcs enter from the inside and exit to the outside.
    // Exiting outside arcs transition to the new layout.
    if (root !== p) enterArc = insideArc, exitArc = outsideArc, outsideAngle.range([p.x, p.x + p.dx]);

    d3.transition().duration(d3.event.altKey ? 7500 : 750).each(function() {
      path.exit().transition()
          .style("fill-opacity", function(d) { return d.depth === 1 + (root === p) ? 1 : 0; })
          .attrTween("d", function(d) { return arcTween.call(this, exitArc(d)); })
          .remove();

      path.enter().append("path")
          .style("fill-opacity", function(d) { return d.depth === 2 - (root === p) ? 1 : 0; })
          .style("fill", function(d) { return d.fill; })
          .style('cursor', 'help')
          .on("mouseover",update_legend)
          .on("mouseout",remove_legend)
          .on("click", zoomIn)
          .each(function(d) { this._current = enterArc(d); });

      path.transition()
          .style("fill-opacity", 1)
          .attrTween("d", function(d) { return arcTween.call(this, updateArc(d)); });
    });
  }
});

function key(d) {
  var k = [];
  var p = d;
  while (p.depth) k.push(p.key), p = p.parent;
  return k.reverse().join(".");
}

function fill(d) {
  var p = d;
  while (p.depth > 1) p = p.parent;
  var c = d3.lab(hue(p.key));
  c.l = luminance(d.sum);
  return c;
}

function arcTween(b) {
  var i = d3.interpolate(this._current, b);
  this._current = i(0);
  return function(t) {
    return arc(i(t));
  };
}

function updateArc(d) {
  return {depth: d.depth, x: d.x, dx: d.dx};
}

All the above gives me this in my browser:

broken bilevel partition


Solution

  • You should be able to exactly re-use the rest of the code by simply transforming the hierarchical output of d3.nest() into the same format as the flare.json dataset, something like this:

    (should be run immediately after the definition of root)

      //rename object keys generated from d3.nest() to match flare.json format
      renameStuff(root);
      function renameStuff(d) {
        d.name = d.key; delete d.key;
        if (typeof d.values === "number") d.size = d.values;
        else d.values.forEach(renameStuff), d.children = d.values;
        delete d.values;
      }
    

    You could also change the accessor functions to the d3.layout.partition() object to match the new object, but you will at a minimum need to change the structure of the original object so that the leaf nodes do not store values in the same field name as the children. The solution above is probably the simplest way to get things working quickly.