OG
JS
Docs
Demos
Examples
Database result lab
Convert backend query results into one graph shape. Credentials and drivers stay on your server.
Neo4j
Gremlin
SPARQL
SQL rows
// Database drivers and credentials stay on the server; this page starts with returned rows. // The selected adapter converts backend-specific records into OGJS graph data, after which // layout, rendering, interaction, and framing use the same public client-side path. import { Ogjs, gremlinToGraph, neo4jToGraph, rowsToGraph, sparqlToGraph, } from '../../src/index'; const og = new Ogjs({ container: '#viz', theme: 'light' }); const panel = document.querySelector('.workflow-panel'); // These fixtures mirror the values a backend would serialize after running each query. // They deliberately keep the native database shapes so the example shows where conversion // belongs instead of pretending every driver already returns OGJS node and edge records. const neo4j = [{ alice: { elementId: 'a', labels: ['Person'], properties: { name: 'Alice' } }, work: { elementId: 'r1', type: 'KNOWS', startNodeElementId: 'a', endNodeElementId: 'b', properties: { channel: 'work' }, }, bob: { elementId: 'b', labels: ['Person'], properties: { name: 'Bob' } }, school: { elementId: 'r2', type: 'KNOWS', startNodeElementId: 'a', endNodeElementId: 'b', properties: { channel: 'school' }, }, transfer: { elementId: 'r3', type: 'SENT', startNodeElementId: 'b', endNodeElementId: 'c', properties: { amount: 240 }, }, carol: { elementId: 'c', labels: ['Person'], properties: { name: 'Carol' } }, }]; const gremlin = [ { id: 'm', label: 'person', name: 'Marko' }, { id: 'v', label: 'person', name: 'Vadas' }, { id: 'l', label: 'software', name: 'Lop' }, { id: 'e1', label: 'knows', OUT: { id: 'm' }, IN: { id: 'v' }, weight: 0.5 }, { id: 'e2', label: 'created', OUT: { id: 'm' }, IN: { id: 'l' }, weight: 0.4 }, ]; const sparql = { results: { bindings: [ { s: { value: 'https://example.test/Alice' }, p: { value: 'https://example.test/knows' }, o: { value: 'https://example.test/Bob' }, }, { s: { value: 'https://example.test/Bob' }, p: { value: 'https://example.test/worksWith' }, o: { value: 'https://example.test/Carol' }, }, ] } }; const rows = [ { source_id: 1, target_id: 2, source_name: 'Acme', target_name: 'Subsidiary', relation: 'OWNS', share: 80 }, { source_id: 1, target_id: 3, source_name: 'Acme', target_name: 'Supplier', relation: 'OWNS', share: 25 }, ]; const payloads = { neo4j, gremlin, sparql, rows }; const colors = { neo4j: ['#2563eb', '#7c3aed', '#059669'], gremlin: ['#ea580c', '#2563eb', '#059669'], sparql: ['#7c3aed', '#2563eb', '#dc2626'], rows: ['#0f172a', '#2563eb', '#059669'], }; // Presentation remains declarative and independent of the query. The converter preserves // Neo4j labels on node data, so the same rule will style later Cypher results without // rewriting the returned records or calculating canvas coordinates. og.styles.addNodeRule(({ data, index }) => Array.isArray(data?.labels) ? { size: index === 0 ? 14 : 10, color: colors.neo4j[index % colors.neo4j.length], } : undefined); function convert(format) { // Each public adapter removes one database-specific concern and returns the same graph // contract. For SQL rows, the field map names which columns identify endpoints, labels, // and relationship types because tabular results do not carry that meaning themselves. if (format === 'neo4j') return neo4jToGraph(neo4j); if (format === 'gremlin') return gremlinToGraph(gremlin); if (format === 'sparql') return sparqlToGraph(sparql); return rowsToGraph(rows, { source: 'source_id', target: 'target_id', edgeType: 'relation', sourceLabel: 'source_name', targetLabel: 'target_name', }); } function relationOf(format, graph) { // Adapter outputs keep source-specific properties in `edge.data`. Reading one property // here demonstrates that normalization does not discard the evidence returned by a query. const data = graph.edges[0]?.data ?? {}; if (format === 'neo4j') return data.channel; if (format === 'gremlin') return data.label; if (format === 'sparql') return data.predicate; return data.type; } async function show(format) { let graph; if (format === 'neo4j') { // The application supplies only a secure query executor. In production this callback // normally POSTs `query` and `params` to your backend with `signal`; this network-free // example returns the same driver-shaped records locally. OGJS then normalizes stable // node/relationship IDs, replaces the graph, lays it out, and frames it around the panel. graph = await og.loadCypher({ query: 'MATCH (a)-[r]->(b) RETURN a, r, b LIMIT $limit', params: { limit: 500 }, execute: async ({ signal }) => { if (signal.aborted) throw new DOMException('Cancelled', 'AbortError'); return { records: neo4j }; }, layout: 'circular', fit: { target: 'all', avoid: panel, paddingPx: 24, maxOccupancy: .48 }, }); og.styles.apply(); } else { graph = convert(format); // The lower-level converters remain useful when an application already owns its load // lifecycle. They return the same graph contract; only semantic presentation is added. graph.nodes = graph.nodes.map((node, index) => ({ ...node, size: index === 0 ? 14 : 10, color: colors[format][index], })); og.setGraph(graph); og.layouts.circular({ radius: 92 }); og.camera.fit({ target: 'all', avoid: panel, paddingPx: 24, maxOccupancy: .48, }); } const counts = `${graph.nodes.length} nodes · ${graph.edges.length} edges`; document.querySelector('#database-status').textContent = graph.nodes.length ? `${format.toUpperCase()} · ${counts}` : 'No graph records returned'; document.querySelector('#database-preview').textContent = JSON.stringify(payloads[format], null, 2).split('\n').slice(0, 14).join('\n'); document.querySelectorAll('[data-database]').forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.database === format))); } document.querySelectorAll('[data-database]').forEach((button) => button.addEventListener('click', () => { void show(button.dataset.database); })); void show('neo4j');
<!doctype html> <html lang="en"><head><meta charset="utf-8"/> <meta name="viewport" content="width=device-width, initial-scale=1"/> <title>Ogjs example — Database result lab</title> <link rel="stylesheet" href="../site.css"/><link rel="stylesheet" href="./data-lifecycle.css"/> </head><body> <header class="rail"><a class="wordmark" href="../index.html">OG<b>JS</b></a><nav> <a href="../docs.html">Docs</a><a href="../demos.html">Demos</a> <a href="./index.html" aria-current="page">Examples</a></nav></header> <div id="viz"></div> <section class="workflow-panel"><h1>Database result lab</h1> <p>Convert backend query results into one graph shape. Credentials and drivers stay on your server.</p> <div class="controls"> <button data-database="neo4j">Neo4j</button> <button data-database="gremlin">Gremlin</button> <button data-database="sparql">SPARQL</button> <button data-database="rows">SQL rows</button> </div> <div class="status" id="database-status" aria-live="polite"></div> <pre id="database-preview"></pre> </section> <script type="module"> // Database drivers and credentials stay on the server; this page starts with returned rows. // The selected adapter converts backend-specific records into OGJS graph data, after which // layout, rendering, interaction, and framing use the same public client-side path. import { Ogjs, gremlinToGraph, neo4jToGraph, rowsToGraph, sparqlToGraph, } from '../../src/index'; const og = new Ogjs({ container: '#viz', theme: 'light' }); const panel = document.querySelector('.workflow-panel'); // These fixtures mirror the values a backend would serialize after running each query. // They deliberately keep the native database shapes so the example shows where conversion // belongs instead of pretending every driver already returns OGJS node and edge records. const neo4j = [{ alice: { elementId: 'a', labels: ['Person'], properties: { name: 'Alice' } }, work: { elementId: 'r1', type: 'KNOWS', startNodeElementId: 'a', endNodeElementId: 'b', properties: { channel: 'work' }, }, bob: { elementId: 'b', labels: ['Person'], properties: { name: 'Bob' } }, school: { elementId: 'r2', type: 'KNOWS', startNodeElementId: 'a', endNodeElementId: 'b', properties: { channel: 'school' }, }, transfer: { elementId: 'r3', type: 'SENT', startNodeElementId: 'b', endNodeElementId: 'c', properties: { amount: 240 }, }, carol: { elementId: 'c', labels: ['Person'], properties: { name: 'Carol' } }, }]; const gremlin = [ { id: 'm', label: 'person', name: 'Marko' }, { id: 'v', label: 'person', name: 'Vadas' }, { id: 'l', label: 'software', name: 'Lop' }, { id: 'e1', label: 'knows', OUT: { id: 'm' }, IN: { id: 'v' }, weight: 0.5 }, { id: 'e2', label: 'created', OUT: { id: 'm' }, IN: { id: 'l' }, weight: 0.4 }, ]; const sparql = { results: { bindings: [ { s: { value: 'https://example.test/Alice' }, p: { value: 'https://example.test/knows' }, o: { value: 'https://example.test/Bob' }, }, { s: { value: 'https://example.test/Bob' }, p: { value: 'https://example.test/worksWith' }, o: { value: 'https://example.test/Carol' }, }, ] } }; const rows = [ { source_id: 1, target_id: 2, source_name: 'Acme', target_name: 'Subsidiary', relation: 'OWNS', share: 80 }, { source_id: 1, target_id: 3, source_name: 'Acme', target_name: 'Supplier', relation: 'OWNS', share: 25 }, ]; const payloads = { neo4j, gremlin, sparql, rows }; const colors = { neo4j: ['#2563eb', '#7c3aed', '#059669'], gremlin: ['#ea580c', '#2563eb', '#059669'], sparql: ['#7c3aed', '#2563eb', '#dc2626'], rows: ['#0f172a', '#2563eb', '#059669'], }; // Presentation remains declarative and independent of the query. The converter preserves // Neo4j labels on node data, so the same rule will style later Cypher results without // rewriting the returned records or calculating canvas coordinates. og.styles.addNodeRule(({ data, index }) => Array.isArray(data?.labels) ? { size: index === 0 ? 14 : 10, color: colors.neo4j[index % colors.neo4j.length], } : undefined); function convert(format) { // Each public adapter removes one database-specific concern and returns the same graph // contract. For SQL rows, the field map names which columns identify endpoints, labels, // and relationship types because tabular results do not carry that meaning themselves. if (format === 'neo4j') return neo4jToGraph(neo4j); if (format === 'gremlin') return gremlinToGraph(gremlin); if (format === 'sparql') return sparqlToGraph(sparql); return rowsToGraph(rows, { source: 'source_id', target: 'target_id', edgeType: 'relation', sourceLabel: 'source_name', targetLabel: 'target_name', }); } function relationOf(format, graph) { // Adapter outputs keep source-specific properties in `edge.data`. Reading one property // here demonstrates that normalization does not discard the evidence returned by a query. const data = graph.edges[0]?.data ?? {}; if (format === 'neo4j') return data.channel; if (format === 'gremlin') return data.label; if (format === 'sparql') return data.predicate; return data.type; } async function show(format) { let graph; if (format === 'neo4j') { // The application supplies only a secure query executor. In production this callback // normally POSTs `query` and `params` to your backend with `signal`; this network-free // example returns the same driver-shaped records locally. OGJS then normalizes stable // node/relationship IDs, replaces the graph, lays it out, and frames it around the panel. graph = await og.loadCypher({ query: 'MATCH (a)-[r]->(b) RETURN a, r, b LIMIT $limit', params: { limit: 500 }, execute: async ({ signal }) => { if (signal.aborted) throw new DOMException('Cancelled', 'AbortError'); return { records: neo4j }; }, layout: 'circular', fit: { target: 'all', avoid: panel, paddingPx: 24, maxOccupancy: .48 }, }); og.styles.apply(); } else { graph = convert(format); // The lower-level converters remain useful when an application already owns its load // lifecycle. They return the same graph contract; only semantic presentation is added. graph.nodes = graph.nodes.map((node, index) => ({ ...node, size: index === 0 ? 14 : 10, color: colors[format][index], })); og.setGraph(graph); og.layouts.circular({ radius: 92 }); og.camera.fit({ target: 'all', avoid: panel, paddingPx: 24, maxOccupancy: .48, }); } const counts = `${graph.nodes.length} nodes · ${graph.edges.length} edges`; document.querySelector('#database-status').textContent = graph.nodes.length ? `${format.toUpperCase()} · ${counts}` : 'No graph records returned'; document.querySelector('#database-preview').textContent = JSON.stringify(payloads[format], null, 2).split('\n').slice(0, 14).join('\n'); document.querySelectorAll('[data-database]').forEach((button) => button.setAttribute('aria-pressed', String(button.dataset.database === format))); } document.querySelectorAll('[data-database]').forEach((button) => button.addEventListener('click', () => { void show(button.dataset.database); })); void show('neo4j'); </script></body></html>