Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/facett

py/facett_demo/panes/graph_pane.py

"""GRAPH pane — the REAL facett graph navigator, embedded as wasm.

Like the map pane, this renders **no graph of its own**. It fetches the scene from
korp-server and hands it to the `graphnav` component in the `facet-wrapped` registry —
`facett_graphnav::GraphNav`, the same Graph-DB IDE the native demo ships (query bar,
canvas, inspector, layouts, path-finding), mounted as a live egui canvas.

It previously re-implemented the graph with a networkx spring layout drawn in plotly.
That is a second renderer with its own layout, styling and interaction model, and it
drifts from the real component by construction.

Data source: korp-server ``KorpControl.Graph`` — a Cypher query against the bound
FalkorDB graph (the fat node answers with a seed scene when no live FalkorDB is
attached).
"""

from __future__ import annotations

import streamlit as st
from ..facet_host import facett_component, is_data_driven

from ..client import KorpClient, KorpUnavailable

#: The facett component this pane embeds (registry name in `facet-wrapped`).
COMPONENT = "graphnav"


def _graph_payload(scene, graph_name: str) -> dict:
    """korp `GraphReply` → the `graphnav` payload (a pure re-shaping)."""
    return {
        "title": f"korp graph{f' · {graph_name}' if graph_name else ''}",
        "nodes": [{"id": n.id, "label": n.label, "key": n.key} for n in scene.nodes],
        "edges": [{"src": e.src, "dst": e.dst, "rel": e.rel} for e in scene.edges],
    }


def render(client: KorpClient) -> None:
    st.subheader("🕸 Graph — the real facett graph navigator")
    st.caption(
        "Renderer: `facet-wrapped` → `graphnav` (facett_graphnav::GraphNav, wasm) · "
        "Data: korp-server `Graph` RPC"
    )

    # Blank = the server's own scene read. korp-server decodes ONE result shape (its
    # `RETURN id(a) AS src, id(b) AS dst, type(r) AS rel, … a.lon AS slon …` tail); a
    # query with another RETURN shape decodes to zero rows and comes back as
    # `korp-graph-9 … holds 0 nodes` even against a full graph (loki, 2026-09-19).
    # A hand-written query must therefore end in that tail — the help text says so.
    default_q = ""
    query = st.text_input(
        "Cypher query (blank = the server's scene read)", value=default_q, key="graph_query",
        help="Ad-hoc queries must RETURN the server's scene columns: id(a) AS src, "
             "id(b) AS dst, type(r) AS rel, labels(a)[0] AS sl, labels(b)[0] AS dl, "
             "a.lon AS slon, a.lat AS slat, b.lon AS dlon, b.lat AS dlat. Any other "
             "RETURN shape reads as an empty graph.",
    )
    graph_name = st.text_input("Graph (blank = server default)", value="", key="graph_name")

    try:
        scene = client.graph(query=query, graph=graph_name)
    except KorpUnavailable as e:
        st.error(f"Graph unavailable — server could not answer.\n\n`{e}`")
        return

    if not scene.nodes:
        st.warning("Server returned an empty graph (honest empty state).")
        st.caption(scene.status)
        return

    c1, c2, c3 = st.columns(3)
    c1.metric("Nodes", len(scene.nodes))
    c2.metric("Edges", len(scene.edges))
    c3.metric("Status", scene.status.split("(")[0].strip() or "ok")

    if not is_data_driven(COMPONENT):
        st.error(
            f"`{COMPONENT}` is not available as a data-driven facett component — the "
            "`facet-wrapped` wasm bundle is missing or stale. Rebuild it with "
            "`cargo xtask wrap-wasm`. Refusing to show a demo fixture in place of "
            "your data."
        )
        return

    facett_component(
        COMPONENT, data=_graph_payload(scene, graph_name), height=620, key="graphnav_graph"
    )
    st.caption(scene.status)