block by nitaku d3fd26207d7468b6c514

Classic hexagonal binning

Full Screen

An example of hexagonal binning using area encoding. Reload to change the random distributions.

The dot plot on the left is made (hopefully) more readable by grouping points into hexagonal cells (bins), then representing the amount of points in each cell by controlling its area.

Data is normalized according to the maximum number of points in a single bin (corresponding to the largest hexagonon the map), so two different instances of this visualization cannot be compared between each other (they may use two different scales for areas).

Other types of encoding can be used to represent the size of bins (e.g. color).

index.js

(function() {
  var apothem, bins, dots, height, hexagons, hexbin, points, radius, radius_scale, svg, upper_left, upper_right, width;

  width = 480;

  height = 500;

  points = d3.range(2000).map(function() {
    return {
      x: d3.random.normal(width / 3, 90)(),
      y: d3.random.normal(height / 3, 90)()
    };
  }).concat(d3.range(1000).map(function() {
    return {
      x: d3.random.normal(2 * width / 3, 70)(),
      y: d3.random.normal(2 * height / 3, 70)()
    };
  }));

  svg = d3.select('svg');

  upper_left = svg.append('g').attr('id', 'dots').attr('clip-path', 'url(#square_window)');

  upper_right = svg.append('g').attr('id', 'bins').attr('clip-path', 'url(#square_window)').attr('transform', "translate(" + width + ",0)");

  svg.append('line').attr({
    "class": 'separator',
    x1: width,
    x2: width,
    y1: 0,
    y2: height
  });

  dots = d3.select('#dots').selectAll('.dot').data(points);

  dots.enter().append('circle').attr({
    "class": 'dot',
    r: 1,
    cx: function(p) {
      return p.x;
    },
    cy: function(p) {
      return p.y;
    }
  });

  radius = 16;

  apothem = Math.sqrt(3) / 2 * radius;

  hexbin = d3.hexbin().size([width, height]).radius(radius).x(function(d) {
    return d.x;
  }).y(function(d) {
    return d.y;
  });

  bins = hexbin(points);

  radius_scale = d3.scale.sqrt().domain([
    0, d3.max(bins, function(b) {
      return b.length;
    })
  ]).range([0, radius]);

  hexagons = d3.select('#bins').selectAll('.hexagon').data(bins);

  hexagons.enter().append('path').attr('class', 'hexagon').attr('d', function(d) {
    return hexbin.hexagon(radius_scale(d.length));
  }).attr('transform', function(d) {
    return "translate(" + d.x + "," + d.y + ")";
  });

}).call(this);

index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <meta name="description" content="Classic hexagonal binning" />
  <title>Classic hexagonal binning</title>
  <link rel="stylesheet" href="index.css">
  <script src="//d3js.org/d3.v3.min.js"></script>
  <script src="//d3js.org/d3.hexbin.v0.min.js?5c6e4f0"></script>
</head>
<body>
  <svg height="500" width="960">
    <defs>
      <clipPath id="square_window">
        <rect x="0" y="0" width="480.5" height="500.5" />
      </clipPath>
    </defs>
  </svg>
  <script src="index.js"></script>
</body>
</html>

index.coffee

width = 480
height = 500

# DATA
points = d3.range(2000).map( () -> {x: d3.random.normal(width/3, 90)(), y: d3.random.normal(height/3, 90)()} )
  .concat d3.range(1000).map( () -> {x: d3.random.normal(2*width/3, 70)(), y: d3.random.normal(2*height/3, 70)()} )

svg = d3.select('svg')

upper_left = svg.append('g')
    .attr('id', 'dots')
    .attr('clip-path', 'url(#square_window)')
  
upper_right = svg.append('g')
    .attr('id', 'bins')
    .attr('clip-path', 'url(#square_window)')
    .attr('transform', "translate(#{width},0)")
    
svg.append('line')
  .attr
    class: 'separator'
    x1: width
    x2: width
    y1: 0
    y2: height
  
# dot density plot
dots = d3.select('#dots').selectAll('.dot')
  .data(points)

dots.enter().append('circle')
  .attr
    class: 'dot'
    r: 1
    cx: (p) -> p.x
    cy: (p) -> p.y
    
# hexagonal binning
radius = 16
apothem = Math.sqrt(3)/2 * radius

hexbin = d3.hexbin()
  .size([width, height])
  .radius(radius)
  .x((d) -> d.x)
  .y((d) -> d.y)
  
bins = hexbin(points)
radius_scale = d3.scale.sqrt()
  .domain([0, d3.max(bins, (b) -> b.length)])
  .range([0, radius])

hexagons = d3.select('#bins').selectAll('.hexagon')
    .data(bins)
  
hexagons.enter().append('path')
    .attr('class', 'hexagon')
    .attr('d', (d) -> hexbin.hexagon(radius_scale(d.length)))
    .attr('transform', (d) -> "translate(#{d.x},#{d.y})")
    

index.css

svg {
  background-color: white;
}
.separator {
  stroke: #DEDEDE;
  fill: none;
  shape-rendering: crispEdges;
}
.hexagon {
  fill: #333;
}