|
<!DOCTYPE html> |
|
|
|
<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> |