Spaces:
Running
Running
thibaud frere
Move assets into content/assets; update imports; clean .gitattributes; fix LFS tracking
b8e1b6c
| <div class="d3-train-diagram" style="width:100%;margin:10px 0;"></div> | |
| <div class="caption">Survolez les blocs pour afficher une explication.</div> | |
| <style> | |
| .d3-train-diagram + .caption { margin-top: 8px; font-size: 14px; color: var(--muted-color); } | |
| </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 mount = document.currentScript ? document.currentScript.previousElementSibling : null; | |
| const container = (mount && mount.querySelector && mount.querySelector('.d3-train-diagram')) || document.querySelector('.d3-train-diagram'); | |
| if (!container) return; | |
| if (container.dataset) { if (container.dataset.mounted === 'true') return; container.dataset.mounted = 'true'; } | |
| // Diagram spec | |
| const numBlocks = 7; | |
| const rows = [ | |
| { key: 'model', label: 'Model', color: '#a78bfa' }, | |
| { key: 'forward', label: 'Forward', color: '#14b8a6' }, | |
| { key: 'backward', label: 'Backward', color: '#f59e0b' }, | |
| { key: 'gradients', label: 'Gradients', color: 'var(--primary-color)' }, | |
| { key: 'optimization', label: 'Optimization', color: '#10b981' }, | |
| { key: 'updated', label: 'Updated', color: '#7c3aed' }, | |
| ]; | |
| const hoverText = { | |
| model: 'Chaque bloc représente un sous-module du modèle.', | |
| forward: 'Propagation avant: calcul des activations couche par couche.', | |
| backward: 'Rétropropagation: calcul des gradients via la chaîne.', | |
| gradients: 'Accumulateurs de gradients pour chaque couche.', | |
| optimization: 'Étape d’optimisation: mise à jour des poids.', | |
| updated: 'Paramètres mis à jour, prêts pour l’itération suivante.' | |
| }; | |
| // SVG | |
| const svg = d3.select(container).append('svg').attr('width', '100%').style('display','block'); | |
| const gRoot = svg.append('g'); | |
| const gLegend = gRoot.append('foreignObject').attr('class','legend'); | |
| const gArrows = gRoot.append('g').attr('class','arrows'); | |
| const gBlocks = gRoot.append('g').attr('class','blocks'); | |
| const gLabels = gRoot.append('g').attr('class','row-labels'); | |
| // Tooltip (reuse style from others) | |
| container.style.position = container.style.position || 'relative'; | |
| let tip = container.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:'8px 10px', borderRadius:'8px', fontSize:'12px', lineHeight:'1.35', border:'1px solid var(--border-color)', background:'var(--surface-bg)', color:'var(--text-color)', boxShadow:'0 4px 24px rgba(0,0,0,.18)', opacity:'0', transition:'opacity .12s ease' }); tipInner = document.createElement('div'); tipInner.className = 'd3-tooltip__inner'; tipInner.style.textAlign='left'; tip.appendChild(tipInner); container.appendChild(tip); } else { tipInner = tip.querySelector('.d3-tooltip__inner') || tip; } | |
| // Layout | |
| let width=800, height=360; const margin = { top: 24, right: 180, bottom: 40, left: 32 }; | |
| const x = d3.scaleBand().domain(d3.range(numBlocks)).paddingInner(0.2).paddingOuter(0.05); | |
| const y = d3.scaleBand().domain(d3.range(rows.length)).paddingInner(0.35); | |
| function updateScales(){ | |
| width = container.clientWidth || 800; | |
| const rowH = Math.max(54, Math.min(80, Math.round(width / 12))); | |
| const innerHeight = rows.length * rowH; | |
| height = innerHeight + margin.top + margin.bottom; | |
| svg.attr('width', width).attr('height', height); | |
| const innerWidth = width - margin.left - margin.right; | |
| gRoot.attr('transform', `translate(${margin.left},${margin.top})`); | |
| x.range([0, innerWidth]); | |
| y.range([0, innerHeight]); | |
| return { innerWidth, innerHeight }; | |
| } | |
| function render(){ | |
| const { innerWidth, innerHeight } = updateScales(); | |
| // Legend right side | |
| const legendWidth = 160, legendHeight = rows.length * 20; | |
| gLegend.attr('x', innerWidth + 16).attr('y', 0).attr('width', legendWidth).attr('height', legendHeight); | |
| const lroot = gLegend.selectAll('div').data([0]).join('xhtml:div'); | |
| lroot.html(` | |
| <div style="display:flex;flex-direction:column;gap:8px;"> | |
| ${rows.map(r => `<div style=\"display:flex;align-items:center;gap:8px;\"><span style=\"width:14px;height:14px;background:${r.color};border-radius:4px;display:inline-block\"></span><span>${r.label}</span></div>`).join('')} | |
| </div> | |
| `); | |
| // Row labels on the right side aligned to centers | |
| gLabels.selectAll('*').remove(); | |
| gLabels.selectAll('text').data(rows).join('text') | |
| .attr('x', innerWidth + 16) | |
| .attr('y', (_,i)=> y(i) + y.bandwidth()/2) | |
| .attr('dy','0.35em') | |
| .style('font-size','14px') | |
| .style('fill','var(--text-color)') | |
| .text(d=>d.label); | |
| // Blocks per row | |
| const blockW = Math.min(84, x.bandwidth()); | |
| const blockH = Math.min(52, Math.round(y.bandwidth() * 0.8)); | |
| const blocks = []; | |
| rows.forEach((row, ri) => { | |
| for (let i=0;i<numBlocks;i++) blocks.push({ row, ri, i }); | |
| }); | |
| const sel = gBlocks.selectAll('rect.block').data(blocks, d=>`${d.row.key}-${d.i}`); | |
| sel.join( | |
| enter => enter.append('rect').attr('class','block') | |
| .attr('x', d=>x(d.i)) | |
| .attr('y', d=>y(d.ri) + (y.bandwidth()-blockH)/2) | |
| .attr('rx', 12).attr('ry', 12) | |
| .attr('width', blockW) | |
| .attr('height', blockH) | |
| .attr('fill', d=>d.row.color) | |
| .attr('opacity', 0.95) | |
| .attr('stroke', 'rgba(0,0,0,0.18)') | |
| .attr('filter', 'url(#shadow)') | |
| .on('mouseenter', function(ev, d){ | |
| d3.select(this).attr('opacity', 1.0).attr('stroke-width', 1.2); | |
| tipInner.innerHTML = `<div><strong>${d.row.label}</strong></div><div>${hoverText[d.row.key]}</div>`; | |
| tip.style.opacity = '1'; | |
| }) | |
| .on('mousemove', function(ev){ const [mx,my] = d3.pointer(ev, container); tip.style.transform = `translate(${mx+12}px, ${my+12}px)`; }) | |
| .on('mouseleave', function(){ tip.style.opacity='0'; tip.style.transform='translate(-9999px,-9999px)'; d3.select(this).attr('opacity', 0.95).attr('stroke-width', 1); }) | |
| ); | |
| // Arrows forward/backward | |
| gArrows.selectAll('*').remove(); | |
| const arrowY = (ri) => y(ri) + y.bandwidth()/2; | |
| const isDark = document.documentElement.getAttribute('data-theme') === 'dark'; | |
| const arrowColor = isDark ? 'rgba(255,255,255,0.55)' : 'rgba(0,0,0,0.55)'; | |
| const defs = svg.select('defs').empty() ? svg.append('defs') : svg.select('defs'); | |
| const marker = defs.append('marker').attr('id','arrow').attr('viewBox','0 0 10 10').attr('refX', 10).attr('refY', 5).attr('markerWidth', 6).attr('markerHeight', 6).attr('orient','auto-start-reverse'); | |
| marker.append('path').attr('d','M 0 0 L 10 5 L 0 10 z').attr('fill', arrowColor); | |
| // drop shadow filter | |
| const flt = defs.append('filter').attr('id','shadow').attr('x','-20%').attr('y','-20%').attr('width','140%').attr('height','140%'); | |
| flt.append('feDropShadow').attr('dx','0').attr('dy','1').attr('stdDeviation','1.5').attr('flood-color','rgba(0,0,0,0.18)'); | |
| // Forward arrow (top orientation) | |
| gArrows.append('line').attr('x1', x(0)).attr('y1', arrowY(1)-28).attr('x2', x(numBlocks-1)+blockW).attr('y2', arrowY(1)-28) | |
| .attr('stroke', rows[1].color).attr('stroke-width', 4).attr('marker-end','url(#arrow)'); | |
| // Backward arrow (orange, reversed) | |
| gArrows.append('line').attr('x1', x(numBlocks-1)+blockW).attr('y1', arrowY(2)-20).attr('x2', x(0)).attr('y2', arrowY(2)-20) | |
| .attr('stroke', rows[2].color).attr('stroke-width', 4).attr('marker-end','url(#arrow)'); | |
| // Vertical arrows (gradients down, updated up) | |
| const midX = x(3) + blockW/2; | |
| gArrows.append('line').attr('x1', midX).attr('y1', arrowY(2)+blockH/2+4).attr('x2', midX).attr('y2', arrowY(3)-blockH/2-6) | |
| .attr('stroke', rows[3].color).attr('stroke-width', 3).attr('marker-end','url(#arrow)'); | |
| gArrows.append('line').attr('x1', midX).attr('y1', arrowY(4)+blockH/2+6).attr('x2', midX).attr('y2', arrowY(5)-blockH/2-6) | |
| .attr('stroke', rows[5].color).attr('stroke-width', 3).attr('marker-end','url(#arrow)'); | |
| } | |
| render(); | |
| if (window.ResizeObserver) { const ro = new ResizeObserver(()=>render()); ro.observe(container); } else { window.addEventListener('resize', render); } | |
| }; | |
| if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', () => ensureD3(bootstrap), { once: true }); } else { ensureD3(bootstrap); } | |
| })(); | |
| </script> | |