Skip to content

Instantly share code, notes, and snippets.

@nitaku
Last active July 18, 2016 15:14
Show Gist options
  • Save nitaku/5853f4965e7f350bb65ad2db324776d5 to your computer and use it in GitHub Desktop.
Save nitaku/5853f4965e7f350bb65ad2db324776d5 to your computer and use it in GitHub Desktop.
Quadtree aggregation II

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.

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])
body, html {
padding: 0;
margin: 0;
height: 100%;
}
svg {
width: 100%;
height: 100%;
background: white;
}
.quad rect {
shape-rendering: crispEdges;
}
.dot {
stroke: white;
}
<!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>
// 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);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment