lerobot/robot-learning-tutorial
508
1<div class="d3-pie-quad"></div>2<style>3 /* Layout piloté par container queries (par rapport au parent) */4 .d3-pie-quad { container-type: inline-size; }5 .d3-pie-quad .legend { width: 80%;margin: 0 auto; font-size: 12px; line-height: 1.35; color: var(--text-color); }6 .d3-pie-quad .legend { margin-bottom: 32px; }7 .d3-pie-quad .legend .items { display:flex; flex-wrap:wrap; gap:8px 14px; align-items:center; justify-content:center; }8 .d3-pie-quad .legend .item { display:flex; align-items:center; gap:8px; white-space:nowrap; }9 .d3-pie-quad .legend .swatch { width:14px; height:14px; border-radius:3px; display:inline-block; border: 1px solid var(--border-color); }10 .d3-pie-quad .legend .title { display:block; text-align:center; font-weight:800; margin-bottom:6px; }11 .d3-pie-quad .caption { font-size: 14px; font-weight: 800; fill: var(--text-color); }12 .d3-pie-quad .caption-subtitle { font-size: 11px; font-weight: 400; fill: var(--muted-color); }13 .d3-pie-quad .nodata { font-size: 12px; fill: var(--muted-color); }14 /* Ghost legend items when hovering slices */15 .d3-pie-quad.hovering .legend .item.ghost { opacity: .35; }16 .d3-pie-quad .slice-label { font-size: 11px; font-weight: 700; fill: var(--text-color); paint-order: stroke; stroke: var(--transparent-page-contrast); stroke-width: 3px; }17 /* Effet ghost synchronisé */18 .d3-pie-quad .slice {19 transition: opacity .15s ease;20 }21 .d3-pie-quad.hovering .slice.ghost {22 opacity: .25;23 }24 /* Layout HTML (pas JS) pour la grille et les cellules */25 .d3-pie-quad .plots-grid {26 display: flex;27 flex-wrap: wrap;28 justify-content: center;29 align-items: flex-start;30 gap: 12px 20px;31 margin-top: 4px;32 margin-left: auto;33 margin-right: auto;34 width: 100%;35 }36 /* Par défaut (flux ~1280): 2 colonnes centrées */37 .content-grid .d3-pie-quad .plots-grid { width: 100%; }38 .content-grid .d3-pie-quad .pie-cell { flex: 0 0 calc((100% - 20px)/2); }39 /* En wrappers larges: viser 4 colonnes si l'espace le permet */40 .wide .d3-pie-quad .plots-grid,41 .full-width .d3-pie-quad .plots-grid { width: 100%; }42 .wide .d3-pie-quad .pie-cell,43 .full-width .d3-pie-quad .pie-cell { flex: 0 0 calc((100% - 60px)/4); }44 /* Forcer 2 colonnes dans le flux lorsque le parent ~1280px */45 .content-grid .d3-pie-quad .plots-grid { width: min(740px, 100%); }46 .d3-pie-quad .pie-cell {47 display: flex;48 flex-direction: column;49 align-items: center;50 flex: 0 0 360px; /* 2 colonnes fixes dans le flux à 1280px */51 }52 /* 4/2/1 colonnes en fonction de la largeur du parent */53 /* @container (min-width: 740px) {54 .d3-pie-quad .plots-grid { width: 740px; }55 }56 @container (max-width: 739.98px) {57 .d3-pie-quad .plots-grid { width: 100%; }58 } */59 @media (max-width: 500px) {60 .d3-pie-quad .pie-cell { flex: 0 0 100%; }61 }62 /* Tooltip styling aligned with filters-quad */63 .d3-pie-quad .d3-tooltip {64 z-index: var(--z-elevated);65 backdrop-filter: saturate(1.12) blur(8px);66 }67 .d3-pie-quad .d3-tooltip__inner {68 display: flex;69 flex-direction: column;70 gap: 6px;71 min-width: 220px;72 }73 .d3-pie-quad .d3-tooltip__inner > div:first-child {74 font-weight: 800;75 letter-spacing: 0.1px;76 margin-bottom: 0;77 }78 .d3-pie-quad .d3-tooltip__inner > div:nth-child(2) {79 font-size: 11px;80 color: var(--muted-color);81 display: block;82 margin-top: -4px;83 margin-bottom: 2px;84 letter-spacing: 0.1px;85 }86 .d3-pie-quad .d3-tooltip__inner > div:nth-child(n+3) {87 padding-top: 6px;88 border-top: 1px solid var(--border-color);89 }90 .d3-pie-quad .d3-tooltip__color-dot {91 display: inline-block;92 width: 12px;93 height: 12px;94 border-radius: 3px;95 border: 1px solid var(--border-color);96 }97</style>98<script>99 (() => {100 const THIS_SCRIPT = document.currentScript;101 const ensureD3 = (cb) => {102 if (window.d3 && typeof window.d3.select === 'function') return cb();103 let s = document.getElementById('d3-cdn-script');104 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); }105 const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };106 s.addEventListener('load', onReady, { once: true });107 if (window.d3) onReady();108 };109 110 const bootstrap = () => {111 const scriptEl = THIS_SCRIPT;112 const host = scriptEl && scriptEl.parentElement;113 let container = null;114 if (host && host.querySelector) {115 container = host.querySelector('.d3-pie-quad');116 }117 if (!container) {118 let sib = scriptEl && scriptEl.previousElementSibling;119 while (sib && !(sib.classList && sib.classList.contains('d3-pie-quad'))) {120 sib = sib.previousElementSibling;121 }122 container = sib || document.querySelector('.d3-pie-quad');123 }124 if (!container) return;125 if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; }126 127 // Tooltip128 container.style.position = container.style.position || 'relative';129 let tip = container.querySelector('.d3-tooltip'); let tipInner;130 if (!tip) {131 tip = document.createElement('div'); tip.className = 'd3-tooltip';132 Object.assign(tip.style, {133 position:'absolute', top:'0px', left:'0px', transform:'translate(-9999px, -9999px)', pointerEvents:'none',134 padding:'10px 12px', borderRadius:'12px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)',135 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',136 zIndex: 'var(--z-elevated)', backdropFilter: 'saturate(1.12) blur(8px)'137 });138 tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip);139 } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }140 141 // HTML scaffolding: legend and plots grid as HTML; only pies are SVG142 const legendHost = document.createElement('div'); legendHost.className = 'legend'; container.appendChild(legendHost);143 const plotsHost = document.createElement('div'); plotsHost.className = 'plots-grid'; container.appendChild(plotsHost);144 145 // Metrics (order and labels as in the Python script)146 const METRICS = [147 { key:'answer_total_tokens', name:'Answer Tokens', title:'Weighted by ', letter:'a' },148 { key:'total_samples', name:'Number of Samples', title:'Weighted by ', letter:'b' },149 { key:'total_turns', name:'Number of Turns', title:'Weighted by ', letter:'c' },150 { key:'total_images', name:'Number of Images', title:'Weighted by ', letter:'d' }151 ];152 153 // CSV: load from public path154 const CSV_PATHS = [155 '/data/vision.csv'156 ];157 158 const fetchFirstAvailable = async (paths) => {159 for (const p of paths) {160 try {161 const res = await fetch(p, { cache: 'no-cache' });162 if (res.ok) { return await res.text(); }163 } catch (_) { /* try next */ }164 }165 throw new Error('CSV not found: vision.csv');166 };167 168 const parseCsv = (text) => d3.csvParse(text, (d) => ({169 subset_name: (d['subset_name']||'').trim(),170 eagle_cathegory: (d['eagle_cathegory']||'').trim(),171 answer_total_tokens: +((d['answer_total_tokens']||'0').toString().trim()) || 0,172 total_samples: +((d['total_samples']||'0').toString().trim()) || 0,173 total_turns: +((d['total_turns']||'0').toString().trim()) || 0,174 total_images: +((d['total_images']||'0').toString().trim()) || 0175 }));176 177 // Layout178 let width=800; const margin = { top: 8, right: 24, bottom: 0, left: 24 };179 const CAPTION_GAP = 36; // espace entre titre et donut180 const GAP_X = 20; // espace entre colonnes181 const GAP_Y = 12; // espace entre lignes182 const TOP_OFFSET = 4; // décalage vertical supplémentaire pour aérer le haut183 const DONUT_INNER_RATIO = 0.58; // ratio du trou central (0 = pie plein, 0.5 = moitié)184 // LEGEND_GAP supprimé: l'espacement est désormais géré en CSS via .d3-pie-quad .legend { margin-bottom }185 const SVG_VPAD = 16; // padding vertical supplémentaire à l'intérieur des SVG pour éviter la coupe186 187 const updateSize = () => {188 width = container.clientWidth || 800;189 return { innerWidth: width - margin.left - margin.right };190 };191 192 function renderLegend(categories, colorOf){193 legendHost.style.display = 'flex';194 legendHost.style.alignItems = 'center';195 legendHost.style.justifyContent = 'center';196 legendHost.innerHTML = `<div class="items">${categories.map(c => `<div class="item" data-category="${c}"><span class=\"swatch\" style=\"background:${colorOf(c)}\"></span><span style=\"font-weight:500\">${c}</span></div>`).join('')}</div>`;197 }198 199 function drawPies(rows){200 const { innerWidth } = updateSize();201 202 // Catégories (triées) + échelle de couleurs harmonisée avec banner.html203 const categories = Array.from(new Set(rows.map(r => r.eagle_cathegory || 'Unknown'))).sort();204 const getCatColors = (n) => {205 try { if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') return window.ColorPalettes.getColors('categorical', n); } catch(_) {}206 return (d3.schemeTableau10 ? d3.schemeTableau10.slice(0, n) : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab'].slice(0, n));207 };208 const color = d3.scaleOrdinal().domain(categories).range(getCatColors(categories.length));209 const colorOf = (cat) => color(cat || 'Unknown');210 211 // Clear plots grid212 plotsHost.innerHTML = '';213 214 // Légende au-dessus, centrée215 renderLegend(categories, colorOf);216 217 // Rayon fixé selon la largeur cible d'une cellule (gérée par CSS)218 const CELL_BASIS = 360; // doit correspondre à .pie-cell { flex-basis }219 const radius = Math.max(80, Math.min(120, Math.floor(CELL_BASIS * 0.42)));220 const innerR = Math.round(radius * DONUT_INNER_RATIO);221 // Placement géré par CSS; ici on ne fait que l'espacement vertical minimal222 plotsHost.style.position = 'relative';223 plotsHost.style.marginTop = (TOP_OFFSET) + 'px';224 225 const pie = d3.pie().sort(null).value(d => d.value).padAngle(0.02);226 const arc = d3.arc().innerRadius(innerR).outerRadius(radius).cornerRadius(3);227 const arcLabel = d3.arc().innerRadius((innerR + radius) / 2).outerRadius((innerR + radius) / 2);228 229 // Légende déjà rendue au-dessus230 231 METRICS.forEach((metric, idx) => {232 // Aggregate by category233 const totals = new Map(); categories.forEach(c => totals.set(c, 0));234 rows.forEach(r => { totals.set(r.eagle_cathegory, totals.get(r.eagle_cathegory) + (r[metric.key] || 0)); });235 const values = categories.map(c => ({ category: c, value: totals.get(c) || 0 }));236 const nonZeroValues = values.filter(v => (v.value || 0) > 0);237 const totalSum = d3.sum(nonZeroValues, d => d.value);238 239 // Create HTML cell container240 const cell = document.createElement('div');241 cell.className = 'pie-cell';242 cell.style.width = (radius * 2) + 'px';243 cell.style.height = (radius * 2 + SVG_VPAD * 2 + CAPTION_GAP + 24) + 'px';244 cell.style.display = 'flex';245 cell.style.flexDirection = 'column';246 cell.style.alignItems = 'center';247 cell.style.justifyContent = 'flex-start';248 plotsHost.appendChild(cell);249 250 // SVG pie inside cell251 const svg = d3.select(cell).append('svg').attr('width', radius * 2).attr('height', radius * 2 + SVG_VPAD * 2).style('display','block');252 const gCell = svg.append('g').attr('transform', `translate(${radius},${radius + SVG_VPAD})`);253 254 if (!totalSum || totalSum <= 0 || nonZeroValues.length === 0) {255 gCell.append('text').attr('class','nodata').attr('text-anchor','middle').attr('dy','0').text('No data for this metric');256 } else {257 const data = pie(nonZeroValues);258 const percent = (v) => (v / totalSum) * 100;259 260 // Slices261 const slices = gCell.selectAll('path.slice').data(data).enter().append('path').attr('class','slice')262 .attr('d', arc)263 .attr('fill', d => colorOf(d.data.category))264 .attr('stroke', 'var(--surface-bg)')265 .attr('stroke-width', 1.2)266 .attr('data-category', d => d.data.category)267 .on('mouseenter', function(ev, d){268 const hoveredCategory = d.data.category;269 d3.select(container).classed('hovering', true);270 d3.select(container).selectAll('path.slice').classed('ghost', s => (s.data && s.data.category) !== hoveredCategory);271 // Ghost legend items that are not hovered272 d3.select(legendHost).selectAll('.item').classed('ghost', function(){ return this.dataset && this.dataset.category !== hoveredCategory; });273 d3.select(this).attr('stroke', 'rgba(0,0,0,0.85)').attr('stroke-width', 1);274 const p = percent(d.data.value);275 const catColor = colorOf(d.data.category);276 let html = `<div style="display:flex;align-items:center;gap:8px;white-space:nowrap;"><span class=\"d3-tooltip__color-dot\" style=\"background:${catColor}\"></span><strong>${d.data.category}</strong></div>`;277 html += `<div>${metric.name}</div>`;278 html += `<div style="display:flex;align-items:center;gap:6px;white-space:nowrap;"><strong>Value</strong><span style="margin-left:auto;text-align:right;">${d.data.value.toLocaleString()}</span></div>`;279 /* Share row removed per request */280 tipInner.innerHTML = html;281 tip.style.opacity = '1';282 })283 .on('mousemove', function(ev){284 const [mx, my] = d3.pointer(ev, container); const offsetX = 12, offsetY = 12; tip.style.transform = `translate(${Math.round(mx+offsetX)}px, ${Math.round(my+offsetY)}px)`;285 })286 .on('mouseleave', function(){287 tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)';288 d3.select(container).classed('hovering', false);289 d3.select(container).selectAll('path.slice').classed('ghost', false);290 d3.select(legendHost).selectAll('.item').classed('ghost', false);291 d3.select(this).attr('stroke','var(--surface-bg)');292 });293 294 // Percentage labels (>= 3%)295 gCell.selectAll('text.slice-label').data(data.filter(d => percent(d.data.value) >= 3)).enter()296 .append('text').attr('class','slice-label').style('pointer-events','none')297 .attr('transform', d => `translate(${arcLabel.centroid(d)})`)298 .attr('text-anchor','middle')299 .text(d => `${percent(d.data.value).toFixed(1)}%`);300 }301 302 // HTML captions under the SVG (keep design)303 const subtitleEl = document.createElement('div'); subtitleEl.className = 'caption-subtitle'; subtitleEl.textContent = metric.title; subtitleEl.style.textAlign = 'center'; cell.appendChild(subtitleEl);304 const titleEl = document.createElement('div'); titleEl.className = 'caption'; titleEl.textContent = metric.name; titleEl.style.textAlign = 'center'; cell.appendChild(titleEl);305 });306 307 // Container height flows naturally with HTML; nothing to do308 309 // Reset global hover/ghost when leaving the plots area310 plotsHost.onmouseleave = () => {311 tip.style.opacity='0';312 tip.style.transform='translate(-9999px, -9999px)';313 d3.select(container).classed('hovering', false);314 d3.select(container).selectAll('path.slice').classed('ghost', false);315 d3.select(legendHost).selectAll('.item').classed('ghost', false);316 };317 }318 319 async function init(){320 try {321 const text = await fetchFirstAvailable(CSV_PATHS);322 const rows = parseCsv(text);323 drawPies(rows);324 325 // Resize handling326 const rerender = () => drawPies(rows);327 if (window.ResizeObserver) { const ro = new ResizeObserver(() => rerender()); ro.observe(container); }328 else { window.addEventListener('resize', rerender); }329 } catch (err) {330 const pre = document.createElement('pre'); pre.textContent = (err && err.message) ? err.message : String(err);331 pre.style.color = 'var(--danger, #b00020)'; pre.style.fontSize = '12px'; pre.style.whiteSpace = 'pre-wrap';332 container.appendChild(pre);333 }334 }335 336 init();337 };338 339 if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }340 })();341</script>342 343 344 345 346 347 348 