1+ import { useEffect , useRef , useState } from "react" ;
2+ import cytoscape from "cytoscape" ;
3+
4+ interface GraphEdge {
5+ source : string ;
6+ target : string ;
7+ weight : number ;
8+ }
9+
10+ interface HeatmapGraphProps {
11+ heatmap : Record < string , number > ;
12+ nodeTypes : Record < string , "Drone" | "Client" | "Server" > ;
13+ }
14+
15+ const getCssVariableAsRGB = ( variable : string ) => {
16+ const hsl = getComputedStyle ( document . documentElement ) . getPropertyValue ( variable ) . trim ( ) ;
17+ return hslToRgb ( hsl ) ;
18+ } ;
19+
20+ const hslToRgb = ( hsl : string ) => {
21+ const match = hsl . match ( / ( \d + ) , ? \s * ( \d + ) % ? , ? \s * ( \d + ) % ? / ) ;
22+ if ( ! match ) return "rgb(0, 0, 0)" ; // Valore di default in caso di errore
23+
24+ let [ h , s , l ] = match . slice ( 1 , 4 ) . map ( Number ) ;
25+ s /= 100 ;
26+ l /= 100 ;
27+
28+ const k = ( n : number ) => ( n + h / 30 ) % 12 ;
29+ const a = s * Math . min ( l , 1 - l ) ;
30+ const f = ( n : number ) => l - a * Math . max ( - 1 , Math . min ( k ( n ) - 3 , Math . min ( 9 - k ( n ) , 1 ) ) ) ;
31+
32+ return `rgb(${ Math . round ( f ( 0 ) * 255 ) } , ${ Math . round ( f ( 8 ) * 255 ) } , ${ Math . round ( f ( 4 ) * 255 ) } )` ;
33+ } ;
34+
35+ const HeatmapGraph = ( { heatmap, nodeTypes } : HeatmapGraphProps ) => {
36+ const cyRef = useRef < HTMLDivElement > ( null ) ;
37+ const cyInstance = useRef < cytoscape . Core | null > ( null ) ;
38+ const [ prevGraph , setPrevGraph ] = useState < { nodes : Set < string > ; edges : string [ ] } | null > ( null ) ;
39+
40+ const nodes = new Set < string > ( ) ;
41+ const edgesMap = new Map < string , GraphEdge > ( ) ;
42+
43+ Object . entries ( heatmap ) . forEach ( ( [ key , weight ] ) => {
44+ let [ src , dest ] = key . split ( "," ) ;
45+ if ( ! src || ! dest ) return ;
46+
47+ if ( src > dest ) [ src , dest ] = [ dest , src ] ;
48+
49+ nodes . add ( src ) ;
50+ nodes . add ( dest ) ;
51+
52+ const edgeKey = `${ src } -${ dest } ` ;
53+ if ( edgesMap . has ( edgeKey ) ) {
54+ edgesMap . get ( edgeKey ) ! . weight += weight ;
55+ } else {
56+ edgesMap . set ( edgeKey , { source : src , target : dest , weight } ) ;
57+ }
58+ } ) ;
59+
60+ const edges = Array . from ( edgesMap . values ( ) ) ;
61+
62+ const maxWeight = Math . max ( ...edges . map ( ( e ) => e . weight ) , 1 ) ;
63+ const minWeight = Math . min ( ...edges . map ( ( e ) => e . weight ) , maxWeight ) ;
64+
65+ useEffect ( ( ) => {
66+ if ( ! cyRef . current ) return ;
67+
68+ const primaryColor = getCssVariableAsRGB ( "--primary" ) ;
69+ const newGraph = {
70+ nodes : new Set ( nodes ) ,
71+ edges : edges . map ( ( e ) => `${ e . source } -${ e . target } ` ) ,
72+ } ;
73+
74+ if (
75+ prevGraph &&
76+ newGraph . nodes . size === prevGraph . nodes . size &&
77+ newGraph . edges . length === prevGraph . edges . length &&
78+ newGraph . edges . every ( ( edge ) => prevGraph . edges . includes ( edge ) )
79+ ) {
80+ edges . forEach ( ( edge ) => {
81+ const cyEdge = cyInstance . current ?. edges ( `[source="${ edge . source } "][target="${ edge . target } "]` ) ;
82+ if ( cyEdge ) {
83+ cyEdge . style ( {
84+ width : normalizeEdgeWeight ( edge . weight , minWeight , maxWeight ) ,
85+ "line-color" : primaryColor ,
86+ } ) ;
87+ }
88+ } ) ;
89+ return ;
90+ }
91+
92+ cyInstance . current ?. destroy ( ) ;
93+
94+ cyInstance . current = cytoscape ( {
95+ container : cyRef . current ,
96+ elements : [
97+ ...Array . from ( nodes ) . map ( ( id ) => ( {
98+ data : { id, label : id , type : nodeTypes [ id ] || "Client" } ,
99+ } ) ) ,
100+ ...edges . map ( ( edge ) => ( {
101+ data : { source : edge . source , target : edge . target , weight : edge . weight } ,
102+ } ) ) ,
103+ ] ,
104+ style : [
105+ {
106+ selector : "node" ,
107+ style : {
108+ label : "data(label)" ,
109+ "text-valign" : "center" ,
110+ "text-halign" : "center" ,
111+ "background-color" : ( ele ) => getNodeColor ( ele . data ( "type" ) ) ,
112+ "border-width" : 2 ,
113+ "border-color" : ( ele ) => getNodeBorderColor ( ele . data ( "type" ) ) ,
114+ color : "#000" ,
115+ "font-size" : "12px" ,
116+ "font-weight" : "bold" ,
117+ width : 24 ,
118+ height : 24 ,
119+ shape : "ellipse" ,
120+ "events" : "no"
121+ } ,
122+ } ,
123+ {
124+ selector : "edge" ,
125+ style : {
126+ "line-color" : primaryColor ,
127+ width : ( ele : cytoscape . NodeSingular ) => normalizeEdgeWeight ( ele . data ( "weight" ) , minWeight , maxWeight ) ,
128+ "curve-style" : "bezier" ,
129+ "events" : "no"
130+ } ,
131+ } ,
132+ ] ,
133+ layout : {
134+ name : "cose" ,
135+ fit : true ,
136+ padding : 40 ,
137+ } ,
138+ userZoomingEnabled : false ,
139+ userPanningEnabled : false ,
140+ boxSelectionEnabled : false ,
141+ } ) ;
142+
143+ setPrevGraph ( newGraph ) ;
144+ } , [ heatmap , nodeTypes ] ) ;
145+
146+ const normalizeEdgeWeight = ( weight : number , minW : number , maxW : number ) : number => {
147+ if ( maxW === minW ) return 3 ;
148+ return 1 + ( ( weight - minW ) / ( maxW - minW ) ) * 5 ; // Normalizza tra 1 e 6
149+ } ;
150+
151+ const getNodeColor = ( type : string ) : string => {
152+ const colors : Record < "Server" | "Drone" | "Client" , string > = {
153+ Server : "#FEFAF4" ,
154+ Drone : "#F5FAFA" ,
155+ Client : "#F9FBF6" ,
156+ } ;
157+ // QUi tutti sono Client
158+ return colors [ type as keyof typeof colors ] || "#ddd" ;
159+ } ;
160+
161+ // 🎨 **Colori dei bordi**
162+ const getNodeBorderColor = ( type : string ) : string => {
163+ const borderColors : Record < "Server" | "Drone" | "Client" , string > = {
164+ Server : "#EDCB95" ,
165+ Drone : "#9ACDC8" ,
166+ Client : "#C3D59D" ,
167+ } ;
168+ return borderColors [ type as keyof typeof borderColors ] || "#aaa" ;
169+ } ;
170+
171+ return < div ref = { cyRef } className = "w-full h-[400px]" > </ div > ;
172+ } ;
173+
174+ export default HeatmapGraph ;
0 commit comments