5

I'm trying to render a d3js force simulation but I'd like to ensure my nodes don't relay false information.

With the following code used to display the nodes but due to the dynamic nature of force layouts, it occasionally pushes some nodes out of its appropriate x-coordinate location.

 inOrder(){
  this.simulation
    .force("x", d3.forceX(d => this.xScale(d.value)))
    .force("y",  d3.forceY(this.height / 2))
    .alpha(1).restart();
},

Here is an egregious example of this happening: The numbers should be in order from left to right. Out of order nodes

I made an attempt to use the fx property on a node to lock the position in place:

inOrder(){
  this.releases.forEach(x => {
    x.fx = this.xScale(x.value)
  })

  this.simulation
    .force("x", d3.forceX(d => this.xScale(d.value)))
    .force("y",  d3.forceY(this.height / 2))
    .alpha(1).restart();
},

Nodes in order

This works as intended for preserving the x position but when the inOrder method is called, the nodes instantly jump to their final x position. This ruins the fluid and dynamic nature of the force simulation.

Is there a way to get the best of both worlds? Perhaps by using the .on("end", () => {}) or the .on("tick", () => {})? event handlers?

Mike Bostock (https://stackoverflow.com/users/365814/mbostock) and Shan Carter created some of the work that serves as the inspiration to what I'm trying to do here:

Click between the Changes and Department totals tabs https://archive.nytimes.com/www.nytimes.com/interactive/2012/02/13/us/politics/2013-budget-proposal-graphic.html?hp

Click betweeen The Overall Picture and the View By Industry tabs https://archive.nytimes.com/www.nytimes.com/interactive/2013/05/25/sunday-review/corporate-taxes.html

RoboKozo
  • 4,981
  • 5
  • 51
  • 79

1 Answers1

2

I may be missing something here, but tinkering with the strength of the x positioning force (and the y) can help ensure that your ordering is completed properly. The default strength of forceX or forceY is 0.1, the strength is implemented as follows:

a value of 0.1 indicates that the node should move a tenth of the way from its current x-position to the target x-position with each application. Higher values moves nodes more quickly to the target position, often at the expense of other forces or constraints. A value outside the range [0,1] is not recommended. (docs)

So we could increase the forceX strength, and to allow freer movement of nodes on the x axis we could decrease the forceY - allowing nodes to hop over each other with greater ease - decreasing the collision strength could help too.

I don't label the circle below (instead they are sequentially shaded), but I do run a check to see if they are in order (logs to console on end of simulation), the below snippet modifies only the x and y forces (not the collision force):

enter image description here

var height = 300;
var width = 500;

var data = d3.range(30).map(function(d,i) {
  return { size: Math.random()+1, index: i}
});

    
var svg = d3.select("body")
  .append("svg")
  .attr("width",width)
  .attr("height",height);
  
var x = d3.scaleLinear()
  .domain([1,30])
  .range([50,width-50]);
  
var color = d3.scaleLinear()
  .domain([0,29])
  .range(["#ccc","#000"])

var simulation = d3.forceSimulation()
    .force("x", d3.forceX(d => x(d.index)).strength(0.20))
    .force("y",  d3.forceY(height / 2).strength(0.05))
    .force("collide", d3.forceCollide().radius(d=> d.size*10))
    .alpha(1).restart();
    
var circles = svg.selectAll(null)
  .data(data)
  .enter()
  .append("circle")
  .attr("r", function(d) { return d.size * 10; })
  .attr("fill", function(d,i) { return color(i); })
    
simulation.nodes(data)
  .on("tick",tick)
  .on("end",verify);
  

function tick() {
  circles
  .attr("cx", function(d) { return d.x; })
  .attr("cy", function(d) { return d.y; })
}

function verify() {
  var n = 0;
  for(var i = 0; i < data.length - 1; i++) {
     if(data[i].x > data[i+1].x) n++;
  }
  console.log(n + " out of place");
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>

The snippet places 30 circles in a 500x300 area without much issue: I've tested a handful of times with 0 out of place. Placing 100 circles in here will cause issues: the circles will not be able to change places in such a cramped area: further modification of the forces might be required, but a larger size plot might be preferential (as opposed to a tiny snippet view) too.


Another option would be to modify forces throughout the maturation of the simulation: start with strong x force strength and low collision force strength, then dial up collision slowly so that subsequent jostling is minimized. Here's an example of modifying the forces in the tick function - though, this example is for link length rather than placement on the x - but adaptation shouldn't be too hard.

Yet another possibility would be to keep alpha high until all circles are properly ordered on the x axis and then begin to cool down, again this would have to occur in the tick function.

Andrew Reid
  • 37,021
  • 7
  • 64
  • 83
  • You gave me a lot to try out. Adjusting the strength of the forceX seems to help... but it's way too "fast"... a very jarring experience – RoboKozo May 29 '19 at 21:09
  • @Robodude, Yeah, small changes in forceX, forceY can drastically alter outcomes - if you note your plot size and how many circles (and average radius) I might be able to show a better example. – Andrew Reid May 29 '19 at 21:13
  • So your ideas of manipulating the force/collision within a tick seems really interesting. I'm testing them out right now and I might be able to make it happen. – RoboKozo May 29 '19 at 21:18
  • Doing this and am pretty satisfied so far. .on("tick", () => { const increasingWithTime = 1 - this.simulation.alpha(); this.collider.strength(increasingWithTime); forceX.strength(increasingWithTime); }) – RoboKozo May 29 '19 at 21:28
  • This might be getting me closer but my x axis is going to map to dates and they are very important. I dont know if I can leave it up to the simulation to get me "close enough" – RoboKozo May 29 '19 at 21:31
  • I think if I set the fx value within the tick after the alpha gets to a certain point will be sufficient. Thank you for your input. – RoboKozo May 29 '19 at 21:44