Start with a ring of 20 nodes, each connected to its 4 nearest neighbors. Then slide the rewiring probability p from 0 (pure ring) toward 1 (fully random). Watch what happens to two numbers: the diameter (longest shortest path) and the clustering coefficient (fraction of a node's neighbors that are also connected to each other).
<!-- Watts-Strogatz small-world rewiring demo -->
<div class="controls">
<label>
<span>Rewiring probability p:</span>
<input type="range" id="pSlider" min="0" max="1" step="0.01" value="0">
<span id="pVal">0.00</span>
</label>
<button id="rewireBtn" type="button">Rewire</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<canvas id="graph" width="460" height="250"></canvas>
<div class="stats">
<span>Diameter: <strong id="diam">-</strong></span>
<span>Clustering: <strong id="clust">-</strong></span>
</div>
<div class="hint-box" id="hintBox">Set p then press Rewire to see the small-world effect.</div>
/* layout and visual styles for the graph canvas */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-bottom: .5rem; }
.controls label { display: flex; align-items: center; gap: .4rem; font-size: .9rem; flex: 1 1 auto; }
input[type=range] { flex: 1 1 120px; accent-color: #1d3557; }
#pVal { min-width: 2.5rem; font-weight: 700; text-align: right; }
canvas { display: block; width: 100%; max-width: 460px; border-radius: 10px;
background: #f0f4f8; border: 1px solid #cdd9e3; }
.stats { display: flex; gap: 1.5rem; margin: .5rem 0; font-size: .9rem; }
.hint-box { font-size: .85rem; color: #444; background: #e8eef3; border-radius: 8px;
padding: .5rem .8rem; line-height: 1.45; min-height: 2.4em; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; white-space: nowrap; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Watts-Strogatz model -- ring lattice + random rewiring
const N = 20; // number of nodes
const K = 4; // each node connects to K/2 neighbors on each side
// node positions on a circle
const cx = 230, cy = 125, R = 105;
const pos = Array.from({ length: N }, (_, i) => ({
x: cx + R * Math.cos(2 * Math.PI * i / N - Math.PI / 2),
y: cy + R * Math.sin(2 * Math.PI * i / N - Math.PI / 2)
}));
// adjacency sets; rebuilt fresh on each rewire
let adj = [];
function buildRing() {
adj = Array.from({ length: N }, () => new Set());
for (let i = 0; i < N; i++) {
for (let d = 1; d <= K / 2; d++) {
const j = (i + d) % N;
adj[i].add(j); adj[j].add(i);
}
}
}
buildRing();
// rewire: for each edge that goes to the right, flip it with probability p
function rewire(p) {
buildRing();
for (let i = 0; i < N; i++) {
for (let d = 1; d <= K / 2; d++) {
if (Math.random() < p) {
const j = (i + d) % N;
adj[i].delete(j); adj[j].delete(i);
let t;
do { t = Math.floor(Math.random() * N); } while (t === i || adj[i].has(t));
adj[i].add(t); adj[t].add(i);
}
}
}
}
// BFS from src, returns distance array
function bfs(src) {
const dist = new Array(N).fill(-1);
dist[src] = 0;
const q = [src];
let qi = 0;
while (qi < q.length) {
const u = q[qi++];
for (const v of adj[u]) {
if (dist[v] === -1) { dist[v] = dist[u] + 1; q.push(v); }
}
}
return dist;
}
function computeDiameter() {
let max = 0;
for (let i = 0; i < N; i++) {
const d = bfs(i);
for (const v of d) if (v > max) max = v;
}
return max;
}
// average local clustering coefficient
function computeClustering() {
let total = 0;
for (let i = 0; i < N; i++) {
const nb = [...adj[i]];
const deg = nb.length;
if (deg < 2) continue;
let tri = 0;
for (let a = 0; a < nb.length; a++)
for (let b = a + 1; b < nb.length; b++)
if (adj[nb[a]].has(nb[b])) tri++;
total += (2 * tri) / (deg * (deg - 1));
}
return total / N;
}
// draw graph on canvas
const canvas = document.getElementById('graph');
const ctx = canvas.getContext('2d');
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// draw edges first
ctx.strokeStyle = '#93b4d0'; ctx.lineWidth = 1;
for (let i = 0; i < N; i++)
for (const j of adj[i]) if (j > i) {
ctx.beginPath(); ctx.moveTo(pos[i].x, pos[i].y);
ctx.lineTo(pos[j].x, pos[j].y); ctx.stroke();
}
// then nodes on top
for (let i = 0; i < N; i++) {
ctx.beginPath(); ctx.arc(pos[i].x, pos[i].y, 7, 0, 2 * Math.PI);
ctx.fillStyle = '#1d3557'; ctx.fill();
ctx.strokeStyle = '#f0f4f8'; ctx.lineWidth = 1.5; ctx.stroke();
}
}
function updateStats() {
const d = computeDiameter();
const c = computeClustering();
document.getElementById('diam').textContent = d;
document.getElementById('clust').textContent = c.toFixed(2);
return { d, c };
}
function updateHint(p, d, c) {
const box = document.getElementById('hintBox');
if (p === 0) {
box.textContent = 'Pure ring: high clustering, large diameter — no shortcuts yet.';
} else if (p >= 0.9) {
box.textContent = 'Nearly random: diameter is tiny, but clustering has collapsed.';
} else if (p <= 0.1) {
box.innerHTML = i18nFmt('Small-world sweet spot! Diameter = %d%, clustering = %c%. A few rewired edges did most of the work.', { d: d, c: c.toFixed(2) });
} else {
box.textContent = 'Intermediate regime — diameter is falling, clustering is still reasonably high.';
}
}
const slider = document.getElementById('pSlider');
const pValEl = document.getElementById('pVal');
slider.addEventListener('input', () => {
pValEl.textContent = parseFloat(slider.value).toFixed(2);
});
document.getElementById('rewireBtn').addEventListener('click', () => {
const p = parseFloat(slider.value);
rewire(p);
draw();
const { d, c } = updateStats();
updateHint(p, d, c);
});
document.getElementById('resetBtn').addEventListener('click', () => {
slider.value = 0;
pValEl.textContent = '0.00';
buildRing();
draw();
const { d, c } = updateStats();
document.getElementById('hintBox').textContent = 'Set p then press Rewire to see the small-world effect.';
});
// initial render
draw();
updateStats();
<!-- Watts-Strogatz small-world rewiring demo -->
<div class="controls">
<label>
<span>Rewiring probability p:</span>
<input type="range" id="pSlider" min="0" max="1" step="0.01" value="0">
<span id="pVal">0.00</span>
</label>
<button id="rewireBtn" type="button">Rewire</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<canvas id="graph" width="460" height="250"></canvas>
<div class="stats">
<span>Diameter: <strong id="diam">-</strong></span>
<span>Clustering: <strong id="clust">-</strong></span>
</div>
<div class="hint-box" id="hintBox">Set p then press Rewire to see the small-world effect.</div>
/* layout and visual styles for the graph canvas */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; flex-wrap: wrap; align-items: center; gap: .5rem; margin-bottom: .5rem; }
.controls label { display: flex; align-items: center; gap: .4rem; font-size: .9rem; flex: 1 1 auto; }
input[type=range] { flex: 1 1 120px; accent-color: #1d3557; }
#pVal { min-width: 2.5rem; font-weight: 700; text-align: right; }
canvas { display: block; width: 100%; max-width: 460px; border-radius: 10px;
background: #f0f4f8; border: 1px solid #cdd9e3; }
.stats { display: flex; gap: 1.5rem; margin: .5rem 0; font-size: .9rem; }
.hint-box { font-size: .85rem; color: #444; background: #e8eef3; border-radius: 8px;
padding: .5rem .8rem; line-height: 1.45; min-height: 2.4em; }
button { font: 600 14px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; white-space: nowrap; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Watts-Strogatz model -- ring lattice + random rewiring
const N = 20; // number of nodes
const K = 4; // each node connects to K/2 neighbors on each side
// node positions on a circle
const cx = 230, cy = 125, R = 105;
const pos = Array.from({ length: N }, (_, i) => ({
x: cx + R * Math.cos(2 * Math.PI * i / N - Math.PI / 2),
y: cy + R * Math.sin(2 * Math.PI * i / N - Math.PI / 2)
}));
// adjacency sets; rebuilt fresh on each rewire
let adj = [];
function buildRing() {
adj = Array.from({ length: N }, () => new Set());
for (let i = 0; i < N; i++) {
for (let d = 1; d <= K / 2; d++) {
const j = (i + d) % N;
adj[i].add(j); adj[j].add(i);
}
}
}
buildRing();
// rewire: for each edge that goes to the right, flip it with probability p
function rewire(p) {
buildRing();
for (let i = 0; i < N; i++) {
for (let d = 1; d <= K / 2; d++) {
if (Math.random() < p) {
const j = (i + d) % N;
adj[i].delete(j); adj[j].delete(i);
let t;
do { t = Math.floor(Math.random() * N); } while (t === i || adj[i].has(t));
adj[i].add(t); adj[t].add(i);
}
}
}
}
// BFS from src, returns distance array
function bfs(src) {
const dist = new Array(N).fill(-1);
dist[src] = 0;
const q = [src];
let qi = 0;
while (qi < q.length) {
const u = q[qi++];
for (const v of adj[u]) {
if (dist[v] === -1) { dist[v] = dist[u] + 1; q.push(v); }
}
}
return dist;
}
function computeDiameter() {
let max = 0;
for (let i = 0; i < N; i++) {
const d = bfs(i);
for (const v of d) if (v > max) max = v;
}
return max;
}
// average local clustering coefficient
function computeClustering() {
let total = 0;
for (let i = 0; i < N; i++) {
const nb = [...adj[i]];
const deg = nb.length;
if (deg < 2) continue;
let tri = 0;
for (let a = 0; a < nb.length; a++)
for (let b = a + 1; b < nb.length; b++)
if (adj[nb[a]].has(nb[b])) tri++;
total += (2 * tri) / (deg * (deg - 1));
}
return total / N;
}
// draw graph on canvas
const canvas = document.getElementById('graph');
const ctx = canvas.getContext('2d');
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// draw edges first
ctx.strokeStyle = '#93b4d0'; ctx.lineWidth = 1;
for (let i = 0; i < N; i++)
for (const j of adj[i]) if (j > i) {
ctx.beginPath(); ctx.moveTo(pos[i].x, pos[i].y);
ctx.lineTo(pos[j].x, pos[j].y); ctx.stroke();
}
// then nodes on top
for (let i = 0; i < N; i++) {
ctx.beginPath(); ctx.arc(pos[i].x, pos[i].y, 7, 0, 2 * Math.PI);
ctx.fillStyle = '#1d3557'; ctx.fill();
ctx.strokeStyle = '#f0f4f8'; ctx.lineWidth = 1.5; ctx.stroke();
}
}
function updateStats() {
const d = computeDiameter();
const c = computeClustering();
document.getElementById('diam').textContent = d;
document.getElementById('clust').textContent = c.toFixed(2);
return { d, c };
}
function updateHint(p, d, c) {
const box = document.getElementById('hintBox');
if (p === 0) {
box.textContent = 'Pure ring: high clustering, large diameter — no shortcuts yet.';
} else if (p >= 0.9) {
box.textContent = 'Nearly random: diameter is tiny, but clustering has collapsed.';
} else if (p <= 0.1) {
box.innerHTML = i18nFmt('Small-world sweet spot! Diameter = %d%, clustering = %c%. A few rewired edges did most of the work.', { d: d, c: c.toFixed(2) });
} else {
box.textContent = 'Intermediate regime — diameter is falling, clustering is still reasonably high.';
}
}
const slider = document.getElementById('pSlider');
const pValEl = document.getElementById('pVal');
slider.addEventListener('input', () => {
pValEl.textContent = parseFloat(slider.value).toFixed(2);
});
document.getElementById('rewireBtn').addEventListener('click', () => {
const p = parseFloat(slider.value);
rewire(p);
draw();
const { d, c } = updateStats();
updateHint(p, d, c);
});
document.getElementById('resetBtn').addEventListener('click', () => {
slider.value = 0;
pValEl.textContent = '0.00';
buildRing();
draw();
const { d, c } = updateStats();
document.getElementById('hintBox').textContent = 'Set p then press Rewire to see the small-world effect.';
});
// initial render
draw();
updateStats();
At p=0 the graph is a tidy ring — high clustering, but a large diameter. At p=1 it's effectively random — tiny diameter, but clustering has collapsed. The sweet spot is in between: even at p≈0.05 the diameter has already fallen most of the way, while clustering barely budged. Those few random wires are doing enormous work.
Comments
Loading comments...