The graph below has six nodes. Click any edge button to toggle that connection on or off and watch the Katz scores update instantly. The bar next to each node shows its score relative to the highest-scoring node.
<!-- Katz Centrality demo — interactive graph with live score bars -->
<p class="hint">Toggle edges on/off and drag the α slider to see how Katz scores change. Higher α = more weight on distant paths.</p>
<div id="main-area">
<canvas id="graph-canvas" width="340" height="260"></canvas>
<div id="scores-panel">
<div class="panel-title">Katz scores</div>
<div id="score-bars"></div>
</div>
</div>
<div class="controls">
<div class="alpha-row">
<label for="alpha-slider">α = <span id="alpha-val">0.20</span></label>
<input id="alpha-slider" type="range" min="5" max="45" value="20" step="1">
</div>
<div id="edge-btns"></div>
</div>
<div class="status" id="status">Scores ready.</div>
/* Layout and node/bar colours for the Katz centrality demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
#main-area { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; }
#graph-canvas { border: 1px solid #cdd9e3; border-radius: 10px; background: #f8fafc; flex-shrink: 0; }
#scores-panel { flex: 1; min-width: 140px; }
.panel-title { font-size: .78rem; font-weight: 700; color: #555; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 6px; }
.score-row { display: flex; align-items: center; gap: 6px; margin-bottom: 5px; font-size: .82rem; }
.score-label { width: 22px; font-weight: 700; color: #1d3557; }
.score-bar-wrap { flex: 1; background: #e4ecf3; border-radius: 4px; height: 12px; overflow: hidden; }
.score-bar { height: 100%; background: #457b9d; border-radius: 4px; transition: width .25s; }
.score-num { width: 38px; text-align: right; color: #444; font-size: .78rem; }
.controls { margin-top: 8px; }
.alpha-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; font-size: .85rem; }
.alpha-row label { white-space: nowrap; }
#alpha-slider { flex: 1; }
#edge-btns { display: flex; flex-wrap: wrap; gap: 5px; }
.edge-btn { font: 600 12px system-ui, sans-serif; padding: .3rem .55rem;
border: 1px solid #adb1b8; background: #e2e5ea;
color: #333; border-radius: 6px; cursor: pointer; transition: all .15s; }
.edge-btn.on { background: #1d3557; border-color: #1d3557; color: #fff; }
.status { font-size: .88rem; font-weight: 600; margin-top: 6px; min-height: 1.3em; color: #555; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Katz centrality via 80-step power iteration — exact for this graph size
// Six labelled nodes with fixed canvas positions
const NODES = [
{ id: 0, label: 'A', x: 60, y: 50 },
{ id: 1, label: 'B', x: 170, y: 30 },
{ id: 2, label: 'C', x: 280, y: 70 },
{ id: 3, label: 'D', x: 60, y: 190 },
{ id: 4, label: 'E', x: 170, y: 210 },
{ id: 5, label: 'F', x: 290, y: 180 }
];
const N = NODES.length;
// All possible edges (index = button id)
const ALL_EDGES = [
[0,1],[1,2],[2,5],[3,4],[4,5],[0,3],[1,4],[2,4],[1,5],[0,4]
];
// Indices of currently active edges
const active = new Set([0,1,2,3,4,5]);
const canvas = document.getElementById('graph-canvas');
const ctx = canvas.getContext('2d');
const R = 18; // node circle radius in pixels
// Compute Katz centrality: x = (I - alpha*A)^-1 * 1 - 1
function katzScores(alpha) {
// Build adjacency matrix from active edges
const A = Array.from({length:N}, () => new Float64Array(N));
for (const i of active) {
const [u,v] = ALL_EDGES[i];
A[u][v] = 1; A[v][u] = 1;
}
// Power iteration converges because alpha < 1/lambda_1
// x^(t+1) = alpha * A * x^(t) + 1
let x = new Float64Array(N).fill(0);
for (let iter = 0; iter < 80; iter++) {
const nx = new Float64Array(N).fill(1);
for (let u = 0; u < N; u++)
for (let v = 0; v < N; v++)
nx[u] += alpha * A[u][v] * x[v];
x = nx;
}
// Subtract 1 to match the standard Katz formula
for (let i = 0; i < N; i++) x[i] -= 1;
return x;
}
function getAlpha() { return parseInt(document.getElementById('alpha-slider').value, 10) / 100; }
function drawGraph(scores) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const max = Math.max(...scores, 0.001);
// Draw active edges first so nodes appear on top
for (const i of active) {
const [u,v] = ALL_EDGES[i];
ctx.beginPath();
ctx.moveTo(NODES[u].x, NODES[u].y);
ctx.lineTo(NODES[v].x, NODES[v].y);
ctx.strokeStyle = '#bcc8d4';
ctx.lineWidth = 2;
ctx.stroke();
}
// Draw nodes coloured by relative score
for (let i = 0; i < N; i++) {
const n = NODES[i];
const t = scores[i] / max;
ctx.beginPath();
ctx.arc(n.x, n.y, R, 0, 2*Math.PI);
const r = Math.round(29 + (69 - 29)*t);
const g = Math.round(85 + (123 - 85)*t);
const b = Math.round(87 + (157 - 87)*t);
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fill();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = 'bold 13px system-ui,sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(n.label, n.x, n.y);
}
}
function updateScoreBars(scores) {
const max = Math.max(...scores, 0.001);
const panel = document.getElementById('score-bars');
panel.innerHTML = '';
for (let i = 0; i < N; i++) {
const pct = Math.round((scores[i] / max) * 100);
const row = document.createElement('div');
row.className = 'score-row';
row.innerHTML =
`<span class="score-label">${NODES[i].label}</span>` +
`<span class="score-bar-wrap"><span class="score-bar" style="width:${pct}%"></span></span>` +
`<span class="score-num">${scores[i].toFixed(2)}</span>`;
panel.appendChild(row);
}
}
function update() {
const alpha = getAlpha();
const scores = katzScores(alpha);
drawGraph(scores);
updateScoreBars(scores);
const top = NODES[scores.indexOf(Math.max(...scores))].label;
document.getElementById('status').textContent = i18nFmt('Top node: %node% (α = %alpha%)', { node: top, alpha: alpha.toFixed(2) });
}
// Create toggle buttons for every possible edge
const btnArea = document.getElementById('edge-btns');
ALL_EDGES.forEach(([u,v], i) => {
const btn = document.createElement('button');
btn.className = 'edge-btn on';
btn.textContent = `${NODES[u].label}–${NODES[v].label}`;
btn.title = i18nFmt('Toggle edge %u%–%v%', { u: NODES[u].label, v: NODES[v].label });
btn.addEventListener('click', () => {
if (active.has(i)) active.delete(i); else active.add(i);
btn.className = 'edge-btn' + (active.has(i) ? ' on' : '');
update();
});
btnArea.appendChild(btn);
});
document.getElementById('alpha-slider').addEventListener('input', () => {
document.getElementById('alpha-val').textContent = getAlpha().toFixed(2);
update();
});
update();
<!-- Katz Centrality demo — interactive graph with live score bars -->
<p class="hint">Toggle edges on/off and drag the α slider to see how Katz scores change. Higher α = more weight on distant paths.</p>
<div id="main-area">
<canvas id="graph-canvas" width="340" height="260"></canvas>
<div id="scores-panel">
<div class="panel-title">Katz scores</div>
<div id="score-bars"></div>
</div>
</div>
<div class="controls">
<div class="alpha-row">
<label for="alpha-slider">α = <span id="alpha-val">0.20</span></label>
<input id="alpha-slider" type="range" min="5" max="45" value="20" step="1">
</div>
<div id="edge-btns"></div>
</div>
<div class="status" id="status">Scores ready.</div>
/* Layout and node/bar colours for the Katz centrality demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
#main-area { display: flex; gap: 12px; align-items: flex-start; flex-wrap: wrap; }
#graph-canvas { border: 1px solid #cdd9e3; border-radius: 10px; background: #f8fafc; flex-shrink: 0; }
#scores-panel { flex: 1; min-width: 140px; }
.panel-title { font-size: .78rem; font-weight: 700; color: #555; text-transform: uppercase; letter-spacing: .05em; margin-bottom: 6px; }
.score-row { display: flex; align-items: center; gap: 6px; margin-bottom: 5px; font-size: .82rem; }
.score-label { width: 22px; font-weight: 700; color: #1d3557; }
.score-bar-wrap { flex: 1; background: #e4ecf3; border-radius: 4px; height: 12px; overflow: hidden; }
.score-bar { height: 100%; background: #457b9d; border-radius: 4px; transition: width .25s; }
.score-num { width: 38px; text-align: right; color: #444; font-size: .78rem; }
.controls { margin-top: 8px; }
.alpha-row { display: flex; align-items: center; gap: 8px; margin-bottom: 6px; font-size: .85rem; }
.alpha-row label { white-space: nowrap; }
#alpha-slider { flex: 1; }
#edge-btns { display: flex; flex-wrap: wrap; gap: 5px; }
.edge-btn { font: 600 12px system-ui, sans-serif; padding: .3rem .55rem;
border: 1px solid #adb1b8; background: #e2e5ea;
color: #333; border-radius: 6px; cursor: pointer; transition: all .15s; }
.edge-btn.on { background: #1d3557; border-color: #1d3557; color: #fff; }
.status { font-size: .88rem; font-weight: 600; margin-top: 6px; min-height: 1.3em; color: #555; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Katz centrality via 80-step power iteration — exact for this graph size
// Six labelled nodes with fixed canvas positions
const NODES = [
{ id: 0, label: 'A', x: 60, y: 50 },
{ id: 1, label: 'B', x: 170, y: 30 },
{ id: 2, label: 'C', x: 280, y: 70 },
{ id: 3, label: 'D', x: 60, y: 190 },
{ id: 4, label: 'E', x: 170, y: 210 },
{ id: 5, label: 'F', x: 290, y: 180 }
];
const N = NODES.length;
// All possible edges (index = button id)
const ALL_EDGES = [
[0,1],[1,2],[2,5],[3,4],[4,5],[0,3],[1,4],[2,4],[1,5],[0,4]
];
// Indices of currently active edges
const active = new Set([0,1,2,3,4,5]);
const canvas = document.getElementById('graph-canvas');
const ctx = canvas.getContext('2d');
const R = 18; // node circle radius in pixels
// Compute Katz centrality: x = (I - alpha*A)^-1 * 1 - 1
function katzScores(alpha) {
// Build adjacency matrix from active edges
const A = Array.from({length:N}, () => new Float64Array(N));
for (const i of active) {
const [u,v] = ALL_EDGES[i];
A[u][v] = 1; A[v][u] = 1;
}
// Power iteration converges because alpha < 1/lambda_1
// x^(t+1) = alpha * A * x^(t) + 1
let x = new Float64Array(N).fill(0);
for (let iter = 0; iter < 80; iter++) {
const nx = new Float64Array(N).fill(1);
for (let u = 0; u < N; u++)
for (let v = 0; v < N; v++)
nx[u] += alpha * A[u][v] * x[v];
x = nx;
}
// Subtract 1 to match the standard Katz formula
for (let i = 0; i < N; i++) x[i] -= 1;
return x;
}
function getAlpha() { return parseInt(document.getElementById('alpha-slider').value, 10) / 100; }
function drawGraph(scores) {
ctx.clearRect(0, 0, canvas.width, canvas.height);
const max = Math.max(...scores, 0.001);
// Draw active edges first so nodes appear on top
for (const i of active) {
const [u,v] = ALL_EDGES[i];
ctx.beginPath();
ctx.moveTo(NODES[u].x, NODES[u].y);
ctx.lineTo(NODES[v].x, NODES[v].y);
ctx.strokeStyle = '#bcc8d4';
ctx.lineWidth = 2;
ctx.stroke();
}
// Draw nodes coloured by relative score
for (let i = 0; i < N; i++) {
const n = NODES[i];
const t = scores[i] / max;
ctx.beginPath();
ctx.arc(n.x, n.y, R, 0, 2*Math.PI);
const r = Math.round(29 + (69 - 29)*t);
const g = Math.round(85 + (123 - 85)*t);
const b = Math.round(87 + (157 - 87)*t);
ctx.fillStyle = `rgb(${r},${g},${b})`;
ctx.fill();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = 'bold 13px system-ui,sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(n.label, n.x, n.y);
}
}
function updateScoreBars(scores) {
const max = Math.max(...scores, 0.001);
const panel = document.getElementById('score-bars');
panel.innerHTML = '';
for (let i = 0; i < N; i++) {
const pct = Math.round((scores[i] / max) * 100);
const row = document.createElement('div');
row.className = 'score-row';
row.innerHTML =
`<span class="score-label">${NODES[i].label}</span>` +
`<span class="score-bar-wrap"><span class="score-bar" style="width:${pct}%"></span></span>` +
`<span class="score-num">${scores[i].toFixed(2)}</span>`;
panel.appendChild(row);
}
}
function update() {
const alpha = getAlpha();
const scores = katzScores(alpha);
drawGraph(scores);
updateScoreBars(scores);
const top = NODES[scores.indexOf(Math.max(...scores))].label;
document.getElementById('status').textContent = i18nFmt('Top node: %node% (α = %alpha%)', { node: top, alpha: alpha.toFixed(2) });
}
// Create toggle buttons for every possible edge
const btnArea = document.getElementById('edge-btns');
ALL_EDGES.forEach(([u,v], i) => {
const btn = document.createElement('button');
btn.className = 'edge-btn on';
btn.textContent = `${NODES[u].label}–${NODES[v].label}`;
btn.title = i18nFmt('Toggle edge %u%–%v%', { u: NODES[u].label, v: NODES[v].label });
btn.addEventListener('click', () => {
if (active.has(i)) active.delete(i); else active.add(i);
btn.className = 'edge-btn' + (active.has(i) ? ' on' : '');
update();
});
btnArea.appendChild(btn);
});
document.getElementById('alpha-slider').addEventListener('input', () => {
document.getElementById('alpha-val').textContent = getAlpha().toFixed(2);
update();
});
update();
Notice how adding a single edge can ripple through scores across the entire graph — a node gains not just from its new direct neighbor but from every node reachable through that neighbor, faded by distance. That cascading discount is the essence of Katz centrality, and exactly what PageRank refines further.
Comments
Loading comments...