# d3-dag layers directed acyclic graphs in the browser without a 500KB layout engine

> d3-dag is a TypeScript-first library for laying out directed acyclic graphs, offering sugiyama, Zherebko and grid layouts with optimal edge crossing minimisation, and a dagre-compatible API that makes it a drop-in swap inside React Flow. The trade-off is that it only handles acyclic graphs, and its quality presets put crossing minimisation cost in front of you as a number.

**erikbrinkman/d3-dag** — Layout algorithms for visualizing directed acyclic graphs

- Repository: https://github.com/erikbrinkman/d3-dag
- Website: https://erikbrinkman.github.io/d3-dag/
- Stars: 1,527 · Forks: 93
- Language: TypeScript
- License: MIT
- Published: 2026-10-16 · Updated: 2026-10-16 · Language: en
- Canonical page: https://hysenlabs.com/projects/erikbrinkman-d3-dag

## Layout for graphs that have no cycles

d3-dag is described as a lightweight, TypeScript-first DAG layout for the web, providing layered graph layout algorithms for directed acyclic graphs. The word acyclic is the constraint that defines the whole library, and it is the right constraint for the graphs people actually want to lay out on the web: dependency trees, build pipelines, inheritance hierarchies, state machines, and the layered flows that a diagramming tool draws.

The audience is a front end developer who already has a graph and needs coordinates for it. The library is published to npm, is an ES module with no side effects, and ships type definitions, so it fits a modern bundler based build without configuration. Node 20.19 or newer is required.

The reasons the project gives for existing are specific rather than generic. It offers optimal edge crossing minimisation, multiple coordinate assignment strategies, and layout algorithms described as not available elsewhere in JavaScript, namely Zherebko and Grid. It calls itself a small bundle, a fraction of elkjs's roughly 500KB of transpiled Java. And it is TypeScript-first, with full type safety through generic operators and immutable builders; the documentation concedes that dagre v3 added TypeScript support, and argues that the generic operator pattern provides deeper type safety.

That last point is the design idea. Instead of a layout function with a bag of options, the algorithms are composed from operators, each of which implements a stage of the pipeline, and the types carry through the composition. The result is a library where the pipeline is visible in the code.

## Three families of layout, chosen by the shape you want

Three layout families are documented, and the distinction between them is the main decision a user makes.

Sugiyama is a layered layout, and it is the default. It is the classic pipeline: assign layers to nodes, order the nodes within layers to reduce crossings, then assign coordinates so the result looks tidy. The README notes that dagre v3 added a customOrder callback and ordering constraints so that users can bring their own node ordering algorithm, whereas d3-dag ships several built-in strategies including ILP-optimal crossing minimisation, along with multiple coordinate assignment and layering algorithms not available in dagre. The integer linear programming solver is in the dependency list, which is the concrete trace of the optimal strategy.

Zherebko is a linear topological layout, and Grid is a grid-based topological layout. Both are selected by name:

```ts
grf.setGraph({ algorithm: "zherebko" }); // linear topological layout
grf.setGraph({ algorithm: "grid" });     // grid-based topological layout
```

A layered Sugiyama layout is what you want when the graph has a clear progression, such as a pipeline where each stage depends on the previous one. A topological layout is what you want when the ordering matters more than the layering, and it tends to read as a sequence rather than as tiers.

Within the Sugiyama family the stages are individually selectable, and the operator form makes that visible in the code:

```ts
import { dagre, sugiyama, decrossOpt, coordQuad } from "d3-dag";

const grf = new dagre.graphlib.Graph();
grf.setGraph({ rankdir: "LR", nodesep: 50, ranksep: 100 });
// ... add nodes/edges ...
dagre.layout(grf);

// Or upgrade to better algorithms
dagre.layout(grf, sugiyama().decross(decrossOpt()).coord(coordQuad()));
```

The same layout call is made either way, and the second call swaps the decrossing strategy for the optimal one and the coordinate assignment for a quadratic method. The layout function is stable and the algorithm is a parameter, which is the ergonomics that the operator pattern buys.

## A dagre compatible API, with the gaps written down

The migration story is the strongest part of the project. Replacing the import is the whole change for most code:

```ts
import { dagre } from "d3-dag";
```

The supported dagre API is enumerated in a table. Setup covers setGraph, graph, setDefaultNodeLabel, setDefaultEdgeLabel, isDirected, isCompound and isMultigraph. Node methods cover setNode, setNodes, removeNode, hasNode, node, nodes, nodeCount and filterNodes. Edge methods cover setEdge, removeEdge, hasEdge, edge, edges, edgeCount and setPath. Traversal covers predecessors, successors, neighbors, inEdges, outEdges, nodeEdges, sources and sinks. And layout is dagre.layout(grf) with an optional Operator, such as sugiyama() or zherebko().

The unsupported parts are listed just as clearly. Compound graphs are out, so setParent, parent and children do not exist. Serialisation is out, so json.write and json.read are not provided. And dagre's alg.* algorithms are not carried over, with the documentation pointing at d3-dag's native operators instead.

The notable differences are the sort that only bite after an afternoon of debugging, so it is good that they are written down. setGraph accepts additional quality, ranker and algorithm options. graph() returns a shallow copy of the config, so mutating it does nothing and you must call setGraph. Default nodesep and ranksep are 50, the same as dagre. Every node needs a positive width and height, and unlike dagre, which tolerates zero-sized nodes, a node created without dimensions, including one auto-created by setEdge or one whose default node label returns no size, throws at layout time. And edge(v, w) returns the same label object each call with its points recomputed on each read, so a label captured before layout reflects the laid-out points afterwards, while any label passed to setEdge is ignored.

That last behaviour is the one to read twice. A label passed to setEdge being ignored is not a bug report, it is documented semantics, and it is exactly the kind of thing that would otherwise cost an afternoon.

## Quality presets make the speed trade-off explicit

The dagre-compatible API takes a quality option that controls the layout speed and quality trade-off, and the documentation publishes both the presets and their cost:

```ts
grf.setGraph({ quality: "fast" }); // faster layout, lower quality
grf.setGraph();                  // default: "medium", better layout, slower
```

The fast preset uses simplex layering, DFS decrossing and greedy coordinates, and the documentation records 5.1 ms on a 184 node graph, described as roughly four times faster than dagre v3. The medium preset, which is the default, uses simplex layering, two-layer decrossing and simplex coordinates, recorded at 49 ms, roughly twice slower than dagre v3. The slow preset uses simplex layering, optimal decrossing and simplex coordinates, and is marked as for small graphs only.

Those numbers are for a specific graph of 184 nodes, and the shape of that graph matters as much as its size, so treat them as an indication rather than a promise for your data. The qualitative picture is what generalises: crossing minimisation is the expensive stage, the fast preset uses a depth first search approximation of it, and the slow preset solves it optimally.

The quality and ranker options apply only when using the sugiyama algorithm, which is the default, so setting them with the zherebko or grid algorithms has no effect. The recommendation in the documentation is to use fast when layout speed matters more than visual quality, for instance for interactive or animated graphs, and to keep the default medium when a better layout is worth roughly twice the time.

For a user building a live editor or a graph that re-lays-out on every change, that default is the first thing to change, because the layout runs on every render.

## Installing it, and using it from React Flow

For a Node project the install is the usual one, and Bun works too:

```bash
npm i d3-dag
```

For a page without a build step, the package can be loaded from unpkg as a browser bundle:

```html
<script src="https://unpkg.com/d3-dag@1.1.0"></script>
<script>
const dag = d3.graphStratify(...);
const layout = d3.sugiyama();
layout(dag);
```

The React Flow case is the one the project leads with. React Flow users who want different layouts from dagre can treat d3-dag as a drop-in replacement, and the documentation walks through a getLayoutedElements function that builds a dagre graph, sets the rank direction, assigns a default width of 172 and height of 36 to each node, adds the edges, runs the layout and converts the resulting positions into the node positions React Flow expects.

The reasoning behind the compatibility layer is worth naming. React Flow's layout is a plugin point, and many applications have already written their layout function against dagre's graphlib API. A library that speaks that API can be adopted by changing an import and a version pin, rather than by rewriting the layout step. The cost is that d3-dag must carry dagre's quirks, including the ones in the difference list above, and the project is candid that a few dagre features are not supported at all.

The documentation also hosts a side-by-side comparison page of d3-dag against dagre, plus separate example galleries for each of the three layout families, which is the fastest way to see whether the visual result suits your graph before you write the integration.

## MIT licensed, typed, and checked by the tooling

The licence is MIT, with the LICENSE file at the repository root, and the package metadata records the same. There is no copyleft obligation, so the library can be used in a commercial front end without a licensing discussion.

The release history is steady and the versions are recent: v1.2.0 on 2026-04-13, v1.2.1 on 2026-04-14 and v1.2.2 on 2026-07-05, with the package version in the manifest at 1.2.2. The last push was on 2026-10-01 and the repository is not archived, so the work is current.

The packaging shows care that is unusual for a small library. The package is an ES module with sideEffects declared false, which lets a bundler tree-shake it, and it ships both a module build and an IIFE bundle for browsers, with the type definitions resolved through the exports map. The scripts include a check that runs publint in strict mode together with attw against the packed tarball for an ESM-only profile, which is a way of catching packaging mistakes that only appear after publish. Formatting and linting are done with Biome, the type check is the TypeScript compiler, and the documentation is built with typedoc, with a lint recipe that emits the docs without writing them.

The dependencies are few and each is justified by a feature: d3-array, a linear programming solver for the optimal crossing minimisation, quadprog for the quadratic coordinate assignment, and stringify-object. The example graphs are in the repository as JSON files, including a square, a triangle, a genealogy, a dag and a materials graph, so you can reproduce the documented layouts with your own data without inventing a test case.

One detail worth planning for: the unpkg example in the documentation pins version 1.1.0 while the current release is 1.2.2, so copy the version from the npm page rather than from the readme when you use the browser build.

## Conclusion

d3-dag suits a web team drawing dependency graphs, pipelines or inheritance trees that needs better edge crossing minimisation than a general purpose layout engine provides, and it suits React Flow users who want to swap the layout engine without rewriting their graph code. It is the wrong choice if your graph contains cycles, because the library is built for directed acyclic graphs, and a poor match if you need compound graphs or dagre's serialisation helpers, all of which are explicitly unsupported. Before adopting, try the quality presets on a graph of your own size, since the documented timings are for a 184 node graph, and give every node a positive width and height, because a node without dimensions throws at layout time rather than rendering.

## FAQ

### How do I install d3-dag and lay out a graph?

Run npm i d3-dag, or bun add d3-dag, or load the browser bundle from unpkg. Build a graph, then call a layout such as d3.sugiyama() and apply it, reading the node x and y coordinates and the link points.

### Can d3-dag replace dagre in a React Flow project?

Yes. Import { dagre } from "d3-dag" instead of the dagre package and most graph construction and layout methods work the same, including the rankdir, nodesep and ranksep options. Compound graphs, dagre's JSON serialisation and its alg.* algorithms are not supported.

### What do the d3-dag quality presets change?

The quality option trades layout speed for visual quality. The fast preset uses simplex layering, DFS decrossing and greedy coordinates and is documented at 5.1 ms for a 184 node graph, the default medium preset uses two-layer decrossing and simplex coordinates at 49 ms, and the slow preset uses optimal decrossing for small graphs only.

### Does d3-dag support graphs with cycles?

The library is built for directed acyclic graphs, which is stated in its description as layered graph layout algorithms for directed acyclic graphs. Its layouts assume a topological ordering exists, so a cyclic graph is outside its scope.

## Sources

- [erikbrinkman/d3-dag on GitHub](https://github.com/erikbrinkman/d3-dag)
- [License: MIT](https://github.com/erikbrinkman/d3-dag/blob/main/LICENSE)
- [Project website](https://erikbrinkman.github.io/d3-dag/)
- [README](https://github.com/erikbrinkman/d3-dag/blob/main/README.md)
- [Releases](https://github.com/erikbrinkman/d3-dag/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/erikbrinkman-d3-dag
