@hpcc-js/wasm-graphviz
Version:
hpcc-js - WASM Graphviz
1,230 lines (1,085 loc) • 67.4 kB
text/typescript
// @ts-expect-error importing from a wasm file is resolved via a custom esbuild plugin
import load, { reset } from "../../../build/packages/graphviz/graphvizlib.wasm";
import type { MainModule, CGraph as CGraphWasm, CSubgraph as CSubgraphWasm } from "../types/graphvizlib.js";
import type {
NodeDotAttr, NodeAttrs,
EdgeDotAttr, EdgeAttrs,
ClusterDotAttr, ClusterAttrs,
GraphDotAttr, GraphAttrs,
Format, Engine
} from "./types.ts";
type AttrValues<T> = Partial<{ [K in keyof T]: NonNullable<T[K]> }>;
function applyAttrs<T extends object>(
attrs: AttrValues<T> | undefined,
apply: (attr: string, value: string | number | boolean) => void
): void {
if (!attrs) {
return;
}
for (const [attr, value] of Object.entries(attrs)) {
if (value !== undefined) {
apply(attr, value as string | number | boolean);
}
}
}
/**
* An edge returned by graph-traversal methods.
*/
export interface EdgeInfo {
/** The tail (source) node id. */
tail: string;
/** The head (target) node id. */
head: string;
/**
* The cgraph edge key that distinguishes parallel edges.
* Typically `""` for single (non-keyed) edges.
*/
key: string;
}
/** @internal Parses a JSON string array returned by C++ traversal methods. */
function parseNames(json: string): string[] {
return JSON.parse(json) as string[];
}
/** @internal Parses a flat JSON triple array `[tail,head,key,...]` into EdgeInfo[]. */
function parseEdges(json: string): EdgeInfo[] {
const flat = JSON.parse(json) as string[];
const result: EdgeInfo[] = [];
for (let i = 0; i + 2 < flat.length; i += 3)
result.push({ tail: flat[i]!, head: flat[i + 1]!, key: flat[i + 2]! });
return result;
}
/** @internal Returns the C++ cgraph name for a subgraph id (always cluster-prefixed). */
function subgraphCppName(id: string): string {
return `cluster_${id}`;
}
/** @internal Strips the cluster_ prefix from a C++ subgraph name to recover the user-facing id. */
function subgraphIdFromCppName(name: string): string {
return name.startsWith("cluster_") ? name.slice("cluster_".length) : name;
}
/**
* Example: Passing a web hosted Image to GraphViz:
* ```ts
* import { Graphviz } from "@hpcc-js/wasm-graphviz";
*
* const graphviz = await Graphviz.load();
* const svg = graphviz.layout('digraph { a[image="https://.../image.png"]; }', "svg", "dot", {
* images: [{
* path: "https://.../image.png",
* width: "272px",
* height: "92px"
* }]
* });
* document.getElementById("placeholder").innerHTML = svg;
* ```
*/
export interface Image {
/**
* Full URL to image
*/
path: string;
width: string;
height: string;
}
export interface File {
path: string;
data: string;
}
export interface Options {
images?: Image[];
files?: File[];
yInvert?: boolean;
nop?: number;
}
function imageToFile(image: Image): File {
return {
path: image.path,
data: `<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<svg width="${image.width}" height="${image.height}"></svg>`
};
}
function imagesToFiles(images: Image[]) {
return images.map(imageToFile);
}
function createFiles(graphviz: any, _options?: Options) {
const options = {
images: [],
files: [],
..._options
};
[...options.files, ...imagesToFiles(options.images)].forEach(file => graphviz.createFile(file.path, file.data));
}
let g_graphviz: Promise<Graphviz> | undefined;
/**
* The type of graph to create.
* - `"directed"` – directed graph (`digraph`)
* - `"undirected"` – undirected graph (`graph`)
* - `"strict directed"` – directed graph that disallows parallel edges and self-loops
* - `"strict undirected"` – undirected graph that disallows parallel edges and self-loops
*/
export type GraphType = "directed" | "undirected" | "strict directed" | "strict undirected";
export interface GraphInit {
/** The graph id, used as the cgraph name and set as the `id` Graphviz attribute. */
id?: string;
type?: GraphType;
attrs?: AttrValues<GraphAttrs>;
htmlAttrs?: AttrValues<GraphAttrs>;
}
export interface NodeInit {
/** The node id, used as the cgraph node name and set as the `id` Graphviz attribute. */
id: string;
attrs?: AttrValues<NodeAttrs>;
htmlAttrs?: AttrValues<NodeAttrs>;
}
export interface EdgeInit {
tail: string;
head: string;
key?: string;
attrs?: AttrValues<EdgeAttrs>;
htmlAttrs?: AttrValues<EdgeAttrs>;
}
export interface SubgraphInit {
/**
* The subgraph id. The `cluster_` prefix is added automatically when
* communicating with the underlying cgraph library; users never need to
* include it. The id is also set as the `id` Graphviz attribute on the
* subgraph.
*/
id: string;
attrs?: AttrValues<ClusterAttrs>;
htmlAttrs?: AttrValues<ClusterAttrs>;
}
export type ClusterInit = SubgraphInit;
/**
* A subgraph (cluster) inside a {@link Graph}.
*
* Obtain via {@link Graph.addSubgraph} or {@link Subgraph.addSubgraph}. All mutation methods return `this`
* for chaining. **Call {@link Subgraph.delete} (or use the `using` keyword)
* when finished** to free the underlying WASM wrapper — the actual subgraph
* data is owned by the parent {@link Graph} and is freed with it.
*
* Every subgraph is automatically rendered as a cluster (the `cluster_` prefix
* is added to the id when communicating with the underlying cgraph library).
* The `id` Graphviz attribute on the subgraph is set to the user-supplied id
* (without the prefix):
*
* ```ts
* using cluster = graph.addSubgraph("0");
* cluster
* .setAttr("label", "My Cluster")
* .setAttr("style", "filled")
* .setAttr("color", "lightblue")
* .addEdge("a", "b");
* ```
*/
export class Subgraph {
private _sg: CSubgraphWasm;
/** @internal */
constructor(sg: CSubgraphWasm) {
this._sg = sg;
}
/**
* Create (or find) a node and add it to this subgraph. The node's `id`
* Graphviz attribute is automatically set to match `id`.
*/
addNode(id: string, attrs?: AttrValues<NodeAttrs>, htmlAttrs?: AttrValues<NodeAttrs>): this;
addNode(init: NodeInit): this;
addNode(idOrInit: string | NodeInit, nodeAttrs?: AttrValues<NodeAttrs>, nodeHtmlAttrs?: AttrValues<NodeAttrs>): this {
const init = typeof idOrInit === "string" ? { id: idOrInit, attrs: nodeAttrs, htmlAttrs: nodeHtmlAttrs } : idOrInit;
const { id, attrs, htmlAttrs } = init;
this._sg.addNode(id);
if (attrs?.id === undefined) {
this.setNodeAttr(id, "id", id);
}
applyAttrs<NodeAttrs>(attrs, (attr, value) => this.setNodeAttr(id, attr, value));
applyAttrs<NodeAttrs>(htmlAttrs, (attr, value) => this.setNodeHtmlAttr(id, attr, value));
return this;
}
/** Alias for {@link addNode}. */
addVertex(id: string, attrs?: AttrValues<NodeAttrs>, htmlAttrs?: AttrValues<NodeAttrs>): this;
addVertex(init: NodeInit): this;
addVertex(idOrInit: string | NodeInit, nodeAttrs?: AttrValues<NodeAttrs>, nodeHtmlAttrs?: AttrValues<NodeAttrs>): this {
return this.addNode(idOrInit as any, nodeAttrs, nodeHtmlAttrs);
}
/**
* Create an edge inside this subgraph. Both endpoints are created
* automatically if they do not already exist.
*/
addEdge(tail: string, head: string, attrs?: AttrValues<EdgeAttrs>, htmlAttrs?: AttrValues<EdgeAttrs>): this;
addEdge(tail: string, head: string, key: string, attrs?: AttrValues<EdgeAttrs>, htmlAttrs?: AttrValues<EdgeAttrs>): this;
addEdge(init: EdgeInit): this;
addEdge(tailOrInit: string | EdgeInit, head?: string, keyOrAttrs: string | AttrValues<EdgeAttrs> = "", attrsOrHtmlAttrs?: AttrValues<EdgeAttrs>, maybeHtmlAttrs?: AttrValues<EdgeAttrs>): this {
const init = typeof tailOrInit === "string"
? typeof keyOrAttrs === "string"
? { tail: tailOrInit, head: head!, key: keyOrAttrs, attrs: attrsOrHtmlAttrs, htmlAttrs: maybeHtmlAttrs }
: { tail: tailOrInit, head: head!, attrs: keyOrAttrs, htmlAttrs: attrsOrHtmlAttrs }
: tailOrInit;
const { tail, head: resolvedHead, key: resolvedKey = "", attrs, htmlAttrs } = init;
this._sg.addEdge(tail, resolvedHead, resolvedKey);
applyAttrs<EdgeAttrs>(attrs, (attr, value) => this.setEdgeAttr(tail, resolvedHead, resolvedKey, attr, value));
applyAttrs<EdgeAttrs>(htmlAttrs, (attr, value) => this.setEdgeHtmlAttr(tail, resolvedHead, resolvedKey, attr, value));
return this;
}
/**
* Create (or return an existing) subgraph under this subgraph. The
* `cluster_` prefix is added to `id` internally so that layout engines
* render it as a bounded cluster. The subgraph's `id` Graphviz attribute
* is set to the user-supplied `id` (without the prefix).
*/
addSubgraph(id: string, attrs?: AttrValues<ClusterAttrs>, htmlAttrs?: AttrValues<ClusterAttrs>): Subgraph;
addSubgraph(init: SubgraphInit): Subgraph;
addSubgraph(idOrInit: string | SubgraphInit, attrs?: AttrValues<ClusterAttrs>, htmlAttrs?: AttrValues<ClusterAttrs>): Subgraph {
const init = typeof idOrInit === "string" ? { id: idOrInit, attrs, htmlAttrs } : idOrInit;
// addSubgraph only returns null when the internal subgraph pointer is null,
// which cannot happen while this Subgraph instance is alive.
return new Subgraph(this._sg.addSubgraph(subgraphCppName(init.id))!).applyInit(init);
}
applyInit(init: SubgraphInit): this {
this.setAttr("id", init.id);
applyAttrs<ClusterAttrs>(init.attrs, (attr, value) => this.setAttr(attr, value));
applyAttrs<ClusterAttrs>(init.htmlAttrs, (attr, value) => this.setHtmlAttr(attr, value));
return this;
}
/**
* Remove a node from this subgraph only. The node (and any edges
* connecting it) remains in the root graph and all other subgraphs.
* No-op if the node is not in this subgraph.
*/
removeNode(id: string): this {
this._sg.removeNode(id);
return this;
}
/** Alias for {@link removeNode}. */
removeVertex(id: string): this {
return this.removeNode(id);
}
/**
* Remove a single edge from this subgraph only. The edge remains in the
* root graph and all other subgraphs. No-op if the edge is not present.
*/
removeEdge(tail: string, head: string, key: string = ""): this {
this._sg.removeEdge(tail, head, key);
return this;
}
/**
* Set an attribute on the subgraph itself (e.g. `"label"`, `"style"`,
* `"color"`, `"bgcolor"`). See the official Graphviz
* [attribute reference](https://graphviz.org/docs/attrs/) for supported
* attributes and values.
*
* When `attr` is a known cluster attribute the value type is inferred
* automatically. A generic `string | number | boolean` fallback is
* provided for custom or less-common attributes. Omit `value` to reset
* the attribute to Graphviz's default empty value. `defaultValue` is
* passed to Graphviz as the default for this attribute if it has not
* already been declared.
*/
setAttr<K extends ClusterDotAttr>(attr: K, value?: NonNullable<ClusterAttrs[K]>, defaultValue?: string | number | boolean): this;
setAttr(attr: string, value?: string | number | boolean, defaultValue?: string | number | boolean): this;
setAttr(attr: string, value: unknown = "", defaultValue?: unknown): this {
if (defaultValue === undefined) {
this._sg.setAttr(attr, String(value));
} else {
this._sg.setAttr(attr, String(value), String(defaultValue));
}
return this;
}
setHtmlAttr<K extends ClusterDotAttr>(attr: K, value: NonNullable<ClusterAttrs[K]>, defaultValue?: string | number | boolean): this;
setHtmlAttr(attr: string, value: string | number | boolean, defaultValue?: string | number | boolean): this;
setHtmlAttr(attr: string, value: unknown, defaultValue?: unknown): this {
if (defaultValue === undefined) {
this._sg.setHtmlAttr(attr, String(value));
} else {
this._sg.setHtmlAttr(attr, String(value), String(defaultValue));
}
return this;
}
setDefaultAttr<K extends ClusterDotAttr>(attr: K, value: NonNullable<ClusterAttrs[K]>): this;
setDefaultAttr(attr: string, value: string | number | boolean): this;
setDefaultAttr(attr: string, value: unknown): this {
this._sg.setDefaultAttr(attr, String(value));
return this;
}
setDefaultHtmlAttr<K extends ClusterDotAttr>(attr: K, value: NonNullable<ClusterAttrs[K]>): this;
setDefaultHtmlAttr(attr: string, value: string | number | boolean): this;
setDefaultHtmlAttr(attr: string, value: unknown): this {
this._sg.setDefaultHtmlAttr(attr, String(value));
return this;
}
/**
* Clear a subgraph-level attribute by resetting it to its default (empty)
* value. Equivalent to `setAttr(attr, "")`.
*/
removeAttr(attr: string): this {
return this.setAttr(attr, "");
}
/**
* Set an attribute on a node inside this subgraph.
*
* When `attr` is a known node attribute the value type is inferred
* automatically. A generic `string | number | boolean` fallback is
* provided for custom attributes. Omit `value` to reset the attribute to
* Graphviz's default empty value. `defaultValue` is passed to Graphviz as
* the default for this attribute if it has not already been declared.
*/
setNodeAttr<K extends NodeDotAttr>(node: string, attr: K, value?: NonNullable<NodeAttrs[K]>, defaultValue?: string | number | boolean): this;
setNodeAttr(node: string, attr: string, value?: string | number | boolean, defaultValue?: string | number | boolean): this;
setNodeAttr(node: string, attr: string, value: unknown = "", defaultValue?: unknown): this {
if (defaultValue === undefined) {
this._sg.setNodeAttr(node, attr, String(value));
} else {
this._sg.setNodeAttr(node, attr, String(value), String(defaultValue));
}
return this;
}
/** Alias for {@link setNodeAttr}. */
setVertexAttr<K extends NodeDotAttr>(node: string, attr: K, value?: NonNullable<NodeAttrs[K]>, defaultValue?: string | number | boolean): this;
setVertexAttr(node: string, attr: string, value?: string | number | boolean, defaultValue?: string | number | boolean): this;
setVertexAttr(node: string, attr: string, value: unknown = "", defaultValue?: unknown): this {
return this.setNodeAttr(node, attr, value as any, defaultValue as any);
}
setNodeHtmlAttr<K extends NodeDotAttr>(node: string, attr: K, value: NonNullable<NodeAttrs[K]>, defaultValue?: string | number | boolean): this;
setNodeHtmlAttr(node: string, attr: string, value: string | number | boolean, defaultValue?: string | number | boolean): this;
setNodeHtmlAttr(node: string, attr: string, value: unknown, defaultValue?: unknown): this {
if (defaultValue === undefined) {
this._sg.setNodeHtmlAttr(node, attr, String(value));
} else {
this._sg.setNodeHtmlAttr(node, attr, String(value), String(defaultValue));
}
return this;
}
/** Alias for {@link setNodeHtmlAttr}. */
setVertexHtmlAttr<K extends NodeDotAttr>(node: string, attr: K, value: NonNullable<NodeAttrs[K]>, defaultValue?: string | number | boolean): this;
setVertexHtmlAttr(node: string, attr: string, value: string | number | boolean, defaultValue?: string | number | boolean): this;
setVertexHtmlAttr(node: string, attr: string, value: unknown, defaultValue?: unknown): this {
return this.setNodeHtmlAttr(node, attr, value as any, defaultValue as any);
}
setDefaultNodeAttr<K extends NodeDotAttr>(attr: K, value: NonNullable<NodeAttrs[K]>): this;
setDefaultNodeAttr(attr: string, value: string | number | boolean): this;
setDefaultNodeAttr(attr: string, value: unknown): this {
this._sg.setDefaultNodeAttr(attr, String(value));
return this;
}
/** Alias for {@link setDefaultNodeAttr}. */
setDefaultVertexAttr<K extends NodeDotAttr>(attr: K, value: NonNullable<NodeAttrs[K]>): this;
setDefaultVertexAttr(attr: string, value: string | number | boolean): this;
setDefaultVertexAttr(attr: string, value: unknown): this {
return this.setDefaultNodeAttr(attr, value as any);
}
setDefaultNodeHtmlAttr<K extends NodeDotAttr>(attr: K, value: NonNullable<NodeAttrs[K]>): this;
setDefaultNodeHtmlAttr(attr: string, value: string | number | boolean): this;
setDefaultNodeHtmlAttr(attr: string, value: unknown): this {
this._sg.setDefaultNodeHtmlAttr(attr, String(value));
return this;
}
/** Alias for {@link setDefaultNodeHtmlAttr}. */
setDefaultVertexHtmlAttr<K extends NodeDotAttr>(attr: K, value: NonNullable<NodeAttrs[K]>): this;
setDefaultVertexHtmlAttr(attr: string, value: string | number | boolean): this;
setDefaultVertexHtmlAttr(attr: string, value: unknown): this {
return this.setDefaultNodeHtmlAttr(attr, value as any);
}
/**
* Clear a node attribute inside this subgraph by resetting it to its
* default (empty) value. Equivalent to `setNodeAttr(node, attr, "")`.
*/
removeNodeAttr(node: string, attr: string): this {
return this.setNodeAttr(node, attr, "");
}
/** Alias for {@link removeNodeAttr}. */
removeVertexAttr(node: string, attr: string): this {
return this.removeNodeAttr(node, attr);
}
/**
* Set an attribute on an edge inside this subgraph (identified by
* `tail`, `head`, and `key`).
*
* When `attr` is a known edge attribute the value type is inferred
* automatically. A generic `string | number | boolean` fallback is
* provided for custom attributes. Omit `value` to reset the attribute to
* Graphviz's default empty value. `defaultValue` is passed to Graphviz as
* the default for this attribute if it has not already been declared.
*/
setEdgeAttr<K extends EdgeDotAttr>(tail: string, head: string, key: string, attr: K, value?: NonNullable<EdgeAttrs[K]>, defaultValue?: string | number | boolean): this;
setEdgeAttr(tail: string, head: string, key: string, attr: string, value?: string | number | boolean, defaultValue?: string | number | boolean): this;
setEdgeAttr(tail: string, head: string, key: string, attr: string, value: unknown = "", defaultValue?: unknown): this {
if (defaultValue === undefined) {
this._sg.setEdgeAttr(tail, head, key, attr, String(value));
} else {
this._sg.setEdgeAttr(tail, head, key, attr, String(value), String(defaultValue));
}
return this;
}
setEdgeHtmlAttr<K extends EdgeDotAttr>(tail: string, head: string, key: string, attr: K, value: NonNullable<EdgeAttrs[K]>, defaultValue?: string | number | boolean): this;
setEdgeHtmlAttr(tail: string, head: string, key: string, attr: string, value: string | number | boolean, defaultValue?: string | number | boolean): this;
setEdgeHtmlAttr(tail: string, head: string, key: string, attr: string, value: unknown, defaultValue?: unknown): this {
if (defaultValue === undefined) {
this._sg.setEdgeHtmlAttr(tail, head, key, attr, String(value));
} else {
this._sg.setEdgeHtmlAttr(tail, head, key, attr, String(value), String(defaultValue));
}
return this;
}
setDefaultEdgeAttr<K extends EdgeDotAttr>(attr: K, value: NonNullable<EdgeAttrs[K]>): this;
setDefaultEdgeAttr(attr: string, value: string | number | boolean): this;
setDefaultEdgeAttr(attr: string, value: unknown): this {
this._sg.setDefaultEdgeAttr(attr, String(value));
return this;
}
setDefaultEdgeHtmlAttr<K extends EdgeDotAttr>(attr: K, value: NonNullable<EdgeAttrs[K]>): this;
setDefaultEdgeHtmlAttr(attr: string, value: string | number | boolean): this;
setDefaultEdgeHtmlAttr(attr: string, value: unknown): this {
this._sg.setDefaultEdgeHtmlAttr(attr, String(value));
return this;
}
/**
* Clear an edge attribute inside this subgraph by resetting it to its
* default (empty) value. Equivalent to `setEdgeAttr(tail, head, key, attr, "")`.
*/
removeEdgeAttr(tail: string, head: string, key: string, attr: string): this {
return this.setEdgeAttr(tail, head, key, attr, "");
}
// ---- Existence checks -----------------------------------------------
/**
* Returns `true` if a node with the given id exists in this subgraph.
*/
hasNode(id: string): boolean {
return this._sg.hasNode(id);
}
/** Alias for {@link hasNode}. */
hasVertex(id: string): boolean {
return this.hasNode(id);
}
/**
* Returns `true` if an edge from `tail` to `head` exists in this subgraph.
* Pass `key` to check for a specific parallel edge; omit (or pass `""`) to
* check for any edge between the two nodes.
*/
hasEdge(tail: string, head: string, key: string = ""): boolean {
return this._sg.hasEdge(tail, head, key);
}
// ---- Count queries --------------------------------------------------
/** Returns the number of nodes in this subgraph. */
nodeCount(): number { return this._sg.nodeCount(); }
/** Alias for {@link nodeCount}. */
vertexCount(): number { return this.nodeCount(); }
/** Returns the number of edges in this subgraph. */
edgeCount(): number { return this._sg.edgeCount(); }
/** Returns the degree of the node with the given id in this subgraph. */
nodeDegree(node: string, inDegree: number = 1, outDegree: number = 1): number {
return this._sg.nodeDegree(node, inDegree, outDegree);
}
/** Alias for {@link nodeDegree}. */
vertexDegree(node: string, inDegree: number = 1, outDegree: number = 1): number {
return this.nodeDegree(node, inDegree, outDegree);
}
// ---- Attribute reading ----------------------------------------------
/**
* Returns the current value of a subgraph-level attribute, or `""` if
* the attribute has not been set.
*/
getAttr(attr: string): string {
return this._sg.getAttr(attr);
}
/**
* Returns the current value of the named attribute on a node in this
* subgraph, or `""` if the node or attribute does not exist.
*/
getNodeAttr(node: string, attr: string): string {
return this._sg.getNodeAttr(node, attr);
}
/** Alias for {@link getNodeAttr}. */
getVertexAttr(node: string, attr: string): string {
return this.getNodeAttr(node, attr);
}
/**
* Returns the current value of the named attribute on an edge in this
* subgraph, or `""` if the edge or attribute does not exist.
* Pass `key = ""` for the first (or only) edge between `tail` and `head`.
*/
getEdgeAttr(tail: string, head: string, key: string, attr: string): string {
return this._sg.getEdgeAttr(tail, head, key, attr);
}
// ---- Graph traversal ------------------------------------------------
/**
* Returns the ids of all nodes in this subgraph (in internal iteration
* order).
*/
nodeNames(): string[] {
return parseNames(this._sg.nodeNames());
}
/** Alias for {@link nodeNames}. */
vertexNames(): string[] {
return this.nodeNames();
}
/**
* Returns all edges in this subgraph. Each unique edge is listed exactly
* once.
*/
edges(): EdgeInfo[] {
return parseEdges(this._sg.edges());
}
/**
* Returns the out-edges of the node with the given id in this subgraph.
* Returns `[]` if the node does not exist.
*/
outEdges(node: string): EdgeInfo[] {
return parseEdges(this._sg.outEdges(node));
}
/**
* Returns the in-edges of the node with the given id in this subgraph.
* Returns `[]` if the node does not exist.
*/
inEdges(node: string): EdgeInfo[] {
return parseEdges(this._sg.inEdges(node));
}
/**
* Returns all edges incident to the node with the given id in this
* subgraph (both in and out). Returns `[]` if the node does not exist.
*/
nodeEdges(node: string): EdgeInfo[] {
return parseEdges(this._sg.nodeEdges(node));
}
/**
* Release the WASM wrapper. The subgraph data itself is freed when the
* parent {@link Graph} is deleted.
*/
delete(): void {
this._sg.delete();
}
/** @internal – supports the `using` keyword (explicit resource management). */
[Symbol.dispose](): void {
this.delete();
}
}
/**
* A programmatic graph builder backed by the cgraph library.
*
* Obtain an instance via {@link Graphviz.createGraph} and call
* {@link Graph.toDot} to serialise to a DOT string that can be passed to
* {@link Graphviz.layout} (or any of its convenience wrappers).
*
* **Always call {@link Graph.delete} (or use the `using` keyword) when
* finished** to release the underlying WASM memory.
*
* ```ts
* const graphviz = await Graphviz.load();
*
* using graph = graphviz.createGraph("G");
* graph.addNode("a");
* graph.addNode("b");
* graph.addEdge("a", "b");
* graph.setNodeAttr("a", "color", "red");
* graph.setEdgeAttr("a", "b", "", "label", "hello");
* graph.setGraphAttr("rankdir", "LR");
*
* const svg = graph.layout(); // render without a DOT round-trip
* ```
*/
export class Graph {
private _graph: CGraphWasm;
private _module: MainModule;
/** @internal */
constructor(g: CGraphWasm, module: MainModule) {
this._graph = g;
this._module = module;
}
private ensureDefaultFonts(includeNodeAndEdgeDefaults: boolean): void {
if (this.getGraphAttr("fontname") === "") {
this.setGraphAttr("fontname", "Arial");
}
this.setDefaultGraphAttr("fontname", "Arial");
if (includeNodeAndEdgeDefaults) {
this.setDefaultNodeAttr("fontname", "Arial");
this.setDefaultEdgeAttr("fontname", "Arial");
}
}
/**
* Create a node with the given `id`. If a node with this id already
* exists it is returned unchanged. The node's `id` Graphviz attribute is
* automatically set to match.
*/
addNode(id: string, attrs?: AttrValues<NodeAttrs>, htmlAttrs?: AttrValues<NodeAttrs>): this;
addNode(init: NodeInit): this;
addNode(idOrInit: string | NodeInit, nodeAttrs?: AttrValues<NodeAttrs>, nodeHtmlAttrs?: AttrValues<NodeAttrs>): this {
const init = typeof idOrInit === "string" ? { id: idOrInit, attrs: nodeAttrs, htmlAttrs: nodeHtmlAttrs } : idOrInit;
const { id, attrs, htmlAttrs } = init;
this._graph.addNode(id);
if (attrs?.id === undefined) {
this.setNodeAttr(id, "id", id);
}
applyAttrs<NodeAttrs>(attrs, (attr, value) => this.setNodeAttr(id, attr, value));
applyAttrs<NodeAttrs>(htmlAttrs, (attr, value) => this.setNodeHtmlAttr(id, attr, value));
return this;
}
/** Alias for {@link addNode}. */
addVertex(id: string, attrs?: AttrValues<NodeAttrs>, htmlAttrs?: AttrValues<NodeAttrs>): this;
addVertex(init: NodeInit): this;
addVertex(idOrInit: string | NodeInit, nodeAttrs?: AttrValues<NodeAttrs>, nodeHtmlAttrs?: AttrValues<NodeAttrs>): this {
return this.addNode(idOrInit as any, nodeAttrs, nodeHtmlAttrs);
}
/**
* Create an edge from `tail` to `head`. Both nodes are created
* automatically if they do not already exist. `key` distinguishes
* parallel edges between the same pair of nodes; omit (or pass `""`) for
* an anonymous edge.
*/
addEdge(tail: string, head: string, attrs?: AttrValues<EdgeAttrs>, htmlAttrs?: AttrValues<EdgeAttrs>): this;
addEdge(tail: string, head: string, key: string, attrs?: AttrValues<EdgeAttrs>, htmlAttrs?: AttrValues<EdgeAttrs>): this;
addEdge(init: EdgeInit): this;
addEdge(tailOrInit: string | EdgeInit, head?: string, keyOrAttrs: string | AttrValues<EdgeAttrs> = "", attrsOrHtmlAttrs?: AttrValues<EdgeAttrs>, maybeHtmlAttrs?: AttrValues<EdgeAttrs>): this {
const init = typeof tailOrInit === "string"
? typeof keyOrAttrs === "string"
? { tail: tailOrInit, head: head!, key: keyOrAttrs, attrs: attrsOrHtmlAttrs, htmlAttrs: maybeHtmlAttrs }
: { tail: tailOrInit, head: head!, attrs: keyOrAttrs, htmlAttrs: attrsOrHtmlAttrs }
: tailOrInit;
const { tail, head: resolvedHead, key: resolvedKey = "", attrs, htmlAttrs } = init;
this._graph.addEdge(tail, resolvedHead, resolvedKey);
applyAttrs<EdgeAttrs>(attrs, (attr, value) => this.setEdgeAttr(tail, resolvedHead, resolvedKey, attr, value));
applyAttrs<EdgeAttrs>(htmlAttrs, (attr, value) => this.setEdgeHtmlAttr(tail, resolvedHead, resolvedKey, attr, value));
return this;
}
applyInit(init: GraphInit): this {
this.ensureDefaultFonts(true);
if (init.id !== undefined) {
this.setGraphAttr("id", init.id);
}
applyAttrs<GraphAttrs>(init.attrs, (attr, value) => this.setGraphAttr(attr, value));
applyAttrs<GraphAttrs>(init.htmlAttrs, (attr, value) => this.setGraphHtmlAttr(attr, value));
return this;
}
/**
* Replace this graph with one parsed from DOT source using Graphviz cgraph
* reading support.
*/
read(dotSource: string): this {
if (!this._graph.read(dotSource)) {
throw new Error("Invalid DOT source");
}
this.ensureDefaultFonts(false);
return this;
}
/**
* Set a graph-level attribute (e.g. `"rankdir"`, `"label"`, `"bgcolor"`).
* See the official Graphviz [attribute reference](https://graphviz.org/docs/attrs/)
* for supported attributes and values.
*
* When `attr` is a known graph attribute the value type is inferred
* automatically. A generic `string | number | boolean` fallback is
* provided for custom or less-common attributes. Omit `value` to reset
* the attribute to Graphviz's default empty value. `defaultValue` is
* passed to Graphviz as the default for this attribute if it has not
* already been declared.
*/
setGraphAttr<K extends GraphDotAttr>(attr: K, value?: NonNullable<GraphAttrs[K]>, defaultValue?: string | number | boolean): this;
setGraphAttr(attr: string, value?: string | number | boolean, defaultValue?: string | number | boolean): this;
setGraphAttr(attr: string, value: unknown = "", defaultValue?: unknown): this {
if (defaultValue === undefined) {
this._graph.setGraphAttr(attr, String(value));
} else {
this._graph.setGraphAttr(attr, String(value), String(defaultValue));
}
return this;
}
setGraphHtmlAttr<K extends GraphDotAttr>(attr: K, value: NonNullable<GraphAttrs[K]>, defaultValue?: string | number | boolean): this;
setGraphHtmlAttr(attr: string, value: string | number | boolean, defaultValue?: string | number | boolean): this;
setGraphHtmlAttr(attr: string, value: unknown, defaultValue?: unknown): this {
if (defaultValue === undefined) {
this._graph.setGraphHtmlAttr(attr, String(value));
} else {
this._graph.setGraphHtmlAttr(attr, String(value), String(defaultValue));
}
return this;
}
setDefaultGraphAttr<K extends GraphDotAttr>(attr: K, value: NonNullable<GraphAttrs[K]>): this;
setDefaultGraphAttr(attr: string, value: string | number | boolean): this;
setDefaultGraphAttr(attr: string, value: unknown): this {
this._graph.setDefaultGraphAttr(attr, String(value));
return this;
}
setDefaultGraphHtmlAttr<K extends GraphDotAttr>(attr: K, value: NonNullable<GraphAttrs[K]>): this;
setDefaultGraphHtmlAttr(attr: string, value: string | number | boolean): this;
setDefaultGraphHtmlAttr(attr: string, value: unknown): this {
this._graph.setDefaultGraphHtmlAttr(attr, String(value));
return this;
}
/**
* Set an attribute on a named node (e.g. `"color"`, `"label"`, `"shape"`).
* The node must exist; call {@link addNode} first if needed.
*
* When `attr` is a known node attribute the value type is inferred
* automatically. A generic `string | number | boolean` fallback is
* provided for custom attributes. Omit `value` to reset the attribute to
* Graphviz's default empty value. `defaultValue` is passed to Graphviz as
* the default for this attribute if it has not already been declared.
*/
setNodeAttr<K extends NodeDotAttr>(node: string, attr: K, value?: NonNullable<NodeAttrs[K]>, defaultValue?: string | number | boolean): this;
setNodeAttr(node: string, attr: string, value?: string | number | boolean, defaultValue?: string | number | boolean): this;
setNodeAttr(node: string, attr: string, value: unknown = "", defaultValue?: unknown): this {
if (defaultValue === undefined) {
this._graph.setNodeAttr(node, attr, String(value));
} else {
this._graph.setNodeAttr(node, attr, String(value), String(defaultValue));
}
return this;
}
/** Alias for {@link setNodeAttr}. */
setVertexAttr<K extends NodeDotAttr>(node: string, attr: K, value?: NonNullable<NodeAttrs[K]>, defaultValue?: string | number | boolean): this;
setVertexAttr(node: string, attr: string, value?: string | number | boolean, defaultValue?: string | number | boolean): this;
setVertexAttr(node: string, attr: string, value: unknown = "", defaultValue?: unknown): this {
return this.setNodeAttr(node, attr, value as any, defaultValue as any);
}
setNodeHtmlAttr<K extends NodeDotAttr>(node: string, attr: K, value: NonNullable<NodeAttrs[K]>, defaultValue?: string | number | boolean): this;
setNodeHtmlAttr(node: string, attr: string, value: string | number | boolean, defaultValue?: string | number | boolean): this;
setNodeHtmlAttr(node: string, attr: string, value: unknown, defaultValue?: unknown): this {
if (defaultValue === undefined) {
this._graph.setNodeHtmlAttr(node, attr, String(value));
} else {
this._graph.setNodeHtmlAttr(node, attr, String(value), String(defaultValue));
}
return this;
}
/** Alias for {@link setNodeHtmlAttr}. */
setVertexHtmlAttr<K extends NodeDotAttr>(node: string, attr: K, value: NonNullable<NodeAttrs[K]>, defaultValue?: string | number | boolean): this;
setVertexHtmlAttr(node: string, attr: string, value: string | number | boolean, defaultValue?: string | number | boolean): this;
setVertexHtmlAttr(node: string, attr: string, value: unknown, defaultValue?: unknown): this {
return this.setNodeHtmlAttr(node, attr, value as any, defaultValue as any);
}
setDefaultNodeAttr<K extends NodeDotAttr>(attr: K, value: NonNullable<NodeAttrs[K]>): this;
setDefaultNodeAttr(attr: string, value: string | number | boolean): this;
setDefaultNodeAttr(attr: string, value: unknown): this {
this._graph.setDefaultNodeAttr(attr, String(value));
return this;
}
/** Alias for {@link setDefaultNodeAttr}. */
setDefaultVertexAttr<K extends NodeDotAttr>(attr: K, value: NonNullable<NodeAttrs[K]>): this;
setDefaultVertexAttr(attr: string, value: string | number | boolean): this;
setDefaultVertexAttr(attr: string, value: unknown): this {
return this.setDefaultNodeAttr(attr, value as any);
}
setDefaultNodeHtmlAttr<K extends NodeDotAttr>(attr: K, value: NonNullable<NodeAttrs[K]>): this;
setDefaultNodeHtmlAttr(attr: string, value: string | number | boolean): this;
setDefaultNodeHtmlAttr(attr: string, value: unknown): this {
this._graph.setDefaultNodeHtmlAttr(attr, String(value));
return this;
}
/** Alias for {@link setDefaultNodeHtmlAttr}. */
setDefaultVertexHtmlAttr<K extends NodeDotAttr>(attr: K, value: NonNullable<NodeAttrs[K]>): this;
setDefaultVertexHtmlAttr(attr: string, value: string | number | boolean): this;
setDefaultVertexHtmlAttr(attr: string, value: unknown): this {
return this.setDefaultNodeHtmlAttr(attr, value as any);
}
/**
* Set an attribute on an edge identified by `(tail, head, key)`.
* Use the same `key` that was passed to {@link addEdge}; pass `""` for
* anonymous edges.
*
* When `attr` is a known edge attribute the value type is inferred
* automatically. A generic `string | number | boolean` fallback is
* provided for custom attributes. Omit `value` to reset the attribute to
* Graphviz's default empty value. `defaultValue` is passed to Graphviz as
* the default for this attribute if it has not already been declared.
*/
setEdgeAttr<K extends EdgeDotAttr>(tail: string, head: string, key: string, attr: K, value?: NonNullable<EdgeAttrs[K]>, defaultValue?: string | number | boolean): this;
setEdgeAttr(tail: string, head: string, key: string, attr: string, value?: string | number | boolean, defaultValue?: string | number | boolean): this;
setEdgeAttr(tail: string, head: string, key: string, attr: string, value: unknown = "", defaultValue?: unknown): this {
if (defaultValue === undefined) {
this._graph.setEdgeAttr(tail, head, key, attr, String(value));
} else {
this._graph.setEdgeAttr(tail, head, key, attr, String(value), String(defaultValue));
}
return this;
}
setEdgeHtmlAttr<K extends EdgeDotAttr>(tail: string, head: string, key: string, attr: K, value: NonNullable<EdgeAttrs[K]>, defaultValue?: string | number | boolean): this;
setEdgeHtmlAttr(tail: string, head: string, key: string, attr: string, value: string | number | boolean, defaultValue?: string | number | boolean): this;
setEdgeHtmlAttr(tail: string, head: string, key: string, attr: string, value: unknown, defaultValue?: unknown): this {
if (defaultValue === undefined) {
this._graph.setEdgeHtmlAttr(tail, head, key, attr, String(value));
} else {
this._graph.setEdgeHtmlAttr(tail, head, key, attr, String(value), String(defaultValue));
}
return this;
}
setDefaultEdgeAttr<K extends EdgeDotAttr>(attr: K, value: NonNullable<EdgeAttrs[K]>): this;
setDefaultEdgeAttr(attr: string, value: string | number | boolean): this;
setDefaultEdgeAttr(attr: string, value: unknown): this {
this._graph.setDefaultEdgeAttr(attr, String(value));
return this;
}
setDefaultEdgeHtmlAttr<K extends EdgeDotAttr>(attr: K, value: NonNullable<EdgeAttrs[K]>): this;
setDefaultEdgeHtmlAttr(attr: string, value: string | number | boolean): this;
setDefaultEdgeHtmlAttr(attr: string, value: unknown): this {
this._graph.setDefaultEdgeHtmlAttr(attr, String(value));
return this;
}
/**
* Remove a node and all its edges from the graph. Removing from the root
* graph also removes the node from every subgraph.
* No-op if the node does not exist.
*/
removeNode(id: string): this {
this._graph.removeNode(id);
return this;
}
/** Alias for {@link removeNode}. */
removeVertex(id: string): this {
return this.removeNode(id);
}
/**
* Remove a single edge identified by `(tail, head, key)`.
* Pass `""` for `key` on anonymous edges.
* No-op if the edge does not exist.
*/
removeEdge(tail: string, head: string, key: string = ""): this {
this._graph.removeEdge(tail, head, key);
return this;
}
/**
* Dissolve the cluster boundary for the subgraph with the given `id`.
* Nodes and edges that belonged to the subgraph remain in the parent graph.
* No-op if no subgraph with that id exists.
*/
removeSubgraph(id: string): this {
this._graph.removeSubgraph(subgraphCppName(id));
return this;
}
/**
* Clear a graph-level attribute by resetting it to its default (empty)
* value. Equivalent to `setGraphAttr(attr, "")`.
*/
removeGraphAttr(attr: string): this {
return this.setGraphAttr(attr, "");
}
/**
* Clear a node attribute by resetting it to its default (empty) value.
* Equivalent to `setNodeAttr(node, attr, "")`.
*/
removeNodeAttr(node: string, attr: string): this {
return this.setNodeAttr(node, attr, "");
}
/** Alias for {@link removeNodeAttr}. */
removeVertexAttr(node: string, attr: string): this {
return this.removeNodeAttr(node, attr);
}
/**
* Clear an edge attribute by resetting it to its default (empty) value.
* Equivalent to `setEdgeAttr(tail, head, key, attr, "")`.
*/
removeEdgeAttr(tail: string, head: string, key: string, attr: string): this {
return this.setEdgeAttr(tail, head, key, attr, "");
}
// ---- Existence checks -----------------------------------------------
/**
* Returns `true` if a node with the given id exists in the graph.
*/
hasNode(id: string): boolean {
return this._graph.hasNode(id);
}
/** Alias for {@link hasNode}. */
hasVertex(id: string): boolean {
return this.hasNode(id);
}
/**
* Returns `true` if an edge from `tail` to `head` exists in the graph.
* Pass `key` to check for a specific parallel edge; omit (or pass `""`) to
* check for any edge between the two nodes.
*/
hasEdge(tail: string, head: string, key: string = ""): boolean {
return this._graph.hasEdge(tail, head, key);
}
/**
* Returns `true` if a subgraph with the given id exists.
*/
hasSubgraph(id: string): boolean {
return this._graph.hasSubgraph(subgraphCppName(id));
}
// ---- Count queries --------------------------------------------------
/** Returns the number of nodes in the graph. */
nodeCount(): number { return this._graph.nodeCount(); }
/** Alias for {@link nodeCount}. */
vertexCount(): number { return this.nodeCount(); }
/** Returns the number of edges in the graph. */
edgeCount(): number { return this._graph.edgeCount(); }
/** Returns the number of direct subgraphs of this graph. */
subgraphCount(): number { return this._graph.subgraphCount(); }
/** Returns the degree of the node with the given id in the graph. */
nodeDegree(node: string, inDegree: number = 1, outDegree: number = 1): number {
return this._graph.nodeDegree(node, inDegree, outDegree);
}
/** Alias for {@link nodeDegree}. */
vertexDegree(node: string, inDegree: number = 1, outDegree: number = 1): number {
return this.nodeDegree(node, inDegree, outDegree);
}
// ---- Attribute reading ----------------------------------------------
/**
* Returns the current value of a graph-level attribute, or `""` if the
* attribute has not been set.
*/
getGraphAttr(attr: string): string {
return this._graph.getGraphAttr(attr);
}
/**
* Returns the current value of the named attribute on a node, or `""` if
* the node or attribute does not exist.
*/
getNodeAttr(node: string, attr: string): string {
return this._graph.getNodeAttr(node, attr);
}
/** Alias for {@link getNodeAttr}. */
getVertexAttr(node: string, attr: string): string {
return this.getNodeAttr(node, attr);
}
/**
* Returns the current value of the named attribute on an edge, or `""` if
* the edge or attribute does not exist.
* Pass `key = ""` for the first (or only) edge between `tail` and `head`.
*/
getEdgeAttr(tail: string, head: string, key: string, attr: string): string {
return this._graph.getEdgeAttr(tail, head, key, attr);
}
// ---- Graph traversal ------------------------------------------------
/**
* Returns the ids of all direct subgraphs (the `cluster_` prefix is
* stripped from the underlying cgraph names).
*/
subgraphNames(): string[] {
return parseNames(this._graph.subgraphNames()).map(subgraphIdFromCppName);
}
/**
* Returns the ids of all nodes in the graph (in internal iteration order).
*/
nodeNames(): string[] {
return parseNames(this._graph.nodeNames());
}
/** Alias for {@link nodeNames}. */
vertexNames(): string[] {
return this.nodeNames();
}
/**
* Returns all edges in the graph. Each unique edge is listed exactly
* once.
*/
edges(): EdgeInfo[] {
return parseEdges(this._graph.edges());
}
/**
* Returns the out-edges of the node with the given id. Returns `[]` if
* the node does not exist.
*/
outEdges(node: string): EdgeInfo[] {
return parseEdges(this._graph.outEdges(node));
}
/** Alias for {@link outEdges}. */
outEdgesFrom(node: string): EdgeInfo[] {
return this.outEdges(node);
}
/**
* Returns the in-edges of the node with the given id. Returns `[]` if
* the node does not exist.
*/
inEdges(node: string): EdgeInfo[] {
return parseEdges(this._graph.inEdges(node));
}
/** Alias for {@link inEdges}. */
inEdgesTo(node: string): EdgeInfo[] {
return this.inEdges(node);
}
/**
* Returns all edges incident to the node with the given id (both in and
* out). Returns `[]` if the node does not exist.
*/
nodeEdges(node: string): EdgeInfo[] {
return parseEdges(this._graph.nodeEdges(node));
}
/** Alias for {@link nodeEdges}. */
vertexEdges(node: string): EdgeInfo[] {
return this.nodeEdges(node);
}
/**
* Create (or return an existing) subgraph with the given `id`. The
* `cluster_` prefix is added to `id` internally so that layout engines
* render it as a bounded cluster. The subgraph's `id` Graphviz attribute
* is set to the user-supplied `id` (without the prefix).
*
* Returns a {@link Subgraph} wrapper. **Call {@link Subgraph.delete} (or
* use the `using` keyword) when finished** to free the WASM wrapper. The
* subgraph data itself is owned by this graph.
*
* ```ts
* using cluster = graph.addSubgraph("0");
* cluster.setAttr("label", "My Cluster").addEdge("a", "b");
* ```
*/
addSubgraph(id: string, attrs?: AttrValues<ClusterAttrs>, htmlAttrs?: AttrValues<ClusterAttrs>): Subgraph;
addSubgraph(init: SubgraphInit): Subgraph;
addSubgraph(idOrInit: string | SubgraphInit, attrs?: AttrValues<ClusterAttrs>, htmlAttrs?: AttrValues<ClusterAttrs>): Subgraph {
const init = typeof idOrInit === "string" ? { id: idOrInit, attrs, htmlAttrs } : idOrInit;
// addSubgraph only returns null when the internal graph pointer is null,
// which cannot happen while this Graph instance is alive.
return new Subgraph(this._graph.addSubgraph(subgraphCppName(init.id))!).applyInit(init);
}
/**
* Look up an existing subgraph by id without creating a new one.
* Returns a {@link Subgraph} wrapper if found, or `null` if it does not
* exist. **Call {@link Subgraph.delete} (or use the `using` keyword)
* when finished** to free the WASM wrapper.
*
* ```ts
* using sg = graph.getSubgraph("0");
* if (sg) {
* console.log(sg.nodeNames());
* }
* ```
*/
getSubgraph(id: string): Subgraph | null {
const sg = this._graph.getSubgraph(subgraphCppName(id));
return sg ? new Subgraph(sg) : null;
}
/**
* Render the graph directly to the specified format without first
* serialising to DOT. Equivalent to:
* ```ts
* graphviz.layout(graph.toDot(), outputFormat, layoutEngine, options)
* ```
* but avoids the DOT round-trip.
*
* @param outputFormat The output format (default `"svg"`).
* @param layoutEngine The layout engine to use (default `"dot"`).
* @param options Optional images / extra files for the renderer.
* @returns The rendered output as a string.
*/
layout(outputFormat: Format = "svg", layoutEngine: Engine = "dot", options?: Options): string {
if (options?.images?.length || options?.files?.length) {
// Write images/files to the Emscripten FS before rendering.
const helper = new this._module.CGraphviz(0, 0);
try {
createFiles(helper, options);
} finally {
helper.delete();
}
}
let retVal = "";
let errorMsg = "";
try {
retVal = this._graph.layout(outputFormat, layoutEngine);
} catch (e: any) {
errorMsg = (e as Error).message;
}
errorMsg = this._module.CGraphviz.lastError() || errorMsg;
if (!retVal && errorMsg) {
throw new Error(errorMsg);
}
return retVal;
}
/**
* Serialise the graph to a DOT-language string.
*/
write(): string {
return this._graph.write();
}
/**
* Serialise the graph to a DOT-language string.
*/
toDot(): string {
return this.write();
}
/**