feat: improve causal graph readability — scale positions, bigger fonts, less overlap
- Scale node positions by 1.55x to spread graph vertically/horizontally - Increase node size: 120x42 -> 130x48 (normal), 130x50 -> 140x54 (editor) - Bigger label font: 11px -> 12px inside nodes - Edge labels: 8.5px -> 9.5px, lag/decay: 8px -> 9px - Better label stagger along bezier: lag at t=0.13, label at t=0.42, decay at t=0.84 - Larger perpendicular offset: 10px -> 16px (less overlap on diagonal edges) - SVG height: 480 -> 560 (normal), 540 -> 620 (editor) - Use scaledNodes array for node rendering to apply position transform Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -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<SVGSVGElement>) {
|
||||
@@ -217,7 +208,7 @@ export function GraphSVG({
|
||||
|
||||
return (
|
||||
<svg ref={svgRef} viewBox={`${minX} ${minY} ${W} ${H}`}
|
||||
style={{ width: '100%', height: compact ? 280 : editorMode ? 540 : 480 }}
|
||||
style={{ width: '100%', height: compact ? 280 : editorMode ? 620 : 560 }}
|
||||
className={clsx('overflow-visible', editorMode && 'cursor-crosshair')}
|
||||
onClick={handleSvgClick}>
|
||||
<defs>
|
||||
@@ -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 (
|
||||
<g key={i}>
|
||||
<path d={edgePath(e)} fill="none"
|
||||
@@ -262,24 +253,24 @@ export function GraphSVG({
|
||||
markerEnd={`url(#arrow-${sign})`} opacity={0.85} />
|
||||
{lpos && (
|
||||
<g>
|
||||
<rect x={lpos.x - 2} y={lpos.y - 9} width={labelW + 4} height={11}
|
||||
rx={3} fill="#0c1220" opacity={0.82} />
|
||||
<text x={lpos.x} y={lpos.y} fill={color} fontSize={8.5} fontWeight="500"
|
||||
<rect x={lpos.x - 3} y={lpos.y - 10} width={labelW + 6} height={13}
|
||||
rx={3} fill="#0c1220" opacity={0.88} />
|
||||
<text x={lpos.x} y={lpos.y} fill={color} fontSize={9.5} fontWeight="500"
|
||||
letterSpacing="0.2">{labelText}</text>
|
||||
</g>
|
||||
)}
|
||||
{lagPos && lagTxt && (
|
||||
<g>
|
||||
<rect x={lagPos.x - 2} y={lagPos.y - 9} width={lagTxt.length * 5 + 4} height={11}
|
||||
rx={3} fill="#0c1220" opacity={0.85} />
|
||||
<text x={lagPos.x} y={lagPos.y} fill="#fbbf24" fontSize={8} fontWeight="600">{lagTxt}</text>
|
||||
<rect x={lagPos.x - 3} y={lagPos.y - 10} width={lagTxt.length * 5.8 + 6} height={13}
|
||||
rx={3} fill="#0c1220" opacity={0.88} />
|
||||
<text x={lagPos.x} y={lagPos.y} fill="#fbbf24" fontSize={9} fontWeight="600">{lagTxt}</text>
|
||||
</g>
|
||||
)}
|
||||
{decayPos && decayTxt && (
|
||||
<g>
|
||||
<rect x={decayPos.x - 2} y={decayPos.y - 9} width={decayTxt.length * 5 + 4} height={11}
|
||||
rx={3} fill="#0c1220" opacity={0.85} />
|
||||
<text x={decayPos.x} y={decayPos.y} fill="#94a3b8" fontSize={8} fontWeight="600">{decayTxt}</text>
|
||||
<rect x={decayPos.x - 3} y={decayPos.y - 10} width={decayTxt.length * 5.8 + 6} height={13}
|
||||
rx={3} fill="#0c1220" opacity={0.88} />
|
||||
<text x={decayPos.x} y={decayPos.y} fill="#94a3b8" fontSize={9} fontWeight="600">{decayTxt}</text>
|
||||
</g>
|
||||
)}
|
||||
</g>
|
||||
@@ -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 (
|
||||
<g key={n.id} transform={`translate(${n.x - NW / 2},${n.y - NH / 2})`}
|
||||
|
||||
Reference in New Issue
Block a user