|
@ -14911,200 +14911,21 @@ Images.prototype.load = function(url) { |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* @constructor Graph |
|
|
|
|
|
* Create a graph visualization, displaying nodes and edges. |
|
|
|
|
|
* |
|
|
|
|
|
* @param {Element} container The DOM element in which the Graph will |
|
|
|
|
|
* be created. Normally a div element. |
|
|
|
|
|
* @param {Object} data An object containing parameters |
|
|
|
|
|
* {Array} nodes |
|
|
|
|
|
* {Array} edges |
|
|
|
|
|
* @param {Object} options Options |
|
|
|
|
|
|
|
|
* @constructor Cluster |
|
|
|
|
|
* Contains the cluster properties for the graph object |
|
|
*/ |
|
|
*/ |
|
|
function Graph (container, data, options) { |
|
|
|
|
|
// create variables and set default values
|
|
|
|
|
|
this.containerElement = container; |
|
|
|
|
|
this.width = '100%'; |
|
|
|
|
|
this.height = '100%'; |
|
|
|
|
|
this.refreshRate = 50; // milliseconds
|
|
|
|
|
|
this.stabilize = true; // stabilize before displaying the graph
|
|
|
|
|
|
this.selectable = true; |
|
|
|
|
|
|
|
|
|
|
|
// set constant values
|
|
|
|
|
|
this.constants = { |
|
|
|
|
|
nodes: { |
|
|
|
|
|
radiusMin: 5, |
|
|
|
|
|
radiusMax: 20, |
|
|
|
|
|
radius: 5, |
|
|
|
|
|
distance: 100, // px
|
|
|
|
|
|
shape: 'ellipse', |
|
|
|
|
|
image: undefined, |
|
|
|
|
|
widthMin: 16, // px
|
|
|
|
|
|
widthMax: 64, // px
|
|
|
|
|
|
fontColor: 'black', |
|
|
|
|
|
fontSize: 14, // px
|
|
|
|
|
|
//fontFace: verdana,
|
|
|
|
|
|
fontFace: 'arial', |
|
|
|
|
|
color: { |
|
|
|
|
|
border: '#2B7CE9', |
|
|
|
|
|
background: '#97C2FC', |
|
|
|
|
|
highlight: { |
|
|
|
|
|
border: '#2B7CE9', |
|
|
|
|
|
background: '#D2E5FF' |
|
|
|
|
|
}, |
|
|
|
|
|
cluster: { |
|
|
|
|
|
border: '#256a2d', |
|
|
|
|
|
background: '#2cd140', |
|
|
|
|
|
highlight: { |
|
|
|
|
|
border: '#899539', |
|
|
|
|
|
background: '#c5dc29' |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
borderColor: '#2B7CE9', |
|
|
|
|
|
backgroundColor: '#97C2FC', |
|
|
|
|
|
highlightColor: '#D2E5FF', |
|
|
|
|
|
group: undefined |
|
|
|
|
|
}, |
|
|
|
|
|
edges: { |
|
|
|
|
|
widthMin: 1, |
|
|
|
|
|
widthMax: 15, |
|
|
|
|
|
width: 1, |
|
|
|
|
|
style: 'line', |
|
|
|
|
|
color: '#343434', |
|
|
|
|
|
fontColor: '#343434', |
|
|
|
|
|
fontSize: 14, // px
|
|
|
|
|
|
fontFace: 'arial', |
|
|
|
|
|
//distance: 100, //px
|
|
|
|
|
|
length: 100, // px
|
|
|
|
|
|
dash: { |
|
|
|
|
|
length: 10, |
|
|
|
|
|
gap: 5, |
|
|
|
|
|
altLength: undefined |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
clustering: { |
|
|
|
|
|
clusterLength: 50, // threshold edge length for clustering
|
|
|
|
|
|
fontSizeMultiplier: 2, // how much the cluster font size grows per node (in px)
|
|
|
|
|
|
forceAmplification: 0.6, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force
|
|
|
|
|
|
distanceAmplification: 0.1, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force
|
|
|
|
|
|
edgeGrowth: 10, // amount of clusterSize connected to the edge is multiplied with this and added to edgeLength
|
|
|
|
|
|
clusterSizeWidthFactor: 10, |
|
|
|
|
|
clusterSizeHeightFactor: 10, |
|
|
|
|
|
clusterSizeRadiusFactor: 10, |
|
|
|
|
|
massTransferCoefficient: 0.2 // parent.mass += massTransferCoefficient * child.mass
|
|
|
|
|
|
}, |
|
|
|
|
|
minForce: 0.05, |
|
|
|
|
|
minVelocity: 0.02, // px/s
|
|
|
|
|
|
maxIterations: 1000 // maximum number of iteration to stabilize
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
var graph = this; |
|
|
|
|
|
|
|
|
function Cluster() { |
|
|
this.clusterSession = 0; |
|
|
this.clusterSession = 0; |
|
|
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
|
|
|
|
|
|
this.scale = 1; // defining the global scale variable in the constructor
|
|
|
|
|
|
this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
|
|
|
|
|
|
// TODO: create a counter to keep track on the number of nodes having values
|
|
|
|
|
|
// TODO: create a counter to keep track on the number of nodes currently moving
|
|
|
|
|
|
// TODO: create a counter to keep track on the number of edges having values
|
|
|
|
|
|
|
|
|
|
|
|
this.nodesData = null; // A DataSet or DataView
|
|
|
|
|
|
this.edgesData = null; // A DataSet or DataView
|
|
|
|
|
|
|
|
|
|
|
|
// create event listeners used to subscribe on the DataSets of the nodes and edges
|
|
|
|
|
|
var me = this; |
|
|
|
|
|
this.nodesListeners = { |
|
|
|
|
|
'add': function (event, params) { |
|
|
|
|
|
me._addNodes(params.items); |
|
|
|
|
|
me.start(); |
|
|
|
|
|
}, |
|
|
|
|
|
'update': function (event, params) { |
|
|
|
|
|
me._updateNodes(params.items); |
|
|
|
|
|
me.start(); |
|
|
|
|
|
}, |
|
|
|
|
|
'remove': function (event, params) { |
|
|
|
|
|
me._removeNodes(params.items); |
|
|
|
|
|
me.start(); |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
this.edgesListeners = { |
|
|
|
|
|
'add': function (event, params) { |
|
|
|
|
|
me._addEdges(params.items); |
|
|
|
|
|
me.start(); |
|
|
|
|
|
}, |
|
|
|
|
|
'update': function (event, params) { |
|
|
|
|
|
me._updateEdges(params.items); |
|
|
|
|
|
me.start(); |
|
|
|
|
|
}, |
|
|
|
|
|
'remove': function (event, params) { |
|
|
|
|
|
me._removeEdges(params.items); |
|
|
|
|
|
me.start(); |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
this.groups = new Groups(); // object with groups
|
|
|
|
|
|
this.images = new Images(); // object with images
|
|
|
|
|
|
this.images.setOnloadCallback(function () { |
|
|
|
|
|
graph._redraw(); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// properties of the data
|
|
|
|
|
|
this.moving = false; // True if any of the nodes have an undefined position
|
|
|
|
|
|
|
|
|
|
|
|
this.selection = []; |
|
|
|
|
|
this.timer = undefined; |
|
|
|
|
|
|
|
|
|
|
|
// create a frame and canvas
|
|
|
|
|
|
this._create(); |
|
|
|
|
|
|
|
|
|
|
|
// apply options
|
|
|
|
|
|
this.setOptions(options); |
|
|
|
|
|
|
|
|
|
|
|
// draw data
|
|
|
|
|
|
this.setData(data); |
|
|
|
|
|
|
|
|
|
|
|
// zoom so all data will fit on the screen
|
|
|
|
|
|
this.zoomToFit(); |
|
|
|
|
|
|
|
|
|
|
|
// cluster if the dataset is big
|
|
|
|
|
|
this.clusterToFit(); |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Graph.prototype.clusterToFit = function() { |
|
|
|
|
|
var numberOfNodes = this.nodeIndices.length; |
|
|
|
|
|
var maxNumberOfNodes = 100; |
|
|
|
|
|
|
|
|
|
|
|
var maxLevels = 10; |
|
|
|
|
|
var level = 0; |
|
|
|
|
|
while (numberOfNodes >= maxNumberOfNodes && level < maxLevels) { |
|
|
|
|
|
this.increaseClusterLevel(); |
|
|
|
|
|
numberOfNodes = this.nodeIndices.length; |
|
|
|
|
|
level += 1; |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
Graph.prototype.zoomToFit = function() { |
|
|
|
|
|
var numberOfNodes = this.nodeIndices.length; |
|
|
|
|
|
var zoomLevel = 105 / (numberOfNodes + 80); // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
|
|
|
|
|
|
if (zoomLevel > 1.0) { |
|
|
|
|
|
zoomLevel = 1.0; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (!('mousewheelScale' in this.pinch)) { |
|
|
|
|
|
this.pinch.mousewheelScale = zoomLevel; |
|
|
|
|
|
} |
|
|
|
|
|
this._setScale(zoomLevel); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* This function can be called to increase the cluster level. This means that the nodes with only one edge connection will |
|
|
* 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. |
|
|
* be clustered with their connected node. This can be repeated as many times as needed. |
|
|
* This can be called externally (by a keybind for instance) to reduce the complexity of big datasets. |
|
|
* This can be called externally (by a keybind for instance) to reduce the complexity of big datasets. |
|
|
*/ |
|
|
*/ |
|
|
Graph.prototype.increaseClusterLevel = function() { |
|
|
|
|
|
|
|
|
Cluster.prototype.increaseClusterLevel = function() { |
|
|
var isMovingBeforeClustering = this.moving; |
|
|
var isMovingBeforeClustering = this.moving; |
|
|
|
|
|
|
|
|
this._formClusters(true); |
|
|
this._formClusters(true); |
|
@ -15122,7 +14943,7 @@ Graph.prototype.increaseClusterLevel = function() { |
|
|
* be unpacked if they are a cluster. This can be repeated as many times as needed. |
|
|
* be unpacked if they are a cluster. This can be repeated as many times as needed. |
|
|
* This can be called externally (by a key-bind for instance) to look into clusters without zooming. |
|
|
* This can be called externally (by a key-bind for instance) to look into clusters without zooming. |
|
|
*/ |
|
|
*/ |
|
|
Graph.prototype.decreaseClusterLevel = function() { |
|
|
|
|
|
|
|
|
Cluster.prototype.decreaseClusterLevel = function() { |
|
|
var isMovingBeforeClustering = this.moving; |
|
|
var isMovingBeforeClustering = this.moving; |
|
|
|
|
|
|
|
|
for (var i = 0; i < this.nodeIndices.length; i++) { |
|
|
for (var i = 0; i < this.nodeIndices.length; i++) { |
|
@ -15150,7 +14971,7 @@ Graph.prototype.decreaseClusterLevel = function() { |
|
|
* |
|
|
* |
|
|
* @param node | Node object: cluster to open. |
|
|
* @param node | Node object: cluster to open. |
|
|
*/ |
|
|
*/ |
|
|
Graph.prototype.fullyOpenCluster = function(node) { |
|
|
|
|
|
|
|
|
Cluster.prototype.fullyOpenCluster = function(node) { |
|
|
var isMovingBeforeClustering = this.moving; |
|
|
var isMovingBeforeClustering = this.moving; |
|
|
|
|
|
|
|
|
this._expandClusterNode(node,true,true); |
|
|
this._expandClusterNode(node,true,true); |
|
@ -15169,7 +14990,7 @@ Graph.prototype.fullyOpenCluster = function(node) { |
|
|
* |
|
|
* |
|
|
* @param node | Node object: cluster to open. |
|
|
* @param node | Node object: cluster to open. |
|
|
*/ |
|
|
*/ |
|
|
Graph.prototype.openCluster = function(node) { |
|
|
|
|
|
|
|
|
Cluster.prototype.openCluster = function(node) { |
|
|
var isMovingBeforeClustering = this.moving; |
|
|
var isMovingBeforeClustering = this.moving; |
|
|
|
|
|
|
|
|
this._expandClusterNode(node,false,true); |
|
|
this._expandClusterNode(node,false,true); |
|
@ -15189,7 +15010,7 @@ Graph.prototype.openCluster = function(node) { |
|
|
* |
|
|
* |
|
|
* @private |
|
|
* @private |
|
|
*/ |
|
|
*/ |
|
|
Graph.prototype._updateClusters = function() { |
|
|
|
|
|
|
|
|
Cluster.prototype._updateClusters = function() { |
|
|
var isMovingBeforeClustering = this.moving; |
|
|
var isMovingBeforeClustering = this.moving; |
|
|
|
|
|
|
|
|
if (this.previousScale > this.scale) { // zoom out
|
|
|
if (this.previousScale > this.scale) { // zoom out
|
|
@ -15214,7 +15035,7 @@ Graph.prototype._updateClusters = function() { |
|
|
* This updates the node labels for all nodes (for debugging purposes) |
|
|
* This updates the node labels for all nodes (for debugging purposes) |
|
|
* @private |
|
|
* @private |
|
|
*/ |
|
|
*/ |
|
|
Graph.prototype._updateLabels = function() { |
|
|
|
|
|
|
|
|
Cluster.prototype._updateLabels = function() { |
|
|
// update node labels
|
|
|
// update node labels
|
|
|
for (var nodeID in this.nodes) { |
|
|
for (var nodeID in this.nodes) { |
|
|
if (this.nodes.hasOwnProperty(nodeID)) { |
|
|
if (this.nodes.hasOwnProperty(nodeID)) { |
|
@ -15228,7 +15049,7 @@ Graph.prototype._updateLabels = function() { |
|
|
* This updates the node labels for all clusters |
|
|
* This updates the node labels for all clusters |
|
|
* @private |
|
|
* @private |
|
|
*/ |
|
|
*/ |
|
|
Graph.prototype._updateClusterLabels = function() { |
|
|
|
|
|
|
|
|
Cluster.prototype._updateClusterLabels = function() { |
|
|
// update node labels
|
|
|
// update node labels
|
|
|
for (var nodeID in this.nodes) { |
|
|
for (var nodeID in this.nodes) { |
|
|
if (this.nodes.hasOwnProperty(nodeID)) { |
|
|
if (this.nodes.hasOwnProperty(nodeID)) { |
|
@ -15244,7 +15065,7 @@ Graph.prototype._updateClusterLabels = function() { |
|
|
* This updates the node labels for all nodes that are NOT clusters |
|
|
* This updates the node labels for all nodes that are NOT clusters |
|
|
* @private |
|
|
* @private |
|
|
*/ |
|
|
*/ |
|
|
Graph.prototype._updateNodeLabels = function() { |
|
|
|
|
|
|
|
|
Cluster.prototype._updateNodeLabels = function() { |
|
|
// update node labels
|
|
|
// update node labels
|
|
|
for (var nodeID in this.nodes) { |
|
|
for (var nodeID in this.nodes) { |
|
|
var node = this.nodes[nodeID]; |
|
|
var node = this.nodes[nodeID]; |
|
@ -15261,7 +15082,7 @@ Graph.prototype._updateNodeLabels = function() { |
|
|
* |
|
|
* |
|
|
* @private |
|
|
* @private |
|
|
*/ |
|
|
*/ |
|
|
Graph.prototype._openClusters = function() { |
|
|
|
|
|
|
|
|
Cluster.prototype._openClusters = function() { |
|
|
var amountOfNodes = this.nodeIndices.length; |
|
|
var amountOfNodes = this.nodeIndices.length; |
|
|
|
|
|
|
|
|
for (var i = 0; i < this.nodeIndices.length; i++) { |
|
|
for (var i = 0; i < this.nodeIndices.length; i++) { |
|
@ -15272,7 +15093,7 @@ Graph.prototype._openClusters = function() { |
|
|
this._updateNodeIndexList(); |
|
|
this._updateNodeIndexList(); |
|
|
|
|
|
|
|
|
if (this.nodeIndices.length != amountOfNodes) { // this means a clustering operation has taken place
|
|
|
if (this.nodeIndices.length != amountOfNodes) { // this means a clustering operation has taken place
|
|
|
this.clusterSession += 1; |
|
|
|
|
|
|
|
|
this.clusterSession -= 1; |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
@ -15287,7 +15108,7 @@ Graph.prototype._openClusters = function() { |
|
|
* @param forceExpand | Boolean: enable or disable forcing the last node to join the cluster to be expelled |
|
|
* @param forceExpand | Boolean: enable or disable forcing the last node to join the cluster to be expelled |
|
|
* @private |
|
|
* @private |
|
|
*/ |
|
|
*/ |
|
|
Graph.prototype._expandClusterNode = function(parentNode, recursive, forceExpand) { |
|
|
|
|
|
|
|
|
Cluster.prototype._expandClusterNode = function(parentNode, recursive, forceExpand) { |
|
|
// first check if node is a cluster
|
|
|
// first check if node is a cluster
|
|
|
if (parentNode.clusterSize > 1) { |
|
|
if (parentNode.clusterSize > 1) { |
|
|
// if the last child has been added on a smaller scale than current scale (@optimization)
|
|
|
// if the last child has been added on a smaller scale than current scale (@optimization)
|
|
@ -15299,7 +15120,6 @@ Graph.prototype._expandClusterNode = function(parentNode, recursive, forceExpand |
|
|
|
|
|
|
|
|
// force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
|
|
|
// force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
|
|
|
// the largest cluster is the one that comes from outside
|
|
|
// the largest cluster is the one that comes from outside
|
|
|
// TODO: introduce a level system for keeping track of which node was added when.
|
|
|
|
|
|
if (forceExpand == true) { |
|
|
if (forceExpand == true) { |
|
|
if (childNode.clusterSession == this.clusterSession - 1) { |
|
|
if (childNode.clusterSession == this.clusterSession - 1) { |
|
|
this._expelChildFromParent(parentNode,containedNodeID,recursive,forceExpand); |
|
|
this._expelChildFromParent(parentNode,containedNodeID,recursive,forceExpand); |
|
@ -15327,7 +15147,7 @@ Graph.prototype._expandClusterNode = function(parentNode, recursive, forceExpand |
|
|
* @param forceExpand | Boolean: This will disregard the zoom level and will expel this child from the parent |
|
|
* @param forceExpand | Boolean: This will disregard the zoom level and will expel this child from the parent |
|
|
* @private |
|
|
* @private |
|
|
*/ |
|
|
*/ |
|
|
Graph.prototype._expelChildFromParent = function(parentNode, containedNodeID, recursive, forceExpand) { |
|
|
|
|
|
|
|
|
Cluster.prototype._expelChildFromParent = function(parentNode, containedNodeID, recursive, forceExpand) { |
|
|
var childNode = parentNode.containedNodes[containedNodeID]; |
|
|
var childNode = parentNode.containedNodes[containedNodeID]; |
|
|
|
|
|
|
|
|
// if child node has been added on smaller scale than current, kick out
|
|
|
// if child node has been added on smaller scale than current, kick out
|
|
@ -15378,13 +15198,13 @@ Graph.prototype._expelChildFromParent = function(parentNode, containedNodeID, re |
|
|
* @private |
|
|
* @private |
|
|
* @param force_level_collapse | Boolean |
|
|
* @param force_level_collapse | Boolean |
|
|
*/ |
|
|
*/ |
|
|
Graph.prototype._formClusters = function(forceLevelCollapse) { |
|
|
|
|
|
|
|
|
Cluster.prototype._formClusters = function(forceLevelCollapse) { |
|
|
var amountOfNodes = this.nodeIndices.length; |
|
|
var amountOfNodes = this.nodeIndices.length; |
|
|
|
|
|
|
|
|
var min_length = this.constants.clustering.clusterLength/this.scale; |
|
|
var min_length = this.constants.clustering.clusterLength/this.scale; |
|
|
|
|
|
|
|
|
var dx,dy,length, |
|
|
var dx,dy,length, |
|
|
edges = this.edges; |
|
|
|
|
|
|
|
|
edges = this.edges; |
|
|
|
|
|
|
|
|
// create an array of edge ids
|
|
|
// create an array of edge ids
|
|
|
var edgesIDarray = [] |
|
|
var edgesIDarray = [] |
|
@ -15441,6 +15261,7 @@ Graph.prototype._formClusters = function(forceLevelCollapse) { |
|
|
if (this.nodeIndices.length != amountOfNodes) { // this means a clustering operation has taken place
|
|
|
if (this.nodeIndices.length != amountOfNodes) { // this means a clustering operation has taken place
|
|
|
this.clusterSession += 1; |
|
|
this.clusterSession += 1; |
|
|
} |
|
|
} |
|
|
|
|
|
console.log(this.clusterSession) |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
@ -15454,7 +15275,7 @@ Graph.prototype._formClusters = function(forceLevelCollapse) { |
|
|
* @param force_level_collapse | Boolean: true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse |
|
|
* @param force_level_collapse | Boolean: true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse |
|
|
* @private |
|
|
* @private |
|
|
*/ |
|
|
*/ |
|
|
Graph.prototype._addToCluster = function(parentNode, childNode, edge, forceLevelCollapse) { |
|
|
|
|
|
|
|
|
Cluster.prototype._addToCluster = function(parentNode, childNode, edge, forceLevelCollapse) { |
|
|
// join child node and edge in parent node
|
|
|
// join child node and edge in parent node
|
|
|
parentNode.containedNodes[childNode.id] = childNode; |
|
|
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
|
|
|
parentNode.containedEdges[childNode.id] = edge; // the edge gets the node ID so we can easily recover it when expanding the cluster
|
|
@ -15491,13 +15312,205 @@ Graph.prototype._addToCluster = function(parentNode, childNode, edge, forceLevel |
|
|
* It has to be called if a level is collapsed. It is called by _formClusters(). |
|
|
* It has to be called if a level is collapsed. It is called by _formClusters(). |
|
|
* @private |
|
|
* @private |
|
|
*/ |
|
|
*/ |
|
|
Graph.prototype._applyClusterLevel = function() { |
|
|
|
|
|
|
|
|
Cluster.prototype._applyClusterLevel = function() { |
|
|
for (var i = 0; i < this.nodeIndices.length; i++) { |
|
|
for (var i = 0; i < this.nodeIndices.length; i++) { |
|
|
var node = this.nodes[this.nodeIndices[i]]; |
|
|
var node = this.nodes[this.nodeIndices[i]]; |
|
|
node.remainingEdges = node.remainingEdges_unapplied; |
|
|
node.remainingEdges = node.remainingEdges_unapplied; |
|
|
} |
|
|
} |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
|
|
|
* @constructor Graph |
|
|
|
|
|
* Create a graph visualization, displaying nodes and edges. |
|
|
|
|
|
* |
|
|
|
|
|
* @param {Element} container The DOM element in which the Graph will |
|
|
|
|
|
* be created. Normally a div element. |
|
|
|
|
|
* @param {Object} data An object containing parameters |
|
|
|
|
|
* {Array} nodes |
|
|
|
|
|
* {Array} edges |
|
|
|
|
|
* @param {Object} options Options |
|
|
|
|
|
*/ |
|
|
|
|
|
function Graph (container, data, options) { |
|
|
|
|
|
// create variables and set default values
|
|
|
|
|
|
this.containerElement = container; |
|
|
|
|
|
this.width = '100%'; |
|
|
|
|
|
this.height = '100%'; |
|
|
|
|
|
this.refreshRate = 50; // milliseconds
|
|
|
|
|
|
this.stabilize = true; // stabilize before displaying the graph
|
|
|
|
|
|
this.selectable = true; |
|
|
|
|
|
|
|
|
|
|
|
// set constant values
|
|
|
|
|
|
this.constants = { |
|
|
|
|
|
nodes: { |
|
|
|
|
|
radiusMin: 5, |
|
|
|
|
|
radiusMax: 20, |
|
|
|
|
|
radius: 5, |
|
|
|
|
|
distance: 100, // px
|
|
|
|
|
|
shape: 'ellipse', |
|
|
|
|
|
image: undefined, |
|
|
|
|
|
widthMin: 16, // px
|
|
|
|
|
|
widthMax: 64, // px
|
|
|
|
|
|
fontColor: 'black', |
|
|
|
|
|
fontSize: 14, // px
|
|
|
|
|
|
//fontFace: verdana,
|
|
|
|
|
|
fontFace: 'arial', |
|
|
|
|
|
color: { |
|
|
|
|
|
border: '#2B7CE9', |
|
|
|
|
|
background: '#97C2FC', |
|
|
|
|
|
highlight: { |
|
|
|
|
|
border: '#2B7CE9', |
|
|
|
|
|
background: '#D2E5FF' |
|
|
|
|
|
}, |
|
|
|
|
|
cluster: { |
|
|
|
|
|
border: '#256a2d', |
|
|
|
|
|
background: '#2cd140', |
|
|
|
|
|
highlight: { |
|
|
|
|
|
border: '#899539', |
|
|
|
|
|
background: '#c5dc29' |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
borderColor: '#2B7CE9', |
|
|
|
|
|
backgroundColor: '#97C2FC', |
|
|
|
|
|
highlightColor: '#D2E5FF', |
|
|
|
|
|
group: undefined |
|
|
|
|
|
}, |
|
|
|
|
|
edges: { |
|
|
|
|
|
widthMin: 1, |
|
|
|
|
|
widthMax: 15, |
|
|
|
|
|
width: 1, |
|
|
|
|
|
style: 'line', |
|
|
|
|
|
color: '#343434', |
|
|
|
|
|
fontColor: '#343434', |
|
|
|
|
|
fontSize: 14, // px
|
|
|
|
|
|
fontFace: 'arial', |
|
|
|
|
|
//distance: 100, //px
|
|
|
|
|
|
length: 100, // px
|
|
|
|
|
|
dash: { |
|
|
|
|
|
length: 10, |
|
|
|
|
|
gap: 5, |
|
|
|
|
|
altLength: undefined |
|
|
|
|
|
} |
|
|
|
|
|
}, |
|
|
|
|
|
clustering: { |
|
|
|
|
|
clusterLength: 30, // threshold edge length for clustering
|
|
|
|
|
|
fontSizeMultiplier: 2, // how much the cluster font size grows per node (in px)
|
|
|
|
|
|
forceAmplification: 0.6, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force
|
|
|
|
|
|
distanceAmplification: 0.1, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force
|
|
|
|
|
|
edgeGrowth: 10, // amount of clusterSize connected to the edge is multiplied with this and added to edgeLength
|
|
|
|
|
|
clusterSizeWidthFactor: 10, |
|
|
|
|
|
clusterSizeHeightFactor: 10, |
|
|
|
|
|
clusterSizeRadiusFactor: 10, |
|
|
|
|
|
massTransferCoefficient: 0.2 // parent.mass += massTransferCoefficient * child.mass
|
|
|
|
|
|
}, |
|
|
|
|
|
minForce: 0.05, |
|
|
|
|
|
minVelocity: 0.02, // px/s
|
|
|
|
|
|
maxIterations: 1000 // maximum number of iteration to stabilize
|
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
Cluster.call(this); |
|
|
|
|
|
|
|
|
|
|
|
var graph = this; |
|
|
|
|
|
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
|
|
|
|
|
|
this.scale = 1; // defining the global scale variable in the constructor
|
|
|
|
|
|
this.previousScale = this.scale; // this is used to check if the zoom operation is zooming in or out
|
|
|
|
|
|
// TODO: create a counter to keep track on the number of nodes having values
|
|
|
|
|
|
// TODO: create a counter to keep track on the number of nodes currently moving
|
|
|
|
|
|
// TODO: create a counter to keep track on the number of edges having values
|
|
|
|
|
|
|
|
|
|
|
|
this.nodesData = null; // A DataSet or DataView
|
|
|
|
|
|
this.edgesData = null; // A DataSet or DataView
|
|
|
|
|
|
|
|
|
|
|
|
// create event listeners used to subscribe on the DataSets of the nodes and edges
|
|
|
|
|
|
var me = this; |
|
|
|
|
|
this.nodesListeners = { |
|
|
|
|
|
'add': function (event, params) { |
|
|
|
|
|
me._addNodes(params.items); |
|
|
|
|
|
me.start(); |
|
|
|
|
|
}, |
|
|
|
|
|
'update': function (event, params) { |
|
|
|
|
|
me._updateNodes(params.items); |
|
|
|
|
|
me.start(); |
|
|
|
|
|
}, |
|
|
|
|
|
'remove': function (event, params) { |
|
|
|
|
|
me._removeNodes(params.items); |
|
|
|
|
|
me.start(); |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
this.edgesListeners = { |
|
|
|
|
|
'add': function (event, params) { |
|
|
|
|
|
me._addEdges(params.items); |
|
|
|
|
|
me.start(); |
|
|
|
|
|
}, |
|
|
|
|
|
'update': function (event, params) { |
|
|
|
|
|
me._updateEdges(params.items); |
|
|
|
|
|
me.start(); |
|
|
|
|
|
}, |
|
|
|
|
|
'remove': function (event, params) { |
|
|
|
|
|
me._removeEdges(params.items); |
|
|
|
|
|
me.start(); |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
this.groups = new Groups(); // object with groups
|
|
|
|
|
|
this.images = new Images(); // object with images
|
|
|
|
|
|
this.images.setOnloadCallback(function () { |
|
|
|
|
|
graph._redraw(); |
|
|
|
|
|
}); |
|
|
|
|
|
|
|
|
|
|
|
// properties of the data
|
|
|
|
|
|
this.moving = false; // True if any of the nodes have an undefined position
|
|
|
|
|
|
|
|
|
|
|
|
this.selection = []; |
|
|
|
|
|
this.timer = undefined; |
|
|
|
|
|
|
|
|
|
|
|
// create a frame and canvas
|
|
|
|
|
|
this._create(); |
|
|
|
|
|
|
|
|
|
|
|
// apply options
|
|
|
|
|
|
this.setOptions(options); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// draw data
|
|
|
|
|
|
this.setData(data); |
|
|
|
|
|
|
|
|
|
|
|
// zoom so all data will fit on the screen
|
|
|
|
|
|
this.zoomToFit(); |
|
|
|
|
|
|
|
|
|
|
|
// cluster if the data set is big
|
|
|
|
|
|
this.clusterToFit(); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Graph.prototype = Object.create(Cluster.prototype); |
|
|
|
|
|
|
|
|
|
|
|
Graph.prototype.clusterToFit = function() { |
|
|
|
|
|
var numberOfNodes = this.nodeIndices.length; |
|
|
|
|
|
var maxNumberOfNodes = 100; |
|
|
|
|
|
|
|
|
|
|
|
var maxLevels = 10; |
|
|
|
|
|
var level = 0; |
|
|
|
|
|
while (numberOfNodes >= maxNumberOfNodes && level < maxLevels) { |
|
|
|
|
|
this.increaseClusterLevel(); |
|
|
|
|
|
numberOfNodes = this.nodeIndices.length; |
|
|
|
|
|
level += 1; |
|
|
|
|
|
} |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
Graph.prototype.zoomToFit = function() { |
|
|
|
|
|
var numberOfNodes = this.nodeIndices.length; |
|
|
|
|
|
var zoomLevel = 105 / (numberOfNodes + 80); // this is obtained from fitting a dataset from 5 points with scale levels that looked good.
|
|
|
|
|
|
if (zoomLevel > 1.0) { |
|
|
|
|
|
zoomLevel = 1.0; |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if (!('mousewheelScale' in this.pinch)) { |
|
|
|
|
|
this.pinch.mousewheelScale = zoomLevel; |
|
|
|
|
|
} |
|
|
|
|
|
this._setScale(zoomLevel); |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* Update the this.nodeIndices with the most recent node index list |
|
|
* Update the this.nodeIndices with the most recent node index list |
|
@ -16387,7 +16400,6 @@ Graph.prototype._getConnectionCount = function(level) { |
|
|
return hubs; |
|
|
return hubs; |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* Set a new size for the graph |
|
|
* Set a new size for the graph |
|
|
* @param {string} width Width in pixels or percentage (for example '800px' |
|
|
* @param {string} width Width in pixels or percentage (for example '800px' |
|
@ -16787,6 +16799,7 @@ Graph.prototype._getTranslation = function() { |
|
|
Graph.prototype._setScale = function(scale) { |
|
|
Graph.prototype._setScale = function(scale) { |
|
|
this.scale = scale; |
|
|
this.scale = scale; |
|
|
}; |
|
|
}; |
|
|
|
|
|
|
|
|
/** |
|
|
/** |
|
|
* Get the current scale of the graph |
|
|
* Get the current scale of the graph |
|
|
* @return {Number} scale Scaling factor 1.0 is unscaled |
|
|
* @return {Number} scale Scaling factor 1.0 is unscaled |
|
@ -16953,8 +16966,8 @@ Graph.prototype._calculateForces = function() { |
|
|
distance = Math.sqrt(dx * dx + dy * dy); |
|
|
distance = Math.sqrt(dx * dx + dy * dy); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// clusters have a larger region of influence
|
|
|
minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification)); |
|
|
minimumDistance = (clusterSize == 0) ? this.constants.nodes.distance : (this.constants.nodes.distance * (1 + clusterSize * this.constants.clustering.distanceAmplification)); |
|
|
|
|
|
|
|
|
if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
|
|
|
if (distance < 2*minimumDistance) { // at 2.0 * the minimum distance, the force is 0.000045
|
|
|
angle = Math.atan2(dy, dx); |
|
|
angle = Math.atan2(dy, dx); |
|
|
|
|
|
|
|
@ -16965,9 +16978,6 @@ Graph.prototype._calculateForces = function() { |
|
|
// TODO: correct factor for repulsing force
|
|
|
// TODO: correct factor for repulsing force
|
|
|
//repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
|
|
|
//repulsingForce = 2 * Math.exp(-5 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
|
|
|
//repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
|
|
|
//repulsingForce = Math.exp(-1 * (distance * distance) / (dmin * dmin) ); // TODO: customize the repulsing force
|
|
|
|
|
|
|
|
|
// clusters have a larger region of influence
|
|
|
|
|
|
|
|
|
|
|
|
repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
|
|
|
repulsingForce = 1 / (1 + Math.exp((distance / minimumDistance - 1) * steepness)); // TODO: customize the repulsing force
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|