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. 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.

1 · Copy

Copy the starter directory and run npm install.

2 · Replace data

Keep stable ids and semantic roles, then replace the sample nodes and relationships.

3 · Run

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.

1 · Install

Extract one vendor-neutral skill into the discovery directory used by your coding host.

2 · Ask by intent

Describe the graph data and behavior. The skill directs the agent to public OGJS APIs instead of canvas plumbing.

3 · Verify

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 texSubImage2D call.
  • 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.

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 weighted or unweighted centrality, spanning-forest, shell-layer, and multilevel or Leiden community jobs over a stable graph snapshot.Run
Temporal playbackNode and relationship lifespans, atomic multi-window queries, exact seek, trailing windows, brushing, and playback.Query controls · Playback
Geographic investigationSelf-hosted raster, GeoJSON and feature sources, heat, clustering, and queries 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
Composable predicatesSerializable named constraints with order-independent intersection and stale-result protection.Run
Reusable controlsFuzzy node search, semantic legends, and operation progress in a graph overlay or application host.Run
Topology synthesisBounded, 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.

CallWhat 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

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,
});

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:portable needs no private input and is the normal push and pull-request gate.
  • verify:consumer copies the repository-only consumer/investigation-desk/ fixture to a temporary directory and installs the generated tarball rather than repository source. evidence:release runs this after the portable gate.
  • evidence:performance captures a fresh report and refuses to compare a different workload, browser major, viewport, pixel ratio, renderer, or configuration.
  • evidence:visual requires 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_float and EXT_float_blend; without them it uses the CPU worker automatically.
  • TypeScript types ship in the package. Strict mode clean.