Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Precalculate and set initial positions of nodes in D3.js

I'm trying to pre-calculate the positions of the stable force directed graph using igraph and pass them into my d3.js graph. This is due to the size of the dataset I will be using which means I cannot rely on the client not to freeze if the full force calculation is done on client-side. I have the positions in JSON format and have used linear scales in order to make them useful in d3.js.

var positions = 
{"positions": [{"x":"-68.824367374", "y": "-6.10824525755"},
{"x":"-80.8080803911", "y": "-3.38997541264"},
{"x":"6.75334817585", "y": "-49.6040729697"},
{"x":"14.6608797291", "y": "-81.8897019921"},
....
var force = d3.layout.force()
                .charge(-100)
                .linkDistance(3)
                .size([width, height])
                .nodes(data.nodes)
                .links(data.links)
                .start();

var posX = d3.scale.linear()
                .range([0,960])
                .domain([-125,120]);

var posY = d3.scale.linear()
                .range([0,500])
                .domain([-125,125]);

And this is the way I've tried to do it. I've experimented with px and py but the results are the same. It's as if this code is never ran. If I throw in a console.log where seen below, the value does not get printed. This is regardless of where I put this code, be it before or after starting the force.

force.on("start", function() {
                    node
                    .data(positions.positions)
                    .attr("cx", function(d) {
                            console.log(posX(d.x));
                            return posX(d.x);
                        })
                        .attr("cy", function(d) {
                            return posY(d.y);
                        })
                });

Why does the on start event not set the initial positions of my nodes? They seem to be initialised randomly still. Alternatively, what's a good way of pre-calculating the stable state of a d3.js force directed graph? I've had a look at doing it on Phantomjs but gave up.

Thank you for taking the time to read my question!

EDIT

Here is a dumbed down example: https://jsfiddle.net/xp0zgqes/ If you run it a few times and pay attention to the starting positions of the nodes you can see they are randomly initialised.

like image 305
Sebastian Smolorz Avatar asked Apr 18 '16 22:04

Sebastian Smolorz


1 Answers

Really don't like answering my own question but I think this will be really useful. Firstly, (this may be specific in my case only) if the initial position data is coming from a different data object than the node/link data, it saves you having to bind to two data sources if you just combine it (or you can be smarter than me and just create the original json object with x and y positions). In my case:

for (var i = 0; i < data.nodes.length; i++){
                    data.nodes[i].x = posX(positions.positions[i].x);
                    data.nodes[i].y = posY(positions.positions[i].y);
                    data.nodes[i].newAttribute = value;
                }

Thanks to the magic of JSON it is that easy to add new attributes to the data which is good e.g. if you want your nodes to be fixed.

The issue seems to be with the force.start() call. If called when initialising the force like so:

var force = d3.layout.force()
                .charge(-100)
                .linkDistance(3)
                .size([width, height])
                .nodes(data.nodes)
                .links(data.links)
                .start();

the layout will randomly initialise the nodes by default. The way I found of getting around that is by adding an on tick event and start the force later.

var force = d3.layout.force()
                    .charge(-100)
                    .linkDistance(3)
                    .size([width, height])
                    .nodes(data.nodes)
                    .links(data.links)
                    .on("tick", tick);

                    force.start(); //Note, not chained


//Standard code that goes in the on.tick event
function tick() {
  link.attr("x1", function(d) { return d.source.x; })
      .attr("y1", function(d) { return d.source.y; })
      .attr("x2", function(d) { return d.target.x; })
      .attr("y2", function(d) { return d.target.y; });

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

And Voilà! We now have a fully functional force directed graph with initial positions set from json.

Working fiddle: https://jsfiddle.net/zbxbzmen/

like image 134
Sebastian Smolorz Avatar answered Oct 21 '22 02:10

Sebastian Smolorz