diff --git a/graphify/exporters/html.py b/graphify/exporters/html.py index 59c0e52e3..63a6d85e8 100644 --- a/graphify/exporters/html.py +++ b/graphify/exporters/html.py @@ -27,6 +27,19 @@ def _viz_node_limit() -> int: except ValueError: return MAX_NODES_FOR_VIZ + +def _edge_types_default() -> bool: + """Whether graph.html starts with relation coloring on (GRAPHIFY_EDGE_TYPES). + + Off by default so a generated report costs nothing until the reader opts in + (#2088). Mirrors GRAPHIFY_VIZ_NODE_LIMIT style: unset/empty => False. + """ + import os + raw = os.environ.get("GRAPHIFY_EDGE_TYPES") + if raw is None or not raw.strip(): + return False + return raw.strip().lower() not in ("0", "false", "no", "off") + def _html_styles() -> str: return """""" def _hyperedge_script(hyperedges_json: str) -> str: @@ -109,17 +137,47 @@ def _hyperedge_script(hyperedges_json: str) -> str: }}); """ -def _html_script(nodes_json: str, edges_json: str, legend_json: str) -> str: +def _html_script( + nodes_json: str, + edges_json: str, + legend_json: str, + *, + show_edge_types: bool = False, + edge_types_color_default: bool = False, +) -> str: + show_edge_js = "true" if show_edge_types else "false" + edge_color_js = "true" if edge_types_color_default else "false" return f"""""" def to_html( @@ -330,15 +484,23 @@ def to_html( member_counts: dict[int, int] | None = None, node_limit: int | None = None, learning_overlay: dict | None = None, + edge_types: bool | None = None, ) -> None: """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. + Optional Connection Types panel colours/filters edges by relation (#2088). Raises ValueError if graph exceeds MAX_NODES_FOR_VIZ. If member_counts is provided (aggregated community view), node sizes are - based on community member counts rather than graph degree. + based on community member counts rather than graph degree. The Connection + Types panel is skipped there — meta-edges are weight labels, not relations. + + ``edge_types`` / ``GRAPHIFY_EDGE_TYPES`` set whether relation colouring + starts on; the panel itself is always present on non-aggregated graphs. + Readers can flip colouring in the sidebar; the choice persists in + ``localStorage``. If node_limit is set and the graph exceeds it, automatically builds an aggregated community-level meta-graph instead of raising ValueError. @@ -492,11 +654,12 @@ def to_html( relation = data.get("relation", "") true_src = data.get("_src", u) true_tgt = data.get("_tgt", v) + rel_label = sanitize_label(str(relation or "")) or "unknown" vis_edges.append({ "from": true_src, "to": true_tgt, - "label": relation, - "title": _html.escape(f"{relation} [{confidence}]"), + "label": rel_label, + "title": _html.escape(f"{rel_label} [{confidence}]"), "dashes": confidence != "EXTRACTED", "width": 2 if confidence == "EXTRACTED" else 1, "color": {"opacity": 0.7 if confidence == "EXTRACTED" else 0.35}, @@ -522,6 +685,21 @@ def _js_safe(obj) -> str: title = _html.escape(sanitize_label(str(output_path))) stats = f"{G.number_of_nodes()} nodes · {G.number_of_edges()} edges · {len(communities)} communities" + # Aggregated community meta-graphs use weight strings as "relation" — a + # Connection Types row per distinct weight is noise (#2088). + show_edge_types = member_counts is None + color_default = _edge_types_default() if edge_types is None else bool(edge_types) + edge_types_panel = "" + if show_edge_types: + edge_types_panel = """ +