Generate an interactive vis.js HTML visualization of the graph. Features: node size by degree, click-to-inspect panel, search box, community filter, physics clustering by community, confidence-styled edges. Raises ValueError if graph exceeds MAX_NODES_FOR_VIZ. If member_counts is p
(
G: nx.Graph,
communities: dict[int, list[str]],
output_path: str,
community_labels: dict[int, str] | None = None,
member_counts: dict[int, int] | None = None,
node_limit: int | None = None,
learning_overlay: dict | None = None,
)
| 630 | |
| 631 | |
| 632 | def to_html( |
| 633 | G: nx.Graph, |
| 634 | communities: dict[int, list[str]], |
| 635 | output_path: str, |
| 636 | community_labels: dict[int, str] | None = None, |
| 637 | member_counts: dict[int, int] | None = None, |
| 638 | node_limit: int | None = None, |
| 639 | learning_overlay: dict | None = None, |
| 640 | ) -> None: |
| 641 | """Generate an interactive vis.js HTML visualization of the graph. |
| 642 | |
| 643 | Features: node size by degree, click-to-inspect panel, search box, |
| 644 | community filter, physics clustering by community, confidence-styled edges. |
| 645 | Raises ValueError if graph exceeds MAX_NODES_FOR_VIZ. |
| 646 | |
| 647 | If member_counts is provided (aggregated community view), node sizes are |
| 648 | based on community member counts rather than graph degree. |
| 649 | |
| 650 | If node_limit is set and the graph exceeds it, automatically builds an |
| 651 | aggregated community-level meta-graph instead of raising ValueError. |
| 652 | """ |
| 653 | limit = node_limit if node_limit is not None else _viz_node_limit() |
| 654 | if G.number_of_nodes() > limit: |
| 655 | if node_limit is not None: |
| 656 | # Build aggregated community meta-graph |
| 657 | from collections import Counter as _Counter |
| 658 | import networkx as _nx |
| 659 | print(f"Graph has {G.number_of_nodes()} nodes (above {limit} limit). Building aggregated community view...") |
| 660 | node_to_community = {nid: cid for cid, members in communities.items() for nid in members} |
| 661 | meta = _nx.Graph() |
| 662 | for cid, members in communities.items(): |
| 663 | meta.add_node(str(cid), label=(community_labels or {}).get(cid, f"Community {cid}")) |
| 664 | edge_counts = _Counter() |
| 665 | for u, v in G.edges(): |
| 666 | cu, cv = node_to_community.get(u), node_to_community.get(v) |
| 667 | if cu is not None and cv is not None and cu != cv: |
| 668 | edge_counts[(min(cu, cv), max(cu, cv))] += 1 |
| 669 | for (cu, cv), w in edge_counts.items(): |
| 670 | meta.add_edge(str(cu), str(cv), weight=w, |
| 671 | relation=f"{w} cross-community edges", confidence="AGGREGATED") |
| 672 | if meta.number_of_nodes() <= 1: |
| 673 | print("Single community - aggregated view not useful. Skipping graph.html.") |
| 674 | return |
| 675 | meta_communities = {cid: [str(cid)] for cid in communities} |
| 676 | mc = {cid: len(members) for cid, members in communities.items()} |
| 677 | # Remap hyperedges from semantic node IDs to community IDs |
| 678 | raw_hyperedges = G.graph.get("hyperedges", []) |
| 679 | if raw_hyperedges: |
| 680 | remapped = [] |
| 681 | for he in raw_hyperedges: |
| 682 | he_members = he.get("nodes", []) |
| 683 | comm_ids, seen = [], set() |
| 684 | for nid in he_members: |
| 685 | c = node_to_community.get(nid) |
| 686 | if c is None: |
| 687 | continue |
| 688 | s = str(c) |
| 689 | if s in seen: |