Browse Source

added support for seperate universes. struggling with mixins.

css_transitions
Alex de Mulder 11 years ago
parent
commit
e91b3bd7a6
7 changed files with 652 additions and 145 deletions
  1. +1
    -0
      Jakefile.js
  2. +326
    -73
      dist/vis.js
  3. +1
    -1
      examples/graph/02.1_really_random_nodes.html
  4. +154
    -69
      src/graph/Graph.js
  5. +11
    -1
      src/graph/Node.js
  6. +9
    -1
      src/graph/cluster.js
  7. +150
    -0
      src/graph/universe.js

+ 1
- 0
Jakefile.js View File

@ -83,6 +83,7 @@ task('build', {async: true}, function () {
'./src/graph/Popup.js',
'./src/graph/Groups.js',
'./src/graph/Images.js',
'./src/graph/Universe.js',
'./src/graph/Cluster.js',
'./src/graph/Graph.js',

+ 326
- 73
dist/vis.js View File

@ -4,8 +4,8 @@
*
* A dynamic, browser-based visualization library.
*
* @version 0.4.0-SNAPSHOT
* @date 2014-01-17
* @version @@version
* @date @@date
*
* @license
* Copyright (C) 2011-2014 Almende B.V, http://almende.com
@ -8794,6 +8794,7 @@ function Node(properties, imagelist, grouplist, constants) {
this.grouplist = grouplist;
this.nodeProperties = properties;
this.setProperties(properties, constants);
// creating the variables for clustering
@ -9604,7 +9605,12 @@ Node.prototype.getTextSize = function(ctx) {
}
};
/**
* this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
* there is a safety margin of 0.3 * width;
*
* @returns {boolean}
*/
Node.prototype.inArea = function() {
if (this.width !== undefined) {
return (this.x + this.width*0.8 >= this.canvasTopLeft.x &&
@ -9617,6 +9623,10 @@ Node.prototype.inArea = function() {
}
}
/**
* checks if the core of the node is in the display area, this is used for opening clusters around zoom
* @returns {boolean}
*/
Node.prototype.inView = function() {
return (this.x >= this.canvasTopLeft.x &&
this.x < this.canvasBottomRight.x &&
@ -10518,6 +10528,156 @@ Images.prototype.load = function(url) {
return img;
};
function Universe() {
this.universe = {};
this.activeUniverse = ["default"];
this.universe["activePockets"] = {};
this.universe["activePockets"][this.activeUniverse[this.activeUniverse.length-1]] = {"nodes":{},"edges":{},"nodeIndices":[]};
this.universe["frozenPockets"] = {};
this.universe["draw"] = {};
this.nodeIndices = this.universe["activePockets"][this.activeUniverse[this.activeUniverse.length-1]]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
}
Universe.prototype._putDataInUniverse = function() {
this.universe["activePockets"][this._universe()].nodes = this.nodes;
this.universe["activePockets"][this._universe()].edges = this.edges;
this.universe["activePockets"][this._universe()].nodeIndices = this.nodeIndices;
};
Universe.prototype._switchToUniverse = function(universeID) {
this.nodeIndices = this.universe["activePockets"][universeID]["nodeIndices"];
this.nodes = this.universe["activePockets"][universeID]["nodes"];
this.edges = this.universe["activePockets"][universeID]["edges"];
};
Universe.prototype._loadActiveUniverse = function() {
this._switchToUniverse(this._universe());
};
Universe.prototype._universe = function() {
return this.activeUniverse[this.activeUniverse.length-1];
};
Universe.prototype._previousUniverse = function() {
if (this.activeUniverse.length > 1) {
return this.activeUniverse[this.activeUniverse.length-2];
}
else {
throw new TypeError('there are not enough universes in the this.activeUniverse array.');
return "";
}
};
Universe.prototype._setActiveUniverse = function(newID) {
this.activeUniverse.push(newID);
};
Universe.prototype._forgetLastUniverse = function() {
this.activeUniverse.pop();
};
Universe.prototype._createNewUniverse = function(newID) {
this.universe["activePockets"][newID] = {"nodes":{},"edges":{},"nodeIndices":[]}
};
Universe.prototype._deleteActiveUniverse = function(universeID) {
delete this.universe["activePockets"][universeID];
};
Universe.prototype._deleteFrozenUniverse = function(universeID) {
delete this.universe["frozenPockets"][universeID];
};
Universe.prototype._freezeUniverse = function(universeID) {
this.universe["frozenPockets"][universeID] = this.universe["activePockets"][universeID];
this._deleteActiveUniverse(universeID);
};
Universe.prototype._activateUniverse = function(universeID) {
this.universe["activePockets"][universeID] = this.universe["frozenPockets"][universeID];
this._deleteFrozenUniverse(universeID);
};
Universe.prototype._mergeThisWithFrozen = function(universeID) {
for (var nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
this.universe["frozenPockets"][universeID]["nodes"][nodeID] = this.nodes[nodeID];
}
}
for (var edgeID in this.edges) {
if (this.edges.hasOwnProperty(edgeID)) {
this.universe["frozenPockets"][universeID]["edges"][edgeID] = this.edges[edgeID];
}
}
for (var i = 0; i < this.nodeIndices.length; i++) {
this.universe["frozenPockets"][universeID]["edges"][nodeIndices].push(this.nodeIndices[i]);
}
};
Universe.prototype._collapseThisToSingleCluster = function() {
this.clusterToFit(1,false);
};
Universe.prototype._addUniverse = function(node) {
var universe = this._universe();
if (this.universe['activePockets'][universe]["nodes"].hasOwnProperty(node.id)) {
console.log("the node is part of the active universe");
}
else {
console.log("I dont konw what the fuck happened!!");
}
delete this.nodes[node.id];
this._freezeUniverse(universe);
this._createNewUniverse(node.id);
this._setActiveUniverse(node.id);
this._switchToUniverse(this._universe());
this.nodes[node.id] = node;
//this.universe["draw"][node.id] = new Node(node.nodeProperties,node.imagelist,node.grouplist,this.constants);
};
Universe.prototype._collapseUniverse = function() {
var universe = this._universe();
if (universe != "default") {
var isMovingBeforeClustering = this.moving;
var previousUniverse = this._previousUniverse();
this._collapseThisToSingleCluster();
this._mergeThisWithFrozen(previousUniverse);
this._deleteActiveUniverse(universe);
this._activateUniverse(previousUniverse);
this._switchToUniverse(previousUniverse);
this._forgetLastUniverse();
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();
}
}
};
Universe.prototype._doInAllActiveUniverses = function(runFunction,arguments) {
}
/**
* @constructor Cluster
* Contains the cluster properties for the graph object
@ -10525,8 +10685,8 @@ Images.prototype.load = function(url) {
function Cluster() {
this.clusterSession = 0;
this.hubThreshold = 5;
}
}
/**
* This function can be called to open up a specific cluster.
@ -10535,6 +10695,9 @@ function Cluster() {
* @param node | Node object: cluster to open.
*/
Cluster.prototype.openCluster = function(node) {
if (node.clusterSize > 15) {
this._addUniverse(node);
}
var isMovingBeforeClustering = this.moving;
this._expandClusterNode(node,false,true);
@ -10594,6 +10757,11 @@ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) {
var isMovingBeforeClustering = this.moving;
var amountOfNodes = this.nodeIndices.length;
// on zoom out collapse the universe back to default
// if (this.previousScale > this.scale && zoomDirection == 0) {
// this._collapseUniverse();
// }
// check if we zoom in or out
if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
this._formClusters(force);
@ -11505,8 +11673,8 @@ function Graph (container, data, options) {
enableClustering: true,
maxNumberOfNodes: 100, // for automatic (initial) clustering
snakeThreshold: 0.5, // maximum percentage of allowed snakes (long strings of connected nodes)
clusterLength: 30, // threshold edge length for clusteringl
relativeOpenFactor: 0.5, // if the width or height of a cluster takes up this much of the screen, open the cluster
clusterLength: 25, // threshold edge length for clusteringl
relativeOpenFactor: 0.2, // if the width or height of a cluster takes up this much of the screen, open the cluster
fontSizeMultiplier: 4, // how much the cluster font size grows per node (in px)
forceAmplification: 0.7, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force
distanceAmplification: 0.3, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force
@ -11525,13 +11693,19 @@ function Graph (container, data, options) {
// call the constructor of the cluster object
Cluster.call(this);
// call the universe constructor
Universe.call(this);
var graph = this;
this.freezeSimulation = false;
this.nodeIndices = []; // the node indices list is used to speed up the computation of the repulsion fields
this.tapTimer = 0;
this.pocketUniverse = {};
this.nodes = {}; // object with Node objects
this.edges = {}; // object with Edge objects
this.freezeSimulation = false;// freeze the simulation
this.tapTimer = 0; // timer to detect doubleclick or double tap
this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
this.nodes = {}; // object with Node objects
this.edges = {}; // object with Edge objects
this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during calcForces.
this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during calcForces
this.zoomCenter = {}; // object with x and y elements used for determining the center of the zoom action
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
@ -11591,10 +11765,8 @@ function Graph (container, data, options) {
// apply options
this.setOptions(options);
var disableStart = this.constants.clustering.enableClustering;
// load data
this.setData(data,disableStart); //
// load data (the disable start variable will be the same as the enable clustering)
this.setData(data,this.constants.clustering.enableClustering); //
// zoom so all data will fit on the screen
this.zoomToFit();
@ -11608,8 +11780,6 @@ function Graph (container, data, options) {
// this is called here because if clusterin is disabled, the start and stabilize are called in
// the setData function.
// find a stable position or start animating to a stable position
if (this.stabilize) {
this._doStabilize();
}
@ -11617,6 +11787,10 @@ function Graph (container, data, options) {
}
}
// add the universe functionality to this
Graph.prototype = Object.create(Universe.prototype);
/**
* We add the functionality of the cluster object to the graph object
* @type {Cluster.prototype}
@ -11632,11 +11806,11 @@ Graph.prototype = Object.create(Cluster.prototype);
Graph.prototype.clusterToFit = function(maxNumberOfNodes, reposition) {
var numberOfNodes = this.nodeIndices.length;
var maxLevels = 10;
var maxLevels = 15;
var level = 0;
// we first cluster the hubs, then we pull in the outliers, repeat
while (numberOfNodes >= maxNumberOfNodes && level < maxLevels) {
while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
if (level % 5 == 0) {
console.log("Aggregating Hubs @ level: ",level,". Threshold:", this.hubThreshold,"clusterSession",this.clusterSession);
this.forceAggregateHubs();
@ -11649,7 +11823,7 @@ Graph.prototype.clusterToFit = function(maxNumberOfNodes, reposition) {
level += 1;
}
// after the clustering we reposition the nodes to avoid initial chaos
// after the clustering we reposition the nodes to reduce the initial chaos
if (level > 1 && reposition == true) {
this.repositionNodes();
}
@ -11677,8 +11851,9 @@ Graph.prototype.zoomToFit = function() {
* @private
*/
Graph.prototype._updateNodeIndexList = function() {
this.nodeIndices = [];
var universe = this.activeUniverse[this.activeUniverse.length-1];
this.universe["activePockets"][universe]["nodeIndices"] = [];
this.nodeIndices = this.universe["activePockets"][universe]["nodeIndices"];
for (var idx in this.nodes) {
if (this.nodes.hasOwnProperty(idx)) {
this.nodeIndices.push(idx);
@ -11724,6 +11899,8 @@ Graph.prototype.setData = function(data, disableStart) {
this._setEdges(data && data.edges);
}
this._putDataInUniverse();
if (!disableStart) {
// find a stable position or start animating to a stable position
if (this.stabilize) {
@ -11868,14 +12045,15 @@ Graph.prototype._create = function () {
this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
/*
this.mouseTrap = mouseTrap;
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.updateClustersDefault.bind(me));
this.mouseTrap.bind("c",this._collapseUniverse.bind(me));
this.mouseTrap.bind("f",this.toggleFreeze.bind(me));
*/
// add the frame to the container element
this.containerElement.appendChild(this.frame);
};
@ -12148,7 +12326,7 @@ Graph.prototype._zoom = function(scale, pointer) {
this.updateClustersDefault();
this._redraw();
console.log("current zoomscale:",this.scale);
// console.log("current zoomscale:",this.scale);
return scale;
};
@ -12433,16 +12611,36 @@ Graph.prototype._selectNodes = function(selection, append) {
* @private
*/
Graph.prototype._getNodesOverlappingWith = function (obj) {
var nodes = this.nodes,
overlappingNodes = [];
var overlappingNodes = [];
var nodes;
// search in all universes for nodes
for (var universe in this.universe["activePockets"]) {
if (this.universe["activePockets"].hasOwnProperty(universe)) {
nodes = this.universe["activePockets"][universe]["nodes"];
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
if (nodes[id].isOverlappingWith(obj)) {
overlappingNodes.push(id);
}
}
}
}
}
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
if (nodes[id].isOverlappingWith(obj)) {
overlappingNodes.push(id);
for (var universe in this.universe["frozenPockets"]) {
if (this.universe["frozenPockets"].hasOwnProperty(universe)) {
nodes = this.universe["frozenPockets"][universe]["nodes"];
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
if (nodes[id].isOverlappingWith(obj)) {
overlappingNodes.push(id);
}
}
}
}
}
this.nodes = this.universe["activePockets"][this.activeUniverse[this.activeUniverse.length-1]]["nodes"];
return overlappingNodes;
};
@ -12936,8 +13134,41 @@ Graph.prototype._redraw = function() {
ctx.translate(this.translation.x, this.translation.y);
ctx.scale(this.scale, this.scale);
this._drawEdges(ctx);
this._drawNodes(ctx);
// this._drawEdges(ctx);
// this._drawNodes(ctx);
for (var universe in this.universe["activePockets"]) {
if (this.universe["activePockets"].hasOwnProperty(universe)) {
this.edges = this.universe["activePockets"][universe]["edges"];
this._drawEdges(ctx);
}
}
for (var universe in this.universe["frozenPockets"]) {
if (this.universe["frozenPockets"].hasOwnProperty(universe)) {
this.edges = this.universe["frozenPockets"][universe]["edges"];
this._drawEdges(ctx);
}
}
for (var universe in this.universe["activePockets"]) {
if (this.universe["activePockets"].hasOwnProperty(universe)) {
this.nodes = this.universe["activePockets"][universe]["nodes"];
this._drawNodes(ctx);
}
}
for (var universe in this.universe["frozenPockets"]) {
if (this.universe["frozenPockets"].hasOwnProperty(universe)) {
this.nodes = this.universe["frozenPockets"][universe]["nodes"];
this._drawNodes(ctx);
}
}
this.nodeIndices = this.universe["activePockets"][this.activeUniverse[this.activeUniverse.length-1]]["nodeIndices"];
this.nodes = this.universe["activePockets"][this.activeUniverse[this.activeUniverse.length-1]]["nodes"];
this.edges = this.universe["activePockets"][this.activeUniverse[this.activeUniverse.length-1]]["edges"];
// restore original scaling and translation
ctx.restore();
@ -13046,15 +13277,9 @@ Graph.prototype._drawNodes = function(ctx) {
var nodes = this.nodes;
var selected = [];
var canvasTopLeft = {"x": (0-this.translation.x)/this.scale,
"y": (0-this.translation.y)/this.scale};
var canvasBottomRight = {"x": (this.frame.canvas.clientWidth -this.translation.x)/this.scale,
"y": (this.frame.canvas.clientHeight-this.translation.y)/this.scale};
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
nodes[id].setScaleAndPos(this.scale,canvasTopLeft,canvasBottomRight);
nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
if (nodes[id].isSelected()) {
selected.push(id);
}
@ -13121,7 +13346,7 @@ Graph.prototype._doStabilize = function() {
* Forces are caused by: edges, repulsing forces between nodes, gravity
* @private
*/
Graph.prototype._calculateForces = function() {
Graph.prototype._calculateForces = function(nodes,edges) {
// stop calculation if there is only one node
if (this.nodeIndices.length == 1) {
this.nodes[this.nodeIndices[0]]._setForce(0,0);
@ -13132,6 +13357,13 @@ Graph.prototype._calculateForces = function() {
this._calculateForces();
}
else {
this.canvasTopLeft = {"x": (0-this.translation.x)/this.scale,
"y": (0-this.translation.y)/this.scale};
this.canvasBottomRight = {"x": (this.frame.canvas.clientWidth -this.translation.x)/this.scale,
"y": (this.frame.canvas.clientHeight-this.translation.y)/this.scale};
var centerPos = {"x":0.5*(this.canvasTopLeft.x + this.canvasBottomRight.x),
"y":0.5*(this.canvasTopLeft.y + this.canvasBottomRight.y)}
// create a local edge to the nodes and edges, that is faster
var dx, dy, angle, distance, fx, fy,
repulsingForce, springForce, length, edgeLength,
@ -13147,12 +13379,18 @@ Graph.prototype._calculateForces = function() {
var gravity = 0.08;
for (i = 0; i < this.nodeIndices.length; i++) {
node = nodes[this.nodeIndices[i]];
dx = -node.x - this.translation.x + this.frame.canvas.clientWidth*0.5;
dy = -node.y - this.translation.y + this.frame.canvas.clientHeight*0.5;
if (this.activeUniverse[this.activeUniverse.length-1] == "default") {
dx = -node.x + centerPos.x;
dy = -node.y + centerPos.y;
angle = Math.atan2(dy, dx);
fx = Math.cos(angle) * gravity;
fy = Math.sin(angle) * gravity;
angle = Math.atan2(dy, dx);
fx = Math.cos(angle) * gravity;
fy = Math.sin(angle) * gravity;
}
else {
fx = 0;
fy = 0;
}
node._setForce(fx, fy);
node.updateDamping(this.nodeIndices.length);
@ -13249,25 +13487,28 @@ Graph.prototype._calculateForces = function() {
if (edges.hasOwnProperty(edgeID)) {
edge = edges[edgeID];
if (edge.connected) {
clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2);
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
//edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin
//edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin
//edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2;
edgeLength = edge.length;
// this implies that the edges between big clusters are longer
edgeLength += clusterSize * this.constants.clustering.edgeGrowth;
length = Math.sqrt(dx * dx + dy * dy);
angle = Math.atan2(dy, dx);
springForce = edge.stiffness * (edgeLength - length);
fx = Math.cos(angle) * springForce;
fy = Math.sin(angle) * springForce;
edge.from._addForce(-fx, -fy);
edge.to._addForce(fx, fy);
// only calculate forces if nodes are in the same universe
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2);
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
//edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin
//edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin
//edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2;
edgeLength = edge.length;
// this implies that the edges between big clusters are longer
edgeLength += clusterSize * this.constants.clustering.edgeGrowth;
length = Math.sqrt(dx * dx + dy * dy);
angle = Math.atan2(dy, dx);
springForce = edge.stiffness * (edgeLength - length);
fx = Math.cos(angle) * springForce;
fy = Math.sin(angle) * springForce;
edge.from._addForce(-fx, -fy);
edge.to._addForce(fx, fy);
}
}
}
}
@ -13348,15 +13589,28 @@ Graph.prototype._discreteStepNodes = function() {
/**
* Start animating nodes and edges
*/
Graph.prototype.start = function() {
if (!this.freezeSimulation) {
if (this.moving) {
var vmin = this.constants.minVelocity;
/*
this._calculateForces();
this._discreteStepNodes();
var vmin = this.constants.minVelocity;
this.moving = this._isMoving(vmin);
*/
//console.log("no",this.nodes)
for (var universe in this.universe["activePockets"]) {
if (this.universe["activePockets"].hasOwnProperty(universe)) {
this._switchToUniverse(universe);
this._calculateForces();
this._discreteStepNodes();
this.moving = this._isMoving(vmin);
}
}
this._loadActiveUniverse();
}
if (this.moving) {
@ -13364,17 +13618,16 @@ Graph.prototype.start = function() {
if (!this.timer) {
var graph = this;
this.timer = window.setTimeout(function () {
var start,end,time;
graph.timer = undefined;
graph.start();
graph.start();
graph._redraw();
// start = window.performance.now();
// var start = window.performance.now();
// graph._redraw();
// end = window.performance.now();
// time = end - start;
// var end = window.performance.now();
// var time = end - start;
// console.log('Drawing time: ' + time);
}, this.refreshRate);
}

+ 1
- 1
examples/graph/02.1_really_random_nodes.html View File

@ -102,7 +102,7 @@
<form onsubmit="draw(); return false;">
<label for="nodeCount">Number of nodes:</label>
<input id="nodeCount" type="text" value="25" style="width: 50px;">
<input id="nodeCount" type="text" value="50" style="width: 50px;">
<input type="submit" value="Go">
</form>
<br>

+ 154
- 69
src/graph/Graph.js View File

@ -67,8 +67,8 @@ function Graph (container, data, options) {
enableClustering: true,
maxNumberOfNodes: 100, // for automatic (initial) clustering
snakeThreshold: 0.5, // maximum percentage of allowed snakes (long strings of connected nodes)
clusterLength: 30, // threshold edge length for clusteringl
relativeOpenFactor: 0.5, // if the width or height of a cluster takes up this much of the screen, open the cluster
clusterLength: 25, // threshold edge length for clusteringl
relativeOpenFactor: 0.2, // if the width or height of a cluster takes up this much of the screen, open the cluster
fontSizeMultiplier: 4, // how much the cluster font size grows per node (in px)
forceAmplification: 0.7, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force
distanceAmplification: 0.3, // amount of clusterSize between two nodes multiply this value (+1) with the repulsion force
@ -87,13 +87,19 @@ function Graph (container, data, options) {
// call the constructor of the cluster object
Cluster.call(this);
// call the universe constructor
Universe.call(this);
var graph = this;
this.freezeSimulation = false;
this.nodeIndices = []; // the node indices list is used to speed up the computation of the repulsion fields
this.tapTimer = 0;
this.pocketUniverse = {};
this.nodes = {}; // object with Node objects
this.edges = {}; // object with Edge objects
this.freezeSimulation = false;// freeze the simulation
this.tapTimer = 0; // timer to detect doubleclick or double tap
this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
this.nodes = {}; // object with Node objects
this.edges = {}; // object with Edge objects
this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during calcForces.
this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during calcForces
this.zoomCenter = {}; // object with x and y elements used for determining the center of the zoom action
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
@ -153,10 +159,8 @@ function Graph (container, data, options) {
// apply options
this.setOptions(options);
var disableStart = this.constants.clustering.enableClustering;
// load data
this.setData(data,disableStart); //
// load data (the disable start variable will be the same as the enable clustering)
this.setData(data,this.constants.clustering.enableClustering); //
// zoom so all data will fit on the screen
this.zoomToFit();
@ -170,8 +174,6 @@ function Graph (container, data, options) {
// this is called here because if clusterin is disabled, the start and stabilize are called in
// the setData function.
// find a stable position or start animating to a stable position
if (this.stabilize) {
this._doStabilize();
}
@ -179,6 +181,10 @@ function Graph (container, data, options) {
}
}
// add the universe functionality to this
Graph.prototype = Object.create(Universe.prototype);
/**
* We add the functionality of the cluster object to the graph object
* @type {Cluster.prototype}
@ -194,11 +200,11 @@ Graph.prototype = Object.create(Cluster.prototype);
Graph.prototype.clusterToFit = function(maxNumberOfNodes, reposition) {
var numberOfNodes = this.nodeIndices.length;
var maxLevels = 10;
var maxLevels = 15;
var level = 0;
// we first cluster the hubs, then we pull in the outliers, repeat
while (numberOfNodes >= maxNumberOfNodes && level < maxLevels) {
while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
if (level % 5 == 0) {
console.log("Aggregating Hubs @ level: ",level,". Threshold:", this.hubThreshold,"clusterSession",this.clusterSession);
this.forceAggregateHubs();
@ -211,7 +217,7 @@ Graph.prototype.clusterToFit = function(maxNumberOfNodes, reposition) {
level += 1;
}
// after the clustering we reposition the nodes to avoid initial chaos
// after the clustering we reposition the nodes to reduce the initial chaos
if (level > 1 && reposition == true) {
this.repositionNodes();
}
@ -239,8 +245,9 @@ Graph.prototype.zoomToFit = function() {
* @private
*/
Graph.prototype._updateNodeIndexList = function() {
this.nodeIndices = [];
var universe = this.activeUniverse[this.activeUniverse.length-1];
this.universe["activePockets"][universe]["nodeIndices"] = [];
this.nodeIndices = this.universe["activePockets"][universe]["nodeIndices"];
for (var idx in this.nodes) {
if (this.nodes.hasOwnProperty(idx)) {
this.nodeIndices.push(idx);
@ -286,6 +293,8 @@ Graph.prototype.setData = function(data, disableStart) {
this._setEdges(data && data.edges);
}
this._putDataInUniverse();
if (!disableStart) {
// find a stable position or start animating to a stable position
if (this.stabilize) {
@ -430,14 +439,15 @@ Graph.prototype._create = function () {
this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
/*
this.mouseTrap = mouseTrap;
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.updateClustersDefault.bind(me));
this.mouseTrap.bind("c",this._collapseUniverse.bind(me));
this.mouseTrap.bind("f",this.toggleFreeze.bind(me));
*/
// add the frame to the container element
this.containerElement.appendChild(this.frame);
};
@ -710,7 +720,7 @@ Graph.prototype._zoom = function(scale, pointer) {
this.updateClustersDefault();
this._redraw();
console.log("current zoomscale:",this.scale);
// console.log("current zoomscale:",this.scale);
return scale;
};
@ -995,16 +1005,36 @@ Graph.prototype._selectNodes = function(selection, append) {
* @private
*/
Graph.prototype._getNodesOverlappingWith = function (obj) {
var nodes = this.nodes,
overlappingNodes = [];
var overlappingNodes = [];
var nodes;
// search in all universes for nodes
for (var universe in this.universe["activePockets"]) {
if (this.universe["activePockets"].hasOwnProperty(universe)) {
nodes = this.universe["activePockets"][universe]["nodes"];
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
if (nodes[id].isOverlappingWith(obj)) {
overlappingNodes.push(id);
}
}
}
}
}
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
if (nodes[id].isOverlappingWith(obj)) {
overlappingNodes.push(id);
for (var universe in this.universe["frozenPockets"]) {
if (this.universe["frozenPockets"].hasOwnProperty(universe)) {
nodes = this.universe["frozenPockets"][universe]["nodes"];
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
if (nodes[id].isOverlappingWith(obj)) {
overlappingNodes.push(id);
}
}
}
}
}
this.nodes = this.universe["activePockets"][this.activeUniverse[this.activeUniverse.length-1]]["nodes"];
return overlappingNodes;
};
@ -1498,8 +1528,41 @@ Graph.prototype._redraw = function() {
ctx.translate(this.translation.x, this.translation.y);
ctx.scale(this.scale, this.scale);
this._drawEdges(ctx);
this._drawNodes(ctx);
// this._drawEdges(ctx);
// this._drawNodes(ctx);
for (var universe in this.universe["activePockets"]) {
if (this.universe["activePockets"].hasOwnProperty(universe)) {
this.edges = this.universe["activePockets"][universe]["edges"];
this._drawEdges(ctx);
}
}
for (var universe in this.universe["frozenPockets"]) {
if (this.universe["frozenPockets"].hasOwnProperty(universe)) {
this.edges = this.universe["frozenPockets"][universe]["edges"];
this._drawEdges(ctx);
}
}
for (var universe in this.universe["activePockets"]) {
if (this.universe["activePockets"].hasOwnProperty(universe)) {
this.nodes = this.universe["activePockets"][universe]["nodes"];
this._drawNodes(ctx);
}
}
for (var universe in this.universe["frozenPockets"]) {
if (this.universe["frozenPockets"].hasOwnProperty(universe)) {
this.nodes = this.universe["frozenPockets"][universe]["nodes"];
this._drawNodes(ctx);
}
}
this.nodeIndices = this.universe["activePockets"][this.activeUniverse[this.activeUniverse.length-1]]["nodeIndices"];
this.nodes = this.universe["activePockets"][this.activeUniverse[this.activeUniverse.length-1]]["nodes"];
this.edges = this.universe["activePockets"][this.activeUniverse[this.activeUniverse.length-1]]["edges"];
// restore original scaling and translation
ctx.restore();
@ -1608,15 +1671,9 @@ Graph.prototype._drawNodes = function(ctx) {
var nodes = this.nodes;
var selected = [];
var canvasTopLeft = {"x": (0-this.translation.x)/this.scale,
"y": (0-this.translation.y)/this.scale};
var canvasBottomRight = {"x": (this.frame.canvas.clientWidth -this.translation.x)/this.scale,
"y": (this.frame.canvas.clientHeight-this.translation.y)/this.scale};
for (var id in nodes) {
if (nodes.hasOwnProperty(id)) {
nodes[id].setScaleAndPos(this.scale,canvasTopLeft,canvasBottomRight);
nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
if (nodes[id].isSelected()) {
selected.push(id);
}
@ -1683,7 +1740,7 @@ Graph.prototype._doStabilize = function() {
* Forces are caused by: edges, repulsing forces between nodes, gravity
* @private
*/
Graph.prototype._calculateForces = function() {
Graph.prototype._calculateForces = function(nodes,edges) {
// stop calculation if there is only one node
if (this.nodeIndices.length == 1) {
this.nodes[this.nodeIndices[0]]._setForce(0,0);
@ -1694,6 +1751,13 @@ Graph.prototype._calculateForces = function() {
this._calculateForces();
}
else {
this.canvasTopLeft = {"x": (0-this.translation.x)/this.scale,
"y": (0-this.translation.y)/this.scale};
this.canvasBottomRight = {"x": (this.frame.canvas.clientWidth -this.translation.x)/this.scale,
"y": (this.frame.canvas.clientHeight-this.translation.y)/this.scale};
var centerPos = {"x":0.5*(this.canvasTopLeft.x + this.canvasBottomRight.x),
"y":0.5*(this.canvasTopLeft.y + this.canvasBottomRight.y)}
// create a local edge to the nodes and edges, that is faster
var dx, dy, angle, distance, fx, fy,
repulsingForce, springForce, length, edgeLength,
@ -1709,12 +1773,18 @@ Graph.prototype._calculateForces = function() {
var gravity = 0.08;
for (i = 0; i < this.nodeIndices.length; i++) {
node = nodes[this.nodeIndices[i]];
dx = -node.x - this.translation.x + this.frame.canvas.clientWidth*0.5;
dy = -node.y - this.translation.y + this.frame.canvas.clientHeight*0.5;
if (this.activeUniverse[this.activeUniverse.length-1] == "default") {
dx = -node.x + centerPos.x;
dy = -node.y + centerPos.y;
angle = Math.atan2(dy, dx);
fx = Math.cos(angle) * gravity;
fy = Math.sin(angle) * gravity;
angle = Math.atan2(dy, dx);
fx = Math.cos(angle) * gravity;
fy = Math.sin(angle) * gravity;
}
else {
fx = 0;
fy = 0;
}
node._setForce(fx, fy);
node.updateDamping(this.nodeIndices.length);
@ -1811,25 +1881,28 @@ Graph.prototype._calculateForces = function() {
if (edges.hasOwnProperty(edgeID)) {
edge = edges[edgeID];
if (edge.connected) {
clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2);
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
//edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin
//edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin
//edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2;
edgeLength = edge.length;
// this implies that the edges between big clusters are longer
edgeLength += clusterSize * this.constants.clustering.edgeGrowth;
length = Math.sqrt(dx * dx + dy * dy);
angle = Math.atan2(dy, dx);
springForce = edge.stiffness * (edgeLength - length);
fx = Math.cos(angle) * springForce;
fy = Math.sin(angle) * springForce;
edge.from._addForce(-fx, -fy);
edge.to._addForce(fx, fy);
// only calculate forces if nodes are in the same universe
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
clusterSize = (edge.to.clusterSize + edge.from.clusterSize - 2);
dx = (edge.to.x - edge.from.x);
dy = (edge.to.y - edge.from.y);
//edgeLength = (edge.from.width + edge.from.height + edge.to.width + edge.to.height)/2 || edge.length; // TODO: dmin
//edgeLength = (edge.from.width + edge.to.width)/2 || edge.length; // TODO: dmin
//edgeLength = 20 + ((edge.from.width + edge.to.width) || 0) / 2;
edgeLength = edge.length;
// this implies that the edges between big clusters are longer
edgeLength += clusterSize * this.constants.clustering.edgeGrowth;
length = Math.sqrt(dx * dx + dy * dy);
angle = Math.atan2(dy, dx);
springForce = edge.stiffness * (edgeLength - length);
fx = Math.cos(angle) * springForce;
fy = Math.sin(angle) * springForce;
edge.from._addForce(-fx, -fy);
edge.to._addForce(fx, fy);
}
}
}
}
@ -1910,15 +1983,28 @@ Graph.prototype._discreteStepNodes = function() {
/**
* Start animating nodes and edges
*/
Graph.prototype.start = function() {
if (!this.freezeSimulation) {
if (this.moving) {
var vmin = this.constants.minVelocity;
/*
this._calculateForces();
this._discreteStepNodes();
var vmin = this.constants.minVelocity;
this.moving = this._isMoving(vmin);
*/
//console.log("no",this.nodes)
for (var universe in this.universe["activePockets"]) {
if (this.universe["activePockets"].hasOwnProperty(universe)) {
this._switchToUniverse(universe);
this._calculateForces();
this._discreteStepNodes();
this.moving = this._isMoving(vmin);
}
}
this._loadActiveUniverse();
}
if (this.moving) {
@ -1926,17 +2012,16 @@ Graph.prototype.start = function() {
if (!this.timer) {
var graph = this;
this.timer = window.setTimeout(function () {
var start,end,time;
graph.timer = undefined;
graph.start();
graph.start();
graph._redraw();
// start = window.performance.now();
// var start = window.performance.now();
// graph._redraw();
// end = window.performance.now();
// time = end - start;
// var end = window.performance.now();
// var time = end - start;
// console.log('Drawing time: ' + time);
}, this.refreshRate);
}

+ 11
- 1
src/graph/Node.js View File

@ -54,6 +54,7 @@ function Node(properties, imagelist, grouplist, constants) {
this.grouplist = grouplist;
this.nodeProperties = properties;
this.setProperties(properties, constants);
// creating the variables for clustering
@ -864,7 +865,12 @@ Node.prototype.getTextSize = function(ctx) {
}
};
/**
* this is used to determine if a node is visible at all. this is used to determine when it needs to be drawn.
* there is a safety margin of 0.3 * width;
*
* @returns {boolean}
*/
Node.prototype.inArea = function() {
if (this.width !== undefined) {
return (this.x + this.width*0.8 >= this.canvasTopLeft.x &&
@ -877,6 +883,10 @@ Node.prototype.inArea = function() {
}
}
/**
* checks if the core of the node is in the display area, this is used for opening clusters around zoom
* @returns {boolean}
*/
Node.prototype.inView = function() {
return (this.x >= this.canvasTopLeft.x &&
this.x < this.canvasBottomRight.x &&

+ 9
- 1
src/graph/cluster.js View File

@ -5,8 +5,8 @@
function Cluster() {
this.clusterSession = 0;
this.hubThreshold = 5;
}
}
/**
* This function can be called to open up a specific cluster.
@ -15,6 +15,9 @@ function Cluster() {
* @param node | Node object: cluster to open.
*/
Cluster.prototype.openCluster = function(node) {
if (node.clusterSize > 15) {
this._addUniverse(node);
}
var isMovingBeforeClustering = this.moving;
this._expandClusterNode(node,false,true);
@ -74,6 +77,11 @@ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) {
var isMovingBeforeClustering = this.moving;
var amountOfNodes = this.nodeIndices.length;
// on zoom out collapse the universe back to default
// if (this.previousScale > this.scale && zoomDirection == 0) {
// this._collapseUniverse();
// }
// check if we zoom in or out
if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
this._formClusters(force);

+ 150
- 0
src/graph/universe.js View File

@ -0,0 +1,150 @@
function Universe() {
this.universe = {};
this.activeUniverse = ["default"];
this.universe["activePockets"] = {};
this.universe["activePockets"][this.activeUniverse[this.activeUniverse.length-1]] = {"nodes":{},"edges":{},"nodeIndices":[]};
this.universe["frozenPockets"] = {};
this.universe["draw"] = {};
this.nodeIndices = this.universe["activePockets"][this.activeUniverse[this.activeUniverse.length-1]]["nodeIndices"]; // the node indices list is used to speed up the computation of the repulsion fields
}
Universe.prototype._putDataInUniverse = function() {
this.universe["activePockets"][this._universe()].nodes = this.nodes;
this.universe["activePockets"][this._universe()].edges = this.edges;
this.universe["activePockets"][this._universe()].nodeIndices = this.nodeIndices;
};
Universe.prototype._switchToUniverse = function(universeID) {
this.nodeIndices = this.universe["activePockets"][universeID]["nodeIndices"];
this.nodes = this.universe["activePockets"][universeID]["nodes"];
this.edges = this.universe["activePockets"][universeID]["edges"];
};
Universe.prototype._loadActiveUniverse = function() {
this._switchToUniverse(this._universe());
};
Universe.prototype._universe = function() {
return this.activeUniverse[this.activeUniverse.length-1];
};
Universe.prototype._previousUniverse = function() {
if (this.activeUniverse.length > 1) {
return this.activeUniverse[this.activeUniverse.length-2];
}
else {
throw new TypeError('there are not enough universes in the this.activeUniverse array.');
return "";
}
};
Universe.prototype._setActiveUniverse = function(newID) {
this.activeUniverse.push(newID);
};
Universe.prototype._forgetLastUniverse = function() {
this.activeUniverse.pop();
};
Universe.prototype._createNewUniverse = function(newID) {
this.universe["activePockets"][newID] = {"nodes":{},"edges":{},"nodeIndices":[]}
};
Universe.prototype._deleteActiveUniverse = function(universeID) {
delete this.universe["activePockets"][universeID];
};
Universe.prototype._deleteFrozenUniverse = function(universeID) {
delete this.universe["frozenPockets"][universeID];
};
Universe.prototype._freezeUniverse = function(universeID) {
this.universe["frozenPockets"][universeID] = this.universe["activePockets"][universeID];
this._deleteActiveUniverse(universeID);
};
Universe.prototype._activateUniverse = function(universeID) {
this.universe["activePockets"][universeID] = this.universe["frozenPockets"][universeID];
this._deleteFrozenUniverse(universeID);
};
Universe.prototype._mergeThisWithFrozen = function(universeID) {
for (var nodeID in this.nodes) {
if (this.nodes.hasOwnProperty(nodeID)) {
this.universe["frozenPockets"][universeID]["nodes"][nodeID] = this.nodes[nodeID];
}
}
for (var edgeID in this.edges) {
if (this.edges.hasOwnProperty(edgeID)) {
this.universe["frozenPockets"][universeID]["edges"][edgeID] = this.edges[edgeID];
}
}
for (var i = 0; i < this.nodeIndices.length; i++) {
this.universe["frozenPockets"][universeID]["edges"][nodeIndices].push(this.nodeIndices[i]);
}
};
Universe.prototype._collapseThisToSingleCluster = function() {
this.clusterToFit(1,false);
};
Universe.prototype._addUniverse = function(node) {
var universe = this._universe();
if (this.universe['activePockets'][universe]["nodes"].hasOwnProperty(node.id)) {
console.log("the node is part of the active universe");
}
else {
console.log("I dont konw what the fuck happened!!");
}
delete this.nodes[node.id];
this._freezeUniverse(universe);
this._createNewUniverse(node.id);
this._setActiveUniverse(node.id);
this._switchToUniverse(this._universe());
this.nodes[node.id] = node;
//this.universe["draw"][node.id] = new Node(node.nodeProperties,node.imagelist,node.grouplist,this.constants);
};
Universe.prototype._collapseUniverse = function() {
var universe = this._universe();
if (universe != "default") {
var isMovingBeforeClustering = this.moving;
var previousUniverse = this._previousUniverse();
this._collapseThisToSingleCluster();
this._mergeThisWithFrozen(previousUniverse);
this._deleteActiveUniverse(universe);
this._activateUniverse(previousUniverse);
this._switchToUniverse(previousUniverse);
this._forgetLastUniverse();
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();
}
}
};
Universe.prototype._doInAllActiveUniverses = function(runFunction,arguments) {
}

Loading…
Cancel
Save