如何在没有节点边缘重叠的情况下进行力导向布局

Posted

技术标签:

【中文标题】如何在没有节点边缘重叠的情况下进行力导向布局【英文标题】:How to make a force directed layout with no node-edge overlapping 【发布时间】:2019-04-24 04:55:24 【问题描述】:

我正在努力改进强制定向布局算法(用于有向图) 基本算法有效,即满足以下代码中的 isStable 条件并且算法结束,但边和节点可以重叠。所以我想在边缘的中间添加一些虚拟顶点(如下图所示)来解决这个问题,因为虚拟顶点会使边缘排斥其他边缘和节点。

我添加了 addDummies 方法,它为每个不是循环的边添加一个节点。 我将添加的节点称为 midNodes。

然后在每次迭代(迭代方法)时,我将 midNodes 的位置设置为边缘的中间。 剩下的就是老算法了。

我获得了一个没有边缘重叠的更好的布局,但是从不满足结束条件,而且,绘图不是那么好,因为 midNodes 在中心节点周围形成了一种“甜甜圈”,正如您从下图(中间节点在红圈内)

我正在寻找一种在边缘使用虚拟节点的算法的详细描述,或者寻求使算法终止并获得更好图纸的任何建议(我希望 midNodes 不要将其他节点排斥在外部区域)

我是否也应该将来自 midNodes 的新边添加到旧节点?

一种解决方案可能是更改 isStable 条件,但该数字通常可以确保我的图表布局正确,所以我不想碰它。

编辑:我这样使用下面的代码

var layouter = new Graph.Layout.Spring();
while !(layouter.isStable()) 
    layouter.iterate(1);

以下是当前代码的摘录

Graph.Layout.Spring = function() 
    this.maxRepulsiveForceDistance = 10;
    this.maxRepulsiveForceDistanceQ = this.maxRepulsiveForceDistance * this.maxRepulsiveForceDistance;
    this.k = 2.5;
    this.k2 = this.k * this.k; 
    this.c = 0.01;

    this.maxVertexMovement = 0.2;

    this.damping = 0.7;
;
Graph.Layout.Spring.prototype = 
resetForUpdate : function() 
    this.addDummies();
    this.currentIteration = 0;
    this.resetVelocities();
,

reset : function() 
    this.pastIterations = 0;
    this.currentIteration = 0;
    this.layoutPrepare();
    this.resetForUpdate();
,


isStable: function() 
    var nARR= this.graph.nodeArray;
    var i = nARR.length -1;
    do 
        var vel = nARR[i].velocity;
        var vx = vel.x;
        var vy = vel.y;
        var v = vx*vx+vy*vy;
        if (v > 0.0002) 
            return false;
        
     while (i--);

    return true;
,



addDummies: function() 
    for (e in this.graph.edges) 
        var edge = this.graph.edges[e];
        var s = edge.source;
        var t = edge.target;
        var id = s.id+"#"+t.id;
        console.log("adding ", id);
        if (!this.graph.nodes[id]) 
            if (s.id != t.id) 
                this.graph.addNode(id, "");
                var node = this.graph.nodes[id];

                node.dummy = true;
                node.fx = 0;
                node.fy = 0;

                node.next1id = s.id;
                node.next2id = t.id;

                node.velocity = 
                        x: 0,
                        y: 0
                ;
            
        
    
,


layoutPrepare : function() 
    for ( var i = 0; i < this.graph.nodeArray.length; ++i) 
        var node = this.graph.nodeArray[i];

        var x = -1+Math.random()*2;
        var y = -1+Math.random()*2;

        node.layoutPosX = x;
        node.layoutPosY = y;
        node.fx = 0;
        node.fy = 0;

        node.velocity = 
                x: 0,
                y: 0
        ;
    

,


resetVelocities: function() 
    for ( var i = 0; i < this.graph.nodeArray.length; ++i) 
        var node = this.graph.nodeArray[i];

        node.velocity = 
                x: 0,
                y: 0
        ;
    

,


iterate: function(iterations) 
    var SQRT        = Math.sqrt;
    var RAND        = Math.random;
    var maxRFQ      = this.maxRepulsiveForceDistanceQ;
    var l_k2        = this.k2;


    var it = iterations-1,
        i, j, node1, node2;

    var L_GRAPH     = this.graph;
    var L_EDGES     = L_GRAPH.edges;
    var nodeArray   = L_GRAPH.nodeArray;
    var L_NLEN      = nodeArray.length;

    /* 
     * addition: update midnodes position
     */
    for (e in L_GRAPH.edges) 
        var edge = L_GRAPH.edges[e];
        var s = edge.source;
        var t = edge.target;
        if (s != t) 
            var id = s.id+"#"+t.id;
            var midNode = L_GRAPH.nodes[id];
            if (midNode) 
                var dx = s.layoutPosX - t.layoutPosX;
                var dy = s.layoutPosY - t.layoutPosY;
                midNode.layoutPosX = s.layoutPosX - dx/2;
                midNode.layoutPosY = s.layoutPosY - dy/2;
            
        
    


    /*
     * repulsive
     */
    do 
        for (i = 0; i < L_NLEN; ++i) 
            node1 = nodeArray[i];

            for (j = i+1; j < L_NLEN; ++j) 
                node2 = nodeArray[j];

                // per cappio
                if (node1 === node2)
                    continue;

                var dx = node2.layoutPosX - node1.layoutPosX;
                var dy = node2.layoutPosY - node1.layoutPosY;
                var d2 = dx * dx + dy * dy;
                if (d2 < 0.001) 
                    dx = 0.1 * RAND() + 0.1;
                    dy = 0.1 * RAND() + 0.1;
                    d2 = dx * dx + dy * dy;
                


                if (d2 < maxRFQ) 
                    var d = SQRT(d2);
                    var f = 2*(l_k2 / d2);

                    var xx = f * dx / d;
                    var yy = f * dy / d;

                    node2.fx += xx;
                    node2.fy += yy;
                    node1.fx -= xx;
                    node1.fy -= yy;
                


             // for j
         // for i



        /*
         * Attractive
         */
        i = (L_EDGES.length)-1;
        if (i >= 0) 
            do 
                var edge = L_EDGES[i];
                var node1 = edge.source;
                var node2 = edge.target;

                // evita self-force, che cmq andrebbe a zero
                if (node1 === node2)
                    continue;

                var dx = node2.layoutPosX - node1.layoutPosX;
                var dy = node2.layoutPosY - node1.layoutPosY;
                var d2 = dx * dx + dy * dy;
                if (d2 < 0.01) 
                    dx = 0.1 * RAND() + 0.1;
                    dy = 0.1 * RAND() + 0.1;
                    d2 = dx * dx + dy * dy;
                

                d = SQRT(d2);
                var f = (l_k2-d2)/l_k2;

                var n2d = node2.edges.length;
                if (n2d > 2) 
                    n2d = 2;
                 
                var n1d = node1.edges.length;
                if (n1d > 2) 
                    n1d = 2;
                 

                var xcomp = f * dx/d;
                var ycomp = f * dy/d;

                node2.fx += xcomp / n2d;
                node2.fy += ycomp / n2d;
                node1.fx -= xcomp / n1d;
                node1.fy -= ycomp / n1d;    
             while (i--);
         // if i>=0



        /*
         * Move by the given force
         */
        var max = this.maxVertexMovement;
        var d = this.damping;
        var c = this.c;
        var i = L_NLEN-1;
        do 
            var node = nodeArray[i];

            var xmove,
                ymove;

            var v = node.velocity;

            v.x = v.x * d + node.fx * c; 
            v.y = v.y * d + node.fy * c;

            xmove = v.x;
            ymove = v.y;

            if (xmove > max)
                xmove = max;
            else if (xmove < -max)
                xmove = -max;

            if (ymove > max)
                ymove = max;
            else if (ymove < -max)
                ymove = -max;

            if (node.isNailed !== undefined) 
                v.x = 0;
                v.y = 0;
             else 
                v.x = xmove;
                v.y = ymove;

                node.layoutPosX += xmove;
                node.layoutPosY += ymove;
            


            node.fx = 0;
            node.fy = 0;
         while (i--);

     while (it--); 
,

【问题讨论】:

您是否有不想使用现有的力导向绘图解决方案的原因? 我不知道现有的力导向绘图解决方案是否支持边缘节点排斥,但是我正在构建一个可视化系统,我想学习如何做这些事情。如果它们支持边缘节点排斥并且它们是开源的,我也对现有解决方案感兴趣 您可以直接使用从节点到附近边缘的距离来计算二次排斥力,而不是添加中点节点。但是,这仍然会导致边缘相互交叉。 谢谢。就在昨天,我确实在考虑像您提出的那样的解决方案。它比我原来的要复杂一些,但更强大。边缘交叉的问题我认为是一个更困难的问题,我只对节点边缘排斥感兴趣 我实际上认为它会更简单,因为 1)您不必创建那些幻像中点节点,并且 2)无论如何它们都必须被视为特殊情况(例如,它们可以相交,而实际节点不能),但我想这是主观的。我一有空就会尝试做一个测试实现。 【参考方案1】:

我会查看vis.js,我已经实现了您之前尝试使用该库执行的操作。他们有多种物理引擎可供您使用。

如果你想自己实现它并被卡住,我建议你挖掘 visjs 源代码以获得灵感。

这是一个很好的例子: https://visjs.github.io/vis-network/examples/network/other/configuration.html

这是该可视化的“选项”。

physics: 
enabled:  boolean: bool ,
barnesHut: 
  gravitationalConstant:  number ,
  centralGravity:  number ,
  springLength:  number ,
  springConstant:  number ,
  damping:  number ,
  avoidOverlap:  number ,
  __type__:  object 
,
forceAtlas2Based: 
  gravitationalConstant:  number ,
  centralGravity:  number ,
  springLength:  number ,
  springConstant:  number ,
  damping:  number ,
  avoidOverlap:  number ,
  __type__:  object 
,
repulsion: 
  centralGravity:  number ,
  springLength:  number ,
  springConstant:  number ,
  nodeDistance:  number ,
  damping:  number ,
  __type__:  object 
,
hierarchicalRepulsion: 
  centralGravity:  number ,
  springLength:  number ,
  springConstant:  number ,
  nodeDistance:  number ,
  damping:  number ,
  avoidOverlap:  number ,
  __type__:  object 
,
maxVelocity:  number ,
minVelocity:  number ,    // px/s
solver:  string: ['barnesHut', 'repulsion', 'hierarchicalRepulsion', 'forceAtlas2Based'] ,
stabilization: 
  enabled:  boolean: bool ,
  iterations:  number ,   // maximum number of iteration to stabilize
  updateInterval:  number ,
  onlyDynamicEdges:  boolean: bool ,
  fit:  boolean: bool ,
  __type__:  object, boolean: bool 
,
timestep:  number ,
adaptiveTimestep:  boolean: bool ,
__type__:  object, boolean: bool 

【讨论】:

【参考方案2】:

您正在寻找的是边缘排斥力。我也在找那个,我没有在任何 npm 包中看到它。

这是一篇论文的链接,如果您有兴趣,它在第 16 页上有一些很有希望的图片。 https://www.researchgate.net/publication/4175452_A_new_force-directed_graph_drawing_method_based_on_edge-edge_repulsion

【讨论】:

以上是关于如何在没有节点边缘重叠的情况下进行力导向布局的主要内容,如果未能解决你的问题,请参考以下文章

ECharts 力导向布局图怎么将数据库里的数据赋值给各个节点

力导向图布局的性能和复杂性?

SVG力导向图,当鼠标经过某一节点时(mouseover),显示两层与其相关联的节点和连线,该如何实现?

D3.js 网络图使用力导向布局和矩形节点

可以为力导向布局指定自定义力函数吗?

如何在没有元素重叠的情况下在 java 中使用布局?