The demo below shows a small network. Each node displays its degree (number of edges). Click Rewire once to perform a single edge-swap: two edges are chosen at random, their endpoints are exchanged, and the network is redrawn. Every node's degree stays exactly the same throughout.
<!-- Degree-preserving rewiring demo: two canvases (original vs rewired) -->
<p class="hint">Each node shows its <b>degree</b> (edge count). Click <b>Rewire once</b> to swap two edges at random — every degree stays the same. Compare the rewired network (right) with the original (left).</p>
<div class="panels">
<div class="panel">
<div class="panel-label">Original</div>
<canvas id="cvOrig" width="220" height="200"></canvas>
</div>
<div class="panel">
<div class="panel-label">Rewired</div>
<canvas id="cvCurr" width="220" height="200"></canvas>
</div>
</div>
<div class="status" id="status">Ready — click Rewire to start.</div>
<div class="btns">
<button id="btnOne" type="button">Rewire once</button>
<button id="btnMany" type="button">Rewire ×10 edges</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
/* Layout: side-by-side canvas panels with status bar and buttons */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.panels { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: .5rem; }
.panel { display: flex; flex-direction: column; align-items: center; }
.panel-label { font-size: .78rem; font-weight: 600; color: #555; margin-bottom: .25rem; letter-spacing: .04em; text-transform: uppercase; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; background: #f5f8fb; }
.status { font-size: .95rem; font-weight: 600; margin: .4rem 0 .6rem; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d5a9e; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Degree-preserving rewiring: pick two edges (a-b) and (c-d),
// swap to (a-d)(b-c) or (a-c)(b-d) if no self-loop or multi-edge results.
// Node positions (x,y) for a 6-node graph on a 220x200 canvas
const NODES = [
{ x: 110, y: 30 },
{ x: 200, y: 90 },
{ x: 175, y: 185 },
{ x: 60, y: 185 },
{ x: 30, y: 90 },
{ x: 110, y: 110 }
];
// Initial edge list — 8 edges, degrees: [2,3,2,3,3,3]
const ORIG_EDGES = [[0,1],[0,4],[1,2],[1,5],[2,3],[3,4],[3,5],[4,5]];
let edges = ORIG_EDGES.map(e => [...e]); // working copy that gets rewired
let rewireCount = 0;
// Compute degree of each node from edge list
function degrees(edgeList) {
const d = new Array(NODES.length).fill(0);
for (const [u, v] of edgeList) { d[u]++; d[v]++; }
return d;
}
const origDeg = degrees(ORIG_EDGES);
// Draw graph on canvas: edges first, then node circles with degree labels
function drawGraph(canvas, edgeList, highlight) {
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
// Draw each edge; highlight recently swapped edges in red
for (let i = 0; i < edgeList.length; i++) {
const [u, v] = edgeList[i];
const isHl = highlight && (highlight[0] === i || highlight[1] === i);
ctx.beginPath();
ctx.moveTo(NODES[u].x, NODES[u].y);
ctx.lineTo(NODES[v].x, NODES[v].y);
ctx.strokeStyle = isHl ? '#e63946' : '#8fa8c0';
ctx.lineWidth = isHl ? 2.5 : 1.5;
ctx.stroke();
}
// Draw node circles with degree numbers inside
const deg = degrees(edgeList);
for (let i = 0; i < NODES.length; i++) {
const { x, y } = NODES[i];
ctx.beginPath();
ctx.arc(x, y, 16, 0, 2 * Math.PI);
ctx.fillStyle = '#1d3557';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = 'bold 13px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(deg[i], x, y);
}
}
// Attempt one degree-preserving rewiring step; return swapped indices or null
function tryRewire() {
const m = edges.length;
// Pick two distinct edge indices
const i = Math.floor(Math.random() * m);
let j = Math.floor(Math.random() * (m - 1));
if (j >= i) j++;
const [a, b] = edges[i];
const [c, d] = edges[j];
// Build current edge set string (for fast lookup), remove the two candidate edges
const newEdgeSet = new Set(edges.map(([u, v]) => u + ',' + v).concat(edges.map(([u, v]) => v + ',' + u)));
newEdgeSet.delete(a + ',' + b); newEdgeSet.delete(b + ',' + a);
newEdgeSet.delete(c + ',' + d); newEdgeSet.delete(d + ',' + c);
// Check swap variant 1: (a,d) and (b,c) — no self-loop, no multi-edge
const ok1 = a !== d && b !== c && !newEdgeSet.has(a + ',' + d) && !newEdgeSet.has(d + ',' + a)
&& !newEdgeSet.has(b + ',' + c) && !newEdgeSet.has(c + ',' + b);
if (ok1) {
edges[i] = [a, d];
edges[j] = [b, c];
rewireCount++;
return [i, j];
}
// Check swap variant 2: (a,c) and (b,d)
const ok2 = a !== c && b !== d && !newEdgeSet.has(a + ',' + c) && !newEdgeSet.has(c + ',' + a)
&& !newEdgeSet.has(b + ',' + d) && !newEdgeSet.has(d + ',' + b);
if (ok2) {
edges[i] = [a, c];
edges[j] = [b, d];
rewireCount++;
return [i, j];
}
return null; // swap rejected — try again
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status ' + cls;
}
function redraw(highlight) {
drawGraph(document.getElementById('cvOrig'), ORIG_EDGES, null);
drawGraph(document.getElementById('cvCurr'), edges, highlight);
}
document.getElementById('btnOne').addEventListener('click', () => {
// Attempt up to 20 times to find a valid rewiring
let hl = null;
for (let attempt = 0; attempt < 20; attempt++) {
hl = tryRewire();
if (hl) break;
}
redraw(hl);
setStatus(i18nFmt('Rewired %n% time(s) — degrees unchanged.', { n: rewireCount }), 'ok');
});
document.getElementById('btnMany').addEventListener('click', () => {
// Perform 10*m successful rewirings (standard Q*m heuristic)
const target = edges.length * 10;
let done = 0;
for (let attempt = 0; attempt < target * 5 && done < target; attempt++) {
if (tryRewire()) done++;
}
redraw(null);
setStatus(i18nFmt('Rewired %n% time(s) — degrees unchanged.', { n: rewireCount }), 'ok');
});
document.getElementById('btnReset').addEventListener('click', () => {
edges = ORIG_EDGES.map(e => [...e]);
rewireCount = 0;
redraw(null);
setStatus('Ready — click Rewire to start.', 'info');
});
// Initial render on page load
redraw(null);
setStatus('Ready — click Rewire to start.', 'info');
<!-- Degree-preserving rewiring demo: two canvases (original vs rewired) -->
<p class="hint">Each node shows its <b>degree</b> (edge count). Click <b>Rewire once</b> to swap two edges at random — every degree stays the same. Compare the rewired network (right) with the original (left).</p>
<div class="panels">
<div class="panel">
<div class="panel-label">Original</div>
<canvas id="cvOrig" width="220" height="200"></canvas>
</div>
<div class="panel">
<div class="panel-label">Rewired</div>
<canvas id="cvCurr" width="220" height="200"></canvas>
</div>
</div>
<div class="status" id="status">Ready — click Rewire to start.</div>
<div class="btns">
<button id="btnOne" type="button">Rewire once</button>
<button id="btnMany" type="button">Rewire ×10 edges</button>
<button id="btnReset" type="button" class="ghost">Reset</button>
</div>
/* Layout: side-by-side canvas panels with status bar and buttons */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .88rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
.panels { display: flex; gap: 1rem; flex-wrap: wrap; margin-bottom: .5rem; }
.panel { display: flex; flex-direction: column; align-items: center; }
.panel-label { font-size: .78rem; font-weight: 600; color: #555; margin-bottom: .25rem; letter-spacing: .04em; text-transform: uppercase; }
canvas { border: 1px solid #cdd9e3; border-radius: 8px; background: #f5f8fb; }
.status { font-size: .95rem; font-weight: 600; margin: .4rem 0 .6rem; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d5a9e; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 13px system-ui, sans-serif; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Degree-preserving rewiring: pick two edges (a-b) and (c-d),
// swap to (a-d)(b-c) or (a-c)(b-d) if no self-loop or multi-edge results.
// Node positions (x,y) for a 6-node graph on a 220x200 canvas
const NODES = [
{ x: 110, y: 30 },
{ x: 200, y: 90 },
{ x: 175, y: 185 },
{ x: 60, y: 185 },
{ x: 30, y: 90 },
{ x: 110, y: 110 }
];
// Initial edge list — 8 edges, degrees: [2,3,2,3,3,3]
const ORIG_EDGES = [[0,1],[0,4],[1,2],[1,5],[2,3],[3,4],[3,5],[4,5]];
let edges = ORIG_EDGES.map(e => [...e]); // working copy that gets rewired
let rewireCount = 0;
// Compute degree of each node from edge list
function degrees(edgeList) {
const d = new Array(NODES.length).fill(0);
for (const [u, v] of edgeList) { d[u]++; d[v]++; }
return d;
}
const origDeg = degrees(ORIG_EDGES);
// Draw graph on canvas: edges first, then node circles with degree labels
function drawGraph(canvas, edgeList, highlight) {
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
ctx.clearRect(0, 0, W, H);
// Draw each edge; highlight recently swapped edges in red
for (let i = 0; i < edgeList.length; i++) {
const [u, v] = edgeList[i];
const isHl = highlight && (highlight[0] === i || highlight[1] === i);
ctx.beginPath();
ctx.moveTo(NODES[u].x, NODES[u].y);
ctx.lineTo(NODES[v].x, NODES[v].y);
ctx.strokeStyle = isHl ? '#e63946' : '#8fa8c0';
ctx.lineWidth = isHl ? 2.5 : 1.5;
ctx.stroke();
}
// Draw node circles with degree numbers inside
const deg = degrees(edgeList);
for (let i = 0; i < NODES.length; i++) {
const { x, y } = NODES[i];
ctx.beginPath();
ctx.arc(x, y, 16, 0, 2 * Math.PI);
ctx.fillStyle = '#1d3557';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
ctx.fillStyle = '#fff';
ctx.font = 'bold 13px system-ui, sans-serif';
ctx.textAlign = 'center';
ctx.textBaseline = 'middle';
ctx.fillText(deg[i], x, y);
}
}
// Attempt one degree-preserving rewiring step; return swapped indices or null
function tryRewire() {
const m = edges.length;
// Pick two distinct edge indices
const i = Math.floor(Math.random() * m);
let j = Math.floor(Math.random() * (m - 1));
if (j >= i) j++;
const [a, b] = edges[i];
const [c, d] = edges[j];
// Build current edge set string (for fast lookup), remove the two candidate edges
const newEdgeSet = new Set(edges.map(([u, v]) => u + ',' + v).concat(edges.map(([u, v]) => v + ',' + u)));
newEdgeSet.delete(a + ',' + b); newEdgeSet.delete(b + ',' + a);
newEdgeSet.delete(c + ',' + d); newEdgeSet.delete(d + ',' + c);
// Check swap variant 1: (a,d) and (b,c) — no self-loop, no multi-edge
const ok1 = a !== d && b !== c && !newEdgeSet.has(a + ',' + d) && !newEdgeSet.has(d + ',' + a)
&& !newEdgeSet.has(b + ',' + c) && !newEdgeSet.has(c + ',' + b);
if (ok1) {
edges[i] = [a, d];
edges[j] = [b, c];
rewireCount++;
return [i, j];
}
// Check swap variant 2: (a,c) and (b,d)
const ok2 = a !== c && b !== d && !newEdgeSet.has(a + ',' + c) && !newEdgeSet.has(c + ',' + a)
&& !newEdgeSet.has(b + ',' + d) && !newEdgeSet.has(d + ',' + b);
if (ok2) {
edges[i] = [a, c];
edges[j] = [b, d];
rewireCount++;
return [i, j];
}
return null; // swap rejected — try again
}
function setStatus(msg, cls) {
const el = document.getElementById('status');
el.textContent = msg;
el.className = 'status ' + cls;
}
function redraw(highlight) {
drawGraph(document.getElementById('cvOrig'), ORIG_EDGES, null);
drawGraph(document.getElementById('cvCurr'), edges, highlight);
}
document.getElementById('btnOne').addEventListener('click', () => {
// Attempt up to 20 times to find a valid rewiring
let hl = null;
for (let attempt = 0; attempt < 20; attempt++) {
hl = tryRewire();
if (hl) break;
}
redraw(hl);
setStatus(i18nFmt('Rewired %n% time(s) — degrees unchanged.', { n: rewireCount }), 'ok');
});
document.getElementById('btnMany').addEventListener('click', () => {
// Perform 10*m successful rewirings (standard Q*m heuristic)
const target = edges.length * 10;
let done = 0;
for (let attempt = 0; attempt < target * 5 && done < target; attempt++) {
if (tryRewire()) done++;
}
redraw(null);
setStatus(i18nFmt('Rewired %n% time(s) — degrees unchanged.', { n: rewireCount }), 'ok');
});
document.getElementById('btnReset').addEventListener('click', () => {
edges = ORIG_EDGES.map(e => [...e]);
rewireCount = 0;
redraw(null);
setStatus('Ready — click Rewire to start.', 'info');
});
// Initial render on page load
redraw(null);
setStatus('Ready — click Rewire to start.', 'info');
After many rewirings the edge arrangement looks completely different, but the degree of every node is identical to the original. That is the whole point: the degree sequence is an invariant of the rewiring process. Compare the rewired network to the original to spot structure that is genuinely non-random.
Comments
Loading comments...