Species that City Nature Challenge 2017 participating cities had in common. Each bar on the rim represents a participating city, and each chord connecting them represents the number of species they had in common. This does not include higher-level taxa like families and genera
Colophon: this is a very crude adaption of Mike Bostock's European Debt adaptation with some stuff cribbed from this useful blog post. For my own reference, I exported pairwise data from the iNat db like this (probably a better way, but this worked, d3.chord is apparently smart enough to handle rows like A-B and B-A):
CREATE TABLE cnc_project_species AS SELECT DISTINCT
po.project_id AS project_id,
ta.taxon_id
FROM
observations o
JOIN taxon_ancestors ta ON ta.taxon_id = o.taxon_id
JOIN taxa t ON t.id = ta.ancestor_taxon_id
LEFT OUTER JOIN project_observations po ON po.observation_id = o.id
WHERE
po.project_id IN (10931, 11013, 11053, 11126, 10768, 10769, 10752, 10764, 11047, 11110, 10788, 10695, 10945, 10917, 10763, 11042)
AND t.rank = 'species';
SELECT
replace(p1.title, 'City Nature Challenge 2017: ', '') AS city1,
replace(p2.title, 'City Nature Challenge 2017: ', '') AS city2,
num_species
FROM
(
SELECT
c1.project_id AS project_id_1,
c2.project_id AS project_id_2,
count(*) AS num_species
FROM
cnc_project_species c1 JOIN cnc_project_species c2 ON c1.taxon_id = c2.taxon_id
WHERE
c1.project_id != c2.project_id
GROUP BY c1.project_id, c2.project_id
ORDER BY c1.project_id, c2.project_id
) pairwise_counts
JOIN projects p1 ON p1.id = project_id_1
JOIN projects p2 ON p2.id = project_id_2
ORDER BY num_species DESC;
xxxxxxxxxx
<meta charset="utf-8">
<head>
<link rel="stylesheet" type="text/css" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.7/css/bootstrap.min.css" />
</head>
<style>
#vis { margin: 0 auto; width: 800px; }
.group text {
font: 9px sans-serif;
pointer-events: none;
}
.group path {
stroke: #000;
}
path.chord {
stroke-width: .75;
fill-opacity: .75;
}
#circle:hover path.fade {
display: none;
}
</style>
<body>
<div id="vis">
</div>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
var width = 800,
height = 800,
outerRadius = Math.min(width, height) / 2 - 4,
innerRadius = outerRadius - 20,
fill = d3.scale.category20();
var format = d3.format(",.3r");
// Square matrices, asynchronously loaded; credits is the transpose of commonSpeciesMatrix.
var commonSpeciesMatrix = [];
// The chord layout, for computing the angles of chords and groups.
var layout = d3.layout.chord()
.sortGroups(d3.descending)
.sortSubgroups(d3.descending)
.sortChords(d3.descending)
.padding(.04);
// The arc generator, for the groups.
var arc = d3.svg.arc()
.innerRadius(innerRadius)
.outerRadius(outerRadius);
// The chord generator (quadratic Bézier), for the chords.
var chord = d3.svg.chord()
.radius(innerRadius);
// Load our data file…
d3.csv("cnc-common-species.csv", type, function(error, data) {
if (error) throw error;
// Add an SVG element for each diagram, and translate the origin to the center.
var svg = d3.select("#vis").selectAll("div.vis-container")
.data([commonSpeciesMatrix])
.enter().append("div")
.style("display", "inline-block")
.style("width", width + "px")
.style("height", height + "px")
.append("svg")
.attr("width", width)
.attr("height", height)
.append("g")
.attr("id", "circle")
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")");
function mouseover( d, i ) {
chordPaths.classed("fade", function(p) {
return p.source.index != i
&& p.target.index != i;
});
chordPaths
.style("fill", d2 => fill( d2.source.index === i ? d2.target.index : d2.source.index ) )
}
var countryByName = d3.map(),
countryIndex = -1,
countryByIndex = [];
// Compute a unique index for each country.
data.forEach(function(d) {
if (countryByName.has(d.city1)) d.city1 = countryByName.get(d.city1);
else countryByName.set(d.city1, d.city1 = {name: d.city1, index: ++countryIndex});
if (countryByName.has(d.city2)) d.city2 = countryByName.get(d.city2);
else countryByName.set(d.city2, d.city2 = {name: d.city2, index: ++countryIndex});
d.city2.risk = 1; //d.risk;
});
// Initialize a square matrix of commonSpeciesMatrix and credits.
for (var i = 0; i <= countryIndex; i++) {
commonSpeciesMatrix[i] = [];
// credits[i] = [];
for (var j = 0; j <= countryIndex; j++) {
commonSpeciesMatrix[i][j] = 0;
// credits[i][j] = 0;
}
}
// Populate the matrices, and stash a map from index to country.
data.forEach(function(d) {
commonSpeciesMatrix[d.city1.index][d.city2.index] = d;
// credits[d.city2.index][d.city1.index] = d;
countryByIndex[d.city1.index] = d.city1;
countryByIndex[d.city2.index] = d.city2;
});
// For each diagram…
// svg.each(function(matrix, j) {
// var svg = d3.select(this);
// Compute the chord layout.
layout.matrix(commonSpeciesMatrix);
// Add chords.
var chordPaths = svg.selectAll(".chord")
.data(layout.chords)
.enter().append("path")
.attr("class", "chord")
.style("fill", function(d) { return "#ccc"; })
.style("stroke", function(d) { return "#aaa"; })
.attr("d", chord)
chordPaths
.append("title")
.text( d => `${d.source.value.city2.name} shared ${d.source.value.num_species} species with ${d.source.value.city1.name}` );
// .text(function(d) { return d.source.value.city2.name + " owes " + d.source.value.city1.name + " $" + format(d.source.value) + "B."; });
// Add groups.
var g = svg.selectAll(".group")
.data(layout.groups)
.enter().append("g")
.attr("class", "group");
// Add the group arc.
g.append("path")
.style("fill", function(d) { return fill(d.index); })
.attr("id", function(d, i) { return "group" + d.index + "-" + j; })
.attr("d", arc)
.on("mouseover", mouseover)
.on("mouseover", mouseover)
.append("title")
// .text(function(d) { return countryByIndex[d.index].name + " " + (j ? "owes" : "is owed") + " $" + format(d.value) + "B."; });
.text( d => countryByIndex[d.index].name )
// Add the group label (but only for large groups, where it will fit).
// An alternative labeling mechanism would be nice for the small groups.
g.append("text")
.attr("x", 6)
.attr("dy", 15)
.filter(function(d) { return d.value > 110; })
.append("textPath")
.attr("xlink:href", function(d) { return "#group" + d.index + "-" + j; })
.text(function(d) { return countryByIndex[d.index].name; });
// });
});
function type(d) {
d.city1 = d.city1.replace( /Minneapolis/, 'Minn.' );
d.city2 = d.city2.replace( /Minneapolis/, 'Minn.' );
d.city1 = d.city1.replace( /Twin Ports/, 'TP' );
d.city2 = d.city2.replace( /Twin Ports/, 'TP' );
d.city1 = d.city1.replace( /The Wasatch Front/, 'Wasatch' );
d.city2 = d.city2.replace( /The Wasatch Front/, 'Wasatch' );
d.num_species = +d.num_species;
d.risk = 1;
d.valueOf = value; // for chord layout
return d;
}
function value() {
return this.num_species;
}
</script>
</body>
https://d3js.org/d3.v3.min.js