eiffel-tower-llama / app /src /content /embeds /d3-sweep-1d-metrics.html
dlouapre's picture
dlouapre HF Staff
Improving charts
e0722b8
<!--
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>