|
|
<div class="line-quad"> |
|
|
|
|
|
<div class="line-quad__grid"> |
|
|
<div class="quad-cell" data-title="Formatting Filter" data-csv="/data/formatting_filters.csv"></div> |
|
|
<div class="quad-cell" data-title="Relevance Filter" data-csv="/data/relevance_filters.csv"></div> |
|
|
<div class="quad-cell" data-title="Visual Dependency Filter" data-csv="/data/visual_dependency_filters.csv"></div> |
|
|
<div class="quad-cell" data-title="Image Correspondence Filter" data-csv="/data/image_correspondence_filters.csv"> |
|
|
</div> |
|
|
</div> |
|
|
<noscript>JavaScript is required to render these charts.</noscript> |
|
|
|
|
|
</div> |
|
|
<style> |
|
|
.line-quad { |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.line-quad .axes path, |
|
|
.line-quad .axes line { |
|
|
stroke: var(--axis-color); |
|
|
} |
|
|
|
|
|
.line-quad .axes text { |
|
|
fill: var(--tick-color); |
|
|
} |
|
|
|
|
|
.line-quad .grid line { |
|
|
stroke: var(--grid-color); |
|
|
} |
|
|
|
|
|
.line-quad__grid { |
|
|
display: grid; |
|
|
grid-template-columns: repeat(2, minmax(0, 1fr)); |
|
|
gap: 12px; |
|
|
} |
|
|
|
|
|
@media (max-width: 980px) { |
|
|
.line-quad__grid { |
|
|
grid-template-columns: 1fr; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
.quad-cell { |
|
|
border: 1px solid var(--border-color); |
|
|
border-radius: 10px; |
|
|
background: var(--surface-bg); |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
|
|
|
.line-quad__grid .quad-cell:nth-child(1) { |
|
|
z-index: 4; |
|
|
} |
|
|
|
|
|
|
|
|
.line-quad__grid .quad-cell:nth-child(3) { |
|
|
z-index: 3; |
|
|
} |
|
|
|
|
|
|
|
|
.line-quad__grid .quad-cell:nth-child(2) { |
|
|
z-index: 2; |
|
|
} |
|
|
|
|
|
|
|
|
.line-quad__grid .quad-cell:nth-child(4) { |
|
|
z-index: 1; |
|
|
} |
|
|
|
|
|
|
|
|
.quad-cell .cell-header { |
|
|
padding: 8px 10px; |
|
|
border-bottom: 1px solid var(--border-color); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
justify-content: space-between; |
|
|
gap: 8px; |
|
|
} |
|
|
|
|
|
.quad-cell .cell-title { |
|
|
font-size: 13px; |
|
|
font-weight: 700; |
|
|
color: var(--text-color); |
|
|
} |
|
|
|
|
|
.quad-cell .cell-controls { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 12px; |
|
|
} |
|
|
|
|
|
.quad-cell .cell-controls label { |
|
|
font-size: 12px; |
|
|
color: var(--muted-color); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 6px; |
|
|
white-space: nowrap; |
|
|
} |
|
|
|
|
|
.quad-cell select { |
|
|
font-size: 12px; |
|
|
padding: 6px 28px 6px 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='1' 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; |
|
|
appearance: none; |
|
|
cursor: pointer; |
|
|
transition: border-color .15s ease, box-shadow .15s ease; |
|
|
} |
|
|
|
|
|
[data-theme="dark"] .quad-cell 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='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); |
|
|
} |
|
|
|
|
|
.quad-cell select:hover { |
|
|
border-color: var(--primary-color); |
|
|
} |
|
|
|
|
|
.quad-cell select:focus { |
|
|
border-color: var(--primary-color); |
|
|
box-shadow: 0 0 0 3px rgba(232, 137, 171, .25); |
|
|
outline: none; |
|
|
} |
|
|
|
|
|
.quad-cell .cell-body { |
|
|
position: relative; |
|
|
} |
|
|
|
|
|
.quad-cell .cell-body { |
|
|
width: 100%; |
|
|
overflow: hidden; |
|
|
} |
|
|
|
|
|
.quad-cell .cell-body svg { |
|
|
max-width: 100%; |
|
|
height: auto; |
|
|
} |
|
|
|
|
|
.line-quad.hovering .lines path.ghost { |
|
|
opacity: .25; |
|
|
} |
|
|
|
|
|
.line-quad.hovering .points circle.ghost { |
|
|
opacity: .25; |
|
|
} |
|
|
|
|
|
.line-quad.hovering .areas path.ghost { |
|
|
opacity: .08; |
|
|
} |
|
|
|
|
|
.line-quad.hovering .legend-bottom .item.ghost { |
|
|
opacity: .35; |
|
|
} |
|
|
|
|
|
|
|
|
.line-quad .d3-tooltip { |
|
|
z-index: 20; |
|
|
backdrop-filter: saturate(1.12) blur(8px); |
|
|
} |
|
|
|
|
|
.line-quad .d3-tooltip__inner { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
gap: 6px; |
|
|
min-width: 220px; |
|
|
} |
|
|
|
|
|
.line-quad .d3-tooltip__inner>div:first-child { |
|
|
font-weight: 800; |
|
|
letter-spacing: 0.1px; |
|
|
margin-bottom: 0; |
|
|
} |
|
|
|
|
|
.line-quad .d3-tooltip__inner>div:nth-child(2) { |
|
|
font-size: 11px; |
|
|
color: var(--muted-color); |
|
|
display: block; |
|
|
margin-top: -4px; |
|
|
margin-bottom: 2px; |
|
|
letter-spacing: 0.1px; |
|
|
} |
|
|
|
|
|
.line-quad .d3-tooltip__inner>div:nth-child(n+3) { |
|
|
padding-top: 6px; |
|
|
border-top: 1px solid var(--border-color); |
|
|
} |
|
|
|
|
|
.line-quad .d3-tooltip__inner svg { |
|
|
display: inline-block; |
|
|
vertical-align: middle; |
|
|
margin-right: 2px; |
|
|
} |
|
|
|
|
|
.line-quad .d3-tooltip__inner strong { |
|
|
margin-right: 6px; |
|
|
} |
|
|
|
|
|
.line-quad .d3-tooltip__color-dot { |
|
|
display: inline-block; |
|
|
width: 12px; |
|
|
height: 12px; |
|
|
border-radius: 3px; |
|
|
border: 1px solid var(--border-color); |
|
|
} |
|
|
|
|
|
|
|
|
.line-quad__header { |
|
|
display: flex; |
|
|
align-items: flex-start; |
|
|
justify-content: flex-start; |
|
|
gap: 12px; |
|
|
margin: 8px 0 0 0; |
|
|
flex-wrap: wrap; |
|
|
} |
|
|
|
|
|
.line-quad__header .legend-bottom { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: flex-start; |
|
|
gap: 6px; |
|
|
font-size: 12px; |
|
|
color: var(--text-color); |
|
|
} |
|
|
|
|
|
.line-quad__header .legend-bottom .legend-title { |
|
|
font-size: 12px; |
|
|
font-weight: 700; |
|
|
color: var(--text-color); |
|
|
} |
|
|
|
|
|
.line-quad__header .legend-bottom .items { |
|
|
display: flex; |
|
|
flex-wrap: wrap; |
|
|
gap: 8px 14px; |
|
|
} |
|
|
|
|
|
.line-quad__header .legend-bottom .item { |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
gap: 6px; |
|
|
white-space: nowrap; |
|
|
} |
|
|
|
|
|
.line-quad__header .legend-bottom .swatch { |
|
|
width: 14px; |
|
|
height: 14px; |
|
|
border-radius: 3px; |
|
|
border: 1px solid var(--border-color); |
|
|
display: inline-block; |
|
|
} |
|
|
|
|
|
.line-quad .controls { |
|
|
margin-top: 0; |
|
|
display: flex; |
|
|
gap: 16px; |
|
|
align-items: center; |
|
|
justify-content: flex-end; |
|
|
width: auto; |
|
|
flex-wrap: wrap; |
|
|
} |
|
|
|
|
|
.line-quad .controls .control-group { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: flex-start; |
|
|
gap: 6px; |
|
|
} |
|
|
|
|
|
.line-quad .controls label { |
|
|
font-size: 12px; |
|
|
color: var(--text-color); |
|
|
display: flex; |
|
|
align-items: center; |
|
|
gap: 6px; |
|
|
white-space: nowrap; |
|
|
font-weight: 700; |
|
|
} |
|
|
|
|
|
.line-quad .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='1' 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; |
|
|
appearance: none; |
|
|
cursor: pointer; |
|
|
transition: border-color .15s ease, box-shadow .15s ease; |
|
|
} |
|
|
|
|
|
[data-theme="dark"] .line-quad .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='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E"); |
|
|
} |
|
|
|
|
|
.line-quad .controls select:hover { |
|
|
border-color: var(--primary-color); |
|
|
} |
|
|
|
|
|
.line-quad .controls select:focus { |
|
|
border-color: var(--primary-color); |
|
|
box-shadow: 0 0 0 3px rgba(232, 137, 171, .25); |
|
|
outline: none; |
|
|
} |
|
|
</style> |
|
|
<script> |
|
|
(() => { |
|
|
const THIS_SCRIPT = document.currentScript; |
|
|
|
|
|
let SHARED_RUN_COLOR = null; |
|
|
|
|
|
const prettyMetricLabel = (key) => { |
|
|
if (!key) return ''; |
|
|
const table = { |
|
|
'ai2d_exact_match': 'AI2D Exact Match', |
|
|
'average_rank': 'Average Rank', |
|
|
}; |
|
|
if (table[key]) return table[key]; |
|
|
const cleaned = String(key).replace(/[_-]+/g, ' ').trim(); |
|
|
return cleaned.split(/\s+/).map(w => { |
|
|
if (/^(ai2d|umap|id|auc|f1)$/i.test(w)) return w.toUpperCase(); |
|
|
return w.charAt(0).toUpperCase() + w.slice(1); |
|
|
}).join(' '); |
|
|
}; |
|
|
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(); |
|
|
}; |
|
|
|
|
|
function initRunLine(cell) { |
|
|
const d3 = window.d3; |
|
|
const csvPath = cell.getAttribute('data-csv'); |
|
|
const titleText = cell.getAttribute('data-title') || ''; |
|
|
|
|
|
|
|
|
const header = document.createElement('div'); header.className = 'cell-header'; |
|
|
const title = document.createElement('div'); title.className = 'cell-title'; title.textContent = titleText; header.appendChild(title); |
|
|
|
|
|
cell.appendChild(header); |
|
|
|
|
|
|
|
|
const body = document.createElement('div'); body.className = 'cell-body'; cell.appendChild(body); |
|
|
const svg = d3.select(body).append('svg').attr('width', '100%').style('display', 'block'); |
|
|
const gRoot = svg.append('g'); |
|
|
const gGrid = gRoot.append('g').attr('class', 'grid'); |
|
|
const gAxes = gRoot.append('g').attr('class', 'axes'); |
|
|
const gAreas = gRoot.append('g').attr('class', 'areas'); |
|
|
const gLines = gRoot.append('g').attr('class', 'lines'); |
|
|
const gPoints = gRoot.append('g').attr('class', 'points'); |
|
|
const gHover = gRoot.append('g').attr('class', 'hover'); |
|
|
|
|
|
|
|
|
|
|
|
cell.style.position = cell.style.position || 'relative'; |
|
|
let tip = cell.querySelector('.d3-tooltip'); let tipInner; let hideTipTimer = null; |
|
|
if (!tip) { |
|
|
tip = document.createElement('div'); |
|
|
tip.className = 'd3-tooltip'; |
|
|
Object.assign(tip.style, { |
|
|
position: 'absolute', |
|
|
top: '0', |
|
|
left: '0', |
|
|
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', |
|
|
backdropFilter: 'saturate(1.12) blur(8px)' |
|
|
}); |
|
|
tipInner = document.createElement('div'); |
|
|
tipInner.className = 'd3-tooltip__inner'; |
|
|
tipInner.style.textAlign = 'left'; |
|
|
tip.appendChild(tipInner); |
|
|
cell.appendChild(tip); |
|
|
} else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; } |
|
|
|
|
|
|
|
|
let metricList = []; let runList = []; let runOrder = []; const dataByMetric = new Map(); |
|
|
let width = 800, height = 340; const margin = { top: 16, right: 20, bottom: 46, left: 56 }; |
|
|
const xScale = d3.scaleLinear(); const yScale = d3.scaleLinear(); |
|
|
const lineGen = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value)); |
|
|
let isRankStrictFlag = false; let isRankMetricFlag = false; let rankTickMax = 1; |
|
|
let sharedYConfig = null; |
|
|
let axisLabelY = 'Value'; |
|
|
|
|
|
|
|
|
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(); |
|
|
return [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...(d3.schemeTableau10 || [])].slice(0, n); |
|
|
}; |
|
|
const pool = getRunColors(12); |
|
|
|
|
|
|
|
|
let readyResolve = null; |
|
|
const ready = new Promise((res) => { readyResolve = res; }); |
|
|
|
|
|
|
|
|
const formatK = (v) => { |
|
|
const abs = Math.abs(v); |
|
|
if (abs >= 1000) { |
|
|
const n = v / 1000; |
|
|
const s = d3.format('.1f')(n); |
|
|
return (s.endsWith('.0') ? s.slice(0, -2) : s) + 'k'; |
|
|
} |
|
|
return d3.format('d')(v); |
|
|
}; |
|
|
|
|
|
function updateScales() { |
|
|
const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; |
|
|
const axisColor = 'var(--axis-color)'; |
|
|
const tickColor = 'var(--tick-color)'; |
|
|
const gridColor = 'var(--grid-color)'; |
|
|
|
|
|
const rect = cell.getBoundingClientRect(); |
|
|
width = Math.max(1, Math.round(rect && rect.width ? rect.width : (cell.clientWidth || 800))); |
|
|
height = Math.max(280, Math.round(width / 2.3)); |
|
|
svg.attr('width', width).attr('height', height).attr('viewBox', `0 0 ${width} ${height}`).attr('preserveAspectRatio', 'xMidYMid meet'); |
|
|
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]); |
|
|
|
|
|
|
|
|
let yTicks = []; |
|
|
if (isRankStrictFlag) { const maxR = Math.max(1, Math.round(rankTickMax)); for (let v = 1; v <= maxR; v += 1) yTicks.push(v); } |
|
|
else { yTicks = yScale.ticks(6); } |
|
|
|
|
|
|
|
|
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'); |
|
|
|
|
|
|
|
|
gAxes.selectAll('*').remove(); |
|
|
let xAxis = d3.axisBottom(xScale).tickSizeOuter(0); xAxis = xAxis.ticks(8); |
|
|
xAxis = xAxis.tickFormat(formatK); |
|
|
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', '11px'); }); |
|
|
gAxes.append('g').call(yAxis).call(g => { g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size', '11px'); }); |
|
|
|
|
|
|
|
|
gAxes.append('text') |
|
|
.attr('class', 'x-axis-label') |
|
|
.attr('x', innerWidth / 2) |
|
|
.attr('y', innerHeight + Math.max(20, Math.min(36, margin.bottom - 10))) |
|
|
.attr('fill', 'var(--text-color)') |
|
|
.attr('text-anchor', 'middle') |
|
|
.style('font-size', '12px') |
|
|
.style('font-weight', '700') |
|
|
.text('Steps'); |
|
|
|
|
|
gAxes.append('text') |
|
|
.attr('class', 'y-axis-label') |
|
|
.attr('transform', 'rotate(-90)') |
|
|
.attr('x', -innerHeight / 2) |
|
|
.attr('y', -Math.max(16, Math.min(28, margin.left - 8) + 10)) |
|
|
.attr('fill', 'var(--text-color)') |
|
|
.attr('text-anchor', 'middle') |
|
|
.style('font-size', '12px') |
|
|
.style('font-weight', '700') |
|
|
.text(axisLabelY); |
|
|
|
|
|
return { innerWidth, innerHeight, tickColor }; |
|
|
} |
|
|
|
|
|
function renderMetric(metricKey) { |
|
|
const map = dataByMetric.get(metricKey) || {}; |
|
|
const runs = runOrder; |
|
|
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 => { (map[r] || []).forEach(pt => { const v = isRankStrict ? Math.round(pt.value) : pt.value; minStep = Math.min(minStep, pt.step); maxStep = Math.max(maxStep, pt.step); maxVal = Math.max(maxVal, v); minVal = Math.min(minVal, v); }); }); |
|
|
if (!isFinite(minStep) || !isFinite(maxStep)) return; |
|
|
xScale.domain([minStep, maxStep]); |
|
|
if (sharedYConfig && sharedYConfig.type === 'rank_strict') { |
|
|
rankTickMax = Math.max(1, Math.round(sharedYConfig.maxRank || 1)); |
|
|
yScale.domain([rankTickMax, 1]); |
|
|
isRankStrictFlag = true; |
|
|
isRankMetricFlag = true; |
|
|
} else if (sharedYConfig && sharedYConfig.type === 'value') { |
|
|
yScale.domain([sharedYConfig.min, sharedYConfig.max]); |
|
|
isRankStrictFlag = isRankStrict; |
|
|
isRankMetricFlag = isRank; |
|
|
} else { |
|
|
if (isRank) { rankTickMax = Math.max(1, Math.round(maxVal)); yScale.domain([rankTickMax, 1]); } |
|
|
else { yScale.domain([minVal, maxVal]).nice(); } |
|
|
isRankStrictFlag = isRankStrict; |
|
|
isRankMetricFlag = isRank; |
|
|
} |
|
|
|
|
|
axisLabelY = isRankStrict ? 'Rank' : prettyMetricLabel(metricKey); |
|
|
const tChange = (window.d3 && d3.transition) ? d3.transition().duration(260).ease(d3.easeCubicOut) : null; |
|
|
const { innerWidth, innerHeight } = updateScales(); |
|
|
|
|
|
const colorForRun = (run, idx) => { |
|
|
if (SHARED_RUN_COLOR && Object.prototype.hasOwnProperty.call(SHARED_RUN_COLOR, run)) return SHARED_RUN_COLOR[run]; |
|
|
const j = (typeof idx === 'number' ? idx : runs.indexOf(run)); |
|
|
return pool[(j >= 0 ? j : 0) % pool.length]; |
|
|
}; |
|
|
const series = runs.map((r, i) => ({ run: r, color: colorForRun(r, i), values: (map[r] || []).slice().sort((a, b) => a.step - b.step).map(pt => isRankStrict ? { step: pt.step, value: Math.round(pt.value), stderr: pt.stderr } : pt) })); |
|
|
|
|
|
|
|
|
gAreas.selectAll('*').remove(); |
|
|
if (!isRank) { |
|
|
series.forEach((s) => { |
|
|
const withErr = s.values.filter(v => v && v.stderr != null && isFinite(v.stderr) && v.stderr > 0 && isFinite(v.value)); |
|
|
if (!withErr.length) return; |
|
|
const upper = withErr.map(d => [xScale(d.step), yScale(d.value + d.stderr)]); |
|
|
const lower = withErr.slice().reverse().map(d => [xScale(d.step), yScale(d.value - d.stderr)]); |
|
|
const coords = upper.concat(lower); |
|
|
const pathData = d3.line().x(d => d[0]).y(d => d[1]).curve(d3.curveLinearClosed)(coords); |
|
|
gAreas.append('path') |
|
|
.attr('class', 'area') |
|
|
.attr('data-run', s.run) |
|
|
.attr('d', pathData) |
|
|
.attr('fill', s.color) |
|
|
.attr('opacity', 0) |
|
|
.attr('stroke', 'none') |
|
|
.transition().duration(450).ease(d3.easeCubicOut) |
|
|
.attr('opacity', 0.15); |
|
|
}); |
|
|
} |
|
|
|
|
|
const paths = gLines.selectAll('path.run-line').data(series, d => d.run); |
|
|
paths.enter() |
|
|
.append('path') |
|
|
.attr('class', 'run-line') |
|
|
.attr('data-run', d => d.run) |
|
|
.attr('fill', 'none') |
|
|
.attr('stroke-width', 1) |
|
|
.attr('opacity', 0) |
|
|
.attr('stroke', d => d.color) |
|
|
.attr('d', d => lineGen(d.values)) |
|
|
.transition(tChange || undefined) |
|
|
.attr('opacity', 0.9); |
|
|
paths |
|
|
.transition(tChange || undefined) |
|
|
.attr('stroke', d => d.color) |
|
|
.attr('opacity', 0.9) |
|
|
.attr('d', d => lineGen(d.values)); |
|
|
paths.exit().remove(); |
|
|
|
|
|
|
|
|
const allPoints = series.flatMap(s => s.values.map(v => ({ run: s.run, color: s.color, step: v.step, value: v.value }))); |
|
|
const ptsSel = gPoints.selectAll('circle.pt').data(allPoints, d => `${d.run}-${d.step}`); |
|
|
ptsSel.enter().append('circle').attr('class', 'pt') |
|
|
.attr('data-run', d => d.run) |
|
|
.attr('r', 1.5) |
|
|
.attr('fill', d => d.color) |
|
|
.attr('fill-opacity', 0.6) |
|
|
.attr('stroke', 'none') |
|
|
.attr('cx', d => xScale(d.step)) |
|
|
.attr('cy', d => yScale(d.value)) |
|
|
.merge(ptsSel) |
|
|
.attr('fill', d => d.color) |
|
|
.transition(tChange || undefined) |
|
|
.attr('r', 2) |
|
|
.attr('cx', d => xScale(d.step)) |
|
|
.attr('cy', d => yScale(d.value)); |
|
|
ptsSel.exit().remove(); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
gHover.selectAll('*').remove(); |
|
|
const overlay = gHover.append('rect').attr('fill', 'transparent').style('cursor', 'crosshair').attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight); |
|
|
const 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'); |
|
|
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(ev) { |
|
|
if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer = null; } const [mx, my] = d3.pointer(ev, overlay.node()); 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); |
|
|
let html = `<div><strong>${titleText}</strong></div><div><strong>step</strong> ${formatK(nearest)}</div>`; |
|
|
const entries = series.map(s => { const map = new Map(s.values.map(v => [v.step, v])); const pt = map.get(nearest); return { run: s.run, color: s.color, pt }; }).filter(e => e.pt && e.pt.value != null); |
|
|
entries.sort((a, b) => (a.pt.value - b.pt.value)); |
|
|
const fmt = (vv) => (isRankStrictFlag ? d3.format('d')(vv) : (+vv).toFixed(4)); |
|
|
entries.forEach(e => { |
|
|
const err = (e.pt.stderr != null && isFinite(e.pt.stderr) && e.pt.stderr > 0) ? ` ± ${fmt(e.pt.stderr)}` : ''; |
|
|
html += `<div style="display:flex;align-items:center;gap:8px;white-space:nowrap;"><span class=\"d3-tooltip__color-dot\" style=\"background:${e.color}\"></span><strong>${e.run}</strong><span style=\"margin-left:auto;text-align:right;\">${fmt(e.pt.value)}${err}</span></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() { hideTipTimer = setTimeout(() => { tip.style.opacity = '0'; tip.style.transform = 'translate(-9999px, -9999px)'; hoverLine.style('display', 'none'); }, 100); } |
|
|
overlay.on('mousemove', onMove).on('mouseleave', onLeave); |
|
|
} |
|
|
|
|
|
async function load() { |
|
|
try { |
|
|
const file = (csvPath || '').split('/').pop(); |
|
|
const CANDIDATES = [ |
|
|
csvPath, |
|
|
`/data/${file}`, |
|
|
`./assets/data/${file}`, |
|
|
`../assets/data/${file}`, |
|
|
`../../assets/data/${file}` |
|
|
].filter(Boolean); |
|
|
let text = null; |
|
|
for (const p of CANDIDATES) { |
|
|
try { const r = await fetch(p, { cache: 'no-cache' }); if (r.ok) { text = await r.text(); break; } } catch (e) { } |
|
|
} |
|
|
if (text == null) throw new Error(`CSV not found: ${file}`); |
|
|
const rows = d3.csvParse(text, d => ({ run: (d.run || '').trim(), step: +d.step, metric: (d.metric || '').trim(), value: +d.value, stderr: (d.stderr != null && d.stderr !== '') ? +d.stderr : null })); |
|
|
metricList = Array.from(new Set(rows.map(r => r.metric))).sort(); |
|
|
runList = Array.from(new Set(rows.map(r => r.run))).sort(); runOrder = runList; |
|
|
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, stderr: r.stderr }); }); dataByMetric.set(m, map); }); |
|
|
const preferred = metricList.find(m => m === 'ai2d_exact_match') || metricList.find(m => /average_rank/i.test(m)); |
|
|
const def = preferred || metricList[0]; |
|
|
renderMetric(def); |
|
|
const ro = window.ResizeObserver ? new ResizeObserver(() => renderMetric(def)) : null; if (ro) ro.observe(cell); |
|
|
if (typeof readyResolve === 'function') readyResolve(); |
|
|
} 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'; cell.appendChild(pre); |
|
|
if (typeof readyResolve === 'function') readyResolve(); |
|
|
} |
|
|
} |
|
|
load(); |
|
|
|
|
|
return { |
|
|
ready, |
|
|
getMetrics: () => metricList.slice(), |
|
|
setMetric: (m) => { if (m) renderMetric(m); }, |
|
|
getYInfo: (m) => { |
|
|
const key = m; const map = dataByMetric.get(key) || {}; const runs = runOrder; |
|
|
let maxVal = 0, minVal = Infinity; let minStep = Infinity, maxStep = -Infinity; |
|
|
const isRank = /rank/i.test(key); const isAverage = /average/i.test(key); const isRankStrict = isRank && !isAverage; |
|
|
runs.forEach(r => { (map[r] || []).forEach(pt => { const v = isRankStrict ? Math.round(pt.value) : pt.value; minStep = Math.min(minStep, pt.step); maxStep = Math.max(maxStep, pt.step); maxVal = Math.max(maxVal, v); minVal = Math.min(minVal, v); }); }); |
|
|
const rankMax = isRank ? Math.max(1, Math.round(maxVal)) : null; |
|
|
return { isRank, isRankStrict, min: maxVal === 0 && minVal === Infinity ? null : minVal, max: maxVal, rankMax }; |
|
|
}, |
|
|
setSharedY: (cfg) => { sharedYConfig = cfg || null; if (metricList && metricList.length) { const current = cfg && cfg.key ? cfg.key : null; const m = current || metricList[0]; renderMetric(m); } } |
|
|
}; |
|
|
} |
|
|
|
|
|
const bootstrap = () => { |
|
|
const scriptEl = THIS_SCRIPT; |
|
|
let host = null; |
|
|
|
|
|
const header = document.createElement('div'); header.className = 'line-quad__header'; |
|
|
const legend = document.createElement('div'); legend.className = 'legend-bottom'; legend.innerHTML = '<div class="legend-title">Legend</div><div class="items"></div>'; |
|
|
const controls = document.createElement('div'); controls.className = 'controls'; controls.innerHTML = '<div class="control-group"><label>Metric</label><select></select></div>'; |
|
|
header.appendChild(legend); |
|
|
header.appendChild(controls); |
|
|
|
|
|
if (scriptEl && scriptEl.parentElement && scriptEl.parentElement.querySelector) { |
|
|
host = scriptEl.parentElement.querySelector('.line-quad'); |
|
|
} |
|
|
|
|
|
if (!host) { |
|
|
let sib = scriptEl && scriptEl.previousElementSibling; |
|
|
while (sib && !(sib.classList && sib.classList.contains('line-quad'))) { |
|
|
sib = sib.previousElementSibling; |
|
|
} |
|
|
host = sib || null; |
|
|
} |
|
|
|
|
|
if (!host) { host = document.querySelector('.line-quad'); } |
|
|
if (!host) return; |
|
|
if (host.dataset && host.dataset.mounted === 'true') return; if (host.dataset) host.dataset.mounted = 'true'; |
|
|
const cells = host.querySelectorAll('.quad-cell'); if (!cells.length) return; |
|
|
host.appendChild(header); |
|
|
const instances = Array.from(cells).map(cell => initRunLine(cell)); |
|
|
|
|
|
(async () => { |
|
|
|
|
|
await Promise.all(instances.map(i => i.ready)); |
|
|
const lists = instances.map(i => i.getMetrics()).filter(a => Array.isArray(a) && a.length); |
|
|
const intersect = (arrs) => arrs.reduce((acc, cur) => acc.filter(x => cur.includes(x))); |
|
|
let metrics = lists.length ? intersect(lists) : []; |
|
|
if (!metrics.length) { metrics = lists[0] || []; } |
|
|
const def = (metrics.includes('ai2d_exact_match') ? 'ai2d_exact_match' : (metrics.find(m => /average_rank/i.test(m)) || metrics[0] || '')); |
|
|
|
|
|
|
|
|
const headerEl = host.querySelector('.line-quad__header'); |
|
|
if (headerEl && !headerEl.isConnected) host.appendChild(header); |
|
|
const select = (headerEl || header).querySelector('.controls select'); |
|
|
if (select) { |
|
|
select.innerHTML = ''; |
|
|
metrics.forEach(m => { const o = document.createElement('option'); o.value = m; o.textContent = prettyMetricLabel(m); select.appendChild(o); }); |
|
|
if (def) select.value = def; |
|
|
} |
|
|
|
|
|
const computeAndApplySharedY = (metric) => { |
|
|
try { |
|
|
const infos = instances.map(i => i && typeof i.getYInfo === 'function' ? i.getYInfo(metric) : null).filter(Boolean); |
|
|
if (!infos.length) return; |
|
|
const anyRank = infos.some(info => info.isRank); |
|
|
if (anyRank) { |
|
|
const maxRank = Math.max(1, ...infos.map(info => Math.round(info.rankMax || 1))); |
|
|
instances.forEach(i => i && typeof i.setSharedY === 'function' && i.setSharedY({ type: 'rank_strict', maxRank, key: metric })); |
|
|
} else { |
|
|
const min = Math.min(...infos.map(info => info.min)); |
|
|
const max = Math.max(...infos.map(info => info.max)); |
|
|
instances.forEach(i => i && typeof i.setSharedY === 'function' && i.setSharedY({ type: 'value', min, max, key: metric })); |
|
|
} |
|
|
} catch (_) { } |
|
|
}; |
|
|
|
|
|
const applyAll = (v) => { computeAndApplySharedY(v); instances.forEach(i => i && typeof i.setMetric === 'function' && i.setMetric(v)); }; |
|
|
if (def) applyAll(def); |
|
|
if (select) select.addEventListener('change', () => applyAll(select.value)); |
|
|
|
|
|
|
|
|
const legendItemsHost = (headerEl || header).querySelector('.legend-bottom .items'); |
|
|
if (legendItemsHost) { |
|
|
try { |
|
|
const f = '/data/formatting_filters.csv'; |
|
|
const r = await fetch(f, { cache: 'no-cache' }); |
|
|
if (r.ok && window.d3 && window.d3.csvParse) { |
|
|
const txt = await r.text(); |
|
|
const rows = window.d3.csvParse(txt); |
|
|
const runList = Array.from(new Set(rows.map(row => String(row.run || '').trim()).filter(Boolean))).sort(); |
|
|
const poolLegend = (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') |
|
|
? window.ColorPalettes.getColors('categorical', runList.length) |
|
|
: (() => { const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim(); return [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...((window.d3 && window.d3.schemeTableau10) ? window.d3.schemeTableau10 : ['#4e79a7', '#f28e2b', '#e15759', '#76b7b2', '#59a14f', '#edc948', '#b07aa1', '#ff9da7', '#9c755f', '#bab0ab'])]; })(); |
|
|
|
|
|
SHARED_RUN_COLOR = {}; |
|
|
runList.forEach((name, i) => { SHARED_RUN_COLOR[name] = poolLegend[i % poolLegend.length]; }); |
|
|
legendItemsHost.innerHTML = runList.map((name) => { |
|
|
const color = SHARED_RUN_COLOR[name]; |
|
|
return `<span class="item" data-run="${name}"><span class=\"swatch\" style=\"background:${color}\"></span><span>${name}</span></span>`; |
|
|
}).join(''); |
|
|
|
|
|
try { |
|
|
const currentMetric = (select && select.value) || def; |
|
|
if (currentMetric) applyAll(currentMetric); |
|
|
} catch { } |
|
|
|
|
|
legendItemsHost.querySelectorAll('.item').forEach(el => { |
|
|
el.addEventListener('mouseenter', () => { |
|
|
const run = el.getAttribute('data-run'); if (!run) return; |
|
|
host.classList.add('hovering'); |
|
|
host.querySelectorAll('.quad-cell').forEach(cell => { |
|
|
cell.querySelectorAll('.lines path.run-line').forEach(p => p.classList.toggle('ghost', p.getAttribute('data-run') !== run)); |
|
|
cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.toggle('ghost', c.getAttribute('data-run') !== run)); |
|
|
cell.querySelectorAll('.areas path.area').forEach(a => a.classList.toggle('ghost', a.getAttribute('data-run') !== run)); |
|
|
}); |
|
|
legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.toggle('ghost', it.getAttribute('data-run') !== run)); |
|
|
}); |
|
|
el.addEventListener('mouseleave', () => { |
|
|
host.classList.remove('hovering'); |
|
|
host.querySelectorAll('.quad-cell').forEach(cell => { |
|
|
cell.querySelectorAll('.lines path.run-line').forEach(p => p.classList.remove('ghost')); |
|
|
cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.remove('ghost')); |
|
|
cell.querySelectorAll('.areas path.area').forEach(a => a.classList.remove('ghost')); |
|
|
}); |
|
|
legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.remove('ghost')); |
|
|
}); |
|
|
}); |
|
|
} |
|
|
} catch { } |
|
|
} |
|
|
})(); |
|
|
}; |
|
|
|
|
|
if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); } |
|
|
})(); |
|
|
</script> |