Skip to content

The Graph Model

A Nodus graph is, at its simplest, two arrays: nodes and edges. You hand Nodus plain JSON in that shape, and from then on you work with rich element wrappers (Node, Edge) and collections (NodeList, EdgeList) that give you accessors, queries and broadcast operations.

This page covers the data shape you feed in and the objects you get back. To actually load and mutate a graph, see Building a Graph and the Graph API reference.

The RawGraph JSON shape

Everything starts as a RawGraph — the plain object you pass to setGraph, addGraph and the parsers:

type RawGraph = {
nodes: RawNode[];
edges: RawEdge[];
};
type RawNode = {
id?: string | number;
attributes?: Record<string, any>; // visual
data?: ND; // your typed payload
};
type RawEdge = {
id?: string | number;
source: string | number; // id of the source node
target: string | number; // id of the target node
attributes?: Record<string, any>; // visual
data?: ED; // your typed payload
};

A minimal graph:

import { Nodus } from '@kortexya/nodus';
const nodus = new Nodus({ container: document.getElementById('graph') });
await nodus.setGraph({
nodes: [{ id: 'a' }, { id: 'b' }, { id: 'c' }],
edges: [
{ source: 'a', target: 'b' },
{ source: 'b', target: 'c' },
],
});

Try it live — turn plain records into a RawGraph and render it:

Build a graph from data Open in new tab ↗

id, source, target

  • A node’s id uniquely identifies it. If you omit it, Nodus assigns one. In practice you almost always supply an id, because edges reference nodes by id.
  • An edge’s source and target are node ids. They are the only required fields on an edge — an edge id, like a node id, is optional.
await nodus.setGraph({
nodes: [{ id: 1 }, { id: 2 }],
edges: [{ id: 'e1', source: 1, target: 2 }],
});

attributes vs data

Each element carries two completely separate bags of values, and keeping them straight is the single most important idea in the model:

attributesdata
PurposeVisual — how the element is drawnYours — application payload
Examplesx, y, color, radius, shape, text{ label, type, score, ... }
Typed bythe fixed attribute catalogyour ND / ED generics
Drivesrendering, styling, layoutyour logic, tooltips, filters

attributes are the knobs Nodus understands and draws — see Styling for the full catalog. data is opaque to Nodus: it stores it, indexes it alongside the element, and hands it back untouched.

type Person = { name: string; role: string; followers: number };
// ND = Person, ED = { weight: number }
const nodus = new Nodus<Person, { weight: number }>({ container: 'graph' });
await nodus.setGraph({
nodes: [
{
id: 'ada',
attributes: { color: '#4f46e5', radius: 12, text: { content: 'Ada' } },
data: { name: 'Ada Lovelace', role: 'author', followers: 1843 },
},
],
edges: [],
});

The generics flow everywhere: node.getData() is typed as Person, and the compiler will catch a typo in a data field.

Element wrappers — Node and Edge

Queries return wrappers, not raw JSON. A Node (and an Edge, which mirrors it) is a thin handle over the element. Both are exported, so you can import the types:

import type { Node, Edge } from '@kortexya/nodus';

Identity and data

const ada = nodus.getNode('ada');
ada.getId(); // 'ada'
ada.getData(); // the whole Person object
ada.getData('role'); // 'author' (path access)
ada.setData('role', 'pioneer');
ada.setData({ followers: 2000 }); // shallow-merge an object

Visual attributes

ada.getAttributes(); // all visual attributes
ada.getAttribute('radius'); // a single one
ada.setAttribute('color', '#ef4444');
ada.setAttributes({ radius: 16, shape: 'hexagon' });
ada.resetAttributes(['color']); // back to rule/default value
ada.getPreviousAttributes(); // values before the last change

Position

Positions are visual attributes too, but they’re so common they get dedicated accessors:

ada.getPosition(); // { x, y } in graph coordinates
ada.setPosition({ x: 100, y: 40 });
ada.getPositionOnScreen(); // { x, y } in screen pixels
ada.isInScreen(); // currently within the viewport?

See Camera & Coordinates for the difference between graph and screen space.

Topology

Nodes know their neighbourhood:

ada.getAdjacentNodes(); // a NodeList of neighbours
ada.getAdjacentEdges(); // an EdgeList of incident edges
ada.getAdjacentElements(); // both
ada.getDegree(); // number of incident edges
ada.getConnectedComponent(); // the NodeList of its component

An Edge additionally exposes its endpoints — it knows its source and target nodes directly.

State, classes and styling

ada.isSelected(); ada.setSelected(true);
ada.isVisible(); ada.setVisible(false);
ada.isDisabled(); ada.setDisabled(true);
ada.addClass('highlight');
ada.hasClass('highlight'); // true
ada.getClassList(); // ['highlight']
ada.removeClass('highlight');

Other handy methods include locate() (pan/zoom to the element), pulse(), getBoundingBox({ includeTexts }) and toJSON().

Collections — NodeList and EdgeList

Most queries that can return more than one element give you a NodeList or EdgeList. These are array-like and iterable, with the functional helpers you’d expect plus set operations and — crucially — broadcast.

Array-like and functional

const nodes = nodus.getNodes();
nodes.size; // count
[...nodes]; // iterable
nodes.toArray(); // plain array of Node
nodes.get(0); // the first Node
nodes
.filter((n) => n.getData('followers') > 1000) // returns a NodeList
.map((n) => n.getId()); // returns an array
nodes.find((n) => n.getId() === 'ada');
nodes.some((n) => n.isSelected());
nodes.sort((a, b) => a.getDegree() - b.getDegree());

.filter, .slice and similar structure-preserving methods return another NodeList/EdgeList, so you can keep chaining.

Set operations

const a = nodus.getSelectedNodes();
const b = ada.getAdjacentNodes();
a.concat(b); // union-ish (may duplicate; use .dedupe())
a.subtract(b); // in a but not b
a.inverse(); // every node not in a
a.includes(ada); // membership

Broadcast

Calling a setter on a list applies it to every member — no loop required:

const hubs = nodus.getNodes().filter((n) => n.getDegree() > 5);
hubs.setAttributes({ color: '#f59e0b', radius: 18 });
hubs.addClass('hub');
hubs.setSelected(true);
hubs.locate(); // fit the camera to all of them
hubs.pulse();

Getters broadcast too, returning an array of results:

hubs.getId(); // ['n3', 'n7', ...]
hubs.getDegree(); // [6, 9, ...]
hubs.getData(); // [ {...}, {...}, ... ]
hubs.getPosition(); // [ {x,y}, {x,y}, ... ]

Querying the graph

Fetch single elements by id, or many with an optional predicate filter:

nodus.getNode('ada'); // a Node, or undefined
nodus.getEdge('e1'); // an Edge
nodus.getNodes(); // every node, as a NodeList
nodus.getNodes().filter((n) => n.getData('role') === 'author');
nodus.getEdges(); // every edge, as an EdgeList
nodus.getEdges().filter((e) => e.getData('weight') > 0.5);

Connected-component queries live alongside:

nodus.getConnectedComponents(); // array of NodeLists
nodus.getConnectedComponentByNode('ada');

Directed, undirected and self-loops

Edges in Nodus have a source and a target, so they carry a direction. Whether you show that direction is a styling choice: an edge’s shape.head / shape.tail control arrowheads (see Styling). Leave both as 'none' and the edge reads as undirected; add an arrow head and it reads as directed.

When an edge’s source and target are the same node, it’s a self-loop — Nodus draws it as an arc beside the node:

await nodus.addEdge({ source: 'ada', target: 'ada' }); // self-loop

Where to go next

  • Building a Graph — loading, adding and removing elements step by step.
  • Styling — the full visual attribute catalog.
  • API: Graph — every graph method and its options.
  • Layouts — positioning the nodes you’ve added.