Skip to content

Graphviz layouts ​

graphty-element can lay a graph out with Graphviz's own engines: dot, neato, fdp, sfdp, circo, twopi, osage and patchwork. They are not ports. They are Graphviz's C code compiled to WebAssembly by the optional package @hpcc-js/wasm-graphviz, so a graph drawn with graphviz-dot is placed exactly where Graphviz places it.

Install ​

bash
npm install @graphty/graphty-element @hpcc-js/wasm-graphviz

The package is an optional peer dependency. Without it graphty-element works as before, and a Graphviz layout fails with the code below instead of drawing.

Use one ​

ts
import "@graphty/graphty-element";

const element = document.querySelector("graphty-element")!;
element.viewMode = "2d";
element.nodeData = [{ id: "start" }, { id: "parse" }, { id: "run" }];
element.edgeData = [
    { src: "start", dst: "parse" },
    { src: "parse", dst: "run" },
];
await element.setLayout("graphviz-dot", { rankdir: "LR" });

The layout runs in a Web Worker, so the page stays responsive while Graphviz works.

Layout idEngineWhat it draws
graphviz-dotdotRanks, with edges pointing one way and few crossings
graphviz-neatoneatoDrawn distances that match graph distances
graphviz-fdpfdpSpring forces
graphviz-sfdpsfdpSpring forces, settled coarse to fine for large graphs
graphviz-circocircoEach tightly connected part on its own circle
graphviz-twopitwopiRings around a root
graphviz-osageosageA packed array
graphviz-patchworkpatchworkSquares tiled into one rectangle

Every Graphviz layout is two-dimensional.

Options ​

OptionTypeEnginesWhat it sets
rankdir"TB", "LR", "BT" or "RL"dotWhich way the ranks run
nodesepnumberdotSpace between nodes in a rank, in scene units
ranksepnumberdotSpace between ranks, in scene units
seednumberneato, fdp, sfdpGraphviz's start: the same seed, the same drawing
overlap"true", "false" or "scale"neato, fdp, sfdpWhether nodes may overlap, or how overlaps are removed
maxNodesnumberallThe node budget (below)
timeoutMsnumberallThe time budget (below)
attributesRecord<string, string>allAny other Graphviz graph attribute, as Graphviz text

attributes is the advanced path: { attributes: { splines: "ortho", mclimit: "4" } } hands those graph attributes to Graphviz as they are. The typed options win over the same name in attributes.

Each node is given to Graphviz as a circle as wide as graphty-element draws it (one scene unit is one inch to Graphviz), with no label. Edge routes are not used: graphty-element draws its own edges between the placed nodes. Two engines do not space nodes by their width, so graphty-element sizes what they do read: patchwork gets each node an area whose cell fits the drawn node, and twopi's rings are ranksep apart scaled to the widest drawn node, unless attributes sets ranksep.

DOT files ​

A DOT file loaded with layout: "recommended" and no coordinates is drawn by the engine it names in its layout attribute, or by dot when it names none. Its layout attributes (rankdir, nodesep, ranksep, splines, overlap, root and the others Graphviz reads) are passed on.

ts
await element.session.data.import({ type: "dot", config: { url: "/graphs/pipeline.gv" } }, { layout: "recommended" });

A file whose nodes all carry pos keeps those positions instead. Only the first graph of a file holding several is read and laid out.

The recommendation says which rule chose the layout, as a code (recommendLayout(...).fact.code):

CodeWhen
recommend.graphviz-fileThe file is a Graphviz file with no coordinates; params.engine names the engine
recommend.graphviz-unavailableThe file asked for a Graphviz engine but @hpcc-js/wasm-graphviz is not installed, so the next rule's layout is used

To ask whether the layouts can run on this page:

ts
import { graphvizAvailability } from "@graphty/graphty-element";

const { available } = await graphvizAvailability();

Budgets ​

Graphviz itself does not finish some engines on large graphs: at 10,000 nodes dot, neato and circo run for more than five minutes and fdp crashes. So every engine has a node budget, and a larger graph fails at once instead of hanging. Every layout also has a time budget of 60 seconds; past it the Worker is stopped. Neither ever switches to another engine.

EngineNode budget
dot1,500
neato2,000
fdp1,000
circo500
sfdp20,000
twopi20,000
osage20,000
patchwork20,000

Raise or lower either with maxNodes and timeoutMs.

When a Graphviz layout fails ​

It fails with a GraphtyError. code is the element's error code and details.reason says what happened in Graphviz:

details.reasoncodedetailsWhen
graphviz.unavailableE_MISSING_PACKAGEpackageThe package is not installed, or cannot load here
graphviz.syntaxE_PARSE_FAILEDengine, line, nearGraphviz could not read the graph
graphviz.errorE_UNSUPPORTEDengine, messagesGraphviz refused the graph
graphviz.crashedE_EXTENSION_FAILEDengine, messageThe engine crashed; the next layout starts fresh
graphviz.timeoutE_CAP_EXCEEDEDengine, msThe time budget ran out
graphviz.too-largeE_CAP_EXCEEDEDengine, nodes, limitThe graph is over the node budget

Every layout runs in a fresh Worker with a fresh WebAssembly instance, so a crash or a stopped layout leaves nothing behind for the next one.

Content Security Policy ​

A page with a Content Security Policy must allow WebAssembly compilation with 'wasm-unsafe-eval' in script-src, and allow Workers from its own origin (worker-src 'self'). Without them the layouts report graphviz.unavailable.

Bundlers ​

Use any bundler that understands new Worker(new URL("...", import.meta.url)) -- Vite, webpack 5, Parcel and Rollup with its worker plugin all do -- and the Worker and the package are bundled for you. The self-contained @graphty/graphty-element/bundle file does not include the package, so on a page with no bundler the Graphviz layouts report graphviz.unavailable.

License ​

@hpcc-js/wasm-graphviz is Apache-2.0, and the Graphviz code inside it is under the Eclipse Public License 1.0. graphty-element (MIT) does not include it: you install it yourself, and an application that ships it ships EPL-1.0 code.