npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

dag-layout

v1.2.0

Published

Graph Layout Helper

Readme

Directed Acyclic Graph Layout

This light weight module provides a graph object which can be used to calculate the positions of its nodes, minimizing total edge crossings or total edge distance. It does not use any third-part dependencies. The algorithm is permutation-based and uses discrete layers. The execution time can be indirectly controlled via threshold parameters.

How to use

Assumptions:

  • Your source graph nodes are or can be transformed to a list of objects which have a unique, numeric or string ID property
  • Your source edges are or can be transformed to pairs of node IDs

Step 1: Create a new Graph

You can pass one optional parameter which corresponds to the name of the node ID property:

const graph = new Graph();       // default ID property name is "id"
const graph = new Graph("uuid");  // use uuid field instead

Step 2: Populate your graph with nodes and edges from your model

The passed objects will not be mutated. In the example, I'm assuming the model objects are in an array "taskList". Each task object has an array property nextTasksId which contains the IDs of all tasks connected to it. Note that no layout related information is required. The tasks in taskList contain pure data.

taskList.forEach(graph.addNode);
taskList.forEach((element) =>
   element.nextTasksId.forEach((s) => graph.addEdge(element.id, s))
);

Step 3: Calculate the layout

The layout is calculated with following command. The method takes an optional configuration object, whose properties are also optional.

graph.layout();

or

const lastGraph = useRef(null);
...
// this is probably in a useEffect hook
graph.layout({ previous: lastGraph.current });
lastGraph.current = graph;

Step 4: Using the results

After the calculation, the nodes are wrapped into VirtualNodes providing layout properties. Additionally, new nodes have been created which represent placeholders that block space for edges. It is up to you whether you use these as focus points for your edges or simply connect the original nodes directly.

Layout properties can be fetched from VirtualNodes with following methods:

  • getData: This is a reference to the wrapped original node objects. null for virtual nodes
  • getFlow: 0-based layer index in flow direction. Transform this to an x or y position manually.
  • getOrth: this is a number in (0,1)-range, otrthogonal to the flow. 0.5 means the node is in the center. Transform this to an x or y position manually.
  • getGroupIndex: 0-based group index. This is only relevant in case your nodes are not connected (for example if the graph is WIP). Transform this to an x or y position manually, if you support unconnected nodes.
  • isVirtual: true means the node was generated during layouting, false means this is a node that was originally added to the graph.
  • getLayoutId: a unique, numeric ID that was assigned during layouting. It allows you to index both real and virtual nodes.

The results can be accessed with following Graph methods:

  • getNodes: returns an object which contains all passed IDs as keys and the corresponding nodes as values. The original node objects are wrapped into VirtualNodes. Use this if you don't care about virtual nodes.
  • getEdges: returns an object which contains all passed predecessor IDs as keys and all successor IDs are part of an array in the value. This also ignores virtual nodes.
  • getVirtualNodes: returns an array of all nodes, including the virtual ones. You can use the virtual nodes as anchor points for multilayer edges,
  • getVirtualEdges: returns an array of all virtual edges which contain references to the nodes they connect (virtual and real ones).
  • getMaxWidth: returns an array containing the maximum width (parallel nodes) of the groups of connected nodes
  • getMaxLength: returns an array containing the maximum length in flow direction of the groups of connected nodes
  • getGroupCount: returns the number of groups of connected nodes
  • getMetricsValue: returns the number of crosssings or the total distance. This can be used to compare the outcomes of several layout() iterations with different config parameters.

VirtualEdge offers following access methods:

  • isCyclic: returns whether this edge has been excluded from processing because it is introducing a cycle to the graph. Cyclic edges are added back after the processing.
  • getPredecessor: returns the VirtualNode predecessor reference
  • getSuccessor: returns the VirtualNode successor reference
  • getFrom: returns the VirtualNode predecessor reference wrapped in an array, if the node is real, otherwise it returns all real predecessors that the edge eventually leads to
  • getTo: returns the VirtualNode successor reference wrapped in an array, if the node is real, otherwise it returns all real successors that the edge eventually leads to

Configuration parameters

  • previous = null : You can skip repetitive calculations by passing another graph with the same node and edge structure. I recommend saving used graphs representing the same object in a ref and always passing it to layout(). It will copy the old model and patch it with the new node data (overwriting the old reference). If the graph structure has changed, it will be detected and the calculation will be run. Node data changes which are not affecting the structure will not trigger the calculation. Replacing the node data object with an updated version though a React state update is also a NOOP.
  • metrics = "crossings": There are 2 options deciding what should be minimized during layouting, both referring to edges: "crossing", "distance" "crossings" is a bit slower than "distance" but also looks a bit better IMO
  • alignNodes = "start": There are 5 options: "start", "center-start", "center-end", "end", "preset" This parameter decides how to position nodes in flow direction on edges that span more than one layer. Center offers 2 options because the layers are discrete. For odd edge lengths, both options will behave in the same manner. If you choose "preset", have to set the flow coordinate yourself (pass it as 2nd parameter to addNode()). In this case, set an integer value for each node starting with 0 as first layer.
  • mergeVirtualNodes = "start": There are 3 options: "start", "end", null. Every edge which is longer than one layer will create a placeholder node on each layer it passes. When this option is activated, it will try to reduce the number of virtual nodes by merging those with the same predecessors or successors into one. "start", "end" are both active options, but "start" priotitizes merging nodes with the same predecessors and "end" with the same successors. Also, "start" sweeps into flow direction, and "end" against it.
  • thresholdEvaluate = 500000: For big graphs with a lot of cross-edges, there is a tradeoff between performance and quality. The algorithm is permutation based and also tries to calculate multiple layers in one run. If you have n layers with widths w_1...w_n, the total possible amount of permutations is the product of w_i! with i from 1 to n. This threshold forces the evaluation as soon as the product of all layers in the frame exceeds it. Only a fraction of the theoretically possible permutations are actually calculated, the rest is skipped as soon as it is obvious they cannot improve the result. The graph is split when the number of actually evaluated permutations threatens to exceed the limit (when this happens, the ongoing evaluation is simply cancelled). Until then, the algorithm tries to add as many layers to the current frame as possible. The parameter is able to control the execution time effectively. It can only grow linear to the threshold and the length of the graph in flow direction, because the counter is reset after each evaluation. The default value should be small enough for real-time execution for medium-sized graphs (~50 nodes).
  • thresholdLayer = 21000: It is not enough to have a limit on the amount of layers that can be processed in one run. The amount of permutations increases extremely quickly and even one large layer might be enough to exhaust any reasonable limit. This is why it can be split into multiple overlapping parts if the amount of permutations exceeds the threshold. The default value allows to swap 10 parallel nodes in 4 adjacent, overlapping groups of 7 (7! times 4 = 20160).
  • debug = false : If true, this prints some debug output like time spent and metrics

Limitations

The nodes don't have to be fully connected (in this case, they will be split into connected groups - if you don't have any validation against that, use groupIndex). If the graph contains cycles, layouting is performed without cyclic edges. They are added back afterwards to the model (see isCyclic method). The threshold parameters prevent any scaling issues. It is recommended to set thresholdEvaluate <= 10 million, and thresholdLayer <= 100000. In any case, thresholdLayer cannot be smaller than thresholdEvaluate, also the product of thresholdLayer and thresholdEvaluate cannot be set above Number.MAX_SAFE_INTEGER.