The network below has 40 nodes wired using a preferential-attachment rule — the same mechanism that generates scale-free structure on the web and in social graphs. A few nodes already have many connections; most have only one or two.
<!-- Network robustness demo — compare random removal vs. targeted hub attack -->
<div class="controls">
<button id="btn-random" type="button">Remove random node</button>
<button id="btn-attack" type="button">Attack top hub</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="stats" id="stats"></div>
<canvas id="canvas" width="480" height="310"></canvas>
<div class="legend">
<span class="dot giant"></span> Giant component
<span class="dot small"></span> Isolated nodes
<span class="dot removed"></span> Removed
</div>
/* Demo styles — canvas layout + button row */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; background: #f7f9fb; color: #222; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
.stats { font-size: .88rem; font-weight: 600; min-height: 1.3em; margin-bottom: .35rem; color: #1d3557; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px; background: #fff;
max-width: 100%; }
.legend { font-size: .8rem; color: #555; margin-top: .4rem; display: flex; gap: 1rem; align-items: center; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 3px; }
.dot.giant { background: #2196f3; }
.dot.small { background: #adb1b8; }
.dot.removed { background: #e0e0e0; border: 1px solid #bbb; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Network robustness demo: preferential-attachment graph, then two removal strategies.
// Giant component = largest connected subgraph among remaining nodes.
const N_NODES = 40;
// Each new node attaches to SEED_EDGES existing nodes (preferential attachment).
const SEED_EDGES = 3;
// Build a Barabasi-Albert scale-free graph with n nodes.
function buildScaleFree(n, m0) {
const deg = new Array(n).fill(0);
const edges = [];
// Fully connect the initial seed nodes.
for (let i = 0; i < m0; i++) {
for (let j = i + 1; j < m0; j++) {
edges.push([i, j]);
deg[i]++;
deg[j]++;
}
}
for (let i = m0; i < n; i++) {
// Pick SEED_EDGES targets by preferential attachment (prob proportional to degree).
const totalDeg = deg.slice(0, i).reduce((a, b) => a + b, 0) || 1;
const chosen = new Set();
let attempts = 0;
while (chosen.size < m0 && attempts < 1000) {
attempts++;
let r = Math.random() * totalDeg, cum = 0;
for (let j = 0; j < i; j++) {
cum += deg[j];
if (r < cum) { chosen.add(j); break; }
}
}
for (const j of chosen) {
edges.push([i, j]);
deg[i]++;
deg[j]++;
}
}
return edges;
}
// Place nodes on an ellipse for a readable layout.
const W = 480, H = 310, PAD = 22;
function layoutNodes(n) {
const pos = [];
for (let i = 0; i < n; i++) {
const angle = (2 * Math.PI * i) / n - Math.PI / 2;
const rx = (W / 2 - PAD) * 0.88;
const ry = (H / 2 - PAD) * 0.88;
pos.push({ x: W / 2 + rx * Math.cos(angle), y: H / 2 + ry * Math.sin(angle) });
}
return pos;
}
// Global mutable state — nodes removed so far + counters per strategy.
let origEdges = buildScaleFree(N_NODES, SEED_EDGES);
let pos = layoutNodes(N_NODES);
let removed = new Set();
let randomCount = 0, attackCount = 0;
function adjList(activeNodes) {
const adj = {};
for (const n of activeNodes) adj[n] = [];
for (const [a, b] of origEdges) {
if (!removed.has(a) && !removed.has(b)) {
adj[a].push(b);
adj[b].push(a);
}
}
return adj;
}
// BFS to find all connected components; return the largest one.
function giantComponent(activeNodes) {
const adj = adjList(activeNodes);
const visited = new Set();
let best = new Set();
for (const start of activeNodes) {
if (visited.has(start)) continue;
const comp = new Set();
const queue = [start];
while (queue.length) {
const cur = queue.shift();
if (comp.has(cur)) continue;
comp.add(cur); visited.add(cur);
for (const nb of (adj[cur] || [])) if (!comp.has(nb)) queue.push(nb);
}
if (comp.size > best.size) best = comp;
}
return best;
}
function degree(node, activeNodes) {
return origEdges.filter(([a, b]) => (a === node || b === node) && !removed.has(a) && !removed.has(b)).length;
}
function draw() {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, W, H);
const active = [];
for (let i = 0; i < N_NODES; i++) if (!removed.has(i)) active.push(i);
const gcc = giantComponent(active);
// Draw edges between active (non-removed) nodes.
ctx.strokeStyle = '#d0d8e0';
ctx.lineWidth = 1;
for (const [a, b] of origEdges) {
if (removed.has(a) || removed.has(b)) continue;
ctx.beginPath();
ctx.moveTo(pos[a].x, pos[a].y);
ctx.lineTo(pos[b].x, pos[b].y);
ctx.stroke();
}
// Draw each node; size = degree, color = giant component membership.
for (let i = 0; i < N_NODES; i++) {
const p = pos[i];
ctx.beginPath();
if (removed.has(i)) {
ctx.arc(p.x, p.y, 5, 0, 2 * Math.PI);
ctx.fillStyle = '#e8e8e8';
ctx.strokeStyle = '#bbb';
ctx.lineWidth = 1;
ctx.fill();
ctx.stroke();
} else {
const deg = degree(i, active);
const r = Math.max(5, Math.min(14, 5 + deg * 1.1));
ctx.arc(p.x, p.y, r, 0, 2 * Math.PI);
ctx.fillStyle = gcc.has(i) ? '#2196f3' : '#adb1b8';
ctx.strokeStyle = gcc.has(i) ? '#1565c0' : '#888';
ctx.lineWidth = 1.5;
ctx.fill();
ctx.stroke();
}
}
const total = N_NODES - removed.size;
const gccSize = gcc.size;
const statsEl = document.getElementById('stats');
statsEl.textContent = i18nFmt('Giant component: %gcc% / %total% nodes remaining (random: %rand%, attack: %attack%)', { gcc: gccSize, total: total, rand: randomCount, attack: attackCount });
const btnRandom = document.getElementById('btn-random');
const btnAttack = document.getElementById('btn-attack');
if (active.length === 0) { btnRandom.disabled = true; btnAttack.disabled = true; }
}
function removeRandom() {
const active = [];
for (let i = 0; i < N_NODES; i++) if (!removed.has(i)) active.push(i);
if (!active.length) return;
const pick = active[Math.floor(Math.random() * active.length)];
removed.add(pick);
randomCount++;
draw();
}
function attackHub() {
let best = -1, bestDeg = -1;
for (let i = 0; i < N_NODES; i++) {
if (removed.has(i)) continue;
const d = degree(i, null);
if (d > bestDeg) { bestDeg = d; best = i; }
}
if (best === -1) return;
removed.add(best);
attackCount++;
draw();
}
function reset() {
removed.clear();
randomCount = 0;
attackCount = 0;
origEdges = buildScaleFree(N_NODES, SEED_EDGES);
pos = layoutNodes(N_NODES);
document.getElementById('btn-random').disabled = false;
document.getElementById('btn-attack').disabled = false;
draw();
}
document.getElementById('btn-random').addEventListener('click', removeRandom);
document.getElementById('btn-attack').addEventListener('click', attackHub);
document.getElementById('btn-reset').addEventListener('click', reset);
draw();
<!-- Network robustness demo — compare random removal vs. targeted hub attack -->
<div class="controls">
<button id="btn-random" type="button">Remove random node</button>
<button id="btn-attack" type="button">Attack top hub</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
<div class="stats" id="stats"></div>
<canvas id="canvas" width="480" height="310"></canvas>
<div class="legend">
<span class="dot giant"></span> Giant component
<span class="dot small"></span> Isolated nodes
<span class="dot removed"></span> Removed
</div>
/* Demo styles — canvas layout + button row */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; background: #f7f9fb; color: #222; }
.controls { display: flex; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: default; }
.stats { font-size: .88rem; font-weight: 600; min-height: 1.3em; margin-bottom: .35rem; color: #1d3557; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px; background: #fff;
max-width: 100%; }
.legend { font-size: .8rem; color: #555; margin-top: .4rem; display: flex; gap: 1rem; align-items: center; }
.dot { display: inline-block; width: 10px; height: 10px; border-radius: 50%; margin-right: 3px; }
.dot.giant { background: #2196f3; }
.dot.small { background: #adb1b8; }
.dot.removed { background: #e0e0e0; border: 1px solid #bbb; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Network robustness demo: preferential-attachment graph, then two removal strategies.
// Giant component = largest connected subgraph among remaining nodes.
const N_NODES = 40;
// Each new node attaches to SEED_EDGES existing nodes (preferential attachment).
const SEED_EDGES = 3;
// Build a Barabasi-Albert scale-free graph with n nodes.
function buildScaleFree(n, m0) {
const deg = new Array(n).fill(0);
const edges = [];
// Fully connect the initial seed nodes.
for (let i = 0; i < m0; i++) {
for (let j = i + 1; j < m0; j++) {
edges.push([i, j]);
deg[i]++;
deg[j]++;
}
}
for (let i = m0; i < n; i++) {
// Pick SEED_EDGES targets by preferential attachment (prob proportional to degree).
const totalDeg = deg.slice(0, i).reduce((a, b) => a + b, 0) || 1;
const chosen = new Set();
let attempts = 0;
while (chosen.size < m0 && attempts < 1000) {
attempts++;
let r = Math.random() * totalDeg, cum = 0;
for (let j = 0; j < i; j++) {
cum += deg[j];
if (r < cum) { chosen.add(j); break; }
}
}
for (const j of chosen) {
edges.push([i, j]);
deg[i]++;
deg[j]++;
}
}
return edges;
}
// Place nodes on an ellipse for a readable layout.
const W = 480, H = 310, PAD = 22;
function layoutNodes(n) {
const pos = [];
for (let i = 0; i < n; i++) {
const angle = (2 * Math.PI * i) / n - Math.PI / 2;
const rx = (W / 2 - PAD) * 0.88;
const ry = (H / 2 - PAD) * 0.88;
pos.push({ x: W / 2 + rx * Math.cos(angle), y: H / 2 + ry * Math.sin(angle) });
}
return pos;
}
// Global mutable state — nodes removed so far + counters per strategy.
let origEdges = buildScaleFree(N_NODES, SEED_EDGES);
let pos = layoutNodes(N_NODES);
let removed = new Set();
let randomCount = 0, attackCount = 0;
function adjList(activeNodes) {
const adj = {};
for (const n of activeNodes) adj[n] = [];
for (const [a, b] of origEdges) {
if (!removed.has(a) && !removed.has(b)) {
adj[a].push(b);
adj[b].push(a);
}
}
return adj;
}
// BFS to find all connected components; return the largest one.
function giantComponent(activeNodes) {
const adj = adjList(activeNodes);
const visited = new Set();
let best = new Set();
for (const start of activeNodes) {
if (visited.has(start)) continue;
const comp = new Set();
const queue = [start];
while (queue.length) {
const cur = queue.shift();
if (comp.has(cur)) continue;
comp.add(cur); visited.add(cur);
for (const nb of (adj[cur] || [])) if (!comp.has(nb)) queue.push(nb);
}
if (comp.size > best.size) best = comp;
}
return best;
}
function degree(node, activeNodes) {
return origEdges.filter(([a, b]) => (a === node || b === node) && !removed.has(a) && !removed.has(b)).length;
}
function draw() {
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, W, H);
const active = [];
for (let i = 0; i < N_NODES; i++) if (!removed.has(i)) active.push(i);
const gcc = giantComponent(active);
// Draw edges between active (non-removed) nodes.
ctx.strokeStyle = '#d0d8e0';
ctx.lineWidth = 1;
for (const [a, b] of origEdges) {
if (removed.has(a) || removed.has(b)) continue;
ctx.beginPath();
ctx.moveTo(pos[a].x, pos[a].y);
ctx.lineTo(pos[b].x, pos[b].y);
ctx.stroke();
}
// Draw each node; size = degree, color = giant component membership.
for (let i = 0; i < N_NODES; i++) {
const p = pos[i];
ctx.beginPath();
if (removed.has(i)) {
ctx.arc(p.x, p.y, 5, 0, 2 * Math.PI);
ctx.fillStyle = '#e8e8e8';
ctx.strokeStyle = '#bbb';
ctx.lineWidth = 1;
ctx.fill();
ctx.stroke();
} else {
const deg = degree(i, active);
const r = Math.max(5, Math.min(14, 5 + deg * 1.1));
ctx.arc(p.x, p.y, r, 0, 2 * Math.PI);
ctx.fillStyle = gcc.has(i) ? '#2196f3' : '#adb1b8';
ctx.strokeStyle = gcc.has(i) ? '#1565c0' : '#888';
ctx.lineWidth = 1.5;
ctx.fill();
ctx.stroke();
}
}
const total = N_NODES - removed.size;
const gccSize = gcc.size;
const statsEl = document.getElementById('stats');
statsEl.textContent = i18nFmt('Giant component: %gcc% / %total% nodes remaining (random: %rand%, attack: %attack%)', { gcc: gccSize, total: total, rand: randomCount, attack: attackCount });
const btnRandom = document.getElementById('btn-random');
const btnAttack = document.getElementById('btn-attack');
if (active.length === 0) { btnRandom.disabled = true; btnAttack.disabled = true; }
}
function removeRandom() {
const active = [];
for (let i = 0; i < N_NODES; i++) if (!removed.has(i)) active.push(i);
if (!active.length) return;
const pick = active[Math.floor(Math.random() * active.length)];
removed.add(pick);
randomCount++;
draw();
}
function attackHub() {
let best = -1, bestDeg = -1;
for (let i = 0; i < N_NODES; i++) {
if (removed.has(i)) continue;
const d = degree(i, null);
if (d > bestDeg) { bestDeg = d; best = i; }
}
if (best === -1) return;
removed.add(best);
attackCount++;
draw();
}
function reset() {
removed.clear();
randomCount = 0;
attackCount = 0;
origEdges = buildScaleFree(N_NODES, SEED_EDGES);
pos = layoutNodes(N_NODES);
document.getElementById('btn-random').disabled = false;
document.getElementById('btn-attack').disabled = false;
draw();
}
document.getElementById('btn-random').addEventListener('click', removeRandom);
document.getElementById('btn-attack').addEventListener('click', attackHub);
document.getElementById('btn-reset').addEventListener('click', reset);
draw();
Click Remove random node to knock out a node chosen uniformly at random, or Attack top hub to remove whichever node currently has the most connections. The giant component — the largest group of nodes still reachable from each other — is highlighted. Watch how many removals it takes under each strategy before the network fragments. The contrast is the entire point.
Comments
Loading comments...