@@ -10,10 +10,10 @@ interface CallGraphProps {
1010
1111const RENDER_THRESHOLD = 100 ;
1212
13- const NODE_COLORS : Record < string , { bg : string ; border : string } > = {
14- function : { bg : "#dbeafe" , border : "#3b82f6" } ,
15- storage : { bg : "#fef3c7" , border : "#f59e0b" } ,
16- external : { bg : "#f3e8ff" , border : "#a855f7" } ,
13+ const NODE_COLORS : Record < string , { bg : string ; border : string ; dark : string } > = {
14+ function : { bg : "#dbeafe" , border : "#3b82f6" , dark : "#1e3a5f" } ,
15+ storage : { bg : "#fef3c7" , border : "#f59e0b" , dark : "#3d2e00" } ,
16+ external : { bg : "#f3e8ff" , border : "#a855f7" , dark : "#2e1a47" } ,
1717} ;
1818
1919const SEVERITY_RING : Record < string , string > = {
@@ -23,10 +23,12 @@ const SEVERITY_RING: Record<string, string> = {
2323 low : "#6b7280" ,
2424} ;
2525
26- const EDGE_COLORS : Record < string , string > = {
27- calls : "#6b7280" ,
28- mutates : "#ef4444" ,
29- reads : "#3b82f6" ,
26+ /** Visual properties for each edge type. */
27+ const EDGE_STYLE : Record < string , { color : string ; dash ?: string ; label : string } > = {
28+ internal : { color : "#10b981" , label : "Internal call" } ,
29+ calls : { color : "#a855f7" , dash : "6 3" , label : "External call" } ,
30+ mutates : { color : "#ef4444" , label : "Mutates" } ,
31+ reads : { color : "#3b82f6" , label : "Reads" } ,
3032} ;
3133
3234interface LayoutNode extends CallGraphNode {
@@ -70,7 +72,7 @@ export const CallGraph = memo(function CallGraph({ nodes, edges }: CallGraphProp
7072 return (
7173 < div className = "rounded-lg border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-6" >
7274 < h3 className = "text-sm font-semibold text-zinc-700 dark:text-zinc-300 mb-4" >
73- Contract Call Graph
75+ Contract Interaction Graph
7476 </ h3 >
7577 < p className = "text-sm text-zinc-500 dark:text-zinc-400 text-center py-8" >
7678 No cross-contract call paths were reported for this scan.
@@ -90,12 +92,24 @@ export const CallGraph = memo(function CallGraph({ nodes, edges }: CallGraphProp
9092 const nodeWidth = 140 ;
9193 const nodeHeight = 40 ;
9294
95+ const internalCount = edges . filter ( ( e ) => e . type === "internal" ) . length ;
96+ const externalCount = edges . filter ( ( e ) => e . type === "calls" ) . length ;
97+
9398 return (
9499 < div className = "rounded-lg border border-zinc-200 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-6" >
95- < div className = "flex justify-between items-center mb-4" >
96- < h3 className = "text-sm font-semibold text-zinc-700 dark:text-zinc-300" >
97- Contract Call Graph ({ nodes . length } nodes)
98- </ h3 >
100+ < div className = "flex flex-wrap justify-between items-start gap-2 mb-4" >
101+ < div >
102+ < h3 className = "text-sm font-semibold text-zinc-700 dark:text-zinc-300" >
103+ Contract Interaction Graph
104+ </ h3 >
105+ < p className = "text-xs text-zinc-500 dark:text-zinc-400 mt-0.5" >
106+ { nodes . length } contract{ nodes . length !== 1 ? "s" : "" }
107+ { " · " }
108+ < span className = "text-emerald-600 dark:text-emerald-400" > { internalCount } internal</ span >
109+ { " · " }
110+ < span className = "text-purple-600 dark:text-purple-400" > { externalCount } external</ span >
111+ </ p >
112+ </ div >
99113 </ div >
100114
101115 { ! shouldRender ? (
@@ -112,43 +126,63 @@ export const CallGraph = memo(function CallGraph({ nodes, edges }: CallGraphProp
112126 </ div >
113127 ) : (
114128 < >
115- < div className = "flex flex-wrap gap-x-4 gap-y-2 mb-4 text-[10px] sm:text-xs text-zinc-500 dark:text-zinc-400" >
116- < span className = "flex items-center gap-1" >
117- < span className = "inline-block w-3 h-3 rounded" style = { { background : NODE_COLORS . function . bg , border : `2px solid ${ NODE_COLORS . function . border } ` } } />
118- Function
119- </ span >
120- < span className = "flex items-center gap-1" >
121- < span className = "inline-block w-3 h-3 rounded" style = { { background : NODE_COLORS . storage . bg , border : `2px solid ${ NODE_COLORS . storage . border } ` } } />
122- Storage
123- </ span >
124- < span className = "flex items-center gap-1" >
125- < span className = "inline-block w-2 h-0.5" style = { { background : EDGE_COLORS . mutates } } />
126- Mutates
127- </ span >
128- < span className = "flex items-center gap-1" >
129- < span className = "inline-block w-2 h-0.5" style = { { background : EDGE_COLORS . calls } } />
130- Calls
131- </ span >
129+ { /* Legend */ }
130+ < div className = "flex flex-wrap gap-x-5 gap-y-2 mb-4 text-[10px] sm:text-xs text-zinc-500 dark:text-zinc-400" >
131+ < span className = "flex items-center gap-1.5 font-medium text-zinc-600 dark:text-zinc-300" > Nodes:</ span >
132+ { ( [ "function" , "storage" , "external" ] as const ) . map ( ( type ) => (
133+ < span key = { type } className = "flex items-center gap-1" >
134+ < span
135+ className = "inline-block w-3 h-3 rounded"
136+ style = { {
137+ background : NODE_COLORS [ type ] . bg ,
138+ border : `2px solid ${ NODE_COLORS [ type ] . border } ` ,
139+ } }
140+ />
141+ { type . charAt ( 0 ) . toUpperCase ( ) + type . slice ( 1 ) }
142+ </ span >
143+ ) ) }
144+ < span className = "flex items-center gap-1.5 font-medium text-zinc-600 dark:text-zinc-300 ml-2" > Edges:</ span >
145+ { ( [ "internal" , "calls" , "mutates" , "reads" ] as const ) . map ( ( type ) => {
146+ const style = EDGE_STYLE [ type ] ;
147+ return (
148+ < span key = { type } className = "flex items-center gap-1" >
149+ < svg width = "20" height = "8" aria-hidden = "true" >
150+ < line
151+ x1 = "0" y1 = "4" x2 = "20" y2 = "4"
152+ stroke = { style . color }
153+ strokeWidth = { 2 }
154+ strokeDasharray = { style . dash }
155+ />
156+ </ svg >
157+ { style . label }
158+ </ span >
159+ ) ;
160+ } ) }
132161 </ div >
162+
133163 < div className = "overflow-auto max-h-[600px]" >
134164 < svg
135165 width = { svgWidth }
136166 height = { svgHeight }
137167 viewBox = { `0 0 ${ svgWidth } ${ svgHeight } ` }
138168 className = "bg-zinc-50 dark:bg-zinc-950 rounded"
139169 role = "img"
140- aria-label = "Contract call graph visualization"
170+ aria-label = "Contract interaction graph visualization"
141171 >
142172 < defs >
143- < marker id = "arrowhead-mutates" markerWidth = "8" markerHeight = "6" refX = "8" refY = "3" orient = "auto" >
144- < polygon points = "0 0, 8 3, 0 6" fill = { EDGE_COLORS . mutates } />
145- </ marker >
146- < marker id = "arrowhead-calls" markerWidth = "8" markerHeight = "6" refX = "8" refY = "3" orient = "auto" >
147- < polygon points = "0 0, 8 3, 0 6" fill = { EDGE_COLORS . calls } />
148- </ marker >
149- < marker id = "arrowhead-reads" markerWidth = "8" markerHeight = "6" refX = "8" refY = "3" orient = "auto" >
150- < polygon points = "0 0, 8 3, 0 6" fill = { EDGE_COLORS . reads } />
151- </ marker >
173+ { ( [ "internal" , "calls" , "mutates" , "reads" ] as const ) . map ( ( type ) => (
174+ < marker
175+ key = { type }
176+ id = { `arrowhead-${ type } ` }
177+ markerWidth = "8"
178+ markerHeight = "6"
179+ refX = "8"
180+ refY = "3"
181+ orient = "auto"
182+ >
183+ < polygon points = "0 0, 8 3, 0 6" fill = { EDGE_STYLE [ type ] . color } />
184+ </ marker >
185+ ) ) }
152186 </ defs >
153187
154188 { edges . map ( ( edge , i ) => {
@@ -160,24 +194,43 @@ export const CallGraph = memo(function CallGraph({ nodes, edges }: CallGraphProp
160194 const y1 = source . y + nodeHeight / 2 ;
161195 const x2 = target . x ;
162196 const y2 = target . y + nodeHeight / 2 ;
163- const color = EDGE_COLORS [ edge . type ] || EDGE_COLORS . calls ;
197+ const style = EDGE_STYLE [ edge . type ] ?? EDGE_STYLE . calls ;
164198
165- return (
199+ // Curved path for internal edges to distinguish from straight external ones.
200+ const isCurved = edge . type === "internal" ;
201+ const midX = ( x1 + x2 ) / 2 ;
202+ const midY = ( y1 + y2 ) / 2 - 30 ;
203+ const d = isCurved
204+ ? `M ${ x1 } ${ y1 } Q ${ midX } ${ midY } ${ x2 } ${ y2 } `
205+ : undefined ;
206+
207+ return isCurved ? (
208+ < path
209+ key = { `edge-${ i } ` }
210+ d = { d }
211+ fill = "none"
212+ stroke = { style . color }
213+ strokeWidth = { 2 }
214+ strokeDasharray = { style . dash }
215+ markerEnd = { `url(#arrowhead-${ edge . type } )` }
216+ />
217+ ) : (
166218 < line
167219 key = { `edge-${ i } ` }
168220 x1 = { x1 }
169221 y1 = { y1 }
170222 x2 = { x2 }
171223 y2 = { y2 }
172- stroke = { color }
224+ stroke = { style . color }
173225 strokeWidth = { 2 }
226+ strokeDasharray = { style . dash }
174227 markerEnd = { `url(#arrowhead-${ edge . type } )` }
175228 />
176229 ) ;
177230 } ) }
178231
179232 { layout . map ( ( node ) => {
180- const colors = NODE_COLORS [ node . type ] || NODE_COLORS . function ;
233+ const colors = NODE_COLORS [ node . type ] ?? NODE_COLORS . function ;
181234 const severityColor = node . severity ? SEVERITY_RING [ node . severity ] : undefined ;
182235
183236 return (
@@ -213,7 +266,7 @@ export const CallGraph = memo(function CallGraph({ nodes, edges }: CallGraphProp
213266 fontWeight = { 600 }
214267 fill = "#1f2937"
215268 >
216- { node . label . length > 16 ? node . label . slice ( 0 , 14 ) + "... " : node . label }
269+ { node . label . length > 16 ? node . label . slice ( 0 , 14 ) + "… " : node . label }
217270 </ text >
218271 </ g >
219272 ) ;
0 commit comments