Search code examples
javascriptjsond3.jsforce-layout

d3.js forceSimulation() with object entries



I have a problem with my bubble chart.
I used forceSimulation() previously with an array of objects and it worked. Now I changed the data source and it doesn't, even if the console displays no errors.
My data is an object called "lightWeight", with the following structure: data
I use it to append circles like so:

// draw circles
var node = bubbleSvg.selectAll("circle")
   .data(d3.entries(lightWeight))
   .enter()
   .append("circle")
   .attr('r', function(d) { return scaleRadius(d.value.length)})
   .attr("fill", function(d) { return colorCircles(d.key)})
   .attr('transform', 'translate(' + [w/2, 150] + ')');

Then I create the simulation:

// simulate physics
  var simulation = d3.forceSimulation()
    .nodes(lightWeight)
    .force("charge", d3.forceCollide(function(d) { return d.r + 10; }))
    .force("x", d3.forceX())
    .force("y", d3.forceY())
  .on("tick", ticked); // updates the position of each circle (from function to DOM)

  // call to check the position of each circle
   function ticked(e) {
      node.attr("cx", function(d) { return d.x; })
          .attr("cy", function(d) { return d.y; });
  }

But the circles remain on top of each other and do not become a bubble chart like they did before.
I apologise if this is probably a dumb question, I am new to d3 and understood very little of how forceSimulation() actually works.
For example, if I call it multiple times with different data, will the resulting simulation affect only the specified data?
Thanks in advance!


Solution

  • There are a couple problems here:

    1. You're using different datasets for rendering and the force simulation, i.e.: .data(d3.entries(lightWeight)) creates a new array of objects that you use to bind to the DOM, whereas .nodes(lightWeight) attempts to run the force simulation on the original lightWeight object (it expects an array, so this isn't going to work).

    Try doing something like var lightWeightList = d3.entries(lightWeight); up before any of this code starts, and use that array for both binding to the DOM and as the parameter to the force simulation. Of course, this should make it clear that you might run into other challenges when it comes to updating which nodes you're looking at—overwriting lightWeightList will nuke any of the previous node positions (as we can't see more of your code, especially how you'd call this a second time, I don't have any helpful ideas).

    1. Especially if you plan to re-call this code, there's one other problem: the way that you chain the .enter() call means that node will only refer to the enter selection—meaning that, if you call this code again, the force simulation is only going to update the new nodes inside ticked.

    With D3, I've found that a good habit is to keep your selections in separate variables, e.g.:

    var lightWeightList = d3.entries(lightWeight);
    
    // ...
    
    var nodes = bubbleSvg.selectAll('circle')
      .data(lightWeightList);
    var nodesEnter = nodes.enter()
      .append('circle');
    // If you're using D3 v4 and above, you'll need to merge the selections:
    nodes = nodes.merge(nodesEnter);
    nodes.select('circle')
         .attr('r', function(d) { return scaleRadius(d.value.length)})
         .attr('fill', function(d) { return colorCircles(d.key)})
         .attr('transform', 'translate(' + [w/2, 150] + ')');
    
    // ...
    
    var simulation = d3.forceSimulation()
      .nodes(lightWeightList)
      .force("charge", d3.forceCollide(function(d) { return d.r + 10; }))
      .force("x", d3.forceX())
      .force("y", d3.forceY())
      .on("tick", ticked);
    
    function ticked(e) {
      nodes.attr("cx", function(d) { return d.x; })
           .attr("cy", function(d) { return d.y; });
    }