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

Dorling Cartograms

View full screen.

Cartograms distort the shape of geographic regions so that the area directly encodes a data variable. A common example is to resize countries proportional to population or GDP. Many types of cartograms have been created; in this example we use the Dorling cartogram, which represents each geographic region as non-overlapping circles.

This example encodes the number of obese people per state as area, and the percentage of obese people as color. California dominates the map due to its large population, while color indicates that Mississippi and Alabama have the highest obesity rate.

Next: Map Projections

Source

<html>
  <head>
    <title>Dorling Cartogram</title>
    <link type="text/css" rel="stylesheet" href="ex.css?3.2"/>
    <link type="text/css" rel="stylesheet" href="../ui-lightness/jquery-ui-1.8rc3.custom.css"/>
    <script type="text/javascript" src="../protovis-r3.2.js"></script>
    <script type="text/javascript" src="../jquery-1.4.2.min.js"></script>
    <script type="text/javascript" src="../jquery-ui-1.8rc3.custom.min.js"></script>
    <script type="text/javascript" src="centroid.js"></script>
    <script type="text/javascript" src="us_lowres.js"></script>
    <script type="text/javascript" src="us_stats.js"></script>
    <script type="text/javascript" src="us_borders.js"></script>
    <style type="text/css">

#fig {
  width: 800px;
  height: 500px;
  margin-top: 10px;
}

#container {
  height: 10px;
}

#yearSlider {
  position: absolute;
  left: 100;
  right: 90;
  margin-top: 3;
}

#yearLabel {
  position: absolute;
  left: 0;
  width: 85;
  text-align: right;
}

#play {
  position: absolute;
  right: 50px;
  cursor: pointer;
}

    </style>
  </head>
  <body><div id="center"><div id="fig">
    <div id="container">
      <b id="yearLabel">Year:</b
      ><div id="yearSlider"></div
      ><img id="play" src="play.png" alt="Play" onclick="playClick()">
    </div>
    <script type="text/javascript+protovis">

$(yearSlider).slider({
  min: us_stats.minYear,
  max: us_stats.maxYear,
  value: us_stats.maxYear,
  slide: function(e, ui) {
    year = ui.value;
    updateYear();
    vis.render();
  }
});

var year = 2008;

us_lowres.forEach(function(c) {
  c.code = c.code.toUpperCase();
  c.center = centroid(c.borders[0]);
});

var i = 0,
    w = 810,
    h = 400,
    mapMargin = 30;

var scale = pv.Geo.scale()
    .domain({lng: -128, lat: 24}, {lng: -64, lat: 50})
    .range({x: mapMargin, y: mapMargin}, {x: w-mapMargin, y: h-mapMargin});

var legendMargin = 20,
    ease = null,
    yearsMargin = 100;

var yearsScale = pv.Scale.linear()
    .domain(us_stats.minYear, us_stats.maxYear)
    .range(yearsMargin + 2, w - yearsMargin);

var legendScale = pv.Scale.linear()
    .domain(14, 35)
    .range(legendMargin, w - legendMargin);

var col = function(v) {
  if (v < 17) return "#008038";
  if (v < 20) return "#A3D396";
  if (v < 23) return "#FDD2AA";
  if (v < 26) return "#F7976B";
  if (v < 29) return "#F26123";
  if (v < 32) return "#E12816";
  /* else */ return "#B7161E";
};

var numToRad = function(n) {
  return Math.sqrt(n)/45;
};

var nodes = [],
    codeToNode = [],
    links = [];

us_lowres.forEach(function(s) {
  if (us_stats[s.code]) {
    var x = scale(s.center).x,
        y = scale(s.center).y,
        numObese = us_stats[s.code].pop[us_stats.yearIdx(year)]*us_stats[s.code].obese[us_stats.yearIdx(year)]/100,
        n = {x: x, y: y, p: {x: x, y: y}, r: numToRad(numObese), code:s.code};
    nodes.push(n);
    codeToNode[s.code] = n;
  }
});

us_lowres.forEach(function(s) {
  if (us_stats[s.code]) {
    var borders = us_borders[s.code];
    borders.forEach(function(b) {
      if (codeToNode[s.code] && codeToNode[b] && s.code < b) {
        var nodeA = codeToNode[s.code];
        var nodeB = codeToNode[b];
        links.push({sourceNode:nodeA, targetNode:nodeB, length:(nodeA.r + nodeB.r + 2)});
      }
    });
  }
});

function updateYear() {
  nodes.forEach(function(n) n.r = numToRad(us_stats[n.code].pop[us_stats.yearIdx(year)]*us_stats[n.code].obese[us_stats.yearIdx(year)]/100));
  links.forEach(function(l) l.length = (l.sourceNode.r + l.targetNode.r + 2));
  i = 0;
  var stepSome = setInterval(function() {
    if (i++ > 10) clearInterval(stepSome);
    sim.step();
    vis.render();
  }, 20);
}

var timer = undefined;
function playClick() {
  if (timer) {
    stop();
  } else {
    if (year == us_stats.maxYear) year = us_stats.minYear;
    $(yearSlider).slider('value', year);
    $(play).attr("src", 'stop.png');
    updateYear();
    vis.render();
    timer = setInterval(function() {
      year++;
      if (year >= us_stats.maxYear) stop();
      $(yearSlider).slider('value', year);
      updateYear();
      vis.render();
    }, 900);
  }
}

// Stop the animation
function stop() {
  clearInterval(timer);
  timer = undefined;
  $(play).attr("src", 'play.png');
}

var collisionConstraint = pv.Constraint.collision(function(d) d.r + 1),
    positionConstraint = pv.Constraint.position(function(d) d.p),
    linkConstraint = pv.Force.spring(100).links(links);

var sim = pv.simulation(nodes)
    .constraint(collisionConstraint)
    .constraint(positionConstraint)
    .constraint(linkConstraint)
    .force(pv.Force.drag());

var vis = new pv.Panel()
    .width(w)
    .height(h)
    .top(50)
    .bottom(30);

// Add the ticks and labels for the year slider
vis.add(pv.Rule)
     .data(pv.range(us_stats.minYear, us_stats.maxYear + .1))
     .left(yearsScale)
     .height(4)
     .top(-40)
   .anchor("bottom").add(pv.Label);

vis.add(pv.Dot)
    .data(nodes)
    .left(function(d) d.x)
    .top(function(d) d.y)
    .radius(function(d) d.r)
    .fillStyle(function(d) col(us_stats[d.code].obese[us_stats.yearIdx(year)]))
    .strokeStyle(null)
    .title(function(d) us_stats[d.code].name + ": " + us_stats[d.code].obese[us_stats.yearIdx(year)] + "%")
   .add(pv.Label)
    .text(function(d) d.code)
    .textStyle("#fee")
    .font(function(d) "bold " + (4*Math.log(d.r)).toFixed(0) + "px sans-serif")
    .textAlign("center")
    .textBaseline("middle");

vis.add(pv.Dot)
    .data([
        {v: 10000000, l: "10M"},
        {v: 1000000, l: "1M"},
        {v: 5000000, l: "5M"},
        {v: 100000, l: "100K"}
      ])
    .fillStyle(null)
    .strokeStyle("#555")
    .left(150)
    .bottom(-30)
    .radius(function(d) numToRad(d.v))
  .anchor("top").add(pv.Label)
    .text(function(d) d.l)

// Add the color bars for the color legend
vis.add(pv.Bar)
    .data(pv.range(14,32.1,3))
    .bottom(function(d) this.index * 15 - 28)
    .height(10)
    .width(10)
    .left(5)
     .fillStyle(function(d) col(14 + 3 * this.index))
    .lineWidth(null)
  .anchor("right").add(pv.Label)
    .textAlign("left")
    .text(function(d) d + " - " + (d+3) + "%");

ease = setInterval(function() {
  if (i++ > 140) {
    clearInterval(ease);
    ease = null;
  }
  sim.step();
  positionConstraint.alpha(Math.pow(.7, i + 2) + .03);
  linkConstraint.damping(Math.pow(.7, i + 2) + .03);
  vis.render();
}, 42);

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

Data

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