@knowvah/dot-engine
v1.6.0
Published
Faithful TypeScript port of Graphviz (dot, neato, fdp, sfdp, circo, twopi, osage, patchwork) — no C: no native binary, no WASM. Oracle-verified against the C implementation, runs in the browser.
Maintainers
Readme
@knowvah/dot-engine
A faithful TypeScript port of Graphviz — the graph-visualization toolkit that originated at AT&T Bell Labs (a foundational technical report dates to 1991). It parses the DOT language, runs Graphviz's layout engines, and emits SVG. Ported line-by-line from the canonical C source; see Graphviz on Wikipedia for background.
The defining property: pure TypeScript — no C. No compiled Graphviz binary, no WASM build of it, no native dependencies.
It runs in a browser or in Node with zero external dependencies at runtime. The
goal is the closest achievable fidelity to the C implementation, which is treated
as the canonical specification (see CLAUDE.md). In practice the
dot engine is conformant with the C binary on the golden corpus: numeric
coordinates agree to a tight deterministic tolerance (±0.01) and non-numeric
content is exactly equal. This is the measured "match" bar, not a claim of
literal byte-for-byte SVG output — see Conformance for
the exact definition and the comparison code, and
known divergences for the documented exceptions.
Status: published and semver-stable from 1.0.0. The public API —
renderSvg,/api,/render— follows semver: a breaking change gets a major bump. The version line does not track C feature coverage, and the two are independent signals.The rendering surface is settled: parsing, all eight layout engines, SVG, and the
json/xdot/dot/plain/ imagemap text formats. It does not cover the whole C feature surface, and the remainder is out of scope rather than pending — the unported C areas stay inventoried in the port catalog so you can check a specific feature before depending on it. Thedotengine is the primary fidelity target. See Status & coverage below.
Why this exists
Existing ways to render DOT in a JS environment shell out to a Graphviz binary, a rendering server, or a WASM build — none of which run everywhere a browser does, and all of which add deployment friction. @knowvah/dot-engine removes that dependency entirely: the layout engine is TypeScript.
Install
npm i @knowvah/dot-engineShips as ESM bundles with TypeScript declarations, zero runtime dependencies.
Entry points: @knowvah/dot-engine (core), @knowvah/dot-engine/api (graph-building API),
@knowvah/dot-engine/render (renderers).
To build from source instead: clone, npm install, npm run build
(esbuild bundles + .d.ts declarations into dist/).
Quick start
import { renderSvg } from '@knowvah/dot-engine';
const dot = `
digraph {
a -> b;
b -> c;
a -> c;
}
`;
const svg = renderSvg(dot, 'dot');
console.log(svg); // <svg ...>...</svg>renderSvg(dotSource, engine) parses the DOT source, runs the named layout
engine, renders to SVG, and returns the SVG string. On failure it throws a
structured error — see Error handling.
Error handling
renderSvg throws on any failure; for a result-style alternative that never
throws, use tryRenderSvg:
import { tryRenderSvg } from '@knowvah/dot-engine';
const result = tryRenderSvg('digraph { a ->', 'dot');
if (result.svg) {
// success
} else {
const err = result.errors[0];
console.error(err.code, err.friendlyMessage, err.location);
// 'SYNTAX_UNEXPECTED_EOF' · 'The DOT source ended unexpectedly …' · { line, column, offset }
}A RenderResult is { svg } or { errors } (never both); errors holds at
most the first failure. Each entry is a plain, JSON-serializable GvError:
| Field | Meaning |
|-------|---------|
| type | 'syntax' · 'semantic' · 'render' |
| code | Stable machine key (an i18n key) — branch on this |
| message | Concise technical text |
| friendlyMessage | Approachable, non-localized English for end users |
| location? | { line, column, offset? } — the real error position |
| expected? | Parser expectation list, for syntax errors only |
The code values are a closed union: SYNTAX_ERROR, SYNTAX_UNEXPECTED_EOF,
EDGE_OP_DIRECTED_IN_UNDIRECTED, EDGE_OP_UNDIRECTED_IN_DIRECTED,
HTML_PARSE_ERROR, RENDER_ERROR, GENERIC_ERROR.
The throwing renderSvg raises the same structured values as real Error
subclasses — ParseError (syntax) and RenderError (render) — each carrying
code, type, friendlyMessage, and (for ParseError) location/expected.
Branch on .code/.type rather than instanceof per subclass.
Layout engines
All eight Graphviz layout engines are registered. Pass the name as the second
argument to renderSvg:
| Engine | Layout style |
|--------------|-----------------------------------------------|
| dot | Hierarchical / layered directed graphs |
| neato | Spring-model (Kamada–Kawai) |
| fdp | Force-directed |
| sfdp | Multiscale force-directed (large graphs) |
| circo | Circular |
| twopi | Radial |
| osage | Clustered |
| patchwork | Squarified treemap |
dot receives the most fidelity attention because the primary consumer is
DOT-centric. Per-engine coverage against the C source is tracked in the
port catalog.
Browser usage
The library uses no Node-only APIs and is safe to bundle for the browser. One caller-supplied hook may be required:
Image sizing. When a graph references external images (e.g.
node [image="foo.png"]), Graphviz needs each image's intrinsic dimensions. Because the library cannot read the filesystem, provide a sizer viasetImageSizer:import { setImageSizer } from '@knowvah/dot-engine'; setImageSizer((src) => ({ w: 64, h: 64 })); // return null if unknown
Text measurement
Layout needs to know how wide each label is. By default this uses a built-in, deterministic metric model — no font files, identical output on every platform. In the browser the library automatically measures with the page's own canvas (the same font the browser renders the SVG with).
For host-faithful Node measurement (real kerning/shaping, matching the local
fonts the SVG will be rendered with), install the optional canvas peer and wire
it once via setTextMeasurer:
import { setTextMeasurer, CanvasTextMeasurer } from '@knowvah/dot-engine';
import { createCanvas } from 'canvas'; // optional peer: `npm i canvas`
setTextMeasurer(new CanvasTextMeasurer(createCanvas(0, 0).getContext('2d')));Trade-off: the built-in model is reproducible across machines; the host-faithful path matches the rendering font but is platform-dependent (as native graphviz is). See the Text measurement guide for the full contract.
CanvasTextMeasurer accepts any 2D context that implements font +
measureText().width — the canvas package is one provider, not a requirement.
@napi-rs/canvas (prebuilt N-API binaries, no prebuild-install) works too:
import { createCanvas } from '@napi-rs/canvas';
setTextMeasurer(new CanvasTextMeasurer(createCanvas(0, 0).getContext('2d') as unknown as CanvasRenderingContext2D));Pick the engine you render with: the two libraries resolve system fonts
differently (measured on macOS: bold Helvetica and Times/Courier substitution
diverge by 4–14% between them, while regular-weight Helvetica/Arial agree to
<0.2%). "Host-faithful" means measuring with the same font stack that later
draws the text — mixing engines reintroduces the mismatch you opted in to avoid.
Security
Treat rendered output as attacker-controlled markup whenever the DOT source is
untrusted. The SVG and image-map strings this library produces embed graph
attribute values (labels, id, class, href/URL, image, stylesheet,
tooltips) directly. All such values are XML-escaped exactly as native Graphviz
does, so they cannot break out of an element or attribute — but, matching
upstream Graphviz, the library does not filter URL schemes or validate resource
origins. A DOT source you did not author can therefore contain:
href="javascript:…"/URL="javascript:…"on a node or edge (executes on click),image="…"(usershape) or an image-maphrefpointing at an arbitrary external origin,- a
stylesheet="…"referencing an external CSS origin.
This is deliberate — scheme/origin policy belongs to the page embedding the
output, not to the layout library. If you render untrusted DOT and embed the
result inline (innerHTML, dangerouslySetInnerHTML, an inline <svg>), apply
a Content-Security-Policy on the host page as the control point:
script-src(without'unsafe-inline') — neutralizesjavascript:hrefs and any inline event handlers,img-src— constrains<image>/ usershape origins,style-src— constrains thestylesheetprocessing instruction.
If you cannot set a CSP, sanitize the returned markup (e.g. DOMPurify with an SVG profile) before inserting it, or render from trusted DOT only.
Public API
// Primary entry point. Throws a structured GvError (ParseError / RenderError).
function renderSvg(dotSource: string, engine: string): string;
// Result-style entry point: returns { svg } or { errors: [GvError] }, never throws.
function tryRenderSvg(dotSource: string, engine: string): RenderResult;
// Structured error contract (see "Error handling").
interface GvError { type; code; message; friendlyMessage; location?; expected?; }
interface RenderResult { svg?: string; errors?: GvError[]; }
type GvErrorType = 'syntax' | 'semantic' | 'render';
type GvErrorCode = 'SYNTAX_ERROR' | 'SYNTAX_UNEXPECTED_EOF' | /* …7 total */ 'GENERIC_ERROR';
class ParseError extends Error implements GvError { /* type:'syntax' */ }
class RenderError extends Error implements GvError { /* type:'render' */ }
// Parse DOT into the in-memory graph model (without laying it out).
function parse(dotSource: string): Graph;
// Supply intrinsic dimensions for external image references (browser/Node).
function setImageSizer(sizer: ImageSizer | null): void;
type ImageSizer = (src: string) => { w: number; h: number } | null;
// Multi-format render + structured xdot draw-ops (from `@knowvah/dot-engine/render`,
// also re-exported from the root package).
function render(g: Graph, format: OutputFormat, opts?: { engine?: string }): string;
function getDrawOps(g: Graph, opts?: { engine?: string }): XdotOp[];
// Programmatic graph construction and computed-geometry readback (from
// `@knowvah/dot-engine/api`, also re-exported from the root package) — build a graph
// without writing DOT source, or read back node/edge/bbox coordinates after
// layout.
function createGraph(opts?: CreateGraphOptions): GvGraphBuilder;
function addEdge(g: Graph, tail: Node, head: Node, name?: string): Edge;
function getLayout(g: Graph, opts?: { yAxis?: 'up' | 'down' }): LayoutSnapshot;
// Lower-level orchestration, for callers that need engine/render control.
class GvcContext { /* register engines/renderers, layout, render */ }
function renderWithContext(ctx: GvcContext, graph: Graph, format: string): string;Most callers only need renderSvg (or tryRenderSvg for result-style error
handling). parse, GvcContext, and renderWithContext are exposed for
advanced use — e.g. inspecting the parsed model, or driving layout and
rendering as separate steps. createGraph/addEdge, getLayout, render,
and getDrawOps are the graph-building, geometry-readback, multi-format
render, and structured-draw-op surfaces respectively — see the
API guide for full
walkthroughs of each.
Development
npm test # run the test suite (vitest)
npm run coverage # run with coverage (v8)
npm run typecheck # tsc --noEmit, strict mode, zero errors required
npm run build # bundle to dist/index.jsThe test suite verifies port fidelity by comparing generated SVG against output
from the canonical C Graphviz. New behavior is pinned to the C source — see
CLAUDE.md for the porting rules and the
port catalog for status.
Status & coverage
- What works: parsing, all eight layout engines, SVG output, and the
intermediate
json/xdot/dot/plain/ imagemap text formats. - Conformance bar: a render is conformant when it matches the C oracle
within a ±0.01 deterministic tolerance (
dot,circo,twopi,osage,patchwork) or is characterized at a looser ±0.5 tolerance for the iterative force-directed engines (neato,fdp,sfdp) — never literal byte equality. Full definition: Conformance. - Current parity:
dotSVG 762/788 conformant (0 unaccepted tracked gaps — every remaining non-conformant graph is a documented, accepted divergence);dotxdot 761/761;patchworkxdot 762/762;osagexdot 755/762;circoxdot 752/762;twopixdot 746/762 (all deterministic, ±0.01).neato/fdp/sfdpare characterized at ±0.5 rather than gated at the deterministic bar, per the tolerance split above. These figures are a snapshot of one row each fromtest/corpus/PARITY.md, which is generated bytest/corpus/parity-report.tsand covers every engine × surface track (SVG, xdot, json, plain, imagemap); read it, or the docs-site parity pages, rather than this bullet for current counts. - What's tracked: every C algorithm and its port status is inventoried in
the port catalog. Items marked
[ ]there are genuinely unported — not footnotes, and not a roadmap either. Treat the catalog as the authoritative answer to "is feature X in?". - Known behavioral divergences from C (differences investigated,
root-caused, and deliberately not chased) are listed in
docs/known-divergences.md.
Known limitations
The feature surface is narrower than C's, by design. The C source defines completeness, and this port does not reach it. The uncovered areas are listed in the port catalog rather than hidden — check it before depending on a specific C feature. They are not a backlog; absence from the shipped surface is a scope decision, not a pending item.
Very large graphs are impractical to lay out at runtime. Graphs beyond roughly 10k nodes or a few MB of DOT source hit a scale ceiling — layout (mincross, ranking, spline routing) is superlinear. This is shared with upstream Graphviz, not a port-specific defect: on such inputs native
dot, the WASM builds (@hpcc-js/wasm-graphviz), and this engine all time out or run out of memory alike (see the large-source note intest/corpus/PERF.md). This engine does not leak — its per-render heap is flat; the limit is strictly graph size.For graphs at that scale, pre-render to SVG once at build time and serve the static
.svgrather than laying out in the browser on every view. The build-time site adapters in knowvah/dot-plugins (published on NPM) do exactly this — e.g.@knowvah/vitepress-plugin-dot,@knowvah/eleventy-plugin-dot,@knowvah/docusaurus-plugin-dot, and the framework-agnostic@knowvah/dot-markdown-it.
License
Eclipse Public License v2.0 (EPL-2.0),
matching upstream Graphviz. Every source file carries an
SPDX-License-Identifier: EPL-2.0 header.
