goollan
goollan

Reputation: 785

How do I get an even spread between my bubbles?

I have a bubble chart in D3. I have it working for displaying about 1,000 bubbles, but I'd like to make it work to display 2 or even 3 thousand bubbles. I'm taking inspiration from Shirley Wu's amazing visualization with Guardian here.

My bubbles have an uneven spread where more of them are packed in towards the center.

enter image description here

I'd like to get it so they are evenly spread out across the whole space, like this.

enter image description here

These are the components that I've been trying to manipulate.

// Forces
const radius = 1.5
const padding1 = 15;
const padding2 = 5;
const strength = .3
const alpha = .1
const alpha_decay = 0
const alpha_min = 0.001
const alpha_Collision = .03;
const charge_strength = -.5
const charge_theta = .9
const veloc_decay = .5

And this is the code where my force is defined.

  // Circle for each node.
    const circle = svg.append("g")
        .selectAll("circle")
        .data(nodes)
        .join("circle")
          .attr("cx", d => d.x)
          .attr("cy", d => d.y)
          .attr("fill", d => d.color)
          .attr("r", d => d.r);

     // Forces
     const simulation = d3.forceSimulation(nodes)
         .force("x", d => d3.forceX(d.x))
         .force("y", d => d3.forceY(d.y))
         .force("cluster", forceCluster())
         .force("collide", forceCollide())
         .force("charge", d3.forceManyBody().strength(charge_strength).theta(charge_theta))
         .alpha(alpha)
         .alphaDecay(alpha_decay)
         .alphaMin(alpha_min)
         .velocityDecay(veloc_decay);

     // Adjust position of circles.
      simulation.on("tick", () => {
          circle
              .attr("cx", d => d.x)
              .attr("cy", d => d.y)
              .attr("fill", d => groups[d.group].color);
          });

      // Force to increment nodes to groups.
      function forceCluster() {
        let nodes;

        function force(alpha) {
          const l = alpha * strength;
          for (const d of nodes) {
            d.vx -= (d.x - groups[d.group].x) * l;
            d.vy -= (d.y - groups[d.group].y) * l;
          }
        }
        force.initialize = _ => nodes = _;

        return force;
      }

      // Force for collision detection.
      function forceCollide() {
        let nodes;
        let maxRadius;

        function force() {
          const quadtree = d3.quadtree(nodes, d => d.x, d => d.y);
          for (const d of nodes) {
            const r = d.r + maxRadius;
            const nx1 = d.x - r, ny1 = d.y - r;
            const nx2 = d.x + r, ny2 = d.y + r;
            quadtree.visit((q, x1, y1, x2, y2) => {

              if (!q.length) do {
                if (q.data !== d) {
                  const r = d.r + q.data.r + (d.group === q.data.group ? padding1 : padding2);
                  let x = d.x - q.data.x, y = d.y - q.data.y, l = Math.hypot(x, y);
                  if (l < r) {
                    l = (l - r) / l * alpha_Collision;
                    d.x -= x *= l, d.y -= y *= l;
                    q.data.x += x, q.data.y += y;
                  }
                }
              } while (q = q.next);
              return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
            });
          }
        }

        force.initialize = _ => maxRadius = d3.max(nodes = _, d => d.r) + Math.max(padding1, padding2);

        return force;
      }

I have the code uploaded here. Part of the problem is that the bubbles in the center are constantly moving. It's like they're trying to find a spot to settle in, but the width of the space is not large enough (except that if I make it bigger, the whole blob becomes too large).

I don't know how to stop the movement. It looks nicer when I set alpha_decay to .1 but the bubbles change location on the visualization from unaware to aware and they won't move if alpha_decay > 0.

How can I get the bubbles evenly spread out without clustering in the center, like shown in the example?

Upvotes: 2

Views: 426

Answers (1)

Adam Genshaft
Adam Genshaft

Reputation: 824

You can use native d3 force methods to achieve it. Just play with the different forces of charge, center and collision.

height = 300;
width = 300;

const nodes = [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, ];
  const simulation = d3.forceSimulation(nodes)
    .force('charge', d3.forceManyBody().strength(0.4))
    .force('center', d3.forceCenter(width / 2, height / 2))
    .force('collision', d3.forceCollide().radius(6))
    .on('tick', ticked);

  function ticked() {
    var u = d3.select('svg')
    .selectAll('circle')
    .data(nodes)

    u.enter()
      .append('circle')
      .attr('r', 5)
      .merge(u)
      .attr('cx', function(d) {
        return d.x
      })
      .attr('cy', function(d) {
        return d.y
      });

    u.exit().remove()
  }
<script src="https://cdnjs.cloudflare.com/ajax/libs/d3/5.7.0/d3.min.js"></script>
<svg width="300" height="300"></svg>

Upvotes: 1

Related Questions