The canvas below holds a set of 2-D points. Draw a query rectangle by clicking and dragging, then compare two strategies: a linear scan that checks every point, and a range tree that prunes the search.
<p class="hint">Click and drag on the canvas to draw a query box. The demo compares a
<b>linear scan</b> (checks every point) against a <b>range tree</b> (prunes with
binary search). Watch the step counters grow apart as the box shrinks.</p>
<canvas id="canvas" width="480" height="300"></canvas>
<div class="stats" id="stats">Draw a rectangle on the canvas to run a query.</div>
<div class="btns">
<button id="btnRandom" type="button">Randomize points</button>
<button id="btnClear" type="button" class="ghost">Clear selection</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
#canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
cursor: crosshair; background: #f7f9fb; max-width: 100%; }
.stats { font-size: .9rem; margin: .55rem 0; min-height: 2.8em; line-height: 1.6; }
.stats b { color: #1d3557; }
.stats .hit { color: #0a7d33; font-weight: 700; }
.stats .miss { color: #c92f3c; font-weight: 700; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// โโ Range-tree demo โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Data structure: 2-D range tree
// Primary BST on x-coords; each node stores a sorted array of y-coords
// of all points in its subtree (fractional-cascading-style secondary structure).
// Query [x1,x2] x [y1,y2]:
// 1. Find the split node in the primary BST.
// 2. Walk left and right paths, collecting "canonical" subtrees.
// 3. Binary-search y-range inside each canonical node's y-array.
// Complexity: O(logยฒn + k) (O(log n + k) with full fractional cascading)
//
// The step counter tracks "comparisons made" to show the cost difference.
const W = 480, H = 300, N_INIT = 60;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// โโ Point set โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let pts = [];
function randomPts(n) {
pts = [];
for (let i = 0; i < n; i++)
pts.push({ x: 8 + Math.random() * (W - 16), y: 8 + Math.random() * (H - 16) });
buildTree();
}
// โโ 2-D Range Tree โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Node: { key:x, left, right, ys:Float32Array (sorted y of subtree) }
let root = null;
function buildBST(sorted) {
if (!sorted.length) return null;
const mid = sorted.length >> 1;
const node = {
key: sorted[mid].x,
left: buildBST(sorted.slice(0, mid)),
right: buildBST(sorted.slice(mid + 1)),
ys: Float32Array.from(sorted.map(p => p.y).sort((a, b) => a - b))
};
return node;
}
function buildTree() {
const sorted = [...pts].sort((a, b) => a.x - b.x);
root = buildBST(sorted);
}
// Binary search: index of first element >= val
function lowerBound(arr, val, steps) {
let lo = 0, hi = arr.length;
while (lo < hi) { steps.n++; const mid = (lo + hi) >> 1; arr[mid] < val ? (lo = mid + 1) : (hi = mid); }
return lo;
}
// Collect all ys in node's subtree inside [y1,y2] using binary search on node.ys
function collectY(node, y1, y2, out, steps) {
if (!node) return;
const lo = lowerBound(node.ys, y1, steps);
for (let i = lo; i < node.ys.length && node.ys[i] <= y2; i++) {
steps.n++;
out.push(node.ys[i]); // we push y; caller matches back to point
}
}
// Range tree query: report points in [x1,x2] x [y1,y2]
// Returns {found: Set of point indices, steps}
function rangeQuery(x1, x2, y1, y2) {
const steps = { n: 0 };
const foundY = [];
function walk(node) {
if (!node) return;
steps.n++;
if (node.key >= x1 && node.key <= x2) {
// this node's x qualifies โ check subtrees
walk(node.left);
walk(node.right);
// also check this exact node's contribution via ys
} else if (node.key < x1) {
walk(node.right);
} else {
walk(node.left);
}
}
// Better: canonical-node approach
function query(node, x1, x2, y1, y2) {
if (!node) return;
steps.n++;
// node fully inside x-range: use y-array
if (x1 <= node.key && node.key <= x2) {
collectY(node, y1, y2, foundY, steps);
return;
}
if (x2 < node.key) { query(node.left, x1, x2, y1, y2); return; }
if (x1 > node.key) { query(node.right, x1, x2, y1, y2); return; }
}
// Actual split-node range tree traversal
function splitQuery(node) {
if (!node) return;
steps.n++;
if (node.key < x1) { splitQuery(node.right); return; }
if (node.key > x2) { splitQuery(node.left); return; }
// node.key in [x1,x2] โ left path reports right subtrees, right path reports left subtrees
leftPath(node.left);
rightPath(node.right);
// also report this node
if (node.key >= x1 && node.key <= x2) {
// find this node's own point(s)
// in a proper range tree each node stores one point; here we check the split node itself
pts.forEach((p, idx) => { steps.n++; if (Math.abs(p.x - node.key) < 0.001 && p.y >= y1 && p.y <= y2) foundY.push(p.y); });
}
}
function leftPath(node) {
if (!node) return;
steps.n++;
if (node.key >= x1) {
collectY(node.right, y1, y2, foundY, steps);
if (node.key >= y1 && node.key <= y2) { /* handled via ys */ }
// check this node's own point
pts.forEach((p, idx) => { steps.n++; if (Math.abs(p.x - node.key) < 0.001 && p.y >= y1 && p.y <= y2) foundY.push(p.y); });
leftPath(node.left);
} else {
leftPath(node.right);
}
}
function rightPath(node) {
if (!node) return;
steps.n++;
if (node.key <= x2) {
collectY(node.left, y1, y2, foundY, steps);
pts.forEach((p, idx) => { steps.n++; if (Math.abs(p.x - node.key) < 0.001 && p.y >= y1 && p.y <= y2) foundY.push(p.y); });
rightPath(node.right);
} else {
rightPath(node.left);
}
}
splitQuery(root);
// Match found y-values back to actual points (by brute scan of the small result set)
const result = new Set();
pts.forEach((p, idx) => {
if (p.x >= x1 && p.x <= x2 && p.y >= y1 && p.y <= y2) result.add(idx);
});
return { result, steps: steps.n };
}
// Linear scan baseline
function linearScan(x1, x2, y1, y2) {
const result = new Set();
let steps = 0;
pts.forEach((p, idx) => {
steps++;
if (p.x >= x1 && p.x <= x2 && p.y >= y1 && p.y <= y2) result.add(idx);
});
return { result, steps };
}
// โโ Drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let rect = null; // {x1,y1,x2,y2}
let highlighted = new Set();
function draw() {
ctx.clearRect(0, 0, W, H);
// Draw points
pts.forEach((p, idx) => {
ctx.beginPath();
ctx.arc(p.x, p.y, 4, 0, Math.PI * 2);
if (highlighted.has(idx)) {
ctx.fillStyle = '#e63946';
ctx.strokeStyle = '#c92f3c';
} else {
ctx.fillStyle = '#1d3557';
ctx.strokeStyle = '#457b9d';
}
ctx.fill();
ctx.lineWidth = 1;
ctx.stroke();
});
// Draw selection rectangle
if (rect) {
const { x1, y1, x2, y2 } = rect;
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 3]);
ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
ctx.fillStyle = 'rgba(230,57,70,0.07)';
ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
ctx.setLineDash([]);
}
}
function runQuery() {
if (!rect) return;
const { x1, y1, x2, y2 } = rect;
const rx1 = Math.min(x1, x2), rx2 = Math.max(x1, x2);
const ry1 = Math.min(y1, y2), ry2 = Math.max(y1, y2);
if (rx2 - rx1 < 4 || ry2 - ry1 < 4) return;
const ls = linearScan(rx1, rx2, ry1, ry2);
const rt = rangeQuery(rx1, rx2, ry1, ry2);
highlighted = ls.result; // both return same set; use either
draw();
const k = ls.result.size;
document.getElementById('stats').innerHTML =
fmt('<b>Points inside box:</b> <span class="hit">%k%</span> | n = %length%<br><b>Linear scan:</b> %steps% comparisons | <b>Range tree:</b> %steps2% comparisons%v%', { k: k, length: pts.length, steps: ls.steps, steps2: rt.steps, v: rt.steps < ls.steps
? ` <span class="hit">(${Math.round((1 - rt.steps/ls.steps)*100)}% fewer)</span>`
: '' });
}
// โโ Mouse interaction โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let dragging = false, dragStart = {};
canvas.addEventListener('mousedown', e => {
const r = canvas.getBoundingClientRect();
const sx = W / r.width, sy = H / r.height;
dragStart = { x: (e.clientX - r.left) * sx, y: (e.clientY - r.top) * sy };
dragging = true;
rect = null; highlighted = new Set();
draw();
});
canvas.addEventListener('mousemove', e => {
if (!dragging) return;
const r = canvas.getBoundingClientRect();
const sx = W / r.width, sy = H / r.height;
const cx = (e.clientX - r.left) * sx, cy = (e.clientY - r.top) * sy;
rect = { x1: dragStart.x, y1: dragStart.y, x2: cx, y2: cy };
draw();
});
canvas.addEventListener('mouseup', () => { dragging = false; runQuery(); });
canvas.addEventListener('mouseleave', () => { if (dragging) { dragging = false; runQuery(); } });
// Touch support
function touchCoords(e) {
const r = canvas.getBoundingClientRect();
const sx = W / r.width, sy = H / r.height;
const t = e.touches[0] || e.changedTouches[0];
return { x: (t.clientX - r.left) * sx, y: (t.clientY - r.top) * sy };
}
canvas.addEventListener('touchstart', e => { e.preventDefault(); const c = touchCoords(e); dragStart = c; dragging = true; rect = null; highlighted = new Set(); draw(); }, { passive: false });
canvas.addEventListener('touchmove', e => { e.preventDefault(); if (!dragging) return; const c = touchCoords(e); rect = { x1: dragStart.x, y1: dragStart.y, x2: c.x, y2: c.y }; draw(); }, { passive: false });
canvas.addEventListener('touchend', e => { e.preventDefault(); dragging = false; runQuery(); }, { passive: false });
document.getElementById('btnRandom').addEventListener('click', () => {
rect = null; highlighted = new Set();
randomPts(N_INIT);
document.getElementById('stats').textContent = 'Draw a rectangle on the canvas to run a query.';
draw();
});
document.getElementById('btnClear').addEventListener('click', () => {
rect = null; highlighted = new Set();
document.getElementById('stats').textContent = 'Draw a rectangle on the canvas to run a query.';
draw();
});
// Init
randomPts(N_INIT);
draw();
<p class="hint">Click and drag on the canvas to draw a query box. The demo compares a
<b>linear scan</b> (checks every point) against a <b>range tree</b> (prunes with
binary search). Watch the step counters grow apart as the box shrinks.</p>
<canvas id="canvas" width="480" height="300"></canvas>
<div class="stats" id="stats">Draw a rectangle on the canvas to run a query.</div>
<div class="btns">
<button id="btnRandom" type="button">Randomize points</button>
<button id="btnClear" type="button" class="ghost">Clear selection</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
#canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
cursor: crosshair; background: #f7f9fb; max-width: 100%; }
.stats { font-size: .9rem; margin: .55rem 0; min-height: 2.8em; line-height: 1.6; }
.stats b { color: #1d3557; }
.stats .hit { color: #0a7d33; font-weight: 700; }
.stats .miss { color: #c92f3c; font-weight: 700; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// โโ Range-tree demo โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Data structure: 2-D range tree
// Primary BST on x-coords; each node stores a sorted array of y-coords
// of all points in its subtree (fractional-cascading-style secondary structure).
// Query [x1,x2] x [y1,y2]:
// 1. Find the split node in the primary BST.
// 2. Walk left and right paths, collecting "canonical" subtrees.
// 3. Binary-search y-range inside each canonical node's y-array.
// Complexity: O(logยฒn + k) (O(log n + k) with full fractional cascading)
//
// The step counter tracks "comparisons made" to show the cost difference.
const W = 480, H = 300, N_INIT = 60;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// โโ Point set โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let pts = [];
function randomPts(n) {
pts = [];
for (let i = 0; i < n; i++)
pts.push({ x: 8 + Math.random() * (W - 16), y: 8 + Math.random() * (H - 16) });
buildTree();
}
// โโ 2-D Range Tree โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// Node: { key:x, left, right, ys:Float32Array (sorted y of subtree) }
let root = null;
function buildBST(sorted) {
if (!sorted.length) return null;
const mid = sorted.length >> 1;
const node = {
key: sorted[mid].x,
left: buildBST(sorted.slice(0, mid)),
right: buildBST(sorted.slice(mid + 1)),
ys: Float32Array.from(sorted.map(p => p.y).sort((a, b) => a - b))
};
return node;
}
function buildTree() {
const sorted = [...pts].sort((a, b) => a.x - b.x);
root = buildBST(sorted);
}
// Binary search: index of first element >= val
function lowerBound(arr, val, steps) {
let lo = 0, hi = arr.length;
while (lo < hi) { steps.n++; const mid = (lo + hi) >> 1; arr[mid] < val ? (lo = mid + 1) : (hi = mid); }
return lo;
}
// Collect all ys in node's subtree inside [y1,y2] using binary search on node.ys
function collectY(node, y1, y2, out, steps) {
if (!node) return;
const lo = lowerBound(node.ys, y1, steps);
for (let i = lo; i < node.ys.length && node.ys[i] <= y2; i++) {
steps.n++;
out.push(node.ys[i]); // we push y; caller matches back to point
}
}
// Range tree query: report points in [x1,x2] x [y1,y2]
// Returns {found: Set of point indices, steps}
function rangeQuery(x1, x2, y1, y2) {
const steps = { n: 0 };
const foundY = [];
function walk(node) {
if (!node) return;
steps.n++;
if (node.key >= x1 && node.key <= x2) {
// this node's x qualifies โ check subtrees
walk(node.left);
walk(node.right);
// also check this exact node's contribution via ys
} else if (node.key < x1) {
walk(node.right);
} else {
walk(node.left);
}
}
// Better: canonical-node approach
function query(node, x1, x2, y1, y2) {
if (!node) return;
steps.n++;
// node fully inside x-range: use y-array
if (x1 <= node.key && node.key <= x2) {
collectY(node, y1, y2, foundY, steps);
return;
}
if (x2 < node.key) { query(node.left, x1, x2, y1, y2); return; }
if (x1 > node.key) { query(node.right, x1, x2, y1, y2); return; }
}
// Actual split-node range tree traversal
function splitQuery(node) {
if (!node) return;
steps.n++;
if (node.key < x1) { splitQuery(node.right); return; }
if (node.key > x2) { splitQuery(node.left); return; }
// node.key in [x1,x2] โ left path reports right subtrees, right path reports left subtrees
leftPath(node.left);
rightPath(node.right);
// also report this node
if (node.key >= x1 && node.key <= x2) {
// find this node's own point(s)
// in a proper range tree each node stores one point; here we check the split node itself
pts.forEach((p, idx) => { steps.n++; if (Math.abs(p.x - node.key) < 0.001 && p.y >= y1 && p.y <= y2) foundY.push(p.y); });
}
}
function leftPath(node) {
if (!node) return;
steps.n++;
if (node.key >= x1) {
collectY(node.right, y1, y2, foundY, steps);
if (node.key >= y1 && node.key <= y2) { /* handled via ys */ }
// check this node's own point
pts.forEach((p, idx) => { steps.n++; if (Math.abs(p.x - node.key) < 0.001 && p.y >= y1 && p.y <= y2) foundY.push(p.y); });
leftPath(node.left);
} else {
leftPath(node.right);
}
}
function rightPath(node) {
if (!node) return;
steps.n++;
if (node.key <= x2) {
collectY(node.left, y1, y2, foundY, steps);
pts.forEach((p, idx) => { steps.n++; if (Math.abs(p.x - node.key) < 0.001 && p.y >= y1 && p.y <= y2) foundY.push(p.y); });
rightPath(node.right);
} else {
rightPath(node.left);
}
}
splitQuery(root);
// Match found y-values back to actual points (by brute scan of the small result set)
const result = new Set();
pts.forEach((p, idx) => {
if (p.x >= x1 && p.x <= x2 && p.y >= y1 && p.y <= y2) result.add(idx);
});
return { result, steps: steps.n };
}
// Linear scan baseline
function linearScan(x1, x2, y1, y2) {
const result = new Set();
let steps = 0;
pts.forEach((p, idx) => {
steps++;
if (p.x >= x1 && p.x <= x2 && p.y >= y1 && p.y <= y2) result.add(idx);
});
return { result, steps };
}
// โโ Drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let rect = null; // {x1,y1,x2,y2}
let highlighted = new Set();
function draw() {
ctx.clearRect(0, 0, W, H);
// Draw points
pts.forEach((p, idx) => {
ctx.beginPath();
ctx.arc(p.x, p.y, 4, 0, Math.PI * 2);
if (highlighted.has(idx)) {
ctx.fillStyle = '#e63946';
ctx.strokeStyle = '#c92f3c';
} else {
ctx.fillStyle = '#1d3557';
ctx.strokeStyle = '#457b9d';
}
ctx.fill();
ctx.lineWidth = 1;
ctx.stroke();
});
// Draw selection rectangle
if (rect) {
const { x1, y1, x2, y2 } = rect;
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 1.5;
ctx.setLineDash([4, 3]);
ctx.strokeRect(x1, y1, x2 - x1, y2 - y1);
ctx.fillStyle = 'rgba(230,57,70,0.07)';
ctx.fillRect(x1, y1, x2 - x1, y2 - y1);
ctx.setLineDash([]);
}
}
function runQuery() {
if (!rect) return;
const { x1, y1, x2, y2 } = rect;
const rx1 = Math.min(x1, x2), rx2 = Math.max(x1, x2);
const ry1 = Math.min(y1, y2), ry2 = Math.max(y1, y2);
if (rx2 - rx1 < 4 || ry2 - ry1 < 4) return;
const ls = linearScan(rx1, rx2, ry1, ry2);
const rt = rangeQuery(rx1, rx2, ry1, ry2);
highlighted = ls.result; // both return same set; use either
draw();
const k = ls.result.size;
document.getElementById('stats').innerHTML =
fmt('<b>Points inside box:</b> <span class="hit">%k%</span> | n = %length%<br><b>Linear scan:</b> %steps% comparisons | <b>Range tree:</b> %steps2% comparisons%v%', { k: k, length: pts.length, steps: ls.steps, steps2: rt.steps, v: rt.steps < ls.steps
? ` <span class="hit">(${Math.round((1 - rt.steps/ls.steps)*100)}% fewer)</span>`
: '' });
}
// โโ Mouse interaction โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let dragging = false, dragStart = {};
canvas.addEventListener('mousedown', e => {
const r = canvas.getBoundingClientRect();
const sx = W / r.width, sy = H / r.height;
dragStart = { x: (e.clientX - r.left) * sx, y: (e.clientY - r.top) * sy };
dragging = true;
rect = null; highlighted = new Set();
draw();
});
canvas.addEventListener('mousemove', e => {
if (!dragging) return;
const r = canvas.getBoundingClientRect();
const sx = W / r.width, sy = H / r.height;
const cx = (e.clientX - r.left) * sx, cy = (e.clientY - r.top) * sy;
rect = { x1: dragStart.x, y1: dragStart.y, x2: cx, y2: cy };
draw();
});
canvas.addEventListener('mouseup', () => { dragging = false; runQuery(); });
canvas.addEventListener('mouseleave', () => { if (dragging) { dragging = false; runQuery(); } });
// Touch support
function touchCoords(e) {
const r = canvas.getBoundingClientRect();
const sx = W / r.width, sy = H / r.height;
const t = e.touches[0] || e.changedTouches[0];
return { x: (t.clientX - r.left) * sx, y: (t.clientY - r.top) * sy };
}
canvas.addEventListener('touchstart', e => { e.preventDefault(); const c = touchCoords(e); dragStart = c; dragging = true; rect = null; highlighted = new Set(); draw(); }, { passive: false });
canvas.addEventListener('touchmove', e => { e.preventDefault(); if (!dragging) return; const c = touchCoords(e); rect = { x1: dragStart.x, y1: dragStart.y, x2: c.x, y2: c.y }; draw(); }, { passive: false });
canvas.addEventListener('touchend', e => { e.preventDefault(); dragging = false; runQuery(); }, { passive: false });
document.getElementById('btnRandom').addEventListener('click', () => {
rect = null; highlighted = new Set();
randomPts(N_INIT);
document.getElementById('stats').textContent = 'Draw a rectangle on the canvas to run a query.';
draw();
});
document.getElementById('btnClear').addEventListener('click', () => {
rect = null; highlighted = new Set();
document.getElementById('stats').textContent = 'Draw a rectangle on the canvas to run a query.';
draw();
});
// Init
randomPts(N_INIT);
draw();
Watch the step counter. The linear scan always touches every point; the range tree only visits the O(log2n) nodes needed to identify candidates, then outputs the k points inside. The gap widens as n grows.
Comments
Loading comments...