JsonGraphsv1.0.2 — Zero Dependencies

JsonGraphs
Interactive JSON
Graph Visualizer

A high-performance, dependency-freelibrary for visualizing complex JSON as beautiful, interactive canvas-based graphs. Streaming parser, tree & radial layouts, built-in minimap, search, and toolbar.

Get Started
$npm install jsongraphs

InstallationGetting Started

terminal
bash
npm install jsongraphs
# or
yarn add jsongraphs
# or
bun add jsongraphs
📝 Note
JsonGraphs is ESM-only. Make sure your bundler supports ESM modules (Vite, Next.js, Rollup, esbuild all work out of the box).

Quick StartGetting Started

The simplest possible integration — create a JsonGraph instance, point it at a container element, and call load().

main.ts
typescript
import { JsonGraph, darkTheme } from class="token-string">'jsongraphs';

const container = document.getElementById(class="token-string">'graph-container')!;

const graph = new JsonGraph({
  container,
  theme: darkTheme,   class="token-comment">// or defaultTheme for light
  layout: class="token-string">'tree',     class="token-comment">// or class="token-string">'radial'
  minimap: true,
  toolbar: true,
  search: true,
});

class="token-comment">// Load from a plain JS object
await graph.load({
  project: class="token-string">"My App",
  version: class="token-string">"class="token-number">2.0",
  dependencies: {
    react: class="token-string">"^class="token-number">19",
    typescript: class="token-string">"^class="token-number">5",
  },
});

React / Next.js IntegrationGetting Started

Use a useRef for the container and initialize inside useEffect. Always call destroy() in the cleanup function.

GraphViewer.tsx
typescript
class="token-string">"use client";
import { useEffect, useRef } from class="token-string">"react";
import { JsonGraph, darkTheme, defaultTheme } from class="token-string">"jsongraphs";

interface Props {
  data: object;
  dark?: boolean;
}

export function GraphViewer({ data, dark = true }: Props) {
  const containerRef = useRef(null);

  useEffect(() => {
    if (!containerRef.current) return;

    const graph = new JsonGraph({
      container: containerRef.current,
      theme: dark ? darkTheme : defaultTheme,
      layout: class="token-string">"tree",
      minimap: true,
      toolbar: true,
      search: true,
      onLoad: (nodes, edges) => {
        console.log(class="token-string">`Loaded ${nodes} nodes, ${edges} edges`);
      },
      onError: (err) => {
        console.error(class="token-string">"Parse error:", err);
      },
    });

    graph.load(data);

    class="token-comment">// Cleanup on unmount
    return () => graph.destroy();
  }, [data, dark]);

  return (
    
class="token-string">"class="token-number">100%", height: class="token-string">"600px" }} /> ); }
💡 Tip
Add dynamic import with ssr: false in Next.js to avoid window-not-defined errors during SSR, or prefix the component with "use client" in the App Router.

Live Demo

Key Features

🚀

Canvas-Based Rendering

60 FPS rendering with optimized spatial indexing. Handles hundreds of thousands of nodes without breaking a sweat.

📡

Streaming Parser

Load multi-megabyte JSON files without blocking the UI thread. Feed fetch() streams directly as they download.

📐

Multiple Layouts

Switch between hierarchical Tree and organic Radial layouts dynamically at runtime with a single method call.

🎨

Themeable

Built-in light and dark themes. Customize every color, size, and geometry via the structured Theme interface.

🧩

Zero Dependencies

Pure TypeScript, no external runtime deps. Ultra-lightweight bundle size, easy to integrate anywhere.

🛠️

Built-in UI

Integrated MiniMap, Toolbar (zoom, fit, layout toggle), and Search overlay — all without any extra configuration.

Node Type Colours

Each JSON primitive type is rendered with a distinct color palette that works in both light and dark themes:

Light theme

object
array
string
number
boolean
null

Dark theme

object
array
string
number
boolean
null

JsonGraphAPI Reference

The main class. Initializes the canvas renderer, overlay UI components (toolbar, search bar, minimap), and event model inside the provided container element.

typescript
import { JsonGraph } from class="token-string">'jsongraphs';

const graph = new JsonGraph(opts: JsonGraphOptions);

Constructor Options

OptionTypeDefaultDescription
container*HTMLElementThe host element for the graph canvas and overlay UI.
themeThemedefaultThemeInitial visual theme. Import defaultTheme or darkTheme, or provide a custom Theme object.
layout'tree' | 'radial''tree'Initial layout algorithm. Can be changed at runtime via setLayout().
minimapbooleantrueShow the interactive mini-map overlay in the bottom-right corner.
toolbarbooleantrueShow the toolbar overlay with zoom, fit, layout toggle, expand/collapse, and theme toggle buttons.
searchbooleantrueShow the node search bar overlay at the top-center of the graph.
maxDepthnumber200Maximum JSON nesting depth before a ParseLimitError is thrown.
maxNodesnumber500_000Maximum total graph nodes before a ParseLimitError is thrown.
onLoad(nodes: number, edges: number) => voidCallback invoked after parsing completes. Receives total node and edge counts.
onError(err: Error) => voidCallback invoked if parsing fails (e.g. invalid JSON or limit exceeded).
📝 Note
The container element gets position: relative and overflow: hidden applied automatically. Set an explicit width and height on it before constructing JsonGraph.

load(source: JsonSource)

Asynchronously parses and renders a JSON source. Accepts five input formats:

object

Plain JS object

Any serialisable JS value. Parsed synchronously.

string

JSON string

A raw JSON string. Parsed synchronously.

File

File object

From an <input type='file'> element. Streamed.

URL

URL object

Fetched via fetch() and streamed.

ReadableStream

ReadableStream

Streamed byte-by-byte. Ideal for huge files.

typescript
class="token-comment">// From a plain object
await graph.load({ name: class="token-string">"Alice", age: class="token-number">30 });

class="token-comment">// From a JSON string
await graph.load(class="token-string">'{"hello": "world"}');

class="token-comment">// From a file input
const file = fileInput.files[class="token-number">0];
await graph.load(file);

class="token-comment">// From a URL (auto-fetches and streams)
await graph.load(new URL(class="token-string">'https:class="token-comment">//api.example.com/data.json'));

class="token-comment">// From a fetch() stream (most memory-efficient for large files)
const res = await fetch(class="token-string">'/huge-data.json');
await graph.load(res.body!);

setLayout(layout: LayoutType)

Switches the layout algorithm at runtime and re-runs the layout engine immediately. The toolbar also calls this method when the user clicks the layout toggle buttons.

typescript
class="token-comment">// Switch to radial layout
graph.setLayout(class="token-string">'radial');

class="token-comment">// Switch back to tree layout
graph.setLayout(class="token-string">'tree');
💡 Tip
The tree layout is best for deep, hierarchical JSON with clear parent-child relationships. The radial layout works great for shallow, wide structures and looks beautiful for network-like data.

setTheme(theme: Theme)

Applies a new Theme object to the canvas renderer, toolbar, search bar, and minimap simultaneously.

typescript
import { darkTheme, defaultTheme } from class="token-string">'jsongraphs';

class="token-comment">// Apply dark theme
graph.setTheme(darkTheme);

class="token-comment">// Apply light theme
graph.setTheme(defaultTheme);

class="token-comment">// Apply a custom theme
graph.setTheme(myCustomTheme);

toggleTheme()

Convenience method that toggles between defaultTheme (light) and darkTheme. Equivalent to checking the current theme name and calling setTheme().

typescript
class="token-comment">// One-liner to toggle light ↔ dark
graph.toggleTheme();

fitView()

Resets pan and zoom so all visible nodes fit within the container with an 80 px padding. Also marks the minimap dirty.

typescript
graph.fitView();

zoomIn() / zoomOut()

Smooth programmatic zoom centered on the container midpoint.zoomIn()scales by ×1.15 andzoomOut()scales by ×0.87.

typescript
graph.zoomIn();   class="token-comment">// scale × class="token-number">1.15
graph.zoomOut();  class="token-comment">// scale × class="token-number">0.87

expandAll() / collapseAll()

expandAll() expands everyobject andarray node.collapseAll() collapses all except the root node. Both re-run the layout engine after toggling visibility.

typescript
graph.expandAll();    class="token-comment">// Show all nodes
graph.collapseAll();  class="token-comment">// Collapse all (keep root visible)

destroy()

Cancels any pending animation frame, destroys the canvas renderer, and unmounts the search bar, minimap, and toolbar overlays. Always call this when removing the graph from the DOM to avoid memory leaks.

typescript
class="token-comment">// In a React useEffect cleanup
return () => graph.destroy();

class="token-comment">// Or manually when done
graph.destroy();
⚠️ Warning
Failing to call destroy() will leave dangling event listeners and canvas elements in memory.

TypesTypes

JsonSource

A union type representing every valid data source for graph.load().

typescript
type JsonSource =
  | File                       class="token-comment">// Browser File API (e.g. from type=class="token-string">"file">)
  | ReadableStream  class="token-comment">// WHATWG Streams (e.g. fetch().body)
  | string                     class="token-comment">// Raw JSON string
  | URL                        class="token-comment">// URL — fetched and streamed automatically
  | object;                    class="token-comment">// Any plain JS object / array

LayoutType

typescript
type LayoutType = class="token-string">'tree' | class="token-string">'radial';
'tree'

Tree Layout

Hierarchical top-down layout. Best for deeply nested, structured JSON.

'radial'

Radial Layout

Organic radial layout emanating from the root. Great for wide, shallow structures.

NodeType

Represents each JSON primitive and container type.

typescript
type NodeType = class="token-string">'object' | class="token-string">'array' | class="token-string">'string' | class="token-string">'number' | class="token-string">'boolean' | class="token-string">'null';

GraphNode

Represents a single node in the graph. Each JSON value (object, array, or primitive) produces one GraphNode.

typescript
interface NodePosition {
  x: number;
  y: number;
  vx: number;  class="token-comment">// velocity used by force layout
  vy: number;
}

interface GraphNode {
  id: string;
  type: NodeType;
  key?: string;
  value?: string;
  childCount: number;
  depth: number;
  parentId?: string;
  expanded: boolean;
  pos: NodePosition;
  width: number;
  height: number;
  highlighted: boolean;
  hovered: boolean;
  alpha: number;
  transparent?: boolean;
}

Fields

OptionTypeDefaultDescription
idstringUnique node identifier.
typeNodeTypeJSON value type: object | array | string | number | boolean | null.
keystring?The JSON key that produced this node. Undefined for the root node.
valuestring?Serialised display value for primitives (string, number, boolean, null).
childCountnumberNumber of direct children (object keys or array items).
depthnumberZero-indexed depth from the root.
parentIdstring?ID of the parent node. Undefined for the root.
expandedbooleanWhether this node is currently expanded in the UI.
posNodePositionLayout position { x, y, vx, vy }. Mutated by the layout engine.
widthnumberBounding box width set after layout measurement.
heightnumberBounding box height set after layout measurement.
highlightedbooleanTrue when this node matches a search query.
hoveredbooleanTrue when the pointer is over this node.
alphanumberAnimation progress 0→1 used for fade-in.
transparentboolean?Anonymous array-item wrapper — hidden from view, edges skipped through.

GraphEdge

Represents a directed edge between two nodes. Edges carry the JSON key label and an optional caption pill.

typescript
interface GraphEdge {
  id: string;
  sourceId: string;
  targetId: string;
  label?: string;    class="token-comment">// JSON key that connects these nodes
  caption?: string;  class="token-comment">// Optional pill label at edge midpoint
  alpha: number;     class="token-comment">// Fade-in animation progress
}
OptionTypeDefaultDescription
idstringUnique edge identifier.
sourceIdstringID of the source (parent) node.
targetIdstringID of the target (child) node.
labelstring?The JSON key label on this edge (same as target.key).
captionstring?Optional text displayed in a pill box at the edge midpoint. Set automatically when the target key is __CAPTION__.
alphanumberAnimation progress 0→1 for fade-in.

ParseOptions

Low-level parser configuration passed via maxDepth and maxNodes on JsonGraphOptions.

typescript
interface ParseOptions {
  maxDepth?: number;  class="token-comment">// Max nesting depth. Default: class="token-number">200
  maxNodes?: number;  class="token-comment">// Max total nodes. Default: class="token-number">500_000
  chunkSize?: number; class="token-comment">// Text decoder chunk in bytes. Default: class="token-number">65_536 (class="token-number">64 KB)
}

ParseLimitError

Error class thrown (and forwarded to onError) when the parser exceeds maxDepth or maxNodes.

typescript
import { ParseLimitError } from class="token-string">'jsongraphs';

class ParseLimitError extends Error {
  readonly kind: class="token-string">'depth' | class="token-string">'nodes';
}

class="token-comment">// Usage
const graph = new JsonGraph({
  container,
  maxNodes: class="token-number">10_000,
  onError: (err) => {
    if (err instanceof ParseLimitError) {
      if (err.kind === class="token-string">'nodes') {
        console.warn(class="token-string">'Too many nodes! Increase maxNodes.');
      } else if (err.kind === class="token-string">'depth') {
        console.warn(class="token-string">'JSON too deeply nested! Increase maxDepth.');
      }
    }
  },
});

Theme

The full theme interface. Every visual property of the graph is configurable through this object.

typescript
interface NodeColors {
  fill:   string;  class="token-comment">// Node background
  stroke: string;  class="token-comment">// Node border
  text:   string;  class="token-comment">// Node label text
  badge:  string;  class="token-comment">// Type badge colour
}

interface Theme {
  name:       string;
  background: string;  class="token-comment">// Canvas background colour
  gridLine:   string;  class="token-comment">// Subtle grid line colour

  nodeColors: Record;  class="token-comment">// Per-type colours

  edge: {
    stroke:        string;  class="token-comment">// Default edge colour
    highlight:     string;  class="token-comment">// Highlighted edge colour
    width:         number;  class="token-comment">// Edge line width in px
    captionBg:     string;  class="token-comment">// Caption pill background
    captionText:   string;  class="token-comment">// Caption pill text
    captionBorder: string;  class="token-comment">// Caption pill border
  };

  font: {
    family:    string;  class="token-comment">// CSS font-family
    keySize:   number;  class="token-comment">// Key label font size
    valueSize: number;  class="token-comment">// Value label font size
    typeSize:  number;  class="token-comment">// Type badge font size
  };

  node: {
    radius:       number;  class="token-comment">// Corner radius
    paddingX:     number;  class="token-comment">// Horizontal padding
    paddingY:     number;  class="token-comment">// Vertical padding
    minWidth:     number;  class="token-comment">// Minimum node width
    shadowBlur:   number;  class="token-comment">// Drop shadow blur radius
    shadowColor:  string;  class="token-comment">// Drop shadow colour
    shadowOffset: number;  class="token-comment">// Drop shadow offset
  };

  ui: {
    panelBg:          string;  class="token-comment">// Toolbar / search / minimap background
    panelBorder:      string;  class="token-comment">// Panel border colour
    panelShadow:      string;  class="token-comment">// Panel box-shadow CSS value
    btnHover:         string;  class="token-comment">// Button hover background
    btnActive:        string;  class="token-comment">// Button active / selected background
    text:             string;  class="token-comment">// Primary UI text colour
    textMuted:        string;  class="token-comment">// Secondary UI text colour
    inputBg:          string;  class="token-comment">// Search input background
    inputText:        string;  class="token-comment">// Search input text colour
    inputPlaceholder: string;  class="token-comment">// Search placeholder colour
    focusRing:        string;  class="token-comment">// Focus ring colour
    separator:        string;  class="token-comment">// Separator line colour
    minimapViewport:  string;  class="token-comment">// Minimap viewport rect stroke
  };
}

ThemesThemes

JsonGraphs ships with two polished built-in themes. Import and use them directly, or compose a custom theme.

Light Theme (defaultTheme)

Dark Theme (darkTheme)

typescript
import { defaultTheme, darkTheme } from class="token-string">'jsongraphs';

class="token-comment">// Apply light theme
graph.setTheme(defaultTheme);

class="token-comment">// Apply dark theme
graph.setTheme(darkTheme);

class="token-comment">// Toggle between them
graph.toggleTheme();

Custom Theme

Build a full custom theme by satisfying the Theme interface. The easiest approach is to spread a built-in theme and override only the properties you want to change.

typescript
import { defaultTheme, type Theme } from class="token-string">'jsongraphs';

const sunsetTheme: Theme = {
  ...defaultTheme,
  name: class="token-string">'sunset',
  background: class="token-string">'#1a0a00',
  gridLine: class="token-string">'rgba(class="token-number">255,class="token-number">120,class="token-number">50,class="token-number">0.05)',

  nodeColors: {
    object:  { fill: class="token-string">'#2a1000', stroke: class="token-string">'#ff6820', text: class="token-string">'#ffd0a0', badge: class="token-string">'#ff6820' },
    array:   { fill: class="token-string">'#001a10', stroke: class="token-string">'#20d080', text: class="token-string">'#a0ffc8', badge: class="token-string">'#20d080' },
    string:  { fill: class="token-string">'#00101a', stroke: class="token-string">'#20a0ff', text: class="token-string">'#a0d8ff', badge: class="token-string">'#20a0ff' },
    number:  { fill: class="token-string">'#1a1000', stroke: class="token-string">'#c0b020', text: class="token-string">'#ffe890', badge: class="token-string">'#c0b020' },
    boolean: { fill: class="token-string">'#1a0020', stroke: class="token-string">'#c040c0', text: class="token-string">'#f0a0f0', badge: class="token-string">'#c040c0' },
    null:    { fill: class="token-string">'#class="token-number">151510', stroke: class="token-string">'#class="token-number">808070', text: class="token-string">'#c8c8b0', badge: class="token-string">'#class="token-number">808070' },
  },

  edge: {
    ...defaultTheme.edge,
    stroke: class="token-string">'rgba(class="token-number">255,class="token-number">120,class="token-number">50,class="token-number">0.4)',
    highlight: class="token-string">'#ff6820',
  },

  ui: {
    ...defaultTheme.ui,
    panelBg: class="token-string">'rgba(class="token-number">26,class="token-number">10,class="token-number">0,class="token-number">0.92)',
    text: class="token-string">'#ffd0a0',
    focusRing: class="token-string">'#ff6820',
  },
};

graph.setTheme(sunsetTheme);
💡 Tip
Use the spread operator (...defaultTheme) to inherit all defaults, then override only what you need. This is much safer than building a theme from scratch.

Advanced UsageAdvanced

__CAPTION__ Key — Custom Edge Labels

Add a special __CAPTION__ key inside any JSON object to place a descriptive label in a pill box at the midpoint of the incoming edge. The node itself is hidden — only the caption is shown on the edge.

example.json
json
{
  class="token-string">"user": {
    class="token-string">"__CAPTION__": class="token-string">"is assigned to",
    class="token-string">"name": class="token-string">"Alice",
    class="token-string">"role": class="token-string">"admin"
  },
  class="token-string">"project": {
    class="token-string">"__CAPTION__": class="token-string">"belongs to",
    class="token-string">"title": class="token-string">"JsonGraphs Docs",
    class="token-string">"status": class="token-string">"active"
  }
}
📝 Note
The __CAPTION__ node is automatically detected during parsing. Its value becomes the caption field on the parent edge, and the node itself is marked transparent.

Streaming Large Files

JsonGraphs uses a chunked streaming parser internally. You can feed it a ReadableStream<Uint8Array> directly, which means it starts rendering as it downloads — perfect for very large JSON files.

typescript
class="token-comment">// Stream directly from fetch — renders as data arrives
const response = await fetch(class="token-string">'/api/huge-dataset.json');
await graph.load(response.body!);

class="token-comment">// From a File input (also streamed)
document.querySelector(class="token-string">'input[type="file"]')
  .addEventListener(class="token-string">'change', async (e) => {
    const file = e.target.files[class="token-number">0];
    await graph.load(file);  class="token-comment">// Streams the file, no full read into memory
  });

class="token-comment">// With limit guards
const graph = new JsonGraph({
  container,
  maxNodes: class="token-number">50_000,   class="token-comment">// Stop at 50k nodes
  maxDepth: class="token-number">100,      class="token-comment">// Stop at depth class="token-number">100
  onError: (err) => {
    console.error(class="token-string">'Limit reached:', err.message);
  },
});
💡 Tip
For very large JSON files (10MB+), always use the streaming approach via fetch().body or a File object. Passing a pre-parsed string works too, but keeps the entire string in memory.