@hpcc-js/wasm-graphviz 1.22.2 → 1.24.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/graphviz.ts CHANGED
@@ -1,14 +1,49 @@
1
1
  // @ts-expect-error importing from a wasm file is resolved via a custom esbuild plugin
2
2
  import load, { reset } from "../../../build/packages/graphviz/graphvizlib.wasm";
3
- import type { MainModule } from "../types/graphvizlib.js";
3
+ import type { MainModule, CGraph as CGraphWasm, CSubgraph as CSubgraphWasm } from "../types/graphvizlib.js";
4
+ import type {
5
+ NodeDotAttr, NodeAttrs,
6
+ EdgeDotAttr, EdgeAttrs,
7
+ ClusterDotAttr, ClusterAttrs,
8
+ GraphDotAttr, GraphAttrs as GraphAttrs
9
+ } from "./types.ts";
4
10
 
5
11
  /**
6
- * Various graphic and data formats for end user, web, documents and other applications. See [Output Formats](https://graphviz.gitlab.io/docs/outputs/) for more information.
12
+ * Various graphic and data formats for end user, web, documents and other applications. See [Output Formats](https://graphviz.org/docs/outputs/) for more information.
7
13
  */
8
14
  export type Format = "svg" | "svg_inline" | "dot" | "json" | "dot_json" | "xdot_json" | "plain" | "plain-ext" | "canon";
9
15
 
10
16
  /**
11
- * Various algorithms for projecting abstract graphs into a space for visualization. See [Layout Engines](https://graphviz.gitlab.io/docs/layouts/) for more details.
17
+ * An edge returned by graph-traversal methods.
18
+ */
19
+ export interface EdgeInfo {
20
+ /** The tail (source) node name. */
21
+ tail: string;
22
+ /** The head (target) node name. */
23
+ head: string;
24
+ /**
25
+ * The cgraph edge key that distinguishes parallel edges.
26
+ * Typically `""` for single (non-keyed) edges.
27
+ */
28
+ key: string;
29
+ }
30
+
31
+ /** @internal Parses a JSON string array returned by C++ traversal methods. */
32
+ function parseNames(json: string): string[] {
33
+ return JSON.parse(json) as string[];
34
+ }
35
+
36
+ /** @internal Parses a flat JSON triple array `[tail,head,key,...]` into EdgeInfo[]. */
37
+ function parseEdges(json: string): EdgeInfo[] {
38
+ const flat = JSON.parse(json) as string[];
39
+ const result: EdgeInfo[] = [];
40
+ for (let i = 0; i + 2 < flat.length; i += 3)
41
+ result.push({ tail: flat[i]!, head: flat[i + 1]!, key: flat[i + 2]! });
42
+ return result;
43
+ }
44
+
45
+ /**
46
+ * Various algorithms for projecting abstract graphs into a space for visualization. See [Layout Engines](https://graphviz.org/docs/layouts/) for more details.
12
47
  */
13
48
  export type Engine = "circo" | "dot" | "fdp" | "sfdp" | "neato" | "osage" | "patchwork" | "twopi" | "nop" | "nop2";
14
49
 
@@ -73,6 +108,836 @@ function createFiles(graphviz: any, _options?: Options) {
73
108
 
74
109
  let g_graphviz: Promise<Graphviz> | undefined;
75
110
 
111
+ /**
112
+ * The type of graph to create.
113
+ * - `"directed"` – directed graph (`digraph`)
114
+ * - `"undirected"` – undirected graph (`graph`)
115
+ * - `"strict directed"` – directed graph that disallows parallel edges and self-loops
116
+ * - `"strict undirected"` – undirected graph that disallows parallel edges and self-loops
117
+ */
118
+ export type GraphType = "directed" | "undirected" | "strict directed" | "strict undirected";
119
+
120
+ /**
121
+ * A subgraph (or cluster) inside a {@link Graph}.
122
+ *
123
+ * Obtain via {@link Graph.addSubgraph}. All mutation methods return `this`
124
+ * for chaining. **Call {@link Subgraph.delete} (or use the `using` keyword)
125
+ * when finished** to free the underlying WASM wrapper — the actual subgraph
126
+ * data is owned by the parent {@link Graph} and is freed with it.
127
+ *
128
+ * Clusters are subgraphs whose name starts with `"cluster"`. Layout engines
129
+ * draw them as a bounded rectangle:
130
+ *
131
+ * ```ts
132
+ * using cluster = graph.addSubgraph("cluster_0");
133
+ * cluster
134
+ * .setAttr("label", "My Cluster")
135
+ * .setAttr("style", "filled")
136
+ * .setAttr("color", "lightblue")
137
+ * .addEdge("a", "b");
138
+ * ```
139
+ */
140
+ export class Subgraph {
141
+ private _sg: CSubgraphWasm;
142
+
143
+ /** @internal */
144
+ constructor(sg: CSubgraphWasm) {
145
+ this._sg = sg;
146
+ }
147
+
148
+ /**
149
+ * Create (or find) a node and add it to this subgraph.
150
+ */
151
+ addNode(name: string): this {
152
+ this._sg.addNode(name);
153
+ return this;
154
+ }
155
+
156
+ /**
157
+ * Create an edge inside this subgraph. Both endpoints are created
158
+ * automatically if they do not already exist.
159
+ */
160
+ addEdge(tail: string, head: string, key: string = ""): this {
161
+ this._sg.addEdge(tail, head, key);
162
+ return this;
163
+ }
164
+
165
+ /**
166
+ * Remove a node from this subgraph only. The node (and any edges
167
+ * connecting it) remains in the root graph and all other subgraphs.
168
+ * No-op if the node is not in this subgraph.
169
+ */
170
+ removeNode(name: string): this {
171
+ this._sg.removeNode(name);
172
+ return this;
173
+ }
174
+
175
+ /**
176
+ * Remove a single edge from this subgraph only. The edge remains in the
177
+ * root graph and all other subgraphs. No-op if the edge is not present.
178
+ */
179
+ removeEdge(tail: string, head: string, key: string = ""): this {
180
+ this._sg.removeEdge(tail, head, key);
181
+ return this;
182
+ }
183
+
184
+ /**
185
+ * Set an attribute on the subgraph itself (e.g. `"label"`, `"style"`,
186
+ * `"color"`, `"bgcolor"`). See the official Graphviz
187
+ * [attribute reference](https://graphviz.org/docs/attrs/) for supported
188
+ * attributes and values.
189
+ *
190
+ * When `attr` is a known cluster attribute the value type is inferred
191
+ * automatically. A generic `string | number | boolean` fallback is
192
+ * provided for custom or less-common attributes. Omit `value` to reset
193
+ * the attribute to Graphviz's default empty value. `defaultValue` is
194
+ * passed to Graphviz as the default for this attribute if it has not
195
+ * already been declared.
196
+ */
197
+ setAttr<K extends ClusterDotAttr>(attr: K, value?: NonNullable<ClusterAttrs[K]>, defaultValue?: string | number | boolean): this;
198
+ setAttr(attr: string, value?: string | number | boolean, defaultValue?: string | number | boolean): this;
199
+ setAttr(attr: string, value: unknown = "", defaultValue?: unknown): this {
200
+ if (defaultValue === undefined) {
201
+ this._sg.setAttr(attr, String(value));
202
+ } else {
203
+ this._sg.setAttr(attr, String(value), String(defaultValue));
204
+ }
205
+ return this;
206
+ }
207
+
208
+ setHtmlAttr<K extends ClusterDotAttr>(attr: K, value: NonNullable<ClusterAttrs[K]>, defaultValue?: string | number | boolean): this;
209
+ setHtmlAttr(attr: string, value: string | number | boolean, defaultValue?: string | number | boolean): this;
210
+ setHtmlAttr(attr: string, value: unknown, defaultValue?: unknown): this {
211
+ if (defaultValue === undefined) {
212
+ this._sg.setHtmlAttr(attr, String(value));
213
+ } else {
214
+ this._sg.setHtmlAttr(attr, String(value), String(defaultValue));
215
+ }
216
+ return this;
217
+ }
218
+
219
+ setDefaultAttr<K extends ClusterDotAttr>(attr: K, value: NonNullable<ClusterAttrs[K]>): this;
220
+ setDefaultAttr(attr: string, value: string | number | boolean): this;
221
+ setDefaultAttr(attr: string, value: unknown): this {
222
+ this._sg.setDefaultAttr(attr, String(value));
223
+ return this;
224
+ }
225
+
226
+ setDefaultHtmlAttr<K extends ClusterDotAttr>(attr: K, value: NonNullable<ClusterAttrs[K]>): this;
227
+ setDefaultHtmlAttr(attr: string, value: string | number | boolean): this;
228
+ setDefaultHtmlAttr(attr: string, value: unknown): this {
229
+ this._sg.setDefaultHtmlAttr(attr, String(value));
230
+ return this;
231
+ }
232
+
233
+ /**
234
+ * Clear a subgraph-level attribute by resetting it to its default (empty)
235
+ * value. Equivalent to `setAttr(attr, "")`.
236
+ */
237
+ removeAttr(attr: string): this {
238
+ return this.setAttr(attr, "");
239
+ }
240
+
241
+ /**
242
+ * Set an attribute on a node inside this subgraph.
243
+ *
244
+ * When `attr` is a known node attribute the value type is inferred
245
+ * automatically. A generic `string | number | boolean` fallback is
246
+ * provided for custom attributes. Omit `value` to reset the attribute to
247
+ * Graphviz's default empty value. `defaultValue` is passed to Graphviz as
248
+ * the default for this attribute if it has not already been declared.
249
+ */
250
+ setNodeAttr<K extends NodeDotAttr>(node: string, attr: K, value?: NonNullable<NodeAttrs[K]>, defaultValue?: string | number | boolean): this;
251
+ setNodeAttr(node: string, attr: string, value?: string | number | boolean, defaultValue?: string | number | boolean): this;
252
+ setNodeAttr(node: string, attr: string, value: unknown = "", defaultValue?: unknown): this {
253
+ if (defaultValue === undefined) {
254
+ this._sg.setNodeAttr(node, attr, String(value));
255
+ } else {
256
+ this._sg.setNodeAttr(node, attr, String(value), String(defaultValue));
257
+ }
258
+ return this;
259
+ }
260
+
261
+ setNodeHtmlAttr<K extends NodeDotAttr>(node: string, attr: K, value: NonNullable<NodeAttrs[K]>, defaultValue?: string | number | boolean): this;
262
+ setNodeHtmlAttr(node: string, attr: string, value: string | number | boolean, defaultValue?: string | number | boolean): this;
263
+ setNodeHtmlAttr(node: string, attr: string, value: unknown, defaultValue?: unknown): this {
264
+ if (defaultValue === undefined) {
265
+ this._sg.setNodeHtmlAttr(node, attr, String(value));
266
+ } else {
267
+ this._sg.setNodeHtmlAttr(node, attr, String(value), String(defaultValue));
268
+ }
269
+ return this;
270
+ }
271
+
272
+ setDefaultNodeAttr<K extends NodeDotAttr>(attr: K, value: NonNullable<NodeAttrs[K]>): this;
273
+ setDefaultNodeAttr(attr: string, value: string | number | boolean): this;
274
+ setDefaultNodeAttr(attr: string, value: unknown): this {
275
+ this._sg.setDefaultNodeAttr(attr, String(value));
276
+ return this;
277
+ }
278
+
279
+ setDefaultNodeHtmlAttr<K extends NodeDotAttr>(attr: K, value: NonNullable<NodeAttrs[K]>): this;
280
+ setDefaultNodeHtmlAttr(attr: string, value: string | number | boolean): this;
281
+ setDefaultNodeHtmlAttr(attr: string, value: unknown): this {
282
+ this._sg.setDefaultNodeHtmlAttr(attr, String(value));
283
+ return this;
284
+ }
285
+
286
+ /**
287
+ * Clear a node attribute inside this subgraph by resetting it to its
288
+ * default (empty) value. Equivalent to `setNodeAttr(node, attr, "")`.
289
+ */
290
+ removeNodeAttr(node: string, attr: string): this {
291
+ return this.setNodeAttr(node, attr, "");
292
+ }
293
+
294
+ /**
295
+ * Set an attribute on an edge inside this subgraph (identified by
296
+ * `tail`, `head`, and `key`).
297
+ *
298
+ * When `attr` is a known edge attribute the value type is inferred
299
+ * automatically. A generic `string | number | boolean` fallback is
300
+ * provided for custom attributes. Omit `value` to reset the attribute to
301
+ * Graphviz's default empty value. `defaultValue` is passed to Graphviz as
302
+ * the default for this attribute if it has not already been declared.
303
+ */
304
+ setEdgeAttr<K extends EdgeDotAttr>(tail: string, head: string, key: string, attr: K, value?: NonNullable<EdgeAttrs[K]>, defaultValue?: string | number | boolean): this;
305
+ setEdgeAttr(tail: string, head: string, key: string, attr: string, value?: string | number | boolean, defaultValue?: string | number | boolean): this;
306
+ setEdgeAttr(tail: string, head: string, key: string, attr: string, value: unknown = "", defaultValue?: unknown): this {
307
+ if (defaultValue === undefined) {
308
+ this._sg.setEdgeAttr(tail, head, key, attr, String(value));
309
+ } else {
310
+ this._sg.setEdgeAttr(tail, head, key, attr, String(value), String(defaultValue));
311
+ }
312
+ return this;
313
+ }
314
+
315
+ setEdgeHtmlAttr<K extends EdgeDotAttr>(tail: string, head: string, key: string, attr: K, value: NonNullable<EdgeAttrs[K]>, defaultValue?: string | number | boolean): this;
316
+ setEdgeHtmlAttr(tail: string, head: string, key: string, attr: string, value: string | number | boolean, defaultValue?: string | number | boolean): this;
317
+ setEdgeHtmlAttr(tail: string, head: string, key: string, attr: string, value: unknown, defaultValue?: unknown): this {
318
+ if (defaultValue === undefined) {
319
+ this._sg.setEdgeHtmlAttr(tail, head, key, attr, String(value));
320
+ } else {
321
+ this._sg.setEdgeHtmlAttr(tail, head, key, attr, String(value), String(defaultValue));
322
+ }
323
+ return this;
324
+ }
325
+
326
+ setDefaultEdgeAttr<K extends EdgeDotAttr>(attr: K, value: NonNullable<EdgeAttrs[K]>): this;
327
+ setDefaultEdgeAttr(attr: string, value: string | number | boolean): this;
328
+ setDefaultEdgeAttr(attr: string, value: unknown): this {
329
+ this._sg.setDefaultEdgeAttr(attr, String(value));
330
+ return this;
331
+ }
332
+
333
+ setDefaultEdgeHtmlAttr<K extends EdgeDotAttr>(attr: K, value: NonNullable<EdgeAttrs[K]>): this;
334
+ setDefaultEdgeHtmlAttr(attr: string, value: string | number | boolean): this;
335
+ setDefaultEdgeHtmlAttr(attr: string, value: unknown): this {
336
+ this._sg.setDefaultEdgeHtmlAttr(attr, String(value));
337
+ return this;
338
+ }
339
+
340
+ /**
341
+ * Clear an edge attribute inside this subgraph by resetting it to its
342
+ * default (empty) value. Equivalent to `setEdgeAttr(tail, head, key, attr, "")`.
343
+ */
344
+ removeEdgeAttr(tail: string, head: string, key: string, attr: string): this {
345
+ return this.setEdgeAttr(tail, head, key, attr, "");
346
+ }
347
+
348
+ // ---- Existence checks -----------------------------------------------
349
+
350
+ /**
351
+ * Returns `true` if a node with the given name exists in this subgraph.
352
+ */
353
+ hasNode(name: string): boolean {
354
+ return this._sg.hasNode(name);
355
+ }
356
+
357
+ /**
358
+ * Returns `true` if an edge from `tail` to `head` exists in this subgraph.
359
+ * Pass `key` to check for a specific parallel edge; omit (or pass `""`) to
360
+ * check for any edge between the two nodes.
361
+ */
362
+ hasEdge(tail: string, head: string, key: string = ""): boolean {
363
+ return this._sg.hasEdge(tail, head, key);
364
+ }
365
+
366
+ // ---- Count queries --------------------------------------------------
367
+
368
+ /** Returns the number of nodes in this subgraph. */
369
+ nodeCount(): number { return this._sg.nodeCount(); }
370
+
371
+ /** Returns the number of edges in this subgraph. */
372
+ edgeCount(): number { return this._sg.edgeCount(); }
373
+
374
+ /** Returns the degree of a named node in this subgraph. */
375
+ nodeDegree(node: string, inDegree: number = 1, outDegree: number = 1): number {
376
+ return this._sg.nodeDegree(node, inDegree, outDegree);
377
+ }
378
+
379
+ // ---- Attribute reading ----------------------------------------------
380
+
381
+ /**
382
+ * Returns the current value of a subgraph-level attribute, or `""` if
383
+ * the attribute has not been set.
384
+ */
385
+ getAttr(attr: string): string {
386
+ return this._sg.getAttr(attr);
387
+ }
388
+
389
+ /**
390
+ * Returns the current value of the named attribute on a node in this
391
+ * subgraph, or `""` if the node or attribute does not exist.
392
+ */
393
+ getNodeAttr(node: string, attr: string): string {
394
+ return this._sg.getNodeAttr(node, attr);
395
+ }
396
+
397
+ /**
398
+ * Returns the current value of the named attribute on an edge in this
399
+ * subgraph, or `""` if the edge or attribute does not exist.
400
+ * Pass `key = ""` for the first (or only) edge between `tail` and `head`.
401
+ */
402
+ getEdgeAttr(tail: string, head: string, key: string, attr: string): string {
403
+ return this._sg.getEdgeAttr(tail, head, key, attr);
404
+ }
405
+
406
+ // ---- Graph traversal ------------------------------------------------
407
+
408
+ /**
409
+ * Returns the names of all nodes in this subgraph (in internal iteration
410
+ * order).
411
+ */
412
+ nodeNames(): string[] {
413
+ return parseNames(this._sg.nodeNames());
414
+ }
415
+
416
+ /**
417
+ * Returns all edges in this subgraph. Each unique edge is listed exactly
418
+ * once.
419
+ */
420
+ edges(): EdgeInfo[] {
421
+ return parseEdges(this._sg.edges());
422
+ }
423
+
424
+ /**
425
+ * Returns the out-edges of the named node in this subgraph. Returns `[]`
426
+ * if the node does not exist.
427
+ */
428
+ outEdges(node: string): EdgeInfo[] {
429
+ return parseEdges(this._sg.outEdges(node));
430
+ }
431
+
432
+ /**
433
+ * Returns the in-edges of the named node in this subgraph. Returns `[]`
434
+ * if the node does not exist.
435
+ */
436
+ inEdges(node: string): EdgeInfo[] {
437
+ return parseEdges(this._sg.inEdges(node));
438
+ }
439
+
440
+ /**
441
+ * Returns all edges incident to the named node in this subgraph (both in
442
+ * and out). Returns `[]` if the node does not exist.
443
+ */
444
+ nodeEdges(node: string): EdgeInfo[] {
445
+ return parseEdges(this._sg.nodeEdges(node));
446
+ }
447
+
448
+ /**
449
+ * Release the WASM wrapper. The subgraph data itself is freed when the
450
+ * parent {@link Graph} is deleted.
451
+ */
452
+ delete(): void {
453
+ this._sg.delete();
454
+ }
455
+
456
+ /** @internal – supports the `using` keyword (explicit resource management). */
457
+ [Symbol.dispose](): void {
458
+ this.delete();
459
+ }
460
+ }
461
+
462
+ /**
463
+ * A programmatic graph builder backed by the cgraph library.
464
+ *
465
+ * Obtain an instance via {@link Graphviz.createGraph} and call
466
+ * {@link Graph.toDot} to serialise to a DOT string that can be passed to
467
+ * {@link Graphviz.layout} (or any of its convenience wrappers).
468
+ *
469
+ * **Always call {@link Graph.delete} (or use the `using` keyword) when
470
+ * finished** to release the underlying WASM memory.
471
+ *
472
+ * ```ts
473
+ * const graphviz = await Graphviz.load();
474
+ *
475
+ * using graph = graphviz.createGraph("G");
476
+ * graph.addNode("a");
477
+ * graph.addNode("b");
478
+ * graph.addEdge("a", "b");
479
+ * graph.setNodeAttr("a", "color", "red");
480
+ * graph.setEdgeAttr("a", "b", "", "label", "hello");
481
+ * graph.setGraphAttr("rankdir", "LR");
482
+ *
483
+ * const svg = graphviz.dot(graph.toDot());
484
+ * ```
485
+ */
486
+ export class Graph {
487
+ private _graph: CGraphWasm;
488
+ private _module: MainModule;
489
+
490
+ /** @internal */
491
+ constructor(g: CGraphWasm, module: MainModule) {
492
+ this._graph = g;
493
+ this._module = module;
494
+ }
495
+
496
+ /**
497
+ * Create a node. If a node with this name already exists it is returned
498
+ * unchanged.
499
+ */
500
+ addNode(name: string): this {
501
+ this._graph.addNode(name);
502
+ return this;
503
+ }
504
+
505
+ /**
506
+ * Create an edge from `tail` to `head`. Both nodes are created
507
+ * automatically if they do not already exist. `key` distinguishes
508
+ * parallel edges between the same pair of nodes; omit (or pass `""`) for
509
+ * an anonymous edge.
510
+ */
511
+ addEdge(tail: string, head: string, key: string = ""): this {
512
+ this._graph.addEdge(tail, head, key);
513
+ return this;
514
+ }
515
+
516
+ /**
517
+ * Replace this graph with one parsed from DOT source using Graphviz cgraph
518
+ * reading support.
519
+ */
520
+ read(dotSource: string): this {
521
+ if (!this._graph.read(dotSource)) {
522
+ throw new Error("Invalid DOT source");
523
+ }
524
+ return this;
525
+ }
526
+
527
+ /**
528
+ * Set a graph-level attribute (e.g. `"rankdir"`, `"label"`, `"bgcolor"`).
529
+ * See the official Graphviz [attribute reference](https://graphviz.org/docs/attrs/)
530
+ * for supported attributes and values.
531
+ *
532
+ * When `attr` is a known graph attribute the value type is inferred
533
+ * automatically. A generic `string | number | boolean` fallback is
534
+ * provided for custom or less-common attributes. Omit `value` to reset
535
+ * the attribute to Graphviz's default empty value. `defaultValue` is
536
+ * passed to Graphviz as the default for this attribute if it has not
537
+ * already been declared.
538
+ */
539
+ setGraphAttr<K extends GraphDotAttr>(attr: K, value?: NonNullable<GraphAttrs[K]>, defaultValue?: string | number | boolean): this;
540
+ setGraphAttr(attr: string, value?: string | number | boolean, defaultValue?: string | number | boolean): this;
541
+ setGraphAttr(attr: string, value: unknown = "", defaultValue?: unknown): this {
542
+ if (defaultValue === undefined) {
543
+ this._graph.setGraphAttr(attr, String(value));
544
+ } else {
545
+ this._graph.setGraphAttr(attr, String(value), String(defaultValue));
546
+ }
547
+ return this;
548
+ }
549
+
550
+ setGraphHtmlAttr<K extends GraphDotAttr>(attr: K, value: NonNullable<GraphAttrs[K]>, defaultValue?: string | number | boolean): this;
551
+ setGraphHtmlAttr(attr: string, value: string | number | boolean, defaultValue?: string | number | boolean): this;
552
+ setGraphHtmlAttr(attr: string, value: unknown, defaultValue?: unknown): this {
553
+ if (defaultValue === undefined) {
554
+ this._graph.setGraphHtmlAttr(attr, String(value));
555
+ } else {
556
+ this._graph.setGraphHtmlAttr(attr, String(value), String(defaultValue));
557
+ }
558
+ return this;
559
+ }
560
+
561
+ setDefaultGraphAttr<K extends GraphDotAttr>(attr: K, value: NonNullable<GraphAttrs[K]>): this;
562
+ setDefaultGraphAttr(attr: string, value: string | number | boolean): this;
563
+ setDefaultGraphAttr(attr: string, value: unknown): this {
564
+ this._graph.setDefaultGraphAttr(attr, String(value));
565
+ return this;
566
+ }
567
+
568
+ setDefaultGraphHtmlAttr<K extends GraphDotAttr>(attr: K, value: NonNullable<GraphAttrs[K]>): this;
569
+ setDefaultGraphHtmlAttr(attr: string, value: string | number | boolean): this;
570
+ setDefaultGraphHtmlAttr(attr: string, value: unknown): this {
571
+ this._graph.setDefaultGraphHtmlAttr(attr, String(value));
572
+ return this;
573
+ }
574
+
575
+ /**
576
+ * Set an attribute on a named node (e.g. `"color"`, `"label"`, `"shape"`).
577
+ * The node must exist; call {@link addNode} first if needed.
578
+ *
579
+ * When `attr` is a known node attribute the value type is inferred
580
+ * automatically. A generic `string | number | boolean` fallback is
581
+ * provided for custom attributes. Omit `value` to reset the attribute to
582
+ * Graphviz's default empty value. `defaultValue` is passed to Graphviz as
583
+ * the default for this attribute if it has not already been declared.
584
+ */
585
+ setNodeAttr<K extends NodeDotAttr>(node: string, attr: K, value?: NonNullable<NodeAttrs[K]>, defaultValue?: string | number | boolean): this;
586
+ setNodeAttr(node: string, attr: string, value?: string | number | boolean, defaultValue?: string | number | boolean): this;
587
+ setNodeAttr(node: string, attr: string, value: unknown = "", defaultValue?: unknown): this {
588
+ if (defaultValue === undefined) {
589
+ this._graph.setNodeAttr(node, attr, String(value));
590
+ } else {
591
+ this._graph.setNodeAttr(node, attr, String(value), String(defaultValue));
592
+ }
593
+ return this;
594
+ }
595
+
596
+ setNodeHtmlAttr<K extends NodeDotAttr>(node: string, attr: K, value: NonNullable<NodeAttrs[K]>, defaultValue?: string | number | boolean): this;
597
+ setNodeHtmlAttr(node: string, attr: string, value: string | number | boolean, defaultValue?: string | number | boolean): this;
598
+ setNodeHtmlAttr(node: string, attr: string, value: unknown, defaultValue?: unknown): this {
599
+ if (defaultValue === undefined) {
600
+ this._graph.setNodeHtmlAttr(node, attr, String(value));
601
+ } else {
602
+ this._graph.setNodeHtmlAttr(node, attr, String(value), String(defaultValue));
603
+ }
604
+ return this;
605
+ }
606
+
607
+ setDefaultNodeAttr<K extends NodeDotAttr>(attr: K, value: NonNullable<NodeAttrs[K]>): this;
608
+ setDefaultNodeAttr(attr: string, value: string | number | boolean): this;
609
+ setDefaultNodeAttr(attr: string, value: unknown): this {
610
+ this._graph.setDefaultNodeAttr(attr, String(value));
611
+ return this;
612
+ }
613
+
614
+ setDefaultNodeHtmlAttr<K extends NodeDotAttr>(attr: K, value: NonNullable<NodeAttrs[K]>): this;
615
+ setDefaultNodeHtmlAttr(attr: string, value: string | number | boolean): this;
616
+ setDefaultNodeHtmlAttr(attr: string, value: unknown): this {
617
+ this._graph.setDefaultNodeHtmlAttr(attr, String(value));
618
+ return this;
619
+ }
620
+
621
+ /**
622
+ * Set an attribute on an edge identified by `(tail, head, key)`.
623
+ * Use the same `key` that was passed to {@link addEdge}; pass `""` for
624
+ * anonymous edges.
625
+ *
626
+ * When `attr` is a known edge attribute the value type is inferred
627
+ * automatically. A generic `string | number | boolean` fallback is
628
+ * provided for custom attributes. Omit `value` to reset the attribute to
629
+ * Graphviz's default empty value. `defaultValue` is passed to Graphviz as
630
+ * the default for this attribute if it has not already been declared.
631
+ */
632
+ setEdgeAttr<K extends EdgeDotAttr>(tail: string, head: string, key: string, attr: K, value?: NonNullable<EdgeAttrs[K]>, defaultValue?: string | number | boolean): this;
633
+ setEdgeAttr(tail: string, head: string, key: string, attr: string, value?: string | number | boolean, defaultValue?: string | number | boolean): this;
634
+ setEdgeAttr(tail: string, head: string, key: string, attr: string, value: unknown = "", defaultValue?: unknown): this {
635
+ if (defaultValue === undefined) {
636
+ this._graph.setEdgeAttr(tail, head, key, attr, String(value));
637
+ } else {
638
+ this._graph.setEdgeAttr(tail, head, key, attr, String(value), String(defaultValue));
639
+ }
640
+ return this;
641
+ }
642
+
643
+ setEdgeHtmlAttr<K extends EdgeDotAttr>(tail: string, head: string, key: string, attr: K, value: NonNullable<EdgeAttrs[K]>, defaultValue?: string | number | boolean): this;
644
+ setEdgeHtmlAttr(tail: string, head: string, key: string, attr: string, value: string | number | boolean, defaultValue?: string | number | boolean): this;
645
+ setEdgeHtmlAttr(tail: string, head: string, key: string, attr: string, value: unknown, defaultValue?: unknown): this {
646
+ if (defaultValue === undefined) {
647
+ this._graph.setEdgeHtmlAttr(tail, head, key, attr, String(value));
648
+ } else {
649
+ this._graph.setEdgeHtmlAttr(tail, head, key, attr, String(value), String(defaultValue));
650
+ }
651
+ return this;
652
+ }
653
+
654
+ setDefaultEdgeAttr<K extends EdgeDotAttr>(attr: K, value: NonNullable<EdgeAttrs[K]>): this;
655
+ setDefaultEdgeAttr(attr: string, value: string | number | boolean): this;
656
+ setDefaultEdgeAttr(attr: string, value: unknown): this {
657
+ this._graph.setDefaultEdgeAttr(attr, String(value));
658
+ return this;
659
+ }
660
+
661
+ setDefaultEdgeHtmlAttr<K extends EdgeDotAttr>(attr: K, value: NonNullable<EdgeAttrs[K]>): this;
662
+ setDefaultEdgeHtmlAttr(attr: string, value: string | number | boolean): this;
663
+ setDefaultEdgeHtmlAttr(attr: string, value: unknown): this {
664
+ this._graph.setDefaultEdgeHtmlAttr(attr, String(value));
665
+ return this;
666
+ }
667
+
668
+ /**
669
+ * Remove a node and all its edges from the graph. Removing from the root
670
+ * graph also removes the node from every subgraph.
671
+ * No-op if the node does not exist.
672
+ */
673
+ removeNode(name: string): this {
674
+ this._graph.removeNode(name);
675
+ return this;
676
+ }
677
+
678
+ /**
679
+ * Remove a single edge identified by `(tail, head, key)`.
680
+ * Pass `""` for `key` on anonymous edges.
681
+ * No-op if the edge does not exist.
682
+ */
683
+ removeEdge(tail: string, head: string, key: string = ""): this {
684
+ this._graph.removeEdge(tail, head, key);
685
+ return this;
686
+ }
687
+
688
+ /**
689
+ * Dissolve a subgraph / cluster boundary. Nodes and edges that belonged
690
+ * to the subgraph remain in the parent graph.
691
+ * No-op if no subgraph with that name exists.
692
+ */
693
+ removeSubgraph(name: string): this {
694
+ this._graph.removeSubgraph(name);
695
+ return this;
696
+ }
697
+
698
+ /**
699
+ * Clear a graph-level attribute by resetting it to its default (empty)
700
+ * value. Equivalent to `setGraphAttr(attr, "")`.
701
+ */
702
+ removeGraphAttr(attr: string): this {
703
+ return this.setGraphAttr(attr, "");
704
+ }
705
+
706
+ /**
707
+ * Clear a node attribute by resetting it to its default (empty) value.
708
+ * Equivalent to `setNodeAttr(node, attr, "")`.
709
+ */
710
+ removeNodeAttr(node: string, attr: string): this {
711
+ return this.setNodeAttr(node, attr, "");
712
+ }
713
+
714
+ /**
715
+ * Clear an edge attribute by resetting it to its default (empty) value.
716
+ * Equivalent to `setEdgeAttr(tail, head, key, attr, "")`.
717
+ */
718
+ removeEdgeAttr(tail: string, head: string, key: string, attr: string): this {
719
+ return this.setEdgeAttr(tail, head, key, attr, "");
720
+ }
721
+
722
+ // ---- Existence checks -----------------------------------------------
723
+
724
+ /**
725
+ * Returns `true` if a node with the given name exists in the graph.
726
+ */
727
+ hasNode(name: string): boolean {
728
+ return this._graph.hasNode(name);
729
+ }
730
+
731
+ /**
732
+ * Returns `true` if an edge from `tail` to `head` exists in the graph.
733
+ * Pass `key` to check for a specific parallel edge; omit (or pass `""`) to
734
+ * check for any edge between the two nodes.
735
+ */
736
+ hasEdge(tail: string, head: string, key: string = ""): boolean {
737
+ return this._graph.hasEdge(tail, head, key);
738
+ }
739
+
740
+ /**
741
+ * Returns `true` if a subgraph with the given name exists.
742
+ */
743
+ hasSubgraph(name: string): boolean {
744
+ return this._graph.hasSubgraph(name);
745
+ }
746
+
747
+ // ---- Count queries --------------------------------------------------
748
+
749
+ /** Returns the number of nodes in the graph. */
750
+ nodeCount(): number { return this._graph.nodeCount(); }
751
+
752
+ /** Returns the number of edges in the graph. */
753
+ edgeCount(): number { return this._graph.edgeCount(); }
754
+
755
+ /** Returns the number of direct subgraphs of this graph. */
756
+ subgraphCount(): number { return this._graph.subgraphCount(); }
757
+
758
+ /** Returns the degree of a named node in the graph. */
759
+ nodeDegree(node: string, inDegree: number = 1, outDegree: number = 1): number {
760
+ return this._graph.nodeDegree(node, inDegree, outDegree);
761
+ }
762
+
763
+ // ---- Attribute reading ----------------------------------------------
764
+
765
+ /**
766
+ * Returns the current value of a graph-level attribute, or `""` if the
767
+ * attribute has not been set.
768
+ */
769
+ getGraphAttr(attr: string): string {
770
+ return this._graph.getGraphAttr(attr);
771
+ }
772
+
773
+ /**
774
+ * Returns the current value of the named attribute on a node, or `""` if
775
+ * the node or attribute does not exist.
776
+ */
777
+ getNodeAttr(node: string, attr: string): string {
778
+ return this._graph.getNodeAttr(node, attr);
779
+ }
780
+
781
+ /**
782
+ * Returns the current value of the named attribute on an edge, or `""` if
783
+ * the edge or attribute does not exist.
784
+ * Pass `key = ""` for the first (or only) edge between `tail` and `head`.
785
+ */
786
+ getEdgeAttr(tail: string, head: string, key: string, attr: string): string {
787
+ return this._graph.getEdgeAttr(tail, head, key, attr);
788
+ }
789
+
790
+ // ---- Graph traversal ------------------------------------------------
791
+
792
+ /**
793
+ * Returns the names of all nodes in the graph (in internal iteration
794
+ * order).
795
+ */
796
+ nodeNames(): string[] {
797
+ return parseNames(this._graph.nodeNames());
798
+ }
799
+
800
+ /**
801
+ * Returns the names of all direct subgraphs.
802
+ */
803
+ subgraphNames(): string[] {
804
+ return parseNames(this._graph.subgraphNames());
805
+ }
806
+
807
+ /**
808
+ * Returns all edges in the graph. Each unique edge is listed exactly
809
+ * once.
810
+ */
811
+ edges(): EdgeInfo[] {
812
+ return parseEdges(this._graph.edges());
813
+ }
814
+
815
+ /**
816
+ * Returns the out-edges of the named node. Returns `[]` if the node does
817
+ * not exist.
818
+ */
819
+ outEdges(node: string): EdgeInfo[] {
820
+ return parseEdges(this._graph.outEdges(node));
821
+ }
822
+
823
+ /**
824
+ * Returns the in-edges of the named node. Returns `[]` if the node does
825
+ * not exist.
826
+ */
827
+ inEdges(node: string): EdgeInfo[] {
828
+ return parseEdges(this._graph.inEdges(node));
829
+ }
830
+
831
+ /**
832
+ * Returns all edges incident to the named node (both in and out).
833
+ * Returns `[]` if the node does not exist.
834
+ */
835
+ nodeEdges(node: string): EdgeInfo[] {
836
+ return parseEdges(this._graph.nodeEdges(node));
837
+ }
838
+
839
+ /**
840
+ * Create (or return an existing) named subgraph. Use a name beginning
841
+ * with `"cluster"` to have layout engines render it as a bounded cluster.
842
+ *
843
+ * Returns a {@link Subgraph} wrapper. **Call {@link Subgraph.delete} (or
844
+ * use the `using` keyword) when finished** to free the WASM wrapper. The
845
+ * subgraph data itself is owned by this graph.
846
+ *
847
+ * ```ts
848
+ * using cluster = graph.addSubgraph("cluster_0");
849
+ * cluster.setAttr("label", "My Cluster").addEdge("a", "b");
850
+ * ```
851
+ */
852
+ addSubgraph(name: string): Subgraph {
853
+ // addSubgraph only returns null when the internal graph pointer is null,
854
+ // which cannot happen while this Graph instance is alive.
855
+ return new Subgraph(this._graph.addSubgraph(name)!);
856
+ }
857
+
858
+ /**
859
+ * Look up an existing subgraph by name without creating a new one.
860
+ * Returns a {@link Subgraph} wrapper if a subgraph with that name exists,
861
+ * or `null` if it does not. **Call {@link Subgraph.delete} (or use the
862
+ * `using` keyword) when finished** to free the WASM wrapper.
863
+ *
864
+ * ```ts
865
+ * using sg = graph.getSubgraph("cluster_0");
866
+ * if (sg) {
867
+ * console.log(sg.nodeNames());
868
+ * }
869
+ * ```
870
+ */
871
+ getSubgraph(name: string): Subgraph | null {
872
+ const sg = this._graph.getSubgraph(name);
873
+ return sg ? new Subgraph(sg) : null;
874
+ }
875
+
876
+ /**
877
+ * Render the graph directly to the specified format without first
878
+ * serialising to DOT. Equivalent to:
879
+ * ```ts
880
+ * graphviz.layout(graph.toDot(), outputFormat, layoutEngine, options)
881
+ * ```
882
+ * but avoids the DOT round-trip.
883
+ *
884
+ * @param outputFormat The output format (default `"svg"`).
885
+ * @param layoutEngine The layout engine to use (default `"dot"`).
886
+ * @param options Optional images / extra files for the renderer.
887
+ * @returns The rendered output as a string.
888
+ */
889
+ layout(outputFormat: Format = "svg", layoutEngine: Engine = "dot", options?: Options): string {
890
+ if (options?.images?.length || options?.files?.length) {
891
+ // Write images/files to the Emscripten FS before rendering.
892
+ const helper = new this._module.CGraphviz(0, 0);
893
+ try {
894
+ createFiles(helper, options);
895
+ } finally {
896
+ helper.delete();
897
+ }
898
+ }
899
+ let retVal = "";
900
+ let errorMsg = "";
901
+ try {
902
+ retVal = this._graph.layout(outputFormat, layoutEngine);
903
+ } catch (e: any) {
904
+ errorMsg = (e as Error).message;
905
+ }
906
+ errorMsg = this._module.CGraphviz.lastError() || errorMsg;
907
+ if (!retVal && errorMsg) {
908
+ throw new Error(errorMsg);
909
+ }
910
+ return retVal;
911
+ }
912
+
913
+ /**
914
+ * Serialise the graph to a DOT-language string.
915
+ */
916
+ write(): string {
917
+ return this._graph.write();
918
+ }
919
+
920
+ /**
921
+ * Serialise the graph to a DOT-language string.
922
+ */
923
+ toDot(): string {
924
+ return this.write();
925
+ }
926
+
927
+ /**
928
+ * Release the underlying WASM object. Must be called when the graph is
929
+ * no longer needed (or use the `using` keyword with TypeScript ≥ 5.2).
930
+ */
931
+ delete(): void {
932
+ this._graph.delete();
933
+ }
934
+
935
+ /** @internal – supports the `using` keyword (explicit resource management). */
936
+ [Symbol.dispose](): void {
937
+ this.delete();
938
+ }
939
+ }
940
+
76
941
  /**
77
942
  * The Graphviz layout algorithms take descriptions of graphs in a simple text language, and make diagrams in useful formats, such as images and SVG for web pages or display in an interactive graph browser.
78
943
  *
@@ -80,6 +945,7 @@ let g_graphviz: Promise<Graphviz> | undefined;
80
945
  *
81
946
  * See [graphviz.org](https://graphviz.org/) for more details.
82
947
  *
948
+ * ### Rendering from a DOT string
83
949
  * ```ts
84
950
  * import { Graphviz } from "@hpcc-js/wasm/graphviz";
85
951
  *
@@ -88,6 +954,23 @@ let g_graphviz: Promise<Graphviz> | undefined;
88
954
  * const dot = "digraph G { Hello -> World }";
89
955
  * const svg = graphviz.dot(dot);
90
956
  * ```
957
+ *
958
+ * ### Programmatic graph construction
959
+ * ```ts
960
+ * import { Graphviz } from "@hpcc-js/wasm/graphviz";
961
+ *
962
+ * const graphviz = await Graphviz.load();
963
+ *
964
+ * using graph = graphviz.createGraph("G");
965
+ * graph
966
+ * .addNode("a")
967
+ * .addNode("b")
968
+ * .addEdge("a", "b")
969
+ * .setNodeAttr("a", "color", "red")
970
+ * .setGraphAttr("rankdir", "LR");
971
+ *
972
+ * const svg = graph.layout(); // render without a DOT round-trip
973
+ * ```
91
974
  *
92
975
  * ### Online Demos
93
976
  * * https://raw.githack.com/hpcc-systems/hpcc-js-wasm/main/index.html
@@ -377,4 +1260,52 @@ export class Graphviz {
377
1260
  nop2(dotSource: string): string {
378
1261
  return this.layout(dotSource, "dot", "nop2");
379
1262
  }
1263
+
1264
+ /**
1265
+ * Programmatically create a graph using the cgraph library.
1266
+ *
1267
+ * Returns a {@link Graph} builder that lets you add nodes, edges, and
1268
+ * attributes without writing DOT source by hand. Call
1269
+ * {@link Graph.toDot} to get the DOT string and pass it to
1270
+ * {@link layout} (or any convenience wrapper).
1271
+ *
1272
+ * **You must call {@link Graph.delete} when finished** to free the
1273
+ * underlying WASM memory, or use the `using` keyword (TypeScript ≥ 5.2).
1274
+ *
1275
+ * @param name The graph name (default `"G"`).
1276
+ * @param type The graph type (default `"directed"`).
1277
+ *
1278
+ * ```ts
1279
+ * using graph = graphviz.createGraph("G");
1280
+ * graph
1281
+ * .addNode("a")
1282
+ * .addNode("b")
1283
+ * .addEdge("a", "b")
1284
+ * .setNodeAttr("a", "color", "red")
1285
+ * .setGraphAttr("rankdir", "LR");
1286
+ *
1287
+ * const svg = graphviz.dot(graph.toDot());
1288
+ * ```
1289
+ */
1290
+ createGraph(name: string = "G", type: GraphType = "directed"): Graph {
1291
+ const directed = !type.includes("undirected") ? 1 : 0;
1292
+ const strict = type.startsWith("strict") ? 1 : 0;
1293
+ return new Graph(new this._module.CGraph(name, directed, strict), this._module);
1294
+ }
1295
+
1296
+ /**
1297
+ * Parse DOT source into a mutable {@link Graph}. The returned graph can
1298
+ * be queried, modified, rendered directly, or serialised back to DOT with
1299
+ * {@link Graph.toDot}.
1300
+ */
1301
+ read(dotSource: string): Graph {
1302
+ const graph = this.createGraph();
1303
+ try {
1304
+ graph.read(dotSource);
1305
+ return graph;
1306
+ } catch (e) {
1307
+ graph.delete();
1308
+ throw e;
1309
+ }
1310
+ }
380
1311
  }