UNPKG

@hpcc-js/wasm-graphviz

Version:
1,230 lines (1,085 loc) 67.4 kB
// @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(); } /**