Home > Net >  How to move a node from one simulation to another
How to move a node from one simulation to another

Time:01-10

I'm trying to move nodes from one forceSimulation to another, and I'm expecting the nodes to leave one cluster and drift over to another. However, it seems that once the simulation has settled into a stable pattern the forces stop being applied.

Here's the codepen: Migrating Nodes

And a snippet:

const width = 600;
const height = 200;

var nodes;
var simulations;

function loaded() {
  d3.select("svg")
    .attr("width", width)
    .attr("height", height)
  test();
}

function test() {

  nodes = [];
  nodes[0] = d3.range(50).map(function() {
    return {
      radius: 4,
      color: "blue"
    };
  })

  nodes[1] = d3.range(50).map(function() {
    return {
      radius: 4,
      color: "red"
    };
  })

  simulations = [null, null];

  update(0);
  update(1);

  setTimeout(startMigration, 1000);

}


function update(index) {

  simulations[index] = d3.forceSimulation(nodes[index])
    .force('charge', d3.forceManyBody().strength(10))
    .force('x', d3.forceX().x(function(d) {
      return width * (index   1) / 3;
    }))
    .force('y', d3.forceY().y(function(d) {
      return height / 2;
    }))
    .force('collision', d3.forceCollide().radius(function(d) {
      return d.radius;
    }))
    .on('tick', ticked);

  function ticked() {
    var u = d3.select('svg')
      .selectAll('.circle'   index)
      .data(nodes[index])
      .join('circle')
      .attr('class', 'circle'   index)
      .attr('r', function(d) {
        return d.radius;
      })
      .style('fill', function(d) {
        //return colorScale(d.category);
        return d.color;
      })
      .attr('cx', function(d) {
        return d.x;
      })
      .attr('cy', function(d) {
        return d.y;
      });
  }

}


function startMigration() {
  setInterval(function() {
    if (nodes[1].length > 10) {
      nodes[0].push(nodes[1].pop());

      d3.select('svg')
        .selectAll('.circle0')
        .data(nodes[0]);
      simulations[0].nodes(nodes[0]);

      d3.select('svg')
        .selectAll('.circle1')
        .data(nodes[1]);
      simulations[1].nodes(nodes[1]);

    }
  }, 250);
}
<html lang="en">

<head>
  <meta charset="UTF-8" />

  <meta http-equiv="X-UA-Compatible" content="ie=edge" />

  <script src="https://d3js.org/d3.v5.min.js"></script>
</head>

<body onl oad="loaded()">
  <div id='layout'>
    <div id='container'>
      <svg id="graph" />
    </div>
  </div>

</body>

</html>

nodes is an array of two datasets, and simulations is an array of two simulations.

Here is where I create the forceSimulations:

function update(index) {
    
    simulations[index] = d3.forceSimulation(nodes[index])
        .force('charge', d3.forceManyBody().strength(10))
        .force('x', d3.forceX().x(function(d) {
            return width * (index   1) / 3;
        }))
        .force('y', d3.forceY().y(function(d) {
            return height/2;
        }))
        .force('collision', d3.forceCollide().radius(function(d) {
            return d.radius;
        }))
        .on('tick', ticked);

    function ticked() {
        var u = d3.select('svg')
            .selectAll('.circle'   index)
            .data(nodes[index])
            .join('circle')
            .attr('class', 'circle'   index)
            .attr('r', function(d) {
                return d.radius;
            })
            .style('fill', function(d) {
                //return colorScale(d.category);
                return d.color;
            })
            .attr('cx', function(d) {
                return d.x;
            })
            .attr('cy', function(d) {
                return d.y;
            });
    }

}

A this is the code that starts getting called periodically 1000ms after the simulation starts to move nodes from one simulation to the other:

nodes[0].push(nodes[1].pop());
            
d3.select('svg')
   .selectAll('.circle0')
   .data(nodes[0]);
simulations[0].nodes(nodes[0]);
            
d3.select('svg')
   .selectAll('.circle1')
   .data(nodes[1]);
simulations[1].nodes(nodes[1]);

The effect I was hoping for is that nodes would leave the red cluster and drift over to join the blue cluster. Why do they slow down and stop? The problem gets worse the longer I wait after the initial load. I get the feeling there's something fundamental about forceSimulation I'm not understanding. I would have thought that they would get as close as they could to the (forceX, forceY) of the simulation to which they belong.

A secondary question is whether all the steps in the code snippet above are necessary. Do I need to reassign data to both selections, and reassign nodes to both simulations?

CodePudding user response:

Lots of force simulations with user interaction 'reheat' the simulation during drag operations with simulation.alpha(n).restart(). See this observable for example.

You can do this without user interaction in setMigration function to keep the simulation animation updated (keep it 'warm' so to speak) until sufficient circles have moved from 'red' to 'blue'. The two lines to add are:

simulations[0].alpha(0.15).restart();
simulations[1].alpha(0.15).restart();

If you increase 0.15 (upto 1) you will see that both simulations are more 'excitable' as the migration occurs. 0.15 seemed like a pleasant visual experience for me. This seems to work without the .restart() so you can play around with that too likely depending on how long you wait to start setMigration.

For your second question - since each simulation was initialised to nodes[0] and nodes[1] then the d3.select('svg').selectAll('.circleN') can be commented out because you already changed the array content with nodes[0].push(nodes[1].pop()).

Working example below:

const width = 600;
const height = 200;

var nodes;
var simulations;

function loaded() {
  d3.select("svg")
    .attr("width", width)
    .attr("height", height)
  test();
}

function test() {

  nodes = [];
  nodes[0] = d3.range(50).map(function() {
    return {
      radius: 4,
      color: "blue"
    };
  })

  nodes[1] = d3.range(50).map(function() {
    return {
      radius: 4,
      color: "red"
    };
  })

  simulations = [null, null];

  update(0);
  update(1);

  setTimeout(startMigration, 1000);

}


function update(index) {

  simulations[index] = d3.forceSimulation(nodes[index])
    .force('charge', d3.forceManyBody().strength(10))
    .force('x', d3.forceX().x(function(d) {
      return width * (index   1) / 3;
    }))
    .force('y', d3.forceY().y(function(d) {
      return height / 2;
    }))
    .force('collision', d3.forceCollide().radius(function(d) {
      return d.radius;
    }))
    .on('tick', ticked);

  function ticked() {
    var u = d3.select('svg')
      .selectAll('.circle'   index)
      .data(nodes[index])
      .join('circle')
      .attr('class', 'circle'   index)
      .attr('r', function(d) {
        return d.radius;
      })
      .style('fill', function(d) {
        //return colorScale(d.category);
        return d.color;
      })
      .attr('cx', function(d) {
        return d.x;
      })
      .attr('cy', function(d) {
        return d.y;
      });
  }

}


function startMigration() {
  setInterval(function() {
    if (nodes[1].length > 10) {
      nodes[0].push(nodes[1].pop());

      //d3.select('svg')
      //  .selectAll('.circle0')
      //  .data(nodes[0]);
      simulations[0].nodes(nodes[0]);

      //d3.select('svg')
      //  .selectAll('.circle1')
      //  .data(nodes[1]);
      simulations[1].nodes(nodes[1]);
      
      // reheat the simulations
      simulations[0].alpha(0.15).restart();
      simulations[1].alpha(0.15).restart();

    }
  }, 250);
}
<html lang="en">

<head>
  <meta charset="UTF-8" />

  <meta http-equiv="X-UA-Compatible" content="ie=edge" />

  <script src="https://d3js.org/d3.v5.min.js"></script>
</head>

<body onl oad="loaded()">
  <div id='layout'>
    <div id='container'>
      <svg id="graph" />
    </div>
  </div>

</body>

</html>

  • Related