lusxvr's picture
new plots
a024e38
raw
history blame
24.3 kB
<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>