Try Live
Add Docs
Rankings
Pricing
Enterprise
Docs
Install
Install
Docs
Pricing
Enterprise
More...
More...
Try Live
Rankings
Add Docs
JSON Canvas Viewer
https://github.com/hesprs/json-canvas-viewer
Admin
JSON Canvas Viewer is an extensible web-based viewer for JSON Canvas files with interactive pan and
...
Tokens:
14,723
Snippets:
171
Trust Score:
6.9
Update:
14 hours ago
Context
Skills
Chat
Benchmark
84.1
Suggestions
Latest
Show doc for...
Code
Info
Show Results
Context Summary (auto-generated)
Raw
Copy
Link
# JSON Canvas Viewer JSON Canvas Viewer (`json-canvas-viewer`, v4.1.1) is an extensible, web-based viewer for the [JSON Canvas](https://jsoncanvas.org/) format — the open canvas specification used by Obsidian. It renders `.canvas` files interactively in any browser, supporting the full official spec including text nodes, file/image/audio/video nodes, link nodes, group nodes, and edges with labels and arrows. The viewer provides interactive pan and zoom, responsive mobile/touchpad support, lazy loading, light/dark themes, and DPI-aware canvas rendering. It is TypeScript-native, tree-shakable, ESM-only, and benchmarks faster than rendering canvases in Obsidian itself. The library ships as two deployment targets: a **chimp version** (single bundled file, ideal for quick CDN trials) and a **full version** (tree-shakable npm package with official adapters for Vanilla JS/TS, React, Preact, Vue 3, and Vite). The architecture is built on the [SynthKernel](https://github.com/hesprs/synthkernel) pattern — a dependency-injection kernel where every feature (including the optional Minimap, Controls, MistouchPreventer, and DebugPanel) is an independently loaded module. Custom modules can be authored and plugged in to extend the viewer's options, methods, and rendering behavior. --- ## Installation ```bash # Core (Vanilla JS/TS) npm install json-canvas-viewer # React adapter npm install @json-canvas-viewer/react # Preact adapter npm install @json-canvas-viewer/preact # Vue 3 adapter npm install @json-canvas-viewer/vue # Vite build-time plugin npm install vite-plugin-json-canvas ``` --- ## `new JSONCanvasViewer(options, modules?)` — Core Constructor Instantiates the interactive canvas viewer and attaches it to a DOM element. Accepts an `options` object (required) and an optional array of module constructors that extend the viewer's capabilities. Returns a `JSONCanvasViewerInterface` instance whose public API grows as modules are added. ```typescript import { JSONCanvasViewer, Minimap, Controls, MistouchPreventer, fetchCanvas, parser, type Options, } from 'json-canvas-viewer'; import canvasData from './my-board.canvas'; // requires vite-plugin-json-canvas // Full construction with all optional modules const viewer = new JSONCanvasViewer( { container: document.getElementById('canvas-root')!, canvas: canvasData, theme: 'dark', markdownParser: parser, // built-in DOMPurify + marked parser attachmentDir: '/assets/', // base dir for embedded files attachments: { // per-file overrides 'diagram.png': '/images/diagram-v2.png', }, loading: 'lazy', // 'normal' | 'lazy' | 'none' shadowed: false, // shadow DOM isolation zoomInOptimization: false, extraCSS: ` .JSON-Canvas-Viewer { border-radius: 12px; } `, colors: { light: { background: '#f8f8f2', text: '#282a36', '1': '#ff5555' }, dark: { background: '#282a36', text: '#f8f8f2', '1': '#ff79c6' }, }, minimapCollapsed: false, // Minimap option controlsCollapsed: false, // Controls option preventMistouchAtStart: true, // MistouchPreventer option mistouchPreventerBannerText: 'Tap to interact', }, [Minimap, Controls, MistouchPreventer], ); // Accessing typed options without instantiating type MyOptions = Options<[typeof Minimap, typeof Controls]>; // Cleanup viewer.dispose(); ``` --- ## `fetchCanvas(path)` — Canvas Loader Fetches a `.canvas` or `.json` file from a URL and returns the parsed `JSONCanvas` object. Used when Vite's static import is unavailable (e.g., no bundler, or runtime-selected files). ```typescript import { JSONCanvasViewer, fetchCanvas, parser } from 'json-canvas-viewer'; // Fetch at runtime — works in any browser environment const canvas = await fetchCanvas('/static/boards/project-overview.canvas'); const viewer = new JSONCanvasViewer({ container: document.getElementById('viewer')!, canvas, markdownParser: parser, }); // Reload with a different canvas later const updatedCanvas = await fetchCanvas('/static/boards/sprint-2.canvas'); viewer.load({ canvas: updatedCanvas, attachmentDir: '/sprint-2-assets/' }); ``` --- ## `parser` — Built-in Markdown Parser An async function exported from `json-canvas-viewer` that converts Markdown strings to sanitized HTML using `marked` and `DOMPurify`. Pass it as the `markdownParser` option to enable rendering of Markdown text nodes. ```typescript import { JSONCanvasViewer, parser, fetchCanvas } from 'json-canvas-viewer'; // parser signature: (markdown: string) => Promise<string> const html = await parser('# Hello\n**bold** and _italic_'); // → "<h1>Hello</h1><p><strong>bold</strong> and <em>italic</em></p>" // Use as the markdownParser option const viewer = new JSONCanvasViewer({ container: document.body, canvas: await fetchCanvas('/board.canvas'), markdownParser: parser, }); // Custom parser example (strip all markdown, plain text only) const plainTextParser = (md: string) => md.replace(/[#*_`]/g, ''); const viewer2 = new JSONCanvasViewer({ container: document.getElementById('viewer2')!, canvas: await fetchCanvas('/board.canvas'), markdownParser: plainTextParser, }); ``` --- ## `renderToString(options)` — Server-Side Prerendering Asynchronously renders the canvas nodes to an HTML string for SSR/SSG use cases. The returned string can be injected as `innerHTML` for above-the-fold display before the interactive viewer hydrates. ```typescript import { renderToString, parser } from 'json-canvas-viewer'; import type { JSONCanvas } from 'json-canvas-viewer'; const canvas: JSONCanvas = { nodes: [ { id: 'n1', type: 'text', text: '## Welcome', x: 0, y: 0, width: 200, height: 100 }, { id: 'n2', type: 'file', file: 'notes/intro.md', x: 220, y: 0, width: 300, height: 200 }, ], edges: [], }; const html = await renderToString({ canvas, attachmentDir: '/public/notes/', attachments: { 'intro.md': '/public/notes/intro.md' }, parser, }); // Use in a React server component import { JSONCanvasViewerComponent } from '@json-canvas-viewer/react'; export default async function Page() { const prerenderHtml = await renderToString({ canvas, parser }); return ( <JSONCanvasViewerComponent canvas={canvas} prerenderHtml={prerenderHtml} /> ); } ``` --- ## Viewer Instance Methods and Hooks The `JSONCanvasViewerInterface` exposes imperative methods for programmatic control and lifecycle hooks for event subscription. ```typescript import { JSONCanvasViewer, fetchCanvas, parser } from 'json-canvas-viewer'; const viewer = new JSONCanvasViewer({ container: document.getElementById('viewer')!, canvas: await fetchCanvas('/board.canvas'), markdownParser: parser, loading: 'none', // manual load control }); // --- Lifecycle hooks (subscribe before loading) --- viewer.onStart.subscribe(() => console.log('Viewer ready')); viewer.onRestart.subscribe(() => console.log('Canvas reloaded')); viewer.onDispose.subscribe(() => console.log('Viewer disposed')); viewer.onRefresh.subscribe(() => console.log('Re-rendered')); viewer.onResize.subscribe((w, h) => console.log(`Resized: ${w}×${h}`)); viewer.onChangeTheme.subscribe((theme) => console.log(`Theme: ${theme}`)); viewer.onToggleFullscreen.subscribe((state) => console.log(`Fullscreen: ${state}`)); viewer.onNodeActive.subscribe((node) => console.log('Selected node:', node.id)); viewer.onNodeLosesActive.subscribe((node) => console.log('Deselected:', node.id)); // --- Manual load / reload --- viewer.load(); // initial load (since loading: 'none') viewer.load({ canvas: await fetchCanvas('/other.canvas'), attachmentDir: '/other/' }); // --- Navigation --- viewer.resetView(); // fit all content in viewport viewer.pan({ x: 100, y: -50 }); // pan by delta viewer.panToCoords({ x: 0, y: 0 }); // pan to absolute coords viewer.zoom(1.2); // zoom by factor (>1 = in, <1 = out) viewer.zoomToScale(0.8); // zoom to exact scale // --- UI controls --- viewer.changeTheme('dark'); // or 'light', or no arg to toggle viewer.toggleFullscreen('enter'); // or 'exit', or no arg to toggle // --- Cleanup --- viewer.refresh(); // force rerender viewer.dispose(); // remove from DOM and clean up ``` --- ## `vite-plugin-json-canvas` — Vite Plugin A Vite transform plugin that parses `.canvas` files at build time, pre-processing all Markdown text nodes so no client-side parsing is needed. Enables direct `import` of `.canvas` files as typed `JSONCanvas` objects. ```typescript // vite.config.ts import { defineConfig } from 'vite'; import canvas from 'vite-plugin-json-canvas'; import { marked } from 'marked'; export default defineConfig({ plugins: [ canvas(marked), // pass any (md: string) => string | Promise<string> // canvas() // defaults to marked // canvas((md) => md) // disable markdown parsing ], }); ``` ```typescript // src/app.ts — after plugin is set up import { JSONCanvasViewer, Minimap } from 'json-canvas-viewer'; import canvasData from './assets/my-board.canvas'; // typed as JSONCanvas const viewer = new JSONCanvasViewer( { container: document.body, canvas: canvasData }, [Minimap], ); ``` --- ## `JSONCanvasViewerComponent` — React / Preact Component A `forwardRef` React (or Preact) component wrapping the core viewer. Reactive props `canvas`, `theme`, `attachments`, and `attachmentDir` trigger efficient updates. Node types can be replaced with custom React components. Access the viewer instance via `ref.viewer`. ```tsx import { useRef } from 'react'; import { JSONCanvasViewerComponent, Minimap, Controls } from '@json-canvas-viewer/react'; import type { JSONCanvas } from '@json-canvas-viewer/react'; import canvas from './board.canvas'; const myCanvas: JSONCanvas = canvas; export default function CanvasPage() { const viewerRef = useRef<{ viewer: any }>(null); return ( <JSONCanvasViewerComponent ref={viewerRef} canvas={myCanvas} theme="dark" modules={[Minimap, Controls]} attachmentDir="/assets/" className="canvas-wrapper" style={{ height: '80vh', width: '100%' }} options={{ zoomInOptimization: true }} // Custom node renderers text={({ content, node, onActive }) => ( <div className="custom-text-node" onClick={() => console.log('Node clicked:', node.id)} dangerouslySetInnerHTML={{ __html: content }} /> )} link={({ node }) => ( <a href={node.url} target="_blank" rel="noopener noreferrer"> 🔗 {node.url} </a> )} /> ); } // Programmatic access via ref // viewerRef.current?.viewer.resetView(); // viewerRef.current?.viewer.changeTheme('light'); ``` --- ## `JSONCanvasViewerComponent` — Vue 3 Component A Vue 3 SFC component with reactive props and scoped slots for each node type. Wrap in `<Suspense>` since the component is async. Enable SSR with the `isPrerendering` prop. ```vue <script setup lang="ts"> import { ref } from 'vue'; import { JSONCanvasViewerComponent, Minimap, Controls, } from '@json-canvas-viewer/vue'; import canvas from './board.canvas'; const viewerRef = ref<{ viewer: any } | null>(null); const currentCanvas = ref(canvas); const currentTheme = ref<'light' | 'dark'>('light'); function switchTheme() { currentTheme.value = currentTheme.value === 'light' ? 'dark' : 'light'; } </script> <template> <button @click="switchTheme">Toggle theme</button> <!-- Suspense is required because the component is async --> <Suspense> <JSONCanvasViewerComponent ref="viewerRef" :canvas="currentCanvas" :theme="currentTheme" :modules="[Minimap, Controls]" attachment-dir="/assets/" :is-prerendering="false" style="height: 80vh; width: 100%;" > <!-- Scoped slot: custom text node renderer --> <template #text="{ content, node }"> <div class="custom-text" v-html="content" :data-node-id="node.id" /> </template> <!-- Scoped slot: custom link node renderer --> <template #link="{ node }"> <a :href="node.url" target="_blank">🔗 {{ node.url }}</a> </template> </JSONCanvasViewerComponent> </Suspense> </template> ``` --- ## Built-in Modules Four optional modules ship with the package. Pass their constructors in the `modules` array to activate them. ```typescript import { JSONCanvasViewer, Minimap, Controls, MistouchPreventer, DebugPanel, fetchCanvas, parser, } from 'json-canvas-viewer'; const viewer = new JSONCanvasViewer( { container: document.getElementById('app')!, canvas: await fetchCanvas('/demo.canvas'), markdownParser: parser, // Minimap options minimapCollapsed: false, // start collapsed? // Controls options controlsCollapsed: false, // start collapsed? // MistouchPreventer options preventMistouchAtStart: true, // freeze canvas on load mistouchPreventerBannerText: 'Tap to interact with canvas', }, [Minimap, Controls, MistouchPreventer, DebugPanel], ); // Module-augmented methods (typed automatically) viewer.toggleMinimapCollapse(); // from Minimap viewer.toggleControlsCollapse(); // from Controls viewer.startMistouchPrevention(); // from MistouchPreventer viewer.endMistouchPrevention(); // from MistouchPreventer ``` --- ## `BaseModule` — Custom Module Development All modules extend `BaseModule`. Modules can declare custom options (type-safe via generics), inject methods into the viewer's public interface via `this.augment()`, and access internal services via DI. ```typescript import { BaseModule, type BaseArgs, type BaseOptions, internal, canvasUtils, } from 'json-canvas-viewer'; const { DataManager, Controller } = internal; // 1. Declare custom options interface Options extends BaseOptions { showNodeCount?: boolean; } // 2. Declare augmentations (methods added to viewer instance) interface Augmentation { getNodeCount: NodeCounterModule['getNodeCount']; } // 3. Implement the module class NodeCounterModule extends BaseModule<Options, Augmentation> { private DM: DataManager; private badge: HTMLDivElement | null = null; constructor(...args: BaseArgs) { super(...args); // Dependency injection this.DM = this.container.get(DataManager); // Subscribe to rerenders this.container.get(Controller).hooks.onRefresh.subscribe(this.update); // Build UI this.badge = document.createElement('div'); this.badge.className = 'node-counter-badge'; canvasUtils.applyStyles(this.DM.data.container, `.node-counter-badge { position: absolute; top: 8px; left: 8px; background: rgba(0,0,0,0.6); color: #fff; padding: 4px 8px; border-radius: 4px; font-size: 12px; }`); this.DM.data.container.appendChild(this.badge); // Expose public method on viewer instance this.augment({ getNodeCount: this.getNodeCount }); // Lifecycle cleanup this.onDispose(this.dispose); } private update = () => { if (!this.badge) return; const count = this.DM.data.canvas?.nodes?.length ?? 0; if (this.options.showNodeCount !== false) { this.badge.textContent = `Nodes: ${count}`; } }; getNodeCount = (): number => { return this.DM.data.canvas?.nodes?.length ?? 0; }; private dispose = () => { this.badge?.remove(); this.badge = null; }; } // Usage import { JSONCanvasViewer, fetchCanvas } from 'json-canvas-viewer'; const viewer = new JSONCanvasViewer( { container: document.body, canvas: await fetchCanvas('/board.canvas'), showNodeCount: true, // custom option is type-safe }, [NodeCounterModule], ); console.log(viewer.getNodeCount()); // augmented method is type-safe ``` --- ## Chimp Version (CDN / No Bundler) The chimp version bundles everything into a single file served via unpkg/jsDelivr. Suitable for quick prototypes, static HTML pages, or environments without a build step. ```html <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0"/> <title>Canvas Viewer</title> <style> body, html { margin: 0; padding: 0; width: 100%; height: 100%; } </style> </head> <body></body> <script type="module"> import { JSONCanvasViewer, parser, fetchCanvas, Minimap, Controls, MistouchPreventer, } from 'https://unpkg.com/json-canvas-viewer'; const canvas = await fetchCanvas('https://example.com/demo.canvas'); const viewer = new JSONCanvasViewer( { container: document.body, canvas, markdownParser: parser, theme: 'dark', loading: 'normal', mistouchPreventerBannerText: 'Tap to unlock', preventMistouchAtStart: true, }, [Minimap, Controls, MistouchPreventer], ); // Programmatic control document.addEventListener('keydown', (e) => { if (e.key === 'r') viewer.resetView(); if (e.key === 't') viewer.changeTheme(); if (e.key === 'f') viewer.toggleFullscreen(); }); </script> </html> ``` --- ## Summary JSON Canvas Viewer is designed for two primary use cases: **embedding interactive Obsidian-exported canvas files into websites**, and **building knowledge-graph or diagramming UIs** with full programmatic control. The CDN chimp version enables zero-setup embedding in any HTML page in under 10 lines of code, while the full npm version integrates cleanly into React, Vue, or Preact apps with reactive props, SSR prerendering support, and custom component slots for each node type — making it straightforward to compose canvas displays into existing design systems. The lazy loading mode and Vite build-time parsing make it production-ready for performance-sensitive applications. Integration patterns center on three approaches: (1) **drop-in CDN** for static sites and documentation pages, (2) **framework component** (`@json-canvas-viewer/react`, `/vue`, `/preact`) for SPAs and SSR apps where reactivity, hydration, and component composition matter, and (3) **programmatic vanilla API** for custom dashboards where imperative control over pan, zoom, theme, and lifecycle events is required. The module system makes JSON Canvas Viewer uniquely extensible — teams can ship their own DI-based modules alongside the four built-in ones, inheriting full type safety for both options and augmented methods without forking the library.