Spaces:
Running
Running
| <div class="d3-line" style="width:100%;margin:10px 0;"></div> | |
| <style> | |
| .d3-line .d3-line__controls select { | |
| font-size: 12px; | |
| padding: 8px 28px 8px 10px; | |
| border: 1px solid var(--border-color); | |
| border-radius: 8px; | |
| background-color: var(--surface-bg); | |
| color: var(--text-color); | |
| background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); | |
| background-repeat: no-repeat; | |
| background-position: right 8px center; | |
| background-size: 12px; | |
| -webkit-appearance: none; | |
| -moz-appearance: none; | |
| appearance: none; | |
| cursor: pointer; | |
| transition: border-color .15s ease, box-shadow .15s ease; | |
| } | |
| [data-theme="dark"] .d3-line .d3-line__controls select { | |
| background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); | |
| } | |
| .d3-line .d3-line__controls select:hover { | |
| border-color: var(--primary-color); | |
| } | |
| .d3-line .d3-line__controls select:focus { | |
| border-color: var(--primary-color); | |
| box-shadow: 0 0 0 3px rgba(232,137,171,.25); | |
| outline: none; | |
| } | |
| .d3-line .d3-line__controls label { gap: 8px; } | |
| /* Range slider themed with --primary-color */ | |
| .d3-line .d3-line__controls input[type="range"] { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 100%; | |
| height: 6px; | |
| border-radius: 999px; | |
| background: var(--border-color); | |
| outline: none; | |
| } | |
| .d3-line .d3-line__controls input[type="range"]::-webkit-slider-runnable-track { | |
| height: 6px; | |
| background: transparent; | |
| border-radius: 999px; | |
| } | |
| .d3-line .d3-line__controls input[type="range"]::-webkit-slider-thumb { | |
| -webkit-appearance: none; | |
| appearance: none; | |
| width: 16px; | |
| height: 16px; | |
| border-radius: 50%; | |
| background: var(--primary-color); | |
| border: 2px solid var(--on-primary); | |
| margin-top: -5px; | |
| cursor: pointer; | |
| } | |
| .d3-line .d3-line__controls input[type="range"]::-moz-range-track { | |
| height: 6px; | |
| background: transparent; | |
| border-radius: 999px; | |
| } | |
| .d3-line .d3-line__controls input[type="range"]::-moz-range-thumb { | |
| width: 16px; | |
| height: 16px; | |
| border-radius: 50%; | |
| background: var(--primary-color); | |
| border: 2px solid var(--on-primary); | |
| cursor: pointer; | |
| } | |
| /* Improved line color via CSS */ | |
| .d3-line .lines path.improved { stroke: var(--primary-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 mount = document.currentScript ? document.currentScript.previousElementSibling : null; | |
| const container = (mount && mount.querySelector && mount.querySelector('.d3-line')) || document.querySelector('.d3-line'); | |
| if (!container) return; | |
| if (container.dataset) { | |
| if (container.dataset.mounted === 'true') return; | |
| container.dataset.mounted = 'true'; | |
| } | |
| // CSV: prefer public path, fallback to relative | |
| const CSV_PATHS = [ | |
| '/data/ss_vs_s1.csv', | |
| './assets/data/ss_vs_s1.csv', | |
| '../assets/data/ss_vs_s1.csv', | |
| '../../assets/data/ss_vs_s1.csv' | |
| ]; | |
| const fetchFirstAvailable = async (paths) => { | |
| for (const p of paths) { | |
| try { const r = await fetch(p, { cache: 'no-cache' }); if (r.ok) return await r.text(); } catch(e) {} | |
| } | |
| throw new Error('CSV not found: ss_vs_s1.csv'); | |
| }; | |
| // Controls UI | |
| const controls = document.createElement('div'); | |
| controls.className = 'd3-line__controls'; | |
| Object.assign(controls.style, { | |
| marginTop: '12px', | |
| display: 'flex', | |
| gap: '16px', | |
| alignItems: 'center', | |
| justifyContent: 'space-between', | |
| width: '100%' | |
| }); | |
| const labelMetric = document.createElement('label'); | |
| Object.assign(labelMetric.style, { | |
| fontSize: '12px', color: 'var(--muted-color)', display: 'flex', alignItems: 'center', gap: '6px', whiteSpace: 'nowrap', padding: '6px 10px', marginLeft: 'auto' | |
| }); | |
| labelMetric.textContent = 'Metric'; | |
| const selectMetric = document.createElement('select'); | |
| Object.assign(selectMetric.style, { fontSize: '12px' }); | |
| labelMetric.appendChild(selectMetric); | |
| // Inline legend on the right of the select | |
| const legendInline = document.createElement('div'); | |
| legendInline.className = 'controls__legend'; | |
| Object.assign(legendInline.style, { | |
| display: 'flex', | |
| gap: '8px', | |
| alignItems: 'center', | |
| flexWrap: 'nowrap', | |
| fontSize: '11px', | |
| marginLeft: '8px' | |
| }); | |
| controls.appendChild(legendInline); | |
| controls.appendChild(labelMetric); | |
| // Create SVG with marker definitions | |
| const svg = d3.select(container).append('svg') | |
| .attr('width', '100%') | |
| .style('display', 'block'); | |
| // Add marker definitions for different shapes | |
| const defs = svg.append('defs'); | |
| // Academic marker shapes | |
| const markerShapes = ['circle', 'square', 'triangle', 'diamond', 'inverted-triangle']; | |
| const markerSize = 8; | |
| // Groups | |
| const gRoot = svg.append('g'); | |
| const gGrid = gRoot.append('g').attr('class', 'grid'); | |
| const gAxes = gRoot.append('g').attr('class', 'axes'); | |
| const gLines = gRoot.append('g').attr('class', 'lines'); | |
| const gPoints = gRoot.append('g').attr('class', 'points'); | |
| const gHover = gRoot.append('g').attr('class', 'hover'); | |
| const gLegend = gRoot.append('foreignObject').attr('class', 'legend'); | |
| // Tooltip | |
| container.style.position = container.style.position || 'relative'; | |
| let tip = container.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: '8px 10px', borderRadius: '8px', fontSize: '12px', lineHeight: '1.35', border: '1px solid var(--border-color)', | |
| background: 'var(--surface-bg)', color: 'var(--text-color)', boxShadow: '0 4px 24px rgba(0,0,0,.18)', opacity: '0', | |
| transition: 'opacity .12s ease' | |
| }); | |
| tipInner = document.createElement('div'); | |
| tipInner.className = 'd3-tooltip__inner'; | |
| tipInner.style.textAlign = 'left'; | |
| tip.appendChild(tipInner); | |
| container.appendChild(tip); | |
| } else { | |
| tipInner = tip.querySelector('.d3-tooltip__inner') || tip; | |
| } | |
| // Colors per run | |
| const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB'; | |
| const pool = [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...(d3.schemeTableau10||[])]; | |
| // Mapping from metric names to display titles | |
| const metricTitleMapping = { | |
| 'docvqa_val_anls': 'DocVQA', | |
| 'infovqa_val_anls': 'InfoVQA', | |
| 'mme_total_score': 'MME Total', | |
| 'mmmu_val_mmmu_acc': 'MMMU', | |
| 'mmstar_average': 'MMStar', | |
| 'ocrbench_ocrbench_accuracy': 'OCRBench', | |
| 'scienceqa_exact_match': 'ScienceQA', | |
| 'textvqa_val_exact_match': 'TextVQA', | |
| 'average': 'Average (excl. MME)', | |
| 'average_rank': 'Average Rank', | |
| 'ai2d_exact_match': 'AI2D', | |
| 'chartqa_relaxed_overall': 'ChartQA', | |
| 'seedbench_seed_all': 'SeedBench' | |
| }; | |
| // Function to get display name for metric | |
| function getMetricDisplayName(metricKey) { | |
| return metricTitleMapping[metricKey] || metricKey; | |
| } | |
| // State and data | |
| let metricList = []; | |
| let runList = []; | |
| let runOrder = []; | |
| const dataByMetric = new Map(); // metric => { run => [{step,value}] } | |
| let isRankStrictFlag = false; | |
| let rankTickMax = 1; | |
| // Scales and layout | |
| let width = 800, height = 360; | |
| let margin = { top: 16, right: 28, bottom: 56, left: 64 }; | |
| let xScale = d3.scaleLinear(); | |
| let yScale = d3.scaleLinear(); | |
| // Line generators - simple linear connections | |
| const lineGen = d3.line() | |
| .x((d) => xScale(d.step)) | |
| .y((d) => yScale(d.value)); | |
| // Function to draw different marker shapes | |
| function drawMarker(selection, shape, size) { | |
| const s = size / 2; | |
| switch (shape) { | |
| case 'circle': | |
| return selection.append('circle').attr('r', s); | |
| case 'square': | |
| return selection.append('rect').attr('x', -s).attr('y', -s).attr('width', size).attr('height', size); | |
| case 'triangle': | |
| return selection.append('path').attr('d', `M0,${-s * 1.2} L${s * 1.1},${s * 0.6} L${-s * 1.1},${s * 0.6} Z`); | |
| case 'diamond': | |
| return selection.append('path').attr('d', `M0,${-s * 1.2} L${s * 1.1},0 L0,${s * 1.2} L${-s * 1.1},0 Z`); | |
| case 'inverted-triangle': | |
| return selection.append('path').attr('d', `M0,${s * 1.2} L${s * 1.1},${-s * 0.6} L${-s * 1.1},${-s * 0.6} Z`); | |
| default: | |
| return selection.append('circle').attr('r', s); | |
| } | |
| } | |
| // Hover elements | |
| const hoverLine = gHover.append('line').attr('stroke-width', 1); | |
| const overlay = gHover.append('rect').attr('fill', 'transparent').style('cursor', 'crosshair'); | |
| function updateScales() { | |
| const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; | |
| const axisColor = isDark ? 'rgba(255,255,255,0.25)' : 'rgba(0,0,0,0.25)'; | |
| const tickColor = isDark ? 'rgba(255,255,255,0.70)' : 'rgba(0,0,0,0.55)'; | |
| const gridColor = isDark ? 'rgba(255,255,255,0.08)' : 'rgba(0,0,0,0.05)'; | |
| width = container.clientWidth || 800; | |
| height = Math.max(360, Math.round(width / 2.2)); | |
| svg.attr('width', width).attr('height', height); | |
| const innerWidth = width - margin.left - margin.right; | |
| const innerHeight = height - margin.top - margin.bottom; | |
| gRoot.attr('transform', `translate(${margin.left},${margin.top})`); | |
| xScale.range([0, innerWidth]); | |
| yScale.range([innerHeight, 0]); | |
| // Compute Y ticks | |
| let yTicks = []; | |
| if (isRankStrictFlag) { | |
| const maxR = Math.max(1, Math.round(rankTickMax)); | |
| for (let v = 1; v <= maxR; v += 1) yTicks.push(v); | |
| } else { | |
| // Use D3's tick generator to produce nice floating-point ticks | |
| yTicks = yScale.ticks(6); | |
| } | |
| // Grid (horizontal) | |
| gGrid.selectAll('*').remove(); | |
| gGrid.selectAll('line') | |
| .data(yTicks) | |
| .join('line') | |
| .attr('x1', 0) | |
| .attr('x2', innerWidth) | |
| .attr('y1', (d) => yScale(d)) | |
| .attr('y2', (d) => yScale(d)) | |
| .attr('stroke', gridColor) | |
| .attr('stroke-width', 1) | |
| .attr('shape-rendering', 'crispEdges'); | |
| // Axes | |
| gAxes.selectAll('*').remove(); | |
| let xAxis = d3.axisBottom(xScale).tickSizeOuter(0); | |
| if (isRankStrictFlag) { | |
| const [dx0, dx1] = xScale.domain(); | |
| const start = Math.ceil(dx0 / 1000) * 1000; | |
| const end = Math.floor(dx1 / 1000) * 1000; | |
| const xTicks = []; | |
| for (let v = start; v <= end; v += 1000) xTicks.push(v); | |
| if (xTicks.length === 0) xTicks.push(Math.round(dx0)); | |
| xAxis = xAxis.tickValues(xTicks).tickFormat(d3.format('d')); | |
| } else { | |
| xAxis = xAxis.ticks(8); | |
| } | |
| const yAxis = d3.axisLeft(yScale) | |
| .tickValues(yTicks) | |
| .tickSizeOuter(0) | |
| .tickFormat(isRankStrictFlag ? d3.format('d') : d3.format('.2f')); | |
| gAxes.append('g') | |
| .attr('transform', `translate(0,${innerHeight})`) | |
| .call(xAxis) | |
| .call((g) => { | |
| g.selectAll('path, line').attr('stroke', axisColor); | |
| g.selectAll('text').attr('fill', tickColor).style('font-size', '12px'); | |
| }); | |
| gAxes.append('g') | |
| .call(yAxis) | |
| .call((g) => { | |
| g.selectAll('path, line').attr('stroke', axisColor); | |
| g.selectAll('text').attr('fill', tickColor).style('font-size', '12px'); | |
| }); | |
| // Axis labels (X and Y) | |
| gAxes.append('text') | |
| .attr('class', 'axis-label axis-label--x') | |
| .attr('x', innerWidth / 2) | |
| .attr('y', innerHeight + 44) | |
| .attr('text-anchor', 'middle') | |
| .style('font-size', '12px') | |
| .style('fill', tickColor) | |
| .text('Step'); | |
| gAxes.append('text') | |
| .attr('class', 'axis-label axis-label--y') | |
| .attr('text-anchor', 'middle') | |
| .attr('transform', `translate(${-44},${innerHeight/2}) rotate(-90)`) | |
| .style('font-size', '12px') | |
| .style('fill', tickColor) | |
| .text('Value'); | |
| overlay.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight); | |
| hoverLine.attr('y1', 0).attr('y2', innerHeight).attr('stroke', axisColor); | |
| // Legend placeholder; actual content set in renderMetric | |
| const legendWidth = Math.min(180, Math.max(120, Math.round(innerWidth * 0.22))); | |
| const legendHeight = 64; | |
| gLegend | |
| .attr('x', innerWidth - legendWidth + 42) | |
| .attr('y', innerHeight - legendHeight - 12) | |
| .attr('width', legendWidth) | |
| .attr('height', legendHeight); | |
| const legendRoot = gLegend.selectAll('div').data([0]).join('xhtml:div'); | |
| Object.assign(legendRoot.node().style, { | |
| background: 'transparent', | |
| border: 'none', | |
| borderRadius: '0', | |
| padding: '0', | |
| fontSize: '12px', | |
| lineHeight: '1.35', | |
| color: 'var(--text-color)' | |
| }); | |
| return { innerWidth, innerHeight }; | |
| } | |
| function renderMetric(metricKey){ | |
| const map = dataByMetric.get(metricKey) || {}; | |
| const runs = runOrder; | |
| // Domain | |
| let minStep = Infinity, maxStep = -Infinity, maxVal = 0, minVal = Infinity; | |
| const isRank = /rank/i.test(metricKey); | |
| const isAverage = /average/i.test(metricKey); | |
| const isRankStrict = isRank && !isAverage; | |
| runs.forEach(r => { | |
| const arr = map[r] || []; | |
| arr.forEach(pt => { | |
| const val = isRankStrict ? Math.round(pt.value) : pt.value; | |
| minStep = Math.min(minStep, pt.step); | |
| maxStep = Math.max(maxStep, pt.step); | |
| maxVal = Math.max(maxVal, val); | |
| minVal = Math.min(minVal, val); | |
| }); | |
| }); | |
| if (!isFinite(minStep) || !isFinite(maxStep)) { return; } | |
| xScale.domain([minStep, maxStep]); | |
| if (isRank) { | |
| rankTickMax = Math.max(1, Math.round(maxVal)); | |
| yScale.domain([rankTickMax, 1]); | |
| } else { | |
| yScale.domain([0, Math.max(1, maxVal)]).nice(); | |
| } | |
| isRankStrictFlag = isRankStrict; | |
| const { innerWidth, innerHeight } = updateScales(); | |
| // Bind lines and markers | |
| const series = runs.map((r, i) => ({ | |
| run: r, | |
| color: pool[i % pool.length], | |
| marker: markerShapes[i % markerShapes.length], | |
| values: (map[r]||[]) | |
| .slice() | |
| .sort((a,b)=>a.step-b.step) | |
| .map(pt => isRankStrict ? { step: pt.step, value: Math.round(pt.value) } : pt) | |
| })); | |
| // Draw lines | |
| const paths = gLines.selectAll('path.run-line').data(series, d=>d.run); | |
| paths.enter().append('path').attr('class','run-line').attr('fill','none').attr('stroke-width',2) | |
| .attr('stroke', d=>d.color).attr('opacity',0.9) | |
| .attr('d', d=>lineGen(d.values)) | |
| .merge(paths) | |
| .transition().duration(200) | |
| .attr('stroke', d=>d.color) | |
| .attr('d', d=>lineGen(d.values)); | |
| paths.exit().remove(); | |
| // Draw markers for each data point | |
| gPoints.selectAll('*').remove(); | |
| series.forEach((s, seriesIndex) => { | |
| const pointGroup = gPoints.selectAll(`.points-${seriesIndex}`) | |
| .data(s.values) | |
| .join('g') | |
| .attr('class', `points-${seriesIndex}`) | |
| .attr('transform', d => `translate(${xScale(d.step)},${yScale(d.value)})`); | |
| drawMarker(pointGroup, s.marker, markerSize) | |
| .attr('fill', s.color) | |
| .attr('stroke', s.color) | |
| .attr('stroke-width', 1.5) | |
| .style('cursor', 'crosshair'); | |
| }); | |
| // Inline legend content with marker shapes | |
| legendInline.innerHTML = ''; | |
| series.forEach(s => { | |
| const legendItem = document.createElement('span'); | |
| legendItem.style.cssText = 'display:inline-flex;align-items:center;gap:6px;white-space:nowrap;'; | |
| // Create small SVG for marker shape | |
| const markerSvg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); | |
| markerSvg.setAttribute('width', '16'); | |
| markerSvg.setAttribute('height', '12'); | |
| markerSvg.style.display = 'inline-block'; | |
| const g = document.createElementNS('http://www.w3.org/2000/svg', 'g'); | |
| g.setAttribute('transform', 'translate(8,6)'); | |
| let shape; | |
| const size = 6; | |
| const halfSize = size / 2; | |
| switch(s.marker) { | |
| case 'circle': | |
| shape = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); | |
| shape.setAttribute('r', halfSize); | |
| break; | |
| case 'square': | |
| shape = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); | |
| shape.setAttribute('x', -halfSize); | |
| shape.setAttribute('y', -halfSize); | |
| shape.setAttribute('width', size); | |
| shape.setAttribute('height', size); | |
| break; | |
| case 'triangle': | |
| shape = document.createElementNS('http://www.w3.org/2000/svg', 'path'); | |
| shape.setAttribute('d', `M0,${-halfSize * 1.2} L${halfSize * 1.1},${halfSize * 0.6} L${-halfSize * 1.1},${halfSize * 0.6} Z`); | |
| break; | |
| case 'diamond': | |
| shape = document.createElementNS('http://www.w3.org/2000/svg', 'path'); | |
| shape.setAttribute('d', `M0,${-halfSize * 1.2} L${halfSize * 1.1},0 L0,${halfSize * 1.2} L${-halfSize * 1.1},0 Z`); | |
| break; | |
| case 'inverted-triangle': | |
| shape = document.createElementNS('http://www.w3.org/2000/svg', 'path'); | |
| shape.setAttribute('d', `M0,${halfSize * 1.2} L${halfSize * 1.1},${-halfSize * 0.6} L${-halfSize * 1.1},${-halfSize * 0.6} Z`); | |
| break; | |
| default: | |
| shape = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); | |
| shape.setAttribute('r', halfSize); | |
| } | |
| shape.setAttribute('fill', s.color); | |
| shape.setAttribute('stroke', s.color); | |
| shape.setAttribute('stroke-width', '1'); | |
| g.appendChild(shape); | |
| markerSvg.appendChild(g); | |
| const label = document.createElement('span'); | |
| label.textContent = s.run; | |
| legendItem.appendChild(markerSvg); | |
| legendItem.appendChild(label); | |
| legendInline.appendChild(legendItem); | |
| }); | |
| // Hover | |
| const stepSet = new Set(); series.forEach(s=>s.values.forEach(v=>stepSet.add(v.step))); | |
| const steps = Array.from(stepSet).sort((a,b)=>a-b); | |
| function onMove(event){ | |
| const [mx, my] = d3.pointer(event, overlay.node()); | |
| const sx = Math.max(steps[0], Math.min(steps[steps.length-1], Math.round(xScale.invert(mx)/1)*1)); | |
| const nearest = steps.reduce((best, s)=> Math.abs(s - xScale.invert(mx)) < Math.abs(best - xScale.invert(mx)) ? s : best, steps[0]); | |
| const xpx = xScale(nearest); | |
| hoverLine.attr('x1', xpx).attr('x2', xpx).style('display', null).attr('stroke', 'rgba(0,0,0,0.25)'); | |
| // Tooltip content | |
| let html = `<div><strong>${getMetricDisplayName(metricKey)}</strong></div><div><strong>step</strong> ${nearest}</div>`; | |
| series.forEach(s=>{ | |
| const m = new Map(s.values.map(v=>[v.step, v.value])); | |
| const val = m.has(nearest) ? m.get(nearest) : null; | |
| if (val != null) { | |
| const formatVal = (vv) => (isRankStrict ? d3.format('d')(vv) : (+vv).toFixed(4)); | |
| html += `<div><span style=\"display:inline-block;width:10px;height:10px;background:${s.color};border-radius:50%;margin-right:6px;\"></span><strong>${s.run}</strong> ${formatVal(val)}</div>`; | |
| } | |
| }); | |
| tipInner.innerHTML = html; | |
| const offsetX = 12, offsetY = 12; | |
| tip.style.opacity = '1'; tip.style.transform = `translate(${Math.round(mx + offsetX + margin.left)}px, ${Math.round(my + offsetY + margin.top)}px)`; | |
| } | |
| function onLeave(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); } | |
| overlay.on('mousemove', onMove).on('mouseleave', onLeave); | |
| } | |
| // (old hover removed; hover is attached in renderMetric) | |
| // Load CSV and wire controls | |
| (async () => { | |
| try { | |
| const text = await fetchFirstAvailable(CSV_PATHS); | |
| const rows = d3.csvParse(text, d => ({ run: (d.run||'').trim(), step: +d.step, metric: (d.metric||'').trim(), value: +d.value })); | |
| metricList = Array.from(new Set(rows.map(r=>r.metric))).sort(); | |
| runList = Array.from(new Set(rows.map(r=>r.run))).sort(); | |
| runOrder = runList; | |
| // Build dataByMetric | |
| metricList.forEach(m => { | |
| const map = {}; | |
| runList.forEach(r => { map[r] = []; }); | |
| rows.filter(r=>r.metric===m).forEach(r => { if (!isNaN(r.step) && !isNaN(r.value)) map[r.run].push({ step:r.step, value:r.value }); }); | |
| dataByMetric.set(m, map); | |
| }); | |
| // Populate metric select (default to average_rank if present) | |
| metricList.forEach((m)=>{ const o=document.createElement('option'); o.value=m; o.textContent=getMetricDisplayName(m); selectMetric.appendChild(o); }); | |
| const def = metricList.find(m => /average_rank/i.test(m)) || metricList[0]; | |
| if (def) selectMetric.value = def; | |
| container.appendChild(controls); | |
| updateScales(); | |
| renderMetric(selectMetric.value); | |
| selectMetric.addEventListener('change', ()=>{ renderMetric(selectMetric.value); }); | |
| const rerender = () => { renderMetric(selectMetric.value); }; | |
| if (window.ResizeObserver) { const ro = new ResizeObserver(()=>rerender()); ro.observe(container); } else { window.addEventListener('resize', rerender); } | |
| } catch (e) { | |
| const pre = document.createElement('pre'); pre.textContent = 'CSV load error: ' + (e && e.message ? e.message : e); | |
| pre.style.color = 'var(--danger, #b00020)'; pre.style.fontSize = '12px'; pre.style.whiteSpace = 'pre-wrap'; | |
| container.appendChild(pre); | |
| } | |
| })(); | |
| }; | |
| if (document.readyState === 'loading') { | |
| document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); | |
| } else { ensureD3(bootstrap); } | |
| })(); | |
| </script> | |