Skip to content

Latest commit

 

History

History
749 lines (561 loc) · 25.1 KB

File metadata and controls

749 lines (561 loc) · 25.1 KB

OrbitGraph API Reference

OrbitGraph is a TypeScript library for exploring connected data in interactive 3D. It is designed for relationship graphs that can start small, load data on demand, and reveal only the connections a user needs.

This reference covers the public APIs provided by the @orbitgraph/core, @orbitgraph/three, and @orbitgraph/react packages.

Contents

Installation

Three.js / Vanilla JavaScript

npm install @orbitgraph/core @orbitgraph/three three

React

npm install @orbitgraph/core @orbitgraph/three @orbitgraph/react three

Packages

Package Use it for
@orbitgraph/core Types, graph utilities, analytics, community detection, and data-source contracts.
@orbitgraph/three The Three.js/WebGL renderer and imperative graph instance.
@orbitgraph/react The declarative React component and its imperative ref API.
@orbitgraph/vue Vue 3 component with all runtime events, reactive inputs, cleanup, and full instance access.
@orbitgraph/svelte Svelte 5 action/controller with all runtime callbacks, reactive inputs, cleanup, and full instance access.
@orbitgraph/headless Stateful Node.js/SSR editing, history, routes, diff, analytics, layouts, JSON, SVG and PDF without DOM.

Quick start

import { createOrbitGraph } from "@orbitgraph/three";
import type { GraphData } from "@orbitgraph/core";

const data: GraphData = {
    nodes: [
        { id: "team", label: "Product Team", type: "team", color: "#22d3ee" },
        { id: "api", label: "Public API", type: "service", color: "#3b82f6" },
    ],
    links: [
        { id: "team-owns-api", source: "team", target: "api", type: "owns", weight: 0.95 },
    ],
};

const container = document.querySelector<HTMLElement>("#graph");

if (!container) {
    throw new Error("Graph container was not found.");
}

const graph = createOrbitGraph(container, {
    backgroundColor: "#050816",
    labels: { mode: "important", importantNodeIds: ["team"] },
    miniMap: { enabled: true, interactive: true },
});

graph.setData(data);

Call graph.destroy() when the container is permanently removed.

Data model

GraphNode

type GraphNode = {
    id: string;
    label?: string;
    type?: string;
    size?: number;
    color?: string;
    data?: Record<string, JSONValue>;
};

id must be unique. type is useful for filtering, labels, styling, and grouping. data preserves arbitrary JSON-compatible metadata and is returned in interaction callbacks.

GraphLink

type GraphLink = {
    id?: string;
    source: string;
    target: string;
    weight?: number;
    type?: string;
    color?: string;
    data?: Record<string, JSONValue>;
};

Links are directed from source to target. A weight normally ranges from 0 to 1; it affects link appearance and force-layout behavior.

GraphData

type GraphData = {
    nodes: GraphNode[];
    links: GraphLink[];
};

Every link should reference nodes present in the same data set. OrbitGraph can generate a relationship ID when one is omitted, but explicit IDs are recommended for updates and remote data.

Creating a graph

import { createOrbitGraph } from "@orbitgraph/three";

const graph = createOrbitGraph(container, options);

createOrbitGraph() returns an OrbitGraph instance. The full data set stays in memory, while exploration and filters determine the subset mounted in WebGL and the force simulation.

Options

type OrbitGraphOptions = {
    backgroundColor?: string;
    nodeColor?: string;
    nodeSize?: number;
    linkColor?: string;
    linkOpacity?: number;

    initialView?: GraphInitialView;
    layout?: GraphLayout;
    layoutOptions?: GraphLayoutOptions;
    dataSource?: GraphDataSource;

    linkFlow?: LinkFlowOptions;
    camera?: OrbitGraphCameraOptions;
    physics?: OrbitGraphPhysicsOptions;
    accessibility?: GraphAccessibilityOptions;
    mobileControls?: GraphMobileControlsOptions;
    labels?: GraphLabelsOptions;
    miniMap?: GraphMiniMapOptions;

    onNodeClick?: (event: NodeClickEvent) => void;
    onLinkClick?: (event: LinkClickEvent) => void;
    onNodeHover?: (event: NodeHoverEvent) => void;
    onLinkHover?: (event: LinkHoverEvent) => void;
    onSelectionChange?: (selection: GraphSelection) => void;
    onVisibleDataChange?: (data: VisibleGraphData) => void;
    onLoadingChange?: (state: GraphLoadingState) => void;
    onDiagnostic?: (diagnostic: GraphDiagnostic) => void;
    onKeyboardFocusChange?: (node: GraphNode | null) => void;
    ui?: GraphUIRenderers;
    performance?: GraphPerformanceOptions;
};

Initial views

initialView decides what is visible immediately after setData().

// Render every node and link.
{ mode: "all" }

// Render exactly one known node.
{ mode: "node", nodeId: "team" }

// Render a node and its relationship neighborhood.
{
    mode: "neighborhood",
    nodeId: "team",
    depth: 2,
    direction: "outgoing",
    relationshipTypes: ["owns", "uses"],
}

// Render a limited category of nodes.
{ mode: "type", nodeType: "service", maxNodes: 100 }

direction is one of "incoming", "outgoing", or "both".

Link flow

linkFlow: {
    enabled: true,
    maxParticles: 100,
    particleSize: 0.08,
    particleSpeed: 0.12,
}

Animated flow is optional. Disable it for the lowest rendering cost on large graphs.

Clustering, collaboration, and UI hooks

graph.clusterCommunities() detects communities in the visible graph, applies a stable color overlay, and returns GraphCluster[]. Call collapseCluster(id) to temporarily hide its members and expandCluster(id) to restore them. getClusters() returns the current state.

Collapsed clusters render a summary node with member count and internal/external relationship metrics. External relationships are consolidated by source, target, and type while the original graph remains unchanged.

graph.enableClusterLevelOfDetail();
graph.enableClusterLevelOfDetail(120); // optional camera-distance threshold
graph.isClusterLevelOfDetailEnabled();
graph.disableClusterLevelOfDetail();

At 5,000+ nodes OrbitGraph starts with aggregate type groups, split into chunks of at most 1,000 members. Untyped graphs use deterministic segments. Zooming in expands only the cluster nearest the camera target; zooming out returns to summaries. disableClusterLevelOfDetail() explicitly mounts full detail and should be used carefully on massive data.

graph.setAdvancedFilters({
  minimumLinkWeight: 0.4,
  maximumLinkWeight: 0.9,
  attributes: [{ field: "region", operator: "contains", value: "north" }],
});

const shared = graph.shareView();
graph.loadSharedView(shared);
graph.saveBookmark("review", "Review graph");

addAnnotation, getAnnotations, removeAnnotation, saveBookmark, getBookmarks, and restoreBookmark operate through the serializable in-memory collaboration store. Persist graph.collaboration.export() in the host application to synchronize it with a server or collaboration provider.

ui.renderTooltip and ui.renderDetails receive the current GraphSelection and return an HTMLElement (or null). OrbitGraph mounts that element but leaves framework integration and styling to the application. Set accessibility.semanticView to render an updated semantic list of visible nodes.

performance.telemetry enables approximately one-second samples through onPerformanceSample({ fps, visibleNodes, visibleLinks }). levelOfDetail is enabled by default and limits persistent labels for views over 1,000 nodes.

Importing data

@orbitgraph/core exports importCSVNodes, importCSVLinks, importJSON, importCytoscape, importJSONLD, and importNeo4j. The CSV helpers recognize standard graph fields and place other columns in data; the Neo4j helper accepts a plain-object projection of driver records.

Editing, routes, streaming, and comparison

Use applyOperations() with GraphOperation records to add, remove, or update nodes and links. undo() and redo() restore bounded data snapshots. stream accepts a host-owned adapter with connect(onMessage, onError) and applies incoming operation batches without a full page reload.

findWeightedPath(sourceId, targetId) uses link weights as inverse traversal cost; findKShortestPaths(sourceId, targetId, count) returns edge-disjoint alternatives. compare(snapshot) returns added, removed, and changed nodes and links. setStyleRules() applies declarative styles by type, metadata, degree, or PageRank. selectNodes() and getSelectedNodeIds() provide framework-neutral multi-selection state.

Renderer extensions and exports

CanvasGraphRenderer draws active nodes and batches relationships into one Canvas path for low-power embedding. MapLibreGraphLayer writes nodes with longitude/latitude metadata to a MapLibre GeoJSON source. GraphPluginRegistry runs application-installed plugins and makes their data sources and style rules available at runtime. SVG is vector output; PDF is loaded on demand and returns a promise.

renderMode: "canvas" does not construct WebGLRenderer or request a WebGL context. WebGL uses InstancedMesh for nodes; frustum virtualization compacts the active instance count so off-camera nodes are omitted from the draw call.

Worker and CRDT runtime

const result = await graph.computeInWorker("radial", abortController.signal);
const session = graph.connectYjs(provider);
session.setPresence({ name: "Ada", color: "#22d3ee", cursor: { x: 10, y: 20 } });
const operations = [{ type: "add-node", node: { id: "new" } }] as const;
graph.applyOperations(operations);
session.applyLocalOperations(operations);

Worker payloads use transferable typed arrays for positions, edge indices, weights, type indices and cluster assignments. GraphYjsProvider is transport-neutral: WebSocket, WebRTC and persisted Yjs providers can forward document and awareness updates.

Headless / SSR

import { HeadlessOrbitGraph } from "@orbitgraph/headless";
const serverGraph = new HeadlessOrbitGraph(data);
serverGraph.applyOperations([{ type: "add-node", node: { id: "new" } }]);
const analytics = serverGraph.analyze();
const positions = serverGraph.layout("radial");
const svg = serverGraph.exportSVG();
const pdf = await serverGraph.exportPDF({ title: "Graph report" });

HeadlessOrbitGraph also provides undo(), redo(), selectNodes(), compare(), findWeightedPath(), findKShortestPaths(), validated operations, and JSON export. Stateless helper functions remain available for one-shot jobs.

const svg = graph.exportSVG();
graph.downloadSVG("graph.svg");
const pdf = await graph.exportPDF({ title: "Architecture report" });
await graph.downloadPDF("report.pdf", { title: "Architecture report" });

Instance API

Data

graph.setData(data);

graph.addNode(node);
graph.removeNode(nodeId);

graph.addLink(link);
graph.removeLink(linkId);

setData() replaces the complete loaded graph, clears temporary analytics styles, clears exploration history, and returns to the configured initial view.

Exploration

graph.setInitialView({ mode: "node", nodeId: "team" });

graph.expandNode("team", {
    depth: 1,
    direction: "outgoing",
    relationshipTypes: ["owns", "uses"],
    limit: 25,
    offset: 0,
});

graph.collapseNode("team");
graph.resetExploration();
graph.showAll();

expandNode() accepts GraphExpansionOptions:

Option Meaning
depth Number of relationship levels to reveal. Defaults to 1.
direction incoming, outgoing, or both. Defaults to both.
relationshipTypes Optional relationship types to include.
limit Optional page size for immediate local expansion.
offset Optional relationship offset for immediate local expansion.

Additional exploration helpers:

graph.focusPath("team", "database", { direction: "outgoing" });
graph.goBack();
graph.goForward();

const state = graph.getNodeExplorationState("team");
const history = graph.getExplorationHistory();

getNodeExplorationState() is useful for custom Expand buttons. It reports whether the node is expanded, visible neighbors, hidden neighbors, and whether expansion is available.

Search and filters

Filters refine the explored subset; they never expose hidden nodes.

graph.search("payments");
graph.toggleTypeFilter("service");
graph.setTypeFilters(["team", "service"]);
graph.setMinimumLinkWeight(0.7);
graph.clearFilters();

Remote and GraphQL data

Generic data source

Use GraphDataSource for on-demand loading. It keeps your API client independent from OrbitGraph.

import type { GraphDataSource } from "@orbitgraph/core";

const dataSource: GraphDataSource = {
    async getNode(nodeId) {
        const response = await fetch(`/api/graph/nodes/${nodeId}`);
        return response.json();
    },

    async getNeighborhood({ nodeId, direction, limit, offset }) {
        const params = new URLSearchParams({
            direction: direction ?? "both",
            limit: String(limit ?? 25),
            offset: String(offset ?? 0),
        });
        const response = await fetch(`/api/graph/nodes/${nodeId}/neighbors?${params}`);
        return response.json();
    },
};

const graph = createOrbitGraph(container, { dataSource });
await graph.loadNode("team");

const result = await graph.loadNeighborhood("team", {
    direction: "outgoing",
    limit: 25,
    offset: 0,
});

loadNeighborhood() merges the received nodes and links, refreshes the visual view, and caches the request. Pass force: true in load options when the same page must be requested again.

GraphQL

@orbitgraph/three also exports createGraphQLDataSource(). It is an optional adapter: OrbitGraph itself does not require GraphQL.

Use it to map your query response into GraphNode, GraphLink, and GraphNeighborhoodResult, then pass the resulting data source through dataSource.

import { createGraphQLDataSource } from "@orbitgraph/three";

const dataSource = createGraphQLDataSource({
    endpoint: "/graphql",
    // Configure your documents and map each response to OrbitGraph data.
});

See the GraphQL data-source type definitions in your editor for the response mapping configuration required by your schema.

Layouts and physics

graph.setLayout("force");

graph.setLayout("radial", { spacing: 14 });
graph.setLayout("grid", { spacing: 12 });
graph.setLayout("hierarchical", {
    rootId: "team",
    direction: "outgoing",
    spacing: 16,
});

Supported layouts are "force", "radial", "grid", "hierarchical", "dag", "sankey", "timeline", "bipartite", "geographic", "concentric", "sphere", and "arc". DAG uses Dagre to rank dependencies and reduce crossings. Sankey uses d3-sankey with widths proportional to weight. Timeline reads layoutOptions.timeField (default "time"), bipartite uses bipartiteTypes, and geographic reads longitude/latitude metadata fields.

The active renderer and physics consolidate only parallel links with equal endpoints and type. Their data includes aggregateCount, aggregateWeight, and defined aggregatedLinkIds; analytics, filters and exports retain every original link.

Worker physics

const graph = createOrbitGraph(container, {
    physics: {
        worker: true,
        tickRate: 60,
    },
});

Physics uses a module Web Worker when the browser supports it, preserving UI responsiveness while D3 calculates force-layout positions. It automatically falls back to the main thread for SSR, test environments, unsupported browsers, or worker initialization failures.

Pinning

Users can drag a node to pin it. Release it programmatically with:

graph.unpinNode("team");

Camera, touch, and keyboard controls

const graph = createOrbitGraph(container, {
    camera: {
        minDistance: 2,
        maxDistance: 100_000,
        movementSpeed: 18,
        boostMultiplier: 2.5,
        keyboardNavigation: true,
    },
});

graph.focusNode("team");
graph.resetCamera();
graph.setCameraMovementSpeed(120);

Camera fits animate smoothly and pointer, wheel, touch, or WASD input cancels an in-progress fit immediately.

Desktop controls:

  • Left drag: orbit.
  • Right drag: pan.
  • Mouse wheel: zoom.
  • W, A, S, D: move horizontally when keyboard camera navigation is enabled.
  • Q and E: move vertically.
  • Shift: movement boost.

Touch controls:

  • One finger: orbit.
  • Two fingers: pan and pinch-to-zoom.

Keyboard graph navigation is configured through accessibility. It supports focus movement across visible nodes, activation, expansion, collapse, camera focus, and clearing selection. Use onKeyboardFocusChange to mirror keyboard focus in external UI.

Labels and mini-map

Intelligent labels

labels: {
    mode: "important",
    importantNodeIds: ["team", "payments-api"],
    maxVisible: 20,
    showNodeType: true,
    fontScale: 1,
}
Option Description
mode hover, selected, important, or all.
importantNodeIds Nodes that should retain a label in important mode.
maxVisible Limit for persistent labels, preventing visual overload.
showNodeType Draw the node type as a smaller second line.
fontScale Adjust label text and padding scale.

The hover label remains available independently, so users can inspect a node even when persistent labels are intentionally limited.

Mini-map

miniMap: {
    enabled: true,
    position: "bottom-right",
    width: 180,
    height: 120,
    interactive: true,
    showViewport: true,
    ariaLabel: "Graph overview",
}

The mini-map visualizes the currently visible graph rather than the hidden source data. With interactive: true, clicking it moves the camera target.

Analytics and visual presentation

The graph instance exposes two focused controllers:

graph.analytics;
graph.presentation;

Centrality

const degree = graph.analytics.degree({ scope: "visible" });
const pageRank = graph.analytics.pageRank({ scope: "visible" });
const betweenness = graph.analytics.betweenness({
    scope: "visible",
    normalized: true,
});

scope is "all" by default or "visible" for the explored and filtered subset.

  • degree() returns degree, in-degree, out-degree, and weighted variants for every node.
  • pageRank() returns ranking scores and convergence metadata.
  • betweenness() returns bridge importance for every node.

Communities

const result = await graph.analytics.detectCommunitiesAsync({
    scope: "visible",
    weighted: true,
});

console.log(result.communities);

Use detectCommunities() for synchronous execution on small graphs. Prefer detectCommunitiesAsync() in interactive interfaces because it yields between passes. For very large graphs, run analysis in application-managed background work.

Turning metrics into a visual explanation

Analytics deliberately do not mutate the graph automatically. Apply temporary styles through presentation:

const pageRank = graph.analytics.pageRank({ scope: "visible" });

graph.presentation.setNodeStyles({
    "payments-api": {
        color: "#facc15",
        scale: 1.65,
        glow: 0.9,
    },
});

graph.presentation.clearNodeStyles();

This separation lets your application decide whether a metric affects color, size, glow, a legend, a details panel, or nothing at all.

Events, loading, and diagnostics

Selection and pointer events

const graph = createOrbitGraph(container, {
    onNodeClick: ({ node }) => console.log(node),
    onLinkClick: ({ link }) => console.log(link),
    onNodeHover: ({ node }) => console.log(node),
    onLinkHover: ({ link }) => console.log(link),
    onSelectionChange: (selection) => console.log(selection),
});

GraphSelection is one of:

type GraphSelection =
    | { kind: "node"; node: GraphNode }
    | { kind: "link"; link: GraphLink }
    | null;

Visible data

onVisibleDataChange: ({ nodes, links }) => {
    visibleCount.textContent = `${nodes.length} nodes · ${links.length} links`;
}

This callback reports the data currently mounted after exploration and filters. It is the right source for counters, mini summaries, and visible-scope analytics UI.

Loading and diagnostics

onLoadingChange: (state) => {
    if (state.loading) {
        console.log(`Loading ${state.operation} for ${state.nodeId}`);
    }
},

onDiagnostic: (diagnostic) => {
    console.error(diagnostic.code, diagnostic.message);
},
const state = graph.getLoadingState();
// {
//   loading: boolean,
//   operation: "node" | "neighborhood" | null,
//   nodeId: string | null,
//   error: GraphLoadError | null,
// }

Use diagnostics to show application-level retry messages. OrbitGraph does not display network errors by itself, which keeps your product’s error handling and wording under your control.

Export and view state

Images and JSON

const png = await graph.exportPNG();
await graph.downloadPNG("architecture.png");

const fullJSON = graph.exportJSON({ scope: "all" });
const visibleJSON = graph.exportJSON({ scope: "visible" });
graph.downloadJSON({ scope: "visible", fileName: "explored-subgraph.json" });

const vectorSVG = graph.exportSVG();
const reportPDF = await graph.exportPDF({ title: "Graph report" });

Persisting user exploration

const viewState = graph.exportViewState();
localStorage.setItem("graph-view", JSON.stringify(viewState));

const saved = localStorage.getItem("graph-view");

if (saved) {
    graph.importViewState(JSON.parse(saved));
}

View state includes exploration, filters, layout, and layout options. It does not replace your graph data; load data first, then restore the view.

React

import { useRef } from "react";
import { OrbitGraph, type OrbitGraphHandle } from "@orbitgraph/react";
import type { GraphData } from "@orbitgraph/core";

export function GraphView({ data }: { data: GraphData }) {
    const graphRef = useRef<OrbitGraphHandle>(null);

    return (
        <>
            <button onClick={() => graphRef.current?.focusNode("team")}>
                Focus team
            </button>

            <OrbitGraph
                ref={graphRef}
                data={data}
                style={{ width: "100%", height: "100vh" }}
                options={{
                    miniMap: { enabled: true },
                    labels: { mode: "important", importantNodeIds: ["team"] },
                }}
                onSelectionChange={(selection) => console.log(selection)}
                onDiagnostic={(diagnostic) => console.error(diagnostic)}
            />
        </>
    );
}

The ref exposes the same user-facing actions as the Three.js instance, including exploration, camera movement, remote loading, exports, loading state, analytics, and presentation controllers.

Keep graph data declarative through the data prop. Use the ref for imperative user actions such as expanding a branch, exporting, or focusing a node.

Performance guidance

  • Start with initialView: { mode: "node" }, neighborhood, or type for large data sets.
  • Leave automatic aggregate LOD enabled for 5,000+ nodes; semantic zoom materializes one nearby cluster at a time.
  • Frustum culling hides already-created offscreen objects. It reduces drawing but does not remove existing GPU allocations.
  • Hidden nodes are excluded from WebGL mounting and the active physics simulation.
  • Keep collision forces enabled for smaller graphs; OrbitGraph automatically disables the expensive collision force above its threshold.
  • Leave animated link flow disabled unless it adds meaningful value.
  • Keep persistent labels capped with labels.maxVisible.
  • Use the Worker physics mode for responsive interaction while larger force layouts settle.
  • Use loadNeighborhood() with limit and offset instead of loading an entire remote graph at once.
  • Run community detection asynchronously in interactive UI and prefer scope: "visible" when users are exploring a subset.
  • In Graph Scale Lab, Worker progress covers data generation; renderer construction and camera framing are reported as separate stages and may be cancelled before handoff.

Lifecycle

graph.destroy();

Always call destroy() when removing a non-React graph. It releases rendering resources, controls, events, active physics work, workers, overlays, and the canvas element.