d3.js - how to automatically calculate arc lengths in radial dendrogram

10,520

I've seen your json data structure here: http://mikeheavers.com/transfers/projects/data/projects.json. Firstly, in order to group the data and append the tag correctly, it'll be better to change your data like this: https://raw.github.com/gist/4172625/4de3e6a68f9721d10e0068d33d1ebb9780db4ae2/flare-imports.json to create a hirarchical structure.

We can then use the groups to draw the arcs.

First we create groups by "selectAll" and filter your nodes. Here you could add other group names of your data:

var groupData = svg.selectAll("g.group")
  .data(nodes.filter(function(d) {return (d.key=='Jobs' || d.key == 'Freelance' || d.key == 'Bayard') && d.children; }))
.enter().append("group")
  .attr("class", "group");

I just checked that in my case, so you'd better verify the result of the filter and make change according to your case (our data structure is a little bit different).

Now we got a list of groups. Then we'll go through the children of each group, and choose the smallest and largest x as the start and end angle. We can create a function like this:

function findStartAngle(children) {
    var min = children[0].x;
    children.forEach(function(d){
       if (d.x < min)
           min = d.x;
});
return degToRad(min);
}

And similarly a findEndAngle function by replacing min by max. Then we can create the arcs' format:

var groupArc = d3.svg.arc()
  .innerRadius(arcData[0].rI)
  .outerRadius(arcData[0].rO)
  .startAngle(function(d){return findStartAngle(d.children);})
  .endAngle(function(d){return findEndAngle(d.children);});

Then we can create arcs in "dynamic" way:

svg.selectAll("g.arc")
  .data(groupData[0])
.enter().append("arc")
  .attr("d", groupArc)
  .attr("class", "arc")
    .append("svg:text")
      ...;

In my case it is groupData[0], maybe you should check it in your case. For adding tags to arcs you just need to add d.key or d.name according to the result of your selection.

the arc example

The full code is available here: https://gist.github.com/4172625. Every time I get json from database so if there's no dynamic way to generic arcs I will be dead :P Hope it helps you!

Share:
10,520

Related videos on Youtube

mheavers
Author by

mheavers

Updated on August 06, 2022

Comments

  • mheavers
    mheavers over 1 year

    I'm creating a modified version of Mike Bostock's hierarchical edge bundling diagram:

    http://mbostock.github.com/d3/talk/20111116/bundle.html

    but I want to make arcs which span certain groups of data, like this:

    enter image description here

    I'm currently just hardcoding the length of the arc, but I want to do it dynamically. How can I accomplish this? Here's my current code:

    /* MH - USER DEFINED VARIABLES */
    var chartConfig = { "Tension" : .85, "canvasSize" : 800, "dataFile" : "../data/projects.json", "linePadding" : 160, "textPadding" : 30, "arcPadding" : 5, "arcWidth" : 30 }
    var pi = Math.PI;
    
    var radius = chartConfig.canvasSize / 2,
        splines = [];
    
    var cluster = d3.layout.cluster() //Cluster is the diagram style, a node to link dendrogram dendrogram (tree diagram)
        .size([360, radius - chartConfig.linePadding]); //MH - sets the size of the circle in relation to the size of the canvas
    
    var bundle = d3.layout.bundle(); //Bundles the node link lines so that they spread at the end but keep close initially
    
    var arcInner = radius - chartConfig.linePadding + chartConfig.arcPadding;
    var arcOuter = arcInner + chartConfig.arcWidth;
    var arc = d3.svg.arc().innerRadius(arcInner).outerRadius(arcOuter);
    
    var line = d3.svg.line.radial()
        .interpolate("bundle")
        .tension(chartConfig.Tension) //How tightly to bundle the lines. No tension creates straight lines
        .radius(function(d) { return d.y; })
        .angle(function(d) { return d.x / 180 * Math.PI; });
    
    var vis = d3.select("#chart").append("svg")
        .attr("width", radius * 2)
        .attr("height", radius * 2)
        .attr("class","svg")
        .append("g")
        .attr("class","chart")
        .attr("transform", "translate(" + radius + "," + radius + ")");
    
    
    d3.json(chartConfig.dataFile, function(classes) {
      var nodes = cluster.nodes(packages.root(classes)),
          links = packages.imports(nodes),
          splines = bundle(links);
    
      var path = vis.selectAll ("path.link")
          .data(links)
          .enter().append("path")
          .attr("class", function(d){ return "link source-" + d.source.key + " target-" + d.target.key; })
          .attr("d", function(d,i){ return line(splines[i]); });
    
      vis.selectAll("g.node")
          .data(nodes.filter(function(n) { return !n.children; }))
        .enter().append("g")
          .attr("class", "node")
          .attr("id",function(d){ return "node-" + d.key; })
          .attr("transform", function(d) { return "rotate(" + (d.x - 90) + ")translate(" + d.y + ")"; })
        .append("text")
          .attr("dx", function(d) { return d.x < 180 ? chartConfig.textPadding : -chartConfig.textPadding; }) //dx Moves The text out away from the lines in a positive or negative direction, depending on which side of the axis it is on
          .attr("dy", ".31em") //moves the text up or down radially around the circle
          .attr("text-anchor", function(d) { return d.x < 180 ? "start" : "end"; })
          .attr("transform", function(d) { return d.x < 180 ? null : "rotate(180)"; })
          .text(function(d) {
            textString = d.key;
            textString = textString.split('_').join(' '); //MH replace underscores with spaces
            return textString; 
          })
          .on("mouseover",textOver)
          .on("mouseout",textOut);
    
    });
    
    
    /* ARCS ARE HARDCODED, SHOULD BE DYNAMIC */
    
    var arcData = [
      {aS: 0, aE: 45,rI:radius - chartConfig.linePadding + chartConfig.arcPadding,rO:radius - chartConfig.linePadding + chartConfig.textPadding-chartConfig.arcPadding}
    ];
    
    var arcJobsData = d3.svg.arc().innerRadius(arcData[0].rI).outerRadius(arcData[0].rO).startAngle(degToRad(1)).endAngle(degToRad(15));
    var g = d3.select(".chart").append("svg:g").attr("class","arcs");
    var arcJobs = d3.select(".arcs").append("svg:path").attr("d",arcJobsData).attr("id","arcJobs").attr("class","arc");
    g.append("svg:text").attr("x",3).attr("dy",15).append("svg:textPath").attr("xlink:href","#arcJobs").text("JOBS").attr("class","arcText"); //x shifts x pixels from the starting point of the arc. dy shifts the text y units from the top of the arc
    
    ... 
    
    function degToRad(degrees){
      return degrees * (pi/180);
    }
    
    function updateNodes(name,value){
      return function(d){
        if (value) this.parentNode.appendChild(this);
        vis.select("#node-"+d[name].key).classed(name,value);
      }
    }
    
  • mccannf
    mccannf over 11 years
    In your findStartAngle(d.children) and findEndAngle(d.children) above, where is d defined?
  • wceo
    wceo over 11 years
    @mccannf Sorry, my mistake, it should be function(d){return findStartAngle(d.children);}
  • mheavers
    mheavers over 11 years
    This looks great. I will test it this weekend and accept your answer if everything goes well. Thanks!
  • wceo
    wceo over 11 years
    @mheavers You won't give me your bounties? :)
  • mheavers
    mheavers over 11 years
    Yes! Sorry - been busy, but this is a great solution.
  • mheavers
    mheavers over 11 years
    One last question - In my example above, I appended some text to the arcs to indicate to what group the arcs belonged - agencies, jobs, freelance, etc. - but in your example that is missing. I know I can retrieve that data using d.__data__.key - but I can't figure out how to place it dynamically inside the arc. Any pointers on how to do that?
  • luftikus143
    luftikus143 over 10 years
    @wceo: I am pretty new to this type of vis. Would it be possible to link say the "Freelancer" to "Hertz", too? I don't see how. But that way, when I hover over "Hertz" I would see that both "Freelance" and "Bayard" link to it. In the moment, if there is textual link between them, then it highlights the two lines; but it should preferably be one line only. Am I clear?! Hmm... Thanks for any hints!
  • wceo
    wceo over 10 years
    @luftikus143 Maybe I don't understand very clearly. Why when "Freelance" and "Bayard" are both linked to "Hertz" there should be one line only?
  • luftikus143
    luftikus143 over 10 years
    Because the same item shouldn't appear twice. And as it appears only once, by hovering over it one can see that it is linked to two items. In this case, it would be a company XY for which the person has worked as a freelancer, but as well as an employee at Bayard. And thus, Bayard and Freelancer are linking to XY. If you hover over Bayard you see the link; same if you hover over Freelancer. And if you hover over XY you see the link to both Bayard and Freelancer. That's for me the big advantage of these types of graphs.
  • wceo
    wceo over 10 years
    @luftikus143 I'm still not sure I know what you mean exactly :P I believe if in the data freelance and bayard are both linked to XY, there will be only one XY, not two. (If this is what you mean...
  • luftikus143
    luftikus143 over 10 years
    @wceo Yes, that's what I mean, indeed.
  • luftikus143
    luftikus143 over 10 years
    @wceo: Any ideas how to proceed?
  • wceo
    wceo over 10 years
    @luftikus143 Sorry I still don't get you even I know what you mean. If in the data " freelance and bayard are both linked to XY, there will be only one XY", what else do you have to achieve exactly? Sorry if I've been misunderstanding...
  • luftikus143
    luftikus143 over 10 years
    Hmm. Can Bayard be linked to items on the left side so that both Freelancer and Bayard are linking to the same item? Or, differently asked, I want to have a connection from one item to both Bayard and Hertz. Is that possible?
  • wceo
    wceo over 10 years
    @luftikus143 Yes you can. Look at the example data here: gist.github.com/geow812/6696698. "region1.Montrose_High_School" is in the "imports" of both "employers.Freelance" and "employers.Bayard", this will do the trick. The "region1" and "region2" are used to separate the two parts :)