"""
Knowledge graph renderer — VOSviewer-faithful directed graph with entity-type
coloring, curved arrows, gap highlighting, left control panel, right detail
panel, and relationship evidence table.
"""
import hashlib as _hashlib
import json
import os as _os
import re
from typing import Any, Dict, List, Optional
import networkx as nx
# Own-file version — uses mtime so it refreshes every time the file is saved,
# guaranteeing the Streamlit session_state cache is busted on every deploy.
try:
_KG_VERSION = "kgmt" + str(int(_os.path.getmtime(__file__)))
except Exception:
try:
with open(__file__, "rb") as _f:
_KG_VERSION = "kg" + _hashlib.md5(_f.read()).hexdigest()[:10]
except Exception:
_KG_VERSION = "kg1"
from config import (
CANVAS_BG,
ENTITY_TYPE_COLORS,
COMMUNITY_COLORS,
NODE_SIZE_MIN,
NODE_SIZE_MAX,
EDGE_WIDTH_MIN,
EDGE_WIDTH_MAX,
COLOR_SURFACE_ELEVATED,
COLOR_SURFACE,
COLOR_TEXT_SECONDARY,
COLOR_PRIMARY,
COLOR_DANGER,
COLOR_SUCCESS,
COLOR_WARNING,
)
from utils.helpers import (
lighten_hex,
hex_to_rgba,
scale_node_size,
scale_edge_width,
truncate,
percentile,
)
from visualization.network_viz import (
get_physics_options,
_TOOLTIP_STYLE,
_wrap_tooltip,
_STABILIZE_JS,
_CONTROLS_JS,
_post_process_html,
_default_edge_tooltip,
_label_font,
_VIZ_VERSION,
)
# ── Pulse animation for gap nodes ─────────────────────────────────────────────
_PULSE_CSS = """
"""
# KG-specific stabilise: disable physics only — NO fit() calls.
# _STABILIZE_JS (from network_viz) also calls fit() at 300/800/1500/2500ms
# which overrides the custom bounding-box zoom in _KG_FIT_JS. We inject
# this script instead of _STABILIZE_JS so the KG fit is never clobbered.
_KG_STABILIZE_JS = """
"""
# Convert string titles → DOM elements AFTER stabilization completes.
# vis.js v7+ uses textContent (not innerHTML) for string titles, so any HTML
# in a string title is displayed as raw escaped text. DOM element titles are
# appended directly (as nodes), so they render correctly as styled HTML.
# We bulk-convert AFTER stabilizationIterationsDone (physics disabled) so
# DataSet.update() does NOT trigger O(n²) avoidOverlap recalculation.
_KG_TOOLTIP_FIX_JS = """
"""
_GAP_HIGHLIGHT_JS = """
"""
_KG_HIGHLIGHT_JS = """
"""
# Robust post-stabilisation fit for the KG.
# Retries at three increasing delays so at least one call lands after
_KG_FIT_JS = """
"""
def _post_process_kg_html(
html: str, gap_nodes: Optional[List] = None
) -> str:
html = html.replace(
"background-color: #ffffff;", f"background-color: {CANVAS_BG};"
).replace(
"background-color:#ffffff;", f"background-color:{CANVAS_BG};"
)
html = re.sub(
r'(id="mynetwork"[^>]*style=")([^"]*)',
rf'\1background:{CANVAS_BG};',
html,
)
gap_json = json.dumps(
[str(n) for n in (gap_nodes or [])]
)
gap_js = _GAP_HIGHLIGHT_JS.replace("__GAP_NODES_JSON__", gap_json)
html = html.replace(
"