score:10

Accepted answer

If I understand the question correctly:

Re-initializing Forces

The functions provided to set parameters of d3 forces such as forceX or forceCollision are executed once per node at initialization of the simulation (when nodes are originally assigned to the layout). This saves a lot of time once the simulation starts: we aren't recalculating force parameters every tick.

However, if you have an existing force layout and want to modify forceX with a new x value or new strength, or forceCollision with a new radius, for example, we can re-initialize the force to perform the recalculation:

 // assign a force to the force diagram:
 simulation.force("someForce", d3.forceSomeForce().someProperty(function(d) { ... }) )

// re-initialize the force
simulation.force("someForce").initialize(nodes);

This means if we have a force such as:

simulation.force("x",d3.forceX().x(function(d) { return fn(d["year"]); }))

And we update the variable year, all we need to do is:

year = "newValue";

simulation.force("x").initialize(nodes);

Positioning

If the forces are re-initialized (or re-assigned), there is no need to touch the tick function: it'll update the nodes as needed. Labels and circles will continue to be updated correctly.

Also, non-positional things such as color need to be updated in the event handler that also re-initializes the forces. Other than radius, most things should either be updated via the force or via modifying the elements directly, not both.

Radius is a special case:

  • With d3.forceCollide, radius affects positioning
  • Radius, however, does not need to be updated every tick.

Therefore, when updating the radius, we need to update the collision force and modify the r attribute of each circle.

If looking for a smooth transition of radius that is reflected graphically and in the collision force, this should be a separate question.

Implementation

I've borrowed from your code to make a fairly generic example. The below code contains the following event listener for some buttons where each button's datum is a year:

buttons.on("click", function(d) {
  // d is the year:
  year = d;

  // reheat the simulation:
  simulation
    .alpha(0.5)
    .alphaTarget(0.3)
    .restart();

  // (re)initialize the forces
  simulation.force("x").initialize(data);
  simulation.force("collide").initialize(data);

  // update altered visual properties:
  bubbles.attr("r", function(d) { 
      return radiusScale(d[year]);
    }).attr("fill", function(d) {
      return colorScale(d[year]);
    })
})

The following snippet uses arbitrary data and due to its size may not allow for nodes to re-organize perfectly every time. For simplicity, position, color, and radius are all based off the same variable. Ultimately, it should address the key part of the question: When year changes, I want to update everything that uses year to set node and force properties.

var data = [
  {year1:2,year2:1,year3:3,label:"a"},
  {year1:3,year2:4,year3:5,label:"b"},
  {year1:5,year2:9,year3:7,label:"c"},
  {year1:8,year2:16,year3:11,label:"d"},
  {year1:13,year2:25,year3:13,label:"e"},
  {year1:21,year2:36,year3:17,label:"f"},
  {year1:34,year2:1,year3:19,label:"g"},
  {year1:2,year2:4,year3:23,label:"h"},
  {year1:3,year2:9,year3:29,label:"i"},
  {year1:5,year2:16,year3:31,label:"j"},
  {year1:8,year2:25,year3:37,label:"k"},
  {year1:13,year2:36,year3:3,label:"l"},
  {year1:21,year2:1,year3:5,label:"m"}
];

// Create some buttons:
var buttons = d3.select("body").selectAll("button")
  .data(["year1","year2","year3"])
  .enter()
  .append("button")
  .text(function(d) { return d; })
  
  
// Go about setting the force layout:
var svg = d3.select("body")
  .append("svg")
  .attr("width", 500)
  .attr("height", 300);

var radiusScale = d3.scaleSqrt()
   .domain([0, 40])
   .range([5,30]);

var colorScale = d3.scaleLinear()
   .domain([0,10,37])
   .range(["#c7e9b4","#41b6c4","#253494"]);  

var year = "year1";
  
var simulation = d3.forceSimulation()
   .force("x", d3.forceX(function(d) {
         if (parseFloat(d[year]) >= 15) {
            return 100
         } else if (parseFloat(d[year]) > 5) {
            return 250
         } else {
            return 400
        }
   }).strength(0.05))
   .force("y", d3.forceY(150).strength(0.05))
   .force("collide", d3.forceCollide()
      .radius(function(d) {
        return radiusScale(d[year])
   }));
   
var bubbles = svg.selectAll("circle")
  .data(data)
  .enter().append("circle")
  .attr("r", function(d) {
     return radiusScale(d[year])
  })
  .attr("fill", function(d) {
    return colorScale(d[year]);
  });
  
var labels = svg.selectAll("text")
  .data(data)
  .enter()
  .append("text")
  .text(function(d) {
    return d.label;
  })
  .style("text-anchor","middle");
 
simulation.nodes(data)
  .on("tick", ticked) 
 
 
function ticked() {

  bubbles.attr("cx", function(d) {
    return d.x;
  }).attr("cy", function(d) {
    return d.y;
  })
  
  labels.attr("x", function(d) {
    return d.x;
  })
  .attr("y", function(d) {
    return d.y +5;
  })
  
}

buttons.on("click", function(d) {
  // d is the year:
  year = d;
  
  simulation
    .alpha(0.5)
    .alphaTarget(0.3)
    .restart();
    
  simulation.force("x").initialize(data);
  simulation.force("collide").initialize(data);

  bubbles.attr("r", function(d) { 
      return radiusScale(d[year]);
    }).attr("fill", function(d) {
      return colorScale(d[year]);
    })
})
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>


Related Query

More Query from same tag