Visualization
Customize layouts, interaction modes, and themes for your knowledge graph.
Visualization
InferaGraph renders your graph in 3D using WebGL and Three.js.
Force-Directed 3D
Physics-based layout using Barnes-Hut N-body simulation. Nodes repel via Coulomb force, edges attract via spring force, with damping and centering for stability.
Tree Layout
Static hierarchical layout. Auto-detects root nodes and positions children in
balanced, centered subtrees. Generic parent → child renderer for org charts,
supply chains, taxonomies, file systems, family trees, and anything else with
hierarchy. Recognized edge types are configurable via
parentEdgeTypes (v0.6.4+).
Layout Modes
import { InferaGraph } from '@inferagraph/core/react';
import { useState } from 'react';
function App() {
const [mode, setMode] = useState<'graph' | 'tree'>('graph');
return (
<>
<button onClick={() => setMode('tree')}>Tree</button>
<button onClick={() => setMode('graph')}>Graph</button>
<InferaGraph data={data} layout={mode} />
</>
);
}For click-to-inspect and hover-to-expand interaction, see Drilldown.
Host-driven dispatch (0.10.0+)
Through 0.9.x, hosts could only receive chat events — the AI engine emitted tool_call / text / debug on the stream and the renderer applied them. As of 0.10.0 the dispatch sink is also a public surface: hosts can send events through the same path the engine uses, so a plain button click ("Camera home", "Clear conversation") behaves identically to the equivalent model-emitted tool call.
Visual dispatch is the structural side of the contract. For the textual side, core 0.10.2 ships a library-rendered <ChatText> component so hosts no longer write inline markdown parsers — same principle, different surface: library renders the structure, host supplies styling and link callbacks.
useInferaGraphCommands() — semantic facade
The recommended entry point. Returns a memoized object with eight named methods that dispatch the equivalent ChatEvent internally. Must be called inside an <InferaGraph> subtree.
import { useInferaGraphCommands } from '@inferagraph/core/react';
function Toolbar() {
const cmds = useInferaGraphCommands();
return (
<>
// Highlight a model-returned set of nodes from outside chat()
<button onClick={() => cmds.setHighlight(new Set(['nodeA', 'nodeB']))}>
Highlight pair
</button>
// Pan the camera to a specific node (URL deep link, sidebar click)
<button onClick={() => cmds.focusOn('nodeA')}>Focus</button>
// Snap the camera back to its captured initial orientation
<button onClick={() => cmds.resetView()}>Camera home</button>
// "Clear conversation" — fresh canvas: highlight + annotations +
// filter all reset, camera back to home.
<button onClick={() => cmds.clearVisualState()}>Reset</button>
</>
);
}Command reference
| Method | Dispatched ChatEvent | Effect |
|---|---|---|
| setHighlight(ids) | { type: 'highlight', ids } | Replace the active highlight set. Empty set clears. |
| focusOn(nodeId) | { type: 'focus', nodeId } | Pan the camera to a node. |
| applyFilter(spec) | { type: 'apply_filter', spec, predicate } | Narrow the visible graph. Empty {} clears. |
| setInferredVisibility(visible) | { type: 'set_inferred_visibility', visible } | Toggle the dashed inferred-edge overlay. |
| annotate(nodeId, text) | { type: 'annotate', nodeId, text } | Attach a callout to a node. |
| clearAnnotations() | { type: 'clear_annotations' } | Drop every annotation currently mounted. |
| resetView() | { type: 'reset_view' } | Snap the camera back to its captured initial orientation. |
| clearVisualState() | { type: 'clear_visual_state' } | Comprehensive reset — highlights + annotations + filter + camera. |
useInferaGraphChatContext() — low-level escape hatch
Returns { dispatch }. Reach for this when you need to fire a ChatEvent variant the semantic facade doesn't surface (a synthetic text event for a UI-only welcome bubble, a debug event for ops visibility, etc.). For the common cases prefer useInferaGraphCommands.
import { useInferaGraphChatContext } from '@inferagraph/core/react';
// Lower-level escape hatch — dispatch any ChatEvent variant the semantic
// facade doesn't surface (e.g. a synthetic 'text' event for a UI-only
// "Welcome" bubble, or a 'debug' event for ops visibility).
function SystemBubble() {
const { dispatch } = useInferaGraphChatContext();
return (
<button
onClick={() => dispatch({ type: 'text', delta: 'Welcome back.' })}
>
Greet
</button>
);
}New ChatEvent members
The host-driven dispatch hooks rely on three parameterless visual-reset events added to the ChatEvent union in 0.10.0. They flow through the same dispatch path as the existing tool calls, so a server-emitted clear_visual_state on the wire and a host-clicked "Reset" button produce identical renderer state.
clear_visual_state
Comprehensive "fresh canvas" reset: drops highlights, removes annotations, clears the filter, and snaps the camera home. Use this for "Clear conversation" / "New session" UX.
reset_view
Camera-only home command. Snaps the camera back to its captured initial orientation with the orbit radius preserved (mid-zoom users keep their zoom level).
clear_annotations
Drop every annotation currently mounted via the AnnotationRenderer. Highlights and filter are untouched.
SceneController public methods
Both events compose existing controller surfaces, exposed as 0.10.0 public methods on SceneController for advanced consumers that drive the renderer directly:
resetView()
Camera home. Composes cameraController.resetRotation() with no introduced state.
clearVisualState()
Sweeps highlight + annotations + filter + camera. Pure composition of setHighlight(new Set()), clearAnnotations(), setFilter(undefined), and cameraController.resetRotation().
Theming
Every visual element is controlled via CSS custom properties.
/* Your custom theme */
:root {
--ig-bg-color: #09090b;
--ig-node-color: #3b82f6;
--ig-node-hover-color: #60a5fa;
--ig-node-selected-color: #2563eb;
--ig-edge-color: #27272a;
--ig-label-color: #a1a1aa;
--ig-label-font: 12px "Inter", sans-serif;
--ig-tooltip-bg: #18181b;
--ig-tooltip-color: #fafafa;
--ig-panel-bg: #18181b;
--ig-panel-color: #fafafa;
}InferaGraph ships with a light and dark theme. Import either, or write your own.
Hover drilldown "+" affordance
The hover-drilldown affordance (a small "+" pip rendered next to a node when its drilldown handler is enabled) is built from a single <button class="ig-expand-affordance"> styled entirely with CSS variables. Twelve tokens cover geometry, glyph, surface, and interaction so hosts can reshape it (round vs. chip, light vs. dark, large vs. tight) without forking a renderer file.
/* The hover-drilldown "+" pip exposes 12 custom properties on
.ig-expand-affordance. Override per host or per InferaGraph instance. */
.ig-expand-affordance {
/* Geometry */
--ig-expand-size: 22px; /* width & height of the pip */
--ig-expand-radius: 50%; /* circle by default; 6px for square chip */
--ig-expand-offset-x: 14px; /* px right of the node anchor */
--ig-expand-offset-y: -14px; /* px above the node anchor */
/* Glyph */
--ig-expand-font-size: 14px;
--ig-expand-font-weight: 600;
/* Surface */
--ig-expand-bg: #18181b;
--ig-expand-color: #fafafa;
--ig-expand-border: 1px solid rgba(255, 255, 255, 0.15);
--ig-expand-shadow: 0 1px 4px rgba(0, 0, 0, 0.5);
/* Interaction */
--ig-expand-hover-bg: #27272a;
--ig-expand-active-scale: 0.92; /* scale on :active for press feedback */
}--ig-expand-size
Width & height of the pip (default 20px).
--ig-expand-radius
Border radius (default 50%; e.g. 6px for a chip).
--ig-expand-font-size
Glyph size (default 14px).
--ig-expand-font-weight
Glyph weight (default 600).
--ig-expand-offset-x
Pixels right of the node anchor (default 12px).
--ig-expand-offset-y
Pixels above the node anchor (default -12px).
--ig-expand-bg
Surface fill. Defaults to var(--ig-panel-bg).
--ig-expand-color
Glyph color. Defaults to var(--ig-panel-color).
--ig-expand-border
CSS border shorthand for the pip outline.
--ig-expand-shadow
CSS box-shadow for elevation.
--ig-expand-hover-bg
Fill on :hover; defaults to a tinted blend of bg + color.
--ig-expand-active-scale
Scale on :active for press feedback (default 0.92).
The bundled themes/dark.css already overrides --ig-expand-bg, --ig-expand-color, --ig-expand-border, and --ig-expand-shadow for dark backgrounds — drop in your own scoped overrides on .ig-container .ig-expand-affordance to match a custom theme.
Custom Node Rendering
Go beyond built-in styles with fully custom node visuals. Use a framework-agnostic render function for vanilla JS, Angular, or .NET apps, or pass a React component directly.
In vanilla JS, provide a renderNode function that receives a DOM container, the node data, and the current render state. In React, pass a component via component -- InferaGraph automatically wraps it using createReactNodeRenderFn under the hood.
import { InferaGraph } from '@inferagraph/core/react';
import type { NodeComponentProps } from '@inferagraph/core/data';
function PersonNode({ node, isSelected, isHighlighted }: NodeComponentProps) {
return (
<div style={{
padding: '8px 12px',
background: isSelected ? '#3b82f6' : '#1e293b',
borderRadius: 8,
color: 'white',
border: isHighlighted ? '2px solid #8b5cf6' : 'none',
}}>
{node.attributes.name || node.id}
</div>
);
}
<InferaGraph
data={graphData}
nodeRender={{ component: PersonNode }}
/>API Reference
renderNode
takes priority over component (container: HTMLElement, node: Node, state: NodeRenderState) => void | (() => void)
Framework-agnostic render function. Receives a DOM container to populate, the node data, and the current state. Return an optional cleanup function called before re-render or removal.
component
React.ComponentType<NodeComponentProps>
React component for custom node rendering. Automatically wrapped via createReactNodeRenderFn. Ignored when renderNode is also provided.
hitboxRadius
number (default: 20)
Invisible sphere radius (in pixels) used for mouse and touch interaction detection around the node.
NodeRenderState
{ isSelected: boolean, isHighlighted: boolean }
State object passed to both renderNode and React component renderers, reflecting the current selection and highlight status of the node.
Custom Tooltips
Customize how tooltips appear for nodes and edges. Use a framework-agnostic render function or pass a React component—same pattern as custom node rendering.
In vanilla JS, provide a renderTooltip function that receives a DOM element and tooltip data. In React, pass a component via component -- InferaGraph handles the rest.
import { InferaGraph } from '@inferagraph/core/react';
import type { TooltipComponentProps } from '@inferagraph/core/data';
function BibleTooltip({ type, node, edge }: TooltipComponentProps) {
if (type === 'node' && node) {
return (
<div style={{ padding: 8, background: '#1e293b', borderRadius: 6, color: 'white' }}>
<div style={{ fontWeight: 600 }}>{node.attributes.name || node.id}</div>
{node.attributes.era && <div style={{ fontSize: 12, opacity: 0.7 }}>{node.attributes.era}</div>}
</div>
);
}
if (type === 'edge' && edge) {
return (
<div style={{ padding: 6, background: '#1e293b', borderRadius: 6, color: 'white', fontSize: 12 }}>
{edge.attributes.type}
</div>
);
}
return null;
}
<InferaGraph
data={graphData}
tooltip={{ component: BibleTooltip }}
/>API Reference
renderTooltip
takes priority over component (container: HTMLElement, data: TooltipData) => void | (() => void)
Framework-agnostic render function. Receives a DOM container to populate and structured tooltip data. Return an optional cleanup function called before re-render or removal.
component
React.ComponentType<TooltipComponentProps>
React component for custom tooltips. Ignored when renderTooltip is also provided.
TooltipData
{ type: 'node' | 'edge', node?: NodeData, edge?: EdgeData }
Data object passed to both renderTooltip and React component renderers, describing which element triggered the tooltip and its associated data.
showNode(node, x, y) / showEdge(edge, x, y)
TooltipOverlay instance methods
Programmatically show a tooltip for a specific node or edge at the given screen coordinates. Useful for triggering tooltips from external UI or keyboard navigation.
Diagnostics
Core 0.8.0 makes the RAG pipeline's internal state observable. Two surfaces: ChatEvent.debug (a first-class member of the chat stream) and the onDiagnostic / onToolCallOutcome callback props on <InferaGraph>. Hosts wire these into chat-bubble badges so users see why an answer looks the way it does — warming up, no graph match, fallback substitution fired, ids the renderer couldn't resolve, etc.
ChatEvent.debug
// ChatEvent gains a 'debug' member in core 0.8.0 — first-class diagnostic surface.
type ChatDebugEvent = {
type: 'debug';
phase: DebugPhase;
detail?: unknown;
counters?: Record<string, number>;
conversationId?: string;
};
// Canonical phases the engine emits today:
// stream-opened — route handler ran; SSE channel is live
// warmup-blocking — first-call await on embedding warmup
// warmup-failed — background warmup hit an error; buffered failure is
// emitted on the next chat() call's debug stream (0.9.0+)
// vector-search — hybrid retrieval ran; counters: hits, durationMs
// rerank — cross-encoder rerank ran; counters: candidates, kept
// pronoun-resolve — prior-turn entities injected into the prompt
// retrieval-complete — final retrieval set chosen for the prompt
// retrieval-empty — no relevant nodes; system prompt forces "no data"
// substitution-fired — empty highlight substituted with retrieval set
// engine-empty — no events from provider; surface as a loud failure
// conversation-cleared — host called clear(); next turn is freshonDiagnostic + onToolCallOutcome
Two optional props on <InferaGraph>. onDiagnostic(ev) fires for every debug event the engine emits. onToolCallOutcome(outcome) fires after each tool-call dispatch with {applied, unknown, appliedIds, unknownIds} so the host can surface "N highlighted, M unknown" badges when the model dispatches against ids the renderer can't resolve.
The appliedIds / unknownIds arrays are populated by SceneController.setHighlight(ids) and SceneController.focusOn(nodeId), both of which return {appliedIds, unknownIds} as of core 0.9.3 (previously void). The chat hook flows the result through onToolCallOutcome so hosts get partition information per tool call without having to wrap the controller themselves.
import { InferaGraph } from '@inferagraph/core/react';
import type { ChatDebugEvent, ToolCallOutcome } from '@inferagraph/core/data';
import { useState } from 'react';
function App() {
const [badges, setBadges] = useState<string[]>([]);
return (
<>
<InferaGraph
data={data}
llm={llm}
onDiagnostic={(ev: ChatDebugEvent) => {
// Render every diagnostic as a small grey chip beneath the chat bubble.
if (ev.phase === 'retrieval-empty') {
setBadges(b => [...b, 'no graph match']);
} else if (ev.phase === 'substitution-fired') {
setBadges(b => [...b, `substituted ${ev.counters?.ids ?? 0} ids`]);
}
}}
onToolCallOutcome={(o: ToolCallOutcome) => {
// Renderer dispatches against partly-unknown ids — surface that to the host.
if (o.unknown > 0) {
setBadges(b => [...b, `${o.applied} highlighted, ${o.unknown} unknown`]);
}
}}
/>
<DiagnosticsPanel badges={badges} />
</>
);
}Tip. Reuse the accent-amber token for diagnostic chips that warn (no match, substitution fired) and a neutral grey for informational ones (warming up, retrieval complete). Keep diagnostic UI collapsible — most users want the answer; ops users want the phase log on demand.
Inferred Edges
InferaGraph can compute "soft" relationships the AI thinks exist but the data doesn't state directly, and overlay them as dashed lines. The merger uses Reciprocal Rank Fusion (RRF) over keyword + semantic signals so the overlay surfaces high-confidence pairs first.
- Hidden by default — set showInferredEdges=true to opt in.
- The chat tool call set_inferred_visibility toggles the overlay from a user prompt.
- Edges are recomputed on demand; no extra latency on initial render.