Pick a preset or click two nodes to add an edge between them. The demo computes the assortativity coefficient r in real time using Newman's degree-correlation formula.
<!-- Assortative mixing demo — measure degree correlation in an interactive network -->
<div class="controls">
<label>Preset:</label>
<div class="presets">
<button type="button" data-preset="star">Star</button>
<button type="button" data-preset="path">Path</button>
<button type="button" data-preset="clique">Clique</button>
<button type="button" data-preset="social">Social</button>
<button type="button" class="ghost" data-preset="clear">Clear</button>
</div>
</div>
<canvas id="canvas" width="480" height="260" title="Click an empty area to add a node; click two nodes to add an edge"></canvas>
<p class="instruction">Click an empty area to add a node. Click a node (it turns red), then click another node to add an edge.</p>
<div class="result" id="result">
<span class="r-label">r =</span>
<span class="r-value" id="rval">—</span>
<span class="r-desc" id="rdesc"></span>
</div>
/* Layout and controls */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .5rem; }
label { font-size: .85rem; font-weight: 600; color: #555; }
.presets { display: flex; gap: .4rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .35rem .75rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 6px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
background: #f7f9fb; width: 100%; max-width: 480px; cursor: crosshair; }
.instruction { font-size: .82rem; color: #666; margin: .4rem 0; }
/* Result display — r value and description */
.result { display: flex; align-items: baseline; gap: .5rem; margin-top: .3rem; min-height: 2rem; }
.r-label { font: 700 1.1rem ui-monospace, monospace; color: #444; }
.r-value { font: 700 1.6rem ui-monospace, monospace; color: #1d3557; }
.r-desc { font-size: .9rem; color: #555; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Demo state
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Graph data — nodes: {x,y,id}, edges: [i,j]
let nodes = [], edges = [], pending = null;
// Built-in preset graphs
const PRESETS = {
star: () => {
// Star: one hub connected to n leaves — strongly disassortative
const cx = W/2, cy = H/2, r = 90, n = 7;
nodes = [{ id: 0, x: cx, y: cy }];
for (let i = 1; i <= n; i++)
nodes.push({ id: i, x: cx + r * Math.cos(2*Math.PI*(i-1)/n),
y: cy + r * Math.sin(2*Math.PI*(i-1)/n) });
edges = Array.from({ length: n }, (_, i) => [0, i + 1]);
},
path: () => {
// Path: a chain of nodes with equal degree (endpoints differ) — mildly disassortative
const n = 6;
nodes = Array.from({ length: n }, (_, i) => ({
id: i, x: 50 + i * (W - 100) / (n - 1), y: H / 2
}));
edges = Array.from({ length: n - 1 }, (_, i) => [i, i + 1]);
},
clique: () => {
// Complete graph: all degrees equal — assortativity = 0
const n = 5, cx = W/2, cy = H/2, r = 80;
nodes = Array.from({ length: n }, (_, i) => ({
id: i, x: cx + r * Math.cos(2*Math.PI*i/n), y: cy + r * Math.sin(2*Math.PI*i/n)
}));
edges = [];
for (let i = 0; i < n; i++) for (let j = i+1; j < n; j++) edges.push([i, j]);
},
social: () => {
// Social-like: two communities bridged by one node — 3 hubs in a triangle, each hub has a leaf-pair; leaf-pairs connect internally
nodes = [
{id:0,x:240,y:70},{id:1,x:140,y:200},{id:2,x:340,y:200},
{id:3,x:150,y:60},{id:4,x:90,y:130},
{id:5,x:60,y:220},{id:6,x:150,y:240},
{id:7,x:390,y:130},{id:8,x:390,y:230}
];
edges = [
[0,1],[1,2],[0,2],
[0,3],[3,4],
[1,5],[5,6],
[2,7],[7,8]
];
},
clear: () => { nodes = []; edges = []; pending = null; }
};
// Compute degree of every node
function computeDegrees() {
const deg = new Array(nodes.length).fill(0);
edges.forEach(([i, j]) => { deg[i]++; deg[j]++; });
return deg;
}
// Compute Newman's assortativity coefficient Newman (2002) formula
function assortativity() {
const m = edges.length;
if (m === 0) return null;
const deg = computeDegrees();
let sumjk = 0, sumhalf = 0, sumhalf2 = 0;
edges.forEach(([i, j]) => {
const ji = deg[i], jj = deg[j];
sumjk += ji * jj;
sumhalf += (ji + jj) / 2;
sumhalf2 += (ji*ji + jj*jj) / 2;
});
const a = sumjk / m;
const b = sumhalf / m;
const c = sumhalf2 / m;
const denom = c - b * b;
if (Math.abs(denom) < 1e-10) return 0;
return (a - b * b) / denom;
}
// Human-readable description of r
function describe(r) {
if (r === null) return '';
if (r > 0.3) return '— assortative (hubs cluster)';
if (r < -0.3) return '— disassortative (hubs reach out)';
return '— neutral';
}
// Render graph on canvas
function draw() {
ctx.clearRect(0, 0, W, H);
const deg = computeDegrees();
const maxDeg = Math.max(1, ...deg);
// Draw edges first
ctx.strokeStyle = '#8fa8c0';
ctx.lineWidth = 1.4;
edges.forEach(([i, j]) => {
ctx.beginPath();
ctx.moveTo(nodes[i].x, nodes[i].y);
ctx.lineTo(nodes[j].x, nodes[j].y);
ctx.stroke();
});
// Draw nodes — size proportional to degree
nodes.forEach((n, idx) => {
const r = 7 + 8 * (deg[idx] / maxDeg);
ctx.beginPath();
ctx.arc(n.x, n.y, r, 0, 2 * Math.PI);
ctx.fillStyle = pending === idx ? '#e63946' : '#1d3557';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = `bold ${r > 10 ? 11 : 9}px ui-monospace, monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(deg[idx], n.x, n.y);
});
// Update r display
const r = assortativity();
const rEl = document.getElementById('rval');
const dEl = document.getElementById('rdesc');
rEl.textContent = r === null ? '—' : r.toFixed(3);
dEl.textContent = describe(r);
}
// Find which node was clicked (returns index or -1)
function hitNode(x, y) {
const deg = computeDegrees();
const maxDeg = Math.max(1, ...deg);
for (let i = nodes.length - 1; i >= 0; i--) {
const n = nodes[i];
const r = 7 + 8 * (deg[i] / maxDeg);
if ((x - n.x)**2 + (y - n.y)**2 <= (r + 4)**2) return i;
}
return -1;
}
// Handle canvas clicks: add node or connect two nodes
canvas.addEventListener('click', e => {
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width, scaleY = H / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
const hit = hitNode(x, y);
if (hit === -1) {
// Clicked empty space — create new node
nodes.push({ id: nodes.length, x, y });
pending = null;
} else if (pending === null) {
// Clicked a node with no pending — select it
pending = hit;
} else if (pending === hit) {
pending = null;
} else {
// Second node clicked — avoid duplicates
const [a, b] = [Math.min(pending, hit), Math.max(pending, hit)];
if (!edges.some(([ei, ej]) => ei === a && ej === b)) edges.push([a, b]);
pending = null;
}
draw();
});
// Wire preset buttons
document.querySelectorAll('[data-preset]').forEach(btn => {
btn.addEventListener('click', () => {
PRESETS[btn.dataset.preset]();
pending = null;
draw();
});
});
PRESETS.social();
draw();
<!-- Assortative mixing demo — measure degree correlation in an interactive network -->
<div class="controls">
<label>Preset:</label>
<div class="presets">
<button type="button" data-preset="star">Star</button>
<button type="button" data-preset="path">Path</button>
<button type="button" data-preset="clique">Clique</button>
<button type="button" data-preset="social">Social</button>
<button type="button" class="ghost" data-preset="clear">Clear</button>
</div>
</div>
<canvas id="canvas" width="480" height="260" title="Click an empty area to add a node; click two nodes to add an edge"></canvas>
<p class="instruction">Click an empty area to add a node. Click a node (it turns red), then click another node to add an edge.</p>
<div class="result" id="result">
<span class="r-label">r =</span>
<span class="r-value" id="rval">—</span>
<span class="r-desc" id="rdesc"></span>
</div>
/* Layout and controls */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .5rem; }
label { font-size: .85rem; font-weight: 600; color: #555; }
.presets { display: flex; gap: .4rem; flex-wrap: wrap; }
button { font: 600 13px system-ui; padding: .35rem .75rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 6px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
canvas { display: block; border: 1px solid #cdd9e3; border-radius: 8px;
background: #f7f9fb; width: 100%; max-width: 480px; cursor: crosshair; }
.instruction { font-size: .82rem; color: #666; margin: .4rem 0; }
/* Result display — r value and description */
.result { display: flex; align-items: baseline; gap: .5rem; margin-top: .3rem; min-height: 2rem; }
.r-label { font: 700 1.1rem ui-monospace, monospace; color: #444; }
.r-value { font: 700 1.6rem ui-monospace, monospace; color: #1d3557; }
.r-desc { font-size: .9rem; color: #555; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Demo state
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
// Graph data — nodes: {x,y,id}, edges: [i,j]
let nodes = [], edges = [], pending = null;
// Built-in preset graphs
const PRESETS = {
star: () => {
// Star: one hub connected to n leaves — strongly disassortative
const cx = W/2, cy = H/2, r = 90, n = 7;
nodes = [{ id: 0, x: cx, y: cy }];
for (let i = 1; i <= n; i++)
nodes.push({ id: i, x: cx + r * Math.cos(2*Math.PI*(i-1)/n),
y: cy + r * Math.sin(2*Math.PI*(i-1)/n) });
edges = Array.from({ length: n }, (_, i) => [0, i + 1]);
},
path: () => {
// Path: a chain of nodes with equal degree (endpoints differ) — mildly disassortative
const n = 6;
nodes = Array.from({ length: n }, (_, i) => ({
id: i, x: 50 + i * (W - 100) / (n - 1), y: H / 2
}));
edges = Array.from({ length: n - 1 }, (_, i) => [i, i + 1]);
},
clique: () => {
// Complete graph: all degrees equal — assortativity = 0
const n = 5, cx = W/2, cy = H/2, r = 80;
nodes = Array.from({ length: n }, (_, i) => ({
id: i, x: cx + r * Math.cos(2*Math.PI*i/n), y: cy + r * Math.sin(2*Math.PI*i/n)
}));
edges = [];
for (let i = 0; i < n; i++) for (let j = i+1; j < n; j++) edges.push([i, j]);
},
social: () => {
// Social-like: two communities bridged by one node — 3 hubs in a triangle, each hub has a leaf-pair; leaf-pairs connect internally
nodes = [
{id:0,x:240,y:70},{id:1,x:140,y:200},{id:2,x:340,y:200},
{id:3,x:150,y:60},{id:4,x:90,y:130},
{id:5,x:60,y:220},{id:6,x:150,y:240},
{id:7,x:390,y:130},{id:8,x:390,y:230}
];
edges = [
[0,1],[1,2],[0,2],
[0,3],[3,4],
[1,5],[5,6],
[2,7],[7,8]
];
},
clear: () => { nodes = []; edges = []; pending = null; }
};
// Compute degree of every node
function computeDegrees() {
const deg = new Array(nodes.length).fill(0);
edges.forEach(([i, j]) => { deg[i]++; deg[j]++; });
return deg;
}
// Compute Newman's assortativity coefficient Newman (2002) formula
function assortativity() {
const m = edges.length;
if (m === 0) return null;
const deg = computeDegrees();
let sumjk = 0, sumhalf = 0, sumhalf2 = 0;
edges.forEach(([i, j]) => {
const ji = deg[i], jj = deg[j];
sumjk += ji * jj;
sumhalf += (ji + jj) / 2;
sumhalf2 += (ji*ji + jj*jj) / 2;
});
const a = sumjk / m;
const b = sumhalf / m;
const c = sumhalf2 / m;
const denom = c - b * b;
if (Math.abs(denom) < 1e-10) return 0;
return (a - b * b) / denom;
}
// Human-readable description of r
function describe(r) {
if (r === null) return '';
if (r > 0.3) return '— assortative (hubs cluster)';
if (r < -0.3) return '— disassortative (hubs reach out)';
return '— neutral';
}
// Render graph on canvas
function draw() {
ctx.clearRect(0, 0, W, H);
const deg = computeDegrees();
const maxDeg = Math.max(1, ...deg);
// Draw edges first
ctx.strokeStyle = '#8fa8c0';
ctx.lineWidth = 1.4;
edges.forEach(([i, j]) => {
ctx.beginPath();
ctx.moveTo(nodes[i].x, nodes[i].y);
ctx.lineTo(nodes[j].x, nodes[j].y);
ctx.stroke();
});
// Draw nodes — size proportional to degree
nodes.forEach((n, idx) => {
const r = 7 + 8 * (deg[idx] / maxDeg);
ctx.beginPath();
ctx.arc(n.x, n.y, r, 0, 2 * Math.PI);
ctx.fillStyle = pending === idx ? '#e63946' : '#1d3557';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 1.5;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = `bold ${r > 10 ? 11 : 9}px ui-monospace, monospace`;
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(deg[idx], n.x, n.y);
});
// Update r display
const r = assortativity();
const rEl = document.getElementById('rval');
const dEl = document.getElementById('rdesc');
rEl.textContent = r === null ? '—' : r.toFixed(3);
dEl.textContent = describe(r);
}
// Find which node was clicked (returns index or -1)
function hitNode(x, y) {
const deg = computeDegrees();
const maxDeg = Math.max(1, ...deg);
for (let i = nodes.length - 1; i >= 0; i--) {
const n = nodes[i];
const r = 7 + 8 * (deg[i] / maxDeg);
if ((x - n.x)**2 + (y - n.y)**2 <= (r + 4)**2) return i;
}
return -1;
}
// Handle canvas clicks: add node or connect two nodes
canvas.addEventListener('click', e => {
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width, scaleY = H / rect.height;
const x = (e.clientX - rect.left) * scaleX;
const y = (e.clientY - rect.top) * scaleY;
const hit = hitNode(x, y);
if (hit === -1) {
// Clicked empty space — create new node
nodes.push({ id: nodes.length, x, y });
pending = null;
} else if (pending === null) {
// Clicked a node with no pending — select it
pending = hit;
} else if (pending === hit) {
pending = null;
} else {
// Second node clicked — avoid duplicates
const [a, b] = [Math.min(pending, hit), Math.max(pending, hit)];
if (!edges.some(([ei, ej]) => ei === a && ej === b)) edges.push([a, b]);
pending = null;
}
draw();
});
// Wire preset buttons
document.querySelectorAll('[data-preset]').forEach(btn => {
btn.addEventListener('click', () => {
PRESETS[btn.dataset.preset]();
pending = null;
draw();
});
});
PRESETS.social();
draw();
Notice how the score shifts. A star graph (one hub connected to many leaves) scores close to −1: the hub's many connections pair with leaves that each have just one. A clique (everyone connected to everyone) scores exactly 0 — all degrees are equal, so there is no preference at all. Real social networks typically land between 0 and +0.5.
Comments
Loading comments...