OGJS

Documentation

The API examples on this page run against the published package. Contributor evidence commands run from a source checkout. The demo bench exercises each section live.

Install

npm install @mizansec/ogjs

The package ships compiled ES modules with TypeScript declarations and has no runtime dependencies. Evaluation is free for 30 days; production use needs a commercial license.

Quickstart

import { Ogjs } from '@mizansec/ogjs';

const og = new Ogjs({ container: '#viz' });

og.addNodes([
  { id: 'a', label: 'Alice', size: 8, color: '#4f8cff' },
  { id: 'b', label: 'Bob', icon: '👤' },
]);
og.addEdges([{ source: 'a', target: 'b' }]);

await og.layouts.forceGPU();
og.camera.fit();

The container needs a width and height; Ogjs fills it, watches it with a ResizeObserver, and creates its own canvases.

How it works

Three decisions drive the performance numbers on the landing page:

Advanced capabilities

These six areas share the same dense graph, camera, history, and rendering lifecycle. Follow a capability to its API contract or open the workbench to inspect it in a running graph.

CapabilityContractExample
Stable editingStable relationship IDs, rollback-safe synchronous transactions, and one-step undo.Run
Editing and edge presentationValidated gestures plus per-relationship routes, text, badges, outlines, halos, pulse, and visibility.Run
Worker insightsCancellable centrality, spanning-forest, and community jobs over a stable graph snapshot.Run
Temporal playbackNode and relationship lifespans, exact seek, trailing windows, brushing, and playback.Run
Vector geographyRaster tiles, anchored nodes, and styled GeoJSON through one camera and export stack.Run
Measured evidencePortable correctness gates plus compatible-profile visual and performance comparisons.Run
Projection pipelineNamed transactional node and edge fields reused by filters and styles.Run

Layouts

CallWhat it does
layouts.forceGPU(opts)Force-directed on the GPU. Falls back to the worker when float-render extensions are missing. Returns a promise that resolves at convergence.
layouts.force(opts)Same physics in a Web Worker; frames stream in without blocking.
layouts.hierarchical(opts)Layered BFS with barycenter ordering. Good for trees and DAGs.
layouts.radial(opts)BFS rings around a root, subtrees kept contiguous.
layouts.circular() / grid() / random()Instant deterministic arrangements.
layouts.stop()Halts a running layout, keeps current positions.

Force options: repulsion, springLength, springK, gravity, velocityDecay, maxVelocity, iterations. Use the performance workbench to choose options on the device and workload you will ship.

Graph integrity

Give relationships stable IDs when they must survive unrelated deletion and dense-slot movement. Public updates resolve the current slot from that id and validate the complete patch before changing stored state.

og.addNodes([
  { id: 'alice' }, { id: 'review' }, { id: 'approved' },
]);
og.addEdges([{ id: 'transfer-42', source: 'alice', target: 'review' }]);

og.edit('approve transfer', (draft) => {
  draft.updateEdge('transfer-42', {
    target: 'approved', caption: 'approved', width: 3,
  });
  draft.updateNode('approved', { color: '#16a34a' });
});

og.history.undo(); // restores both changes together

edit() is deliberately synchronous. It rejects nested transactions and promise-returning callbacks, rolls back completed commands if a later command fails, and records one undo entry when the callback succeeds. Direct typed-array writes stay outside history. Run the editing-history example to inspect stable identity and grouped undo.

Styling

og.styles.addNodeRule(({ degree, data }) => ({
  size: 4 + Math.sqrt(degree),
  color: data?.kind === 'server' ? '#e5484d' : '#4f8cff',
  icon: data?.kind === 'server' ? 'DB' : undefined,
}));
og.styles.addEdgeRule(({ data }) => ({
  width: data?.weight ?? 1,
  curvature: data?.kind === 'dependency' ? 0.18 : 0,
  dash: data?.pending ? [8, 5] : null,
  arrow: data?.directed === true,
  caption: data?.relationship,
  badge: data?.priority,
  outlineWidth: data?.reviewed ? 2 : 0,
  outlineColor: '#0f172a',
  haloWidth: data?.risk > 8 ? 5 : 0,
  haloColor: '#ef444455',
  pulse: data?.live ? 0.7 : 0,
}));

Rules run once per apply() and write straight into the typed arrays — render cost does not depend on rule complexity. Node icons take any short string or emoji; glyphs rasterize once into a shared atlas. Edge rules can assign curves, dashes, arrows, fixed-width outlines and halos, reduced-motion-aware pulse, captions, and badges per relationship. Call og.fanEdges() to spread same-endpoint links into symmetric deterministic routes. The same core properties remain available as renderer-wide defaults: og.renderer.edgeArrows = true, og.renderer.edgeCurvature = 0.15. Interaction feedback uses fixed screen-space geometry: a 2px white inner stroke and 5px colored outer stroke. Configure them with interactionInnerStrokeWidth and interactionOuterStrokeWidth. The smallest visible node stops at 50% of the view while zooming; set maxNodeViewRatio = null to remove that limit.

// Per-node label recipes stay in the bounded Canvas2D label layer
og.labels.setRecipe('database', {
  text: ['Database', 'Primary'], secondary: '99.99% uptime',
  font: '700 15px system-ui', color: '#1d4ed8', pill: '#eff6ff',
  placement: 'right', priority: 30, direction: 'ltr',
});

Run the edge-style gallery or the Forge workbench.

Grouping & filters

// collapse by any key; restore is exact
og.transformations.groupBy(({ data }) => data?.cluster ?? null);
og.transformations.ungroup();

// visual groups keep members visible; grow one without moving the others
og.groupVisually(({ data }) => data?.team ?? null);
og.addNodes([{ id: 'new', x: 0, y: 0, data: { team: 'Data' } }]);
await og.growVisualGroup('Data', 'new', 320);

// strict async expansion validates everything before the first write
await og.expandVisualGroupAsync('Data', async (key) => ({
  nodes: await fetch(`/api/groups/${key}`).then(r => r.json()),
}), 350);

// recursively nest existing root groups; child geometry stays rigid
await og.nestVisualGroups('Product', ['Platform', 'Data'], 350);
await og.nestVisualGroups('Company', ['Product', 'Business'], 350);

// predicate filters stack; hidden nodes drop out of picking and labels
og.transformations.addFilter(({ degree }) => degree > 2);
og.transformations.addEdgeFilter(({ data }) => data?.risk >= 5);
og.transformations.clearEdgeFilters(); // keep node predicates
og.transformations.clearFilters();     // clear both

// named fields are available in filters and styles; refresh after data changes
og.projections.defineNode('risk', ({ degree, data }) => data.base + degree * 10);
og.projections.defineEdge('exposure', ({ sourceValue, targetValue, data }) =>
  sourceValue('risk') + targetValue('risk') + data.amount);
og.transformations.addFilter(({ value }) => value('risk') >= 90);
og.styles.addNodeRule(({ value }) => ({ color: value('risk') >= 90 ? '#dc2626' : '#64748b' }));
og.updateNode('account', { data: { base: 90 } });
og.projections.refresh();

Projection values are transactional snapshots: call og.projections.refresh() after changing data or topology. Run the projection pipeline to inspect a single named field reused by filters and styles.

Timeline

og.timeline.setTimes(({ data }) => data?.timestamp);
og.mountTimeline();            // histogram + drag brush
og.timeline.setWindow(t0, t1); // or drive it programmatically

// overlapping node and relationship lifespans
og.timeline.setIntervals(
  ({ data }) => ({ start: data.opened, end: data.closed }),
  ({ data }) => ({ start: data.activeFrom, end: data.activeUntil }),
);
og.timeline.seek(t1, 30_000);  // exact trailing window
await og.timeline.play({ from: t0, to: t1, trail: 30_000, duration: 5000 });
og.timeline.pause();           // keeps the current frame

Temporal rules compose with independent node and edge filters. Accessors snapshot the current dense graph; run setTimes or setIntervals again after topology or temporal-data changes. Run the interval playback workbench.

Geo mode

const og = new Ogjs({ container: '#viz', geo: true });
og.geo.enable(({ data }) => data?.latlng);  // [lat, lng] per node

// GeoJSON coordinates are standard [longitude, latitude]
og.geo.vectors.set(featureCollection, (feature) => ({
  fill: feature.properties.color,
  fillOpacity: 0.2,
  stroke: '#2563eb',
  strokeWidth: 1.5,      // CSS pixels at every zoom
}));
og.geo.vectors.fit();
og.geo.vectors.clear(); // raster and anchored nodes stay active

Tiles come from OpenStreetMap by default (any XYZ template works) and draw beneath the transparent WebGL canvas. Projection helpers geoProject / geoUnproject are exported. Point, line, polygon, multi-, and geometry-collection GeoJSON render through the same camera and are included in PNG/PDF output. Replacing vectors validates the complete snapshot before changing the layer. Lines crossing the antimeridian split at the map seam; pre-split polygon rings that cross it. No mapping library required. Run the vector geography workbench.

Annotations

og.annotations.add({ type: 'text',  x, y, text: 'suspicious cluster' });
og.annotations.add({ type: 'arrow', x1, y1, x2, y2, label: 'flow' });
og.annotations.add({ type: 'rect',  x, y, w, h });
const saved = og.annotations.toJSON();   // world-anchored, restorable

Undo / redo

addNodes, addEdges, removeNodes, and node drags record automatically. ⌘Z / ⇧⌘Z are bound; og.history.undo() and redo() are the programmatic path, and history.run(command) takes custom do/undo pairs.

// One-shot editing tools commit through the same atomic history
og.forge.join({ color: '#059669', caption: 'new link' }); // drag node → node
og.forge.retarget('relationship-id', 'target');           // drag endpoint
og.forge.scale('case-id');                                // radial resize
og.forge.rename('case-id');                               // accessible inline input
og.forge.grid(25);                                        // shared drag snapping
og.forge.setValidator((proposal) => policy.allows(proposal));

Algorithms

FunctionNotes
shortestPath(g, a, b)BFS, unweighted; returns node ids or null.
dijkstra(g, a, b, weightFn)Binary heap, non-negative weights.
connectedComponents(g)Component id per node plus count.
labelPropagation(g)Community per node; pairs well with a color rule.
buildAdjacency(g)Reusable CSR arrays if you call several of the above.

For longer centrality and community jobs, the instance-level insights engine captures one stable graph snapshot, transfers it to a worker, and returns typed results without modifying node data or styles.

const controller = new AbortController();
const result = await og.insights.run('betweenness', {
  maxSources: 64, seed: 2026, signal: controller.signal,
  onProgress: ({ ratio }) => showPercent(ratio),
});

// result.scores aligns with result.nodeIds from the captured snapshot
og.insights.cancel(); // terminates an active worker immediately

const backbone = await og.insights.run('spanning-forest', {
  weight: (edge, data) => data.cost,
});

Results align with the nodeIds and edgeIds captured when the job started, even if the live graph changes before completion. Run the insights workbench.

Export

const png  = await og.export.png();  // Blob, labels composited
const svg  = og.export.svg();         // vector string, visible viewport
const json = og.export.json();        // round-trips through setGraph
const csv  = og.export.csv();         // { nodes, edges } RFC-4180

File formats

import { parseJSON, parseCSV, parseGraphML, parseGEXF } from '@mizansec/ogjs';

og.setGraph(parseJSON(nodeLinkDoc));        // {nodes, edges|links}
og.setGraph(parseCSV(nodesCsv, edgesCsv));  // RFC-4180, typed columns
og.setGraph(parseGraphML(xmlString));       // keys become node data
og.setGraph(parseGEXF(xmlString));          // viz positions/colors kept

Every parser returns { nodes, edges }. Unrecognized columns and attributes survive on data, so style rules can use them. Export goes the other way: PNG, SVG, JSON, CSV, and an Excel-compatible workbook (og.export.excel(), save as .xls).

Databases

Keep the database behind your own API: the browser should never hold graph-database credentials. Your backend runs the query; a converter turns the response into a graph. The converters are pure functions and ship in the package.

Neo4j

// backend: session.run('MATCH (n)-[r]-(m) RETURN n, r, m LIMIT 500')
import { neo4jToGraph } from '@mizansec/ogjs';
const res = await fetch('/api/neighborhood?id=42').then((r) => r.json());
og.setGraph(neo4jToGraph(res.records));

Neptune · JanusGraph (Gremlin)

// backend: g.V().hasLabel('person').elementMap() + edges .elementMap()
import { gremlinToGraph } from '@mizansec/ogjs';
og.setGraph(gremlinToGraph(results));  // vertices and edges, any mix

SPARQL / RDF

// SELECT ?s ?p ?o WHERE { ... } — standard JSON bindings
import { sparqlToGraph } from '@mizansec/ogjs';
og.setGraph(sparqlToGraph(json, { subject: 's', predicate: 'p', object: 'o' }));

Google Spanner · Oracle · any SQL

// tabular edge lists: one row per relationship
import { rowsToGraph } from '@mizansec/ogjs';
og.setGraph(rowsToGraph(rows, {
  source: 'src_id', target: 'dst_id',
  edgeType: 'rel', sourceLabel: 'src_name', targetLabel: 'dst_name',
}));

Progressive loading composes with all of these: og.expand(id, fetcher) merges a fetched neighborhood into the current view and stays undoable.

Incremental edits are undoable through the same facade: og.addNodes(nodes), og.addEdges(edges), og.removeNodes(ids), and og.removeEdge(index). Use og.graph.getNode(index) or og.graph.getEdge(index) for complete snapshots.

React · Vue · Svelte · Angular

// React — inject your React, get a component; no peer deps
import React from 'react';
import { createOgjsComponent } from '@mizansec/ogjs/react';
const OgjsGraph = createOgjsComponent(React);

// Vue 3
import * as Vue from 'vue';
import { createOgjsVueComponent } from '@mizansec/ogjs/vue';

// Svelte — an action
import { ogjs } from '@mizansec/ogjs/svelte';
<div use:ogjs={{ nodes, edges }} />

// Angular — plain Ogjs in a component; no wrapper needed
@Component({ template: '<div #viz style="height:100%"></div>' })
export class GraphComponent implements AfterViewInit, OnDestroy {
  @ViewChild('viz') viz!: ElementRef<HTMLDivElement>;
  private og?: Ogjs;
  ngAfterViewInit() {
    this.og = new Ogjs({ container: this.viz.nativeElement });
    this.og.setGraph({ nodes, edges });
  }
  ngOnDestroy() { this.og?.destroy(); }
}

Bundlers: the package is plain ESM with a worker referenced via new URL(..., import.meta.url), which Vite, Webpack 5, and Rollup all resolve without configuration. In Node.js the rendering classes need a DOM, but the algorithms, parsers, and database converters are dependency-free and run headless — usable in backend pipelines and tests.

Animation

One promise coordinates node and edge attributes through the same typed-array tween engine. Node ids and edge indices are independently optional, so jobs may target either kind or both.

await og.animate({
  ids: ['account'], size: [18], color: ['#3b82f6ff'],
  edgeIndices: [0, 2], edgeWidth: [5, 3],
  edgeColor: ['#ef4444ff', '#22c55eff'],
}, 1000, 'quadraticInOut');

Valid easing names are linear, the seven quadratic/cubic variants, and the Ogjs aliases easeOut, easeInOut, and backOut.

Events

nodehover, nodeout, nodeclick, nodedragstart, nodedrag, nodedragend, click, viewchanged, animate — all through og.on(type, fn). Hover picking uses a spatial hash; measure its throughput on your target device with the performance workbench.

The animate payload contains duration, easing, nodeIndices, edgeIndices, and updatesPositions.

Evidence

Ogjs separates portable correctness from comparisons that depend on a particular machine or licensed reference input.

npm run evidence:portable     # type, build, unit, browser
npm run verify:consumer       # pack, isolated install, type/build/browser smoke
npm run evidence:release      # portable evidence followed by consumer verification
npm run evidence:performance  # compatible saved hardware/browser profile
npm run evidence:visual       # local licensed-reference comparison

Timing thresholds belong to the named saved profile; they are not a universal hardware-speed claim. Open the performance workbench to capture the workload and device you intend to support.

Requirements