Public
Edited
Mar 22
Paused
1 fork
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
chart = {
// Specify the chart’s dimensions.
// const width = 1100;
const height = p.width;
const cx = p.width * 0.5; // adjust as needed to fit
const cy = height * 0.5; // adjust as needed to fit
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; // for reversing the node at the bottom
// Create a radial cluster layout. The layout’s first dimension (x)
// is the angle, while the second (y) is the radius.
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;
});
// Creates the SVG container.

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();
}
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell
Insert cell

Purpose-built for displays of data

Observable is your go-to platform for exploring data and creating expressive data visualizations. Use reactive JavaScript notebooks for prototyping and a collaborative canvas for visual data exploration and dashboard creation.
Learn more