AgentStateGraph / app /src /content /embeds /transfer-matrix.html
seonglae's picture
Automata from Agent Traces — interactive article
59027a2
Raw
History Blame Contribute Delete
9.5 kB
<!-- Transfer Matrix: 4x4 cross-model AUROC heatmap -->
<div class="transfer-matrix"></div>
<style>
.transfer-matrix { position: relative; width: 100%; min-height: 380px; }
.transfer-matrix svg { display: block; margin: 0 auto; }
.transfer-matrix .cell-text { font-size: 13px; font-weight: 600; text-anchor: middle; font-variant-numeric: tabular-nums; }
.transfer-matrix .axis-text { font-size: 11px; fill: var(--text-color); }
.transfer-matrix .header-text { font-size: 13px; font-weight: 600; fill: var(--text-color); text-anchor: middle; }
.transfer-matrix .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: 260px;
font-variant-numeric: tabular-nums;
}
.transfer-matrix .legend-label { font-size: 10px; fill: var(--text-color); font-variant-numeric: tabular-nums; }
.transfer-matrix .summary {
text-align: center; margin-top: 14px; font-size: 13px; color: var(--text-color); opacity: 0.65;
font-variant-numeric: tabular-nums;
}
@media (max-width: 500px) {
.transfer-matrix { min-height: 300px; }
.transfer-matrix .cell-text { font-size: 11px; }
.transfer-matrix .axis-text { font-size: 9px; }
.transfer-matrix .header-text { font-size: 11px; }
.transfer-matrix .summary { font-size: 11px; }
}
</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('.transfer-matrix:not([data-mounted])');
if (!container) return;
container.dataset.mounted = 'true';
const d3 = window.d3;
const models = ['GPT-4.1', 'Claude 3.7', 'GPT-4.1-mini', 'o4-mini'];
const matrix = [
[0.802, 0.699, 0.671, 0.746],
[0.639, 0.726, 0.730, 0.604],
[0.665, 0.719, 0.790, 0.559],
[0.685, 0.553, 0.604, 0.864],
];
const tip = document.createElement('div');
tip.className = 'tooltip';
container.appendChild(tip);
const summary = document.createElement('div');
summary.className = 'summary';
summary.textContent = 'Diagonal mean: 0.796 | Off-diagonal mean: 0.656';
container.appendChild(summary);
// Warm earth tones: cream → rust → dark brown
function colorInterp(t) {
const c0 = [245, 240, 232]; // warm cream (#f5f0e8)
const c1 = [61, 90, 128]; // rust (#3d5a80)
const c2 = [92, 38, 24]; // deep rust-brown
let r, g, b;
if (t < 0.5) {
const s = t * 2;
r = c0[0] + (c1[0] - c0[0]) * s;
g = c0[1] + (c1[1] - c0[1]) * s;
b = c0[2] + (c1[2] - c0[2]) * s;
} else {
const s = (t - 0.5) * 2;
r = c1[0] + (c2[0] - c1[0]) * s;
g = c1[1] + (c2[1] - c1[1]) * s;
b = c1[2] + (c2[2] - c1[2]) * s;
}
return `rgb(${Math.round(r)},${Math.round(g)},${Math.round(b)})`;
}
function render() {
container.querySelectorAll('svg').forEach(s => s.remove());
const rect = container.getBoundingClientRect();
const maxW = Math.min(520, rect.width - 20);
const cellSize = Math.max(40, Math.floor((maxW - 140) / 4));
const margin = { top: 56, left: 115, bottom: 60, right: 30 };
const gridW = cellSize * 4;
const W = margin.left + gridW + margin.right;
const H = margin.top + gridW + margin.bottom;
const svg = d3.select(container).insert('svg', '.tooltip')
.attr('width', W).attr('height', Math.max(0, H));
const g = svg.append('g').attr('transform', `translate(${margin.left},${margin.top})`);
const colorScale = d3.scaleSequential(t => colorInterp(t)).domain([0.5, 0.9]);
// Headers
svg.append('text').attr('class', 'header-text')
.attr('x', margin.left + gridW / 2).attr('y', 20).text('Test Model');
svg.append('text').attr('class', 'header-text')
.attr('transform', `translate(18, ${margin.top + gridW / 2}) rotate(-90)`)
.text('Train Model');
// Column labels
models.forEach((m, i) => {
g.append('text').attr('class', 'axis-text')
.attr('x', i * cellSize + cellSize / 2).attr('y', -12)
.attr('text-anchor', 'middle').text(m);
});
// Row labels
models.forEach((m, i) => {
g.append('text').attr('class', 'axis-text')
.attr('x', -14).attr('y', i * cellSize + cellSize / 2 + 4)
.attr('text-anchor', 'end').text(m);
});
// Cells
for (let i = 0; i < 4; i++) {
for (let j = 0; j < 4; j++) {
const val = matrix[i][j];
const isDiag = i === j;
const cw = Math.max(0, cellSize - 3);
const ch = Math.max(0, cellSize - 3);
const cell = g.append('rect')
.attr('x', j * cellSize + 1.5).attr('y', i * cellSize + 1.5)
.attr('width', cw).attr('height', ch)
.attr('rx', 6).attr('fill', colorScale(val))
.attr('stroke', isDiag ? 'var(--text-color)' : 'var(--border-color)')
.attr('stroke-width', isDiag ? 2.5 : 0.5)
.attr('stroke-opacity', isDiag ? 0.8 : 0.3)
.attr('cursor', 'pointer')
.style('transition', 'transform 0.12s ease')
.style('transform-origin', `${j * cellSize + cellSize / 2}px ${i * cellSize + cellSize / 2}px`);
cell
.on('mouseenter', function(ev) {
d3.select(this).attr('stroke-width', isDiag ? 3 : 2).attr('stroke-opacity', 1)
.attr('stroke', 'var(--text-color)');
d3.select(this.parentNode).selectAll(`.cell-text-${i}-${j}`).attr('font-weight', 800);
tip.innerHTML = `<strong>Train:</strong> ${models[i]}<br/><strong>Test:</strong> ${models[j]}<br/><strong>AUROC:</strong> ${val.toFixed(3)}<br/>${isDiag ? 'Self-prediction (diagonal)' : 'Cross-model transfer'}`;
tip.style.opacity = '1';
})
.on('mousemove', function(ev) {
const [mx, my] = d3.pointer(ev, container);
const flipX = mx > rect.width * 0.65;
const tx = flipX ? mx - 180 : mx + 14;
tip.style.transform = `translate(${tx}px, ${my - 14}px)`;
})
.on('mouseleave', function() {
d3.select(this).attr('stroke-width', isDiag ? 2.5 : 0.5)
.attr('stroke-opacity', isDiag ? 0.8 : 0.3)
.attr('stroke', isDiag ? 'var(--text-color)' : 'var(--border-color)');
d3.select(this.parentNode).selectAll(`.cell-text-${i}-${j}`).attr('font-weight', 600);
tip.style.opacity = '0';
});
// Text label: white on dark rust, dark on light cream
const textColor = val > 0.72 ? '#ffffff' : '#4a4540';
g.append('text').attr('class', `cell-text cell-text-${i}-${j}`)
.attr('x', j * cellSize + cellSize / 2)
.attr('y', i * cellSize + cellSize / 2 + 5)
.attr('fill', textColor)
.style('pointer-events', 'none')
.text(val.toFixed(2));
}
}
// --- Color legend bar ---
const legendW = Math.max(0, Math.min(200, gridW * 0.7));
const legendH = 10;
const legendX = (gridW - legendW) / 2;
const legendY = gridW + 18;
const defs = svg.append('defs');
const gradId = 'tm-grad-' + Math.random().toString(36).slice(2, 8);
const grad = defs.append('linearGradient').attr('id', gradId);
const nStops = 10;
for (let s = 0; s <= nStops; s++) {
const t = s / nStops;
grad.append('stop')
.attr('offset', `${t * 100}%`)
.attr('stop-color', colorInterp(t));
}
const lg = g.append('g').attr('transform', `translate(${legendX}, ${legendY})`);
lg.append('rect')
.attr('width', Math.max(0, legendW)).attr('height', Math.max(0, legendH))
.attr('rx', 3).attr('fill', `url(#${gradId})`);
lg.append('text').attr('class', 'legend-label')
.attr('x', 0).attr('y', legendH + 14).attr('text-anchor', 'middle').text('0.50');
lg.append('text').attr('class', 'legend-label')
.attr('x', legendW / 2).attr('y', legendH + 14).attr('text-anchor', 'middle').text('0.70');
lg.append('text').attr('class', 'legend-label')
.attr('x', legendW).attr('y', legendH + 14).attr('text-anchor', 'middle').text('0.90');
lg.append('text').attr('class', 'legend-label')
.attr('x', legendW / 2).attr('y', -6).attr('text-anchor', 'middle')
.attr('font-weight', 500).text('AUROC');
}
render();
if (window.ResizeObserver) new ResizeObserver(() => render()).observe(container);
};
if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
else ensureD3(bootstrap);
})();
</script>