jsongraphs·v1.1.1

Visualize JSONas InteractiveGraph Structures

A dependency-free canvas library with a streaming parser, tree & radial layouts, and built-in search, minimap, and toolbar.

Get Started
60 FPSCanvas rendering
500k+Nodes supported
0Dependencies
$npm install jsongraphs
Live Preview — JsonGraphsinteractive

↑ Fully interactive — zoom, pan, search, collapse nodes, and toggle layouts

InstallationGetting Started

JsonGraphs ships as a single zero-dependency ESM/CJS package. Install it with any package manager:

terminal
bash
1npm install jsongraphs
2# or
3yarn add jsongraphs
4# or
5bun add jsongraphs
📝Note
JsonGraphs is ESM-first. Vite, Next.js, Rollup, and esbuild all work out of the box. For CDN/script-tag usage, import from dist/jsongraphs.umd.js.

Quick StartGetting Started

Create a JsonGraph instance, point it at a container element, and call load(). The graph renders immediately — no configuration required beyond a container.

main.ts
typescript
1import { JsonGraph, darkTheme } from 'jsongraphs';
2
3const container = document.getElementById('graph-container')!;
4
5const graph = new JsonGraph({
6 container,
7 theme: darkTheme, // or defaultTheme for light mode
8 layout: 'tree', // or 'radial'
9 minimap: true, // bottom-left minimap
10 toolbar: true, // zoom / layout / theme buttons
11 search: true, // Ctrl+F search overlay
12});
13
14// Load from a plain JS object — synchronous, instant
15await graph.load({
16 project: "My App",
17 version: "2.0",
18 dependencies: {
19 react: "^19",
20 typescript: "^5",
21 },
22});

Live result ↓

Quick Start Demo — JsonGraphsinteractive

Zoom · Pan · Double-click to expand/collapse · Search with Ctrl+K

React / Next.jsGetting Started

Use a useRef for the container and initialize inside useEffect. Always call destroy() in the cleanup to prevent memory leaks.

GraphViewer.tsx
typescript
1"use client";
2import { useEffect, useRef } from "react";
3import { JsonGraph, darkTheme, defaultTheme } from "jsongraphs";
4
5interface Props {
6 data: object;
7 dark?: boolean;
8}
9
10export function GraphViewer({ data, dark = true }: Props) {
11 const containerRef = useRef<HTMLDivElement>(null);
12
13 useEffect(() => {
14 if (!containerRef.current) return;
15
16 const graph = new JsonGraph({
17 container: containerRef.current,
18 theme: dark ? darkTheme : defaultTheme,
19 layout: "tree",
20 minimap: true,
21 toolbar: true,
22 search: true,
23 onLoad: (nodes, edges) => {
24 console.log(`Loaded ${nodes} nodes, ${edges} edges`);
25 },
26 onError: (err) => console.error("Parse error:", err),
27 });
28
29 graph.load(data);
30
31 // Always destroy on unmount — cleans up canvas, events, timers
32 return () => graph.destroy();
33 }, [data, dark]);
34
35 return <div ref={containerRef} style={{ width: "100%", height: "600px" }} />;
36}
💡Tip
In Next.js App Router, add "use client" to the component file. For Pages Router, use dynamic(() => import(…), { ssr: false })to skip server-side rendering of the canvas.

Key Features

🌳

Tree Layout

Renders JSON as a clean left-to-right hierarchy. Parent nodes always appear to the left of their children with even vertical spacing. Every subtree occupies its own band — zero node overlaps, no matter how deeply nested the data is.

Tree Layout — JsonGraphsinteractive

🌀

Radial Layout

Arranges nodes in an organic, outward-growing spiral pattern — inspired by how seeds pack in a sunflower or a nautilus shell. The result is a compact, visually balanced graph where no two nodes ever overlap, even with hundreds of nodes.

Radial Layout — JsonGraphsinteractive

Instant Layout Switching

Switch between Tree and Radial layouts at any time with a single call to graph.setLayout(). The toolbar includes a one-click toggle. Click the buttons below to watch the same graph transform between layouts:


🔄

Streaming Parser

Large JSON files are loaded progressively — the UI never freezes, even with multi-megabyte datasets. Nodes appear as the data is read. Pass a File, ReadableStream, Response, plain string, or JS object — all handled identically.

streaming.ts
typescript
1// From a fetch Response — library streams it without extra fetch() calls
2const res = await fetch('/api/data.json');
3await graph.load(res);
4
5// From a File input
6const [file] = input.files;
7await graph.load(file);
8
9// From a ReadableStream
10await graph.load(response.body);
11
12// From a plain object or JSON string — both work too
13await graph.load({ users: [...] });
14await graph.load('{"users":[...]}');
Large Dataset (24 transactions) — JsonGraphsstreaming

🪗

Expand / Collapse

Double-click any object or array node to toggle its subtree. Collapsed nodes show a child count badge. Large datasets auto-collapse beyond depth 3 on load — keeping the initial view navigable. You can programmatically expand/collapse via graph.expandNode(id) /graph.collapseNode(id).

Expand / Collapse — double-click any node — JsonGraphsinteractive

🔍

Full-Text Search

press Ctrl+K (or ⌘K) to open the search overlay. Results highlight matching nodes and pan the viewport to them. The search index is built at parse-time, making repeated queries O(1).

Search — press Ctrl+K — JsonGraphsinteractive

🎨

Semantic Node Colors

Each JSON primitive type renders with a distinct color in both light and dark themes. Colors are fully customizable via the theme system.

Light theme

object
array
string
number
boolean
null

Dark theme

object
array
string
number
boolean
null
All node types visible — JsonGraphs

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
1import { JsonGraph } from 'jsongraphs';
2
3const 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
1// From a plain object
2await graph.load({ name: "Alice", age: 30 });
3
4// From a JSON string
5await graph.load('{"hello": "world"}');
6
7// From a file input
8const file = fileInput.files[0];
9await graph.load(file);
10
11// From a URL (auto-fetches and streams)
12await graph.load(new URL('https://api.example.com/data.json'));
13
14// From a fetch() stream (most memory-efficient for large files)
15const res = await fetch('/huge-data.json');
16await 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
1// Switch to radial layout
2graph.setLayout('radial');
3
4// Switch back to tree layout
5graph.setLayout('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
1import { darkTheme, defaultTheme } from 'jsongraphs';
2
3// Apply dark theme
4graph.setTheme(darkTheme);
5
6// Apply light theme
7graph.setTheme(defaultTheme);
8
9// Apply a custom theme
10graph.setTheme(myCustomTheme);

toggleTheme()

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

typescript
1// One-liner to toggle light ↔ dark
2graph.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
1graph.fitView();

zoomIn() / zoomOut()

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

typescript
1graph.zoomIn(); // scale × 1.15
2graph.zoomOut(); // scale × 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
1graph.expandAll(); // Show all nodes
2graph.collapseAll(); // Collapse all (keep root visible)

invalidateCache()

Manually trigger a re-calculation of the visible node tree. This is useful if you manipulate the internal GraphModel directly and need to force the O(V) visibility engine to rebuild the render cache.

typescript
1graph.invalidateCache();

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
1// In a React useEffect cleanup
2return () => graph.destroy();
3
4// Or manually when done
5graph.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
1type JsonSource =
2 | File // Browser File API (e.g. from <input type="file">)
3 | ReadableStream<Uint8Array> // WHATWG Streams (e.g. fetch().body)
4 | string // Raw JSON string
5 | URL // URL — fetched and streamed automatically
6 | object; // Any plain JS object / array

LayoutType

typescript
1type LayoutType = 'tree' | '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
1type NodeType = 'object' | 'array' | 'string' | 'number' | 'boolean' | 'null';

GraphNode

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

typescript
1interface NodePosition {
2 x: number;
3 y: number;
4 vx: number; // velocity used by force layout
5 vy: number;
6}
7
8interface GraphNode {
9 id: string;
10 type: NodeType;
11 key?: string;
12 value?: string;
13 childCount: number;
14 depth: number;
15 parentId?: string;
16 expanded: boolean;
17 pos: NodePosition;
18 width: number;
19 height: number;
20 highlighted: boolean;
21 hovered: boolean;
22 alpha: number;
23 transparent?: boolean;
24}

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
1interface GraphEdge {
2 id: string;
3 sourceId: string;
4 targetId: string;
5 label?: string; // JSON key that connects these nodes
6 caption?: string; // Optional pill label at edge midpoint
7 alpha: number; // Fade-in animation progress
8}
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
1interface ParseOptions {
2 maxDepth?: number; // Max nesting depth. Default: 200
3 maxNodes?: number; // Max total nodes. Default: 500_000
4 chunkSize?: number; // Text decoder chunk in bytes. Default: 65_536 (64 KB)
5}

ParseLimitError

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

typescript
1import { ParseLimitError } from 'jsongraphs';
2
3class ParseLimitError extends Error {
4 readonly kind: 'depth' | 'nodes';
5}
6
7// Usage
8const graph = new JsonGraph({
9 container,
10 maxNodes: 10_000,
11 onError: (err) => {
12 if (err instanceof ParseLimitError) {
13 if (err.kind === 'nodes') {
14 console.warn('Too many nodes! Increase maxNodes.');
15 } else if (err.kind === 'depth') {
16 console.warn('JSON too deeply nested! Increase maxDepth.');
17 }
18 }
19 },
20});

Theme

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

typescript
1interface NodeColors {
2 fill: string; // Node background
3 stroke: string; // Node border
4 text: string; // Node label text
5 badge: string; // Type badge colour
6}
7
8interface Theme {
9 name: string;
10 background: string; // Canvas background colour
11 gridLine: string; // Subtle grid line colour
12
13 nodeColors: Record<NodeType, NodeColors>; // Per-type colours
14
15 edge: {
16 stroke: string; // Default edge colour
17 highlight: string; // Highlighted edge colour
18 width: number; // Edge line width in px
19 captionBg: string; // Caption pill background
20 captionText: string; // Caption pill text
21 captionBorder: string; // Caption pill border
22 };
23
24 font: {
25 family: string; // CSS font-family
26 keySize: number; // Key label font size
27 valueSize: number; // Value label font size
28 typeSize: number; // Type badge font size
29 };
30
31 node: {
32 radius: number; // Corner radius
33 paddingX: number; // Horizontal padding
34 paddingY: number; // Vertical padding
35 minWidth: number; // Minimum node width
36 shadowBlur: number; // Drop shadow blur radius
37 shadowColor: string; // Drop shadow colour
38 shadowOffset: number; // Drop shadow offset
39 };
40
41 ui: {
42 panelBg: string; // Toolbar / search / minimap background
43 panelBorder: string; // Panel border colour
44 panelShadow: string; // Panel box-shadow CSS value
45 btnHover: string; // Button hover background
46 btnActive: string; // Button active / selected background
47 text: string; // Primary UI text colour
48 textMuted: string; // Secondary UI text colour
49 inputBg: string; // Search input background
50 inputText: string; // Search input text colour
51 inputPlaceholder: string; // Search placeholder colour
52 focusRing: string; // Focus ring colour
53 separator: string; // Separator line colour
54 minimapViewport: string; // Minimap viewport rect stroke
55 };
56}

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
1import { defaultTheme, darkTheme } from 'jsongraphs';
2
3// Apply light theme
4graph.setTheme(defaultTheme);
5
6// Apply dark theme
7graph.setTheme(darkTheme);
8
9// Toggle between them
10graph.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
1import { defaultTheme, type Theme } from 'jsongraphs';
2
3const sunsetTheme: Theme = {
4 ...defaultTheme,
5 name: 'sunset',
6 background: '#1a0a00',
7 gridLine: 'rgba(255,120,50,0.05)',
8
9 nodeColors: {
10 object: { fill: '#2a1000', stroke: '#ff6820', text: '#ffd0a0', badge: '#ff6820' },
11 array: { fill: '#001a10', stroke: '#20d080', text: '#a0ffc8', badge: '#20d080' },
12 string: { fill: '#00101a', stroke: '#20a0ff', text: '#a0d8ff', badge: '#20a0ff' },
13 number: { fill: '#1a1000', stroke: '#c0b020', text: '#ffe890', badge: '#c0b020' },
14 boolean: { fill: '#1a0020', stroke: '#c040c0', text: '#f0a0f0', badge: '#c040c0' },
15 null: { fill: '#151510', stroke: '#808070', text: '#c8c8b0', badge: '#808070' },
16 },
17
18 edge: {
19 ...defaultTheme.edge,
20 stroke: 'rgba(255,120,50,0.4)',
21 highlight: '#ff6820',
22 },
23
24 ui: {
25 ...defaultTheme.ui,
26 panelBg: 'rgba(26,10,0,0.92)',
27 text: '#ffd0a0',
28 focusRing: '#ff6820',
29 },
30};
31
32graph.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 — Floating Node & Edge Labels

Add a special __CAPTION__ key inside any JSON object to inject contextual meaning into your graph.

  • Top-Level Nodes (Depth 1): Renders as a beautiful floating pill directly above the node itself.
  • Deep Nodes (Depth > 1): Renders elegantly centered on the incoming connecting edge.
example.json
json
1{
2 "user": {
3 "__CAPTION__": "is assigned to",
4 "name": "Alice",
5 "role": "admin"
6 },
7 "project": {
8 "__CAPTION__": "Project Details",
9 "title": "JsonGraphs Docs",
10 "status": "active"
11 }
12}
__CAPTION__ live demo — JsonGraphs
📝Note
The __CAPTION__ node is automatically detected during parsing and extracted. The node itself is then marked transparent to keep the graph clean.

Streaming Large Files

JsonGraphs processes large JSON files progressively without blocking the UI. You can feed it a ReadableStream directly from a fetch() response, a File from a file input, or any JSON string or object. The graph renders nodes as they arrive — your app stays fully responsive throughout.

typescript
1// Stream directly from fetch — renders as data arrives
2const response = await fetch('/api/huge-dataset.json');
3await graph.load(response.body!);
4
5// From a File input (also streamed)
6document.querySelector('input[type="file"]')
7 .addEventListener('change', async (e) => {
8 const file = e.target.files[0];
9 await graph.load(file); // Streams the file, no full read into memory
10 });
11
12// With limit guards
13const graph = new JsonGraph({
14 container,
15 maxNodes: 50_000, // Stop at 50k nodes
16 maxDepth: 100, // Stop at depth 100
17 onError: (err) => {
18 console.error('Limit reached:', err.message);
19 },
20});
💡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.