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:
id, source, target
- A node’s
iduniquely identifies it. If you omit it, Nodus assigns one. In practice you almost always supply anid, because edges reference nodes by id. - An edge’s
sourceandtargetare 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:
attributes | data | |
|---|---|---|
| Purpose | Visual — how the element is drawn | Yours — application payload |
| Examples | x, y, color, radius, shape, text | { label, type, score, ... } |
| Typed by | the fixed attribute catalog | your ND / ED generics |
| Drives | rendering, styling, layout | your 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 objectada.getData('role'); // 'author' (path access)ada.setData('role', 'pioneer');ada.setData({ followers: 2000 }); // shallow-merge an objectVisual attributes
ada.getAttributes(); // all visual attributesada.getAttribute('radius'); // a single oneada.setAttribute('color', '#ef4444');ada.setAttributes({ radius: 16, shape: 'hexagon' });ada.resetAttributes(['color']); // back to rule/default valueada.getPreviousAttributes(); // values before the last changePosition
Positions are visual attributes too, but they’re so common they get dedicated accessors:
ada.getPosition(); // { x, y } in graph coordinatesada.setPosition({ x: 100, y: 40 });ada.getPositionOnScreen(); // { x, y } in screen pixelsada.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 neighboursada.getAdjacentEdges(); // an EdgeList of incident edgesada.getAdjacentElements(); // bothada.getDegree(); // number of incident edgesada.getConnectedComponent(); // the NodeList of its componentAn 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'); // trueada.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]; // iterablenodes.toArray(); // plain array of Nodenodes.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 ba.inverse(); // every node not in aa.includes(ada); // membershipBroadcast
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 themhubs.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 undefinednodus.getEdge('e1'); // an Edge
nodus.getNodes(); // every node, as a NodeListnodus.getNodes().filter((n) => n.getData('role') === 'author');
nodus.getEdges(); // every edge, as an EdgeListnodus.getEdges().filter((e) => e.getData('weight') > 0.5);Connected-component queries live alongside:
nodus.getConnectedComponents(); // array of NodeListsnodus.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-loopWhere 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.