Skip to content

Commit 4493da0

Browse files
committed
feat: GraphML export (--graphml flag) for Gephi and yEd
1 parent 25aff54 commit 4493da0

3 files changed

Lines changed: 48 additions & 3 deletions

File tree

README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ All commands are typed inside Claude Code:
107107
108108
/graphify ./raw --html # also export graph.html (browser, no Obsidian needed)
109109
/graphify ./raw --svg # also export graph.svg (embeds in Notion, GitHub)
110+
/graphify ./raw --graphml # also export graph.graphml (Gephi, yEd, any GraphML tool)
110111
/graphify ./raw --neo4j # generate cypher.txt for Neo4j import
111112
/graphify ./raw --mcp # start MCP stdio server for agent access
112113
```
@@ -218,7 +219,7 @@ graphify/
218219
├── cluster.py Leiden community detection, cohesion scoring
219220
├── analyze.py god nodes, bridge nodes, surprising connections, suggested questions, graph diff
220221
├── report.py render GRAPH_REPORT.md
221-
├── export.py Obsidian vault, graph.json, graph.html, graph.svg, Neo4j Cypher, Canvas
222+
├── export.py Obsidian vault, graph.json, graph.html, graph.svg, graph.graphml, Neo4j Cypher, Canvas
222223
├── ingest.py fetch URLs (arXiv, Twitter/X, PDF, any webpage); save Q&A to .graphify/memory/
223224
├── cache.py SHA256-based per-file extraction cache; check_semantic_cache / save_semantic_cache
224225
├── security.py URL validation (http/https only), safe fetch with size cap, path guards, label sanitisation

graphify/export.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# write graph to HTML, JSON, SVG, Obsidian vault, and Neo4j Cypher
1+
# write graph to HTML, JSON, SVG, GraphML, Obsidian vault, and Neo4j Cypher
22
from __future__ import annotations
33
import json
44
import math
@@ -586,6 +586,23 @@ def _safe_rel(relation: str) -> str:
586586
return {"nodes": nodes_pushed, "edges": edges_pushed}
587587

588588

589+
def to_graphml(
590+
G: nx.Graph,
591+
communities: dict[int, list[str]],
592+
output_path: str,
593+
) -> None:
594+
"""Export graph as GraphML — opens in Gephi, yEd, and any GraphML-compatible tool.
595+
596+
Community IDs are written as a node attribute so Gephi can colour by community.
597+
Edge confidence (EXTRACTED/INFERRED/AMBIGUOUS) is preserved as an edge attribute.
598+
"""
599+
H = G.copy()
600+
node_community = {n: cid for cid, nodes in communities.items() for n in nodes}
601+
for node_id in H.nodes():
602+
H.nodes[node_id]["community"] = node_community.get(node_id, -1)
603+
nx.write_graphml(H, output_path)
604+
605+
589606
def to_svg(
590607
G: nx.Graph,
591608
communities: dict[int, list[str]],

tests/test_export.py

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
from pathlib import Path
44
from graphify.build import build_from_json
55
from graphify.cluster import cluster
6-
from graphify.export import to_json, to_cypher
6+
from graphify.export import to_json, to_cypher, to_graphml
77

88
FIXTURES = Path(__file__).parent / "fixtures"
99

@@ -52,3 +52,30 @@ def test_to_cypher_contains_merge_statements():
5252
to_cypher(G, str(out))
5353
content = out.read_text()
5454
assert "MERGE" in content
55+
56+
def test_to_graphml_creates_file():
57+
G = make_graph()
58+
communities = cluster(G)
59+
with tempfile.TemporaryDirectory() as tmp:
60+
out = Path(tmp) / "graph.graphml"
61+
to_graphml(G, communities, str(out))
62+
assert out.exists()
63+
64+
def test_to_graphml_valid_xml():
65+
G = make_graph()
66+
communities = cluster(G)
67+
with tempfile.TemporaryDirectory() as tmp:
68+
out = Path(tmp) / "graph.graphml"
69+
to_graphml(G, communities, str(out))
70+
content = out.read_text()
71+
assert "<graphml" in content
72+
assert "<node" in content
73+
74+
def test_to_graphml_has_community_attribute():
75+
G = make_graph()
76+
communities = cluster(G)
77+
with tempfile.TemporaryDirectory() as tmp:
78+
out = Path(tmp) / "graph.graphml"
79+
to_graphml(G, communities, str(out))
80+
content = out.read_text()
81+
assert "community" in content

0 commit comments

Comments
 (0)