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/Popup.js',
'./src/graph/Groups.js', './src/graph/Groups.js',
'./src/graph/Images.js', './src/graph/Images.js',
'./src/graph/Universe.js',
'./src/graph/Cluster.js', './src/graph/Cluster.js',
'./src/graph/Graph.js', './src/graph/Graph.js',

+ 326
- 73
dist/vis.js View File

@ -4,8 +4,8 @@
* *
* A dynamic, browser-based visualization library. * A dynamic, browser-based visualization library.
* *
* @version 0.4.0-SNAPSHOT
* @date 2014-01-17
* @version @@version
* @date @@date
* *
* @license * @license
* Copyright (C) 2011-2014 Almende B.V, http://almende.com * Copyright (C) 2011-2014 Almende B.V, http://almende.com
@ -8794,6 +8794,7 @@ function Node(properties, imagelist, grouplist, constants) {
this.grouplist = grouplist; this.grouplist = grouplist;
this.nodeProperties = properties;
this.setProperties(properties, constants); this.setProperties(properties, constants);
// creating the variables for clustering // 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() { Node.prototype.inArea = function() {
if (this.width !== undefined) { if (this.width !== undefined) {
return (this.x + this.width*0.8 >= this.canvasTopLeft.x && 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() { Node.prototype.inView = function() {
return (this.x >= this.canvasTopLeft.x && return (this.x >= this.canvasTopLeft.x &&
this.x < this.canvasBottomRight.x && this.x < this.canvasBottomRight.x &&
@ -10518,6 +10528,156 @@ Images.prototype.load = function(url) {
return img; 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 * @constructor Cluster
* Contains the cluster properties for the graph object * Contains the cluster properties for the graph object
@ -10525,8 +10685,8 @@ Images.prototype.load = function(url) {
function Cluster() { function Cluster() {
this.clusterSession = 0; this.clusterSession = 0;
this.hubThreshold = 5; this.hubThreshold = 5;
}
}
/** /**
* This function can be called to open up a specific cluster. * This function can be called to open up a specific cluster.
@ -10535,6 +10695,9 @@ function Cluster() {
* @param node | Node object: cluster to open. * @param node | Node object: cluster to open.
*/ */
Cluster.prototype.openCluster = function(node) { Cluster.prototype.openCluster = function(node) {
if (node.clusterSize > 15) {
this._addUniverse(node);
}
var isMovingBeforeClustering = this.moving; var isMovingBeforeClustering = this.moving;
this._expandClusterNode(node,false,true); this._expandClusterNode(node,false,true);
@ -10594,6 +10757,11 @@ Cluster.prototype.updateClusters = function(zoomDirection,recursive,force) {
var isMovingBeforeClustering = this.moving; var isMovingBeforeClustering = this.moving;
var amountOfNodes = this.nodeIndices.length; 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 // check if we zoom in or out
if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out if (this.previousScale > this.scale || zoomDirection == -1) { // zoom out
this._formClusters(force); this._formClusters(force);
@ -11505,8 +11673,8 @@ function Graph (container, data, options) {
enableClustering: true, enableClustering: true,
maxNumberOfNodes: 100, // for automatic (initial) clustering maxNumberOfNodes: 100, // for automatic (initial) clustering
snakeThreshold: 0.5, // maximum percentage of allowed snakes (long strings of connected nodes) 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) 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 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 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 // call the constructor of the cluster object
Cluster.call(this); Cluster.call(this);
// call the universe constructor
Universe.call(this);
var graph = 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.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.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 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 // apply options
this.setOptions(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 // zoom so all data will fit on the screen
this.zoomToFit(); 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 // this is called here because if clusterin is disabled, the start and stabilize are called in
// the setData function. // the setData function.
// find a stable position or start animating to a stable position
if (this.stabilize) { if (this.stabilize) {
this._doStabilize(); 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 * We add the functionality of the cluster object to the graph object
* @type {Cluster.prototype} * @type {Cluster.prototype}
@ -11632,11 +11806,11 @@ Graph.prototype = Object.create(Cluster.prototype);
Graph.prototype.clusterToFit = function(maxNumberOfNodes, reposition) { Graph.prototype.clusterToFit = function(maxNumberOfNodes, reposition) {
var numberOfNodes = this.nodeIndices.length; var numberOfNodes = this.nodeIndices.length;
var maxLevels = 10;
var maxLevels = 15;
var level = 0; var level = 0;
// we first cluster the hubs, then we pull in the outliers, repeat // 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) { if (level % 5 == 0) {
console.log("Aggregating Hubs @ level: ",level,". Threshold:", this.hubThreshold,"clusterSession",this.clusterSession); console.log("Aggregating Hubs @ level: ",level,". Threshold:", this.hubThreshold,"clusterSession",this.clusterSession);
this.forceAggregateHubs(); this.forceAggregateHubs();
@ -11649,7 +11823,7 @@ Graph.prototype.clusterToFit = function(maxNumberOfNodes, reposition) {
level += 1; 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) { if (level > 1 && reposition == true) {
this.repositionNodes(); this.repositionNodes();
} }
@ -11677,8 +11851,9 @@ Graph.prototype.zoomToFit = function() {
* @private * @private
*/ */
Graph.prototype._updateNodeIndexList = function() { 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) { for (var idx in this.nodes) {
if (this.nodes.hasOwnProperty(idx)) { if (this.nodes.hasOwnProperty(idx)) {
this.nodeIndices.push(idx); this.nodeIndices.push(idx);
@ -11724,6 +11899,8 @@ Graph.prototype.setData = function(data, disableStart) {
this._setEdges(data && data.edges); this._setEdges(data && data.edges);
} }
this._putDataInUniverse();
if (!disableStart) { if (!disableStart) {
// find a stable position or start animating to a stable position // find a stable position or start animating to a stable position
if (this.stabilize) { if (this.stabilize) {
@ -11868,14 +12045,15 @@ Graph.prototype._create = function () {
this.hammer.on('mousewheel',me._onMouseWheel.bind(me) ); this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) ); this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
/*
this.mouseTrap = mouseTrap; 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("-",this.increaseClusterLevel.bind(me));
this.mouseTrap.bind("s",this.singleStep.bind(me)); this.mouseTrap.bind("s",this.singleStep.bind(me));
this.mouseTrap.bind("h",this.updateClustersDefault.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)); this.mouseTrap.bind("f",this.toggleFreeze.bind(me));
*/
// add the frame to the container element // add the frame to the container element
this.containerElement.appendChild(this.frame); this.containerElement.appendChild(this.frame);
}; };
@ -12148,7 +12326,7 @@ Graph.prototype._zoom = function(scale, pointer) {
this.updateClustersDefault(); this.updateClustersDefault();
this._redraw(); this._redraw();
console.log("current zoomscale:",this.scale);
// console.log("current zoomscale:",this.scale);
return scale; return scale;
}; };
@ -12433,16 +12611,36 @@ Graph.prototype._selectNodes = function(selection, append) {
* @private * @private
*/ */
Graph.prototype._getNodesOverlappingWith = function (obj) { 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; return overlappingNodes;
}; };
@ -12936,8 +13134,41 @@ Graph.prototype._redraw = function() {
ctx.translate(this.translation.x, this.translation.y); ctx.translate(this.translation.x, this.translation.y);
ctx.scale(this.scale, this.scale); 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 // restore original scaling and translation
ctx.restore(); ctx.restore();
@ -13046,15 +13277,9 @@ Graph.prototype._drawNodes = function(ctx) {
var nodes = this.nodes; var nodes = this.nodes;
var selected = []; 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) { for (var id in nodes) {
if (nodes.hasOwnProperty(id)) { if (nodes.hasOwnProperty(id)) {
nodes[id].setScaleAndPos(this.scale,canvasTopLeft,canvasBottomRight);
nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
if (nodes[id].isSelected()) { if (nodes[id].isSelected()) {
selected.push(id); selected.push(id);
} }
@ -13121,7 +13346,7 @@ Graph.prototype._doStabilize = function() {
* Forces are caused by: edges, repulsing forces between nodes, gravity * Forces are caused by: edges, repulsing forces between nodes, gravity
* @private * @private
*/ */
Graph.prototype._calculateForces = function() {
Graph.prototype._calculateForces = function(nodes,edges) {
// stop calculation if there is only one node // stop calculation if there is only one node
if (this.nodeIndices.length == 1) { if (this.nodeIndices.length == 1) {
this.nodes[this.nodeIndices[0]]._setForce(0,0); this.nodes[this.nodeIndices[0]]._setForce(0,0);
@ -13132,6 +13357,13 @@ Graph.prototype._calculateForces = function() {
this._calculateForces(); this._calculateForces();
} }
else { 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 // create a local edge to the nodes and edges, that is faster
var dx, dy, angle, distance, fx, fy, var dx, dy, angle, distance, fx, fy,
repulsingForce, springForce, length, edgeLength, repulsingForce, springForce, length, edgeLength,
@ -13147,12 +13379,18 @@ Graph.prototype._calculateForces = function() {
var gravity = 0.08; var gravity = 0.08;
for (i = 0; i < this.nodeIndices.length; i++) { for (i = 0; i < this.nodeIndices.length; i++) {
node = nodes[this.nodeIndices[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._setForce(fx, fy);
node.updateDamping(this.nodeIndices.length); node.updateDamping(this.nodeIndices.length);
@ -13249,25 +13487,28 @@ Graph.prototype._calculateForces = function() {
if (edges.hasOwnProperty(edgeID)) { if (edges.hasOwnProperty(edgeID)) {
edge = edges[edgeID]; edge = edges[edgeID];
if (edge.connected) { 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 * Start animating nodes and edges
*/ */
Graph.prototype.start = function() { Graph.prototype.start = function() {
if (!this.freezeSimulation) { if (!this.freezeSimulation) {
if (this.moving) { if (this.moving) {
var vmin = this.constants.minVelocity;
/*
this._calculateForces(); this._calculateForces();
this._discreteStepNodes(); this._discreteStepNodes();
var vmin = this.constants.minVelocity;
this.moving = this._isMoving(vmin); 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) { if (this.moving) {
@ -13364,17 +13618,16 @@ Graph.prototype.start = function() {
if (!this.timer) { if (!this.timer) {
var graph = this; var graph = this;
this.timer = window.setTimeout(function () { this.timer = window.setTimeout(function () {
var start,end,time;
graph.timer = undefined; graph.timer = undefined;
graph.start(); graph.start();
graph.start(); graph.start();
graph._redraw(); graph._redraw();
// start = window.performance.now();
// var start = window.performance.now();
// graph._redraw(); // graph._redraw();
// end = window.performance.now();
// time = end - start;
// var end = window.performance.now();
// var time = end - start;
// console.log('Drawing time: ' + time); // console.log('Drawing time: ' + time);
}, this.refreshRate); }, this.refreshRate);
} }

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

@ -102,7 +102,7 @@
<form onsubmit="draw(); return false;"> <form onsubmit="draw(); return false;">
<label for="nodeCount">Number of nodes:</label> <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"> <input type="submit" value="Go">
</form> </form>
<br> <br>

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

@ -67,8 +67,8 @@ function Graph (container, data, options) {
enableClustering: true, enableClustering: true,
maxNumberOfNodes: 100, // for automatic (initial) clustering maxNumberOfNodes: 100, // for automatic (initial) clustering
snakeThreshold: 0.5, // maximum percentage of allowed snakes (long strings of connected nodes) 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) 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 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 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 // call the constructor of the cluster object
Cluster.call(this); Cluster.call(this);
// call the universe constructor
Universe.call(this);
var graph = 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.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.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 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 // apply options
this.setOptions(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 // zoom so all data will fit on the screen
this.zoomToFit(); 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 // this is called here because if clusterin is disabled, the start and stabilize are called in
// the setData function. // the setData function.
// find a stable position or start animating to a stable position
if (this.stabilize) { if (this.stabilize) {
this._doStabilize(); 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 * We add the functionality of the cluster object to the graph object
* @type {Cluster.prototype} * @type {Cluster.prototype}
@ -194,11 +200,11 @@ Graph.prototype = Object.create(Cluster.prototype);
Graph.prototype.clusterToFit = function(maxNumberOfNodes, reposition) { Graph.prototype.clusterToFit = function(maxNumberOfNodes, reposition) {
var numberOfNodes = this.nodeIndices.length; var numberOfNodes = this.nodeIndices.length;
var maxLevels = 10;
var maxLevels = 15;
var level = 0; var level = 0;
// we first cluster the hubs, then we pull in the outliers, repeat // 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) { if (level % 5 == 0) {
console.log("Aggregating Hubs @ level: ",level,". Threshold:", this.hubThreshold,"clusterSession",this.clusterSession); console.log("Aggregating Hubs @ level: ",level,". Threshold:", this.hubThreshold,"clusterSession",this.clusterSession);
this.forceAggregateHubs(); this.forceAggregateHubs();
@ -211,7 +217,7 @@ Graph.prototype.clusterToFit = function(maxNumberOfNodes, reposition) {
level += 1; 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) { if (level > 1 && reposition == true) {
this.repositionNodes(); this.repositionNodes();
} }
@ -239,8 +245,9 @@ Graph.prototype.zoomToFit = function() {
* @private * @private
*/ */
Graph.prototype._updateNodeIndexList = function() { 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) { for (var idx in this.nodes) {
if (this.nodes.hasOwnProperty(idx)) { if (this.nodes.hasOwnProperty(idx)) {
this.nodeIndices.push(idx); this.nodeIndices.push(idx);
@ -286,6 +293,8 @@ Graph.prototype.setData = function(data, disableStart) {
this._setEdges(data && data.edges); this._setEdges(data && data.edges);
} }
this._putDataInUniverse();
if (!disableStart) { if (!disableStart) {
// find a stable position or start animating to a stable position // find a stable position or start animating to a stable position
if (this.stabilize) { if (this.stabilize) {
@ -430,14 +439,15 @@ Graph.prototype._create = function () {
this.hammer.on('mousewheel',me._onMouseWheel.bind(me) ); this.hammer.on('mousewheel',me._onMouseWheel.bind(me) );
this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF this.hammer.on('DOMMouseScroll',me._onMouseWheel.bind(me) ); // for FF
this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) ); this.hammer.on('mousemove', me._onMouseMoveTitle.bind(me) );
/*
this.mouseTrap = mouseTrap; 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("-",this.increaseClusterLevel.bind(me));
this.mouseTrap.bind("s",this.singleStep.bind(me)); this.mouseTrap.bind("s",this.singleStep.bind(me));
this.mouseTrap.bind("h",this.updateClustersDefault.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)); this.mouseTrap.bind("f",this.toggleFreeze.bind(me));
*/
// add the frame to the container element // add the frame to the container element
this.containerElement.appendChild(this.frame); this.containerElement.appendChild(this.frame);
}; };
@ -710,7 +720,7 @@ Graph.prototype._zoom = function(scale, pointer) {
this.updateClustersDefault(); this.updateClustersDefault();
this._redraw(); this._redraw();
console.log("current zoomscale:",this.scale);
// console.log("current zoomscale:",this.scale);
return scale; return scale;
}; };
@ -995,16 +1005,36 @@ Graph.prototype._selectNodes = function(selection, append) {
* @private * @private
*/ */
Graph.prototype._getNodesOverlappingWith = function (obj) { 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; return overlappingNodes;
}; };
@ -1498,8 +1528,41 @@ Graph.prototype._redraw = function() {
ctx.translate(this.translation.x, this.translation.y); ctx.translate(this.translation.x, this.translation.y);
ctx.scale(this.scale, this.scale); 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 // restore original scaling and translation
ctx.restore(); ctx.restore();
@ -1608,15 +1671,9 @@ Graph.prototype._drawNodes = function(ctx) {
var nodes = this.nodes; var nodes = this.nodes;
var selected = []; 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) { for (var id in nodes) {
if (nodes.hasOwnProperty(id)) { if (nodes.hasOwnProperty(id)) {
nodes[id].setScaleAndPos(this.scale,canvasTopLeft,canvasBottomRight);
nodes[id].setScaleAndPos(this.scale,this.canvasTopLeft,this.canvasBottomRight);
if (nodes[id].isSelected()) { if (nodes[id].isSelected()) {
selected.push(id); selected.push(id);
} }
@ -1683,7 +1740,7 @@ Graph.prototype._doStabilize = function() {
* Forces are caused by: edges, repulsing forces between nodes, gravity * Forces are caused by: edges, repulsing forces between nodes, gravity
* @private * @private
*/ */
Graph.prototype._calculateForces = function() {
Graph.prototype._calculateForces = function(nodes,edges) {
// stop calculation if there is only one node // stop calculation if there is only one node
if (this.nodeIndices.length == 1) { if (this.nodeIndices.length == 1) {
this.nodes[this.nodeIndices[0]]._setForce(0,0); this.nodes[this.nodeIndices[0]]._setForce(0,0);
@ -1694,6 +1751,13 @@ Graph.prototype._calculateForces = function() {
this._calculateForces(); this._calculateForces();
} }
else { 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 // create a local edge to the nodes and edges, that is faster
var dx, dy, angle, distance, fx, fy, var dx, dy, angle, distance, fx, fy,
repulsingForce, springForce, length, edgeLength, repulsingForce, springForce, length, edgeLength,
@ -1709,12 +1773,18 @@ Graph.prototype._calculateForces = function() {
var gravity = 0.08; var gravity = 0.08;
for (i = 0; i < this.nodeIndices.length; i++) { for (i = 0; i < this.nodeIndices.length; i++) {
node = nodes[this.nodeIndices[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._setForce(fx, fy);
node.updateDamping(this.nodeIndices.length); node.updateDamping(this.nodeIndices.length);
@ -1811,25 +1881,28 @@ Graph.prototype._calculateForces = function() {
if (edges.hasOwnProperty(edgeID)) { if (edges.hasOwnProperty(edgeID)) {
edge = edges[edgeID]; edge = edges[edgeID];
if (edge.connected) { 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 * Start animating nodes and edges
*/ */
Graph.prototype.start = function() { Graph.prototype.start = function() {
if (!this.freezeSimulation) { if (!this.freezeSimulation) {
if (this.moving) { if (this.moving) {
var vmin = this.constants.minVelocity;
/*
this._calculateForces(); this._calculateForces();
this._discreteStepNodes(); this._discreteStepNodes();
var vmin = this.constants.minVelocity;
this.moving = this._isMoving(vmin); 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) { if (this.moving) {
@ -1926,17 +2012,16 @@ Graph.prototype.start = function() {
if (!this.timer) { if (!this.timer) {
var graph = this; var graph = this;
this.timer = window.setTimeout(function () { this.timer = window.setTimeout(function () {
var start,end,time;
graph.timer = undefined; graph.timer = undefined;
graph.start(); graph.start();
graph.start(); graph.start();
graph._redraw(); graph._redraw();
// start = window.performance.now();
// var start = window.performance.now();
// graph._redraw(); // graph._redraw();
// end = window.performance.now();
// time = end - start;
// var end = window.performance.now();
// var time = end - start;
// console.log('Drawing time: ' + time); // console.log('Drawing time: ' + time);
}, this.refreshRate); }, this.refreshRate);
} }

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

@ -54,6 +54,7 @@ function Node(properties, imagelist, grouplist, constants) {
this.grouplist = grouplist; this.grouplist = grouplist;
this.nodeProperties = properties;
this.setProperties(properties, constants); this.setProperties(properties, constants);
// creating the variables for clustering // 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() { Node.prototype.inArea = function() {
if (this.width !== undefined) { if (this.width !== undefined) {
return (this.x + this.width*0.8 >= this.canvasTopLeft.x && 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() { Node.prototype.inView = function() {
return (this.x >= this.canvasTopLeft.x && return (this.x >= this.canvasTopLeft.x &&
this.x < this.canvasBottomRight.x && this.x < this.canvasBottomRight.x &&

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

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