d3 force directed layout - link distance priority
Asked Answered
P

1

8

Using a force-directed layout in d3, how do I make the link distance a priority, while still keeping a nice graph layout?

If I specify dynamic link distances, but keep the default charge, my graph distances are morphed a bit by the charge function, and are no longer accurate distances:

enter image description here

However, if I remove the charge, the graph looks like this:

enter image description here

Any advice appreciated!

Picardi answered 7/7, 2016 at 19:13 Comment(3)
see here likDistance and linkStrength bl.ocks.org/sathomas/774d02a21dc1c714def8 this should help you.Interstitial
Thanks Cyril for the plug. You can also read the full text of my book, including the chapter on D3, at my website: http://jsDataV.isV2
Hope you may have seen this github.com/d3/d3-force/blob/master/README.md#link_distance for v4 you can give a link distance plnkr.co/edit/12D55owSNuDnSH0hNfWu?p=info but i am not sure if you are looking for the above.Interstitial
N
3

If I understand correctly, I believe there is a potential solution.

To get link distance to be accurate, you need to set charge and collision forces to zero, but as your image suggests, then nodes aren't spaced in a way that accounts for other nodes, just those nodes which they share links with. As d3.force initializes nodes that do not have x,y values in a phyllotaxis arrangement, links and nodes will be clustered in unintended ways. But, if applying a repelling force throughout the simulation, the spacing is improved but the distances are distorted.

The possible solution is to use a repelling force initially because you need to separate nodes into recognizable clusters based on links. Then, after they are separated, reduce the repelling force to nothing so that the only force applied is in relation to the desired link distance.

This requires you to modify the forces in the tick function as the graph evolves. This also requires that all link distances are compatible with one another (a triangle of nodes can't have two corners separated by 100 pixels and the remaining corner connected to the other two by 10 pixels).

Something like this might work within the tick function in simple situations:

var alpha = this.alpha();   // starts at 1 by default, simulation ends at zero

var chargeStrength; // a multiplier for charge strength

if ( alpha > 0.2 ) {
    chargeStrength = (alpha - 0.2 / 0.8); // decrease for the first portion of the simulation
}
else {
    chargeStrength = 0; // leave at zero and give the link distance force time to work without competing forces
}

For more complex visualizations, you could allow more time for cool down by decreasing alphaDecay, or increase it for simpler ones.

I've made a simple example here, at the end of the visualization distances are logged (I've increased alphaDecay in the snippet below to speed it up at the cost of precision, but it's still pretty good) and referenced with desired distances.

var graph = {
  nodes: d3.range(15).map(Object),
  links: [
    {source:  0, target:  1, distance: 20 },
    {source:  0, target:  2, distance: 40},
    {source:  0, target:  3, distance: 80},
    {source:  1, target:  4, distance: 20},
    {source:  1, target:  5, distance: 40},
    {source:  1, target:  6, distance: 80},
    {source:  2, target:  7, distance: 12},
    {source:  2, target:  8, distance: 8},
    {source:  2, target:  9, distance: 6},
    {source:  3, target:  10, distance: 10},
    {source:  3, target:  11, distance: 10},
    {source:  3, target:  12, distance: 2},
	{source:  3, target:  13, distance: 2},
	{source:  3, target:  14, distance: 2}
  ]
};

var svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height");

var color = d3.scaleOrdinal(d3.schemeCategory20);

var simulation = d3.forceSimulation()
    .force("charge", d3.forceManyBody().strength(-30 ))
	.force("link", d3.forceLink().distance(function(d) { return d.distance } ).strength(2) )
    .force("center", d3.forceCenter(width / 2, height / 2))
	.force("collide",d3.forceCollide().strength(0).radius(0))
	.alphaDecay(0.03)
    .velocityDecay(0.4);
	
	
	
  var link = svg.append("g")
      .attr("class", "links")
    .selectAll("line")
    .data(graph.links)
    .enter().append("line")
      .attr("stroke-width", 1);

  var node = svg.append("g")
     .attr("class", "nodes")
    .selectAll("circle")
    .data(graph.nodes)
    .enter().append("circle")
     .attr("r", 3)
	  
 simulation
      .nodes(graph.nodes)
      .on("tick", ticked);

  simulation.force("link")
      .links(graph.links);

  
  
	  
  function ticked() {
	
	var alpha = this.alpha();
	var chargeStrength;

    if ( alpha > 0.2 ) {
		chargeStrength = (alpha - 0.2 / 0.8);
	}
	else {
		chargeStrength = 0;
	}

	this.force("charge", d3.forceManyBody().strength( -30 * chargeStrength ))
	
	
    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; });
		
	// validate:
	if (alpha < 0.001) {
		link.each(function(d,i) {
		
			var a = d.source.x - d.target.x;
			var b = d.source.y - d.target.y;
		    var c = Math.pow(a*a + b*b, 0.5);
			
			console.log("specified length: " + graph.links[i].distance + ", realized distance: " + c );
		})
	}
  }
.links line {
  stroke: #999;
  stroke-opacity: 0.6;
}

.nodes circle {
  stroke: #fff;
  stroke-width: 1.5px;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.10.0/d3.min.js"></script>
<svg width="500" height="300"></svg>

Depending on the complexity of the graph, you might need to tailor cool down time, the repelling force strength and how you alter it as alpha cools, velocityDecay (potentially modifying it in the tick function), and/or the distance force itself.

Needlecraft answered 2/11, 2017 at 21:7 Comment(3)
That's one nice solution! It never occurred to me to modify the forces on the run. I will definitely keep this in mind as it might turn out helpful in other applications as well. The bounty was very well earned!Harrovian
The one thing I cannot figure out, though, is the fact that you are overriding the "link" force in the simulation's initial setup. Since only the last force for the name will be used, this seems somewhat redundant. Could you shed some light on this?Harrovian
That's a pretty bad oversight on my part, not sure why I did that. Thanks for pointing it out.Needlecraft

© 2022 - 2024 — McMap. All rights reserved.