Skip to content

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:

The hypergraph pipeline Open in new tab ↗

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.

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.

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

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

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

Primal & dual views Open in new tab ↗

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}[]

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

nodus.hypergraph.setActiveView('dual'); // 'primal' | 'dual' | 'both'
nodus.hypergraph.refresh();
nodus.hypergraph.hide();
nodus.hypergraph.show();
nodus.hypergraph.destroy();
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));