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 ↗

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

OptionMeaning
alpha, beta, gammaWeights that balance the simplification objective.
adjacencyTAdjacency threshold used while simplifying.
maxScalesCap on how many scales to produce.
stopWhenStopping rule: 'linear', 'noForbidden', 'targetVertices', 'targetHyperedges', or 'manual'.
targetVertexCountTarget vertex count when stopWhen: 'targetVertices'.
targetHyperedgeCountTarget hyperedge count when stopWhen: 'targetHyperedges'.
forbiddenSubgraphsSubgraph patterns to avoid.
betweennessRefreshEveryHow 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

OptionMeaning
weightsPer-term weights: regularity, area, separation, intersection, coordination.
separationItersIterations spent pushing regions apart.
regularityItersIterations spent regularizing region shapes.
solver'lbfgs' or 'adam'.
optimizeDualAlso optimize the dual layout.
targetAreaPerCardinality(k)Target area for a hyperedge of cardinality k.
bufferDistancePadding kept around regions.
useWorkerRun 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

OptionMeaning
view'primal', 'dual', 'both', or '2.5d'.
paletteColors used for hyperedge regions.
showVerticesDraw the vertices themselves.
showIncidenceEdgesDraw vertex–hyperedge incidence edges.
positionDraw 'below' or 'above' the graph.
layerRender on the 'canvas' or 'svg' layer.
fillOpacity / strokeOpacityRegion fill and stroke opacity.
vertexRadiusRadius of drawn vertices.
interactiveEnable hover/selection interaction.
hoverFillOpacityScale / hoverStrokeWidthHover emphasis.
selectionStrokeColor / selectionStrokeWidth / selectionDashSelected-region styling.
levelOf / levelSpacing / obliqueTiltLayout of the '2.5d' view.
vertexTagTagging 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 ↗

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