【问题标题】:Set fixed distance between nodes d3-force设置节点之间的固定距离 d3-force
【发布时间】:2021-03-25 10:58:58
【问题描述】:

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

可以在sn-p中看到运行here或以下的代码(最好全屏查看)。

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

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

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

//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>

谢谢

【问题讨论】:

  • D3 正在平衡不同的力量 - 要求链接必须有一定的距离是困难的(并且在某些情况下,在几何上是不可能的)。 This 答案着眼于确保在强制布局中指定链接距离 - 但添加拖动功能可能需要一个非常不同的解决方案。

标签: javascript d3.js force-layout


【解决方案1】:

如果不修改力布局的工作方式,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>

可选添加

我没有对它进行经验测试,但它似乎做了一点改进:simulation.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);

【讨论】:

  • 谢谢,这看起来比我的好多了。正如你所说,我认为,最好的方法是自己创造一种适合我需要的力量。我不确定我是否有足够的编码技能来做到这一点,但我会尝试。您的解决方案几乎是完美的。唯一的事情是我应该找到一种方法来定义两个绿色节点之间的最大距离,无论如何,它们应该保持连接在一起。一旦我完成,我会让你这样做,这正是我选择的解决方案。如果有人有其他想法,请随时分享
  • @cri_pava,您可以对上述内容进行的一项补充是多次应用链接距离,我没有根据经验进行检查,但在视觉上似乎有所不同:simulation.force("a",distance).force("b",distance)....
  • 对不起,链接是什么意思? d3.forceLink().distance() ?
  • @cri_pava,在答案的底部添加了说明 - 希望它对我的最后评论有意义。
  • 抱歉,如果我回来了就知道了。所以你上面提出的解决方案几乎是完美的。唯一的问题是即使有阻力,两个绿点也应该保持“连接”。我试图将 forceCenter 应用于第二个绿点,x 和 y 指向第一个绿点,但似乎力应用于所有点,而不仅仅是第二个绿点
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-05-25
  • 1970-01-01
  • 2020-12-21
  • 1970-01-01
  • 2017-01-07
相关资源
最近更新 更多