|
<!DOCTYPE html> |
|
<meta charset="utf-8"> |
|
<link href="https://fonts.googleapis.com/css?family=Montserrat:400,700" rel="stylesheet"> |
|
<style> |
|
|
|
svg { |
|
font-family: "Montserrat", sans-serif; |
|
display: block; |
|
font-size: 9px; |
|
line-height: 1.5em; |
|
} |
|
|
|
.borders { |
|
fill: none; |
|
stroke: #ccc; |
|
stroke-linejoin: round; |
|
} |
|
|
|
.country { |
|
fill: none; |
|
} |
|
|
|
.country.visited { |
|
fill: #efefee; |
|
} |
|
|
|
.dimmed { |
|
fill-opacity: 0.8; |
|
} |
|
|
|
.country-label { |
|
fill: none; |
|
} |
|
|
|
.country-label.visited { |
|
fill: #777; |
|
fill-opacity: .4; |
|
font-size: 11px; |
|
font-weight: 700; |
|
letter-spacing: .08em; |
|
text-transform: uppercase; |
|
text-anchor: middle; |
|
} |
|
|
|
path.segment { |
|
fill: none; |
|
stroke: steelblue; |
|
} |
|
|
|
path.segment-plane { |
|
stroke-dasharray: 2 4; |
|
} |
|
|
|
.polygons { |
|
fill: none; |
|
pointer-events: all; |
|
} |
|
|
|
.waypoint.dimmed { |
|
opacity: 0.5; |
|
} |
|
|
|
.waypoint path { |
|
fill: none; |
|
stroke: steelblue; |
|
} |
|
|
|
.waypoint text { |
|
fill: #777; |
|
stroke: none; |
|
} |
|
|
|
.g-legend .waypoint { |
|
opacity: 0.0; |
|
font-size: 11px; |
|
} |
|
|
|
.g-legend .highlighted { |
|
opacity: 1.0; |
|
/*fill: none;*/ |
|
} |
|
|
|
.g-legend .legend-title { |
|
font-weight: bold; |
|
} |
|
|
|
</style> |
|
<body> |
|
<svg width="960" height="650"></svg> |
|
</body> |
|
<script src="https://d3js.org/d3.v4.min.js"></script> |
|
<script src="https://d3js.org/topojson.v1.min.js"></script> |
|
<script> |
|
let svg = d3.select("svg"), |
|
width = +svg.attr("width"), |
|
height = +svg.attr("height"), |
|
margin = {top: 20, right: 20, bottom: 20, left: 20}, |
|
centered; |
|
|
|
let projection = d3.geoMercator() |
|
.center([129, -20]) |
|
.translate([width / 2, height / 2]) |
|
.scale(width / 2); |
|
|
|
let path = d3.geoPath() |
|
.projection(projection) |
|
.pointRadius(2); |
|
|
|
let voronoi = d3.voronoi() |
|
.x(d => projection(d.geometry.coordinates)[0]) |
|
.y(d => projection(d.geometry.coordinates)[1]) |
|
.extent([[0, 0], [width, height]]); |
|
|
|
let g = svg.append("g") |
|
.attr("class", "g-map"); |
|
|
|
let l = svg.append("g") |
|
.attr("class", "g-legend") |
|
.attr("transform", "translate(" + margin.left + "," + (height - 100) + ")"); |
|
|
|
d3.json("asia-map.json", (error, map) => { |
|
if (error) return console.error(error); |
|
|
|
let countries = topojson.feature(map, map.objects.countries).features; |
|
let route = topojson.feature(map, map.objects.route).features; |
|
|
|
// mark countries as 'visited' when it contains a location from the gpx file |
|
// may not always be accurate, due to GeoJSON simplification |
|
countries.forEach(d => d.properties.visited = route.some(e => d3.geoContains(d, e.geometry.coordinates))); |
|
|
|
|
|
// countries |
|
g.append("g") |
|
.attr("id", "country") |
|
.selectAll("path") |
|
.data(countries) |
|
.enter().append("path") |
|
.attr("d", path) |
|
.attr("class", d => "country country-" + d.id); |
|
|
|
d3.selectAll(".country").classed("visited", d => d.properties.visited); |
|
|
|
g.append("path") |
|
.datum(topojson.mesh(map, map.objects.countries)) |
|
.attr("class", "borders") |
|
.attr("d", path); |
|
|
|
g.selectAll(".country-label") |
|
.data(countries) |
|
.enter().append("text") |
|
.attr("class", d => "country-label " + d.id) |
|
.attr("transform", d => "translate(" + path.centroid(d) + ")") |
|
.attr("dy", ".35em") |
|
.text(d => d.properties.country); |
|
|
|
d3.selectAll(".country-label").classed("visited", d => d.properties.visited); |
|
|
|
|
|
// route |
|
let segments = g.selectAll(".segment") |
|
.data(pathSegments(route)) |
|
.enter().append("path") |
|
.attr("d", d => path({ type: "LineString", coordinates: [d[0].geometry.coordinates, d[1].geometry.coordinates] })) |
|
.attr("class", d => "segment segment-" + d[1].properties.sym); |
|
|
|
|
|
// waypoints |
|
let waypoint = g.selectAll(".waypoint") |
|
.data(route.filter(d => d.properties.desc)) // with description only |
|
.enter().append("g") |
|
.attr("class", d => "waypoint waypoint-" + d.properties.name); |
|
|
|
waypoint.append("path") |
|
.attr("d", path); |
|
|
|
waypoint.append("text") |
|
.attr("transform", d => "translate(" + path.centroid(d) + ")") |
|
.attr("y", 10) |
|
.attr("dy", ".35em") |
|
.style("text-anchor", "middle") |
|
.text(d => d.properties.name); |
|
|
|
|
|
// voronoi overlay |
|
g.selectAll(".polygons") |
|
.data(voronoi.polygons(route.filter(d => d.properties.desc))) |
|
.enter().append("path") |
|
.attr("class", "polygons") |
|
.attr("d", function(d) { return d ? "M" + d.join("L") + "Z" : null; }) |
|
.style("stroke", "#A074A0") // show the cells |
|
.on("mouseover", function(d) { |
|
d3.selectAll(".waypoint").classed("dimmed", true); |
|
d3.selectAll(".waypoint-" + d.data.properties.name).classed("dimmed", false).classed("highlighted", true); |
|
}) |
|
.on("mouseout", function() { d3.selectAll(".waypoint").classed("dimmed highlighted", false); }) |
|
.on("click", clicked); |
|
|
|
|
|
// waypoint descriptions |
|
let description = l.selectAll(".waypoint") |
|
.data(route.filter(d => d.properties.desc)) // with description only |
|
.enter().append("g") |
|
.attr("class", d => "waypoint waypoint-" + d.properties.name); |
|
|
|
description.append("text") |
|
.attr("x", 16) |
|
.attr("y", 6) |
|
.attr("dy", ".35em") |
|
.attr("class", "legend-title") |
|
.text(d => d.properties.name); |
|
|
|
description.append("text") |
|
.attr("x", 16) |
|
.attr("y", 30) |
|
.attr("dy", ".35em") |
|
.text(d => d.properties.desc); |
|
}); |
|
|
|
// Produce an array of two-element arrays [x, y] for each segment of values. |
|
function pathSegments(values) { |
|
let i = 0, n = values.length, segments = new Array(n - 1); |
|
while (++i < n) segments[i - 1] = [values[i - 1], values[i]]; |
|
return segments; |
|
} |
|
|
|
function clicked(d) { |
|
let dx, dy, k, i; |
|
|
|
if (d && centered !== d) { |
|
let centroid = projection(d.data.geometry.coordinates); |
|
dx = centroid[0]; |
|
dy = centroid[1]; |
|
k = 4; |
|
centered = d; |
|
} else { |
|
dx = width / 2; |
|
dy = height / 2; |
|
k = 1; |
|
centered = null; |
|
} |
|
|
|
g.selectAll("path") |
|
.classed("active", centered && function(d) { return d === centered; }); |
|
|
|
g.transition() |
|
.duration(750) |
|
.attr("transform", "translate(" + width / 2 + "," + height / 2 + ")scale(" + k + ")translate(" + -dx + "," + -dy + ")"); |
|
} |
|
</script> |