@@ -4,12 +4,14 @@ import { FaTimes } from "react-icons/fa";
44import Slider from "rc-slider" ;
55
66import { GraphContext } from "../lib/context" ;
7+ import { EdgeData } from "../lib/data" ;
8+ import { DEFAULT_EDGE_COLOR } from "../lib/consts" ;
79
810const EdgePanel : FC < { isExpanded : boolean } > = ( { isExpanded } ) => {
9- const { navState, setNavState, setShowEdgePanel } = useContext ( GraphContext ) ;
11+ const { navState, setNavState, setShowEdgePanel, data , graphFile } = useContext ( GraphContext ) ;
1012
1113 // State for edge creation criteria
12- const [ topicThreshold , setTopicThreshold ] = useState ( 2 ) ;
14+ const [ topicThreshold , setTopicThreshold ] = useState ( 1 ) ; // Start with 1 shared topic as default
1315 const [ contributorThreshold , setContributorThreshold ] = useState ( 1 ) ;
1416 const [ stargazerThreshold , setStargazerThreshold ] = useState ( 5 ) ;
1517 const [ enableTopicLinking , setEnableTopicLinking ] = useState ( false ) ;
@@ -18,6 +20,225 @@ const EdgePanel: FC<{ isExpanded: boolean }> = ({ isExpanded }) => {
1820 const [ enableCommonStargazers , setEnableCommonStargazers ] = useState ( false ) ;
1921 const [ enableDependencies , setEnableDependencies ] = useState ( false ) ;
2022
23+ // Function to remove existing topic-based edges
24+ const removeExistingTopicEdges = ( ) => {
25+ if ( ! data || ! data . graph ) return 0 ;
26+
27+ const { graph } = data ;
28+ const edgesToRemove : string [ ] = [ ] ;
29+
30+ graph . forEachEdge ( ( edge , attributes ) => {
31+ if ( attributes . attributes ?. edgeType === 'topic-based' ) {
32+ edgesToRemove . push ( edge ) ;
33+ }
34+ } ) ;
35+
36+ edgesToRemove . forEach ( edge => {
37+ graph . dropEdge ( edge ) ;
38+ } ) ;
39+
40+ console . log ( `Removed ${ edgesToRemove . length } existing topic-based edges` ) ;
41+ return edgesToRemove . length ;
42+ } ;
43+
44+ // Function to create topic-based edges
45+ const createTopicBasedEdges = ( ) => {
46+ if ( ! data || ! data . graph ) return ;
47+
48+ const { graph } = data ;
49+ const nodes = graph . nodes ( ) ;
50+ const edgesCreated : Array < { source : string ; target : string ; sharedTopics : string [ ] } > = [ ] ;
51+
52+ console . log ( `Checking ${ nodes . length } nodes for topic-based edges with threshold: ${ topicThreshold } ` ) ;
53+
54+ // Get all nodes with their topics and filter out nodes without topics
55+ const nodeTopics : Record < string , string [ ] > = { } ;
56+ const nodesWithTopics : string [ ] = [ ] ;
57+
58+ nodes . forEach ( nodeId => {
59+ const nodeData = graph . getNodeAttributes ( nodeId ) ;
60+ const topics = nodeData . attributes ?. topics ;
61+ if ( topics && typeof topics === 'string' ) {
62+ const topicArray = topics . split ( '|' ) . map ( t => t . trim ( ) ) . filter ( Boolean ) ;
63+ if ( topicArray . length > 0 ) {
64+ nodeTopics [ nodeId ] = topicArray ;
65+ nodesWithTopics . push ( nodeId ) ;
66+ }
67+ }
68+ } ) ;
69+
70+ console . log ( `Found ${ nodesWithTopics . length } nodes with topics out of ${ nodes . length } total nodes` ) ;
71+
72+ // Early exit if not enough nodes with topics
73+ if ( nodesWithTopics . length < 2 ) {
74+ console . log ( 'Not enough nodes with topics to create edges' ) ;
75+ return edgesCreated ;
76+ }
77+
78+ // Create a reverse index: topic -> list of nodes that have this topic
79+ const topicToNodes : Record < string , string [ ] > = { } ;
80+ nodesWithTopics . forEach ( nodeId => {
81+ const topics = nodeTopics [ nodeId ] ;
82+ topics . forEach ( topic => {
83+ if ( ! topicToNodes [ topic ] ) {
84+ topicToNodes [ topic ] = [ ] ;
85+ }
86+ topicToNodes [ topic ] . push ( nodeId ) ;
87+ } ) ;
88+ } ) ;
89+
90+ // Find nodes that share topics
91+ const processedPairs = new Set < string > ( ) ;
92+
93+ Object . entries ( topicToNodes ) . forEach ( ( [ , nodeList ] ) => {
94+ // Only process topics that have enough nodes to potentially meet the threshold
95+ if ( nodeList . length >= 2 ) {
96+ // Check all pairs of nodes that share this topic
97+ for ( let i = 0 ; i < nodeList . length ; i ++ ) {
98+ for ( let j = i + 1 ; j < nodeList . length ; j ++ ) {
99+ const node1 = nodeList [ i ] ;
100+ const node2 = nodeList [ j ] ;
101+
102+ // Create a unique key for this pair to avoid duplicates
103+ const pairKey = node1 < node2 ? `${ node1 } -${ node2 } ` : `${ node2 } -${ node1 } ` ;
104+
105+ if ( processedPairs . has ( pairKey ) ) continue ;
106+ processedPairs . add ( pairKey ) ;
107+
108+ // Find all common topics between these two nodes
109+ const topics1 = nodeTopics [ node1 ] ;
110+ const topics2 = nodeTopics [ node2 ] ;
111+ const commonTopics = topics1 . filter ( t => topics2 . includes ( t ) ) ;
112+
113+ // Only create edge if we meet the threshold
114+ if ( commonTopics . length >= topicThreshold ) {
115+ // Check if edge already exists
116+ if ( ! graph . hasEdge ( node1 , node2 ) ) {
117+ // Create edge attributes
118+ const edgeAttributes : EdgeData = {
119+ size : 2 ,
120+ rawSize : 2 ,
121+ color : DEFAULT_EDGE_COLOR ,
122+ rawColor : DEFAULT_EDGE_COLOR ,
123+ label : `Shared topics: ${ commonTopics . join ( ', ' ) } ` ,
124+ directed : false ,
125+ hidden : false ,
126+ type : undefined ,
127+ attributes : {
128+ sharedTopics : commonTopics . join ( '|' ) ,
129+ edgeType : 'topic-based' ,
130+ topicCount : commonTopics . length
131+ }
132+ } ;
133+
134+ // Add undirected edge
135+ const edgeKey = `topic_edge_${ node1 } _${ node2 } ` ;
136+ graph . addUndirectedEdgeWithKey ( edgeKey , node1 , node2 , edgeAttributes ) ;
137+
138+ edgesCreated . push ( {
139+ source : node1 ,
140+ target : node2 ,
141+ sharedTopics : commonTopics
142+ } ) ;
143+ }
144+ }
145+ }
146+ }
147+ }
148+ } ) ;
149+
150+ console . log ( `=== Topic-based edge creation summary ===` ) ;
151+ console . log ( `Threshold: ${ topicThreshold } shared topics` ) ;
152+ console . log ( `Nodes with topics: ${ nodesWithTopics . length } ` ) ;
153+ console . log ( `Edges created: ${ edgesCreated . length } ` ) ;
154+ console . log ( `========================================` ) ;
155+
156+ return edgesCreated ;
157+ } ;
158+
159+ // Function to update GEXF content with new edges
160+ const updateGexfContent = ( edgesCreated : Array < { source : string ; target : string ; sharedTopics : string [ ] } > ) => {
161+ if ( ! graphFile || ! edgesCreated . length ) return ;
162+
163+ // Parse the existing GEXF content
164+ const parser = new DOMParser ( ) ;
165+ const xmlDoc = parser . parseFromString ( graphFile . textContent , "text/xml" ) ;
166+
167+ // Find the graph element
168+ const graphElement = xmlDoc . querySelector ( 'graph' ) ;
169+ if ( ! graphElement ) return ;
170+
171+ // Add edges to the GEXF
172+ edgesCreated . forEach ( ( edge ) => {
173+ const edgeElement = xmlDoc . createElement ( 'edge' ) ;
174+ edgeElement . setAttribute ( 'id' , `topic_edge_${ edge . source } _${ edge . target } ` ) ;
175+ edgeElement . setAttribute ( 'source' , edge . source ) ;
176+ edgeElement . setAttribute ( 'target' , edge . target ) ;
177+ edgeElement . setAttribute ( 'weight' , '1' ) ;
178+
179+ // Add edge attributes
180+ const attrsElement = xmlDoc . createElement ( 'attvalues' ) ;
181+
182+ // Shared topics attribute
183+ const topicAttr = xmlDoc . createElement ( 'attvalue' ) ;
184+ topicAttr . setAttribute ( 'for' , 'sharedTopics' ) ;
185+ topicAttr . setAttribute ( 'value' , edge . sharedTopics . join ( '|' ) ) ;
186+ attrsElement . appendChild ( topicAttr ) ;
187+
188+ // Shared topics attribute
189+ const typeAttr = xmlDoc . createElement ( 'attvalue' ) ;
190+ typeAttr . setAttribute ( 'for' , 'edgeType' ) ;
191+ typeAttr . setAttribute ( 'value' , 'topic-based' ) ;
192+ attrsElement . appendChild ( typeAttr ) ;
193+
194+ edgeElement . appendChild ( attrsElement ) ;
195+ graphElement . appendChild ( edgeElement ) ;
196+ } ) ;
197+
198+ // Convert back to string
199+ const serializer = new XMLSerializer ( ) ;
200+ serializer . serializeToString ( xmlDoc ) ;
201+
202+ // Update the graphFile context by triggering a navState change
203+ // This will force the graph to refresh
204+ setNavState ( { ...navState , role : navState . role } ) ;
205+ } ;
206+
207+ // Handle apply button click
208+ const handleApplyEdgeCreation = ( ) => {
209+ if ( enableTopicLinking ) {
210+ // First remove any existing topic-based edges
211+ const removedCount = removeExistingTopicEdges ( ) ;
212+ console . log ( `Removed ${ removedCount } existing topic-based edges` ) ;
213+
214+ // Then create new edges based on current threshold
215+ const edgesCreated = createTopicBasedEdges ( ) ;
216+ if ( edgesCreated && edgesCreated . length > 0 ) {
217+ console . log ( `Created ${ edgesCreated . length } topic-based edges:` , edgesCreated ) ;
218+ updateGexfContent ( edgesCreated ) ;
219+
220+ // Force graph refresh by updating navState
221+ setNavState ( { ...navState , role : navState . role } ) ;
222+ } else {
223+ console . log ( "No new topic-based edges were created" ) ;
224+ }
225+ }
226+
227+ // Log all criteria for debugging
228+ console . log ( "Creating edges with criteria:" , {
229+ topicThreshold,
230+ contributorThreshold,
231+ stargazerThreshold,
232+ enableTopicLinking,
233+ enableContributorOverlap,
234+ enableSharedOrganization,
235+ enableCommonStargazers,
236+ enableDependencies
237+ } ) ;
238+ } ;
239+
240+
241+
21242 return (
22243 < section
23244 className = { cx (
@@ -199,19 +420,7 @@ const EdgePanel: FC<{ isExpanded: boolean }> = ({ isExpanded }) => {
199420 < div className = "mb-3" >
200421 < button
201422 className = "btn btn-light w-100 text-center"
202- onClick = { ( ) => {
203- // Here you would implement the logic to create edges based on the selected criteria
204- console . log ( "Creating edges with criteria:" , {
205- topicThreshold,
206- contributorThreshold,
207- stargazerThreshold,
208- enableTopicLinking,
209- enableContributorOverlap,
210- enableSharedOrganization,
211- enableCommonStargazers,
212- enableDependencies
213- } ) ;
214- } }
423+ onClick = { handleApplyEdgeCreation }
215424 >
216425 Apply Edge Creation Rules
217426 </ button >
0 commit comments