Packing different sized circles into rectangle - d3.js

后端 未结 5 455
北荒
北荒 2020-11-27 12:59

I was trying to pack circles of different sizes into a rectangular container, not packing in circular container that d3.js bundled with, under

5条回答
  •  庸人自扰
    2020-11-27 13:47

    A completely different approach...

    As I mentioned in a comment, a d3 cluster-force layout could be adapted into a heuristic method for fitting the circles into the box, by progressively changing the scale until you have a tight fit.

    Results so far are not perfect, so I present a few versions:

    Option 1, squeezes in the box to the space occupied by the circles before adjusting for circle overlap. The result is very tightly packed, but with slight overlap between circles that get caught between the walls of the box and each other, unable to move without a conflict:
    https://jsfiddle.net/LeGfW/2/

    Circle Packing results, option 1

    Option 2, squeezes in the box after separating overlapped circles. This avoids overlap, but the packing isn't optimum since we don't ever push the circles into each other to force them to spread out to fill the long dimension of the rectangle:
    https://jsfiddle.net/LeGfW/3/

    Circle packing results, option 2

    Option 3, the happy medium, again squeezes in after adjusting for overlap, but the squeeze factor is based on average out the room in width and height dimensions, instead of the minimum room, so it keeps squeezing until both width and height are filled:
    https://jsfiddle.net/LeGfW/5/

    Circle packing results, option 3

    Key code consists of the updateBubbles method called by the force tick, and the collide method which is called in the first line of updateBubbles. This is the "option 3" version:

    // Create a function for this tick round,
    // with a new quadtree to detect collisions 
    // between a given data element and all
    // others in the layout, or the walls of the box.
    
    //keep track of max and min positions from the quadtree
    var bubbleExtent;
    function collide(alpha) {
      var quadtree = d3.geom.quadtree(data);
      var maxRadius = Math.sqrt(dataMax);
      var scaledPadding = padding/scaleFactor;
      var boxWidth = width/scaleFactor;
      var boxHeight = height/scaleFactor;
    
        //re-set max/min values to min=+infinity, max=-infinity:
      bubbleExtent = [[Infinity, Infinity],[-Infinity, -Infinity]];
    
      return function(d) {
    
          //check if it is pushing out of box:
        var r = Math.sqrt(d.size) + scaledPadding,
            nx1 = d.x - r,
            nx2 = d.x + r,
            ny1 = d.y - r,
            ny2 = d.y + r;
    
          if (nx1 < 0) {
               d.x = r;
          }
          if (nx2 > boxWidth) {
               d.x = boxWidth - r;
          }
          if (ny1 < 0) {
               d.y = r;
          }
          if (ny2 > boxHeight) {
               d.y = boxHeight - r;
          }
    
    
        //check for collisions
        r = r + maxRadius, 
            //radius to center of any possible conflicting nodes
            nx1 = d.x - r,
            nx2 = d.x + r,
            ny1 = d.y - r,
            ny2 = d.y + r;
    
        quadtree.visit(function(quad, x1, y1, x2, y2) {
          if (quad.point && (quad.point !== d)) {
            var x = d.x - quad.point.x,
                y = d.y - quad.point.y,
                l = Math.sqrt(x * x + y * y),
                r = Math.sqrt(d.size) + Math.sqrt(quad.point.size)
                        + scaledPadding;
            if (l < r) {
              l = (l - r) / l * alpha;
              d.x -= x *= l;
              d.y -= y *= l;
              quad.point.x += x;
              quad.point.y += y;
            }
          }
          return x1 > nx2 || x2 < nx1 || y1 > ny2 || y2 < ny1;
        });
    
        //update max and min
        r = r-maxRadius; //return to radius for just this node
        bubbleExtent[0][0] = Math.min(bubbleExtent[0][0], 
                                      d.x - r);
        bubbleExtent[0][1] = Math.min(bubbleExtent[0][1], 
                                      d.y - r);
        bubbleExtent[1][0] = Math.max(bubbleExtent[1][0], 
                                      d.x + r);
        bubbleExtent[1][1] = Math.max(bubbleExtent[1][1], 
                                      d.y + r);
    
      };
    }  
    
    function updateBubbles() {
    
        bubbles
            .each( collide(0.5) ); //check for collisions   
    
        //update the scale to squeeze in the box 
        //to match the current extent of the bubbles
        var bubbleWidth = bubbleExtent[1][0] - bubbleExtent[0][0];
        var bubbleHeight = bubbleExtent[1][1] - bubbleExtent[0][1];
    
        scaleFactor = (height/bubbleHeight +
                               width/bubbleWidth)/2; //average
        /*
        console.log("Box dimensions:", [height, width]);
        console.log("Bubble dimensions:", [bubbleHeight, bubbleWidth]);
        console.log("ScaledBubble:", [scaleFactor*bubbleHeight,
                                     scaleFactor*bubbleWidth]);
        //*/
    
        rScale
            .range([0,  Math.sqrt(dataMax)*scaleFactor]);
    
        //shift the bubble cluster to the top left of the box
        bubbles
            .each( function(d){
                d.x -= bubbleExtent[0][0];
                d.y -= bubbleExtent[0][1];
            });
    
        //update positions and size according to current scale:
        bubbles
            .attr("r", function(d){return rScale(d.size);} )
            .attr("cx", function(d){return scaleFactor*d.x;})
            .attr("cy", function(d){return scaleFactor*d.y;})
    }
    

提交回复
热议问题