Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Welcome To Ask or Share your Answers For Others

Categories

0 votes
295 views
in Technique[技术] by (71.8m points)

javascript - d3 force directed layout - link distance priority

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!

See Question&Answers more detail:os

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
Welcome To Ask or Share your Answers For Others

1 Reply

0 votes
by (71.8m points)

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>

与恶龙缠斗过久,自身亦成为恶龙;凝视深渊过久,深渊将回以凝视…
OGeek|极客中国-欢迎来到极客的世界,一个免费开放的程序员编程交流平台!开放,进步,分享!让技术改变生活,让极客改变未来! Welcome to OGeek Q&A Community for programmer and developer-Open, Learning and Share
Click Here to Ask a Question

...