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

Stemplots

View full screen.

Stemplots use the clever arrangement of text to convey the distribution of data: an alternative to a histogram. Although originally designed for monospaced text displays, such as typewriters and computer terminals, they still find use today for train schedules and other applications that seek quick scanning and efficient use of space.

Here we show CalTrain weekday arrival times. The minutes of each northbound and southbound arrival are placed to the left and right, respectively, of their corresponding hour. The time is colored to indicate whether the train service is normal, limited or bullet. Peak times of morning and evening rush hour can be seen by increased activity.

Next: Merge Sort

Source

<html>
  <head>
    <title>Stemplot</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 {
  width: 240px;
  height: 460px;
}

#station {
  padding-bottom: 10px;
  text-align: center;
}

    </style>
  </head>
  <body><div id="center"><div id="fig">
    <div id="station">
      <b>Station:</b>
      <select id="stationSelect"
        onchange="station = this.value; updateTrains(); vis.render();"
        onkeyup="station = this.value; updateTrains(); vis.render();">
      </select>
    </div>
    <script type="text/javascript+protovis">

var station = "Palo Alto";

var stationSelect = document.getElementById('stationSelect');
stationsNS.forEach(function(s) {
  var optn = document.createElement("OPTION");
  optn.text = s.name;
  optn.value = s.name;
  optn.selected = (s.name == station);
  stationSelect.options.add(optn);
});

// 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");

// Concatenate 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
  if (stop.time.charAt(0) == "i") {
    var time = stop.time.substr(1);
    stop.conn = true;
  } else {
    var time = stop.time;
    stop.conn = false;
  }

  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

  h = h + (pm?12:0); // calculate the number of minutes from midnight
  stop.hour = h + ((h < 3)?24:0); // make the day switch at 3am instead of midnight
  stop.time = m < 10 ? "0" + m : m;
})

var trains;

function updateTrains() {
  var stationTrains = allTrains.filter(function(t) {
    if (t.station != station) return false;
    return (t.type=="N") || (t.type=="L") || (t.type=="B");
  });

  // Nest the trains-stops by train as we will draw one line per train
  trains = pv.nest(stationTrains)
      .key(function(d) d.hour)
      .key(function(d) d.bound)
      .sortValues(function(a, b) a.time - b.time)
      .entries();
}

updateTrains();

// Initialize the variables
var w = 240,
    h = 420,
    boxSize = 20,
    midSize = 30,
    minHour = 2,
    maxHour = 24;

// Make the scale functions
var y = pv.Scale.linear(minHour, maxHour).range(0, h);

// The root panel, with padding for labels
var vis = new pv.Panel()
    .width(w)
    .height(h);

// Add the vertical rules
vis.add(pv.Rule)
    .data([-1, 1])
    .strokeStyle("#ccc")
    .left(function(m) w / 2 + m * midSize / 2.4)
    .top(0)
    .bottom(0);

// Add the destination labels
vis.add(pv.Label)
    .data(['Northbound', 'Southbound'])
    .left(function(d) d == 'Southbound' ? (w / 2 + midSize / 2) : null)
    .right(function(d) d == 'Northbound' ? (w / 2 + midSize / 2) : null)
    .top(boxSize)
    .font("13px sans-serif")
    .textAlign(function(d) d == 'Southbound' ? 'left' : 'right');

// Add a panel for each hour
var hour = vis.add(pv.Panel)
    .data(function() trains)
    .top(function(d) y(d.key));

// Add the center hour label for each hour panel
hour.add(pv.Label)
    .left(w / 2)
    .textAlign("center")
    .font("bold 10px sans-serif")
    .text(function(d) {
       var h = d.key;
       return (h > 12 ? (h > 24 ? h - 24 : h - 12) : h) + ((h > 11 && h < 24) ? 'p' : 'a');
    });

// Add the left and right panels and populate them with the corresponding times
hour.add(pv.Panel)
    .data(function(d) d.values)
    .left(function(d) d.key == 'S' ? (w / 2 + midSize / 2) : 0)
    .right(function(d) d.key == 'N' ? (w / 2 + midSize / 2) : 0)
  .add(pv.Label)
    .data(function(d) d.values)
    .left(function(d) d.bound == 'S' ? (boxSize * this.index) : null)
    .right(function(d) d.bound == 'N' ? (boxSize * this.index) : null)
    .textAlign(function(d) d.bound == 'S' ? "left" : "right")
    .textStyle(function(d) types[d.type])
    .text(function(d) d.time);

// render everything
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