InstallationGetting Started
JsonGraphs ships as a single zero-dependency ESM/CJS package. Install it with any package manager:
1npm install jsongraphs2# or3yarn add jsongraphs4# or5bun add jsongraphsdist/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.
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 mode8 layout: 'tree', // or 'radial'9 minimap: true, // bottom-left minimap10 toolbar: true, // zoom / layout / theme buttons11 search: true, // Ctrl+F search overlay12});13 14// Load from a plain JS object — synchronous, instant15await graph.load({16 project: "My App",17 version: "2.0",18 dependencies: {19 react: "^19",20 typescript: "^5",21 },22});Live result ↓
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.
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, timers32 return () => graph.destroy();33 }, [data, dark]);34 35 return <div ref={containerRef} style={{ width: "100%", height: "600px" }} />;36}"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.
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.
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.
1// From a fetch Response — library streams it without extra fetch() calls2const res = await fetch('/api/data.json');3await graph.load(res);4 5// From a File input6const [file] = input.files;7await graph.load(file);8 9// From a ReadableStream10await graph.load(response.body);11 12// From a plain object or JSON string — both work too13await graph.load({ users: [...] });14await graph.load('{"users":[...]}');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).
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).
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
Dark theme
JsonGraphAPI Reference
The main class. Initializes the canvas renderer, overlay UI components (toolbar, search bar, minimap), and event model inside the provided container element.
1import { JsonGraph } from 'jsongraphs';2 3const graph = new JsonGraph(opts: JsonGraphOptions);Constructor Options
| Option | Type | Default | Description |
|---|---|---|---|
container* | HTMLElement | — | The host element for the graph canvas and overlay UI. |
theme | Theme | defaultTheme | Initial 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(). |
minimap | boolean | true | Show the interactive mini-map overlay in the bottom-right corner. |
toolbar | boolean | true | Show the toolbar overlay with zoom, fit, layout toggle, expand/collapse, and theme toggle buttons. |
search | boolean | true | Show the node search bar overlay at the top-center of the graph. |
maxDepth | number | 200 | Maximum JSON nesting depth before a ParseLimitError is thrown. |
maxNodes | number | 500_000 | Maximum total graph nodes before a ParseLimitError is thrown. |
onLoad | (nodes: number, edges: number) => void | — | Callback invoked after parsing completes. Receives total node and edge counts. |
onError | (err: Error) => void | — | Callback invoked if parsing fails (e.g. invalid JSON or limit exceeded). |
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:
objectPlain JS object
Any serialisable JS value. Parsed synchronously.
stringJSON string
A raw JSON string. Parsed synchronously.
FileFile object
From an <input type='file'> element. Streamed.
URLURL object
Fetched via fetch() and streamed.
ReadableStreamReadableStream
Streamed byte-by-byte. Ideal for huge files.
1// From a plain object2await graph.load({ name: "Alice", age: 30 });3 4// From a JSON string5await graph.load('{"hello": "world"}');6 7// From a file input8const 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.
1// Switch to radial layout2graph.setLayout('radial');3 4// Switch back to tree layout5graph.setLayout('tree');setTheme(theme: Theme)
Applies a new Theme object to the canvas renderer, toolbar, search bar, and minimap simultaneously.
1import { darkTheme, defaultTheme } from 'jsongraphs';2 3// Apply dark theme4graph.setTheme(darkTheme);5 6// Apply light theme7graph.setTheme(defaultTheme);8 9// Apply a custom theme10graph.setTheme(myCustomTheme);toggleTheme()
Convenience method that toggles between defaultTheme (light) and darkTheme. Equivalent to checking the current theme name and calling setTheme().
1// One-liner to toggle light ↔ dark2graph.toggleTheme();fitView()
Resets pan and zoom so all visible nodes fit within the container with an 80 px padding. Also marks the minimap dirty.
1graph.fitView();zoomIn() / zoomOut()
Smooth programmatic zoom centered on the container midpoint.zoomIn()scales by ×1.15 andzoomOut()scales by ×0.87.
1graph.zoomIn(); // scale × 1.152graph.zoomOut(); // scale × 0.87expandAll() / collapseAll()
expandAll() expands everyobject andarray node.collapseAll() collapses all except the root node. Both re-run the layout engine after toggling visibility.
1graph.expandAll(); // Show all nodes2graph.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.
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.
1// In a React useEffect cleanup2return () => graph.destroy();3 4// Or manually when done5graph.destroy();destroy() will leave dangling event listeners and canvas elements in memory.TypesTypes
JsonSource
A union type representing every valid data source for graph.load().
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 string5 | URL // URL — fetched and streamed automatically6 | object; // Any plain JS object / arrayLayoutType
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.
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.
1interface NodePosition {2 x: number;3 y: number;4 vx: number; // velocity used by force layout5 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
| Option | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique node identifier. |
type | NodeType | — | JSON value type: object | array | string | number | boolean | null. |
key | string? | — | The JSON key that produced this node. Undefined for the root node. |
value | string? | — | Serialised display value for primitives (string, number, boolean, null). |
childCount | number | — | Number of direct children (object keys or array items). |
depth | number | — | Zero-indexed depth from the root. |
parentId | string? | — | ID of the parent node. Undefined for the root. |
expanded | boolean | — | Whether this node is currently expanded in the UI. |
pos | NodePosition | — | Layout position { x, y, vx, vy }. Mutated by the layout engine. |
width | number | — | Bounding box width set after layout measurement. |
height | number | — | Bounding box height set after layout measurement. |
highlighted | boolean | — | True when this node matches a search query. |
hovered | boolean | — | True when the pointer is over this node. |
alpha | number | — | Animation progress 0→1 used for fade-in. |
transparent | boolean? | — | 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.
1interface GraphEdge {2 id: string;3 sourceId: string;4 targetId: string;5 label?: string; // JSON key that connects these nodes6 caption?: string; // Optional pill label at edge midpoint7 alpha: number; // Fade-in animation progress8}| Option | Type | Default | Description |
|---|---|---|---|
id | string | — | Unique edge identifier. |
sourceId | string | — | ID of the source (parent) node. |
targetId | string | — | ID of the target (child) node. |
label | string? | — | The JSON key label on this edge (same as target.key). |
caption | string? | — | Optional text displayed in a pill box at the edge midpoint. Set automatically when the target key is __CAPTION__. |
alpha | number | — | Animation progress 0→1 for fade-in. |
ParseOptions
Low-level parser configuration passed via maxDepth and maxNodes on JsonGraphOptions.
1interface ParseOptions {2 maxDepth?: number; // Max nesting depth. Default: 2003 maxNodes?: number; // Max total nodes. Default: 500_0004 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.
1import { ParseLimitError } from 'jsongraphs';2 3class ParseLimitError extends Error {4 readonly kind: 'depth' | 'nodes';5}6 7// Usage8const 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.
1interface NodeColors {2 fill: string; // Node background3 stroke: string; // Node border4 text: string; // Node label text5 badge: string; // Type badge colour6}7 8interface Theme {9 name: string;10 background: string; // Canvas background colour11 gridLine: string; // Subtle grid line colour12 13 nodeColors: Record<NodeType, NodeColors>; // Per-type colours14 15 edge: {16 stroke: string; // Default edge colour17 highlight: string; // Highlighted edge colour18 width: number; // Edge line width in px19 captionBg: string; // Caption pill background20 captionText: string; // Caption pill text21 captionBorder: string; // Caption pill border22 };23 24 font: {25 family: string; // CSS font-family26 keySize: number; // Key label font size27 valueSize: number; // Value label font size28 typeSize: number; // Type badge font size29 };30 31 node: {32 radius: number; // Corner radius33 paddingX: number; // Horizontal padding34 paddingY: number; // Vertical padding35 minWidth: number; // Minimum node width36 shadowBlur: number; // Drop shadow blur radius37 shadowColor: string; // Drop shadow colour38 shadowOffset: number; // Drop shadow offset39 };40 41 ui: {42 panelBg: string; // Toolbar / search / minimap background43 panelBorder: string; // Panel border colour44 panelShadow: string; // Panel box-shadow CSS value45 btnHover: string; // Button hover background46 btnActive: string; // Button active / selected background47 text: string; // Primary UI text colour48 textMuted: string; // Secondary UI text colour49 inputBg: string; // Search input background50 inputText: string; // Search input text colour51 inputPlaceholder: string; // Search placeholder colour52 focusRing: string; // Focus ring colour53 separator: string; // Separator line colour54 minimapViewport: string; // Minimap viewport rect stroke55 };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)
1import { defaultTheme, darkTheme } from 'jsongraphs';2 3// Apply light theme4graph.setTheme(defaultTheme);5 6// Apply dark theme7graph.setTheme(darkTheme);8 9// Toggle between them10graph.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.
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);...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.
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__ 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.
1// Stream directly from fetch — renders as data arrives2const 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 memory10 });11 12// With limit guards13const graph = new JsonGraph({14 container,15 maxNodes: 50_000, // Stop at 50k nodes16 maxDepth: 100, // Stop at depth 10017 onError: (err) => {18 console.error('Limit reached:', err.message);19 },20});fetch().body or a File object. Passing a pre-parsed string works too, but keeps the entire string in memory.