GPU supercharged attraction-graph visualizations for the web built on top of Three.js. Importable as an ES6 module.
- 🧮 Simulation computed on GPU via render targets
- 🕸️ Accepts thousands of nodes and links
- 🎨 Configurable point and link colors
- 〰️ GPU-expanded antialiased line rendering with configurable width
- 📦 Single library dependent (Three.js)
- 🧩 Three.js scene compatible object
- 📝 Simple data schema to populate compatible with d3.js JSON samples
- 🧊 2d & 3d simulation modes
- 🦺 WASM workers to generate textures
Visit the hosted project page for a running demo.
npm install --save three @jonobr1/force-directed-graph
import { ForceDirectedGraph } from '@jonobr1/force-directed-graph';Reference: The accepted
nodes/linksstructure is inspired by the D3 force-directed graph data format: @d3/force-directed-graph-component.
The same data object shape is accepted by:
new ForceDirectedGraph(renderer, data)fdg.set(data[, callback])
Tip
It is recommended to use set and update setters in the callback or after the promise resolves. E.g: fdg.set(data).then(() => {fdg.linewidth = 2})
type GraphData = {
nodes: NodeData[];
links: LinkData[];
};
type NodeData = {
id: string | number; // Required, unique per node
x?: number; // Optional initial / target x position
y?: number; // Optional initial / target y position
z?: number; // Optional initial / target z position
isStatic?: boolean; // Optional, pins node when true
color?: THREE.ColorRepresentation; // Optional Three.js color input
image?: string | HTMLImageElement; // Optional image URL or image element
label?: string | number; // Optional canvas-atlas text label
labelPriority?: number; // Optional label ranking override
size?: number // Optional size for per-node sizing
};
type LinkData = {
source: string | number; // Node reference (must match a node.id)
target: string | number; // Node reference (must match a node.id)
};Note
nodesandlinksare both required.source/targetare resolved by nodeid.- If
x,y, orzis omitted, a random initial position is assigned. - If a node defines at least two of
x,y,z, those values also become that node's target position whenpinStrength > 0. isStaticdefaults tofalse.- If
coloris omitted, the node defaults to white. set(data[, callback])returns aPromisethat resolves when geometry/textures are ready.obscurityis label-density control:0shows all labels,0.75targets roughly 25% visible labels, and1hides all labels. The active subset is now chosen from graph topology and priority, not camera clipspace placement.fdg.labels.alignment('center' | 'left' | 'right') andfdg.labels.baseline('top' | 'middle' | 'bottom') change label anchoring live.fdg.labels.offset(THREE.Vector2) adds extra label padding in label-space x/y.fdg.labels.near(camera-space distance, default0) discards labels at or closer than that depth, which is useful whensizeAttenuationmakes nearby labels too large.fdg.labelsInheritColortoggles whether labels use each node'scolor, andfdg.labelColortints all labels uniformly on top of the white label atlas.fdg.labels.fontSizescales the rendered label planes without rebuilding the atlas;fdg.labels.fontFamilyrebuilds the atlas with a new CSS font stack.fdg.refreshLabels()rebuilds label atlas data after mutating node labels, priorities, sizes, or colors in-place.fdg.getPerformanceInfo(),fdg.isWorkerProcessingAvailable(), andfdg.isWasmAccelerationAvailable()expose worker / WASM capability state.
fdg.pinStrength: controls how strongly nodes are attracted toward their target positions derived fromx,y,z.fdg.refreshLabels(): reparses labels from current node data and updates or removes the labels mesh as needed.fdg.labels: returns the labels mesh when labels exist, otherwisenull.fdg.getPerformanceInfo(): returns{ workerSupported, workerReady, wasmReady, pendingRequests }.
This example creates 512 nodes and links them randomly like big snakes.
<!doctype html>
<html>
<head>
<meta charset="utf-8">
</head>
<body>
<script async src="https://unpkg.com/es-module-shims@1.3.6/dist/es-module-shims.js"></script>
<script type="importmap">
{
"imports": {
"three": "https://cdn.jsdelivr.net/npm/three/build/three.module.js",
"three/examples/jsm/misc/GPUComputationRenderer.js": "https://cdn.jsdelivr.net/npm/three/examples/jsm/misc/GPUComputationRenderer.js",
"@jonobr1/force-directed-graph": "https://cdn.jsdelivr.net/npm/@jonobr1/force-directed-graph/build/fdg.module.js"
}
}
</script>
<script type="module">
import * as THREE from 'three';
import { ForceDirectedGraph } from '@jonobr1/force-directed-graph';
const renderer = new THREE.WebGLRenderer({ antialias: true });
const scene = new THREE.Scene();
const camera = new THREE.PerspectiveCamera();
camera.position.z = 250;
// Generate some fake data
const amount = 512;
const data = {
nodes: [], // Required, each element should be an object
links: [] // Required, each element should be an object
// with source and target properties that are
// ids of their connecting nodes
};
for (let i = 0; i < amount; i++) {
data.nodes.push({ id: i });
if (i > 0) {
data.links.push({ target: Math.floor(Math.random() * i), source: i });
}
}
const fdg = new ForceDirectedGraph(renderer, data);
scene.add(fdg);
setup();
function setup() {
renderer.setClearColor('#fff');
document.body.appendChild(renderer.domElement);
window.addEventListener('resize', resize, false);
resize();
renderer.setAnimationLoop(render);
}
function resize() {
const width = window.innerWidth;
const height = window.innerHeight;
renderer.setSize(width, height);
camera.aspect = width / height;
camera.updateProjectionMatrix();
}
function render(elapsed) {
fdg.update(elapsed);
renderer.render(scene, camera);
}
</script>
</body>
</html>Warning
Due to the reliance on the GPU compute rendering, this project is not built for node.js use.
A free and open source tool by Jono Brandel