chart = {
const height = p.width;
const cx = p.width * 0.5;
const cy = height * 0.5;
const radius = Math.min(p.width, height) / 2 + p.radiusSpace;
const myColor = p.blackWhite
? (d) => (q.darkMode ? "white" : "#000")
: colorFunc(p.colorNumber, p.colorRotation, p.colorSeed, p.darkenFactor);
const offset = 0;
const tree = d3.cluster().size([2 * Math.PI, radius]);
const catRadius = p.addCat ? (p.addTeam ? 2 : 1) * 23 : p.addTeam ? 23 : 0;
const offAngle = 3.5;
if (p.leavesGrouping)
tree.separation((a, b) => (a.data.data.team == b.data.data.team ? 1 : 2));
const root = tree(d3.hierarchy(data));
root.x = Math.PI / 2 + (p.angleRoot * Math.PI) / 180;
root.y = -30;
root.each((d) => {
if (d.depth == 1) d.y += p.radiusDepth1;
});
const svgOrigin = d3
.create("svg")
.attr("width", p.width)
.attr("height", height)
.attr("viewBox", [
-cx / p.zoom,
-cy / p.zoom,
p.width / p.zoom,
height / p.zoom
])
.attr("style", "width: 100%; height: auto; font: 10px sans-serif;")
.attr("cursor", "grab");
const svg = svgOrigin.append("g");
svg
.append("rect")
.attr("x", -p.width / 2 / p.zoom)
.attr("y", -height / 2 / p.zoom)
.attr("width", p.width / p.zoom)
.attr("height", height / p.zoom)
.attr("fill", q.darkMode ? "black" : "white");
// append arc category
let leaves = root.leaves();
let currentCategory = leaves[0].data.data.category;
let currentColor = myColor(
leaves[0].ancestors().find((ancestor) => ancestor.depth == 1)
);
let currentFirstX = leaves[0].x;
let currentLastX = leaves[0].x;
let catArray = [];
for (let i = 1; i < leaves.length; i++) {
if (leaves[i].data.data.category !== currentCategory) {
catArray.push({
color: currentColor,
name: currentCategory,
start: currentFirstX,
end: currentLastX
});
currentColor = myColor(
leaves[i].ancestors().find((ancestor) => ancestor.depth == 1)
);
currentCategory = leaves[i].data.data.category;
currentFirstX = leaves[i].x;
}
currentLastX = leaves[i].x;
}
catArray.push({
color: currentColor,
name: currentCategory,
start: currentFirstX,
end: currentLastX
});
const catData = catArray.map((d) => {
return {
color: d.color,
text: d.name,
startAngle: (d.start / Math.PI) * 180,
endAngle: (d.end / Math.PI) * 180
};
});
if (p.addCat)
drawRing(svg, {
fontSize: 10,
r: radius + 8,
r: radius + 8 + (p.addTeam ? catRadius / 2 : 0),
innerR: catRadius / (p.addTeam ? 2 : 1) - 7,
opacity: 0,
padAngle: 0,
endsPadAngle: -offAngle,
opaque: false,
//radial: true,
cornerRadius: 5,
data: catData
});
// append arc team
let currentTeam = leaves[0].data.data.team;
let tcurrentColor = myColor(
leaves[0].ancestors().find((ancestor) => ancestor.depth == 1)
);
let tcurrentFirstX = leaves[0].x;
let tcurrentLastX = leaves[0].x;
let teamArray = [];
for (let i = 1; i < leaves.length; i++) {
if (leaves[i].data.data.team !== currentTeam) {
teamArray.push({
color: tcurrentColor,
name: currentTeam,
start: tcurrentFirstX,
end: tcurrentLastX
});
tcurrentColor = myColor(
leaves[i].ancestors().find((ancestor) => ancestor.depth == 1)
);
currentTeam = leaves[i].data.data.team;
tcurrentFirstX = leaves[i].x;
}
tcurrentLastX = leaves[i].x;
}
teamArray.push({
color: tcurrentColor,
name: currentTeam,
start: tcurrentFirstX,
end: tcurrentLastX
});
const teamData = teamArray.map((d) => {
return {
color: d.color,
text: d.name,
startAngle: (d.start / Math.PI) * 180,
endAngle: (d.end / Math.PI) * 180
};
});
if (p.addTeam)
drawRing(svg, {
fontSize: 10,
r: radius + 8 + catRadius / 2,
r: radius + 8,
innerR: catRadius / (p.addCat ? 2 : 1) - 7,
opacity: 0,
padAngle: 0,
endsPadAngle: -offAngle,
opaque: false,
//radial: true,
cornerRadius: 5,
data: teamData
});
// 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
? p.blackWhite
? lighten(col, 1)
: col
: q.darkMode
? darken(col, 0.8)
: lighten(col, p.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", (d) =>
(d.height == 0 && p.addTeam) || p.addCat
? q.emailPhone
? "-1em"
: "-0.2em"
: "0.31em"
)
.attr("x", (d) =>
d.x < Math.PI + offset === !d.children
? 6 + (d.height == 0 ? catRadius : 0)
: -6 - (d.height == 0 ? catRadius : 0)
)
.attr("text-anchor", (d) =>
d.x < Math.PI + offset === !d.children ? "start" : "end"
)
.attr("paint-order", "stroke")
.attr("stroke", q.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 + (d.height == 0 ? catRadius : 0)
: -6 - (d.height == 0 ? catRadius : 0)
)
.text((d) => d.data.data.name);
if (q.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 + (d.height == 0 ? catRadius : 0)
: -6 - (d.height == 0 ? catRadius : 0)
)
.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 + (d.height == 0 ? catRadius : 0)
: -6 - (d.height == 0 ? catRadius : 0)
)
.text((d) => d.data.data.phone);
if (p.titleOnTop) drawRing(svg, title);
// --- old zoom function depending of p.scollZoom (tricking the scaleExtent) toggle --- //
// svgOrigin.call(
// d3
// .zoom()
// .scaleExtent([p.scrollZoom ? 0.1 : 1, p.scrollZoom ? 8 : 1])
// .on("zoom", zoomed)
// );
function zoomed({ transform }) {
svg.attr("transform", transform);
}
// --- alternative zoom from https://talk.observablehq.com/t/update-value-of-another-input-when-an-input-is-triggered-in-a-multi-input-single-object/8532/3 independant of toogle and allow zoom only on fullscreen --- //
const zoom = d3.zoom().scaleExtent([0.5, 8]).on("zoom", zoomed);
const toggleZoom = (enabled) => {
if (enabled) {
// Attach zoom handlers
svgOrigin.call(zoom);
} else {
svgOrigin.call(zoom.transform, d3.zoomIdentity);
// Remove all *.zoom event handlers
svgOrigin.on(".zoom", null);
}
};
// Initialize
toggleZoom();
document.addEventListener("fullscreenchange", (e) => {
// Enable zooming if svg.node() is the fullscreen element
toggleZoom(e.target === document.fullscreenElement);
});
return svgOrigin.node();
}