Public
Edited
Sep 15, 2023
Fork of Arc diagram
Insert cell
Insert cell
Insert cell
chart = {
// Specify the chart’s dimensions.
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]);

// A color scale for the nodes and links.
const color = d3.scaleOrdinal()
.domain(nodes.map(d => d.group).sort(d3.ascending))
.range(d3.schemeCategory10)
.unknown("#aaa");

// A function of a link, that checks that source and target have the same group and returns
// the group; otherwise null. Used to color the links.
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;
}

// Create the SVG container.
const svg = d3.create("svg")
.attr("width", width)
.attr("height", height)
.attr("viewBox", [0, 0, width, height])
.attr("style", "max-width: 100%; height: auto;");

// The current position, indexed by id. Will be interpolated.
const Y = new Map(nodes.map(({id}) => [id, y(id)]));
// Add an arc for each link.
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);

// Add a text label and a dot for each node.
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)));

// Add invisible rects that update the class of the elements on mouseover.
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();
});

// Add styles for the hover interaction.
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; }
`);

// A function that updates the positions of the labels and recomputes the arcs
// when passed a new order.
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) // Make the movement start from the top.
.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) // Cover the maximum delay of the label transition.
.attrTween("d", d => () => arc(d));
}

return Object.assign(svg.node(), {update});
}
Insert cell
Insert cell
miserables = FileAttachment("miserables.json").json();

Insert cell
nodes = [
{ id: "online communication", group: 1 },
{ id: "social media", group: 1 },
{ id: "computer-mediated cooperative work", group: 1 },
{ id: "online communities", group: 1 },
{ id: "race/ethnicity", group: 2 },
{ id: "health disparities", group: 2 },
{ id: "social determinants of health", group: 2 },
{ id: "network science", group: 3 },
{ id: "computational social science", group: 3 },
{ id: "social networks", group: 3 },
{ id: "social identities", group: 4 },
{ id: "nationality", group: 4 },
{ id: "political ideologies", group: 4 },
];

Insert cell
Insert cell
// nodes = miserables.nodes
Insert cell
// links = miserables.links
Insert cell
orders = {
const degree = d3.rollup(
links.flatMap(({ source, target, value }) => [
{ node: source, value },
{ node: target, value }
]),
(v) => d3.sum(v, ({ value }) => value),
({ node }) => node
);
return new Map([
["by name", d3.sort(nodes.map((d) => d.id))],
["by group", d3.sort(nodes, ({group}) => group, ({id}) => id).map(({id}) => id)],
// ["input", nodes.map(({id}) => id)],
["by degree", d3.sort(nodes, ({id}) => degree.get(id), ({id}) => id).map(({id}) => id).reverse()]
]);
}
Insert cell
// links.flatMap(({ source, target, value }) => [
// { node: source, value },
// { node: target, value }
// ]).map(
// )
// d3.sum(v, ({ value }) => value),
// ({ node }) => node
// )
Insert cell

One platform to build and deploy the best data apps

Experiment and prototype by building visualizations in live JavaScript notebooks. Collaborate with your team and decide which concepts to build out.
Use Observable Framework to build data apps locally. Use data loaders to build in any language or library, including Python, SQL, and R.
Seamlessly deploy to Observable. Test before you ship, use automatic deploy-on-commit, and ensure your projects are always up-to-date.
Learn more