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. For a first pass, follow the continuous Getting Started tutorial.
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.
Boiler plate
Start with the maintained Vite and TypeScript project in
starters/vite-typescript. Its 36-entity payment
investigation is large enough to show meaningful layout and interaction,
while the application entry point stays focused on graph data and behavior.
Copy the starter directory and run npm install.
Keep stable ids and semantic roles, then replace the sample nodes and relationships.
Use npm run dev; OGJS owns layout, framing, resizing, and rendering.
const og = new Ogjs({ container: '#graph', theme: 'dark' }); og.setGraph(investigation); og.present({ theme: 'dark' }); await og.group(({ data }) => (data as { group: string }).group);
The repository gate packs the real npm artifact, installs it into a temporary copy of this starter, type-checks the app, and builds its production bundle.
Open the Boiler plate for the live preview, maintained source download, and framework integration paths.
Coding agents
Install the same checked OGJS skill in Codex, Claude Code, OpenCode, or GitHub Copilot CLI. It teaches the public intent API, flags private imports and manual rendering plumbing, and runs the consumer project's typecheck and build before making a compatibility claim.
Extract one vendor-neutral skill into the discovery directory used by your coding host.
Describe the graph data and behavior. The skill directs the agent to public OGJS APIs instead of canvas plumbing.
The included checker rejects stale versions, private imports, invented methods, and internal renderer access.
Codex, OpenCode, and Copilot CLI:
mkdir -p .agents/skills curl -fsSL https://ogjs.mizansec.com/agent/ogjs-skill.tgz | tar -xz -C .agents/skills
Claude Code:
mkdir -p .claude/skills curl -fsSL https://ogjs.mizansec.com/agent/ogjs-skill.tgz | tar -xz -C .claude/skills
For a host that does not discover skills, provide the checked OGJS context directly. See the compatibility evidence for exact host, model, version, fixture, and build status. Untested combinations are labelled as such. The complete installation, prompting, ownership, verification, and troubleshooting guide lives in the public OGJS agent-kit repository.
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 capabilities 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 weighted or unweighted centrality, spanning-forest, shell-layer, and multilevel or Leiden community jobs over a stable graph snapshot. | Run |
| Temporal playback | Node and relationship lifespans, atomic multi-window queries, exact seek, trailing windows, brushing, and playback. | Query controls · Playback |
| Geographic investigation | Self-hosted raster, GeoJSON and feature sources, heat, clustering, and queries 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 |
| Composable predicates | Serializable named constraints with order-independent intersection and stale-result protection. | Run |
| Reusable controls | Fuzzy node search, semantic legends, and operation progress in a graph overlay or application host. | Run |
| Topology synthesis | Bounded, seeded graph recipes that return ordinary nodes and edges. | Run |
API Overview
Use this index to locate the shipped surface by task. Every method links to its signature, parameters, return behavior, errors, and example in the detailed API reference. TypeScript declarations remain the exact source for option and event payload types.
Core graph & data
Create, replace, edit, select, and inspect stable graph entities.
Layout & motion
Arrange the whole graph or a local scope, then animate the resulting positions and styles.
Presentation
Map graph data to readable node and relationship styles.
Transformations
Filter, group, unfold, and compose atomic structural views.
Analysis
Run traversal locally or cancellable weighted insight jobs in a worker.
Time & geography
Filter graph lifespans and compose retained geographic context through the same camera.
Controls & export
Mount search, settings, timelines, annotations, and portable output.
Integrations & events
Bind application lifecycles and subscribe to stable interaction events.
Topology synthesis
synthesizeTopology(recipe) creates stable
node and relationship ids with deterministic starting coordinates. Pass
the returned object directly to og.setGraph(),
then apply any layout.
import { Ogjs, synthesizeTopology } from '@mizansec/ogjs'; const data = synthesizeTopology({ model: 'small-world', nodes: 500, neighbors: 6, rewire: 0.12, seed: 17, }); og.setGraph(data); await og.layouts.forceGPU();
const tree = synthesizeTopology({ model: 'tree', children: 2, height: 5, }); og.setGraph(tree); og.layouts.hierarchical({ roots: ['n0'] });
Models: random-links,
grid, tree,
small-world, and
power-law. Generation is dependency-free,
bounded, and repeatable for equal recipes. Tree recipes accept either an
exact nodes count or a complete-tree
height, plus a fixed
children count.
Layouts
Start with await og.arrange(). OGJS reads the
loaded topology, chooses an existing layout, animates it, and frames the
result. Pass { as: 'hierarchy' } only when the
domain requires a specific reading order.
| Call | What it does |
|---|---|
arrange(opts) | Everyday layout action. Auto-selects grid, hierarchy, or adaptive worker force from topology, then frames the result with optional panel-aware camera intent. |
layouts.forceGPU(opts) | Force-directed on the GPU. Falls back to the worker when float-render extensions are missing or data-driven weights are requested. |
layouts.force(opts) | Worker-streamed force with optional adaptive tuning, node inertia, and per-edge pull. Callbacks resolve once before the run starts. |
layouts.hierarchical(opts) | Layered BFS with contiguous branches, parent-over-child centering, cached sibling-lane ranks, four flow directions, size-aware clearance, and separate packing for disconnected components. |
layouts.radial(opts) | BFS rings around a root, subtrees kept contiguous. |
layouts.track(opts) | Pack an ordered local scope along a horizontal or vertical axis while preserving pinned nodes. |
layouts.temporal(opts) | Sort finite number or Date values into stable, size-aware equal-time lanes. Add guide: true or a label recipe for a retained, camera-aware raster guide. |
layouts.orbit(opts) / settleSpacing(opts) | Arrange a local scope in deterministic rings or remove variable-size overlap without moving fixed anchors. |
layouts.circular(opts) / grid() / random() | Instant deterministic arrangements. Circular accepts an ordered nodeIds scope, center, radius, and start angle; other nodes stay put. |
layouts.stop() | Halts a running layout, keeps current positions. |
og.layouts.circular({
nodeIds: teamIds,
center: [240, 0],
radius: 80,
startAngle: -Math.PI / 2,
});
Force options: repulsion,
springLength, springK,
gravity, velocityDecay,
maxVelocity, iterations.
Use the performance
workbench to choose options on the device and workload you will ship.
await og.layouts.hierarchical({ roots: ['gateway'], flow: 'east', components: 'across', layerGap: 72, nodeGap: 24, componentGap: 48, rankBy: ({ data }) => data.priority, });
Try node and edge promotion, spacing, direction, component packing, operational-priority lanes, and edge curvature in the hierarchy workbench. The rank callback runs once per node, must return a finite number, and must not change graph topology; lower values order nodes within the same crossing-reduced sibling lane without splitting unrelated branches.
For event graphs, Temporal lanes
demonstrates layouts.temporal({ value, axis, guide }) and
animatedLayouts.temporal(...). Equal values share one lane;
chronological order is stable and lane spacing accounts for node size. The optional
guide reuses those validated values, follows the camera and layout motion, clears when
the next temporal layout omits it, and participates in PNG/PDF export.
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
For domain graphs, give each node and relationship a role, then ask OGJS to complete a readable graph.
og.setGraph({ nodes, edges });
og.present({ theme: 'dark' });
Node roles: entity, person,
service, device,
data, place,
alert, and group.
Edge roles: relationship, flow,
dependency, membership,
association, signal, and
risk.
The default readers use data.role and
data.label. Run the
payment investigation
example to compare entity and relationship treatments.
Relationship width is the fitted-view width. Zooming out reduces edge,
outline, and halo thickness for a clearer overview; zooming in increases
them for detail. The renderer bounds that change between 0.5× and 2×, and
the next explicit camera.fit() becomes the new
neutral view.
Advanced control
Use og.styles.useSemanticRoles() when an
existing schema needs custom role or label readers:
og.styles.useSemanticRoles({
theme: 'dark',
nodeRole: ({ data }) => data?.kind,
edgeRole: ({ data }) => data?.relationship,
});
Then add ordered rules when the application needs additional control:
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,
}));
// One retained node attribute creates a proportional pie or donut.
og.updateNode('risk', {
dial: {
parts: [
{ value: 7, color: '#ce6f74' },
{ value: 3, color: '#d79a3b' },
],
inset: 0.32,
},
});
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. Use the searchable icon reference
to load the self-hosted Material font and copy a supported name. A
dial accepts one to four weighted color parts;
inset changes the pie into a donut. Dials use the
ordinary node surface, so picking, labels, selection, JSON, and SVG export
keep working without custom canvas callbacks. Run the
node dials and beacons example.
Edge rules can assign curves, dashes, arrows, fixed-width
outlines and halos, reduced-motion-aware pulse, captions, and badges per
relationship. Arrowheads follow the final curve tangent, scale with the
relationship stroke, and meet the visible boundary of circle, square,
diamond, triangle, or hexagon nodes. 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. For dense
views, og.setEdgeLengthFade(true) keeps nearby
structure clear while long crossing links recede as the camera changes. Interaction
feedback uses fixed screen-space geometry: a 2px white inner
stroke and 5px colored outer stroke. Configure them with
interactionInnerStrokeWidth and
interactionOuterStrokeWidth. Each ordinary
node stops at 18% of the view or 120px while zooming, without limiting
camera access to smaller neighbors. Group containers keep their layout
size; set maxNodeViewRatio and
maxNodeDiameterPx to null
to remove both limits. The convenience og.camera.fit()
keeps graphs of 12 nodes or fewer centered within 40% of the viewport;
pass framing intent when an application needs a specific scope or must
keep docked controls clear. OGJS resolves graph bounds, usable canvas
space, camera coordinates, and render invalidation.
// No canvas measurement or post-fit camera correction og.camera.fit({ target: 'visible', avoid: document.querySelector('.controls'), paddingPx: 24, maxOccupancy: .9, offsetPx: [-16, 20], // optional asymmetric composition, retained on resize retain: true, }); // Move smoothly to the content a person needs now await og.focus({ target: 'selected', paddingPx: 32 }, 280);
// 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', minZoom: .6, maxZoom: 4, // semantic zoom band; collisions still resolve inside it });
Run the edge-style gallery or the Forge workbench.
Grouping & filters
// keep members visible while OGJS arranges, animates, and frames each group await og.group(({ data }) => data?.team ?? null, { fit: true }); // advanced structural collapse by any key; restore is exact og.transformations.groupBy(({ data }) => data?.cluster ?? null); og.selectGroup('Payments'); // resolves the visible parent or expanded members og.transformations.ungroup(); // move an existing node into one group; the rest of the graph stays fixed og.groupVisually(({ data }) => data?.team ?? null); await og.growVisualGroup('Data', 'new', 320); // add fetched data, membership, local layout, rollback, and history in one call await og.expandVisualGroupAsync('Data', async (key) => ({ ...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); // retained async levels: cancelled or invalid loads leave no partial state await og.unfold.open('Company', async ({ id, depth, signal }) => { const response = await fetch(`/api/graph/${id}?depth=${depth}`, { signal }); return response.json(); }, { maxDepth: 3, focus: false, gap: 18 }); await og.unfold.back(); await og.unfold.reset(); // 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 // serializable named constraints intersect and preserve spatial context og.predicates.set('risk', { target: 'nodes', active: true, expression: { op: 'range', field: { kind: 'data', path: 'risk' }, min: 60, max: 100 }, }); const matched = og.predicates.resolve().nodes; // detached Uint32Array og.predicates.remove('risk'); // simplify a working view without rewriting the source graph const restore = og.transformations.collapseNodes( ({ data }) => data?.role === 'company', ); restore(); // recover the company and its original relationships // 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();
groupBy keys must be non-empty strings or
null; invalid keys fail before the graph changes.
When multiple source relationships collapse into one edge, its badge shows
the represented relationship count. selectGroup
selects that visible parent—or the visible members after expansion—without
changing the camera.
collapseNodes() reconnects the visible
neighbors of an intermediary, while mergeNeighbors()
copies chosen intermediary data into those neighbors. Both return a restore
function and keep the canonical graph intact. See
Simplify relationships.
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.
og.predicates validates a JSON-shaped tree,
intersects every active named source, and dims unmatched nodes without
changing layout or visibility. Active clauses with a null expression
represent a settled empty result. setAsync()
ignores late results superseded by a newer request for the same name. Try
the composable investigation.
og.unfold keeps loaded descendants in the
graph, so closing and reopening a branch does not fetch it again. Its
placement chooses a nearby clear sector and extends only the affected
branch when space is tight. Cached branches recompute that placement
without refetching; camera focus remains optional. Its visibility rule
composes with filters and timeline windows, and every loader receives an
AbortSignal. Try the
progressive unfold example.
// ordered structural stages compile atomically from one source snapshot const fold = og.transformations.fold({ band: ({ zoom }) => zoom < 1 ? 'region' : 'asset', key: ({ data }, band) => band === 'region' ? data.region : null, }); fold.enable(false).enable(); fold.refresh(); // double-click and keyboard Enter share the retained unfold lifecycle const guide = og.trailGuide(loadChildren, { accept: (id) => expandableIds.has(id), onState: (state) => updateStatus(state), });
Structural fold,
spawn, and absorb
stages preserve their canonical input, validate the complete candidate,
and replace the visible topology only after every stage succeeds. A zoom
fold reruns only when its discrete band changes. Run the
zoom-aware folding example.
Search, settings, and surface controls
const helm = og.mountHelm({
placeholder: 'Find an entity',
searchText: ({ data }) => data.alias,
filterSearch: true,
fullscreen: true,
cursor: 'grab',
grid: { spacing: 40, color: 'rgba(155,119,70,.24)' },
});
helm.setSettings([
{ id: 'labels', label: 'Show labels', kind: 'toggle', value: true,
onChange: (value) => setLabels(value) },
]);
helm.setLegend([
{ id: 'account', label: 'Account', color: '#2563eb', mark: 'node',
filter: { op: 'compare', field: { kind: 'data', path: 'type' },
compare: 'eq', value: 'account' } },
{ id: 'transfer', label: 'Transfer', color: '#94a3b8', mark: 'edge' },
]);
helm.setProgress(0.4, 'Loading records');
helm.clearProgress();
Search uses exact, prefix, substring, then ordered-subsequence ranking.
The default choice clears the old selection and selects the chosen node
without moving the camera. Pass host to place the
control in your own panel, or onChoose to own
selection policy. Toggle, range, and select settings validate as one schema.
Cursor changes preserve pointer feedback over graph items; the grid tracks the
camera and participates in raster export. Fullscreen uses the browser's native
graph-container lifecycle. Applications report work through the progress control;
parsers remain synchronous. Set filterSearch
to keep search as a named filter. A node legend entry can provide a
filter to join that filter state. Both
preserve the camera and are removed when Helm is removed. Filter names
are reserved while the control uses them, so a duplicate name fails
before either filter changes. The older
matches callback remains available when saved
filter state is not needed. Run the
surface
controls example or the search workbench.
PrismPack palettes
Use the OGJS Design Kit to compare the shipped light and dark roles, inspect interaction states, and download JSON or CSS tokens generated from this runtime palette.
import { createPrismLegend, prismPack } from '@mizansec/ogjs'; const theme = 'dark'; const tokens = prismPack.semantic[theme]; const color = prismPack.categorical[categoryIndex][theme]; const mapColors = prismPack.context.map; const exportColors = prismPack.context.export; og.styles.addNodeRule(({ data }) => ({ color: prismPack.categorical[data.category][theme] })); og.styles.addEdgeRule(() => ({ color: tokens.edge, width: 3.5 })); og.mountHelm().setLegend(createPrismLegend(categoryNames, { theme }));
The pack is dependency-free and deeply frozen. Its eight paired category colors and semantic edge, text, surface, and focus tokens carry measured canvas contrast metadata. Map routes are tested over both land and water; export text and edges are tested on the export canvas. Category labels or other redundant marks remain necessary when color alone cannot identify a class. Legend generation validates no more than eight unique, non-blank labels and returns detached entries. Run the light and dark palette comparison.
Timeline
og.timeline.setTimes(({ data }) => data?.timestamp);
og.timeline.setLanes(({ data }) => ({ id: data.team, label: data.team }));
og.mountTimeline(document.querySelector('#timeline'), {
viewMode: 'auto',
filter: { start: 'opened', end: 'closed' },
formatTime: (value) => new Date(value).toLocaleDateString(),
});
og.timeline.setBrush(t0, t1); // select visually; keep graph context
og.timeline.setWindow(t0, t1); // explicitly filter the graph
// overlapping node and relationship lifespans
og.timeline.setIntervals(
({ data }) => ({ start: data.opened, end: data.closed }),
({ data }) => ({ start: data.activeFrom, end: data.activeUntil }),
);
og.timeline.setQueries([
{ start: t0, end: t1, relation: 'overlap' },
{ start: t2, end: t3, relation: 'outside', includeStart: false },
]);
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
setQueries() accepts an array of
TimelineQuery values. An empty array disables
temporal filtering, equivalent to clearQueries().
Non-empty queries combine with OR: an element visible through any query
stays visible. Each query has a
relation of overlap,
before, after,
contained, or outside
(before OR after). Element spans are closed;
includeStart and includeEnd
decide whether a span touching a query boundary matches. Both default to
true, as does the default
overlap relation.
Inputs normalize reversed bounds together with their boundary flags, and the complete batch is validated before it changes playback or visibility; invalid batches leave the previous query state intact. Untimed nodes and relationships remain visible.
setLanes() groups events into labelled tracks.
The mounted view shows event cards when space allows and switches to
aggregate occupancy bars when the range or viewport becomes dense.
setViewMode() requests either presentation.
Detail mode falls back to aggregate bars when the available space cannot keep event cards readable.
setViewport() changes only the displayed time
scale. Dragging the range handles or calling
setBrush() highlights matching events without
moving, hiding, or re-laying out the graph. The brush stays inside the
displayed viewport, keeping its handles, keyboard values, and visible
range aligned. Filtering remains an explicit
setWindow() or setQueries()
action.
setWindow(), seek(),
and playback replace advanced state with one closed overlap query. The
visual range selector does not change query state or visibility. Use
setQueries() when an application needs paired
ranges or relations such as outside. Add the
filter option shown above when time should
join search, legend, or application filters. The start and end strings
are paths inside node data. A missing time value does not match, and a
duplicate filter name leaves the working timeline unchanged.
const stop = og.timeline.onChange((state) => { progress.style.width = `${state.currentTime}%`; pauseButton.disabled = !state.playing; }); stop(); // unsubscribe when the surrounding UI is removed
onChange() publishes a detached
TimelineState after brush, view, filter,
cursor, and playback transitions. Use timeline.state
for an initial snapshot. Application code does not need document-level
pointer listeners, DOM observers, or its own playback polling loop.
The automated runaway ceiling replaces two queries across 100,000 nodes and 99,999 relationships in under 500 ms. It is a deterministic guard, not a device-speed claim.
Temporal rules compose with independent node and edge filters. Accessors
snapshot the current dense graph; rerun setTimes(), setIntervals(), and setLanes()
after topology, temporal-data, or lane-data changes.
Run the investigation timeline
for lanes, range selection, and advanced queries, or the
interval
playback workbench for playback.
Geo mode
const topology = await fetch('/maps/world-110m.json').then((r) => r.json()); const atlas = createAtlasSource(topology); // validated GeoJSON + offline raster const og = new Ogjs({ container: '#viz', geo: { tileUrl: atlas.tileUrl, maxTileZoom: 0 }, }); og.geo.enable(({ data }) => data?.latlng); // [lat, lng] per node // GeoJSON coordinates are standard [longitude, latitude] og.geo.vectors.set(atlas.features, (feature) => ({ fill: feature.properties.color, fillOpacity: 0.2, stroke: '#2563eb', strokeWidth: 1.5, // CSS pixels at every zoom })); og.geo.vectors.fit({ padding: .15 }); const hits = og.geo.vectors.query(25.2, 55.27, 8); og.geo.tiles.set({ format: 'mvt', minZoom: 0, maxZoom: 14, load: ({ z, x, y, signal }) => fetchTile(z, x, y, signal), }); og.geo.heat.set(weightedPoints, { radius: 40, resolution: 0.35 }); og.geo.clusters.enable({ radius: 64, minSize: 2 }); og.geo.clusters.focus(og.geo.clusters.snapshot()[0].key);
Tiles come from OpenStreetMap by default (any XYZ template works) and
draw beneath the transparent WebGL canvas. Projection helpers
geoProject / geoUnproject
are exported. createAtlasSource validates a
caller-hosted TopoJSON polygon collection, decodes it to GeoJSON, and
creates a self-contained raster basemap without a network tile provider.
Point, line, polygon, multi-, and geometry-collection
GeoJSON render through the same camera and are included in PNG/PDF output;
active static geo.vectors are also emitted as
editable paths and points beneath graph marks in current-viewport SVG.
A constructor reference can replace Web Mercator
for tiles, anchored nodes, vectors, feature sources, heat, queries, and
instance projection calls. Feature sources return GeoJSON or an MVT
ArrayBuffer; concurrent requests coalesce and
stale requests are cancelled when the source changes. Heat uses a smaller
density buffer, and clusters hide their member nodes until the view changes.
Use clusters.disable() for a reversible toggle
or clusters.clear() to detach the cluster surface.
Replacing vectors validates the complete snapshot before changing the layer.
On references with wrapX: true, lines and canonical
polygon rings crossing the antimeridian split at the map seam automatically,
including polygon holes and multi-geometries. Rings that wind around the
antimeridian more than once are rejected before the retained layer changes.
No mapping library required. Run the
Gulf incident map.
Contour fields
const og = new Ogjs({ container: '#viz', transparent: true });
og.contours.set({
columns: 3,
rows: 3,
values: [0, 1, 0, 1, 2, 1, 0, 1, 0],
bounds: [-100, -100, 100, 100],
}, {
levels: [
{ value: 0.5, stroke: '#f59e0b', strokeWidth: 2 },
{ value: 1.5, stroke: '#dc6b35', strokeWidth: 2.5 },
],
});
og.contours.hide();
og.contours.show();
og.contours.clear();
ContourField,
ContourLevel, and
ContourOptions describe a copied scalar grid
and ordered line levels. set() validates the
complete replacement before changing the active layer. The linework follows
the graph camera and joins PNG/PDF exports. Contours are an underlay, so
construct OGJS with transparent: true to reveal
them in the live canvas and raster exports. hide()
retains prepared geometry, while clear() detaches it.
Run the contour field 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 });
og.annotations.add({ type: 'polygon', points: [[x1, y1], [x2, y2], [x3, y3]] });
og.annotations.add({
type: 'circle', nodeId: 'case-42', radiusPx: 72, color: '#c45f46',
label: 'Case 42', labelPlacement: 'top-right', labelFontSize: 14,
});
const saved = og.annotations.toJSON(); // world-anchored, restorable
Import MarkupDeck from the optional
@mizansec/ogjs/markup entry to add an accessible
pointer, touch, and keyboard editor over the same annotation store. Completed
gestures are atomic history entries; linked notes and circles follow graph nodes; SVG,
PNG, and PDF include the annotations but omit selection handles and guides.
Drag an outlined shape from its stroke or handles; its open interior remains
available for lasso selection.
Run the MarkupDeck workbench.
import { MarkupDeck } from '@mizansec/ogjs/markup'; const deck = new MarkupDeck(og, { snapThresholdPx: 8 }); deck.setMode('polygon'); const document = deck.exportDocument();
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.
const unsubscribe = og.history.onChange(({ canUndo, canRedo, undoLabel }) => { undoButton.disabled = !canUndo; redoButton.disabled = !canRedo; status.textContent = undoLabel ?? 'Nothing to undo'; }); // Later, when the controls unmount: unsubscribe();
The callback runs after the stack settles, including one notification for a successful transaction. Empty or failed undo/redo operations do not publish a state that never committed.
// One-shot editing tools commit through the same atomic history og.forge.join({ color: '#059669', caption: 'new link' }); // drag node → node og.forge.reconnect('relationship-id', 'target'); // drag one endpoint onto another node og.forge.resize('case-id'); // drag outward or inward to resize og.forge.rename('case-id'); // accessible inline input og.forge.grid(25); // shared drag snapping og.forge.setValidator((proposal) => policy.allows(proposal)); og.forge.onChange(() => syncEditorToolbar()); // after state/history settle
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, }); const bridges = await og.insights.run('betweenness', { weight: (edge, data) => data.transferCost, }); const trustedGroups = await og.insights.run('communities', { weight: (edge, data) => data.trust, communityMethod: 'leiden', });
Results align with the nodeIds and
edgeIds captured when the job started, even
if the live graph changes before completion. Distance weights for
closeness and betweenness must be positive; other weighted jobs accept
finite non-negative edge values. The default community method remains
deterministic multilevel modularity; choose leiden
when connected, refined communities matter. Run the
insights workbench.
Export
const png = await og.export.png(); // Blob, labels composited const evidence = await og.export.png({ crop: 'selection', padding: 24 }); const report = await og.export.png({ scale: 2, background: '#ffffff', layers: ['graph', 'overlay'], }); // RasterExportBlob includes width, height, and format const review = await og.export.pdf({ watermark: { text: 'INTERNAL', position: 'bottom-right' } }); 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 const graphml = og.export.graphml(); // XML graph interchange const gexf = og.export.gexf(); // GEXF 1.3 with viz data const xlsx = og.export.xlsx(); // Uint8Array, real XLSX ZIP
A 'selection' crop uses the visible bounds of
selected nodes. Add padding when labels or curved
relationships must remain around those node bounds.
File formats
For an application-facing import, give OGJS the browser file and the desired result. OGJS chooses the parser from the filename, arranges the graph, reports progress through Helm, and frames the result without canvas calculations.
const helm = og.mountHelm(); const files = og.acceptFiles({ progress: helm, layout: 'force', fit: { target: 'all', avoid: document.querySelector('.side-panel') }, onLoaded: (graph, file) => console.log(file.name, graph.nodes.length), }); document.querySelector('#choose-file').onclick = () => files.open(); // A one-shot import uses the same parser, layout, cancellation, and camera path. await og.loadFile(file, { progress: helm, layout: 'circular' });
acceptFiles() also accepts native drag and drop on the graph
container. Return and retain its handle only when you need to open or remove the control
independently; og.destroy() cleans it up automatically. A pair
of CSV, Arrow, or Parquet files may be supplied as the node file plus
edgeFile. Pass maxNodes or
maxEdges when a deliberate preview is more useful than
loading every row; the returned importSummary distinguishes
rows not read from edge rows rejected because their endpoints were not loaded.
Parser-only boundary
import { parseJSON, parseCSV, parseGraphML, parseGEXF, parseMatrixMarket, parseXLSX } 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 og.setGraph(parseMatrixMarket(mtxString)); // square coordinate adjacency, up to 1M nodes og.setGraph(await parseXLSX(bytes)); // Nodes + Edges worksheets
import { parseArrow, parseParquet } from '@mizansec/ogjs/columnar'; const parsed = await parseParquet(nodeBytes, edgeBytes, { maxNodes: 100_000 }); console.log(parsed.importSummary); // rows read, omitted, rejected, and column statistics og.setGraph(parsed);
Every parser returns { nodes, edges }.
Unrecognized columns and attributes survive on
data, so style rules can use them. GraphML,
Arrow and Parquet keep typed extra columns and add a profile with counts,
missing values, distinct-value status, numeric range, average, and spread.
GEXF and XLSX round-trip stable ids and core styles. The legacy
og.export.excel() path is SpreadsheetML 2003
and must be saved as .xls; use
og.export.xlsx() for a modern workbook.
Databases
Keep the database behind your own API: the browser should never hold graph-database credentials. Your application supplies the query executor; OGJS owns result normalization, stable IDs, replacement or merge, layout, cancellation, and framing.
Cypher · Neo4j
const controller = new AbortController(); await og.loadCypher({ query: 'MATCH (a)-[r]->(b) RETURN a, r, b LIMIT $limit', params: { limit: 500 }, execute: ({ query, params, signal }) => fetch('/api/cypher', { method: 'POST', signal, headers: { 'content-type': 'application/json' }, body: JSON.stringify({ query, params }), }).then((response) => response.json()), mode: 'replace', // use 'merge' to retain existing stable IDs layout: 'force', // adaptive force is the default fit: { target: 'all', paddingPx: 24 }, signal: controller.signal, });
The executor may return a Neo4j records array or
{ records }. Relationship
elementId values become stable OGJS edge IDs,
so parallel relationships remain distinct and subsequent
mode: 'merge' loads update them instead of
duplicating them. A merge query may return only new relationships when
both endpoint nodes are already loaded; OGJS resolves those endpoints
against the live graph. OGJS does not include a Cypher parser, database
driver, credentials, or connection pool.
Neo4j conversion only
import { neo4jToGraph } from '@mizansec/ogjs'; const response = await fetch('/api/neighborhood?id=42').then((r) => r.json()); og.setGraph(neo4jToGraph(response.records));
Use the pure converter when the application already owns the loading,
layout, and camera lifecycle. The other converters below follow the same
low-level { nodes, edges } contract.
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', }));
Use og.expand(id, fetcher) for a one-level
neighborhood merge. Use og.unfold.open() when
branches need nested state, cancellation, cached reopening, or back/reset
navigation. Both paths commit through the same undoable graph edits.
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.
Atomic schema intake
liftSchemaGraph() accepts pages from an
adapter your application or backend owns. Ogjs does not open database
connections, run queries, discover schemas, or handle credentials. It
validates and collects the returned graph data privately; call
og.setGraph() only after the promise fulfils,
so a failed or cancelled page leaves the current graph untouched.
Each adapter result is treated as an immutable page snapshot. Do not
mutate it while load() or
adapt() is in flight.
import { liftSchemaGraph, schemaDataId, schemaToTypeScript, } from '@mizansec/ogjs'; const adapter = { schema: schemaSnapshot, load: async ({ cursor, page, signal }) => { const value = await fetch( `/api/cases?cursor=${cursor ?? ''}&page=${page}`, { signal }, ).then((response) => response.json()); return { value, next: value.next }; }, adapt: ({ rows }, { id }) => ({ nodes: rows.map((row) => ({ id: id('case', row.caseId), label: row.title, data: row, })), edges: [], }), }; const graph = await liftSchemaGraph(adapter, { signal: controller.signal, onProgress: ({ page, nodes, edges }) => showProgress(page, nodes, edges), }); og.setGraph(graph);
schemaDataId(scope, ...parts) creates
deterministic, typed composite IDs. schemaToTypeScript(snapshot)
turns the same versioned schema snapshot into deterministic interfaces.
Exact duplicate IDs keep their first position; conflicting duplicates
reject the collection, and all relationship endpoints are checked after
the final page. The default ceilings are 10,000 pages, 1,000,000 nodes,
and 4,000,000 edges, with cancellation checked around every awaited load
and adaptation. See the schema intake
workbench for failure, cancellation, progress, projection, filtering,
and grouping in one browser flow.
Graph records and inspection
Use og.table() when a graph and a record list
represent the same nodes. OGJS reads the live graph directly, so the application
does not mirror selection, calculate pages, or correct the camera after a row
update. Graph selection opens the page containing the selected record; activating
a row replaces the graph selection with that node.
const records = og.table({ container: '#records', columns: [ { field: 'label', label: 'Case' }, { field: 'data.owner', label: 'Owner' }, { field: 'data.status', label: 'Status' }, ], pageSize: 25, }); records.refresh(); // only when external data inside node.data mutates in place records.destroy(); // removes OGJS-owned table DOM and listeners
Add og.inspect() when the selected record also
needs a readable details panel. It follows the same graph selection without an
application event listener, copied node state, or camera change.
const details = og.inspect({ container: '#details', fields: [ { field: 'data.owner', label: 'Owner' }, { field: 'data.status', label: 'Status' }, ], emptyText: 'Select a case to inspect', });
Columns default to id and
label. Hidden nodes are omitted unless
includeHidden: true. Pagination bounds the DOM for
large graphs; native buttons preserve keyboard and screen-reader operation. See
Graph and records for the complete
table-and-inspector workflow.
React · Vue · Svelte · Angular · custom hosts
All adapters use BindingKit: one owner for
graph replacement, typed event handlers, constructor-option changes,
server-safe setup, and cleanup. Pass instance
instead of options to bind an instance your
application already owns.
// React — inject your React, get a component; no peer deps import React from 'react'; import { createOgjsComponent } from '@mizansec/ogjs/react'; const OgjsGraph = createOgjsComponent(React); <OgjsGraph nodes={nodes} edges={edges} events={{ nodeclick: inspect }} /> // Vue 3 import * as Vue from 'vue'; import { createOgjsVueComponent } from '@mizansec/ogjs/vue'; const VueGraph = createOgjsVueComponent(Vue); // Svelte — an action import { ogjs } from '@mizansec/ogjs/svelte'; <div use:ogjs={{ nodes, edges, events }} /> // Angular and other custom hosts use the same BindingKit lifecycle import { BindingKit } from '@mizansec/ogjs';
The repository includes a complete Angular 21.2.20 AOT consumer in
recipes/angular/. It is built from the packed
library and exercised in Chromium by npm run
verify:angular. Ogjs ships no Angular runtime entry point or Angular
dependency; the zone-free recipe imports the root
BindingKit and verifies Angular-controlled teardown.
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
interactive Ogjs renderer needs a DOM and
WebGL2. The exported Graph,
Camera, and
toSVG
run headlessly in Node, alongside the dependency-free algorithms,
parsers, and database converters.
Application adapters
Keep transport, identity, persistence, model inference, and optional canvas engines in the application. OGJS supplies stable ids, typed events, atomic edits, history, camera mapping, ordered surfaces, export opt-in, and deterministic teardown.
// collaboration: publish stable ids through your own channel og.on('selectionchanged', () => channel.send({ ids: og.getSelected() })); // model output: validate in the app, then commit one undoable edit og.edit('apply reviewed result', draft => draft.updateNode(result.id, patch)); // Canvas2D, PixiJS, Three.js, regl, MapLibre, or Deck.gl const layer = og.strata.attach({ id: 'application-layer', element: canvas, tier: 'underlay', includeInExport: true, render: frame => adapter.render(frame.worldToScreen, frame), }); layer.detach();
Those named renderers are examples of application-owned runtimes, not bundled dependencies or certified integrations. The application adapter lab demonstrates the boundary with a local loopback channel, a reviewed result, and node-anchored risk annotations without network access. Use Strata only when a third-party renderer genuinely needs its own surface.
Jupyter notebooks
@mizansec/ogjs/notebook accepts one
versioned JSON document containing the graph, constructor options,
layout, grouping, and fit policy. It validates the complete document
before changing the current render, ignores stale revisions, and emits
stable node and edge ids instead of internal array positions.
import { NotebookPort } from '@mizansec/ogjs/notebook'; const port = new NotebookPort(host, { onEvent: publish }); await port.update({ protocol: 1, id: 'risk-notebook', revision: 1, graph: { nodes, edges }, layout: { kind: 'hierarchical' }, fit: 'initial', });
from ogjs_notebook import OgjsWidget view = OgjsWidget({"nodes": nodes, "edges": edges}, port_id="risk-notebook") view.layout("hierarchical", flow="south") view.group_by("team") view
The separate Python package uses anywidget's standard render and abort lifecycle. Its wheel contains the local OGJS ESM code, not a CDN URL, Jupyter runtime, fonts, icons, or images. Invalid data and render failures are returned through the synchronized status trait.
JavaScript, Python, Java, C#, Go, PHP, and Rust
Backends do not need a second graph renderer. They emit the same
versioned protocol-1 JSON document with stable node and relationship ids;
NotebookPort validates it before the browser
graph changes. Java, C#, Go, PHP, and Rust recipes are compiled or
interpreted in the repository gate, while Python uses the local anywidget
package and JavaScript/TypeScript calls the bridge directly.
npm run verify:languages const port = new NotebookPort(host); await port.update(document); // same JSON from any supported backend
See the seven-language bridge for the shared document and the graph it renders. Network access, credentials, and transport remain application concerns.
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.
Use keyframes when one human-readable sequence needs several layouts, attribute changes, or camera moves. Frames run in order; work inside one frame runs together.
await og.animateKeyframes([
{ layout: () => og.layouts.radial({ root: 'incident' }), fit: true, duration: 600 },
{
animate: { ids: ['incident'], size: [26] },
camera: { cx: 0, cy: 0, startAfter: 300 },
duration: 600,
},
], { signal: controller.signal });
The complete sequence is validated before it changes the graph.
AbortSignal stops the active frame, reduced-motion
preferences finish at the endpoint, and the
incident sequence plots the actual elapsed time.
Events
nodehover, nodeout,
nodeclick, nodedragstart,
nodedrag, nodedragend,
selectionchanged,
click, viewchanged,
animate, layoutstart,
layoutcomputed, layoutend —
all through og.on(type, fn). Hover picking uses a
spatial hash; measure its throughput on your target device with the
Performance Lab.
selectionchanged fires after programmatic,
lasso, pointer, or keyboard selection changes. Its
nodeIndices payload contains the complete current selection.
og.hitTest(screenX, screenY) resolves the
topmost node or relationship at canvas-local screen coordinates and
returns its kind, id when present, and dense index.
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 lab to measure, lay out, inspect, and group repeatable clustered, degree-hub, hub-and-spoke, or uniform workloads on the 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.