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.
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.
| <!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 |