D3.js 节点位于网格中的D3力布局图

D3.js 节点位于网格中的D3力布局图,d3.js,force-layout,D3.js,Force Layout,我已经建立了一个力定向图,但是,在我的例子中,我还有一个带有80px*80px盒子的网格。我希望图中的每个节点不仅根据现有的重力和力定位,而且还位于最接近的网格方格的中间(没有固定)。 可以在d3js中执行此操作吗?您必须在中应用自定义力 force.on("tick", function() { node.attr("cx", function(d) { return d.x; }) .attr("cy", function(d) { return d.y; }); l

我已经建立了一个力定向图,但是,在我的例子中,我还有一个带有80px*80px盒子的网格。我希望图中的每个节点不仅根据现有的重力和力定位,而且还位于最接近的网格方格的中间(没有固定)。
可以在d3js中执行此操作吗?

您必须在中应用自定义力

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
    };
};
var网格=函数(d){
var fx=d.px%100;
如果(fx<0)
fx+=100;
如果(外汇>50)
fx-=100;
var fy=d.py%100;
如果(fy<0)
fy+=100;
如果(fy>50)
fy-=100;
var k=-1;
返回{
x:k*fx,
y:k*fy
};
};

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

莫里茨·斯蒂凡纳想出了一种方法来实现这一点

代码:

演示:

编辑:

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


力和网格
.菜单{位置:绝对;顶部:20px;右侧:20px;}
var w=700,h=700;
var vis=d3.select(“body”).append(“svg:svg”).attr(“width”,w)、attr(“height”,h);
var背景=相对附加(“g”);
var节点=[];
var-links=[];
var USE_GRID=true;
var GRID_SIZE=60;
var GRID_TYPE=“HEXA”;
//设置事件处理程序
$(文档).ready(函数(){
$(“#使用#网格”)。单击(
函数(){
USE_GRID=$(this).is(“:checked”);
$(this.blur();
force.start();
}
);
//$(“#单元格大小”).rangeinput();
$(“#单元格大小”).bind(“更改”,
函数(){
log($(this.attr(“value”));
网格大小=$(此).attr(“值”);
grid.init();
force.start();
}
);
$(“[name=GRID\u TYPE]”)。单击(
函数(){
GRID_TYPE=$(this.attr(“value”);
grid.init();
force.start();
}
);
});
对于(变量i=0;i<30;i++){
变量节点={
标签:“节点”+i
};
nodes.push(节点);
};
对于(var i=0;i.99 Math.sqrt(i)*.02)
links.push({
资料来源:我,
目标:j,
体重:1
});
}
};
var force=d3.layout.force().size([w,h])。节点(nodes)。链接(links)。重力(1)。链接距离(函数(d){返回(1-d.weight)*100})。电荷(-3000)。链接强度(函数(x){
返回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(“类”、“节点”);
node.append(“svg:circle”).attr(“r”,6).style(“fill”,“#555”).style(“stroke”,“#FFF”).style(“stroke width”,“4px”);
node.call(强制拖动);
var updateLink=函数(){
这个.attr(“x1”,函数(d){
返回d.source.screenX;
}).attr(“y1”,函数(d){
返回d.source.screenY;
}).attr(“x2”,函数(d){
返回d.target.screenX;
}).attr(“y2”,功能(d){
返回d.target.screenY;
});
}
var updateNode=函数(){
this.attr(“转换”,函数(d){
如果(使用网格){
var gridpoint=网格占用最近(d);
if(网格点){
d、 screenX=d.screenX | | gridpoint.x;
d、 screenY=d.screenY | | gridpoint.y;
d、 screenX+=(gridpoint.x-d.screenX)*.2;
d、 屏幕y+=(网格点y-d屏幕y)*.2;
d、 x+=(网格点x-d.x)*.05;
d、 y+=(网格点y-d.y)*.05;
}
}否则{
d、 碎石
<!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>