Skip to content

Instantly share code, notes, and snippets.

@nl-hugo
Last active July 23, 2018 13:40
Show Gist options
  • Save nl-hugo/21305b18ca50bede3bdc843549b8832f to your computer and use it in GitHub Desktop.
Save nl-hugo/21305b18ca50bede3bdc843549b8832f to your computer and use it in GitHub Desktop.
SE Asia map with GPX data
height: 650
license: MIT
node_modules
npm-debug.log
shapes/

A visualisation of GPX waypoints A test case to automate download and conversion of CBS maps into a D3.js map.

This block packs the following features:

  • Locations in the gpx file are encoded in the TopoJSON file by the npm commands in package.json.
  • Colorizes countries (visited, not visited) based on locations in the gpx file. Note that this is not always accurate due to polygon simplification.
  • The gpx track is divided into segments for each pair of subsequent locations. Each of the segments can be styled using the 'sym' tag of the location.
  • Filters 'major' and 'minor' waypoints based on the 'description' tag in the gpx file. All waypoints are used to draw the path, but only major waypoints have mouseover interactivity.
  • Voronoi polygons for easier selection.
  • Mouseover reveals the location description from the gpx file.
  • Click-to-zoom using the voronoi polygons.

To reproduce the json file for the map: npm install npm run map

Resources:

Display the source blob
Display the rendered blob
Raw
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
<!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>
{
"name": "se-asia-map",
"version": "0.0.1",
"description": "SE Asia map",
"keywords": [
"Visualization",
"Map",
"D3.js"
],
"author": "Hugo Janssen <[email protected]>",
"private": true,
"license": "MIT",
"devDependencies": {
"download-cli": "~1.0",
"topojson": "~1.6",
"rimraf": "latest",
"ogr2ogr": "1.0.1"
},
"scripts": {
"clean": "rimraf shapes",
"download": "download --extract --out shapes https://www.naturalearthdata.com/http//www.naturalearthdata.com/download/10m/cultural/ne_10m_admin_0_countries.zip",
"shape:countries": "ogr2ogr -f GeoJSON -t_srs EPSG:4326 -clipdst 71 20 180 -51 shapes/countries.json shapes/ne_10m_admin_0_countries.shp",
"shape:wpts": "ogr2ogr -f GeoJSON -t_srs EPSG:4326 shapes/route.json route.gpx track_points -fieldTypeToString DateTime",
"shape": "npm run shape:wpts && npm run shape:countries",
"topojson": "topojson --id-property ADM0_A3 -p country=NAME -p time -p name -p sym -p desc -s 3e-9 -o asia-map.json -- shapes/countries.json shapes/route.json",
"map": "npm run download && npm run shape && npm run topojson && npm run clean"
}
}
<?xml version="1.0" encoding="UTF-8"?>
<gpx version="1.1" creator="Galileo Offline Maps Pro v3.4.7(3740)" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://www.topografix.com/GPX/1/1" xsi:schemaLocation="http://www.topografix.com/GPX/1/1 http://www.topografix.com/GPX/1/1/gpx.xsd"><metadata><name>AUS-NZ-SEA</name><desc></desc><time>2018-09-16T16:18:48.027Z</time></metadata>
<trk>
<name>Test</name>
<desc>Test tracj</desc>
<type>TrackStyle_3a96ffc8</type>
<trkseg>
<trkpt lat="52.370918" lon="4.892436"><ele>100.018</ele>
<name>Amsterdam</name>
<sym></sym>
<time>2016-09-16T07:48:34.000Z</time><hdop>10.000</hdop><vdop>24.000</vdop></trkpt>
<trkpt lat="-31.950197" lon="115.860381"><ele>100.018</ele>
<name>Perth</name>
<sym>plane</sym>
<desc>A description for Perth</desc>
<time>2016-09-16T07:48:34.000Z</time><hdop>10.000</hdop><vdop>24.000</vdop></trkpt>
<trkpt lat="-34.934987773899998" lon="138.60000484099999"><ele>103.632</ele>
<name>Adelaide</name>
<sym>car</sym>
<desc>A description for Adelaide</desc>
<time>2016-09-16T07:48:35.000Z</time><hdop>10.000</hdop><vdop>24.000</vdop></trkpt>
<trkpt lat="-37.820031312300003" lon="144.975016235"><ele>103.632</ele>
<name>Melbourne</name>
<sym>car</sym>
<desc>A description for Melbourne</desc>
<time>2016-09-16T07:48:35.000Z</time><hdop>10.000</hdop><vdop>24.000</vdop></trkpt>
<trkpt lat="-33.9200109672" lon="151.185179809"><ele>103.632</ele>
<name>Sydney</name>
<sym>car</sym>
<desc>A description for Sydney</desc>
<time>2016-09-16T07:48:35.000Z</time><hdop>10.000</hdop><vdop>24.000</vdop></trkpt>
<trkpt lat="-43.535021595389999" lon="172.63002315616001"><ele>103.632</ele>
<name>Christchurch</name>
<sym>plane</sym>
<desc>A description for Christchurch</desc>
<time>2016-09-16T07:48:35.000Z</time><hdop>10.000</hdop><vdop>24.000</vdop></trkpt>
<trkpt lat="-36.848054894930002" lon="174.76302698708"><ele>103.632</ele>
<name>Auckland</name>
<sym>car</sym>
<desc>A description for Auckland</desc>
<time>2016-09-16T07:48:35.000Z</time><hdop>10.000</hdop><vdop>24.000</vdop></trkpt>
<trkpt lat="-8.732067" lon="115.167623"><ele>103.632</ele>
<name>Denpasar</name>
<sym>plane</sym>
<desc>A description for Denpasar</desc>
<time>2016-09-16T07:48:35.000Z</time><hdop>10.000</hdop><vdop>24.000</vdop></trkpt>
<trkpt lat="-8.348356" lon="116.038438"><ele>103.632</ele>
<name>Gili T</name>
<sym>boat</sym>
<time>2016-09-16T07:48:35.000Z</time><hdop>10.000</hdop><vdop>24.000</vdop></trkpt>
<trkpt lat="3.583907" lon="98.672585"><ele>103.632</ele>
<name>Medan</name>
<sym>plane</sym>
<desc>A description for Medan</desc>
<time>2016-09-16T07:48:35.000Z</time><hdop>10.000</hdop><vdop>24.000</vdop></trkpt>
</trkseg>
</trk>
</gpx>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment