Claire’s idea.
Coordinate space based on this SVG map. To-do: reproject to real geographic coordinates; sprinkle riders like parmesan; music from [http://hrustevich.com/en/recordings](this guy).
height: 1700 |
Claire’s idea.
Coordinate space based on this SVG map. To-do: reproject to real geographic coordinates; sprinkle riders like parmesan; music from [http://hrustevich.com/en/recordings](this guy).
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<meta name="viewport" content="width=device-width, initial-scale=1"> | |
<title>MTA spaghetti</title> | |
<style> | |
html, body { | |
margin: 0; | |
padding: 0; | |
width: 960px; | |
height: 1700px; | |
background: #F2EAD6; | |
} | |
svg { | |
overflow: visible; | |
width: 100%; | |
height: 100%; | |
} | |
.links line { | |
stroke: #aaa; | |
stroke-width: 10; | |
} | |
.nodes circle { | |
pointer-events: all; | |
stroke: black; | |
stroke-width: 2; | |
fill: white; | |
} | |
.nodes circle.forking { | |
stroke: red; | |
} | |
.controls { | |
position: fixed; | |
top: 1em; | |
left: 1em; | |
z-index: 3; | |
} | |
button { | |
background: white; | |
border: 1px solid black; | |
border-radius: 50%; | |
width: 5em; | |
height: 5em; | |
padding: 1em; | |
cursor: pointer; | |
opacity: .5; | |
} | |
button:hover { | |
opacity: 1; | |
} | |
button.active { | |
border: 1px solid white; | |
background: black; | |
color: white; | |
} | |
img.fork { | |
position: absolute; | |
pointer-events: none; | |
z-index: 2; | |
transform: translate(-50%,0%); | |
transform-origin: 50% 0%; | |
} | |
text { | |
font-family: sans-serif; | |
font-size: 10px; | |
fill: rgba(0,0,0,.2); | |
display: none; | |
pointer-events: none; | |
} | |
* { | |
-webkit-touch-callout: none; | |
-webkit-user-select: none; | |
-moz-user-select: none; | |
-ms-user-select: none; | |
user-select: none; | |
} | |
@media (max-width: 666px) { | |
body, html { | |
width: 100%; | |
height: 100%; | |
position: relative; | |
overflow: hidden; | |
} | |
svg { | |
overflow: hidden; | |
} | |
text { | |
display: none; | |
} | |
} | |
</style> | |
<body> | |
<svg></svg> | |
<div class="controls"> | |
<button class="fork">Fork</button> | |
<button class="music">Music</button> | |
<button class="reset">Reset</button> | |
<!-- <button class="repel">Repel</button> --> | |
</div> | |
<audio loop src="http://hrustevich.com/data/uploads/mp3/2013/tr01.mp3"></audio> | |
</body> | |
<script src="https://d3js.org/d3.v4.js"></script> | |
<script> | |
var svg = d3.select("svg"), | |
width = svg.node().getBoundingClientRect().width, | |
height = svg.node().getBoundingClientRect().height; | |
var lineColors = { | |
'1': '#E00034', | |
'3': '#E00034', | |
'A': '#0039A6', | |
'E': '#0039A6', | |
'4': '#009B3A', | |
'R': '#FECB00', | |
'D': '#FF6319', | |
'F': '#FF6319', | |
'M': '#FF6319', | |
'7': '#B634BB', | |
'L': '#939598', | |
'J': '#955214', | |
'transfer': '#4D4D4D', | |
'ground': '#F2EAD6', | |
'water': '#A2CAEA', | |
'park': '#A8D7B9' | |
}; | |
var simulation = d3.forceSimulation() | |
.force("link", d3.forceLink() | |
.id(function(d) { return d.id; }) | |
.distance(function(d) { return d.distance; }) | |
) | |
.force("center", d3.forceCenter(width / 2, height / 2)); | |
d3.queue() | |
.defer(d3.tsv, "stations.tsv") | |
.defer(d3.tsv, "transfers.tsv") | |
.await(function(error, stations, transfers) { | |
if (error) throw error; | |
if(innerWidth <= 666) projectStations(stations); | |
stations.forEach(parseStation); | |
var links = getLinks(stations, transfers); | |
var link = svg.append("g") | |
.attr("class", "links") | |
.selectAll("line") | |
.data(links) | |
.enter().append("line") | |
.style('stroke', function(d) { | |
return lineColors[d.type]; | |
}) | |
.style('stroke-width', function(d) { | |
return d.type === 'transfer' ? 4 : 10; | |
}); | |
var node = svg.append("g") | |
.attr("class", "nodes") | |
.selectAll("circle") | |
.data(stations) | |
.enter().append("g") | |
.call(d3.drag() | |
.on("start", dragstarted) | |
.on("drag", dragged) | |
.on("end", dragended)); | |
node.append("circle") | |
.attr("r", 4) | |
node.append("text") | |
.attr("dx", "0.5em") | |
.attr("dy", "-0.5em") | |
.text(function(d) { return d.name; }); | |
simulation | |
.nodes(stations) | |
.on("tick", ticked); | |
simulation.force("link") | |
.links(links); | |
// d3.select('button.repel') | |
// .on('mouseenter', function() { | |
// simulation | |
// .alphaTarget(0.3).restart() | |
// .force("charge", d3.forceManyBody()); | |
// }) | |
// .on('mouseleave', function() { | |
// simulation | |
// .alphaTarget(0) | |
// .force("charge", null); | |
// }); | |
d3.select('button.music') | |
.on('click', function() { | |
if(!d3.select(this).classed('active')) { | |
d3.select(this).classed('active', true); | |
d3.select('audio').node().play(); | |
} else { | |
d3.select(this).classed('active', false); | |
d3.select('audio').node().pause(); | |
} | |
}); | |
d3.select('button.reset') | |
.on('mousedown', startReset) | |
.on('touchstart', startReset) | |
.on('mouseup', stopReset) | |
.on('touchend', stopReset); | |
function startReset() { | |
d3.select(this).classed('active', true); | |
simulation | |
.alphaTarget(0.3).restart() | |
.force("resetX", d3.forceX(function(d) { | |
return d.x0; | |
})) | |
.force("resetY", d3.forceY(function(d) { | |
return d.y0; | |
})); | |
} | |
function stopReset() { | |
d3.select(this).classed('active', false); | |
simulation | |
.alphaTarget(0) | |
.force("resetX", null) | |
.force("resetY", null); | |
} | |
d3.select('button.fork') | |
.on("click", function() { | |
if(!d3.select(this).classed('active')) { | |
// enable fork | |
d3.select(this).classed('active', true); | |
simulation | |
.force("fork", forceFork(.1, (window.innerWidth > 666 ? 100 : 40), d3.select('body'))) | |
.alphaDecay(0).restart(); | |
} else { | |
// disable fork | |
d3.select(this).classed('active', false); | |
d3.select('img.fork').remove(); | |
simulation | |
.force("fork", null) | |
.alphaDecay(0.0228).restart(); | |
} | |
}) | |
.each(function() { | |
this.click(); | |
}); | |
// ticked(); | |
// simulation.stop(); | |
function ticked() { | |
link | |
.attr("x1", function(d) { return d.source.x; }) | |
.attr("y1", function(d) { return d.source.y; }) | |
.attr("x2", function(d) { return d.target.x; }) | |
.attr("y2", function(d) { return d.target.y; }); | |
node | |
.attr("transform", function(d) { return "translate("+d.x+","+d.y+")"; }) | |
.classed("forking", function(d) { return d.forking; }); | |
} | |
}); | |
// fit stations to screen | |
function projectStations(stations) { | |
var xExtent = d3.extent(stations.map(function(d) { return +d.x; })); | |
var x = d3.scaleLinear() | |
.domain(xExtent) | |
.range([0,window.innerWidth]); | |
var yExtent = d3.extent(stations.map(function(d) { return +d.y; })); | |
var y = d3.scaleLinear() | |
.domain(yExtent) | |
.range([0,window.innerHeight]); | |
// "contain" behavior, in background-position terms: | |
// scale both dimensions by the more-constrained dimension | |
var t = (x(1) - x(0) > y(1) - y(0)) ? y : x; | |
stations.forEach(function(station) { | |
station.x = t(+station.x); | |
station.y = t(+station.y); | |
}) | |
} | |
function parseStation(station) { | |
station.order = +station.order; | |
station.x = +station.x; | |
station.y = +station.y; | |
station.x0 = station.x; | |
station.y0 = station.y; | |
station.id = station.line + ' ' + station.name; | |
} | |
function getLinks(stations, transfers) { | |
var links = []; | |
stations.forEach(function(station) { | |
var nextStation = stations.filter(function(st) { | |
return st.line == station.line && st.order == station.order + 1; | |
}); | |
if(nextStation.length) { | |
links.push({ | |
'source': station.id, | |
'target': nextStation[0].id, | |
'distance': distance(station, nextStation[0]), | |
'type': station.line | |
}) | |
} else { | |
return; | |
} | |
}); | |
transfers.forEach(function(transfer) { | |
var source = stations.filter(function(st) { | |
return st.id == transfer.fromLine + ' ' + transfer.fromStation; | |
})[0]; | |
var target = stations.filter(function(st) { | |
return st.id == transfer.toLine + ' ' + transfer.toStation; | |
})[0]; | |
links.push({ | |
'source': source.id, | |
'target': target.id, | |
'distance': distance(source, target), | |
'type': 'transfer' | |
}); | |
}); | |
return links; | |
} | |
function distance(a,b) { | |
return Math.sqrt(Math.pow(b.x - a.x, 2) + Math.pow(b.y - a.y, 2)); | |
} | |
function difference(a,b) { | |
return d3.zip(a,b).map(function(x) { | |
return x.reduce(function(a, b) { | |
return a - b; | |
}); | |
}); | |
} | |
function dragstarted(d) { | |
d.fx = d.x; | |
d.fy = d.y; | |
if (!d3.event.active) simulation.alphaTarget(0.3).restart() | |
// simulation.fix(d); | |
} | |
function dragged(d) { | |
d.fx = d3.event.x; | |
d.fy = d3.event.y; | |
// simulation.fix(d, d3.event.x, d3.event.y); | |
} | |
function dragended(d) { | |
d.fx = undefined; | |
d.fy = undefined; | |
if (!d3.event.active) simulation.alphaTarget(0); | |
// simulation.unfix(d); | |
} | |
function forceFork(_, __, ___) { | |
var active = false, | |
clockwise = 1, | |
nodes, | |
fork, | |
forkAngle = 0, | |
strength = _ || 1, | |
radius = __ || 100, | |
container = ___ || d3.select('body'), | |
center = {x: 0, y: 0}, | |
moves = []; | |
function force(alpha) { | |
if(!active) return; | |
forkAngle += clockwise * strength; | |
fork.style('transform', 'translate(-50%,0%) rotate(' + forkAngle + 'rad) scale(0.85)'); | |
if(moves.length >= 2) { | |
var x0 = moves[0], | |
x1 = moves[moves.length-1], | |
dx = difference(x1,x0); | |
moves = [x1]; | |
} | |
for (var i = 0, n = nodes.length, node, k = alpha; i < n; ++i) { | |
node = nodes[i]; | |
node.forking = distance(node, center) < radius; | |
if(node.forking) { | |
node.vx += clockwise * strength * -(node.y - center.y); | |
node.vy += clockwise * strength * (node.x - center.x); | |
if(dx) { | |
node.vx += dx[0]; | |
node.vy += dx[1]; | |
} | |
} | |
} | |
} | |
force.initialize = function(_) { | |
nodes = _; | |
fork = container.append('img') | |
.classed('fork', true) | |
.attr('src', 'fork.png?v=2') | |
.attr('width', radius * 2.5); | |
container | |
.on('mousedown.spin', start) | |
.on('touchstart.spin', start) | |
.on('mouseup.spin', stop) | |
.on('touchend.spin', stop) | |
.on('mousemove.position', position) | |
.on('touchstart.position', position) | |
.on('touchmove.position', position); | |
function start() { | |
clockwise = d3.event.shiftKey ? -1 : 1; | |
active = true; | |
} | |
function stop() { | |
active = false; | |
fork.style('transform', 'translate(-50%,0%) rotate(' + forkAngle + 'rad) scale(1)'); | |
moves = []; | |
} | |
function position() { | |
var pt = d3.mouse(this); | |
if(active) moves.push(pt); | |
fork | |
.style('left', pt[0] + 'px') | |
.style('top', pt[1] + 'px'); | |
center = { | |
x: pt[0], | |
y: pt[1] | |
}; | |
} | |
} | |
return force; | |
} | |
</script> |
line | order | name | x | y | |
---|---|---|---|---|---|
1 | 1 | Van Cordlandt Park 242 St | 202 | 132 | |
1 | 2 | 238 St | 202 | 160 | |
1 | 3 | 231 St | 202 | 189 | |
1 | 4 | 225 St Marble Hill | 202 | 218 | |
1 | 5 | 215 St | 156 | 276 | |
1 | 6 | 207 St | 156 | 299 | |
1 | 7 | Dyckman St | 146 | 333 | |
1 | 8 | 191 St | 146 | 356 | |
1 | 9 | 181 St | 146 | 387 | |
1 | 10 | Washington Heights 168 St | 146 | 462 | |
1 | 11 | 157 St | 146 | 505 | |
1 | 12 | 145 St | 146 | 584 | |
1 | 13 | 137 City College | 146 | 623 | |
1 | 14 | 125 St | 146 | 655 | |
1 | 15 | 116 St Columbia University | 146 | 697 | |
1 | 16 | 110 St Cathedral Parkway | 146 | 730 | |
1 | 17 | 103 St | 156 | 775 | |
1 | 18 | 96 St | 156 | 815 | |
1 | 19 | 86 St | 156 | 856 | |
1 | 20 | 79 St | 156 | 905 | |
1 | 21 | 72 St | 156 | 953 | |
1 | 22 | 66 St | 182 | 1012 | |
1 | 23 | 59 St Columbus Circle | 227 | 1057 | |
1 | 24 | 50 St | 321 | 1151 | |
1 | 25 | Times Sq 42 St | 334 | 1211 | |
1 | 26 | 34 St Penn Station | 334 | 1292 | |
1 | 27 | 28 St | 334 | 1321 | |
1 | 28 | 23 St | 334 | 1351 | |
1 | 29 | 18 St | 334 | 1380 | |
1 | 30 | 14 St | 334 | 1409 | |
1 | 31 | Christopher St Sheridan Sq | 334 | 1495 | |
1 | 32 | Houston St | 334 | 1550 | |
1 | 33 | Canal St | 334 | 1614 | |
1 | 34 | Franklin St | 334 | 1652 | |
1 | 35 | Chambers St West Broadway | 355 | 1700 | |
1 | 36 | Cortlandt St | 404 | 1785 | |
1 | 37 | Rector St | 429 | 1827 | |
1 | 38 | South Ferry | 537 | 1889 | |
3 | 1 | Harlem 148 St | 389 | 574 | |
3 | 2 | 145 St | 454 | 589 | |
3 | 3 | 135 St | 454 | 630 | |
3 | 4 | 125 St | 454 | 670 | |
3 | 5 | 116 St | 454 | 701 | |
3 | 6 | 110 St Central Park North | 454 | 722 | |
3 | 7 | 96 St | 166 | 815 | |
3 | 8 | 72 St | 166 | 953 | |
3 | 9 | Times Sq 42 St | 344 | 1211 | |
3 | 10 | 34 St Penn Station | 344 | 1292 | |
3 | 11 | 14 St | 344 | 1409 | |
3 | 12 | Chambers St West Broadway | 364 | 1695 | |
3 | 13 | Park Pl | 463 | 1713 | |
3 | 14 | Fulton St | 656 | 1758 | |
3 | 15 | Wall St | 656 | 1824 | |
A | 1 | Inwood 207 St | 129 | 300 | |
A | 2 | Dyckman St | 111 | 318 | |
A | 3 | 190 St | 107 | 358 | |
A | 4 | 181 St | 107 | 386 | |
A | 5 | 175 St GW Bridge Bus Terminal | 128 | 416 | |
A | 6 | Washington Heights 168 St | 168 | 456 | |
A | 7 | 145 St | 241 | 586 | |
A | 8 | 125 St | 241 | 673 | |
A | 9 | 59 St Columbus Circle | 251 | 1043 | |
A | 10 | 42 St Port Authority Bus Terminal | 251 | 1211 | |
A | 11 | 34 St Penn Station | 251 | 1292 | |
A | 12 | 14 St | 251 | 1398 | |
A | 13 | West 4 St | 433 | 1525 | |
A | 14 | Canal St | 433 | 1620 | |
A | 15 | Chambers St Church St | 433 | 1682 | |
A | 16 | Fulton St | 637 | 1749 | |
E | 1 | Court Sq | 785 | 1137 | |
E | 2 | Lexington Av | 626 | 1137 | |
E | 3 | 5 Av | 497 | 1137 | |
E | 4 | 7 Av | 381 | 1137 | |
E | 5 | 50 St | 261 | 1153 | |
E | 6 | 42 St Port Authority Bus Terminal | 261 | 1211 | |
4 | 1 | 125 St | 605 | 670 | |
4 | 2 | 86 St | 605 | 855 | |
4 | 3 | 59 St Lexington Ave | 605 | 1075 | |
4 | 4 | 42 St Grand Central | 605 | 1231 | |
4 | 5 | 14 St Union Sq | 585 | 1388 | |
4 | 6 | Brooklyn Bridge Chambers St | 585 | 1678 | |
4 | 7 | Fulton St | 557 | 1759 | |
4 | 8 | Wall St | 557 | 1821 | |
4 | 9 | Bowling Green | 557 | 1854 | |
R | 1 | 59 St Lexington Ave | 626 | 1065 | |
R | 2 | Midtown 57 St | 354 | 1089 | |
R | 3 | 49 St | 354 | 1159 | |
R | 4 | Times Sq 42 St | 363 | 1200 | |
R | 5 | 34 St Herald Sq | 435 | 1272 | |
R | 6 | 28 St | 488 | 1325 | |
R | 7 | 23 St | 514 | 1351 | |
R | 8 | 14 St Union Sq | 535 | 1409 | |
R | 9 | 8 St NYU | 535 | 1453 | |
R | 10 | Prince St | 535 | 1560 | |
R | 11 | Canal St | 545 | 1632 | |
R | 12 | City Hall | 545 | 1682 | |
R | 13 | Cortlandt St | 507 | 1766 | |
R | 14 | Rector St | 507 | 1822 | |
R | 15 | Whitehall St | 589 | 1876 | |
D | 1 | 155 St 8 Av | 284 | 515 | |
D | 2 | 145 St | 231 | 586 | |
D | 3 | 125 St | 231 | 673 | |
D | 4 | 59 St Columbus Circle | 241 | 1043 | |
D | 5 | 7 Av | 381 | 1127 | |
D | 6 | 47-50 St Rockefeller Center | 453 | 1157 | |
D | 7 | 42 St Bryant Park | 453 | 1201 | |
D | 8 | 34 St Herald Sq | 453 | 1292 | |
D | 9 | West 4 St | 453 | 1525 | |
D | 10 | Broadway-Lafayette St | 606 | 1547 | |
D | 11 | Grand St | 692 | 1608 | |
F | 1 | Roosevelt Island | 719 | 1023 | |
F | 2 | Lexington Av | 626 | 1023 | |
F | 3 | 57 St | 463 | 1089 | |
F | 4 | 47-50 St Rockefeller Center | 463 | 1157 | |
M | 1 | Court Sq | 785 | 1127 | |
M | 2 | Lexington Av | 626 | 1127 | |
M | 3 | 5 Av | 497 | 1127 | |
M | 4 | 47-50 St Rockefeller Center | 463 | 1157 | |
7 | 1 | 42 St Grand Central | 588 | 1221 | |
7 | 2 | 5 Av | 498 | 1221 | |
7 | 3 | Times Sq 42 St | 322 | 1221 | |
7 | 4 | 34 St Hudson Yards | 140 | 1286 | |
L | 1 | 1 Av | 709 | 1398 | |
L | 2 | 3 Av | 651 | 1398 | |
L | 3 | 14 St Union Sq | 565 | 1398 | |
L | 4 | 6 Av | 420 | 1398 | |
L | 5 | 14 St 8 Av | 271 | 1398 | |
J | 1 | Essex St | 755 | 1581 | |
J | 2 | Bowery | 662 | 1581 | |
J | 3 | Canal St | 638 | 1632 | |
J | 4 | Brooklyn Bridge Chambers St | 638 | 1678 | |
J | 5 | Fulton St | 622 | 1759 | |
J | 6 | Broad St | 622 | 1835 |
fromLine | fromStation | toLine | toStation | |
---|---|---|---|---|
1 | Washington Heights 168 St | A | Washington Heights 168 St | |
1 | 59 St Columbus Circle | D | 59 St Columbus Circle | |
1 | Times Sq 42 St | A | 42 St Port Authority Bus Terminal | |
1 | South Ferry | R | Whitehall St | |
4 | 59 St Lexington Ave | R | 59 St Lexington Ave | |
R | Times Sq 42 St | 1 | Times Sq 42 St | |
R | 34 St Herald Sq | D | 34 St Herald Sq | |
D | 145 St | A | 145 St | |
D | 125 St | A | 125 St | |
D | 59 St Columbus Circle | A | 59 St Columbus Circle | |
7 | 42 St Grand Central | 4 | 42 St Grand Central | |
7 | 5 Av | D | 42 St Bryant Park | |
7 | Times Sq 42 St | 1 | Times Sq 42 St | |
A | West 4 St | D | West 4 St | |
L | 14 St Union Sq | R | 14 St Union Sq | |
L | 14 St Union Sq | 4 | 14 St Union Sq | |
L | 6 Av | 1 | 14 St | |
L | 14 St 8 Av | A | 14 St | |
J | Canal St | R | Canal St | |
J | Brooklyn Bridge Chambers St | 4 | Brooklyn Bridge Chambers St | |
J | Fulton St | A | Fulton St | |
J | Fulton St | 4 | Fulton St | |
3 | 96 St | 1 | 96 St | |
3 | 72 St | 1 | 72 St | |
3 | Times Sq 42 St | 1 | Times Sq 42 St | |
3 | 34 St Penn Station | 1 | 34 St Penn Station | |
3 | 14 St | 1 | 14 St | |
3 | Chambers St West Broadway | 1 | Chambers St West Broadway | |
3 | Park Pl | A | Chambers St Church St | |
3 | Fulton St | A | Fulton St | |
F | Lexington Av | R | 59 St Lexington Ave | |
F | 47-50 St Rockefeller Center | D | 47-50 St Rockefeller Center | |
M | 47-50 St Rockefeller Center | F | 47-50 St Rockefeller Center | |
E | Court Sq | M | Court Sq | |
E | Lexington Av | M | Lexington Av | |
E | 5 Av | M | 5 Av | |
E | 7 Av | D | 7 Av | |
E | 42 St Port Authority Bus Terminal | A | 42 St Port Authority Bus Terminal |