chart = {
const height = width;
const cx = width * 0.5;
const cy = height * 0.5;
const radius = Math.min(width, height) / 2 + radiusSapce;
const myColor = blackWhite
? (d) => (darkMode ? "white" : "#000")
: colorFunc(colorNumber, colorRotation, colorSeed, darkenFactor);
const offset = 0.1;
const tree = d3.cluster().size([2 * Math.PI, radius]);
if (leavesGrouping)
tree.separation((a, b) => (a.parent == b.parent ? 1 : 2) / a.depth);
const root = tree(d3.hierarchy(data));
root.x = Math.PI / 2;
root.y = -30;
// Creates the SVG container.
const svgOrigin = d3
.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [-cx / zoom, -cy / zoom, width / zoom, height / zoom])
.attr("style", "width: 100%; height: auto; font: 10px sans-serif;")
.attr("cursor", "grab");
const svg = svgOrigin.append("g");
if (darkMode)
svg
.append("rect")
.attr("x", -width / 2 / zoom)
.attr("y", -height / 2 / zoom)
.attr("width", width / zoom)
.attr("height", height / zoom)
.attr("fill", "black");
// Append links.
svg
.append("g")
.attr("fill", "none")
.attr("stroke", "#555")
.attr("stroke-opacity", 0.4)
.attr("stroke-width", 1.5)
.selectAll()
.data(root.links())
.join("path")
.attr(
"d",
d3
.linkRadial()
.angle((d) => d.x)
.radius((d) => d.y)
)
.attr("stroke", (d) =>
myColor(d.target.ancestors().find((ancestor) => ancestor.depth == 1))
);
// Append nodes.
svg
.append("g")
.selectAll()
.data(root.descendants())
.join("circle")
.attr(
"transform",
(d) => `rotate(${(d.x * 180) / Math.PI - 90}) translate(${d.y},0)`
)
.attr("fill", (d) => (d.children ? "#555" : "#999"))
.attr("r", 2.5)
.attr("fill", (d) => {
const col = myColor(
d.ancestors().find((ancestor) => ancestor.depth == 1)
);
return d.children
? blackWhite
? lighten(col, 1)
: col
: darkMode
? darken(col, 0.8)
: lighten(col, blackWhite ? 2 : 0.8);
});
// multiline https://observablehq.com/@saneef/svg-multiline-text-line-height
// Append labels.
const label = svg
.append("g")
.attr("stroke-linejoin", "round")
.attr("stroke-width", 3)
.selectAll()
.data(root.descendants())
.join("text")
.attr(
"transform",
(d) =>
`rotate(${(d.x * 180) / Math.PI - 90}) translate(${d.y},0) rotate(${
d.x >= Math.PI + offset ? 180 : 0
})`
)
.attr("dy", "0.31em")
.attr("x", (d) => (d.x < Math.PI + offset === !d.children ? 6 : -6))
.attr("text-anchor", (d) =>
d.x < Math.PI + offset === !d.children ? "start" : "end"
)
.attr("paint-order", "stroke")
.attr("stroke", darkMode ? "black" : "white")
.attr("fill", "currentColor")
.attr("fill", (d) =>
myColor(d.ancestors().find((ancestor) => ancestor.depth == 1))
)
.attr("font-size", (d) => (d.depth == 0 ? 12 : d.depth == 1 ? 10 : 10))
.text((d) => d.data.id)
.append("tspan")
.attr("dy", "1em")
.attr("font-size", (d) => (d.depth == 0 ? 9 : d.depth == 1 ? 8 : 8))
.attr("x", (d) => (d.x < Math.PI + offset === !d.children ? 6 : -6))
.text((d) => d.data.data.name);
if (emailPhone)
label
.append("tspan")
.attr("dy", "1em")
.attr("font-size", (d) => (d.depth == 0 ? 8 : d.depth == 1 ? 8 : 8))
.attr("x", (d) => (d.x < Math.PI + offset === !d.children ? 6 : -6))
.text((d) => d.data.data.email)
.append("tspan")
.attr("dy", "1.2em")
.attr("font-size", (d) => (d.depth == 0 ? 8 : d.depth == 1 ? 8 : 8))
.attr("x", (d) => (d.x < Math.PI + offset === !d.children ? 6 : -6))
.text((d) => d.data.data.phone);
if (titleOnTop) drawRing(svg, title);
// zoom
svgOrigin.call(
d3
.zoom()
.scaleExtent([scrollZoom ? 0.1 : 1, scrollZoom ? 8 : 1])
.on("zoom", zoomed)
);
function zoomed({ transform }) {
svg.attr("transform", transform);
}
return svgOrigin.node();
}