diff --git a/frontend/src/pages/CausalLab.tsx b/frontend/src/pages/CausalLab.tsx index 1939176..07ecacd 100644 --- a/frontend/src/pages/CausalLab.tsx +++ b/frontend/src/pages/CausalLab.tsx @@ -149,18 +149,22 @@ export function GraphSVG({ const nodes = graph.nodes || [] const edges = graph.edges || [] - const xs = nodes.map(n => n.x) - const ys = nodes.map(n => n.y) - const minX = (xs.length ? Math.min(...xs) : 0) - 60 - const minY = (ys.length ? Math.min(...ys) : 0) - 40 - const maxX = (xs.length ? Math.max(...xs) : 400) + 130 - const maxY = (ys.length ? Math.max(...ys) : 400) + 70 + // Scale positions to reduce overlap between nodes and edge labels + const POS_SCALE = compact ? 1 : 1.55 + const scaledNodes = nodes.map(n => ({ ...n, x: n.x * POS_SCALE, y: n.y * POS_SCALE })) + const nodeMap = Object.fromEntries(scaledNodes.map(n => [n.id, n])) + + const xs = scaledNodes.map(n => n.x) + const ys = scaledNodes.map(n => n.y) + const minX = (xs.length ? Math.min(...xs) : 0) - 80 + const minY = (ys.length ? Math.min(...ys) : 0) - 50 + const maxX = (xs.length ? Math.max(...xs) : 400) + 160 + const maxY = (ys.length ? Math.max(...ys) : 400) + 90 const W = maxX - minX; const H = maxY - minY - const nodeMap = Object.fromEntries(nodes.map(n => [n.id, n])) // Nodes larger in editor mode to accommodate type badge - const NW = compact ? 92 : editorMode ? 130 : 120 - const NH = compact ? 32 : editorMode ? 50 : 42 + const NW = compact ? 92 : editorMode ? 140 : 130 + const NH = compact ? 32 : editorMode ? 54 : 48 const statusColor = (s: string) => s === 'correct' ? '#10b981' : s === 'wrong' ? '#ef4444' : '#64748b' @@ -188,25 +192,12 @@ export function GraphSVG({ const dx = x2 - x1; const dy = y2 - y1; const len = Math.sqrt(dx*dx + dy*dy) || 1 const bx = (1-t0)**3*x1 + 3*(1-t0)**2*t0*x1 + 3*(1-t0)*t0**2*x2 + t0**3*x2 const by = (1-t0)**3*y1 + 3*(1-t0)**2*t0*my + 3*(1-t0)*t0**2*my + t0**3*y2 - return { x: bx + (-dy/len)*10, y: by + (dx/len)*10 } + return { x: bx + (-dy/len)*16, y: by + (dx/len)*16 } } - // Label position: 1/3 of the way from source (avoids node centers) + // Label position along bezier path with perpendicular offset to avoid the line function edgeLabelPos(e: CausalEdge): { x: number; y: number } | null { - const s = nodeMap[e.from]; const t = nodeMap[e.to] - if (!s || !t) return null - // Use 30% along the path to stay near source and avoid crowded midpoints - const t0 = 0.3 - const x1 = s.x; const y1 = s.y + NH / 2 - const x2 = t.x; const y2 = t.y - NH / 2 - const my = (y1 + y2) / 2 - // Cubic bezier at t: C(x1,my), C(x2,my) - const bx = (1-t0)**3*x1 + 3*(1-t0)**2*t0*x1 + 3*(1-t0)*t0**2*x2 + t0**3*x2 - const by = (1-t0)**3*y1 + 3*(1-t0)**2*t0*my + 3*(1-t0)*t0**2*my + t0**3*y2 - // Offset perpendicular to avoid sitting on the line - const dx = x2 - x1; const dy = y2 - y1 - const len = Math.sqrt(dx*dx + dy*dy) || 1 - return { x: bx + (-dy/len)*10, y: by + (dx/len)*10 } + return bezierPt(e, 0.42) } function handleSvgClick(e: React.MouseEvent) { @@ -217,7 +208,7 @@ export function GraphSVG({ return ( @@ -247,13 +238,13 @@ export function GraphSVG({ const sw = edgeWidth(e.strength) const lpos = e.label ? edgeLabelPos(e) : null const labelText = e.label ?? '' - const labelW = labelText.length * 5 + const labelW = labelText.length * 5.8 const lagTxtMin = (!compact && e.lag_min) ? fmtLag(e.lag_min) : null const lagTxtDay = (!compact && e.lag_days) ? `+${e.lag_days}j` : null const lagTxt = lagTxtMin && lagTxtDay ? `${lagTxtMin}/${lagTxtDay}` : (lagTxtMin ?? lagTxtDay) const decayTxt = (!compact && e.decay_days != null) ? fmtDecay(e.decay_days) : null - const lagPos = lagTxt ? bezierPt(e, 0.1) : null - const decayPos = decayTxt ? bezierPt(e, 0.88) : null + const lagPos = lagTxt ? bezierPt(e, 0.13) : null + const decayPos = decayTxt ? bezierPt(e, 0.84) : null return ( {lpos && ( - - + {labelText} )} {lagPos && lagTxt && ( - - {lagTxt} + + {lagTxt} )} {decayPos && decayTxt && ( - - {decayTxt} + + {decayTxt} )} @@ -287,15 +278,15 @@ export function GraphSVG({ })} {/* Nodes */} - {nodes.map(n => { + {scaledNodes.map(n => { const col = getNodeColor(n) const val = nodeValues?.[n.id] const isSel = selectedNodeId === n.id - const maxLen = editorMode ? 20 : 19 - const lFS = compact ? 9 : 11 + const maxLen = editorMode ? 22 : 21 + const lFS = compact ? 9 : 12 // vertical layout inside node - const labelY = compact ? 12 : editorMode ? 15 : 14 - const valY = compact ? 22 : 30 + const labelY = compact ? 12 : editorMode ? 17 : 16 + const valY = compact ? 22 : 34 const typeY = NH - 7 // type badge near bottom return (