|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
<div class="d3-two-charts"></div> |
|
|
<style> |
|
|
.d3-two-charts { |
|
|
position: relative; |
|
|
width: 100%; |
|
|
} |
|
|
|
|
|
|
|
|
.d3-two-charts__grid { |
|
|
display: flex; |
|
|
gap: 20px; |
|
|
flex-wrap: wrap; |
|
|
width: 100%; |
|
|
} |
|
|
|
|
|
.chart-cell { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
position: relative; |
|
|
padding: 16px; |
|
|
box-shadow: inset 0 0 0 1px var(--border-color); |
|
|
border-radius: 8px; |
|
|
background: var(--surface-bg, #fff); |
|
|
flex: 1; |
|
|
min-width: 300px; |
|
|
box-sizing: border-box; |
|
|
} |
|
|
|
|
|
.chart-cell__title { |
|
|
font-size: 14px; |
|
|
font-weight: 700; |
|
|
color: var(--text-color); |
|
|
margin-bottom: 12px; |
|
|
} |
|
|
|
|
|
.chart-cell__stat { |
|
|
margin-top: 16px; |
|
|
padding: 10px 12px; |
|
|
background: var(--page-bg, #f9fafb); |
|
|
border-radius: 6px; |
|
|
text-align: center; |
|
|
font-size: 13px; |
|
|
color: var(--text-color); |
|
|
border: 1px solid var(--border-color, #e5e7eb); |
|
|
} |
|
|
|
|
|
.chart-cell__stat-label { |
|
|
font-weight: 600; |
|
|
margin-right: 6px; |
|
|
} |
|
|
|
|
|
.chart-cell__stat-value { |
|
|
font-weight: 700; |
|
|
font-size: 15px; |
|
|
} |
|
|
|
|
|
.chart-cell__body { |
|
|
position: relative; |
|
|
width: 100%; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
.chart-cell__body svg { |
|
|
max-width: 100%; |
|
|
height: auto; |
|
|
display: block; |
|
|
} |
|
|
|
|
|
|
|
|
.chart-cell__legend { |
|
|
display: flex; |
|
|
flex-wrap: wrap; |
|
|
gap: 8px 14px; |
|
|
justify-content: center; |
|
|
margin-top: 12px; |
|
|
font-size: 11px; |
|
|
color: var(--text-color); |
|
|
} |
|
|
|
|
|
.chart-cell__legend .item { |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
gap: 6px; |
|
|
white-space: nowrap; |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.chart-cell__legend .swatch { |
|
|
width: 12px; |
|
|
height: 12px; |
|
|
border-radius: 2px; |
|
|
border: 1px solid var(--border-color); |
|
|
display: inline-block; |
|
|
} |
|
|
|
|
|
|
|
|
.chart-cell .reset-button { |
|
|
position: absolute; |
|
|
top: 16px; |
|
|
right: 16px; |
|
|
z-index: 10; |
|
|
display: none; |
|
|
opacity: 0; |
|
|
transition: opacity 0.2s ease; |
|
|
font-size: 11px; |
|
|
padding: 4px 8px; |
|
|
border-radius: 4px; |
|
|
background: var(--surface-bg); |
|
|
color: var(--text-color); |
|
|
border: 1px solid var(--border-color); |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.chart-cell .reset-button:hover { |
|
|
background: var(--page-bg); |
|
|
} |
|
|
|
|
|
|
|
|
.d3-two-charts .axes path.domain { |
|
|
stroke: var(--axis-color, #e5e7eb); |
|
|
stroke-width: 1; |
|
|
} |
|
|
|
|
|
.d3-two-charts .axes line { |
|
|
stroke: var(--axis-color, #e5e7eb); |
|
|
} |
|
|
|
|
|
.d3-two-charts .axes text { |
|
|
fill: var(--tick-color, #6b7280); |
|
|
font-size: 10px; |
|
|
} |
|
|
|
|
|
.d3-two-charts .axis-label { |
|
|
fill: var(--text-color); |
|
|
font-size: 10px; |
|
|
font-weight: 300; |
|
|
opacity: 0.7; |
|
|
stroke: var(--page-bg, white); |
|
|
stroke-width: 3px; |
|
|
paint-order: stroke fill; |
|
|
} |
|
|
|
|
|
.d3-two-charts .grid line { |
|
|
stroke: var(--grid-color, #f3f4f6); |
|
|
} |
|
|
|
|
|
|
|
|
.d3-two-charts path.main-line { |
|
|
transition: opacity 0.2s ease; |
|
|
} |
|
|
|
|
|
.d3-two-charts path.ghost-line { |
|
|
transition: opacity 0.6s ease; |
|
|
} |
|
|
|
|
|
|
|
|
.d3-two-charts.hovering path.main-line.ghost { |
|
|
opacity: .25; |
|
|
} |
|
|
|
|
|
.d3-two-charts.hovering path.ghost-line.ghost { |
|
|
opacity: .05; |
|
|
} |
|
|
|
|
|
.d3-two-charts.hovering .chart-cell__legend .item.ghost { |
|
|
opacity: .35; |
|
|
} |
|
|
|
|
|
|
|
|
.d3-two-charts .d3-tooltip { |
|
|
z-index: 20; |
|
|
backdrop-filter: saturate(1.12) blur(8px); |
|
|
} |
|
|
|
|
|
.d3-two-charts .d3-tooltip__inner { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 6px; |
|
|
min-width: 180px; |
|
|
} |
|
|
|
|
|
.d3-two-charts .d3-tooltip__inner>div:first-child { |
|
|
font-weight: 800; |
|
|
letter-spacing: 0.1px; |
|
|
margin-bottom: 0; |
|
|
} |
|
|
|
|
|
.d3-two-charts .d3-tooltip__inner>div:nth-child(2) { |
|
|
font-size: 11px; |
|
|
color: var(--muted-color, #9ca3af); |
|
|
display: block; |
|
|
margin-top: -4px; |
|
|
margin-bottom: 2px; |
|
|
letter-spacing: 0.1px; |
|
|
} |
|
|
|
|
|
.d3-two-charts .d3-tooltip__inner>div:nth-child(n+3) { |
|
|
padding-top: 6px; |
|
|
border-top: 1px solid var(--border-color); |
|
|
} |
|
|
|
|
|
.d3-two-charts .d3-tooltip__color-dot { |
|
|
display: inline-block; |
|
|
width: 12px; |
|
|
height: 12px; |
|
|
border-radius: 3px; |
|
|
border: 1px solid var(--border-color); |
|
|
} |
|
|
</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-two-charts'))) { |
|
|
const cs = Array.from(document.querySelectorAll('.d3-two-charts')).filter(el => !(el.dataset && el.dataset.mounted === 'true')); |
|
|
container = cs[cs.length - 1] || null; |
|
|
} |
|
|
if (!container) return; |
|
|
if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; } |
|
|
|
|
|
const d3 = window.d3; |
|
|
|
|
|
|
|
|
const languageMap = { |
|
|
'Arabic': 'ar', 'Turkish': 'tr', 'Swahili': 'sw', 'Russian': 'ru', |
|
|
'Telugu': 'te', 'Thai': 'th', 'Chinese': 'zh', 'French': 'fr', 'Hindi': 'hi' |
|
|
}; |
|
|
|
|
|
|
|
|
const runNameMap = { |
|
|
"orion": "Dataset-A", |
|
|
"helios": "Dataset-B", |
|
|
"lynx": "Dataset-C", |
|
|
"aquila": "Dataset-D", |
|
|
"commoncrawl": "CommonCrawl", |
|
|
"baseline": "Baseline" |
|
|
}; |
|
|
|
|
|
function processRunName(runname) { |
|
|
if (!runname || typeof runname !== 'string') { |
|
|
return String(runname || 'Unknown'); |
|
|
} |
|
|
for (const [key, value] of Object.entries(runNameMap)) { |
|
|
if (runname.toLowerCase().includes(key.toLowerCase())) { |
|
|
return value; |
|
|
} |
|
|
} |
|
|
return runname; |
|
|
} |
|
|
|
|
|
|
|
|
function readEmbedConfig() { |
|
|
let mountEl = container; |
|
|
while (mountEl && !mountEl.getAttribute?.('data-config')) { |
|
|
mountEl = mountEl.parentElement; |
|
|
} |
|
|
|
|
|
let providedConfig = null; |
|
|
try { |
|
|
const cfg = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-config') : null; |
|
|
if (cfg && cfg.trim()) { |
|
|
providedConfig = cfg.trim().startsWith('{') ? JSON.parse(cfg) : cfg; |
|
|
} |
|
|
} catch (e) { |
|
|
console.error('Failed to parse data-config', e); |
|
|
} |
|
|
return providedConfig || {}; |
|
|
} |
|
|
|
|
|
const embedConfig = readEmbedConfig(); |
|
|
|
|
|
|
|
|
const CONFIG = { |
|
|
charts: embedConfig.charts || [], |
|
|
smoothing: embedConfig.smoothing !== undefined ? embedConfig.smoothing : false, |
|
|
smoothingWindow: embedConfig.smoothingWindow || 3, |
|
|
smoothingCurve: embedConfig.smoothingCurve || 'monotoneX', |
|
|
chartHeight: 280, |
|
|
margin: { top: 20, right: 20, bottom: 40, left: 50 }, |
|
|
zoomExtent: [1.0, 8], |
|
|
xAxisLabel: embedConfig.xAxisLabel || 'Tokens (B)', |
|
|
yAxisLabel: embedConfig.yAxisLabel || 'Score', |
|
|
baseUrl: embedConfig.baseUrl || './finetasks/data', |
|
|
statLabel: embedConfig.statLabel || null, |
|
|
statColumn: embedConfig.statColumn || 'avg_spearman', |
|
|
groupSeeds: embedConfig.groupSeeds !== undefined ? embedConfig.groupSeeds : true |
|
|
}; |
|
|
|
|
|
|
|
|
const statColumnMap = { |
|
|
'Monotonicity': 'avg_spearman', |
|
|
'SNR': 'avg_snr', |
|
|
'Kendall\'s Tau': 'avg_kendall_tau_a', |
|
|
'Distance from baseline': 'max_n_std', |
|
|
'Non-Randomness': 'max_n_std', |
|
|
'Randomness': 'max_n_std' |
|
|
}; |
|
|
|
|
|
|
|
|
if (CONFIG.statLabel && !embedConfig.statColumn && statColumnMap[CONFIG.statLabel]) { |
|
|
CONFIG.statColumn = statColumnMap[CONFIG.statLabel]; |
|
|
} |
|
|
|
|
|
if (!CONFIG.charts.length || CONFIG.charts.length !== 2) { |
|
|
container.innerHTML = '<p style="color: var(--danger); font-size: 12px;">Error: Exactly 2 charts must be configured</p>'; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
const grid = document.createElement('div'); |
|
|
grid.className = 'd3-two-charts__grid'; |
|
|
container.appendChild(grid); |
|
|
|
|
|
|
|
|
CONFIG.charts.forEach((chartConfig, idx) => { |
|
|
const cell = document.createElement('div'); |
|
|
cell.className = 'chart-cell'; |
|
|
cell.innerHTML = ` |
|
|
<div class="chart-cell__title">${chartConfig.title}</div> |
|
|
<button class="reset-button">Reset</button> |
|
|
<div class="chart-cell__body"></div> |
|
|
<div class="chart-cell__legend"></div> |
|
|
<div class="chart-cell__stat"></div> |
|
|
`; |
|
|
grid.appendChild(cell); |
|
|
}); |
|
|
|
|
|
|
|
|
const getCurve = (smooth) => { |
|
|
if (!smooth) return d3.curveLinear; |
|
|
switch (CONFIG.smoothingCurve) { |
|
|
case 'catmullRom': return d3.curveCatmullRom.alpha(0.5); |
|
|
case 'monotoneX': return d3.curveMonotoneX; |
|
|
case 'basis': return d3.curveBasis; |
|
|
default: return d3.curveLinear; |
|
|
} |
|
|
}; |
|
|
|
|
|
function movingAverage(values, windowSize) { |
|
|
if (!Array.isArray(values) || values.length === 0 || windowSize <= 1) return values; |
|
|
const half = Math.floor(windowSize / 2); |
|
|
const out = new Array(values.length); |
|
|
for (let i = 0; i < values.length; i++) { |
|
|
let sum = 0; let count = 0; |
|
|
const start = Math.max(0, i - half); |
|
|
const end = Math.min(values.length - 1, i + half); |
|
|
for (let j = start; j <= end; j++) { if (!Number.isNaN(values[j].value)) { sum += values[j].value; count++; } } |
|
|
const avg = count ? (sum / count) : values[i].value; |
|
|
out[i] = { step: values[i].step, value: avg }; |
|
|
} |
|
|
return out; |
|
|
} |
|
|
|
|
|
function applySmoothing(values, smooth) { |
|
|
if (!smooth) return values; |
|
|
return movingAverage(values, CONFIG.smoothingWindow); |
|
|
} |
|
|
|
|
|
|
|
|
function createSmartFormatter(values) { |
|
|
if (!values || values.length === 0) return (v) => v; |
|
|
|
|
|
const min = d3.min(values); |
|
|
const max = d3.max(values); |
|
|
const range = max - min; |
|
|
|
|
|
const allIntegers = values.every(v => Math.abs(v - Math.round(v)) < 0.001); |
|
|
|
|
|
if (max >= 1e9) { |
|
|
return (v) => { |
|
|
const billions = v / 1e9; |
|
|
return allIntegers && billions === Math.round(billions) |
|
|
? d3.format('d')(Math.round(billions)) + 'B' |
|
|
: d3.format('.2f')(billions) + 'B'; |
|
|
}; |
|
|
} |
|
|
|
|
|
if (max >= 1e6) { |
|
|
return (v) => { |
|
|
const millions = v / 1e6; |
|
|
return allIntegers && millions === Math.round(millions) |
|
|
? d3.format('d')(Math.round(millions)) + 'M' |
|
|
: d3.format('.2f')(millions) + 'M'; |
|
|
}; |
|
|
} |
|
|
|
|
|
if (max >= 1000 && range >= 100) { |
|
|
return (v) => { |
|
|
const thousands = v / 1000; |
|
|
return allIntegers && thousands === Math.round(thousands) |
|
|
? d3.format('d')(Math.round(thousands)) + 'k' |
|
|
: d3.format('.1f')(thousands) + 'k'; |
|
|
}; |
|
|
} |
|
|
|
|
|
if (allIntegers) { |
|
|
return (v) => d3.format('d')(Math.round(v)); |
|
|
} |
|
|
|
|
|
if (range < 1) { |
|
|
return (v) => d3.format('.3f')(v); |
|
|
} else if (range < 10) { |
|
|
return (v) => d3.format('.2f')(v); |
|
|
} else { |
|
|
return (v) => d3.format('.1f')(v); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const getRunColors = (n) => { |
|
|
try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') return window.ColorPalettes.getColors('categorical', n); } catch (_) { } |
|
|
const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB'; |
|
|
return [primary, '#4EA5B7', '#E38A42', '#CEC0FA', '#9B59B6', '#16A085', ...(d3.schemeTableau10 || [])].slice(0, n); |
|
|
}; |
|
|
|
|
|
|
|
|
function initChart(cellElement, chartConfig) { |
|
|
const bodyEl = cellElement.querySelector('.chart-cell__body'); |
|
|
const resetBtn = cellElement.querySelector('.reset-button'); |
|
|
const legendEl = cellElement.querySelector('.chart-cell__legend'); |
|
|
const statEl = cellElement.querySelector('.chart-cell__stat'); |
|
|
|
|
|
let smoothEnabled = CONFIG.smoothing; |
|
|
let hasMoved = false; |
|
|
let allData = []; |
|
|
let runList = []; |
|
|
let runColorMap = {}; |
|
|
let baseline = null; |
|
|
let monotonicity = null; |
|
|
|
|
|
|
|
|
let tip = cellElement.querySelector('.d3-tooltip'); |
|
|
let tipInner; |
|
|
if (!tip) { |
|
|
tip = document.createElement('div'); |
|
|
tip.className = 'd3-tooltip'; |
|
|
Object.assign(tip.style, { |
|
|
position: 'absolute', top: '0px', left: '0px', transform: 'translate(-9999px, -9999px)', pointerEvents: 'none', |
|
|
padding: '10px 12px', borderRadius: '12px', fontSize: '12px', lineHeight: '1.35', border: '1px solid var(--border-color)', |
|
|
background: 'var(--surface-bg)', color: 'var(--text-color)', boxShadow: '0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)', opacity: '0', transition: 'opacity .12s ease', zIndex: '20' |
|
|
}); |
|
|
tipInner = document.createElement('div'); |
|
|
tipInner.className = 'd3-tooltip__inner'; |
|
|
tip.appendChild(tipInner); |
|
|
cellElement.appendChild(tip); |
|
|
} else { |
|
|
tipInner = tip.querySelector('.d3-tooltip__inner') || tip; |
|
|
} |
|
|
|
|
|
|
|
|
const svg = d3.select(bodyEl).append('svg').attr('width', '100%').style('display', 'block'); |
|
|
|
|
|
|
|
|
const clipId = 'clip-' + Math.random().toString(36).slice(2); |
|
|
const clipPath = svg.append('defs').append('clipPath').attr('id', clipId); |
|
|
const clipRect = clipPath.append('rect'); |
|
|
|
|
|
|
|
|
const g = svg.append('g'); |
|
|
const gGrid = g.append('g').attr('class', 'grid'); |
|
|
const gAxes = g.append('g').attr('class', 'axes'); |
|
|
const gPlot = g.append('g').attr('class', 'plot').attr('clip-path', `url(#${clipId})`); |
|
|
const gHover = g.append('g').attr('class', 'hover-layer'); |
|
|
const overlay = g.append('rect').attr('class', 'overlay').attr('fill', 'none').attr('pointer-events', 'all').style('cursor', 'grab') |
|
|
.on('mousedown', function () { |
|
|
d3.select(this).style('cursor', 'grabbing'); |
|
|
tip.style.opacity = '0'; |
|
|
if (hoverLine) hoverLine.style('display', 'none'); |
|
|
}) |
|
|
.on('mouseup', function () { d3.select(this).style('cursor', 'grab'); }); |
|
|
|
|
|
|
|
|
const xScale = d3.scaleLinear(); |
|
|
const yScale = d3.scaleLinear(); |
|
|
|
|
|
|
|
|
let hoverLine = null; |
|
|
let steps = []; |
|
|
let hideTipTimer = null; |
|
|
|
|
|
|
|
|
let formatStep = (v) => v; |
|
|
let formatValue = (v) => v; |
|
|
|
|
|
|
|
|
const zoom = d3.zoom().scaleExtent(CONFIG.zoomExtent).on('zoom', zoomed); |
|
|
overlay.call(zoom); |
|
|
|
|
|
function zoomed(event) { |
|
|
const transform = event.transform; |
|
|
hasMoved = transform.k !== 1 || transform.x !== 0 || transform.y !== 0; |
|
|
updateResetButton(); |
|
|
|
|
|
const newXScale = transform.rescaleX(xScale); |
|
|
const newYScale = transform.rescaleY(yScale); |
|
|
|
|
|
const innerWidth = xScale.range()[1]; |
|
|
|
|
|
|
|
|
const gridTicks = newYScale.ticks(5); |
|
|
gGrid.selectAll('line').data(gridTicks).join('line') |
|
|
.attr('x1', 0).attr('x2', innerWidth) |
|
|
.attr('y1', d => newYScale(d)).attr('y2', d => newYScale(d)) |
|
|
.attr('stroke', 'var(--grid-color)'); |
|
|
|
|
|
|
|
|
const line = d3.line() |
|
|
.x(d => newXScale(d.step)) |
|
|
.y(d => newYScale(d.value)) |
|
|
.curve(getCurve(smoothEnabled)); |
|
|
|
|
|
gPlot.selectAll('path.ghost-line') |
|
|
.attr('d', d => { |
|
|
const rawLine = d3.line().x(d => newXScale(d.step)).y(d => newYScale(d.value)).curve(d3.curveLinear); |
|
|
return rawLine(d.values); |
|
|
}); |
|
|
|
|
|
gPlot.selectAll('path.main-line') |
|
|
.attr('d', d => line(applySmoothing(d.values, smoothEnabled))); |
|
|
|
|
|
|
|
|
const newXTicks = newXScale.ticks(5); |
|
|
const newXAxis = d3.axisBottom(newXScale) |
|
|
.tickValues(newXTicks) |
|
|
.tickSizeOuter(0) |
|
|
.tickFormat(formatStep); |
|
|
gAxes.select('.x-axis').call(newXAxis); |
|
|
|
|
|
|
|
|
const formatValueRounded = (v) => { |
|
|
if (v === 0) return '0'; |
|
|
|
|
|
if (v < 1) { |
|
|
|
|
|
return d3.format('.1f')(Math.round(v * 10) / 10); |
|
|
} else if (v < 10) { |
|
|
|
|
|
return d3.format('d')(Math.round(v)); |
|
|
} else if (v < 100) { |
|
|
|
|
|
return d3.format('d')(Math.round(v / 10) * 10); |
|
|
} else { |
|
|
|
|
|
return d3.format('d')(Math.round(v / 10) * 10); |
|
|
} |
|
|
}; |
|
|
|
|
|
gAxes.select('.y-axis').call(d3.axisLeft(newYScale).ticks(5).tickSizeOuter(0).tickFormat(formatValueRounded)); |
|
|
|
|
|
|
|
|
if (baseline !== null) { |
|
|
gAxes.select('.baseline-line') |
|
|
.attr('y1', newYScale(baseline)) |
|
|
.attr('y2', newYScale(baseline)); |
|
|
gAxes.select('.baseline-label') |
|
|
.attr('y', newYScale(baseline) - 5); |
|
|
} |
|
|
} |
|
|
|
|
|
function updateResetButton() { |
|
|
if (hasMoved) { |
|
|
resetBtn.style.display = 'block'; |
|
|
requestAnimationFrame(() => { resetBtn.style.opacity = '1'; }); |
|
|
} else { |
|
|
resetBtn.style.opacity = '0'; |
|
|
setTimeout(() => { if (!hasMoved) resetBtn.style.display = 'none'; }, 200); |
|
|
} |
|
|
} |
|
|
|
|
|
function render() { |
|
|
const rect = bodyEl.getBoundingClientRect(); |
|
|
const width = Math.max(1, Math.round(rect.width || 400)); |
|
|
const height = CONFIG.chartHeight; |
|
|
svg.attr('width', width).attr('height', height); |
|
|
|
|
|
const margin = CONFIG.margin; |
|
|
const innerWidth = width - margin.left - margin.right; |
|
|
const innerHeight = height - margin.top - margin.bottom; |
|
|
|
|
|
g.attr('transform', `translate(${margin.left},${margin.top})`); |
|
|
|
|
|
if (!allData.length) return; |
|
|
|
|
|
|
|
|
const stepExtent = d3.extent(allData, d => d.step); |
|
|
let valueExtent = d3.extent(allData, d => d.value); |
|
|
|
|
|
|
|
|
if (baseline !== null) { |
|
|
const minValue = Math.min(valueExtent[0], baseline); |
|
|
const maxValue = Math.max(valueExtent[1], baseline); |
|
|
const range = maxValue - minValue; |
|
|
|
|
|
|
|
|
valueExtent = [ |
|
|
minValue - range * 0.1, |
|
|
maxValue + range * 0.1 |
|
|
]; |
|
|
} else { |
|
|
|
|
|
const range = valueExtent[1] - valueExtent[0]; |
|
|
valueExtent = [ |
|
|
valueExtent[0] - range * 0.05, |
|
|
valueExtent[1] + range * 0.05 |
|
|
]; |
|
|
} |
|
|
|
|
|
xScale.domain(stepExtent).range([0, innerWidth]); |
|
|
yScale.domain(valueExtent).range([innerHeight, 0]); |
|
|
|
|
|
|
|
|
const stepValues = allData.map(d => d.step); |
|
|
const metricValues = allData.map(d => d.value); |
|
|
|
|
|
|
|
|
const stepMin = d3.min(stepValues); |
|
|
const stepMax = d3.max(stepValues); |
|
|
|
|
|
|
|
|
formatStep = (v) => { |
|
|
if (v === 0) return '0'; |
|
|
|
|
|
if (v < 0.01) return d3.format('.3f')(v); |
|
|
if (v < 0.1) return d3.format('.2f')(v); |
|
|
if (v < 1) return d3.format('.2f')(v); |
|
|
if (v < 10) return d3.format('.1f')(v); |
|
|
|
|
|
if (Math.abs(v - Math.round(v)) < 0.01) { |
|
|
return d3.format('d')(Math.round(v)); |
|
|
} |
|
|
return d3.format('.1f')(v); |
|
|
}; |
|
|
|
|
|
formatValue = createSmartFormatter(metricValues); |
|
|
|
|
|
|
|
|
clipRect.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight); |
|
|
|
|
|
|
|
|
overlay.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight); |
|
|
|
|
|
|
|
|
zoom.extent([[0, 0], [innerWidth, innerHeight]]) |
|
|
.translateExtent([[0, 0], [innerWidth, innerHeight]]); |
|
|
|
|
|
|
|
|
gGrid.selectAll('line').data(yScale.ticks(5)).join('line') |
|
|
.attr('x1', 0).attr('x2', innerWidth) |
|
|
.attr('y1', d => yScale(d)).attr('y2', d => yScale(d)) |
|
|
.attr('stroke', 'var(--grid-color)'); |
|
|
|
|
|
|
|
|
gAxes.selectAll('*').remove(); |
|
|
|
|
|
|
|
|
const xTicks = xScale.ticks(5); |
|
|
const xAxis = d3.axisBottom(xScale) |
|
|
.tickValues(xTicks) |
|
|
.tickSizeOuter(0) |
|
|
.tickFormat(formatStep); |
|
|
|
|
|
gAxes.append('g').attr('class', 'x-axis').attr('transform', `translate(0,${innerHeight})`) |
|
|
.call(xAxis); |
|
|
|
|
|
|
|
|
const formatValueRounded = (v) => { |
|
|
if (v === 0) return '0'; |
|
|
|
|
|
if (v < 1) { |
|
|
|
|
|
return d3.format('.1f')(Math.round(v * 10) / 10); |
|
|
} else if (v < 10) { |
|
|
|
|
|
return d3.format('d')(Math.round(v)); |
|
|
} else if (v < 100) { |
|
|
|
|
|
return d3.format('d')(Math.round(v / 10) * 10); |
|
|
} else { |
|
|
|
|
|
return d3.format('d')(Math.round(v / 10) * 10); |
|
|
} |
|
|
}; |
|
|
|
|
|
gAxes.append('g').attr('class', 'y-axis') |
|
|
.call(d3.axisLeft(yScale).ticks(5).tickSizeOuter(0).tickFormat(formatValueRounded)); |
|
|
gAxes.selectAll('.domain, .tick line').attr('stroke', 'var(--axis-color)'); |
|
|
gAxes.selectAll('text').attr('fill', 'var(--tick-color)'); |
|
|
|
|
|
|
|
|
gAxes.append('text') |
|
|
.attr('class', 'axis-label') |
|
|
.attr('x', innerWidth / 2) |
|
|
.attr('y', innerHeight + 32) |
|
|
.attr('text-anchor', 'middle') |
|
|
.text(CONFIG.xAxisLabel); |
|
|
|
|
|
gAxes.append('text') |
|
|
.attr('class', 'axis-label') |
|
|
.attr('transform', 'rotate(-90)') |
|
|
.attr('x', -innerHeight / 2) |
|
|
.attr('y', -38) |
|
|
.attr('text-anchor', 'middle') |
|
|
.text(CONFIG.yAxisLabel); |
|
|
|
|
|
|
|
|
if (baseline !== null) { |
|
|
gAxes.append('line') |
|
|
.attr('class', 'baseline-line') |
|
|
.attr('x1', 0) |
|
|
.attr('x2', innerWidth) |
|
|
.attr('y1', yScale(baseline)) |
|
|
.attr('y2', yScale(baseline)) |
|
|
.attr('stroke', 'var(--text-color, #666)') |
|
|
.attr('stroke-width', 1.5) |
|
|
.attr('stroke-dasharray', '5,5') |
|
|
.attr('opacity', 0.5); |
|
|
|
|
|
gAxes.append('text') |
|
|
.attr('class', 'baseline-label') |
|
|
.attr('x', innerWidth - 5) |
|
|
.attr('y', yScale(baseline) - 5) |
|
|
.attr('text-anchor', 'end') |
|
|
.attr('font-size', '10px') |
|
|
.attr('fill', 'var(--text-color, #666)') |
|
|
.attr('opacity', 0.7) |
|
|
.text('Baseline'); |
|
|
} |
|
|
|
|
|
|
|
|
const dataByRun = {}; |
|
|
runList.forEach(run => { dataByRun[run] = []; }); |
|
|
allData.forEach(d => { |
|
|
if (dataByRun[d.run]) dataByRun[d.run].push({ step: d.step, value: d.value }); |
|
|
}); |
|
|
runList.forEach(run => { dataByRun[run].sort((a, b) => a.step - b.step); }); |
|
|
|
|
|
const series = runList.map(run => ({ run, color: runColorMap[run], values: dataByRun[run] })).filter(s => s.values.length > 0); |
|
|
|
|
|
|
|
|
const ghostLine = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value)).curve(d3.curveLinear); |
|
|
gPlot.selectAll('path.ghost-line').data(series, d => d.run).join('path') |
|
|
.attr('class', 'ghost-line') |
|
|
.attr('fill', 'none') |
|
|
.attr('stroke', d => d.color) |
|
|
.attr('stroke-width', 1.5) |
|
|
.attr('opacity', smoothEnabled ? 0.15 : 0) |
|
|
.attr('pointer-events', 'none') |
|
|
.attr('d', d => ghostLine(d.values)); |
|
|
|
|
|
|
|
|
const mainLine = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value)).curve(getCurve(smoothEnabled)); |
|
|
gPlot.selectAll('path.main-line').data(series, d => d.run).join('path') |
|
|
.attr('class', 'main-line') |
|
|
.attr('fill', 'none') |
|
|
.attr('stroke', d => d.color) |
|
|
.attr('stroke-width', 2) |
|
|
.attr('opacity', 0.85) |
|
|
.attr('d', d => mainLine(applySmoothing(d.values, smoothEnabled))); |
|
|
|
|
|
|
|
|
setupHover(series, innerWidth, innerHeight); |
|
|
} |
|
|
|
|
|
function setupHover(series, innerWidth, innerHeight) { |
|
|
gHover.selectAll('*').remove(); |
|
|
|
|
|
hoverLine = gHover.append('line') |
|
|
.style('stroke', 'var(--text-color)') |
|
|
.attr('stroke-opacity', 0.25) |
|
|
.attr('stroke-width', 1) |
|
|
.attr('y1', 0) |
|
|
.attr('y2', innerHeight) |
|
|
.style('display', 'none') |
|
|
.attr('pointer-events', 'none'); |
|
|
|
|
|
const stepSet = new Set(); |
|
|
series.forEach(s => s.values.forEach(v => stepSet.add(v.step))); |
|
|
steps = Array.from(stepSet).sort((a, b) => a - b); |
|
|
|
|
|
overlay.on('mousemove', function (ev) { |
|
|
if (ev.buttons === 0) onHoverMove(ev, series); |
|
|
}).on('mouseleave', onHoverLeave); |
|
|
} |
|
|
|
|
|
function onHoverMove(ev, series) { |
|
|
if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer = null; } |
|
|
|
|
|
const [mx, my] = d3.pointer(ev, overlay.node()); |
|
|
const targetStep = xScale.invert(mx); |
|
|
const nearest = steps.reduce((best, t) => Math.abs(t - targetStep) < Math.abs(best - targetStep) ? t : best, steps[0]); |
|
|
|
|
|
const xpx = xScale(nearest); |
|
|
hoverLine.attr('x1', xpx).attr('x2', xpx).style('display', null); |
|
|
|
|
|
let html = `<div><strong>${chartConfig.title}</strong></div>`; |
|
|
html += `<div>${formatStep(nearest)}</div>`; |
|
|
|
|
|
const entries = series.map(s => { |
|
|
const values = s.values; |
|
|
let before = null, after = null; |
|
|
for (let i = 0; i < values.length; i++) { |
|
|
if (values[i].step <= nearest) before = values[i]; |
|
|
if (values[i].step >= nearest && !after) { after = values[i]; break; } |
|
|
} |
|
|
|
|
|
let interpolatedValue = null; |
|
|
if (before && after && before.step !== after.step) { |
|
|
const t = (nearest - before.step) / (after.step - before.step); |
|
|
interpolatedValue = before.value + t * (after.value - before.value); |
|
|
} else if (before && before.step === nearest) { |
|
|
interpolatedValue = before.value; |
|
|
} else if (after && after.step === nearest) { |
|
|
interpolatedValue = after.value; |
|
|
} else if (before) { |
|
|
interpolatedValue = before.value; |
|
|
} else if (after) { |
|
|
interpolatedValue = after.value; |
|
|
} |
|
|
|
|
|
return { run: s.run, color: s.color, value: interpolatedValue }; |
|
|
}).filter(e => e.value != null); |
|
|
|
|
|
entries.sort((a, b) => b.value - a.value); |
|
|
|
|
|
entries.forEach(e => { |
|
|
html += `<div style="display:flex;align-items:center;gap:8px;"><span class="d3-tooltip__color-dot" style="background:${e.color}"></span><span>${e.run}</span><span style="margin-left:auto;font-weight:normal;">${e.value.toFixed(4)}</span></div>`; |
|
|
}); |
|
|
|
|
|
tipInner.innerHTML = html; |
|
|
const offsetX = 12, offsetY = 12; |
|
|
tip.style.opacity = '1'; |
|
|
tip.style.transform = `translate(${Math.round(mx + offsetX + CONFIG.margin.left)}px, ${Math.round(my + offsetY + CONFIG.margin.top)}px)`; |
|
|
} |
|
|
|
|
|
function onHoverLeave() { |
|
|
hideTipTimer = setTimeout(() => { |
|
|
tip.style.opacity = '0'; |
|
|
tip.style.transform = 'translate(-9999px, -9999px)'; |
|
|
if (hoverLine) hoverLine.style('display', 'none'); |
|
|
}, 100); |
|
|
} |
|
|
|
|
|
|
|
|
resetBtn.addEventListener('click', () => { |
|
|
overlay.transition().duration(750).call(zoom.transform, d3.zoomIdentity); |
|
|
}); |
|
|
|
|
|
|
|
|
async function load() { |
|
|
try { |
|
|
const langCode = languageMap[chartConfig.language] || chartConfig.language.toLowerCase(); |
|
|
const task = chartConfig.task; |
|
|
const baseUrl = window.location.origin; |
|
|
const dataUrl = `${baseUrl}/finetasks/data/${langCode}/${task}_data.csv`; |
|
|
const statsUrl = `${baseUrl}/finetasks/data/${langCode}/${task}_stats.csv`; |
|
|
|
|
|
console.log('Loading D3 data from:', dataUrl); |
|
|
console.log('Loading D3 stats from:', statsUrl); |
|
|
|
|
|
const [dataResponse, statsResponse] = await Promise.all([ |
|
|
fetch(dataUrl, { cache: 'no-cache' }).catch(e => { |
|
|
console.error('Failed to fetch data:', dataUrl, e); |
|
|
throw e; |
|
|
}), |
|
|
fetch(statsUrl, { cache: 'no-cache' }).catch(e => { |
|
|
console.error('Failed to fetch stats:', statsUrl, e); |
|
|
throw e; |
|
|
}) |
|
|
]); |
|
|
|
|
|
console.log('Data response status:', dataResponse.status, dataResponse.statusText); |
|
|
console.log('Stats response status:', statsResponse.status, statsResponse.statusText); |
|
|
|
|
|
if (!dataResponse.ok) throw new Error(`Failed to load data: ${dataResponse.status} ${dataResponse.statusText}`); |
|
|
if (!statsResponse.ok) throw new Error(`Failed to load stats: ${statsResponse.status} ${statsResponse.statusText}`); |
|
|
|
|
|
const csvText = await dataResponse.text(); |
|
|
const statsText = await statsResponse.text(); |
|
|
|
|
|
console.log('CSV text length:', csvText.length); |
|
|
console.log('Stats text length:', statsText.length); |
|
|
|
|
|
|
|
|
const rawRows = d3.csvParse(csvText); |
|
|
const statsRows = d3.csvParse(statsText); |
|
|
|
|
|
if (!rawRows || rawRows.length === 0) { |
|
|
throw new Error('No data found in CSV'); |
|
|
} |
|
|
|
|
|
console.log('Raw CSV columns:', Object.keys(rawRows[0])); |
|
|
console.log('Raw CSV first row:', rawRows[0]); |
|
|
console.log('Looking for metric:', chartConfig.metric); |
|
|
console.log('Total raw rows:', rawRows.length); |
|
|
|
|
|
|
|
|
if (statsRows && statsRows.length > 0) { |
|
|
const statsRow = statsRows.find(row => row.metric === chartConfig.metric); |
|
|
if (statsRow && statsRow[CONFIG.statColumn]) { |
|
|
monotonicity = parseFloat(statsRow[CONFIG.statColumn]); |
|
|
console.log(`${CONFIG.statLabel} value (${CONFIG.statColumn}):`, monotonicity); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
const dataByRun = {}; |
|
|
let baselineValue = null; |
|
|
let skippedRows = 0; |
|
|
|
|
|
rawRows.forEach((row, idx) => { |
|
|
const runname = row.runname || row.run_name || 'unknown'; |
|
|
const seed = row.seed || ''; |
|
|
const tokens = parseFloat(row.tokens); |
|
|
const metricValue = parseFloat(row[chartConfig.metric]); |
|
|
|
|
|
if (isNaN(tokens) || isNaN(metricValue)) { |
|
|
if (idx < 3) { |
|
|
console.log(`Skipping row ${idx}: tokens=${row.tokens}, metric=${row[chartConfig.metric]}`); |
|
|
} |
|
|
skippedRows++; |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
if (runname.toLowerCase().includes('baseline')) { |
|
|
if (baselineValue === null) { |
|
|
baselineValue = metricValue; |
|
|
console.log('Baseline value found:', baselineValue); |
|
|
} |
|
|
return; |
|
|
} |
|
|
|
|
|
const processedName = processRunName(runname); |
|
|
|
|
|
|
|
|
const runKey = CONFIG.groupSeeds ? processedName : `${processedName}_${seed}`; |
|
|
|
|
|
if (!dataByRun[runKey]) { |
|
|
dataByRun[runKey] = {}; |
|
|
} |
|
|
|
|
|
const tokenKey = tokens; |
|
|
if (!dataByRun[runKey][tokenKey]) { |
|
|
dataByRun[runKey][tokenKey] = []; |
|
|
} |
|
|
|
|
|
dataByRun[runKey][tokenKey].push(metricValue); |
|
|
}); |
|
|
|
|
|
console.log('Skipped rows:', skippedRows); |
|
|
console.log('Grouped by runs:', Object.keys(dataByRun)); |
|
|
|
|
|
|
|
|
allData = []; |
|
|
Object.keys(dataByRun).forEach(runName => { |
|
|
Object.keys(dataByRun[runName]).forEach(tokenKey => { |
|
|
const values = dataByRun[runName][tokenKey]; |
|
|
const mean = values.reduce((a, b) => a + b, 0) / values.length; |
|
|
allData.push({ |
|
|
run: runName, |
|
|
step: parseFloat(tokenKey), |
|
|
value: mean |
|
|
}); |
|
|
}); |
|
|
}); |
|
|
|
|
|
console.log('Processed data points:', allData.length); |
|
|
console.log('Unique runs found:', Array.from(new Set(allData.map(d => d.run)))); |
|
|
console.log('Sample data points:', allData.slice(0, 5)); |
|
|
|
|
|
|
|
|
if (allData.length === 0) { |
|
|
throw new Error(`No valid data found for metric: ${chartConfig.metric}`); |
|
|
} |
|
|
|
|
|
|
|
|
baseline = baselineValue; |
|
|
|
|
|
|
|
|
if (statEl && monotonicity !== null && CONFIG.statLabel) { |
|
|
statEl.innerHTML = ` |
|
|
<span class="chart-cell__stat-label">${CONFIG.statLabel}:</span> |
|
|
<span class="chart-cell__stat-value">${monotonicity.toFixed(2)}</span> |
|
|
`; |
|
|
} |
|
|
|
|
|
runList = Array.from(new Set(allData.map(d => d.run))).sort(); |
|
|
|
|
|
if (runList.length === 0) { |
|
|
throw new Error('No runs found in data'); |
|
|
} |
|
|
|
|
|
const colors = getRunColors(runList.length); |
|
|
runList.forEach((run, i) => { runColorMap[run] = colors[i % colors.length]; }); |
|
|
|
|
|
|
|
|
if (legendEl) { |
|
|
legendEl.innerHTML = runList.map(run => { |
|
|
const color = runColorMap[run]; |
|
|
return `<span class="item" data-run="${run}"><span class="swatch" style="background:${color}"></span><span>${run}</span></span>`; |
|
|
}).join(''); |
|
|
|
|
|
|
|
|
legendEl.querySelectorAll('.item').forEach(el => { |
|
|
el.addEventListener('mouseenter', () => { |
|
|
const run = el.getAttribute('data-run'); |
|
|
container.classList.add('hovering'); |
|
|
cellElement.querySelectorAll('path.main-line').forEach(path => { |
|
|
const pathRun = d3.select(path).datum()?.run; |
|
|
path.classList.toggle('ghost', pathRun !== run); |
|
|
}); |
|
|
cellElement.querySelectorAll('path.ghost-line').forEach(path => { |
|
|
const pathRun = d3.select(path).datum()?.run; |
|
|
path.classList.toggle('ghost', pathRun !== run); |
|
|
}); |
|
|
legendEl.querySelectorAll('.item').forEach(it => { |
|
|
it.classList.toggle('ghost', it.getAttribute('data-run') !== run); |
|
|
}); |
|
|
}); |
|
|
|
|
|
el.addEventListener('mouseleave', () => { |
|
|
container.classList.remove('hovering'); |
|
|
cellElement.querySelectorAll('path.main-line').forEach(path => path.classList.remove('ghost')); |
|
|
cellElement.querySelectorAll('path.ghost-line').forEach(path => path.classList.remove('ghost')); |
|
|
legendEl.querySelectorAll('.item').forEach(it => it.classList.remove('ghost')); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
render(); |
|
|
|
|
|
} catch (e) { |
|
|
console.error('Error loading chart:', chartConfig.title, e); |
|
|
const pre = document.createElement('pre'); |
|
|
pre.textContent = 'Error loading data: ' + (e && e.message ? e.message : e); |
|
|
pre.style.color = 'var(--danger, #b00020)'; |
|
|
pre.style.fontSize = '12px'; |
|
|
pre.style.padding = '12px'; |
|
|
pre.style.background = 'var(--surface-bg)'; |
|
|
pre.style.borderRadius = '6px'; |
|
|
pre.style.border = '1px solid var(--danger, #b00020)'; |
|
|
bodyEl.appendChild(pre); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
load(); |
|
|
} catch (e) { |
|
|
console.error('Failed to initialize chart:', chartConfig.title, e); |
|
|
} |
|
|
|
|
|
return { render }; |
|
|
} |
|
|
|
|
|
|
|
|
const cells = Array.from(grid.querySelectorAll('.chart-cell')); |
|
|
const chartInstances = cells.map((cell, idx) => initChart(cell, CONFIG.charts[idx])); |
|
|
|
|
|
|
|
|
let resizeTimer; |
|
|
const handleResize = () => { |
|
|
clearTimeout(resizeTimer); |
|
|
resizeTimer = setTimeout(() => { |
|
|
chartInstances.forEach(chart => chart && chart.render && chart.render()); |
|
|
}, 100); |
|
|
}; |
|
|
|
|
|
const ro = window.ResizeObserver ? new ResizeObserver(handleResize) : null; |
|
|
if (ro) { |
|
|
ro.observe(container); |
|
|
} |
|
|
|
|
|
window.addEventListener('resize', handleResize); |
|
|
|
|
|
setTimeout(() => { |
|
|
chartInstances.forEach(chart => chart && chart.render && chart.render()); |
|
|
}, 100); |
|
|
}; |
|
|
|
|
|
if (document.readyState === 'loading') { |
|
|
document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); |
|
|
} else { |
|
|
ensureD3(bootstrap); |
|
|
} |
|
|
})(); |
|
|
</script> |
|
|
|
|
|
|