dlouapre's picture
dlouapre HF Staff
Improving
526a765
<div class="d3-banner-scatter"></div>
<style>
.d3-banner-scatter {
width: 100%;
margin: 10px 0;
position: relative;
font-family: system-ui, -apple-system, sans-serif;
}
.d3-banner-scatter svg {
display: block;
width: 100%;
height: auto;
}
.d3-banner-scatter .axes path,
.d3-banner-scatter .axes line {
stroke: var(--axis-color, var(--text-color));
}
.d3-banner-scatter .axes text {
fill: var(--tick-color, var(--muted-color));
font-size: 18px;
}
.d3-banner-scatter .grid line {
stroke: var(--grid-color, rgba(0,0,0,.15));
}
.d3-banner-scatter .axes text.axis-label {
font-size: 24px;
font-weight: 500;
fill: var(--text-color);
}
.d3-banner-scatter .x-axis text {
transform: translateY(4px);
}
.d3-banner-scatter .point {
cursor: pointer;
transition: opacity 0.15s ease;
}
.d3-banner-scatter .point:hover {
opacity: 0.8;
}
.d3-banner-scatter .point-label {
font-size: 11px;
fill: var(--text-color);
pointer-events: none;
}
.d3-banner-scatter .annotation {
font-size: 11px;
font-style: italic;
fill: var(--muted-color);
}
.d3-banner-scatter .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-banner-scatter .d3-tooltip .model-name {
font-weight: 600;
margin-bottom: 4px;
}
.d3-banner-scatter .d3-tooltip .metric {
display: flex;
justify-content: space-between;
gap: 16px;
}
.d3-banner-scatter .d3-tooltip .metric-label {
color: var(--muted-color);
}
.d3-banner-scatter .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-banner-scatter'))) {
const candidates = Array.from(document.querySelectorAll('.d3-banner-scatter'))
.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');
// Add gradient definition
const defs = svg.append('defs');
const gradient = defs.append('linearGradient')
.attr('id', 'banner-recklessness-gradient')
.attr('x1', '0%')
.attr('x2', '100%')
.attr('y1', '0%')
.attr('y2', '0%');
// Gradient stops: red -> orange -> yellow -> green -> yellow -> orange -> red
gradient.append('stop').attr('offset', '0%').attr('stop-color', 'rgba(239, 83, 80, 0.25)'); // red
gradient.append('stop').attr('offset', '20%').attr('stop-color', 'rgba(255, 152, 0, 0.25)'); // orange
gradient.append('stop').attr('offset', '35%').attr('stop-color', 'rgba(255, 235, 59, 0.25)'); // yellow
gradient.append('stop').attr('offset', '50%').attr('stop-color', 'rgba(102, 187, 106, 0.35)'); // green
gradient.append('stop').attr('offset', '65%').attr('stop-color', 'rgba(255, 235, 59, 0.25)'); // yellow
gradient.append('stop').attr('offset', '80%').attr('stop-color', 'rgba(255, 152, 0, 0.25)'); // orange
gradient.append('stop').attr('offset', '100%').attr('stop-color', 'rgba(239, 83, 80, 0.25)'); // red
// Arrowhead marker
defs.append('marker')
.attr('id', 'banner-arrow-improvement')
.attr('viewBox', '0 0 10 10')
.attr('refX', 8)
.attr('refY', 5)
.attr('markerWidth', 6)
.attr('markerHeight', 6)
.attr('orient', 'auto-start-reverse')
.append('path')
.attr('d', 'M 0 1 L 8 5 L 0 9 Z')
.attr('fill', '#d32f2f');
const gRoot = svg.append('g');
// Chart groups (order matters for layering)
const gBackground = gRoot.append('g').attr('class', 'background');
const gGrid = gRoot.append('g').attr('class', 'grid');
const gAxes = gRoot.append('g').attr('class', 'axes');
const gAnnotations = gRoot.append('g').attr('class', 'annotations');
const gPoints = gRoot.append('g').attr('class', 'points');
const gArrows = gRoot.append('g').attr('class', 'arrows');
const gLabels = gRoot.append('g').attr('class', 'labels');
// State
let data = null;
let width = 800;
let height = 450;
const margin = { top: 20, right: 120, bottom: 56, left: 72 };
// Scales
const xScale = d3.scaleLinear();
const yScale = d3.scaleLinear();
// Data loading
const DATA_URL = '/data/score_vs_recklessness.json';
// Helper function to create a 5-point star path
const starPath = (cx, cy, outerR, innerR) => {
const points = [];
for (let i = 0; i < 10; i++) {
const r = i % 2 === 0 ? outerR : innerR;
const angle = (Math.PI / 2) + (i * Math.PI / 5);
points.push([cx + r * Math.cos(angle), cy - r * Math.sin(angle)]);
}
return 'M' + points.map(p => p.join(',')).join('L') + 'Z';
};
function updateSize() {
width = container.clientWidth || 800;
height = Math.max(300, Math.round(width / 1.5));
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 showTooltip(event, d) {
const rect = container.getBoundingClientRect();
const x = event.clientX - rect.left;
const y = event.clientY - rect.top;
tip.innerHTML = `
<div class="model-name" style="color: ${d.color}">${d.name}</div>
<div class="metric">
<span class="metric-label">Score:</span>
<span class="metric-value">${d.avg_floored_score.toFixed(1)}</span>
</div>
<div class="metric">
<span class="metric-label">Recklessness Index:</span>
<span class="metric-value">${d.recklessness_index.toFixed(2)}</span>
</div>
<div class="metric">
<span class="metric-label">Failed Guesses:</span>
<span class="metric-value">${d.avg_failed_guesses.toFixed(2)}</span>
</div>
<div class="metric">
<span class="metric-label">Caution:</span>
<span class="metric-value">${d.avg_caution.toFixed(2)}</span>
</div>
<div class="metric">
<span class="metric-label">Type:</span>
<span class="metric-value">${d.is_open ? 'Open' : 'Closed'}</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 render() {
if (!data) return;
const { innerWidth, innerHeight } = updateSize();
const models = data.models;
// Fixed symmetric X scale from -8 to 8
xScale
.domain([-8, 8])
.range([0, innerWidth]);
// Y scale based on data
const yExtent = d3.extent(models, d => d.avg_floored_score);
const yPadding = (yExtent[1] - yExtent[0]) * 0.1;
yScale
.domain([yExtent[0], yExtent[1] + yPadding])
.range([innerHeight, 0])
.nice();
// Background gradient rectangle
gBackground.selectAll('.bg-gradient')
.data([0])
.join('rect')
.attr('class', 'bg-gradient')
.attr('x', 0)
.attr('y', 0)
.attr('width', innerWidth)
.attr('height', innerHeight)
.attr('fill', 'url(#banner-recklessness-gradient)');
// Grid lines
const xTicks = xScale.ticks(8);
const yTicks = yScale.ticks(6);
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);
gGrid.selectAll('.grid-y')
.data(yTicks)
.join('line')
.attr('class', 'grid-y')
.attr('x1', 0)
.attr('x2', innerWidth)
.attr('y1', d => yScale(d))
.attr('y2', d => yScale(d));
// Axes with inner ticks
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).tickSizeInner(-tickSize).tickSizeOuter(0));
gAxes.selectAll('.y-axis')
.data([0])
.join('g')
.attr('class', 'y-axis')
.call(d3.axisLeft(yScale).ticks(6).tickSizeInner(-tickSize).tickSizeOuter(0));
// Axis labels
gAxes.selectAll('.x-label')
.data([0])
.join('text')
.attr('class', 'x-label axis-label')
.attr('x', innerWidth / 2)
.attr('y', innerHeight + 44)
.attr('text-anchor', 'middle')
.text('Boldness Index');
gAxes.selectAll('.y-label')
.data([0])
.join('text')
.attr('class', 'y-label axis-label')
.attr('x', -innerHeight / 2)
.attr('y', -52)
.attr('text-anchor', 'middle')
.attr('transform', 'rotate(-90)')
.text('Score');
// Top annotations: Overcautious / Cautious / Measured / Bold / Reckless
const annotations = [
{ label: 'Overcautious', color: 'rgba(239, 83, 80, 0.9)', pos: 0.07}, // red
{ label: 'Cautious', color: 'rgba(255, 180, 0, 0.9)', pos: 0.25 }, // yellow/orange
{ label: 'Measured', color: 'rgba(76, 175, 80, 0.9)', pos: 0.5 }, // green
{ label: 'Bold', color: 'rgba(255, 180, 0, 0.9)', pos: 0.75 }, // yellow/orange
{ label: 'Reckless', color: 'rgba(239, 83, 80, 0.9)', pos: 0.95 } // red
];
gAnnotations.selectAll('.annotation-label')
.data(annotations, d => d.label)
.join('text')
.attr('class', 'annotation annotation-label')
.attr('x', d => d.pos * innerWidth)
.attr('y', 16)
.attr('text-anchor', d => d.pos === 0 ? 'start' : d.pos === 1 ? 'end' : 'middle')
.style('fill', d => d.color)
.style('font-weight', 'bold')
.style('font-size', '13px')
.text(d => d.label);
// Points
const pointRadius = Math.max(8, Math.min(14, innerWidth / 60));
// Closed models as filled circles
const closedModels = models.filter(d => !d.is_open);
gPoints.selectAll('.point-closed')
.data(closedModels, d => d.name)
.join('circle')
.attr('class', 'point point-closed')
.attr('cx', d => xScale(d.recklessness_index))
.attr('cy', d => yScale(d.avg_floored_score))
.attr('r', pointRadius)
.attr('fill', d => d.color)
.attr('stroke', 'none')
.on('mouseenter', showTooltip)
.on('mousemove', showTooltip)
.on('mouseleave', hideTooltip);
// Open models as stars
const openModels = models.filter(d => d.is_open);
gPoints.selectAll('.point-star')
.data(openModels, d => d.name)
.join('path')
.attr('class', 'point point-star')
.attr('d', d => starPath(xScale(d.recklessness_index), yScale(d.avg_floored_score), pointRadius * 1.2, pointRadius * 0.5))
.attr('fill', d => d.color)
.attr('stroke', 'none')
.on('mouseenter', showTooltip)
.on('mousemove', showTooltip)
.on('mouseleave', hideTooltip);
// Point labels with smart positioning
gLabels.selectAll('.point-label')
.data(models, d => d.name)
.join('text')
.attr('class', 'point-label')
.attr('x', d => {
const xPos = xScale(d.recklessness_index);
if (xPos > innerWidth - 100) {
return xPos - pointRadius - 6;
}
return xPos + pointRadius + 6;
})
.attr('y', d => yScale(d.avg_floored_score) + 4)
.attr('text-anchor', d => {
const xPos = xScale(d.recklessness_index);
return xPos > innerWidth - 100 ? 'end' : 'start';
})
.text(d => d.name);
// Improvement arrows: two curved arrows converging near (0, 17.5)
// Control points set so tangent at start is 45° (direction ±1,+1 in data space)
const arrowData = [
// { x0: -2, y0: 15.25, x1: -0.25, y1: 16.5, cpx: -0.25, cpy: 16.0 }, // from left
// { x0: -2, y0: 15.5, x1: -0.25, y1: 17.0, cpx: -0.5, cpy: 16.25 }, // from left
// { x0: -2, y0: 15.75, x1: -0.25, y1: 17.75, cpx: -0.5, cpy: 16.5 }, // from left
// { x0: 2, y0: 15.25, x1: 0.25, y1: 17., cpx: 0.25, cpy: 16.5 }, // from right
// { x0: 2, y0: 15.5, x1: 0.25, y1: 17.5, cpx: 0.5, cpy: 16.5 }, // from right
// { x0: 2, y0: 15.75, x1: 0.45, y1: 17.75, cpx: 0.5, cpy: 16.5 } // from right
];
gArrows.selectAll('.improvement-arrow')
.data(arrowData)
.join('path')
.attr('class', 'improvement-arrow')
.attr('d', d => {
const sx = xScale(d.x0), sy = yScale(d.y0);
const ex = xScale(d.x1), ey = yScale(d.y1);
const cx = xScale(d.cpx), cy = yScale(d.cpy);
return `M ${sx} ${sy} Q ${cx} ${cy} ${ex} ${ey}`;
})
.attr('fill', 'none')
.style('stroke', '#d32f2f')
.attr('stroke-width', 2.5)
.attr('stroke-dasharray', '8 4')
.attr('marker-end', 'url(#banner-arrow-improvement)')
.attr('opacity', 0.7);
}
// 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>