CoolFace
Apppublic

lerobot/robot-learning-tutorial

sourceHugging Faceupdated 1y agoView on Hugging Face
508likes
d3-line-quad.html646 linesDownload Raw Back to embeds
1<div class="line-quad">2  3  <div class="line-quad__grid">4    <div class="quad-cell" data-title="Formatting Filter" data-csv="/data/formatting_filters.csv"></div>5    <div class="quad-cell" data-title="Relevance Filter" data-csv="/data/relevance_filters.csv"></div>6    <div class="quad-cell" data-title="Visual Dependency Filter" data-csv="/data/visual_dependency_filters.csv"></div>7    <div class="quad-cell" data-title="Image Correspondence Filter" data-csv="/data/image_correspondence_filters.csv"></div>8  </div>9  <noscript>JavaScript is required to render these charts.</noscript>10 11</div>12<style>13  .line-quad { position: relative; }14  /* Axis/tick/grid use global variables from _variables.css */15  /* Apply axis/tick/grid purely via CSS */16  .line-quad .axes path,17  .line-quad .axes line { stroke: var(--axis-color); }18  .line-quad .axes text { fill: var(--tick-color); }19  .line-quad .grid line { stroke: var(--grid-color); }20  .line-quad__grid { display:grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 12px; }21  @media (max-width: 980px) { .line-quad__grid { grid-template-columns: 1fr; } }22 23  24 25  .quad-cell { border:1px solid var(--border-color); border-radius:10px; background: var(--surface-bg); display:flex; flex-direction:column; position: relative; }26  /* Stacking order to ensure hover/tooltip overlays are not hidden by neighbors */27  .line-quad__grid .quad-cell:nth-child(1) { z-index: 4; } /* top-left */28  .line-quad__grid .quad-cell:nth-child(3) { z-index: 3; } /* bottom-left */29  .line-quad__grid .quad-cell:nth-child(2) { z-index: 2; } /* top-right */30  .line-quad__grid .quad-cell:nth-child(4) { z-index: 1; } /* bottom-right */31  .quad-cell .cell-header { padding:8px 10px; border-bottom:1px solid var(--border-color); display:flex; align-items:center; justify-content:space-between; gap:8px; }32  .quad-cell .cell-title { font-size: 13px; font-weight: 700; color: var(--text-color); }33  .quad-cell .cell-controls { display:flex; align-items:center; gap:12px; }34  .quad-cell .cell-controls label { font-size:12px; color: var(--muted-color); display:flex; align-items:center; gap:6px; white-space:nowrap; }35  .quad-cell select {36    font-size: 12px; padding: 6px 28px 6px 10px; border: 1px solid var(--border-color); border-radius: 8px;37    background-color: var(--surface-bg); color: var(--text-color);38    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");39    background-repeat: no-repeat; background-position: right 8px center; background-size: 12px;40    -webkit-appearance: none; appearance: none; cursor: pointer; transition: border-color .15s ease, box-shadow .15s ease;41  }42  [data-theme="dark"] .quad-cell select {43    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");44  }45  .quad-cell select:hover { border-color: var(--primary-color); }46  .quad-cell select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }47  .quad-cell .cell-body { position: relative; }48  .quad-cell .cell-body { width:100%; overflow:hidden; }49  .quad-cell .cell-body svg { max-width:100%; height:auto; }50  51  .line-quad.hovering .lines path.ghost { opacity: .25; }52  .line-quad.hovering .points circle.ghost { opacity: .25; }53  .line-quad.hovering .areas path.ghost { opacity: .08; }54  .line-quad.hovering .legend-bottom .item.ghost { opacity: .35; }55  /* Tooltip refined styling */56  .line-quad .d3-tooltip {57    z-index: 20;58    backdrop-filter: saturate(1.12) blur(8px);59  }60  .line-quad .d3-tooltip__inner {61    display: flex;62    flex-direction: column;63    gap: 6px;64    min-width: 220px;65  }66  .line-quad .d3-tooltip__inner > div:first-child {67    font-weight: 800;68    letter-spacing: 0.1px;69    margin-bottom: 0;70  }71  .line-quad .d3-tooltip__inner > div:nth-child(2) {72    font-size: 11px;73    color: var(--muted-color);74    display: block;75    margin-top: -4px;76    margin-bottom: 2px;77    letter-spacing: 0.1px;78  }79  .line-quad .d3-tooltip__inner > div:nth-child(n+3) {80    padding-top: 6px;81    border-top: 1px solid var(--border-color);82  }83  .line-quad .d3-tooltip__inner svg {84    display: inline-block;85    vertical-align: middle;86    margin-right: 2px;87  }88  .line-quad .d3-tooltip__inner strong {89    margin-right: 6px;90  }91  .line-quad .d3-tooltip__color-dot {92    display: inline-block;93    width: 12px;94    height: 12px;95    border-radius: 3px;96    border: 1px solid var(--border-color);97  }98  /* Header layout (like d3-line-simple) */99  .line-quad__header {100    display: flex;101    align-items: flex-start;102    justify-content: flex-start;103    gap: 12px;104    margin: 8px 0 0 0;105    flex-wrap: wrap;106  }107  .line-quad__header .legend-bottom {108    display: flex;109    flex-direction: column;110    align-items: flex-start;111    gap: 6px;112    font-size: 12px;113    color: var(--text-color);114  }115  .line-quad__header .legend-bottom .legend-title {116    font-size: 12px;117    font-weight: 700;118    color: var(--text-color);119  }120  .line-quad__header .legend-bottom .items {121    display: flex;122    flex-wrap: wrap;123    gap: 8px 14px;124  }125  .line-quad__header .legend-bottom .item {126    display: inline-flex;127    align-items: center;128    gap: 6px;129    white-space: nowrap;130  }131  .line-quad__header .legend-bottom .swatch {132    width: 14px;133    height: 14px;134    border-radius: 3px;135    border: 1px solid var(--border-color);136    display: inline-block;137  }138  .line-quad .controls {139    margin-top: 0;140    display: flex;141    gap: 16px;142    align-items: center;143    justify-content: flex-end;144    width: auto;145    flex-wrap: wrap;146  }147  .line-quad .controls .control-group {148    display: flex;149    flex-direction: column;150    align-items: flex-start;151    gap: 6px;152  }153  .line-quad .controls label {154    font-size: 12px;155    color: var(--text-color);156    display: flex;157    align-items: center;158    gap: 6px;159    white-space: nowrap;160    font-weight: 700;161  }162  .line-quad .controls select {163    font-size: 12px;164    padding: 8px 28px 8px 10px;165    border: 1px solid var(--border-color);166    border-radius: 8px;167    background-color: var(--surface-bg);168    color: var(--text-color);169    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%230f1115' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");170    background-repeat: no-repeat;171    background-position: right 8px center;172    background-size: 12px;173    -webkit-appearance: none;174    appearance: none;175    cursor: pointer;176    transition: border-color .15s ease, box-shadow .15s ease;177  }178  [data-theme="dark"] .line-quad .controls select {179    background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24' fill='none' stroke='%23ffffff' stroke-width='1' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpolyline points='6 9 12 15 18 9'/%3E%3C/svg%3E");180  }181  .line-quad .controls select:hover { border-color: var(--primary-color); }182  .line-quad .controls select:focus { border-color: var(--primary-color); box-shadow: 0 0 0 3px rgba(232,137,171,.25); outline: none; }183</style>184<script>185  (() => {186    const THIS_SCRIPT = document.currentScript;187    // Shared run->color mapping to keep legend and series perfectly in sync188    let SHARED_RUN_COLOR = null;189    // Pretty label mapping for metric keys190    const prettyMetricLabel = (key) => {191      if (!key) return '';192      const table = {193        'ai2d_exact_match': 'AI2D Exact Match',194        'average_rank': 'Average Rank',195      };196      if (table[key]) return table[key];197      const cleaned = String(key).replace(/[_-]+/g, ' ').trim();198      return cleaned.split(/\s+/).map(w => {199        if (/^(ai2d|umap|id|auc|f1)$/i.test(w)) return w.toUpperCase();200        return w.charAt(0).toUpperCase() + w.slice(1);201      }).join(' ');202    };203    const ensureD3 = (cb) => {204      if (window.d3 && typeof window.d3.select === 'function') return cb();205      let s = document.getElementById('d3-cdn-script');206      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); }207      const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };208      s.addEventListener('load', onReady, { once: true }); if (window.d3) onReady();209    };210 211    function initRunLine(cell){212      const d3 = window.d3;213      const csvPath = cell.getAttribute('data-csv');214      const titleText = cell.getAttribute('data-title') || '';215 216      // Header217      const header = document.createElement('div'); header.className = 'cell-header';218      const title = document.createElement('div'); title.className = 'cell-title'; title.textContent = titleText; header.appendChild(title);219      // Per-cell controls supprimés (contrôle global utilisé)220      cell.appendChild(header);221 222      // Body & SVG223      const body = document.createElement('div'); body.className = 'cell-body'; cell.appendChild(body);224      const svg = d3.select(body).append('svg').attr('width','100%').style('display','block');225      const gRoot = svg.append('g');226      const gGrid = gRoot.append('g').attr('class','grid');227      const gAxes = gRoot.append('g').attr('class','axes');228      const gAreas = gRoot.append('g').attr('class','areas');229      const gLines = gRoot.append('g').attr('class','lines');230      const gPoints = gRoot.append('g').attr('class','points');231      const gHover = gRoot.append('g').attr('class','hover');232      // Removed per-cell legend; using global footer legend233 234      // Tooltip235      cell.style.position = cell.style.position || 'relative';236      let tip = cell.querySelector('.d3-tooltip'); let tipInner; let hideTipTimer = null;237      if (!tip) {238        tip = document.createElement('div');239        tip.className = 'd3-tooltip';240        Object.assign(tip.style, {241          position:'absolute',242          top:'0',243          left:'0',244          transform:'translate(-9999px,-9999px)',245          pointerEvents:'none',246          padding:'10px 12px',247          borderRadius:'12px',248          fontSize:'12px',249          lineHeight:'1.35',250          border:'1px solid var(--border-color)',251          background:'var(--surface-bg)',252          color:'var(--text-color)',253          boxShadow:'0 8px 32px rgba(0,0,0,.28), 0 2px 8px rgba(0,0,0,.12)',254          opacity:'0',255          transition:'opacity .12s ease',256          backdropFilter:'saturate(1.12) blur(8px)'257        });258        tipInner = document.createElement('div');259        tipInner.className = 'd3-tooltip__inner';260        tipInner.style.textAlign='left';261        tip.appendChild(tipInner);262        cell.appendChild(tip);263      } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }264 265      // State266      let metricList = []; let runList = []; let runOrder = []; const dataByMetric = new Map();267      let width = 800, height = 340; const margin = { top: 16, right: 20, bottom: 46, left: 56 };268      const xScale = d3.scaleLinear(); const yScale = d3.scaleLinear();269      const lineGen = d3.line().x(d => xScale(d.step)).y(d => yScale(d.value));270      let isRankStrictFlag = false; let isRankMetricFlag = false; let rankTickMax = 1;271      let sharedYConfig = null; // { type: 'rank_strict', maxRank } | { type: 'value', min, max }272      let axisLabelY = 'Value';273      274      // Colors and markers (match original embeds)275      const getRunColors = (n) => {276        try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') return window.ColorPalettes.getColors('categorical', n); } catch(_) {}277        const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB';278        return [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...(d3.schemeTableau10||[])].slice(0, n);279      };280      const pool = getRunColors(12);281      // Shapes supprimés: on n'utilise que la couleur282      // Ready signal for async load completion283      let readyResolve = null;284      const ready = new Promise((res)=> { readyResolve = res; });285 286      // Shared formatter for thousands: 5000 -> 5k, 1500 -> 1.5k (trim .0)287      const formatK = (v) => {288        const abs = Math.abs(v);289        if (abs >= 1000) {290          const n = v / 1000;291          const s = d3.format('.1f')(n);292          return (s.endsWith('.0') ? s.slice(0, -2) : s) + 'k';293        }294        return d3.format('d')(v);295      };296 297      function updateScales(){298        const isDark = document.documentElement.getAttribute('data-theme') === 'dark';299        const axisColor = 'var(--axis-color)';300        const tickColor = 'var(--tick-color)';301        const gridColor = 'var(--grid-color)';302 303        const rect = cell.getBoundingClientRect();304        width = Math.max(1, Math.round(rect && rect.width ? rect.width : (cell.clientWidth || 800)));305        height = Math.max(280, Math.round(width / 2.3));306        svg.attr('width', width).attr('height', height).attr('viewBox', `0 0 ${width} ${height}`).attr('preserveAspectRatio','xMidYMid meet');307        const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom;308        gRoot.attr('transform', `translate(${margin.left},${margin.top})`);309        xScale.range([0, innerWidth]); yScale.range([innerHeight, 0]);310 311        // Y ticks312        let yTicks = [];313        if (isRankStrictFlag) { const maxR = Math.max(1, Math.round(rankTickMax)); for (let v=1; v<=maxR; v+=1) yTicks.push(v); }314        else { yTicks = yScale.ticks(6); }315 316        // Grid317        gGrid.selectAll('*').remove();318        gGrid.selectAll('line').data(yTicks).join('line')319          .attr('x1',0).attr('x2',innerWidth).attr('y1',d=>yScale(d)).attr('y2',d=>yScale(d))320          .attr('stroke', gridColor).attr('stroke-width',1).attr('shape-rendering','crispEdges');321 322        // Axes323        gAxes.selectAll('*').remove();324        let xAxis = d3.axisBottom(xScale).tickSizeOuter(0); xAxis = xAxis.ticks(8);325        xAxis = xAxis.tickFormat(formatK);326        const yAxis = d3.axisLeft(yScale).tickValues(yTicks).tickSizeOuter(0).tickFormat(isRankStrictFlag ? d3.format('d') : d3.format('.2f'));327        gAxes.append('g').attr('transform', `translate(0,${innerHeight})`).call(xAxis).call(g=>{ g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size','11px'); });328        gAxes.append('g').call(yAxis).call(g=>{ g.selectAll('path, line').attr('stroke', axisColor); g.selectAll('text').attr('fill', tickColor).style('font-size','11px'); });329 330        // Axis labels331        gAxes.append('text')332          .attr('class', 'x-axis-label')333          .attr('x', innerWidth / 2)334          .attr('y', innerHeight + Math.max(20, Math.min(36, margin.bottom - 10)))335          .attr('fill', 'var(--text-color)')336          .attr('text-anchor', 'middle')337          .style('font-size', '12px')338          .style('font-weight', '700')339          .text('Steps');340 341        gAxes.append('text')342          .attr('class', 'y-axis-label')343          .attr('transform', 'rotate(-90)')344          .attr('x', -innerHeight / 2)345          .attr('y', -Math.max(16, Math.min(28, margin.left - 8) + 10))346          .attr('fill', 'var(--text-color)')347          .attr('text-anchor', 'middle')348          .style('font-size', '12px')349          .style('font-weight', '700')350          .text(axisLabelY);351 352        return { innerWidth, innerHeight, tickColor };353      }354 355      function renderMetric(metricKey){356        const map = dataByMetric.get(metricKey) || {};357        const runs = runOrder;358        let minStep = Infinity, maxStep = -Infinity, maxVal = 0, minVal = Infinity;359        const isRank = /rank/i.test(metricKey); const isAverage = /average/i.test(metricKey); const isRankStrict = isRank && !isAverage;360        runs.forEach(r => { (map[r]||[]).forEach(pt => { const v = isRankStrict ? Math.round(pt.value) : pt.value; minStep=Math.min(minStep,pt.step); maxStep=Math.max(maxStep,pt.step); maxVal=Math.max(maxVal,v); minVal=Math.min(minVal,v); }); });361        if (!isFinite(minStep) || !isFinite(maxStep)) return;362        xScale.domain([minStep, maxStep]);363        if (sharedYConfig && sharedYConfig.type === 'rank_strict') {364          rankTickMax = Math.max(1, Math.round(sharedYConfig.maxRank||1));365          yScale.domain([rankTickMax, 1]);366          isRankStrictFlag = true;367          isRankMetricFlag = true;368        } else if (sharedYConfig && sharedYConfig.type === 'value') {369          yScale.domain([sharedYConfig.min, sharedYConfig.max]);370          isRankStrictFlag = isRankStrict;371          isRankMetricFlag = isRank;372        } else {373          if (isRank) { rankTickMax = Math.max(1, Math.round(maxVal)); yScale.domain([rankTickMax, 1]); }374          else { yScale.domain([minVal, maxVal]).nice(); }375          isRankStrictFlag = isRankStrict;376          isRankMetricFlag = isRank;377        }378 379        axisLabelY = isRankStrict ? 'Rank' : prettyMetricLabel(metricKey);380        const tChange = (window.d3 && d3.transition) ? d3.transition().duration(260).ease(d3.easeCubicOut) : null;381        const { innerWidth, innerHeight } = updateScales();382 383        const colorForRun = (run, idx) => {384          if (SHARED_RUN_COLOR && Object.prototype.hasOwnProperty.call(SHARED_RUN_COLOR, run)) return SHARED_RUN_COLOR[run];385          const j = (typeof idx === 'number' ? idx : runs.indexOf(run));386          return pool[(j >= 0 ? j : 0) % pool.length];387        };388        const series = runs.map((r, i) => ({ run:r, color: colorForRun(r, i), values:(map[r]||[]).slice().sort((a,b)=>a.step-b.step).map(pt => isRankStrict ? { step: pt.step, value: Math.round(pt.value), stderr: pt.stderr } : pt) }));389 390        // zones ± stderr (métriques non rank)391        gAreas.selectAll('*').remove();392        if (!isRank) {393          series.forEach((s) => {394            const withErr = s.values.filter(v => v && v.stderr != null && isFinite(v.stderr) && v.stderr > 0 && isFinite(v.value));395            if (!withErr.length) return;396            const upper = withErr.map(d => [xScale(d.step), yScale(d.value + d.stderr)]);397            const lower = withErr.slice().reverse().map(d => [xScale(d.step), yScale(d.value - d.stderr)]);398            const coords = upper.concat(lower);399            const pathData = d3.line().x(d=>d[0]).y(d=>d[1]).curve(d3.curveLinearClosed)(coords);400            gAreas.append('path')401              .attr('class','area')402              .attr('data-run', s.run)403              .attr('d', pathData)404              .attr('fill', s.color)405              .attr('opacity', 0)406              .attr('stroke', 'none')407              .transition().duration(450).ease(d3.easeCubicOut)408              .attr('opacity', 0.15);409          });410        }411 412        const paths = gLines.selectAll('path.run-line').data(series, d=>d.run);413        paths.enter()414          .append('path')415          .attr('class','run-line')416          .attr('data-run', d=>d.run)417          .attr('fill','none')418          .attr('stroke-width', 1)419          .attr('opacity',0)420          .attr('stroke', d=>d.color)421          .attr('d', d=>lineGen(d.values))422          .transition(tChange || undefined)423          .attr('opacity',0.9);424        paths425          .transition(tChange || undefined)426          .attr('stroke', d=>d.color)427          .attr('opacity',0.9)428          .attr('d', d=>lineGen(d.values));429        paths.exit().remove();430 431        // Draw light point markers at each data sample (subtle)432        const allPoints = series.flatMap(s => s.values.map(v => ({ run:s.run, color:s.color, step:v.step, value:v.value })));433        const ptsSel = gPoints.selectAll('circle.pt').data(allPoints, d=> `${d.run}-${d.step}`);434        ptsSel.enter().append('circle').attr('class','pt')435          .attr('data-run', d=>d.run)436          .attr('r', 1.5)437          .attr('fill', d=>d.color)438          .attr('fill-opacity', 0.6)439          .attr('stroke', 'none')440          .attr('cx', d=>xScale(d.step))441          .attr('cy', d=>yScale(d.value))442          .merge(ptsSel)443          .attr('fill', d=>d.color)444          .transition(tChange || undefined)445          .attr('r', 2)446          .attr('cx', d=>xScale(d.step))447          .attr('cy', d=>yScale(d.value));448        ptsSel.exit().remove();449 450        // No per-cell legend content (handled globally)451 452        // Hover453        gHover.selectAll('*').remove();454        const overlay = gHover.append('rect').attr('fill','transparent').style('cursor','crosshair').attr('x',0).attr('y',0).attr('width', innerWidth).attr('height', innerHeight);455        const hoverLine = gHover.append('line').style('stroke','var(--text-color)').attr('stroke-opacity', 0.25).attr('stroke-width',1).attr('y1',0).attr('y2',innerHeight).style('display','none');456        const stepSet = new Set(); series.forEach(s=>s.values.forEach(v=>stepSet.add(v.step))); const steps = Array.from(stepSet).sort((a,b)=>a-b);457        function onMove(ev){ if (hideTipTimer) { clearTimeout(hideTipTimer); hideTipTimer = null; } const [mx,my]=d3.pointer(ev, overlay.node()); const nearest = steps.reduce((best,s)=> Math.abs(s - xScale.invert(mx)) < Math.abs(best - xScale.invert(mx)) ? s : best, steps[0]); const xpx = xScale(nearest); hoverLine.attr('x1',xpx).attr('x2',xpx).style('display',null);458          let html = `<div><strong>${titleText}</strong></div><div><strong>step</strong> ${formatK(nearest)}</div>`;459          const entries = series.map(s=>{ const map = new Map(s.values.map(v=>[v.step, v])); const pt = map.get(nearest); return { run:s.run, color:s.color, pt }; }).filter(e => e.pt && e.pt.value!=null);460          entries.sort((a,b)=> (a.pt.value - b.pt.value));461          const fmt = (vv)=> (isRankStrictFlag? d3.format('d')(vv) : (+vv).toFixed(4));462          entries.forEach(e => {463            const err = (e.pt.stderr!=null && isFinite(e.pt.stderr) && e.pt.stderr>0) ? ` ± ${fmt(e.pt.stderr)}` : '';464            html += `<div style="display:flex;align-items:center;gap:8px;white-space:nowrap;"><span class=\"d3-tooltip__color-dot\" style=\"background:${e.color}\"></span><strong>${e.run}</strong><span style=\"margin-left:auto;text-align:right;\">${fmt(e.pt.value)}${err}</span></div>`;465          });466          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)`; }467        function onLeave(){ hideTipTimer = setTimeout(()=>{ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; hoverLine.style('display','none'); }, 100); }468        overlay.on('mousemove', onMove).on('mouseleave', onLeave);469      }470 471      async function load(){472        try {473          const file = (csvPath || '').split('/').pop();474          const CANDIDATES = [475            csvPath,476            `/data/${file}`,477            `./assets/data/${file}`,478            `../assets/data/${file}`,479            `../../assets/data/${file}`480          ].filter(Boolean);481          let text = null;482          for (const p of CANDIDATES){483            try { const r = await fetch(p, { cache:'no-cache' }); if (r.ok) { text = await r.text(); break; } } catch(e){}484          }485          if (text == null) throw new Error(`CSV not found: ${file}`);486          const rows = d3.csvParse(text, d => ({ run:(d.run||'').trim(), step:+d.step, metric:(d.metric||'').trim(), value:+d.value, stderr: (d.stderr!=null && d.stderr!=='') ? +d.stderr : null }));487          metricList = Array.from(new Set(rows.map(r=>r.metric))).sort();488          runList = Array.from(new Set(rows.map(r=>r.run))).sort(); runOrder = runList;489          metricList.forEach(m => { const map={}; runList.forEach(r=>map[r]=[]); rows.filter(r=>r.metric===m).forEach(r=>{ if(!isNaN(r.step)&&!isNaN(r.value)) map[r.run].push({ step:r.step, value:r.value, stderr:r.stderr }); }); dataByMetric.set(m, map); });490          const preferred = metricList.find(m => m === 'ai2d_exact_match') || metricList.find(m => /average_rank/i.test(m));491          const def = preferred || metricList[0];492          renderMetric(def);493          const ro = window.ResizeObserver ? new ResizeObserver(()=>renderMetric(def)) : null; if (ro) ro.observe(cell);494          if (typeof readyResolve === 'function') readyResolve();495        } catch (e) {496          const pre = document.createElement('pre'); pre.textContent = 'CSV load error: ' + (e && e.message ? e.message : e);497          pre.style.color = 'var(--danger, #b00020)'; pre.style.fontSize = '12px'; pre.style.whiteSpace = 'pre-wrap'; cell.appendChild(pre);498          if (typeof readyResolve === 'function') readyResolve();499        }500      }501      load();502      503      return {504        ready,505        getMetrics: () => metricList.slice(),506        setMetric: (m) => { if (m) renderMetric(m); },507        getYInfo: (m) => {508          const key = m; const map = dataByMetric.get(key) || {}; const runs = runOrder;509          let maxVal = 0, minVal = Infinity; let minStep = Infinity, maxStep = -Infinity;510          const isRank = /rank/i.test(key); const isAverage = /average/i.test(key); const isRankStrict = isRank && !isAverage;511          runs.forEach(r => { (map[r]||[]).forEach(pt => { const v = isRankStrict ? Math.round(pt.value) : pt.value; minStep=Math.min(minStep,pt.step); maxStep=Math.max(maxStep,pt.step); maxVal=Math.max(maxVal,v); minVal=Math.min(minVal,v); }); });512          const rankMax = isRank ? Math.max(1, Math.round(maxVal)) : null;513          return { isRank, isRankStrict, min: maxVal === 0 && minVal === Infinity ? null : minVal, max: maxVal, rankMax };514        },515        setSharedY: (cfg) => { sharedYConfig = cfg || null; if (metricList && metricList.length) { /* re-render last metric if possible */ const current = cfg && cfg.key ? cfg.key : null; const m = current || metricList[0]; renderMetric(m); } }516      };517    }518 519    const bootstrap = () => {520      const scriptEl = THIS_SCRIPT;521      let host = null;522      // Build header (legend + controls) and append after grid523      const header = document.createElement('div'); header.className = 'line-quad__header';524      const legend = document.createElement('div'); legend.className = 'legend-bottom'; legend.innerHTML = '<div class="legend-title">Legend</div><div class="items"></div>';525      const controls = document.createElement('div'); controls.className = 'controls'; controls.innerHTML = '<div class="control-group"><label>Metric</label><select></select></div>';526      header.appendChild(legend);527      header.appendChild(controls);528      // Try finding within parent (fragment mount is inside parent)529      if (scriptEl && scriptEl.parentElement && scriptEl.parentElement.querySelector) {530        host = scriptEl.parentElement.querySelector('.line-quad');531      }532      // Fallback: scan previous siblings533      if (!host) {534        let sib = scriptEl && scriptEl.previousElementSibling;535        while (sib && !(sib.classList && sib.classList.contains('line-quad'))) {536          sib = sib.previousElementSibling;537        }538        host = sib || null;539      }540      // Last resort: global query541      if (!host) { host = document.querySelector('.line-quad'); }542      if (!host) return;543      if (host.dataset && host.dataset.mounted === 'true') return; if (host.dataset) host.dataset.mounted = 'true';544      const cells = host.querySelectorAll('.quad-cell'); if (!cells.length) return;545      host.appendChild(header);546      const instances = Array.from(cells).map(cell => initRunLine(cell));547 548      (async () => {549        // Wait for all charts to finish loading their CSVs550        await Promise.all(instances.map(i => i.ready));551        const lists = instances.map(i => i.getMetrics()).filter(a => Array.isArray(a) && a.length);552        const intersect = (arrs) => arrs.reduce((acc, cur) => acc.filter(x => cur.includes(x)));553        let metrics = lists.length ? intersect(lists) : [];554        if (!metrics.length) { metrics = lists[0] || []; }555        const def = (metrics.includes('ai2d_exact_match') ? 'ai2d_exact_match' : (metrics.find(m => /average_rank/i.test(m)) || metrics[0] || ''));556 557        // Wire header controls (select under "Metric" label)558        const headerEl = host.querySelector('.line-quad__header');559        if (headerEl && !headerEl.isConnected) host.appendChild(header);560        const select = (headerEl || header).querySelector('.controls select');561        if (select) {562          select.innerHTML = '';563          metrics.forEach(m => { const o=document.createElement('option'); o.value=m; o.textContent=prettyMetricLabel(m); select.appendChild(o); });564          if (def) select.value = def;565        }566 567        const computeAndApplySharedY = (metric) => {568          try {569            const infos = instances.map(i => i && typeof i.getYInfo === 'function' ? i.getYInfo(metric) : null).filter(Boolean);570            if (!infos.length) return;571            const anyRank = infos.some(info => info.isRank);572            if (anyRank) {573              const maxRank = Math.max(1, ...infos.map(info => Math.round(info.rankMax || 1)));574              instances.forEach(i => i && typeof i.setSharedY === 'function' && i.setSharedY({ type: 'rank_strict', maxRank, key: metric }));575            } else {576              const min = Math.min(...infos.map(info => info.min));577              const max = Math.max(...infos.map(info => info.max));578              instances.forEach(i => i && typeof i.setSharedY === 'function' && i.setSharedY({ type: 'value', min, max, key: metric }));579            }580          } catch (_) {}581        };582 583        const applyAll = (v) => { computeAndApplySharedY(v); instances.forEach(i => i && typeof i.setMetric === 'function' && i.setMetric(v)); };584        if (def) applyAll(def);585        if (select) select.addEventListener('change', () => applyAll(select.value));586 587        // Global legend (in header, colors only)588        const legendItemsHost = (headerEl || header).querySelector('.legend-bottom .items');589        if (legendItemsHost) {590          try {591            const f = '/data/formatting_filters.csv';592            const r = await fetch(f, { cache:'no-cache' });593            if (r.ok && window.d3 && window.d3.csvParse) {594              const txt = await r.text();595              const rows = window.d3.csvParse(txt);596              const runList = Array.from(new Set(rows.map(row => String(row.run||'').trim()).filter(Boolean))).sort();597              const poolLegend = (window.ColorPalettes && typeof window.ColorPalettes.getColors==='function')598                ? window.ColorPalettes.getColors('categorical', runList.length)599                : (()=>{ const primary = getComputedStyle(document.documentElement).getPropertyValue('--primary-color').trim() || '#E889AB'; return [primary, '#4EA5B7', '#E38A42', '#CEC0FA', ...((window.d3 && window.d3.schemeTableau10) ? window.d3.schemeTableau10 : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab'])]; })();600              // Build shared run->color map once601              SHARED_RUN_COLOR = {};602              runList.forEach((name, i) => { SHARED_RUN_COLOR[name] = poolLegend[i % poolLegend.length]; });603              legendItemsHost.innerHTML = runList.map((name) => {604                const color = SHARED_RUN_COLOR[name];605                return `<span class="item" data-run="${name}"><span class=\"swatch\" style=\"background:${color}\"></span><span>${name}</span></span>`;606              }).join('');607              // Re-render all cells with the shared mapping to ensure perfect sync608              try {609                const currentMetric = (select && select.value) || def;610                if (currentMetric) applyAll(currentMetric);611              } catch {}612              // Legend hover ghosting across all cells613              legendItemsHost.querySelectorAll('.item').forEach(el => {614                el.addEventListener('mouseenter', () => {615                  const run = el.getAttribute('data-run'); if (!run) return;616                  host.classList.add('hovering');617                  host.querySelectorAll('.quad-cell').forEach(cell => {618                    cell.querySelectorAll('.lines path.run-line').forEach(p => p.classList.toggle('ghost', p.getAttribute('data-run') !== run));619                    cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.toggle('ghost', c.getAttribute('data-run') !== run));620                    cell.querySelectorAll('.areas path.area').forEach(a => a.classList.toggle('ghost', a.getAttribute('data-run') !== run));621                  });622                  legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.toggle('ghost', it.getAttribute('data-run') !== run));623                });624                el.addEventListener('mouseleave', () => {625                  host.classList.remove('hovering');626                  host.querySelectorAll('.quad-cell').forEach(cell => {627                    cell.querySelectorAll('.lines path.run-line').forEach(p => p.classList.remove('ghost'));628                    cell.querySelectorAll('.points circle.pt').forEach(c => c.classList.remove('ghost'));629                    cell.querySelectorAll('.areas path.area').forEach(a => a.classList.remove('ghost'));630                  });631                  legendItemsHost.querySelectorAll('.item').forEach(it => it.classList.remove('ghost'));632                });633              });634            }635          } catch {}636        }637      })();638    };639 640    if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }641  })();642</script>643 644 645 646