Conflict between d3.forceCollide() and d3.forceX/Y() with high strength() value

怎甘沉沦 提交于 2019-12-01 00:28:29

As of v4 the workings of the force layout were switched from position Verlet integration to velocity Verlet integration. The forces, which are consecutively applied, will calculate their changes to the velocities of the nodes. These effects are calculated by each force and are added up to the nodes' vx and vy velocity values. After all forces have been calculated for the actual tick, the new positions of the nodes are calculated by adding the resulting (i.e. integrated over all forces) velocities to the current positions.

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;
}

This is the reason, why the collision seems to be centered with an offset. This is much more obvious in the first snippet because of your choice of forces and their respective parameters. It is worth noting, though, that the effect is also visible in your second example, if much less prominent.

It is important to notice, that the positioning forces d3.forceX and d3.forceY are somewhat powerful and reckless (emphasis mine):

The strength determines how much to increment the node’s x-velocity: (x - node.x) × strength. For example, a value of 0.1 indicates that the node should move a tenth of the way from its current x-position to the target x-position with each application. Higher values moves nodes more quickly to the target position, often at the expense of other forces or constraints.

Setting the strength to 1, which is the upper limit of the recommended range, will almost immediately force the nodes to the end position whereby diminishing the smaller effects of other forces. Integrating this dominant effect together with the less powerful effects of the other forces gives the behaviour as shown in your first snippet. Because of the strong forceX and forceY the nodes are forced to their positions in a rigid grid emphasizing the hole around the previous position.

Another issues adding to the problem is that you are not re-heating the simulation properly. In your dragstarted and dragended handler functions you are using simulation.alphaTarget to heat up the system, which is not the correct way of doing it. To control the entropy of the system, you should always use simulation.alpha instead of simulation.alphaTarget. While the latter will kind of work in some cases because of the way it is used in the formula, it is a hack and the behaviour is most likely not what you want. To wrap this up, use alpha to control the heat and alphaTarget, alphaMin or alphaDecay to tweak the curve of the decay. The following snippet is copied from your question and has the re-heating adjusted to use 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>

As you can see, when dragging and holding the large circle, the offset will slowly decrease and eventually vanish, because the simulation is re-heated enough to keep it running.

Is it a bug? No, it is the way, the force layout works in v4. This is enhanced by some extreme parameter settings.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!