Below is a grid of nodes, each colored blue or red at random. Click Step to advance one full synchronous round — every node simultaneously copies one random neighbor — or click Run to animate the process until one color wins.
<!-- Grid of nodes; each step every node copies a random neighbor's opinion (blue or red) -->
<div class="controls">
<span id="status" class="status">Ready — step or run to start.</span>
<div class="btns">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
</div>
<canvas id="canvas" width="360" height="280" aria-label="Voter model grid"></canvas>
<div class="legend">
<span class="dot blue"></span><span>Blue opinion</span>
<span class="dot red"></span><span>Red opinion</span>
</div>
/* Voter-model demo styles */
* { 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; }
.status { font-size: .9rem; font-weight: 600; flex: 1; min-width: 8rem; }
.btns { display: flex; gap: .4rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
canvas { display: block; border-radius: 8px; border: 1px solid #cdd9e3; width: 100%; max-width: 360px; }
.legend { display: flex; align-items: center; gap: .5rem; font-size: .82rem; margin-top: .4rem; color: #555; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: .25rem; }
.dot.blue { background: #3a7bd5; }
.dot.red { background: #e63946; }
/* Voter model — synchronous update, canvas rendering */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Grid dimensions and constants */
const ROWS = 20, COLS = 28;
const CELL = 13; /* pixels per cell */
const BLUE = 0, RED = 1;
const COLOR = ['#3a7bd5', '#e63946'];
let grid = [];
let animId = null;
let running = false;
let stepCount = 0;
/* Initialize grid with 50% blue / 50% red at random */
function initGrid() {
grid = [];
for (let r = 0; r < ROWS; r++) {
grid.push([]);
for (let c = 0; c < COLS; c++) {
grid[r].push(Math.random() < 0.5 ? BLUE : RED);
}
}
stepCount = 0;
}
/* Draw the current grid state to the canvas */
function draw() {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
ctx.fillStyle = COLOR[grid[r][c]];
ctx.fillRect(c * CELL + 1, r * CELL + 1, CELL - 1, CELL - 1);
}
}
}
/* Return the 4-neighborhood (up, down, left, right) of cell (r,c) */
function neighbors(r, c) {
const out = [];
if (r > 0) out.push([r - 1, c]);
if (r < ROWS - 1) out.push([r + 1, c]);
if (c > 0) out.push([r, c - 1]);
if (c < COLS - 1) out.push([r, c + 1]);
return out;
}
/* Return the winning opinion (BLUE or RED) if all cells agree, else -1 */
function checkConsensus() {
const first = grid[0][0];
for (let r = 0; r < ROWS; r++)
for (let c = 0; c < COLS; c++)
if (grid[r][c] !== first) return -1;
return first;
}
/* Advance one synchronous voter-model round */
function step() {
/* Every node simultaneously copies a random neighbor */
const next = grid.map(row => row.slice());
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const nb = neighbors(r, c);
const [nr, nc] = nb[Math.floor(Math.random() * nb.length)];
next[r][c] = grid[nr][nc];
}
}
grid = next;
stepCount++;
}
function countBlue() {
let b = 0;
for (let r = 0; r < ROWS; r++)
for (let c = 0; c < COLS; c++)
if (grid[r][c] === BLUE) b++;
return b;
}
function setStatus(text, cls) {
const el = document.getElementById('status');
el.textContent = text;
el.className = 'status ' + (cls || '');
}
function updateButtons(isDone) {
document.getElementById('btn-step').disabled = isDone || running;
document.getElementById('btn-run').disabled = isDone;
document.getElementById('btn-run').textContent = running ? 'Pause' : 'Run';
}
function onStep() {
step();
draw();
const winner = checkConsensus();
const pct = Math.round(countBlue() * 100 / (ROWS * COLS));
if (winner !== -1) {
const winColor = winner === BLUE ? 'Blue' : 'Red';
setStatus(i18nFmt('Consensus reached! %color% wins after %steps% steps.', { color: winColor, steps: stepCount }), 'done');
stopRun();
updateButtons(true);
} else {
setStatus(i18nFmt('Step %steps% — %pct% % blue', { steps: stepCount, pct: pct }), '');
updateButtons(false);
}
}
function startRun() {
running = true;
updateButtons(false);
function tick() {
onStep();
const winner = checkConsensus();
if (winner === -1 && running) animId = requestAnimationFrame(tick);
}
animId = requestAnimationFrame(tick);
}
function stopRun() {
running = false;
if (animId) { cancelAnimationFrame(animId); animId = null; }
}
function onRun() {
if (running) { stopRun(); updateButtons(false); return; }
startRun();
}
function onReset() {
stopRun();
initGrid();
draw();
setStatus('Ready — step or run to start.', '');
updateButtons(false);
}
document.getElementById('btn-step').addEventListener('click', onStep);
document.getElementById('btn-run').addEventListener('click', onRun);
document.getElementById('btn-reset').addEventListener('click', onReset);
/* Bootstrap */
initGrid();
draw();
setStatus('Ready — step or run to start.', '');
updateButtons(false);
<!-- Grid of nodes; each step every node copies a random neighbor's opinion (blue or red) -->
<div class="controls">
<span id="status" class="status">Ready — step or run to start.</span>
<div class="btns">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
</div>
<canvas id="canvas" width="360" height="280" aria-label="Voter model grid"></canvas>
<div class="legend">
<span class="dot blue"></span><span>Blue opinion</span>
<span class="dot red"></span><span>Red opinion</span>
</div>
/* Voter-model demo styles */
* { 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; }
.status { font-size: .9rem; font-weight: 600; flex: 1; min-width: 8rem; }
.btns { display: flex; gap: .4rem; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
canvas { display: block; border-radius: 8px; border: 1px solid #cdd9e3; width: 100%; max-width: 360px; }
.legend { display: flex; align-items: center; gap: .5rem; font-size: .82rem; margin-top: .4rem; color: #555; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: .25rem; }
.dot.blue { background: #3a7bd5; }
.dot.red { background: #e63946; }
/* Voter model — synchronous update, canvas rendering */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Grid dimensions and constants */
const ROWS = 20, COLS = 28;
const CELL = 13; /* pixels per cell */
const BLUE = 0, RED = 1;
const COLOR = ['#3a7bd5', '#e63946'];
let grid = [];
let animId = null;
let running = false;
let stepCount = 0;
/* Initialize grid with 50% blue / 50% red at random */
function initGrid() {
grid = [];
for (let r = 0; r < ROWS; r++) {
grid.push([]);
for (let c = 0; c < COLS; c++) {
grid[r].push(Math.random() < 0.5 ? BLUE : RED);
}
}
stepCount = 0;
}
/* Draw the current grid state to the canvas */
function draw() {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
ctx.fillStyle = COLOR[grid[r][c]];
ctx.fillRect(c * CELL + 1, r * CELL + 1, CELL - 1, CELL - 1);
}
}
}
/* Return the 4-neighborhood (up, down, left, right) of cell (r,c) */
function neighbors(r, c) {
const out = [];
if (r > 0) out.push([r - 1, c]);
if (r < ROWS - 1) out.push([r + 1, c]);
if (c > 0) out.push([r, c - 1]);
if (c < COLS - 1) out.push([r, c + 1]);
return out;
}
/* Return the winning opinion (BLUE or RED) if all cells agree, else -1 */
function checkConsensus() {
const first = grid[0][0];
for (let r = 0; r < ROWS; r++)
for (let c = 0; c < COLS; c++)
if (grid[r][c] !== first) return -1;
return first;
}
/* Advance one synchronous voter-model round */
function step() {
/* Every node simultaneously copies a random neighbor */
const next = grid.map(row => row.slice());
for (let r = 0; r < ROWS; r++) {
for (let c = 0; c < COLS; c++) {
const nb = neighbors(r, c);
const [nr, nc] = nb[Math.floor(Math.random() * nb.length)];
next[r][c] = grid[nr][nc];
}
}
grid = next;
stepCount++;
}
function countBlue() {
let b = 0;
for (let r = 0; r < ROWS; r++)
for (let c = 0; c < COLS; c++)
if (grid[r][c] === BLUE) b++;
return b;
}
function setStatus(text, cls) {
const el = document.getElementById('status');
el.textContent = text;
el.className = 'status ' + (cls || '');
}
function updateButtons(isDone) {
document.getElementById('btn-step').disabled = isDone || running;
document.getElementById('btn-run').disabled = isDone;
document.getElementById('btn-run').textContent = running ? 'Pause' : 'Run';
}
function onStep() {
step();
draw();
const winner = checkConsensus();
const pct = Math.round(countBlue() * 100 / (ROWS * COLS));
if (winner !== -1) {
const winColor = winner === BLUE ? 'Blue' : 'Red';
setStatus(i18nFmt('Consensus reached! %color% wins after %steps% steps.', { color: winColor, steps: stepCount }), 'done');
stopRun();
updateButtons(true);
} else {
setStatus(i18nFmt('Step %steps% — %pct% % blue', { steps: stepCount, pct: pct }), '');
updateButtons(false);
}
}
function startRun() {
running = true;
updateButtons(false);
function tick() {
onStep();
const winner = checkConsensus();
if (winner === -1 && running) animId = requestAnimationFrame(tick);
}
animId = requestAnimationFrame(tick);
}
function stopRun() {
running = false;
if (animId) { cancelAnimationFrame(animId); animId = null; }
}
function onRun() {
if (running) { stopRun(); updateButtons(false); return; }
startRun();
}
function onReset() {
stopRun();
initGrid();
draw();
setStatus('Ready — step or run to start.', '');
updateButtons(false);
}
document.getElementById('btn-step').addEventListener('click', onStep);
document.getElementById('btn-run').addEventListener('click', onRun);
document.getElementById('btn-reset').addEventListener('click', onReset);
/* Bootstrap */
initGrid();
draw();
setStatus('Ready — step or run to start.', '');
updateButtons(false);
Notice how small clusters form and grow. Isolated nodes surrounded by the opposite color get quickly overrun; large blobs persist longer. The fraction of blue nodes is a martingale — its expected value stays constant over time — so whichever color holds the majority at the start has a proportional advantage in winning the race.
Comments
Loading comments...