OG
JS
Docs
Demos
Examples
Analysis explorer
Compare literal routes, components, and communities on the same graph.
Fewest hops
Lowest risk
Components
Communities
// The application supplies stable node ids, weighted relationships, and the analysis to run. // OGJS keeps graph storage, selection, camera framing, and result animation synchronized; // the application only maps returned ids and scores to investigation meaning. import { Ogjs, buildAdjacency, connectedComponents, dijkstra, labelPropagation, shortestPath, } from '../../src/index'; const og = new Ogjs({ container: '#viz', theme: 'light' }); const labels = { a0: 'Alert', a1: 'Account', a2: 'Device', a3: 'Analyst', a4: 'Gateway', b0: 'Merchant', b1: 'Wallet', b2: 'Session', b3: 'Transfer', b4: 'Vault', isolate: 'Unlinked', }; // Stable ids are the analytical identity; labels are presentation data. Algorithms return // those ids, allowing the application to explain a result without depending on array order. const nodes = []; for (const prefix of ['a', 'b']) { for (let index = 0; index < 5; index++) { const id = `${prefix}${index}`; nodes.push({ id, label: labels[id], size: 9, color: '#94a3b8', }); } } nodes.push({ id: 'isolate', label: labels.isolate, size: 9, color: '#94a3b8' }); const edges = []; for (const prefix of ['a', 'b']) { for (let source = 0; source < 5; source++) { for (let target = source + 1; target < 5; target++) { edges.push({ id: `${prefix}${source}-${prefix}${target}`, source: `${prefix}${source}`, target: `${prefix}${target}`, data: { risk: 1 } }); } } } edges.push({ id: 'a4-b0', source: 'a4', target: 'b0', data: { risk: 1 } }); edges.push({ id: 'a0-b4', source: 'a0', target: 'b4', data: { risk: 12 } }); og.setGraph({ nodes, edges }); // The analytical components are domain groups, not screen coordinates. OGJS derives each // component's ring and the spacing between components from that grouping intent. og.layouts.clustered({ groupBy: ({ id }) => id === 'isolate' ? 'unlinked' : String(id).slice(0, 1), groupGap: 76, memberGap: 18, }); og.camera.fit({ target: 'all', avoid: document.querySelector('.workflow-panel'), paddingPx: 24, maxOccupancy: .62, }); // Build neighbor lists once for this graph revision, then reuse them across path, component, // and community requests instead of rebuilding adjacency for every interaction. const adjacency = buildAdjacency(og.graph); const ids = nodes.map(({ id }) => id); const edgeIds = edges.map(({ id }) => id); const palette = ['#2563eb', '#7c3aed', '#ea580c', '#059669']; const status = document.querySelector('#analysis-status'); function pathEdges(path) { const result = new Set(); for (let step = 1; step < path.length; step++) { const edge = og.getEdgesBetween(path[step - 1], path[step])[0]; if (edge?.id !== undefined) result.add(edge.id); } return result; } async function paint(state, nodeColors, activeEdges = new Set()) { // Analysis returns evidence rather than mutating styles. The application chooses how to // emphasize that evidence, and OGJS animates all node and edge targets on one frame clock. document.querySelectorAll('[data-analysis]').forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.analysis === state.mode))); await og.animate({ ids, color: nodeColors, size: ids.map((_, index) => nodeColors[index] === '#cbd5e1' ? 7 : 11), edgeIds, edgeColor: edgeIds.map((edge) => activeEdges.has(edge) ? '#2563eb' : '#cbd5e1'), edgeWidth: edgeIds.map((edge) => activeEdges.has(edge) ? 2.4 : 0.8), }, 220, 'easeOut'); } function showPath(mode) { // Fewest-hop routing treats every relationship equally; lowest-risk routing supplies a // weight accessor that gives domain meaning to `edge.data.risk`. const route = mode === 'fewest' ? { path: shortestPath(og.graph, 'a0', 'b4', adjacency), distance: 1 } : dijkstra(og.graph, 'a0', 'b4', (edge) => og.graph.getEdgeData(edge).risk, adjacency); if (!route?.path) { status.textContent = 'No route'; return paint({ mode, path: [], distance: null }, ids.map(() => '#cbd5e1')); } const active = new Set(route.path); const names = route.path.map((id) => labels[id]).join(' → '); status.textContent = mode === 'fewest' ? `Fewest hops · ${route.distance} hop · ${names}` : `Lowest risk · risk ${route.distance} · ${names}`; return paint( { mode, path: route.path, distance: route.distance }, ids.map((id) => active.has(id) ? '#2563eb' : '#cbd5e1'), pathEdges(route.path), ); } function showComponents() { const result = connectedComponents(og.graph, adjacency); status.textContent = `${result.count} connected components`; return paint( { mode: 'components', path: [], distance: null, componentCount: result.count }, ids.map((_, index) => palette[result.component[index] % palette.length]), ); } function showCommunities() { const result = labelPropagation(og.graph, 20, adjacency, 2026); const count = result.length === 0 ? 0 : Math.max(...result) + 1; status.textContent = `${count} communities · seeded and repeatable`; return paint( { mode: 'communities', path: [], distance: null, communityCount: count }, ids.map((_, index) => palette[result[index] % palette.length]), ); } document.querySelector('[data-analysis="fewest"]').onclick = () => showPath('fewest'); document.querySelector('[data-analysis="risk"]').onclick = () => showPath('risk'); document.querySelector('[data-analysis="components"]').onclick = showComponents; document.querySelector('[data-analysis="communities"]').onclick = showCommunities; showComponents();