Below is a small network with two loosely connected groups. Press Find communities and the demo builds the Laplacian, computes its second-smallest eigenvalue's eigenvector — the famous Fiedler vector — and colors each vertex by the sign of its entry. Watch the two communities appear.
<p class="hint">This network has two loosely connected groups. The computer builds the
<b>Laplacian</b> matrix, finds the <b>Fiedler vector</b> (the eigenvector of the second-smallest
eigenvalue), and colors each vertex by the <b>sign</b> of its entry. Nobody told it where the
groups are.</p>
<svg id="net" viewBox="0 0 360 240" aria-label="network"></svg>
<div class="status" id="status">Press "Find communities".</div>
<div class="readout" id="readout"></div>
<div class="btns">
<button id="find" type="button">Find communities</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 .6rem; line-height: 1.45; }
#net { width: 100%; max-width: 360px; height: auto; display: block;
background: #f4f7fa; border: 1px solid #cdd9e3; border-radius: 10px; }
.edge { stroke: #adb1b8; stroke-width: 1.4; }
.node { stroke: #1d3557; stroke-width: 1.5; fill: #c9ccd1; transition: fill .35s; }
.node.a { fill: #2a9d8f; }
.node.b { fill: #e76f51; }
.label { font: 600 11px ui-monospace, monospace; fill: #1d3557; pointer-events: none; }
.status { font-size: 1rem; font-weight: 600; margin: .55rem 0 .2rem; min-height: 1.3em; }
.status.ok { color: #0a7d33; }
.readout { font: 500 .82rem ui-monospace, monospace; color: #345; white-space: pre-wrap;
margin: 0 0 .5rem; min-height: 1.2em; }
.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; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Spectral graph theory: read a network through the eigenvalues of its Laplacian L = D - A.
// The Fiedler vector (eigenvector of the 2nd-smallest eigenvalue) splits the graph by sign.
// Eigenvector via inverse-free shifted power iteration on M = c*I - L (deflated against the
// all-ones vector, which is L’s eigenvector for eigenvalue 0). The top eigenvector of M that
// is orthogonal to ones is exactly the Fiedler vector. This is polynomial-time; the balanced
// cut it approximates is NP-hard (Fiedler 1973; Cheeger’s inequality).
const N = 8;
const POS = [[60,55],[45,120],[80,185],[120,110], // group 0 (left)
[240,55],[295,120],[260,185],[210,120]]; // group 1 (right)
const EDGES = [
[0,1],[0,3],[1,2],[1,3],[2,3], // dense inside left
[4,5],[4,7],[5,6],[5,7],[6,7], // dense inside right
[3,7] // one weak bridge between groups
];
const A = Array.from({length:N},()=>Array(N).fill(0));
EDGES.forEach(([i,j])=>{ A[i][j]=1; A[j][i]=1; });
const deg = A.map(r=>r.reduce((s,x)=>s+x,0));
// L = D - A ; M = c*I - L so M’s top eigenpair = L’s smallest eigenpair.
const c = 2*Math.max(...deg) + 1;
const M = Array.from({length:N},(_,i)=>Array.from({length:N},(_,j)=>
(i===j ? c-deg[i] : A[i][j])));
const ones = Array(N).fill(1/Math.sqrt(N));
function dot(u,v){ let s=0; for(let i=0;i<N;i++)s+=u[i]*v[i]; return s; }
function deflate(v){ const p=dot(v,ones); for(let i=0;i<N;i++)v[i]-=p*ones[i]; } // remove ones component
function norm(v){ const n=Math.sqrt(dot(v,v))||1; for(let i=0;i<N;i++)v[i]/=n; return n; }
function matvec(m,v){ return m.map(row=>{ let s=0; for(let j=0;j<N;j++)s+=row[j]*v[j]; return s; }); }
function fiedler(){
// random start, orthogonal to ones -> power iteration on M converges to Fiedler vector.
let v = Array.from({length:N},()=>Math.random()-0.5);
deflate(v); norm(v);
let lambdaM=0;
for(let it=0; it<400; it++){
let w = matvec(M,v);
deflate(w); // keep orthogonal to the trivial eigenvector
lambdaM = norm(w);
if(dot(w,v)<0){ for(let i=0;i<N;i++)w[i]=-w[i]; } // stabilize sign for convergence test
v = w;
}
const fiedlerEig = c - lambdaM; // convert M’s eigenvalue back to L’s (the Fiedler value)
return { vec:v, val:fiedlerEig };
}
function draw(groups){
const svg=document.getElementById('net'); svg.innerHTML='';
const NS='http://www.w3.org/2000/svg';
EDGES.forEach(([i,j])=>{ const l=document.createElementNS(NS,'line');
l.setAttribute('class','edge'); l.setAttribute('x1',POS[i][0]); l.setAttribute('y1',POS[i][1]);
l.setAttribute('x2',POS[j][0]); l.setAttribute('y2',POS[j][1]); svg.appendChild(l); });
for(let i=0;i<N;i++){ const g = groups?(groups[i]>=0?'a':'b'):'';
const cN=document.createElementNS(NS,'circle'); cN.setAttribute('class','node '+g);
cN.setAttribute('cx',POS[i][0]); cN.setAttribute('cy',POS[i][1]); cN.setAttribute('r',14); svg.appendChild(cN);
const t=document.createElementNS(NS,'text'); t.setAttribute('class','label');
t.setAttribute('x',POS[i][0]); t.setAttribute('y',POS[i][1]+4); t.setAttribute('text-anchor','middle');
t.textContent=i; svg.appendChild(t); }
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+cls; }
function find(){
const {vec,val}=fiedler();
const groups=vec.map(x=>x>=0?1:-1);
draw(groups);
const A=[],B=[]; groups.forEach((g,i)=>(g>=0?A:B).push(i));
setStatus(`Two communities found.`,'ok');
document.getElementById('readout').textContent =
fmt(`Fiedler value λ₂ ≈ %val%
Group A (green): {%A%}
Group B (orange): {%B%}`, { val: val.toFixed(3), A: A.join(', '), B: B.join(', ') });
console.log(`Fiedler vector:`, vec.map(x=>x.toFixed(3)).join(' '), '| λ2 =', val.toFixed(4));
}
document.getElementById('find').addEventListener('click',find);
document.getElementById('reset').addEventListener('click',()=>{ draw(null);
setStatus(`Press "Find communities".`,''); document.getElementById('readout').textContent=''; });
draw(null);
<p class="hint">This network has two loosely connected groups. The computer builds the
<b>Laplacian</b> matrix, finds the <b>Fiedler vector</b> (the eigenvector of the second-smallest
eigenvalue), and colors each vertex by the <b>sign</b> of its entry. Nobody told it where the
groups are.</p>
<svg id="net" viewBox="0 0 360 240" aria-label="network"></svg>
<div class="status" id="status">Press "Find communities".</div>
<div class="readout" id="readout"></div>
<div class="btns">
<button id="find" type="button">Find communities</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 .6rem; line-height: 1.45; }
#net { width: 100%; max-width: 360px; height: auto; display: block;
background: #f4f7fa; border: 1px solid #cdd9e3; border-radius: 10px; }
.edge { stroke: #adb1b8; stroke-width: 1.4; }
.node { stroke: #1d3557; stroke-width: 1.5; fill: #c9ccd1; transition: fill .35s; }
.node.a { fill: #2a9d8f; }
.node.b { fill: #e76f51; }
.label { font: 600 11px ui-monospace, monospace; fill: #1d3557; pointer-events: none; }
.status { font-size: 1rem; font-weight: 600; margin: .55rem 0 .2rem; min-height: 1.3em; }
.status.ok { color: #0a7d33; }
.readout { font: 500 .82rem ui-monospace, monospace; color: #345; white-space: pre-wrap;
margin: 0 0 .5rem; min-height: 1.2em; }
.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; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Spectral graph theory: read a network through the eigenvalues of its Laplacian L = D - A.
// The Fiedler vector (eigenvector of the 2nd-smallest eigenvalue) splits the graph by sign.
// Eigenvector via inverse-free shifted power iteration on M = c*I - L (deflated against the
// all-ones vector, which is L’s eigenvector for eigenvalue 0). The top eigenvector of M that
// is orthogonal to ones is exactly the Fiedler vector. This is polynomial-time; the balanced
// cut it approximates is NP-hard (Fiedler 1973; Cheeger’s inequality).
const N = 8;
const POS = [[60,55],[45,120],[80,185],[120,110], // group 0 (left)
[240,55],[295,120],[260,185],[210,120]]; // group 1 (right)
const EDGES = [
[0,1],[0,3],[1,2],[1,3],[2,3], // dense inside left
[4,5],[4,7],[5,6],[5,7],[6,7], // dense inside right
[3,7] // one weak bridge between groups
];
const A = Array.from({length:N},()=>Array(N).fill(0));
EDGES.forEach(([i,j])=>{ A[i][j]=1; A[j][i]=1; });
const deg = A.map(r=>r.reduce((s,x)=>s+x,0));
// L = D - A ; M = c*I - L so M’s top eigenpair = L’s smallest eigenpair.
const c = 2*Math.max(...deg) + 1;
const M = Array.from({length:N},(_,i)=>Array.from({length:N},(_,j)=>
(i===j ? c-deg[i] : A[i][j])));
const ones = Array(N).fill(1/Math.sqrt(N));
function dot(u,v){ let s=0; for(let i=0;i<N;i++)s+=u[i]*v[i]; return s; }
function deflate(v){ const p=dot(v,ones); for(let i=0;i<N;i++)v[i]-=p*ones[i]; } // remove ones component
function norm(v){ const n=Math.sqrt(dot(v,v))||1; for(let i=0;i<N;i++)v[i]/=n; return n; }
function matvec(m,v){ return m.map(row=>{ let s=0; for(let j=0;j<N;j++)s+=row[j]*v[j]; return s; }); }
function fiedler(){
// random start, orthogonal to ones -> power iteration on M converges to Fiedler vector.
let v = Array.from({length:N},()=>Math.random()-0.5);
deflate(v); norm(v);
let lambdaM=0;
for(let it=0; it<400; it++){
let w = matvec(M,v);
deflate(w); // keep orthogonal to the trivial eigenvector
lambdaM = norm(w);
if(dot(w,v)<0){ for(let i=0;i<N;i++)w[i]=-w[i]; } // stabilize sign for convergence test
v = w;
}
const fiedlerEig = c - lambdaM; // convert M’s eigenvalue back to L’s (the Fiedler value)
return { vec:v, val:fiedlerEig };
}
function draw(groups){
const svg=document.getElementById('net'); svg.innerHTML='';
const NS='http://www.w3.org/2000/svg';
EDGES.forEach(([i,j])=>{ const l=document.createElementNS(NS,'line');
l.setAttribute('class','edge'); l.setAttribute('x1',POS[i][0]); l.setAttribute('y1',POS[i][1]);
l.setAttribute('x2',POS[j][0]); l.setAttribute('y2',POS[j][1]); svg.appendChild(l); });
for(let i=0;i<N;i++){ const g = groups?(groups[i]>=0?'a':'b'):'';
const cN=document.createElementNS(NS,'circle'); cN.setAttribute('class','node '+g);
cN.setAttribute('cx',POS[i][0]); cN.setAttribute('cy',POS[i][1]); cN.setAttribute('r',14); svg.appendChild(cN);
const t=document.createElementNS(NS,'text'); t.setAttribute('class','label');
t.setAttribute('x',POS[i][0]); t.setAttribute('y',POS[i][1]+4); t.setAttribute('text-anchor','middle');
t.textContent=i; svg.appendChild(t); }
}
function setStatus(t,cls){ const s=document.getElementById('status'); s.textContent=t; s.className='status '+cls; }
function find(){
const {vec,val}=fiedler();
const groups=vec.map(x=>x>=0?1:-1);
draw(groups);
const A=[],B=[]; groups.forEach((g,i)=>(g>=0?A:B).push(i));
setStatus(`Two communities found.`,'ok');
document.getElementById('readout').textContent =
fmt(`Fiedler value λ₂ ≈ %val%
Group A (green): {%A%}
Group B (orange): {%B%}`, { val: val.toFixed(3), A: A.join(', '), B: B.join(', ') });
console.log(`Fiedler vector:`, vec.map(x=>x.toFixed(3)).join(' '), '| λ2 =', val.toFixed(4));
}
document.getElementById('find').addEventListener('click',find);
document.getElementById('reset').addEventListener('click',()=>{ draw(null);
setStatus(`Press "Find communities".`,''); document.getElementById('readout').textContent=''; });
draw(null);
The magic is that nobody told the algorithm where the groups are. It only saw who-is-linked-to-whom, formed a matrix, and the second eigenvector sorted the vertices into two camps. The corresponding eigenvalue — the algebraic connectivity, or Fiedler value — even measures how separable the graph is: near zero means it almost falls apart; large means it is tightly knit.
Comments
Loading comments...