thibaud frere
Move assets into content/assets; update imports; clean .gitattributes; fix LFS tracking
b8e1b6c
raw
history blame
9.48 kB
<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>