matteo rulli
matteo rulli

Reputation: 1493

How to manipulate circles in d3 bubble diagram?

Sorry for the silly question, I'm just a poor d3 newbie...

I have the following demo bubble diagram on JSFiddle: what I am trying to achieve is to increase the radius of the circles whenever I click over them and, after that, to adjust the pack layout accordingly.

This is the code in JSFiddle for your convenience:

text {
  font: 20px sans-serif;
}
<!DOCTYPE html>

<body>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.5/d3.min.js"></script>
<script>
        
var root = {
    "name": "Root",
    "children": [
        {
            "name": "Leaf One",
            "children": null,
            "size": 1
        },
        {
            "name": "Leaf Two",
            "children": null,
            "size": 1
        },
        {
            "name": "Leaf Three",
            "children": null,
            "size": 1
        }
    ],
    "size": 1
};

    
var diameter = 400,
    format = d3.format(",d"),
    color = d3.scale.category20c();

var bubble = d3.layout.pack()
    .sort(null)
    .size([diameter, diameter])
    .padding(1.5);

var svg = d3.select("body").append("svg")
    .attr("width", diameter)
    .attr("height", diameter)
    .attr("class", "bubble");

// not needed in JSfiddle, data is hard-coded:
// d3.json("data.json", function(error, root) {
//   if (error) throw error;

  var node = svg.selectAll(".node")
      .data(bubble.nodes(classes(root))
      .filter(function(d) { return !d.children; }))
    .enter().append("g")
      .attr("class", "node")
      .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; });

  node.append("title")
      .text(function(d) { return d.className + ": " + format(d.value); });

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

  node.append("text")
      .attr("dy", ".3em")
      .style("text-anchor", "middle")
      .text(function(d) { return d.className.substring(0, d.r / 3); });
    
    node.on("click", function(e, i){
  		var circle = svg.select("circle");
  		circle.attr("value", function(d) { 
  			d.value += 1;
  			return d.value; 
  		});
  		circle.attr("r", function(d) { 
  			d.r += 1.0;
  			return d.r; 
  		});
      });
// });

// Returns a flattened hierarchy containing all leaf nodes under the root.
function classes(root) {
  var classes = [];

  function recurse(name, node) {
    if (node.children) node.children.forEach(function(child) { recurse(node.name, child); });
    else classes.push({packageName: name, className: node.name, value: node.size});
  }

  recurse(null, root);
  return {children: classes};
}

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

</script>

I tried to do that via the node.on("click", ...) method but I got somehow stuck, as the modified circle is always the first one: what is the best way to select the circle I clicked?

Besides, how can I force the d3 pack layout to refresh after I modify the circle radius?

Upvotes: 0

Views: 492

Answers (1)

eagor
eagor

Reputation: 10035

first, you have to remember that in event handlers the this is bound to the current DOM element (in out case - the <g>). so you can select clicked <g> by d3.select(this), and then select the circle within:

var circle = d3.select(this).select("circle");

(by just doing svg.select("circle") you select the first circle in the DOM, which always happens to be the same one)

in order to refresh the layout you have to update the underlying data, recalculate layout, recompute data join, and update values:

// store the underlying data in a value
var classedRoot = classes(root);

// ...your node creating code here

node.on("click", function(d) {
   // update the data in classedRoot
   d.value += 1;

   // recalculate layout
   var data = bubble.nodes(classedRoot);

   // recompute data join
   node.data(data, function(d) { return d.className; }) // use key function
      // update values
      .attr("transform", function(d) { return "translate(" + d.x + "," + d.y + ")"; })
      .select("circle")
      .attr("r", function(d) { return d.r });
});

note the use of key function here to properly bind updated data with the circles already present. the value you use must be unique for all circles. i used className here, but if it's not unique, you'll have to use some kind of Id.

here's updated jsFiddle

Upvotes: 1

Related Questions