The canvas below shows a 2D linear system — two lines whose intersection is the solution. The algorithm starts from a random point and alternately projects onto each line, bouncing between them.
<!-- Kaczmarz method demo — controls and canvas -->
<div class="controls">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
<span class="iter-label">Iteration: <span id="iter-count">0</span></span>
</div>
<canvas id="canvas" width="460" height="340"></canvas>
<div id="status" class="status"></div>
/* Styles for the Kaczmarz demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
canvas { display: block; border: 1px solid #d0d8e0; border-radius: 8px; max-width: 100%; }
.iter-label { font-size: .85rem; color: #555; margin-left: .3rem; }
.status { font-size: .9rem; font-weight: 600; margin-top: .4rem; min-height: 1.3em; color: #0a7d33; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Kaczmarz method — project alternately onto each equation's hyperplane */
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
/* Coordinate transform from math space to canvas pixels */
const PAD = 50;
const XMIN = -3, XMAX = 5, YMIN = -2, YMAX = 6;
function toScreen(x, y) {
const sx = PAD + (x - XMIN) / (XMAX - XMIN) * (W - 2 * PAD);
const sy = H - PAD - (y - YMIN) / (YMAX - YMIN) * (H - 2 * PAD);
return [sx, sy];
}
/* Linear system with unique solution at (1, 3):
Line 1: 2x + y = 5 (solution: x=1, y=3)
Line 2: x - y = -2 (solution: x=1, y=3) */
const EQS = [
{ a: 2, b: 1, c: 5 },
{ a: 1, b: -1, c: -2 }
];
const SOL = { x: 1, y: 3 };
/* Orthogonal projection of point (px,py) onto line ax+by=c */
function projectOntoLine(px, py, a, b, c) {
const denom = a * a + b * b;
const t = (c - a * px - b * py) / denom;
return { x: px + a * t, y: py + b * t };
}
/* Algorithm state — current point, step counter, path history */
let cur = { x: -2, y: 5 };
let step = 0;
let path = [{ x: cur.x, y: cur.y }];
let running = false;
let timer = null;
function drawGrid() {
ctx.clearRect(0, 0, W, H);
/* Draw light grid lines */
ctx.strokeStyle = '#dce4ec';
ctx.lineWidth = 1;
for (let xi = Math.ceil(XMIN); xi <= Math.floor(XMAX); xi++) {
const [sx] = toScreen(xi, 0);
ctx.beginPath(); ctx.moveTo(sx, PAD); ctx.lineTo(sx, H - PAD); ctx.stroke();
}
for (let yi = Math.ceil(YMIN); yi <= Math.floor(YMAX); yi++) {
const [, sy] = toScreen(0, yi);
ctx.beginPath(); ctx.moveTo(PAD, sy); ctx.lineTo(W - PAD, sy); ctx.stroke();
}
/* Draw coordinate axes */
ctx.strokeStyle = '#aab6c2';
ctx.lineWidth = 1.4;
const [ox, oy] = toScreen(0, 0);
ctx.beginPath(); ctx.moveTo(PAD, oy); ctx.lineTo(W - PAD, oy); ctx.stroke();
ctx.beginPath(); ctx.moveTo(ox, PAD); ctx.lineTo(ox, H - PAD); ctx.stroke();
}
function drawLine(eq, color, label) {
/* Draw the equation's line across the visible canvas region */
const { a, b, c } = eq;
let pts = [];
if (Math.abs(b) > 1e-9) {
pts.push([XMIN, (c - a * XMIN) / b]);
pts.push([XMAX, (c - a * XMAX) / b]);
} else {
const xv = c / a;
pts.push([xv, YMIN]);
pts.push([xv, YMAX]);
}
ctx.strokeStyle = color;
ctx.lineWidth = 2.2;
ctx.beginPath();
pts.forEach(([x, y], i) => {
const [sx, sy] = toScreen(x, y);
i === 0 ? ctx.moveTo(sx, sy) : ctx.lineTo(sx, sy);
});
ctx.stroke();
const [lx, ly] = toScreen(pts[1][0], pts[1][1]);
ctx.fillStyle = color;
ctx.font = '13px system-ui';
ctx.fillText(label, lx - 30, ly - 6);
}
function drawPath() {
/* Draw the dashed red path connecting all iterates */
if (path.length < 2) return;
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 1.6;
ctx.setLineDash([4, 3]);
ctx.beginPath();
path.forEach(({ x, y }, i) => {
const [sx, sy] = toScreen(x, y);
i === 0 ? ctx.moveTo(sx, sy) : ctx.lineTo(sx, sy);
});
ctx.stroke();
ctx.setLineDash([]);
/* Draw a dot at each iterate (larger and red for the current one) */
path.forEach(({ x, y }, i) => {
const [sx, sy] = toScreen(x, y);
ctx.beginPath();
ctx.arc(sx, sy, i === path.length - 1 ? 5 : 3, 0, 2 * Math.PI);
ctx.fillStyle = i === 0 ? '#555' : (i === path.length - 1 ? '#e63946' : '#e6888e');
ctx.fill();
});
}
function drawSolution() {
/* Draw the true solution as a green filled circle */
const [sx, sy] = toScreen(SOL.x, SOL.y);
ctx.beginPath();
ctx.arc(sx, sy, 7, 0, 2 * Math.PI);
ctx.fillStyle = '#0a7d33';
ctx.fill();
ctx.fillStyle = '#0a7d33';
ctx.font = 'bold 12px system-ui';
ctx.fillText('Solution', sx + 10, sy - 8);
}
function render() {
drawGrid();
drawLine(EQS[0], '#1d3557', '2x+y=5');
drawLine(EQS[1], '#457b9d', 'x-y=-2');
drawPath();
drawSolution();
}
function dist() {
const dx = cur.x - SOL.x, dy = cur.y - SOL.y;
return Math.sqrt(dx * dx + dy * dy);
}
function doStep() {
/* Project current point onto the next equation in round-robin order */
const eq = EQS[step % EQS.length];
const p = projectOntoLine(cur.x, cur.y, eq.a, eq.b, eq.c);
cur = p;
path.push({ x: cur.x, y: cur.y });
step++;
document.getElementById('iter-count').textContent = step;
render();
const d = dist();
if (d < 0.01) {
document.getElementById('status').textContent = i18nFmt('Converged in %n% iterations!', { n: step });
stopRun();
} else {
document.getElementById('status').textContent = i18nFmt('Distance to solution: %d%', { d: d.toFixed(4) });
}
}
function startRun() {
running = true;
document.getElementById('btn-run').textContent = 'Pause';
document.getElementById('btn-step').disabled = true;
timer = setInterval(() => {
doStep();
if (dist() < 0.01) stopRun();
}, 260);
}
function stopRun() {
running = false;
clearInterval(timer);
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('btn-step').disabled = false;
}
function resetDemo() {
/* Return to the starting point and clear the path */
cur = { x: -2, y: 5 };
step = 0;
path = [{ x: cur.x, y: cur.y }];
stopRun();
document.getElementById('iter-count').textContent = 0;
document.getElementById('status').textContent = '';
render();
}
document.getElementById('btn-step').addEventListener('click', doStep);
document.getElementById('btn-run').addEventListener('click', () => {
running ? stopRun() : startRun();
});
document.getElementById('btn-reset').addEventListener('click', resetDemo);
render();
<!-- Kaczmarz method demo — controls and canvas -->
<div class="controls">
<button id="btn-step" type="button">Step</button>
<button id="btn-run" type="button">Run</button>
<button id="btn-reset" type="button" class="ghost">Reset</button>
<span class="iter-label">Iteration: <span id="iter-count">0</span></span>
</div>
<canvas id="canvas" width="460" height="340"></canvas>
<div id="status" class="status"></div>
/* Styles for the Kaczmarz demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; align-items: center; gap: .5rem; flex-wrap: wrap; margin-bottom: .5rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button.ghost { background: #fff; color: #1d3557; }
button:disabled { opacity: .45; cursor: default; }
canvas { display: block; border: 1px solid #d0d8e0; border-radius: 8px; max-width: 100%; }
.iter-label { font-size: .85rem; color: #555; margin-left: .3rem; }
.status { font-size: .9rem; font-weight: 600; margin-top: .4rem; min-height: 1.3em; color: #0a7d33; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
/* Kaczmarz method — project alternately onto each equation's hyperplane */
const canvas = document.getElementById('canvas');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
/* Coordinate transform from math space to canvas pixels */
const PAD = 50;
const XMIN = -3, XMAX = 5, YMIN = -2, YMAX = 6;
function toScreen(x, y) {
const sx = PAD + (x - XMIN) / (XMAX - XMIN) * (W - 2 * PAD);
const sy = H - PAD - (y - YMIN) / (YMAX - YMIN) * (H - 2 * PAD);
return [sx, sy];
}
/* Linear system with unique solution at (1, 3):
Line 1: 2x + y = 5 (solution: x=1, y=3)
Line 2: x - y = -2 (solution: x=1, y=3) */
const EQS = [
{ a: 2, b: 1, c: 5 },
{ a: 1, b: -1, c: -2 }
];
const SOL = { x: 1, y: 3 };
/* Orthogonal projection of point (px,py) onto line ax+by=c */
function projectOntoLine(px, py, a, b, c) {
const denom = a * a + b * b;
const t = (c - a * px - b * py) / denom;
return { x: px + a * t, y: py + b * t };
}
/* Algorithm state — current point, step counter, path history */
let cur = { x: -2, y: 5 };
let step = 0;
let path = [{ x: cur.x, y: cur.y }];
let running = false;
let timer = null;
function drawGrid() {
ctx.clearRect(0, 0, W, H);
/* Draw light grid lines */
ctx.strokeStyle = '#dce4ec';
ctx.lineWidth = 1;
for (let xi = Math.ceil(XMIN); xi <= Math.floor(XMAX); xi++) {
const [sx] = toScreen(xi, 0);
ctx.beginPath(); ctx.moveTo(sx, PAD); ctx.lineTo(sx, H - PAD); ctx.stroke();
}
for (let yi = Math.ceil(YMIN); yi <= Math.floor(YMAX); yi++) {
const [, sy] = toScreen(0, yi);
ctx.beginPath(); ctx.moveTo(PAD, sy); ctx.lineTo(W - PAD, sy); ctx.stroke();
}
/* Draw coordinate axes */
ctx.strokeStyle = '#aab6c2';
ctx.lineWidth = 1.4;
const [ox, oy] = toScreen(0, 0);
ctx.beginPath(); ctx.moveTo(PAD, oy); ctx.lineTo(W - PAD, oy); ctx.stroke();
ctx.beginPath(); ctx.moveTo(ox, PAD); ctx.lineTo(ox, H - PAD); ctx.stroke();
}
function drawLine(eq, color, label) {
/* Draw the equation's line across the visible canvas region */
const { a, b, c } = eq;
let pts = [];
if (Math.abs(b) > 1e-9) {
pts.push([XMIN, (c - a * XMIN) / b]);
pts.push([XMAX, (c - a * XMAX) / b]);
} else {
const xv = c / a;
pts.push([xv, YMIN]);
pts.push([xv, YMAX]);
}
ctx.strokeStyle = color;
ctx.lineWidth = 2.2;
ctx.beginPath();
pts.forEach(([x, y], i) => {
const [sx, sy] = toScreen(x, y);
i === 0 ? ctx.moveTo(sx, sy) : ctx.lineTo(sx, sy);
});
ctx.stroke();
const [lx, ly] = toScreen(pts[1][0], pts[1][1]);
ctx.fillStyle = color;
ctx.font = '13px system-ui';
ctx.fillText(label, lx - 30, ly - 6);
}
function drawPath() {
/* Draw the dashed red path connecting all iterates */
if (path.length < 2) return;
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 1.6;
ctx.setLineDash([4, 3]);
ctx.beginPath();
path.forEach(({ x, y }, i) => {
const [sx, sy] = toScreen(x, y);
i === 0 ? ctx.moveTo(sx, sy) : ctx.lineTo(sx, sy);
});
ctx.stroke();
ctx.setLineDash([]);
/* Draw a dot at each iterate (larger and red for the current one) */
path.forEach(({ x, y }, i) => {
const [sx, sy] = toScreen(x, y);
ctx.beginPath();
ctx.arc(sx, sy, i === path.length - 1 ? 5 : 3, 0, 2 * Math.PI);
ctx.fillStyle = i === 0 ? '#555' : (i === path.length - 1 ? '#e63946' : '#e6888e');
ctx.fill();
});
}
function drawSolution() {
/* Draw the true solution as a green filled circle */
const [sx, sy] = toScreen(SOL.x, SOL.y);
ctx.beginPath();
ctx.arc(sx, sy, 7, 0, 2 * Math.PI);
ctx.fillStyle = '#0a7d33';
ctx.fill();
ctx.fillStyle = '#0a7d33';
ctx.font = 'bold 12px system-ui';
ctx.fillText('Solution', sx + 10, sy - 8);
}
function render() {
drawGrid();
drawLine(EQS[0], '#1d3557', '2x+y=5');
drawLine(EQS[1], '#457b9d', 'x-y=-2');
drawPath();
drawSolution();
}
function dist() {
const dx = cur.x - SOL.x, dy = cur.y - SOL.y;
return Math.sqrt(dx * dx + dy * dy);
}
function doStep() {
/* Project current point onto the next equation in round-robin order */
const eq = EQS[step % EQS.length];
const p = projectOntoLine(cur.x, cur.y, eq.a, eq.b, eq.c);
cur = p;
path.push({ x: cur.x, y: cur.y });
step++;
document.getElementById('iter-count').textContent = step;
render();
const d = dist();
if (d < 0.01) {
document.getElementById('status').textContent = i18nFmt('Converged in %n% iterations!', { n: step });
stopRun();
} else {
document.getElementById('status').textContent = i18nFmt('Distance to solution: %d%', { d: d.toFixed(4) });
}
}
function startRun() {
running = true;
document.getElementById('btn-run').textContent = 'Pause';
document.getElementById('btn-step').disabled = true;
timer = setInterval(() => {
doStep();
if (dist() < 0.01) stopRun();
}, 260);
}
function stopRun() {
running = false;
clearInterval(timer);
document.getElementById('btn-run').textContent = 'Run';
document.getElementById('btn-step').disabled = false;
}
function resetDemo() {
/* Return to the starting point and clear the path */
cur = { x: -2, y: 5 };
step = 0;
path = [{ x: cur.x, y: cur.y }];
stopRun();
document.getElementById('iter-count').textContent = 0;
document.getElementById('status').textContent = '';
render();
}
document.getElementById('btn-step').addEventListener('click', doStep);
document.getElementById('btn-run').addEventListener('click', () => {
running ? stopRun() : startRun();
});
document.getElementById('btn-reset').addEventListener('click', resetDemo);
render();
Notice how each projection lands exactly on one of the lines, satisfying that equation perfectly — but potentially violating the other. Cycling back and forth, the iterates spiral inward. For two equations the path is a classic zigzag; add more equations and the bouncing becomes a spiral that still converges to the same point.
Comments
Loading comments...