@@ -174,7 +174,7 @@ const GraphSearch: FC = () => {
174174const GraphControls : FC = ( ) => {
175175 const sigma = useSigma ( ) ;
176176 const graph = sigma . getGraph ( ) ;
177- const { graphFile } = useContext ( GraphContext ) ;
177+ const { graphFile, navState , hovered , computedData } = useContext ( GraphContext ) ;
178178
179179 const zoom = useCallback (
180180 ( ratio ?: number ) : void => {
@@ -201,17 +201,121 @@ const GraphControls: FC = () => {
201201
202202 const downloadGraph = useCallback ( ( ) => {
203203 if ( graphFile ) {
204- const blob = new Blob ( [ graphFile . textContent ] , { type : "application/xml" } ) ;
204+ // Parse the existing GEXF content
205+ const parser = new DOMParser ( ) ;
206+ const xmlDoc = parser . parseFromString ( graphFile . textContent , "text/xml" ) ;
207+
208+ // Get the graph element
209+ const graphElement = xmlDoc . querySelector ( 'graph' ) ;
210+ if ( ! graphElement ) return ;
211+
212+ // Create sets for highlighted nodes and edges
213+ const highlightedNodesSet = new Set < string > ( ) ;
214+ const highlightedEdgesSet = new Set < string > ( ) ;
215+
216+ // Add selected node
217+ if ( navState . selectedNode ) {
218+ highlightedNodesSet . add ( navState . selectedNode ) ;
219+ }
220+
221+ // Add hovered nodes
222+ if ( typeof hovered === "string" ) {
223+ highlightedNodesSet . add ( hovered ) ;
224+ } else if ( hovered instanceof Set ) {
225+ hovered . forEach ( node => highlightedNodesSet . add ( node ) ) ;
226+ }
227+
228+ // If no nodes are highlighted from context, use filtered nodes (exclude faded ones)
229+ if ( highlightedNodesSet . size === 0 && computedData . filteredNodes ) {
230+ // Use the filtered nodes as the base - these are the active/visible nodes
231+ computedData . filteredNodes . forEach ( node => {
232+ highlightedNodesSet . add ( node ) ;
233+ } ) ;
234+ }
235+
236+ // Only include the specifically highlighted nodes (no neighbors)
237+ const nodesToAdd = new Set < string > ( ) ;
238+ highlightedNodesSet . forEach ( node => {
239+ if ( graph . hasNode ( node ) ) {
240+ nodesToAdd . add ( node ) ;
241+ }
242+ } ) ;
243+
244+ // Add edges where BOTH source AND target nodes are visible (to avoid orphaned edges)
245+ graph . forEachEdge ( ( edge , attributes , source , target ) => {
246+ if ( nodesToAdd . has ( source ) && nodesToAdd . has ( target ) ) {
247+ // Additional check: only include edges that are actually visible in the current filtered view
248+ // This excludes edges that were created but are now hidden due to filtering
249+ if ( computedData . filteredEdges && computedData . filteredEdges . has ( edge ) ) {
250+ highlightedEdgesSet . add ( edge ) ;
251+ } else if ( ! computedData . filteredEdges ) {
252+ // If no filtered edges exist, include all edges between visible nodes
253+ highlightedEdgesSet . add ( edge ) ;
254+ }
255+ }
256+ } ) ;
257+
258+
259+
260+ // If no nodes are highlighted, download the entire graph
261+ if ( nodesToAdd . size === 0 ) {
262+ const blob = new Blob ( [ graphFile . textContent ] , { type : "application/xml" } ) ;
263+ const url = URL . createObjectURL ( blob ) ;
264+ const a = document . createElement ( "a" ) ;
265+ a . href = url ;
266+ a . download = graphFile . name || "graph.gexf" ;
267+ document . body . appendChild ( a ) ;
268+ a . click ( ) ;
269+ document . body . removeChild ( a ) ;
270+ URL . revokeObjectURL ( url ) ;
271+ return ;
272+ }
273+
274+ // Create a new XML document for the filtered graph
275+ const newXmlDoc = parser . parseFromString ( graphFile . textContent , "text/xml" ) ;
276+ const newGraphElement = newXmlDoc . querySelector ( 'graph' ) ;
277+ if ( ! newGraphElement ) return ;
278+
279+ // Remove all existing nodes and edges
280+ const existingNodes = newXmlDoc . querySelectorAll ( 'node' ) ;
281+ const existingEdges = newXmlDoc . querySelectorAll ( 'edge' ) ;
282+
283+ existingNodes . forEach ( node => {
284+ const nodeId = node . getAttribute ( 'id' ) ;
285+ if ( nodeId && ! nodesToAdd . has ( nodeId ) ) {
286+ node . remove ( ) ;
287+ }
288+ } ) ;
289+
290+ existingEdges . forEach ( edge => {
291+ const edgeId = edge . getAttribute ( 'id' ) ;
292+ if ( edgeId && ! highlightedEdgesSet . has ( edgeId ) ) {
293+ edge . remove ( ) ;
294+ }
295+ } ) ;
296+
297+ // Serialize the filtered XML
298+ const serializer = new XMLSerializer ( ) ;
299+ const filteredGexfContent = serializer . serializeToString ( newXmlDoc ) ;
300+
301+ // Create and download the filtered file
302+ const blob = new Blob ( [ filteredGexfContent ] , { type : "application/xml" } ) ;
205303 const url = URL . createObjectURL ( blob ) ;
206304 const a = document . createElement ( "a" ) ;
207305 a . href = url ;
208- a . download = graphFile . name || "graph.gexf" ;
306+
307+ // Create filename with indication that it's filtered
308+ const baseName = graphFile . name || "graph" ;
309+ const extension = graphFile . extension || "gexf" ;
310+ const filteredName = `${ baseName } _highlighted.${ extension } ` ;
311+
312+ a . download = filteredName ;
209313 document . body . appendChild ( a ) ;
210314 a . click ( ) ;
211315 document . body . removeChild ( a ) ;
212316 URL . revokeObjectURL ( url ) ;
213317 }
214- } , [ graphFile ] ) ;
318+ } , [ graphFile , graph , navState . selectedNode , hovered ] ) ;
215319
216320 return (
217321 < >
@@ -233,7 +337,7 @@ const GraphControls: FC = () => {
233337 < FaFileImage />
234338 </ button >
235339
236- < button className = "btn btn-outline-dark graph-button mt-3" onClick = { downloadGraph } title = "Download graph file (.gexf)" >
340+ < button className = "btn btn-outline-dark graph-button mt-3" onClick = { downloadGraph } title = "Download highlighted graph file (.gexf)" >
237341 < FaDownload />
238342 </ button >
239343 </ >
0 commit comments