Below is a small set of points on a plane. Drag any of them and watch the curve instantly recompute — it always passes through every point, exactly, because that is what the Lagrange formula guarantees.
<p class="hint">Drag any point and the curve redraws itself instantly. It always passes through every point exactly — that is what the Lagrange formula guarantees. Add or remove points to change the polynomial's degree.</p>
<canvas id="plot" width="560" height="320"></canvas>
<div class="status" id="status">Drag a point to begin.</div>
<div class="btns">
<button id="addpt" type="button">Add point</button>
<button id="delpt" type="button">Remove point</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: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
canvas { width: 100%; max-width: 560px; height: auto; background: #f7f9fb; border: 1px solid #cdd9e3;
border-radius: 8px; touch-action: none; cursor: grab; display: block; }
canvas.dragging { cursor: grabbing; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; 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; }
button:disabled { opacity: .5; cursor: not-allowed; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Lagrange interpolation demo: recompute and redraw the unique polynomial through every draggable point on each move.
const canvas = document.getElementById('plot');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = 30;
// Map data coordinates (xMin..xMax, yMin..yMax) to pixel coordinates and back.
const xMin = -5, xMax = 5, yMin = -5, yMax = 5;
const toPx = (x, y) => [
PAD + (x - xMin) / (xMax - xMin) * (W - 2 * PAD),
H - PAD - (y - yMin) / (yMax - yMin) * (H - 2 * PAD)
];
const toData = (px, py) => [
xMin + (px - PAD) / (W - 2 * PAD) * (xMax - xMin),
yMin + (H - PAD - py) / (H - 2 * PAD) * (yMax - yMin)
];
// The draggable points; index of the point currently being dragged (-1 = none).
let points = [[-3, -2], [-1, 2], [1, -1], [3, 3]];
let dragIndex = -1;
// P(x) = sum_i y_i * prod_{j != i} (x - x_j) / (x_i - x_j) — the Lagrange formula itself.
function lagrangeAt(pts, x) {
let total = 0;
for (let i = 0; i < pts.length; i++) {
const [xi, yi] = pts[i];
let term = yi;
for (let j = 0; j < pts.length; j++) {
if (j === i) continue;
const xj = pts[j][0];
term *= (x - xj) / (xi - xj);
}
total += term;
}
return total;
}
function sortedPoints() {
return [...points].sort((a, b) => a[0] - b[0]);
}
function draw() {
ctx.clearRect(0, 0, W, H);
// Draw the x and y axes through the origin.
ctx.strokeStyle = '#cdd9e3';
ctx.lineWidth = 1;
const [ox, oy] = toPx(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();
const pts = sortedPoints();
// Sample the interpolating polynomial densely and draw it as a smooth curve.
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 2.5;
ctx.beginPath();
const steps = 240;
for (let s = 0; s <= steps; s++) {
const x = xMin + (xMax - xMin) * s / steps;
let y = lagrangeAt(pts, x);
y = Math.max(yMin - 20, Math.min(yMax + 20, y));
const [px, py] = toPx(x, y);
if (s === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.stroke();
// Draw each control point on top of the curve.
for (const [x, y] of points) {
const [px, py] = toPx(x, y);
ctx.beginPath();
ctx.arc(px, py, 7, 0, Math.PI * 2);
ctx.fillStyle = '#1d3557';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
}
}
function setStatus(t) { document.getElementById('status').textContent = t; }
function updateStatus() {
const n = points.length;
setStatus(i18nFmt('%n% points -> a polynomial of degree up to %deg%.', { n: n, deg: n - 1 }));
}
function nearestPoint(px, py) {
let best = -1, bestDist = 16 * 16;
points.forEach(([x, y], i) => {
const [qx, qy] = toPx(x, y);
const d = (qx - px) ** 2 + (qy - py) ** 2;
if (d < bestDist) { bestDist = d; best = i; }
});
return best;
}
function eventPos(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width, scaleY = H / rect.height;
const cx = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left;
const cy = (e.touches ? e.touches[0].clientY : e.clientY) - rect.top;
return [cx * scaleX, cy * scaleY];
}
canvas.addEventListener('pointerdown', (e) => {
const [px, py] = eventPos(e);
dragIndex = nearestPoint(px, py);
if (dragIndex >= 0) canvas.classList.add('dragging');
});
window.addEventListener('pointermove', (e) => {
if (dragIndex < 0) return;
const [px, py] = eventPos(e);
let [x, y] = toData(px, py);
x = Math.max(xMin, Math.min(xMax, x));
y = Math.max(yMin, Math.min(yMax, y));
// Keep x-coordinates distinct so the formula's denominators never hit zero.
const others = points.filter((_, i) => i !== dragIndex).map(p => p[0]);
const tooClose = others.some(ox => Math.abs(ox - x) < 0.15);
if (!tooClose) points[dragIndex] = [x, y];
draw();
});
window.addEventListener('pointerup', () => {
if (dragIndex >= 0) { dragIndex = -1; canvas.classList.remove('dragging'); }
});
function addPoint() {
if (points.length >= 8) return;
// Look for a free x slot half a unit apart from existing points.
const xs = points.map(p => p[0]);
let x = 0;
for (let cand = xMin + 0.5; cand <= xMax - 0.5; cand += 0.5) {
if (!xs.some(ox => Math.abs(ox - cand) < 0.4)) { x = cand; break; }
}
const y = (Math.random() * 6 - 3);
points.push([Math.round(x * 10) / 10, Math.round(y * 10) / 10]);
updateStatus();
draw();
}
function removePoint() {
if (points.length <= 2) return;
points.pop();
updateStatus();
draw();
}
function resetAll() {
points = [[-3, -2], [-1, 2], [1, -1], [3, 3]];
updateStatus();
draw();
}
document.getElementById('addpt').addEventListener('click', addPoint);
document.getElementById('delpt').addEventListener('click', removePoint);
document.getElementById('reset').addEventListener('click', resetAll);
updateStatus();
draw();
<p class="hint">Drag any point and the curve redraws itself instantly. It always passes through every point exactly — that is what the Lagrange formula guarantees. Add or remove points to change the polynomial's degree.</p>
<canvas id="plot" width="560" height="320"></canvas>
<div class="status" id="status">Drag a point to begin.</div>
<div class="btns">
<button id="addpt" type="button">Add point</button>
<button id="delpt" type="button">Remove point</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: .9rem; color: #444; margin: 0 0 .7rem; line-height: 1.45; }
canvas { width: 100%; max-width: 560px; height: auto; background: #f7f9fb; border: 1px solid #cdd9e3;
border-radius: 8px; touch-action: none; cursor: grab; display: block; }
canvas.dragging { cursor: grabbing; }
.status { font-size: .95rem; font-weight: 600; margin: .5rem 0; min-height: 1.4em; 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; }
button:disabled { opacity: .5; cursor: not-allowed; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Lagrange interpolation demo: recompute and redraw the unique polynomial through every draggable point on each move.
const canvas = document.getElementById('plot');
const ctx = canvas.getContext('2d');
const W = canvas.width, H = canvas.height;
const PAD = 30;
// Map data coordinates (xMin..xMax, yMin..yMax) to pixel coordinates and back.
const xMin = -5, xMax = 5, yMin = -5, yMax = 5;
const toPx = (x, y) => [
PAD + (x - xMin) / (xMax - xMin) * (W - 2 * PAD),
H - PAD - (y - yMin) / (yMax - yMin) * (H - 2 * PAD)
];
const toData = (px, py) => [
xMin + (px - PAD) / (W - 2 * PAD) * (xMax - xMin),
yMin + (H - PAD - py) / (H - 2 * PAD) * (yMax - yMin)
];
// The draggable points; index of the point currently being dragged (-1 = none).
let points = [[-3, -2], [-1, 2], [1, -1], [3, 3]];
let dragIndex = -1;
// P(x) = sum_i y_i * prod_{j != i} (x - x_j) / (x_i - x_j) — the Lagrange formula itself.
function lagrangeAt(pts, x) {
let total = 0;
for (let i = 0; i < pts.length; i++) {
const [xi, yi] = pts[i];
let term = yi;
for (let j = 0; j < pts.length; j++) {
if (j === i) continue;
const xj = pts[j][0];
term *= (x - xj) / (xi - xj);
}
total += term;
}
return total;
}
function sortedPoints() {
return [...points].sort((a, b) => a[0] - b[0]);
}
function draw() {
ctx.clearRect(0, 0, W, H);
// Draw the x and y axes through the origin.
ctx.strokeStyle = '#cdd9e3';
ctx.lineWidth = 1;
const [ox, oy] = toPx(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();
const pts = sortedPoints();
// Sample the interpolating polynomial densely and draw it as a smooth curve.
ctx.strokeStyle = '#e63946';
ctx.lineWidth = 2.5;
ctx.beginPath();
const steps = 240;
for (let s = 0; s <= steps; s++) {
const x = xMin + (xMax - xMin) * s / steps;
let y = lagrangeAt(pts, x);
y = Math.max(yMin - 20, Math.min(yMax + 20, y));
const [px, py] = toPx(x, y);
if (s === 0) ctx.moveTo(px, py); else ctx.lineTo(px, py);
}
ctx.stroke();
// Draw each control point on top of the curve.
for (const [x, y] of points) {
const [px, py] = toPx(x, y);
ctx.beginPath();
ctx.arc(px, py, 7, 0, Math.PI * 2);
ctx.fillStyle = '#1d3557';
ctx.fill();
ctx.strokeStyle = '#fff';
ctx.lineWidth = 2;
ctx.stroke();
}
}
function setStatus(t) { document.getElementById('status').textContent = t; }
function updateStatus() {
const n = points.length;
setStatus(i18nFmt('%n% points -> a polynomial of degree up to %deg%.', { n: n, deg: n - 1 }));
}
function nearestPoint(px, py) {
let best = -1, bestDist = 16 * 16;
points.forEach(([x, y], i) => {
const [qx, qy] = toPx(x, y);
const d = (qx - px) ** 2 + (qy - py) ** 2;
if (d < bestDist) { bestDist = d; best = i; }
});
return best;
}
function eventPos(e) {
const rect = canvas.getBoundingClientRect();
const scaleX = W / rect.width, scaleY = H / rect.height;
const cx = (e.touches ? e.touches[0].clientX : e.clientX) - rect.left;
const cy = (e.touches ? e.touches[0].clientY : e.clientY) - rect.top;
return [cx * scaleX, cy * scaleY];
}
canvas.addEventListener('pointerdown', (e) => {
const [px, py] = eventPos(e);
dragIndex = nearestPoint(px, py);
if (dragIndex >= 0) canvas.classList.add('dragging');
});
window.addEventListener('pointermove', (e) => {
if (dragIndex < 0) return;
const [px, py] = eventPos(e);
let [x, y] = toData(px, py);
x = Math.max(xMin, Math.min(xMax, x));
y = Math.max(yMin, Math.min(yMax, y));
// Keep x-coordinates distinct so the formula's denominators never hit zero.
const others = points.filter((_, i) => i !== dragIndex).map(p => p[0]);
const tooClose = others.some(ox => Math.abs(ox - x) < 0.15);
if (!tooClose) points[dragIndex] = [x, y];
draw();
});
window.addEventListener('pointerup', () => {
if (dragIndex >= 0) { dragIndex = -1; canvas.classList.remove('dragging'); }
});
function addPoint() {
if (points.length >= 8) return;
// Look for a free x slot half a unit apart from existing points.
const xs = points.map(p => p[0]);
let x = 0;
for (let cand = xMin + 0.5; cand <= xMax - 0.5; cand += 0.5) {
if (!xs.some(ox => Math.abs(ox - cand) < 0.4)) { x = cand; break; }
}
const y = (Math.random() * 6 - 3);
points.push([Math.round(x * 10) / 10, Math.round(y * 10) / 10]);
updateStatus();
draw();
}
function removePoint() {
if (points.length <= 2) return;
points.pop();
updateStatus();
draw();
}
function resetAll() {
points = [[-3, -2], [-1, 2], [1, -1], [3, 3]];
updateStatus();
draw();
}
document.getElementById('addpt').addEventListener('click', addPoint);
document.getElementById('delpt').addEventListener('click', removePoint);
document.getElementById('reset').addEventListener('click', resetAll);
updateStatus();
draw();
Comments
Loading comments...