network
Large-scale network and map-of-networks rendering. The network engine draws node–link graphs as GPU-instanced glyphs — toward millions of nodes and links — with an in-library force layout (ForceLayout; main-thread, worker, or GPU backend), an adaptive level-of-detail cut over a coarsening or provided module hierarchy (Network.lod), and state (memory) networks (buildStateGraph). buildGraph assembles the graph from an edge list.
import { network, buildGraph } from "@mapequation/d3gl/network";
const graph = buildGraph({ nodeCount, source, target });network(el, { width, height }).data(graph).layout({ backend: "worker" });Classes
Section titled “Classes”| Class | Description |
|---|---|
| ForceLayout | - |
| Network | The network rendering engine (epic #98). A dedicated engine — nodes, links, layout, and LOD are one coupled system — built on the shared BaseEngine host/transform/zoom/interaction shell, rendering through the instanced lane (#100) rather than the retained Scene path. |
Interfaces
Section titled “Interfaces”| Interface | Description |
|---|---|
| BoundaryDiscs | Each module’s disc from a nested layout (#329), per aggregate (index g − leafCount): its centre as an offset (dx, dy) from the module’s leaf centroid, and its radius r, in world units (nestedBoundaryDiscs gives these). Passed to computeLODPositions, they become the module’s LOD geometry — cx/cy the disc centre, extent the disc radius — so the cut culls and expands a module by its disc, and its boundary ring is drawn on it. Keeping the centre relative to the centroid lets the disc follow its members through a drag or a position transition. |
| BuildGraphInput | - |
| BuildStateGraphInput | - |
| CoarseLevel | One coarsening level as a weighted, undirected edge list (parallel edges already collapsed). |
| CoarsenOptions | - |
| CrowdingOptions | Options for computeLODCrowding. |
| CSR | Compressed-sparse-row adjacency. |
| CutBoundaries | The expanded aggregates a cut collects for the module-boundary rings (#329). Pass one as CutOptions.boundaries; each cut overwrites ids / alpha / count (the arrays grow on demand and are reused — keep one per engine, from makeCutBoundaries). |
| CutOptions | - |
| CutScratch | Reusable scratch for cut (#213) — engine-owned so a zoom/pan frame allocates nothing steady-state: the frontier output, the DFS stack, and its parallel cross-fade alpha stack are grow-on-demand typed arrays reused across frames. The returned frontier is a subarray view of frontier, valid until the next cut with the same scratch — every per-frame consumer re-selects before reading, so one scratch per engine serves all paths. |
| DeclutterFrontierScratch | Reusable scratch for declutterFrontier (#213) — engine-owned so the per-frame thinning allocates nothing steady-state: the projected centres/radii, the typed sort keys + visit order, the kept flags, the shared declutter grid, and the output buffer are all grown on demand (to the largest frontier seen) and reused. The returned kept set is a subarray view of out, valid until the next call with the same scratch. |
| DeclutterOptions | - |
| FlowBorderAbsolute | The absolute flow-border mode (the default): the ring is scale(flow) wide. |
| FlowBorderBase | What both FlowBorderSpec modes share. |
| FlowBorderShare | The share flow-border mode (#510): the flow border covers the node’s exit-flow share — its exit flow e (FlowBorderBase.flow) over its flow f (the graph’s node flow, NetworkGraph.flow) — as a share of its area. A node of radius r keeps an inner disc of radius r · √(1 − e/f), with e/f clamped to [0, 1]: e = f covers the whole node, e = f/2 half its area, e = 0 draws no border, and f = 0 or a missing value (no node flow, NaN) draws none. The share does not depend on the radius, so it reads the same in both size modes and on a capped aggregate. |
| FlowGraph | Minimal directed-graph shape (a NetworkGraph satisfies it; raw arrays work too). |
| FlowOptions | - |
| FlowResult | - |
| ForceParams | Force-directed layout core (sub-issue #102, epic #98). Operates directly on a LayoutGraph’s positions buffer + edge list — pure typed-array math, no DOM, so it runs unchanged on the main thread or inside a Web Worker (the worker + SharedArrayBuffer transport land in a later slice). Repulsion is Barnes-Hut O(n log n) via BarnesHutTree. |
| Hierarchy | A coarsening hierarchy: progressively smaller graphs plus the maps that connect them. |
| LayoutGraph | Minimal graph view the force core needs: node count, a directed edge list (used as undirected springs), and the positions buffer it mutates. NetworkGraph satisfies this structurally, and so does a synthetic coarse level (see ./coarsen.js) — so one force core lays out the full graph and every coarsening level without casts. |
| LODCrowdingScratch | Reusable working storage for computeLODCrowding (#426): each aggregate’s members’ box, largest radius, horizon extent and 2-D index root, the index entries of the wide nodes, the walk’s pair stack, and one node’s children gathered at a time — grown on demand to the largest tree seen, so a pass run per streamed frame allocates nothing once warm. 28 B per aggregate; about 6 B per child of an indexed node (a wide node that is not crowded: 36 B per index entry, at most one per 16 children, and 4 B in perm); and 28 B per child of the widest node. |
| LODTransform | Screen-space transform: screen = world * k + (x, y) (matches BaseEngine ViewTransform). |
| LODTree | A retained coarsening tree, flattened to SoA typed arrays for cache-friendly traversal at scale. |
| LumpedStateNetwork | A state network with each physical node’s state nodes in one leaf module lumped into one node (#529): the map equation codes them with one codeword in that module, and Infomap’s physical tree merges them the same way. |
| ModuleColorOptions | - |
| ModuleFillSpec | NodeFillSpec’s module form (#494): the app’s colour for a module path, e.g. a palette keyed by module. Without color it takes the style’s ModuleColor (style({ moduleColor })). |
| ModuleLink | A link between two tree nodes addressed by Infomap path (#199) — the rows of an Infomap .ftree’s per-module *Links sections. source/target are the paths of modules or leaves (e.g. [1, 1] → [1, 2] for a link between sub-modules 1 and 2 of top module 1; [2] for top module 2; a leaf’s full node path). A .ftree stores leaf links only inside bottom modules and every coarser link only aggregated per level, so these are the real data for the map’s inter-module super-edges: each contributes from its endpoints’ own level up to (not including) their lowest common module, exactly like a graph edge does from the leaves up. Don’t repeat links already in the graph’s edges — both are summed. |
| ModuleNode | A graph node’s placement in the provided module tree — the Infomap JSON node shape. Extra fields (flow, name, modules, …) are accepted and ignored here; flow feeds flow-border rendering in N6b. |
| ModulePathNode | A node’s placement in the module tree — path is the Infomap 1-based chain (last entry is the rank). |
| MultilevelLayoutOptions | - |
| NestedLayoutOptions | - |
| NestedLayoutResult | Leaf positions plus every tree node’s disc (centre + radius), all in world units. |
| NetworkDataOptions | A module hierarchy for Network.data (#326) — data the engine owns alongside the graph, like stateNetwork(graph, { modules }) does for a state network. Every module consumer reads it whatever the LOD state: the LOD cut (its default NetworkLODOptions.source), the nested layout (layout({ nested })), the module-aware GPU seed, and NetworkHit.path. lod(false) keeps it; a new data(graph) without it clears it. |
| NetworkDeclutterStats | What the last LOD declutter pass cost (Network.declutterStats). probes and cells are the deterministic cost signature: they depend only on the glyphs and the view, never on machine speed. |
| NetworkGraph | Network graph: directed-edge SoA for rendering + CSR for traversal. |
| NetworkHit | What a network Network.pick resolved — carried as the datum of the HoverHit passed to `on(“hover" |
| NetworkLayoutOptions | How node positions are produced. Applies to plain graphs (Network.layout) and, since #182, state networks (Network.stateNetwork) — there each view keeps a layout of its own (#495): the physical and both views the physical graph’s, the state view the state graph’s. |
| NetworkLinkHit | What GPU-readback link picking (#141) resolved — carried as the datum of the HoverHit from `on(“hover" |
| NetworkLODOptions | Level-of-detail (#103): an adaptive hierarchy cut so a large network draws only what’s visible. Each pan/zoom re-cuts a retained coarsening tree — dense regions collapse to aggregate glyphs and expand into their members as you zoom in, as soon as their glyphs would no longer overlap (#426) — bounding per-frame work to the visible frontier. Opt-in via Network.lod; off by default (every node/link drawn). The tree’s geometry updates as the layout converges (so LOD helps during the solve, not only after), and the zoom-time path re-cuts only the visible frontier. Best paired with style({ sizeMode: "screen" }). |
| NetworkOptions | Options for the network engine. Inherits sizing, backend, and tooltipClass. |
| NetworkStyle | Visual style. Link appearance accessors arrive with the link pass (#100 N2.2). |
| ParsedEdges | Edge-list ingestion (sub-issue #99 / epic #98). |
| ParsedPajek | Edge-list ingestion (sub-issue #99 / epic #98). |
| PhysicalPieWedges | Packed per-physical-node pie wedges. Physical p’s wedges are the slice [offset[p], offset[p + 1]) of the parallel end / color / moduleKey arrays, ordered by module (consistent across physical nodes). end is the cumulative fraction in [0, 1] (a wedge spans [end[i-1] .. end[i]], the first from 0; the last is exactly 1). wedgeCount[p] ≤ 1 ⇒ a single module ⇒ draw a solid disc, not a pie. |
| PhysicalToState | Physical node → its state nodes, as CSR. Physical p’s state nodes are states[offsets[p] .. offsets[p + 1]]. The inverse of StateNetworkGraph.stateToPhysical. |
| PieWedgeOptions | - |
| RosetteOptions | - |
| SeedOptions | Options for seedPositions. |
| StateNetworkGraph | A state network plus its engine-derived physical network and the maps between the two levels. |
| StateNetworkOptions | Options for Network.stateNetwork (#171). The engine ingests a state network + a per-state-node module assignment, derives the physical network’s overlapping-module pie wedges + module colours, and lets you toggle the state ↔ physical view with Network.view. |
Type Aliases
Section titled “Type Aliases”| Type Alias | Description |
|---|---|
| FlowBorderSpec | Flow-border style (#104 N6): a ring around each node/module whose width encodes a per-node flow (e.g. Infomap enter/exit flow). The app supplies the flow; d3gl renders the ring. For LOD aggregates the metric is summed over members (see computeLODStyle), unless a module has its own value (FlowBorderSpec.moduleFlow). |
| ImportanceSpec | How a node’s declutter importance is determined: a NodeMetric ("degree"/"strength"/ "flow"/accessor), a per-node Float32Array, or "order" (a flat priority — so the survivor of a cluster falls back to input order, and an aggregate ranks by its subtree size). Resolved per-leaf and summed up the LOD tree (so a module’s importance is its members’ total — e.g. total flow), then used to break overlaps in the declutter: the highest-importance glyph in a cluster is kept. |
| LinkColorSpec | Link colour. A single CSS colour; a (weight) => cssColour scale (a bare d3 sequential/linear colour scale fits — scaleSqrt().range([light, dark]) interpolates RGBA, alpha included); or { by, scale } for parity with LinkWidthSpec. Whichever form, it resolves to a function of a weight value, so a super-edge colours by its accumulated subsumed weight (darker/heavier reads as more important). |
| LinkStyle | How links are drawn: plain "line" + a separate arrowhead (directed), a fused "half-arrow" (the map glyph), or "none" — a nodes-only network (#157). "none" skips building the link / arrowhead / super-edge geometry altogether (see drawsLinks); it is a rendering choice only, so the edges still drive the force layout and the LOD hierarchy. |
| LinkWidthSpec | Link width. A constant, a (weight) => width scale of the edge weight (a bare d3 scale fits — its input is the edge’s weight, which is the per-edge flow), or { by, scale } for parity with NodeRadiusSpec (by is "weight"/"flow" — the same per-edge quantity — so the scale maps the weight). Whichever form, it resolves to a function of a weight value, so a super-edge applies the same scale to the accumulated weight of the edges it subsumes. |
| ModuleColor | The app’s colour for a module (#494), by its module path ([] the root); undefined: the default fill. |
| NestedLayoutTopology | The topology fields the nested layout reads — a module tree with (optional) super-edges. |
| NetworkFormat | Network text formats parseNetwork can read. |
| NodeFillSpec | How node fill colour is determined. Resolved once per style() call to a per-node RGBA buffer (or one colour), so every form is free at draw time. |
| NodeMetric | A built-in per-node metric d3gl can read directly, or a custom (index, graph) => value accessor. "degree" is the neighbour count (CSR.degree); "strength" the weighted degree (NetworkGraph.strength); "flow" the app-provided NetworkGraph.flow (errors if absent). |
| NodeRadiusSpec | How node radius is determined. Resolves once (per style() call) to a per-node Float32Array, which is already a per-instance GPU attribute — so any of these forms is free at draw time. |
Variables
Section titled “Variables”| Variable | Description |
|---|---|
| DEFAULT_FORCE | - |
Functions
Section titled “Functions”| Function | Description |
|---|---|
| buildCSR | Build undirected (symmetric) CSR adjacency from a directed edge list. Each edge contributes to both endpoints, so layout/traversal see the graph as undirected while the directed edges remain available for arrow rendering. With weight (parallel to source/target), each entry also carries its edge’s weight in weights, scattered in the same order as neighbors. |
| buildGraph | Assemble a NetworkGraph from a directed edge list. |
| buildHierarchy | Build the full coarsening hierarchy, stopping at minNodes or when a pass stops reducing. Reads only the edge list (no positions), so the GPU layout’s LOD worker can coarsen a bare edge list (#377). |
| buildLODTree | Build the retained LOD tree topology from a graph’s coarsening hierarchy. Geometry is left zeroed; call computeLODGeometry once positions have settled. This is the main-thread path (the force/positions backends and LOD enabled after a worker has finished); the worker backend streams an already-built LODTopology instead (#103), assembled via lodTreeFromTopology. |
| buildModuleLODTree | Build a LODTree from a provided module hierarchy (the priority-chain entry that precedes structural coarsening). Geometry is left zeroed — fill it with computeLODGeometry once positions exist. |
| buildStateGraph | Assemble a StateNetworkGraph from a state-level edge list + a per-state-node physical id. The physical network is derived once (its links are the directed, flow-summed aggregation of state edges that cross a physical boundary). Positions in both graphs are left zeroed for layout to fill. |
| coarsenLevel | One coarsening step. Visit nodes in index order and pair each unmatched node with its heaviest unmatched neighbour (lowest index breaks ties → deterministic). A node with no unmatched neighbour is adopted into its heaviest already-matched neighbour’s group rather than left as a singleton: without this, hub/star structures (pervasive in power-law graphs) barely shrink — each pass strips only a couple of nodes, the hierarchy hits its level cap, and the multilevel seed runs dozens of force solves on near-full graphs (#117). Adoption keeps the per-level reduction roughly geometric. Then aggregate the surviving inter-group edges (parallel edges summed, internal edges dropped). |
| computeLODCrowding | The tree’s crowding (#426): each node’s LODTree.clearZoom — the zoom from which no two of its members’ glyphs overlap on screen — from the leaves’ positions (cx/cy) and radii (radius). The cut opens an aggregate once zoomed past it, so an aggregate is drawn only where its members would overlap. |
| computeLODGeometry | Fill the tree’s full geometry from the settled layout, the per-leaf visual radii, and a per-leaf importance weight — computeLODPositions then computeLODStyle. The main-thread path (synchronous solve / supplied positions); the worker backend splits these passes across threads. |
| crowdingHorizon | The horizon for computeLODCrowding (#426): the expand threshold the cut uses — expandPx when set, else defaultExpandPx without its viewport cap, so it is never below the threshold of a cut at any viewport size and a resize needs no new pass. O(1). |
| cut | - |
| declutterFrontier | Thin an LOD frontier in screen space: keep higher-importance glyphs (by tree LODTree.weight = strength) and drop lower-importance ones whose centre lies within spacing·(rᵢ + rⱼ) of a kept glyph. The default spacing 2 also drops near neighbours, so kept glyphs keep a gap around them and the links between them stay visible; in a small graph that can hide a whole non-overlapping module aggregate (#512). Spacing 1 drops only glyphs that really overlap a kept one. Greedy in descending importance over a screen grid, so a dense cluster keeps its most important members and the kept set is overlap-free (no overdraw). Runs per cut, so it’s zoom-dependent — more glyphs resolve as you zoom in. Returns the kept frontier ids (original order). |
| defaultExpandPx | The adaptive default expand threshold, in px, for a tree with no explicit CutOptions.expandPx (#191). |
| detectFormat | Pick a parser: Pajek for a .net filename or text with a *Vertices/*Arcs/*Edges section header; otherwise the plain edge list. A filename extension wins over content sniffing. |
| equilibriumSpacing | Nearest-neighbour spacing of the force model’s equilibrium, √(π·repulsion/centering): 1/d repulsion balanced by linear centering settles into a uniform disc with this much area per node (virial estimate — springs only tighten it). Independent of the node count and the viewport, so it is the natural unit for a seed, the per-tick step cap and the convergence test. 0 when the model has no equilibrium scale (no repulsion or no centering). |
| lumpStateGraph | Lump sg’s state nodes by (leaf module, physical node) (#529), with modules a record per state node (Infomap’s nodes shape; the leaf module is the record’s path less its last entry). Without modules every state node is in the root module, so each physical node’s state nodes are one lump: the lumped network is the physical one. A lump’s links are the summed links of its state nodes to other lumps, its flow the sum of theirs: the same merge as the physical graph’s. O(state nodes + state links + lumps · log lumps). |
| makeCutBoundaries | A fresh, empty CutBoundaries collector. |
| makeCutScratch | Fresh CutScratch. The network engine keeps ONE per instance; cut falls back to a throwaway one when none is passed (backward-compatible, but then every call allocates). |
| makeDeclutterFrontierScratch | Fresh DeclutterFrontierScratch. The network engine keeps ONE per instance; declutterFrontier falls back to a throwaway one when none is passed (backward-compatible, but then every call allocates). |
| makeLODCrowdingScratch | A fresh, empty LODCrowdingScratch. |
| moduleColors | Per-node CSS colours (indexed by node id) for a module hierarchy. A node takes the hue of its enclosing module (path minus the final rank), so all nodes in a module share a colour and sibling modules get neighbouring hues within their parent’s arc. |
| multilevelLayout | Multilevel force layout: multilevelSeed then refine the finest level in place. Writes graph.positions. This is the synchronous main-thread path; the worker streams the seed (multilevelSeedSteps) and then the finest-level refinement instead. |
| nestedBoundaryDiscs | A nested layout’s module boundary discs (#329): each module’s disc from result, with its centre re-expressed as an offset from the module’s leaf centroid in result.positions — the BoundaryDiscs the network engine rings its expanded modules with (lod({ moduleBoundary })), so a ring stays on its members’ disc as they are dragged or eased. One O(tree size) pass, children before parents (a module tree numbers every child below its parent). Aggregates only. |
| nestedLayout | Lay out a module tree top-down, each module’s children inside its disc. |
| nestedRootBounds | The box a cold nested layout’s final positions lie in before any depth is placed (#427): its root disc, centred on the origin, for a radius as in NestedLayoutOptions.radius. A transport streaming the layout posts it as the stream’s first bound, so a fit frames the map from its first paint; each depth’s onDepth bound lies inside it. O(1). |
| network | Create a Network engine on host. |
| parseEdgeList | Parse a whitespace-separated edge list: source target [weight] per line. Blank lines and # comment lines are ignored. |
| parseNetwork | Parse a network from text, choosing the format via detectFormat: Pajek .net or a plain edge list (source target [weight], # comments). Edge lists are reported as undirected. |
| parsePajek | Parse a Pajek .net document into a directed-or-undirected edge list (+ optional positions). |
| physicalPieWedges | Derive per-physical-node pie wedges from a state-node module assignment. modules is Infomap’s nodes array (each record’s id is a state-node index, path its module chain), as consumed by moduleColors / buildModuleLODTree. Records must cover every state node. |
| randomWalkFlow | Compute the random-walk FlowResult for a directed graph. O(iterations · edges); a handful of milliseconds for the thousands-of-nodes scale this is meant for. Run it offline to bake a fixture. |
| rosettePositions | Derive state-node positions by rosette placement around the (already laid-out) physical nodes. Reads graph.physical.positions (fill it first via a layout) and returns a fresh interleaved [x, y, …] array of length 2 * graph.state.nodeCount, ready for net.data(graph.state).layout({ backend: "positions", positions }). |
| seedPositions | Seed node positions deterministically as a phyllotaxis (“sunflower”) disc centred on the viewport — a good, reproducible starting distribution for ForceLayout when a graph arrives without coordinates (no two nodes coincident, no RNG). Without opts the disc fills the viewport (radius min(width, height) / 2); with opts it is sized to the force model’s equilibrium (see SeedOptions.force). |
| sharedMemoryAvailable | Whether this environment can use the SharedArrayBuffer zero-copy position transport: SharedArrayBuffer exists and the page is cross-origin isolated (served with Cross-Origin-Opener-Policy: same-origin + Cross-Origin-Embedder-Policy: require-corp). When false, the worker posts per-frame position snapshots instead. This reports the environment’s capability; whether a given run actually used it is WorkerLayoutHandle.shared (they differ when the worker is unavailable and the layout falls back to a synchronous main-thread solve). |
References
Section titled “References”halfLinkGeometry
Section titled “halfLinkGeometry”Re-exports halfLinkGeometry
HalfLinkGeometry
Section titled “HalfLinkGeometry”Re-exports HalfLinkGeometry
HalfLinkParams
Section titled “HalfLinkParams”Re-exports HalfLinkParams
halfLinkPathString
Section titled “halfLinkPathString”Re-exports halfLinkPathString
PathSink
Section titled “PathSink”Re-exports PathSink
scaleHalfLink
Section titled “scaleHalfLink”Re-exports scaleHalfLink
traceHalfLink
Section titled “traceHalfLink”Re-exports traceHalfLink