├── .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 | [](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 |
46 | We're sorry but puller doesn't work properly without JavaScript enabled. Please enable it to continue.
47 |
48 |
49 |
50 |
51 |
52 |
53 |
--------------------------------------------------------------------------------
/demo/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
Graph
9 |
10 | {{graph}}
11 |
12 |
13 |
14 | This is integration time step value. The higher it is, the faster nodes will move, but setting it too high
15 | can result in lots of jitter and instability.
16 |
17 |
18 | This coefficient defines how strongly each node repels each other.
19 |
20 |
21 | What is the ideal length of each spring?
22 |
23 |
24 | Higher values makes the spring force stronger, pushing edges closer to the ideal spring length.
25 |
26 |
27 | This coefficient introduces "resistance" from environment. When it is close to 0 the forces
28 | will have a lot of freedom, nothing will be stopping them, and that can result in a very
29 | unstable simulation.
30 |
31 |
32 | This coefficient influences when we apply long distance forces approximation. When this value is
33 | close to 0, the simulation compares forces between every single node (giving O(n^2), slow performance).
34 | Recommended value is 0.8.
35 |
36 |
37 |
38 | Defines number of dimensions of the space where layout is performed. For visualization purpose
39 | 2 or 3 dimensions are normally enough. Note: Memory consumptions grows exponentially with number
40 | of dimensions.
41 |
42 |
43 |
44 | Setting this to true will disable pan/zoom but will always keep the graph visible. This is not
45 | part of the layout algorithm. Just a view setting of the renderer.
46 |
47 |
Loading graph...
48 |
49 |
52 |
53 |
54 |
55 |
152 |
153 |
226 |
--------------------------------------------------------------------------------
/demo/src/components/HelpIcon.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/demo/src/components/InputFlag.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{label}}
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
44 |
45 |
--------------------------------------------------------------------------------
/demo/src/components/InputValue.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
{{label}}
5 |
6 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
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 |
--------------------------------------------------------------------------------