Summary
When a graph exceeds GRAPHIFY_VIZ_NODE_LIMIT, to_html falls back to a community-aggregated meta-graph and recursively re-invokes itself. The recursive call never carries hyperedges onto the meta-graph, so the rendered graphify-out/graph.html always emits const hyperedges = []; — even when graph.json contains plenty.
The visible result: the afterDrawing handler that renders hyperedge regions silently no-ops on every large project, even though hyperedges are extracted, persisted to graph.json, and merged correctly through the rest of the pipeline.
A SAST/code-quality scanner caught this on the generated HTML, which is how I noticed.
Version: graphifyy==0.8.17 (also present on main at time of writing)
Repro
- Run graphify on any project that exceeds
_viz_node_limit() (default 1000 nodes) and that produces ≥1 hyperedge.
- Open
graphify-out/graph.json — confirm hyperedges is non-empty.
- Open
graphify-out/graph.html — grep for const hyperedges — it's [].
- Visualization never draws any hyperedge regions.
In my case: ~15k nodes, 30 hyperedges in graph.json, 0 rendered.
Root cause
In graphify/export.py — to_html at the over-limit branch (around lines 636–663) builds a fresh meta-graph:
meta = _nx.Graph()
for cid, members in communities.items():
meta.add_node(str(cid), label=...)
# ... add_edge calls ...
to_html(meta, meta_communities, output_path, ...)
Then the recursive call hits this line (~740):
hyperedges_json = _js_safe(getattr(G, "graph", {}).get("hyperedges", []))
meta.graph["hyperedges"] was never set, so this evaluates to "[]". Meanwhile, the hyperedges in G.graph["hyperedges"] (which exist on the outer call) reference semantic node IDs that don't exist in the meta-graph anyway, so even if they were forwarded as-is, network.getPositions([nid]) would return undefined for every entry and the positions.length < 2 guard in the afterDrawing handler would skip them all.
Suggested fix
Remap hyperedges from semantic node IDs to community IDs (which become the meta-graph node IDs) and attach to meta.graph["hyperedges"] before the recursive call. Drop entries that collapse to <2 distinct communities — they wouldn't render as a polygon anyway.
Patch sketch, inserted between the add_edge loop and the to_html(meta, ...) recursive call:
raw_hyperedges = G.graph.get("hyperedges", [])
remapped = []
for he in raw_hyperedges:
# Hyperedges in the wild use either "nodes" or "members" depending on age/source.
members = he.get("nodes") or he.get("members") or []
comm_ids, seen = [], set()
for nid in members:
c = node_to_community.get(nid)
if c is None:
continue
s = str(c)
if s in seen:
continue
seen.add(s)
comm_ids.append(s)
if len(comm_ids) < 2:
continue
remapped.append({
"id": he.get("id", ""),
# Fall back to a humanized `relation` when label is missing —
# extracted hyperedges sometimes only have `relation`.
"label": he.get("label") or he.get("relation", "").replace("_", " "),
"nodes": comm_ids,
})
meta.graph["hyperedges"] = remapped
After applying locally, I went from const hyperedges = [] to 10 renderable region polygons on the same project.
Additional notes
- Worth double-checking the non-aggregated path too. Even when the graph fits within the viz limit, the
afterDrawing handler assumes h.nodes are vis-network node IDs — that's true in the unaggregated case (semantic IDs match RAW_NODES[].id), but a unit-test/example would lock this in.
- Empty-label hyperedges render with no text in the centroid label. The
relation fallback above is a low-cost improvement.
- Happy to send a PR if useful.
Summary
When a graph exceeds
GRAPHIFY_VIZ_NODE_LIMIT,to_htmlfalls back to a community-aggregated meta-graph and recursively re-invokes itself. The recursive call never carries hyperedges onto the meta-graph, so the renderedgraphify-out/graph.htmlalways emitsconst hyperedges = [];— even whengraph.jsoncontains plenty.The visible result: the
afterDrawinghandler that renders hyperedge regions silently no-ops on every large project, even though hyperedges are extracted, persisted tograph.json, and merged correctly through the rest of the pipeline.A SAST/code-quality scanner caught this on the generated HTML, which is how I noticed.
Version:
graphifyy==0.8.17(also present onmainat time of writing)Repro
_viz_node_limit()(default 1000 nodes) and that produces ≥1 hyperedge.graphify-out/graph.json— confirmhyperedgesis non-empty.graphify-out/graph.html— grep forconst hyperedges— it's[].In my case: ~15k nodes, 30 hyperedges in
graph.json, 0 rendered.Root cause
In
graphify/export.py—to_htmlat the over-limit branch (around lines 636–663) builds a fresh meta-graph:Then the recursive call hits this line (~740):
meta.graph["hyperedges"]was never set, so this evaluates to"[]". Meanwhile, the hyperedges inG.graph["hyperedges"](which exist on the outer call) reference semantic node IDs that don't exist in the meta-graph anyway, so even if they were forwarded as-is,network.getPositions([nid])would returnundefinedfor every entry and thepositions.length < 2guard in theafterDrawinghandler would skip them all.Suggested fix
Remap hyperedges from semantic node IDs to community IDs (which become the meta-graph node IDs) and attach to
meta.graph["hyperedges"]before the recursive call. Drop entries that collapse to <2 distinct communities — they wouldn't render as a polygon anyway.Patch sketch, inserted between the
add_edgeloop and theto_html(meta, ...)recursive call:After applying locally, I went from
const hyperedges = []to 10 renderable region polygons on the same project.Additional notes
afterDrawinghandler assumesh.nodesare vis-network node IDs — that's true in the unaggregated case (semantic IDs matchRAW_NODES[].id), but a unit-test/example would lock this in.relationfallback above is a low-cost improvement.