Skip to content

Bug: hyperedges always empty in graph.html when graph exceeds viz node limit (aggregated meta-graph drops them) #1005

Description

@Principe1218

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

  1. Run graphify on any project that exceeds _viz_node_limit() (default 1000 nodes) and that produces ≥1 hyperedge.
  2. Open graphify-out/graph.json — confirm hyperedges is non-empty.
  3. Open graphify-out/graph.html — grep for const hyperedges — it's [].
  4. Visualization never draws any hyperedge regions.

In my case: ~15k nodes, 30 hyperedges in graph.json, 0 rendered.

Root cause

In graphify/export.pyto_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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions