eleusis-benchmark / app /src /content /embeds /complexity-analysis.html
dlouapre's picture
dlouapre HF Staff
Adding interactive charts + assesment
aee6411
<div class="d3-complexity-analysis"></div>
<style>
.d3-complexity-analysis {
width: 100%;
margin: 10px 0;
position: relative;
font-family: system-ui, -apple-system, sans-serif;
}
.d3-complexity-analysis svg {
display: block;
width: 100%;
height: auto;
}
.d3-complexity-analysis .axes path,
.d3-complexity-analysis .axes line {
stroke: var(--axis-color, var(--text-color));
}
.d3-complexity-analysis .axes text {
fill: var(--tick-color, var(--muted-color));
font-size: 11px;
}
.d3-complexity-analysis .axes text.axis-label {
font-size: 14px;
font-weight: 500;
fill: var(--text-color);
}
.d3-complexity-analysis .axes text.chart-title {
font-size: 16px;
font-weight: 600;
fill: var(--text-color);
}
.d3-complexity-analysis .cell {
stroke: var(--surface-bg, #fff);
stroke-width: 2;
cursor: pointer;
transition: opacity 0.1s ease;
}
.d3-complexity-analysis .cell:hover {
opacity: 0.85;
}
.d3-complexity-analysis .cell-text {
font-size: 13px;
font-weight: 600;
pointer-events: none;
}
.d3-complexity-analysis .model-label {
font-size: 12px;
fill: var(--text-color);
}
.d3-complexity-analysis .quartile-label {
font-size: 12px;
fill: var(--text-color);
}
.d3-complexity-analysis .legend-title {
font-size: 11px;
fill: var(--muted-color);
}
.d3-complexity-analysis .legend-tick {
font-size: 10px;
fill: var(--muted-color);
}
.d3-complexity-analysis .d3-tooltip {
position: absolute;
top: 0;
left: 0;
transform: translate(-9999px, -9999px);
pointer-events: none;
padding: 10px 12px;
border-radius: 8px;
font-size: 12px;
line-height: 1.5;
border: 1px solid var(--border-color);
background: var(--surface-bg);
color: var(--text-color);
box-shadow: 0 4px 24px rgba(0,0,0,.18);
opacity: 0;
transition: opacity 0.12s ease;
z-index: 10;
max-width: 280px;
}
.d3-complexity-analysis .d3-tooltip .model-name {
font-weight: 600;
margin-bottom: 4px;
}
.d3-complexity-analysis .d3-tooltip .metric {
display: flex;
justify-content: space-between;
gap: 16px;
}
.d3-complexity-analysis .d3-tooltip .metric-label {
color: var(--muted-color);
}
.d3-complexity-analysis .d3-tooltip .metric-value {
font-weight: 500;
}
.d3-complexity-analysis .d3-tooltip .interpretation {
margin-top: 6px;
font-size: 11px;
color: var(--muted-color);
font-style: italic;
}
</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);
}
const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };
s.addEventListener('load', onReady, { once: true });
if (window.d3) onReady();
};
const bootstrap = () => {
const scriptEl = document.currentScript;
let container = scriptEl ? scriptEl.previousElementSibling : null;
if (!(container && container.classList && container.classList.contains('d3-complexity-analysis'))) {
const candidates = Array.from(document.querySelectorAll('.d3-complexity-analysis'))
.filter((el) => !(el.dataset && el.dataset.mounted === 'true'));
container = candidates[candidates.length - 1] || null;
}
if (!container) return;
if (container.dataset) {
if (container.dataset.mounted === 'true') return;
container.dataset.mounted = 'true';
}
// Tooltip setup
container.style.position = container.style.position || 'relative';
const tip = document.createElement('div');
tip.className = 'd3-tooltip';
container.appendChild(tip);
// SVG setup
const svg = d3.select(container).append('svg');
const gRoot = svg.append('g');
// Chart groups
const gAxes = gRoot.append('g').attr('class', 'axes');
const gCells = gRoot.append('g').attr('class', 'cells');
const gLegend = gRoot.append('g').attr('class', 'legend');
// State
let data = null;
let width = 700;
let height = 450;
const margin = { top: 60, right: 100, bottom: 60, left: 160 };
// Scales
const xScale = d3.scaleBand();
const yScale = d3.scaleBand();
// Linear color scale: red (0%) -> green (100%+)
const colorScale = d3.scaleLinear()
.interpolate(() => d3.interpolateRdYlGn);
const DATA_URL = '/data/complexity_analysis.json';
function updateSize() {
width = Math.min(container.clientWidth || 700, 800);
const numModels = data ? data.models.length : 10;
const cellHeight = 36;
height = margin.top + margin.bottom + numModels * cellHeight;
svg.attr('width', width).attr('height', height).attr('viewBox', `0 0 ${width} ${height}`);
gRoot.attr('transform', `translate(${margin.left},${margin.top})`);
return {
innerWidth: width - margin.left - margin.right,
innerHeight: height - margin.top - margin.bottom
};
}
function getContrastColor(hexColor) {
const hex = hexColor.replace('#', '');
const r = parseInt(hex.substr(0, 2), 16) / 255;
const g = parseInt(hex.substr(2, 2), 16) / 255;
const b = parseInt(hex.substr(4, 2), 16) / 255;
const luminance = 0.299 * r + 0.587 * g + 0.114 * b;
return luminance > 0.5 ? '#000000' : '#ffffff';
}
function rgbToHex(rgb) {
// Convert rgb(r, g, b) string to #rrggbb
const match = rgb.match(/rgb\((\d+),\s*(\d+),\s*(\d+)\)/);
if (!match) return rgb;
const r = parseInt(match[1]).toString(16).padStart(2, '0');
const g = parseInt(match[2]).toString(16).padStart(2, '0');
const b = parseInt(match[3]).toString(16).padStart(2, '0');
return `#${r}${g}${b}`;
}
function showTooltip(event, d) {
const rect = container.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const pct = d.score * 100;
const interpretation = pct > 100
? `Performs ${(pct - 100).toFixed(0)}% above average on ${d.quartile} rules`
: pct < 100
? `Performs ${(100 - pct).toFixed(0)}% below average on ${d.quartile} rules`
: 'Performs at average on these rules';
const quartileDesc = {
'Q1': 'Easiest (lowest complexity)',
'Q2': 'Easy-Medium',
'Q3': 'Medium-Hard',
'Q4': 'Hardest (highest complexity)'
};
tip.innerHTML = `
<div class="model-name">${d.model}</div>
<div class="metric">
<span class="metric-label">Quartile:</span>
<span class="metric-value">${d.quartile}</span>
</div>
<div class="metric">
<span class="metric-label">Difficulty:</span>
<span class="metric-value">${quartileDesc[d.quartile]}</span>
</div>
<div class="metric">
<span class="metric-label">Relative Score:</span>
<span class="metric-value">${pct.toFixed(0)}%</span>
</div>
<div class="interpretation">${interpretation}</div>
`;
const tipWidth = tip.offsetWidth || 200;
const tipHeight = tip.offsetHeight || 120;
let tipX = x + 12;
let tipY = y - tipHeight / 2;
if (tipX + tipWidth > width) tipX = x - tipWidth - 12;
if (tipY < 0) tipY = 8;
if (tipY + tipHeight > height) tipY = height - tipHeight - 8;
tip.style.transform = `translate(${tipX}px, ${tipY}px)`;
tip.style.opacity = '1';
}
function hideTooltip() {
tip.style.opacity = '0';
tip.style.transform = 'translate(-9999px, -9999px)';
}
function render() {
if (!data) return;
const { innerWidth, innerHeight } = updateSize();
const quartiles = data.quartiles;
const models = data.models;
// Update scales
xScale
.domain(quartiles)
.range([0, innerWidth])
.padding(0.08);
yScale
.domain(models.map(m => m.name))
.range([0, innerHeight])
.padding(0.08);
// Find score extent for color scale (in percentage: 0-100%+)
const allScores = [];
models.forEach(m => {
quartiles.forEach(q => {
allScores.push(m.quartile_scores[q] * 100);
});
});
const minPct = Math.min(...allScores);
const maxPct = Math.max(...allScores);
// Linear scale from 0% (red) to 100%+ (green)
colorScale.domain([0, maxPct]);
// Build cell data (with percentage values)
const cellData = [];
models.forEach(m => {
quartiles.forEach(q => {
cellData.push({
model: m.name,
quartile: q,
score: m.quartile_scores[q],
pct: m.quartile_scores[q] * 100
});
});
});
// Draw cells
gCells.selectAll('.cell')
.data(cellData, d => `${d.model}-${d.quartile}`)
.join('rect')
.attr('class', 'cell')
.attr('x', d => xScale(d.quartile))
.attr('y', d => yScale(d.model))
.attr('width', xScale.bandwidth())
.attr('height', yScale.bandwidth())
.attr('fill', d => colorScale(d.pct))
.attr('rx', 4)
.on('mouseenter', showTooltip)
.on('mousemove', showTooltip)
.on('mouseleave', hideTooltip);
// Draw cell text
gCells.selectAll('.cell-text')
.data(cellData, d => `${d.model}-${d.quartile}`)
.join('text')
.attr('class', 'cell-text')
.attr('x', d => xScale(d.quartile) + xScale.bandwidth() / 2)
.attr('y', d => yScale(d.model) + yScale.bandwidth() / 2)
.attr('text-anchor', 'middle')
.attr('dominant-baseline', 'central')
.style('fill', d => {
const bgColor = colorScale(d.pct);
const hex = bgColor.startsWith('rgb') ? rgbToHex(bgColor) : bgColor;
return getContrastColor(hex);
})
.text(d => `${d.pct.toFixed(0)}%`);
// Model labels (Y-axis)
gAxes.selectAll('.model-label')
.data(models, d => d.name)
.join('text')
.attr('class', 'model-label')
.attr('x', -10)
.attr('y', d => yScale(d.name) + yScale.bandwidth() / 2)
.attr('text-anchor', 'end')
.attr('dominant-baseline', 'central')
.text(d => d.name);
// Quartile labels (X-axis)
gAxes.selectAll('.quartile-label')
.data(quartiles)
.join('text')
.attr('class', 'quartile-label')
.attr('x', d => xScale(d) + xScale.bandwidth() / 2)
.attr('y', -10)
.attr('text-anchor', 'middle')
.text(d => d);
// X-axis title
gAxes.selectAll('.x-title')
.data([0])
.join('text')
.attr('class', 'x-title axis-label')
.attr('x', innerWidth / 2)
.attr('y', innerHeight + 40)
.attr('text-anchor', 'middle')
.text('Complexity Quartile (Q1 = easiest)');
// Chart title
gAxes.selectAll('.chart-title')
.data([0])
.join('text')
.attr('class', 'chart-title')
.attr('x', innerWidth / 2)
.attr('y', -35)
.attr('text-anchor', 'middle')
.text('Model Performance by Rule Complexity');
// Legend
const legendWidth = 20;
const legendHeight = innerHeight * 0.6;
const legendX = innerWidth + 30;
const legendY = (innerHeight - legendHeight) / 2;
// Create gradient
const gradientId = 'complexity-legend-gradient';
let defs = svg.select('defs');
if (defs.empty()) {
defs = svg.append('defs');
}
defs.selectAll(`#${gradientId}`).remove();
const gradient = defs.append('linearGradient')
.attr('id', gradientId)
.attr('x1', '0%')
.attr('x2', '0%')
.attr('y1', '100%')
.attr('y2', '0%');
const numStops = 11;
for (let i = 0; i <= numStops; i++) {
const t = i / numStops;
const value = t * maxPct;
gradient.append('stop')
.attr('offset', `${t * 100}%`)
.attr('stop-color', colorScale(value));
}
// Legend rectangle
gLegend.selectAll('.legend-rect')
.data([0])
.join('rect')
.attr('class', 'legend-rect')
.attr('x', legendX)
.attr('y', legendY)
.attr('width', legendWidth)
.attr('height', legendHeight)
.attr('fill', `url(#${gradientId})`)
.attr('rx', 2)
.attr('stroke', 'var(--border-color)')
.attr('stroke-width', 0.5);
// Legend ticks (in percentage)
const legendScale = d3.scaleLinear()
.domain([0, maxPct])
.range([legendY + legendHeight, legendY]);
// Generate nice tick values for percentage scale
const tickValues = [0, 50, 100];
if (maxPct > 100) tickValues.push(Math.round(maxPct / 10) * 10);
gLegend.selectAll('.legend-tick')
.data(tickValues.filter(v => v <= maxPct))
.join('text')
.attr('class', 'legend-tick')
.attr('x', legendX + legendWidth + 6)
.attr('y', d => legendScale(d))
.attr('dominant-baseline', 'middle')
.text(d => `${d}%`);
// Legend title
gLegend.selectAll('.legend-title')
.data([0])
.join('text')
.attr('class', 'legend-title')
.attr('x', legendX + legendWidth / 2)
.attr('y', legendY - 12)
.attr('text-anchor', 'middle')
.text('Relative Score');
}
// Initialize
fetch(DATA_URL, { cache: 'no-cache' })
.then(r => r.json())
.then(json => {
data = json;
render();
})
.catch(err => {
const pre = document.createElement('pre');
pre.style.color = 'red';
pre.style.padding = '16px';
pre.textContent = `Error loading data: ${err.message}`;
container.appendChild(pre);
});
// Resize handling
if (window.ResizeObserver) {
new ResizeObserver(() => render()).observe(container);
} else {
window.addEventListener('resize', render);
}
// Theme change handling
const observer = new MutationObserver(() => render());
observer.observe(document.documentElement, {
attributes: true,
attributeFilter: ['data-theme']
});
};
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });
} else {
ensureD3(bootstrap);
}
})();
</script>