Browse Source

fixed whitespace and added initial documentation

webworkersNetwork^2^2
Eric VanDever 9 years ago
parent
commit
6813bcd465
3 changed files with 261 additions and 252 deletions
  1. +10
    -1
      docs/network/physics.html
  2. +48
    -48
      lib/network/modules/PhysicsEngine.js
  3. +203
    -203
      lib/network/modules/PhysicsWorker.js

+ 10
- 1
docs/network/physics.html View File

@ -139,7 +139,8 @@ var options = {
fit: true fit: true
}, },
timestep: 0.5, timestep: 0.5,
adaptiveTimestep: true
adaptiveTimestep: true,
useWorker: false
} }
} }
@ -203,6 +204,14 @@ network.setOptions(options);
<tr parent="stabilization" class="hidden"><td class="indent">stabilization.fit</td> <td>Boolean</td> <td><code>true</code></td> <td>Toggle whether or not you want the view to zoom to fit all nodes when the stabilization is finished.</td></tr> <tr parent="stabilization" class="hidden"><td class="indent">stabilization.fit</td> <td>Boolean</td> <td><code>true</code></td> <td>Toggle whether or not you want the view to zoom to fit all nodes when the stabilization is finished.</td></tr>
<tr><td>timestep</td> <td>Number</td> <td><code>0.5</code></td> <td>The physics simulation is discrete. This means we take a step in time, calculate the forces, move the nodes and take another step. If you increase this number the steps will be too large and the network can get unstable. If you see a lot of jittery movement in the network, you may want to reduce this value a little.</td></tr> <tr><td>timestep</td> <td>Number</td> <td><code>0.5</code></td> <td>The physics simulation is discrete. This means we take a step in time, calculate the forces, move the nodes and take another step. If you increase this number the steps will be too large and the network can get unstable. If you see a lot of jittery movement in the network, you may want to reduce this value a little.</td></tr>
<tr><td>adaptiveTimestep</td> <td>Boolean</td> <td><code>true</code></td> <td>If this is enabled, the timestep will intelligently be adapted <b>(only during the stabilization stage if stabilization is enabled!)</b> to greatly decrease stabilization times. The timestep configured above is taken as the minimum timestep. <a href="layout.html#layout" target="_blank">This can be further improved by using the improvedLayout algorithm</a>.</td></tr> <tr><td>adaptiveTimestep</td> <td>Boolean</td> <td><code>true</code></td> <td>If this is enabled, the timestep will intelligently be adapted <b>(only during the stabilization stage if stabilization is enabled!)</b> to greatly decrease stabilization times. The timestep configured above is taken as the minimum timestep. <a href="layout.html#layout" target="_blank">This can be further improved by using the improvedLayout algorithm</a>.</td></tr>
<tr><td>useWorker</td> <td>Boolean</td> <td><code>false</code></td>
<td>If this is enabled, the physics calculation will be performed in a separate thread. The file vis.physics.worker.js must be available from the same webserver hosting vis.js at the same path. If
you are embedding vis into a javascript bundle, you can set the script id to "visjs" to enable
path resolution. If the worker fails for any reason, the system will fall back to standard
physics calculations.
WorkInProgress: This has only been tested with default physics selections and does not attempt to optimize
stabilization.
</td></tr>
</table> </table>
</div> </div>

+ 48
- 48
lib/network/modules/PhysicsEngine.js View File

@ -89,7 +89,7 @@ class PhysicsEngine {
util.extend(this.options, this.defaultOptions); util.extend(this.options, this.defaultOptions);
this.timestep = 0.5; this.timestep = 0.5;
this.layoutFailed = false; this.layoutFailed = false;
this.draggingNodes = [];
this.draggingNodes = [];
this.bindEventListeners(); this.bindEventListeners();
} }
@ -115,9 +115,9 @@ class PhysicsEngine {
this.stopSimulation(false); this.stopSimulation(false);
this.body.emitter.off(); this.body.emitter.off();
}); });
// For identifying which nodes to send to worker thread
this.body.emitter.on('dragStart', (properties) => {this.draggingNodes = properties.nodes;});
this.body.emitter.on('dragEnd', () => {this.draggingNodes = [];});
// For identifying which nodes to send to worker thread
this.body.emitter.on('dragStart', (properties) => {this.draggingNodes = properties.nodes;});
this.body.emitter.on('dragEnd', () => {this.draggingNodes = [];});
this.body.emitter.on('destroy', () => { this.body.emitter.on('destroy', () => {
if (this.physicsWorker) { if (this.physicsWorker) {
this.physicsWorker.terminate(); this.physicsWorker.terminate();
@ -290,20 +290,20 @@ class PhysicsEngine {
*/ */
startSimulation() { startSimulation() {
if (this.physicsEnabled === true && this.options.enabled === true) { if (this.physicsEnabled === true && this.options.enabled === true) {
if (this.physicsWorker) {
for(let i = 0; i < this.draggingNodes.length; i++) {
let nodeId = this.draggingNodes[i];
let node = this.body.nodes[nodeId];
this.physicsWorker.postMessage({
type: 'update',
data: {
id: nodeId,
x: node.x,
y: node.y
}
});
}
}
if (this.physicsWorker) {
for(let i = 0; i < this.draggingNodes.length; i++) {
let nodeId = this.draggingNodes[i];
let node = this.body.nodes[nodeId];
this.physicsWorker.postMessage({
type: 'update',
data: {
id: nodeId,
x: node.x,
y: node.y
}
});
}
}
this.stabilized = false; this.stabilized = false;
// when visible, adaptivity is disabled. // when visible, adaptivity is disabled.
@ -496,36 +496,36 @@ class PhysicsEngine {
} }
} }
for (let edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
let edge = edges[edgeId];
if (edge.options.physics === true) {
physicsWorkerEdges[edgeId] = {
connected: edge.connected,
id: edge.id,
edgeType: {},
toId: edge.toId,
fromId: edge.fromId,
to: {
id: edge.to.id
},
from: {
id: edge.from.id
},
options: {
length: edge.length
}
};
if (edge.edgeType.via) {
physicsWorkerEdges[edgeId].edgeType = {
via: {
id: edge.edgeType.via.id
}
}
}
}
}
}
for (let edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
let edge = edges[edgeId];
if (edge.options.physics === true) {
physicsWorkerEdges[edgeId] = {
connected: edge.connected,
id: edge.id,
edgeType: {},
toId: edge.toId,
fromId: edge.fromId,
to: {
id: edge.to.id
},
from: {
id: edge.from.id
},
options: {
length: edge.length
}
};
if (edge.edgeType.via) {
physicsWorkerEdges[edgeId].edgeType = {
via: {
id: edge.edgeType.via.id
}
}
}
}
}
}
this.physicsWorker.postMessage({ this.physicsWorker.postMessage({
type: 'physicsObjects', type: 'physicsObjects',

+ 203
- 203
lib/network/modules/PhysicsWorker.js View File

@ -8,209 +8,209 @@ import ForceAtlas2BasedRepulsionSolver from './components/physics/FA2BasedR
import ForceAtlas2BasedCentralGravitySolver from './components/physics/FA2BasedCentralGravitySolver'; import ForceAtlas2BasedCentralGravitySolver from './components/physics/FA2BasedCentralGravitySolver';
class PhysicsWorker { class PhysicsWorker {
constructor(postMessage) {
this.body = {};
this.physicsBody = {physicsNodeIndices:[], physicsEdgeIndices:[], forces: {}, velocities: {}};
this.postMessage = postMessage;
this.options = {};
this.stabilized = false;
this.previousStates = {};
this.positions = {};
this.timestep = 0.5;
}
handleMessage(event) {
var msg = event.data;
switch (msg.type) {
case 'calculateForces':
this.calculateForces();
this.moveNodes();
this.postMessage({
type: 'positions',
data: {
positions: this.positions,
stabilized: this.stabilized
}
});
break;
case 'update':
let node = this.body.nodes[msg.data.id];
node.x = msg.data.x;
node.y = msg.data.y;
break;
case 'options':
this.options = msg.data;
this.timestep = this.options.timestep;
this.init();
break;
case 'physicsObjects':
this.body.nodes = msg.data.nodes;
this.body.edges = msg.data.edges;
this.updatePhysicsData();
break;
default:
console.warn('unknown message from PhysicsEngine', msg);
}
}
/**
* configure the engine.
*/
init() {
var options;
if (this.options.solver === 'forceAtlas2Based') {
options = this.options.forceAtlas2Based;
this.nodesSolver = new ForceAtlas2BasedRepulsionSolver(this.body, this.physicsBody, options);
this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
this.gravitySolver = new ForceAtlas2BasedCentralGravitySolver(this.body, this.physicsBody, options);
}
else if (this.options.solver === 'repulsion') {
options = this.options.repulsion;
this.nodesSolver = new Repulsion(this.body, this.physicsBody, options);
this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
}
else if (this.options.solver === 'hierarchicalRepulsion') {
options = this.options.hierarchicalRepulsion;
this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options);
this.edgesSolver = new HierarchicalSpringSolver(this.body, this.physicsBody, options);
this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
}
else { // barnesHut
options = this.options.barnesHut;
this.nodesSolver = new BarnesHutSolver(this.body, this.physicsBody, options);
this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
}
this.modelOptions = options;
}
/**
* Nodes and edges can have the physics toggles on or off. A collection of indices is created here so we can skip the check all the time.
*
* @private
*/
updatePhysicsData() {
this.physicsBody.forces = {};
this.physicsBody.physicsNodeIndices = [];
this.physicsBody.physicsEdgeIndices = [];
let nodes = this.body.nodes;
let edges = this.body.edges;
// get node indices for physics
for (let nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
this.physicsBody.physicsNodeIndices.push(nodeId);
this.positions[nodeId] = {
x: nodes[nodeId].x,
y: nodes[nodeId].y
}
}
}
// get edge indices for physics
for (let edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
this.physicsBody.physicsEdgeIndices.push(edgeId);
}
}
// get the velocity and the forces vector
for (let i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) {
let nodeId = this.physicsBody.physicsNodeIndices[i];
this.physicsBody.forces[nodeId] = {x: 0, y: 0};
// forces can be reset because they are recalculated. Velocities have to persist.
if (this.physicsBody.velocities[nodeId] === undefined) {
this.physicsBody.velocities[nodeId] = {x: 0, y: 0};
}
}
// clean deleted nodes from the velocity vector
for (let nodeId in this.physicsBody.velocities) {
if (nodes[nodeId] === undefined) {
delete this.physicsBody.velocities[nodeId];
}
}
// console.log(this.physicsBody);
}
/**
* move the nodes one timestap and check if they are stabilized
* @returns {boolean}
*/
moveNodes() {
var nodeIndices = this.physicsBody.physicsNodeIndices;
var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1e9;
var maxNodeVelocity = 0;
for (let i = 0; i < nodeIndices.length; i++) {
let nodeId = nodeIndices[i];
let nodeVelocity = this._performStep(nodeId, maxVelocity);
// stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized
maxNodeVelocity = Math.max(maxNodeVelocity,nodeVelocity);
}
// evaluating the stabilized and adaptiveTimestepEnabled conditions
this.stabilized = maxNodeVelocity < this.options.minVelocity;
}
/**
* Perform the actual step
*
* @param nodeId
* @param maxVelocity
* @returns {number}
* @private
*/
_performStep(nodeId,maxVelocity) {
let node = this.body.nodes[nodeId];
let timestep = this.timestep;
let forces = this.physicsBody.forces;
let velocities = this.physicsBody.velocities;
// store the state so we can revert
this.previousStates[nodeId] = {x:node.x, y:node.y, vx:velocities[nodeId].x, vy:velocities[nodeId].y};
if (node.options.fixed.x === false) {
let dx = this.modelOptions.damping * velocities[nodeId].x; // damping force
let ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration
velocities[nodeId].x += ax * timestep; // velocity
velocities[nodeId].x = (Math.abs(velocities[nodeId].x) > maxVelocity) ? ((velocities[nodeId].x > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].x;
node.x += velocities[nodeId].x * timestep; // position
this.positions[nodeId].x = node.x;
}
else {
forces[nodeId].x = 0;
velocities[nodeId].x = 0;
}
if (node.options.fixed.y === false) {
let dy = this.modelOptions.damping * velocities[nodeId].y; // damping force
let ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration
velocities[nodeId].y += ay * timestep; // velocity
velocities[nodeId].y = (Math.abs(velocities[nodeId].y) > maxVelocity) ? ((velocities[nodeId].y > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].y;
node.y += velocities[nodeId].y * timestep; // position
this.positions[nodeId].y = node.y;
}
else {
forces[nodeId].y = 0;
velocities[nodeId].y = 0;
}
let totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x,2) + Math.pow(velocities[nodeId].y,2));
return totalVelocity;
}
/**
* calculate the forces for one physics iteration.
*/
calculateForces() {
this.gravitySolver.solve();
this.nodesSolver.solve();
this.edgesSolver.solve();
}
constructor(postMessage) {
this.body = {};
this.physicsBody = {physicsNodeIndices:[], physicsEdgeIndices:[], forces: {}, velocities: {}};
this.postMessage = postMessage;
this.options = {};
this.stabilized = false;
this.previousStates = {};
this.positions = {};
this.timestep = 0.5;
}
handleMessage(event) {
var msg = event.data;
switch (msg.type) {
case 'calculateForces':
this.calculateForces();
this.moveNodes();
this.postMessage({
type: 'positions',
data: {
positions: this.positions,
stabilized: this.stabilized
}
});
break;
case 'update':
let node = this.body.nodes[msg.data.id];
node.x = msg.data.x;
node.y = msg.data.y;
break;
case 'options':
this.options = msg.data;
this.timestep = this.options.timestep;
this.init();
break;
case 'physicsObjects':
this.body.nodes = msg.data.nodes;
this.body.edges = msg.data.edges;
this.updatePhysicsData();
break;
default:
console.warn('unknown message from PhysicsEngine', msg);
}
}
/**
* configure the engine.
*/
init() {
var options;
if (this.options.solver === 'forceAtlas2Based') {
options = this.options.forceAtlas2Based;
this.nodesSolver = new ForceAtlas2BasedRepulsionSolver(this.body, this.physicsBody, options);
this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
this.gravitySolver = new ForceAtlas2BasedCentralGravitySolver(this.body, this.physicsBody, options);
}
else if (this.options.solver === 'repulsion') {
options = this.options.repulsion;
this.nodesSolver = new Repulsion(this.body, this.physicsBody, options);
this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
}
else if (this.options.solver === 'hierarchicalRepulsion') {
options = this.options.hierarchicalRepulsion;
this.nodesSolver = new HierarchicalRepulsion(this.body, this.physicsBody, options);
this.edgesSolver = new HierarchicalSpringSolver(this.body, this.physicsBody, options);
this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
}
else { // barnesHut
options = this.options.barnesHut;
this.nodesSolver = new BarnesHutSolver(this.body, this.physicsBody, options);
this.edgesSolver = new SpringSolver(this.body, this.physicsBody, options);
this.gravitySolver = new CentralGravitySolver(this.body, this.physicsBody, options);
}
this.modelOptions = options;
}
/**
* Nodes and edges can have the physics toggles on or off. A collection of indices is created here so we can skip the check all the time.
*
* @private
*/
updatePhysicsData() {
this.physicsBody.forces = {};
this.physicsBody.physicsNodeIndices = [];
this.physicsBody.physicsEdgeIndices = [];
let nodes = this.body.nodes;
let edges = this.body.edges;
// get node indices for physics
for (let nodeId in nodes) {
if (nodes.hasOwnProperty(nodeId)) {
this.physicsBody.physicsNodeIndices.push(nodeId);
this.positions[nodeId] = {
x: nodes[nodeId].x,
y: nodes[nodeId].y
}
}
}
// get edge indices for physics
for (let edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
this.physicsBody.physicsEdgeIndices.push(edgeId);
}
}
// get the velocity and the forces vector
for (let i = 0; i < this.physicsBody.physicsNodeIndices.length; i++) {
let nodeId = this.physicsBody.physicsNodeIndices[i];
this.physicsBody.forces[nodeId] = {x: 0, y: 0};
// forces can be reset because they are recalculated. Velocities have to persist.
if (this.physicsBody.velocities[nodeId] === undefined) {
this.physicsBody.velocities[nodeId] = {x: 0, y: 0};
}
}
// clean deleted nodes from the velocity vector
for (let nodeId in this.physicsBody.velocities) {
if (nodes[nodeId] === undefined) {
delete this.physicsBody.velocities[nodeId];
}
}
// console.log(this.physicsBody);
}
/**
* move the nodes one timestap and check if they are stabilized
* @returns {boolean}
*/
moveNodes() {
var nodeIndices = this.physicsBody.physicsNodeIndices;
var maxVelocity = this.options.maxVelocity ? this.options.maxVelocity : 1e9;
var maxNodeVelocity = 0;
for (let i = 0; i < nodeIndices.length; i++) {
let nodeId = nodeIndices[i];
let nodeVelocity = this._performStep(nodeId, maxVelocity);
// stabilized is true if stabilized is true and velocity is smaller than vmin --> all nodes must be stabilized
maxNodeVelocity = Math.max(maxNodeVelocity,nodeVelocity);
}
// evaluating the stabilized and adaptiveTimestepEnabled conditions
this.stabilized = maxNodeVelocity < this.options.minVelocity;
}
/**
* Perform the actual step
*
* @param nodeId
* @param maxVelocity
* @returns {number}
* @private
*/
_performStep(nodeId,maxVelocity) {
let node = this.body.nodes[nodeId];
let timestep = this.timestep;
let forces = this.physicsBody.forces;
let velocities = this.physicsBody.velocities;
// store the state so we can revert
this.previousStates[nodeId] = {x:node.x, y:node.y, vx:velocities[nodeId].x, vy:velocities[nodeId].y};
if (node.options.fixed.x === false) {
let dx = this.modelOptions.damping * velocities[nodeId].x; // damping force
let ax = (forces[nodeId].x - dx) / node.options.mass; // acceleration
velocities[nodeId].x += ax * timestep; // velocity
velocities[nodeId].x = (Math.abs(velocities[nodeId].x) > maxVelocity) ? ((velocities[nodeId].x > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].x;
node.x += velocities[nodeId].x * timestep; // position
this.positions[nodeId].x = node.x;
}
else {
forces[nodeId].x = 0;
velocities[nodeId].x = 0;
}
if (node.options.fixed.y === false) {
let dy = this.modelOptions.damping * velocities[nodeId].y; // damping force
let ay = (forces[nodeId].y - dy) / node.options.mass; // acceleration
velocities[nodeId].y += ay * timestep; // velocity
velocities[nodeId].y = (Math.abs(velocities[nodeId].y) > maxVelocity) ? ((velocities[nodeId].y > 0) ? maxVelocity : -maxVelocity) : velocities[nodeId].y;
node.y += velocities[nodeId].y * timestep; // position
this.positions[nodeId].y = node.y;
}
else {
forces[nodeId].y = 0;
velocities[nodeId].y = 0;
}
let totalVelocity = Math.sqrt(Math.pow(velocities[nodeId].x,2) + Math.pow(velocities[nodeId].y,2));
return totalVelocity;
}
/**
* calculate the forces for one physics iteration.
*/
calculateForces() {
this.gravitySolver.solve();
this.nodesSolver.solve();
this.edgesSolver.solve();
}
} }
export default PhysicsWorker; export default PhysicsWorker;

Loading…
Cancel
Save