在多焦点 d3 力布局中重新定位节点

Posted

技术标签:

【中文标题】在多焦点 d3 力布局中重新定位节点【英文标题】:Reposition nodes in a multi-foci d3 force layout 【发布时间】:2017-03-17 18:34:15 【问题描述】:

我在多焦点力布局中有三组节点。每个节点都已经用 html 呈现。

这是我的强制布局代码的样子:

var node = this.svg.selectAll('path')
    .data(data);

// foci is a dictionary that assigns the x and y value based
// on what group a node belongs to.
var foci = 
    "Blue" : 
         "x" : xScale(0),
         "y": height / 2
    ,
    "Red": 
         "x" : xScale(1),
         "y": height / 2
    ,
    "Purple": 
         "x" : xScale(2),
         "y": height / 2
    ,
;

// This helped me position the nodes to their assigned clusters.
var forceX = d3.forceX((d) => foci[d.group].x);
var forceY = d3.forceY((d) => foci[d.group].y);

var force = d3.forceSimulation(data)
    .force('x', forceX)
    .force('y', forceY)
    .force("collide", d3.forceCollide(8))
    .on('tick', function() 
         node
            .attr('transform', (d) => 
                return 'translate(' + (d.x - 100) + ',' + (-d.y + 25) + ')';
            );
        );

到目前为止,我能够完成的是根据下拉列表中的更改重新绘制布局,这会重新初始化 d3.forceSimulation() 并使集群重新回到页面上,如下面的 gif 所示。

这不是我想要的。我正在努力使重新排列尽可能无缝。

更新:通过不重新初始化d3.forceSimulation(),我可以将新数据绑定到节点并更改它们的颜色。

【问题讨论】:

【参考方案1】:

您可以简单地重新加热模拟,而不是重新初始化d3.forceSimulation(),使用restart()

重新启动模拟的内部计时器并返回模拟。与simulation.alphaTarget 或simulation.alpha 结合使用,此方法可用于在交互过程中“重新加热”模拟,例如在拖动节点时,或在使用simulation.stop 暂时暂停后恢复模拟。

我使用您的部分代码创建了一个演示来向您展示。在这个演示中,按钮随机化了每个数据点的颜色。之后,我们重新加热模拟:

force.alpha(0.8).restart();

检查它,点击“随机化”:

var width = 500, height = 200;

var svg = d3.select("#svgdiv")
	.append("svg")
	.attr("width", width)
	.attr("height", height);
	
var data = d3.range(100).map(function(d, i)
	return 
	group: Math.random()*2 > 1 ? "blue" : "red",
	id: i
	
);

var xScale = d3.scaleOrdinal()
	.domain([0, 1])
	.range([100, width-100]);

var foci = 
    "blue" : 
         "x" : xScale(0),
         "y": height / 2
    ,
    "red": 
         "x" : xScale(1),
         "y": height / 2
    
;

var forceX = d3.forceX((d) => foci[d.group].x);
var forceY = d3.forceY((d) => foci[d.group].y);

var node = svg.append("g")
            .attr("class", "nodes")
            .selectAll("circle")
            .data(data)
            .enter().append("circle")
            .attr("r", 5)
						.attr("fill", (d)=>d.group);


var force = d3.forceSimulation(data)
    .velocityDecay(0.65)
    .force('x', forceX)
    .force('y', forceY)
    .force("collide", d3.forceCollide(8));
		
force.nodes(data)
    .on('tick', function() 
         node
            .attr('transform', (d) => 
                return 'translate(' + (d.x) + ',' + (d.y) + ')';
            );
        );
				
d3.select("#btn").on("click", function()
    data.forEach(function(d)
		d.group = Math.random()*2 > 1 ? "blue" : "red"
		)
		node.transition().duration(500).attr("fill", (d)=>d.group);
		setTimeout(function()
		force.nodes(data);
		force.alpha(0.8).restart();
		, 1500)
)
<script src="https://d3js.org/d3.v4.min.js"></script>
<button id="btn">Randomize</button>
<div id="svgdiv"><div>

PS:我把 reheat 放在了 setTimeout 中,所以你可以先看到圆圈改变颜色,然后移动到焦点位置。

【讨论】:

感谢您的回复。这绝对是朝着正确方向迈出的一步。我实施了你的建议,但它仍然不适合我。当我使用下拉列表更新力模拟时,我仍然会“俯冲”。我相信您的演示和我的演示之间的区别在于数据。您正在将绑定到一个圆圈的相同数据中的组随机化。就我而言,我得到了一个全新的对象数组。 嗯,sn-p 清楚地表明我的代码有效......无论如何,我希望其他人能解决你的问题。干杯。 我在写上一条评论时不小心按了 Enter 按钮,而我还没来得及说完就发布了。如果我听起来直率,我深表歉意! 现在我明白了。所以,你的新数据可以有不同的长度,对吗?在那种情况下,我相信您可以使用密钥来保持我们所谓的“对象恒定性”。 不,先生。我的数据长度也将是 100。所以,假设我的起始数据——最初将为圆圈着色的数据——是 50 个蓝色、49 个红色和 1 个紫色。这是在一个包含 50 个蓝色对象、49 个红色对象等的数组中提供的,我将它们绑定到我的 HTML 中的 100 个 svg 圆圈。现在,当我点击下拉菜单时,我得到了一个新的对象数组:20 个蓝色、40 个红色和 40 个紫色。这就是我卡住的地方。因为我正在将我的数据重新绑定到 HTML,所以发生了猛扑。我是否需要操纵起始数据以匹配我从下拉列表中获得的数据?就像你在处理原始数据一样?

以上是关于在多焦点 d3 力布局中重新定位节点的主要内容,如果未能解决你的问题,请参考以下文章

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

使用 d3.js 保存和重新加载强制布局

修复 D3 强制定向布局中的节点位置

D3.js 重新启动模拟时节点跳转添加或删除节点

修改 d3 的力布局“退火”时间表

更新 D3v4 中的力布局