Skip to content

An Interactive Explorer

In this tutorial you’ll build a compact graph explorer on top of a larger generated dataset. By the end you’ll have hover highlighting, click-to-select with a rectangle tool, the ability to focus the camera on any node, one-click neighbor expansion, and a search box that filters and locates matching nodes.

Each section builds on the last, and the full combined script is at the end. It assumes you’ve installed @kortexya/nodus and read Your First Graph.

Add a container plus a small search input. The CSS below positions the search box over the graph; you only need this HTML and CSS once.

<div id="graph"></div>
<input id="search" type="text" placeholder="Search nodes…" autocomplete="off" />
<style>
html, body { margin: 0; height: 100%; }
#graph { width: 100vw; height: 100vh; }
#search {
position: fixed; top: 16px; left: 16px; z-index: 10;
padding: 8px 12px; font-size: 14px; border-radius: 8px;
border: 1px solid #cbd5e1; background: #ffffff;
}
</style>

2. Create the instance and generate a graph

Construct Nodus, then fabricate a larger graph with one of the seeded generators. barabasiAlbert builds a scale-free network — a realistic shape for an explorer, with a few high-degree hubs and many leaf nodes. Generators are deterministic and return a Promise.

import { Nodus } from '@kortexya/nodus';
const nodus = new Nodus({ container: document.getElementById('graph') });
// Fabricate a scale-free graph to explore — generators return the graph.
const g = await nodus.generate.barabasiAlbert({ nodes: 40, m0: 2, m: 1 });
await nodus.setGraph(g);

3. Style, lay out and frame

Give nodes and edges a base style with rules, then apply a layout and fit the graph to the viewport. The force layout suits an organic network like this one; for a tree-shaped dataset you’d reach for nodus.layouts.hierarchical instead.

nodus.styles.addNodeRule({
color: '#334155',
radius: 10,
shape: 'circle',
});
nodus.styles.addEdgeRule({
color: '#cbd5e1',
width: 1.5,
});
await nodus.layouts.force({ duration: 0 });
await nodus.view.locateGraph();

4. Hover highlighting

styles.setHoveredNodeAttributes defines the visual attributes a node takes on while the pointer is over it. It applies globally — no per-node wiring needed.

nodus.styles.setHoveredNodeAttributes({
color: '#0ea5e9',
radius: 14,
outline: { color: '#0ea5e9' },
});

Now hovering any node enlarges it and gives it a bright outline, then reverts automatically when the pointer leaves.

5. Click-to-select with the rectangle tool

Enable the rectangleSelect tool so dragging a box over the canvas selects the nodes inside it. Read the current selection back with getSelectedNodes, which returns a NodeList. You can also style the selected state with styles.setSelectedNodeAttributes.

nodus.tools.rectangleSelect.enable();
nodus.styles.setSelectedNodeAttributes({
color: '#f59e0b',
outline: { color: '#b45309' },
});
// Log the selection whenever the user finishes a box.
nodus.tools.tooltip.onBackgroundClick(() => {
const selected = nodus.getSelectedNodes();
console.log(`${selected.size} node(s) selected`);
});

getNonSelectedNodes, clearSelection and the per-element node.setSelected(bool) round out the selection API.

6. Focus the camera on a node

When the user clicks a node, focus on it. node.locate() pans and zooms the camera to that single element; pass a { duration } to animate the move.

nodus.tools.tooltip.onNodeClick((node) => {
node.locate({ duration: 400 });
});

7. Neighbor expansion

Highlighting a node’s neighborhood is a common explorer gesture. On double-click, read the node’s neighbors with node.getAdjacentNodes() — which returns a NodeList — and pulse them so they stand out.

nodus.tools.tooltip.onNodeDoubleClick((node) => {
const neighbors = node.getAdjacentNodes();
neighbors.pulse();
console.log(`${node.getId()} has ${neighbors.size} neighbor(s)`);
});

pulse is a broadcast method on a NodeList: it runs on every node in the collection. You could equally neighbors.setSelected(true) or neighbors.addClass('neighborhood') to mark them.

8. A live search filter

Wire the search box to filter nodes and frame the matches. getNodes(predicate) returns a NodeList of nodes for which the predicate is true; calling .locate() on that list pans and zooms the camera to fit all of them.

const searchInput = document.getElementById('search');
searchInput.addEventListener('input', (event) => {
const query = event.target.value.trim().toLowerCase();
if (!query) return;
const matches = nodus.getNodes(
(node) => String(node.getId()).toLowerCase().includes(query),
);
if (matches.size > 0) {
matches.setSelected(true);
matches.locate({ duration: 400 });
}
});

If your nodes carry a label under data, match on node.getData().label instead of the id.

9. The full script

Everything from steps 2–8, combined. Pair it with the container and search-box HTML and CSS from step 1.

import { Nodus } from '@kortexya/nodus';
// 2. Create and generate.
const nodus = new Nodus({ container: document.getElementById('graph') });
const g = await nodus.generate.barabasiAlbert({ nodes: 40, m0: 2, m: 1 });
await nodus.setGraph(g);
// 3. Base style, layout, frame.
nodus.styles.addNodeRule({ color: '#334155', radius: 10, shape: 'circle' });
nodus.styles.addEdgeRule({ color: '#cbd5e1', width: 1.5 });
await nodus.layouts.force({ duration: 0 });
await nodus.view.locateGraph();
// 4. Hover highlighting.
nodus.styles.setHoveredNodeAttributes({
color: '#0ea5e9',
radius: 14,
outline: { color: '#0ea5e9' },
});
// 5. Rectangle selection.
nodus.tools.rectangleSelect.enable();
nodus.styles.setSelectedNodeAttributes({
color: '#f59e0b',
outline: { color: '#b45309' },
});
nodus.tools.tooltip.onBackgroundClick(() => {
console.log(`${nodus.getSelectedNodes().size} node(s) selected`);
});
// 6. Focus on click.
nodus.tools.tooltip.onNodeClick((node) => node.locate({ duration: 400 }));
// 7. Neighbor expansion on double-click.
nodus.tools.tooltip.onNodeDoubleClick((node) => {
const neighbors = node.getAdjacentNodes();
neighbors.pulse();
});
// 8. Live search.
document.getElementById('search').addEventListener('input', (event) => {
const query = event.target.value.trim().toLowerCase();
if (!query) return;
const matches = nodus.getNodes(
(node) => String(node.getId()).toLowerCase().includes(query),
);
if (matches.size > 0) {
matches.setSelected(true);
matches.locate({ duration: 400 });
}
});

Next steps