Try it. Below are shuffled fragments of a hidden DNA sequence. Click them in order to lay them on the assembly track — the demo automatically merges overlapping ends. Rebuild the original as short as possible.
<p class="hint">Shuffled fragments of a hidden DNA sequence. Click them to add to the assembly track in order — overlapping ends merge automatically. Rebuild the original. The repeated <b>ATG</b> is the troublemaker.</p>
<div class="reads" id="reads"></div>
<div class="track-wrap"><span class="lab">Assembly:</span><div class="track" id="track"></div></div>
<div id="result" class="result"></div>
<div class="btns">
<button id="auto" type="button">Auto-assemble</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 .8rem; line-height: 1.45; }
.hint b { color: #c0392b; }
.reads { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: 1rem; }
.read { font: 800 15px ui-monospace, monospace; letter-spacing: 1px; background: #fff; border: 2px solid #457b9d; color: #1d3557; border-radius: 8px; padding: .4rem .6rem; cursor: pointer; }
.read:hover { background: #f0f4f8; }
.read.used { opacity: .35; cursor: default; }
.track-wrap { display: flex; align-items: center; gap: .5rem; margin-bottom: .8rem; flex-wrap: wrap; }
.lab { font: 600 13px system-ui; color: #555; }
.track { display: flex; flex-wrap: wrap; gap: .3rem; min-height: 2.2rem; flex: 1; }
.chip { font: 800 14px ui-monospace, monospace; background: #2a9d8f; color: #fff; border-radius: 6px; padding: .35rem .5rem; cursor: pointer; }
.result { font: 800 16px ui-monospace, monospace; letter-spacing: 1px; word-break: break-all; min-height: 1.5em; margin-bottom: .4rem; }
.verdict { font: 700 14px system-ui; min-height: 1.3em; margin-bottom: .6rem; }
.verdict.ok { color: #0a7d33; } .verdict.bad { color: #b9770e; }
.btns { display: flex; gap: .5rem; }
button { font: 600 14px system-ui, sans-serif; padding: .5rem 1rem; border: 1px solid #457b9d; background: #457b9d; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #457b9d; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Genome assembly: order overlapping reads and merge shared ends to rebuild the original. Finding the shortest superstring is NP-hard; repeats (here ATG) create ambiguity. Auto-assemble runs a greedy max-overlap merge.
const ORIGINAL = 'ATGGCATGAC';
const READS = ['CATGA', 'ATGGC', 'ATGAC', 'GGCAT']; // shuffled
let order = []; // indices into READS
function overlap(a, b) { const m = Math.min(a.length, b.length); for (let k = m; k > 0; k--) { if (a.slice(a.length - k) === b.slice(0, k)) return k; } return 0; }
function merge(a, b) { return a + b.slice(overlap(a, b)); }
function assemble(idxs) { if (!idxs.length) return ''; let s = READS[idxs[0]]; for (let i = 1; i < idxs.length; i++) s = merge(s, READS[idxs[i]]); return s; }
function render() {
document.getElementById('reads').innerHTML = READS.map((r, i) =>
'<div class="read' + (order.includes(i) ? ' used' : '') + '" data-i="' + i + '">' + r + '</div>').join('');
document.querySelectorAll('.read').forEach(el => el.addEventListener('click', () => { const i = +el.dataset.i; if (!order.includes(i)) { order.push(i); render(); } }));
document.getElementById('track').innerHTML = order.map((i, pos) =>
'<span class="chip" data-pos="' + pos + '">' + READS[i] + '</span>').join('') || '<span style="color:#aaa;font-size:.85rem">click fragments above…</span>';
document.querySelectorAll('.chip').forEach(el => el.addEventListener('click', () => { order.splice(+el.dataset.pos, 1); render(); }));
const s = assemble(order);
document.getElementById('result').textContent = s;
const v = document.getElementById('verdict') || (() => { const d = document.createElement('div'); d.id = 'verdict'; d.className = 'verdict'; document.getElementById('result').after(d); return d; })();
if (order.length < READS.length) { v.className = 'verdict'; v.textContent = fmt('Length %length% — place all %length2% fragments.', { length: s.length, length2: READS.length }); }
else if (s === ORIGINAL) { v.className = 'verdict ok'; v.textContent = fmt('✓ Reconstructed the original (%length% bases)!', { length: s.length }); }
else { v.className = 'verdict bad'; v.textContent = fmt('Contains all reads (length %length%) but isn’t the original %ORIGINAL%.', { length: s.length, ORIGINAL: ORIGINAL }); }
}
function auto() {
// greedy: repeatedly merge the two contigs with the largest overlap
let contigs = READS.slice();
while (contigs.length > 1) {
let bi = 0, bj = 1, bo = -1, best = '';
for (let i = 0; i < contigs.length; i++) for (let j = 0; j < contigs.length; j++) if (i !== j) {
const o = overlap(contigs[i], contigs[j]); if (o > bo) { bo = o; bi = i; bj = j; best = merge(contigs[i], contigs[j]); }
}
contigs = contigs.filter((_, k) => k !== bi && k !== bj); contigs.push(best);
}
document.getElementById('result').textContent = contigs[0];
const v = document.getElementById('verdict');
if (contigs[0] === ORIGINAL) { v.className = 'verdict ok'; v.textContent = '✓ Greedy overlap rebuilt the original — but on real genomes the repeats make these joins ambiguous.'; }
else { v.className = 'verdict bad'; v.textContent = fmt('Greedy gave length %length% — the repeat misled it away from %ORIGINAL%.', { length: contigs[0].length, ORIGINAL: ORIGINAL }); }
}
document.getElementById('auto').addEventListener('click', auto);
document.getElementById('reset').addEventListener('click', () => { order = []; render(); });
render();
<p class="hint">Shuffled fragments of a hidden DNA sequence. Click them to add to the assembly track in order — overlapping ends merge automatically. Rebuild the original. The repeated <b>ATG</b> is the troublemaker.</p>
<div class="reads" id="reads"></div>
<div class="track-wrap"><span class="lab">Assembly:</span><div class="track" id="track"></div></div>
<div id="result" class="result"></div>
<div class="btns">
<button id="auto" type="button">Auto-assemble</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 .8rem; line-height: 1.45; }
.hint b { color: #c0392b; }
.reads { display: flex; flex-wrap: wrap; gap: .4rem; margin-bottom: 1rem; }
.read { font: 800 15px ui-monospace, monospace; letter-spacing: 1px; background: #fff; border: 2px solid #457b9d; color: #1d3557; border-radius: 8px; padding: .4rem .6rem; cursor: pointer; }
.read:hover { background: #f0f4f8; }
.read.used { opacity: .35; cursor: default; }
.track-wrap { display: flex; align-items: center; gap: .5rem; margin-bottom: .8rem; flex-wrap: wrap; }
.lab { font: 600 13px system-ui; color: #555; }
.track { display: flex; flex-wrap: wrap; gap: .3rem; min-height: 2.2rem; flex: 1; }
.chip { font: 800 14px ui-monospace, monospace; background: #2a9d8f; color: #fff; border-radius: 6px; padding: .35rem .5rem; cursor: pointer; }
.result { font: 800 16px ui-monospace, monospace; letter-spacing: 1px; word-break: break-all; min-height: 1.5em; margin-bottom: .4rem; }
.verdict { font: 700 14px system-ui; min-height: 1.3em; margin-bottom: .6rem; }
.verdict.ok { color: #0a7d33; } .verdict.bad { color: #b9770e; }
.btns { display: flex; gap: .5rem; }
button { font: 600 14px system-ui, sans-serif; padding: .5rem 1rem; border: 1px solid #457b9d; background: #457b9d; color: #fff; border-radius: 8px; cursor: pointer; }
button.ghost { background: #fff; color: #457b9d; }
const fmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// Genome assembly: order overlapping reads and merge shared ends to rebuild the original. Finding the shortest superstring is NP-hard; repeats (here ATG) create ambiguity. Auto-assemble runs a greedy max-overlap merge.
const ORIGINAL = 'ATGGCATGAC';
const READS = ['CATGA', 'ATGGC', 'ATGAC', 'GGCAT']; // shuffled
let order = []; // indices into READS
function overlap(a, b) { const m = Math.min(a.length, b.length); for (let k = m; k > 0; k--) { if (a.slice(a.length - k) === b.slice(0, k)) return k; } return 0; }
function merge(a, b) { return a + b.slice(overlap(a, b)); }
function assemble(idxs) { if (!idxs.length) return ''; let s = READS[idxs[0]]; for (let i = 1; i < idxs.length; i++) s = merge(s, READS[idxs[i]]); return s; }
function render() {
document.getElementById('reads').innerHTML = READS.map((r, i) =>
'<div class="read' + (order.includes(i) ? ' used' : '') + '" data-i="' + i + '">' + r + '</div>').join('');
document.querySelectorAll('.read').forEach(el => el.addEventListener('click', () => { const i = +el.dataset.i; if (!order.includes(i)) { order.push(i); render(); } }));
document.getElementById('track').innerHTML = order.map((i, pos) =>
'<span class="chip" data-pos="' + pos + '">' + READS[i] + '</span>').join('') || '<span style="color:#aaa;font-size:.85rem">click fragments above…</span>';
document.querySelectorAll('.chip').forEach(el => el.addEventListener('click', () => { order.splice(+el.dataset.pos, 1); render(); }));
const s = assemble(order);
document.getElementById('result').textContent = s;
const v = document.getElementById('verdict') || (() => { const d = document.createElement('div'); d.id = 'verdict'; d.className = 'verdict'; document.getElementById('result').after(d); return d; })();
if (order.length < READS.length) { v.className = 'verdict'; v.textContent = fmt('Length %length% — place all %length2% fragments.', { length: s.length, length2: READS.length }); }
else if (s === ORIGINAL) { v.className = 'verdict ok'; v.textContent = fmt('✓ Reconstructed the original (%length% bases)!', { length: s.length }); }
else { v.className = 'verdict bad'; v.textContent = fmt('Contains all reads (length %length%) but isn’t the original %ORIGINAL%.', { length: s.length, ORIGINAL: ORIGINAL }); }
}
function auto() {
// greedy: repeatedly merge the two contigs with the largest overlap
let contigs = READS.slice();
while (contigs.length > 1) {
let bi = 0, bj = 1, bo = -1, best = '';
for (let i = 0; i < contigs.length; i++) for (let j = 0; j < contigs.length; j++) if (i !== j) {
const o = overlap(contigs[i], contigs[j]); if (o > bo) { bo = o; bi = i; bj = j; best = merge(contigs[i], contigs[j]); }
}
contigs = contigs.filter((_, k) => k !== bi && k !== bj); contigs.push(best);
}
document.getElementById('result').textContent = contigs[0];
const v = document.getElementById('verdict');
if (contigs[0] === ORIGINAL) { v.className = 'verdict ok'; v.textContent = '✓ Greedy overlap rebuilt the original — but on real genomes the repeats make these joins ambiguous.'; }
else { v.className = 'verdict bad'; v.textContent = fmt('Greedy gave length %length% — the repeat misled it away from %ORIGINAL%.', { length: contigs[0].length, ORIGINAL: ORIGINAL }); }
}
document.getElementById('auto').addEventListener('click', auto);
document.getElementById('reset').addEventListener('click', () => { order = []; render(); });
render();
When you've placed them all, the assembly shows whether you matched the original. Press Auto-assemble to let a greedy overlap method try — and notice how the repeated stretch can lure it into the wrong join. That single ambiguity is the whole difficulty of assembly in miniature.
Comments
Loading comments...