Spaces:
Running
Running
| <html lang="en"> | |
| <head> | |
| <meta charset="UTF-8"/> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"/> | |
| <style> | |
| * { box-sizing: border-box; margin: 0; padding: 0; } | |
| body { background: transparent; font-family: system-ui, sans-serif; color: #e8eaf0; } | |
| svg text { font-family: system-ui, sans-serif; } | |
| </style> | |
| </head> | |
| <body> | |
| <svg id="action-rep" style="overflow:visible"></svg> | |
| <script> | |
| function _initActionRep() { | |
| const svgEl = document.getElementById('action-rep'); | |
| const W = Math.min(svgEl.parentElement.clientWidth || 700, 720); | |
| const H = 310; | |
| svgEl.setAttribute('width', W); | |
| svgEl.setAttribute('height', H); | |
| const svg = d3.select('#action-rep').attr('width', W).attr('height', H); | |
| svg.selectAll('*').remove(); | |
| const m = { top: 30, right: 24, bottom: 64, left: 48 }; | |
| const w = W - m.left - m.right; | |
| const h = H - m.top - m.bottom; | |
| const g = svg.append('g').attr('transform', `translate(${m.left},${m.top})`); | |
| const SUB = '#8b8fa8'; | |
| const COL_REL = '#3b82f6'; | |
| const COL_DELTA = '#f59e0b'; | |
| const COL_ABS = '#ef4444'; | |
| // Arrowhead markers | |
| [['rel', COL_REL], ['delta', COL_DELTA]].forEach(([id, col]) => { | |
| svg.append('defs').append('marker') | |
| .attr('id', `ah-${id}`).attr('viewBox', '0 0 8 6').attr('refX', 7).attr('refY', 3) | |
| .attr('markerWidth', 8).attr('markerHeight', 6).attr('orient', 'auto') | |
| .append('path').attr('d', 'M0,0.5 L7,3 L0,5.5 Z').attr('fill', col); | |
| }); | |
| const poses = [ | |
| {t:0, p:0.8}, {t:1, p:1.5}, {t:2, p:2.1}, {t:3, p:2.9}, {t:4, p:3.3}, | |
| {t:5, p:4.0}, {t:6, p:4.5}, {t:7, p:5.1}, {t:8, p:5.6} | |
| ]; | |
| const x = d3.scaleLinear().domain([-0.3, 8.5]).range([0, w]); | |
| const y = d3.scaleLinear().domain([0, 6.5]).range([h, 0]); | |
| // Faint ground-truth path | |
| const line = d3.line().x(d => x(d.t)).y(d => y(d.p)).curve(d3.curveMonotoneX); | |
| g.append('path').datum(poses).attr('d', line) | |
| .attr('fill', 'none').attr('stroke', '#3a3d4a').attr('stroke-width', 1.5).attr('stroke-dasharray', '4,4'); | |
| // Inference boundary | |
| [0, 4].forEach(t => { | |
| g.append('line').attr('x1', x(t)).attr('x2', x(t)).attr('y1', -14).attr('y2', h + 4) | |
| .attr('stroke', '#444').attr('stroke-dasharray', '5,4').attr('stroke-width', 0.8); | |
| }); | |
| g.append('text').attr('x', x(2)).attr('y', -4).attr('text-anchor', 'middle') | |
| .attr('fill', SUB).attr('font-size', 11).text('Inference start at t=0'); | |
| g.append('text').attr('x', x(6)).attr('y', -4).attr('text-anchor', 'middle') | |
| .attr('fill', SUB).attr('font-size', 11).text('Inference start at t=4'); | |
| // Axes | |
| g.append('line').attr('x1', x(0)).attr('x2', x(8.3)).attr('y1', h).attr('y2', h).attr('stroke', '#444'); | |
| g.append('line').attr('x1', x(0)).attr('x2', x(0)).attr('y1', h).attr('y2', y(6.2)).attr('stroke', '#444'); | |
| for (let t = 0; t <= 8; t++) { | |
| g.append('text').attr('x', x(t)).attr('y', h + 14).attr('text-anchor', 'middle') | |
| .attr('fill', SUB).attr('font-size', 12).text(t); | |
| } | |
| g.append('text').attr('x', x(8.4)).attr('y', h + 4).attr('fill', SUB).attr('font-size', 12).text('Time'); | |
| g.append('text').attr('x', x(-0.1)).attr('y', y(6.4)).attr('text-anchor', 'end') | |
| .attr('fill', SUB).attr('font-size', 12).text('Pose'); | |
| // --- DELTA (yellow): straight arrows, offset below trajectory --- | |
| const dOff = 14; | |
| for (let i = 0; i < poses.length - 1; i++) { | |
| g.append('line') | |
| .attr('x1', x(poses[i].t) + 3).attr('y1', y(poses[i].p) + dOff) | |
| .attr('x2', x(poses[i+1].t) - 3).attr('y2', y(poses[i+1].p) + dOff) | |
| .attr('stroke', COL_DELTA).attr('stroke-width', 2).attr('opacity', 0.8) | |
| .attr('marker-end', 'url(#ah-delta)'); | |
| } | |
| // --- RELATIVE (blue): curved arrows above trajectory --- | |
| function curvedArrow(x1, y1, x2, y2) { | |
| const cpY = Math.min(y1, y2) - 20 - Math.abs(x2 - x1) * 0.08; | |
| const cpX = (x1 + x2) / 2; | |
| g.append('path') | |
| .attr('d', `M${x1},${y1 - 4} Q${cpX},${cpY} ${x2},${y2 - 4}`) | |
| .attr('fill', 'none').attr('stroke', COL_REL).attr('stroke-width', 2).attr('opacity', 0.85) | |
| .attr('marker-end', 'url(#ah-rel)'); | |
| } | |
| // Chunk 1: t=0 → t=1,2,3,4 | |
| for (let i = 1; i <= 4; i++) curvedArrow(x(0), y(poses[0].p), x(i), y(poses[i].p)); | |
| // Chunk 2: t=4 → t=5,6,7,8 | |
| for (let i = 5; i <= 8; i++) curvedArrow(x(4), y(poses[4].p), x(i), y(poses[i].p)); | |
| // --- ABSOLUTE (red): dots on trajectory --- | |
| poses.forEach(d => { | |
| g.append('circle').attr('cx', x(d.t)).attr('cy', y(d.p)).attr('r', 5) | |
| .attr('fill', COL_ABS).attr('stroke', '#fff').attr('stroke-width', 1.5); | |
| }); | |
| // Chunk brackets | |
| const bY = h + 22; | |
| [{t0:0, t1:4}, {t0:4, t1:8}].forEach(({t0, t1}) => { | |
| g.append('path') | |
| .attr('d', `M${x(t0)},${bY-3} L${x(t0)},${bY} L${x(t1)},${bY} L${x(t1)},${bY-3}`) | |
| .attr('fill', 'none').attr('stroke', COL_REL).attr('stroke-width', 1.2).attr('opacity', 0.5); | |
| g.append('text').attr('x', (x(t0) + x(t1)) / 2).attr('y', bY + 12) | |
| .attr('text-anchor', 'middle').attr('fill', COL_REL).attr('font-size', 11).attr('opacity', 0.7) | |
| .text('chunk = 4'); | |
| }); | |
| // Legend (bottom) | |
| const legY = h + 44; | |
| const legItems = [ | |
| { col: COL_REL, label: 'Relative trajectory (used here)', type: 'curve' }, | |
| { col: COL_DELTA, label: 'Delta (accumulates error)', type: 'line' }, | |
| { col: COL_ABS, label: 'Absolute', type: 'dot' }, | |
| ]; | |
| let lx = 0; | |
| legItems.forEach(item => { | |
| const sw = 14; | |
| if (item.type === 'curve') { | |
| g.append('line').attr('x1', lx).attr('x2', lx + sw).attr('y1', legY + 2).attr('y2', legY + 2) | |
| .attr('stroke', item.col).attr('stroke-width', 2.5); | |
| } else if (item.type === 'line') { | |
| g.append('line').attr('x1', lx).attr('x2', lx + sw).attr('y1', legY + 2).attr('y2', legY + 2) | |
| .attr('stroke', item.col).attr('stroke-width', 2.5); | |
| } else { | |
| g.append('circle').attr('cx', lx + sw / 2).attr('cy', legY + 2).attr('r', 4.5) | |
| .attr('fill', item.col).attr('stroke', '#fff').attr('stroke-width', 1.2); | |
| } | |
| lx += sw + 6; | |
| const t = g.append('text').attr('x', lx).attr('y', legY + 6) | |
| .attr('fill', item.col).attr('font-size', 12).attr('font-weight', 500).text(item.label); | |
| lx += t.node().getComputedTextLength() + 16; | |
| }); | |
| } | |
| if (typeof d3 !== "undefined") { _initActionRep(); } | |
| else { var s=document.createElement("script"); s.src="https://cdnjs.cloudflare.com/ajax/libs/d3/7.9.0/d3.min.js"; s.onload=_initActionRep; document.head.appendChild(s); } | |
| window.addEventListener('resize', _initActionRep); | |
| </script> | |
| </body> | |
| </html> | |