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:
- Struct-of-arrays store. Node and edge attributes live in flat typed arrays. Layout ticks, picking, and GPU uploads touch contiguous memory and never allocate.
- One position texture. Both node and edge shaders read
positions from a single RGBA32F texture. Updating every position for a
frame is one
texSubImage2Dcall. - GPU simulation.
forceGPU()runs deposit → pyramid → force passes entirely in fragment shaders and the renderer samples the result directly. The CPU never sees intermediate positions.
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.
| Capability | Contract | Example |
|---|---|---|
| Stable editing | Stable relationship IDs, rollback-safe synchronous transactions, and one-step undo. | Run |
| Editing and edge presentation | Validated gestures plus per-relationship routes, text, badges, outlines, halos, pulse, and visibility. | Run |
| Worker insights | Cancellable centrality, spanning-forest, and community jobs over a stable graph snapshot. | Run |
| Temporal playback | Node and relationship lifespans, exact seek, trailing windows, brushing, and playback. | Run |
| Vector geography | Raster tiles, anchored nodes, and styled GeoJSON through one camera and export stack. | Run |
| Measured evidence | Portable correctness gates plus compatible-profile visual and performance comparisons. | Run |
| Projection pipeline | Named transactional node and edge fields reused by filters and styles. | Run |
Layouts
| Call | What 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
| Function | Notes |
|---|---|
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
evidence:portableneeds no private input and is the normal push and pull-request gate.verify:consumercopies the repository-onlyconsumer/investigation-desk/fixture to a temporary directory and installs the generated tarball rather than repository source.evidence:releaseruns this after the portable gate.evidence:performancecaptures a fresh report and refuses to compare a different workload, browser major, viewport, pixel ratio, renderer, or configuration.evidence:visualrequires ignored licensed reference pixels and fails closed when they are absent. CI therefore protects deterministic Ogjs screenshots without claiming third-party pixel parity.
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
- WebGL2 (all evergreen browsers). There is no canvas fallback — Ogjs throws a clear error instead of degrading silently.
- GPU layout additionally wants
EXT_color_buffer_floatandEXT_float_blend; without them it uses the CPU worker automatically. - TypeScript types ship in the package. Strict mode clean.