Below is a network of 20 nodes. One node starts infected (red). Each round, every infected node picks one random neighbor and tells it the news. Click Next Round to advance one step, or Auto Play to watch it run continuously.
<div class="controls">
<span class="label">Round: <strong id="round">0</strong></span>
<span class="label">Informed: <strong id="informed">1</strong> / <strong id="total">20</strong></span>
<button id="btn-step" type="button">Next Round</button>
<button id="btn-auto" type="button">Auto Play</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<canvas id="canvas" width="480" height="330"></canvas>
<div class="status" id="status">One node knows the secret. Watch it spread.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .5rem; }
.label { font-size: .9rem; }
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; }
canvas { display: block; border-radius: 10px; border: 1px solid #cdd9e3; background: #f7f9fb; max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; margin-top: .5rem; min-height: 1.4em; color: #1d3557; }
.status.done { color: #0a7d33; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Gossip protocol โ push model on a random graph (p=0.35 edges, graph seed 1).
// Each round every informed node picks one random neighbour and tells it the news.
// For N=20 the demo converges in ~7 rounds, illustrating O(log N) spreading.
const N = 20;
const W = 480, H = 330;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Deterministic PRNG (mulberry32) โ no Math.random() so behavior is reproducible.
function mulberry32(a) {
return function() {
a |= 0; a = a + 0x6D2B79F5 | 0;
let t = Math.imul(a ^ a >>> 15, 1 | a);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
// โโ Graph construction (graph seed = 1, p = 0.35) โโโโโโโโโโโโโโโโโโโโโโโโโโ
// Nodes are placed in a circle; edges drawn probabilistically.
// This graph is connected and gives ~7 rounds to inform all 20 nodes.
const GRAPH_SEED = 1;
const GOSSIP_SEED = 42;
const P_EDGE = 0.35;
const CX = W / 2, CY = H / 2 - 5, R = Math.min(W, H) / 2 - 46;
// Compute fixed node positions (circle, no jitter needed โ circle is clear enough)
const nodes = [];
for (let i = 0; i < N; i++) {
const angle = (2 * Math.PI * i) / N - Math.PI / 2;
nodes.push({ x: CX + R * Math.cos(angle), y: CY + R * Math.sin(angle) });
}
// Build adjacency using graph seed
function buildAdj(seed) {
const rng = mulberry32(seed);
const adj = Array.from({ length: N }, () => []);
for (let i = 0; i < N; i++) {
for (let j = i + 1; j < N; j++) {
if (rng() < P_EDGE) { adj[i].push(j); adj[j].push(i); }
}
}
return adj;
}
const adj = buildAdj(GRAPH_SEED);
// โโ Simulation state โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let informed = new Set([0]);
let roundNum = 0;
let gossipRng = mulberry32(GOSSIP_SEED);
let autoTimer = null;
function gossipRound() {
if (informed.size >= N) return;
const newlyInformed = new Set();
for (const i of informed) {
const nb = adj[i];
if (!nb.length) continue;
const target = nb[Math.floor(gossipRng() * nb.length)];
if (!informed.has(target)) newlyInformed.add(target);
}
newlyInformed.forEach(n => informed.add(n));
roundNum++;
}
// โโ Drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function draw() {
ctx.clearRect(0, 0, W, H);
// Edges
const drawn = new Set();
for (let i = 0; i < N; i++) {
for (const j of adj[i]) {
const key = Math.min(i, j) + ',' + Math.max(i, j);
if (drawn.has(key)) continue;
drawn.add(key);
const bothInformed = informed.has(i) && informed.has(j);
ctx.beginPath();
ctx.moveTo(nodes[i].x, nodes[i].y);
ctx.lineTo(nodes[j].x, nodes[j].y);
ctx.strokeStyle = bothInformed ? '#f4a26180' : '#cdd9e3';
ctx.lineWidth = bothInformed ? 1.8 : 1;
ctx.stroke();
}
}
// Nodes
for (let i = 0; i < N; i++) {
const isInformed = informed.has(i);
ctx.beginPath();
ctx.arc(nodes[i].x, nodes[i].y, 13, 0, 2 * Math.PI);
if (i === 0) {
ctx.fillStyle = '#e63946';
ctx.strokeStyle = '#c92f3c';
} else if (isInformed) {
ctx.fillStyle = '#e07b39';
ctx.strokeStyle = '#b55e20';
} else {
ctx.fillStyle = '#c9ccd1';
ctx.strokeStyle = '#adb1b8';
}
ctx.fill();
ctx.lineWidth = 1.8;
ctx.stroke();
ctx.fillStyle = isInformed ? '#fff' : '#555';
ctx.font = '700 9px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(i + 1, nodes[i].x, nodes[i].y);
}
}
// โโ UI update โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function updateUI() {
document.getElementById('round').textContent = roundNum;
document.getElementById('informed').textContent = informed.size;
const el = document.getElementById('status');
if (informed.size >= N) {
el.textContent = fmt('โ All %N% nodes informed in %roundNum% rounds! (logโ20 โ 4.3)', { N: N, roundNum: roundNum });
el.className = 'status done';
stopAuto();
} else {
el.textContent = fmt('Round %roundNum%: %size% of %N% nodes know the secret.', { roundNum: roundNum, size: informed.size, N: N });
el.className = 'status';
}
}
function stopAuto() {
if (autoTimer) { clearInterval(autoTimer); autoTimer = null; document.getElementById('btn-auto').textContent = 'Auto Play'; }
}
document.getElementById('btn-step').addEventListener('click', () => {
if (informed.size < N) { gossipRound(); draw(); updateUI(); }
});
document.getElementById('btn-auto').addEventListener('click', () => {
if (autoTimer) { stopAuto(); return; }
document.getElementById('btn-auto').textContent = 'Pause';
autoTimer = setInterval(() => {
if (informed.size < N) { gossipRound(); draw(); updateUI(); }
else stopAuto();
}, 700);
});
document.getElementById('btn-reset').addEventListener('click', () => {
stopAuto();
informed = new Set([0]);
roundNum = 0;
gossipRng = mulberry32(GOSSIP_SEED);
draw();
updateUI();
document.getElementById('status').textContent = 'One node knows the secret. Watch it spread.';
document.getElementById('status').className = 'status';
});
draw();
updateUI();
<div class="controls">
<span class="label">Round: <strong id="round">0</strong></span>
<span class="label">Informed: <strong id="informed">1</strong> / <strong id="total">20</strong></span>
<button id="btn-step" type="button">Next Round</button>
<button id="btn-auto" type="button">Auto Play</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<canvas id="canvas" width="480" height="330"></canvas>
<div class="status" id="status">One node knows the secret. Watch it spread.</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .5rem; }
.label { font-size: .9rem; }
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; }
canvas { display: block; border-radius: 10px; border: 1px solid #cdd9e3; background: #f7f9fb; max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; margin-top: .5rem; min-height: 1.4em; color: #1d3557; }
.status.done { color: #0a7d33; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Gossip protocol โ push model on a random graph (p=0.35 edges, graph seed 1).
// Each round every informed node picks one random neighbour and tells it the news.
// For N=20 the demo converges in ~7 rounds, illustrating O(log N) spreading.
const N = 20;
const W = 480, H = 330;
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
// Deterministic PRNG (mulberry32) โ no Math.random() so behavior is reproducible.
function mulberry32(a) {
return function() {
a |= 0; a = a + 0x6D2B79F5 | 0;
let t = Math.imul(a ^ a >>> 15, 1 | a);
t = t + Math.imul(t ^ t >>> 7, 61 | t) ^ t;
return ((t ^ t >>> 14) >>> 0) / 4294967296;
};
}
// โโ Graph construction (graph seed = 1, p = 0.35) โโโโโโโโโโโโโโโโโโโโโโโโโโ
// Nodes are placed in a circle; edges drawn probabilistically.
// This graph is connected and gives ~7 rounds to inform all 20 nodes.
const GRAPH_SEED = 1;
const GOSSIP_SEED = 42;
const P_EDGE = 0.35;
const CX = W / 2, CY = H / 2 - 5, R = Math.min(W, H) / 2 - 46;
// Compute fixed node positions (circle, no jitter needed โ circle is clear enough)
const nodes = [];
for (let i = 0; i < N; i++) {
const angle = (2 * Math.PI * i) / N - Math.PI / 2;
nodes.push({ x: CX + R * Math.cos(angle), y: CY + R * Math.sin(angle) });
}
// Build adjacency using graph seed
function buildAdj(seed) {
const rng = mulberry32(seed);
const adj = Array.from({ length: N }, () => []);
for (let i = 0; i < N; i++) {
for (let j = i + 1; j < N; j++) {
if (rng() < P_EDGE) { adj[i].push(j); adj[j].push(i); }
}
}
return adj;
}
const adj = buildAdj(GRAPH_SEED);
// โโ Simulation state โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let informed = new Set([0]);
let roundNum = 0;
let gossipRng = mulberry32(GOSSIP_SEED);
let autoTimer = null;
function gossipRound() {
if (informed.size >= N) return;
const newlyInformed = new Set();
for (const i of informed) {
const nb = adj[i];
if (!nb.length) continue;
const target = nb[Math.floor(gossipRng() * nb.length)];
if (!informed.has(target)) newlyInformed.add(target);
}
newlyInformed.forEach(n => informed.add(n));
roundNum++;
}
// โโ Drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function draw() {
ctx.clearRect(0, 0, W, H);
// Edges
const drawn = new Set();
for (let i = 0; i < N; i++) {
for (const j of adj[i]) {
const key = Math.min(i, j) + ',' + Math.max(i, j);
if (drawn.has(key)) continue;
drawn.add(key);
const bothInformed = informed.has(i) && informed.has(j);
ctx.beginPath();
ctx.moveTo(nodes[i].x, nodes[i].y);
ctx.lineTo(nodes[j].x, nodes[j].y);
ctx.strokeStyle = bothInformed ? '#f4a26180' : '#cdd9e3';
ctx.lineWidth = bothInformed ? 1.8 : 1;
ctx.stroke();
}
}
// Nodes
for (let i = 0; i < N; i++) {
const isInformed = informed.has(i);
ctx.beginPath();
ctx.arc(nodes[i].x, nodes[i].y, 13, 0, 2 * Math.PI);
if (i === 0) {
ctx.fillStyle = '#e63946';
ctx.strokeStyle = '#c92f3c';
} else if (isInformed) {
ctx.fillStyle = '#e07b39';
ctx.strokeStyle = '#b55e20';
} else {
ctx.fillStyle = '#c9ccd1';
ctx.strokeStyle = '#adb1b8';
}
ctx.fill();
ctx.lineWidth = 1.8;
ctx.stroke();
ctx.fillStyle = isInformed ? '#fff' : '#555';
ctx.font = '700 9px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(i + 1, nodes[i].x, nodes[i].y);
}
}
// โโ UI update โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
function updateUI() {
document.getElementById('round').textContent = roundNum;
document.getElementById('informed').textContent = informed.size;
const el = document.getElementById('status');
if (informed.size >= N) {
el.textContent = fmt('โ All %N% nodes informed in %roundNum% rounds! (logโ20 โ 4.3)', { N: N, roundNum: roundNum });
el.className = 'status done';
stopAuto();
} else {
el.textContent = fmt('Round %roundNum%: %size% of %N% nodes know the secret.', { roundNum: roundNum, size: informed.size, N: N });
el.className = 'status';
}
}
function stopAuto() {
if (autoTimer) { clearInterval(autoTimer); autoTimer = null; document.getElementById('btn-auto').textContent = 'Auto Play'; }
}
document.getElementById('btn-step').addEventListener('click', () => {
if (informed.size < N) { gossipRound(); draw(); updateUI(); }
});
document.getElementById('btn-auto').addEventListener('click', () => {
if (autoTimer) { stopAuto(); return; }
document.getElementById('btn-auto').textContent = 'Pause';
autoTimer = setInterval(() => {
if (informed.size < N) { gossipRound(); draw(); updateUI(); }
else stopAuto();
}, 700);
});
document.getElementById('btn-reset').addEventListener('click', () => {
stopAuto();
informed = new Set([0]);
roundNum = 0;
gossipRng = mulberry32(GOSSIP_SEED);
draw();
updateUI();
document.getElementById('status').textContent = 'One node knows the secret. Watch it spread.';
document.getElementById('status').className = 'status';
});
draw();
updateUI();
Notice that in the early rounds only a handful of nodes know. Then suddenly, the information seems to jump everywhere at once. That is the exponential doubling in action: once half the network is informed, the other half gets reached almost immediately. The counter shows the round number โ for 20 nodes, log2โ(20) โ 4.3, and this demo typically reaches everyone in 6โ8 rounds (a small constant multiple of log N, exactly as theory predicts).
Comments
Loading comments...