<div class="controls">
<label for="prob-slider">
Edge probability <strong id="prob-val">p = 0.00</strong>
<span class="threshold-tag" id="threshold-tag"></span>
</label>
<input type="range" id="prob-slider" min="0" max="100" value="0" step="1">
<div class="stats" id="stats">Nodes: 20 | Edges: 0 | Largest component: 1</div>
</div>
<canvas id="graph-canvas" width="480" height="300"></canvas>
<div class="btns">
<button id="btn-rebuild" type="button">Resample graph</button>
<button id="btn-reset" type="button" class="ghost">Reset to p = 0</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.controls { margin-bottom: .5rem; }
label { font-size: .9rem; display: block; margin-bottom: .3rem; color: #444; }
label strong { color: #1d3557; font-size: 1rem; }
.threshold-tag { display: inline-block; margin-left: .4rem; font-size: .78rem;
padding: .1rem .4rem; border-radius: 4px; font-weight: 600;
background: #e8eef3; color: #1d3557; vertical-align: middle; }
.threshold-tag.below { background: #f0f4ff; color: #455aa0; }
.threshold-tag.at { background: #fff7e0; color: #9a6800; }
.threshold-tag.above { background: #e8f7ee; color: #0a7d33; }
input[type=range] { width: 100%; accent-color: #1d3557; cursor: pointer; }
.stats { font-size: .82rem; color: #555; margin-top: .3rem; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
background: #f8fafc; width: 100%; max-width: 480px; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .6rem; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Erdős–Rényi G(n,p) demo.
// Each pair of the N nodes is connected independently with probability p.
// We find connected components via union-find and colour nodes by component.
// The slider maps [0,100] → p in [0,1]. Threshold is at p = 1/N.
const N = 20;
const canvas = document.getElementById('graph-canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const slider = document.getElementById('prob-slider');
const probVal = document.getElementById('prob-val');
const thresholdTag = document.getElementById('threshold-tag');
const statsEl = document.getElementById('stats');
// Fixed node positions (deterministic layout – circle)
const nodes = [];
for (let i = 0; i < N; i++) {
const angle = (2 * Math.PI * i) / N - Math.PI / 2;
nodes.push({ x: W / 2 + (W * 0.4) * Math.cos(angle),
y: H / 2 + (H * 0.42) * Math.sin(angle) });
}
// Union-Find
function makeUF(n) {
const p = Array.from({ length: n }, (_, i) => i);
const r = new Array(n).fill(0);
function find(x) { while (p[x] !== x) { p[x] = p[p[x]]; x = p[x]; } return x; }
function union(a, b) { a = find(a); b = find(b); if (a === b) return; if (r[a] < r[b]) [a,b]=[b,a]; p[b]=a; if(r[a]===r[b]) r[a]++; }
return { find, union, p };
}
// Color palette for components (up to 10 distinct, then gray)
const PALETTE = ['#e63946','#457b9d','#2a9d8f','#e9c46a','#f4a261',
'#6a4c93','#1d3557','#3a86ff','#fb5607','#06d6a0'];
let currentEdges = [];
let currentComp = [];
function buildGraph(p) {
const uf = makeUF(N);
const edges = [];
for (let i = 0; i < N; i++) {
for (let j = i + 1; j < N; j++) {
if (Math.random() < p) { edges.push([i, j]); uf.union(i, j); }
}
}
// Assign component ids
const rootMap = {};
let nextId = 0;
const comp = nodes.map((_, i) => {
const r = uf.find(i);
if (!(r in rootMap)) rootMap[r] = nextId++;
return rootMap[r];
});
// Component sizes
const size = {};
comp.forEach(c => { size[c] = (size[c] || 0) + 1; });
const maxSize = Math.max(...Object.values(size));
// Find the giant component id (largest)
const giant = parseInt(Object.entries(size).sort((a,b) => b[1]-a[1])[0][0]);
return { edges, comp, size, maxSize, giant };
}
function draw(p) {
const { edges, comp, size, maxSize, giant } = buildGraph(p);
currentEdges = edges;
currentComp = comp;
ctx.clearRect(0, 0, W, H);
// Draw edges
edges.forEach(([i, j]) => {
const ci = comp[i];
const isGiantEdge = ci === giant && maxSize > 1;
ctx.beginPath();
ctx.moveTo(nodes[i].x, nodes[i].y);
ctx.lineTo(nodes[j].x, nodes[j].y);
ctx.strokeStyle = isGiantEdge ? 'rgba(70,130,80,0.45)' : 'rgba(150,170,190,0.5)';
ctx.lineWidth = isGiantEdge ? 1.8 : 1;
ctx.stroke();
});
// Draw nodes
nodes.forEach((node, i) => {
const c = comp[i];
const isGiant = c === giant && maxSize > 1;
const color = isGiant ? '#0a7d33' : (size[c] > 1 ? PALETTE[c % PALETTE.length] : '#b0bec5');
ctx.beginPath();
ctx.arc(node.x, node.y, isGiant ? 7 : 5, 0, 2 * Math.PI);
ctx.fillStyle = color;
ctx.fill();
ctx.strokeStyle = isGiant ? '#07602a' : 'rgba(0,0,0,0.15)';
ctx.lineWidth = 1;
ctx.stroke();
});
// Stats
statsEl.innerHTML = fmt('Nodes: %N% | Edges: %length% | Largest component: <strong>%maxSize%</strong> nodes', { N: N, length: edges.length, maxSize: maxSize });
// Threshold label
const threshold = 1 / N;
let tag = '', cls = '';
if (p < threshold * 0.85) { tag = 'below threshold'; cls = 'below'; }
else if (p <= threshold * 1.15) { tag = '≈ threshold 1/n'; cls = 'at'; }
else { tag = 'above threshold'; cls = 'above'; }
thresholdTag.textContent = tag;
thresholdTag.className = 'threshold-tag ' + cls;
}
function update() {
const p = parseInt(slider.value) / 100;
probVal.textContent = 'p = ' + p.toFixed(2);
draw(p);
}
slider.addEventListener('input', update);
document.getElementById('btn-rebuild').addEventListener('click', update);
document.getElementById('btn-reset').addEventListener('click', () => {
slider.value = 0;
update();
});
update();
// Node-side verification (run with node to check threshold math)
// At p=1/N=0.05 (N=20), expected edges = C(20,2)*0.05 = 190*0.05 = 9.5
// Giant component expected size ~ 2*eps*N for p=(1+eps)/N
// For p=0.1=(1+1)/N with N=20: eps=1, expected giant ~ 2*1*0.8*20 ≈ (by formula) beta*N
// where beta satisfies beta + e^(-2*beta) = 1 -> beta ≈ 0.797 -> giant ≈ 15.9 nodes
console.log(fmt('Erdős–Rényi demo loaded. N=%N%, threshold p=%v%', { N: N, v: (1/N).toFixed(4) }));
<div class="controls">
<label for="prob-slider">
Edge probability <strong id="prob-val">p = 0.00</strong>
<span class="threshold-tag" id="threshold-tag"></span>
</label>
<input type="range" id="prob-slider" min="0" max="100" value="0" step="1">
<div class="stats" id="stats">Nodes: 20 | Edges: 0 | Largest component: 1</div>
</div>
<canvas id="graph-canvas" width="480" height="300"></canvas>
<div class="btns">
<button id="btn-rebuild" type="button">Resample graph</button>
<button id="btn-reset" type="button" class="ghost">Reset to p = 0</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; padding: 14px; }
.controls { margin-bottom: .5rem; }
label { font-size: .9rem; display: block; margin-bottom: .3rem; color: #444; }
label strong { color: #1d3557; font-size: 1rem; }
.threshold-tag { display: inline-block; margin-left: .4rem; font-size: .78rem;
padding: .1rem .4rem; border-radius: 4px; font-weight: 600;
background: #e8eef3; color: #1d3557; vertical-align: middle; }
.threshold-tag.below { background: #f0f4ff; color: #455aa0; }
.threshold-tag.at { background: #fff7e0; color: #9a6800; }
.threshold-tag.above { background: #e8f7ee; color: #0a7d33; }
input[type=range] { width: 100%; accent-color: #1d3557; cursor: pointer; }
.stats { font-size: .82rem; color: #555; margin-top: .3rem; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
background: #f8fafc; width: 100%; max-width: 480px; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; margin-top: .6rem; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Erdős–Rényi G(n,p) demo.
// Each pair of the N nodes is connected independently with probability p.
// We find connected components via union-find and colour nodes by component.
// The slider maps [0,100] → p in [0,1]. Threshold is at p = 1/N.
const N = 20;
const canvas = document.getElementById('graph-canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const slider = document.getElementById('prob-slider');
const probVal = document.getElementById('prob-val');
const thresholdTag = document.getElementById('threshold-tag');
const statsEl = document.getElementById('stats');
// Fixed node positions (deterministic layout – circle)
const nodes = [];
for (let i = 0; i < N; i++) {
const angle = (2 * Math.PI * i) / N - Math.PI / 2;
nodes.push({ x: W / 2 + (W * 0.4) * Math.cos(angle),
y: H / 2 + (H * 0.42) * Math.sin(angle) });
}
// Union-Find
function makeUF(n) {
const p = Array.from({ length: n }, (_, i) => i);
const r = new Array(n).fill(0);
function find(x) { while (p[x] !== x) { p[x] = p[p[x]]; x = p[x]; } return x; }
function union(a, b) { a = find(a); b = find(b); if (a === b) return; if (r[a] < r[b]) [a,b]=[b,a]; p[b]=a; if(r[a]===r[b]) r[a]++; }
return { find, union, p };
}
// Color palette for components (up to 10 distinct, then gray)
const PALETTE = ['#e63946','#457b9d','#2a9d8f','#e9c46a','#f4a261',
'#6a4c93','#1d3557','#3a86ff','#fb5607','#06d6a0'];
let currentEdges = [];
let currentComp = [];
function buildGraph(p) {
const uf = makeUF(N);
const edges = [];
for (let i = 0; i < N; i++) {
for (let j = i + 1; j < N; j++) {
if (Math.random() < p) { edges.push([i, j]); uf.union(i, j); }
}
}
// Assign component ids
const rootMap = {};
let nextId = 0;
const comp = nodes.map((_, i) => {
const r = uf.find(i);
if (!(r in rootMap)) rootMap[r] = nextId++;
return rootMap[r];
});
// Component sizes
const size = {};
comp.forEach(c => { size[c] = (size[c] || 0) + 1; });
const maxSize = Math.max(...Object.values(size));
// Find the giant component id (largest)
const giant = parseInt(Object.entries(size).sort((a,b) => b[1]-a[1])[0][0]);
return { edges, comp, size, maxSize, giant };
}
function draw(p) {
const { edges, comp, size, maxSize, giant } = buildGraph(p);
currentEdges = edges;
currentComp = comp;
ctx.clearRect(0, 0, W, H);
// Draw edges
edges.forEach(([i, j]) => {
const ci = comp[i];
const isGiantEdge = ci === giant && maxSize > 1;
ctx.beginPath();
ctx.moveTo(nodes[i].x, nodes[i].y);
ctx.lineTo(nodes[j].x, nodes[j].y);
ctx.strokeStyle = isGiantEdge ? 'rgba(70,130,80,0.45)' : 'rgba(150,170,190,0.5)';
ctx.lineWidth = isGiantEdge ? 1.8 : 1;
ctx.stroke();
});
// Draw nodes
nodes.forEach((node, i) => {
const c = comp[i];
const isGiant = c === giant && maxSize > 1;
const color = isGiant ? '#0a7d33' : (size[c] > 1 ? PALETTE[c % PALETTE.length] : '#b0bec5');
ctx.beginPath();
ctx.arc(node.x, node.y, isGiant ? 7 : 5, 0, 2 * Math.PI);
ctx.fillStyle = color;
ctx.fill();
ctx.strokeStyle = isGiant ? '#07602a' : 'rgba(0,0,0,0.15)';
ctx.lineWidth = 1;
ctx.stroke();
});
// Stats
statsEl.innerHTML = fmt('Nodes: %N% | Edges: %length% | Largest component: <strong>%maxSize%</strong> nodes', { N: N, length: edges.length, maxSize: maxSize });
// Threshold label
const threshold = 1 / N;
let tag = '', cls = '';
if (p < threshold * 0.85) { tag = 'below threshold'; cls = 'below'; }
else if (p <= threshold * 1.15) { tag = '≈ threshold 1/n'; cls = 'at'; }
else { tag = 'above threshold'; cls = 'above'; }
thresholdTag.textContent = tag;
thresholdTag.className = 'threshold-tag ' + cls;
}
function update() {
const p = parseInt(slider.value) / 100;
probVal.textContent = 'p = ' + p.toFixed(2);
draw(p);
}
slider.addEventListener('input', update);
document.getElementById('btn-rebuild').addEventListener('click', update);
document.getElementById('btn-reset').addEventListener('click', () => {
slider.value = 0;
update();
});
update();
// Node-side verification (run with node to check threshold math)
// At p=1/N=0.05 (N=20), expected edges = C(20,2)*0.05 = 190*0.05 = 9.5
// Giant component expected size ~ 2*eps*N for p=(1+eps)/N
// For p=0.1=(1+1)/N with N=20: eps=1, expected giant ~ 2*1*0.8*20 ≈ (by formula) beta*N
// where beta satisfies beta + e^(-2*beta) = 1 -> beta ≈ 0.797 -> giant ≈ 15.9 nodes
console.log(fmt('Erdős–Rényi demo loaded. N=%N%, threshold p=%v%', { N: N, v: (1/N).toFixed(4) }));
Notice the asymmetry. Below the threshold the graph is a collection of small, mostly isolated pieces — the largest component stays small. Cross the threshold and a single component suddenly dominates, swallowing most nodes in one leap. The slider moves continuously; the graph changes abruptly. That abruptness is the mathematical signature of a phase transition.
Comments
Loading comments...