A Chord Diagram visualization of global migration data from 2015. Data is from the United Nations International Migrant Stock Database. Inspired by:
forked from curran's block: Global Migration in 2015
forked from jrzief's block: Global Migration in 2015 - new data
xxxxxxxxxx
<html>
<head>
<meta charset="utf-8">
<script src="//d3js.org/d3.v4.min.js"></script>
<link rel="stylesheet" href="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha/css/bootstrap.css">
<script src="//cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0/jquery.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/tether/1.3.2/js/tether.min.js"></script>
<script src="//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha/js/bootstrap.min.js"></script>
</head>
<body>
<script>
// Configuration parameters.
var width = 960,
height = 960,
outerPadding = 150,
labelPadding = 20,
chordPadding = 0.03,
arcThickness = 30,
opacity = 0.5,
fadedOpacity = 0.01,
transitionDuration = 300,
outerRadius = width / 2 - outerPadding,
innerRadius = outerRadius - arcThickness,
valueFormat = d3.format(",");
var formatValue = d3.formatPrefix(",.0", 1e3);
// DOM Elements.
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
g = svg.append("g")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")"),
ribbonsG = g.append("g"),
groupsG = g.append("g");
// D3 layouts, shapes and scales.
var ribbon = d3.ribbon()
.radius(innerRadius),
chord = d3.chord()
.padAngle(chordPadding)
.sortSubgroups(d3.descending),
arc = d3.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius),
color = d3.scaleOrdinal()
.range(d3.schemeCategory20);
var popoverOptions = {
html : true,
template: '<div class="popover" role="tooltip"><div class="popover-arrow"></div><div class="popover-content"></div></div>'
};
// Renders the given data as a chord diagram.
function render(data){
var matrix = generateMatrix(data),
chords = chord(matrix);
color.domain(matrix.map(function (d, i){
return i;
}));
// Render the ribbons.
ribbonsG.selectAll("path")
.data(chords)
.enter().append("path")
.attr("class", "ribbon")
.attr("d", ribbon)
.style("fill", function(d) {
return color(d.source.index);
})
.style("stroke", function(d) {
return d3.rgb(color(d.source.index)).darker();
})
.style("opacity", opacity)
.on("mouseenter", function(d){
var src = matrix.names[d.source.index];
var dest = matrix.names[d.target.index];
popoverOptions.content = [
"<strong>" + src +" to " + dest +"</strong>",
valueFormat(d.target.value),
"<br><strong>" + dest +" to " + src +"</strong>",
valueFormat(d.source.value)
].join("<br>");
$(this).popover(popoverOptions);
$(this).popover("show");
})
.on("mouseleave", function (d){
$(this).popover("hide");
})
// Scaffold the chord groups.
var groups = groupsG
.selectAll("g")
.data(chords.groups)
.enter().append("g");
// Render the chord group arcs.
groups
.append("path")
.attr("class", "arc")
.attr("d", arc)
.style("fill", function(group) {
return color(group.index);
})
.style("stroke", function(group) {
return d3.rgb(color(group.index)).darker();
})
.style("opacity", opacity)
.call(groupHover);
// Render tics for group arcs
var groupTick = groups.selectAll(".group-tick")
.data(function(d) { return groupTicks(d, 1e3); })
.enter().append("g")
.attr("class", "group-tick")
.attr("transform", function(d) { return "rotate(" + (d.angle * 180 / Math.PI - 90)
+ ") translate(" + outerRadius + ",0)"; });
groupTick.append("line")
.attr("x2", 6);
groupTick
.filter(function(d) { return d.value % 5e3 === 0; })
.append("text")
.attr("x", 6)
.attr("dy", ".25em")
.attr("transform", function(d) { return d.angle > Math.PI ? "rotate(180) translate(-16)" : null; })
.style("text-anchor", function(d) { return d.angle > Math.PI ? "end" : null; })
.style("font-size", "8pt")
.text(function(d) { return formatValue(d.value); });
// Render the chord group labels.
var angle = d3.local(),
flip = d3.local();
groups
.append("text")
.each(function(d) {
angle.set(this, (d.startAngle + d.endAngle) / 2)
flip.set(this, angle.get(this) > Math.PI);
})
.attr("transform", function(d) {
return [
"rotate(" + (angle.get(this) / Math.PI * 180 - 90) + ")",
"translate(" + (outerRadius + labelPadding) + ")",
flip.get(this) ? "rotate(180)" : ""
].join("");
})
.attr("text-anchor", function(d) {
return flip.get(this) ? "end" : "start";
})
.text(function(d) {
return matrix.names[d.index];
})
.attr("alignment-baseline", "central")
.style("font-family", '"Helvetica Neue", Helvetica')
.style("font-size", "12pt")
.style("cursor", "default")
.call(groupHover);
}
// Sets up hover interaction to highlight a chord group.
// Used for both the arcs and the text labels.
function groupHover(selection){
selection
.on("mouseover", function (group){
g.selectAll(".ribbon")
.filter(function(ribbon) {
return (
(ribbon.source.index !== group.index) &&
(ribbon.target.index !== group.index)
);
})
.transition().duration(transitionDuration)
.style("opacity", fadedOpacity);
})
.on("mouseout", function (){
g.selectAll(".ribbon")
.transition().duration(transitionDuration)
.style("opacity", opacity);
});
}
// Returns an array of tick angles and values for a given group and step.
function groupTicks(d, step) {
var k = (d.endAngle - d.startAngle) / d.value;
return d3.range(0, d.value, step).map(function(value) {
return {value: value, angle: value * k + d.startAngle};
});
}
// Generates a matrix (2D array) from the given data, which is expected to
// have fields {origin, destination, count}. The matrix data structure is required
// for use with the D3 Chord layout.
function generateMatrix(data){
var nameToIndex = {},
names = [],
matrix = [],
n = 0, i, j;
function recordName(name){
if( !(name in nameToIndex) ){
nameToIndex[name] = n++;
names.push(name);
}
}
data.forEach(function (d){
recordName(d.origin);
recordName(d.destination);
});
for(i = 0; i < n; i++){
matrix.push([]);
for(j = 0; j < n; j++){
matrix[i].push(0);
}
}
data.forEach(function (d){
i = nameToIndex[d.origin];
j = nameToIndex[d.destination];
matrix[j][i] = d.count;
});
matrix.names = names;
return matrix;
}
d3.csv("commuterOut3.csv", type, function (dataForCountries){
var filteredData = dataForCountries.filter(function (d){
return d.count > 100;
});
// Reduce clutter by filtering out links with relatively low counts.
render(filteredData);
// });
});
// Parses a single row of the input table.
function type(d){
d.count = +d.count;
return d;
}
</script>
</body>
</html>
https://d3js.org/d3.v4.min.js
https://cdnjs.cloudflare.com/ajax/libs/jquery/3.0.0/jquery.min.js
https://cdnjs.cloudflare.com/ajax/libs/tether/1.3.2/js/tether.min.js
https://cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/4.0.0-alpha/js/bootstrap.min.js