A recreation of E.J. Marey’s graphical train schedule. Stations are separated vertically in proportion to geography; thus, the slope of the line reflects the speed of the train: the steeper the line, the faster the train. This display also reveals when and where limited service trains (in orange) are passed by baby bullets (in red). This type of plot is sometimes called a “stringline chart”.
See also the earlier Protovis version by Vadim Ogievetsky, and another variation where color encodes speed.
<!DOCTYPE html>
<meta charset="utf-8">
<style>
svg {
font: 10px sans-serif;
}
.axis path {
display: none;
}
.axis line {
stroke: #000;
shape-rendering: crispEdges;
}
.station line {
stroke: #ddd;
stroke-dasharray: 1,1;
shape-rendering: crispEdges;
}
.station text {
text-anchor: end;
}
.train path {
fill: none;
stroke-width: 1.5px;
}
.train circle {
stroke: #fff;
}
.train .N path { stroke: rgb(34,34,34); }
.train .N circle { fill: rgb(34,34,34); }
.train .L path { stroke: rgb(183,116,9); }
.train .L circle { fill: rgb(183,116,9); }
.train .B path { stroke: rgb(192,62,29); }
.train .B circle { fill: rgb(192,62,29); }
</style>
<body>
<script src="//d3js.org/d3.v3.min.js"></script>
<script>
var stations = []; // lazily loaded
var formatTime = d3.time.format("%I:%M%p");
var margin = {top: 20, right: 30, bottom: 20, left: 100},
width = 960 - margin.left - margin.right,
height = 500 - margin.top - margin.bottom;
var x = d3.time.scale()
.domain([parseTime("5:30AM"), parseTime("11:30AM")])
.range([0, width]);
var y = d3.scale.linear()
.range([0, height]);
var xAxis = d3.svg.axis()
.scale(x)
.ticks(8)
.tickFormat(formatTime);
var line = d3.svg.line()
.x(function(d) { return x(d.time); })
.y(function(d) { return y(d.station.distance); });
var svg = d3.select("body").append("svg")
.attr("width", width + margin.left + margin.right)
.attr("height", height + margin.top + margin.bottom)
.append("g")
.attr("transform", "translate(" + margin.left + "," + margin.top + ")");
svg.append("defs").append("clipPath")
.attr("id", "clip")
.append("rect")
.attr("y", -margin.top)
.attr("width", width)
.attr("height", height + margin.top + margin.bottom);
d3.tsv("schedule.tsv", type, function(error, trains) {
y.domain(d3.extent(stations, function(d) { return d.distance; }));
var station = svg.append("g")
.attr("class", "station")
.selectAll("g")
.data(stations)
.enter().append("g")
.attr("transform", function(d) { return "translate(0," + y(d.distance) + ")"; });
station.append("text")
.attr("x", -6)
.attr("dy", ".35em")
.text(function(d) { return d.name; });
station.append("line")
.attr("x2", width);
svg.append("g")
.attr("class", "x top axis")
.call(xAxis.orient("top"));
svg.append("g")
.attr("class", "x bottom axis")
.attr("transform", "translate(0," + height + ")")
.call(xAxis.orient("bottom"));
var train = svg.append("g")
.attr("class", "train")
.attr("clip-path", "url(#clip)")
.selectAll("g")
.data(trains.filter(function(d) { return /[NLB]/.test(d.type); }))
.enter().append("g")
.attr("class", function(d) { return d.type; });
train.append("path")
.attr("d", function(d) { return line(d.stops); });
train.selectAll("circle")
.data(function(d) { return d.stops; })
.enter().append("circle")
.attr("transform", function(d) { return "translate(" + x(d.time) + "," + y(d.station.distance) + ")"; })
.attr("r", 2);
});
function type(d, i) {
// Extract the stations from the "stop|*" columns.
if (!i) for (var k in d) {
if (/^stop\|/.test(k)) {
var p = k.split("|");
stations.push({
key: k,
name: p[1],
distance: +p[2],
zone: +p[3]
});
}
}
return {
number: d.number,
type: d.type,
direction: d.direction,
stops: stations
.map(function(s) { return {station: s, time: parseTime(d[s.key])}; })
.filter(function(s) { return s.time != null; })
};
}
function parseTime(s) {
var t = formatTime.parse(s);
if (t != null && t.getHours() < 3) t.setDate(t.getDate() + 1);
return t;
}
</script>