27 October, 2014
by nitaku

A classic comparison

A comparison of four "classic" types of charts: a bar chart, a pie chart, a treemap and a diagram based on a circle packing layout (sometimes called "bubble chart", not to be confused with this one, which encodes instead three dimensions of data).

Seven classes, represented by color, are each assigned a random value, that is then encoded with the four different techniques. Bars use their length to represent quantity, while the pie chart uses angles. Both the treemap and the bubbles uses area instead. The four layouts are also instructed to try to preserve the ordering of classes.

# seven random values
data = d3.range(7).map (d) -> { category: "cat_#{d}", value: Math.random() }

max = d3.max(data, (d) -> d.value)

width = 960
height = 500
side = Math.min(width,height)
RADIUS = side / 4 - 20


svg = d3.select("body").append("svg")
    .attr("width", width)
    .attr("height", height)
  .append('g')
    .attr
      transform: "translate(#{width/2}, #{height/2})"
    
ul = svg.append("g")
    .attr
      transform: "translate(#{-side/4}, #{-side/4})"
    
ur = svg.append("g")
    .attr
      transform: "translate(#{+side/4}, #{-side/4})"
    
bl = svg.append("g")
    .attr
      transform: "translate(#{-side/4}, #{+side/4})"
    
br = svg.append("g")
    .attr
      transform: "translate(#{+side/4}, #{+side/4}) rotate(180)" # bubble ordering is rotated by 180 degrees

color = d3.scale.ordinal()
  .domain(data.map (d) -> d.category)
  .range(["#1b9e77","#d95f02","#7570b3","#e7298a","#66a61e","#e6ab02","#a6761d","#666666"])

# ---------
# bar chart
# ---------

y_scale = d3.scale.linear()
  .domain([0, max])
  .range([0, 2*RADIUS])
  
x_scale = d3.scale.ordinal()
  .domain(data.map (d) -> d.category)
  .rangeRoundBands([-RADIUS, RADIUS], .05)
  
bars = ul.selectAll('.bar')
  .data(data)
  
bars.enter().append('rect')
  .attr
    class: 'bar'
    x: (d) -> x_scale(d.category)
    y: (d) -> RADIUS-y_scale(d.value)
    width: x_scale.rangeBand()
    height: (d) -> y_scale(d.value)
    fill: (d) -> color(d.category)

# ---------
# pie chart
# ---------

pie = d3.layout.pie()
  .sort(null)
  .value((d) -> d.value )
  
arc_generator = d3.svg.arc()
  .innerRadius(0)
  .outerRadius(RADIUS)
  
ur.selectAll('.arc')
    .data(pie(data))
  .enter().append('path')
    .attr
      class: 'arc'
      d: arc_generator
      fill: (d) -> color(d.data.category)
      
# -----------------
# one-level treemap
# -----------------

treemap = d3.layout.treemap()
  .size([2*RADIUS, 2*RADIUS])
  .value((node) -> node.value)
  .sort((a,b) -> d3.descending(a.category,b.category))
    
tree = {
  children: data
}
nodes = treemap.nodes(tree)
  
bl.selectAll('.node')
    .data(nodes.filter (node) -> node.depth is 1 )
  .enter().append('rect')
    .attr
      class: 'node'
      x: (node) -> node.x - RADIUS
      y: (node) -> node.y - RADIUS
      width: (node) -> node.dx
      height: (node) -> node.dy
      fill: (d) -> color(d.category)
      
# ------------      
# bubble chart
# ------------

pack = d3.layout.pack()
  .size([2.4*RADIUS, 2.4*RADIUS])
  .value((node) -> node.value)
  .sort((a,b) -> d3.descending(a.category,b.category))
  .padding(2)
  
pack_tree = {
  children: data
}
pack_nodes = pack.nodes(pack_tree)

br.selectAll('.bubble')
    .data(nodes.filter (node) -> node.depth is 1 )
  .enter().append('circle')
    .attr
      class: 'bubble'
      cx: (node) -> node.x - 1.2*RADIUS
      cy: (node) -> node.y - 1.2*RADIUS
      r: (node) -> node.r
      fill: (d) -> color(d.category)
svg {
  background-color: white;
}
.bar {
  shape-rendering: crispEdges;
}
.node {
  stroke-width: 1;
  stroke: white;
  shape-rendering: crispEdges;
}
.arc {
  stroke-width: 1;
  stroke: white;
  stroke-linejoin: round;
}
.radius {
  stroke: gray;
  stroke-dasharray: 3 3;
}
.polygon {
  fill: #DDD;
  fill-opacity: 0.5;
  stroke: gray;
}
.outer_polygon {
  fill: none;
  stroke: gray;
  stroke-dasharray: 3 3;
}
.dot {
  stroke: white;
}
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="description" content="A classic comparison" />
  <title>A classic comparison</title>
  <link rel="stylesheet" href="index.css">
  <script src="http://d3js.org/d3.v3.min.js"></script>
</head>
<body>
  <script src="index.js"></script>
</body>
</html>
(function() {
  var RADIUS, arc_generator, bars, bl, br, color, data, height, max, nodes, pack, pack_nodes, pack_tree, pie, side, svg, tree, treemap, ul, ur, width, x_scale, y_scale;

  data = d3.range(7).map(function(d) {
    return {
      category: "cat_" + d,
      value: Math.random()
    };
  });

  max = d3.max(data, function(d) {
    return d.value;
  });

  width = 960;

  height = 500;

  side = Math.min(width, height);

  RADIUS = side / 4 - 20;

  svg = d3.select("body").append("svg").attr("width", width).attr("height", height).append('g').attr({
    transform: "translate(" + (width / 2) + ", " + (height / 2) + ")"
  });

  ul = svg.append("g").attr({
    transform: "translate(" + (-side / 4) + ", " + (-side / 4) + ")"
  });

  ur = svg.append("g").attr({
    transform: "translate(" + (+side / 4) + ", " + (-side / 4) + ")"
  });

  bl = svg.append("g").attr({
    transform: "translate(" + (-side / 4) + ", " + (+side / 4) + ")"
  });

  br = svg.append("g").attr({
    transform: "translate(" + (+side / 4) + ", " + (+side / 4) + ") rotate(180)"
  });

  color = d3.scale.ordinal().domain(data.map(function(d) {
    return d.category;
  })).range(["#1b9e77", "#d95f02", "#7570b3", "#e7298a", "#66a61e", "#e6ab02", "#a6761d", "#666666"]);

  y_scale = d3.scale.linear().domain([0, max]).range([0, 2 * RADIUS]);

  x_scale = d3.scale.ordinal().domain(data.map(function(d) {
    return d.category;
  })).rangeRoundBands([-RADIUS, RADIUS], .05);

  bars = ul.selectAll('.bar').data(data);

  bars.enter().append('rect').attr({
    "class": 'bar',
    x: function(d) {
      return x_scale(d.category);
    },
    y: function(d) {
      return RADIUS - y_scale(d.value);
    },
    width: x_scale.rangeBand(),
    height: function(d) {
      return y_scale(d.value);
    },
    fill: function(d) {
      return color(d.category);
    }
  });

  pie = d3.layout.pie().sort(null).value(function(d) {
    return d.value;
  });

  arc_generator = d3.svg.arc().innerRadius(0).outerRadius(RADIUS);

  ur.selectAll('.arc').data(pie(data)).enter().append('path').attr({
    "class": 'arc',
    d: arc_generator,
    fill: function(d) {
      return color(d.data.category);
    }
  });

  treemap = d3.layout.treemap().size([2 * RADIUS, 2 * RADIUS]).value(function(node) {
    return node.value;
  }).sort(function(a, b) {
    return d3.descending(a.category, b.category);
  });

  tree = {
    children: data
  };

  nodes = treemap.nodes(tree);

  bl.selectAll('.node').data(nodes.filter(function(node) {
    return node.depth === 1;
  })).enter().append('rect').attr({
    "class": 'node',
    x: function(node) {
      return node.x - RADIUS;
    },
    y: function(node) {
      return node.y - RADIUS;
    },
    width: function(node) {
      return node.dx;
    },
    height: function(node) {
      return node.dy;
    },
    fill: function(d) {
      return color(d.category);
    }
  });

  pack = d3.layout.pack().size([2.4 * RADIUS, 2.4 * RADIUS]).value(function(node) {
    return node.value;
  }).sort(function(a, b) {
    return d3.descending(a.category, b.category);
  }).padding(2);

  pack_tree = {
    children: data
  };

  pack_nodes = pack.nodes(pack_tree);

  br.selectAll('.bubble').data(nodes.filter(function(node) {
    return node.depth === 1;
  })).enter().append('circle').attr({
    "class": 'bubble',
    cx: function(node) {
      return node.x - 1.2 * RADIUS;
    },
    cy: function(node) {
      return node.y - 1.2 * RADIUS;
    },
    r: function(node) {
      return node.r;
    },
    fill: function(d) {
      return color(d.category);
    }
  });

}).call(this);
File not shown (binary encoding).