Skip to content

stakwork/system-canvas

 
 

Repository files navigation

system-canvas

Interactive, infinitely zoomable, editable SVG diagrams from JSON Canvas documents.

system-canvas demo

Packages

Package Description
system-canvas Pure TypeScript core. Types, themes, edge routing, viewport math. Zero dependencies.
system-canvas-react React components. Pan/zoom viewport, node renderers, breadcrumb navigation.
system-canvas-standalone Self-contained IIFE bundle for <script> tag / CDN use. No build step required.

Install

npm install system-canvas system-canvas-react

Quick start

import { SystemCanvas } from "system-canvas-react";

const canvas = {
  theme: {
    base: "dark",
    categories: {
      service: {
        defaultWidth: 140,
        defaultHeight: 60,
        fill: "rgba(6, 78, 59, 0.4)",
        stroke: "#34d399",
      },
    },
  },
  nodes: [
    {
      id: "api",
      type: "text",
      text: "API Server\nExpress",
      x: 0,
      y: 0,
      category: "service",
    },
    {
      id: "db",
      type: "text",
      text: "PostgreSQL",
      x: 250,
      y: 0,
      width: 140,
      height: 60,
      color: "6",
    },
  ],
  edges: [{ id: "e1", fromNode: "api", toNode: "db", label: "queries" }],
};

function App() {
  return (
    <div style={{ width: "100vw", height: "100vh" }}>
      <SystemCanvas canvas={canvas} />
    </div>
  );
}

Standalone (no build step)

<div id="app" style="width: 100vw; height: 100vh"></div>
<script src="https://unpkg.com/system-canvas-standalone/dist/system-canvas.min.js"></script>
<script>
  SystemCanvas.render(document.getElementById("app"), {
    canvas: {
      nodes: [
        { id: "a", type: "text", text: "API", x: 0, y: 0, width: 120, height: 60, color: "4" },
        { id: "b", type: "text", text: "DB", x: 220, y: 0, width: 120, height: 60, color: "6" },
      ],
      edges: [{ id: "e1", fromNode: "a", toNode: "b" }],
    },
    theme: "midnight",
    editable: true,
  });
</script>

Features

  • Pan and zoom with mouse/trackpad (d3-zoom)
  • Nested canvases -- nodes with a ref property are clickable; clicking navigates to a sub-canvas with breadcrumb trail back
  • Editable mode -- add, drag, resize, inline-edit, and delete nodes; create edges by dragging between node border handles; inline-edit edge labels; Delete/Backspace removes selected nodes or edges
  • 5 built-in themes -- dark, midnight, light, blueprint, warm
  • 3 edge routing modes -- bezier, straight, orthogonal
  • Categories -- define reusable node styles (dimensions, colors, icons) in the theme
  • JSON Canvas compatible -- extends the JSON Canvas spec with ref, category, and inline theme

Themes

import { SystemCanvas } from 'system-canvas-react'
import { themes } from 'system-canvas'

<SystemCanvas canvas={data} theme={themes.midnight} />
<SystemCanvas canvas={data} theme={themes.blueprint} />

Or set the base theme in the canvas data itself:

{ "theme": { "base": "warm" }, "nodes": [...] }

Navigation

Nodes with a ref property become navigable. Provide an onResolveCanvas callback to load sub-canvases:

<SystemCanvas
  canvas={rootCanvas}
  onResolveCanvas={async (ref) => {
    const response = await fetch(`/api/canvas/${ref}`);
    return response.json();
  }}
/>

Editing

Pass editable and wire the granular mutation callbacks. The library is stateless -- you own CanvasData and pass it back on every render. Core helpers do the immutable merge for you:

import { useState } from "react";
import { SystemCanvas } from "system-canvas-react";
import {
  addNode,
  updateNode,
  removeNode,
  addEdge,
  updateEdge,
  removeEdge,
} from "system-canvas";
import type { CanvasData } from "system-canvas";

function App() {
  const [canvas, setCanvas] = useState<CanvasData>(initial);

  return (
    <SystemCanvas
      canvas={canvas}
      editable
      onNodeAdd={(node) => setCanvas((c) => addNode(c, node))}
      onNodeUpdate={(id, patch) => setCanvas((c) => updateNode(c, id, patch))}
      onNodeDelete={(id) => setCanvas((c) => removeNode(c, id))}
      onEdgeAdd={(edge) => setCanvas((c) => addEdge(c, edge))}
      onEdgeUpdate={(id, patch) => setCanvas((c) => updateEdge(c, id, patch))}
      onEdgeDelete={(id) => setCanvas((c) => removeEdge(c, id))}
    />
  );
}

Every editing callback receives a third canvasRef: string | undefined argument identifying which canvas the change belongs to (undefined at the root). When editable is enabled with sub-canvases, pass a synchronous canvases: Record<string, CanvasData> map so edits to sub-canvases are observable by the library.

Interactions in editable mode:

  • Drag nodes to move them; dragging a group moves its spatially-contained children.
  • Resize the selected node via the four corner handles.
  • Double-click a node to inline-edit its text/file/link/label; double-click an edge to inline-edit its label.
  • Hover a node to reveal four connection handles (one per side); drag from a handle to another node to create an edge. Groups don't participate in edge creation.
  • Click to select, Delete or Backspace to remove the selected node or edge. Escape clears selection.

Props

Prop Type Description
canvas CanvasData Canvas document to render
theme CanvasTheme Theme override (optional, defaults to dark)
edgeStyle 'bezier' | 'straight' | 'orthogonal' Edge routing mode (default: bezier)
onResolveCanvas (ref: string) => Promise<CanvasData> Resolve a ref to sub-canvas data
onNodeClick (node: CanvasNode) => void Node click handler
onNodeDoubleClick (node: CanvasNode) => void Node double-click handler
onEdgeClick (edge: CanvasEdge) => void Edge click handler
onEdgeDoubleClick (edge: CanvasEdge) => void Edge double-click handler
onContextMenu (event: ContextMenuEvent) => void Right-click handler
editable boolean Enable add / edit / move / delete for nodes and edges
onNodeAdd (node, canvasRef) => void Fired after user picks an option from the add-node menu
onNodeUpdate (nodeId, patch, canvasRef) => void Fired after drags and node editor commits
onNodeDelete (nodeId, canvasRef) => void Fired on Delete/Backspace with a selected node
onEdgeAdd (edge, canvasRef) => void Fired when the user drags from a connection handle to another node
onEdgeUpdate (edgeId, patch, canvasRef) => void Fired after edge label editor commits
onEdgeDelete (edgeId, canvasRef) => void Fired on Delete/Backspace with a selected edge
onNavigate (ref: string) => void Called when navigating to a sub-canvas
onBreadcrumbClick (index: number) => void Called when a breadcrumb is clicked
rootLabel string Root breadcrumb label (default: "Home")
minZoom number Minimum zoom level (default: 0.1)
maxZoom number Maximum zoom level (default: 4)
defaultViewport ViewportState Initial viewport position and zoom
onViewportChange (viewport: ViewportState) => void Called on pan/zoom

JSON Canvas extensions

This library extends the JSON Canvas 1.0 spec with three additive fields:

Field On Purpose
ref any node URI pointing to a sub-canvas for drill-down navigation
category any node Maps to a category definition in the theme for reusable styling
theme top-level Inline base theme name and category definitions

Standard JSON Canvas documents render correctly. The extensions are ignored by other viewers.

Development

npm install
npm run build    # build core + react
npm run dev      # start demo at localhost:5173

License

MIT

About

infinite systems diagrams

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages

  • TypeScript 97.3%
  • HTML 2.5%
  • JavaScript 0.2%