Skip to content

Instantly share code, notes, and snippets.

@danbjoseph
Last active July 9, 2016 21:00
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save danbjoseph/8397d08dd41e40a4274248505653760b to your computer and use it in GitHub Desktop.
Save danbjoseph/8397d08dd41e40a4274248505653760b to your computer and use it in GitHub Desktop.
Metro warp

This transformation shows the difference between the Washington, DC Metro map and the actual lines at geographic scale. The map version has been kept proportional and geo-referenced so that Metro Center, Gallery Place, and L'Enfant Plaza are approximately correct. In both versions where multiple lines run the same tracks an artificial offset has been added to improve visibility of all colors in a manner similar to the official map.

Data for the visualization is derived from the WMATA Metro map and datasets available from the District of Columbia Open Data Catalog.

// originally from Nick Rabinowitz https://gist.github.com/1756257
// box should be something like: [[0,0],[w, h]]
function fitProjection(projection, data, box, center) {
// get the bounding box for the data - might be more efficient approaches
var left = Infinity,
bottom = -Infinity,
right = -Infinity,
top = Infinity;
// reset projection
projection
.scale(1)
.translate([0, 0]);
data.forEach(function(feature) {
d3.geoBounds(feature).forEach(function(coords) {
coords = projection(coords);
var x = coords[0],
y = coords[1];
if (x < left) left = x;
if (x > right) right = x;
if (y > bottom) bottom = y;
if (y < top) top = y;
});
});
// project the bounding box, find aspect ratio
function width(bb) {
return (bb[1][0] - bb[0][0])
}
function height(bb) {
return (bb[1][1] - bb[0][1]);
}
function aspect(bb) {
return width(bb) / height(bb);
}
var startbox = [[left, top], [right, bottom]],
a1 = aspect(startbox),
a2 = aspect(box),
widthDetermined = a1 > a2,
scale = widthDetermined ?
// scale determined by width
width(box) / width(startbox) :
// scale determined by height
height(box) / height(startbox),
// set x translation
transX = box[0][0] - startbox[0][0] * scale,
// set y translation
transY = box[0][1] - startbox[0][1] * scale;
// center if requested
if (center) {
if (widthDetermined) {
transY = transY - (transY + startbox[1][1] * scale - box[1][1])/2;
} else {
transX = transX - (transX + startbox[1][0] * scale - box[1][0])/2;
}
}
return projection.scale(scale).translate([transX, transY])
}
<!DOCTYPE html>
<meta charset="utf-8">
<style>
html,
body {
margin: 0;
overflow: hidden;
}
.lines path {
fill: none;
stroke-width:3px;
}
.lines .yellow {
stroke: #ffd204;
}
.lines .yellow-rush-N, .lines .yellow-rush-S {
stroke: #ffd204;
stroke-dasharray: 2 2;
}
.lines .silver {
stroke: #a1a3a1;
}
.lines .red {
stroke: #e51937;
}
.lines .green {
stroke: #00a950;
}
.lines .blue {
stroke: #0077c0;
}
.lines .orange {
stroke: #f7941d;
}
.stations circle {
fill: white;
stroke-width: 1;
stroke: #000;
}
#state {
position: fixed;
top: 12px;
left: 12px;
}
#autoplay {
position: fixed;
top: 12px;
left: 43px;
}
.btn {
border: 1px solid #ccc;
border-radius: 2px;
background-color: #fff;
cursor: pointer;
}
.btn:hover {
background-color: #e6e6e6;
border-color: #adadad;
}
.sprite {
background-image: url(sprite.png);
}
@media (min--moz-device-pixel-ratio: 1.5), (-o-min-device-pixel-ratio: 3/2), (-webkit-min-device-pixel-ratio: 1.5), (min-device-pixel-ratio: 1.5), (min-resolution: 1.5dppx) {
.sprite {
background-image: url(sprite@2x.png);
background-size: 100px 25px;
}
}
.map {
width: 25px;
height: 25px;
background-position: 0 0;
}
.world {
width: 25px;
height: 25px;
background-position: -25px 0px;
}
.play {
width: 25px;
height: 25px;
background-position: -50px 0px;
}
.pause {
width: 25px;
height: 25px;
background-position: -75px 0px;
}
</style>
<body>
<div id="state" class="btn sprite map"></div>
<div id="autoplay" class="btn sprite pause"></div>
<svg></svg>
<script src="//d3js.org/d3.v4.min.js"></script>
<script src="fitProjection.js"></script>
<script>
var mapLines, worldLines, mapStations, worldStations,
playing = true;
projection = d3.geoMercator(),
path = d3.geoPath(),
margin = {"top": 20, "right": 20, "bottom": 20, "left": 20 },
width = window.innerWidth - margin.left - margin.right,
height = window.innerHeight - margin.top - margin.bottom;
var svg = d3.select("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom )
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
d3.queue()
.defer(d3.json, "map-lines.json")
.defer(d3.json, "world-lines.json")
.defer(d3.json, "map-stations.json")
.defer(d3.json, "world-stations.json")
.await(ready);
function ready(error, mapLinesJson, worldLinesJson, mapStationsJson, worldStationsJson){
mapLines = mapLinesJson;
worldLines = worldLinesJson;
mapStations = mapStationsJson;
worldStations = worldStationsJson;
fitProjection(projection, worldLines.features, [[0,0],[width, height]]);
path.projection(projection);
lines = svg.append("g")
.attr("class", "lines")
.selectAll("path")
.data(mapLines.features, function(d) { return d.properties.name; })
.enter().append("path")
.attr("d", path)
.attr("class", function(d){ return d.properties.name; });
stations = svg.append("g")
.attr("class", "stations")
.selectAll("circle")
.data(mapStations.features, function(d) { return d.properties.name; })
.enter().append("circle")
.attr("cx", function (d) { return projection(d.geometry.coordinates)[0]; })
.attr("cy", function (d) { return projection(d.geometry.coordinates)[1]; })
.attr("r", function (d) {
if(d.properties.type === "lg") return 2.5;
if(d.properties.type === "sm") return 1;
})
warp();
}
function warp() {
if(d3.select("#state").classed("world")){
lines.data(mapLines.features, function(d) { return d.properties.name; })
.transition().attr("d", path).duration(4000);
stations.data(mapStations.features, function(d) { return d.properties.name; })
.transition()
.attr("cx", function (d) { return projection(d.geometry.coordinates)[0]; })
.attr("cy", function (d) { return projection(d.geometry.coordinates)[1]; })
.duration(4000);
d3.select("#state").classed("world", false).classed("map", true);
} else {
lines.data(worldLines.features, function(d) { return d.properties.name; })
.transition().attr("d", path).duration(4000);
stations.data(worldStations.features, function(d) { return d.properties.name; })
.transition()
.attr("cx", function (d) { return projection(d.geometry.coordinates)[0]; })
.attr("cy", function (d) { return projection(d.geometry.coordinates)[1]; })
.duration(4000);
d3.select("#state").classed("world", true).classed("map", false);
}
setTimeout(function () {
if(playing){
warp();
}
}, 5000)
}
d3.select("#state").on("click", warp);
d3.select("#autoplay").on("click", function(){
if(d3.select("#autoplay").classed("pause")){
d3.select("#autoplay").classed("pause", false)
.classed("play", true);
playing = false;
} else {
d3.select("#autoplay").classed("pause", true)
.classed("play", false);
playing = true;
warp();
}
});
d3.select(window).on("resize", throttle);
function throttle() {
window.setTimeout(function() {
width = window.innerWidth - margin.left - margin.right;
height = window.innerHeight - margin.top - margin.bottom;
svg.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom );
fitProjection(projection, worldLines.features, [[0,0],[width, height]]);
path.projection(projection);
lines.attr("d", path);
stations.attr("cx", function (d) { return projection(d.geometry.coordinates)[0]; })
.attr("cy", function (d) { return projection(d.geometry.coordinates)[1]; });
}, 600);
}
</script>
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Display the source blob
Display the rendered blob
Raw
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment