Dude
Dude

Reputation: 931

D3 Circle-Packing Clear Labeling Solution

Is there a way in d3's packing layout to set the radius of a child node manually, with a size relative to the parent radius, and then have the other children set their radius' based on the remaining space and using the existing "size by number of children"?

What I would like to do is: 1. for each node add a node to the children array with the same name as the parent 2. set the radius of this extra child to have a significant enough radius to contain text and ensure no overlap from its neighbors 3. set fill and stroke to none on this extra node 4. set the click interaction via css to none on this extra node 5. use only these extra nodes to display "their" names (aka their parent's names)

The result would be a packed circle with a specially designated space for its labels. This does not work without a manual setting of the extra child node's radius because it's size is automatically determined based on the number of children. (adding children unfilled/unstroked nodes to compensate is extremely inefficient. The second hack for the first hack just wouldn't be worth it I don't think)

Upvotes: 2

Views: 1083

Answers (2)

Dude
Dude

Reputation: 931

This answer is the solution I am using for now, and builds upon the solution Ganesh provided.

Summary of Ganesh's solution:

  • Introduce a function that runs over your data and injects an extra node into each node's child array, except for when it examines a leaf node.

Remaining Issues for my use of Ganesh's solution:

  • Not all parent nodes had values, so they did not have labels
  • There was no indication for how node values were calculated, they were already provided in the json data
  • The extra nodes created for labels were too small, as it's neighbors always grew to accomodate it's children.

Solution and key findings:

  • Value is determined based on the sum total of children of a node
  • Leaf nodes in my example had a value of 100
  • The root node had a value of roughly 31,700, which is roughly accurate since it has a total of 316 children (direct and transitive), counting itself
  • Therefore, I updated my data so that each node also includes the size of it's subtree
  • The final version of my value function, based on Ganesh's solution is provided below:

            var pack = d3.layout.pack()
            .value(function(d){ 
                if(d.isExtra){ //the property added to injected label nodes
                    d.value = d.parent.treeSize*100 //treeSize = size of subTree
                    return d.value;
                }
                else{
                    d.value = d.treeSize*100;
                    return d.value;
                }
            })
            .size([width, height -100 ])
    

It is important to note here that while I am introducing a new node with a value equal to it's parent node times 100, this does not mean that it takes 100 times the space of the circle. This is because a parent's value is ultimately the sum of it's children's values, which means that any node that had a child, will see an increase in total value. Multiplying the treeSize for label nodes by significantly higher numbers only provides a logarithmic increase in corresponding circle size, while introducing a larger and larger gap between leaf node size (100) and the rest of the circles.

Upvotes: 0

Ganesh Nemade
Ganesh Nemade

Reputation: 1599

Please check if this helps you.The circles with red background with with class 'extra' is the extra circles with parent's name.

jsbin link

var root = {
 "name": "flare",
 "children": [
  {
   "name": "analytics",
   "children": [
    {
     "name": "cluster",
     "children": [
      {"name": "AgglomerativeCluster", "size": 3938},
      {"name": "CommunityStructure", "size": 3812},
      {"name": "HierarchicalCluster", "size": 6714},
      {"name": "MergeEdge", "size": 743}
     ]
    },
    {
     "name": "graph",
     "children": [
      {"name": "BetweennessCentrality", "size": 3534},
      {"name": "LinkDistance", "size": 5731},
      {"name": "MaxFlowMinCut", "size": 7840},
      {"name": "ShortestPaths", "size": 5914},
      {"name": "SpanningTree", "size": 3416}
     ]
    },
    {
     "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}
   ]
  },
 ]
};
var addExtraNode = function(item, percentSize){
  var percentSizeOfNode = percentSize || 60; //if not given it will occupy 60 percent of the space
  if(!item.children){
    return;
  }
  var totalChildSize = 0;
  item.children.forEach(function(citm, index){
    totalChildSize = totalChildSize + citm.size;
  })
  
  var nodeSize = (percentSizeOfNode / 50) * totalChildSize;
  var name = 'NAME: '+item.name;
  item.children.push({
    'name': name,
    'size': nodeSize,
    'isextra':true
  })
  
  item.children.forEach(function(citm, index){
    if(citm.children){
      addExtraNode(citm, percentSize);
    }
  })
};

addExtraNode(root, 55);

var diameter = 500,
    format = d3.format(",d");

var pack = d3.layout.pack()
    .size([diameter - 4, diameter - 4])
    .value(function(d) { return d.size; });

var svg = d3.select("body").append("svg")
    .attr("width", diameter)
    .attr("height", diameter)
  .append("g")
    .attr("transform", "translate(2,2)");


 
var node = svg.datum(root).selectAll(".node")
    .data(pack.nodes)
	.enter().append("g")
    .attr("class", function(d) {
      
      if(d.isextra){
        return 'extra';
      }
      return d.children ? "node" : "leaf node"; })
    .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });

node.append("title")
    .text(function(d) { return d.name + (d.children ? "" : ": " + format(d.size)); });

node.append("circle")
    .attr("r", function(d) { return d.r; });

node.filter(function(d) { return !d.children; }).append("text")
    .attr("dy", ".3em")
    .style("text-anchor", "middle")
    .text(function(d) { return d.name.substring(0, d.r / 3); });
circle {
  fill: rgb(31, 119, 180);
  fill-opacity: .25;
  stroke: rgb(31, 119, 180);
  stroke-width: 1px;
}

.leaf circle {
  fill: #ff7f0e;
  fill-opacity: 1;
}

text {
  font: 10px sans-serif;
}
.extra circle{
  fill:red;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.4.11/d3.min.js"></script>

Upvotes: 1

Related Questions