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

ego-graph

v0.1.3

Published

Radial ego-network layouts with branch folding, for React Flow and anything else

Readme

ego-graph

npm version CI License: MIT

Radial layouts for ego graphs — a graph seen from one node — with optional branch folding. Built for React Flow, but the core knows nothing about it.

Most layout libraries draw hierarchies (dagre, elkjs) or let physics decide (d3-force). Neither is what you want when there is one node the whole picture is about: a customer and their connections, a person and their network, an account and everything touching it. That is an ego graph, and it wants the root in the middle with its neighbourhood arranged around it.

Documentation

Full documentation and examples Full documentation and examples

full react flow example with source

Install

npm install ego-graph

Zero dependencies. @dagrejs/dagre is an optional peer — install it only if you use radialDagre or sectoredDagre:

npm install ego-graph @dagrejs/dagre

Quick start

import { radialDagre } from 'ego-graph';
import { applyPositions, retargetHandles, toLayoutGraph } from 'ego-graph/react-flow';

const graph = toLayoutGraph(nodes, edges, 'customer-42');
const positions = radialDagre(graph);

const laidOut = applyPositions(nodes, positions);
const rewired = retargetHandles(laidOut, edges);

The core takes { id, width, height } nodes and { source, target } edges and returns Map<id, { x, y }> of top-left positions. It is synchronous, has no dependencies, and does not import React. The react-flow adapter is four pure functions and does not import React either.

The three layouts

All three centre the root at the origin and place unreachable nodes in pockets on an outer arc. They differ only in how a branch is drawn.

| | | dependency | |---|---|---| | radialDagre | A ring of level-1 branches, each drawn by Dagre pointing outward. The workhorse — deep, uneven branches stay legible. | @dagrejs/dagre | | sectoredDagre | Every node lays out its own children, splitting them across sectors once there are more than five. Balances better on wide, shallow graphs. | @dagrejs/dagre | | polarPetal | Pure polar placement all the way down: each node spreads its children over a hemisphere centred on the ray it arrived along. | none |

@dagrejs/dagre is an optional peer dependency. It is reachable from exactly one module, so importing only polarPetal leaves it out of your bundle entirely — 8 kB minified against 49 kB.

Classifying your graph

Two optional predicates, and they are orthogonal axes:

radialDagre(graph, {
  // Does this edge define hierarchy? Default: every edge does.
  isStructural: (edge) => edge.type !== 'same-ip',
  // Keep this node out of the tree and park it on the periphery. Default: none.
  isSatellite: (node) => node.type === 'shared-attribute',
  // How many nodes does this one stand for? Default: 1.
  getWeight: (node) => node.data.memberCount ?? 1,
});

An associative edge (one where isStructural is false) is still rendered — it simply never influences position. This is not the same as being unimportant; it may be the most interesting relationship you have. It is just not hierarchical.

A satellite never joins the tree no matter what edges it has. That is the point: an attribute shared by six otherwise-unrelated people would, if allowed into the hierarchy, drag all six under it and wreck the star. Instead it gets a pocket on the outer arc, with its own island laid out locally.

See CONTEXT.md for the full vocabulary.

Folding

For graphs too large to show at once, collapse whole branches:

import { foldGraph } from 'ego-graph/fold';

const plan = foldGraph(graph, { threshold: 10, isSatellite, isStructural });
// { folds: [{ rootId, memberIds, internalEdgeCount }],
//   mergedEdges: [{ source, target, mergedFrom, allAssociative }],
//   heldOpen: string[], foldOf: Map<memberId, rootId> }

foldGraph returns a plan, never nodes. It says what collapses; you decide what a collapsed branch looks like — its id, its size, its label, its payload. The shallowest branch exceeding threshold folds, so expanding one re-applies the rule to its children and folds nest. Pass expandedRoots to hold specific branches open; they come back in heldOpen so you can offer a regroup control.

Layout runs before React Flow measures

This bites everyone once. React Flow does not know how big a node is until it has rendered it, so the first layout pass runs on estimates. You need two:

function AutoLayout({ nodes, edges, root }) {
  const { setNodes, setEdges } = useReactFlow();
  const nodesInitialized = useNodesInitialized();

  useEffect(() => {
    if (!nodesInitialized) return;
    const positions = radialDagre(toLayoutGraph(nodes, edges, root));
    const laidOut = applyPositions(nodes, positions);
    setNodes(laidOut);
    setEdges(retargetHandles(laidOut, edges));
  }, [nodesInitialized]);

  return null;
}

Render it as a child of <ReactFlow>. Until then, toLayoutGraph falls back to 180×56 per node; pass a fourth argument to change that.

Edge handles

retargetHandles re-points every edge at whichever handle faces its counterpart, which is what stops edges from crossing their own nodes after a radial layout. For that to work your node must actually render the handles it names. Eight of them, invisible:

import { Handle, Position } from '@xyflow/react';

function OmniHandles() {
  const style = { opacity: 0, border: 0 };
  return (
    <>
      <Handle type="target" position={Position.Top} id="t" style={style} />
      <Handle type="target" position={Position.Right} id="r" style={style} />
      <Handle type="target" position={Position.Bottom} id="b" style={style} />
      <Handle type="target" position={Position.Left} id="l" style={style} />
      <Handle type="source" position={Position.Top} id="st" style={style} />
      <Handle type="source" position={Position.Right} id="sr" style={style} />
      <Handle type="source" position={Position.Bottom} id="sb" style={style} />
      <Handle type="source" position={Position.Left} id="sl" style={style} />
    </>
  );
}

Bare sides for targets, s-prefixed for sources. If your nodes name handles differently, pass a mapper: retargetHandles(nodes, edges, (side, kind) => ...).

Spacing

Six dials, all optional. Defaults are tuned for roughly 180×56 nodes — if yours are much bigger, scale them up, because nothing here derives spacing from the nodes it is spacing.

| option | default | | |---|---|---| | nodeSep | 80 | gap between siblings inside a Dagre sub-layout | | rankSep | 100 | gap between ranks inside a Dagre sub-layout | | minRingRadius | 220 | floor on the first ring, so level-1 clears the root | | ringPadding | 60 | extra space between adjacent branches | | satelliteGap | π/3 | satellites within this angle of each other get fanned | | minSatelliteGap | π/12 | floor on the gap between fanned satellites |

Exported as DEFAULT_SPACING.

Contributing

Please read CONTRIBUTING.md. Short version: open an issue first — we triage there and invite collaborators when we want a change. Unsolicited PRs may be closed. See also the Code of Conduct and security policy.

Licence

MIT