首页 > 解决方案 > 设置节点之间的固定距离 d3-force

问题描述

我正在尝试使用 d3.js 重现拉线灯开关的行为。

您可以在代码片段中看到此处或下方运行的代码(最好全屏查看)。

我的问题是如何将节点之间的距离设置为始终相同(就像在真正的绳索中一样)?

唯一应该伸展的链接是两个绿色节点之间的链接

我试图为部队增加更多力量,但看起来并不好。

//create somewhere to put the force directed graph
  const svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height");

  const nodes_data = [
    { name: "c1" },
    { name: "c2" },
    { name: "c3" },
    { name: "c4" },
    { name: "c5" },
    { name: "c6" },
  ];

  const links_data = [
    { source: "c1", target: "c2" },
    { source: "c2", target: "c3" },
    { source: "c3", target: "c4" },
    { source: "c4", target: "c5" },
    { source: "c5", target: "c6" },
  ];

  //set up the simulation

  const simulation = d3.forceSimulation().nodes(nodes_data);

  //add forces

  simulation.force(
    "manyBody",
    d3.forceManyBody().distanceMin(20).distanceMax(21)
  );

  const link_force = d3.forceLink(links_data).distance(40).strength(1);
  link_force.id(function (d) {
    return d.name;
  });

  simulation.force("links", link_force);
  simulation.force("centerx", d3.forceX(width / 2).strength(0.3));
  simulation.force(
    "centery",
    d3
      .forceY()
      .y(function (d, i) {
        return height / 10 + i * 35;
      })
      .strength(function (d, i) {
        return 0.4;
      })
  );

  //draw circles for the nodes
  const node = svg
    .append("g")
    .attr("class", "nodes")
    .selectAll("circle")
    .data(nodes_data)
    .enter()
    .append("circle")
    .attr("r", 10)
    .attr("fill", "red")
    .attr("draggable", "true");

  const circles = d3.selectAll("circle")._groups[0];
  const firstCircle = d3.select(circles[0]);
  const secondCircle = d3.select(circles[1]);
  const lastCircle = d3.select(circles[circles.length - 1]);
  firstCircle.attr("fill", "green").text(function (d) {
    d.fx = width / 2;
    d.fy = height / 10;
    console.log(d.fx, d.fy);
  });

  secondCircle.attr("fill", "green");
  lastCircle.attr("fill", "blue");

  //draw lines for the links
  const link = svg
    .append("g")
    .attr("class", "links")
    .selectAll("line")
    .data(links_data)
    .enter()
    .append("line")
    .attr("stroke-width", 2);

  // The complete tickActions() function
  function tickActions() {
    //update circle positions each tick of the simulation
    node
      .attr("cx", function (d) {
        return d.x;
      })
      .attr("cy", function (d) {
        return d.y;
      });

    //update link positions
    //simply tells one end of the line to follow one node around
    //and the other end of the line to follow the other node around
    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;
      });
  }

  simulation.on("tick", tickActions);

  const drag_handler = d3
    .drag()
    .on("start", drag_start)
    .on("drag", drag_drag)
    .on("end", drag_end);

  function drag_start(event, d) {
    if (!event.active) simulation.alphaTarget(0.3).restart();
    d.fx = d.x;
    d.fy = d.y;
  }

  function drag_drag(event, d) {
    d.fx = event.x;
    d.fy = event.y;
  }

  function drag_end(event, d) {
    if (!event.active) simulation.alphaTarget(0);
    d.fx = null;
    d.fy = null;
    d3.forceY().strength(0.1);
    document.body.style.background == "black"
      ? (document.body.style.background = "white")
      : (document.body.style.background = "black");
    console.log(document.body.style.background == "black");
  }

  drag_handler(lastCircle);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg width="400" height="400"></svg>

谢谢

标签: javascriptd3.jsforce-layout

解决方案


如果不修改力布局的工作方式,D3 不可能创建完美的解决方案。保持在 D3 的范围内,我有一个可以达到预期结果的解决方案(具有最小的弹性,这可能是可以接受的)。

正如我在评论中指出的那样,d3 在模拟运行时正在平衡一堆力。因此,最终的布局是力量之间的妥协。我在评论中链接的解决方案通过在模拟冷却时调低所有其他力来获取指定链接的链接,允许其他力影响总体布局,而链接距离力会调整结果以确保链接正确长度。

此处可以应用相同的原理,但没有多个循环将节点微移到所需的精确位置的好处。

首先,我们像往常一样声明我们所有的力量:

  var manybody = d3.forceManyBody().distanceMin(20).distanceMax(21);
  var x =  d3.forceX(width / 6).strength(0.3)
  var y =  d3.forceY().y(function (d, i) { return height / 10 + i * 35; })
                      .strength(0.4)
  var distance = d3.forceLink(links_data.filter(function(d,i) { return i; }))
     .distance(35)
     .id(function(d) { return d.name; })
     .strength(1);

然后我们应用它们:

  simulation
      .force("centerx",x)
      .force("centery",y)
      .force("link", distance)
      .force("many", manybody);

然后在拖动开始函数中,我们移除除了链接距离函数之外的所有力。我们还提高了 alpha 并消除了 alpha 衰减,以允许力在一个刻度内将节点尽可能靠近它们的预期位置:

  function drag_start(event, d) {
    if (!event.active) simulation.alphaTarget(0.3).restart();
    d.fx = d.x;
    d.fy = d.y;
    // Disable other forces:
    simulation.force("centerx",null)
      .force("centery",null)
      .force("many",null);
      
    // Juice the alpha:
    simulation.alpha(1)
      .alphaDecay(0)
    
  }

在拖动结束时,我们通过重新施加力、减小 alpha 和增加 alpha 衰减来撤消对拖动开始所做的更改:

 function drag_end(event, d) {
    // Reapply forces:
    simulation.force("centerx",x)
      .force("centery",y)
      .force("many",manybody);  
    // De-juice the alpha:
    simulation.alpha(0.2)
      .alphaDecay(0.0228)    

 ...

与规范的 D3 相比,代码中有一些特殊之处,但我刚刚实现了上面的更改:

//create somewhere to put the force directed graph
  const svg = d3.select("svg"),
    width = +svg.attr("width"),
    height = +svg.attr("height");

  const nodes_data = [
    { name: "c1" },
    { name: "c2" },
    { name: "c3" },
    { name: "c4" },
    { name: "c5" },
    { name: "c6" },
  ];

  const links_data = [
    { source: "c1", target: "c2" },
    { source: "c2", target: "c3" },
    { source: "c3", target: "c4" },
    { source: "c4", target: "c5" },
    { source: "c5", target: "c6" },
  ];

  //set up the simulation
  const simulation = d3.forceSimulation().nodes(nodes_data);

  ////////////////////////
  // Changes start: (1/2)

  // Set up forces:
  var manybody = d3.forceManyBody().distanceMin(15).distanceMax(15);
  var x =  d3.forceX(width / 6).strength(0.3)
  var y =  d3.forceY().y(function (d, i) { return 0 + i * 35; })
                      .strength(0.4)
  var distance = d3.forceLink(links_data.filter(function(d,i) { return i; }))
     .distance(35)
     .id(function(d) { return d.name; })
     .strength(1);

  simulation
      .force("centerx",x)
      .force("centery",y)
      .force("link", distance)
      .force("many", manybody);
  // End Changes (1/2)
  /////////////////////////
 
 
  //draw circles for the nodes
  const node = svg
    .append("g")
    .attr("class", "nodes")
    .selectAll("circle")
    .data(nodes_data)
    .enter()
    .append("circle")
    .attr("r", 10)
    .attr("fill", "red")
    .attr("draggable", "true");

  const circles = d3.selectAll("circle")._groups[0];
  const firstCircle = d3.select(circles[0]);
  const secondCircle = d3.select(circles[1]);
  const lastCircle = d3.select(circles[circles.length - 1]);
  firstCircle.attr("fill", "green").text(function (d) {
    d.fx = width / 6;
    d.fy = 0;
    console.log(d.fx, d.fy);
  });

  secondCircle.attr("fill", "green");
  lastCircle.attr("fill", "blue");

  //draw lines for the links
  const link = svg
    .append("g")
    .attr("class", "links")
    .selectAll("line")
    .data(links_data)
    .enter()
    .append("line")
    .attr("stroke-width", 2);

  // The complete tickActions() function
  function tickActions() {
    //update circle positions each tick of the simulation
    node
      .attr("cx", function (d) {
        return d.x;
      })
      .attr("cy", function (d) {
        return d.y;
      });

    //update link positions
    //simply tells one end of the line to follow one node around
    //and the other end of the line to follow the other node around
    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;
      });
  }

  simulation.on("tick", tickActions);

  const drag_handler = d3
    .drag()
    .on("start", drag_start)
    .on("drag", drag_drag)
    .on("end", drag_end);

  ////////////////////////
  // Start changes (2/2)
  function drag_start(event, d) {
    if (!event.active) simulation.alphaTarget(0.3).restart();
    d.fx = d.x;
    d.fy = d.y;
    // Disable other forces:
    simulation.force("centerx",null)
      .force("centery",null)
      .force("many",null);
      
    // Juice the alpha:
    simulation.alpha(1)
      .alphaDecay(0)
    
  }

  function drag_drag(event, d) {
    d.fx = event.x;
    d.fy = event.y;
  }

  function drag_end(event, d) {
    // Reapply forces:
    simulation.force("centerx",x)
      .force("centery",y)
      .force("many",manybody);  
    // De-juice the alpha:
    simulation.alpha(0.2)
      .alphaDecay(0.0228)    

    if (!event.active) simulation.alphaTarget(0);
    d.fx = null;
    d.fy = null;
    d3.forceY().strength(0.1);
    document.body.style.background == "black"
      ? (document.body.style.background = "white")
      : (document.body.style.background = "black");
  }
  // End changes (2/2)
  ////////////////////////

  drag_handler(lastCircle);
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/6.2.0/d3.min.js"></script>
<svg width=400 height=200></svg>

可选添加

我没有对它进行经验测试,但它似乎略有改进:第一个参数 forsimulation.force()只是一个名称,因此您可以替换或删除单个力,如果您应用不同的名称,您可能会多次应用力. 在链接距离的情况下,这可能会使链接在每个刻度上更近一点:

 var distance = d3.forceLink(links_data.filter(function(d,i) { return i; }))
     .distance(35)
     .id(function(d) { return d.name; })
     .strength(1);

  simulation.force("a", distance);
  simulation.force("b", distance);
  simulation.force("c", distance);

推荐阅读