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 fresh

onDiagnostic + 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.