1692 largest cities in the world, weighted by their population sizes. Where does the centroid lie on the surface of the Earth?
d3.geoCentroid()
doesn’t account for different weights on Points. Here the spherical centroid of the largest cities in the world, both not-weighted (in blue), and weighted by their populations (in salmon).
Original work by Philippe Rivière for Visionscarto.net. Comments and variants very welcome!
Using d3-annotation() and d3-voronoi find(). Double-click to enter edit mode.
For performance, the map itself is drawn on a <canvas>
element; the annotations are displayed on a SVG overlay.
xxxxxxxxxx
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://raw.githack.com/susielu/d3-annotation/3f126d6b/d3-annotation.min.js"></script>
<style>
body {
margin: 0;
position: fixed;
top: 0;
right: 0;
bottom: 0;
left: 0;
font-family: "Lucida Grande", "Arial", "Helvetica", sans-serif;
font-size: 12px;
}
.panel {
position: absolute;
top: 0;
left: 0;
}
.annotation path {
fill: none;
stroke: #ff8684;
stroke-width: 2
}
.annotation text {
fill: #ff8684;
}
.annotation tspan {
}
.annotation-note-title {
font-weight: bold;
}
.annotation-note-bg {
fill: #111144;
fill-opacity: 0.9;
filter: url(#blur-effect);
}
circle.handle {
stroke-dasharray: 5;
stroke: #e91e56;
fill: rgba(255, 255, 255, .5);
cursor: move;
stroke-opacity: .4;
}
circle.handle.highlight {
stroke-opacity: 1;
}
.annotation-tip .annotation path {
stroke: white;
}
.annotation-tip .annotation text {
fill: white;
}
</style></head>
<body>
<script>
function weightedCentroid(data){
let X0 = Y0 = Z0 = W0 = 0;
const radians = Math.PI / 180;
function centroidPoint(lambda, phi, w) {
lambda *= radians, phi *= radians;
var cosPhi = Math.cos(phi);
centroidPointCartesian(cosPhi * Math.cos(lambda), cosPhi * Math.sin(lambda), Math.sin(phi), w);
}
function centroidPointCartesian(x, y, z, w) {
W0 += +w;
if (!w || !W0) return;
w /= W0;
X0 += (x - X0) * w;
Y0 += (y - Y0) * w;
Z0 += (z - Z0) * w;
}
data.map(d => centroidPoint(...d));
var x = X0,
y = Y0,
z = Z0,
m = x * x + y * y + z * z;
return [Math.atan2(y, x) / radians, Math.asin(z / Math.sqrt(m)) / radians];
}
const width = 960,
height = 500,
margin = 40,
scalepop = d3.scaleSqrt().domain([0, 100000]).range([0.2, 24]),
scalecountry = d3.scaleOrdinal(d3.schemeCategory20b),
projection = d3.geoEquirectangular().rotate([-10.5,0]);
d3.csv('cities.csv', function (cities) {
const data = cities
.sort((a, b) => d3.descending(+a[2015], +b[2015]))
.map((d, i) => [+d.Longitude, +d.Latitude, +d[2015], +d['Country Code'], d['Urban Agglomeration']]);
const canvas = d3.select("body").append("canvas")
.attr("width", width)
.attr("height", height)
.attr("class", "panel"),
context = canvas.node().getContext("2d");
// retina display
var devicePixelRatio = window.devicePixelRatio || 1;
canvas.style('width', canvas.attr('width')+'px');
canvas.style('height', canvas.attr('height')+'px');
canvas.attr('width', canvas.attr('width') * devicePixelRatio);
canvas.attr('height', canvas.attr('height') * devicePixelRatio);
context.scale(devicePixelRatio,devicePixelRatio);
const svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height)
.attr("class", "panel");
// this almost invisible rect allows our svg to receive mousemove events
svg.append('rect')
.attr("width", width)
.attr("height", height)
.attr("fill", 'rgba(0,0,0,0.01)');
svg.append('defs')
.append('filter')
.attr('id', 'blur-effect')
.append('feGaussianBlur')
.attr('stdDeviation', 4);
const nodes = data.map(d => {
let p = projection(d);
d.x = p[0];
d.y = p[1];
d.r = scalepop(d[2]);
d.color = 'rgba(240,255,240,0.4)';
d.name = d[4];
return d;
});
var gcities = context; //svg.append('g');
drawcanvas(gcities, nodes);
// draw the unweighted geoCentroid
const centroid = d3.geoCentroid({
type: "MultiPoint",
coordinates: data.map(d => [d[0], d[1]])
});
let p1 = projection(centroid);
centroid.r = 6;
drawsvg(svg.append('g'), [{
x: p1[0],
y: p1[1],
r: centroid.r,
color: '#8e84ff',
stroke: 'black',
}]);
// draw the *weighted* geoCentroid
const wcentroid = weightedCentroid(data.map(d => [d[0], d[1], d[2]]));
let p2 = projection(wcentroid);
wcentroid.r = 12;
let wcentroids = [{
x: p2[0],
y: p2[1],
r: wcentroid.r,
data: wcentroid,
color: '#ff8684',
stroke: 'black',
}];
drawsvg(svg.append('g'), wcentroids);
centroid.name = "Centroid";
centroid.dx = -65;
centroid.dy = -40;
wcentroid.name = "Weighted Centroid";
wcentroid.dx = 90;
wcentroid.dy = -30;
function drawsvg (g, nodes) {
g
.selectAll('circle')
.data(nodes)
.enter()
.append('circle')
.attr('r', d => d.r)
.attr('cx', d => d.x)
.attr('cy', d => d.y)
.attr('fill', d => d.color)
.attr('stroke', d => d.stroke || 'none');
}
function drawcanvas(context, nodes) {
context.fillStyle = "#130c30";
context.fillRect(0,0,width,height)
for (var i = 0, n = nodes.length; i < n; ++i) {
var node = nodes[i];
context.beginPath();
context.moveTo(node.x, node.y);
context.arc(node.x, node.y, node.r, 0, 2 * Math.PI);
context.lineWidth = 8;
if (node.stroke){
context.color = node.stroke;
context.stroke();
}
context.fillStyle = node.color;
context.fill();
}
}
annotation = d3.annotation()
.type(d3.annotationCalloutCircle)
.annotations([centroid, wcentroid]
.map(d => {
return {
data: d,
dx: d.dx || 0,
dy: d.dy || 0,
note: {
title: d.name || "??",
label: d.map(d3.format('0.2f')).join(', '),
},
subject: {
radius: d.r,
radiusPadding: 2,
},
}
}))
.accessors({ x: d => projection(d)[0], y: d => projection(d)[1] })
svg.append("g")
.attr("class", "annotation-centroids")
.call(annotation)
.on('dblclick', function() {
annotation.editMode(!annotation.editMode()).update();
});
// create a container for tooltips
tipg = svg.append("g")
.attr("class", "annotation-tip");
// this function will call d3.annotation when a tooltip has to be drawn
function tip (d) {
annotationtip = d3.annotation()
.type(d3.annotationCalloutCircle)
.annotations([d].map(d => {
return {
data: d,
dx: d.dx || (d.x > 450) ? -50 : 50,
dy: d.dy || (d.y > 240) ? -10 : 10,
note: {
label: d.name || "??",
},
subject: {
radius: d.r,
radiusPadding: 2,
},
};
}))
.accessors({ x: d => projection(d)[0], y: d => projection(d)[1] })
tipg.call(annotationtip);
}
// use voronoi.find() on mousemove to decide what tooltip to display
let voronoi = null;
svg.on('mousemove', function() {
if (!voronoi) voronoi = d3.voronoi().x(d => d.x).y(d => d.y)(nodes);
let m = d3.mouse(this);
let f = voronoi.find(m[0], m[1], 15 /* voronoi radius */);
if (f) {
tip(f.data);
} else {
tipg.selectAll("g").remove();
}
});
});
</script>
</body>
https://d3js.org/d3.v4.min.js
https://raw.githack.com/susielu/d3-annotation/3f126d6b/d3-annotation.min.js