block by tophtucker 81c467db46a37dfaf75cacb70552b818

MTA spaghetti

Full Screen

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

index.html

<!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="//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>

stations.tsv

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

transfers.tsv

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