Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calm down initial tick of a force layout

I've just started dabbling with d3, and find the learning curve quite steep. The process is completely different than what I am used to, and the mathematics are mostly way over my head.

Anyway, my project consists of a force layout representing map of integrations between systems. This part works out awesomely well, but I do have one major concern, which is also represented in the force directed layout demo on Michael Bostocks site: When nodes are initiated, they seem to be rendered off canvas. After this, some serious physics math is taking over, simulating a gravitational pull which sends the nodes on a rather confusing path back and forth until they calm down and settle on some random coordinates. Although these movements are ripping cool the first time the demo is run, when you are trying to view the status of network interfaces from a company it admins point of view and the servers just wont stay still, it gets tiresome after a while.

I am sure I have the correct layout setup for this project, because I do want the servers to autolayout, I do want to visualize links between them. I am however ambivalent in regards to the gravitation effect.

I wonder; is it possible to set the initial position of each node manually, so that I can put them closer to the gravitational center and shorten the "bounce time" a bit?

like image 451
Øystein Amundsen Avatar asked Nov 19 '12 22:11

Øystein Amundsen


5 Answers

All the answers above misunderstood Øystein Amundsen's question.

The only way to stabilize the force upon it starts is to set node.x and node.y a proper value. Please note that the node is the data of d3.js, not the represented DOM type.

For example, if you load

nodes = [{"id": a}, {"id": b}, {"id": c}]

into

d3.layout.force().nodes(nodes)

you have to set all .x and .y of all elements in array of nodes it will be like this ( in coffeescript )

nodes = [{"id": a}, {"id": b}, {"id": c}]
for i in [0..2]
  nodes[i].x = 500 #the initial x position of node
  nodes[i].y = 300 #the initial y position of node
d3.layout.force().nodes(nodes).links(links)

then the nodes will start at the position when force.start(). this would avoid the chaos.

like image 187
Henry Chen Avatar answered Nov 15 '22 18:11

Henry Chen


Internally, under "normal" usage, the force layout repeatedly calls its own tick() method asynchronously (via a setInterval or requestAnimationFrame), until the layout settles on a solution. At that point its alpha() value equals or approaches 0.

So, if you want to "fast forward" through this solution process, you can synchronously call that tick() method over and over until the layout's alpha reaches a value that, for your specific requirements, constitutes a "close enough" solution. Like so:

var force = d3.layout.force(),
    safety = 0;
while(force.alpha() > 0.05) { // You'll want to try out different, "small" values for this
    force.tick();
    if(safety++ > 500) {
      break;// Avoids infinite looping in case this solution was a bad idea
    }
}

if(safety < 500) {
  console.log('success??');
}

After this code runs, you can draw your layout based on the state of the nodes. Or, if you're drawing your layout by binding to the tick event (ie force.on('tick', drawMyLayout)), you'll want to do the binding after this code runs, because otherwise you'll needlessly render the layout hundreds of times synchronously during the while loop.

JohnS has boiled down this approach to a single concise function. See his answer somewhere on this page.

like image 31
meetamit Avatar answered Nov 15 '22 19:11

meetamit


I dealt with something a little like this a while ago. There are a few things to consider.

1) The iterating ticks are simulating a system that comes to equilibrium. So there is no way to avoid calling tick as many times as needed before the system settles and you have your auto-layout. That said, you do not need to update you visualization every tick for the simulation to work! The iterations will go a lot faster, in fact, if you don't. The relevant part of my code goes:

var iters = 600; // You can get decent results from 300 if you are pressed for time
var thresh = 0.001;
if(!hasCachedLayout || optionsChanged || minorOptionsChanged) {
    force.start(); // Defaults to alpha = 0.1
    if(hasCachedLayout) {
        force.alpha(optionsChanged ? 0.1 : 0.01);
    }
    for (var i = iters; i > 0; --i) {
        force.tick();
        if(force.alpha() < thresh) {
            //console.log("Reached " + force.alpha() + " for " + data.nodes.length + " node chart after " + (iters - i) + " ticks.");
            break;
        }
    }
    force.stop();
}

This runs synchronously and after it has run I create the dom elements for all the nodes and links. For small graphs this runs quite fast, but you will find there is a delay for larger graphs (100+ nodes) - they are simply much more computationally expensive.

2) You can cache / seed layouts. The force layout will uniformly distribute the nodes upon initialization if no position is set! So if you make sure your nodes have set x and y attribute these will be used. In particular when I am updating an existing graph I will re-use the x and y positions from a previous layout.

You will find with a good initial layout you will need a lot fewer iterations to reach a stable configuration. (This is what hasCachedLayout tracks in the code above). NB: If you are re-using the same nodes form the same layout then you will also have to make sure to set px and py to NaN or you will get weird results.

like image 11
Superboggly Avatar answered Nov 15 '22 17:11

Superboggly


Based on other answers I have made this method:

function forwardAlpha(layout, alpha, max) {
  alpha = alpha || 0;
  max = max || 1000;
  var i = 0;
  while(layout.alpha() > alpha && i++ < max) layout.tick();
}
like image 8
Ali Shakiba Avatar answered Nov 15 '22 17:11

Ali Shakiba


Maybe force.friction(0.5), or some other number lower than the default 0.9, would help? At least it gives a less chaotic impression upon page load.

like image 4
Anton Avatar answered Nov 15 '22 18:11

Anton