@sgummalla-works/sketchon
v0.3.2
Published
Sketchon — coordinate-free diagram rendering. A model authors the spec; a deterministic engine computes every pixel. Spec in, SVG out. Runs in Node and the browser.
Downloads
54
Maintainers
Readme
@sgummalla-works/sketchon
Coordinate-free diagram rendering. The model declares what the diagram is; a deterministic layout engine computes every pixel. No hand-coded coordinates, no overlapping labels.
Architecture
Ports & Adapters (Hexagonal Architecture) + Strategy pattern:
domain/ — DiagramSpec types + validation (pure, zero external deps)
ports/ — Technique interface (the Strategy contract)
application/ — routing + registry (orchestration layer)
adapters/ — concrete engines: Satori, ELK, Sequence, BaselineAdding a new engine = implement Technique, register in application/registry.ts. No other file changes.
Install
npm install @sgummalla-works/sketchonQuick start
import { renderDiagram, validateSpec } from '@sgummalla-works/sketchon';
import type { DiagramSpec } from '@sgummalla-works/sketchon';
const spec: DiagramSpec = {
id: 'my-flow',
kind: 'flow',
nodes: [
{ id: 'a', label: 'Start', shape: 'pill', emphasis: 'muted' },
{ id: 'b', label: 'Process', shape: 'rounded', emphasis: 'primary' },
{ id: 'c', label: 'Done', shape: 'pill', emphasis: 'success' },
],
edges: [
{ from: 'a', to: 'b' },
{ from: 'b', to: 'c', label: 'success' },
],
};
// Validate before rendering
const issues = validateSpec(spec);
if (issues.length) throw new Error(issues.map(i => i.message).join(', '));
// Render — engine is chosen automatically from spec.kind
const { svg, width, height } = await renderDiagram(spec);
// svg is a standalone <svg> string, ready for display or rasterisationNode vs browser
The package runs in both environments and bundlers pick the right build
automatically (via the browser / node export conditions). The only
difference is where the Satori engine gets its fonts + emoji:
| Environment | Default asset source |
|---|---|
| Node (index.js) | Bundled Inter TTFs + Twemoji read from disk (assets/) |
| Browser (index.browser.js) | Inter weights + Twemoji SVGs fetched from a CDN, cached in memory |
Everything else — the spec, validation, routing, all four engines — is identical. The public API is the same on both.
// In a browser app (e.g. React), the import resolves to the browser build:
import { renderDiagram } from '@sgummalla-works/sketchon';
const { svg } = await renderDiagram(spec); // fonts/emoji fetched on first renderOverriding fonts / emoji (configureAssets)
The defaults work out of the box. To go fully offline (no CDN fetch) or use a custom font, inject your own provider — only the methods you pass are replaced:
import { configureAssets } from '@sgummalla-works/sketchon';
configureAssets({
// e.g. bundle the font with your app and hand Satori the bytes directly:
loadFonts: () => [{ name: 'Inter', data: myInterArrayBuffer, weight: 400, style: 'normal' }],
});Diagram kinds and engines
| kind | Engine | Use for |
|----------------|-------------|---------|
| concept | Satori | Explainer / illustrative panels with rich cards, a "why" column, and a legend |
| mindmap | Satori | Central topic + radiating branches |
| sequence | Sequence | Actor lifelines + ordered message arrows |
| flow | ELK | Flowcharts, decision trees |
| architecture | ELK | System components + connections with group containers |
| state | ELK | State machines |
| er | ELK | Entity-relationship schemas |
Override the engine for a spec with spec.renderer: 'satori' | 'elk' | 'sequence'.
DiagramSpec reference
interface DiagramSpec {
id: string; // stable id (used in filenames + gallery)
kind: DiagramKind; // selects layout engine
title?: string; // optional heading
direction?: 'TB'|'BT'|'LR'|'RL'; // flow direction for graph kinds
nodes: DiagramNode[]; // at least one
edges: DiagramEdge[]; // may be empty
groups?: DiagramGroup[]; // visual containers / clusters
annotations?: DiagramAnnotation[]; // callout text beside nodes
categories?: DiagramCategory[]; // legend colour groups (concept diagrams)
columns?: ColumnHeaders; // left/right column headings (concept diagrams)
renderer?: 'satori'|'elk'|'sequence'; // override auto-routing
theme?: ThemeHints; // mode: 'light'|'dark', density
}Node
interface DiagramNode {
id: string;
label: string; // primary text
sublabel?: string; // second line
sublabelParts?: string[]; // dot-joined sub-line tokens (for card shape)
shape?: NodeShape; // box | rounded | pill | circle | diamond |
// cylinder | actor | note | icon | card | checkpoint
emphasis?: Emphasis; // normal | primary | muted | danger | success | accent
icon?: string; // emoji (e.g. '🔍') or semantic name
group?: string; // group id this node belongs to
category?: string; // colour category id (for legend)
}Edge
interface DiagramEdge {
from: string; // source node id
to: string; // target node id
label?: string; // edge text
style?: 'solid'|'dashed'|'dotted';
arrow?: 'none'|'open'|'filled';
semantic?: 'default'|'dependency'|'dataflow'|'message'|'return'|'association';
}Concept / panel diagram example
const spec: DiagramSpec = {
id: 'agent-tools',
kind: 'concept',
columns: { primary: 'Tool', annotation: 'Why it is mandatory' },
categories: [
{ id: 'retrieval', label: 'Retrieval', emphasis: 'primary' },
{ id: 'execution', label: 'Execution', emphasis: 'success' },
],
nodes: [
{ id: 'search', icon: '🔍', label: 'Web search',
sublabelParts: ['real-time lookup', 'citation'],
shape: 'card', category: 'retrieval' },
{ id: 'exec', icon: '⚙️', label: 'Code execution',
sublabelParts: ['sandboxed', 'formula checking'],
shape: 'card', category: 'execution' },
],
edges: [{ from: 'search', to: 'exec', arrow: 'filled' }],
annotations: [
{ target: 'search', text: 'Prevents hallucinations by anchoring claims', placement: 'right' },
{ target: 'exec', text: 'Validates numerical claims safely', placement: 'right' },
],
};Sequence diagram example
const spec: DiagramSpec = {
id: 'oauth',
kind: 'sequence',
title: 'OAuth 2.0',
nodes: [
{ id: 'u', label: 'User', emphasis: 'muted' },
{ id: 'app', label: 'Client app', emphasis: 'primary' },
{ id: 'auth', label: 'Auth server', emphasis: 'accent' },
],
edges: [
{ from: 'u', to: 'app', label: 'click login' },
{ from: 'app', to: 'auth', label: 'redirect /authorize' },
{ from: 'auth',to: 'app', label: 'auth code',
style: 'dashed', semantic: 'return' },
],
};Public API
| Export | Description |
|--------|-------------|
| renderDiagram(spec) | Main entry point. Auto-routes to the right engine. Returns Promise<RenderResult>. |
| validateSpec(spec) | Pure validation. Returns ValidationIssue[] (empty = valid). |
| selectEngineKey(spec) | Returns which engine key would be chosen ('satori'|'elk'|'sequence'|'baseline'). |
| selectEngine(spec) | Returns the Technique that would render the spec. |
| REGISTRY | Map of all registered engines (Record<EngineKey, Technique>). |
| TECHNIQUES | Array of all registered engines (used by the lab harness). |
| getTechnique(key) | Look up a specific engine by key. |
| DiagramSpec + all spec types | Re-exported from domain/spec.ts. |
| configureAssets(overrides) | Override the font/emoji source ({ loadFonts?, emojiToDataUri? }). Only the methods passed are replaced. |
| setAssetProvider(provider) | Replace the whole asset provider (advanced; the entries call this to bind the env default). |
| AssetProvider, SketchonFont | Types for the injection point. |
| loadFonts | Node build only: loads bundled Inter TTFs from disk for Satori. |
| emojiToDataUri | Node build only: resolves an emoji to a Twemoji SVG data-URI from disk. |
How to add a new layout engine
- Create
src/adapters/my-engine.tsimplementing theTechniqueinterface. - Add one entry to
src/application/registry.ts— bothREGISTRYand theEngineKeyunion. - Add the new kind (if applicable) to
KIND_ENGINEinsrc/application/router.ts. - Write tests in
src/adapters/my-engine.test.ts.
That is the only change required. The lab harness and all consumers pick up the new engine automatically.
Testing
npm test # run all tests
npm run test:watch # watch mode
npm run test:coverage # with coverage reportTests: 85 unit tests across domain validation, application routing/registry, the adapters (incl. the asset provider + browser provider), and the architecture guard.
