|
<!DOCTYPE html> |
|
<head> |
|
<meta charset="utf-8"> |
|
<script src="https://unpkg.com/d3@7"></script> |
|
<script src="https://unpkg.com/d3-delaunay@6"></script> |
|
<script src="https://unpkg.com/d3-geo-voronoi@2"></script> |
|
<style> |
|
body { margin:0;position:fixed;top:0;right:0;bottom:0;left:0; } |
|
|
|
.countries path { |
|
stroke: white; |
|
stroke-width: 0.3; |
|
opacity: 0.95; |
|
fill: #dbdbdb; |
|
} |
|
|
|
.links { |
|
stroke: red; |
|
stroke-opacity: 0.8; |
|
stroke-width: 1px; |
|
/*stroke-dasharray: 1 4; */ |
|
fill: none; |
|
} |
|
|
|
.polygons { |
|
stroke: #c7ff84; |
|
stroke-width: 4; |
|
fill: #c7ff84; |
|
fill-opacity: 0.3; |
|
} |
|
|
|
.links { |
|
stroke-linecap: round; |
|
} |
|
|
|
.site { |
|
fill: #ddd; |
|
stroke: #000; |
|
stroke-width: 0.5; |
|
} |
|
</style> |
|
<svg width="960" height="500"></svg> |
|
|
|
|
|
<script> |
|
|
|
var svg = d3.select("svg"), |
|
width = +svg.attr("width"), |
|
height = +svg.attr("height"); |
|
|
|
svg = svg |
|
.append('g'); |
|
|
|
|
|
var projection = d3.geoOrthographic().scale(214), |
|
path = d3.geoPath().projection(projection).pointRadius(1); |
|
|
|
|
|
var g = svg.append('g') |
|
.attr('class', 'world') |
|
.append('g') |
|
.attr("class", "s"); |
|
|
|
var defs = g.append("defs"); |
|
|
|
defs.append("path") |
|
.datum({ |
|
type: "Sphere" |
|
}) |
|
.attr("id", "sphere") |
|
.attr("d", path); |
|
|
|
g.append("use") |
|
.attr("xlink:href", "#sphere") |
|
.attr("fill", "#fcfcff"); |
|
|
|
defs.append("clipPath") |
|
.attr("id", "clip") |
|
.append("use") |
|
.attr("xlink:href", "#sphere"); |
|
|
|
g.attr("clip-path", "url(#clip)") |
|
|
|
g.append('g') |
|
.attr('class', 'countries'); |
|
|
|
g.append("g") |
|
.attr("class", "polygons") |
|
|
|
var site = g.append("g") |
|
.attr("class", "site") |
|
.selectAll('path') |
|
.data([null]); |
|
var enter = site |
|
.enter() |
|
.append('path'); |
|
site = site.merge(enter); |
|
|
|
var legend = svg |
|
.append('text') |
|
.attr('transform', 'translate(' + [width / 2, 30] + ')') |
|
.attr('class', 'legend') |
|
.attr('text-anchor', 'middle') |
|
.attr('font-size', '20px') |
|
.attr('font-family', 'Helvetica'); |
|
|
|
var drag = 0; |
|
|
|
d3.json('countries.geojson').then(function (world) { |
|
var visit = 0; |
|
var countries = d3.select('.countries') |
|
.selectAll('path') |
|
.data(world.features) |
|
.join('path') |
|
.attr('d', path) |
|
|
|
var subregions = [...new Set(world.features.map(d => d.properties.subregion))]; |
|
|
|
go(countries, subregions[visit]); |
|
|
|
d3.interval(visitnext, 1200) |
|
|
|
function visitnext() { |
|
if (drag) return; |
|
visit = (visit + 1) % subregions.length; |
|
go(countries, subregions[visit]); |
|
} |
|
}); |
|
|
|
function go(countries, subregion) { |
|
|
|
legend.text('The convex hull of ' + (subregion == 'Caribbean' || subregion == 'Seven seas (open ocean)' ? 'the ' : '') + subregion) |
|
|
|
var sites = [], |
|
centroids = []; |
|
|
|
countries.data() |
|
.filter(function (d) { |
|
return d.properties.subregion == subregion; |
|
}) |
|
.map(function (d) { |
|
// remove French Guyane for the computation of bounds |
|
var e = JSON.parse(JSON.stringify(d)); |
|
if (e.properties.iso_a3 == 'FRA') { |
|
e.geometry.coordinates = d.geometry.coordinates.slice(2); |
|
} |
|
return e; |
|
}) |
|
.map(function (d) { |
|
var convex = d3.geoBounds(d); |
|
sites.push(convex[0]); |
|
sites.push(convex[1]); |
|
sites.push([convex[0][0], convex[1][1]]); |
|
sites.push([convex[1][0], convex[0][1]]); |
|
centroids.push(d3.geoCentroid(d)); |
|
}); |
|
|
|
|
|
|
|
var hull = d3.geoVoronoi().hull(sites); |
|
|
|
// special case, sorry! |
|
if (subregion == "Antarctica") { |
|
hull = d3.geoCircle().center([0, -90]).radius(29)(); |
|
} |
|
|
|
var rotation = d3.geoCentroid({ |
|
type: 'MultiPoint', |
|
coordinates: sites |
|
}) |
|
.map(function (x) { |
|
return -x; |
|
}); |
|
|
|
projection.rotate(rotation); |
|
|
|
countries.attr("d", path); |
|
|
|
d3.select('.polygons path').remove(); |
|
var poly = d3.select('.polygons') |
|
.append("path") |
|
.datum(hull) |
|
.attr('d', path); |
|
|
|
site.datum({ |
|
type: "MultiPoint", |
|
coordinates: sites |
|
}); |
|
|
|
|
|
|
|
|
|
function draw() { |
|
poly.attr('d', path); |
|
countries.attr("d", path); |
|
site.attr('d', path) |
|
} |
|
|
|
draw(); |
|
|
|
|
|
// drag and zoom |
|
svg.select('.world') |
|
.call(d3.drag() |
|
.on("start", dragstarted) |
|
.on("drag", dragged) |
|
.on("end", dragended) |
|
) |
|
.call(d3.zoom() |
|
.scaleExtent([1, 8]) |
|
.on("zoom", zoomed) |
|
.on("start", function () { |
|
drag++; |
|
}) |
|
.on("end", function () { |
|
setTimeout(function () { |
|
drag--; |
|
}, 1500); |
|
}) |
|
); |
|
|
|
function zoomed({transform}) { |
|
svg.select('.world').attr("transform", transform); |
|
} |
|
|
|
function dragstarted(event) { |
|
drag++; |
|
q = projection.rotate(); |
|
r = d3.pointer(event); |
|
} |
|
|
|
function dragended() { |
|
setTimeout(function () { |
|
drag--; |
|
}, 2000); |
|
} |
|
|
|
var lambda = d3.scaleLinear() |
|
.domain([0, width]) |
|
.range([-180, 180]); |
|
|
|
var phi = d3.scaleLinear() |
|
.domain([0, height]) |
|
.range([90, -90]); |
|
|
|
function dragged(event) { |
|
var p = d3.pointer(event); |
|
projection.rotate([lambda(p[0]) - lambda(r[0]) + q[0], phi(p[1]) - phi(r[1]) + q[1]]); |
|
draw(); |
|
} |
|
|
|
} |
|
</script> |