| | <div class="d3-train-diagram" style="width:100%;margin:10px 0;"></div> |
| | <div class="caption">Hover blocks to show an explanation.</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'; } |
| | |
| | |
| | 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: 'Each block represents a submodule of the model.', |
| | forward: 'Forward pass: compute activations layer by layer.', |
| | backward: 'Backpropagation: compute gradients through the chain.', |
| | gradients: 'Gradient accumulators for each layer.', |
| | optimization: 'Optimization step: update the weights.', |
| | updated: 'Parameters updated, ready for the next iteration.' |
| | }; |
| | |
| | |
| | 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'); |
| | |
| | |
| | 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; } |
| | |
| | |
| | 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(); |
| | |
| | |
| | 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> |
| | `); |
| | |
| | |
| | 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); |
| | |
| | |
| | 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); }) |
| | ); |
| | |
| | |
| | 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); |
| | |
| | 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)'); |
| | |
| | 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)'); |
| | |
| | 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)'); |
| | |
| | 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> |
| |
|
| |
|
| |
|