Public
Edited
Mar 15, 2023
1 star
Insert cell
Insert cell
migration.csv
Type Table, then Shift-Enter. Ctrl-space for more options.

Insert cell
data = FileAttachment("migration.csv").csv({typed: true})
// load the migration data on Observable
Insert cell
names = Array.from(new Set(data.flatMap(d => [d.source, d.target])))
// create a region name list
Insert cell
matrix = {
const index = new Map(names.map((name, i) => [name, i]));
const matrix = Array.from(index, () => new Array(names.length).fill(0));
for (const {source, target, value} of data) matrix[index.get(source)][index.get(target)] += value;
return matrix;
}
// define a matrix to represent the migration flow data in a matrix format
Insert cell
width = 700
Insert cell
height = width
Insert cell
innerRadius = Math.min(width, height) * 0.4 - 20
Insert cell
outerRadius = innerRadius + 20
Insert cell
chord = d3.chordDirected()
.padAngle(10 / innerRadius)
.sortSubgroups(d3.descending)
.sortChords(d3.descending)
// initialize a new chord generator
Insert cell
arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius+15)
// set the configuration of width of innerRadius and outerRadius
Insert cell
ribbon = d3.ribbonArrow()
.radius(innerRadius - 5)
.padAngle(1 / innerRadius)
// initialize a new ribbon generator
Insert cell
unicefColors = [
"#961a49",
"#00aeef",
"#80bd41",
"#00833d",
"#ffc20e",
"#777779",
"#374ea2"
];
// create a UNICEF color array
Insert cell
color = d3.scaleOrdinal()
.domain(names)
.range(Object.values(unicefColors));
Insert cell
d3 = require('d3')
Insert cell
formatValue = x => `${x.toFixed(0)} MM`
Insert cell
viewof groups = html`<input type="number" value="6" min="3">`
Insert cell
colors = {
return Array(groups).fill().map((_, i) => d3.hsl(300 / groups * i, .8, .5));
}
Insert cell
function ticks({startAngle, endAngle, value}) {
const k = (endAngle - startAngle) / value;
return d3.range(0, value, tickStep).map(value => {
return {value, angle: value * k + startAngle};
});
}
Insert cell
tickStep = d3.tickStep(0, d3.sum(data.flat()), 100)
Insert cell
chart = {
const svg = d3.create("svg")
.attr("viewBox", [-width / 2, -height / 2, width-50, height-50]);

const chords = chord(matrix);

const textId = DOM.uid("text");
const group = svg.append("g")
.attr("font-size", 10)
.attr("font-family", "sans-serif")
.selectAll("g")
.data(chords.groups)
.join("g");
svg.append("path")
.attr("id", textId.id)
.attr("fill", "none")
.attr("d", d3.arc()({outerRadius, startAngle: 0, endAngle: 2 * Math.PI}));

svg.append("g")
.attr("fill-opacity", 0.4)
.selectAll("g")
.data(chords)
.join("path")
.attr("class", "ribbon") // add a class to the path element
.attr("d", ribbon)
.attr("fill", d => color(names[d.source.index]))
.style("mix-blend-mode", "multiply")
.on("mouseover", function(d) { // add a mouseover event listener to the path element
d3.select(this)
.attr("stroke-width", 2)
.attr("stroke", "#000");
})
.on("mouseout", function(d) { // add a mouseout event listener to the path element
d3.select(this)
.attr("stroke-width", null)
.attr("stroke", null);
})
.append("title")
.text(d => `${formatValue(d.source.value)} migrants move from ${names[d.source.index]} to ${names[d.target.index]} `)
.style("font-size", "16px !important");

svg.append("g")
.attr("font-family", "roboto")
.attr("font-size", 10)
.selectAll("g")
.data(chords.groups)
.join("g")
.call(g => g.append("path")
.attr("d", arc)
.attr("fill", d => color(names[d.index]))
.attr("stroke", "#fff"))
.call(g => g.append("text")
.attr("dy", 0)
.attr("rotate", d => d.index === 1 ? 180 : null) // rotate Europe by 180 degrees
.attr("text-anchor", "middle") // anchor the text in the center
.append("textPath")
.attr("xlink:href", textId.href)
.attr("startOffset", d => (d.startAngle + d.endAngle) / 2 * outerRadius )

.selectAll("tspan")
.data(d => {
const lines = d.index === 2 ? ["Latin America", "the Caribbean"] : [names[d.index]];
return lines.map(line => ({ line, x: -0.4, dy: "0.8em" }));
})
.join("tspan")
.attr("x", 0)
.attr("dy", d => d.dy)
.text(d => {
if (d.line === "Europe") {
return "eporuE";
}
return d.line;
})
.style("fill", "white")) // set text color to white

.call(g => g.append("title")
.text(d => `${names[d.index]}
owes ${formatValue(d3.sum(matrix[d.index]))}
is owed ${formatValue(d3.sum(matrix, row => row[d.index]))}`));

const tickValues = d3.range(0, d3.max(chords.groups, d => d.value) * 1.1, d3.max(chords.groups, d => d.value) / 50);
const arcInnerRadius = d3.scaleLinear()
.range([innerRadius, outerRadius])
.domain([0, d3.max(chords.groups, d => d.value) * 1.2]);

const arcOuterRadius = d3.scaleLinear()
.range([outerRadius, innerRadius])
.domain([0, d3.max(chords.groups, d => d.value) * 1.2]);

svg.append("g")
.attr("font-size", 5)
.attr("text-anchor", "middle")
.selectAll("g")
.data(chords.groups)
.join("g")
.attr("transform", d => `rotate(${(d.startAngle + d.endAngle) / 2 * 180 / Math.PI - 90}) translate(${outerRadius + 10},0)`)
.call(g => g.selectAll("line")
.data(tickValues)
.join("line")
.attr("stroke", "gray")
.attr("stroke-width", 0.5)
.attr("x1", d => arcInnerRadius(d))
.attr("x2", d => arcOuterRadius(d))
.attr("stroke-opacity", d => d === 0 || d === 1 ? 0 : 1))
.call(g => g.append("text")
.attr("transform", d => (d.startAngle + d.endAngle) / 2 > Math.PI ? "rotate(180) translate(-16)" : null)
.attr("alignment-baseline", "middle")
.attr("font-weight", "bold")
.text(d => formatValue(d.value)));


return svg.node();

}
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