Using d3-annotation() and d3-voronoi find() to annotate a map. Double-click to enter edit mode.
forked from Fil's block: annotate a map
forked from Fil's block: annotate a map (canvas + svg)
xxxxxxxxxx
<head>
<meta charset="utf-8">
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="d3-annotation.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");
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 = canvas; //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(canvas, nodes) {
let context = canvas.node().getContext("2d");
context.fillStyle = "#111144";
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.html('').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