Insert keys one by one and watch the root node move through the four ART node types. Each type is labeled with its current child count and capacity.
<!-- ART demo — shows the node type (Node4/16/48/256) for the root node as keys are added/removed -->
<div class="art-demo">
<p class="hint">Type a word and click <b>Insert</b>. The circle shows the current ART node type and how many slots are filled. Watch it grow when you add more children than the current type can hold.</p>
<div class="input-row">
<input id="key-input" type="text" placeholder="Type a key..." maxlength="12" autocomplete="off" />
<button id="btn-insert" type="button">Insert</button>
<button id="btn-delete" type="button" class="ghost">Delete</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="node-vis" class="node-vis" aria-label="ART node visualisation"></div>
<div id="status" class="status"></div>
<div class="key-list-label">Keys in the tree:</div>
<div id="key-list" class="key-list"></div>
</div>
/* Styles for the ART node-type demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.art-demo { max-width: 560px; margin: 0 auto; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .75rem; line-height: 1.45; }
.input-row { display: flex; gap: .4rem; flex-wrap: wrap; margin-bottom: .75rem; }
input { font: 14px system-ui, sans-serif; padding: .4rem .6rem; border: 1px solid #b0b8c2;
border-radius: 7px; flex: 1 1 120px; min-width: 0; }
input:focus { outline: 2px solid #3a6dad; outline-offset: 1px; border-color: #3a6dad; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem; border-radius: 7px;
border: 1px solid #1d3557; background: #1d3557; color: #fff; cursor: pointer; white-space: nowrap; }
button.ghost { background: #fff; color: #1d3557; }
/* Node visualisation box */
.node-vis { display: flex; flex-direction: column; align-items: center; gap: .5rem;
margin: .5rem 0 .75rem; min-height: 90px; }
.node-box { display: flex; flex-direction: column; align-items: center; gap: .3rem; }
.node-ring { border-radius: 50%; display: flex; align-items: center; justify-content: center;
font: 700 14px system-ui, sans-serif; border: 3px solid; transition: all .35s; }
.node-label { font-size: .8rem; font-weight: 600; text-align: center; }
.node-cap { font-size: .72rem; color: #666; text-align: center; }
/* One accent color per node type */
.type-n4 { border-color: #2a9d8f; color: #2a9d8f; }
.type-n16 { border-color: #3a6dad; color: #3a6dad; }
.type-n48 { border-color: #e76f51; color: #e76f51; }
.type-n256{ border-color: #8338ec; color: #8338ec; }
.type-empty { border-color: #ccc; color: #999; }
/* Slot grid showing used vs free child slots */
.slots { display: flex; flex-wrap: wrap; gap: 3px; justify-content: center; max-width: 320px; }
.slot { width: 14px; height: 14px; border-radius: 3px; }
.slot.used { background: currentColor; }
.slot.free { background: #e8e8e8; }
.status { font-size: .93rem; font-weight: 600; min-height: 1.4em; margin-bottom: .4rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info{ color: #3a6dad; }
.key-list-label { font-size: .8rem; color: #666; margin-bottom: .2rem; }
.key-list { display: flex; flex-wrap: wrap; gap: .3rem; min-height: 1.5em; }
.key-tag { background: #e8eef3; border: 1px solid #c8d5e0; border-radius: 5px;
font: 13px ui-monospace, monospace; padding: .15rem .4rem; }
// Adaptive Radix Tree node-type simulator
// ART node-type thresholds (from the 2013 Leis et al. paper):
// Node4: up to 4 children (grow to Node16 at 5)
// Node16: up to 16 children (grow to Node48 at 17, shrink to Node4 below 4)
// Node48: up to 48 children (grow to Node256 at 49, shrink to Node16 below 13)
// Node256: up to 256 children (shrink to Node48 below 37)
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Node type descriptors: name, max children, shrink threshold, grow threshold, approx. size in bytes
const TYPES = [
{ name: 'Node4', max: 4, shrinkAt: 0, growAt: 5, size: 56 },
{ name: 'Node16', max: 16, shrinkAt: 4, growAt: 17, size: 160 },
{ name: 'Node48', max: 48, shrinkAt: 13, growAt: 49, size: 656 },
{ name: 'Node256', max: 256, shrinkAt: 37, growAt: 257, size: 2064 },
];
const CSS_CLASS = ['type-n4','type-n16','type-n48','type-n256'];
// Tree state — we track only unique string keys and derive the node type from the count
const keys = new Set();
let typeIdx = 0; // index into TYPES array
function nodeTypeForCount(n) {
// choose the smallest node type that fits n children
if (n <= 4) return 0;
if (n <= 16) return 1;
if (n <= 48) return 2;
return 3;
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status ' + cls;
}
function render() {
const n = keys.size;
typeIdx = nodeTypeForCount(n);
const t = TYPES[typeIdx];
// Draw the node circle and metadata
const vis = document.getElementById('node-vis');
vis.innerHTML = '';
const box = document.createElement('div');
box.className = 'node-box';
const diameter = Math.max(64, Math.min(120, 60 + n * 2));
const ring = document.createElement('div');
ring.className = 'node-ring ' + CSS_CLASS[typeIdx];
ring.style.width = diameter + 'px';
ring.style.height = diameter + 'px';
ring.textContent = n;
const label = document.createElement('div');
label.className = 'node-label ' + CSS_CLASS[typeIdx];
label.textContent = t.name;
const cap = document.createElement('div');
cap.className = 'node-cap';
cap.textContent = i18nFmt('%n% / %max% children · ~%bytes% bytes', { n: n, max: t.max, bytes: t.size });
// Draw the slot grid (one square per slot in the current node type)
const slots = document.createElement('div');
slots.className = 'slots';
const displaySlots = Math.min(t.max, 48); // cap displayed slots at 48 for visual clarity even when in Node256
for (let i = 0; i < displaySlots; i++) {
const s = document.createElement('div');
s.className = 'slot ' + (i < n ? 'used' : 'free');
slots.appendChild(s);
}
box.appendChild(ring);
box.appendChild(label);
box.appendChild(cap);
box.appendChild(slots);
vis.appendChild(box);
// Draw sorted key tags below the node
const kl = document.getElementById('key-list');
kl.innerHTML = '';
for (const k of [...keys].sort()) {
const tag = document.createElement('span');
tag.className = 'key-tag';
tag.textContent = k;
kl.appendChild(tag);
}
}
function insert() {
const input = document.getElementById('key-input');
const raw = input.value.trim();
if (!raw) { setStatus('Type a key first.', 'bad'); return; }
if (keys.size >= 256) { setStatus('Node256 is full (256 children).', 'bad'); return; }
if (keys.has(raw)) { setStatus(i18nFmt('%key% is already in the tree.', { key: raw }), 'info'); input.value = ''; return; }
const before = nodeTypeForCount(keys.size);
keys.add(raw);
const after = nodeTypeForCount(keys.size);
render();
if (after > before) {
setStatus(i18nFmt('Node grew: %from% → %to%!', { from: TYPES[before].name, to: TYPES[after].name }), 'ok');
} else {
setStatus(i18nFmt('Inserted "%key%" — still a %type%.', { key: raw, type: TYPES[after].name }), 'ok');
}
input.value = '';
input.focus();
}
function deleteKey() {
const input = document.getElementById('key-input');
const raw = input.value.trim();
if (!raw) { setStatus('Type a key first.', 'bad'); return; }
if (!keys.has(raw)) { setStatus(i18nFmt('%key% is not in the tree.', { key: raw }), 'bad'); return; }
const before = nodeTypeForCount(keys.size);
keys.delete(raw);
const after = nodeTypeForCount(keys.size);
render();
if (after < before) {
setStatus(i18nFmt('Node shrank: %from% → %to%.', { from: TYPES[before].name, to: TYPES[after].name }), 'info');
} else {
setStatus(i18nFmt('Deleted "%key%" — still a %type%.', { key: raw, type: TYPES[after].name }), 'info');
}
input.value = '';
input.focus();
}
function reset() {
keys.clear();
render();
setStatus('Tree cleared.', 'info');
document.getElementById('key-input').value = '';
}
document.getElementById('btn-insert').addEventListener('click', insert);
document.getElementById('btn-delete').addEventListener('click', deleteKey);
document.getElementById('btn-reset').addEventListener('click', reset);
document.getElementById('key-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') insert();
});
// Pre-load 8 sample keys so the demo opens in an interesting state
['cat','car','card','cart','cap','cow','dog','dot'].forEach(k => keys.add(k));
render();
setStatus('8 keys pre-loaded — node type is Node16.', 'ok');
<!-- ART demo — shows the node type (Node4/16/48/256) for the root node as keys are added/removed -->
<div class="art-demo">
<p class="hint">Type a word and click <b>Insert</b>. The circle shows the current ART node type and how many slots are filled. Watch it grow when you add more children than the current type can hold.</p>
<div class="input-row">
<input id="key-input" type="text" placeholder="Type a key..." maxlength="12" autocomplete="off" />
<button id="btn-insert" type="button">Insert</button>
<button id="btn-delete" type="button" class="ghost">Delete</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div id="node-vis" class="node-vis" aria-label="ART node visualisation"></div>
<div id="status" class="status"></div>
<div class="key-list-label">Keys in the tree:</div>
<div id="key-list" class="key-list"></div>
</div>
/* Styles for the ART node-type demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: .5rem; }
.art-demo { max-width: 560px; margin: 0 auto; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .75rem; line-height: 1.45; }
.input-row { display: flex; gap: .4rem; flex-wrap: wrap; margin-bottom: .75rem; }
input { font: 14px system-ui, sans-serif; padding: .4rem .6rem; border: 1px solid #b0b8c2;
border-radius: 7px; flex: 1 1 120px; min-width: 0; }
input:focus { outline: 2px solid #3a6dad; outline-offset: 1px; border-color: #3a6dad; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .8rem; border-radius: 7px;
border: 1px solid #1d3557; background: #1d3557; color: #fff; cursor: pointer; white-space: nowrap; }
button.ghost { background: #fff; color: #1d3557; }
/* Node visualisation box */
.node-vis { display: flex; flex-direction: column; align-items: center; gap: .5rem;
margin: .5rem 0 .75rem; min-height: 90px; }
.node-box { display: flex; flex-direction: column; align-items: center; gap: .3rem; }
.node-ring { border-radius: 50%; display: flex; align-items: center; justify-content: center;
font: 700 14px system-ui, sans-serif; border: 3px solid; transition: all .35s; }
.node-label { font-size: .8rem; font-weight: 600; text-align: center; }
.node-cap { font-size: .72rem; color: #666; text-align: center; }
/* One accent color per node type */
.type-n4 { border-color: #2a9d8f; color: #2a9d8f; }
.type-n16 { border-color: #3a6dad; color: #3a6dad; }
.type-n48 { border-color: #e76f51; color: #e76f51; }
.type-n256{ border-color: #8338ec; color: #8338ec; }
.type-empty { border-color: #ccc; color: #999; }
/* Slot grid showing used vs free child slots */
.slots { display: flex; flex-wrap: wrap; gap: 3px; justify-content: center; max-width: 320px; }
.slot { width: 14px; height: 14px; border-radius: 3px; }
.slot.used { background: currentColor; }
.slot.free { background: #e8e8e8; }
.status { font-size: .93rem; font-weight: 600; min-height: 1.4em; margin-bottom: .4rem; }
.status.ok { color: #0a7d33; }
.status.bad { color: #c92f3c; }
.status.info{ color: #3a6dad; }
.key-list-label { font-size: .8rem; color: #666; margin-bottom: .2rem; }
.key-list { display: flex; flex-wrap: wrap; gap: .3rem; min-height: 1.5em; }
.key-tag { background: #e8eef3; border: 1px solid #c8d5e0; border-radius: 5px;
font: 13px ui-monospace, monospace; padding: .15rem .4rem; }
// Adaptive Radix Tree node-type simulator
// ART node-type thresholds (from the 2013 Leis et al. paper):
// Node4: up to 4 children (grow to Node16 at 5)
// Node16: up to 16 children (grow to Node48 at 17, shrink to Node4 below 4)
// Node48: up to 48 children (grow to Node256 at 49, shrink to Node16 below 13)
// Node256: up to 256 children (shrink to Node48 below 37)
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Node type descriptors: name, max children, shrink threshold, grow threshold, approx. size in bytes
const TYPES = [
{ name: 'Node4', max: 4, shrinkAt: 0, growAt: 5, size: 56 },
{ name: 'Node16', max: 16, shrinkAt: 4, growAt: 17, size: 160 },
{ name: 'Node48', max: 48, shrinkAt: 13, growAt: 49, size: 656 },
{ name: 'Node256', max: 256, shrinkAt: 37, growAt: 257, size: 2064 },
];
const CSS_CLASS = ['type-n4','type-n16','type-n48','type-n256'];
// Tree state — we track only unique string keys and derive the node type from the count
const keys = new Set();
let typeIdx = 0; // index into TYPES array
function nodeTypeForCount(n) {
// choose the smallest node type that fits n children
if (n <= 4) return 0;
if (n <= 16) return 1;
if (n <= 48) return 2;
return 3;
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status ' + cls;
}
function render() {
const n = keys.size;
typeIdx = nodeTypeForCount(n);
const t = TYPES[typeIdx];
// Draw the node circle and metadata
const vis = document.getElementById('node-vis');
vis.innerHTML = '';
const box = document.createElement('div');
box.className = 'node-box';
const diameter = Math.max(64, Math.min(120, 60 + n * 2));
const ring = document.createElement('div');
ring.className = 'node-ring ' + CSS_CLASS[typeIdx];
ring.style.width = diameter + 'px';
ring.style.height = diameter + 'px';
ring.textContent = n;
const label = document.createElement('div');
label.className = 'node-label ' + CSS_CLASS[typeIdx];
label.textContent = t.name;
const cap = document.createElement('div');
cap.className = 'node-cap';
cap.textContent = i18nFmt('%n% / %max% children · ~%bytes% bytes', { n: n, max: t.max, bytes: t.size });
// Draw the slot grid (one square per slot in the current node type)
const slots = document.createElement('div');
slots.className = 'slots';
const displaySlots = Math.min(t.max, 48); // cap displayed slots at 48 for visual clarity even when in Node256
for (let i = 0; i < displaySlots; i++) {
const s = document.createElement('div');
s.className = 'slot ' + (i < n ? 'used' : 'free');
slots.appendChild(s);
}
box.appendChild(ring);
box.appendChild(label);
box.appendChild(cap);
box.appendChild(slots);
vis.appendChild(box);
// Draw sorted key tags below the node
const kl = document.getElementById('key-list');
kl.innerHTML = '';
for (const k of [...keys].sort()) {
const tag = document.createElement('span');
tag.className = 'key-tag';
tag.textContent = k;
kl.appendChild(tag);
}
}
function insert() {
const input = document.getElementById('key-input');
const raw = input.value.trim();
if (!raw) { setStatus('Type a key first.', 'bad'); return; }
if (keys.size >= 256) { setStatus('Node256 is full (256 children).', 'bad'); return; }
if (keys.has(raw)) { setStatus(i18nFmt('%key% is already in the tree.', { key: raw }), 'info'); input.value = ''; return; }
const before = nodeTypeForCount(keys.size);
keys.add(raw);
const after = nodeTypeForCount(keys.size);
render();
if (after > before) {
setStatus(i18nFmt('Node grew: %from% → %to%!', { from: TYPES[before].name, to: TYPES[after].name }), 'ok');
} else {
setStatus(i18nFmt('Inserted "%key%" — still a %type%.', { key: raw, type: TYPES[after].name }), 'ok');
}
input.value = '';
input.focus();
}
function deleteKey() {
const input = document.getElementById('key-input');
const raw = input.value.trim();
if (!raw) { setStatus('Type a key first.', 'bad'); return; }
if (!keys.has(raw)) { setStatus(i18nFmt('%key% is not in the tree.', { key: raw }), 'bad'); return; }
const before = nodeTypeForCount(keys.size);
keys.delete(raw);
const after = nodeTypeForCount(keys.size);
render();
if (after < before) {
setStatus(i18nFmt('Node shrank: %from% → %to%.', { from: TYPES[before].name, to: TYPES[after].name }), 'info');
} else {
setStatus(i18nFmt('Deleted "%key%" — still a %type%.', { key: raw, type: TYPES[after].name }), 'info');
}
input.value = '';
input.focus();
}
function reset() {
keys.clear();
render();
setStatus('Tree cleared.', 'info');
document.getElementById('key-input').value = '';
}
document.getElementById('btn-insert').addEventListener('click', insert);
document.getElementById('btn-delete').addEventListener('click', deleteKey);
document.getElementById('btn-reset').addEventListener('click', reset);
document.getElementById('key-input').addEventListener('keydown', (e) => {
if (e.key === 'Enter') insert();
});
// Pre-load 8 sample keys so the demo opens in an interesting state
['cat','car','card','cart','cap','cow','dog','dot'].forEach(k => keys.add(k));
render();
setStatus('8 keys pre-loaded — node type is Node16.', 'ok');
Comments
Loading comments...