Advanced
Serialization, temporal navigation, semantic clustering, minimap, multi-select, keyboard navigation, and export.
Serialization
Save and restore graphs with versioned JSON serialization. The schema includes version, nodes, edges, and metadata.
import { useState } from 'react';
import { InferaGraph, exportGraph, importGraph } from '@inferagraph/core';
import type { GraphData } from '@inferagraph/core';
const initialData: GraphData = {
nodes: [
{ id: 'abraham', attributes: { name: 'Abraham' } },
{ id: 'sarah', attributes: { name: 'Sarah' } },
],
edges: [
{ id: 'e1', sourceId: 'abraham', targetId: 'sarah',
attributes: { type: 'husband_of' } },
],
};
function GraphWithSerialization() {
const [data, setData] = useState(initialData);
const handleSave = () => {
localStorage.setItem('biblegraph', JSON.stringify(data));
};
const handleLoad = () => {
const saved = localStorage.getItem('biblegraph');
if (saved) setData(JSON.parse(saved));
};
return (
<div>
<button onClick={handleSave}>Save</button>
<button onClick={handleLoad}>Load</button>
<InferaGraph data={data} />
</div>
);
}Temporal Navigation
Define your own eras and navigate through them. The TimelineEngine is fully configurable -- the hosting application provides era definitions, and the engine handles filtering by era or arbitrary time range, plus computing transitions showing which nodes appear, disappear, or persist.
import { useState } from 'react';
import { InferaGraph, TimelineEngine } from '@inferagraph/core';
import type { GraphData, Era } from '@inferagraph/core';
const eras: Era[] = [
{ name: 'Patriarchs', startYear: -2000, endYear: -1500 },
{ name: 'Exodus', startYear: -1500, endYear: -1200 },
{ name: 'United Kingdom', startYear: -1050, endYear: -930 },
];
const data: GraphData = {
nodes: [
{ id: 'abraham', attributes: { name: 'Abraham', era: 'Patriarchs' } },
{ id: 'moses', attributes: { name: 'Moses', era: 'Exodus' } },
{ id: 'david', attributes: { name: 'David', era: 'United Kingdom' } },
],
edges: [],
};
function TimelineGraph() {
const [range, setRange] = useState({ start: -2000, end: -930 });
return (
<div>
<select onChange={(e) => {
const era = eras.find(er => er.name === e.target.value);
if (era) setRange({ start: era.startYear, end: era.endYear });
}}>
{eras.map(e => <option key={e.name}>{e.name}</option>)}
</select>
<InferaGraph data={data} timeRange={range} eras={eras} />
</div>
);
}Semantic Clustering
Automatically detect communities in your graph using Louvain-inspired modularity optimization. Collapse clusters to simplify the view, expand to see details, and compute convex hull boundaries.
import { useState, useCallback } from 'react';
import { InferaGraph, ClusterEngine } from '@inferagraph/core';
import type { GraphData, Cluster } from '@inferagraph/core';
const data: GraphData = {
nodes: [
{ id: 'abraham', attributes: { name: 'Abraham' } },
{ id: 'sarah', attributes: { name: 'Sarah' } },
{ id: 'isaac', attributes: { name: 'Isaac' } },
{ id: 'jacob', attributes: { name: 'Jacob' } },
],
edges: [
{ id: 'e1', sourceId: 'abraham', targetId: 'isaac', attributes: { type: 'father_of' } },
{ id: 'e2', sourceId: 'isaac', targetId: 'jacob', attributes: { type: 'father_of' } },
],
};
function ClusteredGraph() {
const [clusters, setClusters] = useState<Cluster[]>([]);
const handleReady = useCallback((engine: ClusterEngine) => {
const detected = engine.detectCommunities();
setClusters(detected);
}, []);
return (
<div>
{clusters.map(c => (
<button key={c.id} onClick={() => c.toggle()}>
{c.id} ({c.nodeIds.size} nodes)
</button>
))}
<InferaGraph data={data} onClusterReady={handleReady} />
</div>
);
}Minimap
A canvas-based minimap overlay that shows all node positions and the current viewport. Click anywhere on the minimap to navigate the main view.
import { useEffect, useRef } from 'react';
import { InferaGraph, Minimap } from '@inferagraph/core';
import type { GraphData } from '@inferagraph/core';
const data: GraphData = {
nodes: [
{ id: 'jerusalem', attributes: { name: 'Jerusalem', type: 'place' } },
{ id: 'bethlehem', attributes: { name: 'Bethlehem', type: 'place' } },
{ id: 'david', attributes: { name: 'David', type: 'person' } },
],
edges: [
{ id: 'e1', sourceId: 'david', targetId: 'bethlehem', attributes: { type: 'born_in' } },
{ id: 'e2', sourceId: 'david', targetId: 'jerusalem', attributes: { type: 'traveled_to' } },
],
};
function GraphWithMinimap() {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) return;
const minimap = new Minimap(containerRef.current, {
width: 200, height: 150,
nodeColor: '#4a9eff',
});
return () => minimap.destroy();
}, []);
return (
<div ref={containerRef}>
<InferaGraph data={data} />
</div>
);
}Multi-Select
Select multiple nodes with click modes (replace, add, toggle), drag-box selection, and perform batch operations like delete, extract subgraph, and select all.
import { useState, useCallback } from 'react';
import { InferaGraph } from '@inferagraph/core';
import type { GraphData } from '@inferagraph/core';
const data: GraphData = {
nodes: [
{ id: 'abraham', attributes: { name: 'Abraham' } },
{ id: 'sarah', attributes: { name: 'Sarah' } },
{ id: 'isaac', attributes: { name: 'Isaac' } },
{ id: 'jacob', attributes: { name: 'Jacob' } },
{ id: 'joseph', attributes: { name: 'Joseph' } },
],
edges: [
{ id: 'e1', sourceId: 'abraham', targetId: 'isaac', attributes: { type: 'father_of' } },
{ id: 'e2', sourceId: 'isaac', targetId: 'jacob', attributes: { type: 'father_of' } },
{ id: 'e3', sourceId: 'jacob', targetId: 'joseph', attributes: { type: 'father_of' } },
],
};
function MultiSelectGraph() {
const [selected, setSelected] = useState<Set<string>>(new Set());
const handleSelectionChange = useCallback((ids: Set<string>) => {
setSelected(ids);
}, []);
return (
<div>
<p>Selected: {[...selected].join(', ')}</p>
<button onClick={() => setSelected(new Set())}>Clear</button>
<InferaGraph data={data} selectedIds={selected}
onSelectionChange={handleSelectionChange} />
</div>
);
}Keyboard Navigation
Full keyboard support with Tab cycling, arrow key graph traversal, Enter/Space selection, and configurable custom shortcuts. ARIA attributes are set automatically for accessibility.
import { useEffect, useRef } from 'react';
import { InferaGraph, KeyboardManager } from '@inferagraph/core';
import type { GraphData } from '@inferagraph/core';
const data: GraphData = {
nodes: [
{ id: 'abraham', attributes: { name: 'Abraham' } },
{ id: 'sarah', attributes: { name: 'Sarah' } },
{ id: 'isaac', attributes: { name: 'Isaac' } },
{ id: 'jacob', attributes: { name: 'Jacob' } },
],
edges: [],
};
function KeyboardGraph() {
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) return;
const keyboard = new KeyboardManager({
getAllNodeIds: () => ['abraham', 'sarah', 'isaac', 'jacob'],
onFocusChange: (id) => console.log('Focused:', id),
});
keyboard.attach(containerRef.current);
keyboard.addBinding({ key: 'Delete', action: 'delete' });
return () => keyboard.detach();
}, []);
return (
<div ref={containerRef} tabIndex={0}>
<InferaGraph data={data} />
</div>
);
}Default Key Bindings
Tab
Next node
Shift+Tab
Previous node
Arrow Keys
Traverse edges
Escape
Deselect all
Export
Export your graph as PNG (from WebGL canvas), SVG (generated from positions), or JSON (using serialization). All export methods support selection-aware filtering and configurable options.
import { useCallback, useRef } from 'react';
import { InferaGraph, ExportEngine } from '@inferagraph/core';
import type { GraphData, RendererRef } from '@inferagraph/core';
const data: GraphData = {
nodes: [
{ id: 'abraham', attributes: { name: 'Abraham' } },
{ id: 'sarah', attributes: { name: 'Sarah' } },
{ id: 'isaac', attributes: { name: 'Isaac' } },
],
edges: [
{ id: 'e1', sourceId: 'abraham', targetId: 'sarah', attributes: { type: 'husband_of' } },
{ id: 'e2', sourceId: 'abraham', targetId: 'isaac', attributes: { type: 'father_of' } },
],
};
function ExportableGraph() {
const rendererRef = useRef<RendererRef>(null);
const handleExportJSON = useCallback(() => {
const exporter = new ExportEngine(rendererRef.current!);
const json = exporter.toJSON();
exporter.download(json, 'patriarchs.json', 'application/json');
}, []);
const handleExportPNG = useCallback(() => {
const exporter = new ExportEngine(rendererRef.current!);
const png = exporter.toPNG({ scale: 2 });
exporter.download(png, 'patriarchs.png', 'image/png');
}, []);
return (
<div>
<button onClick={handleExportJSON}>Export JSON</button>
<button onClick={handleExportPNG}>Export PNG</button>
<InferaGraph ref={rendererRef} data={data} />
</div>
);
}Ready to dive deeper?
Explore the source code, file issues, or contribute on GitHub.