Files
illustrated-algorithm/shared/algo-viz.js
T

227 lines
8.9 KiB
JavaScript
Raw Normal View History

/* ===== Illustrated Algorithm – Shared Visualization Library ===== */
"use strict";
/* --- Utility --- */
const $ = id => document.getElementById(id);
const $$ = sel => document.querySelectorAll(sel);
/* --- StepController --- */
class StepController {
constructor(steps, opts = {}) {
this.steps = steps;
this.idx = -1;
this.autoTimer = null;
this.onRender = opts.onRender || (() => {});
this.onDone = opts.onDone || (() => {});
this.autoDelay = opts.autoDelay || 800;
}
get current() { return this.idx >= 0 && this.idx < this.steps.length ? this.steps[this.idx] : null; }
get isEnd() { return this.idx >= this.steps.length - 1; }
get total() { return this.steps.length; }
next() {
if (this.idx < this.steps.length - 1) { this.idx++; this._render(); }
else this.stopAuto();
}
prev() {
if (this.idx > 0) { this.idx--; this._render(); }
}
jumpEnd() {
this.idx = this.steps.length - 1;
this._render();
this.stopAuto();
}
jumpTo(i) {
if (i >= 0 && i < this.steps.length) { this.idx = i; this._render(); }
}
reset() {
this.idx = -1;
this.stopAuto();
this._renderPipeline();
if (this.onRender) this.onRender(null);
}
startAuto() {
this.stopAuto();
if (this.idx < 0) this.idx = 0;
this._render();
this.autoTimer = setInterval(() => {
if (this.idx < this.steps.length - 1) { this.idx++; this._render(); }
else { this.stopAuto(); if (this.onDone) this.onDone(); }
}, this.autoDelay);
}
stopAuto() {
if (this.autoTimer) { clearInterval(this.autoTimer); this.autoTimer = null; }
}
_render() {
if (this.onRender) this.onRender(this.current);
this._renderPipeline();
}
_renderPipeline() {
const el = $('pipeline');
if (!el) return;
el.innerHTML = '';
for (let i = 0; i < this.steps.length; i++) {
const d = document.createElement('div');
d.className = 'step-dot' + (i < this.idx ? ' done' : i === this.idx ? ' active' : '');
d.title = `Step ${i + 1}`;
d.style.cursor = 'pointer';
d.addEventListener('click', () => this.jumpTo(i));
el.appendChild(d);
}
}
}
/* --- Render helpers --- */
function renderArray(container, items, opts = {}) {
const { barMaxH = 120, barMinW = 28, barGap = 4, classes = [], tags = [], idxLabels = true } = opts;
const maxVal = Math.max(...items.map(it => typeof it === 'object' ? it.val : it), 1);
container.innerHTML = '';
const row = document.createElement('div');
row.className = 'arr-row';
items.forEach((item, i) => {
const val = typeof item === 'object' ? item.val : item;
const cls = typeof item === 'object' ? (item.cls || '') : (classes[i] || '');
const tag = typeof item === 'object' ? (item.tag || '') : (tags[i] || '');
const h = Math.max(8, (val / maxVal) * barMaxH);
const cell = document.createElement('div');
cell.className = 'arr-cell';
cell.innerHTML = `
<span class="arr-val">${val}</span>
<div class="arr-bar ${cls}" style="height:${h}px;width:${barMinW}px;"></div>
${idxLabels ? `<span class="arr-idx">${i}</span>` : ''}
${tag ? `<span class="tag ${cls === 'min-price' || cls === 'best' ? 'tag-green' : cls === 'current' ? 'tag-orange' : cls === 'checking' ? 'tag-blue' : 'tag-orange'}">${tag}</span>` : ''}
`;
row.appendChild(cell);
});
container.appendChild(row);
}
function renderGrid(container, rows, cols, cellData, opts = {}) {
/* cellData: 2D array of {val, cls} or null */
const { cellSize = 48 } = opts;
container.innerHTML = '';
const grid = document.createElement('div');
grid.className = 'grid-vis';
grid.style.gridTemplateColumns = `repeat(${cols}, ${cellSize}px)`;
for (let i = 0; i < rows; i++) {
for (let j = 0; j < cols; j++) {
const d = cellData && cellData[i] && cellData[i][j] ? cellData[i][j] : { val: '', cls: '' };
const cell = document.createElement('div');
cell.className = 'grid-cell ' + (d.cls || '');
cell.textContent = d.val !== undefined && d.val !== null ? d.val : '';
grid.appendChild(cell);
}
}
container.appendChild(grid);
}
function renderCode(container, code, highlightLine = -1) {
container.innerHTML = '';
const pre = document.createElement('pre');
pre.className = 'code-block';
const lines = code.split('\n');
lines.forEach((line, i) => {
if (i === highlightLine) {
const span = document.createElement('span');
span.className = 'hl';
span.textContent = line;
pre.appendChild(span);
} else {
pre.appendChild(document.createTextNode(line + '\n'));
}
});
container.appendChild(pre);
}
function renderLinkedList(container, nodes, opts = {}) {
/* nodes: [{val, cls}] */
container.innerHTML = '';
const row = document.createElement('div');
row.style.cssText = 'display:flex;align-items:center;gap:4px;flex-wrap:wrap;justify-content:center;';
nodes.forEach((n, i) => {
const box = document.createElement('div');
box.style.cssText = `padding:6px 12px;border-radius:6px;font-family:var(--mono);font-size:.85rem;border:2px solid var(--border);background:var(--surface2);transition:all .2s;${n.cls === 'highlight' ? 'border-color:var(--orange);background:var(--orange-dim);' : n.cls === 'done' ? 'border-color:var(--green);background:var(--green-dim);' : ''}`;
box.textContent = n.val;
row.appendChild(box);
if (i < nodes.length - 1) {
const arrow = document.createElement('span');
arrow.textContent = '→';
arrow.style.cssText = 'color:var(--text2);font-size:1.1rem;';
row.appendChild(arrow);
}
});
container.appendChild(row);
}
function renderBinaryTree(container, tree, opts = {}) {
/* simple SVG tree rendering */
container.innerHTML = '';
if (!tree) return;
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
svg.setAttribute('viewBox', '0 0 500 280');
svg.style.width = '100%';
svg.style.maxWidth = '500px';
const nodeEls = [];
function draw(node, x, y, spread, depth) {
if (!node) return;
const leftSpread = spread / 2;
if (node.left) {
const lx = x - leftSpread, ly = y + 60;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x); line.setAttribute('y1', y);
line.setAttribute('x2', lx); line.setAttribute('y2', ly);
line.setAttribute('stroke', '#2e3347'); line.setAttribute('stroke-width', '2');
svg.appendChild(line);
draw(node.left, lx, ly, leftSpread, depth + 1);
}
if (node.right) {
const rx = x + leftSpread, ry = y + 60;
const line = document.createElementNS('http://www.w3.org/2000/svg', 'line');
line.setAttribute('x1', x); line.setAttribute('y1', y);
line.setAttribute('x2', rx); line.setAttribute('y2', ry);
line.setAttribute('stroke', '#2e3347'); line.setAttribute('stroke-width', '2');
svg.appendChild(line);
draw(node.right, rx, ry, leftSpread, depth + 1);
}
const g = document.createElementNS('http://www.w3.org/2000/svg', 'g');
const circle = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
circle.setAttribute('cx', x); circle.setAttribute('cy', y); circle.setAttribute('r', '18');
const cls = node.cls || '';
circle.setAttribute('fill', cls === 'highlight' ? '#f5a623' : cls === 'done' ? '#3dd68c' : '#242836');
circle.setAttribute('stroke', cls === 'highlight' ? '#f5a623' : cls === 'done' ? '#3dd68c' : '#6c7eff');
circle.setAttribute('stroke-width', '2');
const text = document.createElementNS('http://www.w3.org/2000/svg', 'text');
text.setAttribute('x', x); text.setAttribute('y', y + 5);
text.setAttribute('text-anchor', 'middle'); text.setAttribute('fill', '#e4e6f0');
text.setAttribute('font-size', '12'); text.setAttribute('font-family', 'var(--mono)');
text.textContent = node.val;
g.appendChild(circle); g.appendChild(text);
svg.appendChild(g);
}
draw(tree, 250, 30, 200, 0);
container.appendChild(svg);
}
function renderStack(container, items, opts = {}) {
container.innerHTML = '';
const stack = document.createElement('div');
stack.style.cssText = 'display:flex;flex-direction:column-reverse;align-items:center;gap:3px;';
items.forEach((item, i) => {
const d = typeof item === 'object' ? item : { val: item, cls: '' };
const el = document.createElement('div');
el.style.cssText = `padding:6px 20px;border-radius:4px;font-family:var(--mono);font-size:.82rem;border:1px solid var(--border);background:var(--surface2);min-width:60px;text-align:center;${d.cls === 'top' ? 'border-color:var(--orange);background:var(--orange-dim);' : ''}`;
el.textContent = d.val;
stack.appendChild(el);
});
container.appendChild(stack);
}
function highlightRange(container, start, end, cls) {
/* highlight child elements in range [start, end) with class */
const children = container.children;
for (let i = 0; i < children.length; i++) {
if (i >= start && i < end) children[i].classList.add(cls);
else children[i].classList.remove(cls);
}
}