Search code examples
javascriptjsond3.jszoomingsunburst-diagram

D3 data format like zoomable sunburst chart


I have my data formatted like flare.json that's used in this example :

I am just wondering what function the d3 zoomable chart uses to get the data in this format

In flare.json it's like this

{
   name: "stuff",
   children: [
    ....
   ]

}

and it's converted to this in the example. Which line does this?

{
  children: Array[17]
  depth: 1
  dx: 0.6028744305756647
  dy: 0.25
  name: "A name would appear here"
  parent: Object
  value: 39850000.06
  x: 0
  y: 0.25
}

Chart

    var total_revenue = json.total_revenue;
    json = json.chart_data;
  var width = 840,
      height = width,
      radius = width / 2,
      x = d3.scale.linear().range([0, 2 * Math.PI]),
      y = d3.scale.pow().exponent(1.3).domain([0, 1]).range([0, radius]),
      padding = 5,
      duration = 1000;

  var div = d3.select("#chart_render");

  div.select("img").remove();

  var vis = div.append("svg")
      .attr("width", width + padding * 2)
      .attr("height", height + padding * 2)
    .append("g")
      .attr("transform", "translate(" + [radius + padding, radius + padding] + ")");

  var partition = d3.layout.partition()
      .value(function(d) { return d.size });

  var arc = d3.svg.arc()
      .startAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x))); })
      .endAngle(function(d) { return Math.max(0, Math.min(2 * Math.PI, x(d.x + d.dx))); })
      .innerRadius(function(d) { return Math.max(0, d.y ? y(d.y) : d.y); })
      .outerRadius(function(d) { return Math.max(0, y(d.y + d.dy)); });


    console.log(json);
    var nodes = partition.nodes({children: json});

    var path = vis.selectAll("path").data(nodes);
    path.enter().append("path")
        .attr("id", function(d, i) { return "path-" + i; })
        .attr("d", arc)
        .attr("fill-rule", "evenodd")
        .style("fill", colour)
        .on("click", click);

    var text = vis.selectAll("text").data(nodes);
    var textEnter = text.enter().append("text")
        .style("fill-opacity", function(d) {
            var relative_percent = 0;
            var relative_total = 0;
            //console.log(d);

            if (d.depth != 0) {
                for(var i = 0; i < d.parent.children.length; i++) {
                    relative_total += d.parent.children[i].value;
                }
                //console.log(relative_total);
                relative_percent = d.value/total_revenue*100;

                if (relative_percent > 1) {
                    return '1';
                } else {
                    return '0';
                }
            }
        })
        .style("fill", function(d) {
          return "#fff";
        })
        .attr("text-anchor", function(d) {
          return x(d.x + d.dx / 2) > Math.PI ? "end" : "start";
        })
        .attr("dy", ".2em")
        .attr("transform", function(d) {
          var multiline = (d.name || "").split(" ").length > 1,
              angle = x(d.x + d.dx / 2) * 180 / Math.PI - 90,
              rotate = angle + (multiline ? -.5 : 0);
          return "rotate(" + rotate + ")translate(" + (y(d.y) + padding) + ")rotate(" + (angle > 90 ? -180 : 0) + ")";
        })
        .on("click", click);
    textEnter.append("tspan")
        .attr("x", 0)
        .text(function(d) { return d.depth ? d.name.split(" ")[0] : ""; });
    textEnter.append("tspan")
        .attr("x", 0)
        .attr("dy", "1em")
        .text(function(d) { return d.depth ? d.name.split(" ")[1] || "" : ""; });

    function click(d) {
      path.transition()
        .duration(duration)
        .attrTween("d", arcTween(d));

      // Somewhat of a hack as we rely on arcTween updating the scales.
      text.style("visibility", function(e) {
            return isParentOf(d, e) && e.value > 1500000 ? null : d3.select(this).style("visibility");
          })
        .transition()
          .duration(duration)
          .attrTween("text-anchor", function(d) {
            return function() {
              return x(d.x + d.dx / 2) > Math.PI ? "end" : "start";
            };
          })
          .attrTween("transform", function(d) {
            var multiline = (d.name || "").split(" ").length > 1;
            return function() {
              var angle = x(d.x + d.dx / 2) * 180 / Math.PI - 90,
                  rotate = angle + (multiline ? -.5 : 0);
              return "rotate(" + rotate + ")translate(" + (y(d.y) + padding) + ")rotate(" + (angle > 90 ? -180 : 0) + ")";
            };
          })
          .style("fill-opacity", function(e) { return isParentOf(d, e) ? 1 : 1e-6; })
          .each("end", function(e) {
            d3.select(this).style("visibility", function (d) {

          //    var relative_total = 0;
          //    var relative_percent = 0;

                // for(var i = 0; i < d.parent.children.length; i++) {
                //  relative_total += d.parent.children[i].value;
                // }

                // console.log(relative_total);

                // relative_percent = d.value/relative_total*100;
                // console.log(relative_percent);
                return isParentOf(d, e) && e.value > 1500000 ? null : "hidden";
            })
          });
    }

  function isParentOf(p, c) {
    if (p === c) return true;
    if (p.children) {
      return p.children.some(function(d) {
        return isParentOf(d, c);
      });
    }
    return false;
  }

  function colour(d) {

    if (d.depth == 0) {
      return "rgb(250, 250, 250)";
    } else if (d.depth == 1) {
      return 'rgb(86, 135, 209)';
    } else if (d.depth == 2) {
      return 'rgb(222, 120, 59)';
    } else if (d.depth == 3) {
      return 'rgb(106, 185, 117)';
    }

    // if (d.children) {
    //   // There is a maximum of two children!
    //   var colours = d.children.map(colour),
    //       a = d3.hsl(colours[0]),
    //       b = d3.hsl(colours[1]);
    //   // L*a*b* might be better here...
    //   return d3.hsl((a.h + b.h) / 2, a.s * 1.2, a.l / 1.2);
    // }
    // return d.colour || "#fff";
  }

  // Interpolate the scales!
  function arcTween(d) {
    var my = maxY(d),
        xd = d3.interpolate(x.domain(), [d.x, d.x + d.dx]),
        yd = d3.interpolate(y.domain(), [d.y, my]),
        yr = d3.interpolate(y.range(), [d.y ? 20 : 0, radius]);
    return function(d) {
      return function(t) { x.domain(xd(t)); y.domain(yd(t)).range(yr(t)); return arc(d); };
    };
  }

  function maxY(d) {
    return d.children ? Math.max.apply(Math, d.children.map(maxY)) : d.y + d.dy;
  }

  // http://www.w3.org/WAI/ER/WD-AERT/#color-contrast
  function brightness(rgb) {
    return rgb.r * .299 + rgb.g * .587 + rgb.b * .114;
  }

Solution

  • This line:

    var nodes = partition.nodes({children: json});
    

    Explanation of code that sets up sunburst diagram

    In D3 parlance, sunburst diagram is based on D3 "partition layout". Actually, D3 "partition layout" is in a way more general term, since it can be used for displaying not only sunburst diagram, but also others based on the idea of "partitioning" parents (hence the name "partition"). This is also a useful example for noticing difference between "layout" and "diagram" (in D3 mindset), but this is another story.

    Following 2 lines are first steps in initializing partition layout:

      var partition = d3.layout.partition()
          .value(function(d) { return d.size });
    

    This line does all calculations:

    var nodes = partition.nodes({children: json});
    

    Then variable nodes can be used for defining actual visual appearance of svg elements (arcs and labels):

    var path = vis.selectAll("path").data(nodes);
    

    and

    var text = vis.selectAll("text").data(nodes);
    

    These two lines represent something which is called "data binding" often. They enable programmers to use data to drive visual elements, like in the following line:

    .text(function(d) { return d.depth ? d.name.split(" ")[0] : ""; });
    

    Here, d.name originates from data, and d.depth is added by partition layout. They are both actually part of nodes.

    I tried to explain in simple terms, but probably there are some confusing points to you - don't worry, it will be crystal clear to you soon, if you read the right docs and tutorials. ;)