How to Place D3.js cluster Nodes on a Circle

Introduces how to place nodes around a circle with D3’s cluster layout for hierarchical structures.

For the hierarchy data structure and data preparation, see Hierarchy data structure and usage.

Example program

View code

Sample code

<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>D3 v5 hierarchy cluster radial v4/v5</title>
</head>

<body>
  <svg width="800" height="600"></svg>
  <script src="https://d3js.org/d3.v5.min.js"></script>
  <script>
    // 1. Prepare the data to draw
    var width = document.querySelector("svg").clientWidth;
    var height = document.querySelector("svg").clientHeight;
    var data = {
      "name": "A",
      "children": [
        { "name": "B" },
        {
          "name": "C",
          "children": [{ "name": "D" }, { "name": "E" }, { "name": "F" }]
        },
        { "name": "G" },
        {
          "name": "H",
          "children": [{ "name": "I" }, { "name": "J" }]
        },
        { "name": "K" },
        {
          "name": "L",
          "children": [{ "name": "M" }, { "name": "N" }]
        },
        { "name": "O" },
        { "name": "P" }
      ]
    };

    var rx = width / 2;
    var ry = height / 2

    // 2. Convert the data to draw
    root = d3.hierarchy(data);
    var cluster = d3.cluster().size([360, ry - 80])
    cluster(root);

    // 3. Configure SVG elements
    g = d3.select("svg").append("g").attr("transform", "translate(" + rx + "," + ry + ")");
    var link = g.selectAll(".link")
      .data(root.links())
      .enter()
      .append("path")
      .attr("class", "link")
      .attr("fill", "none")
      .attr("stroke", "#555")
      .attr("stroke-width", "1.5px")
      .attr("opacity", "0.6")
      .attr("d", d3.linkRadial()
        .angle(function (d) { return (d.x + 90) * Math.PI / 180; })
        .radius(function (d) { return d.y; }));

    var node = g.selectAll(".node")
      .data(root.descendants())
      .enter()
      .append("g")
      .attr("transform", function (d) { return "rotate(" + (d.x) + ")translate(" + d.y + ")"; })

    node.append("circle")
      .attr("r", 8)
      .attr("stroke", "steelblue")
      .attr("stroke-width", "1.5px")
      .attr("fill", "white");

    node.append("text")
      .attr("dy", 3)
      .attr("dx", function (d) { return d.x < 90 || d.x > 270 ? 8 : -8; })
      .style("text-anchor", function (d) { return d.x < 90 || d.x > 270 ? "start" : "end"; })
      .attr("font-size", "200%")
      .attr("transform", function (d) { return d.x < 90 || d.x > 270 ? null : "rotate(180)"; })
      .text(function (d) { return d.data.name; });

  </script>
</body>

</html>

Example code explanation

1. Prepare the data to draw

Prepare the data to draw. For the basic usage of cluster, see How to use cluster. For details about the data structure, see Hierarchy data structure and usage.

2. Convert the data to draw

root = d3.hierarchy(data);
var cluster = d3.cluster().size([360, ry - 80])
cluster(root);

Convert the prepared data into the data structure used for drawing. Two conversions are performed: prepared data -> hierarchy data -> data for the drawing type, which is cluster here. In this example, the x coordinate is used as the rotation angle and the y coordinate is used as the radius to calculate positions in a polar coordinate system.

3. Place SVG elements

g = d3.select("svg").append("g").attr("transform", "translate(" + rx + "," + ry + ")");

First, add a "g" group element to the SVG area and set its coordinates to the center. Nodes and links are configured inside this group element.

  var link = g.selectAll(".link")
    .data(root.links())
    .enter()
    .append("path")
    .attr("class", "link")
    .attr("fill", "none")
    .attr("stroke", "#555")
    .attr("stroke-width", "1.5px")
    .attr("opacity", "0.6")
    .attr("d", d3.linkRadial()
      .angle(function(d) { return (d.x + 90) / 180 * Math.PI; })
      .radius(function(d) { return d.y; }));

Configure links.

root.links()

This function extracts links from the hierarchy root as an array. The array has a form such as [{"source": nodedata, "target": nodedata}, ...].

d3.linkRadial()
  .angle(function(d) { return (d.x + 90) / 180 * Math.PI; })
  .radius(function(d) { return d.y; })

This function returns the value for the "d" attribute of an SVG path connecting nodes placed in polar coordinates. Note that the angle is specified in radians.

  var node = g.selectAll(".node")
    .data(root.descendants())
    .enter()
    .append("g")
    .attr("transform", function(d) { return "rotate(" + (d.x) + ")translate(" + d.y + ")"; })

  node.append("circle")
    .attr("r", 8)
    .attr("stroke", "steelblue")
    .attr("stroke-width", "1.5px")
    .attr("fill", "white");

  node.append("text")
    .attr("dy", 3)
    .attr("dx", function(d) { return d.x < 90 || d.x > 270 ? 8 : -8; })
    .style("text-anchor", function(d) { return d.x < 90 || d.x > 270 ? "start" : "end"; })
    .attr("font-size", "200%")
    .attr("transform", function(d) { return d.x < 90 || d.x > 270 ? null : "rotate(180)"; })
    .text(function(d) { return d.data.name; });

Configure nodes. First add a "g" element, then add "circle" and "text" inside it.

root.descendants()

This function retrieves hierarchy nodes as an array. Also note that the rotation angle is set with the transform attribute, but it is shifted by 90 degrees from the link setting. The "text" angle is adjusted depending on the node position so that labels remain readable.

Summary

When the number of nodes increases, you can create an impactful visualization like this example. It is more compact than a horizontally long tournament bracket and also has strong visual design appeal.