我相信我在D3 v4.x(v4.5.0)中发现了一个错误,当在一个强制导向的图表中,一个组合d3.forceCollide以防止节点重叠,d3.forceX/Y设置位置具有高strength
值的节点,我即将在D3 GitHub存储库中创建一个问题。
但是,在创建问题之前,我想在StackOverflow上与D3用户一起检查它:它可能不是一个错误,它可能是使用这种组合的非常期望的行为。
在S.O.下面的片段,我正在绘制900(30 x 30)个圆圈。它们使用d3.forceX
和d3.forceY
定位,具有高强度值(默认值为0.1):
.force("xPos", d3.forceX(d => scale(d.x)).strength(1))
.force("yPos", d3.forceY(d => scale(d.y)).strength(1))
为了防止圆圈重叠,我正在使用d3.forceCollide,基于它们的半径:
.force("collide", d3.forceCollide(d => d.radius * 1.2).strength(1))
半径仅为2px,除了中心节点,我特意做的更大。这是您要拖动以重现问题的节点。
点击“运行代码段”并拖动中心圆圈。它将按照d3.forceCollide
中的配置排斥小圆圈。
然而,与任何其他节点一样,这个大圆圈有x
和y
位置由d3.forceX/Y
设置,这些力量正在将碰撞力的中心移向圆的原始位置。你可以看到,你从中心移动的距离越远,排斥力变得越不准确:它几乎就像有一个排斥小圆圈的鬼圈,鬼魂总是在实际的SVG元素和它的位置集之间d3.forceX/Y
。
var n = 30,
width = 300,
padding = 5,
nodes = [];
for (var y = 0; y < n; ++y) {
for (var x = 0; x < n; ++x) {
nodes.push({
x: x,
y: y
})
}
}
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", width);
var scale = d3.scaleLinear()
.domain([0, 29])
.range([padding, width - padding]);
var simulation = d3.forceSimulation()
.force("charge", d3.forceManyBody().strength(-1))
.force("xPos", d3.forceX(d => scale(d.x)).strength(1))
.force("yPos", d3.forceY(d => scale(d.y)).strength(1))
.force("collide", d3.forceCollide(d => d.radius * 1.2).strength(1));
var circles = svg.selectAll("foo")
.data(nodes)
.enter()
.append("circle")
.attr("fill", "darkslateblue")
.attr("r", d => {
d.x == 14 && d.y == 14 ? d.radius = 25 : d.radius = 2;
return d.radius
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
simulation.nodes(nodes)
.on("tick", ticked);
function ticked() {
circles.attr("cx", d => d.x).attr("cy", d => d.y)
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
在第二个片段中,默认strength
(即0.1),奇怪的行为消失了:
var n = 30,
width = 300,
padding = 5,
nodes = [];
for (var y = 0; y < n; ++y) {
for (var x = 0; x < n; ++x) {
nodes.push({
x: x,
y: y
})
}
}
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", width);
var scale = d3.scaleLinear()
.domain([0, 29])
.range([padding, width - padding]);
var simulation = d3.forceSimulation()
.force("charge", d3.forceManyBody().strength(-1))
.force("xPos", d3.forceX(d => scale(d.x)).strength(0.1))
.force("yPos", d3.forceY(d => scale(d.y)).strength(0.1))
.force("collide", d3.forceCollide(d => d.radius * 1.2).strength(1));
var circles = svg.selectAll("foo")
.data(nodes)
.enter()
.append("circle")
.attr("fill", "darkslateblue")
.attr("r", d => {
d.x == 14 && d.y == 14 ? d.radius = 25 : d.radius = 2;
return d.radius
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
simulation.nodes(nodes)
.on("tick", ticked);
function ticked() {
circles.attr("cx", d => d.x).attr("cy", d => d.y)
}
function dragstarted(d) {
if (!d3.event.active) simulation.alphaTarget(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alphaTarget(0);
d.fx = null;
d.fy = null;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
当d3.forceCollide
和d3.forceX/Y
与strength
混合时,这是预期的行为吗? d3.forceCollide
不应该基于SVG圈元素的实际cx
/ cy
(d.x
/ d.y
)位置?我理解,正如文档明确指出的那样,
较高的值会将节点更快地移动到目标位置
但是,d3.forceX/Y
并未移动实际的SVG元素,只移动了碰撞力的计算中心。
那么,这是一个错误还是预期的行为?
答案 0 :(得分:3)
从第4节开始,力布局的运作从{Verlet积分到速度Verlet积分的位置switched。连续施加的力将计算它们对节点速度的变化。这些效果由每个力计算,并累加到节点的vx
和vy
速度值。在计算出实际刻度的所有力之后,通过将得到的(即所有力的积分)速度加到当前位置,节点的新位置为calculated。
forces.each(function(force) {
force(alpha);
});
for (i = 0; i < n; ++i) {
node = nodes[i];
if (node.fx == null) node.x += node.vx *= velocityDecay;
else node.x = node.fx, node.vx = 0;
if (node.fy == null) node.y += node.vy *= velocityDecay;
else node.y = node.fy, node.vy = 0;
}
这就是为什么碰撞似乎以偏移为中心的原因。这在第一个片段中更为明显,因为您可以选择力和它们各自的参数。值得注意的是,在第二个例子中,效果也很明显,如果不那么突出的话。
重要的是要注意,定位力d3.forceX
和d3.forceY
有点强大,reckless(强调我的):
strength 确定增加节点 x - 速度的程度:( x - node.x )×< EM>强度。例如,值为0.1表示节点应该在每个应用程序中从当前x位置移动到目标x位置的十分之一。较高的值会使节点更快地移动到目标位置,通常以牺牲其他力或约束为代价。
将强度设置为 1 ,这是推荐范围的上限,几乎会立即迫使节点到达终点位置,从而减少其他力的较小影响。将这种显性效应与其他力量的不那么强大的效果结合在一起会产生第一个片段中显示的行为。由于强forceX
和forceY
,节点被强制到刚性网格中的位置,强调前一个位置周围的洞。
此问题的另一个问题是您没有正确地重新加热模拟。在dragstarted
和dragended
处理程序函数中,您使用simulation.alphaTarget
来加热系统,这不是正确的方法。要控制系统的熵,您应始终使用simulation.alpha
而不是simulation.alphaTarget
。虽然后者在某些情况下有点工作,因为它在公式中的使用方式,但它是一种黑客攻击,而且这种行为很可能不是你想要的。要进行换行,请使用alpha
来控制热量,并使用alphaTarget
,alphaMin
或alphaDecay
来调整衰减曲线。从您的问题中复制以下代码段,并将重新加热调整为使用simulation.alpha
。
var n = 30,
width = 300,
padding = 5,
nodes = [];
for (var y = 0; y < n; ++y) {
for (var x = 0; x < n; ++x) {
nodes.push({
x: x,
y: y
})
}
}
var svg = d3.select("body")
.append("svg")
.attr("width", width)
.attr("height", width);
var scale = d3.scaleLinear()
.domain([0, 29])
.range([padding, width - padding]);
var simulation = d3.forceSimulation()
.force("charge", d3.forceManyBody().strength(-1))
.force("xPos", d3.forceX(d => scale(d.x)).strength(1))
.force("yPos", d3.forceY(d => scale(d.y)).strength(1))
.force("collide", d3.forceCollide(d => d.radius * 1.2).strength(1));
var circles = svg.selectAll("foo")
.data(nodes)
.enter()
.append("circle")
.attr("fill", "darkslateblue")
.attr("r", d => {
d.x == 14 && d.y == 14 ? d.radius = 25 : d.radius = 2;
return d.radius
})
.call(d3.drag()
.on("start", dragstarted)
.on("drag", dragged)
.on("end", dragended));
simulation.nodes(nodes)
.on("tick", ticked);
function ticked() {
circles.attr("cx", d => d.x).attr("cy", d => d.y)
}
function dragstarted(d) {
if (!d3.event.active) simulation.alpha(0.3).restart();
d.fx = d.x;
d.fy = d.y;
}
function dragged(d) {
d.fx = d3.event.x;
d.fy = d3.event.y;
}
function dragended(d) {
if (!d3.event.active) simulation.alpha(0);
d.fx = null;
d.fy = null;
}
<script src="https://d3js.org/d3.v4.min.js"></script>
正如您所看到的,当拖动并按住大圆圈时,偏移将缓慢减小并最终消失,因为模拟会重新加热以使其保持运转。
这是一个错误吗?不,这是方式,力布局在v4中工作。一些极端参数设置增强了这一点。