Drag the probability slider to set how densely the grid is filled. Click New grid to generate a fresh random layout at that density. Open cells are shown in blue; the spanning cluster — the largest connected group that links top to bottom — is highlighted in orange. A path exists message confirms the connection.
<div class="controls">
<label for="prob">Probability p = <span id="pval">0.55</span></label>
<input type="range" id="prob" min="0" max="100" value="55" step="1">
<button id="newgrid" type="button">New grid</button>
</div>
<canvas id="canvas" width="300" height="300"></canvas>
<div id="status" class="status">—</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: .6rem; font-size: .9rem; }
label { font-weight: 600; white-space: nowrap; }
input[type=range] { flex: 1; min-width: 120px; max-width: 200px; cursor: pointer; }
button { font: 600 13px system-ui; padding: .35rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; white-space: nowrap; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 6px; max-width: 100%; }
.status { margin-top: .55rem; font-weight: 600; font-size: .95rem; min-height: 1.4em; }
.status.yes { color: #0a7d33; }
.status.no { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Percolation demo: site percolation on an N×N grid.
// Each cell is independently open with probability p.
// We BFS from all open cells in the top row and check whether
// any open cell in the bottom row is reachable — a "spanning cluster".
// The spanning cluster (if any) is highlighted in orange.
// Critical threshold for site percolation on the square lattice ≈ 0.5927.
const N = 30; // grid side
const CELL = 10; // px per cell
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = N * CELL;
canvas.height = N * CELL;
const probSlider = document.getElementById('prob');
const pvalSpan = document.getElementById('pval');
const statusDiv = document.getElementById('status');
// Color palette
const C_BLOCKED = '#e8eef3'; // light grey
const C_OPEN = '#a8c4e0'; // soft blue
const C_SPANNING = '#f4a261'; // orange (spanning cluster)
const C_BORDER = '#cdd9e3';
let grid = []; // grid[r][c] = true if open
function generate(p) {
grid = [];
for (let r = 0; r < N; r++) {
grid[r] = [];
for (let c = 0; c < N; c++) {
grid[r][c] = Math.random() < p;
}
}
}
// BFS from top row open cells. Returns:
// { reached: Set of 'r,c' strings in spanning cluster, spans: boolean }
function percolate() {
const visited = new Array(N).fill(null).map(() => new Uint8Array(N));
const queue = [];
// Seed from top row
for (let c = 0; c < N; c++) {
if (grid[0][c] && !visited[0][c]) {
visited[0][c] = 1;
queue.push([0, c]);
}
}
let head = 0;
while (head < queue.length) {
const [r, c] = queue[head++];
const dirs = [[-1,0],[1,0],[0,-1],[0,1]];
for (const [dr, dc] of dirs) {
const nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < N && nc >= 0 && nc < N && grid[nr][nc] && !visited[nr][nc]) {
visited[nr][nc] = 1;
queue.push([nr, nc]);
}
}
}
// Check if bottom row reached
let spans = false;
for (let c = 0; c < N; c++) {
if (visited[N-1][c]) { spans = true; break; }
}
return { visited, spans };
}
function draw() {
const { visited, spans } = percolate();
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
let color;
if (!grid[r][c]) {
color = C_BLOCKED;
} else if (visited[r][c]) {
color = C_SPANNING; // part of spanning BFS tree
} else {
color = C_OPEN;
}
ctx.fillStyle = color;
ctx.fillRect(c * CELL, r * CELL, CELL, CELL);
ctx.strokeStyle = C_BORDER;
ctx.strokeRect(c * CELL + 0.5, r * CELL + 0.5, CELL - 1, CELL - 1);
}
}
if (spans) {
statusDiv.textContent = '✓ Spanning path exists — percolation!';
statusDiv.className = 'status yes';
} else {
statusDiv.textContent = '✗ No spanning path — grid is blocked.';
statusDiv.className = 'status no';
}
}
function refresh() {
const p = probSlider.value / 100;
pvalSpan.textContent = p.toFixed(2);
generate(p);
draw();
// Log for node verification
const open = grid.flat().filter(Boolean).length;
console.log(fmt('p=%p% open=%open%/%v%', { p: p.toFixed(2), open: open, v: N*N }));
}
probSlider.addEventListener('input', () => {
pvalSpan.textContent = (probSlider.value / 100).toFixed(2);
});
probSlider.addEventListener('change', refresh);
document.getElementById('newgrid').addEventListener('click', refresh);
refresh();
<div class="controls">
<label for="prob">Probability p = <span id="pval">0.55</span></label>
<input type="range" id="prob" min="0" max="100" value="55" step="1">
<button id="newgrid" type="button">New grid</button>
</div>
<canvas id="canvas" width="300" height="300"></canvas>
<div id="status" class="status">—</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: .6rem; font-size: .9rem; }
label { font-weight: 600; white-space: nowrap; }
input[type=range] { flex: 1; min-width: 120px; max-width: 200px; cursor: pointer; }
button { font: 600 13px system-ui; padding: .35rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; white-space: nowrap; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 6px; max-width: 100%; }
.status { margin-top: .55rem; font-weight: 600; font-size: .95rem; min-height: 1.4em; }
.status.yes { color: #0a7d33; }
.status.no { color: #c92f3c; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Percolation demo: site percolation on an N×N grid.
// Each cell is independently open with probability p.
// We BFS from all open cells in the top row and check whether
// any open cell in the bottom row is reachable — a "spanning cluster".
// The spanning cluster (if any) is highlighted in orange.
// Critical threshold for site percolation on the square lattice ≈ 0.5927.
const N = 30; // grid side
const CELL = 10; // px per cell
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
canvas.width = N * CELL;
canvas.height = N * CELL;
const probSlider = document.getElementById('prob');
const pvalSpan = document.getElementById('pval');
const statusDiv = document.getElementById('status');
// Color palette
const C_BLOCKED = '#e8eef3'; // light grey
const C_OPEN = '#a8c4e0'; // soft blue
const C_SPANNING = '#f4a261'; // orange (spanning cluster)
const C_BORDER = '#cdd9e3';
let grid = []; // grid[r][c] = true if open
function generate(p) {
grid = [];
for (let r = 0; r < N; r++) {
grid[r] = [];
for (let c = 0; c < N; c++) {
grid[r][c] = Math.random() < p;
}
}
}
// BFS from top row open cells. Returns:
// { reached: Set of 'r,c' strings in spanning cluster, spans: boolean }
function percolate() {
const visited = new Array(N).fill(null).map(() => new Uint8Array(N));
const queue = [];
// Seed from top row
for (let c = 0; c < N; c++) {
if (grid[0][c] && !visited[0][c]) {
visited[0][c] = 1;
queue.push([0, c]);
}
}
let head = 0;
while (head < queue.length) {
const [r, c] = queue[head++];
const dirs = [[-1,0],[1,0],[0,-1],[0,1]];
for (const [dr, dc] of dirs) {
const nr = r + dr, nc = c + dc;
if (nr >= 0 && nr < N && nc >= 0 && nc < N && grid[nr][nc] && !visited[nr][nc]) {
visited[nr][nc] = 1;
queue.push([nr, nc]);
}
}
}
// Check if bottom row reached
let spans = false;
for (let c = 0; c < N; c++) {
if (visited[N-1][c]) { spans = true; break; }
}
return { visited, spans };
}
function draw() {
const { visited, spans } = percolate();
ctx.clearRect(0, 0, canvas.width, canvas.height);
for (let r = 0; r < N; r++) {
for (let c = 0; c < N; c++) {
let color;
if (!grid[r][c]) {
color = C_BLOCKED;
} else if (visited[r][c]) {
color = C_SPANNING; // part of spanning BFS tree
} else {
color = C_OPEN;
}
ctx.fillStyle = color;
ctx.fillRect(c * CELL, r * CELL, CELL, CELL);
ctx.strokeStyle = C_BORDER;
ctx.strokeRect(c * CELL + 0.5, r * CELL + 0.5, CELL - 1, CELL - 1);
}
}
if (spans) {
statusDiv.textContent = '✓ Spanning path exists — percolation!';
statusDiv.className = 'status yes';
} else {
statusDiv.textContent = '✗ No spanning path — grid is blocked.';
statusDiv.className = 'status no';
}
}
function refresh() {
const p = probSlider.value / 100;
pvalSpan.textContent = p.toFixed(2);
generate(p);
draw();
// Log for node verification
const open = grid.flat().filter(Boolean).length;
console.log(fmt('p=%p% open=%open%/%v%', { p: p.toFixed(2), open: open, v: N*N }));
}
probSlider.addEventListener('input', () => {
pvalSpan.textContent = (probSlider.value / 100).toFixed(2);
});
probSlider.addEventListener('change', refresh);
document.getElementById('newgrid').addEventListener('click', refresh);
refresh();
Notice how quickly the picture changes around p ≈ 0.59. Below that value the grid stays fragmented; above it an orange highway almost always appears. The transition is not gradual — it is a sharp threshold, the hallmark of a phase transition. In an infinite grid the switch from "no path" to "path certain" would be instantaneous at exactly p_c = 0.592746…
This is closely related to the ideas behind small-world networks and the sudden spread of an epidemic past its reproduction number.
Comments
Loading comments...