<!-- Streaming graph sketch demo: one-pass connectivity over insertions and deletions -->
<p class="hint">Edges arrive one at a time — some are inserted (+), one is deleted (−). Click <b>Next edge</b> to process each event. The sketch tracks which nodes share a component, shown by color. No full edge list is stored.</p>
<div class="stream-header">
<span class="stream-label">Edge stream</span>
<span class="edge-counter" id="edge-counter">0 / 7 events</span>
</div>
<div id="event-list" class="event-list"></div>
<div id="canvas-wrap" class="canvas-wrap">
<canvas id="graph-canvas" width="340" height="160"></canvas>
</div>
<div class="status" id="status">Press "Next edge" to start the stream.</div>
<div class="btns">
<button id="btn-next" type="button">Next edge</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Visual styles for the streaming graph sketch demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.stream-header { display: flex; align-items: baseline; gap: .5rem; margin-bottom: .3rem; }
.stream-label { font-size: .75rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .06em; color: #5a7088; }
.edge-counter { font-size: .75rem; color: #888; }
.event-list { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: .5rem; min-height: 28px; }
.event-pill { font-size: .78rem; font-family: ui-monospace, monospace;
padding: 2px 7px; border-radius: 12px; border: 1px solid #cdd9e3;
background: #e8eef3; color: #1d3557; transition: background .2s; }
.event-pill.insert { background: #d4edda; border-color: #8fc9a0; color: #155724; }
.event-pill.delete { background: #fde8e8; border-color: #e6a0a0; color: #721c24; }
.event-pill.active { outline: 2px solid #1d3557; outline-offset: 1px; }
.canvas-wrap { background: #f5f7fa; border: 1px solid #cdd9e3; border-radius: 8px;
margin-bottom: .5rem; overflow: hidden; }
canvas { display: block; width: 100%; }
.status { font-size: .95rem; font-weight: 600; margin: .3rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.status.warn { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .42rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: not-allowed; }
// Streaming graph sketch: union-find connectivity over a dynamic edge stream
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Fixed set of vertices in the graph
const NODES = ['A','B','C','D','E','F'];
const N = NODES.length;
// The edge stream: a sequence of insertion and deletion events
// Deletions are valid only for currently live edges
const STREAM = [
{ type: 'insert', u: 'A', v: 'B' },
{ type: 'insert', u: 'C', v: 'D' },
{ type: 'insert', u: 'B', v: 'C' },
{ type: 'insert', u: 'E', v: 'F' },
{ type: 'delete', u: 'B', v: 'C' },
{ type: 'insert', u: 'D', v: 'E' },
{ type: 'insert', u: 'A', v: 'F' },
];
// One distinct color per connected component
const COMP_COLORS = ['#3a7bd5','#e63946','#2a9d8f','#e9c46a','#f4a261','#9b5de5'];
// Union-find (disjoint-set) structure — the core of the insertion-only sketch
function makeUF() {
const parent = {}, rank = {};
for (const n of NODES) { parent[n] = n; rank[n] = 0; }
return {
find(x) {
// Path-halving find
while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; }
return x;
},
union(a, b) {
// Union by rank
const ra = this.find(a), rb = this.find(b);
if (ra === rb) return false;
if (rank[ra] < rank[rb]) parent[ra] = rb;
else if (rank[ra] > rank[rb]) parent[rb] = ra;
else { parent[rb] = ra; rank[ra]++; }
return true;
},
components() {
// Collect all components as lists of node names
const map = {};
for (const n of NODES) {
const r = this.find(n);
if (!map[r]) map[r] = [];
map[r].push(n);
}
return Object.values(map);
}
};
}
// Fixed pixel positions for each node on the 340x160 canvas
const POS = {
A: [50, 80], B: [120, 40], C: [190, 80],
D: [190, 130], E: [265, 55], F: [265, 130]
};
// Global state: union-find, stream step index, live edges
let uf = makeUF();
let step = 0;
// The set of edges currently in the graph
let liveEdges = [];
function getNodeColor(node) {
// Map each node to its component's color
const comps = uf.components();
for (let i = 0; i < comps.length; i++) {
if (comps[i].includes(node)) return COMP_COLORS[i % COMP_COLORS.length];
}
return '#888';
}
function draw() {
const canvas = document.getElementById('graph-canvas');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw live edges first (behind nodes)
ctx.lineWidth = 2;
ctx.strokeStyle = '#aaa';
for (const { u, v } of liveEdges) {
const [x1, y1] = POS[u], [x2, y2] = POS[v];
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
}
// Draw each node as a colored circle with its label
for (const node of NODES) {
const [x, y] = POS[node];
const color = getNodeColor(node);
ctx.beginPath(); ctx.arc(x, y, 16, 0, 2 * Math.PI);
ctx.fillStyle = color; 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(node, x, y);
}
}
function renderPills() {
// Render the stream as pills; highlight the most recently processed event
const el = document.getElementById('event-list');
el.innerHTML = '';
STREAM.forEach((ev, i) => {
const pill = document.createElement('span');
const sign = ev.type === 'insert' ? '+' : '-';
pill.className = 'event-pill ' + ev.type + (i === step - 1 ? ' active' : '');
pill.textContent = sign + ev.u + '–' + ev.v;
el.appendChild(pill);
});
}
function setStatus(text, cls) {
const el = document.getElementById('status');
el.textContent = text; el.className = 'status ' + cls;
}
function updateCounter() {
document.getElementById('edge-counter').textContent =
i18nFmt('%n% / %total% events', { n: step, total: STREAM.length });
}
function applyStep() {
if (step >= STREAM.length) return;
const ev = STREAM[step];
step++;
if (ev.type === 'insert') {
// Record the new edge and merge the two components
liveEdges.push({ u: ev.u, v: ev.v });
uf.union(ev.u, ev.v);
const comps = uf.components();
setStatus(
i18nFmt('Inserted %u%–%v%. Graph has %k% component(s).', { u: ev.u, v: ev.v, k: comps.length }),
'ok'
);
} else {
// Remove the deleted edge from the live set, then rebuild union-find from scratch
liveEdges = liveEdges.filter(e => !(e.u === ev.u && e.v === ev.v) && !(e.u === ev.v && e.v === ev.u));
// Full rebuild simulates the recovery step of a linear sketch after a deletion
uf = makeUF();
for (const e of liveEdges) uf.union(e.u, e.v);
const comps = uf.components();
setStatus(
i18nFmt('Deleted %u%–%v%. Sketch rebuilt: %k% component(s).', { u: ev.u, v: ev.v, k: comps.length }),
'warn'
);
}
if (step >= STREAM.length) {
document.getElementById('btn-next').disabled = true;
const comps = uf.components();
setStatus(i18nFmt('Stream finished. Final graph: %k% connected component(s).', { k: comps.length }), 'info');
}
renderPills();
updateCounter();
draw();
}
function reset() {
// Reset all state to the initial empty graph
uf = makeUF();
step = 0;
liveEdges = [];
document.getElementById('btn-next').disabled = false;
renderPills();
updateCounter();
draw();
setStatus('Press "Next edge" to start the stream.', '');
}
document.getElementById('btn-next').addEventListener('click', applyStep);
document.getElementById('btn-reset').addEventListener('click', reset);
// Initialize the demo on page load
reset();
<!-- Streaming graph sketch demo: one-pass connectivity over insertions and deletions -->
<p class="hint">Edges arrive one at a time — some are inserted (+), one is deleted (−). Click <b>Next edge</b> to process each event. The sketch tracks which nodes share a component, shown by color. No full edge list is stored.</p>
<div class="stream-header">
<span class="stream-label">Edge stream</span>
<span class="edge-counter" id="edge-counter">0 / 7 events</span>
</div>
<div id="event-list" class="event-list"></div>
<div id="canvas-wrap" class="canvas-wrap">
<canvas id="graph-canvas" width="340" height="160"></canvas>
</div>
<div class="status" id="status">Press "Next edge" to start the stream.</div>
<div class="btns">
<button id="btn-next" type="button">Next edge</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
</div>
/* Visual styles for the streaming graph sketch demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; }
.hint { font-size: .85rem; color: #444; margin: 0 0 .6rem; line-height: 1.45; }
.stream-header { display: flex; align-items: baseline; gap: .5rem; margin-bottom: .3rem; }
.stream-label { font-size: .75rem; font-weight: 700; text-transform: uppercase;
letter-spacing: .06em; color: #5a7088; }
.edge-counter { font-size: .75rem; color: #888; }
.event-list { display: flex; flex-wrap: wrap; gap: 4px; margin-bottom: .5rem; min-height: 28px; }
.event-pill { font-size: .78rem; font-family: ui-monospace, monospace;
padding: 2px 7px; border-radius: 12px; border: 1px solid #cdd9e3;
background: #e8eef3; color: #1d3557; transition: background .2s; }
.event-pill.insert { background: #d4edda; border-color: #8fc9a0; color: #155724; }
.event-pill.delete { background: #fde8e8; border-color: #e6a0a0; color: #721c24; }
.event-pill.active { outline: 2px solid #1d3557; outline-offset: 1px; }
.canvas-wrap { background: #f5f7fa; border: 1px solid #cdd9e3; border-radius: 8px;
margin-bottom: .5rem; overflow: hidden; }
canvas { display: block; width: 100%; }
.status { font-size: .95rem; font-weight: 600; margin: .3rem 0; min-height: 1.4em; }
.status.ok { color: #0a7d33; }
.status.info { color: #1d3557; }
.status.warn { color: #c92f3c; }
.btns { display: flex; gap: .5rem; flex-wrap: wrap; }
button { font: 600 14px system-ui, sans-serif; padding: .42rem .85rem;
border: 1px solid #1d3557; background: #1d3557; color: #fff;
border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .4; cursor: not-allowed; }
// Streaming graph sketch: union-find connectivity over a dynamic edge stream
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Fixed set of vertices in the graph
const NODES = ['A','B','C','D','E','F'];
const N = NODES.length;
// The edge stream: a sequence of insertion and deletion events
// Deletions are valid only for currently live edges
const STREAM = [
{ type: 'insert', u: 'A', v: 'B' },
{ type: 'insert', u: 'C', v: 'D' },
{ type: 'insert', u: 'B', v: 'C' },
{ type: 'insert', u: 'E', v: 'F' },
{ type: 'delete', u: 'B', v: 'C' },
{ type: 'insert', u: 'D', v: 'E' },
{ type: 'insert', u: 'A', v: 'F' },
];
// One distinct color per connected component
const COMP_COLORS = ['#3a7bd5','#e63946','#2a9d8f','#e9c46a','#f4a261','#9b5de5'];
// Union-find (disjoint-set) structure — the core of the insertion-only sketch
function makeUF() {
const parent = {}, rank = {};
for (const n of NODES) { parent[n] = n; rank[n] = 0; }
return {
find(x) {
// Path-halving find
while (parent[x] !== x) { parent[x] = parent[parent[x]]; x = parent[x]; }
return x;
},
union(a, b) {
// Union by rank
const ra = this.find(a), rb = this.find(b);
if (ra === rb) return false;
if (rank[ra] < rank[rb]) parent[ra] = rb;
else if (rank[ra] > rank[rb]) parent[rb] = ra;
else { parent[rb] = ra; rank[ra]++; }
return true;
},
components() {
// Collect all components as lists of node names
const map = {};
for (const n of NODES) {
const r = this.find(n);
if (!map[r]) map[r] = [];
map[r].push(n);
}
return Object.values(map);
}
};
}
// Fixed pixel positions for each node on the 340x160 canvas
const POS = {
A: [50, 80], B: [120, 40], C: [190, 80],
D: [190, 130], E: [265, 55], F: [265, 130]
};
// Global state: union-find, stream step index, live edges
let uf = makeUF();
let step = 0;
// The set of edges currently in the graph
let liveEdges = [];
function getNodeColor(node) {
// Map each node to its component's color
const comps = uf.components();
for (let i = 0; i < comps.length; i++) {
if (comps[i].includes(node)) return COMP_COLORS[i % COMP_COLORS.length];
}
return '#888';
}
function draw() {
const canvas = document.getElementById('graph-canvas');
const ctx = canvas.getContext('2d');
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw live edges first (behind nodes)
ctx.lineWidth = 2;
ctx.strokeStyle = '#aaa';
for (const { u, v } of liveEdges) {
const [x1, y1] = POS[u], [x2, y2] = POS[v];
ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke();
}
// Draw each node as a colored circle with its label
for (const node of NODES) {
const [x, y] = POS[node];
const color = getNodeColor(node);
ctx.beginPath(); ctx.arc(x, y, 16, 0, 2 * Math.PI);
ctx.fillStyle = color; 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(node, x, y);
}
}
function renderPills() {
// Render the stream as pills; highlight the most recently processed event
const el = document.getElementById('event-list');
el.innerHTML = '';
STREAM.forEach((ev, i) => {
const pill = document.createElement('span');
const sign = ev.type === 'insert' ? '+' : '-';
pill.className = 'event-pill ' + ev.type + (i === step - 1 ? ' active' : '');
pill.textContent = sign + ev.u + '–' + ev.v;
el.appendChild(pill);
});
}
function setStatus(text, cls) {
const el = document.getElementById('status');
el.textContent = text; el.className = 'status ' + cls;
}
function updateCounter() {
document.getElementById('edge-counter').textContent =
i18nFmt('%n% / %total% events', { n: step, total: STREAM.length });
}
function applyStep() {
if (step >= STREAM.length) return;
const ev = STREAM[step];
step++;
if (ev.type === 'insert') {
// Record the new edge and merge the two components
liveEdges.push({ u: ev.u, v: ev.v });
uf.union(ev.u, ev.v);
const comps = uf.components();
setStatus(
i18nFmt('Inserted %u%–%v%. Graph has %k% component(s).', { u: ev.u, v: ev.v, k: comps.length }),
'ok'
);
} else {
// Remove the deleted edge from the live set, then rebuild union-find from scratch
liveEdges = liveEdges.filter(e => !(e.u === ev.u && e.v === ev.v) && !(e.u === ev.v && e.v === ev.u));
// Full rebuild simulates the recovery step of a linear sketch after a deletion
uf = makeUF();
for (const e of liveEdges) uf.union(e.u, e.v);
const comps = uf.components();
setStatus(
i18nFmt('Deleted %u%–%v%. Sketch rebuilt: %k% component(s).', { u: ev.u, v: ev.v, k: comps.length }),
'warn'
);
}
if (step >= STREAM.length) {
document.getElementById('btn-next').disabled = true;
const comps = uf.components();
setStatus(i18nFmt('Stream finished. Final graph: %k% connected component(s).', { k: comps.length }), 'info');
}
renderPills();
updateCounter();
draw();
}
function reset() {
// Reset all state to the initial empty graph
uf = makeUF();
step = 0;
liveEdges = [];
document.getElementById('btn-next').disabled = false;
renderPills();
updateCounter();
draw();
setStatus('Press "Next edge" to start the stream.', '');
}
document.getElementById('btn-next').addEventListener('click', applyStep);
document.getElementById('btn-reset').addEventListener('click', reset);
// Initialize the demo on page load
reset();
Notice that the sketch never stores the full edge list — it only tracks which nodes are in the same component. When deletions arrive the sketch must work harder, but a proper linear sketch (based on random ℓ0 sampling) still handles the fully dynamic case in O(nlog2n) bits — far less than the O(n2) needed to store all edges explicitly.
Comments
Loading comments...