The demo below simulates a bit-stream arriving one item at a time. Each incoming bit is either 0 or 1. Watch how DGIM builds and merges buckets as 1s arrive, and how the window evicts old buckets when their timestamps fall outside the last N positions.
<p class="hint">Each incoming bit updates the DGIM bucket set. Buckets hold powers-of-two counts of 1s, stamped with the most-recent 1's timestamp. When the window slides, stale buckets expire.</p>
<div class="controls">
<button id="add0" type="button">Add 0</button>
<button id="add1" type="button">Add 1</button>
<button id="step" type="button">Auto-step</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="window-row">
<span class="label">Window:</span>
<div id="bits" class="bits"></div>
</div>
<div class="bucket-row">
<span class="label">Buckets:</span>
<div id="buckets" class="buckets"></div>
</div>
<div class="counts">
<span>Exact count: <strong id="exact">0</strong></span>
<span>DGIM estimate: <strong id="dgim">0</strong></span>
<span>Error: <strong id="err">0%</strong></span>
</div>
<div class="status" id="status"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.controls { display: flex; gap: .4rem; flex-wrap: wrap; margin-bottom: .8rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.window-row, .bucket-row { display: flex; align-items: flex-start; gap: .5rem; margin-bottom: .5rem; }
.label { font-size: .78rem; font-weight: 700; color: #555; min-width: 68px; padding-top: 5px; }
.bits { display: flex; flex-wrap: wrap; gap: 3px; }
.bit { width: 22px; height: 22px; display: flex; align-items: center; justify-content: center;
font: 700 12px ui-monospace, monospace; border-radius: 4px; }
.bit.b1 { background: #1d3557; color: #fff; }
.bit.b0 { background: #dde3ea; color: #555; }
.bit.new { outline: 2px solid #e63946; outline-offset: 1px; }
.buckets { display: flex; flex-wrap: wrap; gap: 4px; }
.bucket { display: flex; align-items: center; justify-content: center;
border-radius: 6px; background: #4a7c59; color: #fff;
font: 700 11px ui-monospace, monospace; padding: 3px 6px; }
.counts { display: flex; gap: 1.2rem; flex-wrap: wrap; margin: .6rem 0; font-size: .9rem; }
.status { font-size: .88rem; font-weight: 600; min-height: 1.3em; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// DGIM sliding-window 1-count: maintain exponential buckets, each
// N = window size (number of recent bits we care about)
const N = 32;
// Bucket = { size: power-of-two count of 1s it represents,
// ts: timestamp of the most recent 1 in the bucket }
let buckets = [];
// stream = circular buffer of last N bits (for exact reference only)
let stream = [];
// tick = global timestamp counter
let tick = 0;
// autoSeq = pre-generated bit sequence for auto-step mode
let autoSeq = [];
let autoIdx = 0;
// mergeBuckets: enforce DGIM invariant (at most 2 buckets of each size)
function mergeBuckets() {
let changed = true;
while (changed) {
changed = false;
// scan for 3 consecutive same-size buckets
for (let i = 0; i < buckets.length - 2; i++) {
if (buckets[i].size === buckets[i + 1].size && buckets[i].size === buckets[i + 2].size) {
// merge the two oldest into a bucket of double size
const merged = { size: buckets[i].size * 2, ts: buckets[i].ts };
buckets.splice(i, 3, { size: buckets[i + 1].size, ts: buckets[i + 1].ts }, merged);
changed = true;
break;
}
}
// general pass: merge any size that has more than 2 representatives
for (let i = 0; i < buckets.length - 1; i++) {
const sameCount = buckets.filter(b => b.size === buckets[i].size).length;
if (sameCount > 2) {
const idx = buckets.findIndex((b, j) => j > i && b.size === buckets[i].size);
if (idx >= 0) {
const merged = { size: buckets[i].size * 2, ts: Math.max(buckets[i].ts, buckets[idx].ts) };
buckets.splice(idx, 1);
buckets.splice(i, 1);
buckets.push(merged);
buckets.sort((a, b) => b.ts - a.ts);
changed = true;
break;
}
}
}
}
}
// expireBuckets: drop buckets whose most-recent-1 timestamp is outside window
function expireBuckets() {
buckets = buckets.filter(b => b.ts > tick - N);
}
// dgimEstimate: sum all bucket sizes, halve the oldest one
function dgimEstimate() {
if (buckets.length === 0) return 0;
// oldest bucket may be only partially inside the window — count half
const oldest = buckets[buckets.length - 1];
let sum = 0;
for (let i = 0; i < buckets.length - 1; i++) sum += buckets[i].size;
return sum + Math.ceil(oldest.size / 2);
}
// exactCount: brute-force reference count from the circular buffer
function exactCount() {
return stream.filter(b => b === 1).length;
}
function addBit(bit) {
tick++;
stream.push(bit);
if (stream.length > N) stream.shift();
if (bit === 1) {
buckets.unshift({ size: 1, ts: tick });
mergeBuckets();
}
expireBuckets();
render();
}
function render() {
// render the stream bits (newest = rightmost, highlighted in red outline)
const bitsEl = document.getElementById('bits');
bitsEl.innerHTML = '';
stream.forEach((b, i) => {
const d = document.createElement('div');
d.className = 'bit b' + b + (i === stream.length - 1 ? ' new' : '');
d.textContent = b;
bitsEl.appendChild(d);
});
// render active buckets as colored chips
const buckEl = document.getElementById('buckets');
buckEl.innerHTML = '';
buckets.forEach(bk => {
const d = document.createElement('div');
d.className = 'bucket';
d.textContent = bk.size;
d.title = i18nFmt('Bucket size %size% — most recent 1 at tick %ts%', { ts: bk.ts });
buckEl.appendChild(d);
});
const exact = exactCount();
const est = dgimEstimate();
document.getElementById('exact').textContent = exact;
document.getElementById('dgim').textContent = est;
const errPct = exact === 0 ? 0 : Math.round(Math.abs(est - exact) / exact * 100);
document.getElementById('err').textContent = errPct + '%';
const msg = stream.length < N
? i18nFmt('Stream length %n% / %N% — window filling up.', { n: stream.length, N: N })
: i18nFmt('Window full (%n% bits) · %b% active bucket(s).', { n: stream.length, b: buckets.length });
document.getElementById('status').textContent = msg;
}
function stepAuto() {
if (autoSeq.length === 0) {
// generate a fresh 40-bit random sequence biased toward 1s
for (let i = 0; i < 40; i++) autoSeq.push(Math.random() < 0.6 ? 1 : 0);
}
if (autoIdx < autoSeq.length) addBit(autoSeq[autoIdx++]);
}
document.getElementById('add0').addEventListener('click', () => addBit(0));
document.getElementById('add1').addEventListener('click', () => addBit(1));
document.getElementById('step').addEventListener('click', stepAuto);
document.getElementById('reset').addEventListener('click', () => {
buckets = []; stream = []; tick = 0; autoSeq = []; autoIdx = 0;
render();
});
render();
<p class="hint">Each incoming bit updates the DGIM bucket set. Buckets hold powers-of-two counts of 1s, stamped with the most-recent 1's timestamp. When the window slides, stale buckets expire.</p>
<div class="controls">
<button id="add0" type="button">Add 0</button>
<button id="add1" type="button">Add 1</button>
<button id="step" type="button">Auto-step</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
<div class="window-row">
<span class="label">Window:</span>
<div id="bits" class="bits"></div>
</div>
<div class="bucket-row">
<span class="label">Buckets:</span>
<div id="buckets" class="buckets"></div>
</div>
<div class="counts">
<span>Exact count: <strong id="exact">0</strong></span>
<span>DGIM estimate: <strong id="dgim">0</strong></span>
<span>Error: <strong id="err">0%</strong></span>
</div>
<div class="status" id="status"></div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .8rem; line-height: 1.5; }
.controls { display: flex; gap: .4rem; flex-wrap: wrap; margin-bottom: .8rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
.window-row, .bucket-row { display: flex; align-items: flex-start; gap: .5rem; margin-bottom: .5rem; }
.label { font-size: .78rem; font-weight: 700; color: #555; min-width: 68px; padding-top: 5px; }
.bits { display: flex; flex-wrap: wrap; gap: 3px; }
.bit { width: 22px; height: 22px; display: flex; align-items: center; justify-content: center;
font: 700 12px ui-monospace, monospace; border-radius: 4px; }
.bit.b1 { background: #1d3557; color: #fff; }
.bit.b0 { background: #dde3ea; color: #555; }
.bit.new { outline: 2px solid #e63946; outline-offset: 1px; }
.buckets { display: flex; flex-wrap: wrap; gap: 4px; }
.bucket { display: flex; align-items: center; justify-content: center;
border-radius: 6px; background: #4a7c59; color: #fff;
font: 700 11px ui-monospace, monospace; padding: 3px 6px; }
.counts { display: flex; gap: 1.2rem; flex-wrap: wrap; margin: .6rem 0; font-size: .9rem; }
.status { font-size: .88rem; font-weight: 600; min-height: 1.3em; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// DGIM sliding-window 1-count: maintain exponential buckets, each
// N = window size (number of recent bits we care about)
const N = 32;
// Bucket = { size: power-of-two count of 1s it represents,
// ts: timestamp of the most recent 1 in the bucket }
let buckets = [];
// stream = circular buffer of last N bits (for exact reference only)
let stream = [];
// tick = global timestamp counter
let tick = 0;
// autoSeq = pre-generated bit sequence for auto-step mode
let autoSeq = [];
let autoIdx = 0;
// mergeBuckets: enforce DGIM invariant (at most 2 buckets of each size)
function mergeBuckets() {
let changed = true;
while (changed) {
changed = false;
// scan for 3 consecutive same-size buckets
for (let i = 0; i < buckets.length - 2; i++) {
if (buckets[i].size === buckets[i + 1].size && buckets[i].size === buckets[i + 2].size) {
// merge the two oldest into a bucket of double size
const merged = { size: buckets[i].size * 2, ts: buckets[i].ts };
buckets.splice(i, 3, { size: buckets[i + 1].size, ts: buckets[i + 1].ts }, merged);
changed = true;
break;
}
}
// general pass: merge any size that has more than 2 representatives
for (let i = 0; i < buckets.length - 1; i++) {
const sameCount = buckets.filter(b => b.size === buckets[i].size).length;
if (sameCount > 2) {
const idx = buckets.findIndex((b, j) => j > i && b.size === buckets[i].size);
if (idx >= 0) {
const merged = { size: buckets[i].size * 2, ts: Math.max(buckets[i].ts, buckets[idx].ts) };
buckets.splice(idx, 1);
buckets.splice(i, 1);
buckets.push(merged);
buckets.sort((a, b) => b.ts - a.ts);
changed = true;
break;
}
}
}
}
}
// expireBuckets: drop buckets whose most-recent-1 timestamp is outside window
function expireBuckets() {
buckets = buckets.filter(b => b.ts > tick - N);
}
// dgimEstimate: sum all bucket sizes, halve the oldest one
function dgimEstimate() {
if (buckets.length === 0) return 0;
// oldest bucket may be only partially inside the window — count half
const oldest = buckets[buckets.length - 1];
let sum = 0;
for (let i = 0; i < buckets.length - 1; i++) sum += buckets[i].size;
return sum + Math.ceil(oldest.size / 2);
}
// exactCount: brute-force reference count from the circular buffer
function exactCount() {
return stream.filter(b => b === 1).length;
}
function addBit(bit) {
tick++;
stream.push(bit);
if (stream.length > N) stream.shift();
if (bit === 1) {
buckets.unshift({ size: 1, ts: tick });
mergeBuckets();
}
expireBuckets();
render();
}
function render() {
// render the stream bits (newest = rightmost, highlighted in red outline)
const bitsEl = document.getElementById('bits');
bitsEl.innerHTML = '';
stream.forEach((b, i) => {
const d = document.createElement('div');
d.className = 'bit b' + b + (i === stream.length - 1 ? ' new' : '');
d.textContent = b;
bitsEl.appendChild(d);
});
// render active buckets as colored chips
const buckEl = document.getElementById('buckets');
buckEl.innerHTML = '';
buckets.forEach(bk => {
const d = document.createElement('div');
d.className = 'bucket';
d.textContent = bk.size;
d.title = i18nFmt('Bucket size %size% — most recent 1 at tick %ts%', { ts: bk.ts });
buckEl.appendChild(d);
});
const exact = exactCount();
const est = dgimEstimate();
document.getElementById('exact').textContent = exact;
document.getElementById('dgim').textContent = est;
const errPct = exact === 0 ? 0 : Math.round(Math.abs(est - exact) / exact * 100);
document.getElementById('err').textContent = errPct + '%';
const msg = stream.length < N
? i18nFmt('Stream length %n% / %N% — window filling up.', { n: stream.length, N: N })
: i18nFmt('Window full (%n% bits) · %b% active bucket(s).', { n: stream.length, b: buckets.length });
document.getElementById('status').textContent = msg;
}
function stepAuto() {
if (autoSeq.length === 0) {
// generate a fresh 40-bit random sequence biased toward 1s
for (let i = 0; i < 40; i++) autoSeq.push(Math.random() < 0.6 ? 1 : 0);
}
if (autoIdx < autoSeq.length) addBit(autoSeq[autoIdx++]);
}
document.getElementById('add0').addEventListener('click', () => addBit(0));
document.getElementById('add1').addEventListener('click', () => addBit(1));
document.getElementById('step').addEventListener('click', stepAuto);
document.getElementById('reset').addEventListener('click', () => {
buckets = []; stream = []; tick = 0; autoSeq = []; autoIdx = 0;
render();
});
render();
Notice that the exact count and the DGIM estimate stay close — the algorithm guarantees the estimate is within 50% of the true count. In practice the error is usually much smaller. The key insight: we never store the individual bits, only a logarithmic number of bucket summaries.
Comments
Loading comments...