├── .eslintignore ├── .eslintrc.json ├── .github └── workflows │ └── tests.yaml ├── .gitignore ├── Changelog.md ├── LICENSE ├── README.md ├── demo-static ├── README.md └── index.html ├── demo ├── .browserslistrc ├── .eslintrc.js ├── .gitignore ├── README.md ├── babel.config.js ├── deploy.sh ├── package-lock.json ├── package.json ├── public │ └── index.html ├── src │ ├── App.vue │ ├── components │ │ ├── HelpIcon.vue │ │ ├── InputFlag.vue │ │ └── InputValue.vue │ ├── lib │ │ ├── LineCollection.js │ │ ├── PointCollection.js │ │ ├── bus.js │ │ ├── createForceLayout.js │ │ ├── createGraphScene.js │ │ ├── fileDrop.js │ │ ├── findLargestComponent.js │ │ ├── getAvailableGraphs.js │ │ ├── getGraph.js │ │ ├── loadDroppedGraph.js │ │ ├── loadGraph.js │ │ └── utils.js │ └── main.js └── vue.config.js ├── dist ├── ngraph.forcelayout.js ├── ngraph.forcelayout.min.js ├── ngraph.forcelayout2d.js └── ngraph.forcelayout2d.min.js ├── index.d.ts ├── index.js ├── index.v43.d.ts ├── inline-transform.js ├── lib ├── bounds.js ├── codeGenerators │ ├── createPatternBuilder.js │ ├── generateBounds.js │ ├── generateCreateBody.js │ ├── generateCreateDragForce.js │ ├── generateCreateSpringForce.js │ ├── generateIntegrator.js │ ├── generateQuadTree.js │ └── getVariableName.js ├── createPhysicsSimulator.js ├── kdForce.js └── spring.js ├── package-lock.json ├── package.json ├── perf ├── experimental │ ├── README.md │ └── staticOrDynamic.js ├── perfresults.txt └── test.js ├── stress.sh └── test ├── createBody.js ├── dragForce.js ├── eulerIntegrator.js ├── insert.js ├── layout.js ├── primitives.js ├── simulator.js └── springForce.js /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | demo/dist -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | 2 | { 3 | "parserOptions": { 4 | "ecmaVersion": 6, 5 | "sourceType": "module" 6 | }, 7 | "env": { 8 | "browser": true, 9 | "node": true, 10 | "es6": true 11 | }, 12 | "rules": { 13 | "semi": "error", 14 | "no-undef": "error", 15 | "no-unused-vars": "error", 16 | "no-shadow": "error" 17 | } 18 | } -------------------------------------------------------------------------------- /.github/workflows/tests.yaml: -------------------------------------------------------------------------------- 1 | name: Node.js CI 2 | 3 | on: 4 | push: 5 | branches: [ main ] 6 | pull_request: 7 | branches: [ main ] 8 | 9 | jobs: 10 | build: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | strategy: 15 | matrix: 16 | node-version: [20.x, 18.x] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | - name: Use Node.js ${{ matrix.node-version }} 21 | uses: actions/setup-node@v2 22 | with: 23 | node-version: ${{ matrix.node-version }} 24 | - run: npm ci 25 | - run: npm run build --if-present 26 | - run: npm test 27 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | lib-cov 2 | *.seed 3 | *.log 4 | *.csv 5 | *.dat 6 | *.out 7 | *.pid 8 | *.gz 9 | 10 | pids 11 | logs 12 | results 13 | .nyc_output 14 | 15 | npm-debug.log 16 | node_modules 17 | coverage 18 | -------------------------------------------------------------------------------- /Changelog.md: -------------------------------------------------------------------------------- 1 | # v3.0 2 | 3 | The following physics settings are renamed: 4 | 5 | * `springCoeff -> springCoefficient` 6 | * `dragCoeff -> dragCoefficient` 7 | 8 | A new interactive demo is added to the library: http://anvaka.github.io/ngraph.forcelayout/. 9 | 10 | Also the library is now available for consumption via CDN. 11 | 12 | # v2.0 13 | 14 | Major rework on how library treats dimensions of space. Previous versions of the library 15 | required concrete implementation for the given space (e.g. [3d](https://github.com/anvaka/ngraph.forcelayout3d), [N-d](https://github.com/anvaka/ngraph.forcelayout.nd)). 16 | 17 | With version 2.0, `ngraph.forcealyout` generates code on the fly to support layout in the 18 | given dimension. This comes at no extra performance cost to the consumer. 19 | 20 | Second big change, is that custom forces can now be added to the library via `simulator.addForce()` 21 | `simulator.removeForce()` api. 22 | 23 | With this change, the old `physicsSimulator` factory methods became obsolete and were removed 24 | (like `settings.createQuadTree`, `settings,createBounds`, [etc.](https://github.com/anvaka/ngraph.forcelayout/blob/d2eea4a5dd6913fb0002787d91d211916b56ba01/lib/physicsSimulator.js#L50-L55) ) 25 | 26 | # 0.xx - V1.0 27 | 28 | This was original implementation of the ngraph.forcelayout. -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2013 - 2025, Andrei Kashcha 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without modification, 5 | are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, this 11 | list of conditions and the following disclaimer in the documentation and/or 12 | other materials provided with the distribution. 13 | 14 | * Neither the name of the Andrei Kashcha nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND 19 | ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED 20 | WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR 22 | ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES 23 | (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; 24 | LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON 25 | ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 26 | (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS 27 | SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ngraph.forcelayout 2 | 3 | [![build status](https://github.com/anvaka/ngraph.forcelayout/actions/workflows/tests.yaml/badge.svg)](https://github.com/anvaka/ngraph.forcelayout/actions/workflows/tests.yaml) 4 | 5 | This is a [force directed](http://en.wikipedia.org/wiki/Force-directed_graph_drawing) graph layout algorithm, 6 | that works in any dimension (2D, 3D, and above). 7 | 8 | The library uses quad tree to speed up computation of long-distance forces. 9 | 10 | This repository is part of [ngraph family](https://github.com/anvaka/ngraph), and operates on 11 | [`ngraph.graph`](https://github.com/anvaka/ngraph.graph) data structure. 12 | 13 | # API 14 | 15 | All force directed algorithms are iterative. We need to perform multiple iterations of an algorithm, 16 | before graph starts looking good: 17 | 18 | ``` js 19 | // graph is an instance of `ngraph.graph` object. 20 | var createLayout = require('ngraph.forcelayout'); 21 | var layout = createLayout(graph); 22 | for (var i = 0; i < ITERATIONS_COUNT; ++i) { 23 | layout.step(); 24 | } 25 | 26 | // now we can ask layout where each node/link is best positioned: 27 | graph.forEachNode(function(node) { 28 | console.log(layout.getNodePosition(node.id)); 29 | // Node position is pair of x,y coordinates: 30 | // {x: ... , y: ... } 31 | }); 32 | 33 | graph.forEachLink(function(link) { 34 | console.log(layout.getLinkPosition(link.id)); 35 | // link position is a pair of two positions: 36 | // { 37 | // from: {x: ..., y: ...}, 38 | // to: {x: ..., y: ...} 39 | // } 40 | }); 41 | ``` 42 | 43 | If you'd like to perform graph layout in space with more than two dimensions, just add one 44 | argument to this line: 45 | 46 | ``` js 47 | let layout = createLayout(graph, {dimensions: 3}); // 3D layout 48 | let nodePosition = layout.getNodePosition(nodeId); // has {x, y, z} attributes 49 | ``` 50 | 51 | Even higher dimensions are not a problem for this library: 52 | 53 | ``` js 54 | let layout = createLayout(graph, {dimensions: 6}); // 6D layout 55 | // Every layout with more than 3 dimensions, say N, gets additional attributes: 56 | // c4, c5, ... cN 57 | let nodePosition = layout.getNodePosition(nodeId); // has {x, y, z, c4, c5, c6} 58 | ``` 59 | 60 | Note: Higher dimensionality comes at exponential cost of memory for every added 61 | dimension. See a performance section below for more details. 62 | 63 | ## Node position and object reuse 64 | 65 | Recently immutability became a ruling principle of javascript world. This library 66 | doesn't follow the rules, and results of `getNodePosition()`/`getLinkPosition()` will be 67 | always the same for the same node. This is true: 68 | 69 | ``` js 70 | layout.getNodePosition(1) === layout.getNodePosition(1); 71 | ``` 72 | 73 | Reason for this is performance. If you are interested in storing positions 74 | somewhere else, you can do it and they still will be updated after each force 75 | directed layout iteration. 76 | 77 | ## "Pin" node and initial position 78 | 79 | Sometimes it's desirable to tell layout algorithm not to move certain nodes. 80 | This can be done with `pinNode()` method: 81 | 82 | ``` js 83 | var nodeToPin = graph.getNode(nodeId); 84 | layout.pinNode(nodeToPin, true); // now layout will not move this node 85 | ``` 86 | 87 | If you want to check whether node is pinned or not you can use `isNodePinned()` 88 | method. Here is an example how to toggle node pinning, without knowing it's 89 | original state: 90 | 91 | ``` js 92 | var node = graph.getNode(nodeId); 93 | layout.pinNode(node, !layout.isNodePinned(node)); // toggle it 94 | ``` 95 | 96 | What if you still want to move your node according to some external factor (e.g. 97 | you have initial positions, or user drags pinned node)? To do this, call 98 | `setNodePosition()` method: 99 | 100 | ``` js 101 | layout.setNodePosition(nodeId, x, y); 102 | ``` 103 | 104 | ## Monitoring changes 105 | 106 | Like many other algorithms in `ngraph` family, force layout monitors graph changes 107 | via [graph events](https://github.com/anvaka/ngraph.graph#listening-to-events). 108 | It keeps layout up to date whenever graph changes: 109 | 110 | ``` js 111 | var graph = require('ngraph.graph')(); // empty graph 112 | var layout = require('ngraph.layout')(graph); // layout of empty graph 113 | 114 | graph.addLink(1, 2); // create node 1 and 2, and make link between them 115 | layout.getNodePosition(1); // returns position. 116 | ``` 117 | 118 | If you want to stop monitoring graph events, call `dispose()` method: 119 | 120 | ``` js 121 | layout.dispose(); 122 | ``` 123 | 124 | ## Physics Simulator 125 | 126 | Simulator calculates forces acting on each body and then deduces their position via Newton's law. 127 | There are three major forces in the system: 128 | 129 | 1. Spring force keeps connected nodes together via [Hooke's law](http://en.wikipedia.org/wiki/Hooke's_law) 130 | 2. Each body repels each other via [Coulomb's law](http://en.wikipedia.org/wiki/Coulomb's_law) 131 | 3. The drag force slows the entire simulation down, helping with convergence. 132 | 133 | Body forces are calculated in `n*lg(n)` time with help of Barnes-Hut algorithm implemented with quadtree. 134 | 135 | ``` js 136 | // Configure 137 | var physicsSettings = { 138 | timeStep: 0.5, 139 | dimensions: 2, 140 | gravity: -12, 141 | theta: 0.8, 142 | springLength: 10, 143 | springCoefficient: 0.8, 144 | dragCoefficient: 0.9, 145 | }; 146 | 147 | // pass it as second argument to layout: 148 | var layout = require('ngraph.forcelayout')(graph, physicsSettings); 149 | ``` 150 | 151 | You can get current physics simulator from layout by checking `layout.simulator` 152 | property. This is a read only property. 153 | 154 | ## Space occupied by graph 155 | 156 | Finally, it's often desirable to know how much space does our graph occupy. To 157 | quickly get bounding box use `getGraphRect()` method: 158 | 159 | ``` js 160 | var rect = layout.getGraphRect(); 161 | // rect.min_x, rect.min_y - left top coordinates of the bounding box 162 | // rect.max_x, rect.max_y - right bottom coordinates of the bounding box 163 | ``` 164 | 165 | ## Manipulating bodies 166 | 167 | This is advanced technique to get to internal state of the simulator. If you need 168 | to get a node position use regular `layout.getNodePosition(nodeId)` described 169 | above. 170 | 171 | In some cases you really need to manipulate physic attributes on a body level. 172 | To get to a single body by node id: 173 | 174 | ``` js 175 | var graph = createGraph(); 176 | graph.addLink(1, 2); 177 | 178 | // Get body that represents node 1: 179 | var body = layout.getBody(1); 180 | assert( 181 | typeof body.pos.x === 'number' && 182 | typeof body.pos.y === 'number', 'Body has position'); 183 | assert(body.mass, 'Body has mass'); 184 | ``` 185 | 186 | To iterate over all bodies at once: 187 | 188 | ``` js 189 | layout.forEachBody(function(body, nodeId) { 190 | assert( 191 | typeof body.pos.x === 'number' && 192 | typeof body.pos.y === 'number', 'Body has position'); 193 | assert(graph.getNode(nodeId), 'NodeId is coming from the graph'); 194 | }); 195 | ``` 196 | 197 | # Section about performance 198 | 199 | This library is focused on performance of physical simulation. We use quad tree data structure 200 | in 2D space to approximate long distance forces, and reduce amount of required computations. 201 | 202 | When layout is performed in higher dimensions we use analogues tree data structure. By design 203 | such tree requires to store `2^dimensions_count` child nodes on each node. In practice, performing 204 | layout in 6 dimensional space on a graph with a few thousand nodes yields decent performance 205 | on modern mac book (graph can be both rendered and layed out at 60FPS rate). 206 | 207 | Additionally, the vector algebra is optimized by a ad-hoc code generation. Essentially this means 208 | that upon first load of the library, we check the dimension of the space where you want to perform 209 | layout, and generate all required data structure to run fast in this space. 210 | 211 | The code generation happens only once when dimension is requested. Any subsequent layouts in the same 212 | space would reuse generated codes. It is pretty fast and cool. 213 | 214 | # install 215 | 216 | With [npm](https://npmjs.org) do: 217 | 218 | ``` 219 | npm install ngraph.forcelayout 220 | ``` 221 | 222 | Or download from CDN: 223 | 224 | ``` html 225 | 226 | ``` 227 | 228 | If you download from CDN the library will be available under `ngraphCreateLayout` global name. 229 | 230 | # license 231 | 232 | MIT 233 | 234 | # Feedback? 235 | 236 | I'd totally love it! Please email me, open issue here, [tweet](https://twitter.com/anvaka) to me, 237 | or join discussion [on gitter](https://gitter.im/anvaka/VivaGraphJS). 238 | 239 | If you love this library, please consider sponsoring it at https://github.com/sponsors/anvaka or at 240 | https://www.patreon.com/anvaka 241 | -------------------------------------------------------------------------------- /demo-static/README.md: -------------------------------------------------------------------------------- 1 | ## Demo static 2 | 3 | This folder shows how to use prcompiled version of the force layout. 4 | 5 | The entire code is insdide [index.html](index.html) file and is less than 70 lines. 6 | -------------------------------------------------------------------------------- /demo-static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Static example of layout 8 | 23 | 24 | 25 | 26 | 27 | 28 | 68 | 69 | -------------------------------------------------------------------------------- /demo/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /demo/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true 5 | }, 6 | 'extends': [ 7 | 'plugin:vue/essential', 8 | 'eslint:recommended' 9 | ], 10 | rules: { 11 | 'no-console': process.env.NODE_ENV === 'production' ? 'error' : 'off', 12 | 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 13 | 'no-unused-vars': 'off' 14 | }, 15 | parserOptions: { 16 | parser: 'babel-eslint' 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /demo/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | # local env files 6 | .env.local 7 | .env.*.local 8 | 9 | # Log files 10 | npm-debug.log* 11 | yarn-debug.log* 12 | yarn-error.log* 13 | 14 | # Editor directories and files 15 | .idea 16 | .vscode 17 | *.suo 18 | *.ntvs* 19 | *.njsproj 20 | *.sln 21 | *.sw* 22 | -------------------------------------------------------------------------------- /demo/README.md: -------------------------------------------------------------------------------- 1 | # ngraph.forcelayout demo 2 | 3 | This folder contains a demo of the `ngraph.forcelayout` package. 4 | 5 | If you drop any `.dot` file into the browser window the demo will attempt to visualize it. 6 | 7 | 8 | ### Compiles and hot-reloads for development 9 | 10 | ``` 11 | npm start 12 | ``` 13 | 14 | This should render a simple graph and you can do some basic layout. You can drop `.dot` files into it 15 | to load new graphs. 16 | 17 | ### Compiles and minifies for production 18 | 19 | ``` 20 | npm run build 21 | ``` 22 | 23 | ## What's inside? 24 | 25 | * [ngraph.graph](https://github.com/anvaka/ngraph.graph) as a graph data structure 26 | * [ngraph.forcelayout](https://github.com/anvaka/ngraph.forcelayout) for the basic graph layout 27 | * [w-gl](https://github.com/anvaka/w-gl) - super duper obscure (and fast) WebGL renderer. 28 | * vue.js powered UI and dev tools. 29 | 30 | ## Thanks! 31 | 32 | * Stay tuned for updates: https://twitter.com/anvaka 33 | * If you like my work and would like to support it - https://www.patreon.com/anvaka -------------------------------------------------------------------------------- /demo/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [ 3 | '@vue/cli-plugin-babel/preset' 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /demo/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | rm -rf ./dist 3 | npm run build 4 | cd ./dist 5 | git init 6 | git add . 7 | git commit -m 'push to gh-pages' 8 | ## Change the line below to deploy to your gh-pages 9 | git push --force git@github.com:anvaka/ngraph.forcelayout.git main:gh-pages 10 | cd ../ 11 | -------------------------------------------------------------------------------- /demo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "graph-layout-demo", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "serve": "vue-cli-service serve", 7 | "start": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "lint": "vue-cli-service lint" 10 | }, 11 | "dependencies": { 12 | "core-js": "^3.6.5", 13 | "d3-color": "^2.0.0", 14 | "miserables": "^2.0.0", 15 | "ngraph.events": "^1.0.0", 16 | "ngraph.forcelayout": "^3.2.0", 17 | "ngraph.fromdot": "^6.0.0", 18 | "ngraph.fromjson": "^3.0.0", 19 | "ngraph.generators": "^20.0.0", 20 | "ngraph.graph": "^20.0.0", 21 | "ngraph.hde": "^1.0.1", 22 | "query-state": "^4.3.0", 23 | "vue": "^2.6.12", 24 | "w-gl": "^0.19.0" 25 | }, 26 | "devDependencies": { 27 | "@vue/cli-plugin-babel": "^4.5.4", 28 | "@vue/cli-plugin-eslint": "^4.5.4", 29 | "@vue/cli-service": "^4.5.4", 30 | "babel-eslint": "^10.0.3", 31 | "eslint": "^6.7.2", 32 | "eslint-plugin-vue": "^6.2.2", 33 | "stylus": "^0.54.8", 34 | "stylus-loader": "^3.0.2", 35 | "vue-template-compiler": "^2.6.11" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /demo/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | 16 | 17 | 18 | ngraph.forcelayout demo 19 | 20 | 43 | 44 | 45 | 48 | 49 |
50 | 51 | 52 | 53 | -------------------------------------------------------------------------------- /demo/src/App.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 152 | 153 | 226 | -------------------------------------------------------------------------------- /demo/src/components/HelpIcon.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | -------------------------------------------------------------------------------- /demo/src/components/InputFlag.vue: -------------------------------------------------------------------------------- 1 | 15 | 44 | 45 | -------------------------------------------------------------------------------- /demo/src/components/InputValue.vue: -------------------------------------------------------------------------------- 1 | 22 | 55 | 56 | -------------------------------------------------------------------------------- /demo/src/lib/LineCollection.js: -------------------------------------------------------------------------------- 1 | import {GLCollection, defineProgram, InstancedAttribute, ColorAttribute} from 'w-gl'; 2 | 3 | export default class LineCollection extends GLCollection { 4 | constructor(gl, options = {}) { 5 | let program = defineProgram({ 6 | gl, 7 | vertex: ` 8 | uniform mat4 modelViewProjection; 9 | uniform float width; 10 | uniform vec2 resolution; 11 | 12 | attribute vec4 color; 13 | attribute vec3 from, to; 14 | attribute vec2 point; 15 | 16 | varying vec4 vColor; 17 | varying vec2 vPoint; 18 | 19 | void main() { 20 | vec4 clip0 = modelViewProjection * vec4(from, 1.0); 21 | vec4 clip1 = modelViewProjection * vec4(to, 1.0); 22 | 23 | vec2 screen0 = resolution * (0.5 * clip0.xy/clip0.w + 0.5); 24 | vec2 screen1 = resolution * (0.5 * clip1.xy/clip1.w + 0.5); 25 | 26 | vec2 xBasis = normalize(screen1 - screen0); 27 | vec2 yBasis = vec2(-xBasis.y, xBasis.x); 28 | 29 | // Offset the original points: 30 | vec2 pt0 = screen0 + width * point.x * yBasis; 31 | vec2 pt1 = screen1 + width * point.x * yBasis; 32 | 33 | vec2 pt = mix(pt0, pt1, point.y); 34 | vec4 clip = mix(clip0, clip1, point.y); 35 | 36 | gl_Position = vec4(clip.w * (2.0 * pt/resolution - 1.0), clip.z, clip.w); 37 | vColor = color.abgr; // mix(.abgr, aToColor.abgr, aPosition.y); 38 | }`, 39 | 40 | fragment: ` 41 | precision highp float; 42 | varying vec4 vColor; 43 | 44 | void main() { 45 | gl_FragColor = vColor; 46 | }`, 47 | attributes: { 48 | color: new ColorAttribute() 49 | }, 50 | instanced: { 51 | point: new InstancedAttribute([ 52 | -0.5, 0, -0.5, 1, 0.5, 1, // First 2D triangle of the quad 53 | -0.5, 0, 0.5, 1, 0.5, 0 // Second 2D triangle of the quad 54 | ]) 55 | } 56 | }); 57 | super(program); 58 | this.width = options.width || 2; 59 | } 60 | 61 | draw(_, drawContext) { 62 | if (!this.uniforms) { 63 | this.uniforms = { 64 | modelViewProjection: this.modelViewProjection, 65 | width: this.width, 66 | resolution: [drawContext.width, drawContext.height] 67 | } 68 | } 69 | this.uniforms.resolution[0] = drawContext.width; 70 | this.uniforms.resolution[1] = drawContext.height; 71 | this.program.draw(this.uniforms); 72 | } 73 | 74 | // implement lineRenderTrait to allow SVG export via w-gl 75 | forEachLine(cb) { 76 | let count = this.program.getCount() 77 | for (let i = 0; i < count; ++i) { 78 | let vertex = this.program.get(i); 79 | let from = { x: vertex.from[0], y: vertex.from[1], z: vertex.from[2], color: vertex.color } 80 | let to = { x: vertex.to[0], y: vertex.to[1], z: vertex.to[2], color: vertex.color } 81 | cb(from, to); 82 | } 83 | } 84 | 85 | getLineColor(from) { 86 | let count = this.program.getCount() 87 | let c = from ? 88 | from.color : 89 | count > 0 ? this.program.get(0).color : 0xFFFFFFFF; 90 | 91 | return [ 92 | (c >> 24) & 0xFF / 255, 93 | (c >> 16) & 0xFF / 255, 94 | (c >> 8) & 0xFF / 255, 95 | (c >> 0) & 0xFF / 255, 96 | ] 97 | } 98 | } -------------------------------------------------------------------------------- /demo/src/lib/PointCollection.js: -------------------------------------------------------------------------------- 1 | 2 | import {GLCollection, defineProgram, ColorAttribute, InstancedAttribute} from 'w-gl'; 3 | 4 | export default class PointCollection extends GLCollection { 5 | constructor(gl) { 6 | let program = defineProgram({ 7 | gl, 8 | vertex: ` 9 | uniform mat4 modelViewProjection; 10 | 11 | attribute float size; 12 | attribute vec3 position; 13 | attribute vec4 color; 14 | 15 | attribute vec2 point; // instanced 16 | 17 | varying vec4 vColor; 18 | varying vec2 vPoint; 19 | void main() { 20 | gl_Position = modelViewProjection * vec4(position + vec3(point * size, 0.), 1.0); 21 | vColor = color.abgr; 22 | vPoint = point; 23 | }`, 24 | 25 | fragment: ` 26 | precision highp float; 27 | varying vec4 vColor; 28 | varying vec2 vPoint; 29 | void main() { 30 | float dist = length(vPoint); 31 | if (dist >= 0.5) {discard;} 32 | gl_FragColor = vColor; 33 | }`, 34 | // These are just overrides: 35 | attributes: { 36 | color: new ColorAttribute(), 37 | }, 38 | instanced: { 39 | point: new InstancedAttribute([ 40 | -0.5, -0.5, -0.5, 0.5, 0.5, 0.5, 41 | 0.5, 0.5, 0.5, -0.5, -0.5, -0.5, 42 | ]) 43 | }, 44 | 45 | preDrawHook(/* programInfo */) { 46 | return `gl.enable(gl.DEPTH_TEST); 47 | gl.depthFunc(gl.LEQUAL);`; 48 | }, 49 | postDrawHook() { 50 | return 'gl.disable(gl.DEPTH_TEST);'; 51 | }, 52 | }); 53 | 54 | super(program); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /demo/src/lib/bus.js: -------------------------------------------------------------------------------- 1 | import eventify from 'ngraph.events'; 2 | 3 | const bus = eventify({}); 4 | 5 | export default bus; -------------------------------------------------------------------------------- /demo/src/lib/createForceLayout.js: -------------------------------------------------------------------------------- 1 | import createLayout from '../../../'; 2 | 3 | export default function createForceLayout(graph, layoutSettings) { 4 | // return window.ngraphCreate2dLayout(graph, Object.assign({ 5 | return createLayout(graph, Object.assign({ 6 | dimensions: 2, 7 | timeStep: 0.5, 8 | springLength: 10, 9 | gravity: -12, 10 | springCoefficient: 0.8, 11 | dragCoefficient: 0.9, 12 | // adaptiveTimeStepWeight: 0.1, 13 | debug: false, 14 | }, layoutSettings)); 15 | } 16 | -------------------------------------------------------------------------------- /demo/src/lib/createGraphScene.js: -------------------------------------------------------------------------------- 1 | import {createScene, createGuide} from 'w-gl'; 2 | import LineCollection from './LineCollection'; 3 | import PointCollection from './PointCollection'; 4 | import bus from './bus'; 5 | import createHighLayout from 'ngraph.hde' 6 | import createForceLayout from './createForceLayout'; 7 | import findLargestComponent from './findLargestComponent'; 8 | import createGraph from 'ngraph.graph'; 9 | 10 | export default function createGraphScene(canvas, layoutSettings = {}) { 11 | let drawLinks = true; 12 | 13 | // Since graph can be loaded dynamically, we have these uninitialized 14 | // and captured into closure. loadGraph will do the initialization 15 | let graph, layout; 16 | let scene, nodes, lines, guide; 17 | 18 | let fixedViewBox = false; 19 | let isRunning = false; 20 | let rafHandle; 21 | 22 | bus.on('load-graph', loadGraph); 23 | 24 | return { 25 | dispose, 26 | runLayout, 27 | updateLayoutSettings, 28 | setFixedViewBox, 29 | }; 30 | 31 | function loadGraph(newGraph, desiredLayout) { 32 | if (scene) { 33 | scene.dispose(); 34 | layout.dispose(); 35 | scene = null 36 | isRunning = false; 37 | cancelAnimationFrame(rafHandle); 38 | } 39 | // newGraph = createGraph(); newGraph.addLink(1, 2) 40 | scene = initScene(); 41 | 42 | graph = newGraph; //findLargestComponent(newGraph, 1)[0]; 43 | 44 | // Let them play on console with it! 45 | window.graph = graph; 46 | 47 | guide = createGuide(scene, {showGrid: true, lineColor: 0xffffff10, maxAlpha: 0x10, showCursor: false}); 48 | // this is a standard force layout 49 | layout = createForceLayout(graph, layoutSettings); 50 | 51 | //standardizePositions(layout) 52 | let minX = -42, minY = -42; 53 | let maxX = 42, maxY =42 54 | 55 | setSceneSize(Math.max(maxX - minX, maxY - minY) * 1.2); 56 | initUIElements(); 57 | 58 | rafHandle = requestAnimationFrame(frame); 59 | } 60 | 61 | function setSceneSize(sceneSize) { 62 | scene.setViewBox({ 63 | left: -sceneSize, 64 | top: -sceneSize, 65 | right: sceneSize, 66 | bottom: sceneSize, 67 | }); 68 | } 69 | 70 | function runLayout(newIsRunning) { 71 | isRunning = newIsRunning; 72 | } 73 | 74 | function updateLayoutSettings(newLayoutSettings) { 75 | let props = ['timeStep', 'springLength', 'springCoefficient', 'dimensions', 'dragCoefficient', 'gravity', 'theta'] 76 | let previousDimensions = (layoutSettings && layoutSettings.dimensions) || 2; 77 | layoutSettings = props.reduce((settings, name) => (settings[name] = newLayoutSettings[name], settings), {}); 78 | if (!layout) return; 79 | 80 | if (layoutSettings.dimensions !== previousDimensions) { 81 | let prevLayout = layout; 82 | layout = createForceLayout(graph, layoutSettings) 83 | graph.forEachNode(node => { 84 | let prevPos = prevLayout.getNodePosition(node.id); 85 | let positions = Object.keys(prevPos).map(name => prevPos[name]); 86 | for (let i = previousDimensions; i < layoutSettings.dimensions; ++i) { 87 | // If new layout has more dimensions than the previous layout, fill those with random values: 88 | positions.push(Math.random()); 89 | } 90 | positions.unshift(node.id); 91 | layout.setNodePosition.apply(layout, positions); 92 | }); 93 | 94 | prevLayout.dispose(); 95 | } else { 96 | props.forEach(name => { 97 | layout.simulator[name](layoutSettings[name]); 98 | }); 99 | } 100 | } 101 | 102 | function setFixedViewBox(isFixed) { 103 | fixedViewBox = isFixed; 104 | } 105 | 106 | function initScene() { 107 | let scene = createScene(canvas); 108 | scene.setClearColor(12/255, 41/255, 82/255, 1) 109 | return scene; 110 | } 111 | 112 | function initUIElements() { 113 | nodes = new PointCollection(scene.getGL(), { 114 | capacity: graph.getNodesCount() 115 | }); 116 | 117 | graph.forEachNode(node => { 118 | var point = layout.getNodePosition(node.id); 119 | let size = 1; 120 | if (node.data && node.data.size) { 121 | size = node.data.size; 122 | } else { 123 | if (!node.data) node.data = {}; 124 | node.data.size = size; 125 | } 126 | node.ui = {size, position: [point.x, point.y, point.z || 0], color: 0x90f8fcff}; 127 | node.uiId = nodes.add(node.ui); 128 | }); 129 | 130 | lines = new LineCollection(scene.getGL(), { capacity: graph.getLinksCount() }); 131 | 132 | graph.forEachLink(link => { 133 | var from = layout.getNodePosition(link.fromId); 134 | var to = layout.getNodePosition(link.toId); 135 | var line = { from: [from.x, from.y, from.z || 0], to: [to.x, to.y, to.z || 0], color: 0xFFFFFF10 }; 136 | link.ui = line; 137 | link.uiId = lines.add(link.ui); 138 | }); 139 | // lines.add({from: [0, 0, 0], to: [0, 10, 0], color: 0xFF0000FF}) 140 | 141 | scene.appendChild(lines); 142 | scene.appendChild(nodes); 143 | } 144 | 145 | function frame() { 146 | rafHandle = requestAnimationFrame(frame); 147 | 148 | if (isRunning) { 149 | layout.step(); 150 | if (fixedViewBox) { 151 | let rect = layout.getGraphRect(); 152 | scene.setViewBox({ 153 | left: rect.min_x, 154 | top: rect.min_y, 155 | right: rect.max_x, 156 | bottom: rect.max_y, 157 | }); 158 | } 159 | } 160 | drawGraph(); 161 | scene.renderFrame(); 162 | } 163 | 164 | function drawGraph() { 165 | let names = ['x', 'y', 'z'] 166 | // let minR = Infinity; let maxR = -minR; 167 | // let minG = Infinity; let maxG = -minG; 168 | // let minB = Infinity; let maxB = -minB; 169 | // graph.forEachNode(node => { 170 | // let pos = layout.getNodePosition(node.id); 171 | // if (pos.c4 < minR) minR = pos.c4; 172 | // if (pos.c4 > maxR) maxR = pos.c4; 173 | 174 | // if (pos.c5 < minG) minG = pos.c5; 175 | // if (pos.c5 > maxG) maxG = pos.c5; 176 | 177 | // if (pos.c6 < minB) minB = pos.c6; 178 | // if (pos.c6 > maxB) maxB = pos.c6; 179 | // }); 180 | 181 | graph.forEachNode(node => { 182 | let pos = layout.getNodePosition(node.id); 183 | let uiPosition = node.ui.position; 184 | for (let i = 0; i < 3; ++i) { 185 | uiPosition[i] = pos[names[i]] || 0; 186 | } 187 | // let r = Math.floor(255 * (pos.c4 - minR) / (maxR - minR)) << 24; 188 | // let g = Math.floor(255 * (pos.c5 - minG) / (maxG - minG)) << 16; 189 | // let b = Math.floor(255 * (pos.c6 - minB) / (maxB - minB)) << 8; 190 | // [r, g, b] = lab2rgb( 191 | // (pos.c4 - minR) / (maxR - minR), 192 | // (pos.c5 - minG) / (maxG - minG), 193 | // (pos.c6 - minB) / (maxB - minB) 194 | // ); 195 | // node.ui.color = (0x000000FF | r | g | b); 196 | nodes.update(node.uiId, node.ui) 197 | }); 198 | 199 | if (drawLinks) { 200 | graph.forEachLink(link => { 201 | var fromPos = layout.getNodePosition(link.fromId); 202 | var toPos = layout.getNodePosition(link.toId); 203 | let {from, to} = link.ui; 204 | 205 | for (let i = 0; i < 3; ++i) { 206 | from[i] = fromPos[names[i]] || 0; 207 | to[i] = toPos[names[i]] || 0; 208 | } 209 | // from[0] = fromPos.x || 0; from[1] = fromPos.y || 0; from[2] = fromPos.z || 0; 210 | // to[0] = toPos.x || 0; to[1] = toPos.y || 0; to[2] = toPos.z || 0; 211 | // link.ui.color = lerp(graph.getNode(link.fromId).ui.color, graph.getNode(link.toId).ui.color); 212 | lines.update(link.uiId, link.ui); 213 | }) 214 | } 215 | } 216 | 217 | function lerp(aColor, bColor) { 218 | let ar = (aColor >> 24) & 0xFF; 219 | let ag = (aColor >> 16) & 0xFF; 220 | let ab = (aColor >> 8) & 0xFF; 221 | let br = (bColor >> 24) & 0xFF; 222 | let bg = (bColor >> 16) & 0xFF; 223 | let bb = (bColor >> 8) & 0xFF; 224 | let r = Math.floor((ar + br) / 2); 225 | let g = Math.floor((ag + bg) / 2); 226 | let b = Math.floor((ab + bb) / 2); 227 | return (r << 24) | (g << 16) | (b << 8) | 0xF0; 228 | } 229 | 230 | function dispose() { 231 | cancelAnimationFrame(rafHandle); 232 | 233 | scene.dispose(); 234 | bus.off('load-graph', loadGraph); 235 | } 236 | } 237 | 238 | function standardizePositions(layout) { 239 | let arr = []; 240 | let avgX = 0, avgY = 0; 241 | layout.forEachBody(body => { 242 | arr.push(body.pos); 243 | avgX += body.pos.x; 244 | avgY += body.pos.y; 245 | }); 246 | let meanX = avgX / arr.length; 247 | let meanY = avgY / arr.length; 248 | let varX = 0, varY = 0; 249 | arr.forEach(pos => { 250 | varX += Math.pow(pos.x - meanX, 2); 251 | varY += Math.pow(pos.y - meanY, 2); 252 | }); 253 | varX = Math.sqrt(varX / arr.length); 254 | varY = Math.sqrt(varY / arr.length); 255 | arr.forEach(pos => { 256 | pos.x = 10 * (pos.x - meanX) / varX; 257 | pos.y = 10 * (pos.y - meanY) / varY; 258 | }); 259 | } -------------------------------------------------------------------------------- /demo/src/lib/fileDrop.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Handles dropped files into the browser. 3 | */ 4 | export default function fileDrop(dropHandler, onDropped) { 5 | dropHandler.addEventListener('drop', handleDrop, true); 6 | dropHandler.addEventListener('dragover', handleDragOver); 7 | dropHandler.addEventListener('dragenter', prevent); 8 | dropHandler.addEventListener('dragleave', handleDragEnd) 9 | dropHandler.addEventListener('dragend', handleDragEnd); 10 | 11 | return { 12 | dispose 13 | } 14 | 15 | function dispose() { 16 | dropHandler.removeEventListener('drop', handleDrop); 17 | dropHandler.removeEventListener('dragover', handleDragOver); 18 | dropHandler.removeEventListener('dragenter', prevent); 19 | dropHandler.removeEventListener('dragleave', handleDragEnd) 20 | dropHandler.removeEventListener('dragend', handleDragEnd); 21 | } 22 | 23 | function prevent(e) { 24 | if (!hasFiles(e)) return; 25 | 26 | e.preventDefault(); 27 | } 28 | 29 | function handleDrop(ev) { 30 | handleDragEnd(); 31 | ev.preventDefault(); 32 | // If dropped items aren't files, reject them 33 | var dt = ev.dataTransfer; 34 | var files = [] 35 | var i, file; 36 | if (dt.items) { 37 | // Use DataTransferItemList interface to access the file(s) 38 | for (i = 0; i < dt.items.length; i++) { 39 | if (dt.items[i].kind == "file") { 40 | file = dt.items[i].getAsFile(); 41 | files.push(file); 42 | } 43 | } 44 | } else { 45 | // Use DataTransfer interface to access the file(s) 46 | for (i = 0; i < dt.files.length; i++) { 47 | file = dt.files[i]; 48 | files.push(file); 49 | } 50 | } 51 | 52 | onDropped(files); 53 | } 54 | 55 | 56 | function handleDragOver(e) { 57 | if (!hasFiles(e)) return; 58 | 59 | e.preventDefault(); 60 | dropHandler.classList.add('drag-over'); 61 | } 62 | 63 | function hasFiles(e) { 64 | if (!e.dataTransfer) return false; 65 | if (e.dataTransfer.files && e.dataTransfer.files.length > 0) return true; 66 | var items = e.dataTransfer.items; 67 | if (!items) return false; 68 | for (var i = 0; i < items.length; ++i) { 69 | if (items[i].kind === 'file') return true; 70 | } 71 | 72 | return false; 73 | } 74 | 75 | function handleDragEnd() { 76 | dropHandler.classList.remove('drag-over'); 77 | } 78 | } -------------------------------------------------------------------------------- /demo/src/lib/findLargestComponent.js: -------------------------------------------------------------------------------- 1 | import createGraph from 'ngraph.graph'; 2 | 3 | /** 4 | * Returns array of first `count` largest connected components 5 | * of the `graph` 6 | */ 7 | export default function findLargestComponent(graph, count) { 8 | var nodeIdToComponentId = new Map(); 9 | 10 | var connectedComponents = []; 11 | var lastComponentId = 0; 12 | 13 | graph.forEachNode(function(node) { 14 | if (nodeIdToComponentId.has(node.id)) { 15 | // we already seen this cluster. Ignore it. 16 | return; 17 | } 18 | 19 | // We found a new connected component: 20 | nodeIdToComponentId.set(node.id, lastComponentId); 21 | var currentComponent = new Set(); 22 | connectedComponents.push(currentComponent); 23 | 24 | // Let's find what other nodes belong to this component 25 | bfs(graph, node.id, otherNode => { 26 | currentComponent.add(otherNode); 27 | nodeIdToComponentId.set(otherNode, lastComponentId); 28 | }); 29 | 30 | lastComponentId += 1; 31 | }); 32 | 33 | return connectedComponents.sort((a, b) => b.size - a.size) 34 | .slice(0, count) 35 | .map(largestComponent => { 36 | let subGraph = createGraph(); 37 | // not the most efficient way, as we iterate over every single link. 38 | // This could be improved, for example by performing bfs from the component 39 | graph.forEachLink(link => { 40 | if (largestComponent.has(link.fromId)) { 41 | subGraph.addLink(link.fromId, link.toId); 42 | } 43 | }) 44 | 45 | return subGraph; 46 | }); 47 | } 48 | 49 | function bfs(graph, startFromNodeId, visitor) { 50 | let queue = [startFromNodeId]; 51 | let visited = new Set(queue); 52 | 53 | while (queue.length) { 54 | let nodeId = queue.shift(); 55 | visitor(nodeId); 56 | 57 | graph.forEachLinkedNode(nodeId, function(otherNode) { 58 | if (visited.has(otherNode.id)) return; 59 | 60 | queue.push(otherNode.id); 61 | visited.add(otherNode.id); 62 | }); 63 | } 64 | } -------------------------------------------------------------------------------- /demo/src/lib/getAvailableGraphs.js: -------------------------------------------------------------------------------- 1 | export default function getAvailableGraphs() { 2 | return [ 3 | 'Miserables', 4 | 'Binary', 5 | 'HB/blckhole', 6 | 'Bai/rw5151', 7 | 'HB/bcsstm13', 8 | 'HB/lshp1882', 9 | 'HB/plat1919', 10 | 'HB/bcsstk26', 11 | 'Bai/dw256A', 12 | 'Bai/tols2000', 13 | 'Bai/dw1024', 14 | 'Bai/rdb2048', 15 | 'Pajek/CSphd', 16 | 'GHS_indef/laser', 17 | "Bai/bfwa398", 18 | "Bai/bfwa62", 19 | "Bai/bfwb398", 20 | "Bai/bfwb62", 21 | "Bai/bfwb782", 22 | "Bai/bwm200", 23 | "Bai/cdde1", 24 | "Bai/cdde2", 25 | "Bai/cdde3", 26 | "Bai/cdde4", 27 | "Bai/cdde5", 28 | "Bai/cdde6", 29 | "Bai/ck104", 30 | "Bai/ck400", 31 | "Bai/ck656", 32 | "Bai/dw256B", 33 | "Bai/dwa512", 34 | "Bai/dwb512", 35 | "Bai/dwg961a", 36 | "Bai/lop163", 37 | "Bai/mhdb416", 38 | "Bai/odepa400", 39 | "Bai/olm100", 40 | "Bai/olm1000", 41 | "Bai/olm500", 42 | "Bai/pde225", 43 | "Bai/pde900", 44 | "Bai/qh1484", 45 | "Bai/qh768", 46 | "Bai/qh882", 47 | "Bai/rdb1250", 48 | "Bai/rdb1250l", 49 | "Bai/rdb200", 50 | "Bai/rdb200l", 51 | "Bai/rdb450", 52 | "Bai/rdb450l", 53 | "Bai/rdb800l", 54 | "Bai/rdb968", 55 | "Bai/rw136", 56 | "Bai/rw496", 57 | "Bai/tols1090", 58 | "Bai/tols340", 59 | "Bai/tols90", 60 | "Bai/tub100", 61 | "Bai/tub1000", 62 | "Barabasi/NotreDame_yeast", 63 | "Bates/Chem97ZtZ", 64 | "FEMLAB/poisson2D", 65 | "FEMLAB/problem1", 66 | "FIDAP/ex1", 67 | "FIDAP/ex5", 68 | "Grund/b1_ss", 69 | "Grund/d_ss", 70 | "Grund/poli", 71 | "Gset/G11", 72 | "Gset/G12", 73 | "Gset/G13", 74 | "Gset/G14", 75 | "Gset/G15", 76 | "Gset/G16", 77 | "Gset/G17", 78 | "Gset/G18", 79 | "Gset/G19", 80 | "Gset/G20", 81 | "Gset/G21", 82 | "Gset/G32", 83 | "Gset/G33", 84 | "Gset/G34", 85 | "Gset/G35", 86 | "Gset/G36", 87 | "Gset/G37", 88 | "Gset/G38", 89 | "Gset/G39", 90 | "Gset/G40", 91 | "Gset/G41", 92 | "Gset/G42", 93 | "Gset/G43", 94 | "Gset/G44", 95 | "Gset/G45", 96 | "Gset/G46", 97 | "Gset/G47", 98 | "Gset/G48", 99 | "Gset/G49", 100 | "Gset/G50", 101 | "Gset/G51", 102 | "Gset/G52", 103 | "Gset/G53", 104 | "Gset/G54", 105 | "Gset/G55", 106 | "Gset/G57", 107 | "Hamrle/Hamrle1", 108 | "HB/1138_bus", 109 | "HB/494_bus", 110 | "HB/662_bus", 111 | "HB/685_bus", 112 | "HB/abb313", 113 | "HB/arc130", 114 | "HB/ash219", 115 | "HB/ash292", 116 | "HB/ash331", 117 | "HB/ash608", 118 | "HB/ash85", 119 | "HB/ash958", 120 | "HB/bcspwr01", 121 | "HB/bcspwr02", 122 | "HB/bcspwr03", 123 | "HB/bcspwr04", 124 | "HB/bcspwr05", 125 | "HB/bcspwr06", 126 | "HB/bcspwr07", 127 | "HB/bcspwr08", 128 | "HB/bcspwr09", 129 | "HB/bcsstk01", 130 | "HB/bcsstk02", 131 | "HB/bcsstk03", 132 | "HB/bcsstk04", 133 | "HB/bcsstk05", 134 | "HB/bcsstk06", 135 | "HB/bcsstk07", 136 | "HB/bcsstk19", 137 | "HB/bcsstk20", 138 | "HB/bcsstk22", 139 | "HB/bcsstm01", 140 | "HB/bcsstm02", 141 | "HB/bcsstm03", 142 | "HB/bcsstm04", 143 | "HB/bcsstm05", 144 | "HB/bcsstm06", 145 | "HB/bcsstm07", 146 | "HB/bcsstm08", 147 | "HB/bcsstm09", 148 | "HB/bcsstm11", 149 | "HB/bcsstm19", 150 | "HB/bcsstm20", 151 | "HB/bcsstm21", 152 | "HB/bcsstm22", 153 | "HB/bcsstm23", 154 | "HB/bcsstm24", 155 | "HB/bcsstm26", 156 | "HB/bp_0", 157 | "HB/bp_1000", 158 | "HB/bp_1200", 159 | "HB/bp_1400", 160 | "HB/bp_1600", 161 | "HB/bp_200", 162 | "HB/bp_400", 163 | "HB/bp_600", 164 | "HB/bp_800", 165 | "HB/can_1054", 166 | "HB/can_1072", 167 | "HB/can_144", 168 | "HB/can_161", 169 | "HB/can_187", 170 | "HB/can_229", 171 | "HB/can_24", 172 | "HB/can_256", 173 | "HB/can_268", 174 | "HB/can_292", 175 | "HB/can_445", 176 | "HB/can_61", 177 | "HB/can_62", 178 | "HB/can_634", 179 | "HB/can_715", 180 | "HB/can_73", 181 | "HB/can_838", 182 | "HB/can_96", 183 | "HB/curtis54", 184 | "HB/dwt_1005", 185 | "HB/dwt_1007", 186 | "HB/dwt_1242", 187 | "HB/dwt_162", 188 | "HB/dwt_193", 189 | "HB/dwt_198", 190 | "HB/dwt_209", 191 | "HB/dwt_221", 192 | "HB/dwt_234", 193 | "HB/dwt_245", 194 | "HB/dwt_2680", 195 | "HB/dwt_307", 196 | "HB/dwt_310", 197 | "HB/dwt_346", 198 | "HB/dwt_361", 199 | "HB/dwt_419", 200 | "HB/dwt_492", 201 | "HB/dwt_503", 202 | "HB/dwt_512", 203 | "HB/dwt_59", 204 | "HB/dwt_592", 205 | "HB/dwt_607", 206 | "HB/dwt_66", 207 | "HB/dwt_72", 208 | "HB/dwt_758", 209 | "HB/dwt_869", 210 | "HB/dwt_87", 211 | "HB/dwt_878", 212 | "HB/dwt_918", 213 | "HB/dwt_992", 214 | "HB/eris1176", 215 | "HB/fs_183_1", 216 | "HB/fs_183_3", 217 | "HB/fs_183_4", 218 | "HB/fs_183_6", 219 | "HB/fs_541_1", 220 | "HB/fs_541_2", 221 | "HB/fs_541_3", 222 | "HB/fs_541_4", 223 | "HB/fs_680_1", 224 | "HB/fs_680_2", 225 | "HB/fs_680_3", 226 | "HB/gent113", 227 | "HB/gr_30_30", 228 | "HB/gre_1107", 229 | "HB/gre_115", 230 | "HB/gre_185", 231 | "HB/gre_216a", 232 | "HB/gre_216b", 233 | "HB/gre_343", 234 | "HB/gre_512", 235 | "HB/hor_131", 236 | "HB/ibm32", 237 | "HB/illc1033", 238 | "HB/impcol_a", 239 | "HB/impcol_b", 240 | "HB/impcol_c", 241 | "HB/impcol_d", 242 | "HB/impcol_e", 243 | "HB/jagmesh1", 244 | "HB/jagmesh2", 245 | "HB/jagmesh3", 246 | "HB/jagmesh4", 247 | "HB/jagmesh5", 248 | "HB/jagmesh7", 249 | "HB/jagmesh8", 250 | "HB/jagmesh9", 251 | "HB/jgl009", 252 | "HB/jgl011", 253 | "HB/jpwh_991", 254 | "HB/lap_25", 255 | "HB/lns_131", 256 | "HB/lns_511", 257 | "HB/lnsp_131", 258 | "HB/lnsp_511", 259 | "HB/lock_700", 260 | "HB/lshp1009", 261 | "HB/lshp1270", 262 | "HB/lshp1561", 263 | "HB/lshp2233", 264 | "HB/lshp2614", 265 | "HB/lshp3025", 266 | "HB/lshp3466", 267 | "HB/lshp_265", 268 | "HB/lshp_406", 269 | "HB/lshp_577", 270 | "HB/lshp_778", 271 | "HB/lund_a", 272 | "HB/lund_b", 273 | "HB/mcca", 274 | "HB/nnc261", 275 | "HB/nnc666", 276 | "HB/nos1", 277 | "HB/nos2", 278 | "HB/nos4", 279 | "HB/nos5", 280 | "HB/nos6", 281 | "HB/nos7", 282 | "HB/orsirr_1", 283 | "HB/orsirr_2", 284 | "HB/plat362", 285 | "HB/plskz362", 286 | "HB/pores_1", 287 | "HB/pores_3", 288 | "HB/rgg010", 289 | "HB/saylr1", 290 | "HB/saylr3", 291 | "HB/sherman1", 292 | "HB/sherman4", 293 | "HB/shl_0", 294 | "HB/shl_200", 295 | "HB/shl_400", 296 | "HB/sstmodel", 297 | "HB/steam1", 298 | "HB/steam3", 299 | "HB/str_0", 300 | "HB/str_200", 301 | "HB/str_400", 302 | "HB/str_600", 303 | "HB/well1033", 304 | "HB/west0067", 305 | "HB/west0132", 306 | "HB/west0156", 307 | "HB/west0167", 308 | "HB/west0381", 309 | "HB/west0479", 310 | "HB/west0497", 311 | "HB/west0655", 312 | "HB/west0989", 313 | "HB/west1505", 314 | "HB/west2021", 315 | "HB/will199", 316 | "HB/will57", 317 | "HB/wm1", 318 | "HB/wm2", 319 | "HB/wm3", 320 | "HB/young1c", 321 | "HB/young2c", 322 | "HB/young3c", 323 | "HB/young4c", 324 | "JGD_BIBD/bibd_11_5", 325 | "JGD_BIBD/bibd_12_4", 326 | "JGD_BIBD/bibd_12_5", 327 | "JGD_BIBD/bibd_15_3", 328 | "JGD_BIBD/bibd_17_3", 329 | "JGD_BIBD/bibd_17_4", 330 | "JGD_BIBD/bibd_17_4b", 331 | "JGD_BIBD/bibd_81_2", 332 | "JGD_BIBD/bibd_9_3", 333 | "JGD_BIBD/bibd_9_5", 334 | "JGD_CAG/CAG_mat364", 335 | "JGD_CAG/CAG_mat72", 336 | "JGD_Forest/TF10", 337 | "JGD_Forest/TF11", 338 | "JGD_Forest/TF12", 339 | "JGD_Forest/TF13", 340 | "JGD_Franz/Franz1", 341 | "JGD_Franz/Franz3", 342 | "JGD_G5/IG5-10", 343 | "JGD_G5/IG5-6", 344 | "JGD_G5/IG5-7", 345 | "JGD_G5/IG5-8", 346 | "JGD_G5/IG5-9", 347 | "JGD_GL6/GL6_D_10", 348 | "JGD_GL6/GL6_D_6", 349 | "JGD_GL6/GL6_D_7", 350 | "JGD_GL6/GL6_D_8", 351 | "JGD_GL6/GL6_D_9", 352 | "JGD_GL7d/GL7d10", 353 | "JGD_GL7d/GL7d11", 354 | "JGD_GL7d/GL7d26", 355 | "JGD_Homology/ch3-3-b1", 356 | "JGD_Homology/ch3-3-b2", 357 | "JGD_Homology/ch4-4-b1", 358 | "JGD_Homology/ch4-4-b2", 359 | "JGD_Homology/ch4-4-b3", 360 | "JGD_Homology/ch5-5-b1", 361 | "JGD_Homology/ch5-5-b2", 362 | "JGD_Homology/ch5-5-b3", 363 | "JGD_Homology/ch5-5-b4", 364 | "JGD_Homology/ch6-6-b1", 365 | "JGD_Homology/ch6-6-b2", 366 | "JGD_Homology/ch6-6-b5", 367 | "JGD_Homology/ch7-6-b1", 368 | "JGD_Homology/ch7-7-b1", 369 | "JGD_Homology/ch7-8-b1", 370 | "JGD_Homology/ch7-9-b1", 371 | "JGD_Homology/ch8-8-b1", 372 | "JGD_Homology/cis-n4c6-b1", 373 | "JGD_Homology/cis-n4c6-b15", 374 | "JGD_Homology/cis-n4c6-b2", 375 | "JGD_Homology/klein-b1", 376 | "JGD_Homology/klein-b2", 377 | "JGD_Homology/mk10-b1", 378 | "JGD_Homology/mk10-b2", 379 | "JGD_Homology/mk10-b4", 380 | "JGD_Homology/mk11-b1", 381 | "JGD_Homology/mk12-b1", 382 | "JGD_Homology/mk9-b1", 383 | "JGD_Homology/mk9-b2", 384 | "JGD_Homology/mk9-b3", 385 | "JGD_Homology/n2c6-b1", 386 | "JGD_Homology/n2c6-b10", 387 | "JGD_Homology/n2c6-b2", 388 | "JGD_Homology/n2c6-b3", 389 | "JGD_Homology/n2c6-b9", 390 | "JGD_Homology/n3c4-b1", 391 | "JGD_Homology/n3c4-b2", 392 | "JGD_Homology/n3c4-b3", 393 | "JGD_Homology/n3c4-b4", 394 | "JGD_Homology/n3c5-b1", 395 | "JGD_Homology/n3c5-b2", 396 | "JGD_Homology/n3c5-b3", 397 | "JGD_Homology/n3c5-b4", 398 | "JGD_Homology/n3c5-b5", 399 | "JGD_Homology/n3c5-b6", 400 | "JGD_Homology/n3c5-b7", 401 | "JGD_Homology/n3c6-b1", 402 | "JGD_Homology/n3c6-b10", 403 | "JGD_Homology/n3c6-b11", 404 | "JGD_Homology/n3c6-b2", 405 | "JGD_Homology/n3c6-b3", 406 | "JGD_Homology/n4c5-b1", 407 | "JGD_Homology/n4c5-b10", 408 | "JGD_Homology/n4c5-b11", 409 | "JGD_Homology/n4c5-b2", 410 | "JGD_Homology/n4c5-b3", 411 | "JGD_Homology/n4c5-b9", 412 | "JGD_Homology/n4c6-b1", 413 | "JGD_Homology/n4c6-b15", 414 | "JGD_Homology/n4c6-b2", 415 | "JGD_Kocay/Trec10", 416 | "JGD_Kocay/Trec3", 417 | "JGD_Kocay/Trec4", 418 | "JGD_Kocay/Trec5", 419 | "JGD_Kocay/Trec6", 420 | "JGD_Kocay/Trec7", 421 | "JGD_Kocay/Trec8", 422 | "JGD_Kocay/Trec9", 423 | "JGD_Margulies/cat_ears_2_1", 424 | "JGD_Margulies/cat_ears_2_4", 425 | "JGD_Margulies/cat_ears_3_1", 426 | "JGD_Margulies/cat_ears_4_1", 427 | "JGD_Margulies/flower_4_1", 428 | "JGD_Margulies/flower_5_1", 429 | "JGD_Margulies/flower_7_1", 430 | "JGD_Margulies/flower_8_1", 431 | "JGD_Margulies/kneser_6_2_1", 432 | "JGD_Margulies/wheel_3_1", 433 | "JGD_Margulies/wheel_4_1", 434 | "JGD_Margulies/wheel_5_1", 435 | "JGD_Margulies/wheel_6_1", 436 | "JGD_Margulies/wheel_7_1", 437 | "JGD_Relat/rel3", 438 | "JGD_Relat/rel4", 439 | "JGD_Relat/rel5", 440 | "JGD_Relat/rel6", 441 | "JGD_Relat/relat3", 442 | "JGD_Relat/relat4", 443 | "JGD_Relat/relat5", 444 | "JGD_Relat/relat6", 445 | "JGD_SL6/D_10", 446 | "JGD_SL6/D_11", 447 | "JGD_SL6/D_5", 448 | "JGD_SL6/D_6", 449 | "JGD_SPG/08blocks", 450 | "JGD_SPG/EX1", 451 | "JGD_SPG/EX2", 452 | "JGD_Trefethen/Trefethen_150", 453 | "JGD_Trefethen/Trefethen_20", 454 | "JGD_Trefethen/Trefethen_200", 455 | "JGD_Trefethen/Trefethen_200b", 456 | "JGD_Trefethen/Trefethen_20b", 457 | "JGD_Trefethen/Trefethen_300", 458 | "JGD_Trefethen/Trefethen_500", 459 | "JGD_Trefethen/Trefethen_700", 460 | "LPnetlib/lp_adlittle", 461 | "LPnetlib/lp_afiro", 462 | "LPnetlib/lp_agg", 463 | "LPnetlib/lp_agg2", 464 | "LPnetlib/lp_agg3", 465 | "LPnetlib/lp_bandm", 466 | "LPnetlib/lp_beaconfd", 467 | "LPnetlib/lp_blend", 468 | "LPnetlib/lp_bnl1", 469 | "LPnetlib/lp_bore3d", 470 | "LPnetlib/lp_brandy", 471 | "LPnetlib/lp_capri", 472 | "LPnetlib/lp_czprob", 473 | "LPnetlib/lp_degen2", 474 | "LPnetlib/lp_e226", 475 | "LPnetlib/lp_etamacro", 476 | "LPnetlib/lp_fffff800", 477 | "LPnetlib/lp_finnis", 478 | "LPnetlib/lp_fit1p", 479 | "LPnetlib/lp_ganges", 480 | "LPnetlib/lp_gfrd_pnc", 481 | "LPnetlib/lp_grow15", 482 | "LPnetlib/lp_grow7", 483 | "LPnetlib/lp_israel", 484 | "LPnetlib/lp_kb2", 485 | "LPnetlib/lp_ken_07", 486 | "LPnetlib/lp_lotfi", 487 | "LPnetlib/lp_modszk1", 488 | "LPnetlib/lp_perold", 489 | "LPnetlib/lp_pilot4", 490 | "LPnetlib/lp_qap8", 491 | "LPnetlib/lp_recipe", 492 | "LPnetlib/lp_sc105", 493 | "LPnetlib/lp_sc205", 494 | "LPnetlib/lp_sc50a", 495 | "LPnetlib/lp_sc50b", 496 | "LPnetlib/lp_scagr25", 497 | "LPnetlib/lp_scagr7", 498 | "LPnetlib/lp_scfxm1", 499 | "LPnetlib/lp_scfxm2", 500 | "LPnetlib/lp_scfxm3", 501 | "LPnetlib/lp_scorpion", 502 | "LPnetlib/lp_scrs8", 503 | "LPnetlib/lp_scsd1", 504 | "LPnetlib/lp_scsd6", 505 | "LPnetlib/lp_sctap1", 506 | "LPnetlib/lp_sctap2", 507 | "LPnetlib/lp_sctap3", 508 | "LPnetlib/lp_share1b", 509 | "LPnetlib/lp_share2b", 510 | "LPnetlib/lp_shell", 511 | "LPnetlib/lp_ship04l", 512 | "LPnetlib/lp_ship04s", 513 | "LPnetlib/lp_ship08s", 514 | "LPnetlib/lp_ship12s", 515 | "LPnetlib/lp_sierra", 516 | "LPnetlib/lp_stair", 517 | "LPnetlib/lp_standata", 518 | "LPnetlib/lp_standgub", 519 | "LPnetlib/lp_standmps", 520 | "LPnetlib/lp_stocfor1", 521 | "LPnetlib/lp_tuff", 522 | "LPnetlib/lp_vtp_base", 523 | "LPnetlib/lpi_bgdbg1", 524 | "LPnetlib/lpi_bgetam", 525 | "LPnetlib/lpi_bgprtr", 526 | "LPnetlib/lpi_box1", 527 | "LPnetlib/lpi_chemcom", 528 | "LPnetlib/lpi_cplex2", 529 | "LPnetlib/lpi_ex72a", 530 | "LPnetlib/lpi_ex73a", 531 | "LPnetlib/lpi_forest6", 532 | "LPnetlib/lpi_galenet", 533 | "LPnetlib/lpi_itest2", 534 | "LPnetlib/lpi_itest6", 535 | "LPnetlib/lpi_klein1", 536 | "LPnetlib/lpi_klein2", 537 | "LPnetlib/lpi_mondou2", 538 | "LPnetlib/lpi_pang", 539 | "LPnetlib/lpi_pilot4i", 540 | "LPnetlib/lpi_qual", 541 | "LPnetlib/lpi_reactor", 542 | "LPnetlib/lpi_refinery", 543 | "LPnetlib/lpi_vol1", 544 | "LPnetlib/lpi_woodinfe", 545 | "MathWorks/Harvard500", 546 | "MathWorks/Pd_rhs", 547 | "MathWorks/pivtol", 548 | "MathWorks/QRpivot", 549 | "Meszaros/cep1", 550 | "Meszaros/cr42", 551 | "Meszaros/farm", 552 | "Meszaros/gams10a", 553 | "Meszaros/gams10am", 554 | "Meszaros/gams30a", 555 | "Meszaros/gams30am", 556 | "Meszaros/gams60am", 557 | "Meszaros/gas11", 558 | "Meszaros/iiasa", 559 | "Meszaros/iprob", 560 | "Meszaros/kleemin", 561 | "Meszaros/l9", 562 | "Meszaros/model1", 563 | "Meszaros/model2", 564 | "Meszaros/nemsafm", 565 | "Meszaros/nemscem", 566 | "Meszaros/nsic", 567 | "Meszaros/p0033", 568 | "Meszaros/p0040", 569 | "Meszaros/p0201", 570 | "Meszaros/p0282", 571 | "Meszaros/p0291", 572 | "Meszaros/p0548", 573 | "Meszaros/p2756", 574 | "Meszaros/problem", 575 | "Meszaros/qiulp", 576 | "Meszaros/refine", 577 | "Meszaros/rosen7", 578 | "Meszaros/scagr7-2c", 579 | "Meszaros/scrs8-2b", 580 | "Meszaros/scrs8-2c", 581 | "Meszaros/small", 582 | "Meszaros/zed", 583 | "Morandini/robot", 584 | "Morandini/rotor1", 585 | "Muite/Chebyshev1", 586 | "NYPA/Maragal_1", 587 | "NYPA/Maragal_2", 588 | "Oberwolfach/LF10", 589 | "Oberwolfach/LFAT5", 590 | "Pajek/Cities", 591 | "Pajek/divorce", 592 | "Pajek/EPA", 593 | "Pajek/Erdos02", 594 | "Pajek/Erdos971", 595 | "Pajek/Erdos972", 596 | "Pajek/Erdos981", 597 | "Pajek/Erdos982", 598 | "Pajek/Erdos991", 599 | "Pajek/Erdos992", 600 | "Pajek/EVA", 601 | "Pajek/football", 602 | "Pajek/GD00_a", 603 | "Pajek/GD00_c", 604 | "Pajek/GD01_a", 605 | "Pajek/GD01_A", 606 | "Pajek/GD01_b", 607 | "Pajek/GD01_c", 608 | "Pajek/GD02_a", 609 | "Pajek/GD02_b", 610 | "Pajek/GD06_Java", 611 | "Pajek/GD06_theory", 612 | "Pajek/GD95_a", 613 | "Pajek/GD95_b", 614 | "Pajek/GD95_c", 615 | "Pajek/GD96_a", 616 | "Pajek/GD96_b", 617 | "Pajek/GD96_c", 618 | "Pajek/GD96_d", 619 | "Pajek/GD97_a", 620 | "Pajek/GD97_b", 621 | "Pajek/GD97_c", 622 | "Pajek/GD98_a", 623 | "Pajek/GD98_b", 624 | "Pajek/GD98_c", 625 | "Pajek/GD99_b", 626 | "Pajek/GD99_c", 627 | "Pajek/GlossGT", 628 | "Pajek/Journals", 629 | "Pajek/Kohonen", 630 | "Pajek/Ragusa16", 631 | "Pajek/Ragusa18", 632 | "Pajek/Roget", 633 | "Pajek/Sandi_authors", 634 | "Pajek/Sandi_sandi", 635 | "Pajek/SciMet", 636 | "Pajek/SmaGri", 637 | "Pajek/SmallW", 638 | "Pajek/Stranke94", 639 | "Pajek/Tina_AskCal", 640 | "Pajek/Tina_AskCog", 641 | "Pajek/Tina_DisCal", 642 | "Pajek/Tina_DisCog", 643 | "Pajek/USAir97", 644 | "Pajek/USpowerGrid", 645 | "Pajek/WorldCities", 646 | "Pajek/yeast", 647 | "Pothen/mesh1e1", 648 | "Pothen/mesh1em1", 649 | "Pothen/mesh1em6", 650 | "Pothen/mesh2e1", 651 | "Pothen/mesh2em5", 652 | "Pothen/mesh3e1", 653 | "Pothen/mesh3em5", 654 | "Pothen/sphere2", 655 | "Pothen/sphere3", 656 | "Qaplib/lp_nug05", 657 | "Qaplib/lp_nug06", 658 | "Qaplib/lp_nug07", 659 | "Qaplib/lp_nug08", 660 | "Rajat/rajat02", 661 | "Rajat/rajat05", 662 | "Rajat/rajat11", 663 | "Rajat/rajat14", 664 | "Rajat/rajat19", 665 | "Sandia/oscil_dcop_01", 666 | "Sandia/oscil_dcop_02", 667 | "Sandia/oscil_dcop_03", 668 | "Sandia/oscil_dcop_04", 669 | "Sandia/oscil_dcop_05", 670 | "Sandia/oscil_dcop_06", 671 | "Sandia/oscil_dcop_07", 672 | "Sandia/oscil_dcop_08", 673 | "Sandia/oscil_dcop_09", 674 | "Sandia/oscil_dcop_10", 675 | "Sandia/oscil_dcop_11", 676 | "Sandia/oscil_dcop_12", 677 | "Sandia/oscil_dcop_13", 678 | "Sandia/oscil_dcop_14", 679 | "Sandia/oscil_dcop_15", 680 | "Sandia/oscil_dcop_16", 681 | "Sandia/oscil_dcop_17", 682 | "Sandia/oscil_dcop_18", 683 | "Sandia/oscil_dcop_19", 684 | "Sandia/oscil_dcop_20", 685 | "Sandia/oscil_dcop_21", 686 | "Sandia/oscil_dcop_22", 687 | "Sandia/oscil_dcop_23", 688 | "Sandia/oscil_dcop_24", 689 | "Sandia/oscil_dcop_25", 690 | "Sandia/oscil_dcop_26", 691 | "Sandia/oscil_dcop_27", 692 | "Sandia/oscil_dcop_28", 693 | "Sandia/oscil_dcop_29", 694 | "Sandia/oscil_dcop_30", 695 | "Sandia/oscil_dcop_31", 696 | "Sandia/oscil_dcop_32", 697 | "Sandia/oscil_dcop_33", 698 | "Sandia/oscil_dcop_34", 699 | "Sandia/oscil_dcop_35", 700 | "Sandia/oscil_dcop_36", 701 | "Sandia/oscil_dcop_37", 702 | "Sandia/oscil_dcop_38", 703 | "Sandia/oscil_dcop_39", 704 | "Sandia/oscil_dcop_40", 705 | "Sandia/oscil_dcop_41", 706 | "Sandia/oscil_dcop_42", 707 | "Sandia/oscil_dcop_43", 708 | "Sandia/oscil_dcop_44", 709 | "Sandia/oscil_dcop_45", 710 | "Sandia/oscil_dcop_46", 711 | "Sandia/oscil_dcop_47", 712 | "Sandia/oscil_dcop_48", 713 | "Sandia/oscil_dcop_49", 714 | "Sandia/oscil_dcop_50", 715 | "Sandia/oscil_dcop_51", 716 | "Sandia/oscil_dcop_52", 717 | "Sandia/oscil_dcop_53", 718 | "Sandia/oscil_dcop_54", 719 | "Sandia/oscil_dcop_55", 720 | "Sandia/oscil_dcop_56", 721 | "Sandia/oscil_dcop_57", 722 | "Sandia/oscil_trans_01", 723 | "TOKAMAK/utm300", 724 | "vanHeukelum/cage3", 725 | "vanHeukelum/cage4", 726 | "vanHeukelum/cage5", 727 | "vanHeukelum/cage6", 728 | "vanHeukelum/cage7", 729 | "YZhou/circuit204" 730 | ]; 731 | } -------------------------------------------------------------------------------- /demo/src/lib/getGraph.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Load your graph here. 3 | */ 4 | // https://github.com/anvaka/miserables 5 | import miserables from 'miserables'; 6 | 7 | // Other loaders: 8 | // https://github.com/anvaka/ngraph.generators 9 | // import generate from 'ngraph.generators'; 10 | 11 | // https://github.com/anvaka/ngraph.graph 12 | // import createGraph from 'ngraph.graph'; 13 | 14 | // https://github.com/anvaka/ngraph.fromjson 15 | // import fromjson from 'ngraph.fromjson' 16 | 17 | // https://github.com/anvaka/ngraph.fromdot 18 | // import fromdot from 'ngraph.fromdot' 19 | 20 | export default function getGraph() { 21 | return miserables.create(); 22 | // return generate.wattsStrogatz(20, 5, 0.4); 23 | } 24 | 25 | -------------------------------------------------------------------------------- /demo/src/lib/loadDroppedGraph.js: -------------------------------------------------------------------------------- 1 | import fromDot from 'ngraph.fromdot'; 2 | import fromJson from 'ngraph.fromjson'; 3 | import bus from './bus.js'; 4 | 5 | /** 6 | * Loads graph from a dropped file 7 | */ 8 | export default function loadDroppedGraph(files) { 9 | let file = files[0]; 10 | 11 | var reader = new FileReader(); 12 | reader.readAsText(file, "UTF-8"); 13 | reader.onload = e => { 14 | let content = e.target.result; 15 | let graph = tryDot(content) || tryJson(content); 16 | if (graph) bus.fire('load-graph', graph); 17 | } 18 | reader.onerror = (e) => { 19 | //eslint-disable-next-line 20 | console.log('error loading dot file: ', e) 21 | }; 22 | 23 | function tryDot(fileContent) { 24 | try { 25 | return fromDot(fileContent); 26 | } catch (e) { 27 | //eslint-disable-next-line 28 | console.log('error loading dot file: ', e) 29 | } 30 | } 31 | function tryJson(fileContent) { 32 | try { 33 | return fromJson(JSON.parse(fileContent)); 34 | } catch (e) { 35 | //eslint-disable-next-line 36 | console.log('error loading JSON: ', e) 37 | } 38 | } 39 | } -------------------------------------------------------------------------------- /demo/src/lib/loadGraph.js: -------------------------------------------------------------------------------- 1 | import createGraph from 'ngraph.graph'; 2 | import miserables from 'miserables'; 3 | import generate from 'ngraph.generators'; 4 | 5 | let cache = simpleCache(); 6 | 7 | export default function loadGraph(name) { 8 | if (name === 'Miserables') return Promise.resolve(miserables); 9 | if (name === 'Binary') return Promise.resolve(generate.balancedBinTree(10)); 10 | 11 | let mtxObject = cache.get(name); 12 | if (mtxObject) return Promise.resolve(renderGraph(mtxObject.links, mtxObject.recordsPerEdge)); 13 | 14 | return fetch(`https://s3.amazonaws.com/yasiv_uf/out/${name}/index.js`, { 15 | mode: 'cors' 16 | }) 17 | .then(x => x.json()) 18 | .then(mtxObject => { 19 | cache.put(name, mtxObject); 20 | return renderGraph(mtxObject.links, mtxObject.recordsPerEdge); 21 | }); 22 | } 23 | function renderGraph (edges, recordsPerEdge) { 24 | let graph = createGraph(); 25 | for(var i = 0; i < edges.length - 1; i += recordsPerEdge) { 26 | graph.addLink(edges[i], edges[i + 1]); 27 | } 28 | return graph 29 | } 30 | 31 | function simpleCache() { 32 | var supported = 'localStorage' in window; 33 | 34 | return { 35 | get : function(key) { 36 | if (!supported) { return null; } 37 | var graphData = JSON.parse(window.localStorage.getItem(key)); 38 | if (!graphData || graphData.recordsPerEdge === undefined) { 39 | // this is old cache. Invalidate it 40 | return null; 41 | } 42 | return graphData; 43 | }, 44 | put : function(key, value) { 45 | if (!supported) { return false;} 46 | try { 47 | window.localStorage.setItem(key, JSON.stringify(value)); 48 | } catch(err) { 49 | // TODO: make something clever than this in case of quata exceeded. 50 | window.localStorage.clear(); 51 | } 52 | } 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /demo/src/lib/utils.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Set of function that I find useful for explorations. 3 | */ 4 | 5 | /** 6 | * Performs hermit interpolation of `x` between two edges 7 | */ 8 | export function smoothStep(edge0, edge1, x) { 9 | let t = clamp((x - edge0) / (edge1 - edge0), 0.0, 1.0); 10 | return t * t * (3.0 - 2.0 * t); 11 | } 12 | 13 | /** 14 | * Clamp `x` to [min, max] range. 15 | */ 16 | export function clamp(x, min, max) { 17 | return x < min ? min : x > max ? max : x; 18 | } 19 | 20 | /** 21 | * Collects main statistical properties of a collection 22 | */ 23 | export function collectStatistics(array) { 24 | if (array.length === 0) { 25 | return { 26 | min: undefined, 27 | max: undefined, 28 | avg: undefined, 29 | sigma: undefined, 30 | mod: undefined, 31 | count: 0 32 | } 33 | } 34 | let min = Infinity; 35 | let max = -Infinity; 36 | let sum = 0; 37 | let counts = new Map(); 38 | array.forEach(x => { 39 | if (x < min) min = x; 40 | if (x > max) max = x; 41 | sum += x; 42 | counts.set(x, (counts.get(x) || 0) + 1) 43 | }); 44 | let mod = Array.from(counts).sort((a, b) => b[1] - a[1])[0][0] 45 | 46 | let avg = sum /= array.length; 47 | let sigma = 0; 48 | array.forEach(x => { 49 | sigma += (x - avg) * (x - avg); 50 | }); 51 | sigma = Math.sqrt(sigma / (array.length + 1)); 52 | let count = array.length; 53 | return {min, max, avg, sigma, mod, count}; 54 | } -------------------------------------------------------------------------------- /demo/src/main.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue' 2 | import App from './App.vue' 3 | import fileDrop from './lib/fileDrop.js'; 4 | import loadDroppedGraph from './lib/loadDroppedGraph.js'; 5 | 6 | Vue.config.productionTip = false 7 | 8 | new Vue({ 9 | render: h => h(App), 10 | }).$mount('#app') 11 | 12 | // When they drop a `.dot` file into the browser - let's load it. 13 | fileDrop(document.body, loadDroppedGraph); -------------------------------------------------------------------------------- /demo/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | publicPath: '' 3 | } -------------------------------------------------------------------------------- /dist/ngraph.forcelayout2d.min.js: -------------------------------------------------------------------------------- 1 | !function(f){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=f();else if("function"==typeof define&&define.amd)define([],f);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).ngraphCreate2dLayout=f()}}((function(){return function r(e,n,t){function o(i,f){if(!n[i]){if(!e[i]){var c="function"==typeof require&&require;if(!f&&c)return c(i,!0);if(u)return u(i,!0);var a=new Error("Cannot find module '"+i+"'");throw a.code="MODULE_NOT_FOUND",a}var p=n[i]={exports:{}};e[i][0].call(p.exports,(function(r){return o(e[i][1][r]||r)}),p,p.exports,r,e,n,t)}return n[i].exports}for(var u="function"==typeof require&&require,i=0;i19?function(nodeId){var links=graph.getLinks(nodeId);return links?1+links.size/3:1}:function(nodeId){var links=graph.getLinks(nodeId);return links?1+links.length/3:1};physicsSettings&&"function"==typeof physicsSettings.nodeMass&&(nodeMass=physicsSettings.nodeMass);var nodeBodies=new Map,springs={},bodiesCount=0,springTransform=physicsSimulator.settings.springTransform||noop;bodiesCount=0,graph.forEachNode((function(node){initBody(node.id),bodiesCount+=1})),graph.forEachLink(initLink),graph.on("changed",onGraphChanged);var wasStable=!1,api={step:function(){if(0===bodiesCount)return updateStableStatus(!0),!0;var lastMove=physicsSimulator.step();api.lastMove=lastMove,api.fire("step");var isStableNow=lastMove/bodiesCount<=.01;return updateStableStatus(isStableNow),isStableNow},getNodePosition:function(nodeId){return getInitializedBody(nodeId).pos},setNodePosition:function(nodeId){var body=getInitializedBody(nodeId);body.setPosition.apply(body,Array.prototype.slice.call(arguments,1))},getLinkPosition:function(linkId){var spring=springs[linkId];if(spring)return{from:spring.from.pos,to:spring.to.pos}},getGraphRect:function(){return physicsSimulator.getBBox()},forEachBody:forEachBody,pinNode:function(node,isPinned){getInitializedBody(node.id).isPinned=!!isPinned},isNodePinned:function(node){return getInitializedBody(node.id).isPinned},dispose:function(){graph.off("changed",onGraphChanged),api.fire("disposed")},getBody:function(nodeId){return nodeBodies.get(nodeId)},getSpring:function(fromId,toId){var linkId;if(void 0===toId)linkId="object"!=typeof fromId?fromId:fromId.id;else{var link=graph.hasLink(fromId,toId);if(!link)return;linkId=link.id}return springs[linkId]},getForceVectorLength:function(){var fx=0,fy=0;return forEachBody((function(body){fx+=Math.abs(body.force.x),fy+=Math.abs(body.force.y)})),Math.sqrt(fx*fx+fy*fy)},simulator:physicsSimulator,graph:graph,lastMove:0};return eventify(api),api;function updateStableStatus(isStableNow){var isStable;wasStable!==isStableNow&&(wasStable=isStableNow,isStable=isStableNow,api.fire("stable",isStable))}function forEachBody(cb){nodeBodies.forEach(cb)}function onGraphChanged(changes){for(var i=0;imax_x&&(max_x=bodyPos.x),bodyPos.y>max_y&&(max_y=bodyPos.y)}boundingBox.min_x=min_x,boundingBox.min_y=min_y,boundingBox.max_x=max_x,boundingBox.max_y=max_y},reset:function(){boundingBox.min_x=boundingBox.max_x=0,boundingBox.min_y=boundingBox.max_y=0},getBestNewPosition:function(neighbors){var base_x=0,base_y=0;if(neighbors.length){for(var i=0;i0?spring.coefficient:options.springCoefficient)*d/r;body1.force.x+=coefficient*dx,body1.force.y+=coefficient*dy,body1.springCount+=1,body1.springLength+=r,body2.force.x-=coefficient*dx,body2.force.y-=coefficient*dy,body2.springCount+=1,body2.springLength+=r}}}}},{}],6:[function(require,module,exports){module.exports=function(){return function(bodies,timeStep,adaptiveTimeStepWeight){var length=bodies.length;if(0===length)return 0;for(var dx=0,tx=0,dy=0,ty=0,i=0;i1&&(body.velocity.x=vx/v,body.velocity.y=vy/v),dx=timeStep*body.velocity.x,dy=timeStep*body.velocity.y,body.pos.x+=dx,body.pos.y+=dy,tx+=Math.abs(dx),ty+=Math.abs(dy)}}return(tx*tx+ty*ty)/length}}},{}],7:[function(require,module,exports){function InsertStack(){this.stack=[],this.popIdx=0}function InsertStackElement(node,body){this.node=node,this.body=body}function QuadNode(){this.body=null,this.quad0=null,this.quad1=null,this.quad2=null,this.quad3=null,this.mass=0,this.mass_x=0,this.mass_y=0,this.min_x=0,this.min_y=0,this.max_x=0,this.max_y=0}function isSamePosition(point1,point2){var dx=Math.abs(point1.x-point2.x),dy=Math.abs(point1.y-point2.y);return dx<1e-8&&dy<1e-8}function getChild(node,idx){return 0===idx?node.quad0:1===idx?node.quad1:2===idx?node.quad2:3===idx?node.quad3:null}function setChild(node,idx,child){0===idx?node.quad0=child:1===idx?node.quad1=child:2===idx?node.quad2=child:3===idx&&(node.quad3=child)}InsertStack.prototype={isEmpty:function(){return 0===this.popIdx},push:function(node,body){var item=this.stack[this.popIdx];item?(item.node=node,item.body=body):this.stack[this.popIdx]=new InsertStackElement(node,body),++this.popIdx},pop:function(){if(this.popIdx>0)return this.stack[--this.popIdx]},reset:function(){this.popIdx=0}},module.exports=function(){return function(options,random){(options=options||{}).gravity="number"==typeof options.gravity?options.gravity:-1,options.theta="number"==typeof options.theta?options.theta:.8;var gravity=options.gravity,updateQueue=[],insertStack=new InsertStack,theta=options.theta,nodesCache=[],currentInCache=0,root=newNode();return{insertBodies:function(bodies){var xmin=Number.MAX_VALUE,ymin=Number.MAX_VALUE,xmax=Number.MIN_VALUE,ymax=Number.MIN_VALUE,i=bodies.length;for(;i--;){var pos=bodies[i].pos;pos.xxmax&&(xmax=pos.x),pos.y>ymax&&(ymax=pos.y)}var maxSideLength=-1/0;xmax-xmin>maxSideLength&&(maxSideLength=xmax-xmin);ymax-ymin>maxSideLength&&(maxSideLength=ymax-ymin);currentInCache=0,(root=newNode()).min_x=xmin,root.min_y=ymin,root.max_x=xmin+maxSideLength,root.max_y=ymin+maxSideLength,(i=bodies.length-1)>=0&&(root.body=bodies[i]);for(;i--;)insert(bodies[i])},getRoot:function(){return root},updateBodyForce:function(sourceBody){var v,dx,dy,r,queue=updateQueue,fx=0,fy=0,queueLength=1,shiftIdx=0,pushIdx=1;queue[0]=root;for(;queueLength;){var node=queue[shiftIdx],body=node.body;queueLength-=1,shiftIdx+=1;var differentBody=body!==sourceBody;body&&differentBody?(dx=body.pos.x-sourceBody.pos.x,dy=body.pos.y-sourceBody.pos.y,0===(r=Math.sqrt(dx*dx+dy*dy))&&(dx=(random.nextDouble()-.5)/50,dy=(random.nextDouble()-.5)/50,r=Math.sqrt(dx*dx+dy*dy)),v=gravity*body.mass*sourceBody.mass/(r*r*r),fx+=v*dx,fy+=v*dy):differentBody&&(dx=node.mass_x/node.mass-sourceBody.pos.x,dy=node.mass_y/node.mass-sourceBody.pos.y,0===(r=Math.sqrt(dx*dx+dy*dy))&&(dx=(random.nextDouble()-.5)/50,dy=(random.nextDouble()-.5)/50,r=Math.sqrt(dx*dx+dy*dy)),(node.max_x-node.min_x)/r0&&isSamePosition(oldBody.pos,body.pos));if(0===retriesCount&&isSamePosition(oldBody.pos,body.pos))return}insertStack.push(node,oldBody),insertStack.push(node,body)}else{var x=body.pos.x,y=body.pos.y;node.mass+=body.mass,node.mass_x+=body.mass*x,node.mass_y+=body.mass*y;var quadIdx=0,min_x=node.min_x,min_y=node.min_y,max_x=(min_x+node.max_x)/2,max_y=(min_y+node.max_y)/2;x>max_x&&(quadIdx+=1,min_x=max_x,max_x=node.max_x),y>max_y&&(quadIdx+=2,min_y=max_y,max_y=node.max_y);var child=getChild(node,quadIdx);child?insertStack.push(child,body):((child=newNode()).min_x=min_x,child.min_y=min_y,child.max_x=max_x,child.max_y=max_y,child.body=body,setChild(node,quadIdx,child))}}}}}},{}],8:[function(require,module,exports){module.exports=function(settings){var Spring=require("./spring"),merge=require("ngraph.merge"),eventify=require("ngraph.events");if(settings){if(void 0!==settings.springCoeff)throw new Error("springCoeff was renamed to springCoefficient");if(void 0!==settings.dragCoeff)throw new Error("dragCoeff was renamed to dragCoefficient")}settings=merge(settings,{springLength:10,springCoefficient:.8,gravity:-12,theta:.8,dragCoefficient:.9,timeStep:.5,adaptiveTimeStepWeight:0,dimensions:2,debug:!1});var factory=dimensionalCache[settings.dimensions];if(!factory){var dimensions=settings.dimensions;factory={Body:generateCreateBodyFunction(dimensions,settings.debug),createQuadTree:generateQuadTreeFunction(dimensions),createBounds:generateBoundsFunction(dimensions),createDragForce:generateCreateDragForceFunction(dimensions),createSpringForce:generateCreateSpringForceFunction(dimensions),integrate:generateIntegratorFunction(dimensions)},dimensionalCache[dimensions]=factory}var Body=factory.Body,createQuadTree=factory.createQuadTree,createBounds=factory.createBounds,createDragForce=factory.createDragForce,createSpringForce=factory.createSpringForce,integrate=factory.integrate,random=require("ngraph.random").random(42),bodies=[],springs=[],quadTree=createQuadTree(settings,random),bounds=createBounds(bodies,settings,random),springForce=createSpringForce(settings,random),dragForce=createDragForce(settings),forces=[],forceMap=new Map,iterationNumber=0;addForce("nbody",(function(){if(0===bodies.length)return;quadTree.insertBodies(bodies);var i=bodies.length;for(;i--;){var body=bodies[i];body.isPinned||(body.reset(),quadTree.updateBodyForce(body),dragForce.update(body))}})),addForce("spring",(function(){var i=springs.length;for(;i--;)springForce.update(springs[i])}));var publicApi={bodies:bodies,quadTree:quadTree,springs:springs,settings:settings,addForce:addForce,removeForce:function(forceName){var forceIndex=forces.indexOf(forceMap.get(forceName));if(forceIndex<0)return;forces.splice(forceIndex,1),forceMap.delete(forceName)},getForces:function(){return forceMap},step:function(){for(var i=0;inew Body(pos))(pos);return bodies.push(body),body},removeBody:function(body){if(body){var idx=bodies.indexOf(body);if(!(idx<0))return bodies.splice(idx,1),0===bodies.length&&bounds.reset(),!0}},addSpring:function(body1,body2,springLength,springCoefficient){if(!body1||!body2)throw new Error("Cannot add null spring to force simulator");"number"!=typeof springLength&&(springLength=-1);var spring=new Spring(body1,body2,springLength,springCoefficient>=0?springCoefficient:-1);return springs.push(spring),spring},getTotalMovement:function(){return 0},removeSpring:function(spring){if(spring){var idx=springs.indexOf(spring);return idx>-1?(springs.splice(idx,1),!0):void 0}},getBestNewBodyPosition:function(neighbors){return bounds.getBestNewPosition(neighbors)},getBBox:getBoundingBox,getBoundingBox:getBoundingBox,invalidateBBox:function(){console.warn("invalidateBBox() is deprecated, bounds always recomputed on `getBBox()` call")},gravity:function(value){return void 0!==value?(settings.gravity=value,quadTree.options({gravity:value}),this):settings.gravity},theta:function(value){return void 0!==value?(settings.theta=value,quadTree.options({theta:value}),this):settings.theta},random:random};return function(settings,target){for(var key in settings)augment(settings,target,key)}(settings,publicApi),eventify(publicApi),publicApi;function getBoundingBox(){return bounds.update(),bounds.box}function addForce(forceName,forceFunction){if(forceMap.has(forceName))throw new Error("Force "+forceName+" is already added");forceMap.set(forceName,forceFunction),forces.push(forceFunction)}};var generateCreateBodyFunction=require("./codeGenerators/generateCreateBody"),generateQuadTreeFunction=require("./codeGenerators/generateQuadTree"),generateBoundsFunction=require("./codeGenerators/generateBounds"),generateCreateDragForceFunction=require("./codeGenerators/generateCreateDragForce"),generateCreateSpringForceFunction=require("./codeGenerators/generateCreateSpringForce"),generateIntegratorFunction=require("./codeGenerators/generateIntegrator"),dimensionalCache={};function augment(source,target,key){if(source.hasOwnProperty(key)&&"function"!=typeof target[key]){var sourceIsNumber=Number.isFinite(source[key]);target[key]=sourceIsNumber?function(value){if(void 0!==value){if(!Number.isFinite(value))throw new Error("Value of "+key+" should be a valid number.");return source[key]=value,target}return source[key]}:function(value){return void 0!==value?(source[key]=value,target):source[key]}}}},{"./codeGenerators/generateBounds":2,"./codeGenerators/generateCreateBody":3,"./codeGenerators/generateCreateDragForce":4,"./codeGenerators/generateCreateSpringForce":5,"./codeGenerators/generateIntegrator":6,"./codeGenerators/generateQuadTree":7,"./spring":9,"ngraph.events":10,"ngraph.merge":11,"ngraph.random":12}],9:[function(require,module,exports){module.exports=function(fromBody,toBody,length,springCoefficient){this.from=fromBody,this.to=toBody,this.length=length,this.coefficient=springCoefficient}},{}],10:[function(require,module,exports){module.exports=function(subject){!function(subject){if(!subject)throw new Error("Eventify cannot use falsy object as events subject");for(var reservedWords=["on","fire","off"],i=0;i1&&(fireArguments=Array.prototype.splice.call(arguments,1));for(var i=0;i>>19))+374761393+(seed<<5)&4294967295)+3550635116^seed<<9))+4251993797+(seed<<3)&4294967295)^seed>>>16),this.seed=seed,(268435455&seed)/268435456}module.exports=random,module.exports.random=random,module.exports.randomIterator=function(array,customRandom){var localRandom=customRandom||random();if("function"!=typeof localRandom.next)throw new Error("customRandom does not match expected API: next() function is missing");return{forEach:function(callback){var i,j,t;for(i=array.length-1;i>0;--i)j=localRandom.next(i+1),t=array[j],array[j]=array[i],array[i]=t,callback(t);array.length&&callback(array[0])},shuffle:function(){var i,j,t;for(i=array.length-1;i>0;--i)j=localRandom.next(i+1),t=array[j],array[j]=array[i],array[i]=t;return array}}},Generator.prototype.next=function(maxValue){return Math.floor(this.nextDouble()*maxValue)},Generator.prototype.nextDouble=nextDouble,Generator.prototype.uniform=nextDouble,Generator.prototype.gaussian=function(){var r,x,y;do{x=2*this.nextDouble()-1,y=2*this.nextDouble()-1,r=x*x+y*y}while(r>=1||0===r);return x*Math.sqrt(-2*Math.log(r)/r)},Generator.prototype.levy=function(){var sigma=Math.pow(gamma(2.5)*Math.sin(1.5*Math.PI/2)/(1.5*gamma(1.25)*Math.pow(2,.25)),1/1.5);return this.gaussian()*sigma/Math.pow(Math.abs(this.gaussian()),1/1.5)}},{}]},{},[1])(1)})); -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | declare module "ngraph.forcelayout" { 2 | import { Graph, NodeId, LinkId, Node, Link } from "ngraph.graph"; 3 | import { EventedType } from "ngraph.events"; 4 | 5 | export type ForceFunction = (iterationNumber: number) => void; 6 | 7 | export interface Vector { 8 | x: number; 9 | y: number; 10 | z?: number; 11 | [coord: `c${number}`]: number 12 | } 13 | 14 | export interface Body { 15 | isPinned: boolean; 16 | pos: Vector; 17 | force: Vector; 18 | velocity: Vector; 19 | mass: number; 20 | springCount: number; 21 | springLength: number; 22 | reset(): void; 23 | setPosition(x: number, y: number, z?: number, ...c: number[]): void; 24 | } 25 | 26 | export interface Spring { 27 | from: Body; 28 | to: Body; 29 | length: number; 30 | coefficient: number; 31 | } 32 | 33 | export interface QuadNode { 34 | body: Body | null; 35 | mass: number; 36 | mass_x: number; 37 | mass_y: number; 38 | mass_z?: number; 39 | [mass: `mass_c${number}`]: number | null; 40 | [mass: `min_c${number}`]: number | null; 41 | [mass: `max_c${number}`]: number | null; 42 | [quad: `quad${number}`]: number | null; 43 | } 44 | 45 | export interface QuadTree { 46 | insertBodies(bodies: Body[]): void; 47 | getRoot(): QuadNode; 48 | updateBodyForce(sourceBody: Body): void; 49 | options(newOptions: { gravity: number; theta: number }): { gravity: number; theta: number }; 50 | } 51 | 52 | export interface BoundingBox { 53 | min_x: number; 54 | max_x: number; 55 | min_y: number; 56 | max_y: number; 57 | min_z?: number; 58 | max_z?: number; 59 | [min: `min_c${number}`]: number; 60 | [max: `max_c${number}`]: number; 61 | } 62 | 63 | /** 64 | * Settings for a PhysicsSimulator 65 | */ 66 | export interface PhysicsSettings { 67 | /** 68 | * Ideal length for links (springs in physical model). 69 | */ 70 | springLength: number; 71 | 72 | /** 73 | * Hook's law coefficient. 1 - solid spring. 74 | */ 75 | springCoefficient: number; 76 | 77 | /** 78 | * Coulomb's law coefficient. It's used to repel nodes thus should be negative 79 | * if you make it positive nodes start attract each other :). 80 | */ 81 | gravity: number; 82 | 83 | /** 84 | * Theta coefficient from Barnes Hut simulation. Ranged between (0, 1). 85 | * The closer it's to 1 the more nodes algorithm will have to go through. 86 | * Setting it to one makes Barnes Hut simulation no different from 87 | * brute-force forces calculation (each node is considered). 88 | */ 89 | theta: number; 90 | 91 | /** 92 | * Drag force coefficient. Used to slow down system, thus should be less than 1. 93 | * The closer it is to 0 the less tight system will be. 94 | */ 95 | dragCoefficient: number; 96 | 97 | /** 98 | * Default time step (dt) for forces integration 99 | */ 100 | timeStep: number; 101 | 102 | /** 103 | * Adaptive time step uses average spring length to compute actual time step: 104 | * See: https://twitter.com/anvaka/status/1293067160755957760 105 | */ 106 | adaptiveTimeStepWeight: number; 107 | 108 | /** 109 | * This parameter defines number of dimensions of the space where simulation 110 | * is performed. 111 | */ 112 | dimensions: number; 113 | 114 | /** 115 | * In debug mode more checks are performed, this will help you catch errors 116 | * quickly, however for production build it is recommended to turn off this flag 117 | * to speed up computation. 118 | */ 119 | debug: boolean; 120 | } 121 | 122 | /** 123 | * Manages a simulation of physical forces acting on bodies and springs. 124 | */ 125 | export interface PhysicsSimulator { 126 | /** 127 | * Array of bodies, registered with current simulator 128 | * 129 | * Note: To add new body, use addBody() method. This property is only 130 | * exposed for testing/performance purposes. 131 | */ 132 | bodies: Body[]; 133 | 134 | quadTree: QuadTree; 135 | 136 | /** 137 | * Array of springs, registered with current simulator 138 | * 139 | * Note: To add new spring, use addSpring() method. This property is only 140 | * exposed for testing/performance purposes. 141 | */ 142 | springs: Spring[]; 143 | 144 | /** 145 | * Returns settings with which current simulator was initialized 146 | */ 147 | settings: PhysicsSettings; 148 | 149 | /** 150 | * Adds a new force to simulation 151 | * @param forceName force identifier 152 | * @param forceFunction the function to apply 153 | */ 154 | addForce(forceName: string, forceFunction: ForceFunction): void; 155 | 156 | /** 157 | * Removes a force from the simulation 158 | * @param forceName force identifier 159 | */ 160 | removeForce(forceName: string): void; 161 | 162 | /** 163 | * Returns a map of all registered forces 164 | */ 165 | getForces(): Map; 166 | 167 | /** 168 | * Performs one step of force simulation. 169 | * 170 | * @returns true if system is considered stable; False otherwise. 171 | */ 172 | step(): boolean; 173 | 174 | /** 175 | * Adds body to the system 176 | * @param body physical body 177 | * @returns added body 178 | */ 179 | addBody(body: Body): Body; 180 | 181 | /** 182 | * Adds body to the system at given position 183 | * @param pos position of a body 184 | * @returns added body 185 | */ 186 | addBodyAt(pos: Vector): Body; 187 | 188 | /** 189 | * Removes body from the system 190 | * @param body to remove 191 | * @returns true if body found and removed. falsy otherwise; 192 | */ 193 | removeBody(body: Body): boolean; 194 | 195 | /** 196 | * Adds a spring to this simulation 197 | * @param body1 first body 198 | * @param body2 second body 199 | * @param springLength Ideal length for links 200 | * @param springCoefficient Hook's law coefficient. 1 - solid spring 201 | * @returns a handle for a spring. If you want to later remove 202 | * spring pass it to removeSpring() method. 203 | */ 204 | addSpring(body1: Body, body2: Body, springLength: number, springCoefficient: number): Spring; 205 | 206 | /** 207 | * Returns amount of movement performed on last step() call 208 | */ 209 | getTotalMovement(): number; 210 | 211 | /** 212 | * Removes spring from the system 213 | * @param spring to remove. Spring is an object returned by addSpring 214 | * @returns true if spring found and removed. falsy otherwise; 215 | */ 216 | removeSpring(spring: Spring): boolean; 217 | 218 | getBestNewBodyPosition(neighbors: Body[]): Vector; 219 | 220 | /** 221 | * Returns bounding box which covers all bodies 222 | */ 223 | getBBox(): BoundingBox; 224 | 225 | /** 226 | * Returns bounding box which covers all bodies 227 | */ 228 | getBoundingBox(): BoundingBox; 229 | 230 | /** @deprecated invalidateBBox() is deprecated, bounds always recomputed on `getBBox()` call */ 231 | invalidateBBox(): void; 232 | 233 | /** 234 | * Changes the gravity for the system 235 | * @param value Coulomb's law coefficient 236 | */ 237 | gravity(value: number): number; 238 | 239 | /** 240 | * Changes the theta coeffitient for the system 241 | * @param value Theta coefficient from Barnes Hut simulation 242 | */ 243 | theta(value: number): number; 244 | 245 | // TODO: create types declaration file for ngraph.random 246 | /** 247 | * Returns pseudo-random number generator instance 248 | */ 249 | random: any; 250 | } 251 | 252 | /** 253 | * Force based layout for a given graph. 254 | */ 255 | export interface Layout { 256 | /** 257 | * Performs one step of iterative layout algorithm 258 | * @returns true if the system should be considered stable; False otherwise. 259 | * The system is stable if no further call to `step()` can improve the layout. 260 | */ 261 | step(): boolean; 262 | 263 | /** 264 | * For a given `nodeId` returns position 265 | * @param nodeId node identifier 266 | */ 267 | getNodePosition(nodeId: NodeId): Vector; 268 | 269 | /** 270 | * Sets position of a node to a given coordinates 271 | * @param nodeId node identifier 272 | * @param x position of a node 273 | * @param y position of a node 274 | * @param z position of node (only if applicable to body) 275 | */ 276 | setNodePosition(nodeId: NodeId, x: number, y: number, z?: number, ...c: number[]): void; 277 | 278 | /** 279 | * Gets Link position by link id 280 | * @param linkId link identifier 281 | * @returns from: {x, y} coordinates of link start 282 | * @returns to: {x, y} coordinates of link end 283 | */ 284 | getLinkPosition(linkId: LinkId): { from: Vector; to: Vector }; 285 | 286 | /** 287 | * @returns area required to fit in the graph. Object contains 288 | * `x1`, `y1` - top left coordinates 289 | * `x2`, `y2` - bottom right coordinates 290 | */ 291 | getGraphRect(): { x1: number; y1: number; x2: number; y2: number }; 292 | 293 | /** 294 | * Iterates over each body in the layout simulator and performs a callback(body, nodeId) 295 | * @param callbackfn the callback function 296 | */ 297 | forEachBody(callbackfn: (value: Body, key: NodeId, map: Map) => void): void; 298 | 299 | /** 300 | * Requests layout algorithm to pin/unpin node to its current position 301 | * Pinned nodes should not be affected by layout algorithm and always 302 | * remain at their position 303 | * @param node the node to pin/unpin 304 | * @param isPinned true to pin, false to unpin 305 | */ 306 | pinNode(node: Node, isPinned: boolean): void; 307 | 308 | /** 309 | * Checks whether given graph's node is currently pinned 310 | * @param node the node to check 311 | */ 312 | isNodePinned(node: Node): boolean; 313 | 314 | /** 315 | * Request to release all resources 316 | */ 317 | dispose(): void; 318 | 319 | /** 320 | * Gets physical body for a given node id. If node is not found undefined 321 | * value is returned. 322 | * @param nodeId node identifier 323 | */ 324 | getBody(nodeId: NodeId): Body | undefined; 325 | 326 | /** 327 | * Gets spring for a given edge. 328 | * 329 | * @param linkId link identifer. 330 | */ 331 | getSpring(linkId: LinkId | Link): Spring; 332 | 333 | /** 334 | * Gets spring for a given edge. 335 | * 336 | * @param fromId node identifer - tail of the link 337 | * @param toId head of the link - head of the link 338 | */ 339 | getSpring(fromId: NodeId, toId: NodeId): Spring | undefined; 340 | 341 | /** 342 | * Returns length of cumulative force vector. The closer this to zero - the more stable the system is 343 | */ 344 | getForceVectorLength(): number; 345 | 346 | /** 347 | * @readonly Gets current physics simulator 348 | */ 349 | readonly simulator: PhysicsSimulator; 350 | 351 | /** 352 | * Gets the graph that was used for layout 353 | */ 354 | graph: T; 355 | 356 | /** 357 | * Gets amount of movement performed during last step operation 358 | */ 359 | lastMove: number; 360 | } 361 | 362 | /** 363 | * Creates force based layout for a given graph. 364 | * 365 | * @param graph which needs to be laid out 366 | * @param physicsSettings if you need custom settings 367 | * for physics simulator you can pass your own settings here. If it's not passed 368 | * a default one will be created. 369 | */ 370 | export default function createLayout( 371 | graph: T, 372 | physicsSettings?: Partial 373 | ): Layout & EventedType; 374 | } 375 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | module.exports = createLayout; 2 | module.exports.simulator = require('./lib/createPhysicsSimulator'); 3 | 4 | var eventify = require('ngraph.events'); 5 | 6 | /** 7 | * Creates force based layout for a given graph. 8 | * 9 | * @param {ngraph.graph} graph which needs to be laid out 10 | * @param {object} physicsSettings if you need custom settings 11 | * for physics simulator you can pass your own settings here. If it's not passed 12 | * a default one will be created. 13 | */ 14 | function createLayout(graph, physicsSettings) { 15 | if (!graph) { 16 | throw new Error('Graph structure cannot be undefined'); 17 | } 18 | 19 | var createSimulator = (physicsSettings && physicsSettings.createSimulator) || require('./lib/createPhysicsSimulator'); 20 | var physicsSimulator = createSimulator(physicsSettings); 21 | if (Array.isArray(physicsSettings)) throw new Error('Physics settings is expected to be an object'); 22 | 23 | var nodeMass = graph.version > 19 ? defaultSetNodeMass : defaultArrayNodeMass; 24 | if (physicsSettings && typeof physicsSettings.nodeMass === 'function') { 25 | nodeMass = physicsSettings.nodeMass; 26 | } 27 | 28 | var nodeBodies = new Map(); 29 | var springs = {}; 30 | var bodiesCount = 0; 31 | 32 | var springTransform = physicsSimulator.settings.springTransform || noop; 33 | 34 | // Initialize physics with what we have in the graph: 35 | initPhysics(); 36 | listenToEvents(); 37 | 38 | var wasStable = false; 39 | 40 | var api = { 41 | /** 42 | * Performs one step of iterative layout algorithm 43 | * 44 | * @returns {boolean} true if the system should be considered stable; False otherwise. 45 | * The system is stable if no further call to `step()` can improve the layout. 46 | */ 47 | step: function() { 48 | if (bodiesCount === 0) { 49 | updateStableStatus(true); 50 | return true; 51 | } 52 | 53 | var lastMove = physicsSimulator.step(); 54 | 55 | // Save the movement in case if someone wants to query it in the step 56 | // callback. 57 | api.lastMove = lastMove; 58 | 59 | // Allow listeners to perform low-level actions after nodes are updated. 60 | api.fire('step'); 61 | 62 | var ratio = lastMove/bodiesCount; 63 | var isStableNow = ratio <= 0.01; // TODO: The number is somewhat arbitrary... 64 | updateStableStatus(isStableNow); 65 | 66 | 67 | return isStableNow; 68 | }, 69 | 70 | /** 71 | * For a given `nodeId` returns position 72 | */ 73 | getNodePosition: function (nodeId) { 74 | return getInitializedBody(nodeId).pos; 75 | }, 76 | 77 | /** 78 | * Sets position of a node to a given coordinates 79 | * @param {string} nodeId node identifier 80 | * @param {number} x position of a node 81 | * @param {number} y position of a node 82 | * @param {number=} z position of node (only if applicable to body) 83 | */ 84 | setNodePosition: function (nodeId) { 85 | var body = getInitializedBody(nodeId); 86 | body.setPosition.apply(body, Array.prototype.slice.call(arguments, 1)); 87 | }, 88 | 89 | /** 90 | * @returns {Object} Link position by link id 91 | * @returns {Object.from} {x, y} coordinates of link start 92 | * @returns {Object.to} {x, y} coordinates of link end 93 | */ 94 | getLinkPosition: function (linkId) { 95 | var spring = springs[linkId]; 96 | if (spring) { 97 | return { 98 | from: spring.from.pos, 99 | to: spring.to.pos 100 | }; 101 | } 102 | }, 103 | 104 | /** 105 | * @returns {Object} area required to fit in the graph. Object contains 106 | * `x1`, `y1` - top left coordinates 107 | * `x2`, `y2` - bottom right coordinates 108 | */ 109 | getGraphRect: function () { 110 | return physicsSimulator.getBBox(); 111 | }, 112 | 113 | /** 114 | * Iterates over each body in the layout simulator and performs a callback(body, nodeId) 115 | */ 116 | forEachBody: forEachBody, 117 | 118 | /* 119 | * Requests layout algorithm to pin/unpin node to its current position 120 | * Pinned nodes should not be affected by layout algorithm and always 121 | * remain at their position 122 | */ 123 | pinNode: function (node, isPinned) { 124 | var body = getInitializedBody(node.id); 125 | body.isPinned = !!isPinned; 126 | }, 127 | 128 | /** 129 | * Checks whether given graph's node is currently pinned 130 | */ 131 | isNodePinned: function (node) { 132 | return getInitializedBody(node.id).isPinned; 133 | }, 134 | 135 | /** 136 | * Request to release all resources 137 | */ 138 | dispose: function() { 139 | graph.off('changed', onGraphChanged); 140 | api.fire('disposed'); 141 | }, 142 | 143 | /** 144 | * Gets physical body for a given node id. If node is not found undefined 145 | * value is returned. 146 | */ 147 | getBody: getBody, 148 | 149 | /** 150 | * Gets spring for a given edge. 151 | * 152 | * @param {string} linkId link identifier. If two arguments are passed then 153 | * this argument is treated as formNodeId 154 | * @param {string=} toId when defined this parameter denotes head of the link 155 | * and first argument is treated as tail of the link (fromId) 156 | */ 157 | getSpring: getSpring, 158 | 159 | /** 160 | * Returns length of cumulative force vector. The closer this to zero - the more stable the system is 161 | */ 162 | getForceVectorLength: getForceVectorLength, 163 | 164 | /** 165 | * [Read only] Gets current physics simulator 166 | */ 167 | simulator: physicsSimulator, 168 | 169 | /** 170 | * Gets the graph that was used for layout 171 | */ 172 | graph: graph, 173 | 174 | /** 175 | * Gets amount of movement performed during last step operation 176 | */ 177 | lastMove: 0 178 | }; 179 | 180 | eventify(api); 181 | 182 | return api; 183 | 184 | function updateStableStatus(isStableNow) { 185 | if (wasStable !== isStableNow) { 186 | wasStable = isStableNow; 187 | onStableChanged(isStableNow); 188 | } 189 | } 190 | 191 | function forEachBody(cb) { 192 | nodeBodies.forEach(cb); 193 | } 194 | 195 | function getForceVectorLength() { 196 | var fx = 0, fy = 0; 197 | forEachBody(function(body) { 198 | fx += Math.abs(body.force.x); 199 | fy += Math.abs(body.force.y); 200 | }); 201 | return Math.sqrt(fx * fx + fy * fy); 202 | } 203 | 204 | function getSpring(fromId, toId) { 205 | var linkId; 206 | if (toId === undefined) { 207 | if (typeof fromId !== 'object') { 208 | // assume fromId as a linkId: 209 | linkId = fromId; 210 | } else { 211 | // assume fromId to be a link object: 212 | linkId = fromId.id; 213 | } 214 | } else { 215 | // toId is defined, should grab link: 216 | var link = graph.hasLink(fromId, toId); 217 | if (!link) return; 218 | linkId = link.id; 219 | } 220 | 221 | return springs[linkId]; 222 | } 223 | 224 | function getBody(nodeId) { 225 | return nodeBodies.get(nodeId); 226 | } 227 | 228 | function listenToEvents() { 229 | graph.on('changed', onGraphChanged); 230 | } 231 | 232 | function onStableChanged(isStable) { 233 | api.fire('stable', isStable); 234 | } 235 | 236 | function onGraphChanged(changes) { 237 | for (var i = 0; i < changes.length; ++i) { 238 | var change = changes[i]; 239 | if (change.changeType === 'add') { 240 | if (change.node) { 241 | initBody(change.node.id); 242 | } 243 | if (change.link) { 244 | initLink(change.link); 245 | } 246 | } else if (change.changeType === 'remove') { 247 | if (change.node) { 248 | releaseNode(change.node); 249 | } 250 | if (change.link) { 251 | releaseLink(change.link); 252 | } 253 | } 254 | } 255 | bodiesCount = graph.getNodesCount(); 256 | } 257 | 258 | function initPhysics() { 259 | bodiesCount = 0; 260 | 261 | graph.forEachNode(function (node) { 262 | initBody(node.id); 263 | bodiesCount += 1; 264 | }); 265 | 266 | graph.forEachLink(initLink); 267 | } 268 | 269 | function initBody(nodeId) { 270 | var body = nodeBodies.get(nodeId); 271 | if (!body) { 272 | var node = graph.getNode(nodeId); 273 | if (!node) { 274 | throw new Error('initBody() was called with unknown node id'); 275 | } 276 | 277 | var pos = node.position; 278 | if (!pos) { 279 | var neighbors = getNeighborBodies(node); 280 | pos = physicsSimulator.getBestNewBodyPosition(neighbors); 281 | } 282 | 283 | body = physicsSimulator.addBodyAt(pos); 284 | body.id = nodeId; 285 | 286 | nodeBodies.set(nodeId, body); 287 | updateBodyMass(nodeId); 288 | 289 | if (isNodeOriginallyPinned(node)) { 290 | body.isPinned = true; 291 | } 292 | } 293 | } 294 | 295 | function releaseNode(node) { 296 | var nodeId = node.id; 297 | var body = nodeBodies.get(nodeId); 298 | if (body) { 299 | nodeBodies.delete(nodeId); 300 | physicsSimulator.removeBody(body); 301 | } 302 | } 303 | 304 | function initLink(link) { 305 | updateBodyMass(link.fromId); 306 | updateBodyMass(link.toId); 307 | 308 | var fromBody = nodeBodies.get(link.fromId), 309 | toBody = nodeBodies.get(link.toId), 310 | spring = physicsSimulator.addSpring(fromBody, toBody, link.length); 311 | 312 | springTransform(link, spring); 313 | 314 | springs[link.id] = spring; 315 | } 316 | 317 | function releaseLink(link) { 318 | var spring = springs[link.id]; 319 | if (spring) { 320 | var from = graph.getNode(link.fromId), 321 | to = graph.getNode(link.toId); 322 | 323 | if (from) updateBodyMass(from.id); 324 | if (to) updateBodyMass(to.id); 325 | 326 | delete springs[link.id]; 327 | 328 | physicsSimulator.removeSpring(spring); 329 | } 330 | } 331 | 332 | function getNeighborBodies(node) { 333 | // TODO: Could probably be done better on memory 334 | var neighbors = []; 335 | if (!node.links) { 336 | return neighbors; 337 | } 338 | // TODO: Previously I was looking only at two links. Should I look at all of them? 339 | node.links.forEach(function(link) { 340 | var otherBody = link.fromId !== node.id ? nodeBodies.get(link.fromId) : nodeBodies.get(link.toId); 341 | if (otherBody && otherBody.pos) { 342 | neighbors.push(otherBody); 343 | } 344 | }); 345 | 346 | return neighbors; 347 | } 348 | 349 | function updateBodyMass(nodeId) { 350 | var body = nodeBodies.get(nodeId); 351 | body.mass = nodeMass(nodeId); 352 | if (Number.isNaN(body.mass)) { 353 | throw new Error('Node mass should be a number'); 354 | } 355 | } 356 | 357 | /** 358 | * Checks whether graph node has in its settings pinned attribute, 359 | * which means layout algorithm cannot move it. Node can be marked 360 | * as pinned, if it has "isPinned" attribute, or when node.data has it. 361 | * 362 | * @param {Object} node a graph node to check 363 | * @return {Boolean} true if node should be treated as pinned; false otherwise. 364 | */ 365 | function isNodeOriginallyPinned(node) { 366 | return (node && (node.isPinned || (node.data && node.data.isPinned))); 367 | } 368 | 369 | function getInitializedBody(nodeId) { 370 | var body = nodeBodies.get(nodeId); 371 | if (!body) { 372 | initBody(nodeId); 373 | body = nodeBodies.get(nodeId); 374 | } 375 | return body; 376 | } 377 | 378 | /** 379 | * Calculates mass of a body, which corresponds to node with given id. 380 | * 381 | * @param {String|Number} nodeId identifier of a node, for which body mass needs to be calculated 382 | * @returns {Number} recommended mass of the body; 383 | */ 384 | function defaultArrayNodeMass(nodeId) { 385 | // This function is for older versions of ngraph.graph. 386 | var links = graph.getLinks(nodeId); 387 | if (!links) return 1; 388 | return 1 + links.length / 3.0; 389 | } 390 | 391 | function defaultSetNodeMass(nodeId) { 392 | var links = graph.getLinks(nodeId); 393 | if (!links) return 1; 394 | return 1 + links.size / 3.0; 395 | } 396 | } 397 | 398 | function noop() { } 399 | -------------------------------------------------------------------------------- /index.v43.d.ts: -------------------------------------------------------------------------------- 1 | declare module "ngraph.forcelayout" { 2 | import { Graph, NodeId, LinkId, Node, Link } from "ngraph.graph"; 3 | import { EventedType } from "ngraph.events"; 4 | 5 | export type ForceFunction = (iterationNumber: number) => void; 6 | 7 | export interface Vector { 8 | x: number; 9 | y: number; 10 | z?: number; 11 | [coord: string]: number | undefined; 12 | } 13 | 14 | export interface Body { 15 | isPinned: boolean; 16 | pos: Vector; 17 | force: Vector; 18 | velocity: Vector; 19 | mass: number; 20 | springCount: number; 21 | springLength: number; 22 | reset(): void; 23 | setPosition(x: number, y: number, z?: number, ...c: number[]): void; 24 | } 25 | 26 | export interface Spring { 27 | from: Body; 28 | to: Body; 29 | length: number; 30 | coefficient: number; 31 | } 32 | 33 | export interface QuadNode { 34 | body: Body | null; 35 | mass: number; 36 | mass_x: number; 37 | mass_y: number; 38 | mass_z?: number; 39 | } 40 | 41 | export interface QuadTree { 42 | insertBodies(bodies: Body[]): void; 43 | getRoot(): QuadNode & Record; 44 | updateBodyForce(sourceBody: Body): void; 45 | options(newOptions: { gravity: number; theta: number }): { gravity: number; theta: number }; 46 | } 47 | 48 | export interface BoundingBox { 49 | min_x: number; 50 | max_x: number; 51 | min_y: number; 52 | max_y: number; 53 | min_z?: number; 54 | max_z?: number; 55 | [min_max: string]: number | undefined; 56 | } 57 | 58 | /** 59 | * Settings for a PhysicsSimulator 60 | */ 61 | export interface PhysicsSettings { 62 | /** 63 | * Ideal length for links (springs in physical model). 64 | */ 65 | springLength: number; 66 | 67 | /** 68 | * Hook's law coefficient. 1 - solid spring. 69 | */ 70 | springCoefficient: number; 71 | 72 | /** 73 | * Coulomb's law coefficient. It's used to repel nodes thus should be negative 74 | * if you make it positive nodes start attract each other :). 75 | */ 76 | gravity: number; 77 | 78 | /** 79 | * Theta coefficient from Barnes Hut simulation. Ranged between (0, 1). 80 | * The closer it's to 1 the more nodes algorithm will have to go through. 81 | * Setting it to one makes Barnes Hut simulation no different from 82 | * brute-force forces calculation (each node is considered). 83 | */ 84 | theta: number; 85 | 86 | /** 87 | * Drag force coefficient. Used to slow down system, thus should be less than 1. 88 | * The closer it is to 0 the less tight system will be. 89 | */ 90 | dragCoefficient: number; 91 | 92 | /** 93 | * Default time step (dt) for forces integration 94 | */ 95 | timeStep: number; 96 | 97 | /** 98 | * Adaptive time step uses average spring length to compute actual time step: 99 | * See: https://twitter.com/anvaka/status/1293067160755957760 100 | */ 101 | adaptiveTimeStepWeight: number; 102 | 103 | /** 104 | * This parameter defines number of dimensions of the space where simulation 105 | * is performed. 106 | */ 107 | dimensions: number; 108 | 109 | /** 110 | * In debug mode more checks are performed, this will help you catch errors 111 | * quickly, however for production build it is recommended to turn off this flag 112 | * to speed up computation. 113 | */ 114 | debug: boolean; 115 | } 116 | 117 | /** 118 | * Manages a simulation of physical forces acting on bodies and springs. 119 | */ 120 | export interface PhysicsSimulator { 121 | /** 122 | * Array of bodies, registered with current simulator 123 | * 124 | * Note: To add new body, use addBody() method. This property is only 125 | * exposed for testing/performance purposes. 126 | */ 127 | bodies: Body[]; 128 | 129 | quadTree: QuadTree; 130 | 131 | /** 132 | * Array of springs, registered with current simulator 133 | * 134 | * Note: To add new spring, use addSpring() method. This property is only 135 | * exposed for testing/performance purposes. 136 | */ 137 | springs: Spring[]; 138 | 139 | /** 140 | * Returns settings with which current simulator was initialized 141 | */ 142 | settings: PhysicsSettings; 143 | 144 | /** 145 | * Adds a new force to simulation 146 | * @param forceName force identifier 147 | * @param forceFunction the function to apply 148 | */ 149 | addForce(forceName: string, forceFunction: ForceFunction): void; 150 | 151 | /** 152 | * Removes a force from the simulation 153 | * @param forceName force identifier 154 | */ 155 | removeForce(forceName: string): void; 156 | 157 | /** 158 | * Returns a map of all registered forces 159 | */ 160 | getForces(): Map; 161 | 162 | /** 163 | * Performs one step of force simulation. 164 | * 165 | * @returns true if system is considered stable; False otherwise. 166 | */ 167 | step(): boolean; 168 | 169 | /** 170 | * Adds body to the system 171 | * @param body physical body 172 | * @returns added body 173 | */ 174 | addBody(body: Body): Body; 175 | 176 | /** 177 | * Adds body to the system at given position 178 | * @param pos position of a body 179 | * @returns added body 180 | */ 181 | addBodyAt(pos: Vector): Body; 182 | 183 | /** 184 | * Removes body from the system 185 | * @param body to remove 186 | * @returns true if body found and removed. falsy otherwise; 187 | */ 188 | removeBody(body: Body): boolean; 189 | 190 | /** 191 | * Adds a spring to this simulation 192 | * @param body1 first body 193 | * @param body2 second body 194 | * @param springLength Ideal length for links 195 | * @param springCoefficient Hook's law coefficient. 1 - solid spring 196 | * @returns a handle for a spring. If you want to later remove 197 | * spring pass it to removeSpring() method. 198 | */ 199 | addSpring(body1: Body, body2: Body, springLength: number, springCoefficient: number): Spring; 200 | 201 | /** 202 | * Returns amount of movement performed on last step() call 203 | */ 204 | getTotalMovement(): number; 205 | 206 | /** 207 | * Removes spring from the system 208 | * @param spring to remove. Spring is an object returned by addSpring 209 | * @returns true if spring found and removed. falsy otherwise; 210 | */ 211 | removeSpring(spring: Spring): boolean; 212 | 213 | getBestNewBodyPosition(neighbors: Body[]): Vector; 214 | 215 | /** 216 | * Returns bounding box which covers all bodies 217 | */ 218 | getBBox(): BoundingBox; 219 | 220 | /** 221 | * Returns bounding box which covers all bodies 222 | */ 223 | getBoundingBox(): BoundingBox; 224 | 225 | /** @deprecated invalidateBBox() is deprecated, bounds always recomputed on `getBBox()` call */ 226 | invalidateBBox(): void; 227 | 228 | /** 229 | * Changes the gravity for the system 230 | * @param value Coulomb's law coefficient 231 | */ 232 | gravity(value: number): number; 233 | 234 | /** 235 | * Changes the theta coeffitient for the system 236 | * @param value Theta coefficient from Barnes Hut simulation 237 | */ 238 | theta(value: number): number; 239 | 240 | // TODO: create types declaration file for ngraph.random 241 | /** 242 | * Returns pseudo-random number generator instance 243 | */ 244 | random: any; 245 | } 246 | 247 | /** 248 | * Force based layout for a given graph. 249 | */ 250 | export interface Layout { 251 | /** 252 | * Performs one step of iterative layout algorithm 253 | * @returns true if the system should be considered stable; False otherwise. 254 | * The system is stable if no further call to `step()` can improve the layout. 255 | */ 256 | step(): boolean; 257 | 258 | /** 259 | * For a given `nodeId` returns position 260 | * @param nodeId node identifier 261 | */ 262 | getNodePosition(nodeId: NodeId): Vector; 263 | 264 | /** 265 | * Sets position of a node to a given coordinates 266 | * @param nodeId node identifier 267 | * @param x position of a node 268 | * @param y position of a node 269 | * @param z position of node (only if applicable to body) 270 | */ 271 | setNodePosition(nodeId: NodeId, x: number, y: number, z?: number, ...c: number[]): void; 272 | 273 | /** 274 | * Gets Link position by link id 275 | * @param linkId link identifier 276 | * @returns from: {x, y} coordinates of link start 277 | * @returns to: {x, y} coordinates of link end 278 | */ 279 | getLinkPosition(linkId: LinkId): { from: Vector; to: Vector }; 280 | 281 | /** 282 | * @returns area required to fit in the graph. Object contains 283 | * `x1`, `y1` - top left coordinates 284 | * `x2`, `y2` - bottom right coordinates 285 | */ 286 | getGraphRect(): { x1: number; y1: number; x2: number; y2: number }; 287 | 288 | /** 289 | * Iterates over each body in the layout simulator and performs a callback(body, nodeId) 290 | * @param callbackfn the callback function 291 | */ 292 | forEachBody(callbackfn: (value: Body, key: NodeId, map: Map) => void): void; 293 | 294 | /** 295 | * Requests layout algorithm to pin/unpin node to its current position 296 | * Pinned nodes should not be affected by layout algorithm and always 297 | * remain at their position 298 | * @param node the node to pin/unpin 299 | * @param isPinned true to pin, false to unpin 300 | */ 301 | pinNode(node: Node, isPinned: boolean): void; 302 | 303 | /** 304 | * Checks whether given graph's node is currently pinned 305 | * @param node the node to check 306 | */ 307 | isNodePinned(node: Node): boolean; 308 | 309 | /** 310 | * Request to release all resources 311 | */ 312 | dispose(): void; 313 | 314 | /** 315 | * Gets physical body for a given node id. If node is not found undefined 316 | * value is returned. 317 | * @param nodeId node identifier 318 | */ 319 | getBody(nodeId: NodeId): Body | undefined; 320 | 321 | /** 322 | * Gets spring for a given edge. 323 | * 324 | * @param linkId link identifer. 325 | */ 326 | getSpring(linkId: LinkId | Link): Spring; 327 | 328 | /** 329 | * Gets spring for a given edge. 330 | * 331 | * @param fromId node identifer - tail of the link 332 | * @param toId head of the link - head of the link 333 | */ 334 | getSpring(fromId: NodeId, toId: NodeId): Spring | undefined; 335 | 336 | /** 337 | * Returns length of cumulative force vector. The closer this to zero - the more stable the system is 338 | */ 339 | getForceVectorLength(): number; 340 | 341 | /** 342 | * @readonly Gets current physics simulator 343 | */ 344 | readonly simulator: PhysicsSimulator; 345 | 346 | /** 347 | * Gets the graph that was used for layout 348 | */ 349 | graph: T; 350 | 351 | /** 352 | * Gets amount of movement performed during last step operation 353 | */ 354 | lastMove: number; 355 | } 356 | 357 | /** 358 | * Creates force based layout for a given graph. 359 | * 360 | * @param graph which needs to be laid out 361 | * @param physicsSettings if you need custom settings 362 | * for physics simulator you can pass your own settings here. If it's not passed 363 | * a default one will be created. 364 | */ 365 | export default function createLayout( 366 | graph: T, 367 | physicsSettings?: Partial 368 | ): Layout & EventedType; 369 | } 370 | -------------------------------------------------------------------------------- /inline-transform.js: -------------------------------------------------------------------------------- 1 | var through = require('through2'); 2 | 3 | module.exports = function (file) { 4 | return through(function (buf, enc, next) { 5 | let originalContent = buf.toString('utf8'); 6 | let dimensions = 2; // change this if you need different number of dimensions 7 | if (file.match(/codeGenerators\/generate/)) { 8 | let content = require(file); 9 | let matches = originalContent.match(/^\/\/ InlineTransform: (.+)$/gm); 10 | let additionalTransform = matches ? matches.map(name => { 11 | let f = name.substr('// InlineTransform: '.length); 12 | return content[f](dimensions); 13 | }).join('\n') : ''; 14 | let exportCodeMatch = originalContent.match(/^\/\/ InlineTransformExport: (.+)$/m); 15 | let codeExport = exportCodeMatch ? exportCodeMatch[1] : 16 | `module.exports = function() { return ${content(dimensions).toString()} }`; 17 | this.push(`${additionalTransform}\n${codeExport}`); 18 | } else { 19 | this.push(originalContent); 20 | } 21 | next(); 22 | }); 23 | }; -------------------------------------------------------------------------------- /lib/bounds.js: -------------------------------------------------------------------------------- 1 | const generateBoundsFunction = require('./codeGenerators/generateBounds'); 2 | 3 | module.exports = generateBoundsFunction(2); 4 | -------------------------------------------------------------------------------- /lib/codeGenerators/createPatternBuilder.js: -------------------------------------------------------------------------------- 1 | const getVariableName = require('./getVariableName'); 2 | 3 | module.exports = function createPatternBuilder(dimension) { 4 | 5 | return pattern; 6 | 7 | function pattern(template, config) { 8 | let indent = (config && config.indent) || 0; 9 | let join = (config && config.join !== undefined) ? config.join : '\n'; 10 | let indentString = Array(indent + 1).join(' '); 11 | let buffer = []; 12 | for (let i = 0; i < dimension; ++i) { 13 | let variableName = getVariableName(i); 14 | let prefix = (i === 0) ? '' : indentString; 15 | buffer.push(prefix + template.replace(/{var}/g, variableName)); 16 | } 17 | return buffer.join(join); 18 | } 19 | }; 20 | -------------------------------------------------------------------------------- /lib/codeGenerators/generateBounds.js: -------------------------------------------------------------------------------- 1 | 2 | module.exports = generateBoundsFunction; 3 | module.exports.generateFunctionBody = generateBoundsFunctionBody; 4 | 5 | const createPatternBuilder = require('./createPatternBuilder'); 6 | 7 | function generateBoundsFunction(dimension) { 8 | let code = generateBoundsFunctionBody(dimension); 9 | return new Function('bodies', 'settings', 'random', code); 10 | } 11 | 12 | function generateBoundsFunctionBody(dimension) { 13 | let pattern = createPatternBuilder(dimension); 14 | 15 | let code = ` 16 | var boundingBox = { 17 | ${pattern('min_{var}: 0, max_{var}: 0,', {indent: 4})} 18 | }; 19 | 20 | return { 21 | box: boundingBox, 22 | 23 | update: updateBoundingBox, 24 | 25 | reset: resetBoundingBox, 26 | 27 | getBestNewPosition: function (neighbors) { 28 | var ${pattern('base_{var} = 0', {join: ', '})}; 29 | 30 | if (neighbors.length) { 31 | for (var i = 0; i < neighbors.length; ++i) { 32 | let neighborPos = neighbors[i].pos; 33 | ${pattern('base_{var} += neighborPos.{var};', {indent: 10})} 34 | } 35 | 36 | ${pattern('base_{var} /= neighbors.length;', {indent: 8})} 37 | } else { 38 | ${pattern('base_{var} = (boundingBox.min_{var} + boundingBox.max_{var}) / 2;', {indent: 8})} 39 | } 40 | 41 | var springLength = settings.springLength; 42 | return { 43 | ${pattern('{var}: base_{var} + (random.nextDouble() - 0.5) * springLength,', {indent: 8})} 44 | }; 45 | } 46 | }; 47 | 48 | function updateBoundingBox() { 49 | var i = bodies.length; 50 | if (i === 0) return; // No bodies - no borders. 51 | 52 | ${pattern('var max_{var} = -Infinity;', {indent: 4})} 53 | ${pattern('var min_{var} = Infinity;', {indent: 4})} 54 | 55 | while(i--) { 56 | // this is O(n), it could be done faster with quadtree, if we check the root node bounds 57 | var bodyPos = bodies[i].pos; 58 | ${pattern('if (bodyPos.{var} < min_{var}) min_{var} = bodyPos.{var};', {indent: 6})} 59 | ${pattern('if (bodyPos.{var} > max_{var}) max_{var} = bodyPos.{var};', {indent: 6})} 60 | } 61 | 62 | ${pattern('boundingBox.min_{var} = min_{var};', {indent: 4})} 63 | ${pattern('boundingBox.max_{var} = max_{var};', {indent: 4})} 64 | } 65 | 66 | function resetBoundingBox() { 67 | ${pattern('boundingBox.min_{var} = boundingBox.max_{var} = 0;', {indent: 4})} 68 | } 69 | `; 70 | return code; 71 | } 72 | -------------------------------------------------------------------------------- /lib/codeGenerators/generateCreateBody.js: -------------------------------------------------------------------------------- 1 | 2 | const createPatternBuilder = require('./createPatternBuilder'); 3 | 4 | module.exports = generateCreateBodyFunction; 5 | module.exports.generateCreateBodyFunctionBody = generateCreateBodyFunctionBody; 6 | 7 | // InlineTransform: getVectorCode 8 | module.exports.getVectorCode = getVectorCode; 9 | // InlineTransform: getBodyCode 10 | module.exports.getBodyCode = getBodyCode; 11 | // InlineTransformExport: module.exports = function() { return Body; } 12 | 13 | function generateCreateBodyFunction(dimension, debugSetters) { 14 | let code = generateCreateBodyFunctionBody(dimension, debugSetters); 15 | let {Body} = (new Function(code))(); 16 | return Body; 17 | } 18 | 19 | function generateCreateBodyFunctionBody(dimension, debugSetters) { 20 | let code = ` 21 | ${getVectorCode(dimension, debugSetters)} 22 | ${getBodyCode(dimension, debugSetters)} 23 | return {Body: Body, Vector: Vector}; 24 | `; 25 | return code; 26 | } 27 | 28 | function getBodyCode(dimension) { 29 | let pattern = createPatternBuilder(dimension); 30 | let variableList = pattern('{var}', {join: ', '}); 31 | return ` 32 | function Body(${variableList}) { 33 | this.isPinned = false; 34 | this.pos = new Vector(${variableList}); 35 | this.force = new Vector(); 36 | this.velocity = new Vector(); 37 | this.mass = 1; 38 | 39 | this.springCount = 0; 40 | this.springLength = 0; 41 | } 42 | 43 | Body.prototype.reset = function() { 44 | this.force.reset(); 45 | this.springCount = 0; 46 | this.springLength = 0; 47 | } 48 | 49 | Body.prototype.setPosition = function (${variableList}) { 50 | ${pattern('this.pos.{var} = {var} || 0;', {indent: 2})} 51 | };`; 52 | } 53 | 54 | function getVectorCode(dimension, debugSetters) { 55 | let pattern = createPatternBuilder(dimension); 56 | let setters = ''; 57 | if (debugSetters) { 58 | setters = `${pattern("\n\ 59 | var v{var};\n\ 60 | Object.defineProperty(this, '{var}', {\n\ 61 | set: function(v) { \n\ 62 | if (!Number.isFinite(v)) throw new Error('Cannot set non-numbers to {var}');\n\ 63 | v{var} = v; \n\ 64 | },\n\ 65 | get: function() { return v{var}; }\n\ 66 | });")}`; 67 | } 68 | 69 | let variableList = pattern('{var}', {join: ', '}); 70 | return `function Vector(${variableList}) { 71 | ${setters} 72 | if (typeof arguments[0] === 'object') { 73 | // could be another vector 74 | let v = arguments[0]; 75 | ${pattern('if (!Number.isFinite(v.{var})) throw new Error("Expected value is not a finite number at Vector constructor ({var})");', {indent: 4})} 76 | ${pattern('this.{var} = v.{var};', {indent: 4})} 77 | } else { 78 | ${pattern('this.{var} = typeof {var} === "number" ? {var} : 0;', {indent: 4})} 79 | } 80 | } 81 | 82 | Vector.prototype.reset = function () { 83 | ${pattern('this.{var} = ', {join: ''})}0; 84 | };`; 85 | } -------------------------------------------------------------------------------- /lib/codeGenerators/generateCreateDragForce.js: -------------------------------------------------------------------------------- 1 | const createPatternBuilder = require('./createPatternBuilder'); 2 | 3 | module.exports = generateCreateDragForceFunction; 4 | module.exports.generateCreateDragForceFunctionBody = generateCreateDragForceFunctionBody; 5 | 6 | function generateCreateDragForceFunction(dimension) { 7 | let code = generateCreateDragForceFunctionBody(dimension); 8 | return new Function('options', code); 9 | } 10 | 11 | function generateCreateDragForceFunctionBody(dimension) { 12 | let pattern = createPatternBuilder(dimension); 13 | let code = ` 14 | if (!Number.isFinite(options.dragCoefficient)) throw new Error('dragCoefficient is not a finite number'); 15 | 16 | return { 17 | update: function(body) { 18 | ${pattern('body.force.{var} -= options.dragCoefficient * body.velocity.{var};', {indent: 6})} 19 | } 20 | }; 21 | `; 22 | return code; 23 | } 24 | -------------------------------------------------------------------------------- /lib/codeGenerators/generateCreateSpringForce.js: -------------------------------------------------------------------------------- 1 | const createPatternBuilder = require('./createPatternBuilder'); 2 | 3 | module.exports = generateCreateSpringForceFunction; 4 | module.exports.generateCreateSpringForceFunctionBody = generateCreateSpringForceFunctionBody; 5 | 6 | function generateCreateSpringForceFunction(dimension) { 7 | let code = generateCreateSpringForceFunctionBody(dimension); 8 | return new Function('options', 'random', code); 9 | } 10 | 11 | function generateCreateSpringForceFunctionBody(dimension) { 12 | let pattern = createPatternBuilder(dimension); 13 | let code = ` 14 | if (!Number.isFinite(options.springCoefficient)) throw new Error('Spring coefficient is not a number'); 15 | if (!Number.isFinite(options.springLength)) throw new Error('Spring length is not a number'); 16 | 17 | return { 18 | /** 19 | * Updates forces acting on a spring 20 | */ 21 | update: function (spring) { 22 | var body1 = spring.from; 23 | var body2 = spring.to; 24 | var length = spring.length < 0 ? options.springLength : spring.length; 25 | ${pattern('var d{var} = body2.pos.{var} - body1.pos.{var};', {indent: 6})} 26 | var r = Math.sqrt(${pattern('d{var} * d{var}', {join: ' + '})}); 27 | 28 | if (r === 0) { 29 | ${pattern('d{var} = (random.nextDouble() - 0.5) / 50;', {indent: 8})} 30 | r = Math.sqrt(${pattern('d{var} * d{var}', {join: ' + '})}); 31 | } 32 | 33 | var d = r - length; 34 | var coefficient = ((spring.coefficient > 0) ? spring.coefficient : options.springCoefficient) * d / r; 35 | 36 | ${pattern('body1.force.{var} += coefficient * d{var}', {indent: 6})}; 37 | body1.springCount += 1; 38 | body1.springLength += r; 39 | 40 | ${pattern('body2.force.{var} -= coefficient * d{var}', {indent: 6})}; 41 | body2.springCount += 1; 42 | body2.springLength += r; 43 | } 44 | }; 45 | `; 46 | return code; 47 | } 48 | -------------------------------------------------------------------------------- /lib/codeGenerators/generateIntegrator.js: -------------------------------------------------------------------------------- 1 | const createPatternBuilder = require('./createPatternBuilder'); 2 | 3 | module.exports = generateIntegratorFunction; 4 | module.exports.generateIntegratorFunctionBody = generateIntegratorFunctionBody; 5 | 6 | function generateIntegratorFunction(dimension) { 7 | let code = generateIntegratorFunctionBody(dimension); 8 | return new Function('bodies', 'timeStep', 'adaptiveTimeStepWeight', code); 9 | } 10 | 11 | function generateIntegratorFunctionBody(dimension) { 12 | let pattern = createPatternBuilder(dimension); 13 | let code = ` 14 | var length = bodies.length; 15 | if (length === 0) return 0; 16 | 17 | ${pattern('var d{var} = 0, t{var} = 0;', {indent: 2})} 18 | 19 | for (var i = 0; i < length; ++i) { 20 | var body = bodies[i]; 21 | if (body.isPinned) continue; 22 | 23 | if (adaptiveTimeStepWeight && body.springCount) { 24 | timeStep = (adaptiveTimeStepWeight * body.springLength/body.springCount); 25 | } 26 | 27 | var coeff = timeStep / body.mass; 28 | 29 | ${pattern('body.velocity.{var} += coeff * body.force.{var};', {indent: 4})} 30 | ${pattern('var v{var} = body.velocity.{var};', {indent: 4})} 31 | var v = Math.sqrt(${pattern('v{var} * v{var}', {join: ' + '})}); 32 | 33 | if (v > 1) { 34 | // We normalize it so that we move within timeStep range. 35 | // for the case when v <= 1 - we let velocity to fade out. 36 | ${pattern('body.velocity.{var} = v{var} / v;', {indent: 6})} 37 | } 38 | 39 | ${pattern('d{var} = timeStep * body.velocity.{var};', {indent: 4})} 40 | 41 | ${pattern('body.pos.{var} += d{var};', {indent: 4})} 42 | 43 | ${pattern('t{var} += Math.abs(d{var});', {indent: 4})} 44 | } 45 | 46 | return (${pattern('t{var} * t{var}', {join: ' + '})})/length; 47 | `; 48 | return code; 49 | } 50 | -------------------------------------------------------------------------------- /lib/codeGenerators/generateQuadTree.js: -------------------------------------------------------------------------------- 1 | const createPatternBuilder = require('./createPatternBuilder'); 2 | const getVariableName = require('./getVariableName'); 3 | 4 | module.exports = generateQuadTreeFunction; 5 | module.exports.generateQuadTreeFunctionBody = generateQuadTreeFunctionBody; 6 | 7 | // These exports are for InlineTransform tool. 8 | // InlineTransform: getInsertStackCode 9 | module.exports.getInsertStackCode = getInsertStackCode; 10 | // InlineTransform: getQuadNodeCode 11 | module.exports.getQuadNodeCode = getQuadNodeCode; 12 | // InlineTransform: isSamePosition 13 | module.exports.isSamePosition = isSamePosition; 14 | // InlineTransform: getChildBodyCode 15 | module.exports.getChildBodyCode = getChildBodyCode; 16 | // InlineTransform: setChildBodyCode 17 | module.exports.setChildBodyCode = setChildBodyCode; 18 | 19 | function generateQuadTreeFunction(dimension) { 20 | let code = generateQuadTreeFunctionBody(dimension); 21 | return (new Function(code))(); 22 | } 23 | 24 | function generateQuadTreeFunctionBody(dimension) { 25 | let pattern = createPatternBuilder(dimension); 26 | let quadCount = Math.pow(2, dimension); 27 | 28 | let code = ` 29 | ${getInsertStackCode()} 30 | ${getQuadNodeCode(dimension)} 31 | ${isSamePosition(dimension)} 32 | ${getChildBodyCode(dimension)} 33 | ${setChildBodyCode(dimension)} 34 | 35 | function createQuadTree(options, random) { 36 | options = options || {}; 37 | options.gravity = typeof options.gravity === 'number' ? options.gravity : -1; 38 | options.theta = typeof options.theta === 'number' ? options.theta : 0.8; 39 | 40 | var gravity = options.gravity; 41 | var updateQueue = []; 42 | var insertStack = new InsertStack(); 43 | var theta = options.theta; 44 | 45 | var nodesCache = []; 46 | var currentInCache = 0; 47 | var root = newNode(); 48 | 49 | return { 50 | insertBodies: insertBodies, 51 | 52 | /** 53 | * Gets root node if it is present 54 | */ 55 | getRoot: function() { 56 | return root; 57 | }, 58 | 59 | updateBodyForce: update, 60 | 61 | options: function(newOptions) { 62 | if (newOptions) { 63 | if (typeof newOptions.gravity === 'number') { 64 | gravity = newOptions.gravity; 65 | } 66 | if (typeof newOptions.theta === 'number') { 67 | theta = newOptions.theta; 68 | } 69 | 70 | return this; 71 | } 72 | 73 | return { 74 | gravity: gravity, 75 | theta: theta 76 | }; 77 | } 78 | }; 79 | 80 | function newNode() { 81 | // To avoid pressure on GC we reuse nodes. 82 | var node = nodesCache[currentInCache]; 83 | if (node) { 84 | ${assignQuads(' node.')} 85 | node.body = null; 86 | node.mass = ${pattern('node.mass_{var} = ', {join: ''})}0; 87 | ${pattern('node.min_{var} = node.max_{var} = ', {join: ''})}0; 88 | } else { 89 | node = new QuadNode(); 90 | nodesCache[currentInCache] = node; 91 | } 92 | 93 | ++currentInCache; 94 | return node; 95 | } 96 | 97 | function update(sourceBody) { 98 | var queue = updateQueue; 99 | var v; 100 | ${pattern('var d{var};', {indent: 4})} 101 | var r; 102 | ${pattern('var f{var} = 0;', {indent: 4})} 103 | var queueLength = 1; 104 | var shiftIdx = 0; 105 | var pushIdx = 1; 106 | 107 | queue[0] = root; 108 | 109 | while (queueLength) { 110 | var node = queue[shiftIdx]; 111 | var body = node.body; 112 | 113 | queueLength -= 1; 114 | shiftIdx += 1; 115 | var differentBody = (body !== sourceBody); 116 | if (body && differentBody) { 117 | // If the current node is a leaf node (and it is not source body), 118 | // calculate the force exerted by the current node on body, and add this 119 | // amount to body's net force. 120 | ${pattern('d{var} = body.pos.{var} - sourceBody.pos.{var};', {indent: 8})} 121 | r = Math.sqrt(${pattern('d{var} * d{var}', {join: ' + '})}); 122 | 123 | if (r === 0) { 124 | // Poor man's protection against zero distance. 125 | ${pattern('d{var} = (random.nextDouble() - 0.5) / 50;', {indent: 10})} 126 | r = Math.sqrt(${pattern('d{var} * d{var}', {join: ' + '})}); 127 | } 128 | 129 | // This is standard gravitation force calculation but we divide 130 | // by r^3 to save two operations when normalizing force vector. 131 | v = gravity * body.mass * sourceBody.mass / (r * r * r); 132 | ${pattern('f{var} += v * d{var};', {indent: 8})} 133 | } else if (differentBody) { 134 | // Otherwise, calculate the ratio s / r, where s is the width of the region 135 | // represented by the internal node, and r is the distance between the body 136 | // and the node's center-of-mass 137 | ${pattern('d{var} = node.mass_{var} / node.mass - sourceBody.pos.{var};', {indent: 8})} 138 | r = Math.sqrt(${pattern('d{var} * d{var}', {join: ' + '})}); 139 | 140 | if (r === 0) { 141 | // Sorry about code duplication. I don't want to create many functions 142 | // right away. Just want to see performance first. 143 | ${pattern('d{var} = (random.nextDouble() - 0.5) / 50;', {indent: 10})} 144 | r = Math.sqrt(${pattern('d{var} * d{var}', {join: ' + '})}); 145 | } 146 | // If s / r < θ, treat this internal node as a single body, and calculate the 147 | // force it exerts on sourceBody, and add this amount to sourceBody's net force. 148 | if ((node.max_${getVariableName(0)} - node.min_${getVariableName(0)}) / r < theta) { 149 | // in the if statement above we consider node's width only 150 | // because the region was made into square during tree creation. 151 | // Thus there is no difference between using width or height. 152 | v = gravity * node.mass * sourceBody.mass / (r * r * r); 153 | ${pattern('f{var} += v * d{var};', {indent: 10})} 154 | } else { 155 | // Otherwise, run the procedure recursively on each of the current node's children. 156 | 157 | // I intentionally unfolded this loop, to save several CPU cycles. 158 | ${runRecursiveOnChildren()} 159 | } 160 | } 161 | } 162 | 163 | ${pattern('sourceBody.force.{var} += f{var};', {indent: 4})} 164 | } 165 | 166 | function insertBodies(bodies) { 167 | ${pattern('var {var}min = Number.MAX_VALUE;', {indent: 4})} 168 | ${pattern('var {var}max = Number.MIN_VALUE;', {indent: 4})} 169 | var i = bodies.length; 170 | 171 | // To reduce quad tree depth we are looking for exact bounding box of all particles. 172 | while (i--) { 173 | var pos = bodies[i].pos; 174 | ${pattern('if (pos.{var} < {var}min) {var}min = pos.{var};', {indent: 6})} 175 | ${pattern('if (pos.{var} > {var}max) {var}max = pos.{var};', {indent: 6})} 176 | } 177 | 178 | // Makes the bounds square. 179 | var maxSideLength = -Infinity; 180 | ${pattern('if ({var}max - {var}min > maxSideLength) maxSideLength = {var}max - {var}min ;', {indent: 4})} 181 | 182 | currentInCache = 0; 183 | root = newNode(); 184 | ${pattern('root.min_{var} = {var}min;', {indent: 4})} 185 | ${pattern('root.max_{var} = {var}min + maxSideLength;', {indent: 4})} 186 | 187 | i = bodies.length - 1; 188 | if (i >= 0) { 189 | root.body = bodies[i]; 190 | } 191 | while (i--) { 192 | insert(bodies[i], root); 193 | } 194 | } 195 | 196 | function insert(newBody) { 197 | insertStack.reset(); 198 | insertStack.push(root, newBody); 199 | 200 | while (!insertStack.isEmpty()) { 201 | var stackItem = insertStack.pop(); 202 | var node = stackItem.node; 203 | var body = stackItem.body; 204 | 205 | if (!node.body) { 206 | // This is internal node. Update the total mass of the node and center-of-mass. 207 | ${pattern('var {var} = body.pos.{var};', {indent: 8})} 208 | node.mass += body.mass; 209 | ${pattern('node.mass_{var} += body.mass * {var};', {indent: 8})} 210 | 211 | // Recursively insert the body in the appropriate quadrant. 212 | // But first find the appropriate quadrant. 213 | var quadIdx = 0; // Assume we are in the 0's quad. 214 | ${pattern('var min_{var} = node.min_{var};', {indent: 8})} 215 | ${pattern('var max_{var} = (min_{var} + node.max_{var}) / 2;', {indent: 8})} 216 | 217 | ${assignInsertionQuadIndex(8)} 218 | 219 | var child = getChild(node, quadIdx); 220 | 221 | if (!child) { 222 | // The node is internal but this quadrant is not taken. Add 223 | // subnode to it. 224 | child = newNode(); 225 | ${pattern('child.min_{var} = min_{var};', {indent: 10})} 226 | ${pattern('child.max_{var} = max_{var};', {indent: 10})} 227 | child.body = body; 228 | 229 | setChild(node, quadIdx, child); 230 | } else { 231 | // continue searching in this quadrant. 232 | insertStack.push(child, body); 233 | } 234 | } else { 235 | // We are trying to add to the leaf node. 236 | // We have to convert current leaf into internal node 237 | // and continue adding two nodes. 238 | var oldBody = node.body; 239 | node.body = null; // internal nodes do not cary bodies 240 | 241 | if (isSamePosition(oldBody.pos, body.pos)) { 242 | // Prevent infinite subdivision by bumping one node 243 | // anywhere in this quadrant 244 | var retriesCount = 3; 245 | do { 246 | var offset = random.nextDouble(); 247 | ${pattern('var d{var} = (node.max_{var} - node.min_{var}) * offset;', {indent: 12})} 248 | 249 | ${pattern('oldBody.pos.{var} = node.min_{var} + d{var};', {indent: 12})} 250 | retriesCount -= 1; 251 | // Make sure we don't bump it out of the box. If we do, next iteration should fix it 252 | } while (retriesCount > 0 && isSamePosition(oldBody.pos, body.pos)); 253 | 254 | if (retriesCount === 0 && isSamePosition(oldBody.pos, body.pos)) { 255 | // This is very bad, we ran out of precision. 256 | // if we do not return from the method we'll get into 257 | // infinite loop here. So we sacrifice correctness of layout, and keep the app running 258 | // Next layout iteration should get larger bounding box in the first step and fix this 259 | return; 260 | } 261 | } 262 | // Next iteration should subdivide node further. 263 | insertStack.push(node, oldBody); 264 | insertStack.push(node, body); 265 | } 266 | } 267 | } 268 | } 269 | return createQuadTree; 270 | 271 | `; 272 | return code; 273 | 274 | 275 | function assignInsertionQuadIndex(indentCount) { 276 | let insertionCode = []; 277 | let indent = Array(indentCount + 1).join(' '); 278 | for (let i = 0; i < dimension; ++i) { 279 | insertionCode.push(indent + `if (${getVariableName(i)} > max_${getVariableName(i)}) {`); 280 | insertionCode.push(indent + ` quadIdx = quadIdx + ${Math.pow(2, i)};`); 281 | insertionCode.push(indent + ` min_${getVariableName(i)} = max_${getVariableName(i)};`); 282 | insertionCode.push(indent + ` max_${getVariableName(i)} = node.max_${getVariableName(i)};`); 283 | insertionCode.push(indent + `}`); 284 | } 285 | return insertionCode.join('\n'); 286 | // if (x > max_x) { // somewhere in the eastern part. 287 | // quadIdx = quadIdx + 1; 288 | // left = right; 289 | // right = node.right; 290 | // } 291 | } 292 | 293 | function runRecursiveOnChildren() { 294 | let indent = Array(11).join(' '); 295 | let recursiveCode = []; 296 | for (let i = 0; i < quadCount; ++i) { 297 | recursiveCode.push(indent + `if (node.quad${i}) {`); 298 | recursiveCode.push(indent + ` queue[pushIdx] = node.quad${i};`); 299 | recursiveCode.push(indent + ` queueLength += 1;`); 300 | recursiveCode.push(indent + ` pushIdx += 1;`); 301 | recursiveCode.push(indent + `}`); 302 | } 303 | return recursiveCode.join('\n'); 304 | // if (node.quad0) { 305 | // queue[pushIdx] = node.quad0; 306 | // queueLength += 1; 307 | // pushIdx += 1; 308 | // } 309 | } 310 | 311 | function assignQuads(indent) { 312 | // this.quad0 = null; 313 | // this.quad1 = null; 314 | // this.quad2 = null; 315 | // this.quad3 = null; 316 | let quads = []; 317 | for (let i = 0; i < quadCount; ++i) { 318 | quads.push(`${indent}quad${i} = null;`); 319 | } 320 | return quads.join('\n'); 321 | } 322 | } 323 | 324 | function isSamePosition(dimension) { 325 | let pattern = createPatternBuilder(dimension); 326 | return ` 327 | function isSamePosition(point1, point2) { 328 | ${pattern('var d{var} = Math.abs(point1.{var} - point2.{var});', {indent: 2})} 329 | 330 | return ${pattern('d{var} < 1e-8', {join: ' && '})}; 331 | } 332 | `; 333 | } 334 | 335 | function setChildBodyCode(dimension) { 336 | var quadCount = Math.pow(2, dimension); 337 | return ` 338 | function setChild(node, idx, child) { 339 | ${setChildBody()} 340 | }`; 341 | function setChildBody() { 342 | let childBody = []; 343 | for (let i = 0; i < quadCount; ++i) { 344 | let prefix = (i === 0) ? ' ' : ' else '; 345 | childBody.push(`${prefix}if (idx === ${i}) node.quad${i} = child;`); 346 | } 347 | 348 | return childBody.join('\n'); 349 | // if (idx === 0) node.quad0 = child; 350 | // else if (idx === 1) node.quad1 = child; 351 | // else if (idx === 2) node.quad2 = child; 352 | // else if (idx === 3) node.quad3 = child; 353 | } 354 | } 355 | 356 | function getChildBodyCode(dimension) { 357 | return `function getChild(node, idx) { 358 | ${getChildBody()} 359 | return null; 360 | }`; 361 | 362 | function getChildBody() { 363 | let childBody = []; 364 | let quadCount = Math.pow(2, dimension); 365 | for (let i = 0; i < quadCount; ++i) { 366 | childBody.push(` if (idx === ${i}) return node.quad${i};`); 367 | } 368 | 369 | return childBody.join('\n'); 370 | // if (idx === 0) return node.quad0; 371 | // if (idx === 1) return node.quad1; 372 | // if (idx === 2) return node.quad2; 373 | // if (idx === 3) return node.quad3; 374 | } 375 | } 376 | 377 | function getQuadNodeCode(dimension) { 378 | let pattern = createPatternBuilder(dimension); 379 | let quadCount = Math.pow(2, dimension); 380 | var quadNodeCode = ` 381 | function QuadNode() { 382 | // body stored inside this node. In quad tree only leaf nodes (by construction) 383 | // contain bodies: 384 | this.body = null; 385 | 386 | // Child nodes are stored in quads. Each quad is presented by number: 387 | // 0 | 1 388 | // ----- 389 | // 2 | 3 390 | ${assignQuads(' this.')} 391 | 392 | // Total mass of current node 393 | this.mass = 0; 394 | 395 | // Center of mass coordinates 396 | ${pattern('this.mass_{var} = 0;', {indent: 2})} 397 | 398 | // bounding box coordinates 399 | ${pattern('this.min_{var} = 0;', {indent: 2})} 400 | ${pattern('this.max_{var} = 0;', {indent: 2})} 401 | } 402 | `; 403 | return quadNodeCode; 404 | 405 | function assignQuads(indent) { 406 | // this.quad0 = null; 407 | // this.quad1 = null; 408 | // this.quad2 = null; 409 | // this.quad3 = null; 410 | let quads = []; 411 | for (let i = 0; i < quadCount; ++i) { 412 | quads.push(`${indent}quad${i} = null;`); 413 | } 414 | return quads.join('\n'); 415 | } 416 | } 417 | 418 | function getInsertStackCode() { 419 | return ` 420 | /** 421 | * Our implementation of QuadTree is non-recursive to avoid GC hit 422 | * This data structure represent stack of elements 423 | * which we are trying to insert into quad tree. 424 | */ 425 | function InsertStack () { 426 | this.stack = []; 427 | this.popIdx = 0; 428 | } 429 | 430 | InsertStack.prototype = { 431 | isEmpty: function() { 432 | return this.popIdx === 0; 433 | }, 434 | push: function (node, body) { 435 | var item = this.stack[this.popIdx]; 436 | if (!item) { 437 | // we are trying to avoid memory pressure: create new element 438 | // only when absolutely necessary 439 | this.stack[this.popIdx] = new InsertStackElement(node, body); 440 | } else { 441 | item.node = node; 442 | item.body = body; 443 | } 444 | ++this.popIdx; 445 | }, 446 | pop: function () { 447 | if (this.popIdx > 0) { 448 | return this.stack[--this.popIdx]; 449 | } 450 | }, 451 | reset: function () { 452 | this.popIdx = 0; 453 | } 454 | }; 455 | 456 | function InsertStackElement(node, body) { 457 | this.node = node; // QuadTree node 458 | this.body = body; // physical body which needs to be inserted to node 459 | } 460 | `; 461 | } -------------------------------------------------------------------------------- /lib/codeGenerators/getVariableName.js: -------------------------------------------------------------------------------- 1 | module.exports = function getVariableName(index) { 2 | if (index === 0) return 'x'; 3 | if (index === 1) return 'y'; 4 | if (index === 2) return 'z'; 5 | return 'c' + (index + 1); 6 | }; -------------------------------------------------------------------------------- /lib/createPhysicsSimulator.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Manages a simulation of physical forces acting on bodies and springs. 3 | */ 4 | module.exports = createPhysicsSimulator; 5 | 6 | var generateCreateBodyFunction = require('./codeGenerators/generateCreateBody'); 7 | var generateQuadTreeFunction = require('./codeGenerators/generateQuadTree'); 8 | var generateBoundsFunction = require('./codeGenerators/generateBounds'); 9 | var generateCreateDragForceFunction = require('./codeGenerators/generateCreateDragForce'); 10 | var generateCreateSpringForceFunction = require('./codeGenerators/generateCreateSpringForce'); 11 | var generateIntegratorFunction = require('./codeGenerators/generateIntegrator'); 12 | 13 | var dimensionalCache = {}; 14 | 15 | function createPhysicsSimulator(settings) { 16 | var Spring = require('./spring'); 17 | var merge = require('ngraph.merge'); 18 | var eventify = require('ngraph.events'); 19 | if (settings) { 20 | // Check for names from older versions of the layout 21 | if (settings.springCoeff !== undefined) throw new Error('springCoeff was renamed to springCoefficient'); 22 | if (settings.dragCoeff !== undefined) throw new Error('dragCoeff was renamed to dragCoefficient'); 23 | } 24 | 25 | settings = merge(settings, { 26 | /** 27 | * Ideal length for links (springs in physical model). 28 | */ 29 | springLength: 10, 30 | 31 | /** 32 | * Hook's law coefficient. 1 - solid spring. 33 | */ 34 | springCoefficient: 0.8, 35 | 36 | /** 37 | * Coulomb's law coefficient. It's used to repel nodes thus should be negative 38 | * if you make it positive nodes start attract each other :). 39 | */ 40 | gravity: -12, 41 | 42 | /** 43 | * Theta coefficient from Barnes Hut simulation. Ranged between (0, 1). 44 | * The closer it's to 1 the more nodes algorithm will have to go through. 45 | * Setting it to one makes Barnes Hut simulation no different from 46 | * brute-force forces calculation (each node is considered). 47 | */ 48 | theta: 0.8, 49 | 50 | /** 51 | * Drag force coefficient. Used to slow down system, thus should be less than 1. 52 | * The closer it is to 0 the less tight system will be. 53 | */ 54 | dragCoefficient: 0.9, // TODO: Need to rename this to something better. E.g. `dragCoefficient` 55 | 56 | /** 57 | * Default time step (dt) for forces integration 58 | */ 59 | timeStep : 0.5, 60 | 61 | /** 62 | * Adaptive time step uses average spring length to compute actual time step: 63 | * See: https://twitter.com/anvaka/status/1293067160755957760 64 | */ 65 | adaptiveTimeStepWeight: 0, 66 | 67 | /** 68 | * This parameter defines number of dimensions of the space where simulation 69 | * is performed. 70 | */ 71 | dimensions: 2, 72 | 73 | /** 74 | * In debug mode more checks are performed, this will help you catch errors 75 | * quickly, however for production build it is recommended to turn off this flag 76 | * to speed up computation. 77 | */ 78 | debug: false 79 | }); 80 | 81 | var factory = dimensionalCache[settings.dimensions]; 82 | if (!factory) { 83 | var dimensions = settings.dimensions; 84 | factory = { 85 | Body: generateCreateBodyFunction(dimensions, settings.debug), 86 | createQuadTree: generateQuadTreeFunction(dimensions), 87 | createBounds: generateBoundsFunction(dimensions), 88 | createDragForce: generateCreateDragForceFunction(dimensions), 89 | createSpringForce: generateCreateSpringForceFunction(dimensions), 90 | integrate: generateIntegratorFunction(dimensions), 91 | }; 92 | dimensionalCache[dimensions] = factory; 93 | } 94 | 95 | var Body = factory.Body; 96 | var createQuadTree = factory.createQuadTree; 97 | var createBounds = factory.createBounds; 98 | var createDragForce = factory.createDragForce; 99 | var createSpringForce = factory.createSpringForce; 100 | var integrate = factory.integrate; 101 | var createBody = pos => new Body(pos); 102 | 103 | var random = require('ngraph.random').random(42); 104 | var bodies = []; // Bodies in this simulation. 105 | var springs = []; // Springs in this simulation. 106 | 107 | var quadTree = createQuadTree(settings, random); 108 | var bounds = createBounds(bodies, settings, random); 109 | var springForce = createSpringForce(settings, random); 110 | var dragForce = createDragForce(settings); 111 | 112 | var totalMovement = 0; // how much movement we made on last step 113 | var forces = []; 114 | var forceMap = new Map(); 115 | var iterationNumber = 0; 116 | 117 | addForce('nbody', nbodyForce); 118 | addForce('spring', updateSpringForce); 119 | 120 | var publicApi = { 121 | /** 122 | * Array of bodies, registered with current simulator 123 | * 124 | * Note: To add new body, use addBody() method. This property is only 125 | * exposed for testing/performance purposes. 126 | */ 127 | bodies: bodies, 128 | 129 | quadTree: quadTree, 130 | 131 | /** 132 | * Array of springs, registered with current simulator 133 | * 134 | * Note: To add new spring, use addSpring() method. This property is only 135 | * exposed for testing/performance purposes. 136 | */ 137 | springs: springs, 138 | 139 | /** 140 | * Returns settings with which current simulator was initialized 141 | */ 142 | settings: settings, 143 | 144 | /** 145 | * Adds a new force to simulation 146 | */ 147 | addForce: addForce, 148 | 149 | /** 150 | * Removes a force from the simulation. 151 | */ 152 | removeForce: removeForce, 153 | 154 | /** 155 | * Returns a map of all registered forces. 156 | */ 157 | getForces: getForces, 158 | 159 | /** 160 | * Performs one step of force simulation. 161 | * 162 | * @returns {boolean} true if system is considered stable; False otherwise. 163 | */ 164 | step: function () { 165 | for (var i = 0; i < forces.length; ++i) { 166 | forces[i](iterationNumber); 167 | } 168 | var movement = integrate(bodies, settings.timeStep, settings.adaptiveTimeStepWeight); 169 | iterationNumber += 1; 170 | return movement; 171 | }, 172 | 173 | /** 174 | * Adds body to the system 175 | * 176 | * @param {ngraph.physics.primitives.Body} body physical body 177 | * 178 | * @returns {ngraph.physics.primitives.Body} added body 179 | */ 180 | addBody: function (body) { 181 | if (!body) { 182 | throw new Error('Body is required'); 183 | } 184 | bodies.push(body); 185 | 186 | return body; 187 | }, 188 | 189 | /** 190 | * Adds body to the system at given position 191 | * 192 | * @param {Object} pos position of a body 193 | * 194 | * @returns {ngraph.physics.primitives.Body} added body 195 | */ 196 | addBodyAt: function (pos) { 197 | if (!pos) { 198 | throw new Error('Body position is required'); 199 | } 200 | var body = createBody(pos); 201 | bodies.push(body); 202 | 203 | return body; 204 | }, 205 | 206 | /** 207 | * Removes body from the system 208 | * 209 | * @param {ngraph.physics.primitives.Body} body to remove 210 | * 211 | * @returns {Boolean} true if body found and removed. falsy otherwise; 212 | */ 213 | removeBody: function (body) { 214 | if (!body) { return; } 215 | 216 | var idx = bodies.indexOf(body); 217 | if (idx < 0) { return; } 218 | 219 | bodies.splice(idx, 1); 220 | if (bodies.length === 0) { 221 | bounds.reset(); 222 | } 223 | return true; 224 | }, 225 | 226 | /** 227 | * Adds a spring to this simulation. 228 | * 229 | * @returns {Object} - a handle for a spring. If you want to later remove 230 | * spring pass it to removeSpring() method. 231 | */ 232 | addSpring: function (body1, body2, springLength, springCoefficient) { 233 | if (!body1 || !body2) { 234 | throw new Error('Cannot add null spring to force simulator'); 235 | } 236 | 237 | if (typeof springLength !== 'number') { 238 | springLength = -1; // assume global configuration 239 | } 240 | 241 | var spring = new Spring(body1, body2, springLength, springCoefficient >= 0 ? springCoefficient : -1); 242 | springs.push(spring); 243 | 244 | // TODO: could mark simulator as dirty. 245 | return spring; 246 | }, 247 | 248 | /** 249 | * Returns amount of movement performed on last step() call 250 | */ 251 | getTotalMovement: function () { 252 | return totalMovement; 253 | }, 254 | 255 | /** 256 | * Removes spring from the system 257 | * 258 | * @param {Object} spring to remove. Spring is an object returned by addSpring 259 | * 260 | * @returns {Boolean} true if spring found and removed. falsy otherwise; 261 | */ 262 | removeSpring: function (spring) { 263 | if (!spring) { return; } 264 | var idx = springs.indexOf(spring); 265 | if (idx > -1) { 266 | springs.splice(idx, 1); 267 | return true; 268 | } 269 | }, 270 | 271 | getBestNewBodyPosition: function (neighbors) { 272 | return bounds.getBestNewPosition(neighbors); 273 | }, 274 | 275 | /** 276 | * Returns bounding box which covers all bodies 277 | */ 278 | getBBox: getBoundingBox, 279 | getBoundingBox: getBoundingBox, 280 | 281 | invalidateBBox: function () { 282 | console.warn('invalidateBBox() is deprecated, bounds always recomputed on `getBBox()` call'); 283 | }, 284 | 285 | // TODO: Move the force specific stuff to force 286 | gravity: function (value) { 287 | if (value !== undefined) { 288 | settings.gravity = value; 289 | quadTree.options({gravity: value}); 290 | return this; 291 | } else { 292 | return settings.gravity; 293 | } 294 | }, 295 | 296 | theta: function (value) { 297 | if (value !== undefined) { 298 | settings.theta = value; 299 | quadTree.options({theta: value}); 300 | return this; 301 | } else { 302 | return settings.theta; 303 | } 304 | }, 305 | 306 | /** 307 | * Returns pseudo-random number generator instance. 308 | */ 309 | random: random 310 | }; 311 | 312 | // allow settings modification via public API: 313 | expose(settings, publicApi); 314 | 315 | eventify(publicApi); 316 | 317 | return publicApi; 318 | 319 | function getBoundingBox() { 320 | bounds.update(); 321 | return bounds.box; 322 | } 323 | 324 | function addForce(forceName, forceFunction) { 325 | if (forceMap.has(forceName)) throw new Error('Force ' + forceName + ' is already added'); 326 | 327 | forceMap.set(forceName, forceFunction); 328 | forces.push(forceFunction); 329 | } 330 | 331 | function removeForce(forceName) { 332 | var forceIndex = forces.indexOf(forceMap.get(forceName)); 333 | if (forceIndex < 0) return; 334 | forces.splice(forceIndex, 1); 335 | forceMap.delete(forceName); 336 | } 337 | 338 | function getForces() { 339 | // TODO: Should I trust them or clone the forces? 340 | return forceMap; 341 | } 342 | 343 | function nbodyForce(/* iterationUmber */) { 344 | if (bodies.length === 0) return; 345 | 346 | quadTree.insertBodies(bodies); 347 | var i = bodies.length; 348 | while (i--) { 349 | var body = bodies[i]; 350 | if (!body.isPinned) { 351 | body.reset(); 352 | quadTree.updateBodyForce(body); 353 | dragForce.update(body); 354 | } 355 | } 356 | } 357 | 358 | function updateSpringForce() { 359 | var i = springs.length; 360 | while (i--) { 361 | springForce.update(springs[i]); 362 | } 363 | } 364 | 365 | } 366 | 367 | function expose(settings, target) { 368 | for (var key in settings) { 369 | augment(settings, target, key); 370 | } 371 | } 372 | 373 | function augment(source, target, key) { 374 | if (!source.hasOwnProperty(key)) return; 375 | if (typeof target[key] === 'function') { 376 | // this accessor is already defined. Ignore it 377 | return; 378 | } 379 | var sourceIsNumber = Number.isFinite(source[key]); 380 | 381 | if (sourceIsNumber) { 382 | target[key] = function (value) { 383 | if (value !== undefined) { 384 | if (!Number.isFinite(value)) throw new Error('Value of ' + key + ' should be a valid number.'); 385 | source[key] = value; 386 | return target; 387 | } 388 | return source[key]; 389 | }; 390 | } else { 391 | target[key] = function (value) { 392 | if (value !== undefined) { 393 | source[key] = value; 394 | return target; 395 | } 396 | return source[key]; 397 | }; 398 | } 399 | } 400 | -------------------------------------------------------------------------------- /lib/kdForce.js: -------------------------------------------------------------------------------- 1 | /** 2 | * This is not used anywhere, but it is a good exploration into kd-tree based 3 | * simulation. 4 | */ 5 | module.exports = createKDForce; 6 | 7 | function createKDForce(bodies, settings) { 8 | var KDBush = require('kdbush').default; 9 | var random = require('ngraph.random').random(1984); 10 | 11 | return kdForce; 12 | 13 | function kdForce(iterationNumber) { 14 | if (iterationNumber < 500) return; 15 | var gravity = settings.gravity; 16 | var points = new KDBush(bodies, p => p.pos.x0, p => p.pos.x1); 17 | var i = bodies.length; 18 | while (i--) { 19 | var body = bodies[i]; 20 | body.reset(); 21 | var neighbors = points.within(body.pos.x0, body.pos.x1, settings.springLength); 22 | var fx = 0, fy = 0; 23 | for (var j = 0; j < neighbors.length; ++j) { 24 | var other = bodies[neighbors[j]]; 25 | if (other === body) continue; 26 | 27 | var dx = other.pos.x0 - body.pos.x0; 28 | var dy = other.pos.x1 - body.pos.x1; 29 | var r = Math.sqrt(dx * dx + dy * dy); 30 | if (r === 0) { 31 | // Poor man's protection against zero distance. 32 | dx = (random.nextDouble() - 0.5) / 50; 33 | dy = (random.nextDouble() - 0.5) / 50; 34 | r = Math.sqrt(dx * dx + dy * dy); 35 | } 36 | var v = gravity * other.mass * body.mass / (r * r * r); 37 | fx += v * dx; 38 | fy += v * dy; 39 | } 40 | body.force.x0 = fx; 41 | body.force.x1 = fy; 42 | //dragForce.update(body); 43 | } 44 | } 45 | } -------------------------------------------------------------------------------- /lib/spring.js: -------------------------------------------------------------------------------- 1 | module.exports = Spring; 2 | 3 | /** 4 | * Represents a physical spring. Spring connects two bodies, has rest length 5 | * stiffness coefficient and optional weight 6 | */ 7 | function Spring(fromBody, toBody, length, springCoefficient) { 8 | this.from = fromBody; 9 | this.to = toBody; 10 | this.length = length; 11 | this.coefficient = springCoefficient; 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ngraph.forcelayout", 3 | "version": "3.3.1", 4 | "description": "Force directed graph drawing layout", 5 | "main": "index.js", 6 | "jsdelivr": "dist/ngraph.forcelayout.min.js", 7 | "unpkg": "dist/ngraph.forcelayout.min.js", 8 | "types": "./index.d.ts", 9 | "typesVersions": { 10 | "<=4.3": { 11 | "index.d.ts": [ 12 | "index.v43.d.ts" 13 | ] 14 | } 15 | }, 16 | "scripts": { 17 | "test": "tap --branches=80 --lines=80 --statements=80 --functions=80 test/*.js", 18 | "lint": "eslint .", 19 | "perf": "npm version && node perf/test.js", 20 | "build": "browserify index.js -s ngraphCreateLayout -o dist/ngraph.forcelayout.js && terser --compress -o dist/ngraph.forcelayout.min.js -- dist/ngraph.forcelayout.js", 21 | "build2d": "browserify index.js -t ./inline-transform.js -s ngraphCreate2dLayout > dist/ngraph.forcelayout2d.js && terser --compress -o dist/ngraph.forcelayout2d.min.js -- dist/ngraph.forcelayout2d.js" 22 | }, 23 | "repository": { 24 | "type": "git", 25 | "url": "https://github.com/anvaka/ngraph.forcelayout.git" 26 | }, 27 | "keywords": [ 28 | "ngraph", 29 | "ngraphjs" 30 | ], 31 | "author": "Andrei Kashcha", 32 | "license": "BSD-3-Clause", 33 | "bugs": { 34 | "url": "https://github.com/anvaka/ngraph.forcelayout/issues" 35 | }, 36 | "devDependencies": { 37 | "benchmark": "~1.0.0", 38 | "browserify": "^17.0.0", 39 | "eslint": "^7.12.1", 40 | "ngraph.generators": "^20.0.0", 41 | "ngraph.graph": "^20.0.0", 42 | "tap": "^16.3.0", 43 | "terser": "^5.3.0", 44 | "through2": "^4.0.2" 45 | }, 46 | "dependencies": { 47 | "ngraph.events": "^1.0.0", 48 | "ngraph.merge": "^1.0.0", 49 | "ngraph.random": "^1.0.0" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /perf/experimental/README.md: -------------------------------------------------------------------------------- 1 | This folder contains my experiments with performance, trying to figure out the 2 | most efficient way of performing simulation -------------------------------------------------------------------------------- /perf/experimental/staticOrDynamic.js: -------------------------------------------------------------------------------- 1 | // Comparing the difference of compiled adhoc vs compiled in runtime function execution 2 | // This should let build functions specific to dimension, without affecting performance. 3 | 4 | var Benchmark = require('benchmark'); 5 | var suite = new Benchmark.Suite; 6 | let bodies; 7 | let total = 0; 8 | 9 | let srcCode = staticCompiled.toString().split('\n').slice(1, -1).join('\n'); 10 | let dynamicCompiled = compile(srcCode); 11 | resetBodies(); 12 | 13 | suite.add('static compiled', function() { 14 | for (var i = 0; i < 2000; ++i) { 15 | if (staticCompiled(bodies, 0.5, 0) > 0) { 16 | total = 1; 17 | } 18 | } 19 | }) 20 | .add('dynamic pre-compiled', function() { 21 | for (var i = 0; i < 2000; ++i) { 22 | if (dynamicCompiled(bodies, 0.5, 0) > 0) { 23 | total = 1; 24 | } 25 | } 26 | }) 27 | .add('dynamic ad-hoc pre-compiled', function() { 28 | let fn = compile(srcCode); 29 | for (var i = 0; i < 2000; ++i) { 30 | if (fn(bodies, 0.5, 0) > 0) { 31 | total = 1; 32 | } 33 | } 34 | }) 35 | .on('cycle', function(event) { 36 | console.log(String(event.target), total); 37 | total = 0; 38 | resetBodies(); 39 | }) 40 | .on('complete', function() { 41 | console.log('Fastest is ' + this.filter('fastest').pluck('name')); 42 | }) 43 | // run async 44 | .run({ 'async': true }); 45 | 46 | function resetBodies() { 47 | bodies = []; 48 | for (let i = 0; i < 100; ++i) { 49 | bodies.push(createBody(i)); 50 | } 51 | } 52 | 53 | function createBody(i) { 54 | return { 55 | springCount: 0, 56 | springLength: 10, 57 | mass: 1, 58 | force: {x: i, y: i}, 59 | velocity: {x: 0, y: 0}, 60 | pos: {x: i, y: i} 61 | }; 62 | } 63 | 64 | function staticCompiled(bodyCollection, timeStep, adaptiveTimeStepWeight) { 65 | var dx = 0, tx = 0, 66 | dy = 0, ty = 0, 67 | i, 68 | max = bodyCollection.length; 69 | 70 | if (max === 0) { 71 | return 0; 72 | } 73 | 74 | for (i = 0; i < max; ++i) { 75 | var body = bodyCollection[i]; 76 | if (adaptiveTimeStepWeight && body.springCount) { 77 | timeStep = (adaptiveTimeStepWeight * body.springLength/body.springCount); 78 | } 79 | 80 | var coefficient = timeStep / body.mass; 81 | 82 | body.velocity.x += coefficient * body.force.x; 83 | body.velocity.y += coefficient * body.force.y; 84 | var vx = body.velocity.x, 85 | vy = body.velocity.y, 86 | v = Math.sqrt(vx * vx + vy * vy); 87 | 88 | if (v > 1) { 89 | // We normalize it so that we move within timeStep range. 90 | // for the case when v <= 1 - we let velocity to fade out. 91 | body.velocity.x = vx / v; 92 | body.velocity.y = vy / v; 93 | } 94 | 95 | dx = timeStep * body.velocity.x; 96 | dy = timeStep * body.velocity.y; 97 | 98 | body.pos.x += dx; 99 | body.pos.y += dy; 100 | 101 | tx += Math.abs(dx); ty += Math.abs(dy); 102 | } 103 | 104 | return (tx * tx + ty * ty)/max; 105 | } 106 | 107 | function compile(body) { 108 | return new Function('bodies', 'timeStep', 'adaptiveTimeStepWeight', body); 109 | 110 | } -------------------------------------------------------------------------------- /perf/perfresults.txt: -------------------------------------------------------------------------------- 1 | > npm version && node perf/test.js 2 | 3 | { 4 | 'ngraph.forcelayout': '1.0.0', 5 | npm: '6.13.1', 6 | ares: '1.15.0', 7 | brotli: '1.0.7', 8 | cldr: '35.1', 9 | icu: '64.2', 10 | llhttp: '1.1.4', 11 | modules: '79', 12 | napi: '5', 13 | nghttp2: '1.40.0', 14 | node: '13.2.0', 15 | openssl: '1.1.1d', 16 | tz: '2019c', 17 | unicode: '12.1', 18 | uv: '1.33.1', 19 | v8: '7.9.317.23-node.20', 20 | zlib: '1.2.11' 21 | } 22 | Run default x 58.06 ops/sec ±2.20% (61 runs sampled) 23 | Fastest is Run default 24 | -------------------------------------------------------------------------------- /perf/test.js: -------------------------------------------------------------------------------- 1 | var graph = require('ngraph.generators').grid(20, 20); 2 | 3 | var Benchmark = require('benchmark'); 4 | var suite = new Benchmark.Suite; 5 | 6 | // add tests 7 | suite.add('Run default', function() { 8 | var layout = require('../')(graph); 9 | for (var i = 0; i < 20; ++i) { 10 | layout.step(); 11 | } 12 | }) 13 | .on('cycle', function(event) { 14 | console.log(String(event.target)); 15 | }) 16 | .on('complete', function() { 17 | console.log('Fastest is ' + this.filter('fastest').pluck('name')); 18 | }) 19 | // run async 20 | .run({ 'async': true }); 21 | -------------------------------------------------------------------------------- /stress.sh: -------------------------------------------------------------------------------- 1 | node perf/test.js 2 | node perf/test.js 3 | node perf/test.js 4 | node perf/test.js 5 | node perf/test.js 6 | node perf/test.js 7 | node perf/test.js 8 | node perf/test.js 9 | node perf/test.js 10 | node perf/test.js 11 | -------------------------------------------------------------------------------- /test/createBody.js: -------------------------------------------------------------------------------- 1 | 2 | var test = require('tap').test; 3 | var dimensions = 2; 4 | 5 | test('can debug setters', function (t) { 6 | var Body = require('../lib/codeGenerators/generateCreateBody')(dimensions, true); 7 | let b = new Body(); 8 | t.throws(() => b.pos.x = 'foo', /Cannot set non-numbers to x/); 9 | t.end(); 10 | }); -------------------------------------------------------------------------------- /test/dragForce.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var dimensions = 2; 3 | var createDragForce = require('../lib/codeGenerators/generateCreateDragForce')(dimensions); 4 | var Body = require('../lib/codeGenerators/generateCreateBody')(dimensions); 5 | 6 | test('reduces force value', function (t) { 7 | var body = new Body(); 8 | body.force.x = 1; body.force.y = 1; 9 | body.velocity.x = 1; body.velocity.y = 1; 10 | 11 | var dragForce = createDragForce({ dragCoefficient: 0.1 }); 12 | dragForce.update(body); 13 | 14 | t.ok(body.force.x < 1 && body.force.y < 1, 'Force value is reduced'); 15 | t.end(); 16 | }); 17 | 18 | test('Initialized with default value', function (t) { 19 | t.throws(() => createDragForce()); 20 | t.end(); 21 | }); -------------------------------------------------------------------------------- /test/eulerIntegrator.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var dimensions = 2; 3 | var Body = require('../lib/codeGenerators/generateCreateBody')(dimensions); 4 | var integrate = require('../lib/codeGenerators/generateIntegrator')(dimensions); 5 | 6 | test('Body preserves velocity without forces', function (t) { 7 | var body = new Body(); 8 | var timeStep = 1; 9 | body.mass = 1; body.velocity.x = 1; 10 | 11 | integrate([body], timeStep); 12 | t.equal(body.pos.x, 1, 'Should move by 1 pixel on first iteration'); 13 | 14 | timeStep = 2; // let's increase time step: 15 | integrate([body], timeStep); 16 | t.equal(body.pos.x, 3, 'Should move by 2 pixel on second iteration'); 17 | t.end(); 18 | }); 19 | 20 | test('Body gains velocity under force', function (t) { 21 | var body = new Body(); 22 | var timeStep = 1; 23 | body.mass = 1; body.force.x = 0.1; 24 | 25 | // F = m * a; 26 | // since mass = 1 => F = a = y'; 27 | integrate([body], timeStep); 28 | t.equal(body.velocity.x, 0.1, 'Should increase velocity'); 29 | 30 | integrate([body], timeStep); 31 | t.equal(body.velocity.x, 0.2, 'Should increase velocity'); 32 | // floating point math: 33 | t.ok(0.29 < body.pos.x && body.pos.x < 0.31, 'Position should be at 0.3 now'); 34 | 35 | t.end(); 36 | }); 37 | 38 | test('No bodies yield 0 movement', function (t) { 39 | var movement = integrate([], 2); 40 | t.equal(movement, 0, 'Nothing has moved'); 41 | t.end(); 42 | }); 43 | 44 | test('Body does not move faster than 1px', function (t) { 45 | var body = new Body(); 46 | var timeStep = 1; 47 | body.mass = 1; body.force.x = 2; 48 | 49 | integrate([body], timeStep); 50 | t.ok(body.velocity.x <= 1, 'Velocity should be within speed limit'); 51 | 52 | integrate([body], timeStep); 53 | t.ok(body.velocity.x <= 1, 'Velocity should be within speed limit'); 54 | 55 | t.end(); 56 | }); 57 | 58 | test('Can get total system movement', function (t) { 59 | var body = new Body(); 60 | var timeStep = 1; 61 | body.mass = 1; body.velocity.x = 0.2; 62 | 63 | var movement = integrate([body], timeStep); 64 | // to improve performance, integrator does not take square root, thus 65 | // total movement is .2 * .2 = 0.04; 66 | t.ok(0.04 <= movement && movement <= 0.041, 'System should travel by 0.2 pixels'); 67 | t.end(); 68 | }); 69 | -------------------------------------------------------------------------------- /test/insert.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | 3 | var dimensions = 2; 4 | var createQuadTree = require('../lib/codeGenerators/generateQuadTree')(dimensions); 5 | var Body = require('../lib/codeGenerators/generateCreateBody')(dimensions); 6 | var random = require('ngraph.random').random(42); 7 | 8 | test('insert and update update forces', function (t) { 9 | var tree = createQuadTree({}, random); 10 | var body = new Body(); 11 | var clone = JSON.parse(JSON.stringify(body)); 12 | 13 | tree.insertBodies([body]); 14 | tree.updateBodyForce(body); 15 | t.same(body, clone, 'The body should not be changed - there are no forces acting on it'); 16 | t.end(); 17 | }); 18 | 19 | test('it can get root', function (t) { 20 | var tree = createQuadTree({}, random); 21 | var body = new Body(); 22 | 23 | tree.insertBodies([body]); 24 | var root = tree.getRoot(); 25 | t.ok(root, 'Root is present'); 26 | t.equal(root.body, body, 'Body is initialized'); 27 | t.end(); 28 | }); 29 | 30 | test('Two bodies repel each other', function (t) { 31 | var tree = createQuadTree({}, random); 32 | var bodyA = new Body(); bodyA.pos.x = 1; bodyA.pos.y = 0; 33 | var bodyB = new Body(); bodyB.pos.x = 2; bodyB.pos.y = 0; 34 | 35 | tree.insertBodies([bodyA, bodyB]); 36 | tree.updateBodyForce(bodyA); 37 | tree.updateBodyForce(bodyB); 38 | // based on our physical model construction forces should be equivalent, with 39 | // opposite sign: 40 | t.ok(bodyA.force.x + bodyB.force.x === 0, 'Forces should be same, with opposite sign'); 41 | t.ok(bodyA.force.x !== 0, 'X-force for body A should not be zero'); 42 | t.ok(bodyB.force.x !== 0, 'X-force for body B should not be zero'); 43 | // On the other hand, our bodies should not move by Y axis: 44 | t.ok(bodyA.force.y === 0, 'Y-force for body A should be zero'); 45 | t.ok(bodyB.force.y === 0, 'Y-force for body B should be zero'); 46 | 47 | t.end(); 48 | }); 49 | 50 | test('Can handle two bodies at the same location', function (t) { 51 | var tree = createQuadTree({}, random); 52 | var bodyA = new Body(); 53 | var bodyB = new Body(); 54 | 55 | tree.insertBodies([bodyA, bodyB]); 56 | tree.updateBodyForce(bodyA); 57 | tree.updateBodyForce(bodyB); 58 | 59 | t.end(); 60 | }); 61 | 62 | test('it does not stuck', function(t) { 63 | var count = 60000; 64 | var bodies = []; 65 | 66 | for (var i = 0; i < count; ++i) { 67 | bodies.push(new Body(Math.random(), Math.random())); 68 | } 69 | 70 | var quadTree = createQuadTree({}, random); 71 | quadTree.insertBodies(bodies); 72 | 73 | bodies.forEach(function(body) { 74 | quadTree.updateBodyForce(body); 75 | }); 76 | t.ok(1); 77 | t.end(); 78 | }); 79 | -------------------------------------------------------------------------------- /test/layout.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow */ 2 | var test = require('tap').test, 3 | createGraph = require('ngraph.graph'), 4 | createLayout = require('..'); 5 | 6 | test('it exposes simulator', function(t) { 7 | t.ok(typeof createLayout.simulator === 'function', 'Simulator is exposed'); 8 | t.end(); 9 | }); 10 | 11 | test('it returns spring', function(t) { 12 | var g = createGraph(); 13 | var layout = createLayout(g); 14 | 15 | var link = g.addLink(1, 2); 16 | 17 | var springForLink = layout.getSpring(link); 18 | var springForLinkId = layout.getSpring(link.id); 19 | var springForFromTo = layout.getSpring(1, 2); 20 | 21 | t.ok(springForLink, 'spring is here'); 22 | t.ok(springForLinkId === springForLink, 'Spring is the same'); 23 | t.ok(springForFromTo === springForLink, 'Spring is the same'); 24 | t.end(); 25 | }); 26 | 27 | test('it returns same position', function(t) { 28 | var g = createGraph(); 29 | var layout = createLayout(g); 30 | 31 | g.addLink(1, 2); 32 | 33 | var firstNodePos = layout.getNodePosition(1); 34 | layout.step(); 35 | t.ok(firstNodePos === layout.getNodePosition(1), 'Position is the same object'); 36 | layout.step(); 37 | t.ok(firstNodePos === layout.getNodePosition(1), 'Position is the same object after multiple steps'); 38 | t.end(); 39 | }); 40 | 41 | test('it returns body', function(t) { 42 | var g = createGraph(); 43 | var layout = createLayout(g); 44 | 45 | g.addLink(1, 2); 46 | 47 | t.ok(layout.getBody(1), 'node 1 has body'); 48 | t.ok(layout.getBody(2), 'node 2 has body'); 49 | t.notOk(layout.getBody(4), 'there is no node 4'); 50 | 51 | var body = layout.getBody(1); 52 | t.ok(body.pos.x && body.pos.y, 'Body has a position'); 53 | t.ok(body.mass, 'Body has a mass'); 54 | 55 | t.end(); 56 | }); 57 | 58 | test('it can set node mass', function(t) { 59 | var g = createGraph(); 60 | g.addNode('anvaka'); 61 | 62 | var layout = createLayout(g, { 63 | nodeMass: function (nodeId) { 64 | t.equal(nodeId, 'anvaka', 'correct node is called'); 65 | return 84; // my mass in kilograms :P 66 | } 67 | }); 68 | 69 | var body = layout.getBody('anvaka'); 70 | t.equal(body.mass, 84, 'Mass is okay'); 71 | 72 | t.end(); 73 | }); 74 | 75 | test('does not tolerate bad input', function (t) { 76 | t.throws(missingGraph); 77 | t.throws(invalidNodeId); 78 | t.end(); 79 | 80 | function missingGraph() { 81 | // graph is missing: 82 | createLayout(); 83 | } 84 | 85 | function invalidNodeId() { 86 | var graph = createGraph(); 87 | var layout = createLayout(graph); 88 | 89 | // we don't have nodes in the graph. This should throw: 90 | layout.getNodePosition(1); 91 | } 92 | }); 93 | 94 | test('it fires stable on empty graph', function(t) { 95 | var graph = createGraph(); 96 | var layout = createLayout(graph); 97 | layout.on('stable', endTest); 98 | layout.step(); 99 | 100 | function endTest() { 101 | t.end(); 102 | } 103 | }); 104 | 105 | test('can add bodies which are standard prototype names', function (t) { 106 | var graph = createGraph(); 107 | graph.addLink('constructor', 'watch'); 108 | 109 | var layout = createLayout(graph); 110 | layout.step(); 111 | 112 | graph.forEachNode(function (node) { 113 | var pos = layout.getNodePosition(node.id); 114 | t.ok(pos && typeof pos.x === 'number' && 115 | typeof pos.y === 'number', 'Position is defined'); 116 | }); 117 | 118 | t.end(); 119 | }); 120 | 121 | test('it can step when no links present', function (t) { 122 | var graph = createGraph(); 123 | graph.addNode('constructor'); 124 | graph.addNode('watch'); 125 | 126 | var layout = createLayout(graph); 127 | layout.step(); 128 | 129 | graph.forEachNode(function (node) { 130 | var pos = layout.getNodePosition(node.id); 131 | t.ok(pos && typeof pos.x === 'number' && 132 | typeof pos.y === 'number', 'Position is defined'); 133 | }); 134 | 135 | t.end(); 136 | }); 137 | 138 | test('layout initializes nodes positions', function (t) { 139 | var graph = createGraph(); 140 | graph.addLink(1, 2); 141 | 142 | var layout = createLayout(graph); 143 | 144 | // perform one iteration of layout: 145 | layout.step(); 146 | 147 | graph.forEachNode(function (node) { 148 | var pos = layout.getNodePosition(node.id); 149 | t.ok(pos && typeof pos.x === 'number' && 150 | typeof pos.y === 'number', 'Position is defined'); 151 | }); 152 | 153 | graph.forEachLink(function (link) { 154 | var linkPos = layout.getLinkPosition(link.id); 155 | t.ok(linkPos && linkPos.from && linkPos.to, 'Link position is defined'); 156 | var fromPos = layout.getNodePosition(link.fromId); 157 | t.ok(linkPos.from === fromPos, '"From" should be identical to getNodePosition'); 158 | var toPos = layout.getNodePosition(link.toId); 159 | t.ok(linkPos.to === toPos, '"To" should be identical to getNodePosition'); 160 | }); 161 | 162 | t.end(); 163 | }); 164 | 165 | test('Layout can set node position', function (t) { 166 | var graph = createGraph(); 167 | graph.addLink(1, 2); 168 | 169 | var layout = createLayout(graph); 170 | 171 | layout.pinNode(graph.getNode(1), true); 172 | layout.setNodePosition(1, 42, 42); 173 | 174 | // perform one iteration of layout: 175 | layout.step(); 176 | 177 | // and make sure node 1 was not moved: 178 | var actualPosition = layout.getNodePosition(1); 179 | t.equal(actualPosition.x, 42, 'X has not changed'); 180 | t.equal(actualPosition.y, 42, 'Y has not changed'); 181 | 182 | t.end(); 183 | }); 184 | 185 | test('Layout updates bounding box when it sets node position', function (t) { 186 | var graph = createGraph(); 187 | graph.addLink(1, 2); 188 | 189 | var layout = createLayout(graph); 190 | layout.setNodePosition(1, 42, 42); 191 | layout.setNodePosition(2, 40, 40); 192 | var rect = layout.getGraphRect(); 193 | t.ok(rect.max_x <= 42); t.ok(rect.max_y <= 42); 194 | t.ok(rect.min_x >= 40); t.ok(rect.min_y >= 40); 195 | 196 | t.end(); 197 | }); 198 | 199 | test('layout initializes links', function (t) { 200 | var graph = createGraph(); 201 | var node1 = graph.addNode(1); node1.position = {x : -1000, y: 0}; 202 | var node2 = graph.addNode(2); node2.position = {x : 1000, y: 0}; 203 | 204 | graph.addLink(1, 2); 205 | 206 | var layout = createLayout(graph); 207 | 208 | // perform one iteration of layout: 209 | layout.step(); 210 | 211 | // since both nodes are connected by spring and distance is too large between 212 | // them, they should start attracting each other 213 | var pos1 = layout.getNodePosition(1); 214 | var pos2 = layout.getNodePosition(2); 215 | 216 | t.ok(pos1.x > -1000, 'Node 1 moves towards node 2'); 217 | t.ok(pos2.x < 1000, 'Node 1 moves towards node 2'); 218 | 219 | t.end(); 220 | }); 221 | 222 | test('layout respects proposed original position', function (t) { 223 | var graph = createGraph(); 224 | var node = graph.addNode(1); 225 | 226 | var initialPosition = {x: 100, y: 100}; 227 | node.position = copy(initialPosition); 228 | 229 | var layout = createLayout(graph); 230 | layout.step(); 231 | 232 | t.same(layout.getNodePosition(node.id), initialPosition, 'original position preserved'); 233 | 234 | t.end(); 235 | }); 236 | 237 | test('layout has defined graph rectangle', function (t) { 238 | t.test('empty graph', function (t) { 239 | var graph = createGraph(); 240 | var layout = createLayout(graph); 241 | 242 | var rect = layout.getGraphRect(); 243 | var expectedProperties = ['min_x', 'min_y', 'max_x', 'max_y']; 244 | t.ok(rect && expectedProperties.reduce(hasProperties, true), 'Values are present before step()'); 245 | 246 | layout.step(); 247 | 248 | t.ok(rect && expectedProperties.reduce(hasProperties, true), 'Values are present after step()'); 249 | t.end(); 250 | 251 | function hasProperties(result, key) { 252 | return result && typeof rect[key] === 'number'; 253 | } 254 | }); 255 | 256 | t.test('two nodes', function (t) { 257 | var graph = createGraph(); 258 | graph.addLink(1, 2); 259 | var layout = createLayout(graph); 260 | layout.step(); 261 | 262 | var rect = layout.getGraphRect(); 263 | t.ok(!rectangleIsEmpty(rect), 'Graph rectangle is not empty'); 264 | 265 | t.end(); 266 | }); 267 | 268 | t.end(); 269 | }); 270 | 271 | test('it does not move pinned nodes', function (t) { 272 | t.test('respects original data.isPinned attribute', function (t) { 273 | var graph = createGraph(); 274 | var testNode = graph.addNode(1, { isPinned: true }); 275 | var layout = createLayout(graph); 276 | t.ok(layout.isNodePinned(testNode), 'Node is pinned'); 277 | t.end(); 278 | }); 279 | 280 | t.test('respects node.isPinned attribute', function (t) { 281 | var graph = createGraph(); 282 | var testNode = graph.addNode(1); 283 | 284 | // this was possible in vivagraph. Port it over to ngraph: 285 | testNode.isPinned = true; 286 | var layout = createLayout(graph); 287 | t.ok(layout.isNodePinned(testNode), 'Node is pinned'); 288 | t.end(); 289 | }); 290 | 291 | t.test('can pin nodes after graph is initialized', function (t) { 292 | var graph = createGraph(); 293 | graph.addLink(1, 2); 294 | 295 | var layout = createLayout(graph); 296 | layout.pinNode(graph.getNode(1), true); 297 | layout.step(); 298 | var pos1 = copy(layout.getNodePosition(1)); 299 | var pos2 = copy(layout.getNodePosition(2)); 300 | 301 | // make one more step and make sure node 1 did not move: 302 | layout.step(); 303 | 304 | t.ok(!positionChanged(pos1, layout.getNodePosition(1)), 'Node 1 was not moved'); 305 | t.ok(positionChanged(pos2, layout.getNodePosition(2)), 'Node 2 has moved'); 306 | 307 | t.end(); 308 | }); 309 | 310 | t.end(); 311 | }); 312 | 313 | test('it listens to graph events', function (t) { 314 | // we first initialize with empty graph: 315 | var graph = createGraph(); 316 | var layout = createLayout(graph); 317 | 318 | // and only then add nodes: 319 | graph.addLink(1, 2); 320 | 321 | // make two iterations 322 | layout.step(); 323 | var pos1 = copy(layout.getNodePosition(1)); 324 | var pos2 = copy(layout.getNodePosition(2)); 325 | 326 | layout.step(); 327 | 328 | t.ok(positionChanged(pos1, layout.getNodePosition(1)), 'Node 1 has moved'); 329 | t.ok(positionChanged(pos2, layout.getNodePosition(2)), 'Node 2 has moved'); 330 | 331 | t.end(); 332 | }); 333 | 334 | test('can stop listen to events', function (t) { 335 | // we first initialize with empty graph: 336 | var graph = createGraph(); 337 | var layout = createLayout(graph); 338 | layout.dispose(); 339 | 340 | graph.addLink(1, 2); 341 | layout.step(); 342 | t.ok(layout.simulator.bodies.length === 0, 'No bodies in the simulator'); 343 | 344 | t.end(); 345 | }); 346 | 347 | test('physics simulator', function (t) { 348 | t.test('has default simulator', function (t) { 349 | var graph = createGraph(); 350 | var layout = createLayout(graph); 351 | 352 | t.ok(layout.simulator, 'physics simulator is present'); 353 | t.end(); 354 | }); 355 | 356 | t.test('can override default settings', function (t) { 357 | var graph = createGraph(); 358 | var layout = createLayout(graph, { 359 | theta: 1.5 360 | }); 361 | t.equal(layout.simulator.theta(), 1.5, 'Simulator settings are overridden'); 362 | t.end(); 363 | }); 364 | 365 | t.end(); 366 | }); 367 | 368 | test('it removes removed nodes', function (t) { 369 | var graph = createGraph(); 370 | var layout = createLayout(graph); 371 | graph.addLink(1, 2); 372 | 373 | layout.step(); 374 | graph.clear(); 375 | 376 | // since we removed everything from graph rect should be empty: 377 | var rect = layout.getGraphRect(); 378 | 379 | t.ok(rectangleIsEmpty(rect), 'Graph rect is empty'); 380 | t.end(); 381 | }); 382 | 383 | test('it can iterate over bodies', function(t) { 384 | var graph = createGraph(); 385 | var layout = createLayout(graph); 386 | graph.addLink(1, 2); 387 | var calledCount = 0; 388 | 389 | layout.forEachBody(function(body, bodyId) { 390 | t.ok(body.pos, bodyId + ' has position'); 391 | t.ok(graph.getNode(bodyId), bodyId + ' matches a graph node'); 392 | calledCount += 1; 393 | }); 394 | 395 | t.equal(calledCount, 2, 'Both bodies are visited'); 396 | t.end(); 397 | }); 398 | 399 | test('it handles large graphs', function (t) { 400 | var graph = createGraph(); 401 | var layout = createLayout(graph); 402 | 403 | var count = 60000; 404 | 405 | var i = count; 406 | while (i--) { 407 | graph.addNode(i); 408 | } 409 | 410 | // link each node to 2 other random nodes 411 | i = count; 412 | while (i--) { 413 | graph.addLink(i, Math.ceil(Math.random() * count)); 414 | graph.addLink(i, Math.ceil(Math.random() * count)); 415 | } 416 | 417 | layout.step(); 418 | 419 | t.ok(layout.simulator.bodies.length !== 0, 'Bodies in the simulator'); 420 | t.end(); 421 | }); 422 | 423 | test('it can create high dimensional layout', function(t) { 424 | var graph = createGraph(); 425 | graph.addLink(1, 2); 426 | var layout = createLayout(graph, {dimensions: 6}); 427 | layout.step(); 428 | 429 | var pos = layout.getNodePosition(1); 430 | t.ok(pos.x !== undefined, 'Position has x'); 431 | t.ok(pos.y !== undefined, 'Position has y'); 432 | t.ok(pos.z !== undefined, 'Position has z'); 433 | t.ok(pos.c4 !== undefined, 'Position has c4'); 434 | t.ok(pos.c5 !== undefined, 'Position has c5'); 435 | t.ok(pos.c6 !== undefined, 'Position has c6'); 436 | t.end(); 437 | }); 438 | 439 | test('it can layout two graphs independently', function(t) { 440 | var graph1 = createGraph(); 441 | var graph2 = createGraph(); 442 | var layout1 = createLayout(graph1); 443 | var layout2 = createLayout(graph2); 444 | graph1.addLink(1, 2); 445 | graph2.addLink(1, 2); 446 | layout1.step(); 447 | layout2.step(); 448 | layout2.step(); 449 | t.ok(layout1.getNodePosition(1).x !== layout2.getNodePosition(1).x, 'Positions are different'); 450 | t.end(); 451 | }); 452 | 453 | function positionChanged(pos1, pos2) { 454 | return (pos1.x !== pos2.x) || (pos1.y !== pos2.y); 455 | } 456 | 457 | function copy(obj) { 458 | return JSON.parse(JSON.stringify(obj)); 459 | } 460 | 461 | function rectangleIsEmpty(rect) { 462 | return rect.min_x === 0 && rect.min_y === 0 && rect.max_x === 0 && rect.max_y === 0; 463 | } 464 | -------------------------------------------------------------------------------- /test/primitives.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var {generateCreateBodyFunctionBody} = require('../lib/codeGenerators/generateCreateBody'); 3 | 4 | function primitive(dimension) { 5 | let res = (new Function(generateCreateBodyFunctionBody(dimension)))(); 6 | return res; 7 | } 8 | 9 | test('Body has properties force, pos and mass', function(t) { 10 | debugger; 11 | var body = new (primitive(2).Body)(); 12 | t.ok(body.force, 'Force attribute is missing on body'); 13 | t.ok(body.pos, 'Pos attribute is missing on body'); 14 | t.ok(body.velocity, 'Velocity attribute is missing on body'); 15 | t.ok(typeof body.mass === 'number' && body.mass !== 0, 'Body should have a mass'); 16 | t.end(); 17 | }); 18 | 19 | test('Vector has x and y', function(t) { 20 | var vector = new (primitive(2).Vector)(); 21 | t.ok(typeof vector.x === 'number', 'Vector has x coordinates'); 22 | t.ok(typeof vector.y === 'number', 'Vector has y coordinates'); 23 | 24 | var initialized = new (primitive(2).Vector)(1, 2); 25 | t.equal(initialized.x, 1, 'Vector initialized properly'); 26 | t.equal(initialized.y, 2, 'Vector initialized properly'); 27 | 28 | var badInput = new (primitive(2).Vector)('hello world'); 29 | t.equal(badInput.x, 0, 'Vector should be resilient to bed input'); 30 | t.equal(badInput.y, 0, 'Vector should be resilient to bed input'); 31 | t.end(); 32 | }); 33 | 34 | test('Body3d has properties force, pos and mass', function(t) { 35 | var body = new (primitive(3).Body)(); 36 | t.ok(body.force, 'Force attribute is missing on body'); 37 | t.ok(body.pos, 'Pos attribute is missing on body'); 38 | t.ok(body.velocity, 'Velocity attribute is missing on body'); 39 | t.ok(typeof body.mass === 'number' && body.mass !== 0, 'Body should have a mass'); 40 | t.end(); 41 | }); 42 | 43 | test('Vector3d has x and y and z', function(t) { 44 | var vector = new (primitive(3).Vector)(); 45 | t.ok(typeof vector.x === 'number', 'Vector has x coordinates'); 46 | t.ok(typeof vector.y === 'number', 'Vector has y coordinates'); 47 | t.ok(typeof vector.z === 'number', 'Vector has z coordinates'); 48 | 49 | var initialized = new (primitive(3).Vector)(1, 2, 3); 50 | t.equal(initialized.x, 1, 'Vector initialized properly'); 51 | t.equal(initialized.y, 2, 'Vector initialized properly'); 52 | t.equal(initialized.z, 3, 'Vector initialized properly'); 53 | 54 | var badInput = new (primitive(3).Vector)('hello world'); 55 | t.equal(badInput.x, 0, 'Vector should be resilient to bed input'); 56 | t.equal(badInput.y, 0, 'Vector should be resilient to bed input'); 57 | t.equal(badInput.z, 0, 'Vector should be resilient to bed input'); 58 | t.end(); 59 | }); 60 | 61 | test('reset vector', function(t) { 62 | var v3 = new (primitive(3).Vector)(1, 2, 3); 63 | v3.reset(); 64 | t.equal(v3.x, 0, 'Reset to 0'); 65 | t.equal(v3.y, 0, 'Reset to 0'); 66 | t.equal(v3.z, 0, 'Reset to 0'); 67 | var v2 = new (primitive(2).Vector)(1, 2); 68 | v2.reset(); 69 | t.equal(v2.x, 0, 'Reset to 0'); 70 | t.equal(v2.y, 0, 'Reset to 0'); 71 | t.end(); 72 | }); 73 | 74 | test('vector can use copy constructor', function(t) { 75 | var a = new (primitive(3).Vector)(1, 2, 3); 76 | var b = new (primitive(3).Vector)(a); 77 | t.equal(b.x, a.x, 'Value copied'); 78 | t.equal(b.y, a.y, 'Value copied'); 79 | t.equal(b.z, a.z, 'Value copied'); 80 | t.end(); 81 | }); 82 | 83 | test('Body3d can set position', function(t) { 84 | var body = new (primitive(3).Body)(); 85 | body.setPosition(10, 11, 12); 86 | t.equal(body.pos.x, 10, 'x is correct'); 87 | t.equal(body.pos.y, 11, 'y is correct'); 88 | t.equal(body.pos.z, 12, 'z is correct'); 89 | 90 | t.end(); 91 | }); 92 | 93 | test('Body can set position', function(t) { 94 | var body = new (primitive(2).Body)(); 95 | body.setPosition(10, 11); 96 | t.equal(body.pos.x, 10, 'x is correct'); 97 | t.equal(body.pos.y, 11, 'y is correct'); 98 | 99 | t.end(); 100 | }); 101 | -------------------------------------------------------------------------------- /test/simulator.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow */ 2 | var test = require('tap').test; 3 | var dimensions = 2; 4 | var Body = require('../lib/codeGenerators/generateCreateBody')(dimensions); 5 | var createSimulator = require('../lib/createPhysicsSimulator'); 6 | 7 | test('Can step without bodies', function (t) { 8 | var simulator = createSimulator(); 9 | t.equal(simulator.bodies.length, 0, 'There should be no bodies'); 10 | t.equal(simulator.springs.length, 0, 'There should be no springs'); 11 | simulator.step(); 12 | t.end(); 13 | }); 14 | 15 | test('it has settings exposed', function(t) { 16 | var mySettings = { }; 17 | var simulator = createSimulator(mySettings); 18 | t.ok(mySettings === simulator.settings, 'settings are exposed'); 19 | t.end(); 20 | }); 21 | 22 | test('it gives amount of total movement', function(t) { 23 | var simulator = createSimulator(); 24 | var body1 = new Body(-10, 0); 25 | var body2 = new Body(10, 0); 26 | simulator.addBody(body1); 27 | simulator.addBody(body2); 28 | simulator.step(); 29 | 30 | var totalMoved = simulator.getTotalMovement(); 31 | t.ok(!isNaN(totalMoved), 'Amount of total movement is returned'); 32 | t.end(); 33 | }); 34 | 35 | test('it can add a body at given position', function(t) { 36 | var simulator = createSimulator(); 37 | var pos1 = {x: -10, y: 0}; 38 | var pos2 = {x: 10, y: 0}; 39 | simulator.addBodyAt(pos1); 40 | simulator.addBodyAt(pos2); 41 | 42 | t.equal(simulator.bodies.length, 2, 'All bodies are added'); 43 | var body1 = simulator.bodies[0]; 44 | 45 | t.equal(body1.pos.x, -10, 'X is there'); 46 | t.equal(body1.pos.y, 0, 'Y is there'); 47 | 48 | var body2 = simulator.bodies[1]; 49 | t.equal(body2.pos.x, 10, 'X is there'); 50 | t.equal(body2.pos.y, 0, 'Y is there'); 51 | t.end(); 52 | }); 53 | 54 | test('Does not update position of one body', function (t) { 55 | var simulator = createSimulator(); 56 | var body = new Body(0, 0); 57 | simulator.addBody(body); 58 | 59 | simulator.step(1); 60 | t.equal(simulator.bodies.length, 1, 'Number of bodies is 1'); 61 | t.equal(simulator.springs.length, 0, 'Number of springs is 0'); 62 | t.equal(simulator.bodies[0], body, 'Body points to actual object'); 63 | t.equal(body.pos.x, 0, 'X is not changed'); 64 | t.equal(body.pos.y, 0, 'Y is not changed'); 65 | t.end(); 66 | }); 67 | 68 | test('throws on no body or no pos', t => { 69 | var simulator = createSimulator(); 70 | t.throws(() => simulator.addBody(), /Body is required/); 71 | t.throws(() => simulator.addBodyAt(), /Body position is required/); 72 | t.end(); 73 | }); 74 | 75 | test('throws on no spring', t => { 76 | var simulator = createSimulator(); 77 | t.throws(() => simulator.addSpring(), /Cannot add null spring to force simulator/); 78 | t.end(); 79 | }); 80 | 81 | test('Can add and remove forces', function (t) { 82 | var simulator = createSimulator(); 83 | var testForce = function () {}; 84 | simulator.addForce('foo', testForce); 85 | t.equal(simulator.getForces().get('foo'), testForce); 86 | 87 | simulator.removeForce('foo'); 88 | t.equal(simulator.getForces().get('foo'), undefined); 89 | 90 | simulator.removeForce('foo'); 91 | // should still be good 92 | t.end(); 93 | }); 94 | 95 | test('Can configure forces', function (t) { 96 | t.test('Gravity', function (t) { 97 | var simulator = createSimulator(); 98 | var body1 = new Body(0, 0); 99 | var body2 = new Body(1, 0); 100 | 101 | simulator.addBody(body1); 102 | simulator.addBody(body2); 103 | 104 | simulator.step(); 105 | // by default gravity is negative, bodies should repel each other: 106 | var x1 = body1.pos.x; 107 | var x2 = body2.pos.x; 108 | t.ok(x1 < 0, 'Body 1 moves away from body 2'); 109 | t.ok(x2 > 1, 'Body 2 moves away from body 1'); 110 | 111 | // now reverse gravity, and bodies should attract each other: 112 | simulator.gravity(100); 113 | simulator.step(); 114 | t.ok(body1.pos.x > x1, 'Body 1 moved towards body 2'); 115 | t.ok(body2.pos.x < x2, 'Body 2 moved towards body 1'); 116 | 117 | t.end(); 118 | }); 119 | 120 | t.test('Drag', function (t) { 121 | var simulator = createSimulator(); 122 | var body1 = new Body(0, 0); 123 | body1.velocity.x = -1; // give it small impulse 124 | simulator.addBody(body1); 125 | 126 | simulator.step(); 127 | 128 | var x1 = body1.velocity.x; 129 | // by default drag force will slow down entire system: 130 | t.ok(x1 > -1, 'Body 1 moves at reduced speed'); 131 | 132 | // Restore original velocity, but now set drag force to 0 133 | body1.velocity.x = -1; 134 | simulator.dragCoefficient(0); 135 | simulator.step(); 136 | t.ok(body1.velocity.x === -1, 'Velocity should remain unchanged'); 137 | t.end(); 138 | }); 139 | t.end(); 140 | }); 141 | 142 | test('Can remove bodies', function (t) { 143 | var simulator = createSimulator(); 144 | var body = new Body(0, 0); 145 | simulator.addBody(body); 146 | t.equal(simulator.bodies.length, 1, 'Number of bodies is 1'); 147 | var result = simulator.removeBody(body); 148 | t.equal(result, true, 'body successfully removed'); 149 | t.equal(simulator.bodies.length, 0, 'Number of bodies is 0'); 150 | t.end(); 151 | }); 152 | 153 | test('Updates position for two bodies', function (t) { 154 | var simulator = createSimulator(); 155 | var body1 = new Body(-1, 0); 156 | var body2 = new Body(1, 0); 157 | simulator.addBody(body1); 158 | simulator.addBody(body2); 159 | 160 | simulator.step(); 161 | t.equal(simulator.bodies.length, 2, 'Number of bodies is 2'); 162 | t.ok(body1.pos.x !== 0, 'Body1.X has changed'); 163 | t.ok(body2.pos.x !== 0, 'Body2.X has changed'); 164 | 165 | t.equal(body1.pos.y, 0, 'Body1.Y has not changed'); 166 | t.equal(body2.pos.y, 0, 'Body2.Y has not changed'); 167 | t.end(); 168 | }); 169 | 170 | test('add spring should not add bodies', function (t) { 171 | var simulator = createSimulator(); 172 | var body1 = new Body(-1, 0); 173 | var body2 = new Body(1, 0); 174 | 175 | simulator.addSpring(body1, body2, 10); 176 | 177 | t.equal(simulator.bodies.length, 0, 'Should not add two bodies'); 178 | t.equal(simulator.bodies.length, 0, 'Should not add two bodies'); 179 | t.equal(simulator.springs.length, 1, 'Should have a spring'); 180 | t.end(); 181 | }); 182 | 183 | test('Spring affects bodies positions', function (t) { 184 | var simulator = createSimulator(); 185 | var body1 = new Body(-10, 0); 186 | var body2 = new Body(10, 0); 187 | simulator.addBody(body1); 188 | simulator.addBody(body2); 189 | // If you take this out, bodies will repel each other: 190 | simulator.addSpring(body1, body2, 1); 191 | 192 | simulator.step(); 193 | 194 | t.ok(body1.pos.x > -10, 'Body 1 should move towards body 2'); 195 | t.ok(body2.pos.x < 10, 'Body 2 should move towards body 1'); 196 | 197 | t.end(); 198 | }); 199 | 200 | test('Can remove springs', function (t) { 201 | var simulator = createSimulator(); 202 | var body1 = new Body(-10, 0); 203 | var body2 = new Body(10, 0); 204 | simulator.addBody(body1); 205 | simulator.addBody(body2); 206 | var spring = simulator.addSpring(body1, body2, 1); 207 | simulator.removeSpring(spring); 208 | 209 | simulator.step(); 210 | 211 | t.ok(body1.pos.x < -10, 'Body 1 should move away from body 2'); 212 | t.ok(body2.pos.x > 10, 'Body 2 should move away from body 1'); 213 | 214 | t.end(); 215 | }); 216 | 217 | test('Get bounding box', function (t) { 218 | var simulator = createSimulator(); 219 | var body1 = new Body(0, 0); 220 | var body2 = new Body(10, 10); 221 | simulator.addBody(body1); 222 | simulator.addBody(body2); 223 | simulator.step(); // this will move bodies farther away 224 | var bbox = simulator.getBBox(); 225 | t.ok(bbox.min_x <= 0, 'Left is 0'); 226 | t.ok(bbox.min_y <= 0, 'Top is 0'); 227 | t.ok(bbox.max_x >= 10, 'right is 10'); 228 | t.ok(bbox.max_y >= 10, 'bottom is 10'); 229 | t.end(); 230 | }); 231 | 232 | test('it updates bounding box', function (t) { 233 | var simulator = createSimulator(); 234 | var body1 = new Body(0, 0); 235 | var body2 = new Body(10, 10); 236 | simulator.addBody(body1); 237 | simulator.addBody(body2); 238 | var bbox = simulator.getBBox(); 239 | 240 | t.ok(bbox.min_x === 0, 'Left is 0'); 241 | t.ok(bbox.min_y === 0, 'Top is 0'); 242 | t.ok(bbox.max_x === 10, 'right is 10'); 243 | t.ok(bbox.max_y === 10, 'bottom is 10'); 244 | 245 | body1.setPosition(15, 15); 246 | simulator.invalidateBBox(); 247 | bbox = simulator.getBBox(); 248 | 249 | t.ok(bbox.min_x === 10, 'Left is 10'); 250 | t.ok(bbox.min_y === 10, 'Top is 10'); 251 | t.ok(bbox.max_x === 15, 'right is 15'); 252 | t.ok(bbox.max_y === 15, 'bottom is 15'); 253 | t.end(); 254 | }); 255 | 256 | test('Get best position', function (t) { 257 | t.test('can get with empty simulator', function (t) { 258 | var simulator = createSimulator(); 259 | var empty = simulator.getBestNewBodyPosition([]); 260 | t.ok(typeof empty.x === 'number', 'Has X'); 261 | t.ok(typeof empty.y === 'number', 'Has Y'); 262 | 263 | t.end(); 264 | }); 265 | 266 | t.end(); 267 | }); 268 | 269 | test('it can change settings', function(t) { 270 | var simulator = createSimulator(); 271 | 272 | var currentTheta = simulator.theta(); 273 | t.ok(typeof currentTheta === 'number', 'theta is here'); 274 | simulator.theta(1.2); 275 | t.equal(simulator.theta(), 1.2, 'theta is changed'); 276 | 277 | var currentSpringCoefficient = simulator.springCoefficient(); 278 | t.ok(typeof currentSpringCoefficient === 'number', 'springCoefficient is here'); 279 | simulator.springCoefficient(0.8); 280 | t.equal(simulator.springCoefficient(), 0.8, 'springCoefficient is changed'); 281 | 282 | var gravity = simulator.gravity(); 283 | t.ok(typeof gravity === 'number', 'gravity is here'); 284 | simulator.gravity(-0.8); 285 | t.equal(simulator.gravity(), -0.8, 'gravity is changed'); 286 | 287 | var springLength = simulator.springLength(); 288 | t.ok(typeof springLength === 'number', 'springLength is here'); 289 | simulator.springLength(80); 290 | t.equal(simulator.springLength(), 80, 'springLength is changed'); 291 | 292 | var dragCoefficient = simulator.dragCoefficient(); 293 | t.ok(typeof dragCoefficient === 'number', 'dragCoefficient is here'); 294 | simulator.dragCoefficient(0.8); 295 | t.equal(simulator.dragCoefficient(), 0.8, 'dragCoefficient is changed'); 296 | 297 | var timeStep = simulator.timeStep(); 298 | t.ok(typeof timeStep === 'number', 'timeStep is here'); 299 | simulator.timeStep(8); 300 | t.equal(simulator.timeStep(), 8, 'timeStep is changed'); 301 | 302 | t.end(); 303 | }); 304 | 305 | test('it can augment string setter values', function (t) { 306 | var simulator = createSimulator({ 307 | name: 'John' 308 | }); 309 | 310 | simulator.name('Alisa'); 311 | t.equal(simulator.name(), 'Alisa', 'name is Alisa'); 312 | t.end(); 313 | }); 314 | 315 | test('it ignores body that does not exist', function(t) { 316 | var simulator = createSimulator(); 317 | var body = new Body(0, 0); 318 | simulator.addBody(body); 319 | simulator.removeBody({}); 320 | t.equal(simulator.bodies.length, 1, 'Should ignore body that does not exist'); 321 | t.end(); 322 | }); 323 | 324 | test('it throws on springCoeff', function (t) { 325 | t.throws(function () { 326 | createSimulator({springCoeff: 1}); 327 | }, 'springCoeff was renamed to springCoefficient'); 328 | t.end(); 329 | }); 330 | 331 | test('it throws on dragCoeff', function (t) { 332 | t.throws(function () { 333 | createSimulator({dragCoeff: 1}); 334 | }, 'dragCoeff was renamed to dragCoefficient'); 335 | t.end(); 336 | }); -------------------------------------------------------------------------------- /test/springForce.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow */ 2 | var test = require('tap').test; 3 | 4 | var dimensions = 2; 5 | var createSpringForce = require('../lib/codeGenerators/generateCreateSpringForce')(dimensions); 6 | var Body = require('../lib/codeGenerators/generateCreateBody')(dimensions); 7 | var Spring = require('../lib/spring'); 8 | var random = require('ngraph.random')(42); 9 | 10 | test('Initialized with default value', function (t) { 11 | t.throws(() => createSpringForce()); 12 | t.end(); 13 | }); 14 | 15 | 16 | test('Should bump bodies at same position', function (t) { 17 | var body1 = new Body(0, 0); 18 | var body2 = new Body(0, 0); 19 | // length between two bodies is 2, while ideal length is 1. Each body 20 | // should start moving towards each other after force update 21 | var idealLength = 1; 22 | var spring = new Spring(body1, body2, idealLength); 23 | var springForce = createSpringForce({springCoefficient: 0.1, springLength: 1}, random); 24 | springForce.update(spring); 25 | 26 | t.ok(body1.force.x > 0, 'Body 1 should go right'); 27 | t.ok(body2.force.x < 0, 'Body 2 should go left'); 28 | t.end(); 29 | }); 30 | 31 | test('Check spring force direction', function (t) { 32 | var springForce = createSpringForce({springCoefficient: 0.1, springLength: 1}); 33 | 34 | t.test('Should contract two bodies when ideal length is smaller than actual', function (t) { 35 | var body1 = new Body(-1, 0); 36 | var body2 = new Body(+1, 0); 37 | // length between two bodies is 2, while ideal length is 1. Each body 38 | // should start moving towards each other after force update 39 | var idealLength = 1; 40 | var spring = new Spring(body1, body2, idealLength); 41 | springForce.update(spring); 42 | 43 | t.ok(body1.force.x > 0, 'Body 1 should go right'); 44 | t.ok(body2.force.x < 0, 'Body 2 should go left'); 45 | t.end(); 46 | }); 47 | 48 | t.test('Should repel two bodies when ideal length is larger than actual', function (t) { 49 | var body1 = new Body(-1, 0); 50 | var body2 = new Body(+1, 0); 51 | // length between two bodies is 2, while ideal length is 1. Each body 52 | // should start moving towards each other after force update 53 | var idealLength = 3; 54 | var spring = new Spring(body1, body2, idealLength); 55 | springForce.update(spring); 56 | 57 | t.ok(body1.force.x < 0, 'Body 1 should go left'); 58 | t.ok(body2.force.x > 0, 'Body 2 should go right'); 59 | t.end(); 60 | }); 61 | 62 | t.end(); 63 | }); 64 | --------------------------------------------------------------------------------