How to Place D3.js cluster Nodes on a Circle
For the hierarchy data structure and data preparation, see Hierarchy data structure and usage.
Example program
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.