Most basic circular packing





This post describes how to build a very basic circular packing with d3.js. In this example, 8 nodes are represented by 8 circles of same size. You can see many other examples in the circular packing section of the gallery. This example works with d3.js v4 and v6


Circular packing section

Steps:

  • Circular packing requires information on nodes as input. Here the input data is pretty simple, each node just have a name.

  • For each node, a circle is created using a classic enter() and append() approach. All circles are initialized with the same position: the center of the svg area.

  • D3’s force layout will use a physics based simulator to find the best circle positions.
    d3.forceSimulation() allows to define the forces we want to apply to circles

  • Here, circles are attracted toward the center (forceCenter), are slightly attracted one each other (forceManyBody), and overlapping is avoided (forceCollide).

  • This forces are applied to nodes through several iteration. At each iteration (tick), node position is updated

  • D3 knows when the circle position is satisfying and will stop the simulations.
|
<!DOCTYPE html>
<meta charset="utf-8">

<!-- Load d3.js -->
<script src="https://d3js.org/d3.v4.js"></script>

<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>

<!DOCTYPE html>
<meta charset="utf-8">
          
<!-- Load d3.js -->
<script src="https://d3js.org/d3.v6.js"></script>
          
<!-- Create a div where the graph will take place -->
<div id="my_dataviz"></div>
      
<script>

// set the dimensions and margins of the graph
var width = 450
var height = 450

// append the svg object to the body of the page
var svg = d3.select("#my_dataviz")
  .append("svg")
    .attr("width", 450)
    .attr("height", 450)

// create dummy data -> just one element per circle
var data = [{ "name": "A" }, { "name": "B" }, { "name": "C" }, { "name": "D" }, { "name": "E" }, { "name": "F" }, { "name": "G" }, { "name": "H" }]

// Initialize the circle: all located at the center of the svg area
var node = svg.append("g")
  .selectAll("circle")
  .data(data)
  .enter()
  .append("circle")
    .attr("r", 25)
    .attr("cx", width / 2)
    .attr("cy", height / 2)
    .style("fill", "#69b3a2")
    .style("fill-opacity", 0.3)
    .attr("stroke", "#69a2b2")
    .style("stroke-width", 4)

// Features of the forces applied to the nodes:
var simulation = d3.forceSimulation()
    .force("center", d3.forceCenter().x(width / 2).y(height / 2)) // Attraction to the center of the svg area
    .force("charge", d3.forceManyBody().strength(0.5)) // Nodes are attracted one each other of value is > 0
    .force("collide", d3.forceCollide().strength(.01).radius(30).iterations(1)) // Force that avoids circle overlapping

// Apply these forces to the nodes and update their positions.
// Once the force algorithm is happy with positions ('alpha' value is low enough), simulations will stop.
simulation
    .nodes(data)
    .on("tick", function(d){
      node
          .attr("cx", function(d){ return d.x; })
          .attr("cy", function(d){ return d.y; })
    });


</script>
<script>

// set the dimensions and margins of the graph
const width = 450
const height = 450

// append the svg object to the body of the page
const svg = d3.select("#my_dataviz")
  .append("svg")
    .attr("width", 450)
    .attr("height", 450)

// create dummy data -> just one element per circle
const data = [{ "name": "A" }, { "name": "B" }, { "name": "C" }, { "name": "D" }, { "name": "E" }, { "name": "F" }, { "name": "G" }, { "name": "H" }]

// Initialize the circle: all located at the center of the svg area
const node = svg.append("g")
  .selectAll("circle")
  .data(data)
  .join("circle")
    .attr("r", 25)
    .attr("cx", width / 2)
    .attr("cy", height / 2)
    .style("fill", "#69b3a2")
    .style("fill-opacity", 0.3)
    .attr("stroke", "#69a2b2")
    .style("stroke-width", 4)

// Features of the forces applied to the nodes:
const simulation = d3.forceSimulation()
    .force("center", d3.forceCenter().x(width / 2).y(height / 2)) // Attraction to the center of the svg area
    .force("charge", d3.forceManyBody().strength(0.5)) // Nodes are attracted one each other of value is > 0
    .force("collide", d3.forceCollide().strength(.01).radius(30).iterations(1)) // Force that avoids circle overlapping

// Apply these forces to the nodes and update their positions.
// Once the force algorithm is happy with positions ('alpha' value is low enough), simulations will stop.
simulation
    .nodes(data)
    .on("tick", function(d){
      node
          .attr("cx", d => d.x)
          .attr("cy", d => d.y)
    });

</script>

Related blocks →