@fortemi/graph
v2026.7.5
Published
Framework-agnostic graph tooling for fortemi: pure TypeScript helpers for community-graph layout, filtering, color assignment, degree sizing, bounds/fit, neighborhood expansion, and static snapshot serialization, plus an optional controller subpath for da
Maintainers
Readme
@fortemi/graph
Framework-agnostic graph projection helpers for rendering Fortemi community graphs anywhere
Turn a CommunityGraph into something you can draw — deterministic layout, filtering, community coloring, degree-based sizing, bounds/fit, neighborhood expansion, and static snapshot serialization. Framework-agnostic (no React). The root graph helpers stay database-free and tree-shakeable for JS-only hosts, and run entirely client-side — no network, no database. Database-backed graph-source selection is available from @fortemi/graph/controller.
pnpm add @fortemi/graphInstall · Why Fortemi · Quick Start · API · Data Model · Docs · License
What @fortemi/graph Is
@fortemi/graph is the framework-agnostic projection layer for Fortemi relationship graphs. It takes a plain CommunityGraph — the shape produced by @fortemi/core's GraphRepository and AIWG index export — and provides the pure, deterministic helpers needed to lay it out, filter it, color it, size it, fit it to a viewport, and render it as SVG or canvas.
It is an add-on, not a base layer. @fortemi/core remains the foundation and owns graph production (similarity and link graphs built from the PGlite store) and community detection. The @fortemi/graph root entry owns graph projection and has no runtime dependency on @fortemi/core. The optional @fortemi/graph/controller subpath owns live graph-source orchestration (GraphController) and is the only graph entry that touches the PGlite-backed repositories. @fortemi/core never depends on @fortemi/graph.
Why Fortemi Graph
Rendering a knowledge graph usually means re-implementing layout math, community coloring, degree sizing, and viewport fitting in every host. Fortemi centralizes that logic in one package, so a React app and a static site stay visually aligned and share the exact same deterministic projection.
| Need | What Fortemi Graph provides |
|---|---|
| Deterministic layout | layoutCommunityGraph — a seeded force settlement plus closed-form radial/community/manual positions; identical input always yields identical coordinates |
| Visibility control | filterCommunityGraph — filter by community, edge kind, node allow-list, or predicate |
| Readable encoding | Degree-based node sizing and deterministic community color assignment |
| Viewport math | Bounding box plus a centered fit transform for SVG/canvas |
| Interaction | Neighborhood expansion and induced-subgraph helpers for selection |
| Portable views | Stable snapshot serialization for static, precomputed graphs |
| Zero lock-in | Pure TypeScript, no React, structurally compatible with @fortemi/core data |
What You Can Build
- Static documentation relationship maps rendered from a precomputed snapshot
- Custom React, Svelte, or Vue graph views that share Fortemi's projection logic
- Server- or build-time SVG generation of AIWG relationship graphs
- Selection and "expand neighborhood" interactions over a community graph
- Themed community visualizations using a custom color palette
Architecture at a Glance
@fortemi/core produces a CommunityGraph and detects communities. @fortemi/graph consumes that shape; it depends on @fortemi/core but never on React, and @fortemi/core never depends on it. filterCommunityGraph narrows what is shown, layoutCommunityGraph assigns deterministic coordinates, and the color/degree/bounds helpers turn the positioned graph into draw calls. serializeGraphSnapshot freezes a graph to portable JSON for hosts that render without recomputing it.
Installation
pnpm add @fortemi/graph
# or
npm install @fortemi/graphNo peer dependencies. Ships ESM with type declarations and works in browsers, Node, and bundlers.
Quick Start
import {
filterCommunityGraph,
layoutCommunityGraph,
colorForCommunity,
nodeRadius,
type CommunityGraph,
} from '@fortemi/graph'
const graph: CommunityGraph = {
nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
edges: [{ source: 'a', target: 'b', weight: 1, kind: 'similarity' }],
communities: [{ id: 'c1', nodes: ['a', 'b'] }],
}
const visible = filterCommunityGraph(graph, { edgeKinds: ['similarity'] })
const { nodes, nodeIndex } = layoutCommunityGraph(visible, {
algorithm: 'community',
width: 800,
height: 600,
})
for (const node of nodes) {
// deterministic coordinates, degree-based radius, community color
console.log(node.id, node.x, node.y, nodeRadius(node.degree), colorForCommunity(node.communityId))
}Data Model
interface GraphNode { id: string }
interface GraphEdge { source: string; target: string; weight: number; kind?: string }
interface GraphCommunity { id: string; nodes: string[] }
interface CommunityGraph {
nodes: GraphNode[]
edges: GraphEdge[]
communities: GraphCommunity[]
}These shapes are structurally identical to the ones @fortemi/core produces (GraphRepository.buildLinkGraph(), buildSimilarityGraph(), and aiwgFortemiIndexToCommunityGraph()), so a core graph drops straight into these helpers without conversion.
API
| Helper | Purpose |
|---|---|
| layoutCommunityGraph(graph, opts?) | Deterministic 2D positions — a seeded force settlement (link attraction, charge repulsion, collision spacing, community cohesion, centering, bounds clamping) or closed-form radial/community/manual. Returns per-node x, y, r, degree, community, plus community centroids |
| filterCommunityGraph(graph, filter?) | Filter by community, edge kind, node allow-list, or predicate; drops emptied communities |
| computeDegrees(graph) / nodeRadius(degree, opts?) | Undirected degree map and degree → render radius |
| colorForCommunity(id, palette?) | Deterministic community → color (themeable palette) |
| computeGraphBounds(nodes) / fitGraphToViewport(bounds, viewport, opts?) | Bounding box and a centered fit transform |
| neighborsOf / expandNeighborhood / subgraphForNodes / neighborhoodSubgraph / buildAdjacency | Selection and BFS neighborhood expansion |
| serializeGraphSnapshot / stringifyGraphSnapshot / deserializeGraphSnapshot | Stable, reproducible static snapshots for JS-only hosts |
| mapCommunityGraph(graph, opts?) | CommunityGraph → render-ready RenderGraph (labels, degree size, per-community tone, baked positions) shared by every renderer tier |
| bakeRenderGraph(graph, opts?) / stringifyRenderGraph | Build-time writer: run layout once, emit a deterministic baked-position snapshot |
| loadRenderSnapshot(source, opts?) | Snapshot-first warm start: load a precomputed baked-position graph (URL/object/thunk); null → fall back to a live build |
Layout options (force)
The force algorithm runs a fixed-iteration, seeded settlement — fully synchronous and headless (no animation frames), so identical (graph, options) always yields identical coordinates. Suitable for static SVG generation, SSR, and browser rendering. The radial, community, and manual algorithms are closed-form and honor width/height/boundsPadding only.
| Option | Default | Purpose |
|---|---|---|
| algorithm | 'force' | force (settlement) · radial · community · manual (closed-form) |
| width / height | 760 / 460 | Canvas size |
| seed | 1 | PRNG seed for initial jitter (identical seed ⇒ identical output) |
| ticks | 300 | Settlement iterations |
| nodeRadius | degree-derived | Per-node radius: a fixed number, NodeRadiusOptions, or (degree, node) => number |
| linkDistance / linkStrength | 60 / 0.08 | Spring target length and stiffness |
| chargeStrength | -240 | Repulsion magnitude (negative pushes apart) |
| collisionPadding | 2 | Extra spacing beyond r_i + r_j |
| communityStrength | 0.05 | Pull toward the node's community centroid |
| boundsPadding | 24 | Keep every node center this many px from each edge |
Each positioned node carries a stable render radius r (degree-derived by default, overrideable via nodeRadius), and the result includes communities centroids over the final positions.
All helpers are pure: they never mutate their inputs and (except for an optional snapshot timestamp) produce identical output for identical input.
What You Get
- Deterministic, dependency-free graph projection that renders identically across hosts
- One source of truth for layout, filtering, coloring, and sizing shared with
@fortemi/react - Portable snapshots so a static site can render an AIWG graph without React or PGlite
- Full TypeScript types, re-exported
CommunityGraphmodel, ESM output
Produce a Snapshot from @fortemi/core
The core package builds the graph; the graph package serializes it to a static file. Core does not depend on graph — you compose them at the call site:
import { GraphRepository } from '@fortemi/core'
import { stringifyGraphSnapshot } from '@fortemi/graph'
import { writeFileSync } from 'node:fs'
const graph = await new GraphRepository(db).buildLinkGraph()
writeFileSync(
'public/graph-snapshot.json',
stringifyGraphSnapshot(graph, { layout: { algorithm: 'community', width: 800, height: 600 } }),
)Choosing a Renderer
The same CommunityGraph can be drawn by more than one renderer tier. Every tier honors the shared GraphControlContract — algorithm, filters (community / edge-kind / node allow-list / minDegree), selectedNodeId + onSelectNode, onNavigate, labelFor, colors — so switching tiers is mechanical: pass the same options object.
| Tier | Import | Deps | Best for |
|---|---|---|---|
| JS-only SVG | renderCommunityGraph (@fortemi/graph) | none | Static sites, SSR/snapshot pipelines, no-framework hosts |
| React (static) | GraphView (@fortemi/react) | React | React apps wanting the component + hooks ecosystem |
| Interactive 2D | SigmaGraphView (@fortemi/react/graph-2d) | sigma + graphology (optional, lazy) | Live force settling, camera focus, LOD labels |
| 3D | ForceGraph3DView (@fortemi/react/graph-3d) | react-force-graph-3d (optional, lazy) | Orbitable 3D force graph, 2D/3D toggle |
communityLegend(graph) returns { communityId, color, count }[] — the shared data any tier can render as a legend and use to drive the communityIds show/hide filter. applyControlFilters(graph, filters) is the one filter function every tier runs, so visibility is identical across tiers.
Render in a Vanilla JS Host (no React)
renderCommunityGraph is a batteries-included SVG renderer built on the engine — zoom/pan, hover-neighborhood highlighting, click + keyboard selection, navigation, a labeled popup, and focus(id) search — with no React and no canvas library. It stays pixel-comparable to @fortemi/react's GraphView:
import { renderCommunityGraph, deserializeGraphSnapshot } from '@fortemi/graph'
const graph = deserializeGraphSnapshot(await (await fetch('./graph-snapshot.json')).json())
const view = renderCommunityGraph(document.getElementById('graph'), graph, {
algorithm: 'community',
filters: { edgeKinds: ['similarity', 'link'], minDegree: 1 },
labelFor: (id) => graph.nodes.find((n) => n.id === id)?.id ?? id,
onSelectNode: (id) => console.log('selected', id),
onNavigate: (id) => location.assign(`#/note/${id}`),
})
view.update({ filters: { communityIds: ['c1'] } }) // reactive filter change
view.focus('note-42') // search / focus a node
// view.destroy() // tear downThe React tier takes the same contract fields, so a consumer can offer both and switch with no rewiring:
import { GraphView } from '@fortemi/react'
<GraphView
graph={graph}
layout={{ algorithm: 'community' }}
filters={{ edgeKinds: ['similarity', 'link'], minDegree: 1 }}
selectedNodeId={selected}
onSelectNode={setSelected}
onNavigate={(id) => navigate(`/note/${id}`)}
labelFor={(id) => titleOf(id)}
/>Roll your own projection
If you need a fully custom view, the projection helpers are exposed directly — renderCommunityGraph is just a consumer of them — so you can emit your own SVG/canvas with no PGlite:
import {
deserializeGraphSnapshot,
filterCommunityGraph,
layoutCommunityGraph,
computeGraphBounds,
fitGraphToViewport,
colorForCommunity,
nodeRadius,
} from '@fortemi/graph'
const width = 800
const height = 600
const snapshot = await (await fetch('./graph-snapshot.json')).json()
const graph = deserializeGraphSnapshot(snapshot)
// 1. filter (e.g. hide a private community, or keep only certain edge kinds)
const visible = filterCommunityGraph(graph, { edgeKinds: ['similarity', 'link'] })
// 2. lay out — deterministic, so the same data always renders identically
const { nodes, edges, nodeIndex } = layoutCommunityGraph(visible, {
algorithm: 'community',
width,
height,
})
// 3. fit the laid-out graph into the viewport
const view = fitGraphToViewport(computeGraphBounds(nodes), { width, height }, { padding: 24 })
// 4. project to SVG
const line = (e) => {
const a = nodeIndex.get(e.source)
const b = nodeIndex.get(e.target)
if (!a || !b) return ''
const w = Math.max(1, Math.min(5, e.weight))
return `<line x1="${a.x}" y1="${a.y}" x2="${b.x}" y2="${b.y}" stroke="#9aa0a6" stroke-width="${w}" opacity="0.55" />`
}
const circle = (n) =>
`<circle cx="${n.x}" cy="${n.y}" r="${nodeRadius(n.degree)}" ` +
`fill="${colorForCommunity(n.communityId)}" stroke="#fff" stroke-width="1.5">` +
`<title>${n.id}</title></circle>`
document.getElementById('graph').innerHTML = `
<svg viewBox="0 0 ${width} ${height}" xmlns="http://www.w3.org/2000/svg" width="100%">
<g transform="translate(${view.offsetX} ${view.offsetY}) scale(${view.scale})">
${edges.map(line).join('')}
${nodes.map(circle).join('')}
</g>
</svg>`The same helpers back @fortemi/react's GraphView, so a React app and a static site stay visually aligned.
Determinism
layoutCommunityGraph, colorForCommunity, filterCommunityGraph, and serializeGraphSnapshot are deterministic functions of their inputs. That makes them safe for build-time snapshot generation, content-addressable caching, and stable visual diffs. serializeGraphSnapshot sorts nodes, edges, and community members so equal graphs serialize byte-for-byte identically; pass generatedAt only when you want a timestamp in the output.
Community detection lives in @fortemi/core (the base layer); this package only projects graphs it is given.
Documentation
| Guide | Description |
|---|---|
| API Reference | Full @fortemi/graph, @fortemi/core, and @fortemi/react surface |
React Bindings
Using React? @fortemi/react ships a GraphView component built on these helpers, plus hooks for building and caching graphs from a live archive.
License
AGPL-3.0-only.
