block by nitaku 5853f4965e7f350bb65ad2db324776d5

Quadtree aggregation II

Full Screen

A not-so-useful visualization that uses a quadtree to bin a lot of points in square cells. Each original point has a third numerical value, which is used to color it. Bins are also colored, according to the mean value of contained points.

A pattern, which has been artificially introduced into random data, can be seen from the gradient of color. There is a correlation between the value and the x coordinate.

The result is not that informative, because it fails to represent the density of points (i.e., the amount of points in a bin). Color could have been used to convey that information, but there are techniques better than binning to do that (e.g., smooth interpolation and/or contour lines). Perhaps contour lines could be added to this diagram to help.

index.js

// Generated by CoffeeScript 1.10.0
(function() {
  var color, data, depth_walk, desc_walk, dots, enter_quads, height, leaves, level, levels, qside, quads, quadtree, random, side, svg, vis, width;

  svg = d3.select('svg');

  width = svg.node().getBoundingClientRect().width;

  height = svg.node().getBoundingClientRect().height;

  side = Math.min(width, height);

  random = d3.randomNormal(side / 2, 50);

  data = d3.range(30000).map(function(i) {
    var a;
    a = random();
    return [a, random(), a + random() * 0.5 * (i % 2)];
  });

  quadtree = d3.quadtree().extent([[0, 0], [side, side]]).addAll(data);

  qside = quadtree._x1 - quadtree._x0;

  quadtree.visitAfter(function(n) {
    if (n.length == null) {
      return n.size = 1;
    } else {
      return n.size = d3.sum(n, function(d) {
        if (d != null) {
          return d.size;
        } else {
          return 0;
        }
      });
    }
  });

  quadtree.visit(function(n, x0, y0, x1, y1) {
    if (n != null) {
      n.x0 = x0;
      n.y0 = y0;
      n.x1 = x1;
      n.y1 = y1;
      return false;
    }
    return true;
  });

  levels = {};

  depth_walk = function(n, l) {
    if (n != null) {
      n.depth = l;
      if (!(l in levels)) {
        levels[l] = [];
      }
      levels[l].push(n);
      if (n.length != null) {
        return n.forEach(function(c) {
          return depth_walk(c, l + 1);
        });
      }
    }
  };

  depth_walk(quadtree.root(), 0);

  desc_walk = function(n) {
    if (n != null) {
      if (n.length != null) {
        n.descendants = d3.merge(n.filter(function(d) {
          return d != null;
        }).map(function(c) {
          return desc_walk(c);
        }));
      } else {
        n.descendants = [n.data];
      }
      return n.descendants;
    }
    return [];
  };

  desc_walk(quadtree.root());

  level = 6;

  color = d3.scaleSequential(function(t) {
    return d3.interpolateMagma(1 - t);
  }).domain([
    0, d3.max(data, function(d) {
      return d[2];
    })
  ]);

  vis = svg.append('g').attrs({
    transform: "translate(" + ((width - qside) / 2) + "," + ((height - qside) / 2) + ")"
  });

  quads = vis.selectAll('.quad').data(levels[level].filter(function(d) {
    return d.length != null;
  }));

  enter_quads = quads.enter().append('g').attrs({
    "class": 'quad',
    transform: function(d) {
      return "translate(" + d.x0 + "," + d.y0 + ")";
    }
  });

  enter_quads.append('rect').attrs({
    width: function(d) {
      return d.x1 - d.x0;
    },
    height: function(d) {
      return d.y1 - d.y0;
    },
    fill: function(d) {
      return color(d3.mean(d.descendants, function(x) {
        return x[2];
      }));
    }
  });

  enter_quads.append('title').text(function(d) {
    return d.size;
  });

  leaves = [];

  Object.keys(levels).forEach(function(l) {
    var d;
    if (l <= level) {
      d = levels[l];
      return d.forEach(function(q) {
        if (q.length == null) {
          return leaves.push(q);
        }
      });
    }
  });

  dots = vis.selectAll('.dot').data(leaves);

  dots.enter().append('circle').attrs({
    "class": 'dot',
    cx: function(d) {
      return d.data[0];
    },
    cy: function(d) {
      return d.data[1];
    },
    r: 2,
    fill: function(d) {
      return color(d.data[2]);
    }
  });

}).call(this);

index.html

<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Quadtree aggregation II</title>
  <link type="text/css" href="index.css" rel="stylesheet"/>
  <script src="https://d3js.org/d3.v4.min.js"></script>
  <script src="https://d3js.org/d3-selection-multi.v0.4.min.js"></script>
</head>
<body>
  <svg></svg>
  <script src="index.js"></script>
</body>
</html>

index.coffee

svg = d3.select 'svg'
width = svg.node().getBoundingClientRect().width
height = svg.node().getBoundingClientRect().height

side = Math.min(width,height)

random = d3.randomNormal(side/2, 50)
data = d3.range(30000).map (i) ->
  a = random()
  return [a, random(), a+random()*0.5*(i%2)]


quadtree = d3.quadtree()
  .extent [[0,0], [side, side]]
  .addAll data
  
qside = quadtree._x1 - quadtree._x0

# store a counter for the number of elements in each quad
quadtree.visitAfter (n) ->
  if not n.length?
    n.size = 1
  else
    n.size = d3.sum n, (d) -> if d? then d.size else 0

# store the quad extent in each node
quadtree.visit (n, x0, y0, x1, y1) ->
  if n?
    n.x0 = x0
    n.y0 = y0
    n.x1 = x1
    n.y1 = y1
    return false
  return true

# store the nodes according to their depth
levels = {}
depth_walk = (n, l) ->
  if n?
    n.depth = l
    if l not of levels
      levels[l] = []
    levels[l].push n
    
    if n.length?
      n.forEach (c) -> depth_walk(c, l+1)
  
depth_walk quadtree.root(), 0

# store descendants data in all nodes
desc_walk = (n) ->
  if n?
    if n.length?
      n.descendants = d3.merge n.filter( (d) -> d? ).map (c) -> desc_walk(c)
    else
      n.descendants = [n.data]
      
    return n.descendants
  return []

desc_walk quadtree.root()


# VIS
level = 6

color = d3.scaleSequential( (t) -> d3.interpolateMagma(1-t) )
  .domain [0, d3.max data, (d) -> d[2]]

vis = svg.append 'g'
  .attrs
    transform: "translate(#{(width-qside)/2},#{(height-qside)/2})"
  
quads = vis.selectAll '.quad'
  .data levels[level].filter (d) -> d.length?
  
enter_quads = quads.enter().append 'g'
  .attrs
    class: 'quad'
    transform: (d) -> "translate(#{d.x0},#{d.y0})"

enter_quads.append 'rect'
  .attrs
    width: (d) -> d.x1 - d.x0
    height: (d) -> d.y1 - d.y0
    fill: (d) -> color d3.mean d.descendants, (x) -> x[2]
    
enter_quads.append 'title'
  .text (d) -> d.size
    
leaves = []
Object.keys(levels).forEach (l) ->
  if l <= level
    d = levels[l]
    d.forEach (q) ->
      if not q.length?
        leaves.push q
    
dots = vis.selectAll '.dot'
  .data leaves
  
dots.enter().append 'circle'
  .attrs
    class: 'dot'
    cx: (d) -> d.data[0]
    cy: (d) -> d.data[1]
    r: 2
    fill: (d) -> color(d.data[2])
    

index.css

body, html {
  padding: 0;
  margin: 0;
  height: 100%;
}
svg {
  width: 100%;
  height: 100%;
  background: white;
}

.quad rect {
  shape-rendering: crispEdges;
}

.dot {
  stroke: white;
}