@peterhudongpin/sna.js
v0.4.0
Published
A browser-compatible TypeScript/JavaScript port of the R sna social network analysis package.
Downloads
624
Maintainers
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 baresna.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
sna2.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),lnampoint estimates, and thebbnam.fixedclosed-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), andbn,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/threeQuick 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 countsLike 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-styleprint*/summary*/plot*helpers.@peterhudongpin/sna.js/compat— thesnaRR-name table and package hooks.@peterhudongpin/sna.js/visualizationand/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 withmode: "graph"is not silently symmetrized. Usesymmetrize: "weak" | "strong" | "upper" | "lower"to opt in.directedcan overridemode.diag: truepreserves loops; by default loops are suppressed, as in R.ignoreEvaltreats valued ties as binary. Per-function defaults match Rsna(see table in the source of each function).thresholddichotomizes numeric ties by absolute value during graph normalization.indexBase: 1converts one-based edge-list vertices at input boundaries.NaNrepresents 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.svgescapes& < > " '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.Rruns the real Rsna2.8 over a deterministic 16-graph corpus (paths, stars, rings, bipartite, transitive/asymmetric triads, valued n=20, 10%NAn=20, sparse n=50 digraph, disconnected, self-loops) and writesfixtures/r-sna-2.8/parity.jsonwith provenance (R version, sna version, seeds, timestamp).tests/parity/parity.test.tsreplays 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
sna2.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()andisIsolate()follow R loop handling: loops are ignored unlessdiag: 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, andupper.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 Rread.dot;readNos(),writeNos(), andwriteDl()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,0–3for undirected graphs, ornullfor 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(), andloadcent()share one shortest-path core and port the native accumulation modes; positive edge values are distances whenignoreEval: 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 whenignoreEval: false, rejecting negative weights like Rgeodist_val_R.flowbet()ports R's Edmonds-Karp based raw, normalized, and fractional modes. Edge values are capacities unlessignoreEval: true.bonpow()andinfocent()use a browser-safe dense linear solver; singular systems throw, matching Rsolve()failure semantics.evcent()does not symmetrize asymmetric data before extracting eigenvectors, like R;useEigen: trueforces 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, andstructure.statistics. Census routines enumerate exhaustively — use them on small-to-medium graphs. kcores()follows R's valued-edge default; useignoreEval: truefor binary cores.- Graph comparison helpers port
centralgraph,gcor,gcov,gscor,gscov,hdist,sdmat,structdist,gclust.centralgraph, andgclust.boxstats. - Role-analysis helpers port
sedist,redist,equiv.clust,blockmodel, andblockmodel.expand(density blockmodels, the only expansion rule in Rsna2.8). - Random graph helpers port
rgraph,rgnm,rguman,rgws,rgbn(including the CFTP subset),rewire.ws, andrewire.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(), andbn()use browser-safe optimizers, so estimates are tolerance-based rather than bit-for-bitoptim()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
snaRcompatibility 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).
