Spaces:
Running
Running
| <!-- | |
| Sweep 1D Metrics - Six Metrics Grid | |
| A grid of 6 line charts showing metrics as a function of steering coefficient alpha. | |
| Configuration via data-config attribute: | |
| { | |
| "dataUrl": "./assets/data/sweep_1d_metrics.csv", | |
| "xColumn": "alpha", | |
| "metrics": [ | |
| { "key": "concept_inclusion", "label": "LLM concept score", "yAxisLabel": "Score" }, | |
| { "key": "eiffel", "label": "Explicit concept inclusion", "yAxisLabel": "Fraction" }, | |
| { "key": "instruction_following", "label": "LLM instruction score", "yAxisLabel": "Score" }, | |
| { "key": "surprise", "label": "Surprise in reference model", "yAxisLabel": "Value" }, | |
| { "key": "fluency", "label": "LLM fluency score", "yAxisLabel": "Score" }, | |
| { "key": "repetition", "label": "3-gram repetition", "yAxisLabel": "Fraction" } | |
| ] | |
| } | |
| CSV format (with mean/std): | |
| alpha, concept_inclusion_mean, concept_inclusion_std, instruction_following_mean, instruction_following_std, ... | |
| CSV format (simple, fallback): | |
| alpha, concept_inclusion, instruction_following, fluency, surprise, repetition, eiffel | |
| Example usage in MDX: | |
| <HtmlEmbed | |
| src="embeds/d3-sweep-1d-metrics.html" | |
| config={{ | |
| dataUrl: "./assets/data/sweep_1d_metrics.csv" | |
| }} | |
| /> | |
| --> | |
| <div class="d3-sweep-1d"></div> | |
| <style> | |
| .d3-sweep-1d { | |
| position: relative; | |
| container-type: inline-size; | |
| } | |
| /* Grid - 2 columns x 3 rows */ | |
| .d3-sweep-1d__grid { | |
| display: grid; | |
| grid-template-columns: repeat(2, minmax(0, 1fr)); | |
| gap: 16px; | |
| } | |
| /* Container queries - basées sur la largeur du container parent */ | |
| @container (max-width: 600px) { | |
| .d3-sweep-1d__grid { | |
| grid-template-columns: 1fr; | |
| } | |
| } | |
| .chart-cell { | |
| display: flex; | |
| flex-direction: column; | |
| position: relative; | |
| padding: 12px; | |
| box-shadow: inset 0 0 0 1px var(--border-color); | |
| border-radius: 8px; | |
| background: var(--page-bg); | |
| } | |
| .chart-cell__title { | |
| font-size: 13px; | |
| font-weight: 700; | |
| color: var(--text-color); | |
| margin-bottom: 8px; | |
| padding-bottom: 8px; | |
| } | |
| .chart-cell__body { | |
| position: relative; | |
| width: 100%; | |
| overflow: hidden; | |
| } | |
| .chart-cell__body svg { | |
| max-width: 100%; | |
| height: auto; | |
| display: block; | |
| } | |
| .d3-sweep-1d__legend { | |
| display: flex; | |
| gap: 16px; | |
| margin-top: 16px; | |
| font-size: 11px; | |
| color: var(--text-color); | |
| align-items: center; | |
| justify-content: center; | |
| } | |
| .d3-sweep-1d__legend-item { | |
| display: flex; | |
| align-items: center; | |
| gap: 6px; | |
| } | |
| .d3-sweep-1d__legend-line { | |
| width: 20px; | |
| height: 2px; | |
| border-radius: 1px; | |
| } | |
| .d3-sweep-1d__legend-band { | |
| width: 20px; | |
| height: 12px; | |
| border-radius: 2px; | |
| } | |
| /* Reset button */ | |
| .chart-cell .reset-button { | |
| position: absolute; | |
| top: 12px; | |
| right: 12px; | |
| z-index: 10; | |
| display: none; | |
| opacity: 0; | |
| transition: opacity 0.2s ease; | |
| font-size: 11px; | |
| padding: 3px 6px; | |
| border-radius: 4px; | |
| background: var(--surface-bg); | |
| color: var(--text-color); | |
| border: 1px solid var(--border-color); | |
| cursor: pointer; | |
| } | |
| /* Axes */ | |
| .d3-sweep-1d .axes path { | |
| display: none; | |
| } | |
| .d3-sweep-1d .axes line { | |
| stroke: var(--axis-color); | |
| } | |
| .d3-sweep-1d .axes text { | |
| fill: var(--tick-color); | |
| font-size: 10px; | |
| } | |
| .d3-sweep-1d .axis-label { | |
| fill: var(--text-color); | |
| font-size: 10px; | |
| font-weight: 300; | |
| opacity: 0.7; | |
| stroke: var(--page-bg, white); | |
| stroke-width: 3px; | |
| paint-order: stroke fill; | |
| } | |
| .d3-sweep-1d .grid line { | |
| stroke: var(--grid-color); | |
| } | |
| /* Lines */ | |
| .d3-sweep-1d path.main-line { | |
| fill: none; | |
| stroke-width: 2; | |
| transition: opacity 0.2s ease; | |
| } | |
| /* Uncertainty band */ | |
| .d3-sweep-1d path.uncertainty-band { | |
| fill: var(--primary-color, #E889AB); | |
| fill-opacity: 0.2; | |
| stroke: none; | |
| } | |
| /* Tooltip */ | |
| .d3-sweep-1d .d3-tooltip { | |
| z-index: 20; | |
| backdrop-filter: saturate(1.12) blur(8px); | |
| } | |
| .d3-sweep-1d .d3-tooltip__inner { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px; | |
| min-width: 200px; | |
| } | |
| .d3-sweep-1d .d3-tooltip__inner>div:first-child { | |
| font-weight: 800; | |
| letter-spacing: 0.1px; | |
| margin-bottom: 0; | |
| } | |
| .d3-sweep-1d .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; | |
| } | |
| </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-sweep-1d'))) { | |
| const cs = Array.from(document.querySelectorAll('.d3-sweep-1d')).filter(el => !(el.dataset && el.dataset.mounted === 'true')); | |
| container = cs[cs.length - 1] || null; | |
| } | |
| if (!container) return; | |
| if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; } | |
| const d3 = window.d3; | |
| // Read config from HtmlEmbed props | |
| 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) { | |
| // Failed to parse data-config | |
| } | |
| return providedConfig || {}; | |
| } | |
| const embedConfig = readEmbedConfig(); | |
| // Also check for data-datafiles attribute (used by HtmlEmbed component) | |
| let providedData = null; | |
| try { | |
| let mountEl = container; | |
| while (mountEl && !mountEl.getAttribute?.('data-datafiles')) { | |
| mountEl = mountEl.parentElement; | |
| } | |
| const attr = mountEl && mountEl.getAttribute ? mountEl.getAttribute('data-datafiles') : null; | |
| if (attr && attr.trim()) { | |
| providedData = attr.trim(); | |
| } | |
| } catch (e) {} | |
| // Default metrics configuration - order matches the image exactly | |
| const DEFAULT_METRICS = [ | |
| { key: 'concept_inclusion', label: 'LLM concept score', yAxisLabel: 'Score' }, | |
| { key: 'eiffel', label: 'Explicit concept inclusion', yAxisLabel: 'Fraction' }, | |
| { key: 'instruction_following', label: 'LLM instruction score', yAxisLabel: 'Score' }, | |
| { key: 'surprise', label: 'Surprise in reference model', yAxisLabel: 'Value' }, | |
| { key: 'fluency', label: 'LLM fluency score', yAxisLabel: 'Score' }, | |
| { key: 'repetition', label: '3-gram repetition', yAxisLabel: 'Fraction' } | |
| ]; | |
| // Determine data URL - try config first, then data attribute, then default | |
| const dataUrlFromConfig = embedConfig.dataUrl; | |
| const dataUrlFromAttr = providedData; | |
| const DEFAULT_CSV = '/data/stats_L15F21576.csv'; | |
| const ensureDataPrefix = (p) => (typeof p === 'string' && p && !p.includes('/')) ? `/data/${p}` : p; | |
| const CSV_PATHS = dataUrlFromConfig | |
| ? [dataUrlFromConfig] | |
| : (dataUrlFromAttr | |
| ? [ensureDataPrefix(dataUrlFromAttr)] | |
| : [ | |
| DEFAULT_CSV, | |
| './assets/data/stats_L15F21576.csv', | |
| '../assets/data/stats_L15F21576.csv', | |
| '../../assets/data/stats_L15F21576.csv', | |
| './assets/data/sweep_1d_metrics.csv', | |
| '../assets/data/sweep_1d_metrics.csv' | |
| ]); | |
| // Get categorical colors for lines | |
| const getLineColor = () => { | |
| try { | |
| if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') { | |
| const colors = window.ColorPalettes.getColors('categorical', 1); | |
| if (colors && colors.length > 0) return colors[0]; | |
| } | |
| } catch (_) {} | |
| return getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB'; | |
| }; | |
| // Configuration | |
| const CONFIG = { | |
| csvPaths: CSV_PATHS, | |
| xColumn: embedConfig.xColumn || 'alpha', | |
| metrics: embedConfig.metrics || DEFAULT_METRICS, | |
| chartHeight: 240, | |
| margin: { top: 20, right: 20, bottom: 40, left: 50 }, | |
| zoomExtent: [1.0, 8], | |
| xAxisLabel: embedConfig.xAxisLabel || 'Steering coefficient α', | |
| lineColor: embedConfig.lineColor || getLineColor() | |
| }; | |
| // Create grid | |
| const grid = document.createElement('div'); | |
| grid.className = 'd3-sweep-1d__grid'; | |
| container.appendChild(grid); | |
| // Create legend container | |
| const legend = document.createElement('div'); | |
| legend.className = 'd3-sweep-1d__legend'; | |
| container.appendChild(legend); | |
| // Create chart cells | |
| CONFIG.metrics.forEach((metricConfig, idx) => { | |
| const cell = document.createElement('div'); | |
| cell.className = 'chart-cell'; | |
| cell.style.zIndex = CONFIG.metrics.length - idx; | |
| cell.innerHTML = ` | |
| <div class="chart-cell__title">${metricConfig.label}</div> | |
| <button class="reset-button">Reset</button> | |
| <div class="chart-cell__body"></div> | |
| `; | |
| grid.appendChild(cell); | |
| }); | |
| // Data | |
| let allData = []; | |
| // Function to determine smart format based on data values | |
| function createSmartFormatter(values) { | |
| if (!values || values.length === 0) return (v) => v; | |
| const min = d3.min(values); | |
| const max = d3.max(values); | |
| const range = max - min; | |
| // Check if all values are effectively integers (within 0.001 tolerance) | |
| const allIntegers = values.every(v => Math.abs(v - Math.round(v)) < 0.001); | |
| // Large numbers (billions): format as "X.XXB" | |
| 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'; | |
| }; | |
| } | |
| // Millions: format as "X.XXM" or "XM" | |
| 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'; | |
| }; | |
| } | |
| // Thousands: format as "X.Xk" or "Xk" | |
| 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'; | |
| }; | |
| } | |
| // Regular numbers | |
| if (allIntegers) { | |
| return (v) => d3.format('d')(Math.round(v)); | |
| } | |
| // Small decimals: use appropriate precision | |
| 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); | |
| } | |
| } | |
| // Init each chart | |
| function initChart(cellElement, metricConfig) { | |
| const bodyEl = cellElement.querySelector('.chart-cell__body'); | |
| const resetBtn = cellElement.querySelector('.reset-button'); | |
| const metricKey = metricConfig.key; | |
| let hasMoved = false; | |
| // Tooltip | |
| let tip = cellElement.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', zIndex: '20' | |
| }); | |
| tipInner = document.createElement('div'); | |
| tipInner.className = 'd3-tooltip__inner'; | |
| tip.appendChild(tipInner); | |
| cellElement.appendChild(tip); | |
| } else { | |
| tipInner = tip.querySelector('.d3-tooltip__inner') || tip; | |
| } | |
| // Create SVG | |
| const svg = d3.select(bodyEl).append('svg').attr('width', '100%').style('display', 'block'); | |
| // Clip path | |
| const clipId = 'clip-' + Math.random().toString(36).slice(2); | |
| const clipPath = svg.append('defs').append('clipPath').attr('id', clipId); | |
| const clipRect = clipPath.append('rect'); | |
| // Groups | |
| const g = svg.append('g'); | |
| const gGrid = g.append('g').attr('class', 'grid'); | |
| const gAxes = g.append('g').attr('class', 'axes'); | |
| 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'); | |
| tip.style.opacity = '0'; | |
| if (hoverLine) hoverLine.style('display', 'none'); | |
| }) | |
| .on('mouseup', function () { d3.select(this).style('cursor', 'grab'); }); | |
| // Scales | |
| const xScale = d3.scaleLinear(); | |
| const yScale = d3.scaleLinear(); | |
| // Hover state | |
| let hoverLine = null; | |
| let dataPoints = []; | |
| let hideTipTimer = null; | |
| let hasMeanStd = false; | |
| // Formatters (will be set in render()) | |
| let formatX = (v) => v; | |
| let formatY = (v) => v; | |
| // Zoom | |
| 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]; | |
| // Update grid | |
| const gridTicks = newYScale.ticks(5); | |
| 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)'); | |
| // Update uncertainty band (if mean/std available) | |
| if (hasMeanStd && dataPoints[0] && dataPoints[0].yUpper != null) { | |
| const area = d3.area() | |
| .x(d => newXScale(d.x)) | |
| .y0(d => newYScale(d.yLower)) | |
| .y1(d => newYScale(d.yUpper)) | |
| .curve(d3.curveLinear); | |
| gPlot.selectAll('path.uncertainty-band') | |
| .attr('d', area(dataPoints)); | |
| } | |
| // Update line | |
| const line = d3.line() | |
| .x(d => newXScale(d.x)) | |
| .y(d => newYScale(d.y)) | |
| .curve(d3.curveLinear); | |
| gPlot.selectAll('path.main-line') | |
| .attr('d', line(dataPoints)); | |
| // Update markers | |
| gPlot.selectAll('circle.data-marker') | |
| .data(dataPoints) | |
| .join('circle') | |
| .attr('class', 'data-marker') | |
| .attr('cx', d => newXScale(d.x)) | |
| .attr('cy', d => newYScale(d.y)) | |
| .attr('r', 3) | |
| .attr('fill', CONFIG.lineColor) | |
| .attr('stroke', 'var(--page-bg)') | |
| .attr('stroke-width', 1.5); | |
| // Update axes | |
| gAxes.select('.x-axis').call(d3.axisBottom(newXScale).ticks(5).tickSizeOuter(0).tickFormat(formatX)); | |
| gAxes.select('.y-axis').call(d3.axisLeft(newYScale).ticks(5).tickSizeOuter(0).tickFormat(formatY)); | |
| } | |
| 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() { | |
| const rect = bodyEl.getBoundingClientRect(); | |
| const width = Math.max(1, Math.round(rect.width || 400)); | |
| const height = CONFIG.chartHeight; | |
| svg.attr('width', width).attr('height', height); | |
| const margin = CONFIG.margin; | |
| const innerWidth = width - margin.left - margin.right; | |
| const innerHeight = height - margin.top - margin.bottom; | |
| g.attr('transform', `translate(${margin.left},${margin.top})`); | |
| // Filter and prepare data for this metric | |
| // Support both mean/std columns and direct value columns | |
| const meanKey = `${metricKey}_mean`; | |
| const stdKey = `${metricKey}_std`; | |
| hasMeanStd = allData.some(d => d[meanKey] != null && d[stdKey] != null); | |
| if (hasMeanStd) { | |
| // Data has mean and std columns | |
| dataPoints = allData | |
| .filter(d => d[CONFIG.xColumn] != null && !isNaN(d[CONFIG.xColumn]) && | |
| d[meanKey] != null && !isNaN(d[meanKey]) && | |
| d[stdKey] != null && !isNaN(d[stdKey])) | |
| .map(d => ({ | |
| x: +d[CONFIG.xColumn], | |
| y: +d[meanKey], | |
| yUpper: +d[meanKey] + +d[stdKey], | |
| yLower: +d[meanKey] - +d[stdKey] | |
| })) | |
| .sort((a, b) => a.x - b.x); | |
| } else { | |
| // Data has direct value columns (fallback) | |
| dataPoints = allData | |
| .filter(d => d[CONFIG.xColumn] != null && !isNaN(d[CONFIG.xColumn]) && d[metricKey] != null && !isNaN(d[metricKey])) | |
| .map(d => ({ x: +d[CONFIG.xColumn], y: +d[metricKey] })) | |
| .sort((a, b) => a.x - b.x); | |
| } | |
| if (!dataPoints.length) { | |
| return; | |
| } | |
| // Auto-compute domains from data | |
| const xExtent = d3.extent(dataPoints, d => d.x); | |
| const yExtent = hasMeanStd | |
| ? d3.extent([...dataPoints.map(d => d.yUpper), ...dataPoints.map(d => d.yLower)]) | |
| : d3.extent(dataPoints, d => d.y); | |
| // Ensure Y axis never goes below 0 | |
| const yDomain = [Math.max(0, yExtent[0]), yExtent[1]]; | |
| xScale.domain(xExtent).range([0, innerWidth]); | |
| yScale.domain(yDomain).range([innerHeight, 0]); | |
| // Create smart formatters based on actual data | |
| const xValues = dataPoints.map(d => d.x); | |
| const yValues = dataPoints.map(d => d.y); | |
| formatX = createSmartFormatter(xValues); | |
| formatY = createSmartFormatter(yValues); | |
| // Update clip | |
| clipRect.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight); | |
| // Update overlay | |
| overlay.attr('x', 0).attr('y', 0).attr('width', innerWidth).attr('height', innerHeight); | |
| // Update zoom extent | |
| zoom.extent([[0, 0], [innerWidth, innerHeight]]) | |
| .translateExtent([[0, 0], [innerWidth, innerHeight]]); | |
| // Grid | |
| gGrid.selectAll('line').data(yScale.ticks(5)).join('line') | |
| .attr('x1', 0).attr('x2', innerWidth) | |
| .attr('y1', d => yScale(d)).attr('y2', d => yScale(d)) | |
| .attr('stroke', 'var(--grid-color)'); | |
| // Axes | |
| gAxes.selectAll('*').remove(); | |
| gAxes.append('g').attr('class', 'x-axis').attr('transform', `translate(0,${innerHeight})`) | |
| .call(d3.axisBottom(xScale).ticks(5).tickSizeOuter(0).tickFormat(formatX)); | |
| gAxes.append('g').attr('class', 'y-axis') | |
| .call(d3.axisLeft(yScale).ticks(5).tickSizeOuter(0).tickFormat(formatY)); | |
| gAxes.selectAll('.domain, .tick line').attr('stroke', 'var(--axis-color)'); | |
| gAxes.selectAll('text').attr('fill', 'var(--tick-color)'); | |
| // Axis labels | |
| gAxes.append('text') | |
| .attr('class', 'axis-label') | |
| .attr('x', innerWidth / 2) | |
| .attr('y', innerHeight + 32) | |
| .attr('text-anchor', 'middle') | |
| .text(CONFIG.xAxisLabel); | |
| gAxes.append('text') | |
| .attr('class', 'axis-label') | |
| .attr('transform', 'rotate(-90)') | |
| .attr('x', -innerHeight / 2) | |
| .attr('y', -38) | |
| .attr('text-anchor', 'middle') | |
| .text(metricConfig.yAxisLabel || 'Value'); | |
| // Uncertainty band (if mean/std available) | |
| if (hasMeanStd) { | |
| const area = d3.area() | |
| .x(d => xScale(d.x)) | |
| .y0(d => yScale(d.yLower)) | |
| .y1(d => yScale(d.yUpper)) | |
| .curve(d3.curveLinear); | |
| gPlot.selectAll('path.uncertainty-band').data([dataPoints]).join('path') | |
| .attr('class', 'uncertainty-band') | |
| .attr('d', area); | |
| } | |
| // Main line | |
| const mainLine = d3.line() | |
| .x(d => xScale(d.x)) | |
| .y(d => yScale(d.y)) | |
| .curve(d3.curveLinear); | |
| gPlot.selectAll('path.main-line').data([dataPoints]).join('path') | |
| .attr('class', 'main-line') | |
| .attr('fill', 'none') | |
| .attr('stroke', CONFIG.lineColor) | |
| .attr('stroke-width', 2) | |
| .attr('opacity', 0.85) | |
| .attr('d', mainLine); | |
| // Markers | |
| gPlot.selectAll('circle.data-marker') | |
| .data(dataPoints) | |
| .join('circle') | |
| .attr('class', 'data-marker') | |
| .attr('cx', d => xScale(d.x)) | |
| .attr('cy', d => yScale(d.y)) | |
| .attr('r', 3) | |
| .attr('fill', CONFIG.lineColor) | |
| .attr('stroke', 'var(--page-bg)') | |
| .attr('stroke-width', 1.5); | |
| // Hover | |
| setupHover(innerWidth, innerHeight); | |
| } | |
| function setupHover(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'); | |
| overlay.on('mousemove', function (ev) { | |
| if (ev.buttons === 0) onHoverMove(ev); | |
| }).on('mouseleave', onHoverLeave); | |
| } | |
| function onHoverMove(ev) { | |
| if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer = null; } | |
| const [mx, my] = d3.pointer(ev, overlay.node()); | |
| const targetX = xScale.invert(mx); | |
| // Find nearest data point | |
| let nearest = dataPoints[0]; | |
| let minDist = Math.abs(dataPoints[0].x - targetX); | |
| for (let i = 1; i < dataPoints.length; i++) { | |
| const dist = Math.abs(dataPoints[i].x - targetX); | |
| if (dist < minDist) { | |
| minDist = dist; | |
| nearest = dataPoints[i]; | |
| } | |
| } | |
| const xpx = xScale(nearest.x); | |
| hoverLine.attr('x1', xpx).attr('x2', xpx).style('display', null); | |
| let html = `<div><strong>${metricConfig.label}</strong></div>`; | |
| html += `<div>α = ${formatX(nearest.x)}</div>`; | |
| if (hasMeanStd && nearest.yUpper != null && nearest.yLower != null) { | |
| html += `<div>Mean: ${formatY(nearest.y)}</div>`; | |
| html += `<div>± 1 std dev: [${formatY(nearest.yLower)}, ${formatY(nearest.yUpper)}]</div>`; | |
| } else { | |
| html += `<div>${metricConfig.yAxisLabel || 'Value'}: ${formatY(nearest.y)}</div>`; | |
| } | |
| tipInner.innerHTML = html; | |
| const offsetX = 12, offsetY = 12; | |
| tip.style.opacity = '1'; | |
| tip.style.transform = `translate(${Math.round(mx + offsetX + CONFIG.margin.left)}px, ${Math.round(my + offsetY + CONFIG.margin.top)}px)`; | |
| } | |
| function onHoverLeave() { | |
| hideTipTimer = setTimeout(() => { | |
| tip.style.opacity = '0'; | |
| tip.style.transform = 'translate(-9999px, -9999px)'; | |
| if (hoverLine) hoverLine.style('display', 'none'); | |
| }, 100); | |
| } | |
| // Reset button | |
| resetBtn.addEventListener('click', () => { | |
| overlay.transition().duration(750).call(zoom.transform, d3.zoomIdentity); | |
| }); | |
| return { render }; | |
| } | |
| // Transform long format CSV to wide format | |
| function transformLongToWide(longData) { | |
| // Mapping from CSV quantity names to embed metric keys | |
| const quantityMap = { | |
| 'llm_score_concept': 'concept_inclusion', | |
| 'eiffel': 'eiffel', | |
| 'llm_score_instruction': 'instruction_following', | |
| 'surprise': 'surprise', | |
| 'llm_score_fluency': 'fluency', | |
| 'rep3': 'repetition' | |
| }; | |
| // Group by steering_intensity | |
| const grouped = {}; | |
| longData.forEach(row => { | |
| const intensity = parseFloat(row.steering_intensity); | |
| if (isNaN(intensity)) return; | |
| if (!grouped[intensity]) { | |
| grouped[intensity] = { alpha: intensity, steering_intensity: intensity }; | |
| } | |
| const quantity = row.quantity; | |
| const statType = row.stat_type; | |
| const value = parseFloat(row.value); | |
| if (isNaN(value)) return; | |
| // Map quantity name to metric key | |
| const metricKey = quantityMap[quantity] || quantity; | |
| // Store mean and std | |
| if (statType === 'mean') { | |
| grouped[intensity][`${metricKey}_mean`] = value; | |
| } else if (statType === 'std') { | |
| grouped[intensity][`${metricKey}_std`] = value; | |
| } | |
| }); | |
| return Object.values(grouped); | |
| } | |
| // Load data | |
| async function load() { | |
| try { | |
| 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 at any of the paths: ' + paths.join(', ')); | |
| }; | |
| const csvText = await fetchFirstAvailable(CONFIG.csvPaths); | |
| const rawData = d3.csvParse(csvText); | |
| // Check if data is in long format (has quantity, stat_type, value columns) | |
| const isLongFormat = rawData.length > 0 && | |
| rawData[0].hasOwnProperty('quantity') && | |
| rawData[0].hasOwnProperty('stat_type') && | |
| rawData[0].hasOwnProperty('value'); | |
| if (isLongFormat) { | |
| allData = transformLongToWide(rawData); | |
| // Update xColumn to use steering_intensity if available | |
| if (allData.length > 0 && allData[0].steering_intensity != null) { | |
| CONFIG.xColumn = 'steering_intensity'; | |
| } | |
| } else { | |
| allData = rawData; | |
| } | |
| // Init all charts | |
| const cells = Array.from(grid.querySelectorAll('.chart-cell')); | |
| const chartInstances = cells.map((cell, idx) => initChart(cell, CONFIG.metrics[idx])); | |
| // Render all | |
| chartInstances.forEach(chart => chart.render()); | |
| // Update legend (once for the whole group) | |
| const hasMeanStd = allData.some(d => { | |
| return CONFIG.metrics.some(m => { | |
| const meanKey = `${m.key}_mean`; | |
| const stdKey = `${m.key}_std`; | |
| return d[meanKey] != null && d[stdKey] != null; | |
| }); | |
| }); | |
| if (hasMeanStd) { | |
| legend.innerHTML = ` | |
| <div class="d3-sweep-1d__legend-item"> | |
| <div class="d3-sweep-1d__legend-line" style="background: ${CONFIG.lineColor};"></div> | |
| <span>Mean</span> | |
| </div> | |
| <div class="d3-sweep-1d__legend-item"> | |
| <div class="d3-sweep-1d__legend-band" style="background: ${CONFIG.lineColor}; opacity: 0.2;"></div> | |
| <span>± 1 std dev</span> | |
| </div> | |
| `; | |
| } else { | |
| legend.innerHTML = ` | |
| <div class="d3-sweep-1d__legend-item"> | |
| <div class="d3-sweep-1d__legend-line" style="background: ${CONFIG.lineColor};"></div> | |
| <span>Mean</span> | |
| </div> | |
| `; | |
| } | |
| // Responsive - observe container for resize | |
| let resizeTimer; | |
| const handleResize = () => { | |
| clearTimeout(resizeTimer); | |
| resizeTimer = setTimeout(() => { | |
| chartInstances.forEach(chart => chart.render()); | |
| }, 100); | |
| }; | |
| const ro = window.ResizeObserver ? new ResizeObserver(handleResize) : null; | |
| if (ro) { | |
| ro.observe(container); | |
| } | |
| // Also observe window resize as fallback | |
| window.addEventListener('resize', handleResize); | |
| // Force a re-render after a short delay to ensure proper sizing | |
| setTimeout(() => { | |
| chartInstances.forEach(chart => chart.render()); | |
| }, 100); | |
| } catch (e) { | |
| const pre = document.createElement('pre'); | |
| pre.textContent = 'Error loading data: ' + (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> | |