OG
JS
Docs
Demos
Examples
Hierarchy workbench
Click a node to promote it. Click an edge to promote both endpoints.
Hierarchy controls
Level distance
54
Node distance
24
Component distance
54
Direction
Top down
Bottom up
Left to right
Right to left
Arrange components
Across flow
Stacked
Priority lanes
Topology
Operational priority
Edge routing
Straight
Soft curve
Root · Company
// Provide a tree or directed graph plus hierarchy options such as direction and spacing. // OGJS assigns levels, routes edges, packs components, scales for the viewport, and animates // root changes; applications never calculate canvas width, level coordinates, or curves. import { Ogjs, synthesizeTopology } from '../../src/index'; const og = new Ogjs({ container: '#viz', theme: 'light', labels: { minZoom: 0 } }); const tree = synthesizeTopology({ model: 'tree', children: 2, height: 5, idPrefix: 'hier-', }); const upperLabels = ['Company', 'Product', 'Operations', 'Platform', 'Data', 'Sales', 'Finance']; const branchColors = ['#334155', '#2563eb', '#7c3aed']; const nodes = tree.nodes.map((node, index) => { const depth = Math.floor(Math.log2(index + 1)); let branch = index; while (branch > 2) branch = Math.floor((branch - 1) / 2); return { ...node, label: upperLabels[index], size: depth === 0 ? 12 : depth <= 2 ? 9 : 5.5, color: branchColors[branch], data: { priority: index * 3 % 5 }, }; }); const edges = [...tree.edges]; for (const companion of [ { prefix: 'support-', labels: ['Support', 'Helpdesk', 'Quality'], color: '#059669' }, { prefix: 'governance-', labels: ['Governance', 'Risk', 'Audit'], color: '#d97706' }, ]) { const draft = synthesizeTopology({ model: 'tree', nodes: 3, children: 2, idPrefix: companion.prefix, }); nodes.push(...draft.nodes.map((node, index) => ({ ...node, label: companion.labels[index], size: index === 0 ? 12 : 7, color: companion.color, data: { priority: index }, }))); edges.push(...draft.edges); } og.setGraph({ nodes, edges }); og.styles.addEdgeRule(() => ({ color: '#b9c5d4', width: 1.25 })); let roots = ['hier-n0']; const settings = { layerGap: 54, nodeGap: 24, componentGap: 54, flow: 'south', components: 'across', priority: 'topology', routing: 'straight', }; function state(moving) { } state(false); function layout() { // These are hierarchy intentions, not coordinates. OGJS ranks nodes, routes edges, and // packs disconnected components for the selected reading direction. og.layouts.hierarchical({ roots, flow: settings.flow, layerGap: settings.layerGap, nodeGap: settings.nodeGap, componentGap: settings.componentGap, components: settings.components, routing: settings.routing, rankBy: settings.priority === 'operations' ? ({ data }) => data.priority : undefined, }); } async function arrange() { state(true); await og.transitionLayout(layout, { duration: 280 }); state(false); const rootNames = roots.map((id) => { return og.getNode(id)?.label || String(id); }); document.querySelector('#status').textContent = `Root · ${rootNames.join(' + ')} · ${settings.flow}`; } og.on('nodeclick', ({ nodeId }) => { if (nodeId === undefined) return; roots = [nodeId]; void arrange(); }); og.on('edgeclick', ({ sourceId, targetId }) => { if (sourceId === undefined || targetId === undefined) return; roots = [sourceId, targetId]; void arrange(); }); for (const [id, key, output] of [ ['level-gap', 'layerGap', 'level-value'], ['node-gap', 'nodeGap', 'node-value'], ['component-gap', 'componentGap', 'component-value'], ]) { document.querySelector(`#${id}`).addEventListener('input', (event) => { settings[key] = Number(event.currentTarget.value); document.querySelector(`#${output}`).textContent = String(settings[key]); void arrange(); }); } document.querySelector('#flow').addEventListener('change', (event) => { settings.flow = event.currentTarget.value; void arrange(); }); document.querySelector('#components').addEventListener('change', (event) => { settings.components = event.currentTarget.value; void arrange(); }); document.querySelector('#priority').addEventListener('change', (event) => { settings.priority = event.currentTarget.value; void arrange(); }); document.querySelector('#routing').addEventListener('change', (event) => { settings.routing = event.currentTarget.value; layout(); state(false); }); layout(); og.camera.fit({ target: 'visible', avoid: document.querySelector('.panel'), paddingPx: 24, maxOccupancy: .72, retain: true, }); // Keep the hierarchy legible without letting root nodes dominate or overlap dense leaves. // The scale is a presentation intent; graph coordinates remain untouched. og.setNodeScale(3, { maxDiameterPx: 28 });
<!doctype html> <html lang="en"><head><meta charset="utf-8"/> <meta name="viewport" content="width=device-width,initial-scale=1"/> <meta name="ogjs-api" content="layouts-hierarchical:layouts.hierarchical,ogjs-transition-layout:transitionLayout,camera-fit:camera.fit"/> <title>Ogjs example — Hierarchy workbench</title> <link rel="stylesheet" href="../site.css"/><link rel="stylesheet" href="./data-lifecycle.css"/> <style> body { overflow: hidden; background: #fff; } #viz { position: fixed; top: 52px; right: 0; bottom: 0; left: 356px; } .panel { position: fixed; z-index: 5; top: 66px; left: 14px; width: 328px; background: #fff; color: #0f172a; border: 1px solid #dbe2ea; border-radius: 10px; padding: 16px; box-shadow: 0 8px 24px rgba(15, 23, 42, .08); } .panel h1 { font: 700 17px/1.25 var(--sans); margin: 0 0 6px; } .panel p { font: 400 12.5px/1.5 var(--sans); color: #526176; margin: 0; } .controls { border: 0; margin: 14px 0 0; padding: 0; display: grid; gap: 9px; } .controls legend { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; } .control { display: grid; grid-template-columns: 1fr auto; gap: 4px 10px; align-items: center; } .control label { font: 600 11.5px/1.3 var(--sans); color: #334155; } .control output { font: 600 11px/1.3 var(--mono); color: #64748b; } .control input { grid-column: 1 / -1; width: 100%; accent-color: #f59e0b; } .control select { grid-column: 1 / -1; min-width: 0; width: 100%; padding: 6px 8px; border: 1px solid #cbd5e1; border-radius: 5px; background: #f8fafc; color: #0f172a; font: 500 11.5px var(--sans); } #status { display: block; margin-top: 12px; border-top: 1px solid #e2e8f0; padding-top: 10px; color: #334155; font: 600 11.5px/1.4 var(--mono); } @media (max-width: 760px), (max-width: 1000px) and (max-height: 500px) { #viz { top: 246px; left: 0; } .panel { top: 62px; left: 10px; right: 10px; width: auto; padding: 12px 14px; } .panel p { display: none; } .controls { grid-template-columns: repeat(3, 1fr); gap: 7px 10px; } .control { gap: 3px 6px; } .control:has(select) { grid-template-rows: 30px 44px; align-self: stretch; } .control:has(select) label { align-self: end; } .control select { padding: 5px 6px; } } </style></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> <main class="workbench-frame"> <div id="viz" aria-label="Interactive hierarchy graph"></div> <section class="panel workbench-panel" aria-labelledby="title"> <h1 id="title">Hierarchy workbench</h1> <p>Click a node to promote it. Click an edge to promote both endpoints.</p> <fieldset class="controls"> <legend>Hierarchy controls</legend> <div class="control"><label for="level-gap">Level distance</label><output id="level-value">54</output> <input id="level-gap" type="range" min="28" max="120" value="54"/></div> <div class="control"><label for="node-gap">Node distance</label><output id="node-value">24</output> <input id="node-gap" type="range" min="8" max="64" value="24"/></div> <div class="control"><label for="component-gap">Component distance</label><output id="component-value">54</output> <input id="component-gap" type="range" min="16" max="120" value="54"/></div> <div class="control"><label for="flow">Direction</label> <select id="flow"><option value="south">Top down</option><option value="north">Bottom up</option> <option value="east">Left to right</option><option value="west">Right to left</option></select></div> <div class="control"><label for="components">Arrange components</label> <select id="components"><option value="across">Across flow</option><option value="stacked">Stacked</option></select></div> <div class="control"><label for="priority">Priority lanes</label> <select id="priority"><option value="topology">Topology</option><option value="operations">Operational priority</option></select></div> <div class="control"><label for="routing">Edge routing</label> <select id="routing"><option value="straight">Straight</option><option value="soft">Soft curve</option></select></div> </fieldset> <output id="status" aria-live="polite">Root · Company</output> </section> </main> <script type="module"> // Provide a tree or directed graph plus hierarchy options such as direction and spacing. // OGJS assigns levels, routes edges, packs components, scales for the viewport, and animates // root changes; applications never calculate canvas width, level coordinates, or curves. import { Ogjs, synthesizeTopology } from '../../src/index'; const og = new Ogjs({ container: '#viz', theme: 'light', labels: { minZoom: 0 } }); const tree = synthesizeTopology({ model: 'tree', children: 2, height: 5, idPrefix: 'hier-', }); const upperLabels = ['Company', 'Product', 'Operations', 'Platform', 'Data', 'Sales', 'Finance']; const branchColors = ['#334155', '#2563eb', '#7c3aed']; const nodes = tree.nodes.map((node, index) => { const depth = Math.floor(Math.log2(index + 1)); let branch = index; while (branch > 2) branch = Math.floor((branch - 1) / 2); return { ...node, label: upperLabels[index], size: depth === 0 ? 12 : depth <= 2 ? 9 : 5.5, color: branchColors[branch], data: { priority: index * 3 % 5 }, }; }); const edges = [...tree.edges]; for (const companion of [ { prefix: 'support-', labels: ['Support', 'Helpdesk', 'Quality'], color: '#059669' }, { prefix: 'governance-', labels: ['Governance', 'Risk', 'Audit'], color: '#d97706' }, ]) { const draft = synthesizeTopology({ model: 'tree', nodes: 3, children: 2, idPrefix: companion.prefix, }); nodes.push(...draft.nodes.map((node, index) => ({ ...node, label: companion.labels[index], size: index === 0 ? 12 : 7, color: companion.color, data: { priority: index }, }))); edges.push(...draft.edges); } og.setGraph({ nodes, edges }); og.styles.addEdgeRule(() => ({ color: '#b9c5d4', width: 1.25 })); let roots = ['hier-n0']; const settings = { layerGap: 54, nodeGap: 24, componentGap: 54, flow: 'south', components: 'across', priority: 'topology', routing: 'straight', }; function state(moving) { } state(false); function layout() { // These are hierarchy intentions, not coordinates. OGJS ranks nodes, routes edges, and // packs disconnected components for the selected reading direction. og.layouts.hierarchical({ roots, flow: settings.flow, layerGap: settings.layerGap, nodeGap: settings.nodeGap, componentGap: settings.componentGap, components: settings.components, routing: settings.routing, rankBy: settings.priority === 'operations' ? ({ data }) => data.priority : undefined, }); } async function arrange() { state(true); await og.transitionLayout(layout, { duration: 280 }); state(false); const rootNames = roots.map((id) => { return og.getNode(id)?.label || String(id); }); document.querySelector('#status').textContent = `Root · ${rootNames.join(' + ')} · ${settings.flow}`; } og.on('nodeclick', ({ nodeId }) => { if (nodeId === undefined) return; roots = [nodeId]; void arrange(); }); og.on('edgeclick', ({ sourceId, targetId }) => { if (sourceId === undefined || targetId === undefined) return; roots = [sourceId, targetId]; void arrange(); }); for (const [id, key, output] of [ ['level-gap', 'layerGap', 'level-value'], ['node-gap', 'nodeGap', 'node-value'], ['component-gap', 'componentGap', 'component-value'], ]) { document.querySelector(`#${id}`).addEventListener('input', (event) => { settings[key] = Number(event.currentTarget.value); document.querySelector(`#${output}`).textContent = String(settings[key]); void arrange(); }); } document.querySelector('#flow').addEventListener('change', (event) => { settings.flow = event.currentTarget.value; void arrange(); }); document.querySelector('#components').addEventListener('change', (event) => { settings.components = event.currentTarget.value; void arrange(); }); document.querySelector('#priority').addEventListener('change', (event) => { settings.priority = event.currentTarget.value; void arrange(); }); document.querySelector('#routing').addEventListener('change', (event) => { settings.routing = event.currentTarget.value; layout(); state(false); }); layout(); og.camera.fit({ target: 'visible', avoid: document.querySelector('.panel'), paddingPx: 24, maxOccupancy: .72, retain: true, }); // Keep the hierarchy legible without letting root nodes dominate or overlap dense leaves. // The scale is a presentation intent; graph coordinates remain untouched. og.setNodeScale(3, { maxDiameterPx: 28 }); </script></body></html>