This animation shows how successively adding points to a quadtree changes its structure. Each red dot represents a point; each gray rectangle represents a leaf node in the quadtree; black lines separate internal nodes. Assuming no coincident points, each gray leaf contains exactly one point. (If some points are coincident, the leaf node contains a linked list of the coincident points.)
Last active
January 4, 2018 20:05
-
-
Save mbostock/87fbdfc4533df2ffa02b5bb62bfb5cb6 to your computer and use it in GitHub Desktop.
Animated Quadtree
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
license: gpl-3.0 | |
height: 960 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
(function (global, factory) { | |
typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : | |
typeof define === 'function' && define.amd ? define(['exports'], factory) : | |
(factory((global.d3_quadtree = global.d3_quadtree || {}))); | |
}(this, function (exports) { 'use strict'; | |
var version = "0.4.0"; | |
function tree_add(point) { | |
if (isNaN(x = +point.x) || isNaN(y = +point.y)) return this; // ignore invalid points | |
var node = this._root, | |
parent, | |
point0, | |
x, y, | |
xm, ym, | |
xp, yp, | |
x0 = this._x0, y0 = this._y0, | |
x1 = this._x1, y1 = this._y1, | |
right, | |
bottom, | |
i, | |
j; | |
this._added = null; | |
// Check if this point was previously in a quadtree! | |
if (point.next) delete point.next; | |
// If the tree is empty, initialize the root as a leaf. | |
if (!node) { | |
this._root = {point: point}; | |
this._x0 = this._x1 = x; | |
this._y0 = this._y1 = y; | |
this._added = {node: this._root, x0: x, x1: x, y0: y, y1: y}; | |
return this; | |
} | |
// Expand the tree to cover the new point, if necessary. | |
if (x0 > x || x > x1 || y0 > y || y > y1) { | |
xm = x0 === x1 ? Math.max(Math.abs(x0 - x), Math.abs(y0 - y)) : (x1 - x0) * 2; | |
switch (i = (y < (y0 + y1) / 2) << 1 | (x < (x0 + x1) / 2)) { | |
case 0: do parent = new Array(4), parent[i] = node, node = parent, x1 = x0 + xm, y1 = y0 + xm, xm *= 2; while (x > x1 || y > y1); break; | |
case 1: do parent = new Array(4), parent[i] = node, node = parent, x0 = x1 - xm, y1 = y0 + xm, xm *= 2; while (x0 > x || y > y1); break; | |
case 2: do parent = new Array(4), parent[i] = node, node = parent, x1 = x0 + xm, y0 = y1 - xm, xm *= 2; while (x > x1 || y0 > y); break; | |
case 3: do parent = new Array(4), parent[i] = node, node = parent, x0 = x1 - xm, y0 = y1 - xm, xm *= 2; while (x0 > x || y0 > y); break; | |
} | |
this._root = node; | |
this._x0 = x0, this._x1 = x1; | |
this._y0 = y0, this._y1 = y1; | |
} | |
// Find the appropriate leaf node for the new point. | |
// If there is no leaf node, add it. | |
while (!(point0 = node.point)) { | |
if (right = x >= (xm = (x0 + x1) / 2)) x0 = xm; else x1 = xm; | |
if (bottom = y >= (ym = (y0 + y1) / 2)) y0 = ym; else y1 = ym; | |
if (parent = node, !(node = node[i = bottom << 1 | right])) { | |
node = parent[i] = {point: point}; | |
this._added = {node: node, x0: x0, x1: x1, y0: y0, y1: y1}; | |
return this; | |
} | |
} | |
// If the new point is exactly coincident with the specified point, add it. | |
if (xp = point0.x, yp = point0.y, x === xp && y === yp) { | |
node = {point: point, next: node}; | |
if (parent) parent[i] = node; | |
else this._root = node; | |
this._added = {node: node, x0: x0, x1: x1, y0: y0, y1: y1}; | |
return this; | |
} | |
// Otherwise, split the leaf node until the old and new point are separated. | |
do { | |
parent = parent[i] = new Array(4); | |
if (!this._added) this._added = {node: parent, x0: x0, x1: x1, y0: y0, y1: y1}; | |
if (right = x >= (xm = (x0 + x1) / 2)) x0 = xm; else x1 = xm; | |
if (bottom = y >= (ym = (y0 + y1) / 2)) y0 = ym; else y1 = ym; | |
} while ((i = bottom << 1 | right) === (j = (yp >= ym) << 1 | (xp >= xm))); | |
parent[i] = {point: point}; | |
parent[j] = node; | |
return this; | |
} | |
function Quad(node, x0, y0, x1, y1) { | |
this.node = node; | |
this.x0 = x0; | |
this.y0 = y0; | |
this.x1 = x1; | |
this.y1 = y1; | |
} | |
function tree_visit(callback) { | |
var quads = [], q, node = this._root, child, x0, y0, x1, y1; | |
if (node) quads.push(new Quad(node, this._x0, this._y0, this._x1, this._y1)); | |
while (q = quads.pop()) { | |
if (!callback(node = q.node, x0 = q.x0, y0 = q.y0, x1 = q.x1, y1 = q.y1)) { | |
var xm = (x0 + x1) / 2, ym = (y0 + y1) / 2; | |
if (child = node[3]) quads.push(new Quad(child, xm, ym, x1, y1)); | |
if (child = node[2]) quads.push(new Quad(child, x0, ym, xm, y1)); | |
if (child = node[1]) quads.push(new Quad(child, xm, y0, x1, ym)); | |
if (child = node[0]) quads.push(new Quad(child, x0, y0, xm, ym)); | |
} | |
} | |
return this; | |
} | |
function tree_visitAfter(callback) { | |
var quads = [], next = [], q; | |
if (this._root) quads.push(new Quad(this._root, this._x0, this._y0, this._x1, this._y1)); | |
while (q = quads.pop()) { | |
var node = q.node, child, x0 = q.x0, y0 = q.y0, x1 = q.x1, y1 = q.y1, xm = (x0 + x1) / 2, ym = (y0 + y1) / 2; | |
if (child = node[0]) quads.push(new Quad(child, x0, y0, xm, ym)); | |
if (child = node[1]) quads.push(new Quad(child, xm, y0, x1, ym)); | |
if (child = node[2]) quads.push(new Quad(child, x0, ym, xm, y1)); | |
if (child = node[3]) quads.push(new Quad(child, xm, ym, x1, y1)); | |
next.push(q); | |
} | |
while (q = next.pop()) { | |
callback(q.node, q.x0, q.y0, q.x1, q.y1); | |
} | |
return this; | |
} | |
function tree_find(x, y) { | |
var minDistance2 = Infinity, | |
minPoint, | |
x0 = this._x0, | |
y0 = this._y0, | |
x1, | |
y1, | |
x2, | |
y2, | |
x3 = this._x1, | |
y3 = this._y1, | |
quads = [], | |
node = this._root, | |
q, | |
i; | |
if (node) quads.push(new Quad(node, x0, y0, x3, y3)); | |
while (q = quads.pop()) { | |
node = q.node, x1 = q.x0, y1 = q.y0, x2 = q.x1, y2 = q.y1; | |
// Stop searching if this quadrant can’t contain a closer node. | |
if (!node || x1 > x3 || y1 > y3 || x2 < x0 || y2 < y0) continue; | |
// Visit this point. (Visiting coincident points isn’t necessary!) | |
if (node.point) { | |
var dx = x - node.point.x, | |
dy = y - node.point.y, | |
d2 = dx * dx + dy * dy; | |
if (d2 < minDistance2) { | |
var d = Math.sqrt(minDistance2 = d2); | |
x0 = x - d, y0 = y - d; | |
x3 = x + d, y3 = y + d; | |
minPoint = node.point; | |
} | |
} | |
// Bisect the current quadrant. | |
var xm = (x1 + x2) / 2, | |
ym = (y1 + y2) / 2; | |
quads.push( | |
new Quad(node[3], xm, ym, x2, y2), | |
new Quad(node[2], x1, ym, xm, y2), | |
new Quad(node[1], xm, y1, x2, ym), | |
new Quad(node[0], x1, y1, xm, ym) | |
); | |
// Visit the closest quadrant first. | |
if (i = (y >= ym) << 1 | (x >= xm)) { | |
q = quads[quads.length - 1]; | |
quads[quads.length - 1] = quads[quads.length - 1 - i]; | |
quads[quads.length - 1 - i] = q; | |
} | |
} | |
return minPoint; | |
} | |
function tree_remove(point) { | |
var parent, | |
node = this._root, | |
previous, | |
xm, ym, | |
x = +point.x, y = +point.y, | |
x0 = this._x0, y0 = this._y0, | |
x1 = this._x1, y1 = this._y1, | |
right, | |
bottom, | |
i; | |
// If the tree is empty, initialize the root as a leaf. | |
if (!node) return false; | |
// Find the leaf node for the point. | |
while (!node.point) { | |
if (right = x >= (xm = (x0 + x1) / 2)) x0 = xm; else x1 = xm; | |
if (bottom = y >= (ym = (y0 + y1) / 2)) y0 = ym; else y1 = ym; | |
if (!(parent = node, node = node[i = bottom << 1 | right])) return false; | |
} | |
// Find the point to remove. | |
while (node.point !== point) if (!(previous = node, node = node.next)) return false; | |
// Remove the point, or the leaf if it’s the only point. | |
if (previous) previous.next = node.next; | |
else if (parent) parent[i] = node.next; | |
else this._root = node.next; | |
return true; | |
} | |
function quadtree(x0, y0, x1, y1) { | |
if (arguments.length === 2) x1 = x0, y1 = y0, x0 = y0 = 0; | |
return new Quadtree(x0, y0, x1, y1); | |
} | |
function Quadtree(x0, y0, x1, y1) { | |
var dx = (x1 = +x1) - (x0 = +x0), dy = (y1 = +y1) - (y0 = +y0); | |
if (dy > dx) x1 = (x0 -= (dy - dx) / 2) + dy; | |
else y1 = (y0 -= (dx - dy) / 2) + dx; | |
this._x0 = x0, this._y0 = y0; | |
this._x1 = x1, this._y1 = y1; | |
this._root = isNaN(dx) || isNaN(dy) ? undefined : new Array(4); | |
} | |
var treeProto = Quadtree.prototype = quadtree.prototype; | |
treeProto.add = tree_add; | |
treeProto.visit = tree_visit; | |
treeProto.visitAfter = tree_visitAfter; | |
treeProto.find = tree_find; | |
treeProto.remove = tree_remove; | |
exports.version = version; | |
exports.quadtree = quadtree; | |
})); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<!DOCTYPE html> | |
<meta charset="utf-8"> | |
<style> | |
canvas { | |
position: absolute; | |
} | |
</style> | |
<canvas id="quadtree" width="960" height="960"></canvas> | |
<canvas id="points" width="960" height="960"></canvas> | |
<script src="d3-quadtree.debug.js"></script> | |
<script src="https://d3js.org/d3-timer.v0.4.min.js"></script> | |
<script src="https://d3js.org/d3-random.v0.2.min.js"></script> | |
<script> | |
var canvas0 = document.querySelector("#quadtree"), | |
canvas1 = document.querySelector("#points"); | |
var width = canvas0.width, | |
height = canvas0.height, | |
radius = 1.5, | |
scale = window.devicePixelRatio; | |
if (scale > 1) { | |
canvas1.style.width = canvas0.style.width = width + "px"; | |
canvas1.style.height = canvas0.style.height = height + "px"; | |
canvas1.width = canvas0.width = width *= scale; | |
canvas1.height = canvas0.height = height *= scale; | |
radius *= scale; | |
} | |
var randomX = d3_random.randomNormal(width / 2, 100 * scale), | |
randomY = d3_random.randomNormal(height / 2, 100 * scale); | |
var quadtree = d3_quadtree.quadtree(width - 1, height - 1), | |
x0 = quadtree._x0, | |
y0 = quadtree._y0, | |
x1 = quadtree._x1, | |
y1 = quadtree._y1; | |
var context0 = canvas0.getContext("2d"), | |
context1 = canvas1.getContext("2d"); | |
context0.fillStyle = "rgba(0,0,0,0.1)"; | |
context0.strokeStyle = "#666"; | |
context1.fillStyle = "rgba(240,0,0,1)"; | |
context1.globalCompositeOperation = "multiply"; | |
redraw({node: quadtree._root, x0: x0, y0: y0, x1: x1, y1: y1}); | |
d3_timer.timer(function() { | |
var x, y; | |
do x = randomX(), y = randomY(); | |
while (x0 > x || x > x1 || y0 > y || y > y1); | |
quadtree.add({x: x, y: y}); | |
redraw(quadtree._added); | |
context1.beginPath(); | |
context1.arc(x, y, radius, 0, 2 * Math.PI); | |
context1.fill(); | |
}); | |
function redraw(quad) { | |
var quads = [], xm, ym, x0 = quad.x0, y0 = quad.y0, x1 = quad.x1, y1 = quad.y1; | |
context0.clearRect(round(x0), round(y0), round(x1) - round(x0), round(y1) - round(y0)); | |
quads.push(quad); | |
while (quad = quads.pop()) { | |
node = quad.node, x0 = quad.x0, y0 = quad.y0, x1 = quad.x1, y1 = quad.y1; | |
if (node.point) context0.fillRect(round(x0), round(y0), round(x1) - round(x0), round(y1) - round(y0)); | |
xm = (x0 + x1) / 2, ym = (y0 + y1) / 2; | |
if (child = node[3]) quads.push({node: child, x0: xm, y0: ym, x1: x1, y1: y1}); | |
if (child = node[2]) quads.push({node: child, x0: x0, y0: ym, x1: xm, y1: y1}); | |
if (child = node[1]) quads.push({node: child, x0: xm, y0: y0, x1: x1, y1: ym}); | |
if (child = node[0]) quads.push({node: child, x0: x0, y0: y0, x1: xm, y1: ym}); | |
context0.strokeRect(round(x0) + 0.5, round(y0) + 0.5, round(x1) - round(x0), round(y1) - round(y0)); | |
} | |
} | |
function round(x) { | |
return Math.round(x); | |
} | |
</script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment