Search code examples
javascriptd3.jsbeeswarm

Resetting all isolated forces in d3 forceSimulation


I am trying to create a swarm graph that transitions between a few states, but have hit a few roadblocks. The best way I've found to set this up is to cluster my nodes in the center and then isolate forceX and forceY based on my data. However, I am finding that once I have done that, it is impossible to 'reset' the whole swarm and just bring every node back to the center. It seems almost as if every node starts moving relative to the last isolated force even if I add forceCenters.

I am admittedly new to d3-force so this may be a dumb question, but I have done a lot of searching with no answers.

var width = 400;
var height = 150;
var radius = 3;
var data = [
  {"id":1, "a":1, "b":1, "color":"#ff0000"},
  {"id":2, "a":1, "b":2, "color":"#ff0000"},
  {"id":3, "a":2, "b":1, "color":"#00ff00"},
  {"id":4, "a":2, "b":2, "color":"#00ff00"},
  {"id":5, "a":3, "b":1, "color":"#0000ff"},
  {"id":6, "a":3, "b":2, "color":"#0000ff"},
];


$(document).ready(function(){
  createGraph();
  makeForce();
});

var svg;

function createGraph(){
    svg = d3.select("body")
        .append("svg")
        .attr("width", width)
        .attr("height", height)
        .style("background-color", "#dddddd");
}

var simulation;

function makeForce(){

  
  var nodes=data;
  
  node = svg.append("g").attr("stroke", "#bbb").attr("stroke-width", .5).selectAll(".node");
  
  var attractForce = d3.forceManyBody().strength(20).distanceMax(40).distanceMin(60);
  var repelForce = d3.forceManyBody().strength(-10).distanceMax(50).distanceMin(10);

  simulation = d3.forceSimulation(nodes)
      .alphaDecay(0.03)
      // .force("attractForce",attractForce)
      .force("repelForce",repelForce)
      .force("x", d3.forceX(width/2))
      .force("y", d3.forceY(height/2))
      .force('collision', d3.forceCollide().radius(function(d) {
        return (radius+2);
      }))
      // .alphaTarget(.1)
      .on("tick", ticked);
  
  restart(0);
  
  function restart(split){
    if(split==0){
      
      node = node.data(nodes, function(d) { return d.id;});

      node.exit().remove();
      node = node.enter().append("circle").attr("fill", function(d) { return d.color; }).attr("r", radius).merge(node);

      simulation.nodes(nodes);

      simulation.alpha(1).restart();
    }else if(split==1){
              d3.select("#comments").html("Dots split");
              node = node.data(nodes, function(d) { return d.id;});

              node.exit().remove();

              node = node.enter().append("circle").attr("fill", function(d) { return d.color; }).attr("r", radius).merge(node);


              // Update and restart the simulation.
              simulation.nodes(nodes);
              simulation.force("y", d3.forceY(height/2))
                .force("A", isolate(d3.forceX(width), function(d) {
                    return (d.b == 2);
                }))
                .force("B", isolate(d3.forceX(0), function(d) {
                    return (d.b == 1);
                }))
                .on("tick", ticked);


            // simulation.alpha(1).restart();
          }else if(split==2){
            
              d3.select("svg").style("background-color", "#ffffdd");
              d3.select("#comments").html("Nothing happens here, but I'd like to clear out all the forces on the dots and have them return to the center");
            
              node = node.data(nodes, function(d) { return d.id;});

              node.exit().remove();

              node = node.enter().append("circle").attr("fill", function(d) { return d.color; }).attr("r", radius).merge(node);

              // Update and restart the simulation.
              simulation.nodes(nodes);
              simulation.force("x", d3.forceX(width/2))
                .force("y", d3.forceY(width/2))
                .on("tick", ticked);


            // simulation.alpha(1).restart();
          }
    
    
    
    function isolate(force, filter) {
      var initialize = force.initialize;
      force.initialize = function() { initialize.call(force, nodes.filter(filter)); };
      return force;
    }
    
   
  }
  
   setTimeout(function(){
        restart(1);
    }, 1000);
    
    setTimeout(function(){
        restart(2);
    }, 4000);
  
  function ticked() {
    node.attr("cx", function(d) { return d.x; })
        .attr("cy", function(d) { return d.y; })
  }
  
}
<head>
  <script src="https://d3js.org/d3.v4.js"></script>
  <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
</head>
<body>
  <p id="comments">Dots load</p>
</body>

As a secondary question, if anyone can explain to me why forceX()ing to 0 and the width does not bring the dots to the edges, that would also be useful. I imagine that is rooted in my misunderstanding of the above.


Solution

  • Keep in mind, that forces which are registered on a simulation will stay attached until you remove them by setting them to null:

    # simulation.force(name[, force])

    […]

    To remove the force with the given name, pass null as the force.

    Just adding new forces will not affect any previously added forces as long as their names differ. To manipulate a force, that has already been attached to the simulation, you need to re-register the force / its clone / a new force using the same name. Similarly, to de-register a force you set it to null.

    To remove your isolating forces "A" and "B" you need to do:

    simulation
      .force("A", null)
      .force("B", null);
    

    This also answers your second question, why using a d3.forceX with values 0 and width will not position the circles on the boundaries. All previously registered forces, namely "repelForce", "x", "y" and "collision", will still be applied while adding the new isolating forces "A" and "B". It is the combination of these six forces which determines the position of the circles in the second step.

    Have a look at the following working demo:

    var width = 400;
    var height = 150;
    var radius = 3;
    var data = [
      {"id":1, "a":1, "b":1, "color":"#ff0000"},
      {"id":2, "a":1, "b":2, "color":"#ff0000"},
      {"id":3, "a":2, "b":1, "color":"#00ff00"},
      {"id":4, "a":2, "b":2, "color":"#00ff00"},
      {"id":5, "a":3, "b":1, "color":"#0000ff"},
      {"id":6, "a":3, "b":2, "color":"#0000ff"},
    ];
    
    
    $(document).ready(function(){
      createGraph();
      makeForce();
    });
    
    var svg;
    
    function createGraph(){
        svg = d3.select("body")
            .append("svg")
            .attr("width", width)
            .attr("height", height)
            .style("background-color", "#dddddd");
    }
    
    var simulation;
    
    function makeForce(){
    
      
      var nodes=data;
      
      node = svg.append("g").attr("stroke", "#bbb").attr("stroke-width", .5).selectAll(".node");
      
      var attractForce = d3.forceManyBody().strength(20).distanceMax(40).distanceMin(60);
      var repelForce = d3.forceManyBody().strength(-10).distanceMax(50).distanceMin(10);
    
      simulation = d3.forceSimulation(nodes)
          .alphaDecay(0.03)
          // .force("attractForce",attractForce)
          .force("repelForce",repelForce)
          .force("x", d3.forceX(width/2))
          .force("y", d3.forceY(height/2))
          .force('collision', d3.forceCollide().radius(function(d) {
            return (radius+2);
          }))
          // .alphaTarget(.1)
          .on("tick", ticked);
      
      restart(0);
      
      function restart(split){
        if(split==0){
          
          node = node.data(nodes, function(d) { return d.id;});
    
          node.exit().remove();
          node = node.enter().append("circle").attr("fill", function(d) { return d.color; }).attr("r", radius).merge(node);
    
          simulation.nodes(nodes);
    
          simulation.alpha(1).restart();
        }else if(split==1){
                  d3.select("#comments").html("Dots split");
                  node = node.data(nodes, function(d) { return d.id;});
    
                  node.exit().remove();
    
                  node = node.enter().append("circle").attr("fill", function(d) { return d.color; }).attr("r", radius).merge(node);
    
    
                  // Update and restart the simulation.
                  simulation.nodes(nodes);
                  simulation.force("y", d3.forceY(height/2))
                    .force("A", isolate(d3.forceX(width), function(d) {
                        return (d.b == 2);
                    }))
                    .force("B", isolate(d3.forceX(0), function(d) {
                        return (d.b == 1);
                    }))
                    .on("tick", ticked);
    
    
                // simulation.alpha(1).restart();
              }else if(split==2){
                
                  d3.select("svg").style("background-color", "#ffffdd");
                  d3.select("#comments").html("Nothing happens here, but I'd like to clear out all the forces on the dots and have them return to the center");
                
                  node = node.data(nodes, function(d) { return d.id;});
    
                  node.exit().remove();
    
                  node = node.enter().append("circle").attr("fill", function(d) { return d.color; }).attr("r", radius).merge(node);
    
                  // Update and restart the simulation.
                  simulation.nodes(nodes);
                  simulation.force("x", d3.forceX(width/2))
                    .force("y", d3.forceY(height/2))
                    .force("A", null)
                    .force("B", null)
                    .on("tick", ticked);
    
    
                 simulation.alpha(1).restart();
              }
        
        
        
        function isolate(force, filter) {
          var initialize = force.initialize;
          force.initialize = function() { initialize.call(force, nodes.filter(filter)); };
          return force;
        }
        
       
      }
      
       setTimeout(function(){
            restart(1);
        }, 1000);
        
        setTimeout(function(){
            restart(2);
        }, 4000);
      
      function ticked() {
        node.attr("cx", function(d) { return d.x; })
            .attr("cy", function(d) { return d.y; })
      }
      
    }
    <head>
      <script src="https://d3js.org/d3.v4.js"></script>
      <script src="https://ajax.googleapis.com/ajax/libs/jquery/3.1.0/jquery.min.js"></script>
    </head>
    <body>
      <p id="comments">Dots load</p>
    </body>