【问题标题】:D3 force layout graph with nodes positioned in a gridD3 力布局图,节点位于网格中
【发布时间】:2014-10-27 20:42:10
【问题描述】:

我已经构建了一个力导向图,但是,就我而言,我还有一个带有 80px*80px 框的网格。我希望图中的每个节点不仅根据现有的重力和力定位,而且还位于最近的网格框的中间(没有被固定)。 在 d3js 中可以做到这一点吗?

【问题讨论】:

    标签: d3.js force-layout


    【解决方案1】:

    Moritz Stefaner 想出了一个办法来做到这一点

    代码:https://github.com/moritzstefaner/gridexperiments/

    演示:http://moritzstefaner.github.io/gridexperiments/

    编辑:

    正如@altocumulus 所提到的,这没有代码副本。通常我只从个人网站复制代码,因为它们比 github 上的东西更有可能消失。或者我会在它很短时复制它(小于 50 loc?)。无论如何,由于代码的肉可能可以被拉出,我已经复制了下面的 mortiz 的 index.html 文件。其他引用的 js 文件可以很容易地在其他地方找到。 (请注意,您可能应该提取截至 2011 年 12 月 9 日的每个库的版本)

    <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN">
    <html lang="en">
    <head>
        <meta charset="utf-8">
        <title>Forces and grids</title>
        <script type="text/javascript" src="d3.min.js"></script>
        <script type="text/javascript" src="d3.layout.min.js"></script>
        <script type="text/javascript" src="d3.geom.min.js"></script>
        <script type="text/javascript" src="underscore-min.js"></script>
        <script src="jquery-1.7.2.min.js" charset="utf-8"></script>
        <style type="text/css" media="screen">
            .menu { position:absolute; top :20px; right:20px; }
        </style>    
    </head>
    <body>
        <script type="text/javascript" charset="utf-8">
            var w = 700, h = 700;
            var vis = d3.select("body").append("svg:svg").attr("width", w).attr("height", h);
            var background = vis.append("g");
            var nodes = [];
            var links = [];
            var USE_GRID = true;
            var GRID_SIZE = 60;
            var GRID_TYPE = "HEXA";
    
            // set up event handlers
            $(document).ready(function(){
                $("#USE_GRID").click(
                    function(){
                        USE_GRID = $(this).is(":checked");
                        $(this).blur();
                        force.start();
                    }
                );
    
                //$("#CELL_SIZE").rangeinput();
                $("#CELL_SIZE").bind("change", 
                    function(){
                        console.log($(this).attr("value"));
                        GRID_SIZE = $(this).attr("value");
                        grid.init();
                        force.start();
                    }
                );
    
                $("[name=GRID_TYPE]").click( 
                    function(){
                        GRID_TYPE = $(this).attr("value");
                        grid.init();
                        force.start();
                    }
                );
            });
            for(var i = 0; i < 30; i++) {
                var node = {
                    label : "node " + i
                };
                nodes.push(node);
            };
            for(var i = 0; i < nodes.length; i++) {
                for(var j = 0; j < i; j++) {
                    if(Math.random() > .99-Math.sqrt(i)*.02)
                        links.push({
                            source : i,
                            target : j,
                            weight :1
                        });
                }
            };
            var force = d3.layout.force().size([w, h]).nodes(nodes).links(links).gravity(1).linkDistance(function(d){return (1-d.weight)*100}).charge(-3000).linkStrength(function(x) {
                return x.weight * 5
            });
            force.start();
            var link = vis.selectAll("line.link").data(links).enter().append("svg:line").attr("class", "link").style("stroke-width", 1.5).style("stroke", "#555").style("opacity", function(d){return d.weight*.7});
            var node = vis.selectAll("g.node").data(force.nodes()).enter().append("svg:g").attr("class", "node");
            node.append("svg:circle").attr("r", 6).style("fill", "#555").style("stroke", "#FFF").style("stroke-width", "4px");
            node.call(force.drag);
            var updateLink = function() {
                this.attr("x1", function(d) {
                    return d.source.screenX;
                }).attr("y1", function(d) {
                    return d.source.screenY;
                }).attr("x2", function(d) {
                    return d.target.screenX;
                }).attr("y2", function(d) {
                    return d.target.screenY;
                });
            }
            var updateNode = function() {
                this.attr("transform", function(d) {
                    if(USE_GRID) {
                        var gridpoint = grid.occupyNearest(d);
                        if(gridpoint) {
                            d.screenX = d.screenX || gridpoint.x;                           
                            d.screenY = d.screenY || gridpoint.y;                                                       
                            d.screenX += (gridpoint.x - d.screenX) * .2;
                            d.screenY += (gridpoint.y - d.screenY) * .2;
    
                            d.x += (gridpoint.x - d.x) * .05;
                            d.y += (gridpoint.y - d.y) * .05;
                        }
                    } else {
                        d.screenX = d.x;
                        d.screenY = d.y;
                    }
                    return "translate(" + d.screenX + "," + d.screenY + ")";
                });
            };
            var grid = function(width, height) {
                return {
                    cells : [],
                    init : function() {
                        this.cells = [];
                        for(var i = 0; i < width / GRID_SIZE; i++) {
                            for(var j = 0; j < height / GRID_SIZE; j++) {
                                // HACK: ^should be a better way to determine number of rows and cols
                                var cell;
                                switch (GRID_TYPE) {
                                    case "PLAIN":
                                        cell = {
                                            x : i * GRID_SIZE,
                                            y : j * GRID_SIZE
                                        };
                                        break;
                                    case "SHIFT_ODD_ROWS":
                                        cell = {
                                            x : i * GRID_SIZE,
                                            y : 1.5 * (j * GRID_SIZE + (i % 2) * GRID_SIZE * .5)
                                        };
                                        break;
                                    case "HEXA":
                                        cell = {
                                            x : i * GRID_SIZE + (j % 2) * GRID_SIZE * .5,
                                            y : j * GRID_SIZE * .85
                                        };
                                        break;
                                }
                                this.cells.push(cell);
                            };
                        };
                    },
                    sqdist : function(a, b) {
                        return Math.pow(a.x - b.x, 2) + Math.pow(a.y - b.y, 2);
                    },
                    occupyNearest : function(p) {
                        var minDist = 1000000;
                        var d;
                        var candidate = null;
                        for(var i = 0; i < this.cells.length; i++) {
                            if(!this.cells[i].occupied && ( d = this.sqdist(p, this.cells[i])) < minDist) {
                                minDist = d;
                                candidate = this.cells[i];
                            }
                        }
                        if(candidate)
                            candidate.occupied = true;
                        return candidate;
                    }
                }
            }(w, h);
            force.on("tick", function() {
                vis.select("g.gridcanvas").remove();
                if(USE_GRID) {
                    grid.init();
                    var gridCanvas = vis.append("svg:g").attr("class", "gridcanvas");
                    _.each(grid.cells, function(c) {
                        gridCanvas.append("svg:circle").attr("cx", c.x).attr("cy", c.y).attr("r", 2).style("fill", "#555").style("opacity", .3);
                    });
                }
                node.call(updateNode);
                link.call(updateLink);
            });
        </script>
        <div class="menu">
            <div>
                <input type="checkbox" id="USE_GRID" checked>use grid</input>       
            </div>
            <div>
                <input type="range" min="30" step="10" max="150" id="CELL_SIZE" value="60"></input>
            </div>
            <div>
                <input type="radio" name="GRID_TYPE" value="PLAIN">plain</input>
                <input type="radio" name="GRID_TYPE" value="SHIFT_ODD_ROWS">Shift odd rows</input>
                <input type="radio" name="GRID_TYPE" value="HEXA" checked>Hexa</input>                              
            </div>
        </div>
    </body>
    

    【讨论】:

    • 这是一个非常好的例子!但是,由于仅链接的答案被认为是 VLQ,因此在 SO 上不受欢迎,您介意至少分享一些代码细节和见解吗?
    • @altocumulus - 我从 github 复制了核心代码。我已经对它的工作方式/原因进行了任何真正的分析,只是它应该与 OP 的预期结果非常匹配。
    【解决方案2】:

    你必须在

    中应用你的自定义力量
    force.on("tick", function() {
    
      node.attr("cx", function(d) { return d.x; })
          .attr("cy", function(d) { return d.y; });
    
      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; });
    
    });
    

    所以没有内置的方法来做这样的事情......

    因此,在您的情况下,您必须找到接近的网格框中心,并使用节点与框中心之间的距离以及一些重力方程来计算 x 和 y 值。

    在你的情况下

    node
        .attr("cx", function(d) {
            d.x += f(d).x;
            return d.x;
        })
        .attr("cy", function(d) {
            d.y += f(d).y;
            return d.y;
        });
    

    其中f(d) 是重力矢量,取决于框中心与实际节点d 之间的距离。例如

    var blackHole = function (d) {
        var gc = {
            x: 100,
            y: 100
        };
        var k = 0.1;
    
        var dx = gc.x - d.px;
        var dy = gc.y - d.py;
    
        return {
            x: k * dx,
            y: k * dy
        };
    };
    

    很难找到真正适用于多个重心的f(d),因此我建议您阅读此类力算法。我尝试了一些有趣的例子,但没有一个能按照你想要的方式工作。 ;-)

    至少现在:

    var grid = function (d) {
        var fx = d.px % 100;
        if (fx < 0)
            fx += 100;
        if (fx > 50)
            fx -= 100;
    
        var fy = d.py % 100;
        if (fy < 0)
            fy += 100;
        if (fy > 50)
            fy -= 100;
        var k = -1;
    
        return {
            x: k * fx,
            y: k * fy
        };
    };
    

    这是一个 100px 的密集网格,具有非常简单的力...但我猜结果不是你所期望的,节点可以重叠,因为强制布局只有具有公共链接的节点相互排斥,至少这是我的经验(编辑:那是因为负电荷)...我认为使用 d3 quad 构建自定义力布局可能要容易得多...

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2014-06-23
      • 2013-02-28
      • 1970-01-01
      • 2017-03-17
      • 1970-01-01
      • 2015-08-12
      • 1970-01-01
      • 2014-02-09
      相关资源
      最近更新 更多