AgentStateGraph / app /src /content /embeds /markov-bound-scatter.html
seonglae's picture
Automata from Agent Traces — interactive article
59027a2
Raw
History Blame Contribute Delete
11.6 kB
<!-- Markov Bound Scatter: |Q| vs |A| for 12 datasets, all on y=x+1 line -->
<div class="markov-scatter"></div>
<style>
.markov-scatter { position: relative; width: 100%; min-height: 400px; }
.markov-scatter svg { display: block; width: 100%; }
.markov-scatter .axis-label {
fill: var(--text-color); font-size: 12px; font-weight: 500;
font-variant-numeric: tabular-nums;
}
.markov-scatter .tick text {
fill: var(--text-color); font-size: 11px; opacity: 0.6;
font-variant-numeric: tabular-nums;
}
.markov-scatter .tick line { display: none; }
.markov-scatter .domain { stroke: var(--border-color); opacity: 0.3; }
.markov-scatter .grid line {
stroke: var(--border-color); opacity: 0.15;
stroke-dasharray: 2,4;
}
.markov-scatter .grid .domain { display: none; }
.markov-scatter circle {
transition: opacity 0.15s ease, stroke-width 0.15s ease;
}
.markov-scatter .legend-pill {
font-size: 11px; font-variant-numeric: tabular-nums;
}
.markov-scatter .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), 0 0 0 1px rgba(0,0,0,0.04);
backdrop-filter: blur(12px); -webkit-backdrop-filter: blur(12px);
opacity: 0; transition: opacity 0.15s ease;
z-index: 100; max-width: 240px;
font-variant-numeric: tabular-nums;
}
.markov-scatter .bound-label {
font-size: 11px; font-style: italic; opacity: 0.5;
font-variant-numeric: tabular-nums;
}
</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('.markov-scatter:not([data-mounted])');
if (!container) return;
container.dataset.mounted = 'true';
const d3 = window.d3;
const data = [
{ name: 'GUI-Odyssey', domain: 'Mobile GUI', A: 6, Q: 7, traces: 7735 },
{ name: 'Mind2Web', domain: 'Web', A: 7, Q: 8, traces: 500 },
{ name: 'Who & When', domain: 'Multi-agent', A: 8, Q: 9, traces: 184 },
{ name: 'SWE-smith', domain: 'Coding', A: 9, Q: 10, traces: 500 },
{ name: 'ATBench', domain: 'Safety', A: 14, Q: 15, traces: 1000 },
{ name: 'tau2-bench airline', domain: 'Customer svc', A: 17, Q: 18, traces: 800 },
{ name: 'tau2-bench retail', domain: 'Customer svc', A: 18, Q: 19, traces: 1824 },
{ name: 'SWE-agent', domain: 'Coding', A: 24, Q: 25, traces: 2000 },
{ name: 'WebArena', domain: 'Web', A: 24, Q: 25, traces: 8337 },
{ name: 'AgentNet', domain: 'Desktop GUI', A: 24, Q: 25, traces: 5000 },
{ name: 'OSWorld', domain: 'Desktop GUI', A: 26, Q: 27, traces: 2166 },
{ name: 'tau2-bench telecom', domain: 'Customer svc', A: 42, Q: 43, traces: 1824 },
];
const domainColors = {
'Coding': '#3d5a80',
'Web': '#8fa6c4',
'Desktop GUI': '#6b8f71',
'Mobile GUI': '#8e6bb5',
'Multi-agent': '#c49a3a',
'Customer svc': '#2a9d8f',
'Safety': '#4d6278',
};
// Manual label offsets: [dx, dy, anchor]
// Carefully placed to avoid overlap
const labelOffsets = {
'GUI-Odyssey': [-8, -12, 'end'],
'Mind2Web': [10, -8, 'start'],
'Who & When': [10, 4, 'start'],
'SWE-smith': [10, 4, 'start'],
'tau2-bench airline': [10, -6, 'start'],
'OSWorld': [10, 4, 'start'],
'SWE-agent': [-8, -10, 'end'],
'WebArena': [10, -4, 'start'],
'AgentNet': [10, 10, 'start'],
'tau2-bench telecom': [-8, -10, 'end'],
};
// These points will NOT have labels to reduce clutter
const hideLabels = new Set(['ATBench', 'tau2-bench retail']);
const margin = { top: 50, right: 30, bottom: 55, left: 60 };
const tip = document.createElement('div');
tip.className = 'tooltip';
container.appendChild(tip);
let firstRender = true;
function render() {
container.querySelectorAll('svg').forEach(s => s.remove());
const rect = container.getBoundingClientRect();
const W = Math.max(300, Math.round(rect.width));
const H = Math.max(350, Math.round(W * 0.7));
const w = Math.max(0, W - margin.left - margin.right);
const h = Math.max(0, H - margin.top - margin.bottom);
const svg = d3.select(container).insert('svg', '.tooltip')
.attr('width', W).attr('height', H);
const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`);
const x = d3.scaleLinear().domain([0, 48]).range([0, Math.max(0, w)]);
const y = d3.scaleLinear().domain([0, 50]).range([Math.max(0, h), 0]);
// Grid lines
g.append('g').attr('class', 'grid')
.attr('transform', `translate(0,${Math.max(0, h)})`)
.call(d3.axisBottom(x).ticks(8).tickSize(Math.max(0, -h)).tickFormat(''));
g.append('g').attr('class', 'grid')
.call(d3.axisLeft(y).ticks(8).tickSize(Math.max(0, -w)).tickFormat(''));
// y = x + 1 band (subtle filled area)
const bandWidth = 1.2; // visual band half-width in data units
const bandPoints = [];
for (let a = 0; a <= 48; a += 1) {
bandPoints.push([a, a + 1]);
}
const areaAbove = d3.area()
.x(d => x(d[0]))
.y0(d => y(Math.min(50, d[1] + bandWidth)))
.y1(d => y(Math.max(0, d[1] - bandWidth)));
g.append('path')
.datum(bandPoints)
.attr('d', areaAbove)
.attr('fill', 'var(--text-color)')
.attr('opacity', 0.06);
// y = x + 1 center line (subtle dashed)
g.append('line')
.attr('x1', x(0)).attr('y1', y(1))
.attr('x2', x(47)).attr('y2', y(48))
.attr('stroke', 'var(--text-color)')
.attr('stroke-width', 1.2)
.attr('stroke-dasharray', '4,3')
.attr('opacity', 0.3);
g.append('text').attr('class', 'bound-label')
.attr('x', x(36)).attr('y', y(38.8))
.attr('fill', 'var(--text-color)').attr('text-anchor', 'start')
.text('|Q| = |A| + 1');
// Axes
g.append('g').attr('transform', `translate(0,${Math.max(0, h)})`)
.call(d3.axisBottom(x).ticks(8));
g.append('g').call(d3.axisLeft(y).ticks(8));
// Axis labels
g.append('text').attr('class', 'axis-label')
.attr('x', Math.max(0, w) / 2).attr('y', Math.max(0, h) + 42).attr('text-anchor', 'middle')
.text('Activity alphabet size |A|');
g.append('text').attr('class', 'axis-label')
.attr('transform', 'rotate(-90)').attr('x', -Math.max(0, h) / 2).attr('y', -45)
.attr('text-anchor', 'middle').text('FSM state count |Q|');
// Horizontal legend pills at top
const domains = [...new Set(data.map(d => d.domain))];
const legendGroup = svg.append('g')
.attr('transform', `translate(${margin.left}, ${12})`);
let legendX = 0;
domains.forEach((dom) => {
const pill = legendGroup.append('g')
.attr('class', 'legend-pill')
.attr('transform', `translate(${legendX}, 0)`);
pill.append('rect')
.attr('x', 0).attr('y', -8)
.attr('width', 22).attr('height', 16)
.attr('rx', 8).attr('ry', 8)
.attr('fill', domainColors[dom])
.attr('opacity', 0.15);
pill.append('circle')
.attr('cx', 6).attr('cy', 0)
.attr('r', 4)
.attr('fill', domainColors[dom]);
pill.append('text')
.attr('x', 16).attr('y', 3.5)
.attr('fill', 'var(--text-color)')
.attr('font-size', '10px')
.attr('opacity', 0.7)
.text(dom);
// Measure text width for spacing
const textNode = pill.select('text').node();
const textW = textNode ? textNode.getComputedTextLength() : dom.length * 6;
legendX += textW + 28;
});
// Points
const rScale = d3.scaleSqrt().domain([0, 9000]).range([5, 15]);
const points = g.selectAll('.point').data(data).enter().append('circle')
.attr('class', 'point')
.attr('cx', d => x(d.A))
.attr('cy', d => y(d.Q))
.attr('fill', d => domainColors[d.domain])
.attr('stroke', 'var(--surface-bg)')
.attr('stroke-width', 2)
.attr('opacity', 0.9)
.attr('cursor', 'pointer');
if (firstRender) {
points
.attr('r', 0)
.transition()
.duration(500)
.delay((d, i) => i * 50)
.ease(d3.easeBackOut.overshoot(1.2))
.attr('r', d => rScale(d.traces));
} else {
points.attr('r', d => rScale(d.traces));
}
// Hover interactions
points
.on('mouseenter', function(ev, d) {
d3.select(this).attr('opacity', 1).attr('stroke-width', 3);
tip.innerHTML = [
'<strong>' + d.name + '</strong>',
'Domain: ' + d.domain,
'|A| = ' + d.A + ', |Q| = ' + d.Q,
'Traces: ' + d.traces.toLocaleString()
].join('<br/>');
tip.style.opacity = '1';
})
.on('mousemove', function(ev) {
const [mx, my] = d3.pointer(ev, container);
const tipW = tip.offsetWidth || 180;
const tipH = tip.offsetHeight || 80;
const cRect = container.getBoundingClientRect();
let tx = mx + 14;
let ty = my - 14;
if (tx + tipW > cRect.width) tx = mx - tipW - 14;
if (ty + tipH > cRect.height) ty = my - tipH - 14;
tip.style.transform = `translate(${tx}px, ${ty}px)`;
})
.on('mouseleave', function() {
d3.select(this).attr('opacity', 0.9).attr('stroke-width', 2);
tip.style.opacity = '0';
});
// Labels with manual offsets (skip hidden ones)
const labelData = data.filter(d => !hideLabels.has(d.name));
const labels = g.selectAll('.point-label').data(labelData)
.enter().append('text')
.attr('class', 'point-label')
.attr('x', d => {
const off = labelOffsets[d.name];
return x(d.A) + (off ? off[0] : rScale(d.traces) + 6);
})
.attr('y', d => {
const off = labelOffsets[d.name];
return y(d.Q) + (off ? off[1] : 4);
})
.attr('text-anchor', d => {
const off = labelOffsets[d.name];
return off ? off[2] : 'start';
})
.attr('fill', 'var(--text-color)')
.attr('font-size', '10px')
.attr('font-variant-numeric', 'tabular-nums')
.text(d => d.name);
if (firstRender) {
labels
.attr('opacity', 0)
.transition()
.duration(300)
.delay((d, i) => i * 50 + 400)
.attr('opacity', 0.65);
} else {
labels.attr('opacity', 0.65);
}
firstRender = false;
}
render();
if (window.ResizeObserver) new ResizeObserver(() => render()).observe(container);
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
else ensureD3(bootstrap);
})();
</script>