UNPKG

@jonobr1/force-directed-graph

Version:

GPU supercharged attraction-graph visualizations for the web built on top of Three.js

178 lines (138 loc) 6.99 kB
# Force Directed Graph GPU supercharged attraction-graph visualizations for the web built on top of [Three.js](http://threejs.org). Importable as an ES6 module. 1. 🧮 Simulation computed on GPU via render targets 2. 🕸️ Accepts thousands of nodes and links 3. 🎨 Configurable point and link colors 4. 〰️ GPU-expanded antialiased line rendering with configurable width 5. 📦 Single library dependent (Three.js) 6. 🧩 Three.js scene compatible object 7. 📝 Simple data schema to populate compatible with d3.js JSON samples 8. 🧊 2d & 3d simulation modes 9. 🦺 WASM workers to generate textures Visit the hosted [project page](https://jonobr1.com/force-directed-graph/) for a running demo. ## Usage ``` npm install --save three @jonobr1/force-directed-graph ``` ### Import in ES6 environment ```javascript import { ForceDirectedGraph } from '@jonobr1/force-directed-graph'; ``` ### Data Schema (`constructor` and `set`) > Reference: The accepted `nodes` / `links` structure is inspired by the D3 force-directed graph data format: [@d3/force-directed-graph-component](https://observablehq.com/@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})` ```ts 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] > - `nodes` and `links` are both required. > - `source` / `target` are resolved by node `id`. > - If `x`, `y`, or `z` is 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 when `pinStrength > 0`. > - `isStatic` defaults to `false`. > - If `color` is omitted, the node defaults to white. > - `set(data[, callback])` returns a `Promise` that resolves when geometry/textures are ready. > - `obscurity` is label-density control: `0` shows all labels, `0.75` targets roughly 25% visible labels, and `1` hides all labels. The active subset is now chosen from graph topology and priority, not camera clipspace placement. > - `fdg.labels.alignment` (`'center' | 'left' | 'right'`) and `fdg.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, default `0`) discards labels at or closer than that depth, which is useful when `sizeAttenuation` makes nearby labels too large. > - `fdg.labelsInheritColor` toggles whether labels use each node's `color`, and `fdg.labelColor` tints all labels uniformly on top of the white label atlas. > - `fdg.labels.fontSize` scales the rendered label planes without rebuilding the atlas; `fdg.labels.fontFamily` rebuilds 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()`, and `fdg.isWasmAccelerationAvailable()` expose worker / WASM capability state. ### Selected Instance API - `fdg.pinStrength`: controls how strongly nodes are attracted toward their target positions derived from `x`, `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, otherwise `null`. - `fdg.getPerformanceInfo()`: returns `{ workerSupported, workerReady, wasmReady, pendingRequests }`. ### Load Script in HTML file: This example creates 512 nodes and links them randomly like big snakes. ```html <!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](http://jono.fyi/)