| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| <div class="d3-line-chart"></div> |
| <style> |
| .d3-line-chart { |
| position: relative; |
| } |
| |
| .d3-line-chart .axis-label { |
| fill: var(--text-color); |
| font-size: 12px; |
| font-weight: 300; |
| opacity: 0.7; |
| stroke: var(--page-bg, white); |
| background-color: var(--surface-bg) !important; |
| stroke-width: 6px; |
| paint-order: stroke fill; |
| } |
| |
| .d3-line-chart .axes path { |
| display: none; |
| } |
| |
| .d3-line-chart .axes line { |
| stroke: var(--axis-color); |
| } |
| |
| .d3-line-chart .axes text { |
| fill: var(--tick-color); |
| } |
| |
| .d3-line-chart .grid line { |
| stroke: var(--grid-color); |
| } |
| |
| |
| .d3-line-chart .legend-bottom { |
| display: flex; |
| align-items: flex-start; |
| justify-content: flex-start; |
| font-size: 12px; |
| color: var(--text-color); |
| flex-direction: column; |
| gap: 6px; |
| } |
| |
| .d3-line-chart .legend-bottom .legend-title { |
| font-size: 12px; |
| font-weight: 700; |
| color: var(--text-color); |
| } |
| |
| .d3-line-chart .legend-bottom .items { |
| display: flex; |
| flex-wrap: wrap; |
| gap: 8px 14px; |
| } |
| |
| .d3-line-chart .legend-bottom .item { |
| display: inline-flex; |
| align-items: center; |
| gap: 6px; |
| white-space: nowrap; |
| cursor: pointer; |
| } |
| |
| .d3-line-chart .legend-bottom .swatch { |
| width: 14px; |
| height: 14px; |
| border-radius: 3px; |
| border: 1px solid var(--border-color); |
| display: inline-block; |
| } |
| |
| .d3-line-chart path.main-line { |
| transition: opacity 0.2s ease; |
| } |
| |
| .d3-line-chart path.ghost-line { |
| transition: opacity 0.6s ease; |
| } |
| |
| |
| .d3-line-chart.hovering .legend-bottom .item.ghost { |
| opacity: .35; |
| } |
| |
| .d3-line-chart.hovering path.main-line.ghost { |
| opacity: .25; |
| } |
| |
| .d3-line-chart.hovering path.ghost-line.ghost { |
| opacity: .05; |
| } |
| |
| |
| .d3-line-chart .d3-tooltip { |
| z-index: 20; |
| backdrop-filter: saturate(1.12) blur(8px); |
| } |
| |
| .d3-line-chart .d3-tooltip__inner { |
| display: flex; |
| flex-direction: column; |
| gap: 6px; |
| min-width: 220px; |
| } |
| |
| .d3-line-chart .d3-tooltip__inner>div:first-child { |
| font-weight: 800; |
| letter-spacing: 0.1px; |
| margin-bottom: 0; |
| } |
| |
| .d3-line-chart .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; |
| } |
| |
| .d3-line-chart .d3-tooltip__inner>div:nth-child(n+3) { |
| padding-top: 6px; |
| border-top: 1px solid var(--border-color); |
| } |
| |
| .d3-line-chart .d3-tooltip__color-dot { |
| display: inline-block; |
| width: 12px; |
| height: 12px; |
| border-radius: 3px; |
| border: 1px solid var(--border-color); |
| } |
| |
| |
| .d3-line-chart .reference-line { |
| stroke: #e15759; |
| stroke-width: 2; |
| stroke-dasharray: 3, 3; |
| } |
| |
| .d3-line-chart .reference-label { |
| fill: var(--text-color); |
| font-size: 11px; |
| font-weight: 600; |
| } |
| |
| |
| .d3-line-chart .chart-card { |
| padding: 0; |
| } |
| |
| .d3-line-chart .chart-header { |
| display: flex; |
| align-items: flex-start; |
| justify-content: flex-start; |
| gap: 12px; |
| margin: 12px 0 0 0; |
| flex-wrap: wrap; |
| } |
| |
| |
| </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-line-chart'))) { |
| |
| let currentEl = scriptEl; |
| while (currentEl && currentEl.parentNode) { |
| currentEl = currentEl.parentNode; |
| const found = currentEl.querySelector && currentEl.querySelector('.d3-line-chart:not([data-mounted="true"])'); |
| if (found) { |
| container = found; |
| break; |
| } |
| } |
| |
| |
| if (!container) { |
| const cs = Array.from(document.querySelectorAll('.d3-line-chart')).filter(el => !(el.dataset && el.dataset.mounted === 'true')); |
| container = cs[0] || 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'; |
| Object.assign(tip.style, { |
| position: 'absolute', top: '0px', left: '0px', transform: 'translate(-9999px, -9999px)', pointerEvents: 'none', |
| padding: '10px 12px', borderRadius: '12px', fontSize: '12px', lineHeight: '1.35', border: '1px solid var(--border-color)', |
| background: 'var(--surface-bg)', color: 'var(--text-color)', boxShadow: '0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)', opacity: '0', transition: 'opacity .12s ease' |
| }); |
| 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; } |
| |
| |
| const header = document.createElement('div'); |
| header.className = 'chart-header'; |
| const legend = document.createElement('div'); |
| legend.className = 'legend-bottom'; |
| legend.innerHTML = '<div class="legend-title">Legend</div><div class="items"></div>'; |
| header.appendChild(legend); |
| |
| const chartCard = document.createElement('div'); |
| chartCard.className = 'chart-card'; |
| chartCard.style.position = 'relative'; |
| |
| |
| const resetBtn = document.createElement('button'); |
| resetBtn.className = 'reset-button'; |
| resetBtn.textContent = 'Reset View'; |
| Object.assign(resetBtn.style, { |
| position: 'absolute', |
| top: '12px', |
| right: '12px', |
| zIndex: '10', |
| display: 'none', |
| opacity: '0', |
| transition: 'opacity 0.2s ease', |
| fontSize: '12px', |
| padding: '4px 8px', |
| borderRadius: '6px', |
| background: 'var(--surface-bg)', |
| color: 'var(--text-color)', |
| border: '1px solid var(--border-color)', |
| cursor: 'pointer' |
| }); |
| chartCard.appendChild(resetBtn); |
| |
| container.appendChild(chartCard); |
| container.appendChild(header); |
| |
| |
| |
| const d3 = window.d3; |
| |
| |
| function readEmbedConfig() { |
| let mountEl = container; |
| while (mountEl && !mountEl.getAttribute?.('data-config')) { |
| mountEl = mountEl.parentElement; |
| } |
| |
| let providedConfig = null; |
| try { |
| const cfg = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-config') : null; |
| if (cfg && cfg.trim()) { |
| providedConfig = cfg.trim().startsWith('{') ? JSON.parse(cfg) : cfg; |
| } |
| } catch (e) { |
| |
| } |
| return providedConfig || {}; |
| } |
| |
| const embedConfig = readEmbedConfig(); |
| |
| |
| const CONFIG = { |
| width: 800, |
| height: 320, |
| margin: { top: 20, right: 10, bottom: 52, left: 52 }, |
| xDomain: embedConfig.xDomain || null, |
| yDomain: embedConfig.yDomain || null, |
| zoomExtent: [1.0, 8], |
| smoothing: embedConfig.smoothing !== undefined ? embedConfig.smoothing : false, |
| smoothingWindow: embedConfig.smoothingWindow || 15, |
| smoothingCurve: embedConfig.smoothingCurve || 'monotoneX', |
| transitionDuration: 600, |
| dataUrl: embedConfig.dataUrl || null, |
| title: embedConfig.title || 'Chart', |
| xAxisLabel: embedConfig.xAxisLabel || 'Consumed tokens', |
| yAxisLabel: embedConfig.yAxisLabel || 'Loss', |
| xColumn: embedConfig.xColumn || 'tokens', |
| yColumn: embedConfig.yColumn || 'loss', |
| runColumn: embedConfig.runColumn || 'run_name', |
| xScaleType: embedConfig.xScaleType || 'linear', |
| yScaleType: embedConfig.yScaleType || 'linear', |
| referenceLine: embedConfig.referenceLine || null, |
| xFormatAsFileSize: embedConfig.xFormatAsFileSize || false |
| }; |
| |
| let width = CONFIG.width; |
| let height = CONFIG.height; |
| const margin = CONFIG.margin; |
| let smoothEnabled = CONFIG.smoothing; |
| let hasMoved = false; |
| |
| |
| const svg = d3.select(chartCard) |
| .append('svg') |
| .attr('width', '100%') |
| .style('display', 'block'); |
| |
| |
| const clipId = 'clip-' + Math.random().toString(36).slice(2); |
| const clipPath = svg.append('defs') |
| .append('clipPath') |
| .attr('id', clipId); |
| const clipRect = clipPath.append('rect'); |
| |
| |
| const g = svg.append('g'); |
| |
| |
| const gGrid = g.append('g').attr('class', 'grid'); |
| const gAxes = g.append('g').attr('class', 'axes'); |
| const gReference = g.append('g').attr('class', 'reference'); |
| |
| |
| const gPlot = g.append('g') |
| .attr('class', 'plot') |
| .attr('clip-path', `url(#${clipId})`); |
| |
| |
| const gHover = g.append('g').attr('class', 'hover-layer'); |
| |
| |
| const overlay = g.append('rect') |
| .attr('class', 'overlay') |
| .attr('fill', 'none') |
| .attr('pointer-events', 'all') |
| .style('cursor', 'grab') |
| .on('mousedown', function () { |
| d3.select(this).style('cursor', 'grabbing'); |
| |
| if (tip) tip.style.opacity = '0'; |
| if (hoverLine) hoverLine.style('display', 'none'); |
| }) |
| .on('mouseup', function () { d3.select(this).style('cursor', 'grab'); }); |
| |
| |
| const xScale = (CONFIG.xScaleType === 'log' ? d3.scaleLog() : d3.scaleLinear()).domain(CONFIG.xDomain || [0.1, 1]); |
| const yScale = (CONFIG.yScaleType === 'log' ? d3.scaleLog() : d3.scaleLinear()).domain(CONFIG.yDomain || [0.1, 1]); |
| |
| |
| let data = []; |
| let runList = []; |
| let runColorMap = {}; |
| |
| |
| let hoverLine = null; |
| let tokens = []; |
| let hideTipTimer = null; |
| |
| |
| const getCurve = (smooth) => { |
| if (!smooth) return d3.curveLinear; |
| switch (CONFIG.smoothingCurve) { |
| case 'catmullRom': return d3.curveCatmullRom.alpha(0.5); |
| case 'monotoneX': return d3.curveMonotoneX; |
| case 'basis': return d3.curveBasis; |
| default: return d3.curveLinear; |
| } |
| }; |
| |
| function movingAverage(values, windowSize) { |
| if (!Array.isArray(values) || values.length === 0 || windowSize <= 1) return values; |
| const half = Math.floor(windowSize / 2); |
| const out = new Array(values.length); |
| for (let i = 0; i < values.length; i++) { |
| let sum = 0; let count = 0; |
| const start = Math.max(0, i - half); |
| const end = Math.min(values.length - 1, i + half); |
| for (let j = start; j <= end; j++) { if (!Number.isNaN(values[j].loss)) { sum += values[j].loss; count++; } } |
| const avg = count ? (sum / count) : values[i].loss; |
| out[i] = { tokens: values[i].tokens, loss: avg }; |
| } |
| return out; |
| } |
| |
| function applySmoothing(values, smooth) { |
| if (!smooth) return values; |
| return movingAverage(values, CONFIG.smoothingWindow); |
| } |
| |
| |
| let formatX = (v) => v; |
| let formatY = (v) => v; |
| const MAX_TICKS = 10; |
| |
| function limitTicks(ticks, maxCount, domain) { |
| if (!Array.isArray(ticks)) return ticks; |
| if (ticks.length <= maxCount) return ticks; |
| const first = ticks[0]; |
| const last = ticks[ticks.length - 1]; |
| const step = Math.ceil(ticks.length / maxCount); |
| const sampled = ticks.filter((_, i) => i % step === 0); |
| if (sampled[0] !== first && first >= domain[0]) sampled.unshift(first); |
| if (sampled[sampled.length - 1] !== last && last <= domain[1]) sampled.push(last); |
| return sampled.slice(0, maxCount); |
| } |
| |
| |
| function createSmartFormatter(values, isFileSize = false) { |
| if (!values || values.length === 0) return (v) => v; |
| |
| const min = d3.min(values); |
| const max = d3.max(values); |
| const range = max - min; |
| |
| |
| const allIntegers = values.every(v => Math.abs(v - Math.round(v)) < 0.001); |
| |
| |
| if (isFileSize) { |
| const KiB = 1024; |
| const MiB = KiB * 1024; |
| const GiB = MiB * 1024; |
| return (v) => { |
| if (v >= GiB) { |
| const gib = v / GiB; |
| return (gib % 1 === 0 ? d3.format('d')(gib) : d3.format('.2f')(gib)) + ' GiB'; |
| } else if (v >= MiB) { |
| const mib = v / MiB; |
| return (mib % 1 === 0 ? d3.format('d')(mib) : d3.format('.2f')(mib)) + ' MiB'; |
| } else if (v >= KiB) { |
| const kib = v / KiB; |
| return (kib % 1 === 0 ? d3.format('d')(kib) : d3.format('.1f')(kib)) + ' KiB'; |
| } else { |
| return d3.format('d')(Math.round(v)) + ' B'; |
| } |
| }; |
| } |
| |
| |
| if (max >= 1e9) { |
| return (v) => { |
| const billions = v / 1e9; |
| return allIntegers && billions === Math.round(billions) |
| ? d3.format('d')(Math.round(billions)) + 'B' |
| : d3.format('.2f')(billions) + 'B'; |
| }; |
| } |
| |
| |
| if (max >= 1e6) { |
| return (v) => { |
| const millions = v / 1e6; |
| return allIntegers && millions === Math.round(millions) |
| ? d3.format('d')(Math.round(millions)) + 'M' |
| : d3.format('.2f')(millions) + 'M'; |
| }; |
| } |
| |
| |
| if (max >= 1000 && range >= 100) { |
| return (v) => { |
| const thousands = v / 1000; |
| return allIntegers && thousands === Math.round(thousands) |
| ? d3.format('d')(Math.round(thousands)) + 'k' |
| : d3.format('.1f')(thousands) + 'k'; |
| }; |
| } |
| |
| |
| if (allIntegers) { |
| return (v) => d3.format('d')(Math.round(v)); |
| } |
| |
| |
| if (range < 1) { |
| return (v) => d3.format('.3f')(v); |
| } else if (range < 10) { |
| return (v) => d3.format('.2f')(v); |
| } else { |
| return (v) => d3.format('.1f')(v); |
| } |
| } |
| |
| |
| const getRunColors = (n) => { |
| try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') return window.ColorPalettes.getColors('categorical', n); } catch (_) { } |
| const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB'; |
| return [primary, '#4EA5B7', '#E38A42', '#CEC0FA', '#9B59B6', '#16A085', ...(d3.schemeTableau10 || [])].slice(0, n); |
| }; |
| |
| |
| const zoom = d3.zoom() |
| .scaleExtent(CONFIG.zoomExtent) |
| .on('zoom', zoomed); |
| |
| |
| overlay.call(zoom); |
| |
| function zoomed(event) { |
| const transform = event.transform; |
| hasMoved = transform.k !== 1 || transform.x !== 0 || transform.y !== 0; |
| updateResetButton(); |
| |
| |
| const newXScale = transform.rescaleX(xScale); |
| const newYScale = transform.rescaleY(yScale); |
| |
| |
| const innerWidth = xScale.range()[1]; |
| |
| |
| |
| const getGridTicks = (scale, scaleType) => { |
| if (scaleType !== 'log') { |
| return scale.ticks(Math.min(6, MAX_TICKS)); |
| } |
| |
| const domain = scale.domain(); |
| const minLog = Math.log10(domain[0]); |
| const maxLog = Math.log10(domain[1]); |
| const logRange = maxLog - minLog; |
| |
| if (logRange < 2) { |
| return scale.ticks(Math.min(4, Math.ceil(logRange * 2))); |
| } |
| |
| const ticks = []; |
| const minPower = Math.ceil(minLog); |
| const maxPower = Math.floor(maxLog); |
| |
| for (let power = minPower; power <= maxPower; power++) { |
| ticks.push(Math.pow(10, power)); |
| } |
| |
| if (ticks.length < 4 && logRange < 3) { |
| const intermediateTicks = []; |
| for (let power = minPower; power < maxPower; power++) { |
| intermediateTicks.push(5 * Math.pow(10, power)); |
| } |
| ticks.push(...intermediateTicks); |
| ticks.sort((a, b) => a - b); |
| } |
| |
| const within = ticks.filter(t => t >= domain[0] && t <= domain[1]); |
| return limitTicks(within, MAX_TICKS, domain); |
| }; |
| |
| |
| const gridTicks = getGridTicks(newYScale, CONFIG.yScaleType); |
| gGrid.selectAll('line') |
| .data(gridTicks) |
| .join('line') |
| .attr('x1', 0) |
| .attr('x2', innerWidth) |
| .attr('y1', d => newYScale(d)) |
| .attr('y2', d => newYScale(d)) |
| .attr('stroke', 'var(--grid-color)'); |
| |
| |
| const line = d3.line() |
| .x(d => newXScale(d.tokens)) |
| .y(d => newYScale(d.loss)) |
| .curve(getCurve(smoothEnabled)); |
| |
| |
| gPlot.selectAll('path.ghost-line') |
| .attr('d', d => { |
| const rawLine = d3.line() |
| .x(d => newXScale(d.tokens)) |
| .y(d => newYScale(d.loss)) |
| .curve(d3.curveLinear); |
| return rawLine(d.values); |
| }); |
| |
| |
| gPlot.selectAll('path.main-line') |
| .attr('d', d => line(applySmoothing(d.values, smoothEnabled))); |
| |
| |
| const getSmartTicksZoom = (scale, scaleType, useBinary = false) => { |
| if (scaleType !== 'log') { |
| return scale.ticks(Math.min(6, MAX_TICKS)); |
| } |
| |
| const domain = scale.domain(); |
| if (useBinary) { |
| const minPow2 = Math.ceil(Math.log2(domain[0])); |
| const maxPow2 = Math.floor(Math.log2(domain[1])); |
| const ticks = []; |
| for (let p = minPow2; p <= maxPow2; p++) ticks.push(Math.pow(2, p)); |
| const within = ticks.filter(t => t >= domain[0] && t <= domain[1]); |
| return limitTicks(within, MAX_TICKS, domain); |
| } |
| |
| const minLog = Math.log10(domain[0]); |
| const maxLog = Math.log10(domain[1]); |
| const logRange = maxLog - minLog; |
| |
| if (logRange < 2) { |
| return scale.ticks(Math.min(MAX_TICKS, Math.min(4, Math.ceil(logRange * 2)))); |
| } |
| |
| const ticks = []; |
| const minPower = Math.ceil(minLog); |
| const maxPower = Math.floor(maxLog); |
| |
| for (let power = minPower; power <= maxPower; power++) { |
| ticks.push(Math.pow(10, power)); |
| } |
| |
| if (ticks.length < 4 && logRange < 3) { |
| const intermediateTicks = []; |
| for (let power = minPower; power < maxPower; power++) { |
| intermediateTicks.push(5 * Math.pow(10, power)); |
| } |
| ticks.push(...intermediateTicks); |
| ticks.sort((a, b) => a - b); |
| } |
| |
| const within = ticks.filter(t => t >= domain[0] && t <= domain[1]); |
| return limitTicks(within, MAX_TICKS, domain); |
| }; |
| |
| const newXTicks = getSmartTicksZoom(newXScale, CONFIG.xScaleType, !!CONFIG.xFormatAsFileSize); |
| const newYTicks = getSmartTicksZoom(newYScale, CONFIG.yScaleType); |
| |
| |
| gAxes.select('.x-axis').call(d3.axisBottom(newXScale).tickValues(newXTicks).tickSizeOuter(0).tickFormat(formatX)); |
| gAxes.select('.y-axis').call(d3.axisLeft(newYScale).tickValues(newYTicks).tickSizeOuter(0).tickFormat(formatY)); |
| |
| |
| if (CONFIG.referenceLine) { |
| const refValue = CONFIG.referenceLine.value; |
| gReference.select('.reference-line') |
| .attr('y1', newYScale(refValue)) |
| .attr('y2', newYScale(refValue)); |
| |
| gReference.select('.reference-label') |
| .attr('y', newYScale(refValue) - 5); |
| } |
| } |
| |
| function updateResetButton() { |
| if (hasMoved) { |
| resetBtn.style.display = 'block'; |
| requestAnimationFrame(() => { resetBtn.style.opacity = '1'; }); |
| } else { |
| resetBtn.style.opacity = '0'; |
| setTimeout(() => { if (!hasMoved) resetBtn.style.display = 'none'; }, 200); |
| } |
| } |
| |
| function render() { |
| if (!data.length) return; |
| |
| |
| const rect = container.getBoundingClientRect(); |
| width = Math.max(1, Math.round(rect && rect.width ? rect.width : (container.clientWidth || 800))); |
| height = Math.max(CONFIG.height, 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; |
| |
| |
| g.attr('transform', `translate(${margin.left},${margin.top})`); |
| |
| |
| xScale.range([0, innerWidth]); |
| yScale.range([innerHeight, 0]); |
| |
| |
| clipRect |
| .attr('x', 0) |
| .attr('y', 0) |
| .attr('width', innerWidth) |
| .attr('height', innerHeight); |
| |
| |
| overlay |
| .attr('x', 0) |
| .attr('y', 0) |
| .attr('width', innerWidth) |
| .attr('height', innerHeight); |
| |
| |
| zoom |
| .extent([[0, 0], [innerWidth, innerHeight]]) |
| .translateExtent([[0, 0], [innerWidth, innerHeight]]); |
| |
| |
| const getSmartTicks = (scale, scaleType, targetCount = 6, useBinary = false) => { |
| if (scaleType !== 'log') { |
| return scale.ticks(Math.min(targetCount, MAX_TICKS)); |
| } |
| |
| const domain = scale.domain(); |
| if (useBinary) { |
| const minPow2 = Math.ceil(Math.log2(domain[0])); |
| const maxPow2 = Math.floor(Math.log2(domain[1])); |
| const ticks = []; |
| for (let p = minPow2; p <= maxPow2; p++) ticks.push(Math.pow(2, p)); |
| const within = ticks.filter(t => t >= domain[0] && t <= domain[1]); |
| return limitTicks(within, MAX_TICKS, domain); |
| } |
| |
| |
| const minLog = Math.log10(domain[0]); |
| const maxLog = Math.log10(domain[1]); |
| const logRange = maxLog - minLog; |
| |
| if (logRange < 2) { |
| return scale.ticks(Math.min(MAX_TICKS, Math.min(4, Math.ceil(logRange * 2)))); |
| } |
| |
| const ticks = []; |
| const minPower = Math.ceil(minLog); |
| const maxPower = Math.floor(maxLog); |
| |
| for (let power = minPower; power <= maxPower; power++) { |
| ticks.push(Math.pow(10, power)); |
| } |
| |
| if (ticks.length < 4 && logRange < 3) { |
| const intermediateTicks = []; |
| for (let power = minPower; power < maxPower; power++) { |
| intermediateTicks.push(5 * Math.pow(10, power)); |
| } |
| ticks.push(...intermediateTicks); |
| ticks.sort((a, b) => a - b); |
| } |
| |
| const within = ticks.filter(t => t >= domain[0] && t <= domain[1]); |
| return limitTicks(within, MAX_TICKS, domain); |
| }; |
| |
| const xTicks = getSmartTicks(xScale, CONFIG.xScaleType, 6, !!CONFIG.xFormatAsFileSize); |
| const yTicks = getSmartTicks(yScale, CONFIG.yScaleType, 6); |
| |
| |
| 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', 'var(--grid-color)'); |
| |
| |
| gAxes.selectAll('*').remove(); |
| |
| gAxes.append('g') |
| .attr('class', 'x-axis') |
| .attr('transform', `translate(0,${innerHeight})`) |
| .call(d3.axisBottom(xScale).tickValues(xTicks).tickSizeOuter(0).tickFormat(formatX)) |
| .selectAll('text').attr('fill', 'var(--tick-color)'); |
| |
| gAxes.append('g') |
| .attr('class', 'y-axis') |
| .call(d3.axisLeft(yScale).tickValues(yTicks).tickSizeOuter(0).tickFormat(formatY)) |
| .selectAll('text').attr('fill', 'var(--tick-color)'); |
| |
| gAxes.selectAll('.domain, .tick line').attr('stroke', 'var(--axis-color)'); |
| |
| |
| gAxes.append('text') |
| .attr('class', 'axis-label') |
| .attr('x', innerWidth / 2) |
| .attr('y', innerHeight + 38) |
| .attr('text-anchor', 'middle') |
| .text(CONFIG.xAxisLabel); |
| |
| gAxes.append('text') |
| .attr('class', 'axis-label') |
| .attr('transform', 'rotate(-90)') |
| .attr('x', -innerHeight / 2) |
| .attr('y', -43) |
| .attr('text-anchor', 'middle') |
| .text(CONFIG.yAxisLabel); |
| |
| |
| gReference.selectAll('*').remove(); |
| if (CONFIG.referenceLine) { |
| const refValue = CONFIG.referenceLine.value; |
| const refLabel = CONFIG.referenceLine.label || null; |
| const refColor = CONFIG.referenceLine.color || '#e15759'; |
| const refDashArray = CONFIG.referenceLine.dashArray || '3,3'; |
| |
| gReference.append('line') |
| .attr('class', 'reference-line') |
| .attr('x1', 0) |
| .attr('x2', innerWidth) |
| .attr('y1', yScale(refValue)) |
| .attr('y2', yScale(refValue)) |
| .style('stroke', refColor) |
| .style('stroke-dasharray', refDashArray); |
| |
| if (refLabel) { |
| gReference.append('text') |
| .attr('class', 'reference-label') |
| .attr('x', innerWidth - 5) |
| .attr('y', yScale(refValue) - 5) |
| .attr('text-anchor', 'end') |
| .text(refLabel); |
| } |
| } |
| |
| |
| const dataByRun = {}; |
| runList.forEach(run => { dataByRun[run] = []; }); |
| data.forEach(d => { if (dataByRun[d.run]) dataByRun[d.run].push(d); }); |
| runList.forEach(run => { dataByRun[run].sort((a, b) => a.tokens - b.tokens); }); |
| |
| |
| const series = runList.map(run => ({ run, color: runColorMap[run], values: dataByRun[run] })); |
| |
| |
| const ghostLine = d3.line() |
| .x(d => xScale(d.tokens)) |
| .y(d => yScale(d.loss)) |
| .curve(d3.curveLinear); |
| |
| gPlot.selectAll('path.ghost-line').data(series, d => d.run) |
| .join('path') |
| .attr('class', 'ghost-line') |
| .attr('fill', 'none') |
| .attr('stroke', d => d.color) |
| .attr('stroke-width', 1.5) |
| .attr('opacity', smoothEnabled ? 0.15 : 0) |
| .attr('pointer-events', 'none') |
| .attr('d', d => ghostLine(d.values)); |
| |
| |
| const mainLine = d3.line() |
| .x(d => xScale(d.tokens)) |
| .y(d => yScale(d.loss)) |
| .curve(getCurve(smoothEnabled)); |
| |
| gPlot.selectAll('path.main-line').data(series, d => d.run) |
| .join('path') |
| .attr('class', 'main-line') |
| .attr('fill', 'none') |
| .attr('stroke', d => d.color) |
| .attr('stroke-width', 2) |
| .attr('opacity', 0.85) |
| .attr('d', d => mainLine(applySmoothing(d.values, smoothEnabled))); |
| |
| |
| setupHover(series, innerWidth, innerHeight); |
| } |
| |
| |
| function setupHover(series, innerWidth, innerHeight) { |
| gHover.selectAll('*').remove(); |
| |
| hoverLine = gHover.append('line') |
| .style('stroke', 'var(--text-color)') |
| .attr('stroke-opacity', 0.25) |
| .attr('stroke-width', 1) |
| .attr('y1', 0) |
| .attr('y2', innerHeight) |
| .style('display', 'none') |
| .attr('pointer-events', 'none'); |
| |
| |
| const tokenSet = new Set(); |
| series.forEach(s => s.values.forEach(v => tokenSet.add(v.tokens))); |
| tokens = Array.from(tokenSet).sort((a, b) => a - b); |
| |
| |
| overlay.on('mousemove', function (ev) { |
| |
| onHoverMove(ev, series); |
| }).on('mouseleave', onHoverLeave); |
| } |
| |
| function onHoverMove(ev, series) { |
| if (hideTipTimer) { |
| clearTimeout(hideTipTimer); |
| hideTipTimer = null; |
| } |
| |
| const [mx, my] = d3.pointer(ev, overlay.node()); |
| const targetTokens = xScale.invert(mx); |
| |
| |
| const nearest = tokens.reduce((best, t) => |
| Math.abs(t - targetTokens) < Math.abs(best - targetTokens) ? t : best, |
| tokens[0] |
| ); |
| |
| const xpx = xScale(nearest); |
| hoverLine.attr('x1', xpx).attr('x2', xpx).style('display', null); |
| |
| |
| let html = `<div><strong>${CONFIG.title}</strong></div>`; |
| html += `<div>${formatX(nearest)}</div>`; |
| |
| |
| const entries = series.map(s => { |
| const values = s.values; |
| |
| |
| let before = null, after = null; |
| for (let i = 0; i < values.length; i++) { |
| if (values[i].tokens <= nearest) { |
| before = values[i]; |
| } |
| if (values[i].tokens >= nearest && !after) { |
| after = values[i]; |
| break; |
| } |
| } |
| |
| let interpolatedLoss = null; |
| |
| if (before && after && before.tokens !== after.tokens) { |
| |
| const t = (nearest - before.tokens) / (after.tokens - before.tokens); |
| interpolatedLoss = before.loss + t * (after.loss - before.loss); |
| } else if (before && before.tokens === nearest) { |
| |
| interpolatedLoss = before.loss; |
| } else if (after && after.tokens === nearest) { |
| |
| interpolatedLoss = after.loss; |
| } else if (before) { |
| |
| interpolatedLoss = before.loss; |
| } else if (after) { |
| |
| interpolatedLoss = after.loss; |
| } |
| |
| return { |
| run: s.run, |
| color: s.color, |
| loss: interpolatedLoss |
| }; |
| }).filter(e => e.loss != null); |
| |
| entries.sort((a, b) => a.loss - b.loss); |
| |
| entries.forEach(e => { |
| html += `<div style="display:flex;align-items:center;gap:8px;"><span class="d3-tooltip__color-dot" style="background:${e.color}"></span><span>${e.run}</span><span style="margin-left:auto;font-weight:normal;">${e.loss.toFixed(4)}</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 onHoverLeave() { |
| hideTipTimer = setTimeout(() => { |
| tip.style.opacity = '0'; |
| tip.style.transform = 'translate(-9999px, -9999px)'; |
| if (hoverLine) hoverLine.style('display', 'none'); |
| }, 100); |
| } |
| |
| |
| function toggleSmoothing(enabled) { |
| smoothEnabled = enabled; |
| |
| |
| const dataByRun = {}; |
| runList.forEach(run => { dataByRun[run] = []; }); |
| data.forEach(d => { if (dataByRun[d.run]) dataByRun[d.run].push(d); }); |
| runList.forEach(run => { dataByRun[run].sort((a, b) => a.tokens - b.tokens); }); |
| const series = runList.map(run => ({ run, color: runColorMap[run], values: dataByRun[run] })); |
| |
| |
| gPlot.selectAll('path.ghost-line') |
| .transition() |
| .duration(CONFIG.transitionDuration) |
| .attr('opacity', enabled ? 0.15 : 0); |
| |
| |
| const mainLine = d3.line() |
| .x(d => xScale(d.tokens)) |
| .y(d => yScale(d.loss)) |
| .curve(getCurve(enabled)); |
| |
| gPlot.selectAll('path.main-line') |
| .data(series, d => d.run) |
| .attr('d', d => mainLine(applySmoothing(d.values, enabled))); |
| } |
| |
| |
| async function load() { |
| try { |
| |
| const csvPaths = CONFIG.dataUrl |
| ? [CONFIG.dataUrl] |
| : [ |
| '/data/attention_loss.csv', |
| './assets/data/attention_loss.csv', |
| '../assets/data/attention_loss.csv', |
| '../../assets/data/attention_loss.csv' |
| ]; |
| |
| let csvText = null; |
| for (const path of csvPaths) { |
| try { |
| const response = await fetch(path, { cache: 'no-cache' }); |
| if (response.ok) { |
| csvText = await response.text(); |
| break; |
| } |
| } catch (_) { } |
| } |
| |
| if (!csvText) throw new Error('CSV file not found'); |
| |
| const rows = d3.csvParse(csvText, d => ({ |
| run: (d[CONFIG.runColumn] || '').trim(), |
| tokens: +d[CONFIG.xColumn], |
| loss: +d[CONFIG.yColumn] |
| })); |
| |
| data = rows.filter(d => !isNaN(d.tokens) && !isNaN(d.loss)); |
| runList = Array.from(new Set(data.map(d => d.run))).sort(); |
| |
| |
| if (!CONFIG.xDomain) { |
| const xValues = data.map(d => d.tokens); |
| const xMin = d3.min(xValues); |
| const xMax = d3.max(xValues); |
| |
| if (CONFIG.xScaleType === 'log') { |
| |
| const minVal = Math.max(xMin / 1.1, 0.1); |
| const maxVal = xMax * 1.1; |
| CONFIG.xDomain = [minVal, maxVal]; |
| } else { |
| const xPadding = (xMax - xMin) * 0.02; |
| CONFIG.xDomain = [Math.max(0, xMin - xPadding), xMax + xPadding]; |
| } |
| } |
| |
| if (!CONFIG.yDomain) { |
| const yValues = data.map(d => d.loss); |
| const yMin = d3.min(yValues); |
| const yMax = d3.max(yValues); |
| |
| if (CONFIG.yScaleType === 'log') { |
| |
| const minVal = Math.max(yMin / 1.1, 0.1); |
| const maxVal = yMax * 1.1; |
| CONFIG.yDomain = [minVal, maxVal]; |
| } else { |
| const yPadding = (yMax - yMin) * 0.05; |
| CONFIG.yDomain = [yMin - yPadding, yMax + yPadding]; |
| } |
| } |
| |
| |
| xScale.domain(CONFIG.xDomain); |
| yScale.domain(CONFIG.yDomain); |
| |
| |
| const xValues = data.map(d => d.tokens); |
| const yValues = data.map(d => d.loss); |
| |
| |
| const shouldFormatAsFileSize = CONFIG.xFormatAsFileSize || |
| CONFIG.xColumn.toLowerCase().includes('size') || |
| CONFIG.xColumn.toLowerCase().includes('message'); |
| |
| |
| CONFIG.xFormatAsFileSize = shouldFormatAsFileSize; |
| |
| formatX = createSmartFormatter(xValues, shouldFormatAsFileSize); |
| formatY = createSmartFormatter(yValues); |
| |
| const colors = getRunColors(runList.length); |
| runList.forEach((run, i) => { |
| runColorMap[run] = colors[i % colors.length]; |
| }); |
| |
| |
| if (runList.length > 1) { |
| const legendItemsHost = header.querySelector('.legend-bottom .items'); |
| if (legendItemsHost) { |
| legendItemsHost.innerHTML = runList.map(run => { |
| const color = runColorMap[run]; |
| return `<span class="item" data-run="${run}"><span class="swatch" style="background:${color}"></span><span>${run}</span></span>`; |
| }).join(''); |
| |
| legendItemsHost.querySelectorAll('.item').forEach(el => { |
| el.addEventListener('mouseenter', () => { |
| const run = el.getAttribute('data-run'); |
| container.classList.add('hovering'); |
| gPlot.selectAll('path.main-line').classed('ghost', function () { return d3.select(this).datum().run !== run; }); |
| gPlot.selectAll('path.ghost-line').classed('ghost', function () { return d3.select(this).datum().run !== run; }); |
| legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.toggle('ghost', it.getAttribute('data-run') !== run)); |
| }); |
| el.addEventListener('mouseleave', () => { |
| container.classList.remove('hovering'); |
| gPlot.selectAll('path.main-line').classed('ghost', false); |
| gPlot.selectAll('path.ghost-line').classed('ghost', false); |
| legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.remove('ghost')); |
| }); |
| }); |
| } |
| } else { |
| |
| header.style.display = 'none'; |
| } |
| |
| |
| resetBtn.addEventListener('click', () => { |
| overlay.transition().duration(750).call(zoom.transform, d3.zoomIdentity); |
| }); |
| |
| render(); |
| const ro = window.ResizeObserver ? new ResizeObserver(() => render()) : null; |
| if (ro) ro.observe(container); |
| } 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'; |
| container.appendChild(pre); |
| } |
| } |
| |
| load(); |
| }; |
| |
| if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); } |
| })(); |
| </script> |