@molecule/app-feature-graph-view-react
Feature · graph-view · App (browser) · v1.0.2 · Apache-2.0
Force-directed graph view (Obsidian-style) for visualizing note / page linkages
npm install @molecule/app-feature-graph-view-reactHow it works
@molecule/app-feature-graph-view-react is a ready-made graph-view feature for the app (browser) side. It composes the core interfaces it needs, so it works with whichever providers your app has bonded.
import { GraphView, type GraphNode, type GraphEdge } from '@molecule/app-feature-graph-view-react'
function NoteGraph({ notes, links }: { notes: Note[]; links: Link[] }) {
const nodes: GraphNode[] = notes.map((n) => ({ id: n.id, label: n.title, weight: n.linkCount }))
const edges: GraphEdge[] = links.map((l) => ({ id: l.id, source: l.from, target: l.to }))
return <GraphView nodes={nodes} edges={edges} onNodeClick={(n) => open(n.id)} />
}Works with: @molecule/app-react, @molecule/app-ui
Reference
React force-directed graph view (Obsidian-style).
Exports:
<GraphView>— top-level node-and-edge visualization with built-inforce,circular, andgridlayouts, click + selection support, and pluggable node / edge renderers.GraphNode,GraphEdge,GraphLayout,GraphPoint,PositionedNode,GraphNodeRenderer,GraphEdgeRenderertypes.- Pure layout helpers (
forceLayout,circularLayout,gridLayout,layoutNodes,boundingBox,createRng).
Used by note-taking and other apps to visualize page / note linkages the same way Obsidian does — a force-directed map of which notes link to which.
Auto-generated, AI-first package reference for the molecule.dev ecosystem. It is written to be read by coding agents as much as by people, and is generated from this package's source — edit
src/index.tsJSDoc, not this file.
Quick Start
import { GraphView, type GraphNode, type GraphEdge } from '@molecule/app-feature-graph-view-react'
function NoteGraph({ notes, links }: { notes: Note[]; links: Link[] }) {
const nodes: GraphNode[] = notes.map((n) => ({ id: n.id, label: n.title, weight: n.linkCount }))
const edges: GraphEdge[] = links.map((l) => ({ id: l.id, source: l.from, target: l.to }))
return <GraphView nodes={nodes} edges={edges} onNodeClick={(n) => open(n.id)} />
}
Type
feature
Installation
npm install @molecule/app-feature-graph-view-react @molecule/app-react @molecule/app-ui react
npm install -D @types/react
API
Interfaces
GraphViewProps
<GraphView> props.
interface GraphViewProps {
/** Nodes to render. */
nodes: GraphNode[]
/** Edges to render. */
edges: GraphEdge[]
/** Fired when the user clicks (or activates) a node. */
onNodeClick?: (node: GraphNode) => void
/** Currently-selected node id (highlighted in the view). */
selectedNodeId?: string
/** Layout strategy. Defaults to `'force'`. */
layout?: GraphLayout
/** Custom node renderer (overrides the default circle + label). */
nodeRenderer?: GraphNodeRenderer
/** Custom edge renderer (overrides the default `<line>`). */
edgeRenderer?: GraphEdgeRenderer
/** Force-layout tunables; ignored for non-force layouts. */
forceOptions?: ForceLayoutOptions
/** Extra classes merged onto the outer wrapper. */
className?: string
}
ForceLayoutOptions
Force-layout tunables.
interface ForceLayoutOptions {
/** Hard cap on iterations. Defaults to `DEFAULT_FORCE_ITERATIONS`. */
iterations?: number
/** Repulsive constant between every pair of nodes. */
repulsion?: number
/** Spring (Hooke) constant for edges. */
spring?: number
/** Ideal edge length (rest distance). */
restLength?: number
/** Velocity damping per step (0–1). */
damping?: number
/** Optional deterministic seed (for tests). Defaults to a fixed value. */
seed?: number
}
GraphPoint
A 2-D point in graph (world) coordinates.
interface GraphPoint {
/** X coordinate. */
x: number
/** Y coordinate. */
y: number
}
GraphNode
Single node in the graph.
group is an opaque grouping discriminator (e.g. tag, folder) the
caller can use for colouring; weight scales the rendered radius.
interface GraphNode {
/** Unique node id. */
id: string
/** Human-readable label rendered next to the node. */
label: string
/** Optional grouping discriminator (e.g. tag id). */
group?: string
/** Optional weight; default 1. Scales the rendered radius. */
weight?: number
}
GraphEdge
Edge connecting two nodes by id.
interface GraphEdge {
/** Unique edge id. */
id: string
/** Source node id. */
source: string
/** Target node id. */
target: string
/** Optional weight; default 1. Stronger edges pull harder under force layout. */
weight?: number
}
PositionedNode
Internal positioned node used by the renderer; the x/y coordinates
are in world (graph) space, with (0, 0) at the canvas centre.
interface PositionedNode extends GraphNode {
/** World-space X coordinate. */
x: number
/** World-space Y coordinate. */
y: number
}
Types
GraphLayout
Available layout strategies.
type GraphLayout = 'force' | 'circular' | 'grid'
GraphNodeRenderer
Renderer for an individual node. Receives the positioned node.
type GraphNodeRenderer = (node: PositionedNode) => ReactNode
GraphEdgeRenderer
Renderer for an individual edge. Receives the edge plus both endpoints.
type GraphEdgeRenderer = (
edge: GraphEdge,
source: PositionedNode,
target: PositionedNode,
) => ReactNode
Functions
GraphView(props)
Force-directed graph view (Obsidian-style).
Renders nodes + edges into an SVG, computing positions via a built-in
minimal velocity-Verlet simulation. Three layouts are supported —
force (default), circular and grid — and either nodes or edges
can be overridden via custom renderers.
Style is driven by getClassMap(). Inline styles are reserved for
SVG geometry — transform, viewBox computation, stroke widths — which
classes can't express.
function GraphView(
props: GraphViewProps,
): ReactElement<unknown, string | JSXElementConstructor<any>>
props— Component props.
Returns: The graph-view element.
createRng(seed)
Tiny deterministic PRNG (mulberry32). Allows reproducible initial
positions in tests without depending on Math.random().
function createRng(seed: number): () => number
seed— 32-bit unsigned seed.
Returns: A function returning floats in [0, 1).
circularLayout(nodes, radius)
Place nodes evenly around the unit circle (scaled by radius).
function circularLayout(nodes: GraphNode[], radius: number): PositionedNode[]
nodes— Nodes to position.radius— Circle radius in world units.
Returns: Positioned nodes, in input order.
gridLayout(nodes, spacing)
Place nodes on a square-ish grid, centred on the origin.
function gridLayout(nodes: GraphNode[], spacing: number): PositionedNode[]
nodes— Nodes to position.spacing— Pixel spacing between adjacent grid cells.
Returns: Positioned nodes, in input order.
forceLayout(nodes, edges, options)
Run a minimal velocity-Verlet force-directed simulation.
Implements:
- Pairwise repulsion (Coulomb-like, O(n²)) between every node pair.
- Spring (Hooke) attraction along each edge toward
restLength. - Per-step velocity damping for stability.
Stops early once the maximum per-node displacement-squared drops below
CONVERGENCE_EPSILON, or after iterations steps — whichever
comes first.
function forceLayout(
nodes: GraphNode[],
edges: GraphEdge[],
options?: ForceLayoutOptions,
): PositionedNode[]
nodes— Nodes to lay out.edges— Edges (used only for attractive springs).options— Force-layout tunables.
Returns: Positioned nodes, in input order.
layoutNodes(nodes, edges, layout, options)
Compute world-space positions for every node using the chosen layout.
function layoutNodes(
nodes: GraphNode[],
edges: GraphEdge[],
layout: GraphLayout,
options?: ForceLayoutOptions,
): PositionedNode[]
nodes— Nodes to position.edges— Edges (consumed only byforce).layout— Layout strategy.options— Optional force-layout tunables (ignored for non-force).
Returns: Positioned nodes.
boundingBox(nodes, padding)
Compute the world-space bounding box of a set of positioned nodes, with a small padding for visual breathing room.
function boundingBox(
nodes: PositionedNode[],
padding?: number,
): { minX: number; minY: number; maxX: number; maxY: number } | null
nodes— Positioned nodes.padding— Padding (world units) added to every side. Default 40.
Returns: Bounding box { minX, minY, maxX, maxY } — or null if empty.
Constants
DEFAULT_FORCE_ITERATIONS
Default cap on force-layout iterations.
const DEFAULT_FORCE_ITERATIONS: 200
DEFAULT_REPULSION
Default repulsion strength used by forceLayout.
const DEFAULT_REPULSION: 800
DEFAULT_SPRING
Default spring stiffness used by forceLayout.
const DEFAULT_SPRING: 0.05
DEFAULT_REST_LENGTH
Default ideal edge length used by forceLayout.
const DEFAULT_REST_LENGTH: 80
DEFAULT_DAMPING
Default velocity damping used by forceLayout.
const DEFAULT_DAMPING: 0.85
CONVERGENCE_EPSILON
Convergence threshold (max per-node displacement squared).
const CONVERGENCE_EPSILON: 0.01
Injection Notes
Requirements
Peer dependencies:
@molecule/app-react^1.0.1@molecule/app-ui^1.0.1react^18.0.0 || ^19.0.0
Runtime Dependencies
@molecule/app-react@molecule/app-uireact
The root element fills 100% of its parent — the PARENT must have an explicit height (e.g. a fixed-height panel or a flex/grid track) or the graph renders zero-tall and appears blank.
Interaction surface is click / keyboard-activate + selectedNodeId
highlighting only. Nodes are NOT draggable and there is no built-in
pan / zoom — the SVG viewBox auto-fits the layout bounds. For a
pannable / zoomable surface, compose with
@molecule/app-feature-canvas-react instead.
Layout is deterministic (seeded RNG), computed once per
(nodes, edges, layout, forceOptions) change. forceOptions is
ignored by the circular and grid layouts.
Translations
Translation strings are provided by @molecule/app-locales-feature-graph-view.