Skip to content

Instantly share code, notes, and snippets.

@curran
Last active June 8, 2019 00:25
Show Gist options
  • Save curran/96823ad84b0415536980b1cbf57b1dcc to your computer and use it in GitHub Desktop.
Save curran/96823ad84b0415536980b1cbf57b1dcc to your computer and use it in GitHub Desktop.
Cross Country Trip
license: gpl-3.0

Ever since I found out about Nasa's "Earth at Night" imagery, I wanted to make a map that puts the splotches of light into context. With this layered visualization, you can clearly see which splotches of light correspond to which cities, and into which states or countries they fall.

Click to start and stop the motion.

You can explore the whole world by panning and zooming.

This visualization uses d3-tile to show 4 layers:

Derived from mbostock's block: Raster & Vector III.

web counter
<!DOCTYPE html>
<meta charset="utf-8">
<style>
body {
margin: 0;
background-color: black;
overflow: hidden;
}
path {
fill: none;
stroke: rgb(24,205,205);
}
</style>
<body>
<script src="https://d3js.org/d3.v4.min.js"></script>
<script src="https://d3js.org/topojson.v1.min.js"></script>
<script src="https://unpkg.com/d3-tile@0.0"></script>
<script>
function tileUrl(pattern){
return function (d){
return pattern
.replace("{x}", d.x)
.replace("{y}", d.y)
.replace("{z}", d.z)
.replace("{s}", ["a", "b", "c"][Math.random() * 3 | 0]);
};
}
d3.tileProviders = {
earthAtNight: tileUrl("http://map1.vis.earthdata.nasa.gov/wmts-webmerc/VIIRS_CityLights_2012/default/GoogleMapsCompatible_Level8/{z}/{y}/{x}.jpg"),
stamenTonerLabels: tileUrl("http://stamen-tiles-{s}.a.ssl.fastly.net/toner-labels/{z}/{x}/{y}.png")
}
function tileImages(tile){
var url = d3.tileProviders.openStreetMap;
var images = function (raster){
var tiles = tile();
var image = raster
.attr("transform", stringify(tiles.scale, tiles.translate))
.selectAll("image")
.data(tiles, function(d) { return [d.tx, d.ty, d.z]; });
image.exit()
.remove();
image.enter().append("image")
.attr("xlink:href", url)
.attr("width", 256)
.attr("height", 256)
.attr("opacity", 0)
.attr("x", function(d) { return d.tx; })
.attr("y", function(d) { return d.ty; })
.on("load", function (){
d3.select(this).transition().duration(2000)
.attr("opacity", 1);
})
};
images.url = function (_){
return arguments.length ? (url = _, images) : url;
}
return images;
}
function stringify(scale, translate) {
var k = scale / 256, r = scale % 1 ? Number : Math.round;
return "translate(" + r(translate[0] * scale) + "," + r(translate[1] * scale) + ") scale(" + k + ")";
}
var width = Math.max(960, window.innerWidth),
height = Math.max(500, window.innerHeight);
var tile = d3.tile()
.size([width, height])
.zoomDelta(0.5)
.wrap(true);
var images1 = tileImages(tile)
.url(d3.tileProviders.earthAtNight);
var images2;
// Load the labels after 5 seconds.
setTimeout(function(){
images2 = tileImages(tile)
.url(d3.tileProviders.stamenTonerLabels);
raster2.call(images2);
}, 5000);
var projection = d3.geoMercator()
.scale((1 << 12) / 2 / Math.PI)
.translate([width / 2, height / 2]);
var center = projection([-100, 40]);
var path = d3.geoPath()
.projection(projection);
var zoom = d3.zoom()
.on("zoom", zoomed);
var initialTransform = d3.zoomIdentity
.translate(4103.11, 2119.35)
.scale(16384);
// Move to the left by 1/2 pixel each frame.
var dx = .5/initialTransform.k;
// With the center computed, now adjust the projection such that
// it uses the zoom behavior’s translate and scale.
projection
.scale(1 / 2 / Math.PI)
.translate([0, 0]);
var svg = d3.select("body").append("svg")
.attr("width", width)
.attr("height", height);
var raster1 = svg.append("g");
var vector1 = svg.append("path");
var vector2 = svg.append("g");
var raster2 = svg.append("g")
.attr("opacity", 0.8);
d3.json("https://gist.githubusercontent.com/mbostock/4090846/raw/07e73f3c2d21558489604a0bc434b3a5cf41a867/world-110m.json", function(error, world) {
if (error) throw error;
var features = topojson.feature(world, world.objects.countries);
svg.call(zoom).call(zoom.transform, initialTransform);
vector1
.attr("opacity", 0)
.attr("d", path(features))
.transition().duration(2000)
.attr("opacity", 1);
d3.json("https://gist.githubusercontent.com/michellechandra/0b2ce4923dc9b5809922/raw/a476b9098ba0244718b496697c5b350460d32f99/us-states.json", function(error, us) {
if (error) throw error;
// topojson.mesh(us, us.objects.states, function(a, b) { return a !== b; }))
var features = topojson.feature(us, us.features);
svg.call(zoom).call(zoom.transform, initialTransform);
vector2.selectAll("path")
.data(us.features)
.enter()
.append("path")
.attr("opacity", 0)
.attr("d", path)
.transition().duration(2000)
.attr("opacity", 1);
});
});
function zoomed() {
var transform = d3.event.transform;
vector1
.attr("transform", transform)
.style("stroke-width", 1 / transform.k);
vector2
.attr("transform", transform)
.style("stroke-width", 1 / transform.k);
tile.scale(transform.k)
.translate([transform.x, transform.y])
();
raster1.call(images1);
if(images2){
raster2.call(images2);
}
}
var moving = true;
svg.on("click", function (){
moving = !moving;
})
// Start moving West after 8 seconds.
setTimeout(function (){
// Assume 30 FPS.
var frameTime = 1000 / 30;
var x = 0, t0 = 0;
d3.timer(function (t1){
// Ease into the westward motion.
if(x < dx){
x += dx / 80;
}
if(moving){
zoom.translateBy(svg, (t1 - t0) / frameTime * x, 0);
}
t0 = t1;
});
}, 8000);
</script>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment