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

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions