Skip to content

Commit ca1f165

Browse files
committed
add topic link
1 parent efd73dd commit ca1f165

1 file changed

Lines changed: 224 additions & 15 deletions

File tree

src/views/EdgePanel.tsx

Lines changed: 224 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@ import { FaTimes } from "react-icons/fa";
44
import Slider from "rc-slider";
55

66
import { GraphContext } from "../lib/context";
7+
import { EdgeData } from "../lib/data";
8+
import { DEFAULT_EDGE_COLOR } from "../lib/consts";
79

810
const 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

Comments
 (0)