<!-- Demo layout — inputs at top, scoring matrix below, alignment result at bottom -->
<div class="inputs">
<label>Sequence A <input id="seqA" type="text" maxlength="14" placeholder="e.g. ACGTACGT" value="ACGTACGT" /></label>
<label>Sequence B <input id="seqB" type="text" maxlength="14" placeholder="e.g. TACGTTAC" value="TACGTTAC" /></label>
<div class="scores-row">
<label class="score-lbl">Match<input id="matchScore" type="number" value="2" min="1" max="5" /></label>
<label class="score-lbl">Mismatch<input id="mismatchScore" type="number" value="-1" min="-5" max="0" /></label>
<label class="score-lbl">Gap<input id="gapScore" type="number" value="-1" min="-5" max="0" /></label>
</div>
<button id="btnAlign" type="button">Align</button>
</div>
<div id="matrixWrap" class="matrix-wrap" role="region" aria-label="Smith-Waterman scoring matrix"></div>
<div id="resultBox" class="result-box" aria-live="polite"></div>
/* Global reset and input layout */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.inputs { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .8rem; }
.inputs label { display: flex; flex-direction: column; gap: .2rem; font-weight: 600; font-size: .85rem; color: #444; }
.inputs input[type="text"] { font: 700 1rem ui-monospace,monospace; padding: .3rem .5rem; border: 1px solid #adb1b8; border-radius: 6px; width: 100%; letter-spacing: .08em; }
.scores-row { display: flex; gap: .7rem; flex-wrap: wrap; }
.score-lbl { display: flex; flex-direction: column; gap: .2rem; font-weight: 600; font-size: .8rem; color: #444; }
.score-lbl input[type="number"] { width: 60px; padding: .3rem .4rem; border: 1px solid #adb1b8; border-radius: 6px; font-size: .9rem; }
button { font: 600 14px system-ui,sans-serif; padding: .45rem 1rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; margin-top: .2rem; }
button:hover { background: #162942; }
/* Scoring matrix color coding — green=high, red=best cell, teal=traceback path */
.matrix-wrap { overflow-x: auto; margin: .6rem 0; }
table { border-collapse: collapse; font: .75rem ui-monospace,monospace; }
th, td { width: 28px; height: 28px; text-align: center; border: 1px solid #d0d7de; }
th { background: #e8eef3; font-weight: 700; color: #1d3557; font-size: .7rem; }
td { color: #222; }
td.zero { color: #bbb; }
td.hi { background: #2d9e6b; color: #fff; font-weight: 700; }
td.path { background: #a8e6c9; color: #0a5c3a; font-weight: 700; }
td.best { background: #e63946; color: #fff; font-weight: 800; }
/* Result box shown after alignment */
.result-box { margin-top: .6rem; padding: .6rem .8rem; border-radius: 8px; background: #f0f6fa; border: 1px solid #cdd9e3; min-height: 2.5rem; }
.result-box.ok { background: #e6f7ee; border-color: #7ecba3; }
.result-box .score-line { font-weight: 700; color: #1d3557; margin-bottom: .3rem; }
.result-box .align-row { font: .95rem ui-monospace,monospace; letter-spacing: .08em; white-space: pre; }
.result-box .align-match { color: #0a7d33; }
.result-box .align-label { display: inline-block; width: 2.4rem; font-size: .75rem; color: #666; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Smith-Waterman local alignment: fill an (m+1)x(n+1) matrix with zero floor.
// Each cell = max(0, diag+sub, up+gap, left+gap). Zero floor is the key local-vs-global difference.
// Traceback starts at the highest cell and follows pointers until a zero cell is reached.
function smithWaterman(a, b, match, mismatch, gap) {
// Build (m+1)x(n+1) scoring matrix H and traceback pointer matrix T, both zero-initialized
const m = a.length, n = b.length;
const H = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
const T = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); // traceback pointer matrix
// Fill each cell with the Smith-Waterman recurrence; track global maximum
let bestScore = 0, bestI = 0, bestJ = 0;
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
const diag = H[i-1][j-1] + (a[i-1] === b[j-1] ? match : mismatch);
const up = H[i-1][j] + gap;
const left = H[i][j-1] + gap;
const best4 = Math.max(0, diag, up, left);
H[i][j] = best4;
// Record which predecessor gave the maximum (1=diagonal, 2=up, 3=left, 0=zero-floor)
if (best4 === 0) T[i][j] = 0;
else if (best4 === diag) T[i][j] = 1; // diagonal match/mismatch
else if (best4 === up) T[i][j] = 2; // gap in sequence B
else T[i][j] = 3; // gap in sequence A
if (best4 > bestScore) { bestScore = best4; bestI = i; bestJ = j; }
}
}
// Traceback from highest cell, building aligned strings
const path = new Set();
let ci = bestI, cj = bestJ;
const aAligned = [], bAligned = [], midLine = [];
while (ci > 0 && cj > 0 && H[ci][cj] > 0) {
path.add(ci + ',' + cj);
const dir = T[ci][cj];
if (dir === 1) { aAligned.unshift(a[ci-1]); bAligned.unshift(b[cj-1]); midLine.unshift(a[ci-1]===b[cj-1]?'|':' '); ci--; cj--; }
else if (dir === 2) { aAligned.unshift(a[ci-1]); bAligned.unshift('-'); midLine.unshift(' '); ci--; }
else { aAligned.unshift('-'); bAligned.unshift(b[cj-1]); midLine.unshift(' '); cj--; }
}
return { H, bestScore, bestI, bestJ, path, aAligned, bAligned, midLine };
}
function renderMatrix(a, b, H, path, bestI, bestJ) {
// Build HTML table from matrix values, coloring best cell, path, and zeros
const wrap = document.getElementById('matrixWrap');
const tbl = document.createElement('table');
// Header row with sequence B characters as column labels
const hdr = tbl.insertRow();
hdr.insertCell().textContent = '';
hdr.insertCell().textContent = '';
for (const ch of b) { const th = document.createElement('th'); th.textContent = ch; hdr.appendChild(th); }
for (let i = 0; i <= a.length; i++) {
const tr = tbl.insertRow();
const lh = document.createElement('th');
lh.textContent = i === 0 ? '' : a[i-1];
tr.appendChild(lh);
for (let j = 0; j <= b.length; j++) {
const td = tr.insertCell();
td.textContent = H[i][j];
if (i === bestI && j === bestJ) td.className = 'best';
else if (path.has(i + ',' + j)) td.className = 'path';
else if (H[i][j] === 0) td.className = 'zero';
else td.className = 'hi';
}
}
wrap.innerHTML = '';
wrap.appendChild(tbl);
}
function renderResult(bestScore, aAligned, bAligned, midLine) {
// Display score and aligned sequences with pipe characters for exact matches
const box = document.getElementById('resultBox');
if (bestScore === 0) {
box.className = 'result-box';
box.innerHTML = '<span class="score-line">No local match found (all cells are zero).</span>';
return;
}
box.className = 'result-box ok';
box.innerHTML =
'<div class="score-line">' + i18nFmt('Best local alignment score: %score%', { score: bestScore }) + '</div>' +
'<div class="align-row"><span class="align-label">A:</span>' + aAligned.join('') + '</div>' +
'<div class="align-row align-match"><span class="align-label"></span>' + midLine.join('') + '</div>' +
'<div class="align-row"><span class="align-label">B:</span>' + bAligned.join('') + '</div>';
}
function run() {
const a = document.getElementById('seqA').value.toUpperCase().replace(/[^A-Z]/g, '');
const b = document.getElementById('seqB').value.toUpperCase().replace(/[^A-Z]/g, '');
const match = parseInt(document.getElementById('matchScore').value, 10) || 2;
const mismatch = parseInt(document.getElementById('mismatchScore').value, 10) || -1;
const gap = parseInt(document.getElementById('gapScore').value, 10) || -1;
if (!a || !b) return;
const res = smithWaterman(a, b, match, mismatch, gap);
renderMatrix(a, b, res.H, res.path, res.bestI, res.bestJ);
renderResult(res.bestScore, res.aAligned, res.bAligned, res.midLine);
}
document.getElementById('btnAlign').addEventListener('click', run);
run();
<!-- Demo layout — inputs at top, scoring matrix below, alignment result at bottom -->
<div class="inputs">
<label>Sequence A <input id="seqA" type="text" maxlength="14" placeholder="e.g. ACGTACGT" value="ACGTACGT" /></label>
<label>Sequence B <input id="seqB" type="text" maxlength="14" placeholder="e.g. TACGTTAC" value="TACGTTAC" /></label>
<div class="scores-row">
<label class="score-lbl">Match<input id="matchScore" type="number" value="2" min="1" max="5" /></label>
<label class="score-lbl">Mismatch<input id="mismatchScore" type="number" value="-1" min="-5" max="0" /></label>
<label class="score-lbl">Gap<input id="gapScore" type="number" value="-1" min="-5" max="0" /></label>
</div>
<button id="btnAlign" type="button">Align</button>
</div>
<div id="matrixWrap" class="matrix-wrap" role="region" aria-label="Smith-Waterman scoring matrix"></div>
<div id="resultBox" class="result-box" aria-live="polite"></div>
/* Global reset and input layout */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; color: #222; margin: 0; font-size: 14px; }
.inputs { display: flex; flex-direction: column; gap: .5rem; margin-bottom: .8rem; }
.inputs label { display: flex; flex-direction: column; gap: .2rem; font-weight: 600; font-size: .85rem; color: #444; }
.inputs input[type="text"] { font: 700 1rem ui-monospace,monospace; padding: .3rem .5rem; border: 1px solid #adb1b8; border-radius: 6px; width: 100%; letter-spacing: .08em; }
.scores-row { display: flex; gap: .7rem; flex-wrap: wrap; }
.score-lbl { display: flex; flex-direction: column; gap: .2rem; font-weight: 600; font-size: .8rem; color: #444; }
.score-lbl input[type="number"] { width: 60px; padding: .3rem .4rem; border: 1px solid #adb1b8; border-radius: 6px; font-size: .9rem; }
button { font: 600 14px system-ui,sans-serif; padding: .45rem 1rem; border: 1px solid #1d3557; background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; margin-top: .2rem; }
button:hover { background: #162942; }
/* Scoring matrix color coding — green=high, red=best cell, teal=traceback path */
.matrix-wrap { overflow-x: auto; margin: .6rem 0; }
table { border-collapse: collapse; font: .75rem ui-monospace,monospace; }
th, td { width: 28px; height: 28px; text-align: center; border: 1px solid #d0d7de; }
th { background: #e8eef3; font-weight: 700; color: #1d3557; font-size: .7rem; }
td { color: #222; }
td.zero { color: #bbb; }
td.hi { background: #2d9e6b; color: #fff; font-weight: 700; }
td.path { background: #a8e6c9; color: #0a5c3a; font-weight: 700; }
td.best { background: #e63946; color: #fff; font-weight: 800; }
/* Result box shown after alignment */
.result-box { margin-top: .6rem; padding: .6rem .8rem; border-radius: 8px; background: #f0f6fa; border: 1px solid #cdd9e3; min-height: 2.5rem; }
.result-box.ok { background: #e6f7ee; border-color: #7ecba3; }
.result-box .score-line { font-weight: 700; color: #1d3557; margin-bottom: .3rem; }
.result-box .align-row { font: .95rem ui-monospace,monospace; letter-spacing: .08em; white-space: pre; }
.result-box .align-match { color: #0a7d33; }
.result-box .align-label { display: inline-block; width: 2.4rem; font-size: .75rem; color: #666; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Smith-Waterman local alignment: fill an (m+1)x(n+1) matrix with zero floor.
// Each cell = max(0, diag+sub, up+gap, left+gap). Zero floor is the key local-vs-global difference.
// Traceback starts at the highest cell and follows pointers until a zero cell is reached.
function smithWaterman(a, b, match, mismatch, gap) {
// Build (m+1)x(n+1) scoring matrix H and traceback pointer matrix T, both zero-initialized
const m = a.length, n = b.length;
const H = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0));
const T = Array.from({ length: m + 1 }, () => new Array(n + 1).fill(0)); // traceback pointer matrix
// Fill each cell with the Smith-Waterman recurrence; track global maximum
let bestScore = 0, bestI = 0, bestJ = 0;
for (let i = 1; i <= m; i++) {
for (let j = 1; j <= n; j++) {
const diag = H[i-1][j-1] + (a[i-1] === b[j-1] ? match : mismatch);
const up = H[i-1][j] + gap;
const left = H[i][j-1] + gap;
const best4 = Math.max(0, diag, up, left);
H[i][j] = best4;
// Record which predecessor gave the maximum (1=diagonal, 2=up, 3=left, 0=zero-floor)
if (best4 === 0) T[i][j] = 0;
else if (best4 === diag) T[i][j] = 1; // diagonal match/mismatch
else if (best4 === up) T[i][j] = 2; // gap in sequence B
else T[i][j] = 3; // gap in sequence A
if (best4 > bestScore) { bestScore = best4; bestI = i; bestJ = j; }
}
}
// Traceback from highest cell, building aligned strings
const path = new Set();
let ci = bestI, cj = bestJ;
const aAligned = [], bAligned = [], midLine = [];
while (ci > 0 && cj > 0 && H[ci][cj] > 0) {
path.add(ci + ',' + cj);
const dir = T[ci][cj];
if (dir === 1) { aAligned.unshift(a[ci-1]); bAligned.unshift(b[cj-1]); midLine.unshift(a[ci-1]===b[cj-1]?'|':' '); ci--; cj--; }
else if (dir === 2) { aAligned.unshift(a[ci-1]); bAligned.unshift('-'); midLine.unshift(' '); ci--; }
else { aAligned.unshift('-'); bAligned.unshift(b[cj-1]); midLine.unshift(' '); cj--; }
}
return { H, bestScore, bestI, bestJ, path, aAligned, bAligned, midLine };
}
function renderMatrix(a, b, H, path, bestI, bestJ) {
// Build HTML table from matrix values, coloring best cell, path, and zeros
const wrap = document.getElementById('matrixWrap');
const tbl = document.createElement('table');
// Header row with sequence B characters as column labels
const hdr = tbl.insertRow();
hdr.insertCell().textContent = '';
hdr.insertCell().textContent = '';
for (const ch of b) { const th = document.createElement('th'); th.textContent = ch; hdr.appendChild(th); }
for (let i = 0; i <= a.length; i++) {
const tr = tbl.insertRow();
const lh = document.createElement('th');
lh.textContent = i === 0 ? '' : a[i-1];
tr.appendChild(lh);
for (let j = 0; j <= b.length; j++) {
const td = tr.insertCell();
td.textContent = H[i][j];
if (i === bestI && j === bestJ) td.className = 'best';
else if (path.has(i + ',' + j)) td.className = 'path';
else if (H[i][j] === 0) td.className = 'zero';
else td.className = 'hi';
}
}
wrap.innerHTML = '';
wrap.appendChild(tbl);
}
function renderResult(bestScore, aAligned, bAligned, midLine) {
// Display score and aligned sequences with pipe characters for exact matches
const box = document.getElementById('resultBox');
if (bestScore === 0) {
box.className = 'result-box';
box.innerHTML = '<span class="score-line">No local match found (all cells are zero).</span>';
return;
}
box.className = 'result-box ok';
box.innerHTML =
'<div class="score-line">' + i18nFmt('Best local alignment score: %score%', { score: bestScore }) + '</div>' +
'<div class="align-row"><span class="align-label">A:</span>' + aAligned.join('') + '</div>' +
'<div class="align-row align-match"><span class="align-label"></span>' + midLine.join('') + '</div>' +
'<div class="align-row"><span class="align-label">B:</span>' + bAligned.join('') + '</div>';
}
function run() {
const a = document.getElementById('seqA').value.toUpperCase().replace(/[^A-Z]/g, '');
const b = document.getElementById('seqB').value.toUpperCase().replace(/[^A-Z]/g, '');
const match = parseInt(document.getElementById('matchScore').value, 10) || 2;
const mismatch = parseInt(document.getElementById('mismatchScore').value, 10) || -1;
const gap = parseInt(document.getElementById('gapScore').value, 10) || -1;
if (!a || !b) return;
const res = smithWaterman(a, b, match, mismatch, gap);
renderMatrix(a, b, res.H, res.path, res.bestI, res.bestJ);
renderResult(res.bestScore, res.aAligned, res.bAligned, res.midLine);
}
document.getElementById('btnAlign').addEventListener('click', run);
run();
Notice what happens when the sequences share only a short common region: the high scores cluster in one corner of the matrix and the traceback arrow path stops as soon as the score would drop to zero — that stop is exactly the "local" in local alignment. A global algorithm like Needleman-Wunsch would be forced to align the entire sequences; Smith-Waterman freely ignores the mismatching ends.
Comments
Loading comments...