@fortemi/graph

Framework-agnostic graph tooling: layout, filtering, snapshots.

<div align="center">

@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/graph

<img src="https://img.shields.io/npm/v/@fortemi/graph/latest?label=npm&amp;color=CB3837&amp;logo=npm&amp;style=flat-square" alt="npm version"> <img src="https://img.shields.io/npm/dm/@fortemi/graph?color=CB3837&amp;logo=npm&amp;style=flat-square" alt="npm downloads"> License: AGPL-3.0-only <img src="https://img.shields.io/badge/node-%3E%3D22.0.0-brightgreen?style=flat-square&amp;logo=node.js" alt="Node Version"> <img src="https://img.shields.io/badge/TypeScript-5.x-blue?style=flat-square&amp;logo=typescript" alt="TypeScript"> <img src="https://img.shields.io/npm/v/aiwg?label=built%20with%20aiwg&amp;color=CB3837&amp;logo=npm&amp;style=flat-square" alt="Built with aiwg">

Install · Why Fortemi · Quick Start · API · Data Model · Docs · License

</div>


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.

NeedWhat 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 encodingDegree-based node sizing and deterministic community color assignment
Viewport mathBounding box plus a centered fit transform for SVG/canvas
InteractionNeighborhood expansion and induced-subgraph helpers for selection
Portable viewsStable snapshot serialization for static, precomputed graphs
Zero lock-inPure 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/graph

No 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

HelperPurpose
`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.

OptionDefaultPurpose
`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-derivedPer-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 `CommunityGraph` model, 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.

TierImportDepsBest for
JS-only SVG`renderCommunityGraph` (`@fortemi/graph`)noneStatic sites, SSR/snapshot pipelines, no-framework hosts
React (static)`GraphView` (`@fortemi/react`)ReactReact 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 down

The 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

GuideDescription
API ReferenceFull `@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.