Spaces:
Running
Running
| <!-- 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> |