AgentStateGraph / app /src /content /embeds /convergence-curves.html
seonglae's picture
Automata from Agent Traces — interactive article
59027a2
Raw
History Blame Contribute Delete
9.93 kB
<!-- Convergence Curves: Small-multiples sparkline cards with convergence gauges -->
<div class="convergence-curves"></div>
<style>
.convergence-curves { position: relative; width: 100%; }
.convergence-curves .cc-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
gap: 12px;
}
.convergence-curves .cc-card {
border: 1px solid var(--border-color);
border-radius: 12px;
padding: 16px 18px 14px;
background: var(--surface-bg);
position: relative;
overflow: hidden;
transition: border-color 0.2s ease, box-shadow 0.2s ease;
cursor: default;
}
.convergence-curves .cc-card:hover {
border-color: var(--text-color);
box-shadow: 0 2px 12px rgba(0,0,0,0.06);
}
.convergence-curves .cc-name {
font-size: 12px; font-weight: 600; color: var(--text-color);
margin-bottom: 2px; letter-spacing: 0.01em;
}
.convergence-curves .cc-meta {
font-size: 10px; color: var(--text-color); opacity: 0.4;
font-variant-numeric: tabular-nums; margin-bottom: 10px;
}
.convergence-curves .cc-big {
font-size: 28px; font-weight: 800; line-height: 1;
font-variant-numeric: tabular-nums;
margin-bottom: 2px;
}
.convergence-curves .cc-sub {
font-size: 10px; color: var(--text-color); opacity: 0.45;
margin-bottom: 10px;
}
.convergence-curves .cc-spark { display: block; width: 100%; }
.convergence-curves .cc-bar-track {
height: 4px; border-radius: 2px; background: var(--border-color);
opacity: 0.3; margin-top: 10px; overflow: hidden;
}
.convergence-curves .cc-bar-fill {
height: 100%; border-radius: 2px;
transition: width 1.2s cubic-bezier(0.22, 1, 0.36, 1);
}
.convergence-curves .cc-fitness {
font-size: 10px; color: var(--text-color); opacity: 0.5;
font-variant-numeric: tabular-nums; margin-top: 6px;
text-align: right;
}
.convergence-curves .tooltip {
position: absolute; top: 0; left: 0; pointer-events: none; padding: 10px 14px; border-radius: 8px;
font-size: 12px; line-height: 1.6; border: 1px solid var(--border-color);
background: var(--surface-bg); color: var(--text-color);
box-shadow: 0 4px 20px rgba(0,0,0,0.12);
backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
opacity: 0; transition: opacity 0.15s ease;
z-index: 100; max-width: 220px;
font-variant-numeric: tabular-nums;
}
@media (max-width: 600px) {
.convergence-curves .cc-grid { grid-template-columns: repeat(2, 1fr); gap: 8px; }
.convergence-curves .cc-card { padding: 12px 14px 10px; }
.convergence-curves .cc-big { font-size: 22px; }
}
</style>
<script>
(() => {
const ensureD3 = (cb) => {
if (window.d3 && typeof window.d3.select === 'function') return cb();
let s = document.getElementById('d3-cdn-script');
if (!s) { s = document.createElement('script'); s.id = 'd3-cdn-script'; s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js'; document.head.appendChild(s); }
s.addEventListener('load', () => cb(), { once: true });
};
const bootstrap = () => {
const container = document.querySelector('.convergence-curves:not([data-mounted])');
if (!container) return;
container.dataset.mounted = 'true';
const d3 = window.d3;
// Real incremental-convergence runs (experiments/convergence-rate/convergence-results.json).
// convPct = smallest % of training data at which replay fitness first reaches 0.95.
// curve = the experiment's actual fitnessTrajectory: [pctData, fitness].
const datasets = [
{ name: 'SWE-agent', traces: '2,000', states: 25, finalFitness: 0.996,
convPct: 1, color: '#3d5a80',
curve: [[0.01,0.9851],[0.05,0.9851],[0.10,0.9961],[0.20,0.9961],[0.50,0.9961],[1.0,0.9961]] },
{ name: 'SWE-smith', traces: '500', states: 10, finalFitness: 1.000,
convPct: 1, color: '#9e5e5a',
curve: [[0.01,0.9996],[0.05,0.9996],[0.10,0.9996],[0.20,0.9996],[0.50,1.000],[1.0,1.000]] },
{ name: 'Mind2Web', traces: '500', states: 8, finalFitness: 0.999,
convPct: 5, color: '#4d6278',
curve: [[0.01,0.7921],[0.05,0.9503],[0.10,0.9503],[0.20,0.9665],[0.50,0.999],[1.0,0.999]] },
{ name: 'Who & When', traces: '184', states: 9, finalFitness: 1.000,
convPct: 10, color: '#8fa6c4',
curve: [[0.01,0.1709],[0.05,0.7945],[0.10,0.9776],[0.20,0.9797],[0.50,0.9797],[1.0,1.000]] },
];
// Build grid
const grid = document.createElement('div');
grid.className = 'cc-grid';
container.appendChild(grid);
const tip = document.createElement('div');
tip.className = 'tooltip';
container.appendChild(tip);
datasets.forEach((ds, idx) => {
const card = document.createElement('div');
card.className = 'cc-card';
card.innerHTML = `
<div class="cc-name">${ds.name}</div>
<div class="cc-meta">${ds.traces} traces &middot; ${ds.states} states</div>
<div class="cc-big" style="color:${ds.color}">${ds.convPct}%</div>
<div class="cc-sub">of training data to reach 0.95 fitness</div>
<svg class="cc-spark" data-idx="${idx}"></svg>
<div class="cc-bar-track"><div class="cc-bar-fill" style="background:${ds.color};width:0%"></div></div>
<div class="cc-fitness">final fitness ${ds.finalFitness.toFixed(3)}</div>
`;
grid.appendChild(card);
// Hover tooltip
card.addEventListener('mouseenter', (ev) => {
const convTraces = Math.round(ds.convPct / 100 * parseInt(ds.traces.replace(/,/g, '')));
tip.innerHTML = `<strong>${ds.name}</strong><br/>
Converges at <strong>${ds.convPct}%</strong> (${convTraces.toLocaleString()} traces)<br/>
Final fitness: <strong>${ds.finalFitness.toFixed(3)}</strong><br/>
FSM states: <strong>${ds.states}</strong>`;
tip.style.opacity = '1';
});
card.addEventListener('mousemove', (ev) => {
const cr = container.getBoundingClientRect();
const mx = ev.clientX - cr.left;
const my = ev.clientY - cr.top;
const tw = tip.offsetWidth || 200;
let tx = mx + 14;
if (tx + tw > cr.width) tx = mx - tw - 14;
tip.style.transform = `translate(${tx}px, ${my - 14}px)`;
});
card.addEventListener('mouseleave', () => { tip.style.opacity = '0'; });
});
// Draw sparklines
function drawSparklines() {
container.querySelectorAll('.cc-spark').forEach(svgEl => {
const idx = +svgEl.dataset.idx;
const ds = datasets[idx];
const rect = svgEl.parentElement.getBoundingClientRect();
const W = Math.max(100, Math.round(rect.width - 36));
const H = 48;
svgEl.setAttribute('width', W);
svgEl.setAttribute('height', H);
svgEl.setAttribute('viewBox', `0 0 ${W} ${H}`);
svgEl.innerHTML = '';
const svg = d3.select(svgEl);
// Full trajectory on x (0-100% of data); y adapts to each curve's real floor.
const fitVals = ds.curve.map(p => p[1]);
const yFloor = Math.max(0, Math.min(...fitVals) - 0.03);
const x = d3.scaleLinear().domain([0, 1]).range([0, W]);
const y = d3.scaleLinear().domain([yFloor, 1.005]).range([H, 0]);
const clipped = ds.curve;
// Area fill
const areaGen = d3.area()
.x(d => x(d[0])).y0(H).y1(d => y(d[1]))
.curve(d3.curveMonotoneX);
svg.append('path')
.datum(clipped)
.attr('d', areaGen)
.attr('fill', ds.color)
.attr('opacity', 0.08);
// Line
const lineGen = d3.line()
.x(d => x(d[0])).y(d => y(d[1]))
.curve(d3.curveMonotoneX);
svg.append('path')
.datum(clipped)
.attr('fill', 'none')
.attr('stroke', ds.color)
.attr('stroke-width', 2)
.attr('opacity', 0.7)
.attr('d', lineGen);
// 0.999 reference line
if (y(0.999) > 2 && y(0.999) < H - 2) {
svg.append('line')
.attr('x1', 0).attr('x2', W)
.attr('y1', y(0.999)).attr('y2', y(0.999))
.attr('stroke', 'var(--text-color)')
.attr('stroke-width', 0.5)
.attr('stroke-dasharray', '2,3')
.attr('opacity', 0.2);
}
// Convergence marker
const convX = x(ds.convPct / 100);
if (convX >= 0 && convX <= W) {
// Vertical line at convergence
svg.append('line')
.attr('x1', convX).attr('x2', convX)
.attr('y1', 0).attr('y2', H)
.attr('stroke', ds.color)
.attr('stroke-width', 1)
.attr('stroke-dasharray', '2,2')
.attr('opacity', 0.35);
// Dot
const convY = y(ds.finalFitness);
svg.append('circle')
.attr('cx', convX).attr('cy', convY)
.attr('r', 3.5)
.attr('fill', ds.color)
.attr('stroke', 'var(--surface-bg)')
.attr('stroke-width', 1.5);
}
});
}
drawSparklines();
// Animate progress bars on scroll into view
let animated = false;
const animateBars = () => {
if (animated) return;
const rect = container.getBoundingClientRect();
if (rect.top < window.innerHeight * 0.85) {
animated = true;
container.querySelectorAll('.cc-bar-fill').forEach((bar, i) => {
setTimeout(() => {
bar.style.width = datasets[i].convPct + '%';
}, i * 120);
});
}
};
animateBars();
window.addEventListener('scroll', animateBars, { passive: true });
if (window.ResizeObserver) new ResizeObserver(() => drawSparklines()).observe(container);
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
else ensureD3(bootstrap);
})();
</script>