Skip to content
This repository was archived by the owner on Jun 18, 2026. It is now read-only.

Commit c300ac7

Browse files
feat: GraphML export — standard XML graph format for Gephi, Cytoscape, NetworkX, yEd
Add GraphMLExporter class that converts the JUNG graph to GraphML format, a widely-supported XML-based graph interchange standard. Features: - Full vertex and edge export with type, weight, and label metadata - Human-readable type labels (Friend, Classmate, Stranger, etc.) - Graph metadata (timestamp, description) in desc element - GraphML key definitions for all custom attributes - XML character escaping for safe output - exportToString() for in-memory use, export(File) for file output - exportVisibleToString() for only currently-visible edges - Sorted vertex output for deterministic results - Automatic .graphml extension if not specified - Overwrite confirmation dialog UI integration: - Export GraphML button in tool panel alongside existing CSV export - File chooser with auto-named default (graph_YYYY-MM-DD.graphml) - Success dialog showing node/edge counts Tests: 35 new tests covering empty graphs, all 5 edge types, XML escaping, metadata, file I/O, large graphs, deterministic output, visible-only export, key definitions, and edge cases. Usage: Click Export GraphML in the Tools panel, choose a save location. Open the .graphml file in Gephi, Cytoscape, yEd, or load it with NetworkX (nx.read_graphml()).
1 parent 036f77c commit c300ac7

5 files changed

Lines changed: 824 additions & 2 deletions

File tree

.github/workflows/ci.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,4 +59,5 @@ jobs:
5959
app.UtilMethodsTest \
6060
gvisual.EdgeTest \
6161
gvisual.GraphStatsTest \
62-
gvisual.ShortestPathFinderTest
62+
gvisual.ShortestPathFinderTest \
63+
gvisual.GraphMLExporterTest

.github/workflows/coverage.yml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ jobs:
6969
gvisual.EdgeTest \
7070
gvisual.GraphStatsTest \
7171
gvisual.ShortestPathFinderTest \
72-
gvisual.CommunityDetectorTest
72+
gvisual.CommunityDetectorTest \
73+
gvisual.GraphMLExporterTest
7374
7475
- name: Generate coverage report
7576
working-directory: Gvisual
Lines changed: 270 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,270 @@
1+
package gvisual;
2+
3+
import edu.uci.ics.jung.graph.Graph;
4+
5+
import java.io.*;
6+
import java.nio.charset.StandardCharsets;
7+
import java.util.*;
8+
9+
/**
10+
* Exports a JUNG graph to GraphML format — a standard XML-based graph
11+
* interchange format supported by Gephi, Cytoscape, NetworkX, yEd,
12+
* and many other graph analysis tools.
13+
*
14+
* <p>The exported file includes:</p>
15+
* <ul>
16+
* <li>All vertices with their node IDs</li>
17+
* <li>All edges with type, weight, label, and endpoint metadata</li>
18+
* <li>Graph metadata (timestamp, node count, edge count)</li>
19+
* <li>GraphML key definitions for custom attributes</li>
20+
* </ul>
21+
*
22+
* <p>Usage:</p>
23+
* <pre>
24+
* GraphMLExporter exporter = new GraphMLExporter(graph, allEdges);
25+
* exporter.setTimestamp("2011-03-15");
26+
* exporter.export(new File("graph.graphml"));
27+
* // or
28+
* String xml = exporter.exportToString();
29+
* </pre>
30+
*
31+
* @author zalenix
32+
*/
33+
public class GraphMLExporter {
34+
35+
private final Graph<String, edge> graph;
36+
private final List<edge> allEdges;
37+
private String timestamp;
38+
private String description;
39+
40+
/**
41+
* Creates a new GraphML exporter for the given graph.
42+
*
43+
* @param graph the JUNG graph to export
44+
* @param allEdges all edges (including those not currently visible in graph)
45+
*/
46+
public GraphMLExporter(Graph<String, edge> graph, List<edge> allEdges) {
47+
if (graph == null) {
48+
throw new IllegalArgumentException("Graph must not be null");
49+
}
50+
this.graph = graph;
51+
this.allEdges = (allEdges != null) ? allEdges : new ArrayList<edge>();
52+
this.timestamp = "";
53+
this.description = "";
54+
}
55+
56+
/**
57+
* Sets the timestamp metadata for the export.
58+
*
59+
* @param timestamp the timestamp string (e.g., "2011-03-15")
60+
*/
61+
public void setTimestamp(String timestamp) {
62+
this.timestamp = (timestamp != null) ? timestamp : "";
63+
}
64+
65+
/**
66+
* Gets the timestamp metadata.
67+
*
68+
* @return the timestamp string
69+
*/
70+
public String getTimestamp() {
71+
return timestamp;
72+
}
73+
74+
/**
75+
* Sets an optional description for the graph.
76+
*
77+
* @param description the description string
78+
*/
79+
public void setDescription(String description) {
80+
this.description = (description != null) ? description : "";
81+
}
82+
83+
/**
84+
* Gets the description.
85+
*
86+
* @return the description string
87+
*/
88+
public String getDescription() {
89+
return description;
90+
}
91+
92+
/**
93+
* Exports the graph to a GraphML file.
94+
*
95+
* @param file the output file
96+
* @throws IOException if writing fails
97+
*/
98+
public void export(File file) throws IOException {
99+
try (Writer writer = new BufferedWriter(
100+
new OutputStreamWriter(new FileOutputStream(file), StandardCharsets.UTF_8))) {
101+
writer.write(exportToString());
102+
}
103+
}
104+
105+
/**
106+
* Exports the graph to a GraphML XML string.
107+
*
108+
* @return the complete GraphML XML as a string
109+
*/
110+
public String exportToString() {
111+
StringBuilder sb = new StringBuilder();
112+
113+
// XML header
114+
sb.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n");
115+
sb.append("<graphml xmlns=\"http://graphml.graphstruct.org/xmlns\"\n");
116+
sb.append(" xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n");
117+
sb.append(" xsi:schemaLocation=\"http://graphml.graphstruct.org/xmlns\n");
118+
sb.append(" http://graphml.graphstruct.org/xmlns/1.0/graphml.xsd\">\n");
119+
120+
// Key definitions for node and edge attributes
121+
sb.append(" <!-- Node attribute keys -->\n");
122+
sb.append(" <key id=\"d0\" for=\"node\" attr.name=\"label\" attr.type=\"string\"/>\n");
123+
124+
sb.append(" <!-- Edge attribute keys -->\n");
125+
sb.append(" <key id=\"d1\" for=\"edge\" attr.name=\"type\" attr.type=\"string\"/>\n");
126+
sb.append(" <key id=\"d2\" for=\"edge\" attr.name=\"type_label\" attr.type=\"string\"/>\n");
127+
sb.append(" <key id=\"d3\" for=\"edge\" attr.name=\"weight\" attr.type=\"double\"/>\n");
128+
sb.append(" <key id=\"d4\" for=\"edge\" attr.name=\"label\" attr.type=\"string\"/>\n");
129+
130+
// Graph element
131+
sb.append(" <graph id=\"G\" edgedefault=\"undirected\">\n");
132+
133+
// Graph metadata as desc element
134+
if (!timestamp.isEmpty() || !description.isEmpty()) {
135+
sb.append(" <desc>");
136+
if (!description.isEmpty()) {
137+
sb.append(escapeXml(description));
138+
}
139+
if (!timestamp.isEmpty()) {
140+
if (!description.isEmpty()) sb.append(" | ");
141+
sb.append("Timestamp: ").append(escapeXml(timestamp));
142+
}
143+
sb.append("</desc>\n");
144+
}
145+
146+
// Nodes — sorted for deterministic output
147+
List<String> vertices = new ArrayList<String>(graph.getVertices());
148+
Collections.sort(vertices);
149+
150+
for (String vertex : vertices) {
151+
sb.append(" <node id=\"").append(escapeXml(vertex)).append("\">\n");
152+
sb.append(" <data key=\"d0\">").append(escapeXml(vertex)).append("</data>\n");
153+
sb.append(" </node>\n");
154+
}
155+
156+
// Edges — use allEdges if provided (includes filtered edges), otherwise graph edges
157+
List<edge> edgesToExport;
158+
if (!allEdges.isEmpty()) {
159+
edgesToExport = allEdges;
160+
} else {
161+
edgesToExport = new ArrayList<edge>(graph.getEdges());
162+
}
163+
164+
int edgeIndex = 0;
165+
for (edge e : edgesToExport) {
166+
String edgeId = "e" + edgeIndex++;
167+
sb.append(" <edge id=\"").append(edgeId).append("\"");
168+
sb.append(" source=\"").append(escapeXml(e.getVertex1())).append("\"");
169+
sb.append(" target=\"").append(escapeXml(e.getVertex2())).append("\">\n");
170+
171+
// Edge type code
172+
sb.append(" <data key=\"d1\">").append(escapeXml(e.getType())).append("</data>\n");
173+
174+
// Human-readable type label
175+
sb.append(" <data key=\"d2\">").append(escapeXml(getTypeLabel(e.getType()))).append("</data>\n");
176+
177+
// Weight
178+
sb.append(" <data key=\"d3\">").append(String.format("%.1f", e.getWeight())).append("</data>\n");
179+
180+
// Label (if set)
181+
if (e.getLabel() != null && !e.getLabel().isEmpty()) {
182+
sb.append(" <data key=\"d4\">").append(escapeXml(e.getLabel())).append("</data>\n");
183+
}
184+
185+
sb.append(" </edge>\n");
186+
}
187+
188+
sb.append(" </graph>\n");
189+
sb.append("</graphml>\n");
190+
191+
return sb.toString();
192+
}
193+
194+
/**
195+
* Exports only the edges currently visible in the graph (not all loaded edges).
196+
*
197+
* @return GraphML XML string with only visible edges
198+
*/
199+
public String exportVisibleToString() {
200+
// Temporarily swap allEdges to only graph edges
201+
List<edge> saved = new ArrayList<edge>(allEdges);
202+
allEdges.clear();
203+
String result = exportToString();
204+
allEdges.addAll(saved);
205+
return result;
206+
}
207+
208+
/**
209+
* Returns a human-readable label for an edge type code.
210+
*
211+
* @param typeCode the edge type code (f, fs, c, s, sg)
212+
* @return human-readable label
213+
*/
214+
static String getTypeLabel(String typeCode) {
215+
if (typeCode == null) return "Unknown";
216+
switch (typeCode) {
217+
case "f": return "Friend";
218+
case "fs": return "Familiar Stranger";
219+
case "c": return "Classmate";
220+
case "s": return "Stranger";
221+
case "sg": return "Study Group";
222+
default: return typeCode;
223+
}
224+
}
225+
226+
/**
227+
* Escapes special XML characters in a string.
228+
*
229+
* @param text the input text
230+
* @return XML-safe text
231+
*/
232+
static String escapeXml(String text) {
233+
if (text == null) return "";
234+
StringBuilder sb = new StringBuilder(text.length());
235+
for (int i = 0; i < text.length(); i++) {
236+
char c = text.charAt(i);
237+
switch (c) {
238+
case '&': sb.append("&amp;"); break;
239+
case '<': sb.append("&lt;"); break;
240+
case '>': sb.append("&gt;"); break;
241+
case '"': sb.append("&quot;"); break;
242+
case '\'': sb.append("&apos;"); break;
243+
default: sb.append(c); break;
244+
}
245+
}
246+
return sb.toString();
247+
}
248+
249+
/**
250+
* Returns the number of vertices that will be exported.
251+
*
252+
* @return vertex count
253+
*/
254+
public int getVertexCount() {
255+
return graph.getVertexCount();
256+
}
257+
258+
/**
259+
* Returns the number of edges that will be exported.
260+
* If allEdges is non-empty, returns allEdges count; otherwise graph edges.
261+
*
262+
* @return edge count
263+
*/
264+
public int getEdgeCount() {
265+
if (!allEdges.isEmpty()) {
266+
return allEdges.size();
267+
}
268+
return graph.getEdgeCount();
269+
}
270+
}

Gvisual/src/gvisual/Main.java

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2173,6 +2173,59 @@ public void actionPerformed(ActionEvent e) {
21732173
});
21742174
toolPanel.add(exportButton);
21752175

2176+
JButton graphmlButton = new JButton("<html><center>Export GraphML<br/>Export to GraphML<br/> for Gephi,<br/> Cytoscape, yEd,<br/> NetworkX</center></html>");
2177+
graphmlButton.setPreferredSize(new Dimension(140, 100));
2178+
graphmlButton.addActionListener(new ActionListener() {
2179+
2180+
public void actionPerformed(ActionEvent e) {
2181+
// Collect all edges from all categories
2182+
java.util.List<edge> allEdges = new java.util.ArrayList<edge>();
2183+
allEdges.addAll(friendEdges);
2184+
allEdges.addAll(fsEdges);
2185+
allEdges.addAll(classmateEdges);
2186+
allEdges.addAll(strangerEdges);
2187+
allEdges.addAll(studyGEdges);
2188+
2189+
GraphMLExporter exporter = new GraphMLExporter(g, allEdges);
2190+
exporter.setTimestamp(timeStamp);
2191+
exporter.setDescription("GraphVisual network — student community evolution");
2192+
2193+
JFileChooser fileChooser = new JFileChooser(System.getProperty("user.dir"));
2194+
fileChooser.setDialogTitle("Export as GraphML");
2195+
fileChooser.setSelectedFile(new File("graph_" + timeStamp + ".graphml"));
2196+
int returnVal = fileChooser.showSaveDialog(null);
2197+
if (returnVal != JFileChooser.APPROVE_OPTION) return;
2198+
2199+
File outFile = fileChooser.getSelectedFile();
2200+
if (!outFile.getName().endsWith(".graphml")) {
2201+
outFile = new File(outFile.getAbsolutePath() + ".graphml");
2202+
}
2203+
2204+
if (outFile.exists()) {
2205+
int confirm = JOptionPane.showConfirmDialog(null,
2206+
"File already exists. Overwrite?",
2207+
"Confirm Overwrite", JOptionPane.YES_NO_OPTION);
2208+
if (confirm != JOptionPane.YES_OPTION) return;
2209+
}
2210+
2211+
try {
2212+
exporter.export(outFile);
2213+
JOptionPane.showMessageDialog(null,
2214+
"GraphML exported successfully!\n"
2215+
+ "Nodes: " + exporter.getVertexCount() + "\n"
2216+
+ "Edges: " + exporter.getEdgeCount() + "\n"
2217+
+ "File: " + outFile.getName(),
2218+
"Export Complete", JOptionPane.INFORMATION_MESSAGE);
2219+
} catch (IOException ex1) {
2220+
Logger.getLogger(Main.class.getName()).log(Level.SEVERE, null, ex1);
2221+
JOptionPane.showMessageDialog(null,
2222+
"Export failed: " + ex1.getMessage(),
2223+
"Error", JOptionPane.ERROR_MESSAGE);
2224+
}
2225+
}
2226+
});
2227+
toolPanel.add(graphmlButton);
2228+
21762229
toolPanel.add(legendPanel);
21772230
contentPanel.add(toolPanel, BorderLayout.WEST);
21782231
}

0 commit comments

Comments
 (0)