eleusis-benchmark / app /src /content /embeds /complexity-ratio.html
dlouapre's picture
dlouapre HF Staff
Improved charts
cafe265
<div class="d3-complexity-ratio"></div>
<style>
.d3-complexity-ratio {
width: 100%;
margin: 10px 0;
position: relative;
font-family: system-ui, -apple-system, sans-serif;
}
.d3-complexity-ratio svg {
display: block;
width: 100%;
height: auto;
}
.d3-complexity-ratio .axes path,
.d3-complexity-ratio .axes line {
stroke: var(--axis-color, var(--text-color));
}
.d3-complexity-ratio .axes text {
fill: var(--tick-color, var(--muted-color));
font-size: 11px;
}
.d3-complexity-ratio .grid line {
stroke: var(--grid-color, rgba(0,0,0,.08));
}
.d3-complexity-ratio .axes text.axis-label {
font-size: 14px;
font-weight: 500;
fill: var(--text-color);
}
.d3-complexity-ratio .reference-line {
stroke: var(--muted-color);
stroke-dasharray: 5, 5;
stroke-width: 1.5;
}
.d3-complexity-ratio .whisker-line {
stroke-width: 1.5;
}
.d3-complexity-ratio .whisker-cap {
stroke-width: 1.5;
}
.d3-complexity-ratio .model-point {
stroke-width: 2;
cursor: pointer;
}
.d3-complexity-ratio .model-point:hover {
stroke-width: 3;
}
.d3-complexity-ratio .ratio-label {
font-size: 11px;
fill: var(--muted-color);
}
.d3-complexity-ratio .legend-item {
cursor: default;
}
.d3-complexity-ratio .legend-text {
font-size: 11px;
fill: var(--text-color);
}
.d3-complexity-ratio .subtitle {
font-size: 11px;
fill: var(--muted-color);
}
.d3-complexity-ratio .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.4;
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;
}
.d3-complexity-ratio .d3-tooltip .model-name {
font-weight: 600;
margin-bottom: 4px;
}
.d3-complexity-ratio .d3-tooltip .metric {
display: flex;
justify-content: space-between;
gap: 16px;
}
.d3-complexity-ratio .d3-tooltip .metric-label {
color: var(--muted-color);
}
.d3-complexity-ratio .d3-tooltip .metric-value {
font-weight: 500;
}
</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-ratio'))) {
const candidates = Array.from(document.querySelectorAll('.d3-complexity-ratio'))
.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 gGrid = gRoot.append('g').attr('class', 'grid');
const gReference = gRoot.append('g').attr('class', 'reference');
const gAxes = gRoot.append('g').attr('class', 'axes');
const gWhiskers = gRoot.append('g').attr('class', 'whiskers');
const gPoints = gRoot.append('g').attr('class', 'points');
const gLabels = gRoot.append('g').attr('class', 'labels');
const gLegend = gRoot.append('g').attr('class', 'legend');
// State
let data = null;
let width = 800;
let height = 500;
const margin = { top: 30, right: 100, bottom: 60, left: 180 };
// Scales
const xScale = d3.scaleLinear();
const yScale = d3.scaleBand();
// Data loading
const DATA_URL = '/data/complexity_ratio.json';
function showTooltip(event, model) {
const rect = container.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
const interpretation = model.median_ratio > 1.05
? 'Tends to overcomplicate'
: model.median_ratio < 0.95
? 'Tends to oversimplify'
: 'Matches complexity well';
tip.innerHTML = `
<div class="model-name" style="color: ${model.color}">${model.name}</div>
<div class="metric">
<span class="metric-label">Median ratio:</span>
<span class="metric-value">${model.median_ratio.toFixed(2)}</span>
</div>
<div class="metric">
<span class="metric-label">IQR:</span>
<span class="metric-value">${model.q25.toFixed(2)}${model.q75.toFixed(2)}</span>
</div>
<div class="metric">
<span class="metric-label">Samples:</span>
<span class="metric-value">n=${model.count}</span>
</div>
<div class="metric" style="margin-top: 4px;">
<span class="metric-label">Interpretation:</span>
<span class="metric-value">${interpretation}</span>
</div>
`;
const tipWidth = tip.offsetWidth || 180;
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 updateSize() {
width = container.clientWidth || 800;
height = Math.max(420, Math.round(width * 0.55));
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 render() {
if (!data) return;
const { innerWidth, innerHeight } = updateSize();
// Sort models by median ratio (ascending - lowest at top)
const models = [...data.models].sort((a, b) => a.median_ratio - b.median_ratio);
// X scale: ratio values with padding
const xMin = d3.min(models, m => m.q25);
const xMax = d3.max(models, m => m.q75);
const xPadding = (xMax - xMin) * 0.1;
xScale
.domain([Math.min(0.6, xMin - xPadding), Math.max(2.4, xMax + xPadding)])
.range([0, innerWidth]);
// Y scale: categorical (model names)
yScale
.domain(models.map(m => m.name))
.range([0, innerHeight])
.padding(0.4);
// Grid lines (vertical)
const xTicks = xScale.ticks(8);
gGrid.selectAll('.grid-x')
.data(xTicks)
.join('line')
.attr('class', 'grid-x')
.attr('x1', d => xScale(d))
.attr('x2', d => xScale(d))
.attr('y1', 0)
.attr('y2', innerHeight);
// Reference line at x=1
gReference.selectAll('.reference-line')
.data([1])
.join('line')
.attr('class', 'reference-line')
.attr('x1', d => xScale(d))
.attr('x2', d => xScale(d))
.attr('y1', 0)
.attr('y2', innerHeight);
// Axes
const tickSize = 6;
gAxes.selectAll('.x-axis')
.data([0])
.join('g')
.attr('class', 'x-axis')
.attr('transform', `translate(0,${innerHeight})`)
.call(d3.axisBottom(xScale)
.ticks(8)
.tickFormat(d3.format('.2f'))
.tickSizeInner(-tickSize)
.tickSizeOuter(0));
gAxes.selectAll('.y-axis')
.data([0])
.join('g')
.attr('class', 'y-axis')
.call(d3.axisLeft(yScale)
.tickSizeInner(-tickSize)
.tickSizeOuter(0));
// X-axis label
gAxes.selectAll('.x-label')
.data([0])
.join('text')
.attr('class', 'x-label axis-label')
.attr('x', innerWidth / 2)
.attr('y', innerHeight + 40)
.attr('text-anchor', 'middle')
.text('Complexity Ratio (Tentative / Actual)');
// Subtitle
gAxes.selectAll('.subtitle')
.data([0])
.join('text')
.attr('class', 'subtitle')
.attr('x', innerWidth / 2)
.attr('y', innerHeight + 54)
.attr('text-anchor', 'middle')
.text('>1: Overcomplicates | <1: Oversimplifies | =1: Matches complexity');
const bandHeight = yScale.bandwidth();
const capHeight = bandHeight * 0.4;
const pointSize = Math.min(8, bandHeight * 0.35);
// Whiskers (IQR lines)
gWhiskers.selectAll('.whisker-line')
.data(models, d => d.name)
.join('line')
.attr('class', 'whisker-line')
.attr('x1', d => xScale(d.q25))
.attr('x2', d => xScale(d.q75))
.attr('y1', d => yScale(d.name) + bandHeight / 2)
.attr('y2', d => yScale(d.name) + bandHeight / 2)
.attr('stroke', d => d.color);
// Left whisker caps
gWhiskers.selectAll('.whisker-cap-left')
.data(models, d => d.name)
.join('line')
.attr('class', 'whisker-cap whisker-cap-left')
.attr('x1', d => xScale(d.q25))
.attr('x2', d => xScale(d.q25))
.attr('y1', d => yScale(d.name) + bandHeight / 2 - capHeight / 2)
.attr('y2', d => yScale(d.name) + bandHeight / 2 + capHeight / 2)
.attr('stroke', d => d.color);
// Right whisker caps
gWhiskers.selectAll('.whisker-cap-right')
.data(models, d => d.name)
.join('line')
.attr('class', 'whisker-cap whisker-cap-right')
.attr('x1', d => xScale(d.q75))
.attr('x2', d => xScale(d.q75))
.attr('y1', d => yScale(d.name) + bandHeight / 2 - capHeight / 2)
.attr('y2', d => yScale(d.name) + bandHeight / 2 + capHeight / 2)
.attr('stroke', d => d.color);
// Model points - circles for closed, squares for open
const closedModels = models.filter(m => !m.is_open);
const openModels = models.filter(m => m.is_open);
// Closed models: circles
gPoints.selectAll('.model-point-circle')
.data(closedModels, d => d.name)
.join('circle')
.attr('class', 'model-point model-point-circle')
.attr('cx', d => xScale(d.median_ratio))
.attr('cy', d => yScale(d.name) + bandHeight / 2)
.attr('r', pointSize)
.attr('fill', d => d.color)
.attr('stroke', d => d.color)
.on('mouseenter', (event, d) => showTooltip(event, d))
.on('mousemove', (event, d) => showTooltip(event, d))
.on('mouseleave', hideTooltip);
// Open models: squares
gPoints.selectAll('.model-point-square')
.data(openModels, d => d.name)
.join('rect')
.attr('class', 'model-point model-point-square')
.attr('x', d => xScale(d.median_ratio) - pointSize)
.attr('y', d => yScale(d.name) + bandHeight / 2 - pointSize)
.attr('width', pointSize * 2)
.attr('height', pointSize * 2)
.attr('fill', 'none')
.attr('stroke', d => d.color)
.attr('stroke-width', 2)
.on('mouseenter', (event, d) => showTooltip(event, d))
.on('mousemove', (event, d) => showTooltip(event, d))
.on('mouseleave', hideTooltip);
// Ratio labels on the right
gLabels.selectAll('.ratio-label')
.data(models, d => d.name)
.join('text')
.attr('class', 'ratio-label')
.attr('x', innerWidth + 8)
.attr('y', d => yScale(d.name) + bandHeight / 2)
.attr('dy', '0.35em')
.text(d => `${d.median_ratio.toFixed(2)} (n=${d.count})`);
// Legend
const legendY = -15;
const legendItems = [
{ label: 'Closed model', shape: 'circle' },
{ label: 'Open model', shape: 'square' }
];
const legendGroup = gLegend.selectAll('.legend-item')
.data(legendItems)
.join('g')
.attr('class', 'legend-item')
.attr('transform', (d, i) => `translate(${innerWidth - 80 - i * 100}, ${legendY})`);
legendGroup.selectAll('.legend-shape-circle')
.data(d => d.shape === 'circle' ? [d] : [])
.join('circle')
.attr('class', 'legend-shape-circle')
.attr('cx', 0)
.attr('cy', 0)
.attr('r', 5)
.attr('fill', 'var(--muted-color)');
legendGroup.selectAll('.legend-shape-square')
.data(d => d.shape === 'square' ? [d] : [])
.join('rect')
.attr('class', 'legend-shape-square')
.attr('x', -5)
.attr('y', -5)
.attr('width', 10)
.attr('height', 10)
.attr('fill', 'none')
.attr('stroke', 'var(--muted-color)')
.attr('stroke-width', 2);
legendGroup.selectAll('.legend-text')
.data(d => [d])
.join('text')
.attr('class', 'legend-text')
.attr('x', 10)
.attr('y', 0)
.attr('dy', '0.35em')
.text(d => d.label);
}
// 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>