问题
I would like to position circles on a d3 scale and relax them in such a way that they do not overlap. I know that this decreases accuracy, but that's okay for the type of chart that I would like to generate.
This is my minimum (non-)working example: https://jsfiddle.net/wmxh0gpb/1/
<body>
<div id="content">
<svg width="700" height="200">
<g transform="translate(50, 100)"></g>
</svg>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.2/d3.min.js"></script>
<script>
var width = 600, height = 400;
var xScale = d3.scaleLinear().domain([0, 1]).range([0, 300]);
var numNodes = 5;
var nodes = d3.range(numNodes).map(function(d, i) {
return {
value: Math.random()
}
});
var simulation = d3.forceSimulation(nodes)
.force('x', d3.forceX().strength(0.5).x(function(d) {
return xScale(d.value);
}))
.force('collision', d3.forceCollide().strength(1).radius(50))
.on('tick', ticked);
function ticked() {
var u = d3.select('svg g')
.selectAll('circle')
.data(nodes);
u.enter()
.append('circle')
.attr('r', function(d) {
return 25;
})
.style('fill', function(d) {
return "black";
})
.merge(u)
.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return 0
})
.attr("opacity", 0.5)
u.exit().remove();
}
</script>
</body>
The circles are positioned using the forceX force and collision should be prevented using forceCollide. However, the circles seem to find a stable position regardless of the overlap instead of avoiding it.
What am I doing wrong?
回答1:
Because you ignore the y coord of the force simulation
Add this as the last line of the tick
function. Now you force the nodes to be at y==0
nodes.forEach(e => { e.fy = 0 });
And set the radius of the collision force to the real radius (25)
.force('collision', d3.forceCollide().strength(1).radius(25))
回答2:
The technical name for this is beeswarm chart: only one axis contains meaningful information, the other one is used only to separate the nodes.
For creating a beeswarm chart in D3 you have to pass the y
position to the force (as d3.forceY
) as well, in this case with 0
(since you're already translating the group), like:
var simulation = d3.forceSimulation(nodes)
.force('x', d3.forceX(function(d) {
return xScale(d.value);
}).strength(0.8))
.force('y', d3.forceY(0).strength(0.2))
As you can see, the forceX
and forceY
have different strength
values. You have to play with them until you find a combination that suits you: after all, a beeswarm chart is a trade-off between accuracy and avoiding overlap the nodes.
Not related to the question, but very important: remove everything from the ticked
function that is not related to repositioning the nodes. The ticked
function will run dozens of times per second, normally running 300 times before the simulation cools down. There is no sense in updating, entering and exiting selections 300 times!
Here is your code with those changes:
<body>
<div id="content">
<svg width="700" height="200">
<g transform="translate(50, 100)"></g>
</svg>
</div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/4.2.2/d3.min.js"></script>
<script>
var width = 600,
height = 400;
var xScale = d3.scaleLinear().domain([0, 1]).range([0, 300]);
var numNodes = 5;
var nodes = d3.range(numNodes).map(function(d, i) {
return {
value: Math.random()
}
});
var simulation = d3.forceSimulation(nodes)
.force('x', d3.forceX(function(d) {
return xScale(d.value);
}).strength(0.8))
.force('y', d3.forceY(0).strength(0.2))
.force('collision', d3.forceCollide().strength(1).radius(25))
.on('tick', ticked);
var u = d3.select('svg g')
.selectAll('circle')
.data(nodes);
u = u.enter()
.append('circle')
.attr('r', function(d) {
return 25;
})
.style('fill', function(d) {
return "black";
})
.merge(u)
.attr("opacity", 0.5)
u.exit().remove();
function ticked() {
u.attr('cx', function(d) {
return d.x;
})
.attr('cy', function(d) {
return d.y
})
}
</script>
</body>
来源:https://stackoverflow.com/questions/53109918/position-circles-on-a-horizontal-axis-without-overlapping-using-force-layout