|
|
<div class="d3-lambda-ablations"></div> |
|
|
<style> |
|
|
.d3-lambda-ablations { |
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
|
|
position: relative; |
|
|
} |
|
|
.d3-lambda-ablations svg { |
|
|
display: block; |
|
|
width: 100%; |
|
|
} |
|
|
.d3-lambda-ablations .axes path, |
|
|
.d3-lambda-ablations .axes line { |
|
|
stroke: var(--axis-color, #333); |
|
|
} |
|
|
.d3-lambda-ablations .axes text { |
|
|
fill: var(--tick-color, #666); |
|
|
font-size: 12px; |
|
|
} |
|
|
.d3-lambda-ablations .grid line { |
|
|
stroke: var(--grid-color, rgba(0,0,0,.08)); |
|
|
} |
|
|
.d3-lambda-ablations .d3-tooltip { |
|
|
position: absolute; |
|
|
pointer-events: none; |
|
|
padding: 8px 10px; |
|
|
border-radius: 8px; |
|
|
font-size: 12px; |
|
|
line-height: 1.35; |
|
|
border: 1px solid var(--border-color, #ddd); |
|
|
background: var(--surface-bg, #fff); |
|
|
color: var(--text-color, #000); |
|
|
box-shadow: 0 4px 24px rgba(0,0,0,.18); |
|
|
opacity: 0; |
|
|
transition: opacity .12s ease; |
|
|
} |
|
|
.d3-lambda-ablations .header { |
|
|
display: flex; |
|
|
justify-content: space-between; |
|
|
align-items: flex-start; |
|
|
margin-bottom: 12px; |
|
|
flex-wrap: wrap; |
|
|
gap: 12px; |
|
|
} |
|
|
.d3-lambda-ablations .legend { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: flex-start; |
|
|
gap: 6px; |
|
|
} |
|
|
.d3-lambda-ablations .legend-title { |
|
|
font-size: 12px; |
|
|
font-weight: 700; |
|
|
color: var(--text-color, #000); |
|
|
} |
|
|
.d3-lambda-ablations .legend .items { |
|
|
display: flex; |
|
|
flex-wrap: wrap; |
|
|
gap: 8px 14px; |
|
|
} |
|
|
.d3-lambda-ablations .legend .item { |
|
|
display: inline-flex; |
|
|
align-items: center; |
|
|
gap: 6px; |
|
|
white-space: nowrap; |
|
|
font-size: 12px; |
|
|
color: var(--text-color, #000); |
|
|
} |
|
|
.d3-lambda-ablations .legend .swatch { |
|
|
width: 14px; |
|
|
height: 14px; |
|
|
border-radius: 3px; |
|
|
border: 1px solid var(--border-color, #ddd); |
|
|
} |
|
|
.d3-lambda-ablations .legend .swatch-line { |
|
|
width: 20px; |
|
|
height: 2px; |
|
|
border: none; |
|
|
border-radius: 0; |
|
|
background: none; |
|
|
border-top: 2px dashed #2ca02c; |
|
|
opacity: 0.7; |
|
|
} |
|
|
.d3-lambda-ablations .controls { |
|
|
display: flex; |
|
|
gap: 16px; |
|
|
align-items: center; |
|
|
flex-wrap: wrap; |
|
|
} |
|
|
.d3-lambda-ablations .controls .control-group { |
|
|
display: flex; |
|
|
flex-direction: column; |
|
|
align-items: flex-start; |
|
|
gap: 6px; |
|
|
} |
|
|
.d3-lambda-ablations .controls label { |
|
|
font-size: 12px; |
|
|
font-weight: 700; |
|
|
color: var(--text-color, #000); |
|
|
} |
|
|
.d3-lambda-ablations .controls select { |
|
|
font-size: 12px; |
|
|
padding: 8px 28px 8px 10px; |
|
|
border: 1px solid var(--border-color, #ddd); |
|
|
border-radius: 8px; |
|
|
background: var(--surface-bg, #fff); |
|
|
color: var(--text-color, #000); |
|
|
cursor: pointer; |
|
|
appearance: none; |
|
|
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 12 12'%3E%3Cpath fill='%23666' d='M6 9L1 4h10z'/%3E%3C/svg%3E"); |
|
|
background-repeat: no-repeat; |
|
|
background-position: right 8px center; |
|
|
} |
|
|
.d3-lambda-ablations .controls select:focus { |
|
|
outline: 2px solid var(--primary-color, #0066cc); |
|
|
outline-offset: 2px; |
|
|
} |
|
|
</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-lambda-ablations'))) { |
|
|
const candidates = Array.from(document.querySelectorAll('.d3-lambda-ablations')) |
|
|
.filter((el) => !(el.dataset && el.dataset.mounted === 'true')); |
|
|
container = candidates[candidates.length - 1] || null; |
|
|
} |
|
|
if (!container) return; |
|
|
if (container.dataset) { |
|
|
if (container.dataset.mounted === 'true') return; |
|
|
container.dataset.mounted = 'true'; |
|
|
} |
|
|
|
|
|
|
|
|
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'; |
|
|
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; |
|
|
} |
|
|
|
|
|
|
|
|
let header = container.querySelector('.header'); |
|
|
if (!header) { |
|
|
header = document.createElement('div'); |
|
|
header.className = 'header'; |
|
|
container.appendChild(header); |
|
|
} |
|
|
|
|
|
|
|
|
let legend = header.querySelector('.legend'); |
|
|
if (!legend) { |
|
|
legend = document.createElement('div'); |
|
|
legend.className = 'legend'; |
|
|
header.appendChild(legend); |
|
|
} |
|
|
|
|
|
|
|
|
let controls = header.querySelector('.controls'); |
|
|
if (!controls) { |
|
|
controls = document.createElement('div'); |
|
|
controls.className = 'controls'; |
|
|
header.appendChild(controls); |
|
|
} |
|
|
|
|
|
let viewControl = controls.querySelector('.control-group'); |
|
|
if (!viewControl) { |
|
|
viewControl = document.createElement('div'); |
|
|
viewControl.className = 'control-group'; |
|
|
const label = document.createElement('label'); |
|
|
label.textContent = 'View'; |
|
|
label.setAttribute('for', 'view-select-lambda'); |
|
|
const select = document.createElement('select'); |
|
|
select.id = 'view-select-lambda'; |
|
|
select.innerHTML = '<option value="line">Learning Curve</option><option value="bar-teacher">Distilled Ratio</option>'; |
|
|
viewControl.appendChild(label); |
|
|
viewControl.appendChild(select); |
|
|
controls.appendChild(viewControl); |
|
|
} |
|
|
|
|
|
const viewSelect = controls.querySelector('select'); |
|
|
|
|
|
|
|
|
const svg = d3.select(container).append('svg').attr('width', '100%').style('display', 'block'); |
|
|
const gRoot = svg.append('g'); |
|
|
|
|
|
|
|
|
let width = 800, height = 360; |
|
|
const margin = { top: 16, right: 28, bottom: 56, left: 64 }; |
|
|
let currentView = 'line'; |
|
|
let data = []; |
|
|
let lambdas = []; |
|
|
let colorScale; |
|
|
let teacherPerformance = 0; |
|
|
|
|
|
function updateSize() { |
|
|
width = container.clientWidth || 800; |
|
|
height = Math.max(260, Math.round(width / 3)); |
|
|
svg.attr('width', width).attr('height', height); |
|
|
gRoot.attr('transform', `translate(${margin.left},${margin.top})`); |
|
|
return { |
|
|
innerWidth: width - margin.left - margin.right, |
|
|
innerHeight: height - margin.top - margin.bottom |
|
|
}; |
|
|
} |
|
|
|
|
|
function makeLegend(seriesNames, colorFor, includeTeacher = false) { |
|
|
let title = legend.querySelector('.legend-title'); |
|
|
if (!title) { |
|
|
title = document.createElement('div'); |
|
|
title.className = 'legend-title'; |
|
|
title.textContent = 'Lambda'; |
|
|
legend.appendChild(title); |
|
|
} |
|
|
let items = legend.querySelector('.items'); |
|
|
if (!items) { |
|
|
items = document.createElement('div'); |
|
|
items.className = 'items'; |
|
|
legend.appendChild(items); |
|
|
} |
|
|
items.innerHTML = ''; |
|
|
seriesNames.forEach(name => { |
|
|
const el = document.createElement('span'); |
|
|
el.className = 'item'; |
|
|
const sw = document.createElement('span'); |
|
|
sw.className = 'swatch'; |
|
|
sw.style.background = colorFor(name); |
|
|
const txt = document.createElement('span'); |
|
|
|
|
|
const displayName = name.replace('Lambda=', ''); |
|
|
txt.textContent = displayName; |
|
|
el.appendChild(sw); |
|
|
el.appendChild(txt); |
|
|
items.appendChild(el); |
|
|
}); |
|
|
|
|
|
|
|
|
if (includeTeacher) { |
|
|
const el = document.createElement('span'); |
|
|
el.className = 'item'; |
|
|
const sw = document.createElement('span'); |
|
|
sw.className = 'swatch-line'; |
|
|
const txt = document.createElement('span'); |
|
|
txt.textContent = 'Teacher'; |
|
|
el.appendChild(sw); |
|
|
el.appendChild(txt); |
|
|
items.appendChild(el); |
|
|
} |
|
|
} |
|
|
|
|
|
function renderLine() { |
|
|
const { innerWidth, innerHeight } = updateSize(); |
|
|
|
|
|
gRoot.selectAll('*').remove(); |
|
|
|
|
|
const x = d3.scaleLinear() |
|
|
.domain([0, d3.max(data, d => d.step) || 1]) |
|
|
.range([0, innerWidth]); |
|
|
|
|
|
const y = d3.scaleLinear() |
|
|
.domain([0, d3.max(data, d => Math.max(d.value, teacherPerformance)) || 1]) |
|
|
.range([innerHeight, 0]) |
|
|
.nice(); |
|
|
|
|
|
|
|
|
const gridGroup = gRoot.append('g').attr('class', 'grid'); |
|
|
gridGroup.selectAll('line.grid-y') |
|
|
.data(y.ticks(6)) |
|
|
.join('line') |
|
|
.attr('class', 'grid-y') |
|
|
.attr('x1', 0) |
|
|
.attr('x2', innerWidth) |
|
|
.attr('y1', d => y(d)) |
|
|
.attr('y2', d => y(d)); |
|
|
|
|
|
|
|
|
const axesGroup = gRoot.append('g').attr('class', 'axes'); |
|
|
axesGroup.append('g') |
|
|
.attr('transform', `translate(0,${innerHeight})`) |
|
|
.call(d3.axisBottom(x).ticks(6)); |
|
|
axesGroup.append('g') |
|
|
.call(d3.axisLeft(y).ticks(6)); |
|
|
|
|
|
|
|
|
axesGroup.append('text') |
|
|
.attr('x', innerWidth / 2) |
|
|
.attr('y', innerHeight + 40) |
|
|
.attr('text-anchor', 'middle') |
|
|
.attr('fill', 'var(--text-color, #000)') |
|
|
.style('font-size', '12px') |
|
|
.style('font-weight', '700') |
|
|
.text('Training Step'); |
|
|
|
|
|
axesGroup.append('text') |
|
|
.attr('transform', 'rotate(-90)') |
|
|
.attr('x', -innerHeight / 2) |
|
|
.attr('y', -48) |
|
|
.attr('text-anchor', 'middle') |
|
|
.attr('fill', 'var(--text-color, #000)') |
|
|
.style('font-size', '12px') |
|
|
.style('font-weight', '700') |
|
|
.text('Pass Rate'); |
|
|
|
|
|
|
|
|
const grouped = d3.group(data, d => d.Lambda); |
|
|
|
|
|
|
|
|
const lineGroup = gRoot.append('g').attr('class', 'lines'); |
|
|
|
|
|
|
|
|
lineGroup.append('line') |
|
|
.attr('x1', 0) |
|
|
.attr('x2', innerWidth) |
|
|
.attr('y1', y(teacherPerformance)) |
|
|
.attr('y2', y(teacherPerformance)) |
|
|
.attr('stroke', '#2ca02c') |
|
|
.attr('stroke-width', 2) |
|
|
.attr('stroke-dasharray', '5,5') |
|
|
.attr('opacity', 0.5); |
|
|
|
|
|
lambdas.forEach((lambda, i) => { |
|
|
const lambdaData = grouped.get(lambda) || []; |
|
|
const sortedData = lambdaData.sort((a, b) => a.step - b.step); |
|
|
|
|
|
const line = d3.line() |
|
|
.x(d => x(d.step)) |
|
|
.y(d => y(d.value)); |
|
|
|
|
|
lineGroup.append('path') |
|
|
.datum(sortedData) |
|
|
.attr('fill', 'none') |
|
|
.attr('stroke', colorScale(lambda)) |
|
|
.attr('stroke-width', 2) |
|
|
.attr('d', line); |
|
|
|
|
|
|
|
|
lineGroup.selectAll(`.point-${i}`) |
|
|
.data(sortedData) |
|
|
.join('circle') |
|
|
.attr('class', `point-${i}`) |
|
|
.attr('cx', d => x(d.step)) |
|
|
.attr('cy', d => y(d.value)) |
|
|
.attr('r', 4) |
|
|
.attr('fill', colorScale(lambda)) |
|
|
.attr('stroke', 'var(--surface-bg, #fff)') |
|
|
.attr('stroke-width', 1.5) |
|
|
.style('cursor', 'pointer') |
|
|
.on('mouseenter', (event, d) => { |
|
|
tipInner.innerHTML = `<strong>${d.Lambda}</strong><br/>Step: ${d.step}<br/>pass@1:4: ${d.value.toFixed(4)}`; |
|
|
tip.style.opacity = '1'; |
|
|
const [px, py] = d3.pointer(event, container); |
|
|
tip.style.transform = `translate(${px + 10}px, ${py - 10}px)`; |
|
|
}) |
|
|
.on('mouseleave', () => { |
|
|
tip.style.opacity = '0'; |
|
|
}); |
|
|
}); |
|
|
} |
|
|
|
|
|
function renderBarTeacher() { |
|
|
const { innerWidth, innerHeight } = updateSize(); |
|
|
|
|
|
gRoot.selectAll('*').remove(); |
|
|
|
|
|
|
|
|
const grouped = d3.group(data, d => d.Lambda); |
|
|
const bestData = []; |
|
|
lambdas.forEach(lambda => { |
|
|
const lambdaData = grouped.get(lambda) || []; |
|
|
if (lambdaData.length > 0) { |
|
|
const best = lambdaData.reduce((a, b) => a.value > b.value ? a : b); |
|
|
const improvement = best.value / teacherPerformance; |
|
|
bestData.push({ |
|
|
lambda: lambda, |
|
|
displayLambda: lambda.replace('Lambda=', ''), |
|
|
improvement: improvement, |
|
|
value: best.value |
|
|
}); |
|
|
} |
|
|
}); |
|
|
|
|
|
bestData.sort((a, b) => b.improvement - a.improvement); |
|
|
|
|
|
const x = d3.scaleBand() |
|
|
.domain(bestData.map(d => d.displayLambda)) |
|
|
.range([0, innerWidth]) |
|
|
.padding(0.2); |
|
|
|
|
|
const y = d3.scaleLinear() |
|
|
.domain([0, d3.max(bestData, d => d.improvement) || 1]) |
|
|
.range([innerHeight, 0]) |
|
|
.nice(); |
|
|
|
|
|
|
|
|
const gridGroup = gRoot.append('g').attr('class', 'grid'); |
|
|
gridGroup.selectAll('line.grid-y') |
|
|
.data(y.ticks(6)) |
|
|
.join('line') |
|
|
.attr('class', 'grid-y') |
|
|
.attr('x1', 0) |
|
|
.attr('x2', innerWidth) |
|
|
.attr('y1', d => y(d)) |
|
|
.attr('y2', d => y(d)); |
|
|
|
|
|
|
|
|
const axesGroup = gRoot.append('g').attr('class', 'axes'); |
|
|
axesGroup.append('g') |
|
|
.attr('transform', `translate(0,${innerHeight})`) |
|
|
.call(d3.axisBottom(x)); |
|
|
|
|
|
axesGroup.append('g') |
|
|
.call(d3.axisLeft(y).ticks(6)); |
|
|
|
|
|
|
|
|
axesGroup.append('text') |
|
|
.attr('x', innerWidth / 2) |
|
|
.attr('y', innerHeight + 40) |
|
|
.attr('text-anchor', 'middle') |
|
|
.attr('fill', 'var(--text-color, #000)') |
|
|
.style('font-size', '12px') |
|
|
.style('font-weight', '700') |
|
|
.text('Lambda'); |
|
|
|
|
|
axesGroup.append('text') |
|
|
.attr('transform', 'rotate(-90)') |
|
|
.attr('x', -innerHeight / 2) |
|
|
.attr('y', -48) |
|
|
.attr('text-anchor', 'middle') |
|
|
.attr('fill', 'var(--text-color, #000)') |
|
|
.style('font-size', '12px') |
|
|
.style('font-weight', '700') |
|
|
.text('Student / Teacher Performance'); |
|
|
|
|
|
|
|
|
const barGroup = gRoot.append('g').attr('class', 'bars'); |
|
|
barGroup.selectAll('rect') |
|
|
.data(bestData) |
|
|
.join('rect') |
|
|
.attr('x', d => x(d.displayLambda)) |
|
|
.attr('y', d => y(d.improvement)) |
|
|
.attr('width', x.bandwidth()) |
|
|
.attr('height', d => Math.max(0, innerHeight - y(d.improvement))) |
|
|
.attr('fill', d => colorScale(d.lambda)) |
|
|
.style('cursor', 'pointer') |
|
|
.on('mouseenter', (event, d) => { |
|
|
tipInner.innerHTML = `<strong>Lambda=${d.displayLambda}</strong><br/>Student: ${d.value.toFixed(4)}<br/>Teacher: ${teacherPerformance.toFixed(4)}<br/>Ratio: ${d.improvement.toFixed(4)}`; |
|
|
tip.style.opacity = '1'; |
|
|
const [px, py] = d3.pointer(event, container); |
|
|
tip.style.transform = `translate(${px + 10}px, ${py - 10}px)`; |
|
|
}) |
|
|
.on('mouseleave', () => { |
|
|
tip.style.opacity = '0'; |
|
|
}); |
|
|
} |
|
|
|
|
|
function render() { |
|
|
if (currentView === 'line') { |
|
|
renderLine(); |
|
|
} else if (currentView === 'bar-teacher') { |
|
|
renderBarTeacher(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
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 (_) {} |
|
|
} |
|
|
throw new Error('CSV not found'); |
|
|
}; |
|
|
|
|
|
const CSV_PATHS = [ |
|
|
'/data/lambda_ablations.csv', |
|
|
'./assets/data/lambda_ablations.csv', |
|
|
'../assets/data/lambda_ablations.csv', |
|
|
'../../assets/data/lambda_ablations.csv' |
|
|
]; |
|
|
|
|
|
fetchFirstAvailable(CSV_PATHS) |
|
|
.then(csvText => { |
|
|
data = d3.csvParse(csvText, d => ({ |
|
|
Lambda: d.Lambda, |
|
|
value: +d.value, |
|
|
step: +d.step, |
|
|
teacher_performance: +d.teacher_performance |
|
|
})); |
|
|
|
|
|
lambdas = [...new Set(data.map(d => d.Lambda))]; |
|
|
teacherPerformance = data[0].teacher_performance; |
|
|
|
|
|
|
|
|
const grouped = d3.group(data, d => d.Lambda); |
|
|
const lambdaPerf = lambdas.map(l => { |
|
|
const lData = grouped.get(l) || []; |
|
|
const maxVal = d3.max(lData, d => d.value) || 0; |
|
|
return { lambda: l, maxVal }; |
|
|
}); |
|
|
lambdaPerf.sort((a, b) => b.maxVal - a.maxVal); |
|
|
lambdas = lambdaPerf.map(d => d.lambda); |
|
|
|
|
|
|
|
|
const catColors = window.ColorPalettes |
|
|
? window.ColorPalettes.getColors('categorical', lambdas.length) |
|
|
: ['#1f77b4', '#ff7f0e', '#2ca02c', '#d62728', '#9467bd', '#8c564b', '#e377c2', '#7f7f7f']; |
|
|
colorScale = d3.scaleOrdinal().domain(lambdas).range(catColors); |
|
|
|
|
|
makeLegend(lambdas, colorScale, true); |
|
|
render(); |
|
|
|
|
|
|
|
|
viewSelect.addEventListener('change', (e) => { |
|
|
currentView = e.target.value; |
|
|
render(); |
|
|
}); |
|
|
|
|
|
|
|
|
const rerender = () => render(); |
|
|
if (window.ResizeObserver) { |
|
|
const ro = new ResizeObserver(() => rerender()); |
|
|
ro.observe(container); |
|
|
} else { |
|
|
window.addEventListener('resize', rerender); |
|
|
} |
|
|
}) |
|
|
.catch(err => { |
|
|
const pre = document.createElement('pre'); |
|
|
pre.style.color = 'red'; |
|
|
pre.style.fontSize = '12px'; |
|
|
pre.textContent = `Failed to load data: ${err.message}`; |
|
|
container.appendChild(pre); |
|
|
}); |
|
|
}; |
|
|
|
|
|
if (document.readyState === 'loading') { |
|
|
document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); |
|
|
} else { |
|
|
ensureD3(bootstrap); |
|
|
} |
|
|
})(); |
|
|
</script> |
|
|
|