CoolFace
Apppublic

lerobot/robot-learning-tutorial

sourceHugging Faceupdated 1y agoView on Hugging Face
508likes
d3-matrix.html516 linesDownload Raw Back to embeds
1<div class="d3-matrix"  ></div>2<style>3  .d3-matrix {4    position: relative;5  }6  .d3-matrix .panels {7    display: flex;8    flex-wrap: wrap;9    gap: 16px;10    margin-bottom: 4px;11  }12  .d3-matrix .panel {13    flex: 1 1 320px;14    min-width: 280px;15  }16  .d3-matrix .panel__title {17    color: var(--text-color);18    font-size: 12px;19    line-height: 1.35;20    margin: 0 0 6px 0;21    font-weight: 600;22  }23  .d3-matrix .axis-label {24    fill: var(--text-color);25    font-size: 11px;26    font-weight: 700;27  }28  .d3-matrix .cell-border {29    stroke: var(--border-color);30    stroke-width: 1px;31    fill: none;32  }33  .d3-matrix .cell-text {34    fill: var(--muted-color);35    font-size: 11px;36    pointer-events: none;37  }38  .d3-matrix .chart-card { background: var(--surface-bg); border: 1px solid var(--border-color); border-radius: 10px; padding: 8px; }39</style>40<script>41  (() => {42    // Load D3 from CDN once43    const ensureD3 = (cb) => {44      if (window.d3 && typeof window.d3.select === 'function') return cb();45      let s = document.getElementById('d3-cdn-script');46      if (!s) {47        s = document.createElement('script');48        s.id = 'd3-cdn-script';49        s.src = 'https://cdn.jsdelivr.net/npm/d3@7/dist/d3.min.js';50        document.head.appendChild(s);51      }52      const onReady = () => { if (window.d3 && typeof window.d3.select === 'function') cb(); };53      s.addEventListener('load', onReady, { once: true });54      if (window.d3) onReady();55    };56 57    const bootstrap = () => {58      const scriptEl = document.currentScript;59      let container = scriptEl ? scriptEl.previousElementSibling : null;60      if (!(container && container.classList && container.classList.contains('d3-matrix'))){61        const cs = Array.from(document.querySelectorAll('.d3-matrix')).filter(el => !(el.dataset && el.dataset.mounted === 'true'));62        container = cs[cs.length - 1] || null;63      }64      if (!container) return;65      if (container.dataset) {66        if (container.dataset.mounted === 'true') return;67        container.dataset.mounted = 'true';68      }69 70      // Tooltip (HTML, single instance inside container)71      container.style.position = container.style.position || 'relative';72      let tip = container.querySelector('.d3-tooltip');73      let tipInner;74      if (!tip) {75        tip = document.createElement('div');76        tip.className = 'd3-tooltip';77        Object.assign(tip.style, {78          position: 'absolute',79          top: '0px',80          left: '0px',81          transform: 'translate(-9999px, -9999px)',82          pointerEvents: 'none',83          padding: '8px 10px',84          borderRadius: '8px',85          fontSize: '12px',86          lineHeight: '1.35',87          border: '1px solid var(--border-color)',88          background: 'var(--surface-bg)',89          color: 'var(--text-color)',90          boxShadow: '0 4px 24px rgba(0,0,0,.18)',91          opacity: '0',92          transition: 'opacity .12s ease'93        });94        tipInner = document.createElement('div');95        tipInner.className = 'd3-tooltip__inner';96        tipInner.style.textAlign = 'left';97        tip.appendChild(tipInner);98        container.appendChild(tip);99      } else {100        tipInner = tip.querySelector('.d3-tooltip__inner') || tip;101      }102 103      // Panels container (two side-by-side matrices)104      const panels = document.createElement('div');105      panels.className = 'panels';106      const panelA = document.createElement('div');107      panelA.className = 'panel';108      const titleA = document.createElement('div'); titleA.className = 'panel__title'; titleA.textContent = 'Baseline (row-normalized %)';109      panelA.appendChild(titleA);110      const mountA = document.createElement('div'); panelA.appendChild(mountA);111      const panelB = document.createElement('div');112      panelB.className = 'panel';113      const titleB = document.createElement('div'); titleB.className = 'panel__title'; titleB.textContent = 'Delta (Improved − Baseline, pp)';114      panelB.appendChild(titleB);115      const mountB = document.createElement('div'); panelB.appendChild(mountB);116      panels.appendChild(panelA);117      panels.appendChild(panelB);118      container.appendChild(panels);119 120      // SVG scaffolding121      const cardA = document.createElement('div'); cardA.className = 'chart-card'; mountA.appendChild(cardA);122      const svgA = d3.select(cardA).append('svg').attr('width', '100%').style('display', 'block');123      const gRootA = svgA.append('g');124      const gCellsA = gRootA.append('g');125      const gAxesA = gRootA.append('g');126      const cardB = document.createElement('div'); cardB.className = 'chart-card'; mountB.appendChild(cardB);127      const svgB = d3.select(cardB).append('svg').attr('width', '100%').style('display', 'block');128      const gRootB = svgB.append('g');129      const gCellsB = gRootB.append('g');130      const gAxesB = gRootB.append('g');131 132      // Demo data (two distinct 10x10 matrices: Baseline vs Improved)133      // Rows / Columns are generic class labels134      const classes = ['0','1','2','3','4','5','6','7','8','9'];135      const matrixA = [136        [90,  2,  1,  0,  0,  0,  1,  0,  5,  1],137        [ 3, 85,  5,  1,  0,  1,  2,  1,  1,  1],138        [ 1,  6, 70, 10,  4,  4,  1,  1,  1,  2],139        [ 0,  1,  8, 65, 10, 10,  2,  1,  1,  2],140        [ 0,  0,  2,  6, 83,  3,  1,  1,  3,  1],141        [ 0,  1,  2, 12,  4, 70,  5,  2,  2,  2],142        [ 1,  2,  1,  0,  1,  2, 88,  1,  3,  1],143        [ 0,  1,  1,  1,  1,  1,  2, 90,  1,  2],144        [ 6,  2,  2,  4,  6,  3,  3,  2, 70,  2],145        [ 1,  1,  1,  1,  2,  1,  1,  2,  1, 89]146      ];147      const matrixB = [148        [94,  1,  0,  0,  0,  0,  1,  0,  3,  1],149        [ 2, 90,  3,  1,  0,  0,  1,  1,  1,  1],150        [ 1,  4, 78,  7,  3,  3,  1,  1,  1,  1],151        [ 0,  1,  5, 74,  7,  8,  1,  1,  1,  2],152        [ 0,  0,  1,  4, 88,  2,  1,  1,  2,  1],153        [ 0,  1,  1,  9,  3, 78,  3,  1,  2,  2],154        [ 1,  1,  1,  0,  1,  1, 91,  1,  2,  1],155        [ 0,  1,  1,  1,  1,  1,  1, 92,  1,  1],156        [ 4,  1,  1,  3,  4,  2,  2,  2, 79,  2],157        [ 1,  1,  1,  1,  2,  1,  1,  1,  1, 90]158      ];159 160      // Colors: sequential palette via window.ColorPalettes with graceful fallback161      const getSequentialColors = (count) => {162        try {163          if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {164            return window.ColorPalettes.getColors('sequential', count);165          }166        } catch (_) {}167        // Fallback: generate a monochrome scale using the primary color with varying opacity168        const arr = [];169        for (let i = 0; i < count; i++) arr.push('var(--primary-color)');170        return arr;171      };172 173      const palette = getSequentialColors(13);174      const getDivergingColors = (count) => {175        try {176          if (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function') {177            return window.ColorPalettes.getColors('diverging', count);178          }179        } catch (_) {}180        const steps = Math.max(3, count|0);181        const arr = [];182        for (let i = 0; i < steps; i++) {183          const t = i / (steps - 1);184          const pct = Math.round(t * 100);185          arr.push(`color-mix(in srgb, #D64545 ${100-pct}%, #3A7BD5 ${pct}%)`);186        }187        return arr;188      };189 190      let width = 800;191      let height = 480;192      const margin = { top: 36, right: 24, bottom: 26, left: 56 };193 194      function updateSize() {195        const isDark = document.documentElement.getAttribute('data-theme') === 'dark';196        width = container.clientWidth || 800;197        const gap = 16; // matches CSS .panels gap198        const minPanel = 320;199        const nCols = (width >= (minPanel * 2 + gap)) ? 2 : 1;200        const panelWidth = nCols === 2 ? Math.max(minPanel, Math.floor((width - gap) / 2)) : Math.max(minPanel, width);201        const base = Math.max(minPanel, Math.round(panelWidth * 0.92));202        height = base;203        // Responsive SVG: width 100%, height auto, preserve aspect via viewBox204        svgA205          .attr('viewBox', `0 0 ${panelWidth} ${height}`)206          .attr('preserveAspectRatio', 'xMidYMid meet')207          .style('width', '100%')208          .style('height', 'auto');209        svgB210          .attr('viewBox', `0 0 ${panelWidth} ${height}`)211          .attr('preserveAspectRatio', 'xMidYMid meet')212          .style('width', '100%')213          .style('height', 'auto');214        gRootA.attr('transform', `translate(${margin.left},${margin.top})`);215        gRootB.attr('transform', `translate(${margin.left},${margin.top})`);216        const innerWidth = panelWidth - margin.left - margin.right;217        const innerHeight = height - margin.top - margin.bottom;218        return { innerWidth, innerHeight, isDark };219      }220 221      function computeValues(normalization, matrix) {222        const n = classes.length;223        const totalsByRow = matrix.map(row => row.reduce((a, b) => a + b, 0));224        const flat = [];225        let minV = Infinity, maxV = -Infinity;226        for (let r = 0; r < n; r++) {227          for (let c = 0; c < n; c++) {228            const count = matrix[r][c];229            const value = normalization === 'row' ? (totalsByRow[r] ? count / totalsByRow[r] : 0) : count;230            if (value < minV) minV = value;231            if (value > maxV) maxV = value;232            flat.push({ r, c, count, value });233          }234        }235        return { data: flat, minV, maxV };236      }237 238      function getColorScale(values, minV, maxV) {239        // If ColorPalettes is available, use quantiles to enhance visual variation across the distribution240        const hasPalette = !(palette.length === 0);241        if (hasPalette && (window.ColorPalettes && typeof window.ColorPalettes.getColors === 'function')) {242          const scale = d3.scaleQuantile().domain(values).range(palette);243          return (v) => scale(v);244        }245        // Fallback: primary color with opacity mapped to normalized value246        const norm = d3.scaleLinear().domain([minV, maxV]).range([0.08, 0.9]).clamp(true);247        return (v) => `color-mix(in oklab, var(--primary-color) ${Math.round(norm(v) * 100)}%, var(--surface-bg))`;248      }249 250      // Compute a fixed readable text color from a CSS rgb()/rgba() string251      function chooseFixedReadableTextOnBg(bgCss){252        try {253          const m = String(bgCss||'').match(/rgba?\(([^)]+)\)/);254          if (!m) return '#0e1116';255          const parts = m[1].split(',').map(s => parseFloat(s.trim()));256          const [r, g, b] = parts;257          // sRGB → relative luminance258          const srgb = [r, g, b].map(v => Math.max(0, Math.min(255, v)) / 255);259          const linear = srgb.map(c => (c <= 0.03928 ? c/12.92 : Math.pow((c + 0.055)/1.055, 2.4)));260          const L = 0.2126*linear[0] + 0.7152*linear[1] + 0.0722*linear[2];261          // Threshold ~ 0.5 for readability; darker BG → white text, else near-black262          return L < 0.5 ? '#ffffff' : '#0e1116';263        } catch(_) { return '#0e1116'; }264      }265 266      function render() {267        const { innerWidth, innerHeight } = updateSize();268        const n = classes.length;269        const gridSize = Math.min(innerWidth, innerHeight);270        const cellSize = gridSize / n;271 272        const x = d3.scaleBand().domain(d3.range(n)).range([0, gridSize]).paddingInner(0.06);273        const y = d3.scaleBand().domain(d3.range(n)).range([0, gridSize]).paddingInner(0.06);274 275        // Panel A: Baseline (row-normalized)276        const dataA = computeValues('row', matrixA);277        const colorA = getColorScale(dataA.data.map(d => d.value), dataA.minV, dataA.maxV);278 279        gCellsA.selectAll('rect.cell-bg')280          .data([0])281          .join('rect')282          .attr('class', 'cell-bg')283          .attr('x', 0)284          .attr('y', 0)285          .attr('width', gridSize)286          .attr('height', gridSize)287          .attr('fill', 'none')288          .attr('stroke', 'var(--border-color)')289          .attr('stroke-width', 1);290 291        const cellsA = gCellsA.selectAll('g.cell')292          .data(dataA.data, d => `${d.r}-${d.c}-A`);293 294        const cellsEnterA = cellsA.enter()295          .append('g')296          .attr('class', 'cell');297 298        cellsEnterA.append('rect')299          .attr('rx', 2)300          .attr('ry', 2)301          .on('mousemove', (event, d) => {302            const [px, py] = d3.pointer(event, container);303            tipInner.innerHTML = `<strong>${classes[d.r]}</strong> → <strong>${classes[d.c]}</strong><br/>${(d.value * 100).toFixed(1)}% (${d.count})`;304            tip.style.transform = `translate(${px + 10}px, ${py + 10}px)`;305            tip.style.opacity = '1';306          })307          .on('mouseleave', () => {308            tip.style.opacity = '0';309          });310 311        cellsEnterA.append('text')312          .attr('class', 'cell-text')313          .attr('text-anchor', 'middle')314          .attr('dominant-baseline', 'middle');315 316        const cellsMergedA = cellsEnterA.merge(cellsA);317 318        cellsMergedA.select('text')319          .attr('x', d => x(d.c) + x.bandwidth() / 2)320          .attr('y', d => y(d.r) + y.bandwidth() / 2)321          .text(d => `${Math.round(d.value * 100)}`)322          .style('fill', function(d){323            try {324              const rect = this && this.parentNode ? this.parentNode.querySelector('rect') : null;325              const bg = rect ? getComputedStyle(rect).fill : colorA(d.value);326              return chooseFixedReadableTextOnBg(bg);327            } catch (_) {328              return '#0e1116';329            }330          });331 332        cellsMergedA.select('rect')333          .attr('x', d => x(d.c))334          .attr('y', d => y(d.r))335          .attr('width', Math.max(1, x.bandwidth()))336          .attr('height', Math.max(1, y.bandwidth()))337          .attr('fill', d => colorA(d.value));338 339        cellsA.exit().remove();340 341        gAxesA.selectAll('*').remove();342 343        gAxesA.append('g')344          .selectAll('text')345          .data(classes)346          .join('text')347          .attr('class', 'axis-label')348          .attr('text-anchor', 'middle')349          .attr('x', (_, i) => x(i) + x.bandwidth() / 2)350          .attr('y', -8)351          .text(d => d);352 353        gAxesA.append('g')354          .selectAll('text')355          .data(classes)356          .join('text')357          .attr('class', 'axis-label')358          .attr('text-anchor', 'end')359          .attr('x', -8)360          .attr('y', (_, i) => y(i) + y.bandwidth() / 2)361          .attr('dominant-baseline', 'middle')362          .text(d => d);363 364        gAxesA.append('text')365          .attr('class', 'axis-label')366          .attr('text-anchor', 'middle')367          .attr('x', gridSize / 2)368          .attr('y', innerHeight + 20)369          .text('Columns');370 371        gAxesA.append('text')372          .attr('class', 'axis-label')373          .attr('text-anchor', 'middle')374          .attr('transform', `translate(${-40}, ${gridSize / 2}) rotate(-90)`)375          .text('Rows');376 377        // Panel B: Delta (Improved − Baseline), row-normalized differences in percentage points378        const dataB = computeValues('row', matrixB);379        const diverging = getDivergingColors(13);380        // Build delta values aligned to A's ordering381        const mapA = new Map(dataA.data.map(d => [d.r + '-' + d.c, d.value]));382        const delta = dataB.data.map(d => ({ r: d.r, c: d.c, count: d.count, value: (d.value - (mapA.get(d.r + '-' + d.c) || 0)) }));383        // Symmetric domain around 0 (in proportions), express later as pp in labels384        const maxAbsDelta = Math.max(0.01, d3.max(delta, d => Math.abs(d.value)) || 0.01);385        const colorB = d3.scaleQuantize().domain([-maxAbsDelta, maxAbsDelta]).range(diverging);386 387        gCellsB.selectAll('rect.cell-bg')388          .data([0])389          .join('rect')390          .attr('class', 'cell-bg')391          .attr('x', 0)392          .attr('y', 0)393          .attr('width', gridSize)394          .attr('height', gridSize)395          .attr('fill', 'none')396          .attr('stroke', 'var(--border-color)')397          .attr('stroke-width', 1);398 399        const cellsB = gCellsB.selectAll('g.cell')400          .data(dataB.data, d => `${d.r}-${d.c}-B`);401 402        const cellsEnterB = cellsB.enter()403          .append('g')404          .attr('class', 'cell');405 406        cellsEnterB.append('rect')407          .attr('rx', 2)408          .attr('ry', 2)409          .on('mousemove', (event, d) => {410            const [px, py] = d3.pointer(event, container);411            const a = dataA.data.find(x => x.r===d.r && x.c===d.c);412            const b = dataB.data.find(x => x.r===d.r && x.c===d.c);413            const dv = ((b ? b.value : 0) - (a ? a.value : 0)) * 100;414            tipInner.innerHTML = `<strong>${classes[d.r]}</strong> → <strong>${classes[d.c]}</strong>` +415              `<br/>baseline ${(a ? a.value*100 : 0).toFixed(1)}%` +416              `<br/>improved ${(b ? b.value*100 : 0).toFixed(1)}%` +417              `<br/>delta ${dv.toFixed(1)} pp`;418            tip.style.transform = `translate(${px + 10}px, ${py + 10}px)`;419            tip.style.opacity = '1';420          })421          .on('mouseleave', () => {422            tip.style.opacity = '0';423          });424 425        cellsEnterB.append('text')426          .attr('class', 'cell-text')427          .attr('text-anchor', 'middle')428          .attr('dominant-baseline', 'middle');429 430        const cellsMergedB = cellsEnterB.merge(cellsB);431 432        cellsMergedB.select('rect')433          .attr('x', d => x(d.c))434          .attr('y', d => y(d.r))435          .attr('width', Math.max(1, x.bandwidth()))436          .attr('height', Math.max(1, y.bandwidth()))437          .attr('fill', d => colorB(delta.find(x => x.r===d.r && x.c===d.c).value));438 439        cellsMergedB.select('text')440          .attr('x', d => x(d.c) + x.bandwidth() / 2)441          .attr('y', d => y(d.r) + y.bandwidth() / 2)442          .text(d => {443            const dv = delta.find(x => x.r===d.r && x.c===d.c).value; return `${Math.round(dv * 100)}`;444          })445          .style('fill', function(d){446            try {447              const rect = this && this.parentNode ? this.parentNode.querySelector('rect') : null;448              const dv = delta.find(x => x.r===d.r && x.c===d.c).value;449              const bg = rect ? getComputedStyle(rect).fill : colorB(dv);450              return chooseFixedReadableTextOnBg(bg);451            } catch (_) {452              return '#0e1116';453            }454          });455 456        cellsB.exit().remove();457 458        gAxesB.selectAll('*').remove();459 460        gAxesB.append('g')461          .selectAll('text')462          .data(classes)463          .join('text')464          .attr('class', 'axis-label')465          .attr('text-anchor', 'middle')466          .attr('x', (_, i) => x(i) + x.bandwidth() / 2)467          .attr('y', -8)468          .text(d => d);469 470        gAxesB.append('g')471          .selectAll('text')472          .data(classes)473          .join('text')474          .attr('class', 'axis-label')475          .attr('text-anchor', 'end')476          .attr('x', -8)477          .attr('y', (_, i) => y(i) + y.bandwidth() / 2)478          .attr('dominant-baseline', 'middle')479          .text(d => d);480 481        gAxesB.append('text')482          .attr('class', 'axis-label')483          .attr('text-anchor', 'middle')484          .attr('x', gridSize / 2)485          .attr('y', innerHeight + 20)486          .text('Columns');487 488        gAxesB.append('text')489          .attr('class', 'axis-label')490          .attr('text-anchor', 'middle')491          .attr('transform', `translate(${-40}, ${gridSize / 2}) rotate(-90)`)492          .text('Rows');493      }494 495      // Initial render + resize handling496      render();497      const rerender = () => render();498      if (window.ResizeObserver) {499        const ro = new ResizeObserver(() => rerender());500        ro.observe(container);501      } else {502        window.addEventListener('resize', rerender);503      }504    };505 506    if (document.readyState === 'loading') {507      document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true });508    } else {509      ensureD3(bootstrap);510    }511  })();512</script>513 514 515 516