The simulator below starts with a small seed network and adds one node at a time. Each new node makes m connections, choosing targets with probability proportional to their current degree — nodes with more links are more likely to be chosen. That single rule is preferential attachment.
<!-- Main layout: left = network canvas, right = degree-distribution bar chart -->
<div id="controls">
<label>
<span>Mode:</span>
<select id="modeSelect">
<option value="preferential">Preferential attachment</option>
<option value="random">Random attachment</option>
</select>
</label>
<label>
<span>Links per node (m):</span>
<select id="mSelect">
<option value="1">1</option>
<option value="2" selected>2</option>
<option value="3">3</option>
</select>
</label>
<button id="stepBtn" type="button">+ Add node</button>
<button id="runBtn" type="button">Auto-grow</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div id="main">
<canvas id="netCanvas" width="280" height="280"></canvas>
<div id="chartArea">
<div id="chartTitle">Degree distribution</div>
<canvas id="barCanvas" width="200" height="200"></canvas>
<div id="stats"></div>
</div>
</div>
<div id="statusBar"></div>
/* Layout and visual styles for the network simulator */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; font-size: 13px; color: #222; background: #fff; }
#controls {
display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
padding: 6px 8px; background: #f0f4f8; border-bottom: 1px solid #d0d8e4;
}
label { display: flex; align-items: center; gap: 4px; font-size: 12px; }
select { font-size: 12px; padding: 2px 4px; border: 1px solid #aab; border-radius: 4px; }
button {
font: 600 12px system-ui; padding: 4px 10px;
background: #1d4ed8; color: #fff; border: none; border-radius: 6px; cursor: pointer;
}
button.ghost { background: #fff; color: #1d4ed8; border: 1px solid #1d4ed8; }
button:disabled { opacity: .45; cursor: default; }
#main { display: flex; gap: 8px; padding: 8px; align-items: flex-start; }
canvas { display: block; border-radius: 6px; background: #f7f9fb; border: 1px solid #dde3ea; }
#chartArea { display: flex; flex-direction: column; gap: 4px; }
#chartTitle { font-size: 11px; font-weight: 600; color: #555; text-align: center; }
#stats { font-size: 11px; color: #555; text-align: center; line-height: 1.5; }
#statusBar { padding: 4px 8px; font-size: 12px; font-weight: 600; color: #1d4ed8; min-height: 20px; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Network state: nodes array, edges array, animation handle */
const M0 = 3; /* seed clique size */
let nodes = [], edges = [], running = false, raf = null;
const netCtx = document.getElementById('netCanvas').getContext('2d');
const barCtx = document.getElementById('barCanvas').getContext('2d');
const netW = 280, netH = 280, barW = 200, barH = 200;
function initSeed() {
/* Build seed clique of M0 fully-connected nodes */
nodes = [];
edges = [];
for (let i = 0; i < M0; i++) {
const angle = (2 * Math.PI * i) / M0;
nodes.push({ id: i, x: netW / 2 + 60 * Math.cos(angle), y: netH / 2 + 60 * Math.sin(angle), degree: 0 });
}
/* Connect all seed nodes to each other */
for (let i = 0; i < M0; i++) {
for (let j = i + 1; j < M0; j++) {
edges.push([i, j]);
nodes[i].degree++;
nodes[j].degree++;
}
}
}
function totalDegree() {
return nodes.reduce((s, n) => s + n.degree, 0);
}
function pickTarget(exclude, mode) {
/* Choose one target not already connected to the new node */
const candidates = nodes.filter(n => !exclude.has(n.id));
if (!candidates.length) return null;
if (mode === 'random') {
return candidates[Math.floor(Math.random() * candidates.length)];
}
/* Preferential: probability proportional to degree+1 (ensures non-zero chance for degree-0 nodes) */
const td = candidates.reduce((s, n) => s + n.degree + 1, 0);
let r = Math.random() * td;
for (const n of candidates) {
r -= (n.degree + 1);
if (r <= 0) return n;
}
return candidates[candidates.length - 1];
}
function addNode() {
const m = parseInt(document.getElementById('mSelect').value);
const mode = document.getElementById('modeSelect').value;
const id = nodes.length;
/* Place new node at a random position */
const angle = Math.random() * 2 * Math.PI;
const radius = 20 + Math.random() * (Math.min(netW, netH) / 2 - 30);
const nx = netW / 2 + radius * Math.cos(angle);
const ny = netH / 2 + radius * Math.sin(angle);
nodes.push({ id, x: nx, y: ny, degree: 0 });
const chosen = new Set([id]);
const targets = Math.min(m, nodes.length - 1);
for (let t = 0; t < targets; t++) {
const target = pickTarget(chosen, mode);
if (!target) break;
chosen.add(target.id);
edges.push([id, target.id]);
nodes[id].degree++;
target.degree++;
}
/* Slight repulsion to avoid total overlap */
for (const n of nodes) {
if (n.id === id) continue;
const dx = n.x - nx, dy = n.y - ny;
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
if (dist < 30) {
n.x += (dx / dist) * (30 - dist) * 0.5;
n.y += (dy / dist) * (30 - dist) * 0.5;
n.x = Math.max(10, Math.min(netW - 10, n.x));
n.y = Math.max(10, Math.min(netH - 10, n.y));
}
}
}
function drawNet() {
netCtx.clearRect(0, 0, netW, netH);
/* Draw all edges first */
netCtx.strokeStyle = '#b0bec5';
netCtx.lineWidth = 0.8;
for (const [a, b] of edges) {
netCtx.beginPath();
netCtx.moveTo(nodes[a].x, nodes[a].y);
netCtx.lineTo(nodes[b].x, nodes[b].y);
netCtx.stroke();
}
/* Draw nodes; size and color encode degree */
const maxDeg = Math.max(...nodes.map(n => n.degree), 1);
for (const n of nodes) {
const r = 2.5 + 6 * (n.degree / maxDeg);
const t = n.degree / maxDeg;
const red = Math.round(29 + (220 - 29) * t);
const green = Math.round(78 + (57 - 78) * t);
const blue = Math.round(216 + (70 - 216) * t);
netCtx.beginPath();
netCtx.arc(n.x, n.y, r, 0, 2 * Math.PI);
netCtx.fillStyle = `rgb(${red},${green},${blue})`;
netCtx.fill();
}
}
function drawBar() {
barCtx.clearRect(0, 0, barW, barH);
if (nodes.length < 2) return;
/* Build frequency table of degrees */
const freq = {};
for (const n of nodes) freq[n.degree] = (freq[n.degree] || 0) + 1;
const maxK = Math.max(...Object.keys(freq).map(Number));
const maxF = Math.max(...Object.values(freq));
const pad = { l: 28, r: 6, t: 6, b: 22 };
const bw = (barW - pad.l - pad.r) / (maxK + 1);
const bh = barH - pad.t - pad.b;
/* Draw axes */
barCtx.strokeStyle = '#999';
barCtx.lineWidth = 1;
barCtx.beginPath();
barCtx.moveTo(pad.l, pad.t);
barCtx.lineTo(pad.l, barH - pad.b);
barCtx.lineTo(barW - pad.r, barH - pad.b);
barCtx.stroke();
/* Draw bars and axis labels */
for (let k = 0; k <= maxK; k++) {
const f = freq[k] || 0;
const hh = (f / maxF) * bh;
barCtx.fillStyle = '#1d4ed8';
barCtx.fillRect(pad.l + k * bw, barH - pad.b - hh, bw * 0.75, hh);
}
barCtx.fillStyle = '#666';
barCtx.font = '9px system-ui';
barCtx.textAlign = 'center';
for (let k = 0; k <= maxK; k += Math.max(1, Math.floor(maxK / 5))) {
barCtx.fillText(k, pad.l + k * bw + bw * 0.375, barH - pad.b + 10);
}
barCtx.save();
barCtx.translate(10, barH / 2);
barCtx.rotate(-Math.PI / 2);
barCtx.textAlign = 'center';
barCtx.fillText('count', 0, 0);
barCtx.restore();
barCtx.fillText('degree k', pad.l + (maxK / 2) * bw, barH - 2);
}
function updateStats() {
const maxDeg = Math.max(...nodes.map(n => n.degree), 0);
const avgDeg = nodes.length ? (2 * edges.length / nodes.length).toFixed(2) : 0;
document.getElementById('stats').innerHTML =
i18nFmt('Nodes: %n%', { n: nodes.length }) + '<br>' +
i18nFmt('Edges: %e%', { e: edges.length }) + '<br>' +
i18nFmt('Max degree: %k%', { k: maxDeg }) + '<br>' +
i18nFmt('Avg degree: %a%', { a: avgDeg });
}
function updateStatus() {
const mode = document.getElementById('modeSelect').value;
const label = mode === 'preferential' ? 'Preferential attachment — rich nodes attract more links.' : 'Random attachment — no hub bias; degrees spread evenly.';
document.getElementById('statusBar').textContent = label;
}
function render() { drawNet(); drawBar(); updateStats(); updateStatus(); }
function step() { addNode(); render(); }
function startRun() {
if (running) return;
running = true;
document.getElementById('runBtn').disabled = true;
document.getElementById('stepBtn').disabled = true;
function loop() {
if (nodes.length >= 80) { stopRun(); return; }
addNode();
render();
raf = requestAnimationFrame(loop);
}
raf = requestAnimationFrame(loop);
}
function stopRun() {
running = false;
cancelAnimationFrame(raf);
document.getElementById('runBtn').disabled = false;
document.getElementById('stepBtn').disabled = false;
}
function reset() {
stopRun();
initSeed();
render();
}
document.getElementById('stepBtn').addEventListener('click', step);
document.getElementById('runBtn').addEventListener('click', startRun);
document.getElementById('resetBtn').addEventListener('click', reset);
document.getElementById('modeSelect').addEventListener('change', reset);
document.getElementById('mSelect').addEventListener('change', reset);
reset();
<!-- Main layout: left = network canvas, right = degree-distribution bar chart -->
<div id="controls">
<label>
<span>Mode:</span>
<select id="modeSelect">
<option value="preferential">Preferential attachment</option>
<option value="random">Random attachment</option>
</select>
</label>
<label>
<span>Links per node (m):</span>
<select id="mSelect">
<option value="1">1</option>
<option value="2" selected>2</option>
<option value="3">3</option>
</select>
</label>
<button id="stepBtn" type="button">+ Add node</button>
<button id="runBtn" type="button">Auto-grow</button>
<button id="resetBtn" type="button" class="ghost">Reset</button>
</div>
<div id="main">
<canvas id="netCanvas" width="280" height="280"></canvas>
<div id="chartArea">
<div id="chartTitle">Degree distribution</div>
<canvas id="barCanvas" width="200" height="200"></canvas>
<div id="stats"></div>
</div>
</div>
<div id="statusBar"></div>
/* Layout and visual styles for the network simulator */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; font-size: 13px; color: #222; background: #fff; }
#controls {
display: flex; flex-wrap: wrap; gap: 6px; align-items: center;
padding: 6px 8px; background: #f0f4f8; border-bottom: 1px solid #d0d8e4;
}
label { display: flex; align-items: center; gap: 4px; font-size: 12px; }
select { font-size: 12px; padding: 2px 4px; border: 1px solid #aab; border-radius: 4px; }
button {
font: 600 12px system-ui; padding: 4px 10px;
background: #1d4ed8; color: #fff; border: none; border-radius: 6px; cursor: pointer;
}
button.ghost { background: #fff; color: #1d4ed8; border: 1px solid #1d4ed8; }
button:disabled { opacity: .45; cursor: default; }
#main { display: flex; gap: 8px; padding: 8px; align-items: flex-start; }
canvas { display: block; border-radius: 6px; background: #f7f9fb; border: 1px solid #dde3ea; }
#chartArea { display: flex; flex-direction: column; gap: 4px; }
#chartTitle { font-size: 11px; font-weight: 600; color: #555; text-align: center; }
#stats { font-size: 11px; color: #555; text-align: center; line-height: 1.5; }
#statusBar { padding: 4px 8px; font-size: 12px; font-weight: 600; color: #1d4ed8; min-height: 20px; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Network state: nodes array, edges array, animation handle */
const M0 = 3; /* seed clique size */
let nodes = [], edges = [], running = false, raf = null;
const netCtx = document.getElementById('netCanvas').getContext('2d');
const barCtx = document.getElementById('barCanvas').getContext('2d');
const netW = 280, netH = 280, barW = 200, barH = 200;
function initSeed() {
/* Build seed clique of M0 fully-connected nodes */
nodes = [];
edges = [];
for (let i = 0; i < M0; i++) {
const angle = (2 * Math.PI * i) / M0;
nodes.push({ id: i, x: netW / 2 + 60 * Math.cos(angle), y: netH / 2 + 60 * Math.sin(angle), degree: 0 });
}
/* Connect all seed nodes to each other */
for (let i = 0; i < M0; i++) {
for (let j = i + 1; j < M0; j++) {
edges.push([i, j]);
nodes[i].degree++;
nodes[j].degree++;
}
}
}
function totalDegree() {
return nodes.reduce((s, n) => s + n.degree, 0);
}
function pickTarget(exclude, mode) {
/* Choose one target not already connected to the new node */
const candidates = nodes.filter(n => !exclude.has(n.id));
if (!candidates.length) return null;
if (mode === 'random') {
return candidates[Math.floor(Math.random() * candidates.length)];
}
/* Preferential: probability proportional to degree+1 (ensures non-zero chance for degree-0 nodes) */
const td = candidates.reduce((s, n) => s + n.degree + 1, 0);
let r = Math.random() * td;
for (const n of candidates) {
r -= (n.degree + 1);
if (r <= 0) return n;
}
return candidates[candidates.length - 1];
}
function addNode() {
const m = parseInt(document.getElementById('mSelect').value);
const mode = document.getElementById('modeSelect').value;
const id = nodes.length;
/* Place new node at a random position */
const angle = Math.random() * 2 * Math.PI;
const radius = 20 + Math.random() * (Math.min(netW, netH) / 2 - 30);
const nx = netW / 2 + radius * Math.cos(angle);
const ny = netH / 2 + radius * Math.sin(angle);
nodes.push({ id, x: nx, y: ny, degree: 0 });
const chosen = new Set([id]);
const targets = Math.min(m, nodes.length - 1);
for (let t = 0; t < targets; t++) {
const target = pickTarget(chosen, mode);
if (!target) break;
chosen.add(target.id);
edges.push([id, target.id]);
nodes[id].degree++;
target.degree++;
}
/* Slight repulsion to avoid total overlap */
for (const n of nodes) {
if (n.id === id) continue;
const dx = n.x - nx, dy = n.y - ny;
const dist = Math.sqrt(dx * dx + dy * dy) || 1;
if (dist < 30) {
n.x += (dx / dist) * (30 - dist) * 0.5;
n.y += (dy / dist) * (30 - dist) * 0.5;
n.x = Math.max(10, Math.min(netW - 10, n.x));
n.y = Math.max(10, Math.min(netH - 10, n.y));
}
}
}
function drawNet() {
netCtx.clearRect(0, 0, netW, netH);
/* Draw all edges first */
netCtx.strokeStyle = '#b0bec5';
netCtx.lineWidth = 0.8;
for (const [a, b] of edges) {
netCtx.beginPath();
netCtx.moveTo(nodes[a].x, nodes[a].y);
netCtx.lineTo(nodes[b].x, nodes[b].y);
netCtx.stroke();
}
/* Draw nodes; size and color encode degree */
const maxDeg = Math.max(...nodes.map(n => n.degree), 1);
for (const n of nodes) {
const r = 2.5 + 6 * (n.degree / maxDeg);
const t = n.degree / maxDeg;
const red = Math.round(29 + (220 - 29) * t);
const green = Math.round(78 + (57 - 78) * t);
const blue = Math.round(216 + (70 - 216) * t);
netCtx.beginPath();
netCtx.arc(n.x, n.y, r, 0, 2 * Math.PI);
netCtx.fillStyle = `rgb(${red},${green},${blue})`;
netCtx.fill();
}
}
function drawBar() {
barCtx.clearRect(0, 0, barW, barH);
if (nodes.length < 2) return;
/* Build frequency table of degrees */
const freq = {};
for (const n of nodes) freq[n.degree] = (freq[n.degree] || 0) + 1;
const maxK = Math.max(...Object.keys(freq).map(Number));
const maxF = Math.max(...Object.values(freq));
const pad = { l: 28, r: 6, t: 6, b: 22 };
const bw = (barW - pad.l - pad.r) / (maxK + 1);
const bh = barH - pad.t - pad.b;
/* Draw axes */
barCtx.strokeStyle = '#999';
barCtx.lineWidth = 1;
barCtx.beginPath();
barCtx.moveTo(pad.l, pad.t);
barCtx.lineTo(pad.l, barH - pad.b);
barCtx.lineTo(barW - pad.r, barH - pad.b);
barCtx.stroke();
/* Draw bars and axis labels */
for (let k = 0; k <= maxK; k++) {
const f = freq[k] || 0;
const hh = (f / maxF) * bh;
barCtx.fillStyle = '#1d4ed8';
barCtx.fillRect(pad.l + k * bw, barH - pad.b - hh, bw * 0.75, hh);
}
barCtx.fillStyle = '#666';
barCtx.font = '9px system-ui';
barCtx.textAlign = 'center';
for (let k = 0; k <= maxK; k += Math.max(1, Math.floor(maxK / 5))) {
barCtx.fillText(k, pad.l + k * bw + bw * 0.375, barH - pad.b + 10);
}
barCtx.save();
barCtx.translate(10, barH / 2);
barCtx.rotate(-Math.PI / 2);
barCtx.textAlign = 'center';
barCtx.fillText('count', 0, 0);
barCtx.restore();
barCtx.fillText('degree k', pad.l + (maxK / 2) * bw, barH - 2);
}
function updateStats() {
const maxDeg = Math.max(...nodes.map(n => n.degree), 0);
const avgDeg = nodes.length ? (2 * edges.length / nodes.length).toFixed(2) : 0;
document.getElementById('stats').innerHTML =
i18nFmt('Nodes: %n%', { n: nodes.length }) + '<br>' +
i18nFmt('Edges: %e%', { e: edges.length }) + '<br>' +
i18nFmt('Max degree: %k%', { k: maxDeg }) + '<br>' +
i18nFmt('Avg degree: %a%', { a: avgDeg });
}
function updateStatus() {
const mode = document.getElementById('modeSelect').value;
const label = mode === 'preferential' ? 'Preferential attachment — rich nodes attract more links.' : 'Random attachment — no hub bias; degrees spread evenly.';
document.getElementById('statusBar').textContent = label;
}
function render() { drawNet(); drawBar(); updateStats(); updateStatus(); }
function step() { addNode(); render(); }
function startRun() {
if (running) return;
running = true;
document.getElementById('runBtn').disabled = true;
document.getElementById('stepBtn').disabled = true;
function loop() {
if (nodes.length >= 80) { stopRun(); return; }
addNode();
render();
raf = requestAnimationFrame(loop);
}
raf = requestAnimationFrame(loop);
}
function stopRun() {
running = false;
cancelAnimationFrame(raf);
document.getElementById('runBtn').disabled = false;
document.getElementById('stepBtn').disabled = false;
}
function reset() {
stopRun();
initSeed();
render();
}
document.getElementById('stepBtn').addEventListener('click', step);
document.getElementById('runBtn').addEventListener('click', startRun);
document.getElementById('resetBtn').addEventListener('click', reset);
document.getElementById('modeSelect').addEventListener('change', reset);
document.getElementById('mSelect').addEventListener('change', reset);
reset();
Watch what happens as the network grows: a few nodes accumulate far more links than average, while most remain modestly connected. The bar chart on the right tracks the degree distribution — notice how it develops a long tail, the hallmark of a power law. Compare this with what you see when you switch to random attachment: without the rich-get-richer bias, degrees cluster around the average and no hubs emerge.
Comments
Loading comments...