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

@peterhudongpin/sna.js

v0.4.0

Published

A browser-compatible TypeScript/JavaScript port of the R sna social network analysis package.

Downloads

624

Readme

SNA.js

SNA.js is a TypeScript/JavaScript port of the R sna package (Carter T. Butts) into a browser- and Node-compatible social network analysis library.

The goal is not a blind line-by-line translation. The goal is to preserve user-visible semantics, make the algorithms usable in web applications, and prove parity with the R reference implementation with executable tests instead of asserting it.

The npm package is @peterhudongpin/sna.js (the npm registry reserves bare sna.js-style names that are too similar to existing packages). The project itself is simply called SNA.js.

Status

Version 0.1.0 — research preview.

  • The core statistics and centrality families (degree, density, geodesics, betweenness/closeness/stress/load/graph/eigenvector centrality, Bonacich power, information centrality, prestige, Gil-Schmidt, flow betweenness, dyad/triad census, reciprocity, transitivity, Krackhardt indices, components, reachability, k-cores, symmetrize, centralization) are gated by a golden parity suite: 207 fixture cases generated by actually running R sna 2.8 over a 16-graph corpus that includes valued ties, missing (NA) ties, asymmetric matrices in graph mode, self-loops, and disconnected graphs. See Parity testing.
  • Statistical models are partially parity-gated: netlm/netlogit (classical null), lnam point estimates, and the bbnam.fixed closed-form posterior are compared against R in the golden suite. Their permutation/CUG nulls and MCMC draws are stochastic (seeded-reproducible, but never bit-identical to R), and bn, pstar, and role/blockmodel analysis remain experimental.
  • Stochastic functions (rgraph, qaptest, cugtest, …) cannot match R's RNG stream bit-for-bit; pass { seed } for reproducible JavaScript runs. See Divergence from R.

Install

npm install @peterhudongpin/sna.js   # library only; three is NOT pulled in
npm install three         # only if you use @peterhudongpin/sna.js/visualization/three

Quick start

import { betweenness, degree, gden, geodist, triadCensus } from "@peterhudongpin/sna.js";

const graph = [
  [0, 1, 0, 0],
  [1, 0, 1, 0],
  [0, 1, 0, 1],
  [0, 0, 1, 0],
];

degree(graph, { mode: "graph" });                    // [1, 2, 2, 1]
gden(graph, { mode: "graph" });                      // 0.5
betweenness(graph, { mode: "graph" });               // [0, 2, 2, 0]
geodist(graph, { mode: "graph" }).distances;         // shortest-path matrix
triadCensus(graph, { mode: "graph" });               // named triad counts

Like R sna, tie values are used by default where R uses them (degree, gden, evcent, …) and ignored where R ignores them (betweenness, closeness, geodist, …). Pass ignoreEval: true/false explicitly to override per call.

Browser / CDN

<script type="module">
  import { gden } from "https://cdn.jsdelivr.net/npm/@peterhudongpin/[email protected]/dist/browser.min.js";
  console.log(gden([[0, 1], [0, 0]], { mode: "digraph" }));
</script>

@peterhudongpin/sna.js/browser remains available as a bundler subpath and resolves to the same code as the root entry.

Subpath entries

  • @peterhudongpin/sna.js — the analysis core (this is all most users need).
  • @peterhudongpin/sna.js/display — R-style print*/summary*/plot* helpers.
  • @peterhudongpin/sna.js/compat — the snaR R-name table and package hooks.
  • @peterhudongpin/sna.js/visualization and /visualization/three — SVG and Three.js renderers.

The display helpers and snaR are still re-exported from the root for compatibility, but those root re-exports are deprecated; new code should import the subpaths. Every entry's export list is pinned by an API-surface test, so unintended surface changes fail CI. Three.js-based 3D helpers stay isolated under @peterhudongpin/sna.js/visualization/three; importing the root package never touches Three.js.

Graph inputs

Graph inputs may be square adjacency matrices or explicit edge-list objects:

const matrix = [
  [0, 1, 0],
  [0, 0, 1],
  [0, 0, 0],
];

const edgeList = {
  order: 3,
  directed: true,
  edges: [
    [0, 1],       // tail, head
    [1, 2, 2],    // tail, head, value
  ],
};

Common GraphOptions:

  • mode: "digraph" | "graph" selects directed or undirected interpretation. Matrix data is used as given either way, exactly like R — an asymmetric matrix with mode: "graph" is not silently symmetrized. Use symmetrize: "weak" | "strong" | "upper" | "lower" to opt in.
  • directed can override mode.
  • diag: true preserves loops; by default loops are suppressed, as in R.
  • ignoreEval treats valued ties as binary. Per-function defaults match R sna (see table in the source of each function).
  • threshold dichotomizes numeric ties by absolute value during graph normalization.
  • indexBase: 1 converts one-based edge-list vertices at input boundaries.
  • NaN represents an R-like missing tie (NA); null, undefined, and non-finite values are treated as no tie.

Visualization

Visualization lives outside the core entry so import "@peterhudongpin/sna.js" stays lightweight. The 2D entry returns structured drawing data plus an SVG string:

import { gplot, plotSociomatrix } from "@peterhudongpin/sna.js/visualization";

const plot = gplot(graph, { mode: "circle", label: ["A", "B", "C"], displaylabels: true });
container.innerHTML = plot.svg;

Security note: plot.svg escapes & < > " ' in labels and attributes, so inserting it into the DOM as above is safe for the generated markup itself. If you post-process or concatenate the SVG string with other user-supplied content, sanitize that content yourself.

The 3D entry imports Three.js only from @peterhudongpin/sna.js/visualization/three (requires the optional three peer dependency):

import { gplot3d } from "@peterhudongpin/sna.js/visualization/three";
const result = gplot3d(graph, { mode: "random", seed: "demo" });
renderer.render(result.scene, result.camera);

Parity testing against R

Parity is enforced, not claimed:

  • scripts/generate-r-snapshots.R runs the real R sna 2.8 over a deterministic 16-graph corpus (paths, stars, rings, bipartite, transitive/asymmetric triads, valued n=20, 10% NA n=20, sparse n=50 digraph, disconnected, self-loops) and writes fixtures/r-sna-2.8/parity.json with provenance (R version, sna version, seeds, timestamp).
  • tests/parity/parity.test.ts replays all 207 cases through the TypeScript port with a relative tolerance of 1e-9 (1e-6 for iterative linear-algebra paths).
  • CI runs the parity suite from the committed fixtures on every push; regenerating fixtures requires a local R with sna 2.8 (npm run r:parity).

To extend coverage, add cases to the R generator, rerun npm run r:parity, and commit the updated fixture.

Divergence from R

Deliberate, documented differences — everything else is a bug, and the parity suite exists to catch it:

| Area | R sna 2.8 | SNA.js | |---|---|---| | Vertex indices | 1-based | 0-based everywhere (isolates, nodes, edge lists, …) | | evcent non-convergence | Returns the stale power-method iterate with a console warning (e.g. on bipartite graphs) | Detects non-convergence and falls back to the dense eigen solver, returning the correct principal eigenvector | | closeness(gmode="graph", cmode="suminvdir") | Crashes (internal typo maps to a nonexistent mode) | Treated as suminvundir | | degree cmode "total" | Not an R mode | Accepted as an alias of "freeman" | | dyadCensus / triadCensus return shape | 1-row matrices | Named objects ({ mutual, asymmetric, nullDyads, … }, { "003": …, "012": … }) | | Random numbers | R RNG streams | Seeded mulberry32-style RNG ({ seed }) or injected { rng }; unseeded calls use Math.random. Stochastic results are reproducible within SNA.js but never bit-identical to R | | equivClust tie-breaking | R hclust internal order | Stable JavaScript tie-breaking; merge order can differ on exactly tied distances | | rewireWs on saturated graphs | Can search forever | Skips dyads with no legal rewiring target | | redist on valued graphs | Fails later inside CATREGE | Throws immediately with a clear error |

For reproducible research runs, always pass { seed: … } to stochastic functions.

Extensions beyond R sna

Two functions have no R sna 2.8 counterpart. They exist because downstream consumers (e.g. SENA) need igraph-comparable summary metrics from the same graph normalization as the rest of this package; they were ported unchanged from the sna.js 0.0.x template those consumers previously vendored, and are gated by unit tests instead of the R parity suite:

| Function | Behavior | |---|---| | averagePathLength(input, options) | Mean geodesic distance over ordered vertex pairs i !== j with a finite positive distance (via geodist); unreachable pairs are excluded unless infReplace maps them to a finite value; 0 when no finite pair exists. Comparable to igraph average.path.length. | | labelPropagation(input, options) | Deterministic weighted label propagation returning { method, labels, sizes, count }: index-order sweeps, greatest symmetrized tie weight wins, exact ties break toward the smaller label. Edge values weight the propagation by default (ignoreEval: true for binary); maxIterations defaults to 50. |

Porting notes

  • isolates() and isIsolate() follow R loop handling: loops are ignored unless diag: true.
  • reachability() is reflexive, matching R: each vertex reaches itself via a length-0 path.
  • componentDist() returns zero-based component memberships; R memberships are one-based.
  • Data-preparation helpers port add.isolates, as.edgelist.sna, as.sociomatrix.sna, diag.remove, ego.extract, event2dichot, gt, gvectorize, interval.graph, is.edgelist.sna, lower.tri.remove, make.stochastic, sr2css, stackcount, symmetrize, and upper.tri.remove. SNA.js edge lists use zero-based vertices and explicit { order, edges, indexBase } fields instead of R matrix attributes.
  • File format helpers are browser-safe string parsers/serializers. readDot() implements the directed -> subset used by R read.dot; readNos(), writeNos(), and writeDl() leave actual file access to the caller.
  • dyadCensus() omits missing dyads from the MAN counts, like R.
  • triadClassify() uses zero-based triad vertices and returns Davis-Leinhardt class strings for directed graphs, 03 for undirected graphs, or null for missing triads. triadCensus() skips missing triads and returns zero-filled counts for graphs with fewer than three vertices.
  • grecip() supports dyadic, non-null dyadic, edgewise, edgewise log-relative-risk, and correlation reciprocity; missing dyads are omitted where R omits them.
  • gtrans() supports weak, strong, weakcensus, strongcensus, rank, and correlation transitivity, including missing ordered-triad omission in the rank path.
  • closeness() follows R's directed, undirected, inverse-distance, and Gil-Schmidt modes; undirected cmodes symmetrize weakly first, exactly like R (the first edge of each dyad in column-major order wins for valued ties).
  • betweenness(), stresscent(), and loadcent() share one shortest-path core and port the native accumulation modes; positive edge values are distances when ignoreEval: false; loadcent() follows R's directed transpose convention.
  • geodist() defaults to hop counting (ignoreEval: true, like R) and runs a Dijkstra-style search on edge values when ignoreEval: false, rejecting negative weights like R geodist_val_R.
  • flowbet() ports R's Edmonds-Karp based raw, normalized, and fractional modes. Edge values are capacities unless ignoreEval: true.
  • bonpow() and infocent() use a browser-safe dense linear solver; singular systems throw, matching R solve() failure semantics.
  • evcent() does not symmetrize asymmetric data before extracting eigenvectors, like R; useEigen: true forces the dense eigen path.
  • prestige() supports indegree, normalized indegree, eigenvector, domain, and domain-proximity modes.
  • Structural helpers port cutpoints, bicomponent.dist, kcores, clique.census, kpath.census, kcycle.census, maxflow, simmelian, and structure.statistics. Census routines enumerate exhaustively — use them on small-to-medium graphs.
  • kcores() follows R's valued-edge default; use ignoreEval: true for binary cores.
  • Graph comparison helpers port centralgraph, gcor, gcov, gscor, gscov, hdist, sdmat, structdist, gclust.centralgraph, and gclust.boxstats.
  • Role-analysis helpers port sedist, redist, equiv.clust, blockmodel, and blockmodel.expand (density blockmodels, the only expansion rule in R sna 2.8).
  • Random graph helpers port rgraph, rgnm, rguman, rgws, rgbn (including the CFTP subset), rewire.ws, and rewire.ud.
  • qaptest()/cugtest() return typed data objects; print*/summary*/plot* helpers return strings, structured tables, and SVG plot data instead of drawing with R graphics.
  • netlm()/netlogit() support classical, QAP (incl. SPP and X/Y permutation variants), and CUG nulls; lnam(), bbnam*(), pstar(), and bn() use browser-safe optimizers, so estimates are tolerance-based rather than bit-for-bit optim() parity.
  • R graph stacks are partially represented: data-prep helpers accept arrays of graphs; most analysis functions operate on one graph at a time.
  • R-style names are available through the snaR compatibility object: snaR["triad.census"], snaR["component.dist"], snaR["as.edgelist.sna"], ….

Workers and cancellation

Heavy routines accept { signal, onProgress } (betweenness, closeness, stresscent, loadcent, geodist, triadCensus, qaptest, cugTest, ...):

const controller = new AbortController();
betweenness(graph, {
  mode: "digraph",
  signal: controller.signal,                      // checked once per source vertex
  onProgress: (done, total) => update(done / total),
});

On the main thread a synchronous loop can only observe an abort raised from inside onProgress (or a pre-aborted signal). For real cancellation, run the routine in a Web Worker via @peterhudongpin/sna.js/worker — aborting terminates the worker thread:

import { createSnaWorker } from "@peterhudongpin/sna.js/worker";

// The same module self-registers when loaded inside a worker.
const client = createSnaWorker(() => new Worker(workerUrl, { type: "module" }));
const controller = new AbortController();
const scores = await client.run("betweenness", { input: graph }, { mode: "digraph" }, {
  signal: controller.signal,          // abort -> worker.terminate(), auto-respawn on next run
  onProgress: (done, total) => bar.value = done / total,
});

Permutation tests cross the worker boundary with named statistics (gcor, gscor, hdist for qaptest; gtrans, gden, grecip, mutuality, hierarchy for cugTest) since functions cannot be structured-cloned. A runnable demo with a progress bar and cancel button lives in examples/worker.html; executeSnaTask is exported for wiring other runtimes (e.g. Node worker_threads).

Dense graph normalization refuses orders above 5000 by default (a 50k-vertex matrix would allocate ~25 GB); raise it deliberately with setMaxGraphOrder(n).

Performance notes

Most centrality and graph-summary helpers are fine for interactive small-to-medium graphs in the main thread (n=300 sparse: betweenness ≈ 30 ms, geodist ≈ 15 ms, triad census ≈ 100 ms on a laptop). Exhaustive routines (cliqueCensus, kpathCensus, kcycleCensus, exact structural distance, large permutation tests, model fits) grow combinatorially — put them behind explicit UI controls and the worker adapter for user-supplied networks. Weighted shortest paths use a binary-heap Dijkstra. Dense O(n²) memory is allocated per graph; very large orders (n ≫ 10⁴) are not the target use case.

License

SNA.js is a derivative port of the GPL-licensed R sna package and is therefore licensed GPL-2.0-or-later. See LICENSE and NOTICE.

Practical implications:

  • Using SNA.js as an npm dependency and distributing it (including bundling it into frontend JavaScript served to browsers) distributes GPL code; the conventional reading is that the distributed program must be GPL-compatible. Server-side-only use has different obligations.
  • If that is a problem for your application, keep SNA.js behind a network service boundary or consult a license professional.

Original algorithms and reference implementation: Carter T. Butts, sna: Tools for Social Network Analysis, version 2.8 (CRAN). Please cite the original package in academic work — see citation("sna") in R.

Contributing

See CONTRIBUTING.md for the development setup, the parity-fixture workflow, and the porting standards (strict TypeScript, no any, parity tests or documented divergence for every ported function).