Skip to content

jonobr1/force-directed-graph

Repository files navigation

Force Directed Graph

GPU supercharged attraction-graph visualizations for the web built on top of Three.js. 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 for a running demo.

Usage

npm install --save three @jonobr1/force-directed-graph

Import in ES6 environment

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.

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

  • 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.

<!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

About

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

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors