d3-dag

repository·main·Indexed 23 days ago

https://github.com/erikbrinkman/d3-dag

A lightweight, TypeScript-first library for performing layered graph layouts for directed acyclic graphs (DAGs) in web applications. It features a dagre-compatible API for use as a drop-in replacement in tools like React Flow, supporting various topological layout algorithms including Sugiyama, Zherebko, and grid-based layouts. The library utilizes an immutable Operator pattern for fine-grained layout control and provides utilities like graphConnect() for constructing graphs from link data.

Tokens
13.5K
Snippets
34
Records
79
Agent score
81%

What's inside d3-dag

  1. Use the dagre-compatible wrapper in d3-dag

    main

    d3-dag provides a wrapper that allows you to use the dagre API while leveraging the d3-dag layout engine for the actual calculations. This is useful for migrating existing projects that rely on dagre syntax.

    When using this wrapper, you can control the speed/quality trade-off of the layout using the Quality preset. For a complete list of supported methods and variables, refer to the official dagre API documentation.

  2. Understand the Operator pattern in d3-dag

    main

    d3-dag is built around Operators. Operators use a fluent interface to modify behavior, but they are immutable. Every function call that modifies an operator returns a new copy of the operator and does not mutate the original instance.

    When configuring an operator (like graphStratify), you must capture the returned instance to use the updated configuration.

    // INCORRECT: stratify is not modified in place
    const stratify = graphStratify();
    stratify.id(({ myid }: { myid: string }) => myid);
    const dag = stratify([{ myid: "parent" }, { myid: "child", parentIds: ["parent"] }]);
    
    // CORRECT: Capture the new instance returned by the method
    const myStratify = stratify.id(({ myid }: { myid: string }) => myid);
    const dag = myStratify([{ myid: "parent" }, { myid: "child", parentIds: ["parent"] }]);
  3. Install d3-dag

    main

    You can install d3-dag using npm or bun for Node.js environments, or load it via unpkg for direct browser usage.

    npm:

    npm i d3-dag

    bun:

    bun add d3-dag

    Browser (unpkg):

    <script src="https://unpkg.com/d3-dag@1.1.0"></script>
  4. Migrate from dagre to d3-dag

    main

    To migrate, replace import dagre from "dagre" with import { dagre } from "d3-dag".

    Supported API

    • Setup: setGraph, graph, setDefaultNodeLabel, setDefaultEdgeLabel, isDirected, isCompound, isMultigraph
    • Nodes: setNode, setNodes, removeNode, hasNode, node, nodes, nodeCount, filterNodes
    • Edges: setEdge, removeEdge, hasEdge, edge, edges, edgeCount, setPath
    • Traversal: predecessors, successors, neighbors, inEdges, outEdges, nodeEdges, sources, sinks
    • Layout: dagre.layout(grf) with optional Operator (e.g., sugiyama() or zherebko())

    Unsupported API

    • Compound graphs: setParent, parent, children
    • Serialization: json.write, json.read
    • Algorithms: alg.* (use d3-dag's native operators instead)

    Important Differences

    • Node Dimensions: Every node must have a positive width and height. Unlike dagre, nodes with zero dimensions (including those auto-created by setEdge without labels) will throw an error at layout time.
    • Graph Config: graph() returns a shallow copy of the config; use setGraph for mutations.
    • Edge Labels: edge(v, w) returns the same label object each call, and its points is recomputed on each read. Any label passed to setEdge is ignored; instead, use setDefaultEdgeLabel or similar mechanisms.
  5. Use d3-dag as a drop-in replacement for dagre in React Flow

    main

    If you are using React Flow, you can replace dagre with d3-dag to use its advanced layout algorithms. The dagre export from d3-dag provides a compatible API for graph construction and layout.

    To use it, construct a dagre.graphlib.Graph, set node dimensions (width and height are required), add edges, and call dagre.layout(grf).

    import { dagre } from "d3-dag";
    
    function getLayoutedElements(nodes, edges, direction = "TB") {
      const grf = new dagre.graphlib.Graph();
      grf.setGraph({ rankdir: direction });
      grf.setDefaultEdgeLabel(() => ({}));
      for (const node of nodes) {
        grf.setNode(node.id, {
          width: node.measured?.width ?? 172,
          height: node.measured?.height ?? 36,
        });
      }
      for (const edge of edges) grf.setEdge(edge.source, edge.target);
      dagre.layout(grf);
      return {
        nodes: nodes.map((node) => {
          const pos = grf.node(node.id);
          return {
            ...node,
            position: { x: pos.x - pos.width / 2, y: pos.y - pos.height / 2 },
          };
        }),
        edges,
      };
    }
  6. How d3-dag handles type inference with Ops

    main

    d3-dag uses Ops types to track typing requirements dynamically as callbacks are passed. This allows for sound type inference, but it requires users to be explicit with types in anonymous functions to avoid never types.

    When using operators that take callbacks (like coordinate assignments or weights), you should explicitly type the arguments in your callback functions so the library can correctly infer the data type.

  7. Use the Graph interface for immutable graph traversal

    main

    The Graph interface represents an immutable collection of GraphNodes and GraphLinks. It provides methods to iterate over the graph structure and query its properties.

    Key Methods:

    • nodes(): Returns an iterator over every node in the graph.
    • links(): Returns an iterator over every link in the graph.
    • topological(rank?: Rank): Computes a topological order of the nodes. If the graph contains cycles, it attempts to minimize edge inversions. You can provide a Rank accessor to constrain node positions.
    • roots(): Returns nodes that are not descendants of any other node (handles cycles by picking a minimal set).
    • leaves(): Returns nodes that are not ancestors of any other node (handles cycles by picking a minimal set).
    • sources(): Returns nodes with in-degree zero (no parents).
    • sinks(): Returns nodes with out-degree zero (no children).
    • split(): Returns an iterator over the individual connected components of the graph.
    • connected(): Returns true if the graph is a single connected component.
    • multi(): Returns true if any node has multiple links to the same child.
    • acyclic(): Returns true if the graph contains no cycles.
    • nnodes(): Returns the number of nodes.
    • nlinks(): Returns the number of links.
  8. Define node sizes for layouts

    main

    When configuring a layout, you can specify how the size of each node is determined using a NodeSize. A NodeSize can be either a constant tuple of [width, height] or a callable function that takes a GraphNode and returns a [number, number] tuple.

    Important Note on Types: Due to TypeScript inference, using a constant function like () => [1, 1] might infer data types as never. To avoid errors, use a constant tuple [1, 1] instead of a function if the size is uniform.

    Example of a dynamic node size based on data:

    function widthSize({ data }: GraphNode<{ name: string }>): [number, number] {
      return [data.name.length, 1];
    }
  9. Interact with GraphNode and its neighborhood

    main

    A GraphNode behaves like a Graph representing only its own connected component. It provides access to its own data and its immediate neighborhood.

    Node Properties:

    • data: The user-provided datum attached to the node.
    • x, y: The raw coordinates of the node (throws if ux or uy are undefined).
    • ux, uy: The raw (possibly undefined) x and y coordinates.

    Neighborhood Methods:

    • parents(): Iterator over unique parent nodes.
    • children(): Iterator over unique child nodes.
    • parentLinks(): Iterator over links coming from parents.
    • childLinks(): Iterator over links going to children.
    • parentCounts(): Iterator of [GraphNode, number] representing parents and the number of links from them.
    • childCounts(): Iterator of [GraphNode, number] representing children and the number of links to them.
    • ancestors(): Iterator of all nodes reachable through parents (includes the node itself).
    • descendants(): Iterator of all nodes reachable through children (includes the node itself).
    • nparents(), nchildren(), nparentLinks(), nchildLinks(): Counts for parents, children, and links.
  10. Understand SugiDatum and the Sugiyama graph structure

    main

    A Sugiyama graph (sugi graph) is a specialized representation used during the Sugiyama layout process. It consists of two types of nodes defined by SugiDatum:

    1. Node Datum (role: "node"): Represents an actual node from your original graph. It includes topLayer and bottomLayer (the range of layers the node spans) and a reference to the original node.
    2. Link Datum (role: "link"): Represents a 'dummy node' inserted into a link to facilitate multi-layer spanning. It includes the layer it resides on and a reference to the original link.

    This structure ensures the graph is a non-multi DAG where every edge connects nodes in adjacent layers.

  11. Implement a custom Layering operator

    main

    A Layering operator is responsible for assigning every node in a graph a y-coordinate that respects a provided Separation function. The operator must return the total height of the layout, such that all node coordinates plus their separation are within the returned height.

    While built-in operators cover most use cases, you can implement a custom one if your nodes already contain y-coordinate data. Note that a custom implementation must still respect the sep function to be valid.

    function exampleLayering<N extends { y: number }, L>(dag: Graph<N, L>, sep: Separation<N, L>): number {
        // determine span of ys
        let min = Infinity;
        let max = -Infinity;
        for (const node of dag) {
            const y = node.y = node.data.y;
            min = Math.min(min, y - sep(undefined, node));
            max = Math.max(max, y + sep(node, undefined));
        }
        // assign ys
        for (const node of dag) {
            node.y -= min;
        }
        return max - min;
    }
  12. Use the MutGraph interface to build and modify graphs

    main

    The MutGraph interface extends Graph and allows for structural modifications, such as adding nodes and links or removing them.

    Modification Methods:

    • node(...datum): Adds a new node to the graph. If NodeDatum allows undefined, the datum can be omitted.
    • link(source, target, ...datum): Adds a new link between two existing MutGraphNodes. If LinkDatum allows undefined, the datum can be omitted.

    Note on Performance: Adding links runs union-find internally to track connectivity. While modifications are efficient, some queries may run in linear time if called between modifications.