Below is a small weighted graph with eight nodes. Node A is the source. Click Step to advance Dijkstra's algorithm one edge relaxation at a time, or Run to watch it finish automatically.
<p class="hint">Node <b>A</b> is the source. Each edge shows its weight. Click <b>Step</b> to relax one edge at a time โ the algorithm always picks the unvisited node with the smallest tentative distance. Green nodes are <b>settled</b> (distance final).</p>
<canvas id="canvas" width="480" height="300"></canvas>
<div class="info">
<div id="queue-box" class="queue-box"><span class="label">Priority queue:</span> <span id="queue-txt">โ</span></div>
<div id="msg" class="msg">Press Step or Run to start Dijkstra from A.</div>
</div>
<div class="btns">
<button id="step" type="button">Step</button>
<button id="run" type="button">Run</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.45; }
canvas { display: block; background: #f5f7fa; border: 1px solid #d0d7de; border-radius: 10px; max-width: 100%; }
.info { margin: .5rem 0; }
.queue-box { font-size: .85rem; color: #333; margin-bottom: .25rem; }
.label { font-weight: 600; }
.msg { font-size: .95rem; font-weight: 600; min-height: 1.4em; }
.msg.ok { color: #0a7d33; }
.msg.relax { color: #1d3557; }
.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] ?? ''));
// Dijkstra's algorithm โ step-by-step visualization on a small weighted graph.
// Nodes: A-H placed on a canvas. Edges have non-negative integer weights.
// The algorithm uses a min-heap (simulated with a sorted array for clarity).
// Once a node is "settled" (extracted from the queue) its distance is final โ
// that is the core invariant that makes Dijkstra correct for non-negative weights.
const NODES = {
A: { x: 60, y: 150, label: 'A' },
B: { x: 160, y: 60, label: 'B' },
C: { x: 160, y: 240, label: 'C' },
D: { x: 260, y: 150, label: 'D' },
E: { x: 260, y: 50, label: 'E' },
F: { x: 260, y: 250, label: 'F' },
G: { x: 380, y: 100, label: 'G' },
H: { x: 380, y: 220, label: 'H' }
};
const EDGES = [
['A','B',4], ['A','C',2],
['B','D',5], ['B','E',1],
['C','D',8], ['C','F',10],
['D','G',2], ['D','H',6],
['E','G',3],
['F','H',4],
['G','H',1]
];
// Build adjacency list
const adj = {};
Object.keys(NODES).forEach(n => adj[n] = []);
EDGES.forEach(([u, v, w]) => { adj[u].push([v, w]); adj[v].push([u, w]); });
const INF = Infinity;
let dist, prev, settled, queue, relaxLog, stepIdx, done;
function initAlgo() {
dist = {}; prev = {}; settled = new Set(); queue = []; relaxLog = []; stepIdx = 0; done = false;
Object.keys(NODES).forEach(n => { dist[n] = INF; prev[n] = null; });
dist['A'] = 0;
queue.push([0, 'A']);
}
// Pre-compute all steps so we can replay them
function precompute() {
// work on clones
const d = {...dist}; const s = new Set(); const q = [[0,'A']];
const log = [];
while (q.length) {
q.sort((a,b) => a[0]-b[0]);
const [du, u] = q.shift();
if (s.has(u)) continue;
s.add(u);
log.push({ type: 'settle', node: u, dist: du, queue: q.map(x=>x[1]+':'+x[0]).join(', ') });
for (const [v, w] of adj[u]) {
if (s.has(v)) continue;
const nd = du + w;
if (nd < d[v]) {
d[v] = nd;
prev[v] = u;
q.push([nd, v]);
log.push({ type: 'relax', from: u, to: v, newDist: nd, queue: q.map(x=>x[1]+':'+x[0]).join(', ') });
}
}
}
return { log, finalDist: d, finalPrev: prev };
}
let allSteps, finalDist, finalPrev;
function reset() {
initAlgo();
const r = precompute();
allSteps = r.log; finalDist = r.finalDist; finalPrev = r.finalPrev;
stepIdx = 0; done = false;
// reset visual state
dist = {}; prev = {}; settled = new Set();
Object.keys(NODES).forEach(n => { dist[n] = INF; prev[n] = null; });
dist['A'] = 0;
draw();
document.getElementById('msg').textContent = 'Press Step or Run to start Dijkstra from A.';
document.getElementById('msg').className = 'msg';
document.getElementById('queue-txt').textContent = 'A:0';
}
function applyStep() {
if (done || stepIdx >= allSteps.length) { done = true; return; }
const s = allSteps[stepIdx++];
if (s.type === 'settle') {
settled.add(s.node);
dist[s.node] = s.dist;
document.getElementById('queue-txt').textContent = s.queue || 'โ';
document.getElementById('msg').textContent = fmt('Settled %node% with distance %dist%.', { node: s.node, dist: s.dist });
document.getElementById('msg').className = 'msg ok';
} else {
dist[s.to] = s.newDist;
prev[s.to] = s.from;
document.getElementById('queue-txt').textContent = s.queue || 'โ';
document.getElementById('msg').textContent = fmt('Relaxed %from%โ%to% โ new dist %newDist%.', { from: s.from, to: s.to, newDist: s.newDist });
document.getElementById('msg').className = 'msg relax';
}
if (stepIdx >= allSteps.length) {
done = true;
document.getElementById('queue-txt').textContent = 'โ';
document.getElementById('msg').textContent = 'โ Done! Shortest-path tree from A complete.';
document.getElementById('msg').className = 'msg ok';
}
draw();
}
// โโ Drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
function nodeColor(n) {
if (settled.has(n)) return '#2ecc71'; // settled = green
if (dist[n] < INF) return '#f9c74f'; // reached = yellow
return '#c9ccd1'; // unreached = grey
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw settled-path tree edges in green
const treeEdges = new Set();
Object.keys(NODES).forEach(n => { if (prev[n]) treeEdges.add(prev[n]+'-'+n); });
// Edges
EDGES.forEach(([u, v, w]) => {
const a = NODES[u], b = NODES[v];
const inTree = treeEdges.has(u+'-'+v) || treeEdges.has(v+'-'+u);
ctx.beginPath();
ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y);
ctx.strokeStyle = inTree ? '#0a7d33' : '#adb5bd';
ctx.lineWidth = inTree ? 2.5 : 1.5;
ctx.stroke();
// Weight label
const mx = (a.x+b.x)/2, my = (a.y+b.y)/2;
ctx.fillStyle = '#555';
ctx.font = '12px system-ui, sans-serif';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(w, mx, my - 7);
});
// Nodes
Object.entries(NODES).forEach(([id, n]) => {
ctx.beginPath();
ctx.arc(n.x, n.y, 20, 0, Math.PI*2);
ctx.fillStyle = nodeColor(id);
ctx.fill();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 1.8;
ctx.stroke();
ctx.fillStyle = '#1d3557';
ctx.font = 'bold 13px system-ui, sans-serif';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(id, n.x, n.y);
// Distance label above node
const d = dist[id];
const lbl = (d === INF) ? 'โ' : String(d);
ctx.fillStyle = '#444';
ctx.font = '11px system-ui, sans-serif';
ctx.fillText(lbl, n.x, n.y - 28);
});
}
let running = false;
function runAll() {
if (done) return;
running = true;
function tick() {
if (done || !running) { running = false; return; }
applyStep();
setTimeout(tick, 300);
}
tick();
}
document.getElementById('step').addEventListener('click', () => { running = false; applyStep(); });
document.getElementById('run').addEventListener('click', runAll);
document.getElementById('reset').addEventListener('click', () => { running = false; reset(); });
reset();
<p class="hint">Node <b>A</b> is the source. Each edge shows its weight. Click <b>Step</b> to relax one edge at a time โ the algorithm always picks the unvisited node with the smallest tentative distance. Green nodes are <b>settled</b> (distance final).</p>
<canvas id="canvas" width="480" height="300"></canvas>
<div class="info">
<div id="queue-box" class="queue-box"><span class="label">Priority queue:</span> <span id="queue-txt">โ</span></div>
<div id="msg" class="msg">Press Step or Run to start Dijkstra from A.</div>
</div>
<div class="btns">
<button id="step" type="button">Step</button>
<button id="run" type="button">Run</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.45; }
canvas { display: block; background: #f5f7fa; border: 1px solid #d0d7de; border-radius: 10px; max-width: 100%; }
.info { margin: .5rem 0; }
.queue-box { font-size: .85rem; color: #333; margin-bottom: .25rem; }
.label { font-weight: 600; }
.msg { font-size: .95rem; font-weight: 600; min-height: 1.4em; }
.msg.ok { color: #0a7d33; }
.msg.relax { color: #1d3557; }
.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] ?? ''));
// Dijkstra's algorithm โ step-by-step visualization on a small weighted graph.
// Nodes: A-H placed on a canvas. Edges have non-negative integer weights.
// The algorithm uses a min-heap (simulated with a sorted array for clarity).
// Once a node is "settled" (extracted from the queue) its distance is final โ
// that is the core invariant that makes Dijkstra correct for non-negative weights.
const NODES = {
A: { x: 60, y: 150, label: 'A' },
B: { x: 160, y: 60, label: 'B' },
C: { x: 160, y: 240, label: 'C' },
D: { x: 260, y: 150, label: 'D' },
E: { x: 260, y: 50, label: 'E' },
F: { x: 260, y: 250, label: 'F' },
G: { x: 380, y: 100, label: 'G' },
H: { x: 380, y: 220, label: 'H' }
};
const EDGES = [
['A','B',4], ['A','C',2],
['B','D',5], ['B','E',1],
['C','D',8], ['C','F',10],
['D','G',2], ['D','H',6],
['E','G',3],
['F','H',4],
['G','H',1]
];
// Build adjacency list
const adj = {};
Object.keys(NODES).forEach(n => adj[n] = []);
EDGES.forEach(([u, v, w]) => { adj[u].push([v, w]); adj[v].push([u, w]); });
const INF = Infinity;
let dist, prev, settled, queue, relaxLog, stepIdx, done;
function initAlgo() {
dist = {}; prev = {}; settled = new Set(); queue = []; relaxLog = []; stepIdx = 0; done = false;
Object.keys(NODES).forEach(n => { dist[n] = INF; prev[n] = null; });
dist['A'] = 0;
queue.push([0, 'A']);
}
// Pre-compute all steps so we can replay them
function precompute() {
// work on clones
const d = {...dist}; const s = new Set(); const q = [[0,'A']];
const log = [];
while (q.length) {
q.sort((a,b) => a[0]-b[0]);
const [du, u] = q.shift();
if (s.has(u)) continue;
s.add(u);
log.push({ type: 'settle', node: u, dist: du, queue: q.map(x=>x[1]+':'+x[0]).join(', ') });
for (const [v, w] of adj[u]) {
if (s.has(v)) continue;
const nd = du + w;
if (nd < d[v]) {
d[v] = nd;
prev[v] = u;
q.push([nd, v]);
log.push({ type: 'relax', from: u, to: v, newDist: nd, queue: q.map(x=>x[1]+':'+x[0]).join(', ') });
}
}
}
return { log, finalDist: d, finalPrev: prev };
}
let allSteps, finalDist, finalPrev;
function reset() {
initAlgo();
const r = precompute();
allSteps = r.log; finalDist = r.finalDist; finalPrev = r.finalPrev;
stepIdx = 0; done = false;
// reset visual state
dist = {}; prev = {}; settled = new Set();
Object.keys(NODES).forEach(n => { dist[n] = INF; prev[n] = null; });
dist['A'] = 0;
draw();
document.getElementById('msg').textContent = 'Press Step or Run to start Dijkstra from A.';
document.getElementById('msg').className = 'msg';
document.getElementById('queue-txt').textContent = 'A:0';
}
function applyStep() {
if (done || stepIdx >= allSteps.length) { done = true; return; }
const s = allSteps[stepIdx++];
if (s.type === 'settle') {
settled.add(s.node);
dist[s.node] = s.dist;
document.getElementById('queue-txt').textContent = s.queue || 'โ';
document.getElementById('msg').textContent = fmt('Settled %node% with distance %dist%.', { node: s.node, dist: s.dist });
document.getElementById('msg').className = 'msg ok';
} else {
dist[s.to] = s.newDist;
prev[s.to] = s.from;
document.getElementById('queue-txt').textContent = s.queue || 'โ';
document.getElementById('msg').textContent = fmt('Relaxed %from%โ%to% โ new dist %newDist%.', { from: s.from, to: s.to, newDist: s.newDist });
document.getElementById('msg').className = 'msg relax';
}
if (stepIdx >= allSteps.length) {
done = true;
document.getElementById('queue-txt').textContent = 'โ';
document.getElementById('msg').textContent = 'โ Done! Shortest-path tree from A complete.';
document.getElementById('msg').className = 'msg ok';
}
draw();
}
// โโ Drawing โโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโโ
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
function nodeColor(n) {
if (settled.has(n)) return '#2ecc71'; // settled = green
if (dist[n] < INF) return '#f9c74f'; // reached = yellow
return '#c9ccd1'; // unreached = grey
}
function draw() {
ctx.clearRect(0, 0, canvas.width, canvas.height);
// Draw settled-path tree edges in green
const treeEdges = new Set();
Object.keys(NODES).forEach(n => { if (prev[n]) treeEdges.add(prev[n]+'-'+n); });
// Edges
EDGES.forEach(([u, v, w]) => {
const a = NODES[u], b = NODES[v];
const inTree = treeEdges.has(u+'-'+v) || treeEdges.has(v+'-'+u);
ctx.beginPath();
ctx.moveTo(a.x, a.y); ctx.lineTo(b.x, b.y);
ctx.strokeStyle = inTree ? '#0a7d33' : '#adb5bd';
ctx.lineWidth = inTree ? 2.5 : 1.5;
ctx.stroke();
// Weight label
const mx = (a.x+b.x)/2, my = (a.y+b.y)/2;
ctx.fillStyle = '#555';
ctx.font = '12px system-ui, sans-serif';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(w, mx, my - 7);
});
// Nodes
Object.entries(NODES).forEach(([id, n]) => {
ctx.beginPath();
ctx.arc(n.x, n.y, 20, 0, Math.PI*2);
ctx.fillStyle = nodeColor(id);
ctx.fill();
ctx.strokeStyle = '#1d3557';
ctx.lineWidth = 1.8;
ctx.stroke();
ctx.fillStyle = '#1d3557';
ctx.font = 'bold 13px system-ui, sans-serif';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText(id, n.x, n.y);
// Distance label above node
const d = dist[id];
const lbl = (d === INF) ? 'โ' : String(d);
ctx.fillStyle = '#444';
ctx.font = '11px system-ui, sans-serif';
ctx.fillText(lbl, n.x, n.y - 28);
});
}
let running = false;
function runAll() {
if (done) return;
running = true;
function tick() {
if (done || !running) { running = false; return; }
applyStep();
setTimeout(tick, 300);
}
tick();
}
document.getElementById('step').addEventListener('click', () => { running = false; applyStep(); });
document.getElementById('run').addEventListener('click', runAll);
document.getElementById('reset').addEventListener('click', () => { running = false; reset(); });
reset();
Comments
Loading comments...