Here is a tiny web of five pages and the links between them. Each page starts with the same importance. Press Step to push importance along the links once: every page hands its current score to the pages it links to, split evenly. Keep stepping and watch the numbers stop moving.
<p class="hint">Five pages link to each other. Every page starts with the <b>same</b> importance. Each <b>Step</b> pushes importance along the links — a page splits its score evenly among the pages it points to. Watch the bars stop moving: that resting state is the dominant eigenvector.</p>
<div id="pages" class="pages"></div>
<div class="status" id="status">Step 0 — everyone starts equal.</div>
<div class="btns">
<button id="step" type="button">Step</button>
<button id="run" type="button">Run to convergence</button>
<button id="reset" type="button" class="ghost">Reset</button>
</div>
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .9rem; color: #444; margin: 0 0 .8rem; line-height: 1.45; }
.pages { display: flex; flex-direction: column; gap: .5rem; margin: .4rem 0 .2rem; }
.row { display: flex; align-items: center; gap: .6rem; }
.label { width: 92px; font: 600 13px ui-monospace, monospace; color: #1d3557; }
.label small { display: block; font: 400 11px system-ui; color: #6b7785; }
.track { flex: 1; height: 22px; background: #eef2f6; border-radius: 6px; overflow: hidden; }
.bar { height: 100%; width: 0; background: #2a6f97; border-radius: 6px;
transition: width .35s ease; }
.bar.lead { background: #1d3557; }
.pct { width: 54px; text-align: right; font: 700 13px ui-monospace, monospace; color: #1d3557; }
.status { font-size: .95rem; font-weight: 600; margin: .7rem 0 .5rem; min-height: 1.3em; color: #2a6f97; }
.status.done { color: #0a7d33; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .45rem .9rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// PageRank by power iteration. Pages A..E with a fixed link structure.
// rank' = (1-d)/N + d * M * rank, where M[i][j] = 1/outdeg(j) if j links to i.
// This is the random-surfer / stationary-distribution view: the rank vector
// converges to the dominant eigenvector of the (damped) link matrix. In P.
const NAMES = ['A','B','C','D','E'];
const N = NAMES.length;
const d = 0.85; // damping (teleport prob = 0.15)
// out-links: index -> list of indices it points to
const LINKS = [
[1,2], // A -> B, C
[2], // B -> C
[0,3], // C -> A, D
[4], // D -> E
[0,3], // E -> A, D
];
let rank = new Array(N).fill(1 / N);
let step = 0;
// One power-iteration step (handles dangling nodes by spreading their mass).
function iterate(r) {
const next = new Array(N).fill((1 - d) / N);
let dangling = 0;
for (let j = 0; j < N; j++) if (LINKS[j].length === 0) dangling += r[j];
for (let j = 0; j < N; j++) {
const outs = LINKS[j];
if (outs.length === 0) continue;
const share = d * r[j] / outs.length;
for (const i of outs) next[i] += share;
}
// distribute dangling mass uniformly (keeps it a probability vector)
if (dangling > 0) for (let i = 0; i < N; i++) next[i] += d * dangling / N;
return next;
}
function maxDelta(a, b) { let m = 0; for (let i = 0; i < N; i++) m = Math.max(m, Math.abs(a[i]-b[i])); return m; }
function render() {
const max = Math.max(...rank);
const order = [...rank.keys()].sort((a,b)=>rank[b]-rank[a]);
const lead = order[0];
const box = document.getElementById('pages');
box.innerHTML = '';
for (let i = 0; i < N; i++) {
const row = document.createElement('div'); row.className = 'row';
const outs = LINKS[i].map(k => NAMES[k]).join(', ') || '(none)';
row.innerHTML =
fmt('<div class="label">Page %v%<small>→ %outs%</small></div><div class="track"><div class="bar%v2%" style="width:%v3%%"></div></div><div class="pct">%v4%%</div>', { v: NAMES[i], outs: outs, v2: i===lead?' lead':'', v3: max>0 ? (rank[i]/max*100).toFixed(1) : 0, v4: (rank[i]*100).toFixed(1) });
box.appendChild(row);
}
}
function setStatus(t, cls) { const s = document.getElementById('status'); s.textContent = t; s.className = 'status ' + (cls||''); }
function doStep() {
const next = iterate(rank);
const delta = maxDelta(rank, next);
rank = next; step++;
render();
if (delta < 1e-4) {
const order = [...rank.keys()].sort((a,b)=>rank[b]-rank[a]).map(i=>NAMES[i]);
setStatus(fmt('Converged after %step% steps. Ranking: %order%', { step: step, order: order.join(' > ') }), 'done');
document.getElementById('step').disabled = true;
document.getElementById('run').disabled = true;
return false;
}
setStatus(fmt('Step %step% — importance flowing along the links (Δ=%delta%).', { step: step, delta: delta.toFixed(4) }));
return true;
}
function run() {
let guard = 0;
const tick = () => { if (doStep() && guard++ < 60) setTimeout(tick, 400); };
tick();
}
function reset() {
rank = new Array(N).fill(1 / N); step = 0;
document.getElementById('step').disabled = false;
document.getElementById('run').disabled = false;
render(); setStatus('Step 0 — everyone starts equal.');
}
document.getElementById('step').addEventListener('click', doStep);
document.getElementById('run').addEventListener('click', run);
document.getElementById('reset').addEventListener('click', reset);
render();
This is power iteration. Notice two things. First, the ranking converges — after a handful of steps the scores barely change, no matter where you started. Second, that stable distribution is exactly the dominant eigenvector of the link matrix: the one vector the system keeps returning to. Checking a proposed ranking is one matrix multiply; finding it is just repeating that multiply until it stops moving.
Comments
Loading comments...