= {
seed: options.seed ?? 42,
iterations: options.iterations ?? 200,
width: options.width ?? 1400,
height: options.height ?? 900,
};
const nodes = [...graph.nodes.values()].sort((a, b) =>
a.id.localeCompare(b.id),
);
// Empty-state.
if (nodes.length === 0) {
return emptyStateHtml();
}
const positions = layout(graph, opts);
const minPr = nodes.reduce((m, n) => Math.min(m, n.pageRank), Infinity);
const maxPr = nodes.reduce((m, n) => Math.max(m, n.pageRank), -Infinity);
const edgeSvg: string[] = [];
for (const e of graph.edges) {
const p1 = positions.get(e.source);
const p2 = positions.get(e.target);
if (!p1 || !p2) continue;
edgeSvg.push(
` `,
);
}
const nodeSvg: string[] = [];
for (const n of nodes) {
const p = positions.get(n.id);
if (!p) continue;
const r = radiusForPageRank(n.pageRank, minPr, maxPr);
const path = articlePathForNode(n, articles);
// tabindex makes the node focusable so keyboard users can Tab between
// nodes; the inline JS turns Enter/Space into a navigation when a
// `data-path` is set.
nodeSvg.push(
``,
);
nodeSvg.push(
` `,
);
nodeSvg.push(
`${escapeAttr(n.label)} `,
);
nodeSvg.push(` `);
}
return pageHtml({
width: opts.width,
height: opts.height,
edgeSvg: edgeSvg.join(""),
nodeSvg: nodeSvg.join(""),
summary: `${nodes.length} nodes · ${graph.edges.length} edges`,
});
}
// ─── HTML templates ────────────────────────────────────────────────────
function pageHtml(ctx: {
width: number;
height: number;
edgeSvg: string;
nodeSvg: string;
summary: string;
}): string {
return `
maina wiki · graph
${ctx.edgeSvg}
${ctx.nodeSvg}
`;
}
function emptyStateHtml(): string {
return `
maina wiki · graph (empty)
No graph yet
Run maina wiki compile to build the knowledge graph. This page will show a force-directed view of its nodes, edges, and communities once compilation produces at least one node.
`;
}