d3.js
circle packing
data visualization
rectangle packing
SVG graphics

Packing different sized circles into rectangle - d3.js

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

Introduction

Packing circles of different sizes inside a rectangle is not something d3.pack() solves directly. D3's built-in pack layout arranges circles inside an enclosing circle, so for a rectangular container you normally use a force simulation with collision handling and boundary constraints.

Why a Force Simulation Works

Each circle has an x, y, and r value. A force simulation nudges circles until overlaps are resolved. On every tick, you can clamp each circle back inside the rectangular bounds.

That will not always produce a mathematically optimal packing, but it is practical, animated, and easy to adapt for interactive graphics.

Basic D3 Example

The example below creates circles with different radii and packs them into an 800 by 400 SVG area.

html
1<!DOCTYPE html>
2<html lang="en">
3<head>
4  <meta charset="UTF-8">
5  <script src="https://d3js.org/d3.v7.min.js"></script>
6</head>
7<body>
8  <svg id="chart" width="800" height="400"></svg>
9
10  <script>
11    const width = 800;
12    const height = 400;
13
14    const data = [40, 35, 30, 28, 24, 22, 18, 16, 14, 12].map((r, i) => ({
15      id: i,
16      r,
17      x: Math.random() * width,
18      y: Math.random() * height
19    }));
20
21    const svg = d3.select("#chart");
22
23    const circles = svg.selectAll("circle")
24      .data(data)
25      .enter()
26      .append("circle")
27      .attr("r", d => d.r)
28      .attr("fill", "steelblue")
29      .attr("stroke", "#1f3b5c");
30
31    const simulation = d3.forceSimulation(data)
32      .force("x", d3.forceX(width / 2).strength(0.03))
33      .force("y", d3.forceY(height / 2).strength(0.03))
34      .force("collide", d3.forceCollide().radius(d => d.r + 1).iterations(4))
35      .on("tick", ticked);
36
37    function ticked() {
38      data.forEach(d => {
39        d.x = Math.max(d.r, Math.min(width - d.r, d.x));
40        d.y = Math.max(d.r, Math.min(height - d.r, d.y));
41      });
42
43      circles
44        .attr("cx", d => d.x)
45        .attr("cy", d => d.y);
46    }
47  </script>
48</body>
49</html>

The collision force keeps circles from overlapping, while the clamp logic ensures each one stays inside the rectangle.

Improving the Layout

A plain center force tends to create a blob near the middle. If you want a more even fill, seed the initial positions on a grid or use several weak attractors instead of only the center.

For example, starting circles near the center can reduce settling time, while starting them on a grid can reduce early collisions. The best choice depends on whether you want a dense central cluster or a more uniformly distributed arrangement.

You can also sort circles by radius before binding data. Larger circles tend to stabilize better when the layout gives them space early.

When d3.pack() Is Still Useful

Even though d3.pack() targets circular enclosures, it can still provide reasonable initial coordinates for a force simulation. One workflow is:

  1. Generate a rough packed layout with d3.pack().
  2. Copy those coordinates into your nodes.
  3. Run a rectangular force simulation to adjust them.

That hybrid approach is useful when the circles represent hierarchy and you want the force layout to preserve some of the packed structure.

Boundary Handling Matters

Without explicit boundary logic, the collision force can push circles outside the SVG area. Clamping during each tick is the simplest fix, but it can make circles stick to walls.

If you want smoother wall behavior, add custom forces that push circles away from edges before they cross the boundary. That requires more code, but the motion looks more natural than hard clamping.

Performance Considerations

Force simulations are fine for dozens or a few hundred circles, but performance drops as the number of circles grows. Large datasets may need fewer iterations, smaller collision padding, or offscreen precomputation.

If the layout is static, you can let the simulation settle once and then stop it.

javascript
simulation.alpha(1).restart();
setTimeout(() => simulation.stop(), 3000);

That avoids continuous animation cost after the layout has stabilized.

Common Pitfalls

A common mistake is trying to use d3.pack() alone and expecting it to respect a rectangular boundary. It will not, because its geometry is based on a circle-packing model.

Another issue is forgetting that collision radius should usually include a little padding. Without it, circles can visually touch or flicker at the boundary between collisions.

Developers also sometimes clamp position only once instead of on every tick. The simulation will immediately push nodes out again unless the bounds are enforced continuously.

Summary

  • 'd3.pack() does not directly pack circles into a rectangle.'
  • A force simulation with forceCollide() is the usual D3 solution.
  • Clamp node positions on every tick to keep circles inside the rectangle.
  • Seed positions and sort by size if you want a more stable layout.
  • For static charts, stop the simulation after it settles.

Related reading
Free course
Beginner
7 lessons
2 hours
Tackling System Design Interview Problems

A short course that equips you with the skills to approach system design interviews methodically.

Start the free course
Track what you have practised

A free account saves your progress, solutions and study plan across every problem on Codemia.

ML System Design practice on Codemia

Design recommenders, ranking systems and training pipelines the way ML interviews actually ask for them, with worked solutions.

Practice ML system design

All Rights Reserved.