The graph below has six nodes, each initialized with a simple scalar feature (shown inside the circle). Click Run one round to execute one step of message passing: each node receives the features of its neighbors, averages them, and adds the result to its own value โ a classic mean-aggregation GNN layer.
<p class="hint">Each node starts with a feature value. Click <b>Run one round</b> to perform one message-passing step: every node averages its neighbors' values and adds that to its own. Depth = reach โ run multiple rounds to propagate information further across the graph.</p>
<div id="canvas-wrap"><canvas id="gc" width="460" height="280"></canvas></div>
<div class="status" id="status">Round 0 โ initial embeddings.</div>
<div class="btns">
<button id="step" type="button">Run one round</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: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.5; }
#canvas-wrap { background: #f0f4f8; border-radius: 10px; overflow: hidden; margin-bottom: .5rem; }
canvas { display: block; max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin: .3rem 0 .5rem; }
.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] ?? ''));
// Graph Neural Network โ mean-aggregation message passing demo.
// Each node h_v โ h_v + mean({ h_u : u โ neighbours(v) })
// This mirrors the "GraphSAGE mean" aggregation / basic GCN update rule.
// โโ Graph definition โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// 6 nodes, 0-indexed. Positions are in canvas coordinates (460ร280).
const NODES = [
{ x: 100, y: 60, init: 1.0 }, // 0
{ x: 260, y: 50, init: 3.0 }, // 1
{ x: 390, y: 110, init: 0.5 }, // 2
{ x: 320, y: 220, init: 2.0 }, // 3
{ x: 130, y: 230, init: 1.5 }, // 4
{ x: 230, y: 150, init: 0.0 }, // 5 (hub)
];
// Undirected edges stored as pairs [a,b] with a<b.
const EDGES = [[0,5],[1,5],[2,3],[2,5],[3,5],[4,5],[0,4],[1,2]];
// Build adjacency list
const adj = NODES.map(() => []);
EDGES.forEach(([a,b]) => { adj[a].push(b); adj[b].push(a); });
// โโ State โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let h = NODES.map(n => n.init); // current embeddings
let round = 0;
// Color ramp: map value in [0, MAX] to a blue gradient
const MAX_VAL = 8;
function valToColor(v) {
const t = Math.min(1, Math.max(0, v / MAX_VAL));
// interpolate from #c8d8e8 (light) to #1d3557 (dark)
const r = Math.round(200 - t * 169);
const g = Math.round(216 - t * 163);
const b = Math.round(232 - t * 145);
return `rgb(${r},${g},${b})`;
}
function textColor(v) {
return (v / MAX_VAL) > 0.45 ? '#fff' : '#1d3557';
}
// โโ Canvas drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const canvas = document.getElementById('gc');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const R = 24; // node radius
function draw() {
ctx.clearRect(0, 0, W, H);
// Edges
ctx.strokeStyle = '#9fb2c8';
ctx.lineWidth = 2;
EDGES.forEach(([a, b]) => {
ctx.beginPath();
ctx.moveTo(NODES[a].x, NODES[a].y);
ctx.lineTo(NODES[b].x, NODES[b].y);
ctx.stroke();
});
// Nodes
NODES.forEach((n, i) => {
const v = h[i];
ctx.beginPath();
ctx.arc(n.x, n.y, R, 0, Math.PI * 2);
ctx.fillStyle = valToColor(v);
ctx.fill();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 2;
ctx.stroke();
// Value label
ctx.fillStyle = textColor(v);
ctx.font = 'bold 13px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(v.toFixed(1), n.x, n.y);
// Node index (small, outside)
ctx.fillStyle = '#5a7088';
ctx.font = '10px system-ui, sans-serif';
ctx.fillText('v' + i, n.x, n.y - R - 7);
});
}
// โโ Message passing step โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// h_v โ h_v + mean_neighbours(h_u)
function step() {
const next = h.slice();
let changed = 0;
h.forEach((hv, v) => {
const nbrs = adj[v];
if (nbrs.length === 0) return;
const meanNbr = nbrs.reduce((s, u) => s + h[u], 0) / nbrs.length;
next[v] = hv + meanNbr;
changed += Math.abs(next[v] - hv);
});
h = next;
round++;
const delta = changed.toFixed(2);
document.getElementById('status').textContent =
fmt('Round %round% complete โ total embedding shift: %delta%', { round: round, delta: delta });
draw();
// Log to console for verification
console.log('Round', round, '| embeddings:', h.map(x => x.toFixed(3)));
}
function reset() {
h = NODES.map(n => n.init);
round = 0;
document.getElementById('status').textContent =
'Round 0 โ initial embeddings.';
draw();
}
document.getElementById('step').addEventListener('click', step);
document.getElementById('reset').addEventListener('click', reset);
draw();
<p class="hint">Each node starts with a feature value. Click <b>Run one round</b> to perform one message-passing step: every node averages its neighbors' values and adds that to its own. Depth = reach โ run multiple rounds to propagate information further across the graph.</p>
<div id="canvas-wrap"><canvas id="gc" width="460" height="280"></canvas></div>
<div class="status" id="status">Round 0 โ initial embeddings.</div>
<div class="btns">
<button id="step" type="button">Run one round</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: .88rem; color: #444; margin: 0 0 .6rem; line-height: 1.5; }
#canvas-wrap { background: #f0f4f8; border-radius: 10px; overflow: hidden; margin-bottom: .5rem; }
canvas { display: block; max-width: 100%; }
.status { font-size: .95rem; font-weight: 600; min-height: 1.4em; margin: .3rem 0 .5rem; }
.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] ?? ''));
// Graph Neural Network โ mean-aggregation message passing demo.
// Each node h_v โ h_v + mean({ h_u : u โ neighbours(v) })
// This mirrors the "GraphSAGE mean" aggregation / basic GCN update rule.
// โโ Graph definition โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// 6 nodes, 0-indexed. Positions are in canvas coordinates (460ร280).
const NODES = [
{ x: 100, y: 60, init: 1.0 }, // 0
{ x: 260, y: 50, init: 3.0 }, // 1
{ x: 390, y: 110, init: 0.5 }, // 2
{ x: 320, y: 220, init: 2.0 }, // 3
{ x: 130, y: 230, init: 1.5 }, // 4
{ x: 230, y: 150, init: 0.0 }, // 5 (hub)
];
// Undirected edges stored as pairs [a,b] with a<b.
const EDGES = [[0,5],[1,5],[2,3],[2,5],[3,5],[4,5],[0,4],[1,2]];
// Build adjacency list
const adj = NODES.map(() => []);
EDGES.forEach(([a,b]) => { adj[a].push(b); adj[b].push(a); });
// โโ State โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
let h = NODES.map(n => n.init); // current embeddings
let round = 0;
// Color ramp: map value in [0, MAX] to a blue gradient
const MAX_VAL = 8;
function valToColor(v) {
const t = Math.min(1, Math.max(0, v / MAX_VAL));
// interpolate from #c8d8e8 (light) to #1d3557 (dark)
const r = Math.round(200 - t * 169);
const g = Math.round(216 - t * 163);
const b = Math.round(232 - t * 145);
return `rgb(${r},${g},${b})`;
}
function textColor(v) {
return (v / MAX_VAL) > 0.45 ? '#fff' : '#1d3557';
}
// โโ Canvas drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const canvas = document.getElementById('gc');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const R = 24; // node radius
function draw() {
ctx.clearRect(0, 0, W, H);
// Edges
ctx.strokeStyle = '#9fb2c8';
ctx.lineWidth = 2;
EDGES.forEach(([a, b]) => {
ctx.beginPath();
ctx.moveTo(NODES[a].x, NODES[a].y);
ctx.lineTo(NODES[b].x, NODES[b].y);
ctx.stroke();
});
// Nodes
NODES.forEach((n, i) => {
const v = h[i];
ctx.beginPath();
ctx.arc(n.x, n.y, R, 0, Math.PI * 2);
ctx.fillStyle = valToColor(v);
ctx.fill();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 2;
ctx.stroke();
// Value label
ctx.fillStyle = textColor(v);
ctx.font = 'bold 13px ui-monospace, monospace';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(v.toFixed(1), n.x, n.y);
// Node index (small, outside)
ctx.fillStyle = '#5a7088';
ctx.font = '10px system-ui, sans-serif';
ctx.fillText('v' + i, n.x, n.y - R - 7);
});
}
// โโ Message passing step โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
// h_v โ h_v + mean_neighbours(h_u)
function step() {
const next = h.slice();
let changed = 0;
h.forEach((hv, v) => {
const nbrs = adj[v];
if (nbrs.length === 0) return;
const meanNbr = nbrs.reduce((s, u) => s + h[u], 0) / nbrs.length;
next[v] = hv + meanNbr;
changed += Math.abs(next[v] - hv);
});
h = next;
round++;
const delta = changed.toFixed(2);
document.getElementById('status').textContent =
fmt('Round %round% complete โ total embedding shift: %delta%', { round: round, delta: delta });
draw();
// Log to console for verification
console.log('Round', round, '| embeddings:', h.map(x => x.toFixed(3)));
}
function reset() {
h = NODES.map(n => n.init);
round = 0;
document.getElementById('status').textContent =
'Round 0 โ initial embeddings.';
draw();
}
document.getElementById('step').addEventListener('click', step);
document.getElementById('reset').addEventListener('click', reset);
draw();
Notice how nodes with well-connected neighbors change more dramatically. After a single round every node has absorbed a snapshot of its immediate neighborhood. Click again to propagate information two hops out. This is how GNNs build context: depth equals reach.
Comments
Loading comments...