The demo below shows a set of reservations (colored bars) on a time line. Drag the vertical query line to any position and the interval tree instantly reports every reservation that covers that moment โ highlighted in orange.
<div class="hint">
<b>Drag the query line</b> to find every reservation that covers that moment.
The interval tree skips branches whose max endpoint falls below the query point.
</div>
<div id="vis-wrap">
<canvas id="vis" width="560" height="200"></canvas>
</div>
<div class="controls">
<button id="addBtn" type="button">Add reservation</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div id="info" class="info"></div>
<div id="log" class="log"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px 4px; }
.hint { font-size: .85rem; color: #555; margin: 0 0 .6rem; line-height: 1.4; }
#vis-wrap { overflow-x: auto; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
background: #f7f9fb; cursor: ew-resize; max-width: 100%; }
.controls { display: flex; gap: .5rem; margin: .6rem 0; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.info { font-size: .9rem; font-weight: 600; min-height: 1.4em; margin-bottom: .3rem; }
.info.hit { color: #c45200; }
.info.miss { color: #555; }
.log { font-size: .78rem; font-family: ui-monospace, monospace; color: #447; line-height: 1.5;
background: #eef2f7; border-radius: 6px; padding: .4rem .6rem; min-height: 2.4em;
white-space: pre-wrap; word-break: break-all; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Interval tree demo โ stabbing query: which intervals contain point x?
// Each node stores: interval [lo,hi], key=lo, maxHi = max right endpoint in subtree.
// Query: prune any subtree whose maxHi < x; report node if lo <= x <= hi.
const PAD = 28, W = 560, ROW_H = 22, HEADER = 30, RULER_H = 24;
const TMIN = 0, TMAX = 24; // hour scale
const COLORS = ['#4e79a7','#f28e2b','#76b7b2','#59a14f','#edc948',
'#b07aa1','#ff9da7','#9c755f','#bab0ac','#e15759'];
// Default reservations
let intervals = [
{lo:2, hi:6, label:'Room A'},
{lo:4, hi:10, label:'Room B'},
{lo:8, hi:14, label:'Hall C'},
{lo:11, hi:16, label:'Room D'},
{lo:13, hi:19, label:'Hall E'},
{lo:17, hi:22, label:'Room F'},
{lo:1, hi:5, label:'Suite G'},
];
let queryX = 12; // current query point (hours)
let dragging = false;
let tree = null;
let traversalLog = [];
let hitSet = new Set();
// โโ Interval Tree โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function buildTree(ivs) {
if (!ivs.length) return null;
// sort by lo endpoint, pick median as root for balance
const sorted = [...ivs].sort((a,b)=>a.lo-b.lo);
const mid = Math.floor(sorted.length / 2);
const node = { iv: sorted[mid], left: null, right: null, maxHi: 0 };
node.left = buildTree(sorted.slice(0, mid));
node.right = buildTree(sorted.slice(mid + 1));
node.maxHi = Math.max(
node.iv.hi,
node.left ? node.left.maxHi : -Infinity,
node.right ? node.right.maxHi : -Infinity
);
return node;
}
function stabQuery(node, x, results, log) {
if (!node) return;
log.push(fmt(`visit [%lo%,%hi%] maxHi=%hi% subtreeMax=%maxHi%`, { lo: node.iv.lo, hi: node.iv.hi, maxHi: node.maxHi }));
if (node.maxHi < x) { log.push(' ' + `โ prune (subtree max < x)`); return; }
if (node.iv.lo <= x && x <= node.iv.hi) {
results.push(node.iv);
log.push(" " + fmt(`โ HIT: "%label%"`, { label: node.iv.label }));
}
stabQuery(node.left, x, results, log);
stabQuery(node.right, x, results, log);
}
// โโ Canvas rendering โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const canvas = document.getElementById('vis');
const ctx = canvas.getContext('2d');
function toPixel(t) { return PAD + (t - TMIN) / (TMAX - TMIN) * (W - 2*PAD); }
function draw() {
// Resize canvas height to fit all intervals
const H = HEADER + intervals.length * ROW_H + RULER_H + 10;
canvas.height = H;
ctx.clearRect(0, 0, W, H);
// Ruler
ctx.fillStyle = '#c8d4de';
ctx.fillRect(PAD, H - RULER_H - 4, W - 2*PAD, 1);
ctx.fillStyle = '#667';
ctx.font = '10px system-ui';
ctx.textAlign = 'center';
for (let t = TMIN; t <= TMAX; t += 2) {
const px = toPixel(t);
ctx.fillStyle = '#c8d4de';
ctx.fillRect(px, H - RULER_H, 1, 6);
ctx.fillStyle = '#667';
ctx.fillText(t+'h', px, H - 2);
}
// Header
ctx.fillStyle = '#334';
ctx.font = '600 11px system-ui';
ctx.textAlign = 'left';
ctx.fillText(`Reservations`, 2, 14);
// Interval bars
intervals.forEach((iv, i) => {
const y = HEADER + i * ROW_H + 4;
const x1 = toPixel(iv.lo), x2 = toPixel(iv.hi);
const isHit = hitSet.has(iv);
ctx.fillStyle = isHit ? '#e07030' : COLORS[i % COLORS.length];
ctx.globalAlpha = isHit ? 1 : 0.65;
ctx.beginPath();
ctx.roundRect(x1, y, x2 - x1, ROW_H - 6, 4);
ctx.fill();
ctx.globalAlpha = 1;
// label
ctx.fillStyle = '#fff';
ctx.font = '600 10px system-ui';
ctx.textAlign = 'center';
const mid = (x1+x2)/2;
ctx.fillText(iv.label, mid, y + ROW_H - 12);
});
// Query line
const qx = toPixel(queryX);
ctx.strokeStyle = '#c92f3c';
ctx.lineWidth = 2;
ctx.setLineDash([5,3]);
ctx.beginPath();
ctx.moveTo(qx, HEADER - 6);
ctx.lineTo(qx, H - RULER_H - 4);
ctx.stroke();
ctx.setLineDash([]);
// Query label
ctx.fillStyle = '#c92f3c';
ctx.font = '700 11px system-ui';
ctx.textAlign = 'center';
ctx.fillText('x='+queryX.toFixed(1)+'h', qx, HEADER - 10);
runQuery();
}
function runQuery() {
traversalLog = [];
const results = [];
stabQuery(tree, queryX, results, traversalLog);
hitSet = new Set(results);
const info = document.getElementById('info');
if (results.length) {
info.textContent = fmt(`%length% reservation(s) hit: %map%`, { length: results.length, map: results.map(r=>r.label).join(', ') });
info.className = 'info hit';
} else {
info.textContent = fmt(`No reservations cover x = %queryX%h`, { queryX: queryX.toFixed(1) });
info.className = 'info miss';
}
document.getElementById('log').textContent =
fmt(`Tree traversal (O(log n + k)):
%traversalLog%`, { traversalLog: traversalLog.join('\n') });
}
// โโ Mouse/touch drag โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function pixelToTime(px) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const cx = (px - rect.left) * scaleX;
const t = TMIN + (cx - PAD) / (W - 2*PAD) * (TMAX - TMIN);
return Math.max(TMIN, Math.min(TMAX, Math.round(t * 2) / 2)); // snap to 0.5h
}
canvas.addEventListener('mousedown', e => { dragging=true; queryX=pixelToTime(e.clientX); draw(); });
canvas.addEventListener('mousemove', e => { if(dragging){ queryX=pixelToTime(e.clientX); draw(); }});
window.addEventListener('mouseup', () => dragging=false);
canvas.addEventListener('touchstart', e => { dragging=true; queryX=pixelToTime(e.touches[0].clientX); draw(); e.preventDefault(); });
canvas.addEventListener('touchmove', e => { if(dragging){ queryX=pixelToTime(e.touches[0].clientX); draw(); } e.preventDefault(); });
window.addEventListener('touchend', () => dragging=false);
// โโ Buttons โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const ROOMS = ['Conf. Room H','Suite I','Ballroom J','Room K','Loft L','Studio M'];
let addCount = 0;
document.getElementById('addBtn').addEventListener('click', () => {
const lo = Math.floor(Math.random() * 20);
const hi = lo + 1 + Math.floor(Math.random() * 5);
const label = ROOMS[addCount % ROOMS.length];
addCount++;
intervals.push({lo, hi, label});
tree = buildTree(intervals);
draw();
});
document.getElementById('resetBtn').addEventListener('click', () => {
intervals = [
{lo:2, hi:6, label:'Room A'},
{lo:4, hi:10, label:'Room B'},
{lo:8, hi:14, label:'Hall C'},
{lo:11, hi:16, label:'Room D'},
{lo:13, hi:19, label:'Hall E'},
{lo:17, hi:22, label:'Room F'},
{lo:1, hi:5, label:'Suite G'},
];
addCount = 0; queryX = 12;
tree = buildTree(intervals);
draw();
});
// โโ Init โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
tree = buildTree(intervals);
draw();
<div class="hint">
<b>Drag the query line</b> to find every reservation that covers that moment.
The interval tree skips branches whose max endpoint falls below the query point.
</div>
<div id="vis-wrap">
<canvas id="vis" width="560" height="200"></canvas>
</div>
<div class="controls">
<button id="addBtn" type="button">Add reservation</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div id="info" class="info"></div>
<div id="log" class="log"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px 4px; }
.hint { font-size: .85rem; color: #555; margin: 0 0 .6rem; line-height: 1.4; }
#vis-wrap { overflow-x: auto; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
background: #f7f9fb; cursor: ew-resize; max-width: 100%; }
.controls { display: flex; gap: .5rem; margin: .6rem 0; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.info { font-size: .9rem; font-weight: 600; min-height: 1.4em; margin-bottom: .3rem; }
.info.hit { color: #c45200; }
.info.miss { color: #555; }
.log { font-size: .78rem; font-family: ui-monospace, monospace; color: #447; line-height: 1.5;
background: #eef2f7; border-radius: 6px; padding: .4rem .6rem; min-height: 2.4em;
white-space: pre-wrap; word-break: break-all; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Interval tree demo โ stabbing query: which intervals contain point x?
// Each node stores: interval [lo,hi], key=lo, maxHi = max right endpoint in subtree.
// Query: prune any subtree whose maxHi < x; report node if lo <= x <= hi.
const PAD = 28, W = 560, ROW_H = 22, HEADER = 30, RULER_H = 24;
const TMIN = 0, TMAX = 24; // hour scale
const COLORS = ['#4e79a7','#f28e2b','#76b7b2','#59a14f','#edc948',
'#b07aa1','#ff9da7','#9c755f','#bab0ac','#e15759'];
// Default reservations
let intervals = [
{lo:2, hi:6, label:'Room A'},
{lo:4, hi:10, label:'Room B'},
{lo:8, hi:14, label:'Hall C'},
{lo:11, hi:16, label:'Room D'},
{lo:13, hi:19, label:'Hall E'},
{lo:17, hi:22, label:'Room F'},
{lo:1, hi:5, label:'Suite G'},
];
let queryX = 12; // current query point (hours)
let dragging = false;
let tree = null;
let traversalLog = [];
let hitSet = new Set();
// โโ Interval Tree โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function buildTree(ivs) {
if (!ivs.length) return null;
// sort by lo endpoint, pick median as root for balance
const sorted = [...ivs].sort((a,b)=>a.lo-b.lo);
const mid = Math.floor(sorted.length / 2);
const node = { iv: sorted[mid], left: null, right: null, maxHi: 0 };
node.left = buildTree(sorted.slice(0, mid));
node.right = buildTree(sorted.slice(mid + 1));
node.maxHi = Math.max(
node.iv.hi,
node.left ? node.left.maxHi : -Infinity,
node.right ? node.right.maxHi : -Infinity
);
return node;
}
function stabQuery(node, x, results, log) {
if (!node) return;
log.push(fmt(`visit [%lo%,%hi%] maxHi=%hi% subtreeMax=%maxHi%`, { lo: node.iv.lo, hi: node.iv.hi, maxHi: node.maxHi }));
if (node.maxHi < x) { log.push(' ' + `โ prune (subtree max < x)`); return; }
if (node.iv.lo <= x && x <= node.iv.hi) {
results.push(node.iv);
log.push(" " + fmt(`โ HIT: "%label%"`, { label: node.iv.label }));
}
stabQuery(node.left, x, results, log);
stabQuery(node.right, x, results, log);
}
// โโ Canvas rendering โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const canvas = document.getElementById('vis');
const ctx = canvas.getContext('2d');
function toPixel(t) { return PAD + (t - TMIN) / (TMAX - TMIN) * (W - 2*PAD); }
function draw() {
// Resize canvas height to fit all intervals
const H = HEADER + intervals.length * ROW_H + RULER_H + 10;
canvas.height = H;
ctx.clearRect(0, 0, W, H);
// Ruler
ctx.fillStyle = '#c8d4de';
ctx.fillRect(PAD, H - RULER_H - 4, W - 2*PAD, 1);
ctx.fillStyle = '#667';
ctx.font = '10px system-ui';
ctx.textAlign = 'center';
for (let t = TMIN; t <= TMAX; t += 2) {
const px = toPixel(t);
ctx.fillStyle = '#c8d4de';
ctx.fillRect(px, H - RULER_H, 1, 6);
ctx.fillStyle = '#667';
ctx.fillText(t+'h', px, H - 2);
}
// Header
ctx.fillStyle = '#334';
ctx.font = '600 11px system-ui';
ctx.textAlign = 'left';
ctx.fillText(`Reservations`, 2, 14);
// Interval bars
intervals.forEach((iv, i) => {
const y = HEADER + i * ROW_H + 4;
const x1 = toPixel(iv.lo), x2 = toPixel(iv.hi);
const isHit = hitSet.has(iv);
ctx.fillStyle = isHit ? '#e07030' : COLORS[i % COLORS.length];
ctx.globalAlpha = isHit ? 1 : 0.65;
ctx.beginPath();
ctx.roundRect(x1, y, x2 - x1, ROW_H - 6, 4);
ctx.fill();
ctx.globalAlpha = 1;
// label
ctx.fillStyle = '#fff';
ctx.font = '600 10px system-ui';
ctx.textAlign = 'center';
const mid = (x1+x2)/2;
ctx.fillText(iv.label, mid, y + ROW_H - 12);
});
// Query line
const qx = toPixel(queryX);
ctx.strokeStyle = '#c92f3c';
ctx.lineWidth = 2;
ctx.setLineDash([5,3]);
ctx.beginPath();
ctx.moveTo(qx, HEADER - 6);
ctx.lineTo(qx, H - RULER_H - 4);
ctx.stroke();
ctx.setLineDash([]);
// Query label
ctx.fillStyle = '#c92f3c';
ctx.font = '700 11px system-ui';
ctx.textAlign = 'center';
ctx.fillText('x='+queryX.toFixed(1)+'h', qx, HEADER - 10);
runQuery();
}
function runQuery() {
traversalLog = [];
const results = [];
stabQuery(tree, queryX, results, traversalLog);
hitSet = new Set(results);
const info = document.getElementById('info');
if (results.length) {
info.textContent = fmt(`%length% reservation(s) hit: %map%`, { length: results.length, map: results.map(r=>r.label).join(', ') });
info.className = 'info hit';
} else {
info.textContent = fmt(`No reservations cover x = %queryX%h`, { queryX: queryX.toFixed(1) });
info.className = 'info miss';
}
document.getElementById('log').textContent =
fmt(`Tree traversal (O(log n + k)):
%traversalLog%`, { traversalLog: traversalLog.join('\n') });
}
// โโ Mouse/touch drag โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function pixelToTime(px) {
const rect = canvas.getBoundingClientRect();
const scaleX = canvas.width / rect.width;
const cx = (px - rect.left) * scaleX;
const t = TMIN + (cx - PAD) / (W - 2*PAD) * (TMAX - TMIN);
return Math.max(TMIN, Math.min(TMAX, Math.round(t * 2) / 2)); // snap to 0.5h
}
canvas.addEventListener('mousedown', e => { dragging=true; queryX=pixelToTime(e.clientX); draw(); });
canvas.addEventListener('mousemove', e => { if(dragging){ queryX=pixelToTime(e.clientX); draw(); }});
window.addEventListener('mouseup', () => dragging=false);
canvas.addEventListener('touchstart', e => { dragging=true; queryX=pixelToTime(e.touches[0].clientX); draw(); e.preventDefault(); });
canvas.addEventListener('touchmove', e => { if(dragging){ queryX=pixelToTime(e.touches[0].clientX); draw(); } e.preventDefault(); });
window.addEventListener('touchend', () => dragging=false);
// โโ Buttons โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const ROOMS = ['Conf. Room H','Suite I','Ballroom J','Room K','Loft L','Studio M'];
let addCount = 0;
document.getElementById('addBtn').addEventListener('click', () => {
const lo = Math.floor(Math.random() * 20);
const hi = lo + 1 + Math.floor(Math.random() * 5);
const label = ROOMS[addCount % ROOMS.length];
addCount++;
intervals.push({lo, hi, label});
tree = buildTree(intervals);
draw();
});
document.getElementById('resetBtn').addEventListener('click', () => {
intervals = [
{lo:2, hi:6, label:'Room A'},
{lo:4, hi:10, label:'Room B'},
{lo:8, hi:14, label:'Hall C'},
{lo:11, hi:16, label:'Room D'},
{lo:13, hi:19, label:'Hall E'},
{lo:17, hi:22, label:'Room F'},
{lo:1, hi:5, label:'Suite G'},
];
addCount = 0; queryX = 12;
tree = buildTree(intervals);
draw();
});
// โโ Init โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
tree = buildTree(intervals);
draw();
The bottom panel shows the O(log n + k) tree traversal path. Increase the number of reservations and watch how the path length grows only logarithmically, even as the reservation count doubles.
Comments
Loading comments...