CoolFace
Apppublic

lerobot/robot-learning-tutorial

sourceHugging Faceupdated 1y agoView on Hugging Face
508likes
d3-scatter.html301 linesDownload Raw Back to embeds
1<div class="d3-scatter"  ></div>2<style>3  /* Frameless: no controls, no axes, only dots */4  .d3-scatter svg { display: block; }5  /* Tooltip refined styling (align with filters-quad) */6  .d3-scatter .d3-tooltip {7    z-index: 20;8    backdrop-filter: saturate(1.12) blur(8px);9  }10  .d3-scatter .d3-tooltip__inner {11    display: flex;12    flex-direction: column;13    gap: 6px;14    min-width: 200px;15  }16  .d3-scatter .d3-tooltip__inner > div:first-child {17    font-weight: 800;18    letter-spacing: 0.1px;19    margin-bottom: 0;20  }21  .d3-scatter .d3-tooltip__inner > div:nth-child(2) {22    font-size: 11px;23    color: var(--muted-color);24    display: block;25    margin-top: -4px;26    margin-bottom: 2px;27    letter-spacing: 0.1px;28  }29  .d3-scatter .d3-tooltip__inner > div:nth-child(n+3) {30    padding-top: 6px;31    border-top: 1px solid var(--border-color);32  }33</style>34<script>35  (() => {36    const ensureD3 = (cb) => {37      if (window.d3 && typeof window.d3.select === 'function') return cb();38      let s = document.getElementById('d3-cdn-script');39      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); }40      const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };41      s.addEventListener('load', onReady, { once: true });42      if (window.d3) onReady();43    };44 45    const bootstrap = () => {46      const scriptEl = document.currentScript;47      let container = scriptEl ? scriptEl.previousElementSibling : null;48      if (!(container && container.classList && container.classList.contains('d3-scatter'))){49        const cs = Array.from(document.querySelectorAll('.d3-scatter')).filter(el => !(el.dataset && el.dataset.mounted==='true'));50        container = cs[cs.length-1] || null;51      }52      if (!container) return;53      if (container.dataset){ if (container.dataset.mounted==='true') return; container.dataset.mounted='true'; }54 55      // Tooltip56      container.style.position = container.style.position || 'relative';57      let tip = container.querySelector('.d3-tooltip'); let tipInner;58      if (!tip) {59        tip = document.createElement('div'); tip.className = 'd3-tooltip';60        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', backdropFilter:'saturate(1.12) blur(8px)' });61        tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip);62      } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; }63 64      // SVG65      const svg = d3.select(container).append('svg').attr('width','100%').style('display','block');66      const gRoot = svg.append('g');67      const gGrid = gRoot.append('g').attr('class','grid');68      const gAxes = gRoot.append('g').attr('class','axes');69      const gDots = gRoot.append('g').attr('class','dots');70      const gCentroids = gRoot.append('g').attr('class','centroids');71      const gLegend = gRoot.append('foreignObject').attr('class','legend');72 73      // State & scales74      let width=800, height=360; const margin = { top: 8, right: 12, bottom: 8, left: 12 };75      const x = d3.scaleLinear();76      const y = d3.scaleLinear();77      const color = d3.scaleOrdinal();78      const radius = () => 4;79      let isDarkMode = false;80      function getDotStrokeColor(fillColor = null){ 81        if (!fillColor) return 'var(--muted-color)';82        83        // Résoudre les variables CSS en couleurs réelles84        let resolvedColor = fillColor;85        if (fillColor.startsWith('var(')) {86          const tempEl = document.createElement('div');87          tempEl.style.color = fillColor;88          document.body.appendChild(tempEl);89          resolvedColor = getComputedStyle(tempEl).color;90          document.body.removeChild(tempEl);91        }92        93        try {94          const colorObj = d3.color(resolvedColor);95          if (!colorObj) return 'var(--muted-color)';96          97          // En mode light: bordure plus claire, en mode dark: bordure plus sombre98          return isDarkMode ? 99            colorObj.darker(0.3).toString() : 100            colorObj.brighter(0.8).toString();101        } catch {102          return 'var(--muted-color)';103        }104      }105 106      // Data loading (real): banner visualization positions by category107      async function fetchFirstAvailable(paths){108        for (const p of paths){109          try {110            const res = await fetch(p, { cache: 'no-cache' });111            if (res.ok){ return await res.text(); }112          } catch (e) {}113        }114        throw new Error('Failed to load data from provided paths');115      }116 117      let data = [];118      let categories = [];119      let colorMode = 'group';120 121      function renderLegend(innerWidth){ gLegend.remove(); }122 123      function updateScales(data){124        const isDark = document.documentElement.getAttribute('data-theme') === 'dark';125        isDarkMode = !!isDark;126        const axisColor = "var(--page-bg)";127        const tickColor = "var(--page-bg)";128        const gridColor = "var(--page-bg)";129 130        width = container.clientWidth || 800; height = Math.max(260, Math.round(width/3)); svg.attr('width', width).attr('height', height);131        const innerWidth = width - margin.left - margin.right; const innerHeight = height - margin.top - margin.bottom; gRoot.attr('transform', `translate(${margin.left},${margin.top})`);132 133        const xExtent = d3.extent(data, d=>d.x);134        const yExtent = d3.extent(data, d=>d.y);135        x.domain([xExtent[0], xExtent[1]]).range([0, innerWidth]).nice();136        y.domain([yExtent[0], yExtent[1]]).range([innerHeight, 0]).nice();137 138        // Frameless: no grid, no axes139        gGrid.selectAll('*').remove();140        gAxes.selectAll('*').remove();141 142        renderLegend(innerWidth);143 144        return { innerWidth, innerHeight };145      }146 147      function refreshPalette(){148        try {149          const cats = categories && categories.length ? categories.length : 6;150          if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {151            const arr = window.ColorPalettes.getColors('categorical', cats) || [];152            if (arr && arr.length) { color.range(arr); return; }153          }154          // fallback155          color.range((d3.schemeTableau10 ? d3.schemeTableau10 : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab']).slice(0, cats));156        } catch {157          const cats = categories && categories.length ? categories.length : 6;158          color.range((d3.schemeTableau10 ? d3.schemeTableau10 : ['#4e79a7','#f28e2b','#e15759','#76b7b2','#59a14f','#edc948','#b07aa1','#ff9da7','#9c755f','#bab0ab']).slice(0, cats));159        }160        // Recolor existing marks/labels after palette changes161        try { if (data && data.length) draw(); } catch {}162      }163 164      function draw(){165        if (!data || !data.length) return;166        const { innerWidth, innerHeight } = updateScales(data);167        const fillFor = d => colorMode === 'group' ? color(d.group) : 'var(--primary-color)';168 169        const dots = gDots.selectAll('circle.dot').data(data, (d,i)=>d.id || i);170        dots.enter().append('circle').attr('class','dot')171          .attr('cx', d=>x(d.x)).attr('cy', d=>y(d.y)).attr('r', radius())172          .attr('fill', fillFor).attr('fill-opacity', 0.85)173          .attr('stroke', d => getDotStrokeColor(fillFor(d))).attr('stroke-width', '0.75px')174          .on('mouseenter', function(ev, d){175            d3.select(this).style('stroke','var(--text-color)').style('stroke-width','1.5px').attr('fill-opacity', 1);176            const swatch = `<svg width="10" height="10" viewBox="0 0 10 10" aria-hidden="true"><circle cx="5" cy="5" r="5" fill="${fillFor(d)}" /></svg>`;177            tipInner.innerHTML = `178              <div><strong>${d.label || 'Item'}</strong></div>179              <div style="display:flex;align-items:center;gap:6px;">${swatch}<span>${d.group}</span></div>180              <div style="display:flex;align-items:center;gap:6px;white-space:nowrap;"><strong>x</strong><span style="margin-left:auto;text-align:right;">${d.x.toFixed(2)}</span></div>181              <div style="display:flex;align-items:center;gap:6px;white-space:nowrap;"><strong>y</strong><span style="margin-left:auto;text-align:right;">${d.y.toFixed(2)}</span></div>`;182            tip.style.opacity = '1';183          })184          .on('mousemove', function(ev){ const [mx, my] = d3.pointer(ev, container); const ox=12, oy=12; tip.style.transform = `translate(${Math.round(mx+ox)}px, ${Math.round(my+oy)}px)`; })185          .on('mouseleave', function(ev, d){ tip.style.opacity='0'; tip.style.transform='translate(-9999px, -9999px)'; d3.select(this).style('stroke', getDotStrokeColor(fillFor(d))).style('stroke-width','0.75px').attr('fill-opacity', 0.85); })186          .merge(dots)187          .transition().duration(180)188          .attr('cx', d=>x(d.x)).attr('cy', d=>y(d.y)).attr('r', radius())189          .attr('fill', fillFor).attr('fill-opacity', 0.85)190          .attr('stroke', d => getDotStrokeColor(fillFor(d))).attr('stroke-width','0.75px');191        dots.exit().remove();192 193        // Compute centroids per category194        const centroids = Array.from(195          d3.rollup(196            data,197            (v) => ({198              category: v[0] ? v[0].group : 'Unknown',199              x: d3.mean(v, (d) => d.x),200              y: d3.mean(v, (d) => d.y),201              count: v.length202            }),203            (d) => d.group204          ).values()205        );206 207        // Map to pixel space nodes for collision-avoiding label placement208        const nodes = centroids.map((c) => ({209          category: c.category,210          count: c.count,211          targetX: x(c.x),212          targetY: y(c.y),213          x: x(c.x),214          y: y(c.y),215          width: Math.max(18, (String(c.category || '').length || 6) * 11),216          height: 16217        }));218 219        if (nodes.length > 1) {220          const sim = d3.forceSimulation(nodes)221            .force('x', d3.forceX((d) => d.targetX).strength(0.9))222            .force('y', d3.forceY((d) => d.targetY).strength(0.9))223            .force('collide', d3.forceCollide((d) => Math.hypot(d.width/2, d.height/2) + 15))224            .stop();225          for (let i = 0; i < 650; i++) sim.tick();226          const maxOffset = 45;227          nodes.forEach((n) => {228            const dx = n.x - n.targetX, dy = n.y - n.targetY; const dist = Math.hypot(dx, dy);229            if (dist > maxOffset && dist > 0) { const s = maxOffset / dist; n.x = n.targetX + dx * s; n.y = n.targetY + dy * s; }230          });231        }232 233        const labels = gCentroids.selectAll('g.centroid').data(nodes, d => d.category || 'Unknown');234        const enter = labels.enter().append('g').attr('class','centroid').attr('pointer-events','none');235        enter.append('text').attr('class','label-bg').attr('text-anchor','middle').attr('dominant-baseline','middle');236        enter.append('text').attr('class','label-fg').attr('text-anchor','middle').attr('dominant-baseline','middle');237        const merged = enter.merge(labels);238        merged239          .attr('transform', d => `translate(${Math.round(d.x)}, ${Math.round(d.y)})`)240          .each(function(d){241            const base = color(d.category || 'Unknown') || 'var(--text-color)';242            const bg = getComputedStyle(document.documentElement).getPropertyValue('--page-bg').trim() || '#fff';243            const bgNode = this.querySelector('text.label-bg');244            const fgNode = this.querySelector('text.label-fg');245            if (bgNode) {246              bgNode.textContent = d.category;247              bgNode.style.setProperty('fill', "var(--page-bg)", 'important');248              bgNode.style.setProperty('stroke', "var(--page-bg)");249              bgNode.style.setProperty('stroke-width', '8px');250              bgNode.style.setProperty('paint-order', 'stroke fill');251              bgNode.style.setProperty('font-weight','800');252              bgNode.style.setProperty('font-size','16px');253            }254            if (fgNode) {255              fgNode.textContent = d.category;256              fgNode.style.setProperty('fill', base, 'important');257              fgNode.style.setProperty('font-weight','800');258              fgNode.style.setProperty('font-size','16px');259            }260          });261        labels.exit().remove();262      }263 264      // Initial load265      refreshPalette();266      document.addEventListener('palettes:updated', refreshPalette);267 268      (async () => {269        try {270          const csvText = await fetchFirstAvailable([271            '/data/banner_visualisation_data.csv',272            './assets/data/banner_visualisation_data.csv',273            '../assets/data/banner_visualisation_data.csv',274            '/data/banner_visualisation_data.csv'275          ]);276          const rows = d3.csvParse(csvText);277          data = rows.map((r, i) => ({278            id: +r.original_id ?? i,279            x: +r.x_position,280            y: +r.y_position,281            group: r.category || 'Unknown',282            label: r.subset || r.category || `Item ${i+1}`283          })).filter(d => Number.isFinite(d.x) && Number.isFinite(d.y));284          categories = Array.from(new Set(data.map(d=>d.group)));285          color.domain(categories);286          draw();287        } catch (e) {288          const pre = document.createElement('pre'); pre.style.color = 'crimson'; pre.textContent = 'Failed to load scatter data.'; container.appendChild(pre);289        }290      })();291 292      const rerender = () => { draw(); };293      if (window.ResizeObserver) { const ro = new ResizeObserver(()=>rerender()); ro.observe(container); } else { window.addEventListener('resize', rerender); }294    };295 296    if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); }297  })();298</script>299 300 301