在D3力导向布局中修复节点位置

77

我想在我的力导向布局中,让某些节点基于节点属性保持固定位置,同时仍然能够被拖动并对其他节点施加排斥力并维持它们的链接线。

我以为这很简单:

force.on("tick", function() {
    vis.selectAll("g.node")
        .attr("transform", function(d) {
            return (d.someAttribute == true) ?
               "translate(" + d.xcoordFromAttribute + "," + d.ycoordFromAttribute +")" :
               "translate(" + d.x + "," + d.y + ")"
        });
  });

我也尝试过每次tick手动设置节点的x和y属性,但是链接还是会漂浮到节点受力影响时所在的位置。

显然我对它应该如何工作有基本的误解。如何在保持链接可拖动的同时将节点固定在一个位置?

2个回答

85

将所需节点上的d.fixed设置为true,并将d.xd.y初始化为所需位置。这些节点仍然是模拟的一部分,您可以使用正常的显示代码(例如,设置transform属性);但是,因为它们被标记为固定,所以只能通过拖动而不是通过模拟移动。

有关更多细节,请参见力导向布局文档(v3文档当前文档),还可以查看根节点在此示例中的位置。


49

在d3v4和d4v5中固定力布局的节点

在d3v3中,d.fixed可以将节点固定在d.xd.y位置上;然而,在d3v4/5中不再支持这种方法。根据d3文档

为了将节点固定在给定位置,你可以指定两个附加属性:

fx - 节点的固定x坐标

fy - 节点的固定y坐标

在每个tick结束后,在应用任何力之后,具有已定义node.fx的节点会将node.x重置为此值,并将node.vx设置为零;同样,具有已定义node.fy的节点会将node.y重置为此值,并将node.vy设置为零。要取消先前固定的节点,请将node.fx和node.fy设置为null,或删除这些属性。

您可以为数据源中的力节点设置fxfy属性,也可以动态添加和删除fxfy值。下面的代码片段在拖动事件结束时设置这些属性,只需拖动节点即可固定其位置:

var data ={ 
 "nodes": 
  [{"id": "A"},{"id": "B"},{"id": "C"},{"id":"D"}], 
 "links": 
  [{"source": "A", "target": "B"}, 
   {"source": "B", "target": "C"},
   {"source": "C", "target": "A"},
   {"source": "D", "target": "A"}]
}
var height = 250;
var width = 400;

var svg = d3.select("body").append("svg")
  .attr("width",width)
  .attr("height",height);
  
var simulation = d3.forceSimulation()
    .force("link", d3.forceLink().id(function(d) { return d.id; }).distance(50))
    .force("charge", d3.forceManyBody())
    .force("center", d3.forceCenter(width / 2, height / 2));
    
var link = svg.append("g")
  .selectAll("line")
  .data(data.links)
  .enter().append("line")
  .attr("stroke","black");

var node = svg.append("g")
 .selectAll("circle")
 .data(data.nodes)
 .enter().append("circle")
 .attr("r", 5)
 .call(d3.drag()
   .on("drag", dragged)
   .on("end", dragended));
 
simulation
 .nodes(data.nodes)
 .on("tick", ticked)
 .alphaDecay(0);

simulation.force("link")
 .links(data.links);
      
function ticked() {
 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; });
 node
   .attr("cx", function(d) { return d.x; })
   .attr("cy", function(d) { return d.y; });
}    
    
function dragged(d) {
  d.fx = d3.event.x;
  d.fy = d3.event.y;
}

function dragended(d) {
  d.fx = d3.event.x;
  d.fy = d3.event.y;
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.6.0/d3.min.js"></script>

d3v6事件监听器的更改

在上面的片段中,拖动事件使用以下形式:

function dragged(d) {
  d.fx = d3.event.x;
  d.fy = d3.event.y;
}

其中d表示正在拖动的节点的数据。在d3v6中,现在形式如下:

function dragged(event) {
  event.subject.fx = event.x;
  event.subject.fy = event.y;
}

或:
function dragged(event,d) {
  d.fx = event.x;
  d.fy = event.y;
}

现在事件直接传递给监听器,事件监听器传递的第二个参数是数据。 这里 是 Observable 上的经典示例。


但是d3如何知道某些节点是否已经固定了呢?如果为null,则表示未固定,但是d3会覆盖fx和fy的值... - Aral Roca
我不确定我是否完全理解了这个评论 - 如果fx或fy未定义或为空,则节点将不会被固定,因为我没有为任何节点定义它们来开始,因此没有一个是固定的。除非你告诉它,否则D3不会覆盖fx / fy值,就像我在这里所做的那样。(虽然与此无关,但我通过在“dragged”中设置fx / fy值而不是设置x / y(因此该点保留在鼠标上并且免受其他力量的影响)来使片段运行得更顺畅)。 - Andrew Reid

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接