Browse Source

hierarchical system is now working and original x or y values are retained

flowchartTest
Alex de Mulder 9 years ago
parent
commit
b28af6faa5
19 changed files with 678 additions and 1087 deletions
  1. +317
    -319
      dist/vis.js
  2. +5
    -5
      examples/network/23_hierarchical_layout.html
  3. +7
    -2
      examples/network/24_hierarchical_layout_userdefined.html
  4. +19
    -58
      examples/network/32_hierarchicaLayoutMethods.html
  5. +7
    -7
      examples/network/34_circular_images.html
  6. +25
    -29
      lib/network/Network.js
  7. +0
    -396
      lib/network/mixins/HierarchicalLayoutMixin.js
  8. +1
    -1
      lib/network/modules/CanvasRenderer.js
  9. +42
    -9
      lib/network/modules/EdgesHandler.js
  10. +201
    -230
      lib/network/modules/LayoutEngine.js
  11. +4
    -1
      lib/network/modules/NodesHandler.js
  12. +38
    -21
      lib/network/modules/PhysicsEngine.js
  13. +6
    -3
      lib/network/modules/components/Node.js
  14. +1
    -1
      lib/network/modules/components/physics/BarnesHutSolver.js
  15. +1
    -1
      lib/network/modules/components/physics/CentralGravitySolver.js
  16. +1
    -1
      lib/network/modules/components/physics/HierarchicalRepulsionSolver.js
  17. +1
    -1
      lib/network/modules/components/physics/HierarchicalSpringSolver.js
  18. +1
    -1
      lib/network/modules/components/physics/RepulsionSolver.js
  19. +1
    -1
      lib/network/modules/components/physics/SpringSolver.js

+ 317
- 319
dist/vis.js
File diff suppressed because it is too large
View File


+ 5
- 5
examples/network/23_hierarchical_layout.html View File

@ -84,12 +84,12 @@
nodes: nodes,
edges: edges
};
var directionInput = document.getElementById("direction");
var directionInput = document.getElementById("direction").value;
var options = {
stabilize: false,
smoothCurves: false,
hierarchicalLayout: {
direction: directionInput.value
layout: {
hierarchical:{
direction: directionInput
}
}
};
network = new vis.Network(container, data, options);

+ 7
- 2
examples/network/24_hierarchical_layout_userdefined.html View File

@ -123,8 +123,13 @@
};
var options = {
hierarchicalLayout: {
direction: directionInput.value
edges: {
smooth: true
},
layout: {
hierarchical:{
direction: directionInput.value
}
}
};
network = new vis.Network(container, data, options);

+ 19
- 58
examples/network/32_hierarchicaLayoutMethods.html View File

@ -40,61 +40,20 @@
label: String(i)
});
}
edges.push({
from: 0,
to: 1
});
edges.push({
from: 0,
to: 6
});
edges.push({
from: 0,
to: 13
});edges.push({
from: 0,
to: 11
});
edges.push({
from: 1,
to: 2
});
edges.push({
from: 2,
to: 3
});
edges.push({
from: 2,
to: 4
});
edges.push({
from: 3,
to: 5
});
edges.push({
from: 1,
to: 10
});
edges.push({
from: 1,
to: 7
});
edges.push({
from: 2,
to: 8
});
edges.push({
from: 2,
to: 9
});
edges.push({
from: 3,
to: 14
});
edges.push({
from: 1,
to: 12
});
edges.push({from: 0, to: 1});
edges.push({from: 0, to: 6});
edges.push({from: 0, to: 13});
edges.push({from: 0, to: 11});
edges.push({from: 1, to: 2});
edges.push({from: 2, to: 3});
edges.push({from: 2, to: 4});
edges.push({from: 3, to: 5});
edges.push({from: 1, to: 10});
edges.push({from: 1, to: 7});
edges.push({from: 2, to: 8});
edges.push({from: 2, to: 9});
edges.push({from: 3, to: 14});
edges.push({from: 1, to: 12});
// create a network
var container = document.getElementById('mynetwork');
@ -104,10 +63,12 @@
};
var options = {
hierarchicalLayout: {
layout: layoutMethod
layout: {
hierarchical: {
sortMethod:layoutMethod
}
},
edges: {style:"arrow"},
edges: {arrows:{to:true}},
smoothCurves:false
};
network = new vis.Network(container, data, options);

+ 7
- 7
examples/network/34_circular_images.html View File

@ -34,18 +34,18 @@
{id: 1, shape: 'circularImage', image: DIR + '1.png'},
{id: 2, shape: 'circularImage', image: DIR + '2.png'},
{id: 3, shape: 'circularImage', image: DIR + '3.png'},
{id: 4, label:"pictures by this guy!", shape: 'circularImage', image: DIR + '4.png'},
{id: 4, shape: 'circularImage', image: DIR + '4.png', label:"pictures by this guy!"},
{id: 5, shape: 'circularImage', image: DIR + '5.png'},
{id: 6, shape: 'circularImage', image: DIR + '6.png'},
{id: 7, shape: 'circularImage', image: DIR + '7.png'},
{id: 8, shape: 'circularImage', image: DIR + '8.png'},
{id: 9, shape: 'circularImage', image: DIR + '9.png'},
{id: 10, shape: 'circularImage', image: DIR + '10.png'},
{id: 11, shape: 'circularImage', image: DIR + '11.png'},
{id: 12, shape: 'circularImage', image: DIR + '12.png'},
{id: 13, shape: 'circularImage', image: DIR + '13.png'},
{id: 14, shape: 'circularImage', image: DIR + '14.png'},
{id: 15, label:"when images\nfail\nto load", shape: 'circularImage', image: DIR + 'missing.png', brokenImage: DIR + 'missingBrokenImage.png'}
{id: 10, shape: 'circularImage', image: DIR + '10.png'},
{id: 11, shape: 'circularImage', image: DIR + '11.png'},
{id: 12, shape: 'circularImage', image: DIR + '12.png'},
{id: 13, shape: 'circularImage', image: DIR + '13.png'},
{id: 14, shape: 'circularImage', image: DIR + '14.png'},
{id: 15, shape: 'circularImage', image: DIR + 'missing.png', brokenImage: DIR + 'missingBrokenImage.png', label:"when images\nfail\nto load", }
];
// create connetions between people

+ 25
- 29
lib/network/Network.js View File

@ -49,13 +49,6 @@ function Network (container, data, options) {
enabled: false,
initiallyVisible: false
},
hierarchicalLayout: {
enabled:false,
levelSeparation: 150,
nodeSpacing: 100,
direction: "UD", // UD, DU, LR, RL
layout: "hubsize" // hubsize, directed
},
locale: 'en',
locales: locales,
useDefaultGroups: true
@ -101,23 +94,6 @@ function Network (container, data, options) {
}
};
// todo think of good comment for this set
var groups = new Groups(); // object with groups
var images = new Images(() => this.body.emitter.emit("_requestRedraw")); // object with images
// data handling modules
this.canvas = new Canvas(this.body); // DOM handler
this.selectionHandler = new SelectionHandler(this.body, this.canvas); // Selection handler
this.interactionHandler = new InteractionHandler(this.body, this.canvas, this.selectionHandler); // Interaction handler handles all the hammer bindings (that are bound by canvas), key
this.view = new View(this.body, this.canvas); // camera handler, does animations and zooms
this.renderer = new CanvasRenderer(this.body, this.canvas); // renderer, starts renderloop, has events that modules can hook into
this.physics = new PhysicsEngine(this.body); // physics engine, does all the simulations
this.layoutEngine = new LayoutEngine(this.body);
this.clustering = new ClusterEngine(this.body); // clustering api
this.nodesHandler = new NodesHandler(this.body, images, groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options
this.edgesHandler = new EdgesHandler(this.body, images, groups); // Handle adding, deleting and updating of edges as well as global options
// this event will trigger a rebuilding of the cache everything. Used when nodes or edges have been added or removed.
this.body.emitter.on("_dataChanged", (params) => {
var t0 = new Date().valueOf();
@ -149,6 +125,23 @@ function Network (container, data, options) {
console.log("_dataUpdated took:", new Date().valueOf() - t0);
});
// todo think of good comment for this set
var groups = new Groups(); // object with groups
var images = new Images(() => this.body.emitter.emit("_requestRedraw")); // object with images
// data handling modules
this.canvas = new Canvas(this.body); // DOM handler
this.selectionHandler = new SelectionHandler(this.body, this.canvas); // Selection handler
this.interactionHandler = new InteractionHandler(this.body, this.canvas, this.selectionHandler); // Interaction handler handles all the hammer bindings (that are bound by canvas), key
this.view = new View(this.body, this.canvas); // camera handler, does animations and zooms
this.renderer = new CanvasRenderer(this.body, this.canvas); // renderer, starts renderloop, has events that modules can hook into
this.physics = new PhysicsEngine(this.body); // physics engine, does all the simulations
this.layoutEngine = new LayoutEngine(this.body);
this.clustering = new ClusterEngine(this.body); // clustering api
this.nodesHandler = new NodesHandler(this.body, images, groups, this.layoutEngine); // Handle adding, deleting and updating of nodes as well as global options
this.edgesHandler = new EdgesHandler(this.body, images, groups); // Handle adding, deleting and updating of edges as well as global options
// create the DOM elements
this.canvas.create();
@ -252,7 +245,7 @@ Network.prototype.setData = function(data) {
* @param {Object} options
*/
Network.prototype.setOptions = function (options) {
if (options) {
if (options !== undefined) {
//var fields = ['nodes','edges','smoothCurves','hierarchicalLayout','navigation',
// 'keyboard','dataManipulation','onAdd','onEdit','onEditEdge','onConnect','onDelete','clickToUse'
//];
@ -263,6 +256,9 @@ Network.prototype.setOptions = function (options) {
//this.groups.useDefaultGroups = this.constants.useDefaultGroups;
// the hierarchical system can adapt the edges and the physics to it's own options because not all combinations work with the hierarichical system.
options = this.layoutEngine.setOptions(options.layout, options);
this.nodesHandler.setOptions(options.nodes);
this.edgesHandler.setOptions(options.edges);
this.physics.setOptions(options.physics);
@ -271,7 +267,6 @@ Network.prototype.setOptions = function (options) {
this.view.setOptions(options.view);
this.interactionHandler.setOptions(options.interaction);
this.selectionHandler.setOptions(options.selection);
this.layoutEngine.setOptions(options.layout);
this.clustering.setOptions(options.clustering);
//util.mergeOptions(this.constants, options,'smoothCurves');
@ -494,9 +489,10 @@ Network.prototype._markAllEdgesAsDirty = function() {
* @private
*/
Network.prototype._reconnectEdges = function() {
var id,
nodes = this.body.nodes,
edges = this.body.edges;
var id;
var nodes = this.body.nodes;
var edges = this.body.edges;
for (id in nodes) {
if (nodes.hasOwnProperty(id)) {
nodes[id].edges = [];

+ 0
- 396
lib/network/mixins/HierarchicalLayoutMixin.js View File

@ -1,396 +0,0 @@
exports._resetLevels = function() {
for (var nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
var node = this.body.nodes[nodeId];
if (node.preassignedLevel == false) {
node.level = -1;
node.hierarchyEnumerated = false;
}
}
}
};
/**
* This is the main function to layout the nodes in a hierarchical way.
* It checks if the node details are supplied correctly
*
* @private
*/
exports._setupHierarchicalLayout = function() {
if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) {
// get the size of the largest hubs and check if the user has defined a level for a node.
var hubsize = 0;
var node, nodeId;
var definedLevel = false;
var undefinedLevel = false;
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
if (node.level != -1) {
definedLevel = true;
}
else {
undefinedLevel = true;
}
if (hubsize < node.edges.length) {
hubsize = node.edges.length;
}
}
}
// if the user defined some levels but not all, alert and run without hierarchical layout
if (undefinedLevel == true && definedLevel == true) {
throw new Error("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
this.zoomExtent({duration:0},true,this.constants.clustering.enabled);
if (!this.constants.clustering.enabled) {
this.start();
}
}
else {
// setup the system to use hierarchical method.
this._changeConstants();
// define levels if undefined by the users. Based on hubsize
if (undefinedLevel == true) {
if (this.constants.hierarchicalLayout.layout == "hubsize") {
this._determineLevels(hubsize);
}
else {
this._determineLevelsDirected(false);
}
}
// check the distribution of the nodes per level.
var distribution = this._getDistribution();
// place the nodes on the canvas. This also stablilizes the system. Redraw in started automatically after stabilize.
this._placeNodesByHierarchy(distribution);
}
}
};
/**
* This function places the nodes on the canvas based on the hierarchial distribution.
*
* @param {Object} distribution | obtained by the function this._getDistribution()
* @private
*/
exports._placeNodesByHierarchy = function(distribution) {
var nodeId, node;
// start placing all the level 0 nodes first. Then recursively position their branches.
for (var level in distribution) {
if (distribution.hasOwnProperty(level)) {
for (nodeId in distribution[level].nodes) {
if (distribution[level].nodes.hasOwnProperty(nodeId)) {
node = distribution[level].nodes[nodeId];
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
if (node.xFixed) {
node.x = distribution[level].minPos;
node.xFixed = false;
distribution[level].minPos += distribution[level].nodeSpacing;
}
}
else {
if (node.yFixed) {
node.y = distribution[level].minPos;
node.yFixed = false;
distribution[level].minPos += distribution[level].nodeSpacing;
}
}
this._placeBranchNodes(node.edges,node.id,distribution,node.level);
}
}
}
}
// stabilize the system after positioning. This function calls zoomExtent.
this._stabilize();
};
/**
* This function get the distribution of levels based on hubsize
*
* @returns {Object}
* @private
*/
exports._getDistribution = function() {
var distribution = {};
var nodeId, node, level;
// we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time.
// the fix of X is removed after the x value has been set.
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
node.xFixed = true;
node.yFixed = true;
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
}
else {
node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
}
if (distribution[node.level] === undefined) {
distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
}
distribution[node.level].amount += 1;
distribution[node.level].nodes[nodeId] = node;
}
}
// determine the largest amount of nodes of all levels
var maxCount = 0;
for (level in distribution) {
if (distribution.hasOwnProperty(level)) {
if (maxCount < distribution[level].amount) {
maxCount = distribution[level].amount;
}
}
}
// set the initial position and spacing of each nodes accordingly
for (level in distribution) {
if (distribution.hasOwnProperty(level)) {
distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
distribution[level].nodeSpacing /= (distribution[level].amount + 1);
distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
}
}
return distribution;
};
/**
* this function allocates nodes in levels based on the recursive branching from the largest hubs.
*
* @param hubsize
* @private
*/
exports._determineLevels = function(hubsize) {
var nodeId, node;
// determine hubs
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
if (node.edges.length == hubsize) {
node.level = 0;
}
}
}
// branch from hubs
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
if (node.level == 0) {
this._setLevel(1,node.edges,node.id);
}
}
}
};
/**
* this function allocates nodes in levels based on the direction of the edges
*
* @param hubsize
* @private
*/
exports._determineLevelsDirected = function() {
var nodeId, node, firstNode;
var minLevel = 10000;
// set first node to source
firstNode = this.body.nodes[this.nodeIndices[0]];
firstNode.level = minLevel;
this._setLevelDirected(minLevel,firstNode.edges,firstNode.id);
// get the minimum level
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
minLevel = node.level < minLevel ? node.level : minLevel;
}
}
// subtract the minimum from the set so we have a range starting from 0
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
node.level -= minLevel;
}
}
};
/**
* Since hierarchical layout does not support:
* - smooth curves (based on the physics),
* - clustering (based on dynamic node counts)
*
* We disable both features so there will be no problems.
*
* @private
*/
exports._changeConstants = function() {
this.constants.clustering.enabled = false;
this.constants.physics.barnesHut.enabled = false;
this.constants.physics.hierarchicalRepulsion.enabled = true;
this._loadSelectedForceSolver();
if (this.constants.smoothCurves.enabled == true) {
this.constants.smoothCurves.dynamic = false;
}
this._configureSmoothCurves();
var config = this.constants.hierarchicalLayout;
config.levelSeparation = Math.abs(config.levelSeparation);
if (config.direction == "RL" || config.direction == "DU") {
config.levelSeparation *= -1;
}
if (config.direction == "RL" || config.direction == "LR") {
if (this.constants.smoothCurves.enabled == true) {
this.constants.smoothCurves.type = "vertical";
}
}
else {
if (this.constants.smoothCurves.enabled == true) {
this.constants.smoothCurves.type = "horizontal";
}
}
};
/**
* This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
* on a X position that ensures there will be no overlap.
*
* @param edges
* @param parentId
* @param distribution
* @param parentLevel
* @private
*/
exports._placeBranchNodes = function(edges, parentId, distribution, parentLevel) {
for (var i = 0; i < edges.length; i++) {
var childNode = null;
if (edges[i].toId == parentId) {
childNode = edges[i].from;
}
else {
childNode = edges[i].to;
}
// if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
var nodeMoved = false;
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
if (childNode.xFixed && childNode.level > parentLevel) {
childNode.xFixed = false;
childNode.x = distribution[childNode.level].minPos;
nodeMoved = true;
}
}
else {
if (childNode.yFixed && childNode.level > parentLevel) {
childNode.yFixed = false;
childNode.y = distribution[childNode.level].minPos;
nodeMoved = true;
}
}
if (nodeMoved == true) {
distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
if (childNode.edges.length > 1) {
this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
}
}
}
};
/**
* this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
*
* @param level
* @param edges
* @param parentId
* @private
*/
exports._setLevel = function(level, edges, parentId) {
for (var i = 0; i < edges.length; i++) {
var childNode = null;
if (edges[i].toId == parentId) {
childNode = edges[i].from;
}
else {
childNode = edges[i].to;
}
if (childNode.level == -1 || childNode.level > level) {
childNode.level = level;
if (childNode.edges.length > 1) {
this._setLevel(level+1, childNode.edges, childNode.id);
}
}
}
};
/**
* this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction
*
* @param level
* @param edges
* @param parentId
* @private
*/
exports._setLevelDirected = function(level, edges, parentId) {
this.body.nodes[parentId].hierarchyEnumerated = true;
var childNode, direction;
for (var i = 0; i < edges.length; i++) {
direction = 1;
if (edges[i].toId == parentId) {
childNode = edges[i].from;
direction = -1;
}
else {
childNode = edges[i].to;
}
if (childNode.level == -1) {
childNode.level = level + direction;
}
}
for (var i = 0; i < edges.length; i++) {
if (edges[i].toId == parentId) {childNode = edges[i].from;}
else {childNode = edges[i].to;}
if (childNode.edges.length > 1 && childNode.hierarchyEnumerated === false) {
this._setLevelDirected(childNode.level, childNode.edges, childNode.id);
}
}
};
/**
* Unfix nodes
*
* @private
*/
exports._restoreNodes = function() {
for (var nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
this.body.nodes[nodeId].xFixed = false;
this.body.nodes[nodeId].yFixed = false;
}
}
};

+ 1
- 1
lib/network/modules/CanvasRenderer.js View File

@ -29,7 +29,7 @@ class CanvasRenderer {
this.body.emitter.on("dragStart", () => {this.dragging = true;});
this.body.emitter.on("dragEnd", () => this.dragging = false);
this.body.emitter.on("_redraw", () => {if (this.renderingActive === false) {this._redraw();}});
this.body.emitter.on("_redraw", () => {console.log(this.renderingActive); if (this.renderingActive === false) {this._redraw();}});
this.body.emitter.on("_requestRedraw", this._requestRedraw.bind(this));
this.body.emitter.on("_startRendering", () => {this.renderRequests += 1; this.renderingActive = true; this.startRendering();});
this.body.emitter.on("_stopRendering", () => {this.renderRequests -= 1; this.renderingActive = this.renderRequests > 0;});

+ 42
- 9
lib/network/modules/EdgesHandler.js View File

@ -96,20 +96,53 @@ class EdgesHandler {
};
util.extend(this.options, this.defaultOptions);
// this allows external modules to force all dynamic curves to turn static.
this.body.emitter.on("_forceDisableDynamicCurves", (type) => {
let emitChange = false;
for (let edgeId in this.body.edges) {
if (this.body.edges.hasOwnProperty(edgeId)) {
let edgeOptions = this.body.edges[edgeId].options.smooth;
if (edgeOptions.enabled === true && edgeOptions.dynamic === true) {
if (type === undefined) {
edge.setOptions({smooth:false});
}
else {
edge.setOptions({smooth:{dynamic:false, type:type}});
}
emitChange = true;
}
}
}
if (emitChange === true) {
this.body.emitter.emit("_dataChanged");
}
})
}
setOptions(options) {
if (options) {
if (options.color !== undefined) {
if (util.isString(options.color)) {
util.assignAllKeys(this.options.color, options.color);
}
else {
util.extend(this.options.color, options.color);
}
this.options.color.inherit.enabled = false;
if (options !== undefined) {
if (options.color !== undefined) {
if (util.isString(options.color)) {
util.assignAllKeys(this.options.color, options.color);
}
else {
util.extend(this.options.color, options.color);
}
this.options.color.inherit.enabled = false;
}
util.mergeOptions(this.options, options, 'smooth');
util.mergeOptions(this.options, options, 'dashes');
if (options.arrows !== undefined) {
util.mergeOptions(this.options.arrows, options.arrows, 'to');
util.mergeOptions(this.options.arrows, options.arrows, 'middle');
util.mergeOptions(this.options.arrows, options.arrows, 'from');
}
}
}

+ 201
- 230
lib/network/modules/LayoutEngine.js View File

@ -2,34 +2,99 @@
* Created by Alex on 3/3/2015.
*/
var util = require('../../util');
class LayoutEngine {
constructor(body) {
this.body = body;
this.options = {};
this.defaultOptions = {
hierarchical: {
enabled:false,
levelSeparation: 150,
direction: "UD", // UD, DU, LR, RL
sortMethod: "hubsize" // hubsize, directed
}
}
util.extend(this.options, this.defaultOptions);
this.hierarchicalLevels = {};
this.body.emitter.on("_dataChanged", () => {
this.setupHierarchicalLayout();
})
}
setOptions(options) {
setOptions(options, allOptions) {
if (options !== undefined) {
util.mergeOptions(this.options, options, 'hierarchical');
if (this.options.hierarchical.enabled === true) {
// make sure the level seperation is the right way up
if (this.options.hierarchical.direction == "RL" || this.options.hierarchical.direction == "DU") {
if (this.options.hierarchical.levelSeparation > 0) {
this.options.hierarchical.levelSeparation *= -1;
}
}
else {
if (this.options.hierarchical.levelSeparation < 0) {
this.options.hierarchical.levelSeparation *= -1;
}
}
// because the hierarchical system needs it's own physics and smooth curve settings, we adapt the other options if needed.
return this.adaptAllOptions(allOptions);
}
}
return allOptions;
}
positionInitially(nodesArray) {
for (var i = 0; i < nodesArray.length; i++) {
let node = nodesArray[i];
if ((!node.isFixed()) && (node.x === null || node.y === null)) {
var radius = 10 * 0.1*nodesArray.length + 10;
var angle = 2 * Math.PI * Math.random();
if (node.options.fixed.x == false) {node.x = radius * Math.cos(angle);}
if (node.options.fixed.x == false) {node.y = radius * Math.sin(angle);}
adaptAllOptions(allOptions) {
if (this.options.hierarchical.enabled === true) {
// set the physics
if (allOptions.physics === undefined || allOptions.physics === true) {
allOptions.physics = {solver: 'hierarchicalRepulsion'};
}
else if (options.physics !== false) {
allOptions.physics['solver'] = 'hierarchicalRepulsion';
}
// get the type of static smooth curve in case it is required
let type = 'horizontal';
if (this.options.hierarchical.direction == "RL" || this.options.hierarchical.direction == "LR") {
type = 'vertical';
}
// disable smooth curves if nothing is defined. If smooth curves have been turned on, turn them into static smooth curves.
if (allOptions.edges === undefined) {
allOptions.edges = {smooth: false};
}
else if (allOptions.edges.smooth === undefined) {
allOptions.edges.smooth = false;
}
else {
allOptions.edges.smooth = {enabled: true, dynamic: false, type:type}
}
// force all edges into static smooth curves.
this.body.emitter.emit('_forceDisableDynamicCurves', type);
}
return allOptions;
}
_resetLevels() {
for (var nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
var node = this.body.nodes[nodeId];
if (node.preassignedLevel == false) {
node.level = -1;
node.hierarchyEnumerated = false;
positionInitially(nodesArray) {
if (this.options.hierarchical.enabled !== true) {
for (let i = 0; i < nodesArray.length; i++) {
let node = nodesArray[i];
if ((!node.isFixed()) && (node.x === undefined || node.y === undefined)) {
let radius = 10 * 0.1 * nodesArray.length + 10;
let angle = 2 * Math.PI * Math.random();
if (node.options.fixed.x == false) {
node.x = radius * Math.cos(angle);
}
if (node.options.fixed.x == false) {
node.y = radius * Math.sin(angle);
}
}
}
}
@ -41,61 +106,58 @@ class LayoutEngine {
*
* @private
*/
_setupHierarchicalLayout() {
if (this.constants.hierarchicalLayout.enabled == true && this.nodeIndices.length > 0) {
setupHierarchicalLayout() {
if (this.options.hierarchical.enabled == true && this.body.nodeIndices.length > 0) {
// get the size of the largest hubs and check if the user has defined a level for a node.
var hubsize = 0;
var node, nodeId;
var definedLevel = false;
var undefinedLevel = false;
let hubsize = 0;
let node, nodeId;
let definedLevel = false;
let undefinedLevel = false;
this.hierarchicalLevels = {};
this.nodeSpacing = 100;
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
if (node.level != -1) {
if (node.options.level !== undefined) {
definedLevel = true;
this.hierarchicalLevels[nodeId] = node.options.level;
}
else {
undefinedLevel = true;
}
if (hubsize < node.edges.length) {
hubsize = node.edges.length;
}
hubsize = hubsize < node.edges.length ? node.edges.length : hubsize;
}
}
// if the user defined some levels but not all, alert and run without hierarchical layout
if (undefinedLevel == true && definedLevel == true) {
throw new Error("To use the hierarchical layout, nodes require either no predefined levels or levels have to be defined for all nodes.");
this.zoomExtent({duration:0},true,this.constants.clustering.enabled);
if (!this.constants.clustering.enabled) {
this.start();
}
return;
}
else {
// setup the system to use hierarchical method.
this._changeConstants();
//this._changeConstants();
// define levels if undefined by the users. Based on hubsize
if (undefinedLevel == true) {
if (this.constants.hierarchicalLayout.layout == "hubsize") {
if (this.options.hierarchical.sortMethod == "hubsize") {
this._determineLevels(hubsize);
}
else {
this._determineLevelsDirected(false);
else if (this.options.hierarchical.sortMethod == "directed" || "direction") {
this._determineLevelsDirected();
}
}
// check the distribution of the nodes per level.
var distribution = this._getDistribution();
let distribution = this._getDistribution();
// place the nodes on the canvas. This also stablilizes the system. Redraw in started automatically after stabilize.
// place the nodes on the canvas.
this._placeNodesByHierarchy(distribution);
}
}
}
/**
* This function places the nodes on the canvas based on the hierarchial distribution.
*
@ -103,39 +165,31 @@ class LayoutEngine {
* @private
*/
_placeNodesByHierarchy(distribution) {
var nodeId, node;
let nodeId, node;
this.positionedNodes = {};
// start placing all the level 0 nodes first. Then recursively position their branches.
for (var level in distribution) {
for (let level in distribution) {
if (distribution.hasOwnProperty(level)) {
for (nodeId in distribution[level].nodes) {
if (distribution[level].nodes.hasOwnProperty(nodeId)) {
node = distribution[level].nodes[nodeId];
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
if (node.xFixed) {
node.x = distribution[level].minPos;
node.xFixed = false;
distribution[level].minPos += distribution[level].nodeSpacing;
}
node = distribution[level].nodes[nodeId];
if (this.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") {
if (node.x === undefined) {node.x = distribution[level].distance;}
distribution[level].distance = node.x + this.nodeSpacing;
}
else {
if (node.yFixed) {
node.y = distribution[level].minPos;
node.yFixed = false;
distribution[level].minPos += distribution[level].nodeSpacing;
}
if (node.y === undefined) {node.y = distribution[level].distance;}
distribution[level].distance = node.y + this.nodeSpacing;
}
this._placeBranchNodes(node.edges,node.id,distribution,node.level);
this.positionedNodes[nodeId] = true;
this._placeBranchNodes(node.edges,node.id,distribution,level);
}
}
}
}
// stabilize the system after positioning. This function calls zoomExtent.
this._stabilize();
}
@ -146,49 +200,29 @@ class LayoutEngine {
* @private
*/
_getDistribution() {
var distribution = {};
var nodeId, node, level;
let distribution = {};
let nodeId, node;
// we fix Y because the hierarchy is vertical, we fix X so we do not give a node an x position for a second time.
// the fix of X is removed after the x value has been set.
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
node.xFixed = true;
node.yFixed = true;
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
node.y = this.constants.hierarchicalLayout.levelSeparation*node.level;
if (this.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") {
node.y = this.options.hierarchical.levelSeparation * this.hierarchicalLevels[nodeId];
node.options.fixed.y = true;
}
else {
node.x = this.constants.hierarchicalLayout.levelSeparation*node.level;
node.x = this.options.hierarchical.levelSeparation * this.hierarchicalLevels[nodeId];
node.options.fixed.x = true;
}
if (distribution[node.level] === undefined) {
distribution[node.level] = {amount: 0, nodes: {}, minPos:0, nodeSpacing:0};
if (distribution[this.hierarchicalLevels[nodeId]] === undefined) {
distribution[this.hierarchicalLevels[nodeId]] = {amount: 0, nodes: {}, distance: 0};
}
distribution[node.level].amount += 1;
distribution[node.level].nodes[nodeId] = node;
distribution[this.hierarchicalLevels[nodeId]].amount += 1;
distribution[this.hierarchicalLevels[nodeId]].nodes[nodeId] = node;
}
}
// determine the largest amount of nodes of all levels
var maxCount = 0;
for (level in distribution) {
if (distribution.hasOwnProperty(level)) {
if (maxCount < distribution[level].amount) {
maxCount = distribution[level].amount;
}
}
}
// set the initial position and spacing of each nodes accordingly
for (level in distribution) {
if (distribution.hasOwnProperty(level)) {
distribution[level].nodeSpacing = (maxCount + 1) * this.constants.hierarchicalLayout.nodeSpacing;
distribution[level].nodeSpacing /= (distribution[level].amount + 1);
distribution[level].minPos = distribution[level].nodeSpacing - (0.5 * (distribution[level].amount + 1) * distribution[level].nodeSpacing);
}
}
return distribution;
}
@ -200,14 +234,14 @@ class LayoutEngine {
* @private
*/
_determineLevels(hubsize) {
var nodeId, node;
let nodeId, node;
// determine hubs
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
if (node.edges.length == hubsize) {
node.level = 0;
this.hierarchicalLevels[nodeId] = 0;
}
}
}
@ -216,13 +250,39 @@ class LayoutEngine {
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
if (node.level == 0) {
if (this.hierarchicalLevels[nodeId] == 0) {
this._setLevel(1,node.edges,node.id);
}
}
}
}
/**
* this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
*
* @param level
* @param edges
* @param parentId
* @private
*/
_setLevel(level, edges, parentId) {
for (let i = 0; i < edges.length; i++) {
let childNode;
if (edges[i].toId == parentId) {
childNode = edges[i].from;
}
else {
childNode = edges[i].to;
}
if (this.hierarchicalLevels[childNode.id] === undefined || this.hierarchicalLevels[childNode.id] > level) {
this.hierarchicalLevels[childNode.id] = level;
if (childNode.edges.length > 1) {
this._setLevel(level+1, childNode.edges, childNode.id);
}
}
}
}
/**
@ -232,70 +292,57 @@ class LayoutEngine {
* @private
*/
_determineLevelsDirected() {
var nodeId, node, firstNode;
var minLevel = 10000;
let nodeId, firstNode;
let minLevel = 10000;
// set first node to source
firstNode = this.body.nodes[this.nodeIndices[0]];
firstNode.level = minLevel;
this._setLevelDirected(minLevel,firstNode.edges,firstNode.id);
firstNode = this.body.nodes[this.body.nodeIndices[0]];
this._setLevelDirected(minLevel,firstNode);
// get the minimum level
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
minLevel = node.level < minLevel ? node.level : minLevel;
minLevel = this.hierarchicalLevels[nodeId] < minLevel ? this.hierarchicalLevels[nodeId] : minLevel;
}
}
// subtract the minimum from the set so we have a range starting from 0
for (nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
node = this.body.nodes[nodeId];
node.level -= minLevel;
this.hierarchicalLevels[nodeId] -= minLevel;
}
}
}
/**
* Since hierarchical layout does not support:
* - smooth curves (based on the physics),
* - clustering (based on dynamic node counts)
*
* We disable both features so there will be no problems.
* this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction
*
* @param level
* @param edges
* @param parentId
* @private
*/
_changeConstants() {
this.constants.clustering.enabled = false;
this.constants.physics.barnesHut.enabled = false;
this.constants.physics.hierarchicalRepulsion.enabled = true;
this._loadSelectedForceSolver();
if (this.constants.smoothCurves.enabled == true) {
this.constants.smoothCurves.dynamic = false;
}
this._configureSmoothCurves();
_setLevelDirected(level, node) {
if (this.hierarchicalLevels[node.id] !== undefined)
return;
var config = this.constants.hierarchicalLayout;
config.levelSeparation = Math.abs(config.levelSeparation);
if (config.direction == "RL" || config.direction == "DU") {
config.levelSeparation *= -1;
}
let childNode;
this.hierarchicalLevels[node.id] = level;
if (config.direction == "RL" || config.direction == "LR") {
if (this.constants.smoothCurves.enabled == true) {
this.constants.smoothCurves.type = "vertical";
for (let i = 0; i < node.edges.length; i++) {
if (node.edges[i].toId == node.id) {
childNode = node.edges[i].from;
this._setLevelDirected(level - 1, childNode);
}
}
else {
if (this.constants.smoothCurves.enabled == true) {
this.constants.smoothCurves.type = "horizontal";
else {
childNode = node.edges[i].to;
this._setLevelDirected(level + 1, childNode);
}
}
}
/**
* This is a recursively called function to enumerate the branches from the largest hubs and place the nodes
* on a X position that ensures there will be no overlap.
@ -307,119 +354,43 @@ class LayoutEngine {
* @private
*/
_placeBranchNodes(edges, parentId, distribution, parentLevel) {
for (var i = 0; i < edges.length; i++) {
var childNode = null;
for (let i = 0; i < edges.length; i++) {
let childNode = undefined;
let parentNode = undefined;
if (edges[i].toId == parentId) {
childNode = edges[i].from;
parentNode = edges[i].to;
}
else {
childNode = edges[i].to;
parentNode = edges[i].from;
}
let childNodeLevel = this.hierarchicalLevels[childNode.id];
if (this.positionedNodes[childNode.id] === undefined) {
// if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
if (childNodeLevel > parentLevel) {
if (this.options.hierarchical.direction == "UD" || this.options.hierarchical.direction == "DU") {
if (childNode.x === undefined) {
childNode.x = Math.max(distribution[childNodeLevel].distance, parentNode.x);
}
distribution[childNodeLevel].distance = childNode.x + this.nodeSpacing;
this.positionedNodes[childNode.id] = true;
}
else {
if (childNode.y === undefined) {
childNode.y = Math.max(distribution[childNodeLevel].distance, parentNode.y)
}
distribution[childNodeLevel].distance = childNode.y + this.nodeSpacing;
}
this.positionedNodes[childNode.id] = true;
// if a node is conneceted to another node on the same level (or higher (means lower level))!, this is not handled here.
var nodeMoved = false;
if (this.constants.hierarchicalLayout.direction == "UD" || this.constants.hierarchicalLayout.direction == "DU") {
if (childNode.xFixed && childNode.level > parentLevel) {
childNode.xFixed = false;
childNode.x = distribution[childNode.level].minPos;
nodeMoved = true;
}
}
else {
if (childNode.yFixed && childNode.level > parentLevel) {
childNode.yFixed = false;
childNode.y = distribution[childNode.level].minPos;
nodeMoved = true;
}
}
if (nodeMoved == true) {
distribution[childNode.level].minPos += distribution[childNode.level].nodeSpacing;
if (childNode.edges.length > 1) {
this._placeBranchNodes(childNode.edges,childNode.id,distribution,childNode.level);
}
}
}
}
/**
* this function is called recursively to enumerate the barnches of the largest hubs and give each node a level.
*
* @param level
* @param edges
* @param parentId
* @private
*/
_setLevel(level, edges, parentId) {
for (var i = 0; i < edges.length; i++) {
var childNode = null;
if (edges[i].toId == parentId) {
childNode = edges[i].from;
}
else {
childNode = edges[i].to;
}
if (childNode.level == -1 || childNode.level > level) {
childNode.level = level;
if (childNode.edges.length > 1) {
this._setLevel(level+1, childNode.edges, childNode.id);
if (childNode.edges.length > 1) {
this._placeBranchNodes(childNode.edges, childNode.id, distribution, childNodeLevel);
}
}
}
}
}
/**
* this function is called recursively to enumerate the branched of the first node and give each node a level based on edge direction
*
* @param level
* @param edges
* @param parentId
* @private
*/
_setLevelDirected(level, edges, parentId) {
this.body.nodes[parentId].hierarchyEnumerated = true;
var childNode, direction;
for (var i = 0; i < edges.length; i++) {
direction = 1;
if (edges[i].toId == parentId) {
childNode = edges[i].from;
direction = -1;
}
else {
childNode = edges[i].to;
}
if (childNode.level == -1) {
childNode.level = level + direction;
}
}
for (var i = 0; i < edges.length; i++) {
if (edges[i].toId == parentId) {childNode = edges[i].from;}
else {childNode = edges[i].to;}
if (childNode.edges.length > 1 && childNode.hierarchyEnumerated === false) {
this._setLevelDirected(childNode.level, childNode.edges, childNode.id);
}
}
}
/**
* Unfix nodes
*
* @private
*/
_restoreNodes() {
for (var nodeId in this.body.nodes) {
if (this.body.nodes.hasOwnProperty(nodeId)) {
this.body.nodes[nodeId].xFixed = false;
this.body.nodes[nodeId].yFixed = false;
}
}
}
}
export default LayoutEngine;

+ 4
- 1
lib/network/modules/NodesHandler.js View File

@ -64,6 +64,7 @@ class NodesHandler {
},
image: undefined, // --> URL
label: undefined,
level: undefined,
mass: 1,
physics: true,
scaling: {
@ -88,7 +89,9 @@ class NodesHandler {
},
shape: 'ellipse',
size: 25,
value: 1
value: 1,
x: undefined,
y: undefined
};
util.extend(this.options, this.defaultOptions);

+ 38
- 21
lib/network/modules/PhysicsEngine.js View File

@ -2,12 +2,12 @@
* Created by Alex on 2/23/2015.
*/
import {BarnesHutSolver} from "./components/physics/BarnesHutSolver";
import {Repulsion} from "./components/physics/RepulsionSolver";
import {HierarchicalRepulsion} from "./components/physics/HierarchicalRepulsionSolver";
import {SpringSolver} from "./components/physics/SpringSolver";
import {HierarchicalSpringSolver} from "./components/physics/HierarchicalSpringSolver";
import {CentralGravitySolver} from "./components/physics/CentralGravitySolver";
import BarnesHutSolver from "./components/physics/BarnesHutSolver";
import Repulsion from "./components/physics/RepulsionSolver";
import HierarchicalRepulsion from "./components/physics/HierarchicalRepulsionSolver";
import SpringSolver from "./components/physics/SpringSolver";
import HierarchicalSpringSolver from "./components/physics/HierarchicalSpringSolver";
import CentralGravitySolver from "./components/physics/CentralGravitySolver";
var util = require('../../util');
@ -17,6 +17,7 @@ class PhysicsEngine {
this.body = body;
this.physicsBody = {physicsNodeIndices:[], physicsEdgeIndices:[], forces: {}, velocities: {}};
this.physicsEnabled = true;
this.simulationInterval = 1000 / 60;
this.requiresTimeout = true;
this.previousStates = {};
@ -49,7 +50,7 @@ class PhysicsEngine {
centralGravity: 0.0,
springLength: 100,
springConstant: 0.01,
nodeDistance: 150,
nodeDistance: 120,
damping: 0.09
},
solver: 'BarnesHut',
@ -74,15 +75,20 @@ class PhysicsEngine {
this.runSimulation();
}
})
this.body.emitter.on("stopSimulation", () => {this.stopSimulation();});
this.body.emitter.on("stopSimulation", () => {console.log(4);this.stopSimulation();});
}
setOptions(options) {
if (options !== undefined) {
util.selectiveNotDeepExtend(['stabilization'],this.options, options);
util.mergeOptions(this.options, options, 'stabilization')
if (options === false) {
this.physicsEnabled = false;
}
else {
if (options !== undefined) {
util.selectiveNotDeepExtend(['stabilization'],this.options, options);
util.mergeOptions(this.options, options, 'stabilization')
}
this.init();
}
this.init();
}
@ -109,14 +115,20 @@ class PhysicsEngine {
}
initPhysics() {
this.stabilized = false;
if (this.options.stabilization.enabled === true) {
this.stabilize();
if (this.physicsEnabled === true) {
this.stabilized = false;
if (this.options.stabilization.enabled === true) {
this.stabilize();
}
else {
this.ready = true;
this.body.emitter.emit("zoomExtent", {duration: 0}, true)
this.runSimulation();
}
}
else {
this.ready = true;
this.body.emitter.emit("zoomExtent", {duration:0}, true)
this.runSimulation();
this.body.emitter.emit("_redraw");
}
}
@ -130,10 +142,15 @@ class PhysicsEngine {
}
runSimulation() {
if (this.viewFunction === undefined) {
this.viewFunction = this.simulationStep.bind(this);
this.body.emitter.on("initRedraw", this.viewFunction);
this.body.emitter.emit("_startRendering");
if (this.physicsEnabled === true) {
if (this.viewFunction === undefined) {
this.viewFunction = this.simulationStep.bind(this);
this.body.emitter.on("initRedraw", this.viewFunction);
this.body.emitter.emit("_startRendering");
}
}
else {
this.body.emitter.emit("_redraw");
}
}

+ 6
- 3
lib/network/modules/components/Node.js View File

@ -54,8 +54,8 @@ class Node {
this.grouplist = grouplist;
// state options
this.x = null;
this.y = null;
this.x = undefined;
this.y = undefined;
this.predefinedPosition = false; // used to check if initial zoomExtent should just take the range or approximate
this.selected = false;
this.hover = false;
@ -117,10 +117,13 @@ class Node {
'id',
'image',
'label',
'level',
'physics',
'shape',
'size',
'value'
'value',
'x',
'y'
];
util.selectiveDeepExtend(fields, this.options, options);
// basic options

+ 1
- 1
lib/network/modules/components/physics/BarnesHutSolver.js View File

@ -443,4 +443,4 @@ class BarnesHutSolver {
}
export {BarnesHutSolver};
export default BarnesHutSolver;

+ 1
- 1
lib/network/modules/components/physics/CentralGravitySolver.js View File

@ -38,4 +38,4 @@ class CentralGravitySolver {
}
export {CentralGravitySolver};
export default CentralGravitySolver;

+ 1
- 1
lib/network/modules/components/physics/HierarchicalRepulsionSolver.js View File

@ -70,4 +70,4 @@ class HierarchicalRepulsionSolver {
}
export {HierarchicalRepulsionSolver};
export default HierarchicalRepulsionSolver;

+ 1
- 1
lib/network/modules/components/physics/HierarchicalSpringSolver.js View File

@ -100,4 +100,4 @@ class HierarchicalSpringSolver {
}
export {HierarchicalSpringSolver};
export default HierarchicalSpringSolver;

+ 1
- 1
lib/network/modules/components/physics/RepulsionSolver.js View File

@ -72,4 +72,4 @@ class RepulsionSolver {
}
export {RepulsionSolver};
export default RepulsionSolver;

+ 1
- 1
lib/network/modules/components/physics/SpringSolver.js View File

@ -80,4 +80,4 @@ class SpringSolver {
}
}
export {SpringSolver};
export default SpringSolver;

Loading…
Cancel
Save