diff --git a/README.md b/README.md index 494e646e..0038ed72 100644 --- a/README.md +++ b/README.md @@ -107,6 +107,7 @@ All commands are typed inside Claude Code: /graphify ./raw --html # also export graph.html (browser, no Obsidian needed) /graphify ./raw --svg # also export graph.svg (embeds in Notion, GitHub) +/graphify ./raw --graphml # also export graph.graphml (Gephi, yEd, any GraphML tool) /graphify ./raw --neo4j # generate cypher.txt for Neo4j import /graphify ./raw --mcp # start MCP stdio server for agent access ``` @@ -218,7 +219,7 @@ graphify/ ├── cluster.py Leiden community detection, cohesion scoring ├── analyze.py god nodes, bridge nodes, surprising connections, suggested questions, graph diff ├── report.py render GRAPH_REPORT.md -├── export.py Obsidian vault, graph.json, graph.html, graph.svg, Neo4j Cypher, Canvas +├── export.py Obsidian vault, graph.json, graph.html, graph.svg, graph.graphml, Neo4j Cypher, Canvas ├── ingest.py fetch URLs (arXiv, Twitter/X, PDF, any webpage); save Q&A to .graphify/memory/ ├── cache.py SHA256-based per-file extraction cache; check_semantic_cache / save_semantic_cache ├── security.py URL validation (http/https only), safe fetch with size cap, path guards, label sanitisation diff --git a/graphify/export.py b/graphify/export.py index eb4e02d4..2edf8259 100644 --- a/graphify/export.py +++ b/graphify/export.py @@ -1,4 +1,4 @@ -# write graph to HTML, JSON, SVG, Obsidian vault, and Neo4j Cypher +# write graph to HTML, JSON, SVG, GraphML, Obsidian vault, and Neo4j Cypher from __future__ import annotations import json import math @@ -586,6 +586,23 @@ def push_to_neo4j( return {"nodes": nodes_pushed, "edges": edges_pushed} +def to_graphml( + G: nx.Graph, + communities: dict[int, list[str]], + output_path: str, +) -> None: + """Export graph as GraphML — opens in Gephi, yEd, and any GraphML-compatible tool. + + Community IDs are written as a node attribute so Gephi can colour by community. + Edge confidence (EXTRACTED/INFERRED/AMBIGUOUS) is preserved as an edge attribute. + """ + H = G.copy() + node_community = {n: cid for cid, nodes in communities.items() for n in nodes} + for node_id in H.nodes(): + H.nodes[node_id]["community"] = node_community.get(node_id, -1) + nx.write_graphml(H, output_path) + + def to_svg( G: nx.Graph, communities: dict[int, list[str]], diff --git a/tests/test_export.py b/tests/test_export.py index 86d5746f..af2ade9d 100644 --- a/tests/test_export.py +++ b/tests/test_export.py @@ -3,7 +3,7 @@ import tempfile from pathlib import Path from graphify.build import build_from_json from graphify.cluster import cluster -from graphify.export import to_json, to_cypher +from graphify.export import to_json, to_cypher, to_graphml FIXTURES = Path(__file__).parent / "fixtures" @@ -52,3 +52,30 @@ def test_to_cypher_contains_merge_statements(): to_cypher(G, str(out)) content = out.read_text() assert "MERGE" in content + +def test_to_graphml_creates_file(): + G = make_graph() + communities = cluster(G) + with tempfile.TemporaryDirectory() as tmp: + out = Path(tmp) / "graph.graphml" + to_graphml(G, communities, str(out)) + assert out.exists() + +def test_to_graphml_valid_xml(): + G = make_graph() + communities = cluster(G) + with tempfile.TemporaryDirectory() as tmp: + out = Path(tmp) / "graph.graphml" + to_graphml(G, communities, str(out)) + content = out.read_text() + assert "