Skip to content

Link graph (vaultkg.viz)

vaultkg.viz

viz.py -- the vault's link graph as a self-contained interactive HTML page.

The renderer is kg_utils.viz.build_graph_html, shared with every other KG module. What lives here is only what is about a vault: which kinds exist, how relations are coloured, what a tooltip shows, and how notes are chosen when the whole vault is too big to draw.

Notes are sized by backlinks (distinct notes linking in), so hubs stand out without reading labels. pyvis arrives with the viz extra and is imported by the SDK inside the render call; the CLI imports this module inside its command, so a bare install never pulls it.

Distinct notes linking in, per note, as a score set for node sizing.

Parameters:

Name Type Description Default
kg VaultKG

An open vault graph.

required

Returns:

Type Description
ScoreSet

A ScoreSet over every note; rank 1 is the most linked.

Source code in src/vaultkg/viz.py
112
113
114
115
116
117
118
119
120
121
122
def backlink_scores(kg: VaultKG) -> ScoreSet:
    """Distinct notes linking in, per note, as a score set for node sizing.

    :param kg: An open vault graph.
    :return: A ``ScoreSet`` over every note; rank 1 is the most linked.
    """
    health = vault_health(kg.store.con)
    scores = {n: float(len(health.in_links.get(n, ()))) for n in health.notes}
    ordered = sorted(scores, key=lambda n: (-scores[n], n))
    ranks = {n: i + 1 for i, n in enumerate(ordered)}
    return ScoreSet(metric="backlinks", table="vault", scores=scores, ranks=ranks)

Render the vault's link graph as a self-contained HTML page.

With root, the picture is that note's neighbourhood, hops links out in either direction. Without one it is the most connected part of the vault: nodes ranked by how many edges touch them, cut at max_nodes.

Parameters:

Name Type Description Default
kg VaultKG

An open vault graph.

required
root str | None

A node id or a note's vault path to centre on, or None.

None
hops int

Links to expand from root.

1
max_nodes int

Node budget.

200
kinds tuple[str, ...]

Node kinds to draw; the root is drawn whatever its kind.

DEFAULT_KINDS
height str

CSS height of the canvas.

'800px'
edge_labels bool

Print each link's relation on the canvas. Off by default: in a note's neighbourhood nearly every edge is LINKS_TO, and the relation is still on hover and in the edge colour.

False

Returns:

Type Description
tuple[str, int, int]

(html, nodes drawn, edges drawn).

Raises:

Type Description
ValueError

If root is not in the graph, a kind is unknown, or nothing is left to draw.

Source code in src/vaultkg/viz.py
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
def link_graph_html(
    kg: VaultKG,
    *,
    root: str | None = None,
    hops: int = 1,
    max_nodes: int = 200,
    kinds: tuple[str, ...] = DEFAULT_KINDS,
    height: str = "800px",
    edge_labels: bool = False,
) -> tuple[str, int, int]:
    """Render the vault's link graph as a self-contained HTML page.

    With ``root``, the picture is that note's neighbourhood, ``hops`` links
    out in either direction. Without one it is the most connected part of the
    vault: nodes ranked by how many edges touch them, cut at ``max_nodes``.

    :param kg: An open vault graph.
    :param root: A node id or a note's vault path to centre on, or ``None``.
    :param hops: Links to expand from ``root``.
    :param max_nodes: Node budget.
    :param kinds: Node kinds to draw; the root is drawn whatever its kind.
    :param height: CSS height of the canvas.
    :param edge_labels: Print each link's relation on the canvas. Off by
        default: in a note's neighbourhood nearly every edge is ``LINKS_TO``,
        and the relation is still on hover and in the edge colour.
    :return: ``(html, nodes drawn, edges drawn)``.
    :raises ValueError: If ``root`` is not in the graph, a kind is unknown, or
        nothing is left to draw.
    """
    unknown = set(kinds) - set(ALL_KINDS)
    if unknown:
        raise ValueError(f"unknown kind(s): {', '.join(sorted(unknown))}")
    store = kg.store
    root_id = normalize_node_id(root) if root is not None else None
    degree = _degree(kg)

    if root_id is not None:
        if store.node(root_id) is None:
            raise ValueError(f"no such node: {root_id}")
        reach = store.expand({root_id}, hop=hops, rels=kg.relations())
        # Nearest first, then the most connected, so a budget cut keeps the
        # neighbourhood's core.
        ids = sorted(reach, key=lambda i: (reach[i].best_hop, -degree.get(i, 0), i))
        nodes = [n for n in (store.node(i) for i in ids) if n is not None]
        nodes = [n for n in nodes if n["id"] == root_id or n["kind"] in kinds]
    else:
        nodes = store.query_nodes(kinds=list(kinds))
        nodes.sort(key=lambda n: (-degree.get(n["id"], 0), n["id"]))

    if not nodes:
        raise ValueError("nothing to draw")
    nodes = nodes[:max_nodes]
    edges = store.edges_within({n["id"] for n in nodes})

    counts = Counter(
        top_folder(str(n.get("module_path") or "")) for n in nodes if n["kind"] == "note"
    )
    folders = [f for f, _ in counts.most_common()]
    html = build_graph_html(
        nodes,
        edges,
        theme=vault_theme(list(kg.relations()), folders),
        tooltip=VAULT_TOOLTIP,
        scores=backlink_scores(kg),
        height=height,
        highlight_ids={root_id} if root_id else None,
        edge_labels=edge_labels,
    )
    return html, len(nodes), len(edges)

vault_theme(relations, folders=None)

The visual vocabulary for a vault.

Parameters:

Name Type Description Default
relations list[str]

Relations present in the graph; typed ones not in :data:~vaultkg.theme.REL_COLOR get the shared typed-link colour.

required
folders list[str] | None

Top-level folders, most notes first. Notes are coloured by folder, matching the 3-D tree; None colours every note alike.

None

Returns:

Type Description
GraphTheme

A :class:~kg_utils.viz.GraphTheme.

Source code in src/vaultkg/viz.py
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
def vault_theme(relations: list[str], folders: list[str] | None = None) -> GraphTheme:
    """The visual vocabulary for a vault.

    :param relations: Relations present in the graph; typed ones not in
        :data:`~vaultkg.theme.REL_COLOR` get the shared typed-link colour.
    :param folders: Top-level folders, most notes first. Notes are coloured
        by folder, matching the 3-D tree; ``None`` colours every note alike.
    :return: A :class:`~kg_utils.viz.GraphTheme`.
    """
    kinds = {
        k: KindStyle(color=KIND_COLOR[k], shape=KIND_SHAPE[k], size=KIND_SIZE[k])
        for k in KIND_COLOR
    }
    for folder, color in category_colors(folders or []).items():
        kinds[f"note:{folder}"] = KindStyle(
            color=color, shape=KIND_SHAPE["note"], size=KIND_SIZE["note"]
        )

    def resolve(node: Mapping[str, Any]) -> str:
        kind = str(node.get("kind", ""))
        if kind != "note" or not folders:
            return kind
        return f"note:{top_folder(str(node.get('module_path') or ''))}"

    rels = {r: REL_COLOR.get(r, TYPED_REL_COLOR) for r in relations}
    return GraphTheme(
        kinds=kinds,
        fallback=KindStyle(color="#8C8C8C", shape="dot", size=10),
        relations=rels,
        relation_fallback=TYPED_REL_COLOR,
        resolve_kind=resolve,
    )