D3 Canvas 强制布局 - 脱节组排斥另一组

D3 Canvas force layout - disjointed group replelling the other group

我有一组节点(A、B、C、D、E、F、G、H、I、J、K)但在 2 个不相交的组(A、B、C)中相互连接和 (D,E,F,G,H,I,J,K):

<!DOCTYPE html>
<html>

<body onload="connect1();">
    <canvas width="300" height="100"></canvas>
    <!--button id="ref" onclick="refresh()">refresh </button-->
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <script>
    
  var canvas = document.querySelector("canvas"),
      context = canvas.getContext("2d"),
      width = canvas.width,
      height = canvas.height;
    var links =[] , nodes = [] ;

    var graph={nodes,links}, wsConn;    

function connect1(){

    addNodeCanvas("A", "1");
    addNodeCanvas("B", "6");
    addNodeCanvas("C", "4");
    addNodeCanvas("D", "2");
    addNodeCanvas("E", "3");
 addNodeCanvas("F", "1");
    addNodeCanvas("G", "1");
    addNodeCanvas("H", "1");
    addNodeCanvas("I", "1");
    addNodeCanvas("J", "1");
    addNodeCanvas("K", "1");
 
    addLinkCanvas("A","B");
    addLinkCanvas("A","C");

 
    addLinkCanvas("E","D");
 addLinkCanvas("F","D");
 addLinkCanvas("G","D");
 addLinkCanvas("H","D");
 addLinkCanvas("I","D");
 addLinkCanvas("J","D");
 addLinkCanvas("K","D");

    refresh();
}


var nodeColors = d3.scaleOrdinal().range(d3.schemeCategory20);

function addNodeCanvas(nodeName,g) {
  var node = {
    x: 400,
    y: 400,
    id: nodeName,
    grp:g
  };
  var n = nodes.push(node);
}


function addLinkCanvas(idSrc, idTarget) {

    if (idSrc != idTarget) {
    var s = {},
      t = {};
    nodes.forEach(function(curNode) {
      if (typeof curNode.id != "undefined") {
        if (curNode.id == idSrc) {
          s = curNode;
        }
        if (curNode.id == idTarget) {
          t = curNode;
        }
      }
    });

    links.push({
      source: s,
      target: t
    });
  };

}




    function refresh() {

            var simulation = d3.forceSimulation()
            .force("link", d3.forceLink().id(function(d) { 
                return d.id; 
            }))
            .force("charge", d3.forceManyBody())
            .force("center", d3.forceCenter(width / 2, height / 2));

        simulation
            .nodes(nodes)
            .on("tick", ticked)
            .force("link")
            .links(links);
        d3.select(canvas)
            .call(d3.drag()
                .container(canvas)
                .subject(dragsubject)
                .on("start", dragstarted)
                .on("drag", dragged)
                .on("end", dragended));


        function ticked() {
            var margin = 20;
            nodes.forEach(function(d) {
                d.x = Math.max(margin, Math.min(width - margin, d.x))
                d.y = Math.max(margin, Math.min(height - margin, d.y))
            });

            function dblclick() {
                nodes.forEach(function(d) {
                    d.fx = d.fy = null;
                })
            };
            context.clearRect(0, 0, width, height);
            context.beginPath();
            links.forEach(drawLink);
            context.strokeStyle = "#aaa";
            context.stroke();
            context.beginPath();
            nodes.forEach(drawNode);

        }

        function dragsubject() {
            return simulation.find(d3.event.x, d3.event.y);
        }
        var clickDate = new Date();
        var difference_ms;

        function dragstarted() {
            
        }

        function dragged() {
           
        }


        function dragended() {
           
        }


        function drawLink(d) {
            context.moveTo(d.source.x, d.source.y);
            context.lineTo(d.target.x, d.target.y);
        }

        var nodeColors = d3.scaleOrdinal().range(d3.schemeCategory20),
            labelColors = d3.scaleOrdinal().range(['red', 'orange', 'blue', 'green', 'purple']);

        function drawNode(d) {
            context.beginPath();
            context.moveTo(d.x + 10, d.y);
            context.arc(d.x, d.y, 10, 0, 2 * Math.PI);
            context.strokeStyle = "#fff";
            context.stroke();
            context.fillStyle = nodeColors(d.grp);
            context.closePath();
            context.fill();
            context.beginPath();
            context.font = (d.labelSize ? d.labelSize : 10) + 'px Arial';
            context.fillStyle = labelColors(d.grp);
            context.fillText(d.id ? d.id : d.grp, d.x, d.y);
            context.closePath();
        }
    }
    </script>
</body>

</html>

由于这 2 组在加载页面后脱节,它们开始相互排斥并继续推动直到到达 canvas 的末尾。

如何避免这件事,使 2 组彼此远离但仍保持在 canvas 的中心附近。

Please try it running a few times, and you will see nodes A,B & C getting pushed to the end and remain there in a straight line.

问题在于,一旦施加了初始排斥力,就没有其他力起作用来抵消它。所以运动一直持续到 canvas 容器的边界,节点在那里撞到墙上(相当于 normal force 并带来平衡)。

要使无限排斥运动停止,您需要一个viscosity force,它会逐渐减慢节点组的速度,直到它们达到零速度。

d3 的强制布局为此提供了一个参数:velocityDecay:

var simulation = d3.forceSimulation()
  .force("link", d3.forceLink().id(function(d) { return d.id; }))
  .force("charge", d3.forceManyBody())
  .force("center", d3.forceCenter(width / 2, height / 2))
  .velocityDecay(0.8); // the velocity decay setting

您可以使用 velocityDecay 的值,它可以介于 0 和 1 之间,其中 0 相当于不应用设置(无粘度),1 相当于具有法向力且没有移动开始。

例如粘度为0.8:

<!DOCTYPE html>
<html>

<body onload="connect1();">
<canvas width="500" height="350"></canvas>
<!--button id="ref" onclick="refresh()">refresh </button-->
<script src="https://d3js.org/d3.v4.min.js"></script>
<script>

  var canvas = document.querySelector("canvas"),
    context = canvas.getContext("2d"),
    width = canvas.width,
    height = canvas.height;
  var links =[] , nodes = [] ;

  var graph={nodes,links}, wsConn;

  function connect1(){

    addNodeCanvas("A", "1");
    addNodeCanvas("B", "6");
    addNodeCanvas("C", "4");
    addNodeCanvas("D", "2");
    addNodeCanvas("E", "3");
    addNodeCanvas("F", "1");
    addNodeCanvas("G", "1");
    addNodeCanvas("H", "1");
    addNodeCanvas("I", "1");
    addNodeCanvas("J", "1");
    addNodeCanvas("K", "1");

    addLinkCanvas("A","B");
    addLinkCanvas("A","C");


    addLinkCanvas("E","D");
    addLinkCanvas("F","D");
    addLinkCanvas("G","D");
    addLinkCanvas("H","D");
    addLinkCanvas("I","D");
    addLinkCanvas("J","D");
    addLinkCanvas("K","D");

    refresh();
  }


  var nodeColors = d3.scaleOrdinal().range(d3.schemeCategory20);

  function addNodeCanvas(nodeName,g) {
    var node = {
      x: width / 2,
      y: height / 2,
      id: nodeName,
      grp:g
    };
    var n = nodes.push(node);
  }


  function addLinkCanvas(idSrc, idTarget) {

    if (idSrc != idTarget) {
      var s = {},
        t = {};
      nodes.forEach(function(curNode) {
        if (typeof curNode.id != "undefined") {
          if (curNode.id == idSrc) {
            s = curNode;
          }
          if (curNode.id == idTarget) {
            t = curNode;
          }
        }
      });

      links.push({
        source: s,
        target: t
      });
    };

  }




  function refresh() {

    var simulation = d3.forceSimulation()
    .force("link", d3.forceLink().id(function(d) {
      return d.id;
    }))
    .force("charge", d3.forceManyBody())
    .force("center", d3.forceCenter(width / 2, height / 2))
    .velocityDecay(0.8);

    simulation
    .nodes(nodes)
    .on("tick", ticked)
    .force("link")
    .links(links);
    d3.select(canvas)
    .call(d3.drag()
    .container(canvas)
    .subject(dragsubject)
    .on("start", dragstarted)
    .on("drag", dragged)
    .on("end", dragended));


    function ticked() {
      var margin = 20;
      nodes.forEach(function(d) {
        d.x = Math.max(margin, Math.min(width - margin, d.x))
        d.y = Math.max(margin, Math.min(height - margin, d.y))
      });

      function dblclick() {
        nodes.forEach(function(d) {
          d.fx = d.fy = null;
        })
      };
      context.clearRect(0, 0, width, height);
      context.beginPath();
      links.forEach(drawLink);
      context.strokeStyle = "#aaa";
      context.stroke();
      context.beginPath();
      nodes.forEach(drawNode);

    }

    function dragsubject() {
      return simulation.find(d3.event.x, d3.event.y);
    }
    var clickDate = new Date();
    var difference_ms;

    function dragstarted() {

    }

    function dragged() {

    }


    function dragended() {

    }


    function drawLink(d) {
      context.moveTo(d.source.x, d.source.y);
      context.lineTo(d.target.x, d.target.y);
    }

    var nodeColors = d3.scaleOrdinal().range(d3.schemeCategory20),
      labelColors = d3.scaleOrdinal().range(['red', 'orange', 'blue', 'green', 'purple']);

    function drawNode(d) {
      context.beginPath();
      context.moveTo(d.x + 10, d.y);
      context.arc(d.x, d.y, 10, 0, 2 * Math.PI);
      context.strokeStyle = "#fff";
      context.stroke();
      context.fillStyle = nodeColors(d.grp);
      context.closePath();
      context.fill();
      context.beginPath();
      context.font = (d.labelSize ? d.labelSize : 10) + 'px Arial';
      context.fillStyle = labelColors(d.grp);
      context.fillText(d.id ? d.id : d.grp, d.x, d.y);
      context.closePath();
    }
  }
</script>
</body>

</html>

forceCenter看起来不够强,所以我用一组forceX()forceY()力量代替了它。

通过为多体设置最大距离,推力受到限制,通过增加力量,它仍然在近距离排斥。

您指定了 link id 函数,但您设置的 sourcetarget 不等于 id

<!DOCTYPE html>
<html>

<body onload="connect1();">
    <canvas width="400" height="400"></canvas>
    <!--button id="ref" onclick="refresh()">refresh </button-->
    <script src="https://d3js.org/d3.v4.min.js"></script>
    <script>
    
  var canvas = document.querySelector("canvas"),
      context = canvas.getContext("2d"),
      width = canvas.width,
      height = canvas.height;
    var links =[] , nodes = [] ;

    var graph={nodes,links}, wsConn;

function connect2(){

    addNodeCanvas("A", "1");
    addNodeCanvas("B", "6");
    addNodeCanvas("C", "4");
    addNodeCanvas("D", "2");
    addNodeCanvas("E", "3");
   addNodeCanvas("F", "1");
 
    addLinkCanvas("A","B");
    addLinkCanvas("A","C");
 
    addLinkCanvas("E","D");
   addLinkCanvas("F","D");

    refresh();
}

function connect1(){

    addNodeCanvas("A", "1");
    addNodeCanvas("B", "6");
    addNodeCanvas("C", "4");
    addNodeCanvas("D", "2");
    addNodeCanvas("E", "3");
    addNodeCanvas("F", "1");
    addNodeCanvas("G", "1");
    addNodeCanvas("H", "1");
    addNodeCanvas("I", "1");
    addNodeCanvas("J", "1");
    addNodeCanvas("K", "1");

    addLinkCanvas("A","B");
    addLinkCanvas("A","C");


    addLinkCanvas("E","D");
    addLinkCanvas("F","D");
    addLinkCanvas("G","D");
    addLinkCanvas("H","D");
    addLinkCanvas("I","D");
    addLinkCanvas("J","D");
    addLinkCanvas("K","D");

    refresh();
}

var nodeColors = d3.scaleOrdinal().range(d3.schemeCategory20);

function addNodeCanvas(nodeName,g) {
  var node = {
    x: 400,
    y: 400,
    id: nodeName,
    grp:g
  };
  var n = nodes.push(node);
}


function addLinkCanvas_1(idSrc, idTarget) {

    if (idSrc != idTarget) {
        var s = {},
        t = {};
        nodes.forEach(function(curNode) {
        if (typeof curNode.id != "undefined") {
            if (curNode.id == idSrc) { s = curNode; }
            if (curNode.id == idTarget) { t = curNode; }
        }
        });

        links.push({
        source: s,
        target: t
        });
    };
}

function addLinkCanvas(idSrc, idTarget) {

    if (idSrc != idTarget) {
        links.push({
            source: idSrc,
            target: idTarget
        });
    };
}

    function refresh() {

            var simulation = d3.forceSimulation()
            .force("link", d3.forceLink().id(function(d) { return d.id; }))
            .force("charge", d3.forceManyBody().distanceMax(70).strength(d=>-50))
            // .force("center", d3.forceCenter(width*0.5, height*0.5))
            .force("centerX", d3.forceX(width*0.5).strength(0.02))
            .force("centerY", d3.forceY(height*0.5).strength(0.02));

        simulation
            .nodes(nodes)
            .on("tick", ticked)
            .force("link")
            .links(links);
        d3.select(canvas)
            .call(d3.drag()
                .container(canvas)
                .subject(dragsubject)
                .on("start", dragstarted)
                .on("drag", dragged)
                .on("end", dragended));


        function ticked() {
            var margin = 20;
            nodes.forEach(function(d) {
                d.x = Math.max(margin, Math.min(width - margin, d.x))
                d.y = Math.max(margin, Math.min(height - margin, d.y))
            });

            function dblclick() {
                nodes.forEach(function(d) {
                    d.fx = d.fy = null;
                })
            };
            context.clearRect(0, 0, width, height);
            context.beginPath();
            links.forEach(drawLink);
            context.strokeStyle = "#aaa";
            context.stroke();
            context.beginPath();
            nodes.forEach(drawNode);

        }

        function dragsubject() {
            return simulation.find(d3.event.x, d3.event.y);
        }
        var clickDate = new Date();
        var difference_ms;

        function dragstarted() {
        }

        function dragged() {
        }

        function dragended() {
        }

        function drawLink(d) {
            context.moveTo(d.source.x, d.source.y);
            context.lineTo(d.target.x, d.target.y);
        }

        var nodeColors = d3.scaleOrdinal().range(d3.schemeCategory20),
            labelColors = d3.scaleOrdinal().range(['red', 'orange', 'blue', 'green', 'purple']);

        function drawNode(d) {
            context.beginPath();
            context.moveTo(d.x + 10, d.y);
            context.arc(d.x, d.y, 10, 0, 2 * Math.PI);
            context.strokeStyle = "#fff";
            context.stroke();
            context.fillStyle = nodeColors(d.grp);
            context.closePath();
            context.fill();
            context.beginPath();
            context.font = (d.labelSize ? d.labelSize : 10) + 'px Arial';
            context.fillStyle = labelColors(d.grp);
            context.fillText(d.id ? d.id : d.grp, d.x, d.y);
            context.closePath();
        }
    }
    </script>
</body>

</html>