Hypergraph Visualization
A hypergraph generalizes a graph: a hyperedge connects any number of
vertices, not just two. Nodus visualizes hypergraphs through the
nodus.hypergraph module, which implements a scalable rendering pipeline — set
data, simplify, lay out, and render hyperedges as regions around their vertices.
This guide walks the practical pipeline end to end. For the concepts behind it,
see Hypergraphs.
The pipeline is chainable:
setData(data) → simplify(opts?) → layout(opts?) → render(opts?)Try it live — the full set→simplify→layout→render pipeline running end to end:
1. Provide the data
A hypergraph is a set of vertices and a set of hyperedges, where each
hyperedge lists the vertex ids it contains:
nodus.hypergraph.setData({ vertices: [ { id: 'alice' }, { id: 'bob' }, { id: 'carol' }, { id: 'dave' }, ], hyperedges: [ { id: 'paper-1', vertices: ['alice', 'bob', 'carol'] }, { id: 'paper-2', vertices: ['carol', 'dave'] }, ],});A vertex may optionally carry a nodeId (to tie it to a node in your main graph)
and arbitrary data; a hyperedge may carry data too.
From an existing graph
If you already have a graph loaded, build a hypergraph from it with
fromNodesAndEdges. Provide a groupBy function that returns, for each node, the
list of hyperedge ids it belongs to:
nodus.hypergraph.fromNodesAndEdges({ // Group nodes into hyperedges by some attribute of your data. groupBy: (node) => node.getData()?.communities ?? [],});Both setData and fromNodesAndEdges return the module, so you can chain.
2. Simplify
simplify(opts?) reduces visual clutter by merging and scaling the structure. It
returns { scales, operations }.
const { scales, operations } = nodus.hypergraph.simplify({ alpha: 1, beta: 1, gamma: 1, stopWhen: 'noForbidden',});simplify options
| Option | Meaning |
|---|---|
alpha, beta, gamma | Weights that balance the simplification objective. |
adjacencyT | Adjacency threshold used while simplifying. |
maxScales | Cap on how many scales to produce. |
stopWhen | Stopping rule: 'linear', 'noForbidden', 'targetVertices', 'targetHyperedges', or 'manual'. |
targetVertexCount | Target vertex count when stopWhen: 'targetVertices'. |
targetHyperedgeCount | Target hyperedge count when stopWhen: 'targetHyperedges'. |
forbiddenSubgraphs | Subgraph patterns to avoid. |
betweennessRefreshEvery | How often to refresh betweenness during simplification. |
// Simplify down to a target number of hyperedges.nodus.hypergraph.simplify({ stopWhen: 'targetHyperedges', targetHyperedgeCount: 12,});3. Lay out
layout(opts?) positions the vertices and hyperedge regions. It’s a Promise and
resolves with { totalEnergy, overlapCount } so you can judge quality.
const result = await nodus.hypergraph.layout({ weights: { regularity: 1, area: 1, separation: 1, intersection: 1, coordination: 1, }, separationIters: 200, regularityIters: 200, solver: 'lbfgs', onProgress: (info) => console.log('layout progress', info),});
console.log('energy', result.totalEnergy, 'overlaps', result.overlapCount);layout options
| Option | Meaning |
|---|---|
weights | Per-term weights: regularity, area, separation, intersection, coordination. |
separationIters | Iterations spent pushing regions apart. |
regularityIters | Iterations spent regularizing region shapes. |
solver | 'lbfgs' or 'adam'. |
optimizeDual | Also optimize the dual layout. |
targetAreaPerCardinality(k) | Target area for a hyperedge of cardinality k. |
bufferDistance | Padding kept around regions. |
useWorker | Run in a Web Worker (default true). |
progressEvery / onProgress(info) | Progress reporting cadence and callback. |
4. Render
render(opts?) draws the result. It returns { primal?, dual? }.
nodus.hypergraph.render({ view: 'primal', palette: ['#0ea5e9', '#f97316', '#22c55e', '#a855f7'], showVertices: true, position: 'below', fillOpacity: 0.25, strokeOpacity: 0.8,});render options
| Option | Meaning |
|---|---|
view | 'primal', 'dual', 'both', or '2.5d'. |
palette | Colors used for hyperedge regions. |
showVertices | Draw the vertices themselves. |
showIncidenceEdges | Draw vertex–hyperedge incidence edges. |
position | Draw 'below' or 'above' the graph. |
layer | Render on the 'canvas' or 'svg' layer. |
fillOpacity / strokeOpacity | Region fill and stroke opacity. |
vertexRadius | Radius of drawn vertices. |
interactive | Enable hover/selection interaction. |
hoverFillOpacityScale / hoverStrokeWidth | Hover emphasis. |
selectionStrokeColor / selectionStrokeWidth / selectionDash | Selected-region styling. |
levelOf / levelSpacing / obliqueTilt | Layout of the '2.5d' view. |
vertexTag | Tagging used when drawing vertices. |
The '2.5d' view stacks hyperedges on tilted levels for a layered look:
nodus.hypergraph.render({ view: '2.5d', levelSpacing: 40, obliqueTilt: 0.5,});Try it live — switch between the primal and dual views of the same hypergraph:
Querying the result
Read back positions and quality metrics:
const positions = nodus.hypergraph.getVertexPositions(); // Map<id, {x, y}>const xy = positions.get('alice');
const metrics = nodus.hypergraph.getQualityMetrics();// { overlapCount, overlapArea, avgRegularity, forbiddenSubgraphCount }console.log('regularity', metrics.avgRegularity);You can also get a hyperedge’s outline polygon to draw or hit-test yourself:
const polygon = nodus.hypergraph.getHyperedgePolygon('paper-1'); // {x, y}[]Selection and events
When you render with interactive: true, the module tracks hovered and selected
hyperedges and vertices. Toggle selection programmatically, and read it back:
nodus.hypergraph.setSelected({ hyperedges: ['paper-1'], replace: true });nodus.hypergraph.toggleSelected({ vertex: 'carol' });
const selected = nodus.hypergraph.getSelectedHyperedges();nodus.hypergraph.clearSelection();Subscribe to interaction with on(event, cb):
nodus.hypergraph.on('hyperedgeClick', (e) => { console.log('clicked hyperedge', e);});
nodus.hypergraph.on('vertexHover', (v) => highlight(v));nodus.hypergraph.on('selectionChanged', () => updatePanel());The available events are simplifyStart, simplifyEnd, layoutStart,
layoutPhase, layoutIter, layoutEnd, render, viewChanged,
hyperedgeHover, hyperedgeOut, hyperedgeClick, vertexHover, vertexOut,
vertexClick, and selectionChanged. Remove a handler with off(cb).
Switching views and lifecycle
nodus.hypergraph.setActiveView('dual'); // 'primal' | 'dual' | 'both'nodus.hypergraph.refresh();nodus.hypergraph.hide();nodus.hypergraph.show();nodus.hypergraph.destroy();Putting it together
import { Nodus } from '@kortexya/nodus';
const nodus = new Nodus({ container: document.getElementById('graph') });
await nodus.hypergraph .setData({ vertices: [{ id: 'a' }, { id: 'b' }, { id: 'c' }, { id: 'd' }], hyperedges: [ { id: 'h1', vertices: ['a', 'b', 'c'] }, { id: 'h2', vertices: ['c', 'd'] }, ], }) .simplify({ stopWhen: 'noForbidden' }) .layout({ solver: 'lbfgs', separationIters: 200, regularityIters: 200 });
nodus.hypergraph.render({ view: 'both', showVertices: true, palette: ['#0ea5e9', '#f97316'], interactive: true,});
nodus.hypergraph.on('hyperedgeClick', (e) => console.log('clicked', e));Next
- Hypergraphs — the model and the ideas behind the pipeline.
- API: Hypergraph — the full module reference.
- Applying Layouts — ordinary graph layouts for the non-hypergraph case.