Browse Source

Included hubaggregation clustering, changed the launch sequence to speed up the loading of big data.

css_transitions
Alex de Mulder 10 years ago
parent
commit
93c77fdfaf
6 changed files with 658 additions and 213 deletions
  1. +6
    -0
      src/graph/Edge.js
  2. +27
    -23
      src/graph/Graph.js
  3. +7
    -6
      src/graph/Node.js
  4. +273
    -73
      src/graph/cluster.js
  5. +338
    -104
      vis.js
  6. +7
    -7
      vis.min.js

+ 6
- 0
src/graph/Edge.js View File

@ -35,6 +35,12 @@ function Edge (properties, graph, constants) {
this.from = null; // a node
this.to = null; // a node
// we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
// by storing the original information we can revert to the original connection when the cluser is opened.
this.originalFromID = [];
this.originalToID = [];
this.connected = false;
// Added to support dashed lines

+ 27
- 23
src/graph/Graph.js View File

@ -92,7 +92,6 @@ function Graph (container, data, options) {
Cluster.call(this);
var graph = this;
this.simulationStep = 100;
this.nodeIndices = []; // the node indices list is used to speed up the computation of the repulsion fields
this.nodes = {}; // object with Node objects
this.edges = {}; // object with Edge objects
@ -136,7 +135,7 @@ function Graph (container, data, options) {
me.start();
}
};
console.log("here")
this.groups = new Groups(); // object with groups
this.images = new Images(); // object with images
this.images.setOnloadCallback(function () {
@ -164,6 +163,15 @@ function Graph (container, data, options) {
// cluster if the data set is big
this.clusterToFit();
// updates the lables after clustering
this._updateLabels();
// find a stable position or start animating to a stable position
if (this.stabilize) {
this._doStabilize();
}
this.start();
}
/**
@ -172,22 +180,25 @@ function Graph (container, data, options) {
*/
Graph.prototype = Object.create(Cluster.prototype);
/**
* This function clusters untill the maxNumberOfNodes has been reached
*/
Graph.prototype.clusterToFit = function() {
var numberOfNodes = this.nodeIndices.length;
var maxNumberOfNodes = 100;
var maxNumberOfNodes = 100; // TODO: set in constants
var maxLevels = 10;
var level = 0;
this.simulationStep = 100;
while (numberOfNodes >= maxNumberOfNodes && level < maxLevels) {
console.log(level)
this.increaseClusterLevel();
if (level % 2 == 0) {
this.increaseClusterLevel();
}
else {
this.aggregateHubs();
}
numberOfNodes = this.nodeIndices.length;
level += 1;
this.simulationStep -= 20;
}
// after the clustering we reposition the nodes to avoid initial chaos
@ -257,13 +268,6 @@ Graph.prototype.setData = function(data) {
this._setEdges(data && data.edges);
}
// updating the list of node indices
// find a stable position or start animating to a stable position
if (this.stabilize) {
this._doStabilize();
}
this.start();
};
/**
@ -404,10 +408,10 @@ Graph.prototype._create = function () {
this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
this.mouseTrap = mouseTrap;
this.mouseTrap.bind("=", this.decreaseClusterLevel.bind(me));
this.mouseTrap.bind("=",this.decreaseClusterLevel.bind(me));
this.mouseTrap.bind("-",this.increaseClusterLevel.bind(me));
this.mouseTrap.bind("s",this.singleStep.bind(me));
this.mouseTrap.bind("h",this._forceClustersByHub.bind(me));
// add the frame to the container element
this.containerElement.appendChild(this.frame);
@ -664,8 +668,8 @@ Graph.prototype._zoom = function(scale, pointer) {
var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
this.zoomCenter = {"x" : pointer.x - translation.x,
"y" : pointer.y - translation.y};
this.zoomCenter = {"x" : this._canvasToX(pointer.x),
"y" : this._canvasToY(pointer.y)};
// this.zoomCenter = {"x" : pointer.x,"y" : pointer.y };
this._setScale(scale);
@ -1304,6 +1308,7 @@ Graph.prototype._setEdges = function(edges) {
Graph.prototype._addEdges = function (ids) {
var edges = this.edges,
edgesData = this.edgesData;
for (var i = 0, len = ids.length; i < len; i++) {
var id = ids[i];
@ -1312,7 +1317,7 @@ Graph.prototype._addEdges = function (ids) {
oldEdge.disconnect();
}
var data = edgesData.get(id);
var data = edgesData.get(id, {"showInternalIds" : true});
edges[id] = new Edge(data, this, this.constants);
}
@ -1638,8 +1643,7 @@ Graph.prototype._calculateForces = function() {
// the graph
// Also, the forces are reset to zero in this loop by using _setForce instead
// of _addForce
var dynamicGravity = 100.0 - 1*this.simulationStep;
var gravity = (dynamicGravity < 0.05 ? 0.05 : dynamicGravity);
var gravity = 0.05;
for (id in nodes) {
if (nodes.hasOwnProperty(id)) {
var node = nodes[id];

+ 7
- 6
src/graph/Node.js View File

@ -27,6 +27,8 @@ function Node(properties, imagelist, grouplist, constants) {
this.edges = []; // all edges connected to this node
this.dynamicEdges = [];
this.reroutedEdges = {};
this.group = constants.nodes.group;
this.fontSize = constants.nodes.fontSize;
@ -55,8 +57,7 @@ function Node(properties, imagelist, grouplist, constants) {
// creating the variables for clustering
this.resetCluster();
this.remainingEdges = 0;
this.remainingEdges_unapplied = 0;
this.dynamicEdgesLength = 0;
this.clusterSession = 0;
this.clusterSizeWidthFactor = constants.clustering.clusterSizeWidthFactor;
@ -92,8 +93,8 @@ Node.prototype.attachEdge = function(edge) {
if (this.edges.indexOf(edge) == -1) {
this.edges.push(edge);
}
this.remainingEdges = this.edges.length;
this.remainingEdges_unapplied = this.remainingEdges;
this.dynamicEdges = this.edges;
this.dynamicEdgesLength = this.edges.length;
this._updateMass();
};
@ -106,8 +107,8 @@ Node.prototype.detachEdge = function(edge) {
if (index != -1) {
this.edges.splice(index, 1);
}
this.remainingEdges = this.edges.length;
this.remainingEdges_unapplied = this.remainingEdges;
this.dynamicEdges = this.edges;
this.dynamicEdgesLength = this.edges.length;
this._updateMass();
};

+ 273
- 73
src/graph/cluster.js View File

@ -6,8 +6,6 @@ function Cluster() {
this.clusterSession = 0;
}
/**
* This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
* be clustered with their connected node. This can be repeated as many times as needed.
@ -20,7 +18,7 @@ Cluster.prototype.increaseClusterLevel = function() {
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
//this.start();
this.start();
}
};
@ -46,25 +44,8 @@ Cluster.prototype.decreaseClusterLevel = function() {
if (this.moving != isMovingBeforeClustering) {
this.start();
}
};
/**
* This function can be called to open up a specific cluster.
* It will recursively unpack the entire cluster back to individual nodes.
*
* @param node | Node object: cluster to open.
*/
Cluster.prototype.fullyOpenCluster = function(node) {
var isMovingBeforeClustering = this.moving;
this._expandClusterNode(node,true,true);
this._updateNodeIndexList();
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
this._updateLabels();
};
@ -121,14 +102,17 @@ Cluster.prototype._updateClusters = function() {
*/
Cluster.prototype._updateLabels = function() {
// update node labels
//this._updateClusterLabels();
// this._updateNodeLabels();
// Debug :
for (var nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
var node = this.nodes[nodeID];
node.label = String(node.remainingEdges).concat(":",node.remainingEdges_unapplied,":",String(node.clusterSize));
// node.label = String(Math.round(this.zoomCenter.x)).concat(",",String(Math.round(this.zoomCenter.y)),
// "::",String(Math.round(node.x)),"x",String(Math.round(node.y)));
node.label = String(node.dynamicEdges.length).concat(":",node.dynamicEdgesLength,":",String(node.clusterSize),":::",String(node.id));
}
}
};
/**
@ -224,10 +208,8 @@ Cluster.prototype._expandClusterNode = function(parentNode, recursive, forceExpa
Cluster.prototype._parentNodeInActiveArea = function(node) {
if (node.selected)
console.log(node.x,this.zoomCenter.x,node.y, this.zoomCenter.y)
if (Math.abs(node.x - this.zoomCenter.x) <= this.constants.clustering.activeAreaRadius &&
Math.abs(node.y - this.zoomCenter.y) <= this.constants.clustering.activeAreaRadius) {
if (Math.abs(node.x - this.zoomCenter.x) <= this.constants.clustering.activeAreaRadius/this.scale &&
Math.abs(node.y - this.zoomCenter.y) <= this.constants.clustering.activeAreaRadius/this.scale) {
return true;
}
else {
@ -252,16 +234,21 @@ Cluster.prototype._expelChildFromParent = function(parentNode, containedNodeID,
// if child node has been added on smaller scale than current, kick out
if (childNode.formationScale < this.scale || forceExpand == true) {
// put the child node back in the global nodes object and the corresponding edge in the global edges object
// put the child node back in the global nodes object
this.nodes[containedNodeID] = childNode;
this.edges[parentNode.containedEdges[containedNodeID].id] = parentNode.containedEdges[containedNodeID];
// release the contained edges from this childNode back into the global edges
this._releaseContainedEdges(parentNode,childNode)
// reconnect rerouted edges to the childNode
this._connectEdgeBackToChild(parentNode,childNode);
// undo the changes from the clustering operation on the parent node
parentNode.mass -= this.constants.clustering.massTransferCoefficient * childNode.mass;
parentNode.fontSize -= this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
parentNode.clusterSize -= childNode.clusterSize;
parentNode.remainingEdges += 1;
parentNode.remainingEdges_unapplied = parentNode.remainingEdges;
parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
// place the child node near the parent, not at the exact same location to avoid chaos in the system
childNode.x = parentNode.x + this.constants.edges.length * 0.2 * (0.5 - Math.random()) * parentNode.clusterSize;
@ -272,7 +259,6 @@ Cluster.prototype._expelChildFromParent = function(parentNode, containedNodeID,
// remove node from the list
delete parentNode.containedNodes[containedNodeID];
delete parentNode.containedEdges[containedNodeID];
// restart the simulation to reorganise all nodes
this.moving = true;
@ -288,7 +274,6 @@ Cluster.prototype._expelChildFromParent = function(parentNode, containedNodeID,
};
/**
* This function checks if any nodes at the end of their trees have edges below a threshold length
* This function is called only from _updateClusters()
@ -299,34 +284,48 @@ Cluster.prototype._expelChildFromParent = function(parentNode, containedNodeID,
* @param force_level_collapse | Boolean
*/
Cluster.prototype._formClusters = function(forceLevelCollapse) {
var minLength = this.constants.clustering.clusterLength/this.scale;
var amountOfNodes = this.nodeIndices.length;
var dx,dy,length,
edges = this.edges;
if (forceLevelCollapse == false) {
this._formClustersByZoom();
}
else {
this._forceClustersByZoom();
}
if (this.nodeIndices.length != amountOfNodes) { // this means a clustering operation has taken place
this.clusterSession += 1;
}
};
/**
* This function handles the clustering by zooming out, this is based on minimum edge distance
*
* @private
*/
Cluster.prototype._formClustersByZoom = function() {
var dx,dy,length,
minLength = this.constants.clustering.clusterLength/this.scale;
// create an array of edge ids
var edgesIDarray = []
for (var id in edges) {
if (edges.hasOwnProperty(id)) {
for (var id in this.edges) {
if (this.edges.hasOwnProperty(id)) {
edgesIDarray.push(id);
}
}
// check if any edges are shorter than minLength and start the clustering
// the clustering favours the node with the larger mass
for (var i = 0; i < edgesIDarray.length; i++) {
var edgeID = edgesIDarray[i];
var edge = edges[edgeID];
edge.id = edgeID;
var edge = this.edges[edgeID];
if (edge.connected) {
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
length = Math.sqrt(dx * dx + dy * dy);
if (length < minLength || forceLevelCollapse == true) {
// checking for clustering possibilities
if (length < minLength) {
// first check which node is larger
var parentNode = edge.from
var childNode = edge.to
@ -335,51 +334,124 @@ Cluster.prototype._formClusters = function(forceLevelCollapse) {
childNode = edge.from
}
// we allow clustering from outside in, ideally the child node in on the outside
// if we do not cluster from outside in, we would have to reconnect edges or keep a second set of edges for the
// clusters. This will also have to be altered in the force calculation and rendering.
// we allow clustering from outside in
// if we do not cluster from outside in, we would have to reconnect edges or keep
// a second set of edges for the clusters.
// This will also have to be altered in the force calculation and rendering.
// This method is non-destructive and does not require a second set of data.
if (childNode.remainingEdges == 1 && childNode.remainingEdges_unapplied != 0) {
this._addToCluster(parentNode,childNode,edge,forceLevelCollapse);
delete this.edges[edgesIDarray[i]];
if (childNode.dynamicEdgesLength == 1) {
this._addToCluster(parentNode,childNode,edge,false);
}
else if (parentNode.remainingEdges == 1 && parentNode.remainingEdges_unapplied != 0) {
this._addToCluster(childNode,parentNode,edge,forceLevelCollapse);
delete this.edges[edgesIDarray[i]];
else if (parentNode.dynamicEdgesLength == 1) {
this._addToCluster(childNode,parentNode,edge,false);
}
}
}
}
this._updateNodeIndexList();
};
if (forceLevelCollapse == true) {
this._applyClusterLevel();
/**
* This function forces the graph to cluster all nodes with only one connecting edge to their
* connected node.
*
* @private
*/
Cluster.prototype._forceClustersByZoom = function() {
for (var nodeID = 0; nodeID < this.nodeIndices.length; nodeID++) {
var childNode = this.nodes[this.nodeIndices[nodeID]];
if (childNode.dynamicEdgesLength == 1) {
var edge = childNode.dynamicEdges[0];
var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
this._addToCluster(parentNode,childNode,true);
}
}
this._updateNodeIndexList();
this._updateDynamicEdges();
};
if (this.nodeIndices.length != amountOfNodes) { // this means a clustering operation has taken place
this.clusterSession += 1;
Cluster.prototype.aggregateHubs = function() {
var isMovingBeforeClustering = this.moving;
var hubThreshold = 4;
this._forceClustersByHub(hubThreshold);
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
};
/**
*
* @param hubThreshold
* @private
*/
Cluster.prototype._forceClustersByHub = function(hubThreshold) {
for (var nodeID = 0; nodeID < this.nodeIndices.length; nodeID++) {
if (this.nodes.hasOwnProperty(this.nodeIndices[nodeID])) {
var hubNode = this.nodes[this.nodeIndices[nodeID]];
if (hubNode.dynamicEdges.length >= hubThreshold) {
var edgesIDarray = []
var amountOfInitialEdges = hubNode.dynamicEdges.length;
for (var i = 0; i < amountOfInitialEdges; i++) {
edgesIDarray.push(hubNode.dynamicEdges[i].id);
}
for (var i = 0; i < amountOfInitialEdges; i++) {
var edge = this.edges[edgesIDarray[i]];
var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
this._addToCluster(hubNode,childNode,true);
}
break;
}
}
}
this._updateNodeIndexList();
this._updateDynamicEdges();
this._updateLabels();
if (this.moving != isMovingBeforeClustering) {
this.start();
}
};
/**
* This function adds the child node to the parent node, creating a cluster if it is not already.
* This function is called only from _updateClusters()
*
* @param parent_node | Node object: this is the node that will house the child node
* @param child_node | Node object: this node will be deleted from the global this.nodes and stored in the parent node
* @param edge | Edge object: this edge will be deleted from the global this.edges and stored in the parent node
* @param force_level_collapse | Boolean: true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
* @private
*/
Cluster.prototype._addToCluster = function(parentNode, childNode, edge, forceLevelCollapse) {
// join child node and edge in parent node
Cluster.prototype._addToCluster = function(parentNode, childNode, forceLevelCollapse) {
// join child node in the parent node
parentNode.containedNodes[childNode.id] = childNode;
parentNode.containedEdges[childNode.id] = edge; // the edge gets the node ID so we can easily recover it when expanding the cluster
if (this.nodes.hasOwnProperty(childNode.id)) {
delete this.nodes[childNode.id];
if (forceLevelCollapse == false) {
parentNode.dynamicEdgesLength += childNode.dynamicEdges.length - 2;
}
for (var i = 0; i < childNode.dynamicEdges.length; i++) {
var edge = childNode.dynamicEdges[i];
if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
this._addToContainedEdges(parentNode,childNode,edge);
}
else {
console.log('connecting edge to cluster');
this._connectEdgeToCluster(parentNode,childNode,edge);
}
}
childNode.dynamicEdges = [];
// remove the childNode from the global nodes object
delete this.nodes[childNode.id];
childNode.clusterSession = this.clusterSession;
parentNode.mass += this.constants.clustering.massTransferCoefficient * childNode.mass;
@ -392,18 +464,13 @@ Cluster.prototype._addToCluster = function(parentNode, childNode, edge, forceLev
}
else {
parentNode.formationScale = this.scale; // The latest child has been added on this scale
parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
}
// recalculate the size of the node on the next time the node is rendered
parentNode.clearSizeCache();
parentNode.containedNodes[childNode.id].formationScale = this.scale; // this child has been added at this scale.
if (forceLevelCollapse == true) {
parentNode.remainingEdges_unapplied -= 1;
}
else {
parentNode.remainingEdges -= 1;
}
// restart the simulation to reorganise all nodes
this.moving = true;
@ -416,10 +483,10 @@ Cluster.prototype._addToCluster = function(parentNode, childNode, edge, forceLev
* It has to be called if a level is collapsed. It is called by _formClusters().
* @private
*/
Cluster.prototype._applyClusterLevel = function() {
Cluster.prototype._updateDynamicEdges = function() {
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
node.remainingEdges = node.remainingEdges_unapplied;
node.dynamicEdgesLength = node.dynamicEdges.length;
}
};
@ -430,13 +497,146 @@ Cluster.prototype._repositionNodes = function() {
if (!node.isFixed()) {
// TODO: position new nodes in a smarter way!
var radius = this.constants.edges.length * (1 + 0.5*node.clusterSize);
var count = this.nodeIndices.length;
var angle = 2 * Math.PI * Math.random();
node.x = radius * Math.cos(angle);
node.y = radius * Math.sin(angle);
}
}
};
/**
* This adds an edge from the childNode to the contained edges of the parent node
*
* @param parentNode | Node object
* @param childNode | Node object
* @param edge | Edge object
* @private
*/
Cluster.prototype._addToContainedEdges = function(parentNode, childNode, edge) {
// create an array object if it does not yet exist for this childNode
if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
parentNode.containedEdges[childNode.id] = []
}
// add this edge to the list
parentNode.containedEdges[childNode.id].push(edge);
// remove the edge from the global edges object
delete this.edges[edge.id];
// remove the edge from the parent object
for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
if (parentNode.dynamicEdges[i].id == edge.id) {
parentNode.dynamicEdges.splice(i,1);
break;
}
}
};
/**
* This function connects an edge that was connected to a child node to the parent node.
* It keeps track of which nodes it has been connected to with the originalID array.
*
* @param parentNode | Node object
* @param childNode | Node object
* @param edge | Edge object
* @private
*/
Cluster.prototype._connectEdgeToCluster = function(parentNode, childNode, edge) {
if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
edge.originalToID.push(childNode.id);
edge.to = parentNode;
edge.toId = parentNode.id;
}
else { // edge connected to other node with the "from" side
edge.originalFromID.push(childNode.id);
edge.from = parentNode;
edge.fromId = parentNode.id;
}
};
this._addToReroutedEdges(parentNode,childNode,edge);
};
/**
* This adds an edge from the childNode to the rerouted edges of the parent node
*
* @param parentNode | Node object
* @param childNode | Node object
* @param edge | Edge object
* @private
*/
Cluster.prototype._addToReroutedEdges = function(parentNode, childNode, edge) {
// create an array object if it does not yet exist for this childNode
// we store the edge in the rerouted edges so we can restore it when the cluster pops open
if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
parentNode.reroutedEdges[childNode.id] = [];
}
parentNode.reroutedEdges[childNode.id].push(edge);
// this edge becomes part of the dynamicEdges of the cluster node
parentNode.dynamicEdges.push(edge);
};
/**
* This function connects an edge that was connected to a cluster node back to the child node.
*
* @param parentNode | Node object
* @param childNode | Node object
* @private
*/
Cluster.prototype._connectEdgeBackToChild = function(parentNode, childNode) {
if (parentNode.reroutedEdges[childNode.id] != undefined) {
for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
var edge = parentNode.reroutedEdges[childNode.id][i];
if (edge.originalFromID[edge.originalFromID.length-1] == childNode.id) {
edge.originalFromID.pop();
edge.fromId = childNode.id;
edge.from = childNode;
}
else {
edge.originalToID.pop();
edge.toId = childNode.id;
edge.to = childNode;
}
// append this edge to the list of edges connecting to the childnode
childNode.dynamicEdges.push(edge);
// remove the edge from the parent object
for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
if (parentNode.dynamicEdges[j].id == edge.id) {
parentNode.dynamicEdges.splice(j,1);
break;
}
}
}
// remove the entry from the rerouted edges
delete parentNode.reroutedEdges[childNode.id];
}
};
/**
* This function released the contained edges back into the global domain and puts them back into the
* dynamic edges of both parent and child.
*
* @param parentNode | Node object
* @param childNode | Node object
* @private
*/
Cluster.prototype._releaseContainedEdges = function(parentNode, childNode) {
for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
var edge = parentNode.containedEdges[childNode.id][i];
// put the edge back in the global edges object
this.edges[edge.id] = edge;
// put the edge back in the dynamic edges of the child and parent
childNode.dynamicEdges.push(edge);
parentNode.dynamicEdges.push(edge);
}
// remove the entry from the contained edges
delete parentNode.containedEdges[childNode.id];
};

+ 338
- 104
vis.js View File

@ -5835,6 +5835,7 @@ function DataSet (options) {
this.data = {}; // map with data indexed by id
this.fieldId = this.options.fieldId || 'id'; // name of the field containing id
this.convert = {}; // field types by field name
this.showInternalIds = this.options.showInternalIds || false; // show internal ids with the get function
if (this.options.convert) {
for (var field in this.options.convert) {
@ -6068,6 +6069,7 @@ DataSet.prototype.update = function (data, senderId) {
*/
DataSet.prototype.get = function (args) {
var me = this;
var globalShowInternalIds = this.showInternalIds;
// parse the arguments
var id, ids, options, data;
@ -6111,6 +6113,13 @@ DataSet.prototype.get = function (args) {
type = 'Array';
}
// we allow the setting of this value for a single get request.
if (options != undefined) {
if (options.showInternalIds != undefined) {
this.showInternalIds = options.showInternalIds;
}
}
// build options
var convert = options && options.convert || this.options.convert;
var filter = options && options.filter;
@ -6145,6 +6154,9 @@ DataSet.prototype.get = function (args) {
}
}
// restore the global value of showInternalIds
this.showInternalIds = globalShowInternalIds;
// order the results
if (options && options.order && id == undefined) {
this._sort(items, options.order);
@ -6624,7 +6636,7 @@ DataSet.prototype._getItem = function (id, convert) {
if (raw.hasOwnProperty(field)) {
value = raw[field];
// output all fields, except internal ids
if ((field != fieldId) || !(value in internalIds)) {
if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
converted[field] = util.convert(value, convert[field]);
}
}
@ -6636,13 +6648,12 @@ DataSet.prototype._getItem = function (id, convert) {
if (raw.hasOwnProperty(field)) {
value = raw[field];
// output all fields, except internal ids
if ((field != fieldId) || !(value in internalIds)) {
if ((field != fieldId) || (!(value in internalIds) || this.showInternalIds)) {
converted[field] = value;
}
}
}
}
return converted;
};
@ -6676,6 +6687,17 @@ DataSet.prototype._updateItem = function (item) {
return id;
};
/**
* check if an id is an internal or external id
* @param id
* @returns {boolean}
* @private
*/
DataSet.prototype.isInternalId = function(id) {
return (id in this.internalIds);
};
/**
* Get an array with the column names of a Google DataTable
* @param {DataTable} dataTable
@ -13289,6 +13311,8 @@ function Node(properties, imagelist, grouplist, constants) {
this.edges = []; // all edges connected to this node
this.dynamicEdges = [];
this.reroutedEdges = {};
this.group = constants.nodes.group;
this.fontSize = constants.nodes.fontSize;
@ -13317,8 +13341,7 @@ function Node(properties, imagelist, grouplist, constants) {
// creating the variables for clustering
this.resetCluster();
this.remainingEdges = 0;
this.remainingEdges_unapplied = 0;
this.dynamicEdgesLength = 0;
this.clusterSession = 0;
this.clusterSizeWidthFactor = constants.clustering.clusterSizeWidthFactor;
@ -13354,8 +13377,8 @@ Node.prototype.attachEdge = function(edge) {
if (this.edges.indexOf(edge) == -1) {
this.edges.push(edge);
}
this.remainingEdges = this.edges.length;
this.remainingEdges_unapplied = this.remainingEdges;
this.dynamicEdges = this.edges;
this.dynamicEdgesLength = this.edges.length;
this._updateMass();
};
@ -13368,8 +13391,8 @@ Node.prototype.detachEdge = function(edge) {
if (index != -1) {
this.edges.splice(index, 1);
}
this.remainingEdges = this.edges.length;
this.remainingEdges_unapplied = this.remainingEdges;
this.dynamicEdges = this.edges;
this.dynamicEdgesLength = this.edges.length;
this._updateMass();
};
@ -14114,6 +14137,12 @@ function Edge (properties, graph, constants) {
this.from = null; // a node
this.to = null; // a node
// we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
// by storing the original information we can revert to the original connection when the cluser is opened.
this.originalFromID = [];
this.originalToID = [];
this.connected = false;
// Added to support dashed lines
@ -14918,8 +14947,6 @@ function Cluster() {
this.clusterSession = 0;
}
/**
* This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
* be clustered with their connected node. This can be repeated as many times as needed.
@ -14932,7 +14959,7 @@ Cluster.prototype.increaseClusterLevel = function() {
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
//this.start();
this.start();
}
};
@ -14958,25 +14985,8 @@ Cluster.prototype.decreaseClusterLevel = function() {
if (this.moving != isMovingBeforeClustering) {
this.start();
}
};
/**
* This function can be called to open up a specific cluster.
* It will recursively unpack the entire cluster back to individual nodes.
*
* @param node | Node object: cluster to open.
*/
Cluster.prototype.fullyOpenCluster = function(node) {
var isMovingBeforeClustering = this.moving;
this._expandClusterNode(node,true,true);
this._updateNodeIndexList();
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
this._updateLabels();
};
@ -15033,14 +15043,17 @@ Cluster.prototype._updateClusters = function() {
*/
Cluster.prototype._updateLabels = function() {
// update node labels
//this._updateClusterLabels();
// this._updateNodeLabels();
// Debug :
for (var nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
var node = this.nodes[nodeID];
node.label = String(node.remainingEdges).concat(":",node.remainingEdges_unapplied,":",String(node.clusterSize));
// node.label = String(Math.round(this.zoomCenter.x)).concat(",",String(Math.round(this.zoomCenter.y)),
// "::",String(Math.round(node.x)),"x",String(Math.round(node.y)));
node.label = String(node.dynamicEdges.length).concat(":",node.dynamicEdgesLength,":",String(node.clusterSize),":::",String(node.id));
}
}
};
/**
@ -15136,10 +15149,8 @@ Cluster.prototype._expandClusterNode = function(parentNode, recursive, forceExpa
Cluster.prototype._parentNodeInActiveArea = function(node) {
if (node.selected)
console.log(node.x,this.zoomCenter.x,node.y, this.zoomCenter.y)
if (Math.abs(node.x - this.zoomCenter.x) <= this.constants.clustering.activeAreaRadius &&
Math.abs(node.y - this.zoomCenter.y) <= this.constants.clustering.activeAreaRadius) {
if (Math.abs(node.x - this.zoomCenter.x) <= this.constants.clustering.activeAreaRadius/this.scale &&
Math.abs(node.y - this.zoomCenter.y) <= this.constants.clustering.activeAreaRadius/this.scale) {
return true;
}
else {
@ -15164,16 +15175,21 @@ Cluster.prototype._expelChildFromParent = function(parentNode, containedNodeID,
// if child node has been added on smaller scale than current, kick out
if (childNode.formationScale < this.scale || forceExpand == true) {
// put the child node back in the global nodes object and the corresponding edge in the global edges object
// put the child node back in the global nodes object
this.nodes[containedNodeID] = childNode;
this.edges[parentNode.containedEdges[containedNodeID].id] = parentNode.containedEdges[containedNodeID];
// release the contained edges from this childNode back into the global edges
this._releaseContainedEdges(parentNode,childNode)
// reconnect rerouted edges to the childNode
this._connectEdgeBackToChild(parentNode,childNode);
// undo the changes from the clustering operation on the parent node
parentNode.mass -= this.constants.clustering.massTransferCoefficient * childNode.mass;
parentNode.fontSize -= this.constants.clustering.fontSizeMultiplier * childNode.clusterSize;
parentNode.clusterSize -= childNode.clusterSize;
parentNode.remainingEdges += 1;
parentNode.remainingEdges_unapplied = parentNode.remainingEdges;
parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
// place the child node near the parent, not at the exact same location to avoid chaos in the system
childNode.x = parentNode.x + this.constants.edges.length * 0.2 * (0.5 - Math.random()) * parentNode.clusterSize;
@ -15184,7 +15200,6 @@ Cluster.prototype._expelChildFromParent = function(parentNode, containedNodeID,
// remove node from the list
delete parentNode.containedNodes[containedNodeID];
delete parentNode.containedEdges[containedNodeID];
// restart the simulation to reorganise all nodes
this.moving = true;
@ -15200,7 +15215,6 @@ Cluster.prototype._expelChildFromParent = function(parentNode, containedNodeID,
};
/**
* This function checks if any nodes at the end of their trees have edges below a threshold length
* This function is called only from _updateClusters()
@ -15211,34 +15225,48 @@ Cluster.prototype._expelChildFromParent = function(parentNode, containedNodeID,
* @param force_level_collapse | Boolean
*/
Cluster.prototype._formClusters = function(forceLevelCollapse) {
var minLength = this.constants.clustering.clusterLength/this.scale;
var amountOfNodes = this.nodeIndices.length;
var dx,dy,length,
edges = this.edges;
if (forceLevelCollapse == false) {
this._formClustersByZoom();
}
else {
this._forceClustersByZoom();
}
if (this.nodeIndices.length != amountOfNodes) { // this means a clustering operation has taken place
this.clusterSession += 1;
}
};
/**
* This function handles the clustering by zooming out, this is based on minimum edge distance
*
* @private
*/
Cluster.prototype._formClustersByZoom = function() {
var dx,dy,length,
minLength = this.constants.clustering.clusterLength/this.scale;
// create an array of edge ids
var edgesIDarray = []
for (var id in edges) {
if (edges.hasOwnProperty(id)) {
for (var id in this.edges) {
if (this.edges.hasOwnProperty(id)) {
edgesIDarray.push(id);
}
}
// check if any edges are shorter than minLength and start the clustering
// the clustering favours the node with the larger mass
for (var i = 0; i < edgesIDarray.length; i++) {
var edgeID = edgesIDarray[i];
var edge = edges[edgeID];
edge.id = edgeID;
var edge = this.edges[edgeID];
if (edge.connected) {
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
length = Math.sqrt(dx * dx + dy * dy);
if (length < minLength || forceLevelCollapse == true) {
// checking for clustering possibilities
if (length < minLength) {
// first check which node is larger
var parentNode = edge.from
var childNode = edge.to
@ -15247,51 +15275,124 @@ Cluster.prototype._formClusters = function(forceLevelCollapse) {
childNode = edge.from
}
// we allow clustering from outside in, ideally the child node in on the outside
// if we do not cluster from outside in, we would have to reconnect edges or keep a second set of edges for the
// clusters. This will also have to be altered in the force calculation and rendering.
// we allow clustering from outside in
// if we do not cluster from outside in, we would have to reconnect edges or keep
// a second set of edges for the clusters.
// This will also have to be altered in the force calculation and rendering.
// This method is non-destructive and does not require a second set of data.
if (childNode.remainingEdges == 1 && childNode.remainingEdges_unapplied != 0) {
this._addToCluster(parentNode,childNode,edge,forceLevelCollapse);
delete this.edges[edgesIDarray[i]];
if (childNode.dynamicEdgesLength == 1) {
this._addToCluster(parentNode,childNode,edge,false);
}
else if (parentNode.remainingEdges == 1 && parentNode.remainingEdges_unapplied != 0) {
this._addToCluster(childNode,parentNode,edge,forceLevelCollapse);
delete this.edges[edgesIDarray[i]];
else if (parentNode.dynamicEdgesLength == 1) {
this._addToCluster(childNode,parentNode,edge,false);
}
}
}
}
this._updateNodeIndexList();
};
if (forceLevelCollapse == true) {
this._applyClusterLevel();
/**
* This function forces the graph to cluster all nodes with only one connecting edge to their
* connected node.
*
* @private
*/
Cluster.prototype._forceClustersByZoom = function() {
for (var nodeID = 0; nodeID < this.nodeIndices.length; nodeID++) {
var childNode = this.nodes[this.nodeIndices[nodeID]];
if (childNode.dynamicEdgesLength == 1) {
var edge = childNode.dynamicEdges[0];
var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
this._addToCluster(parentNode,childNode,true);
}
}
this._updateNodeIndexList();
this._updateDynamicEdges();
};
if (this.nodeIndices.length != amountOfNodes) { // this means a clustering operation has taken place
this.clusterSession += 1;
Cluster.prototype.aggregateHubs = function() {
var isMovingBeforeClustering = this.moving;
var hubThreshold = 4;
this._forceClustersByHub(hubThreshold);
// if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
if (this.moving != isMovingBeforeClustering) {
this.start();
}
};
/**
*
* @param hubThreshold
* @private
*/
Cluster.prototype._forceClustersByHub = function(hubThreshold) {
for (var nodeID = 0; nodeID < this.nodeIndices.length; nodeID++) {
if (this.nodes.hasOwnProperty(this.nodeIndices[nodeID])) {
var hubNode = this.nodes[this.nodeIndices[nodeID]];
if (hubNode.dynamicEdges.length >= hubThreshold) {
var edgesIDarray = []
var amountOfInitialEdges = hubNode.dynamicEdges.length;
for (var i = 0; i < amountOfInitialEdges; i++) {
edgesIDarray.push(hubNode.dynamicEdges[i].id);
}
for (var i = 0; i < amountOfInitialEdges; i++) {
var edge = this.edges[edgesIDarray[i]];
var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
this._addToCluster(hubNode,childNode,true);
}
break;
}
}
}
this._updateNodeIndexList();
this._updateDynamicEdges();
this._updateLabels();
if (this.moving != isMovingBeforeClustering) {
this.start();
}
};
/**
* This function adds the child node to the parent node, creating a cluster if it is not already.
* This function is called only from _updateClusters()
*
* @param parent_node | Node object: this is the node that will house the child node
* @param child_node | Node object: this node will be deleted from the global this.nodes and stored in the parent node
* @param edge | Edge object: this edge will be deleted from the global this.edges and stored in the parent node
* @param force_level_collapse | Boolean: true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
* @private
*/
Cluster.prototype._addToCluster = function(parentNode, childNode, edge, forceLevelCollapse) {
// join child node and edge in parent node
Cluster.prototype._addToCluster = function(parentNode, childNode, forceLevelCollapse) {
// join child node in the parent node
parentNode.containedNodes[childNode.id] = childNode;
parentNode.containedEdges[childNode.id] = edge; // the edge gets the node ID so we can easily recover it when expanding the cluster
if (this.nodes.hasOwnProperty(childNode.id)) {
delete this.nodes[childNode.id];
if (forceLevelCollapse == false) {
parentNode.dynamicEdgesLength += childNode.dynamicEdges.length - 2;
}
for (var i = 0; i < childNode.dynamicEdges.length; i++) {
var edge = childNode.dynamicEdges[i];
if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
this._addToContainedEdges(parentNode,childNode,edge);
}
else {
console.log('connecting edge to cluster');
this._connectEdgeToCluster(parentNode,childNode,edge);
}
}
childNode.dynamicEdges = [];
// remove the childNode from the global nodes object
delete this.nodes[childNode.id];
childNode.clusterSession = this.clusterSession;
parentNode.mass += this.constants.clustering.massTransferCoefficient * childNode.mass;
@ -15304,18 +15405,13 @@ Cluster.prototype._addToCluster = function(parentNode, childNode, edge, forceLev
}
else {
parentNode.formationScale = this.scale; // The latest child has been added on this scale
parentNode.dynamicEdgesLength = parentNode.dynamicEdges.length;
}
// recalculate the size of the node on the next time the node is rendered
parentNode.clearSizeCache();
parentNode.containedNodes[childNode.id].formationScale = this.scale; // this child has been added at this scale.
if (forceLevelCollapse == true) {
parentNode.remainingEdges_unapplied -= 1;
}
else {
parentNode.remainingEdges -= 1;
}
// restart the simulation to reorganise all nodes
this.moving = true;
@ -15328,10 +15424,10 @@ Cluster.prototype._addToCluster = function(parentNode, childNode, edge, forceLev
* It has to be called if a level is collapsed. It is called by _formClusters().
* @private
*/
Cluster.prototype._applyClusterLevel = function() {
Cluster.prototype._updateDynamicEdges = function() {
for (var i = 0; i < this.nodeIndices.length; i++) {
var node = this.nodes[this.nodeIndices[i]];
node.remainingEdges = node.remainingEdges_unapplied;
node.dynamicEdgesLength = node.dynamicEdges.length;
}
};
@ -15342,16 +15438,150 @@ Cluster.prototype._repositionNodes = function() {
if (!node.isFixed()) {
// TODO: position new nodes in a smarter way!
var radius = this.constants.edges.length * (1 + 0.5*node.clusterSize);
var count = this.nodeIndices.length;
var angle = 2 * Math.PI * Math.random();
node.x = radius * Math.cos(angle);
node.y = radius * Math.sin(angle);
}
}
};
/**
* This adds an edge from the childNode to the contained edges of the parent node
*
* @param parentNode | Node object
* @param childNode | Node object
* @param edge | Edge object
* @private
*/
Cluster.prototype._addToContainedEdges = function(parentNode, childNode, edge) {
// create an array object if it does not yet exist for this childNode
if (!(parentNode.containedEdges.hasOwnProperty(childNode.id))) {
parentNode.containedEdges[childNode.id] = []
}
// add this edge to the list
parentNode.containedEdges[childNode.id].push(edge);
// remove the edge from the global edges object
delete this.edges[edge.id];
// remove the edge from the parent object
for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
if (parentNode.dynamicEdges[i].id == edge.id) {
parentNode.dynamicEdges.splice(i,1);
break;
}
}
};
/**
* This function connects an edge that was connected to a child node to the parent node.
* It keeps track of which nodes it has been connected to with the originalID array.
*
* @param parentNode | Node object
* @param childNode | Node object
* @param edge | Edge object
* @private
*/
Cluster.prototype._connectEdgeToCluster = function(parentNode, childNode, edge) {
if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
edge.originalToID.push(childNode.id);
edge.to = parentNode;
edge.toId = parentNode.id;
}
else { // edge connected to other node with the "from" side
edge.originalFromID.push(childNode.id);
edge.from = parentNode;
edge.fromId = parentNode.id;
}
this._addToReroutedEdges(parentNode,childNode,edge);
};
/**
* This adds an edge from the childNode to the rerouted edges of the parent node
*
* @param parentNode | Node object
* @param childNode | Node object
* @param edge | Edge object
* @private
*/
Cluster.prototype._addToReroutedEdges = function(parentNode, childNode, edge) {
// create an array object if it does not yet exist for this childNode
// we store the edge in the rerouted edges so we can restore it when the cluster pops open
if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
parentNode.reroutedEdges[childNode.id] = [];
}
parentNode.reroutedEdges[childNode.id].push(edge);
// this edge becomes part of the dynamicEdges of the cluster node
parentNode.dynamicEdges.push(edge);
};
/**
* This function connects an edge that was connected to a cluster node back to the child node.
*
* @param parentNode | Node object
* @param childNode | Node object
* @private
*/
Cluster.prototype._connectEdgeBackToChild = function(parentNode, childNode) {
if (parentNode.reroutedEdges[childNode.id] != undefined) {
for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
var edge = parentNode.reroutedEdges[childNode.id][i];
if (edge.originalFromID[edge.originalFromID.length-1] == childNode.id) {
edge.originalFromID.pop();
edge.fromId = childNode.id;
edge.from = childNode;
}
else {
edge.originalToID.pop();
edge.toId = childNode.id;
edge.to = childNode;
}
// append this edge to the list of edges connecting to the childnode
childNode.dynamicEdges.push(edge);
// remove the edge from the parent object
for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
if (parentNode.dynamicEdges[j].id == edge.id) {
parentNode.dynamicEdges.splice(j,1);
break;
}
}
}
// remove the entry from the rerouted edges
delete parentNode.reroutedEdges[childNode.id];
}
};
/**
* This function released the contained edges back into the global domain and puts them back into the
* dynamic edges of both parent and child.
*
* @param parentNode | Node object
* @param childNode | Node object
* @private
*/
Cluster.prototype._releaseContainedEdges = function(parentNode, childNode) {
for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
var edge = parentNode.containedEdges[childNode.id][i];
// put the edge back in the global edges object
this.edges[edge.id] = edge;
// put the edge back in the dynamic edges of the child and parent
childNode.dynamicEdges.push(edge);
parentNode.dynamicEdges.push(edge);
}
// remove the entry from the contained edges
delete parentNode.containedEdges[childNode.id];
};
/**
* @constructor Graph
* Create a graph visualization, displaying nodes and edges.
@ -15446,7 +15676,6 @@ function Graph (container, data, options) {
Cluster.call(this);
var graph = this;
this.simulationStep = 100;
this.nodeIndices = []; // the node indices list is used to speed up the computation of the repulsion fields
this.nodes = {}; // object with Node objects
this.edges = {}; // object with Edge objects
@ -15490,7 +15719,7 @@ function Graph (container, data, options) {
me.start();
}
};
console.log("here")
this.groups = new Groups(); // object with groups
this.images = new Images(); // object with images
this.images.setOnloadCallback(function () {
@ -15518,6 +15747,15 @@ function Graph (container, data, options) {
// cluster if the data set is big
this.clusterToFit();
// updates the lables after clustering
this._updateLabels();
// find a stable position or start animating to a stable position
if (this.stabilize) {
this._doStabilize();
}
this.start();
}
/**
@ -15526,22 +15764,25 @@ function Graph (container, data, options) {
*/
Graph.prototype = Object.create(Cluster.prototype);
/**
* This function clusters untill the maxNumberOfNodes has been reached
*/
Graph.prototype.clusterToFit = function() {
var numberOfNodes = this.nodeIndices.length;
var maxNumberOfNodes = 100;
var maxNumberOfNodes = 100; // TODO: set in constants
var maxLevels = 10;
var level = 0;
this.simulationStep = 100;
while (numberOfNodes >= maxNumberOfNodes && level < maxLevels) {
console.log(level)
this.increaseClusterLevel();
if (level % 2 == 0) {
this.increaseClusterLevel();
}
else {
this.aggregateHubs();
}
numberOfNodes = this.nodeIndices.length;
level += 1;
this.simulationStep -= 20;
}
// after the clustering we reposition the nodes to avoid initial chaos
@ -15611,13 +15852,6 @@ Graph.prototype.setData = function(data) {
this._setEdges(data && data.edges);
}
// updating the list of node indices
// find a stable position or start animating to a stable position
if (this.stabilize) {
this._doStabilize();
}
this.start();
};
/**
@ -15758,10 +15992,10 @@ Graph.prototype._create = function () {
this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
this.mouseTrap = mouseTrap;
this.mouseTrap.bind("=", this.decreaseClusterLevel.bind(me));
this.mouseTrap.bind("=",this.decreaseClusterLevel.bind(me));
this.mouseTrap.bind("-",this.increaseClusterLevel.bind(me));
this.mouseTrap.bind("s",this.singleStep.bind(me));
this.mouseTrap.bind("h",this._forceClustersByHub.bind(me));
// add the frame to the container element
this.containerElement.appendChild(this.frame);
@ -16018,8 +16252,8 @@ Graph.prototype._zoom = function(scale, pointer) {
var tx = (1 - scaleFrac) * pointer.x + translation.x * scaleFrac;
var ty = (1 - scaleFrac) * pointer.y + translation.y * scaleFrac;
this.zoomCenter = {"x" : pointer.x - translation.x,
"y" : pointer.y - translation.y};
this.zoomCenter = {"x" : this._canvasToX(pointer.x),
"y" : this._canvasToY(pointer.y)};
// this.zoomCenter = {"x" : pointer.x,"y" : pointer.y };
this._setScale(scale);
@ -16658,6 +16892,7 @@ Graph.prototype._setEdges = function(edges) {
Graph.prototype._addEdges = function (ids) {
var edges = this.edges,
edgesData = this.edgesData;
for (var i = 0, len = ids.length; i < len; i++) {
var id = ids[i];
@ -16666,7 +16901,7 @@ Graph.prototype._addEdges = function (ids) {
oldEdge.disconnect();
}
var data = edgesData.get(id);
var data = edgesData.get(id, {"showInternalIds" : true});
edges[id] = new Edge(data, this, this.constants);
}
@ -16992,8 +17227,7 @@ Graph.prototype._calculateForces = function() {
// the graph
// Also, the forces are reset to zero in this loop by using _setForce instead
// of _addForce
var dynamicGravity = 100.0 - 1*this.simulationStep;
var gravity = (dynamicGravity < 0.05 ? 0.05 : dynamicGravity);
var gravity = 0.05;
for (id in nodes) {
if (nodes.hasOwnProperty(id)) {
var node = nodes[id];

+ 7
- 7
vis.min.js
File diff suppressed because it is too large
View File


Loading…
Cancel
Save