A graphical toolkit for visualization
Protovis
Overview
Examples
Documentation
Download
Index
« Previous / Next »

Marey’s Trains

View full screen.

This example recreates the CalTrain timetable in the style of E. J. Marey’s graphical schedule. Stations are separated vertically in proportion to geography; thus, the slope of the line reflects the actual speed of the train: the steeper the line, the faster the train. This display also reveals when and where limited service trains are passed by baby bullets.

Next: Stemplots

Source

<html>
  <head>
    <title>CalTrain Timetable</title>
    <link type="text/css" rel="stylesheet" href="ex.css?3.2"/>
    <script type="text/javascript" src="../protovis-r3.2.js"></script>
    <script type="text/javascript" src="caltrain.js"></script>
    <style type="text/css">

#fig {
  height: 680px;
  width: 1400px;
}

#controls {
  padding-left: 100px;
}

    </style>
  </head>
  <body><div id="center"><div id="fig">
    <div id="controls" >
      <b>Speed:</b>
      <input type="checkbox" id="normal" checked onchange="speedn = this.checked; vis.render();"
      ><label for="normal" style="color:rgb(34,34,34);">Normal</label>
      <input type="checkbox" id="limited" checked onchange="speedl = this.checked; vis.render();"
      ><label for="limited" style="color:rgb(183,116,9);">Limited</label>
      <input type="checkbox" id="bullet" checked onchange="speedb = this.checked; vis.render();"
      ><label for="bullet" style="color:rgb(192,62,29);">Bullet</label>
      <b style="margin-left:6em;">Direction:</b>
      <input type="checkbox" id="northbound" checked onchange="dirn = this.checked; vis.render();"
      ><label for="northbound">Northbound</label>
      <input type="checkbox" id="southbound" checked onchange="dirs = this.checked; vis.render();"
      ><label for="southbound">Southbound</label>
      <b style="margin-left:6em;">Days:</b>
      <input id="weekdays" name="daySelect" value="Wk" type="radio" checked onchange="oper = this.value; vis.render();"
      ><label for="weekdays">Weekdays</label>
      <input name="daySelect" id="saturday" value="Sa" type="radio" onchange="oper = this.value; vis.render();"
      ><label for="saturday">Saturday</label>
      <input name="daySelect" id="sunday" value="Su" type="radio" onchange="oper = this.value; vis.render();"
      ><label for="sunday">Sunday</label>
    </div>
    <script type="text/javascript+protovis">

// Start with showing all trains on weekdays
var dirn = true,
    dirs = true,
    oper = "Wk",
    speedn = true,
    speedl = true,
    speedb = true;

// Flatten the data so that we have an array of (Train x Station x Time)
northbound = pv.flatten(northbound)
    .key("train")
    .key("station", function(i) stationsNS[stationsNS.length - i - 1].name)
    .key("time")
    .array();

southbound = pv.flatten(southbound)
    .key("train")
    .key("station", function(i) stationsNS[i].name)
    .key("time")
    .array();

// Label the trains with their directions of travel
northbound.forEach(function(stop) stop.bound = "N");
southbound.forEach(function(stop) stop.bound = "S");

// Concatinate the northbound and southbound trains to make a list of all trains
var allTrains = northbound.concat(southbound).filter(function(stop) stop.time.length > 1);

// Parse the stop time and do some extra clean up
allTrains.forEach(function(stop) {
  // parse type
  stop.type = stop.train.charAt(3);
  stop.train = stop.train.substr(0,3);

  // check to see if the stop is an interconnect
  var time = stop.time.charAt(0) == "i" ? stop.time.substr(1) : stop.time;
  if (time.length == 6) {
    var h = parseInt(time.substr(0, 1), 10);
    var m = parseInt(time.substr(2, 2), 10);
    var pm = (time.substr(4, 2) == "pm");
  } else {
    var h = parseInt(time.substr(0, 2), 10);
    var m = parseInt(time.substr(3, 2), 10); // parseInt("09") == 0 because it assumes octal
    var pm = (time.substr(5, 2) == "pm");
  }

  if (h == 12) pm = !pm // for 12 we have to swap am and pm because time is retarded
  time = h*60 + m + (pm?720:0); // calculate the number of minutes from midnight
  stop.mins = time + ((time < 180)?1440:0); // make the day switch at 3am instead of midnight
});

// Nest the trains-stops by train as we will draw one line per train
var trains = pv.nest(allTrains)
    .key(function(d) d.train)
    .entries();

// Initialize the variables
var w = 1200,
    h = 600,
    minHour = 4,
    maxHour = 26,
    showMin = 4 * 60,
    showMax = 26 * 60;

// Make the scale functions
var x = pv.Scale.linear(showMin, showMax).range(0, w),
    s2d = pv.Scale.ordinal(stationsNS, function(s) s.name).range(stationsNS, function(s) s.dist),
    y = pv.Scale.linear(stationsNS, function(s) s.dist).range(h, 0).by(s2d);

function hourText(d) {
  var h = d/60 % 24;
  return (h==0?'MIDNIGHT':(h==12?'NOON':(h<12?h:h-12)));
}

// The root panel, with padding for labels
var vis = new pv.Panel()
    .width(w)
    .height(h)
    .left(100)
    .right(100)
    .bottom(30)
    .top(30);

// Add hour lines
var hour = vis.add(pv.Rule)
    .data(function() pv.range(Math.ceil(showMin / 60) * 60, showMax + 1, 60))
    .left(x)
    .strokeStyle("#eee");

hour.anchor("top").add(pv.Label)
    .textStyle(function(h) (h / 60 % 24) % 12 ? "#999" : "#000")
    .text(hourText);

hour.anchor("bottom").add(pv.Label)
    .textStyle(function(h) (h / 60 % 24) % 12 ? "#999" : "#000")
    .text(hourText);

// Add station lines
var station = vis.add(pv.Rule)
    .data(stationsNS)
    .bottom(function(s) y(s.name))
    .strokeStyle(function(s) s.bullet ? "#bbb" : "#eee");

station.anchor("left").add(pv.Label)
    .textStyle(function(s) s.bullet ? "#000" : "#888")
    .text(function(s) s.name);

station.anchor("right").add(pv.Label)
    .textStyle(function(s) s.bullet ? "#000" : "#888")
    .text(function(s) s.name);

// A panel for each train
var panel = vis.add(pv.Panel)
    .data(trains)
    .visible(function(train) {
          var t = train.values[0];
          if (!speedb && t.type == "B") return false;
          if (!speedl && t.type == "L") return false;
          if (!speedn && /N|W|S/.test(t.type)) return false;
          if (!dirn && t.bound == "N") return false;
          if (!dirs && t.bound == "S") return false;
          switch (oper) {
            case "Wk": return /N|L|B/.test(t.type);
            case "Su": return /W|S/.test(t.type);
            default: return t.type == "W";
          }
        });

// A line curve (with dots) for each station
var line = panel.add(pv.Line)
    .data(function(d) d.values)
    .left(function(stop) x(stop.mins))
    .bottom(function(stop) y(stop.station))
    .strokeStyle(function(stop) types[stop.type])
    .lineWidth(1);

// Ticks that indicate the stops
line.add(pv.Dot)
    .radius(2.5)
    .lineWidth(.75)
    .strokeStyle("white")
    .fillStyle(function(stop) types[stop.type])
    .title(function(stop) stop.type + stop.train + "-" + stop.bound + "B: "
        + stop.station + " at " + stop.time);

vis.render();

    </script>
  </div></div></body>
</html>

Data

Due to size, the data file is omitted from this example. See caltrain.js.
Copyright 2010 Stanford Visualization Group