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

Minard’s Napoleon

View full screen.

Charles Minard’s depiction of Napleon’s disastrous march to Moscow, in the winter of 1812, is heralded by Edward Tufte as "probably the best statistical graphic ever drawn." Our adaption of this famous flow map includes interactivity using the Google Maps API.

Next: Oakland Crimespotting

Source

<html>
  <head>
    <title>Minard&rsquo;s Napoleon</title>
    <link type="text/css" rel="stylesheet" href="ex.css?3.2"/>
    <script src="../protovis-r3.2.js" type="text/javascript"></script>
    <script src="http://maps.google.com/maps?file=api&amp;v=2&amp;sensor=false&amp;key=ABQIAAAAYZ9eMYFYusxZt-1RKXLI7RRHhAsaxs5VjhFCDs4CxQ-G2qh_dhSstmN5IR0MH0kI1flLEIY43Z2-gw" type="text/javascript"></script>
    <script src="napoleon.js" type="text/javascript"></script>
    <style type="text/css">

#fig, #map, #temp {
  width: 895px;
  height: 455px;
}

#map {
  height: 380px;
  border: solid 1px #999;
}

#map .canvas, #temp svg {
  position: absolute;
}

#temp {
  height: 75px;
}

    </style>
    <script type="text/javascript+protovis">

/* Nest data by direction, then group. */
var army = pv.nest(napoleon.army)
    .key(function(d) d.dir).sortKeys()
    .key(function(d) d.group)
    .entries();

/* Define a new GOverlay class to contain the visualization. */
function Canvas() {}
Canvas.prototype = pv.extend(GOverlay);

/* Add our canvas to the map pane when initialized. */
Canvas.prototype.initialize = function(map) {
  this.map = map;
  this.canvas = document.createElement("div");
  this.canvas.setAttribute("class", "canvas");
  map.getPane(G_MAP_MAP_PANE).parentNode.appendChild(this.canvas);
};

/* Redraw the visualizations when the map is moved. */
Canvas.prototype.redraw = function(force) {
  var m = this.map;
  napoleon.temp.forEach(function(d) {
      d.x = m.fromLatLngToContainerPixel(new GLatLng(55, d.lon)).x;
    });

  /* Temperature visualization. */
  new pv.Panel()
      .canvas("temp")
    .add(pv.Rule)
      .data([0, -10, -20, -30])
      .bottom(function(d) 1.5 * d + 60 - 0.5)
      .left(0)
      .right(30)
      .strokeStyle("#ccc")
      .lineWidth(1)
    .anchor("right").add(pv.Label)
      .text(function(d) d + "\u00b0")
      .textMargin(6)
      .textBaseline("middle")
    .root.add(pv.Line)
      .data(napoleon.temp)
      .strokeStyle("#000")
      .lineWidth(2)
      .left(function(d) d.x)
      .bottom(function(d) 1.5 * d.temp + 60 - 0.5)
    .add(pv.Label)
      .textBaseline("top")
      .textMargin(6)
      .textAlign("center")
      .text(function(d) d.date.substr(0, 6))
    .add(pv.Rule)
      .left(function() this.proto.left() + .5)
      .lineWidth(.5)
      .top(0)
    .root.render();

  /* Only update troop counts when the map is zoomed. */
  if (!force) return;
  var c = this.canvas, r = 50;

  /* Convert latitude and longitude to pixel locations. */
  napoleon.army.forEach(function(d) {
      var p = m.fromLatLngToDivPixel(new GLatLng(d.lat, d.lon));
      d.x = p.x; d.y = p.y;
    });

  function x(p) p.x; function y(p) p.y;
  var x = { min: pv.min(napoleon.army, x), max: pv.max(napoleon.army, x) };
  var y = { min: pv.min(napoleon.army, y), max: pv.max(napoleon.army, y) };
  var k = (y.max - y.min) / 1000000;

  /* Update the canvas bounds. Note: may be large. */
  c.style.width = (x.max - x.min + 2 * r) + "px";
  c.style.height = (y.max - y.min + 2 * r) + "px";
  c.style.left = x.min - r + "px";
  c.style.top = y.min - r + "px";

  /* Troop count visualization. */
  new pv.Panel()
      .canvas(c)
      .data(army)
    .add(pv.Panel)
      .data(function(d) d.values)
    .add(pv.Line)
      .segmented(true)
      .data(function(d) d.values)
      .left(function(d) d.x - x.min + r)
      .top(function(d) d.y - y.min + r)
      .lineWidth(function(d) Math.max(1, k * d.size))
      .strokeStyle(pv.colors("black", "brown").by(function(d) d.dir))
      .title(function(d) d.size)
    .root.render();
};

/* Restrict minimum and maximum zoom levels. */
[G_NORMAL_MAP, G_HYBRID_MAP, G_PHYSICAL_MAP].forEach(function(t) {
  t.getMinimumResolution = function() 4;
  t.getMaximumResolution = function() 8;
});

/* Create the map, embedding our visualization! */
var map = new GMap2(document.getElementById("map"));
map.setCenter(new GLatLng(55, 31), 6);
var ui = map.getDefaultUI();
ui.maptypes.satellite = false;
map.setUI(ui);
map.setMapType(G_PHYSICAL_MAP);
map.addOverlay(new Canvas());

    </script>
  </head>
  <body onunload="GUnload()"><div id="center"><div id="fig">
    <div id="map"></div>
    <div id="submap">
      <div id="temp"></div>
    </div>
  </div></div></body>
</html>

Data

var napoleon = {};

napoleon.temp = [
  {lon:37.6, temp:0,   date:"18 Oct 1812"},
  {lon:36.0, temp:0,   date:"24 Oct 1812"},
  {lon:33.2, temp:-9,  date:"09 Nov 1812"},
  {lon:32.0, temp:-21, date:"14 Nov 1812"},
  {lon:29.2, temp:-11, date:"24 Nov 1812"},
  {lon:28.5, temp:-20, date:"28 Nov 1812"},
  {lon:27.2, temp:-24, date:"01 Dec 1812"},
  {lon:26.7, temp:-30, date:"06 Dec 1812"},
  {lon:25.3, temp:-26, date:"07 Dec 1812"}
];

napoleon.army = [
  /* Group 1 */
  {lon:24.0,  lat:54.9,  size:340000, dir:1,  group:1},
  {lon:24.5,  lat:55.0,  size:340000, dir:1,  group:1},
  {lon:25.5,  lat:54.6,  size:340000, dir:1,  group:1},
  {lon:26.0,  lat:54.7,  size:320000, dir:1,  group:1},
  {lon:27.0,  lat:54.8,  size:300000, dir:1,  group:1},
  {lon:28.0,  lat:54.9,  size:280000, dir:1,  group:1},
  {lon:28.5,  lat:55.0,  size:240000, dir:1,  group:1},
  {lon:29.0,  lat:55.1,  size:210000, dir:1,  group:1},
  {lon:30.0,  lat:55.2,  size:180000, dir:1,  group:1},
  {lon:30.3,  lat:55.3,  size:175000, dir:1,  group:1},
  {lon:32.0,  lat:54.8,  size:145000, dir:1,  group:1},
  {lon:33.2,  lat:54.9,  size:140000, dir:1,  group:1},
  {lon:34.4,  lat:55.5,  size:127100, dir:1,  group:1},
  {lon:35.5,  lat:55.4,  size:100000, dir:1,  group:1},
  {lon:36.0,  lat:55.5,  size:100000, dir:1,  group:1},
  {lon:37.6,  lat:55.8,  size:100000, dir:1,  group:1},
  {lon:37.65, lat:55.65, size:100000, dir:-1, group:1},
  {lon:37.45, lat:55.62, size:98000,  dir:-1, group:1},
  {lon:37.0,  lat:55.0,  size:97000,  dir:-1, group:1},
  {lon:36.8,  lat:55.0,  size:96000,  dir:-1, group:1},
  {lon:35.4,  lat:55.3,  size:87000,  dir:-1, group:1},
  {lon:34.3,  lat:55.2,  size:55000,  dir:-1, group:1},
  {lon:33.3,  lat:54.8,  size:37000,  dir:-1, group:1},
  {lon:32.0,  lat:54.6,  size:24000,  dir:-1, group:1},
  {lon:30.4,  lat:54.4,  size:20000,  dir:-1, group:1},
  {lon:29.2,  lat:54.3,  size:20000,  dir:-1, group:1},
  {lon:29.13, lat:54.29, size:50000,  dir:-1, group:1}, /* joined by group 2 */
  {lon:28.5,  lat:54.2,  size:50000,  dir:-1, group:1},
  {lon:28.3,  lat:54.3,  size:48000,  dir:-1, group:1},
  {lon:26.8,  lat:54.3,  size:12000,  dir:-1, group:1},
  {lon:26.8,  lat:54.4,  size:14000,  dir:-1, group:1},
  {lon:25.0,  lat:54.4,  size:8000,   dir:-1, group:1},
  {lon:24.4,  lat:54.4,  size:4000,   dir:-1, group:1},
  {lon:24.2,  lat:54.4,  size:4000,   dir:-1, group:1},
  {lon:24.1,  lat:54.4,  size:4000,   dir:-1, group:1},
  /* Group 2 */
  {lon:24.0,  lat:55.1,  size:60000,  dir:1,  group:2},
  {lon:24.5,  lat:55.2,  size:60000,  dir:1,  group:2},
  {lon:25.5,  lat:54.7,  size:60000,  dir:1,  group:2},
  {lon:26.6,  lat:55.7,  size:40000,  dir:1,  group:2},
  {lon:27.4,  lat:55.6,  size:33000,  dir:1,  group:2},
  {lon:28.7,  lat:55.5,  size:33000,  dir:1,  group:2},
  {lon:28.7,  lat:55.5,  size:33000,  dir:-1, group:2},
  {lon:29.2,  lat:54.29, size:30000,  dir:-1, group:2},
  /* Group 3 */
  {lon:24.0,  lat:55.2,  size:22000,  dir:1,  group:3},
  {lon:24.5,  lat:55.3,  size:22000,  dir:1,  group:3},
  {lon:24.6,  lat:55.8,  size:6000,   dir:1,  group:3},
  {lon:24.6,  lat:55.8,  size:6000,   dir:-1, group:3},
  {lon:24.2,  lat:54.4,  size:6000,   dir:-1, group:3},
  {lon:24.1,  lat:54.4,  size:6000,   dir:-1, group:3}
];
Copyright 2010 Stanford Visualization Group