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
<html>
<head>
<title>Minard’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&v=2&sensor=false&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>
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}
];