Position circles on a horizontal axis without overlapping using force layout

末鹿安然 提交于 2019-12-24 15:53:22

问题


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

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