chart = {
const width = 640;
const step = 14;
const marginTop = 20;
const marginRight = 20;
const marginBottom = 20;
const marginLeft = 130;
const height = (nodes.length - 1) * step + marginTop + marginBottom;
const y = d3.scalePoint(orders.get("by name"), [marginTop, height - marginBottom]);
const color = d3.scaleOrdinal()
.domain(nodes.map(d => d.group).sort(d3.ascending))
.range(d3.schemeCategory10)
.unknown("#aaa");
const groups = new Map(nodes.map(d => [d.id, d.group]));
function samegroup({ source, target }) {
return groups.get(source) === groups.get(target) ? groups.get(source) : null;
}
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");
const Y = new Map(nodes.map(({id}) => [id, y(id)]));
function arc(d) {
const y1 = Y.get(d.source);
const y2 = Y.get(d.target);
const r = Math.abs(y2 - y1) / 2;
return `M${marginLeft},${y1}A${r},${r} 0,0,${y1 < y2 ? 1 : 0} ${marginLeft},${y2}`;
}
const path = svg.insert("g", "*")
.attr("fill", "none")
.attr("stroke-opacity", 0.6)
.attr("stroke-width", 1.5)
.selectAll("path")
.data(links)
.join("path")
.attr("stroke", d => color(samegroup(d)))
.attr("d", arc);
const label = svg.append("g")
.attr("font-family", "sans-serif")
.attr("font-size", 10)
.attr("text-anchor", "end")
.selectAll("g")
.data(nodes)
.join("g")
.attr("transform", d => `translate(${marginLeft},${Y.get(d.id)})`)
.call(g => g.append("text")
.attr("x", -6)
.attr("dy", "0.35em")
.attr("fill", d => d3.lab(color(d.group)).darker(2))
.text(d => d.id))
.call(g => g.append("circle")
.attr("r", 3)
.attr("fill", d => color(d.group)));
label.append("rect")
.attr("fill", "none")
.attr("width", marginLeft + 40)
.attr("height", step)
.attr("x", -marginLeft)
.attr("y", -step / 2)
.attr("fill", "none")
.attr("pointer-events", "all")
.on("pointerenter", (event, d) => {
svg.classed("hover", true);
label.classed("primary", n => n === d);
label.classed("secondary", n => links.some(({source, target}) => (
n.id === source && d.id == target || n.id === target && d.id === source
)));
path.classed("primary", l => l.source === d.id || l.target === d.id).filter(".primary").raise();
})
.on("pointerout", () => {
svg.classed("hover", false);
label.classed("primary", false);
label.classed("secondary", false);
path.classed("primary", false).order();
});
svg.append("style").text(`
.hover text { fill: #aaa; }
.hover g.primary text { font-weight: bold; fill: #333; }
.hover g.secondary text { fill: #333; }
.hover path { stroke: #ccc; }
.hover path.primary { stroke: #333; }
`);
function update(order) {
y.domain(order);
label
.sort((a, b) => d3.ascending(Y.get(a.id), Y.get(b.id)))
.transition()
.duration(750)
.delay((d, i) => i * 20)
.attrTween("transform", d => {
const i = d3.interpolateNumber(Y.get(d.id), y(d.id));
return t => {
const y = i(t);
Y.set(d.id, y);
return `translate(${marginLeft},${y})`;
}
});
path.transition()
.duration(750 + nodes.length * 20)
.attrTween("d", d => () => arc(d));
}
return Object.assign(svg.node(), {update});
}