Choose a number of counters k and press Stream. Items flow in one at a time. If an item already has a counter, its count increments. If there is a free slot, a new counter starts. Otherwise every counter drops by 1 and those that reach zero are discarded — the cancellation step.
<!-- Misra-Gries demo layout -->
<div class="controls">
<label>Counters k = <input id="k-input" type="number" min="2" max="8" value="4"></label>
<label>Stream (space-separated items): <input id="stream-input" type="text" value="A A A B B B C C D E A B F G A B C"></label>
</div>
<div class="btns">
<button id="btn-stream" type="button">Stream</button>
<button id="btn-step" type="button">Step</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="stream-row" id="stream-row"></div>
<div class="section-label">Active counters</div>
<div id="counters-area" class="counters-area"></div>
<div id="status" class="status"></div>
<div id="summary" class="summary" hidden></div>
/* Misra-Gries demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem 1rem; margin-bottom: .5rem; align-items: center; }
.controls label { display: flex; align-items: center; gap: .4rem; font-weight: 600; font-size: .85rem; }
input[type=number] { width: 52px; padding: .25rem .4rem; border: 1px solid #aaa; border-radius: 6px; font-size: .9rem; }
input[type=text] { width: 260px; padding: .25rem .5rem; border: 1px solid #aaa; border-radius: 6px; font-size: .85rem; font-family: ui-monospace, monospace; }
.btns { display: flex; gap: .4rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.stream-row { display: flex; flex-wrap: wrap; gap: 4px; min-height: 36px; margin-bottom: .4rem; }
.token { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
font: 700 13px ui-monospace, monospace; border-radius: 5px; border: 1px solid #cdd9e3; background: #e8eef3; transition: background .2s, transform .2s; }
.token.current { background: #f4a261; border-color: #d4763a; color: #fff; transform: scale(1.2); }
.token.done { background: #c5e3c5; border-color: #6aa96a; color: #1a4d1a; }
.token.cancelled { background: #f0d0d0; border-color: #c08080; color: #7a2020; }
.section-label { font-size: .75rem; font-weight: 700; color: #666; text-transform: uppercase; letter-spacing: .06em; margin-bottom: .3rem; }
.counters-area { display: flex; flex-wrap: wrap; gap: 6px; min-height: 52px; margin-bottom: .5rem; }
.counter { display: flex; flex-direction: column; align-items: center; padding: .3rem .6rem;
border-radius: 8px; border: 2px solid #1d3557; background: #e8eef3; min-width: 44px;
transition: background .25s, transform .25s; }
.counter .c-item { font: 700 15px ui-monospace, monospace; color: #1d3557; }
.counter .c-count { font: 600 12px system-ui; color: #555; }
.counter.bump { background: #cfe8cf; border-color: #3a8a3a; transform: scale(1.15); }
.counter.new-slot { background: #d8e8f8; border-color: #3a5a8a; }
.counter.dying { background: #f8d8d8; border-color: #c04040; opacity: .5; transform: scale(.85); }
.status { font-size: .9rem; font-weight: 600; min-height: 1.3em; margin: .2rem 0; }
.status.info { color: #1d3557; }
.status.cancel { color: #c03030; }
.status.done-msg { color: #1a7030; }
.summary { margin-top: .5rem; padding: .5rem .7rem; background: #f0f8f0; border: 1px solid #8aba8a; border-radius: 8px; font-size: .85rem; }
.summary strong { color: #1a5030; }
// Misra-Gries summary: stream items one by one through k-1 counters.
// Guarantee: any item with frequency > n/k survives in the final summary.
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
let tokens = [], k = 4, pos = 0, counters = {}, cancelRounds = 0, running = false, stepTimer = null;
function parseStream() {
return document.getElementById('stream-input').value.trim().split(/\s+/).filter(Boolean);
}
function readK() {
const v = parseInt(document.getElementById('k-input').value, 10);
return (isNaN(v) || v < 2) ? 2 : Math.min(v, 8);
}
function reset() {
clearTimeout(stepTimer);
running = false;
tokens = parseStream();
k = readK();
pos = 0;
counters = {};
cancelRounds = 0;
renderStream();
renderCounters();
setStatus('Ready — press Stream or Step.', 'info');
document.getElementById('summary').hidden = true;
document.getElementById('btn-step').disabled = false;
document.getElementById('btn-stream').disabled = false;
}
function renderStream() {
const row = document.getElementById('stream-row');
row.innerHTML = '';
tokens.forEach((t, i) => {
const d = document.createElement('div');
d.className = 'token' + (i === pos ? ' current' : i < pos ? ' done' : '');
d.textContent = t;
row.appendChild(d);
});
}
function renderCounters() {
const area = document.getElementById('counters-area');
area.innerHTML = '';
const entries = Object.entries(counters).sort((a, b) => b[1] - a[1]);
if (entries.length === 0) {
const ph = document.createElement('span');
ph.style.cssText = 'color:#aaa;font-size:.85rem;align-self:center';
ph.textContent = '(no counters yet)';
area.appendChild(ph);
return;
}
entries.forEach(([item, cnt]) => {
const d = document.createElement('div');
d.className = 'counter';
d.innerHTML = '<span class="c-item">' + item + '</span><span class="c-count">' + cnt + '</span>';
area.appendChild(d);
});
}
function setStatus(msg, cls) {
const s = document.getElementById('status');
s.textContent = msg;
s.className = 'status ' + cls;
}
// Process one item from the stream.
function step() {
if (pos >= tokens.length) {
finishSummary();
return;
}
const item = tokens[pos];
if (counters[item] !== undefined) {
// Item already tracked — increment its counter.
counters[item]++;
setStatus(i18nFmt('Incremented counter for ’%item%’.', { item }), 'info');
} else if (Object.keys(counters).length < k - 1) {
// Free slot available — start a new counter.
counters[item] = 1;
setStatus(i18nFmt('New counter started for ’%item%’.', { item }), 'info');
} else {
// All slots full — cancellation round.
cancelRounds++;
for (const key of Object.keys(counters)) {
counters[key]--;
if (counters[key] <= 0) delete counters[key];
}
// After cancellation the triggering item is not inserted — it is subsumed.
setStatus(i18nFmt('Cancellation round %cancelRounds% — decremented all counters (item: ’%item%’).', { item, cancelRounds }), 'cancel');
}
// Advance to the next stream position.
const oldPos = pos;
pos++;
renderCounters();
// Update token highlights in the stream row.
const row = document.getElementById('stream-row');
const tokenEls = row.querySelectorAll('.token');
if (tokenEls[oldPos]) {
tokenEls[oldPos].className = 'token done';
}
if (tokenEls[pos]) {
tokenEls[pos].className = 'token current';
}
if (pos >= tokens.length) {
finishSummary();
}
}
function finishSummary() {
running = false;
document.getElementById('btn-step').disabled = true;
document.getElementById('btn-stream').disabled = true;
const n = tokens.length;
const threshold = Math.floor(n / k);
const entries = Object.entries(counters).sort((a, b) => b[1] - a[1]);
const summaryEl = document.getElementById('summary');
if (entries.length === 0) {
summaryEl.innerHTML = '<strong>No survivors — every item was cancelled out.</strong>';
} else {
const rows = entries.map(([item, cnt]) =>
'<span class="c-item" style="display:inline-block;min-width:2em;font-weight:700">' + item + '</span> → ' + i18nFmt('reported count %cnt%', { cnt })
).join('<br>');
summaryEl.innerHTML =
'<strong>Final summary (candidates):</strong><br>' + rows +
'<br><small>' + i18nFmt('Stream length n = %n%, k = %k%. Items with true frequency > %threshold% are guaranteed to appear. Cancellation rounds: %cancelRounds%.', { n, k, threshold, cancelRounds }) + '</small>';
}
summaryEl.hidden = false;
setStatus('Done — see the final summary below.', 'done-msg');
}
function streamAll() {
if (running) return;
running = true;
function tick() {
if (!running || pos >= tokens.length) { running = false; return; }
step();
stepTimer = setTimeout(tick, 350);
}
tick();
}
document.getElementById('btn-stream').addEventListener('click', streamAll);
document.getElementById('btn-step').addEventListener('click', () => { running = false; step(); });
document.getElementById('btn-reset').addEventListener('click', reset);
reset();
<!-- Misra-Gries demo layout -->
<div class="controls">
<label>Counters k = <input id="k-input" type="number" min="2" max="8" value="4"></label>
<label>Stream (space-separated items): <input id="stream-input" type="text" value="A A A B B B C C D E A B F G A B C"></label>
</div>
<div class="btns">
<button id="btn-stream" type="button">Stream</button>
<button id="btn-step" type="button">Step</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="stream-row" id="stream-row"></div>
<div class="section-label">Active counters</div>
<div id="counters-area" class="counters-area"></div>
<div id="status" class="status"></div>
<div id="summary" class="summary" hidden></div>
/* Misra-Gries demo styles */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.controls { display: flex; flex-wrap: wrap; gap: .5rem 1rem; margin-bottom: .5rem; align-items: center; }
.controls label { display: flex; align-items: center; gap: .4rem; font-weight: 600; font-size: .85rem; }
input[type=number] { width: 52px; padding: .25rem .4rem; border: 1px solid #aaa; border-radius: 6px; font-size: .9rem; }
input[type=text] { width: 260px; padding: .25rem .5rem; border: 1px solid #aaa; border-radius: 6px; font-size: .85rem; font-family: ui-monospace, monospace; }
.btns { display: flex; gap: .4rem; flex-wrap: wrap; margin-bottom: .6rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
.stream-row { display: flex; flex-wrap: wrap; gap: 4px; min-height: 36px; margin-bottom: .4rem; }
.token { width: 28px; height: 28px; display: flex; align-items: center; justify-content: center;
font: 700 13px ui-monospace, monospace; border-radius: 5px; border: 1px solid #cdd9e3; background: #e8eef3; transition: background .2s, transform .2s; }
.token.current { background: #f4a261; border-color: #d4763a; color: #fff; transform: scale(1.2); }
.token.done { background: #c5e3c5; border-color: #6aa96a; color: #1a4d1a; }
.token.cancelled { background: #f0d0d0; border-color: #c08080; color: #7a2020; }
.section-label { font-size: .75rem; font-weight: 700; color: #666; text-transform: uppercase; letter-spacing: .06em; margin-bottom: .3rem; }
.counters-area { display: flex; flex-wrap: wrap; gap: 6px; min-height: 52px; margin-bottom: .5rem; }
.counter { display: flex; flex-direction: column; align-items: center; padding: .3rem .6rem;
border-radius: 8px; border: 2px solid #1d3557; background: #e8eef3; min-width: 44px;
transition: background .25s, transform .25s; }
.counter .c-item { font: 700 15px ui-monospace, monospace; color: #1d3557; }
.counter .c-count { font: 600 12px system-ui; color: #555; }
.counter.bump { background: #cfe8cf; border-color: #3a8a3a; transform: scale(1.15); }
.counter.new-slot { background: #d8e8f8; border-color: #3a5a8a; }
.counter.dying { background: #f8d8d8; border-color: #c04040; opacity: .5; transform: scale(.85); }
.status { font-size: .9rem; font-weight: 600; min-height: 1.3em; margin: .2rem 0; }
.status.info { color: #1d3557; }
.status.cancel { color: #c03030; }
.status.done-msg { color: #1a7030; }
.summary { margin-top: .5rem; padding: .5rem .7rem; background: #f0f8f0; border: 1px solid #8aba8a; border-radius: 8px; font-size: .85rem; }
.summary strong { color: #1a5030; }
// Misra-Gries summary: stream items one by one through k-1 counters.
// Guarantee: any item with frequency > n/k survives in the final summary.
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
let tokens = [], k = 4, pos = 0, counters = {}, cancelRounds = 0, running = false, stepTimer = null;
function parseStream() {
return document.getElementById('stream-input').value.trim().split(/\s+/).filter(Boolean);
}
function readK() {
const v = parseInt(document.getElementById('k-input').value, 10);
return (isNaN(v) || v < 2) ? 2 : Math.min(v, 8);
}
function reset() {
clearTimeout(stepTimer);
running = false;
tokens = parseStream();
k = readK();
pos = 0;
counters = {};
cancelRounds = 0;
renderStream();
renderCounters();
setStatus('Ready — press Stream or Step.', 'info');
document.getElementById('summary').hidden = true;
document.getElementById('btn-step').disabled = false;
document.getElementById('btn-stream').disabled = false;
}
function renderStream() {
const row = document.getElementById('stream-row');
row.innerHTML = '';
tokens.forEach((t, i) => {
const d = document.createElement('div');
d.className = 'token' + (i === pos ? ' current' : i < pos ? ' done' : '');
d.textContent = t;
row.appendChild(d);
});
}
function renderCounters() {
const area = document.getElementById('counters-area');
area.innerHTML = '';
const entries = Object.entries(counters).sort((a, b) => b[1] - a[1]);
if (entries.length === 0) {
const ph = document.createElement('span');
ph.style.cssText = 'color:#aaa;font-size:.85rem;align-self:center';
ph.textContent = '(no counters yet)';
area.appendChild(ph);
return;
}
entries.forEach(([item, cnt]) => {
const d = document.createElement('div');
d.className = 'counter';
d.innerHTML = '<span class="c-item">' + item + '</span><span class="c-count">' + cnt + '</span>';
area.appendChild(d);
});
}
function setStatus(msg, cls) {
const s = document.getElementById('status');
s.textContent = msg;
s.className = 'status ' + cls;
}
// Process one item from the stream.
function step() {
if (pos >= tokens.length) {
finishSummary();
return;
}
const item = tokens[pos];
if (counters[item] !== undefined) {
// Item already tracked — increment its counter.
counters[item]++;
setStatus(i18nFmt('Incremented counter for ’%item%’.', { item }), 'info');
} else if (Object.keys(counters).length < k - 1) {
// Free slot available — start a new counter.
counters[item] = 1;
setStatus(i18nFmt('New counter started for ’%item%’.', { item }), 'info');
} else {
// All slots full — cancellation round.
cancelRounds++;
for (const key of Object.keys(counters)) {
counters[key]--;
if (counters[key] <= 0) delete counters[key];
}
// After cancellation the triggering item is not inserted — it is subsumed.
setStatus(i18nFmt('Cancellation round %cancelRounds% — decremented all counters (item: ’%item%’).', { item, cancelRounds }), 'cancel');
}
// Advance to the next stream position.
const oldPos = pos;
pos++;
renderCounters();
// Update token highlights in the stream row.
const row = document.getElementById('stream-row');
const tokenEls = row.querySelectorAll('.token');
if (tokenEls[oldPos]) {
tokenEls[oldPos].className = 'token done';
}
if (tokenEls[pos]) {
tokenEls[pos].className = 'token current';
}
if (pos >= tokens.length) {
finishSummary();
}
}
function finishSummary() {
running = false;
document.getElementById('btn-step').disabled = true;
document.getElementById('btn-stream').disabled = true;
const n = tokens.length;
const threshold = Math.floor(n / k);
const entries = Object.entries(counters).sort((a, b) => b[1] - a[1]);
const summaryEl = document.getElementById('summary');
if (entries.length === 0) {
summaryEl.innerHTML = '<strong>No survivors — every item was cancelled out.</strong>';
} else {
const rows = entries.map(([item, cnt]) =>
'<span class="c-item" style="display:inline-block;min-width:2em;font-weight:700">' + item + '</span> → ' + i18nFmt('reported count %cnt%', { cnt })
).join('<br>');
summaryEl.innerHTML =
'<strong>Final summary (candidates):</strong><br>' + rows +
'<br><small>' + i18nFmt('Stream length n = %n%, k = %k%. Items with true frequency > %threshold% are guaranteed to appear. Cancellation rounds: %cancelRounds%.', { n, k, threshold, cancelRounds }) + '</small>';
}
summaryEl.hidden = false;
setStatus('Done — see the final summary below.', 'done-msg');
}
function streamAll() {
if (running) return;
running = true;
function tick() {
if (!running || pos >= tokens.length) { running = false; return; }
step();
stepTimer = setTimeout(tick, 350);
}
tick();
}
document.getElementById('btn-stream').addEventListener('click', streamAll);
document.getElementById('btn-step').addEventListener('click', () => { running = false; step(); });
document.getElementById('btn-reset').addEventListener('click', reset);
reset();
Watch how frequent items (A, B, C in the default stream) survive the cancellations while rare ones vanish. The algorithm never promises exact counts — it promises that any item appearing more than n/k times will be in the final summary. The demo also shows how many cancellation rounds occurred, which is bounded by ⌊n/(k−1)⌋.
Comments
Loading comments...