chart = {
const width = 900;
const height = 900;
const innerRadius = Math.min(width, height) * 0.35;
const outerRadius = innerRadius + 20;
const color = d3.scaleOrdinal()
.domain([...sectors, ...metrics])
.range(d3.schemePaired.concat(d3.schemeSet2, d3.schemeSet3));
const chord = d3.chord()
.padAngle(0.08)
.sortSubgroups(d3.descending)
.sortChords(d3.descending);
const chords = chord(matrix);
const svg = d3.create("svg")
.attr("viewBox", [-width / 2, -height / 2, width, height])
.attr("width", width)
.attr("height", height)
.attr("style", "max-width: 100%; height: auto;");
svg.append("circle")
.attr("r", outerRadius + 100)
.attr("fill", "#fff");
const group = svg.append("g")
.selectAll("g")
.data(chords.groups)
.join("g");
group.append("path")
.attr("fill", d => color(d.index < sectors.length ?
sectors[d.index] :
metrics[d.index - sectors.length]))
.attr("d", d3.arc().innerRadius(innerRadius).outerRadius(outerRadius))
.attr("stroke", "#fff")
.attr("stroke-width", 2);
group.append("text")
.each(d => {
d.angle = (d.startAngle + d.endAngle) / 2;
d.name = d.index < sectors.length ?
sectors[d.index].split(" - ")[0] :
metrics[d.index - sectors.length];
})
.attr("dy", "0.35em")
.attr("transform", d => `
rotate(${(d.angle * 180 / Math.PI - 90)})
translate(${outerRadius + 20})
${d.angle > Math.PI ? "rotate(180)" : ""}
`)
.attr("text-anchor", d => d.angle > Math.PI ? "end" : null)
.text(d => d.name)
.attr("font-size", "14px")
.attr("font-weight", "bold")
.attr("font-family", "Arial, sans-serif")
.attr("fill", "#000");
const ribbons = svg.append("g")
.attr("fill-opacity", 0.6)
.selectAll("path")
.data(chords)
.join("path")
.attr("class", "chord")
.attr("d", d3.ribbon().radius(innerRadius))
.attr("fill", d => color(d.source.index < sectors.length ?
sectors[d.source.index] :
metrics[d.source.index - sectors.length]))
.attr("stroke", "#fff")
.attr("stroke-width", 0.5);
ribbons.append("title")
.text(d => {
const sourceName = d.source.index < sectors.length ?
sectors[d.source.index] :
metrics[d.source.index - sectors.length];
const targetName = d.target.index < sectors.length ?
sectors[d.target.index] :
metrics[d.target.index - sectors.length];
return `${sourceName} → ${targetName}: ${d.source.value.toFixed(2)}`;
});
group.on("mouseover", function(event, d) {
svg.selectAll(".chord")
.attr("opacity", 0.1);
svg.selectAll(".chord")
.filter(c => c.source.index === d.index || c.target.index === d.index)
.attr("opacity", 0.9)
.attr("stroke-width", 1.5);
d3.select(this).select("path")
.attr("stroke-width", 3)
.attr("stroke", "#000");
d3.select(this).select("text")
.attr("font-size", "16px")
.attr("font-weight", "bold")
.attr("fill", "#000");
}).on("mouseout", function() {
svg.selectAll(".chord")
.attr("opacity", 0.6)
.attr("stroke-width", 0.5);
group.selectAll("path")
.attr("stroke-width", 2)
.attr("stroke", "#fff");
group.selectAll("text")
.attr("font-size", "14px")
.attr("font-weight", "bold")
.attr("fill", "#000");
});
return svg.node();
}