diff --git a/HISTORY.md b/HISTORY.md
index 8ee7ce20..e04e9ae8 100644
--- a/HISTORY.md
+++ b/HISTORY.md
@@ -22,8 +22,33 @@ http://visjs.org
- Fixed invalid css names for time axis grid, renamed hours class names from
`4-8h` to `h4-h8`.
+### Network
+
+- Rebuilt the cluster system
+
+
+
+## not yet released, version 3.10.1-SNAPSHOT
+
+### Network
+
+- (added gradient coloring for lines, but set for release in 4.0 due to required refactoring of options)
+- Fixed bug where a network that has frozen physics would resume redrawing after setData, setOptions etc.
+- (add docs) Added option to bypass default groups. If more groups are specified in the nodes than there are in the groups, loop over supplied groups instead of default.
+- (add docs) Added two new static smooth curves modes: curveCW and curve CCW.
+- Added request redraw for certain internal processes to reduce number of draw calls.
+- Added pull request for usage of Icons. Thanks @Dude9177!
+- Allow hierarchical view to be set in setOptions.
+
+### Graph2d
+
+
+
+### Timeline
+
+- Fixed not property initializing with a DataView for groups.
+
-## not yet released, version 3.9.2-SNAPSHOT
## 2015-02-11, version 3.10.0
### Network
diff --git a/docs/graph2d.html b/docs/graph2d.html
index bcc03402..a4355b1b 100644
--- a/docs/graph2d.html
+++ b/docs/graph2d.html
@@ -175,6 +175,12 @@ var items = [
no
The ID of the group this point belongs to.
+
+ label
+ object
+ no
+ A label object which will be displayed near to the item. A label object has one requirement - a content property. In addition you can set the xOffset, yOffset and className for further appearance customisations
+
Groups
diff --git a/docs/network.html b/docs/network.html
index d1c2e9af..dfbac7e0 100644
--- a/docs/network.html
+++ b/docs/network.html
@@ -1009,7 +1009,6 @@ mySize = minSize + diff * scale;
'white'
The color of the label stroke.
-
shape
string
@@ -1018,7 +1017,7 @@ mySize = minSize + diff * scale;
Choose from
ellipse
(default), circle
, box
,
database
, image
, circularImage
, label
, dot
,
- star
, triangle
, triangleDown
, and square
.
+ star
, triangle
, triangleDown
, square
and icon
.
In case of image
and circularImage
, a property with name image
must
@@ -1095,6 +1094,30 @@ mySize = minSize + diff * scale;
The maximum radius for a scaled node. Only applicable to shapes dot
,
star
, triangle
, triangleDown
, and square
. This only does something if you supply a value.
+
+ iconFontFace
+ String
+ undefined
+ Font face for icons, for example FontAwesome
or Ionicon
.You have to link to the css defining the font by yourself (see Examples)
+
+
+ icon
+ String
+ undefined
+ Unicode of the icon f.e. \uf0c0
(user-icon in FontAwesome)
+
+
+ iconSize
+ Number
+ 50
+ Size of the icon
+
+
+ color
+ String
+ black
+ Color of the icon
+
diff --git a/examples/graph2d/19_labels.html b/examples/graph2d/19_labels.html
new file mode 100644
index 00000000..7f140180
--- /dev/null
+++ b/examples/graph2d/19_labels.html
@@ -0,0 +1,65 @@
+
+
+
+
+
+
+ Graph2d | Basic Example
+
+
+
+
+
+
+
+Graph2d | Label Example
+
+ This example shows the how to add a label to each point in Graph2d. Each item can have a label object which contains the content and CSS class.In addition, xOffset and yOffset will adjust the location of the label relative to the point being labelled.
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/examples/network/06_groups.html b/examples/network/06_groups.html
index 19964045..1e2ecf3b 100644
--- a/examples/network/06_groups.html
+++ b/examples/network/06_groups.html
@@ -8,9 +8,10 @@
font: 10pt arial;
}
#mynetwork {
- width: 600px;
- height: 600px;
+ width: 1900px;
+ height: 900px;
border: 1px solid lightgray;
+ background-color:#222222;
}
@@ -139,11 +140,14 @@
edges: edges
};
var options = {
- stabilize: false,
+ stabilize: true,
nodes: {
- shape: 'dot'
+ shape: 'dot',
+ radius:30,
+ fontColor:'#ffffff',
+ borderWidth:2
},
- physics: {barnesHut:{springLength: 200}}
+ physics: {barnesHut:{springLength: 100}}
};
network = new vis.Network(container, data, options);
}
@@ -154,9 +158,9 @@
diff --git a/examples/network/25_physics_configuration.html b/examples/network/25_physics_configuration.html
index 1d42c625..104040be 100644
--- a/examples/network/25_physics_configuration.html
+++ b/examples/network/25_physics_configuration.html
@@ -78,7 +78,6 @@
};
var options = {
- edges:{opacity:0.2},
stabilize: false,
configurePhysics:true
};
diff --git a/examples/network/26_staticSmoothCurves.html b/examples/network/26_staticSmoothCurves.html
index 1c338269..d6b06dde 100644
--- a/examples/network/26_staticSmoothCurves.html
+++ b/examples/network/26_staticSmoothCurves.html
@@ -32,19 +32,26 @@
Smooth curve type:
- continuous
+ continuous
discrete
diagonalCross
straightCross
horizontal
vertical
-
+ curvedCW
+ curvedCCW
+
+Roundness (0..1): (0.5 is max roundness for continuous, 1.0 for the others)
diff --git a/examples/network/27_world_cup_network.html b/examples/network/27_world_cup_network.html
index 54e85cb5..7361849d 100644
--- a/examples/network/27_world_cup_network.html
+++ b/examples/network/27_world_cup_network.html
@@ -39,6 +39,8 @@ Smooth curve type:
straightCross
horizontal
vertical
+ curvedCW
+ curvedCCW
inheritColor option:
diff --git a/examples/network/38_node_as_icon.html b/examples/network/38_node_as_icon.html
new file mode 100644
index 00000000..e306cb81
--- /dev/null
+++ b/examples/network/38_node_as_icon.html
@@ -0,0 +1,166 @@
+
+
+
+
+
+ Network | node as icon
+
+
+
+
+
+
+
+
+
+
+
+
+ Use FontAwesome-icons for node
+
+
+ Use Ionicons-icons for node
+
+
+
+
+
diff --git a/examples/network/39_newClustering.html b/examples/network/39_newClustering.html
new file mode 100644
index 00000000..4de947aa
--- /dev/null
+++ b/examples/network/39_newClustering.html
@@ -0,0 +1,103 @@
+
+
+
+ Network | Basic usage
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/examples/network/index.html b/examples/network/index.html
index 13de6942..a44be6d0 100644
--- a/examples/network/index.html
+++ b/examples/network/index.html
@@ -49,6 +49,7 @@
35_label_stroke.html
36_HTML_in_Nodes.html
37_label_alignment.html
+ 38_node_as_icon.html
graphviz_gallery.html
diff --git a/lib/DOMutil.js b/lib/DOMutil.js
index 5cc01f6f..da476a07 100644
--- a/lib/DOMutil.js
+++ b/lib/DOMutil.js
@@ -130,9 +130,10 @@ exports.getDOMElement = function (elementType, JSONcontainer, DOMContainer, inse
* @param group
* @param JSONcontainer
* @param svgContainer
+ * @param labelObj
* @returns {*}
*/
-exports.drawPoint = function(x, y, group, JSONcontainer, svgContainer) {
+exports.drawPoint = function(x, y, group, JSONcontainer, svgContainer, labelObj) {
var point;
if (group.options.drawPoints.style == 'circle') {
point = exports.getSVGElement('circle',JSONcontainer,svgContainer);
@@ -152,6 +153,28 @@ exports.drawPoint = function(x, y, group, JSONcontainer, svgContainer) {
point.setAttributeNS(null, "style", group.group.options.drawPoints.styles);
}
point.setAttributeNS(null, "class", group.className + " point");
+ //handle label
+ var label = exports.getSVGElement('text',JSONcontainer,svgContainer);
+ if (labelObj){
+ if (labelObj.xOffset) {
+ x = x + labelObj.xOffset;
+ }
+
+ if (labelObj.yOffset) {
+ y = y + labelObj.yOffset;
+ }
+ if (labelObj.content) {
+ label.textContent = labelObj.content;
+ }
+
+ if (labelObj.className) {
+ label.setAttributeNS(null, "class", labelObj.className + " label");
+ }
+
+
+ }
+ label.setAttributeNS(null, "x", x);
+ label.setAttributeNS(null, "y", y);
return point;
};
diff --git a/lib/network/Edge.js b/lib/network/Edge.js
index 2a7358e4..adb87b18 100644
--- a/lib/network/Edge.js
+++ b/lib/network/Edge.js
@@ -23,6 +23,7 @@ function Edge (properties, network, networkConstants) {
var fields = ['edges','physics'];
var constants = util.selectiveBridgeObject(fields,networkConstants);
this.options = constants.edges;
+
this.physics = constants.physics;
this.options['smoothCurves'] = networkConstants['smoothCurves'];
@@ -46,13 +47,13 @@ function Edge (properties, network, networkConstants) {
this.to = null; // a node
this.via = null; // a temp node
- this.fromBackup = null; // used to clean up after reconnect
- this.toBackup = null;; // used to clean up after reconnect
+ this.fromBackup = null; // used to clean up after reconnect (used for manipulation)
+ this.toBackup = null; // used to clean up after reconnect (used for manipulation)
// we use this to be able to reconnect the edge to a cluster if its node is put into a cluster
// by storing the original information we can revert to the original connection when the cluser is opened.
- this.originalFromId = [];
- this.originalToId = [];
+ this.fromArray = [];
+ this.toArray = [];
this.connected = false;
@@ -76,10 +77,11 @@ Edge.prototype.setProperties = function(properties) {
if (!properties) {
return;
}
+ this.properties = properties;
var fields = ['style','fontSize','fontFace','fontColor','fontFill','fontStrokeWidth','fontStrokeColor','width',
'widthSelectionMultiplier','hoverWidth','arrowScaleFactor','dash','inheritColor','labelAlignment', 'opacity',
- 'customScalingFunction'
+ 'customScalingFunction','useGradients','value'
];
util.selectiveDeepExtend(fields, this.options, properties);
@@ -135,9 +137,9 @@ Edge.prototype.connect = function () {
this.from = this.network.nodes[this.fromId] || null;
this.to = this.network.nodes[this.toId] || null;
- this.connected = (this.from && this.to);
+ this.connected = (this.from !== null && this.to !== null);
- if (this.connected) {
+ if (this.connected === true) {
this.from.attachEdge(this);
this.to.attachEdge(this);
}
@@ -234,9 +236,32 @@ Edge.prototype.isOverlappingWith = function(obj) {
}
};
-Edge.prototype._getColor = function() {
+Edge.prototype._getColor = function(ctx) {
var colorObj = this.options.color;
+ if (this.options.useGradients == true) {
+ var grd = ctx.createLinearGradient(this.from.x, this.from.y, this.to.x, this.to.y);
+ var fromColor, toColor;
+ fromColor = this.from.options.color.highlight.border;
+ toColor = this.to.options.color.highlight.border;
+
+
+ if (this.from.selected == false && this.to.selected == false) {
+ fromColor = util.overrideOpacity(this.from.options.color.border, this.options.opacity);
+ toColor = util.overrideOpacity(this.to.options.color.border, this.options.opacity);
+ }
+ else if (this.from.selected == true && this.to.selected == false) {
+ toColor = this.to.options.color.border;
+ }
+ else if (this.from.selected == false && this.to.selected == true) {
+ fromColor = this.from.options.color.border;
+ }
+ grd.addColorStop(0, fromColor);
+ grd.addColorStop(1, toColor);
+ return grd;
+ }
+
if (this.colorDirty === true) {
+
if (this.options.inheritColor == "to") {
colorObj = {
highlight: this.to.options.color.highlight.border,
@@ -255,6 +280,8 @@ Edge.prototype._getColor = function() {
this.colorDirty = false;
}
+
+
if (this.selected == true) {return colorObj.highlight;}
else if (this.hover == true) {return colorObj.hover;}
else {return colorObj.color;}
@@ -270,7 +297,7 @@ Edge.prototype._getColor = function() {
*/
Edge.prototype._drawLine = function(ctx) {
// set style
- ctx.strokeStyle = this._getColor();
+ ctx.strokeStyle = this._getColor(ctx);
ctx.lineWidth = this._getLineWidth();
if (this.from != this.to) {
@@ -344,7 +371,6 @@ Edge.prototype._getViaCoordinates = function () {
var yVia = null;
var factor = this.options.smoothCurves.roundness;
var type = this.options.smoothCurves.type;
-
var dx = Math.abs(this.from.x - this.to.x);
var dy = Math.abs(this.from.y - this.to.y);
if (type == 'discrete' || type == 'diagonalCross') {
@@ -437,17 +463,39 @@ Edge.prototype._getViaCoordinates = function () {
yVia = this.to.y + (1 - factor) * dy;
}
}
+ else if (type == 'curvedCW') {
+ var dx = this.to.x - this.from.x;
+ var dy = this.from.y - this.to.y;
+ var radius = Math.sqrt(dx*dx + dy*dy);
+ var pi = Math.PI;
+
+ var originalAngle = Math.atan2(dy,dx);
+ var myAngle = (originalAngle + ((factor * 0.5) + 0.5) * pi) % (2 * pi);
+
+ xVia = this.from.x + (factor*0.5 + 0.5)*radius*Math.sin(myAngle);
+ yVia = this.from.y + (factor*0.5 + 0.5)*radius*Math.cos(myAngle);
+ }
+ else if (type == 'curvedCCW') {
+ var dx = this.to.x - this.from.x;
+ var dy = this.from.y - this.to.y;
+ var radius = Math.sqrt(dx*dx + dy*dy);
+ var pi = Math.PI;
+
+ var originalAngle = Math.atan2(dy,dx);
+ var myAngle = (originalAngle + ((-factor * 0.5) + 0.5) * pi) % (2 * pi);
+
+ xVia = this.from.x + (factor*0.5 + 0.5)*radius*Math.sin(myAngle);
+ yVia = this.from.y + (factor*0.5 + 0.5)*radius*Math.cos(myAngle);
+ }
else { // continuous
if (Math.abs(this.from.x - this.to.x) < Math.abs(this.from.y - this.to.y)) {
if (this.from.y > this.to.y) {
if (this.from.x < this.to.x) {
-// console.log(1)
xVia = this.from.x + factor * dy;
yVia = this.from.y - factor * dy;
xVia = this.to.x < xVia ? this.to.x : xVia;
}
else if (this.from.x > this.to.x) {
-// console.log(2)
xVia = this.from.x - factor * dy;
yVia = this.from.y - factor * dy;
xVia = this.to.x > xVia ? this.to.x : xVia;
@@ -455,13 +503,11 @@ Edge.prototype._getViaCoordinates = function () {
}
else if (this.from.y < this.to.y) {
if (this.from.x < this.to.x) {
-// console.log(3)
xVia = this.from.x + factor * dy;
yVia = this.from.y + factor * dy;
xVia = this.to.x < xVia ? this.to.x : xVia;
}
else if (this.from.x > this.to.x) {
-// console.log(4, this.from.x, this.to.x)
xVia = this.from.x - factor * dy;
yVia = this.from.y + factor * dy;
xVia = this.to.x > xVia ? this.to.x : xVia;
@@ -471,13 +517,11 @@ Edge.prototype._getViaCoordinates = function () {
else if (Math.abs(this.from.x - this.to.x) > Math.abs(this.from.y - this.to.y)) {
if (this.from.y > this.to.y) {
if (this.from.x < this.to.x) {
-// console.log(5)
xVia = this.from.x + factor * dx;
yVia = this.from.y - factor * dx;
yVia = this.to.y > yVia ? this.to.y : yVia;
}
else if (this.from.x > this.to.x) {
-// console.log(6)
xVia = this.from.x - factor * dx;
yVia = this.from.y - factor * dx;
yVia = this.to.y > yVia ? this.to.y : yVia;
@@ -485,13 +529,11 @@ Edge.prototype._getViaCoordinates = function () {
}
else if (this.from.y < this.to.y) {
if (this.from.x < this.to.x) {
-// console.log(7)
xVia = this.from.x + factor * dx;
yVia = this.from.y + factor * dx;
yVia = this.to.y < yVia ? this.to.y : yVia;
}
else if (this.from.x > this.to.x) {
-// console.log(8)
xVia = this.from.x - factor * dx;
yVia = this.from.y + factor * dx;
yVia = this.to.y < yVia ? this.to.y : yVia;
@@ -527,6 +569,8 @@ Edge.prototype._line = function (ctx) {
// this.via.y = via.y;
ctx.quadraticCurveTo(via.x,via.y,this.to.x, this.to.y);
ctx.stroke();
+ //ctx.circle(via.x,via.y,2)
+ //ctx.stroke();
return via;
}
}
@@ -715,7 +759,7 @@ Edge.prototype._drawLabelText = function(ctx, x, yLine, lines, lineCount, fontSi
*/
Edge.prototype._drawDashLine = function(ctx) {
// set style
- ctx.strokeStyle = this._getColor();
+ ctx.strokeStyle = this._getColor(ctx);
ctx.lineWidth = this._getLineWidth();
var via = null;
@@ -820,7 +864,7 @@ Edge.prototype._pointOnCircle = function (x, y, radius, percentage) {
Edge.prototype._drawArrowCenter = function(ctx) {
var point;
// set style
- ctx.strokeStyle = this._getColor();
+ ctx.strokeStyle = this._getColor(ctx);
ctx.fillStyle = ctx.strokeStyle;
ctx.lineWidth = this._getLineWidth();
@@ -956,7 +1000,7 @@ Edge.prototype._findBorderPosition = function(from,ctx) {
*/
Edge.prototype._drawArrow = function(ctx) {
// set style
- ctx.strokeStyle = this._getColor();
+ ctx.strokeStyle = this._getColor(ctx);
ctx.fillStyle = ctx.strokeStyle;
ctx.lineWidth = this._getLineWidth();
diff --git a/lib/network/Groups.js b/lib/network/Groups.js
index dc45ae34..6756735f 100644
--- a/lib/network/Groups.js
+++ b/lib/network/Groups.js
@@ -7,6 +7,9 @@ var util = require('../util');
function Groups() {
this.clear();
this.defaultIndex = 0;
+ this.groupsArray = [];
+ this.groupIndex = 0;
+ this.useDefaultGroups = true;
}
@@ -14,16 +17,29 @@ function Groups() {
* default constants for group colors
*/
Groups.DEFAULT = [
- {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}, hover: {border: "#2B7CE9", background: "#D2E5FF"}}, // blue
- {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}, hover: {border: "#FFA500", background: "#FFFFA3"}}, // yellow
- {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}, hover: {border: "#FA0A10", background: "#FFAFB1"}}, // red
- {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}, hover: {border: "#41A906", background: "#A1EC76"}}, // green
- {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}, hover: {border: "#E129F0", background: "#F0B3F5"}}, // magenta
- {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}, hover: {border: "#7C29F0", background: "#D3BDF0"}}, // purple
- {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}, hover: {border: "#C37F00", background: "#FFCA66"}}, // orange
- {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}, hover: {border: "#4220FB", background: "#9B9BFD"}}, // darkblue
- {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}, hover: {border: "#FD5A77", background: "#FFD1D9"}}, // pink
- {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}, hover: {border: "#4AD63A", background: "#E6FFE3"}} // mint
+ {border: "#2B7CE9", background: "#97C2FC", highlight: {border: "#2B7CE9", background: "#D2E5FF"}, hover: {border: "#2B7CE9", background: "#D2E5FF"}}, // 0: blue
+ {border: "#FFA500", background: "#FFFF00", highlight: {border: "#FFA500", background: "#FFFFA3"}, hover: {border: "#FFA500", background: "#FFFFA3"}}, // 1: yellow
+ {border: "#FA0A10", background: "#FB7E81", highlight: {border: "#FA0A10", background: "#FFAFB1"}, hover: {border: "#FA0A10", background: "#FFAFB1"}}, // 2: red
+ {border: "#41A906", background: "#7BE141", highlight: {border: "#41A906", background: "#A1EC76"}, hover: {border: "#41A906", background: "#A1EC76"}}, // 3: green
+ {border: "#E129F0", background: "#EB7DF4", highlight: {border: "#E129F0", background: "#F0B3F5"}, hover: {border: "#E129F0", background: "#F0B3F5"}}, // 4: magenta
+ {border: "#7C29F0", background: "#AD85E4", highlight: {border: "#7C29F0", background: "#D3BDF0"}, hover: {border: "#7C29F0", background: "#D3BDF0"}}, // 5: purple
+ {border: "#C37F00", background: "#FFA807", highlight: {border: "#C37F00", background: "#FFCA66"}, hover: {border: "#C37F00", background: "#FFCA66"}}, // 6: orange
+ {border: "#4220FB", background: "#6E6EFD", highlight: {border: "#4220FB", background: "#9B9BFD"}, hover: {border: "#4220FB", background: "#9B9BFD"}}, // 7: darkblue
+ {border: "#FD5A77", background: "#FFC0CB", highlight: {border: "#FD5A77", background: "#FFD1D9"}, hover: {border: "#FD5A77", background: "#FFD1D9"}}, // 8: pink
+ {border: "#4AD63A", background: "#C2FABC", highlight: {border: "#4AD63A", background: "#E6FFE3"}, hover: {border: "#4AD63A", background: "#E6FFE3"}}, // 9: mint
+
+ {border: "#990000", background: "#EE0000", highlight: {border: "#BB0000", background: "#FF3333"}, hover: {border: "#BB0000", background: "#FF3333"}}, // 10:bright red
+
+ {border: "#FF6000", background: "#FF6000", highlight: {border: "#FF6000", background: "#FF6000"}, hover: {border: "#FF6000", background: "#FF6000"}}, // 12: real orange
+ {border: "#97C2FC", background: "#2B7CE9", highlight: {border: "#D2E5FF", background: "#2B7CE9"}, hover: {border: "#D2E5FF", background: "#2B7CE9"}}, // 13: blue
+ {border: "#399605", background: "#255C03", highlight: {border: "#399605", background: "#255C03"}, hover: {border: "#399605", background: "#255C03"}}, // 14: green
+ {border: "#B70054", background: "#FF007E", highlight: {border: "#B70054", background: "#FF007E"}, hover: {border: "#B70054", background: "#FF007E"}}, // 15: magenta
+ {border: "#AD85E4", background: "#7C29F0", highlight: {border: "#D3BDF0", background: "#7C29F0"}, hover: {border: "#D3BDF0", background: "#7C29F0"}}, // 16: purple
+ {border: "#4557FA", background: "#000EA1", highlight: {border: "#6E6EFD", background: "#000EA1"}, hover: {border: "#6E6EFD", background: "#000EA1"}}, // 17: darkblue
+ {border: "#FFC0CB", background: "#FD5A77", highlight: {border: "#FFD1D9", background: "#FD5A77"}, hover: {border: "#FFD1D9", background: "#FD5A77"}}, // 18: pink
+ {border: "#C2FABC", background: "#74D66A", highlight: {border: "#E6FFE3", background: "#74D66A"}, hover: {border: "#E6FFE3", background: "#74D66A"}}, // 19: mint
+
+ {border: "#EE0000", background: "#990000", highlight: {border: "#FF3333", background: "#BB0000"}, hover: {border: "#FF3333", background: "#BB0000"}}, // 20:bright red
];
@@ -54,12 +70,22 @@ Groups.prototype.clear = function () {
Groups.prototype.get = function (groupname) {
var group = this.groups[groupname];
if (group == undefined) {
- // create new group
- var index = this.defaultIndex % Groups.DEFAULT.length;
- this.defaultIndex++;
- group = {};
- group.color = Groups.DEFAULT[index];
- this.groups[groupname] = group;
+ if (this.useDefaultGroups === false && this.groupsArray.length > 0) {
+ // create new group
+ var index = this.groupIndex % this.groupsArray.length;
+ this.groupIndex++;
+ group = {};
+ group.color = this.groups[this.groupsArray[index]];
+ this.groups[groupname] = group;
+ }
+ else {
+ // create new group
+ var index = this.defaultIndex % Groups.DEFAULT.length;
+ this.defaultIndex++;
+ group = {};
+ group.color = Groups.DEFAULT[index];
+ this.groups[groupname] = group;
+ }
}
return group;
@@ -67,13 +93,14 @@ Groups.prototype.get = function (groupname) {
/**
* Add a custom group style
- * @param {String} groupname
+ * @param {String} groupName
* @param {Object} style An object containing borderColor,
* backgroundColor, etc.
* @return {Object} group The created group object
*/
-Groups.prototype.add = function (groupname, style) {
- this.groups[groupname] = style;
+Groups.prototype.add = function (groupName, style) {
+ this.groups[groupName] = style;
+ this.groupsArray.push(groupName);
return style;
};
diff --git a/lib/network/Network.js b/lib/network/Network.js
index 397fffc5..d4f890a2 100644
--- a/lib/network/Network.js
+++ b/lib/network/Network.js
@@ -45,7 +45,6 @@ function Network (container, data, options) {
this.renderRefreshRate = 60; // hz (fps)
this.renderTimestep = 1000 / this.renderRefreshRate; // ms -- saves calculation later on
this.renderTime = 0; // measured time it takes to render a frame
- this.physicsTime = 0; // measured time it takes to render a frame
this.runDoubleSpeed = false;
this.physicsDiscreteStepsize = 0.50; // discrete stepsize of the simulation
@@ -85,6 +84,7 @@ function Network (container, data, options) {
fontSizeMin: 14,
fontSizeMax: 30,
fontSizeMaxVisible: 30,
+ value: 1,
level: -1,
color: {
border: '#2B7CE9',
@@ -109,6 +109,7 @@ function Network (container, data, options) {
width: 1,
widthSelectionMultiplier: 2,
hoverWidth: 1.5,
+ value:1,
style: 'line',
color: {
color:'#848484',
@@ -129,7 +130,8 @@ function Network (container, data, options) {
gap: 5,
altLength: undefined
},
- inheritColor: "from" // to, from, false, true (== from)
+ inheritColor: "from", // to, from, false, true (== from)
+ useGradients: false // release in 4.0
},
configurePhysics:false,
physics: {
@@ -163,26 +165,7 @@ function Network (container, data, options) {
springConstant: null
},
clustering: { // Per Node in Cluster = PNiC
- enabled: false, // (Boolean) | global on/off switch for clustering.
- initialMaxNodes: 100, // (# nodes) | if the initial amount of nodes is larger than this, we cluster until the total number is less than this threshold.
- clusterThreshold:500, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than this. If it is, cluster until reduced to reduceToNodes
- reduceToNodes:300, // (# nodes) | during calculate forces, we check if the total number of nodes is larger than clusterThreshold. If it is, cluster until reduced to this
- chainThreshold: 0.4, // (% of all drawn nodes)| maximum percentage of allowed chainnodes (long strings of connected nodes) within all nodes. (lower means less chains).
- clusterEdgeThreshold: 20, // (px) | edge length threshold. if smaller, this node is clustered.
- sectorThreshold: 100, // (# nodes in cluster) | cluster size threshold. If larger, expanding in own sector.
- screenSizeThreshold: 0.2, // (% of canvas) | relative size threshold. If the width or height of a clusternode takes up this much of the screen, decluster node.
- fontSizeMultiplier: 4.0, // (px PNiC) | how much the cluster font size grows per node in cluster (in px).
- maxFontSize: 1000,
- forceAmplification: 0.1, // (multiplier PNiC) | factor of increase fo the repulsion force of a cluster (per node in cluster).
- distanceAmplification: 0.1, // (multiplier PNiC) | factor how much the repulsion distance of a cluster increases (per node in cluster).
- edgeGrowth: 20, // (px PNiC) | amount of clusterSize connected to the edge is multiplied with this and added to edgeLength.
- nodeScaling: {width: 1, // (px PNiC) | growth of the width per node in cluster.
- height: 1, // (px PNiC) | growth of the height per node in cluster.
- radius: 1}, // (px PNiC) | growth of the radius per node in cluster.
- maxNodeSizeIncrements: 600, // (# increments) | max growth of the width per node in cluster.
- activeAreaBoxSize: 80, // (px) | box area around the curser where clusters are popped open.
- clusterLevelDifference: 2, // used for normalization of the cluster levels
- clusterByZoom: true // enable clustering through zooming in and out
+ enabled: false // (Boolean) | global on/off switch for clustering.
},
navigation: {
enabled: false
@@ -214,6 +197,7 @@ function Network (container, data, options) {
minVelocity: 0.1, // px/s
stabilize: true, // stabilize before displaying the network
stabilizationIterations: 1000, // maximum number of iteration to stabilize
+ stabilizationStepsize: 100,
zoomExtentOnStabilize: true,
locale: 'en',
locales: locales,
@@ -235,7 +219,8 @@ function Network (container, data, options) {
hideNodesOnDrag: false,
width : '100%',
height : '100%',
- selectable: true
+ selectable: true,
+ useDefaultGroups: true
};
this.constants = util.extend({}, this.defaultOptions);
this.pixelRatio = 1;
@@ -257,13 +242,14 @@ function Network (container, data, options) {
this.lockedOnNodeId = null;
this.lockedOnNodeOffset = null;
this.touchTime = 0;
+ this.redrawRequested = false;
// Node variables
var network = this;
this.groups = new Groups(); // object with groups
this.images = new Images(); // object with images
this.images.setOnloadCallback(function (status) {
- network._redraw();
+ network._requestRedraw();
});
// keyboard navigation variables
@@ -300,6 +286,14 @@ function Network (container, data, options) {
this.draggingNodes = false;
// containers for nodes and edges
+ this.body = {
+ calculationNodes: {},
+ calculationNodeIndices: {},
+ nodeIndices: {},
+ nodes: {},
+ edges: {}
+ }
+
this.calculationNodes = {};
this.calculationNodeIndices = [];
this.nodeIndices = []; // array with all the indices of the nodes. Used to speed up forces calculation
@@ -310,9 +304,7 @@ function Network (container, data, options) {
this.canvasTopLeft = {"x": 0,"y": 0}; // coordinates of the top left of the canvas. they will be set during _redraw.
this.canvasBottomRight = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
this.pointerPosition = {"x": 0,"y": 0}; // coordinates of the bottom right of the canvas. they will be set during _redraw
- this.areaCenter = {}; // 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
// datasets or dataviews
this.nodesData = null; // A DataSet or DataView
@@ -353,10 +345,9 @@ function Network (container, data, options) {
this.timer = undefined; // Scheduling function. Is definded in this.start();
// load data (the disable start variable will be the same as the enabled clustering)
- this.setData(data,this.constants.clustering.enabled || this.constants.hierarchicalLayout.enabled);
+ this.setData(data, this.constants.hierarchicalLayout.enabled);
// hierarchical layout
- this.initializing = false;
if (this.constants.hierarchicalLayout.enabled == true) {
this._setupHierarchicalLayout();
}
@@ -367,10 +358,11 @@ function Network (container, data, options) {
}
}
- // if clustering is disabled, the simulation will have started in the setData function
- if (this.constants.clustering.enabled) {
- this.startWithClustering();
+ if (this.constants.stabilize == false) {
+ this.initializing = false;
}
+
+ this.on("stabilizationIterationsDone", function () {this.initializing = false; this.start();}.bind(this));
}
// Extend Network with an Emitter mixin
@@ -580,11 +572,7 @@ Network.prototype.zoomExtent = function(options, initialZoom, disableStart) {
*/
Network.prototype._updateNodeIndexList = function() {
this._clearNodeIndexList();
- for (var idx in this.nodes) {
- if (this.nodes.hasOwnProperty(idx)) {
- this.nodeIndices.push(idx);
- }
- }
+ this.nodeIndices = Object.keys(this.nodes);
};
@@ -644,6 +632,7 @@ Network.prototype.setData = function(data, disableStart) {
this._setEdges(data && data.edges);
}
this._putDataInSector();
+
if (disableStart == false) {
if (this.constants.hierarchicalLayout.enabled == true) {
this._resetLevels();
@@ -654,10 +643,15 @@ Network.prototype.setData = function(data, disableStart) {
if (this.constants.stabilize == true) {
this._stabilize();
}
+ else {
+ this.moving = true;
+ this.start();
+ }
}
- this.start();
}
- this.initializing = false;
+ else {
+ this.initializing = false;
+ }
};
/**
@@ -675,6 +669,7 @@ Network.prototype.setOptions = function (options) {
util.selectiveNotDeepExtend(['color'],this.constants.nodes, options.nodes);
util.selectiveNotDeepExtend(['color','length'],this.constants.edges, options.edges);
+ this.groups.useDefaultGroups = this.constants.useDefaultGroups;
if (options.physics) {
util.mergeOptions(this.constants.physics, options.physics,'barnesHut');
util.mergeOptions(this.constants.physics, options.physics,'repulsion');
@@ -785,7 +780,6 @@ Network.prototype.setOptions = function (options) {
throw new Error('Option "labels" is deprecated. Use options "locale" and "locales" instead.');
}
-
// (Re)loading the mixins that can be enabled or disabled in the options.
// load the force calculation functions, grouped under the physics system.
this._loadPhysicsSystem();
@@ -804,8 +798,15 @@ Network.prototype.setOptions = function (options) {
this._markAllEdgesAsDirty();
this.setSize(this.constants.width, this.constants.height);
- this.moving = true;
- this.start();
+ if (this.constants.hierarchicalLayout.enabled == true && this.initializing == false) {
+ this._resetLevels();
+ this._setupHierarchicalLayout();
+ }
+
+ if (this.initializing !== true) {
+ this.moving = true;
+ this.start();
+ }
}
};
@@ -830,7 +831,6 @@ Network.prototype._create = function () {
this.frame.style.overflow = 'hidden';
this.frame.tabIndex = 900;
-
//////////////////////////////////////////////////////////////////
this.frame.canvas = document.createElement("canvas");
@@ -945,10 +945,6 @@ Network.prototype._createKeyBinds = function() {
this.keycharm.bind("pagedown",this._zoomOut.bind(me),"keydown");
this.keycharm.bind("pagedown",this._stopZoom.bind(me), "keyup");
}
- //this.keycharm.bind("1",this.increaseClusterLevel.bind(me), "keydown");
- //this.keycharm.bind("2",this.decreaseClusterLevel.bind(me), "keydown");
- //this.keycharm.bind("3",this.forceAggregateHubs.bind(me,true),"keydown");
- //this.keycharm.bind("4",this.normalizeClusterLevels.bind(me), "keydown");
if (this.constants.dataManipulation.enabled == true) {
this.keycharm.bind("esc",this._createManipulatorBar.bind(me));
@@ -1284,7 +1280,6 @@ Network.prototype._zoom = function(scale, pointer) {
this._setScale(scale);
this._setTranslation(tx, ty);
- this.updateClustersDefault();
if (preScaleDragPointer != null) {
var postScaleDragPointer = this.canvasToDOM(preScaleDragPointer);
@@ -1358,10 +1353,20 @@ Network.prototype._onMouseWheel = function(event) {
Network.prototype._onMouseMoveTitle = function (event) {
var gesture = hammerUtil.fakeGesture(this, event);
var pointer = this._getPointer(gesture.center);
+ var popupVisible = false;
// check if the previously selected node is still selected
- if (this.popupObj) {
- this._checkHidePopup(pointer);
+ if (this.popup !== undefined) {
+ if (this.popup.hidden === false) {
+ this._checkHidePopup(pointer);
+ }
+
+ // if the popup was not hidden above
+ if (this.popup.hidden === false) {
+ popupVisible = true;
+ this.popup.setPosition(pointer.x + 3,pointer.y - 5)
+ this.popup.show();
+ }
}
// if we bind the keyboard to the div, we have to highlight it to use it. This highlights it on mouse over
@@ -1369,20 +1374,20 @@ Network.prototype._onMouseMoveTitle = function (event) {
this.frame.focus();
}
- // start a timeout that will check if the mouse is positioned above
- // an element
- var me = this;
- var checkShow = function() {
- me._checkShowPopup(pointer);
- };
- if (this.popupTimer) {
- clearInterval(this.popupTimer); // stop any running calculationTimer
- }
- if (!this.drag.dragging) {
- this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay);
+ // start a timeout that will check if the mouse is positioned above an element
+ if (popupVisible === false) {
+ var me = this;
+ var checkShow = function () {
+ me._checkShowPopup(pointer);
+ };
+ if (this.popupTimer) {
+ clearInterval(this.popupTimer); // stop any running calculationTimer
+ }
+ if (!this.drag.dragging) {
+ this.popupTimer = setTimeout(checkShow, this.constants.tooltip.delay);
+ }
}
-
/**
* Adding hover highlights
*/
@@ -1434,8 +1439,9 @@ Network.prototype._checkShowPopup = function (pointer) {
};
var id;
- var lastPopupNode = this.popupObj;
+ var previousPopupObjId = this.popupObj === undefined ? "" : this.popupObj.id;
var nodeUnderCursor = false;
+ var popupType = "node";
if (this.popupObj == undefined) {
// search the nodes for overlap, select the top one in case of multiple nodes
@@ -1468,7 +1474,7 @@ Network.prototype._checkShowPopup = function (pointer) {
for (id in edges) {
if (edges.hasOwnProperty(id)) {
var edge = edges[id];
- if (edge.connected && (edge.getTitle() !== undefined) &&
+ if (edge.connected === true && (edge.getTitle() !== undefined) &&
edge.isOverlappingWith(obj)) {
overlappingEdges.push(id);
}
@@ -1477,23 +1483,26 @@ Network.prototype._checkShowPopup = function (pointer) {
if (overlappingEdges.length > 0) {
this.popupObj = this.edges[overlappingEdges[overlappingEdges.length - 1]];
+ popupType = "edge";
}
}
if (this.popupObj) {
// show popup message window
- if (this.popupObj != lastPopupNode) {
- var me = this;
- if (!me.popup) {
- me.popup = new Popup(me.frame, me.constants.tooltip);
+ if (this.popupObj.id != previousPopupObjId) {
+ if (this.popup === undefined) {
+ this.popup = new Popup(this.frame, this.constants.tooltip);
}
+ this.popup.popupTargetType = popupType;
+ this.popup.popupTargetId = this.popupObj.id;
+
// adjust a small offset such that the mouse cursor is located in the
// bottom left location of the popup, and you can easily move over the
// popup area
- me.popup.setPosition(pointer.x - 3, pointer.y - 3);
- me.popup.setText(me.popupObj.getTitle());
- me.popup.show();
+ this.popup.setPosition(pointer.x + 3, pointer.y - 5);
+ this.popup.setText(this.popupObj.getTitle());
+ this.popup.show();
}
}
else {
@@ -1505,18 +1514,38 @@ Network.prototype._checkShowPopup = function (pointer) {
/**
- * Check if the popup must be hided, which is the case when the mouse is no
+ * Check if the popup must be hidden, which is the case when the mouse is no
* longer hovering on the object
* @param {{x:Number, y:Number}} pointer
* @private
*/
Network.prototype._checkHidePopup = function (pointer) {
- if (!this.popupObj || !this._getNodeAt(pointer) ) {
- this.popupObj = undefined;
- if (this.popup) {
- this.popup.hide();
+ var pointerObj = {
+ left: this._XconvertDOMtoCanvas(pointer.x),
+ top: this._YconvertDOMtoCanvas(pointer.y),
+ right: this._XconvertDOMtoCanvas(pointer.x),
+ bottom: this._YconvertDOMtoCanvas(pointer.y)
+ };
+
+ var stillOnObj = false;
+ if (this.popup.popupTargetType == 'node') {
+ stillOnObj = this.nodes[this.popup.popupTargetId].isOverlappingWith(pointerObj);
+ if (stillOnObj === true) {
+ var overNode = this._getNodeAt(pointer);
+ stillOnObj = overNode.id == this.popup.popupTargetId;
+ }
+ }
+ else {
+ if (this._getNodeAt(pointer) === null) {
+ stillOnObj = this.edges[this.popup.popupTargetId].isOverlappingWith(pointerObj);
}
}
+
+
+ if (stillOnObj === false) {
+ this.popupObj = undefined;
+ this.popup.hide();
+ }
};
@@ -1640,7 +1669,6 @@ Network.prototype._addNodes = function(ids) {
this._updateCalculationNodes();
this._reconnectEdges();
this._updateValueRange(this.nodes);
- this.updateLabels();
};
/**
@@ -1876,7 +1904,6 @@ Network.prototype._reconnectEdges = function() {
for (id in nodes) {
if (nodes.hasOwnProperty(id)) {
nodes[id].edges = [];
- nodes[id].dynamicEdges = [];
}
}
@@ -1940,7 +1967,23 @@ Network.prototype.redraw = function() {
* @param hidden | used to get the first estimate of the node sizes. only the nodes are drawn after which they are quickly drawn over.
* @private
*/
-Network.prototype._redraw = function(hidden) {
+Network.prototype._requestRedraw = function(hidden) {
+ if (this.redrawRequested !== true) {
+ this.redrawRequested = true;
+ if (this.requiresTimeout === true) {
+ window.setTimeout(this._redraw.bind(this, hidden),0);
+ }
+ else {
+ window.requestAnimationFrame(this._redraw.bind(this, hidden, true));
+ }
+ }
+};
+
+Network.prototype._redraw = function(hidden, requested) {
+ if (hidden === undefined) {
+ hidden = false;
+ }
+ this.redrawRequested = false;
var ctx = this.frame.canvas.getContext('2d');
ctx.setTransform(this.pixelRatio, 0, 0, this.pixelRatio, 0, 0);
@@ -1964,7 +2007,7 @@ Network.prototype._redraw = function(hidden) {
"y": this._YconvertDOMtoCanvas(this.frame.canvas.clientHeight)
};
- if (!(hidden == true)) {
+ if (hidden === false) {
this._doInAllSectors("_drawAllSectorNodes", ctx);
if (this.drag.dragging == false || this.drag.dragging === undefined || this.constants.hideEdgesOnDrag == false) {
this._doInAllSectors("_drawEdges", ctx);
@@ -1975,22 +2018,22 @@ Network.prototype._redraw = function(hidden) {
this._doInAllSectors("_drawNodes",ctx,false);
}
- if (!(hidden == true)) {
+ if (hidden === false) {
if (this.controlNodesActive == true) {
this._doInAllSectors("_drawControlNodes", ctx);
}
}
-// this._doInSupportSector("_drawNodes",ctx,true);
+ //this._doInSupportSector("_drawNodes",ctx,true);
// this._drawTree(ctx,"#F00F0F");
// restore original scaling and translation
ctx.restore();
- if (hidden == true) {
+ if (hidden === true) {
ctx.clearRect(0, 0, w, h);
}
-};
+}
/**
* Set the translation of the network
@@ -2161,7 +2204,7 @@ Network.prototype._drawEdges = function(ctx) {
if (edges.hasOwnProperty(id)) {
var edge = edges[id];
edge.setScale(this.scale);
- if (edge.connected) {
+ if (edge.connected === true) {
edges[id].draw(ctx);
}
}
@@ -2191,19 +2234,29 @@ Network.prototype._stabilize = function() {
if (this.constants.freezeForStabilization == true) {
this._freezeDefinedNodes();
}
+ this.stabilizationSteps = 0;
- // find stable position
+ setTimeout(this._stabilizationBatch.bind(this),0);
+};
+
+Network.prototype._stabilizationBatch = function() {
var count = 0;
- while (this.moving && count < this.constants.stabilizationIterations) {
+ while (this.moving && count < this.constants.stabilizationStepsize && this.stabilizationSteps < this.constants.stabilizationIterations) {
this._physicsTick();
- // TODO: cleanup
- //if (count % 100 == 0) {
- // console.log("stabilizationIterations",count);
- //}
+ this.stabilizationSteps++;
count++;
}
+ if (this.moving && this.stabilizationSteps < this.constants.stabilizationIterations) {
+ this.emit("stabilizationProgress", {steps: this.stabilizationSteps, total: this.constants.stabilizationIterations});
+ setTimeout(this._stabilizationBatch.bind(this),0);
+ }
+ else {
+ this._finalizeStabilization();
+ }
+}
+Network.prototype._finalizeStabilization = function() {
if (this.constants.zoomExtentOnStabilize == true) {
this.zoomExtent({duration:0}, false, true);
}
@@ -2213,7 +2266,7 @@ Network.prototype._stabilize = function() {
}
this.emit("stabilizationIterationsDone");
-};
+}
/**
* When initializing and stabilizing, we can freeze nodes with a predefined position. This greatly speeds up stabilization
@@ -2381,12 +2434,18 @@ Network.prototype._animationStep = function() {
// reset the timer so a new scheduled animation step can be set
this.timer = undefined;
+ if (this.requiresTimeout == true) {
+ // this schedules a new animation step
+ this.start();
+ }
+
// handle the keyboad movement
this._handleNavigation();
// check if the physics have settled
if (this.moving == true) {
var startTime = Date.now();
+
this._physicsTick();
var physicsTime = Date.now() - startTime;
@@ -2405,8 +2464,10 @@ Network.prototype._animationStep = function() {
this._redraw();
this.renderTime = Date.now() - renderStartTime;
- // this schedules a new animation step
- this.start();
+ if (this.requiresTimeout == false) {
+ // this schedules a new animation step
+ this.start();
+ }
};
if (typeof window !== 'undefined') {
@@ -2418,6 +2479,9 @@ if (typeof window !== 'undefined') {
* Schedule a animation step with the refreshrate interval.
*/
Network.prototype.start = function() {
+ if (this.freezeSimulationEnabled == true) {
+ this.moving = false;
+ }
if (this.moving == true || this.xIncrement != 0 || this.yIncrement != 0 || this.zoomIncrement != 0 || this.animating == true) {
if (!this.timer) {
if (this.requiresTimeout == true) {
@@ -2429,7 +2493,7 @@ Network.prototype.start = function() {
}
}
else {
- this._redraw();
+ this._requestRedraw();
// this check is to ensure that the network does not emit these events if it was already stabilized and setOptions is called (setting moving to true and calling start())
if (this.stabilizationIterations > 1) {
// trigger the "stabilized" event.
@@ -2535,11 +2599,14 @@ Network.prototype._configureSmoothCurves = function(disableStart) {
*
* @private
*/
-Network.prototype._createBezierNodes = function() {
+Network.prototype._createBezierNodes = function(specificEdges) {
+ if (specificEdges === undefined) {
+ specificEdges = this.edges;
+ }
if (this.constants.smoothCurves.enabled == true && this.constants.smoothCurves.dynamic == true) {
- for (var edgeId in this.edges) {
- if (this.edges.hasOwnProperty(edgeId)) {
- var edge = this.edges[edgeId];
+ for (var edgeId in specificEdges) {
+ if (specificEdges.hasOwnProperty(edgeId)) {
+ var edge = specificEdges[edgeId];
if (edge.via == null) {
var nodeId = "edgeId:".concat(edge.id);
this.sectors['support']['nodes'][nodeId] = new Node(
@@ -2880,4 +2947,21 @@ Network.prototype.getConnectedNodes = function(nodeId) {
return nodeList;
}
+
+Network.prototype.getEdgesFromNode = function(nodeId) {
+ var edgesList = [];
+ if (this.nodes[nodeId] !== undefined) {
+ var node = this.nodes[nodeId];
+ for (var i = 0; i < node.edges.length; i++) {
+ edgesList.push(node.edges[i].id);
+ }
+ }
+ return edgesList;
+}
+
+Network.prototype.generateColorObject = function(color) {
+ return util.parseColor(color);
+
+}
+
module.exports = Network;
diff --git a/lib/network/Node.js b/lib/network/Node.js
index c29d93dc..6dd229a4 100644
--- a/lib/network/Node.js
+++ b/lib/network/Node.js
@@ -13,7 +13,7 @@ var util = require('../util');
* "database", "circle", "ellipse",
* "box", "image", "text", "dot",
* "star", "triangle", "triangleDown",
- * "square"
+ * "square", "icon"
* {string} image An image url
* {string} title An title text, can be HTML
* {anytype} group A group name or number
@@ -33,8 +33,6 @@ function Node(properties, imagelist, grouplist, networkConstants) {
this.hover = false;
this.edges = []; // all edges connected to this node
- this.dynamicEdges = [];
- this.reroutedEdges = {};
// set defaults for the properties
this.id = undefined;
@@ -72,15 +70,6 @@ function Node(properties, imagelist, grouplist, networkConstants) {
this.setProperties(properties, constants);
- // creating the variables for clustering
- this.resetCluster();
- this.clusterSession = 0;
- this.clusterSizeWidthFactor = networkConstants.clustering.nodeScaling.width;
- this.clusterSizeHeightFactor = networkConstants.clustering.nodeScaling.height;
- this.clusterSizeRadiusFactor = networkConstants.clustering.nodeScaling.radius;
- this.maxNodeSizeIncrements = networkConstants.clustering.maxNodeSizeIncrements;
- this.growthIndicator = 0;
-
// variables to tell the node about the network.
this.networkScaleInv = 1;
this.networkScale = 1;
@@ -101,18 +90,6 @@ Node.prototype.revertPosition = function() {
}
-/**
- * (re)setting the clustering variables and objects
- */
-Node.prototype.resetCluster = function() {
- // clustering variables
- this.formationScale = undefined; // this is used to determine when to open the cluster
- this.clusterSize = 1; // this signifies the total amount of nodes in this cluster
- this.containedNodes = {};
- this.containedEdges = {};
- this.clusterSessions = [];
-};
-
/**
* Attach a edge to the node
* @param {Edge} edge
@@ -121,9 +98,6 @@ Node.prototype.attachEdge = function(edge) {
if (this.edges.indexOf(edge) == -1) {
this.edges.push(edge);
}
- if (this.dynamicEdges.indexOf(edge) == -1) {
- this.dynamicEdges.push(edge);
- }
};
/**
@@ -135,10 +109,6 @@ Node.prototype.detachEdge = function(edge) {
if (index != -1) {
this.edges.splice(index, 1);
}
- index = this.dynamicEdges.indexOf(edge);
- if (index != -1) {
- this.dynamicEdges.splice(index, 1);
- }
};
@@ -151,10 +121,12 @@ Node.prototype.setProperties = function(properties, constants) {
if (!properties) {
return;
}
+ this.properties = properties;
- var fields = ['borderWidth','borderWidthSelected','shape','image','brokenImage','radius','fontColor',
- 'fontSize','fontFace','fontFill','fontStrokeWidth','fontStrokeColor','group','mass','fontDrawThreshold',
- 'scaleFontWithValue','fontSizeMaxVisible','customScalingFunction'
+ var fields = ['borderWidth', 'borderWidthSelected', 'shape', 'image', 'brokenImage', 'radius', 'fontColor',
+ 'fontSize', 'fontFace', 'fontFill', 'fontStrokeWidth', 'fontStrokeColor', 'group', 'mass', 'fontDrawThreshold',
+ 'scaleFontWithValue', 'fontSizeMaxVisible', 'customScalingFunction', 'iconFontFace', 'icon', 'iconColor', 'iconSize',
+ 'value'
];
util.selectiveDeepExtend(fields, this.options, properties);
@@ -235,6 +207,7 @@ Node.prototype.setProperties = function(properties, constants) {
case 'triangle': this.draw = this._drawTriangle; this.resize = this._resizeShape; break;
case 'triangleDown': this.draw = this._drawTriangleDown; this.resize = this._resizeShape; break;
case 'star': this.draw = this._drawStar; this.resize = this._resizeShape; break;
+ case 'icon': this.draw = this._drawIcon; this.resize = this._resizeIcon; break;
default: this.draw = this._drawEllipse; this.resize = this._resizeEllipse; break;
}
// reset the size of the node, this can be changed
@@ -423,6 +396,7 @@ Node.prototype.discreteStepLimited = function(interval, maxVelocity) {
this.fy = 0;
this.vy = 0;
}
+
};
/**
@@ -546,29 +520,11 @@ Node.prototype._resizeImage = function (ctx) {
}
this.width = width;
this.height = height;
-
- this.growthIndicator = 0;
- if (this.width > 0 && this.height > 0) {
- this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
- this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
- this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
- this.growthIndicator = this.width - width;
- }
}
};
Node.prototype._drawImageAtPosition = function (ctx) {
if (this.imageObj.width != 0 ) {
- // draw the shade
- if (this.clusterSize > 1) {
- var lineWidth = ((this.clusterSize > 1) ? 10 : 0.0);
- lineWidth *= this.networkScaleInv;
- lineWidth = Math.min(0.2 * this.width,lineWidth);
-
- ctx.globalAlpha = 0.5;
- ctx.drawImage(this.imageObj, this.left - lineWidth, this.top - lineWidth, this.width + 2*lineWidth, this.height + 2*lineWidth);
- }
-
// draw the image
ctx.globalAlpha = 1.0;
ctx.drawImage(this.imageObj, this.left, this.top, this.width, this.height);
@@ -618,12 +574,6 @@ Node.prototype._resizeCircularImage = function (ctx) {
var diameter = this.options.radius * 2;
this.width = diameter;
this.height = diameter;
-
- // scaling used for clustering
- //this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
- //this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
- this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
- this.growthIndicator = this.options.radius- 0.5*diameter;
this._swapToImageResizeWhenImageLoaded = true;
}
}
@@ -677,12 +627,6 @@ Node.prototype._resizeBox = function (ctx) {
var textSize = this.getTextSize(ctx);
this.width = textSize.width + 2 * margin;
this.height = textSize.height + 2 * margin;
-
- this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
- this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
- this.growthIndicator = this.width - (textSize.width + 2 * margin);
-// this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
-
}
};
@@ -692,22 +636,11 @@ Node.prototype._drawBox = function (ctx) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
- var clusterLineWidth = 2.5;
var borderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
-
- // draw the outer border
- if (this.clusterSize > 1) {
- ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.networkScaleInv;
- ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
-
- ctx.roundRect(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth, this.options.radius);
- ctx.stroke();
- }
- ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
+ ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth);
ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
@@ -733,12 +666,6 @@ Node.prototype._resizeDatabase = function (ctx) {
var size = textSize.width + 2 * margin;
this.width = size;
this.height = size;
-
- // scaling used for clustering
- this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
- this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
- this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
- this.growthIndicator = this.width - size;
}
};
@@ -747,22 +674,11 @@ Node.prototype._drawDatabase = function (ctx) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
- var clusterLineWidth = 2.5;
var borderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
-
- // draw the outer border
- if (this.clusterSize > 1) {
- ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.networkScaleInv;
- ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
-
- ctx.database(this.x - this.width/2 - 2*ctx.lineWidth, this.y - this.height*0.5 - 2*ctx.lineWidth, this.width + 4*ctx.lineWidth, this.height + 4*ctx.lineWidth);
- ctx.stroke();
- }
- ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
+ ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth);
ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
@@ -789,32 +705,16 @@ Node.prototype._resizeCircle = function (ctx) {
this.width = diameter;
this.height = diameter;
-
- // scaling used for clustering
-// this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeWidthFactor;
-// this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeHeightFactor;
- this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
- this.growthIndicator = this.options.radius- 0.5*diameter;
}
};
Node.prototype._drawRawCircle = function (ctx, x, y, radius) {
- var clusterLineWidth = 2.5;
var borderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
- // draw the outer border
- if (this.clusterSize > 1) {
- ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.networkScaleInv;
- ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
-
- ctx.circle(x, y, radius+2*ctx.lineWidth);
- ctx.stroke();
- }
- ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
+ ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth);
ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
@@ -849,12 +749,6 @@ Node.prototype._resizeEllipse = function (ctx) {
this.width = this.height;
}
var defaultSize = this.width;
-
- // scaling used for clustering
- this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
- this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
- this.options.radius += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
- this.growthIndicator = this.width - defaultSize;
}
};
@@ -863,22 +757,12 @@ Node.prototype._drawEllipse = function (ctx) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
- var clusterLineWidth = 2.5;
var borderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
- // draw the outer border
- if (this.clusterSize > 1) {
- ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.networkScaleInv;
- ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
-
- ctx.ellipse(this.left-2*ctx.lineWidth, this.top-2*ctx.lineWidth, this.width+4*ctx.lineWidth, this.height+4*ctx.lineWidth);
- ctx.stroke();
- }
- ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
+ ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth);
ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
@@ -922,12 +806,6 @@ Node.prototype._resizeShape = function (ctx) {
var size = 2 * this.options.radius;
this.width = size;
this.height = size;
-
- // scaling used for clustering
- this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
- this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
- this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * 0.5 * this.clusterSizeRadiusFactor;
- this.growthIndicator = this.width - size;
}
};
@@ -937,7 +815,6 @@ Node.prototype._drawShape = function (ctx, shape) {
this.left = this.x - this.width / 2;
this.top = this.y - this.height / 2;
- var clusterLineWidth = 2.5;
var borderWidth = this.options.borderWidth;
var selectionLineWidth = this.options.borderWidthSelected || 2 * this.options.borderWidth;
var radiusMultiplier = 2;
@@ -952,16 +829,7 @@ Node.prototype._drawShape = function (ctx, shape) {
}
ctx.strokeStyle = this.selected ? this.options.color.highlight.border : this.hover ? this.options.color.hover.border : this.options.color.border;
- // draw the outer border
- if (this.clusterSize > 1) {
- ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
- ctx.lineWidth *= this.networkScaleInv;
- ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
-
- ctx[shape](this.x, this.y, this.options.radius+ radiusMultiplier * ctx.lineWidth);
- ctx.stroke();
- }
- ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth) + ((this.clusterSize > 1) ? clusterLineWidth : 0.0);
+ ctx.lineWidth = (this.selected ? selectionLineWidth : borderWidth);
ctx.lineWidth *= this.networkScaleInv;
ctx.lineWidth = Math.min(this.width,ctx.lineWidth);
@@ -989,12 +857,6 @@ Node.prototype._resizeText = function (ctx) {
var textSize = this.getTextSize(ctx);
this.width = textSize.width + 2 * margin;
this.height = textSize.height + 2 * margin;
-
- // scaling used for clustering
- this.width += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeWidthFactor;
- this.height += Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeHeightFactor;
- this.options.radius+= Math.min(this.clusterSize - 1, this.maxNodeSizeIncrements) * this.clusterSizeRadiusFactor;
- this.growthIndicator = this.width - (textSize.width + 2 * margin);
}
};
@@ -1011,7 +873,61 @@ Node.prototype._drawText = function (ctx) {
this.boundingBox.bottom = this.top + this.height;
};
+Node.prototype._resizeIcon = function (ctx) {
+ if (!this.width) {
+ var margin = 5;
+ var iconSize =
+ {
+ width: Number(this.options.iconSize),
+ height: Number(this.options.iconSize)
+ };
+ this.width = iconSize.width + 2 * margin;
+ this.height = iconSize.height + 2 * margin;
+ }
+};
+
+Node.prototype._drawIcon = function (ctx) {
+ this._resizeIcon(ctx);
+ this.options.iconSize = this.options.iconSize || 50;
+
+ this.left = this.x - this.width / 2;
+ this.top = this.y - this.height / 2;
+ this._icon(ctx);
+
+
+ this.boundingBox.top = this.y - this.options.iconSize/2;
+ this.boundingBox.left = this.x - this.options.iconSize/2;
+ this.boundingBox.right = this.x + this.options.iconSize/2;
+ this.boundingBox.bottom = this.y + this.options.iconSize/2;
+
+ if (this.label) {
+ var iconTextSpacing = 5;
+ this._label(ctx, this.label, this.x, this.y + this.height / 2 + iconTextSpacing, 'top', true);
+
+ this.boundingBox.left = Math.min(this.boundingBox.left, this.labelDimensions.left);
+ this.boundingBox.right = Math.max(this.boundingBox.right, this.labelDimensions.left + this.labelDimensions.width);
+ this.boundingBox.bottom = Math.max(this.boundingBox.bottom, this.boundingBox.bottom + this.labelDimensions.height);
+ }
+};
+
+Node.prototype._icon = function (ctx) {
+ var relativeIconSize = Number(this.options.iconSize) * this.networkScale;
+
+ if (this.options.icon && relativeIconSize > this.options.fontDrawThreshold - 1) {
+
+ var iconSize = Number(this.options.iconSize);
+
+ ctx.font = (this.selected ? "bold " : "") + iconSize + "px " + this.options.iconFontFace;
+
+ // draw icon
+ ctx.fillStyle = this.options.iconColor || "black";
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+ ctx.fillText(this.options.icon, this.x, this.y);
+ }
+};
+
Node.prototype._label = function (ctx, text, x, y, align, baseline, labelUnderNode) {
var relativeFontSize = Number(this.options.fontSize) * this.networkScale;
if (text && relativeFontSize >= this.options.fontDrawThreshold - 1) {
diff --git a/lib/network/Popup.js b/lib/network/Popup.js
index a0284fe0..65659b88 100644
--- a/lib/network/Popup.js
+++ b/lib/network/Popup.js
@@ -40,8 +40,9 @@ function Popup(container, x, y, text, style) {
this.x = 0;
this.y = 0;
this.padding = 5;
+ this.hidden = false;
- if (x !== undefined && y !== undefined ) {
+ if (x !== undefined && y !== undefined) {
this.setPosition(x, y);
}
if (text !== undefined) {
@@ -116,6 +117,7 @@ Popup.prototype.show = function (show) {
this.frame.style.left = left + "px";
this.frame.style.top = top + "px";
this.frame.style.visibility = "visible";
+ this.hidden = false;
}
else {
this.hide();
@@ -126,6 +128,7 @@ Popup.prototype.show = function (show) {
* Hide the popup window
*/
Popup.prototype.hide = function () {
+ this.hidden = true;
this.frame.style.visibility = "hidden";
};
diff --git a/lib/network/mixins/ClusterMixin.js b/lib/network/mixins/ClusterMixin.js
index f3a33587..bde1f858 100644
--- a/lib/network/mixins/ClusterMixin.js
+++ b/lib/network/mixins/ClusterMixin.js
@@ -1,1049 +1,593 @@
-/**
- * Creation of the ClusterMixin var.
- *
- * This contains all the functions the Network object can use to employ clustering
- */
-
-/**
-* This is only called in the constructor of the network object
-*
-*/
-exports.startWithClustering = function() {
- // cluster if the data set is big
- this.clusterToFit(this.constants.clustering.initialMaxNodes, true);
-
- // updates the lables after clustering
- this.updateLabels();
-
- // this is called here because if clusterin is disabled, the start and stabilize are called in
- // the setData function.
- if (this.constants.stabilize == true) {
- this._stabilize();
- }
- this.start();
-};
+var Node = require('../Node');
+var Edge = require('../Edge');
+var util = require('../../util');
/**
- * This function clusters until the initialMaxNodes has been reached
*
- * @param {Number} maxNumberOfNodes
- * @param {Boolean} reposition
+ * @param hubsize
+ * @param options
*/
-exports.clusterToFit = function(maxNumberOfNodes, reposition) {
- var numberOfNodes = this.nodeIndices.length;
-
- var maxLevels = 50;
- var level = 0;
-
- // we first cluster the hubs, then we pull in the outliers, repeat
- while (numberOfNodes > maxNumberOfNodes && level < maxLevels) {
- if (level % 3 == 0.0) {
- this.forceAggregateHubs(true);
- this.normalizeClusterLevels();
- }
- else {
- this.increaseClusterLevel(); // this also includes a cluster normalization
- }
- this.forceAggregateHubs(true);
- numberOfNodes = this.nodeIndices.length;
- level += 1;
+exports.clusterByConnectionCount = function(hubsize, options) {
+ if (hubsize === undefined) {
+ hubsize = this._getHubSize();
}
-
- // after the clustering we reposition the nodes to reduce the initial chaos
- if (level > 0 && reposition == true) {
- this.repositionNodes();
+ else if (tyepof(hubsize) == "object") {
+ options = this._checkOptions(hubsize);
+ hubsize = this._getHubSize();
}
- this._updateCalculationNodes();
-};
-/**
- * This function can be called to open up a specific cluster.
- * It will unpack the cluster back one level.
- *
- * @param node | Node object: cluster to open.
- */
-exports.openCluster = function(node) {
- var isMovingBeforeClustering = this.moving;
- if (node.clusterSize > this.constants.clustering.sectorThreshold && this._nodeInActiveArea(node) &&
- !(this._sector() == "default" && this.nodeIndices.length == 1)) {
- // this loads a new sector, loads the nodes and edges and nodeIndices of it.
- this._addSector(node);
- var level = 0;
-
- // we decluster until we reach a decent number of nodes
- while ((this.nodeIndices.length < this.constants.clustering.initialMaxNodes) && (level < 10)) {
- this.decreaseClusterLevel();
- level += 1;
+ var nodesToCluster = [];
+ for (var i = 0; i < this.nodeIndices.length; i++) {
+ var node = this.nodes[this.nodeIndices[i]];
+ if (node.edges.length >= hubsize) {
+ nodesToCluster.push(node.id);
}
-
- }
- else {
- this._expandClusterNode(node,false,true);
-
- // update the index list and labels
- this._updateNodeIndexList();
- this._updateCalculationNodes();
- this.updateLabels();
}
- // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
- if (this.moving != isMovingBeforeClustering) {
- this.start();
+ for (var i = 0; i < nodesToCluster.length; i++) {
+ var node = this.nodes[nodesToCluster[i]];
+ this.clusterByConnection(node,options,{},{},true);
}
-};
+ this._wrapUp();
+}
/**
- * This calls the updateClustes with default arguments
+ * loop over all nodes, check if they adhere to the condition and cluster if needed.
+ * @param options
+ * @param doNotUpdateCalculationNodes
*/
-exports.updateClustersDefault = function() {
- if (this.constants.clustering.enabled == true && this.constants.clustering.clusterByZoom == true) {
- this.updateClusters(0,false,false);
- }
-};
+exports.clusterByNodeData = function(options, doNotUpdateCalculationNodes) {
+ if (options === undefined) {throw new Error("Cannot call clusterByNodeData without options.");}
+ if (options.joinCondition === undefined) {throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options.");}
+ // check if the options object is fine, append if needed
+ options = this._checkOptions(options);
-/**
- * This function can be called to increase the cluster level. This means that the nodes with only one edge connection will
- * be clustered with their connected node. This can be repeated as many times as needed.
- * This can be called externally (by a keybind for instance) to reduce the complexity of big datasets.
- */
-exports.increaseClusterLevel = function() {
- this.updateClusters(-1,false,true);
-};
+ var childNodesObj = {};
+ var childEdgesObj = {}
+ // collect the nodes that will be in the cluster
+ for (var i = 0; i < this.nodeIndices.length; i++) {
+ var nodeId = this.nodeIndices[i];
+ var clonedOptions = this._cloneOptions(nodeId);
+ if (options.joinCondition(clonedOptions) == true) {
+ childNodesObj[nodeId] = this.nodes[nodeId];
+ }
+ }
-/**
- * This function can be called to decrease the cluster level. This means that the nodes with only one edge connection will
- * be unpacked if they are a cluster. This can be repeated as many times as needed.
- * This can be called externally (by a key-bind for instance) to look into clusters without zooming.
- */
-exports.decreaseClusterLevel = function() {
- this.updateClusters(1,false,true);
-};
+ this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
+}
/**
- * This is the main clustering function. It clusters and declusters on zoom or forced
- * This function clusters on zoom, it can be called with a predefined zoom direction
- * If out, check if we can form clusters, if in, check if we can open clusters.
- * This function is only called from _zoom()
- *
- * @param {Number} zoomDirection | -1 / 0 / +1 for zoomOut / determineByZoom / zoomIn
- * @param {Boolean} recursive | enabled or disable recursive calling of the opening of clusters
- * @param {Boolean} force | enabled or disable forcing
- * @param {Boolean} doNotStart | if true do not call start
- *
+ * Cluster all nodes in the network that have only 1 edge
+ * @param options
+ * @param doNotUpdateCalculationNodes
*/
-exports.updateClusters = function(zoomDirection,recursive,force,doNotStart) {
- var isMovingBeforeClustering = this.moving;
- var amountOfNodes = this.nodeIndices.length;
-
- var detectedZoomingIn = (this.previousScale < this.scale && zoomDirection == 0);
- var detectedZoomingOut = (this.previousScale > this.scale && zoomDirection == 0);
+exports.clusterOutliers = function(options, doNotUpdateCalculationNodes) {
+ options = this._checkOptions(options);
- // on zoom out collapse the sector if the scale is at the level the sector was made
- if (detectedZoomingOut == true) {
- this._collapseSector();
- }
+ var clusters = []
- // check if we zoom in or out
- if (detectedZoomingOut == true || zoomDirection == -1) { // zoom out
- // forming clusters when forced pulls outliers in. When not forced, the edge length of the
- // outer nodes determines if it is being clustered
- this._formClusters(force);
- }
- else if (detectedZoomingIn == true || zoomDirection == 1) { // zoom in
- if (force == true) {
- // _openClusters checks for each node if the formationScale of the cluster is smaller than
- // the current scale and if so, declusters. When forced, all clusters are reduced by one step
- this._openClusters(recursive,force);
- }
- else {
- // if a cluster takes up a set percentage of the active window
- //this._openClustersBySize();
- this._openClusters(recursive, false);
+ // collect the nodes that will be in the cluster
+ for (var i = 0; i < this.nodeIndices.length; i++) {
+ var childNodesObj = {};
+ var childEdgesObj = {};
+ var nodeId = this.nodeIndices[i];
+ if (this.nodes[nodeId].edges.length == 1) {
+ var edge = this.nodes[nodeId].edges[0];
+ var childNodeId = this._getConnectedId(edge, nodeId);
+ if (childNodeId != nodeId) {
+ if (options.joinCondition === undefined) {
+ childNodesObj[nodeId] = this.nodes[nodeId];
+ childNodesObj[childNodeId] = this.nodes[childNodeId];
+ }
+ else {
+ var clonedOptions = this._cloneOptions(nodeId);
+ if (options.joinCondition(clonedOptions) == true) {
+ childNodesObj[nodeId] = this.nodes[nodeId];
+ }
+ clonedOptions = this._cloneOptions(childNodeId);
+ if (options.joinCondition(clonedOptions) == true) {
+ childNodesObj[childNodeId] = this.nodes[childNodeId];
+ }
+ }
+ clusters.push({nodes:childNodesObj, edges:childEdgesObj})
+ }
}
}
- this._updateNodeIndexList();
- // if a cluster was NOT formed and the user zoomed out, we try clustering by hubs
- if (this.nodeIndices.length == amountOfNodes && (detectedZoomingOut == true || zoomDirection == -1)) {
- this._aggregateHubs(force);
- this._updateNodeIndexList();
+ for (var i = 0; i < clusters.length; i++) {
+ this._cluster(clusters[i].nodes, clusters[i].edges, options, true)
}
- // we now reduce chains.
- if (detectedZoomingOut == true || zoomDirection == -1) { // zoom out
- this.handleChains();
- this._updateNodeIndexList();
+ if (doNotUpdateCalculationNodes !== true) {
+ this._wrapUp();
}
-
- this.previousScale = this.scale;
-
- // update labels
- this.updateLabels();
-
- // if a cluster was formed, we increase the clusterSession
- if (this.nodeIndices.length < amountOfNodes) { // this means a clustering operation has taken place
- this.clusterSession += 1;
- // if clusters have been made, we normalize the cluster level
- this.normalizeClusterLevels();
- }
-
- if (doNotStart == false || doNotStart === undefined) {
- // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
- if (this.moving != isMovingBeforeClustering) {
- this.start();
- }
- }
-
- this._updateCalculationNodes();
-};
+}
/**
- * This function handles the chains. It is called on every updateClusters().
- */
-exports.handleChains = function() {
- // after clustering we check how many chains there are
- var chainPercentage = this._getChainFraction();
- if (chainPercentage > this.constants.clustering.chainThreshold) {
- this._reduceAmountOfChains(1 - this.constants.clustering.chainThreshold / chainPercentage)
-
- }
-};
-
-/**
- * this functions starts clustering by hubs
- * The minimum hub threshold is set globally
- *
- * @private
- */
-exports._aggregateHubs = function(force) {
- this._getHubSize();
- this._formClustersByHub(force,false);
-};
-
-
-/**
- * This function forces hubs to form.
*
+ * @param nodeId
+ * @param options
+ * @param doNotUpdateCalculationNodes
*/
-exports.forceAggregateHubs = function(doNotStart) {
- var isMovingBeforeClustering = this.moving;
- var amountOfNodes = this.nodeIndices.length;
-
- this._aggregateHubs(true);
+exports.clusterByConnection = function(nodeId, options, doNotUpdateCalculationNodes) {
+ // kill conditions
+ if (nodeId === undefined) {throw new Error("No nodeId supplied to clusterByConnection!");}
+ if (this.nodes[nodeId] === undefined) {throw new Error("The nodeId given to clusterByConnection does not exist!");}
- // update the index list, dynamic edges and labels
- this._updateNodeIndexList();
- this.updateLabels();
-
- this._updateCalculationNodes();
+ var node = this.nodes[nodeId];
+ options = this._checkOptions(options, node);
+ if (options.clusterNodeProperties.x === undefined) {options.clusterNodeProperties.x = node.x; options.clusterNodeProperties.allowedToMoveX = !node.xFixed;}
+ if (options.clusterNodeProperties.y === undefined) {options.clusterNodeProperties.y = node.y; options.clusterNodeProperties.allowedToMoveY = !node.yFixed;}
- // if a cluster was formed, we increase the clusterSession
- if (this.nodeIndices.length != amountOfNodes) {
- this.clusterSession += 1;
- }
+ var childNodesObj = {};
+ var childEdgesObj = {}
+ var parentNodeId = node.id;
+ var parentClonedOptions = this._cloneOptions(parentNodeId);
+ childNodesObj[parentNodeId] = node;
- if (doNotStart == false || doNotStart === undefined) {
- // if the simulation was settled, we restart the simulation if a cluster has been formed or expanded
- if (this.moving != isMovingBeforeClustering) {
- this.start();
- }
- }
-};
+ // collect the nodes that will be in the cluster
+ for (var i = 0; i < node.edges.length; i++) {
+ var edge = node.edges[i];
+ var childNodeId = this._getConnectedId(edge, parentNodeId);
-/**
- * If a cluster takes up more than a set percentage of the screen, open the cluster
- *
- * @private
- */
-exports._openClustersBySize = function() {
- if (this.constants.clustering.clusterByZoom == true) {
- for (var nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- var node = this.nodes[nodeId];
- if (node.inView() == true) {
- if ((node.width * this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientWidth) ||
- (node.height * this.scale > this.constants.clustering.screenSizeThreshold * this.frame.canvas.clientHeight)) {
- this.openCluster(node);
- }
+ if (childNodeId !== parentNodeId) {
+ if (options.joinCondition === undefined) {
+ childEdgesObj[edge.id] = edge;
+ childNodesObj[childNodeId] = this.nodes[childNodeId];
+ }
+ else {
+ // clone the options and insert some additional parameters that could be interesting.
+ var childClonedOptions = this._cloneOptions(childNodeId);
+ if (options.joinCondition(parentClonedOptions, childClonedOptions) == true) {
+ childEdgesObj[edge.id] = edge;
+ childNodesObj[childNodeId] = this.nodes[childNodeId];
}
}
}
+ else {
+ childEdgesObj[edge.id] = edge;
+ }
}
-};
+ this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
+}
-/**
- * This function loops over all nodes in the nodeIndices list. For each node it checks if it is a cluster and if it
- * has to be opened based on the current zoom level.
- *
- * @private
- */
-exports._openClusters = function(recursive,force) {
- for (var i = 0; i < this.nodeIndices.length; i++) {
- var node = this.nodes[this.nodeIndices[i]];
- this._expandClusterNode(node,recursive,force);
- this._updateCalculationNodes();
- }
-};
/**
- * This function checks if a node has to be opened. This is done by checking the zoom level.
- * If the node contains child nodes, this function is recursively called on the child nodes as well.
- * This recursive behaviour is optional and can be set by the recursive argument.
- *
- * @param {Node} parentNode | to check for cluster and expand
- * @param {Boolean} recursive | enabled or disable recursive calling
- * @param {Boolean} force | enabled or disable forcing
- * @param {Boolean} [openAll] | This will recursively force all nodes in the parent to be released
+ * This returns a clone of the options or properties of the edge or node to be used for construction of new edges or check functions for new nodes.
+ * @param objId
+ * @param type
+ * @returns {{}}
* @private
*/
-exports._expandClusterNode = function(parentNode, recursive, force, openAll) {
- // first check if node is a cluster
- if (parentNode.clusterSize > 1) {
- if (openAll === undefined) {
- openAll = false;
- }
- // this means that on a double tap event or a zoom event, the cluster fully unpacks if it is smaller than 20
-
- recursive = openAll || recursive;
- // if the last child has been added on a smaller scale than current scale decluster
- if (parentNode.formationScale < this.scale || force == true) {
- // we will check if any of the contained child nodes should be removed from the cluster
- for (var containedNodeId in parentNode.containedNodes) {
- if (parentNode.containedNodes.hasOwnProperty(containedNodeId)) {
- var childNode = parentNode.containedNodes[containedNodeId];
-
- // force expand will expand the largest cluster size clusters. Since we cluster from outside in, we assume that
- // the largest cluster is the one that comes from outside
- if (force == true) {
- if (childNode.clusterSession == parentNode.clusterSessions[parentNode.clusterSessions.length-1]
- || openAll) {
- this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
- }
- }
- else {
- if (this._nodeInActiveArea(parentNode)) {
- this._expelChildFromParent(parentNode,containedNodeId,recursive,force,openAll);
- }
- }
- }
- }
- }
+exports._cloneOptions = function(objId, type) {
+ var clonedOptions = {};
+ if (type === undefined || type == 'node') {
+ util.deepExtend(clonedOptions, this.nodes[objId].options, true);
+ util.deepExtend(clonedOptions, this.nodes[objId].properties, true);
+ clonedOptions.amountOfConnections = this.nodes[objId].edges.length;
}
-};
+ else {
+ util.deepExtend(clonedOptions, this.edges[objId].properties, true);
+ }
+ return clonedOptions;
+}
+
/**
- * ONLY CALLED FROM _expandClusterNode
- *
- * This function will expel a child_node from a parent_node. This is to de-cluster the node. This function will remove
- * the child node from the parent contained_node object and put it back into the global nodes object.
- * The same holds for the edge that was connected to the child node. It is moved back into the global edges object.
+ * This function creates the edges that will be attached to the cluster.
*
- * @param {Node} parentNode | the parent node
- * @param {String} containedNodeId | child_node id as it is contained in the containedNodes object of the parent node
- * @param {Boolean} recursive | This will also check if the child needs to be expanded.
- * With force and recursive both true, the entire cluster is unpacked
- * @param {Boolean} force | This will disregard the zoom level and will expel this child from the parent
- * @param {Boolean} openAll | This will recursively force all nodes in the parent to be released
+ * @param childNodesObj
+ * @param childEdgesObj
+ * @param newEdges
+ * @param options
* @private
*/
-exports._expelChildFromParent = function(parentNode, containedNodeId, recursive, force, openAll) {
- var childNode = parentNode.containedNodes[containedNodeId]
-
- // if child node has been added on smaller scale than current, kick out
- if (childNode.formationScale < this.scale || force == true) {
- // unselect all selected items
- this._unselectAll();
-
- // put the child node back in the global nodes object
- this.nodes[containedNodeId] = childNode;
+exports._createClusterEdges = function (childNodesObj, childEdgesObj, newEdges, options) {
+ var edge, childNodeId, childNode;
- // release the contained edges from this childNode back into the global edges
- this._releaseContainedEdges(parentNode,childNode);
+ var childKeys = Object.keys(childNodesObj);
+ for (var i = 0; i < childKeys.length; i++) {
+ childNodeId = childKeys[i];
+ childNode = childNodesObj[childNodeId];
- // reconnect rerouted edges to the childNode
- this._connectEdgeBackToChild(parentNode,childNode);
+ // mark all edges for removal from global and construct new edges from the cluster to others
+ for (var j = 0; j < childNode.edges.length; j++) {
+ edge = childNode.edges[j];
+ childEdgesObj[edge.id] = edge;
- // validate all edges in dynamicEdges
- this._validateEdges(parentNode);
-
- // undo the changes from the clustering operation on the parent node
- parentNode.options.mass -= childNode.options.mass;
- parentNode.clusterSize -= childNode.clusterSize;
- parentNode.options.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*(parentNode.clusterSize-1));
-
- // place the child node near the parent, not at the exact same location to avoid chaos in the system
- childNode.x = parentNode.x + parentNode.growthIndicator * (0.5 - Math.random());
- childNode.y = parentNode.y + parentNode.growthIndicator * (0.5 - Math.random());
+ var otherNodeId = edge.toId;
+ var otherOnTo = true;
+ if (edge.toId != childNodeId) {
+ otherNodeId = edge.toId;
+ otherOnTo = true;
+ }
+ else if (edge.fromId != childNodeId) {
+ otherNodeId = edge.fromId;
+ otherOnTo = false;
+ }
- // remove node from the list
- delete parentNode.containedNodes[containedNodeId];
+ if (childNodesObj[otherNodeId] === undefined) {
+ var clonedOptions = this._cloneOptions(edge.id, 'edge');
+ util.deepExtend(clonedOptions, options.clusterEdgeProperties);
+ // avoid forcing the default color on edges that inherit color
+ if (edge.properties.color === undefined) {
+ delete clonedOptions.color;
+ }
- // check if there are other childs with this clusterSession in the parent.
- var othersPresent = false;
- for (var childNodeId in parentNode.containedNodes) {
- if (parentNode.containedNodes.hasOwnProperty(childNodeId)) {
- if (parentNode.containedNodes[childNodeId].clusterSession == childNode.clusterSession) {
- othersPresent = true;
- break;
+ if (otherOnTo === true) {
+ clonedOptions.from = options.clusterNodeProperties.id;
+ clonedOptions.to = otherNodeId;
}
+ else {
+ clonedOptions.from = otherNodeId;
+ clonedOptions.to = options.clusterNodeProperties.id;
+ }
+ clonedOptions.id = 'clusterEdge:' + util.randomUUID();
+ newEdges.push(new Edge(clonedOptions,this,this.constants))
}
}
- // if there are no others, remove the cluster session from the list
- if (othersPresent == false) {
- parentNode.clusterSessions.pop();
- }
-
- this._repositionBezierNodes(childNode);
-// this._repositionBezierNodes(parentNode);
-
- // remove the clusterSession from the child node
- childNode.clusterSession = 0;
-
- // recalculate the size of the node on the next time the node is rendered
- parentNode.clearSizeCache();
-
- // restart the simulation to reorganise all nodes
- this.moving = true;
}
-
- // check if a further expansion step is possible if recursivity is enabled
- if (recursive == true) {
- this._expandClusterNode(childNode,recursive,force,openAll);
- }
-};
+}
/**
- * position the bezier nodes at the center of the edges
- *
- * @param node
+ * This function checks the options that can be supplied to the different cluster functions
+ * for certain fields and inserts defaults if needed
+ * @param options
+ * @returns {*}
* @private
*/
-exports._repositionBezierNodes = function(node) {
- for (var i = 0; i < node.dynamicEdges.length; i++) {
- node.dynamicEdges[i].positionBezierNode();
- }
-};
+exports._checkOptions = function(options) {
+ if (options === undefined) {options = {};}
+ if (options.clusterEdgeProperties === undefined) {options.clusterEdgeProperties = {};}
+ if (options.clusterNodeProperties === undefined) {options.clusterNodeProperties = {};}
+ return options;
+}
/**
- * This function checks if any nodes at the end of their trees have edges below a threshold length
- * This function is called only from updateClusters()
- * forceLevelCollapse ignores the length of the edge and collapses one level
- * This means that a node with only one edge will be clustered with its connected node
*
+ * @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node
+ * @param {Object} childEdgesObj | object with edge objects, id as keys
+ * @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties}
+ * @param {Boolean} doNotUpdateCalculationNodes | when true, do not wrap up
* @private
- * @param {Boolean} force
*/
-exports._formClusters = function(force) {
- if (force == false) {
- if (this.constants.clustering.clusterByZoom == true) {
- this._formClustersByZoom();
- }
- }
- else {
- this._forceClustersByZoom();
- }
-};
+exports._cluster = function(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes) {
+ // kill condition: no children so cant cluster
+ if (Object.keys(childNodesObj).length == 0) {return;}
+ // check if we have an unique id;
+ if (options.clusterNodeProperties.id === undefined) {options.clusterNodeProperties.id = 'cluster:' + util.randomUUID();}
+ var clusterId = options.clusterNodeProperties.id;
-/**
- * This function handles the clustering by zooming out, this is based on a minimum edge distance
- *
- * @private
- */
-exports._formClustersByZoom = function() {
- var dx,dy,length;
- var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
-
- // check if any edges are shorter than minLength and start the clustering
- // the clustering favours the node with the larger mass
- for (var edgeId in this.edges) {
- if (this.edges.hasOwnProperty(edgeId)) {
- var edge = this.edges[edgeId];
- if (edge.connected) {
- if (edge.toId != edge.fromId) {
- dx = (edge.to.x - edge.from.x);
- dy = (edge.to.y - edge.from.y);
- length = Math.sqrt(dx * dx + dy * dy);
-
-
- if (length < minLength) {
- // first check which node is larger
- var parentNode = edge.from;
- var childNode = edge.to;
- if (edge.to.options.mass > edge.from.options.mass) {
- parentNode = edge.to;
- childNode = edge.from;
- }
-
- if (childNode.dynamicEdges.length == 1) {
- this._addToCluster(parentNode,childNode,false);
- }
- else if (parentNode.dynamicEdges.length == 1) {
- this._addToCluster(childNode,parentNode,false);
- }
- }
- }
- }
- }
- }
-};
+ // create the new edges that will connect to the cluster
+ var newEdges = [];
+ this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options);
-/**
- * This function forces the network to cluster all nodes with only one connecting edge to their
- * connected node.
- *
- * @private
- */
-exports._forceClustersByZoom = function() {
- for (var nodeId in this.nodes) {
- // another node could have absorbed this child.
- if (this.nodes.hasOwnProperty(nodeId)) {
- var childNode = this.nodes[nodeId];
-
- // the edges can be swallowed by another decrease
- if (childNode.dynamicEdges.length == 1) {
- var edge = childNode.dynamicEdges[0];
- var parentNode = (edge.toId == childNode.id) ? this.nodes[edge.fromId] : this.nodes[edge.toId];
- // group to the largest node
- if (childNode.id != parentNode.id) {
- if (parentNode.options.mass > childNode.options.mass) {
- this._addToCluster(parentNode,childNode,true);
- }
- else {
- this._addToCluster(childNode,parentNode,true);
- }
- }
- }
+ // construct the clusterNodeProperties
+ var clusterNodeProperties = options.clusterNodeProperties;
+ if (options.processProperties !== undefined) {
+ // get the childNode options
+ var childNodesOptions = [];
+ for (var nodeId in childNodesObj) {
+ var clonedOptions = this._cloneOptions(nodeId);
+ childNodesOptions.push(clonedOptions);
}
- }
-};
-
-
-/**
- * To keep the nodes of roughly equal size we normalize the cluster levels.
- * This function clusters a node to its smallest connected neighbour.
- *
- * @param node
- * @private
- */
-exports._clusterToSmallestNeighbour = function(node) {
- var smallestNeighbour = -1;
- var smallestNeighbourNode = null;
- for (var i = 0; i < node.dynamicEdges.length; i++) {
- if (node.dynamicEdges[i] !== undefined) {
- var neighbour = null;
- if (node.dynamicEdges[i].fromId != node.id) {
- neighbour = node.dynamicEdges[i].from;
- }
- else if (node.dynamicEdges[i].toId != node.id) {
- neighbour = node.dynamicEdges[i].to;
- }
+ // get clusterproperties based on childNodes
+ var childEdgesOptions = [];
+ for (var edgeId in childEdgesObj) {
+ var clonedOptions = this._cloneOptions(edgeId, 'edge');
+ childEdgesOptions.push(clonedOptions);
+ }
- if (neighbour != null && smallestNeighbour > neighbour.clusterSessions.length) {
- smallestNeighbour = neighbour.clusterSessions.length;
- smallestNeighbourNode = neighbour;
- }
+ clusterNodeProperties = options.processProperties(clusterNodeProperties, childNodesOptions, childEdgesOptions);
+ if (!clusterNodeProperties) {
+ throw new Error("The processClusterProperties function does not return properties!");
}
}
-
- if (neighbour != null && this.nodes[neighbour.id] !== undefined) {
- this._addToCluster(neighbour, node, true);
+ if (clusterNodeProperties.label === undefined) {
+ clusterNodeProperties.label = 'cluster';
}
-};
-/**
- * This function forms clusters from hubs, it loops over all nodes
- *
- * @param {Boolean} force | Disregard zoom level
- * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
- * @private
- */
-exports._formClustersByHub = function(force, onlyEqual) {
- // we loop over all nodes in the list
- for (var nodeId in this.nodes) {
- // we check if it is still available since it can be used by the clustering in this loop
- if (this.nodes.hasOwnProperty(nodeId)) {
- this._formClusterFromHub(this.nodes[nodeId],force,onlyEqual);
+ // give the clusterNode a postion if it does not have one.
+ var pos = undefined
+ if (clusterNodeProperties.x === undefined) {
+ pos = this._getClusterPosition(childNodesObj);
+ clusterNodeProperties.x = pos.x;
+ clusterNodeProperties.allowedToMoveX = true;
+ }
+ if (clusterNodeProperties.x === undefined) {
+ if (pos === undefined) {
+ pos = this._getClusterPosition(childNodesObj);
}
+ clusterNodeProperties.y = pos.y;
+ clusterNodeProperties.allowedToMoveY = true;
}
-};
-/**
- * This function forms a cluster from a specific preselected hub node
- *
- * @param {Node} hubNode | the node we will cluster as a hub
- * @param {Boolean} force | Disregard zoom level
- * @param {Boolean} onlyEqual | This only clusters a hub with a specific number of edges
- * @param {Number} [absorptionSizeOffset] |
- * @private
- */
-exports._formClusterFromHub = function(hubNode, force, onlyEqual, absorptionSizeOffset) {
- if (absorptionSizeOffset === undefined) {
- absorptionSizeOffset = 0;
- }
- //this.hubThreshold = 43
- //if (hubNode.dynamicEdgesLength < 0) {
- // console.error(hubNode.dynamicEdgesLength, this.hubThreshold, onlyEqual)
- //}
- // we decide if the node is a hub
- if ((hubNode.dynamicEdges.length >= this.hubThreshold && onlyEqual == false) ||
- (hubNode.dynamicEdges.length == this.hubThreshold && onlyEqual == true)) {
- // initialize variables
- var dx,dy,length;
- var minLength = this.constants.clustering.clusterEdgeThreshold/this.scale;
- var allowCluster = false;
-
- // we create a list of edges because the dynamicEdges change over the course of this loop
- var edgesIdarray = [];
- var amountOfInitialEdges = hubNode.dynamicEdges.length;
- for (var j = 0; j < amountOfInitialEdges; j++) {
- edgesIdarray.push(hubNode.dynamicEdges[j].id);
- }
- // if the hub clustering is not forced, we check if one of the edges connected
- // to a cluster is small enough based on the constants.clustering.clusterEdgeThreshold
- if (force == false) {
- allowCluster = false;
- for (j = 0; j < amountOfInitialEdges; j++) {
- var edge = this.edges[edgesIdarray[j]];
- if (edge !== undefined) {
- if (edge.connected) {
- if (edge.toId != edge.fromId) {
- dx = (edge.to.x - edge.from.x);
- dy = (edge.to.y - edge.from.y);
- length = Math.sqrt(dx * dx + dy * dy);
-
- if (length < minLength) {
- allowCluster = true;
- break;
- }
- }
- }
- }
- }
- }
+ // force the ID to remain the same
+ clusterNodeProperties.id = clusterId;
- // start the clustering if allowed
- if ((!force && allowCluster) || force) {
- var children = [];
- var childrenIds = {};
- // we loop over all edges INITIALLY connected to this hub to get a list of the childNodes
- for (j = 0; j < amountOfInitialEdges; j++) {
- edge = this.edges[edgesIdarray[j]];
- var childNode = this.nodes[(edge.fromId == hubNode.id) ? edge.toId : edge.fromId];
- if (childrenIds[childNode.id] === undefined) {
- childrenIds[childNode.id] = true;
- children.push(childNode);
- }
- }
- for (j = 0; j < children.length; j++) {
- var childNode = children[j];
- // we do not want hubs to merge with other hubs nor do we want to cluster itself.
- if ((childNode.dynamicEdges.length <= (this.hubThreshold + absorptionSizeOffset)) &&
- (childNode.id != hubNode.id)) {
- this._addToCluster(hubNode,childNode,force);
+ // create the clusterNode
+ var clusterNode = new Node(clusterNodeProperties, this.images, this.groups, this.constants);
+ clusterNode.isCluster = true;
+ clusterNode.containedNodes = childNodesObj;
+ clusterNode.containedEdges = childEdgesObj;
+
+ // delete contained edges from global
+ for (var edgeId in childEdgesObj) {
+ if (childEdgesObj.hasOwnProperty(edgeId)) {
+ if (this.edges[edgeId] !== undefined) {
+ if (this.edges[edgeId].via !== null) {
+ var viaId = this.edges[edgeId].via.id;
+ if (viaId) {
+ this.edges[edgeId].via = null
+ delete this.sectors['support']['nodes'][viaId];
+ }
}
- else {
- //console.log("WILL NOT MERGE:",childNode.dynamicEdges.length , (this.hubThreshold + absorptionSizeOffset))
- }
+ this.edges[edgeId].disconnect();
+ delete this.edges[edgeId];
}
-
}
}
-};
-
-/**
- * This function adds the child node to the parent node, creating a cluster if it is not already.
- *
- * @param {Node} parentNode | this is the node that will house the child node
- * @param {Node} childNode | this node will be deleted from the global this.nodes and stored in the parent node
- * @param {Boolean} force | true will only update the remainingEdges at the very end of the clustering, ensuring single level collapse
- * @private
- */
-exports._addToCluster = function(parentNode, childNode, force) {
- // join child node in the parent node
- parentNode.containedNodes[childNode.id] = childNode;
- //console.log(parentNode.id, childNode.id)
- // manage all the edges connected to the child and parent nodes
- for (var i = 0; i < childNode.dynamicEdges.length; i++) {
- var edge = childNode.dynamicEdges[i];
- if (edge.toId == parentNode.id || edge.fromId == parentNode.id) { // edge connected to parentNode
- //console.log("COLLECT",parentNode.id, childNode.id, edge.toId, edge.fromId)
- this._addToContainedEdges(parentNode,childNode,edge);
- }
- else {
- //console.log("REWIRE",parentNode.id, childNode.id, edge.toId, edge.fromId)
- this._connectEdgeToCluster(parentNode,childNode,edge);
+ // remove contained nodes from global
+ for (var nodeId in childNodesObj) {
+ if (childNodesObj.hasOwnProperty(nodeId)) {
+ this.clusteredNodes[nodeId] = {clusterId:clusterNodeProperties.id, node: this.nodes[nodeId]};
+ delete this.nodes[nodeId];
}
}
- // a contained node has no dynamic edges.
- childNode.dynamicEdges = [];
-
- // remove circular edges from clusters
- this._containCircularEdgesFromNode(parentNode,childNode);
- // remove the childNode from the global nodes object
- delete this.nodes[childNode.id];
+ // finally put the cluster node into global
+ this.nodes[clusterNodeProperties.id] = clusterNode;
- // update the properties of the child and parent
- var massBefore = parentNode.options.mass;
- childNode.clusterSession = this.clusterSession;
- parentNode.options.mass += childNode.options.mass;
- parentNode.clusterSize += childNode.clusterSize;
- parentNode.options.fontSize = Math.min(this.constants.clustering.maxFontSize, this.constants.nodes.fontSize + this.constants.clustering.fontSizeMultiplier*parentNode.clusterSize);
- // keep track of the clustersessions so we can open the cluster up as it has been formed.
- if (parentNode.clusterSessions[parentNode.clusterSessions.length - 1] != this.clusterSession) {
- parentNode.clusterSessions.push(this.clusterSession);
- }
-
- // forced clusters only open from screen size and double tap
- if (force == true) {
- parentNode.formationScale = 0;
- }
- else {
- parentNode.formationScale = this.scale; // The latest child has been added on this scale
+ // push new edges to global
+ for (var i = 0; i < newEdges.length; i++) {
+ this.edges[newEdges[i].id] = newEdges[i];
+ this.edges[newEdges[i].id].connect();
}
- // recalculate the size of the node on the next time the node is rendered
- parentNode.clearSizeCache();
-
- // set the pop-out scale for the childnode
- parentNode.containedNodes[childNode.id].formationScale = parentNode.formationScale;
- // nullify the movement velocity of the child, this is to avoid hectic behaviour
- childNode.clearVelocity();
+ // create bezier nodes for smooth curves if needed
+ this._createBezierNodes(newEdges);
- // the mass has altered, preservation of energy dictates the velocity to be updated
- parentNode.updateVelocity(massBefore);
- // restart the simulation to reorganise all nodes
- this.moving = true;
-};
+ // set ID to undefined so no duplicates arise
+ clusterNodeProperties.id = undefined;
-/**
- * This adds an edge from the childNode to the contained edges of the parent node
- *
- * @param parentNode | Node object
- * @param childNode | Node object
- * @param edge | Edge object
- * @private
- */
-exports._addToContainedEdges = function(parentNode, childNode, edge) {
- // create an array object if it does not yet exist for this childNode
- if (parentNode.containedEdges[childNode.id] === undefined) {
- parentNode.containedEdges[childNode.id] = []
+ // wrap up
+ if (doNotUpdateCalculationNodes !== true) {
+ this._wrapUp();
}
- // add this edge to the list
- parentNode.containedEdges[childNode.id].push(edge);
-
- // remove the edge from the global edges object
- delete this.edges[edge.id];
+}
- // remove the edge from the parent object
- for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
- if (parentNode.dynamicEdges[i].id == edge.id) {
- parentNode.dynamicEdges.splice(i,1);
- break;
- }
- }
-};
/**
- * This function connects an edge that was connected to a child node to the parent node.
- * It keeps track of which nodes it has been connected to with the originalId array.
- *
- * @param {Node} parentNode | Node object
- * @param {Node} childNode | Node object
- * @param {Edge} edge | Edge object
- * @private
+ * Check if a node is a cluster.
+ * @param nodeId
+ * @returns {*}
*/
-exports._connectEdgeToCluster = function(parentNode, childNode, edge) {
- // handle circular edges
- if (edge.toId == edge.fromId) {
- this._addToContainedEdges(parentNode, childNode, edge);
+exports.isCluster = function(nodeId) {
+ if (this.nodes[nodeId] !== undefined) {
+ return this.nodes[nodeId].isCluster;
}
else {
- if (edge.toId == childNode.id) { // edge connected to other node on the "to" side
- edge.originalToId.push(childNode.id);
- edge.to = parentNode;
- edge.toId = parentNode.id;
- }
- else { // edge connected to other node with the "from" side
- edge.originalFromId.push(childNode.id);
- edge.from = parentNode;
- edge.fromId = parentNode.id;
- }
-
- this._addToReroutedEdges(parentNode,childNode,edge);
+ console.log("Node does not exist.")
+ return false;
}
-};
+}
/**
- * If a node is connected to itself, a circular edge is drawn. When clustering we want to contain
- * these edges inside of the cluster.
- *
- * @param parentNode
- * @param childNode
+ * get the position of the cluster node based on what's inside
+ * @param {object} childNodesObj | object with node objects, id as keys
+ * @returns {{x: number, y: number}}
* @private
*/
-exports._containCircularEdgesFromNode = function(parentNode, childNode) {
- // manage all the edges connected to the child and parent nodes
- for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
- var edge = parentNode.dynamicEdges[i];
- // handle circular edges
- if (edge.toId == edge.fromId) {
- this._addToContainedEdges(parentNode, childNode, edge);
- }
+exports._getClusterPosition = function(childNodesObj) {
+ var childKeys = Object.keys(childNodesObj);
+ var minX = childNodesObj[childKeys[0]].x;
+ var maxX = childNodesObj[childKeys[0]].x;
+ var minY = childNodesObj[childKeys[0]].y;
+ var maxY = childNodesObj[childKeys[0]].y;
+ var node;
+ for (var i = 0; i < childKeys.lenght; i++) {
+ node = childNodesObj[childKeys[0]];
+ minX = node.x < minX ? node.x : minX;
+ maxX = node.x > maxX ? node.x : maxX;
+ minY = node.y < minY ? node.y : minY;
+ maxY = node.y > maxY ? node.y : maxY;
}
-};
+ return {x: 0.5*(minX + maxX), y: 0.5*(minY + maxY)};
+}
/**
- * This adds an edge from the childNode to the rerouted edges of the parent node
- *
- * @param parentNode | Node object
- * @param childNode | Node object
- * @param edge | Edge object
- * @private
+ * Open a cluster by calling this function.
+ * @param {String} clusterNodeId | the ID of the cluster node
+ * @param {Boolean} doNotUpdateCalculationNodes | wrap up afterwards if not true
*/
-exports._addToReroutedEdges = function(parentNode, childNode, edge) {
- // create an array object if it does not yet exist for this childNode
- // we store the edge in the rerouted edges so we can restore it when the cluster pops open
- if (!(parentNode.reroutedEdges.hasOwnProperty(childNode.id))) {
- parentNode.reroutedEdges[childNode.id] = [];
- }
- parentNode.reroutedEdges[childNode.id].push(edge);
-
- // this edge becomes part of the dynamicEdges of the cluster node
- parentNode.dynamicEdges.push(edge);
- };
+exports.openCluster = function(clusterNodeId, doNotUpdateCalculationNodes) {
+ // kill conditions
+ if (clusterNodeId === undefined) {throw new Error("No clusterNodeId supplied to openCluster.");}
+ if (this.nodes[clusterNodeId] === undefined) {throw new Error("The clusterNodeId supplied to openCluster does not exist.");}
+ if (this.nodes[clusterNodeId].containedNodes === undefined) {console.log("The node:" + clusterNodeId + " is not a cluster."); return};
+ var node = this.nodes[clusterNodeId];
+ var containedNodes = node.containedNodes;
+ var containedEdges = node.containedEdges;
+ // release nodes
+ for (var nodeId in containedNodes) {
+ if (containedNodes.hasOwnProperty(nodeId)) {
+ this.nodes[nodeId] = containedNodes[nodeId];
+ // inherit position
+ this.nodes[nodeId].x = node.x;
+ this.nodes[nodeId].y = node.y;
-/**
- * This function connects an edge that was connected to a cluster node back to the child node.
- *
- * @param parentNode | Node object
- * @param childNode | Node object
- * @private
- */
-exports._connectEdgeBackToChild = function(parentNode, childNode) {
- if (parentNode.reroutedEdges.hasOwnProperty(childNode.id)) {
- for (var i = 0; i < parentNode.reroutedEdges[childNode.id].length; i++) {
- var edge = parentNode.reroutedEdges[childNode.id][i];
- if (edge.originalFromId[edge.originalFromId.length-1] == childNode.id) {
- edge.originalFromId.pop();
- edge.fromId = childNode.id;
- edge.from = childNode;
- }
- else {
- edge.originalToId.pop();
- edge.toId = childNode.id;
- edge.to = childNode;
- }
+ // inherit speed
+ this.nodes[nodeId].vx = node.vx;
+ this.nodes[nodeId].vy = node.vy;
- // append this edge to the list of edges connecting to the childnode
- childNode.dynamicEdges.push(edge);
+ delete this.clusteredNodes[nodeId];
+ }
+ }
- // remove the edge from the parent object
- for (var j = 0; j < parentNode.dynamicEdges.length; j++) {
- if (parentNode.dynamicEdges[j].id == edge.id) {
- parentNode.dynamicEdges.splice(j,1);
- break;
+ // release edges
+ for (var edgeId in containedEdges) {
+ if (containedEdges.hasOwnProperty(edgeId)) {
+ this.edges[edgeId] = containedEdges[edgeId];
+ this.edges[edgeId].connect();
+ var edge = this.edges[edgeId];
+ if (edge.connected === false) {
+ if (this.clusteredNodes[edge.fromId] !== undefined) {
+ this._connectEdge(edge, edge.fromId, true);
+ }
+ if (this.clusteredNodes[edge.toId] !== undefined) {
+ this._connectEdge(edge, edge.toId, false);
}
}
}
- // remove the entry from the rerouted edges
- delete parentNode.reroutedEdges[childNode.id];
}
-};
+ this._createBezierNodes(containedEdges);
+ var edgeIds = [];
+ for (var i = 0; i < node.edges.length; i++) {
+ edgeIds.push(node.edges[i].id);
+ }
-/**
- * When loops are clustered, an edge can be both in the rerouted array and the contained array.
- * This function is called last to verify that all edges in dynamicEdges are in fact connected to the
- * parentNode
- *
- * @param parentNode | Node object
- * @private
- */
-exports._validateEdges = function(parentNode) {
- var dynamicEdges = []
- for (var i = 0; i < parentNode.dynamicEdges.length; i++) {
- var edge = parentNode.dynamicEdges[i];
- if (parentNode.id == edge.toId || parentNode.id == edge.fromId) {
- dynamicEdges.push(edge);
+ // remove edges in clusterNode
+ for (var i = 0; i < edgeIds.length; i++) {
+ var edge = this.edges[edgeIds[i]];
+ // if the edge should have been connected to a contained node
+ if (edge.fromArray.length > 0 && edge.fromId == clusterNodeId) {
+ // the node in the from array was contained in the cluster
+ if (this.nodes[edge.fromArray[0].id] !== undefined) {
+ this._connectEdge(edge, edge.fromArray[0].id, true);
+ }
+ }
+ else if (edge.toArray.length > 0 && edge.toId == clusterNodeId) {
+ // the node in the to array was contained in the cluster
+ if (this.nodes[edge.toArray[0].id] !== undefined) {
+ this._connectEdge(edge, edge.toArray[0].id, false);
+ }
+ }
+ else {
+ var edgeId = edgeIds[i];
+ var viaId = this.edges[edgeId].via.id;
+ if (viaId) {
+ this.edges[edgeId].via = null
+ delete this.sectors['support']['nodes'][viaId];
+ }
+ // this removes the edge from node.edges, which is why edgeIds is formed
+ this.edges[edgeId].disconnect();
+ delete this.edges[edgeId];
}
}
- parentNode.dynamicEdges = dynamicEdges;
-};
+ // remove clusterNode
+ delete this.nodes[clusterNodeId];
-/**
- * This function released the contained edges back into the global domain and puts them back into the
- * dynamic edges of both parent and child.
- *
- * @param {Node} parentNode |
- * @param {Node} childNode |
- * @private
- */
-exports._releaseContainedEdges = function(parentNode, childNode) {
- for (var i = 0; i < parentNode.containedEdges[childNode.id].length; i++) {
- var edge = parentNode.containedEdges[childNode.id][i];
-
- // put the edge back in the global edges object
- this.edges[edge.id] = edge;
-
- // put the edge back in the dynamic edges of the child and parent
- childNode.dynamicEdges.push(edge);
- parentNode.dynamicEdges.push(edge);
+ if (doNotUpdateCalculationNodes !== true) {
+ this._wrapUp();
}
- // remove the entry from the contained edges
- delete parentNode.containedEdges[childNode.id];
-
-};
-
-
-
-
-// ------------------- UTILITY FUNCTIONS ---------------------------- //
+}
/**
- * This updates the node labels for all nodes (for debugging purposes)
+ * Recalculate navigation nodes, color edges dirty, update nodes list etc.
+ * @private
*/
-exports.updateLabels = function() {
- var nodeId;
- // update node labels
- for (nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- var node = this.nodes[nodeId];
- if (node.clusterSize > 1) {
- node.label = "[".concat(String(node.clusterSize),"]");
- }
- }
- }
-
- // update node labels
- for (nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- node = this.nodes[nodeId];
- if (node.clusterSize == 1) {
- if (node.originalLabel !== undefined) {
- node.label = node.originalLabel;
- }
- else {
- node.label = String(node.id);
- }
- }
- }
+exports._wrapUp = function() {
+ this._updateNodeIndexList();
+ this._updateCalculationNodes();
+ this._markAllEdgesAsDirty();
+ if (this.initializing !== true) {
+ this.moving = true;
+ this.start();
}
-
-// /* Debug Override */
-// for (nodeId in this.nodes) {
-// if (this.nodes.hasOwnProperty(nodeId)) {
-// node = this.nodes[nodeId];
-// node.label = String(node.clusterSize + ":" + node.dynamicEdges.length);
-// }
-// }
-
-};
+}
/**
- * We want to keep the cluster level distribution rather small. This means we do not want unclustered nodes
- * if the rest of the nodes are already a few cluster levels in.
- * To fix this we use this function. It determines the min and max cluster level and sends nodes that have not
- * clustered enough to the clusterToSmallestNeighbours function.
+ * Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to
+ * is currently residing in cluster B
+ * @param edge
+ * @param nodeId
+ * @param from
+ * @private
*/
-exports.normalizeClusterLevels = function() {
- var maxLevel = 0;
- var minLevel = 1e9;
- var clusterLevel = 0;
- var nodeId;
-
- // we loop over all nodes in the list
- for (nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- clusterLevel = this.nodes[nodeId].clusterSessions.length;
- if (maxLevel < clusterLevel) {maxLevel = clusterLevel;}
- if (minLevel > clusterLevel) {minLevel = clusterLevel;}
- }
+exports._connectEdge = function(edge, nodeId, from) {
+ var clusterStack = this._getClusterStack(nodeId);
+ if (from == true) {
+ edge.from = clusterStack[clusterStack.length - 1];
+ edge.fromId = clusterStack[clusterStack.length - 1].id;
+ clusterStack.pop()
+ edge.fromArray = clusterStack;
}
-
- if (maxLevel - minLevel > this.constants.clustering.clusterLevelDifference) {
- var amountOfNodes = this.nodeIndices.length;
- var targetLevel = maxLevel - this.constants.clustering.clusterLevelDifference;
- // we loop over all nodes in the list
- for (nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- if (this.nodes[nodeId].clusterSessions.length < targetLevel) {
- this._clusterToSmallestNeighbour(this.nodes[nodeId]);
- }
- }
- }
- this._updateNodeIndexList();
- // if a cluster was formed, we increase the clusterSession
- if (this.nodeIndices.length != amountOfNodes) {
- this.clusterSession += 1;
- }
+ else {
+ edge.to = clusterStack[clusterStack.length - 1];
+ edge.toId = clusterStack[clusterStack.length - 1].id;
+ clusterStack.pop();
+ edge.toArray = clusterStack;
}
-};
-
-
+ edge.connect();
+}
/**
- * This function determines if the cluster we want to decluster is in the active area
- * this means around the zoom center
- *
- * @param {Node} node
- * @returns {boolean}
+ * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node
+ * @param nodeId
+ * @returns {Array}
* @private
*/
-exports._nodeInActiveArea = function(node) {
- return (
- Math.abs(node.x - this.areaCenter.x) <= this.constants.clustering.activeAreaBoxSize/this.scale
- &&
- Math.abs(node.y - this.areaCenter.y) <= this.constants.clustering.activeAreaBoxSize/this.scale
- )
-};
+exports._getClusterStack = function(nodeId) {
+ var stack = [];
+ var max = 100;
+ var counter = 0;
+
+ while (this.clusteredNodes[nodeId] !== undefined && counter < max) {
+ stack.push(this.clusteredNodes[nodeId].node);
+ nodeId = this.clusteredNodes[nodeId].clusterId;
+ counter++;
+ }
+ stack.push(this.nodes[nodeId]);
+ return stack;
+}
/**
- * This is an adaptation of the original repositioning function. This is called if the system is clustered initially
- * It puts large clusters away from the center and randomizes the order.
- *
+ * Get the Id the node is connected to
+ * @param edge
+ * @param nodeId
+ * @returns {*}
+ * @private
*/
-exports.repositionNodes = function() {
- for (var i = 0; i < this.nodeIndices.length; i++) {
- var node = this.nodes[this.nodeIndices[i]];
- if ((node.xFixed == false || node.yFixed == false)) {
- var radius = 10 * 0.1*this.nodeIndices.length * Math.min(100,node.options.mass);
- var angle = 2 * Math.PI * Math.random();
- if (node.xFixed == false) {node.x = radius * Math.cos(angle);}
- if (node.yFixed == false) {node.y = radius * Math.sin(angle);}
- this._repositionBezierNodes(node);
- }
+exports._getConnectedId = function(edge, nodeId) {
+ if (edge.toId != nodeId) {
+ return edge.toId;
}
-};
-
+ else if (edge.fromId != nodeId) {
+ return edge.fromId;
+ }
+ else {
+ return edge.fromId;
+ }
+}
/**
* We determine how many connections denote an important hub.
@@ -1058,72 +602,27 @@ exports._getHubSize = function() {
var largestHub = 0;
for (var i = 0; i < this.nodeIndices.length; i++) {
-
var node = this.nodes[this.nodeIndices[i]];
- if (node.dynamicEdges.length > largestHub) {
- largestHub = node.dynamicEdges.length;
+ if (node.edges.length > largestHub) {
+ largestHub = node.edges.length;
}
- average += node.dynamicEdges.length;
- averageSquared += Math.pow(node.dynamicEdges.length,2);
+ average += node.edges.length;
+ averageSquared += Math.pow(node.edges.length,2);
hubCounter += 1;
}
average = average / hubCounter;
averageSquared = averageSquared / hubCounter;
var variance = averageSquared - Math.pow(average,2);
-
var standardDeviation = Math.sqrt(variance);
- this.hubThreshold = Math.floor(average + 2*standardDeviation);
+ var hubThreshold = Math.floor(average + 2*standardDeviation);
// always have at least one to cluster
- if (this.hubThreshold > largestHub) {
- this.hubThreshold = largestHub;
+ if (hubThreshold > largestHub) {
+ hubThreshold = largestHub;
}
-// console.log("average",average,"averageSQ",averageSquared,"var",variance,"std",standardDeviation);
-// console.log("hubThreshold:",this.hubThreshold);
-};
-
-
-/**
- * We reduce the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
- * with this amount we can cluster specifically on these chains.
- *
- * @param {Number} fraction | between 0 and 1, the percentage of chains to reduce
- * @private
- */
-exports._reduceAmountOfChains = function(fraction) {
- this.hubThreshold = 2;
- var reduceAmount = Math.floor(this.nodeIndices.length * fraction);
- for (var nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- if (this.nodes[nodeId].dynamicEdges.length == 2) {
- if (reduceAmount > 0) {
- this._formClusterFromHub(this.nodes[nodeId],true,true,1);
- reduceAmount -= 1;
- }
- }
- }
- }
+ return hubThreshold;
};
-/**
- * We get the amount of "extension nodes" or chains. These are not quickly clustered with the outliers and hubs methods
- * with this amount we can cluster specifically on these chains.
- *
- * @private
- */
-exports._getChainFraction = function() {
- var chains = 0;
- var total = 0;
- for (var nodeId in this.nodes) {
- if (this.nodes.hasOwnProperty(nodeId)) {
- if (this.nodes[nodeId].dynamicEdges.length == 2) {
- chains += 1;
- }
- total += 1;
- }
- }
- return chains/total;
-};
diff --git a/lib/network/mixins/HierarchicalLayoutMixin.js b/lib/network/mixins/HierarchicalLayoutMixin.js
index 4d4cd54e..c0fe8d50 100644
--- a/lib/network/mixins/HierarchicalLayoutMixin.js
+++ b/lib/network/mixins/HierarchicalLayoutMixin.js
@@ -64,11 +64,8 @@ exports._setupHierarchicalLayout = function() {
// check the distribution of the nodes per level.
var distribution = this._getDistribution();
- // place the nodes on the canvas. This also stablilizes the system.
+ // place the nodes on the canvas. This also stablilizes the system. Redraw in started automatically after stabilize.
this._placeNodesByHierarchy(distribution);
-
- // start the simulation.
- this.start();
}
}
};
diff --git a/lib/network/mixins/MixinLoader.js b/lib/network/mixins/MixinLoader.js
index cd7e96cf..afcf038b 100644
--- a/lib/network/mixins/MixinLoader.js
+++ b/lib/network/mixins/MixinLoader.js
@@ -59,8 +59,7 @@ exports._loadPhysicsSystem = function () {
* @private
*/
exports._loadClusterSystem = function () {
- this.clusterSession = 0;
- this.hubThreshold = 5;
+ this.clusteredNodes = {};
this._loadMixin(ClusterMixin);
};
diff --git a/lib/network/mixins/SelectionMixin.js b/lib/network/mixins/SelectionMixin.js
index b2e87ee2..7bbd0c7a 100644
--- a/lib/network/mixins/SelectionMixin.js
+++ b/lib/network/mixins/SelectionMixin.js
@@ -355,8 +355,8 @@ exports._clusterInSelection = function() {
* @private
*/
exports._selectConnectedEdges = function(node) {
- for (var i = 0; i < node.dynamicEdges.length; i++) {
- var edge = node.dynamicEdges[i];
+ for (var i = 0; i < node.edges.length; i++) {
+ var edge = node.edges[i];
edge.select();
this._addToSelection(edge);
}
@@ -369,8 +369,8 @@ exports._selectConnectedEdges = function(node) {
* @private
*/
exports._hoverConnectedEdges = function(node) {
- for (var i = 0; i < node.dynamicEdges.length; i++) {
- var edge = node.dynamicEdges[i];
+ for (var i = 0; i < node.edges.length; i++) {
+ var edge = node.edges[i];
edge.hover = true;
this._addToHover(edge);
}
@@ -384,8 +384,8 @@ exports._hoverConnectedEdges = function(node) {
* @private
*/
exports._unselectConnectedEdges = function(node) {
- for (var i = 0; i < node.dynamicEdges.length; i++) {
- var edge = node.dynamicEdges[i];
+ for (var i = 0; i < node.edges.length; i++) {
+ var edge = node.edges[i];
edge.unselect();
this._removeFromSelection(edge);
}
@@ -512,7 +512,7 @@ exports._handleTap = function(pointer) {
canvas: {x: this._XconvertDOMtoCanvas(pointer.x), y: this._YconvertDOMtoCanvas(pointer.y)}
}
this.emit("click", properties);
- this._redraw();
+ this._requestRedraw();
};
@@ -556,7 +556,7 @@ exports._handleOnHold = function(pointer) {
this._selectObject(edge,true);
}
}
- this._redraw();
+ this._requestRedraw();
};
diff --git a/lib/network/mixins/physics/BarnesHutMixin.js b/lib/network/mixins/physics/BarnesHutMixin.js
index b70f474c..b8ae1969 100644
--- a/lib/network/mixins/physics/BarnesHutMixin.js
+++ b/lib/network/mixins/physics/BarnesHutMixin.js
@@ -19,7 +19,7 @@ exports._calculateNodeForces = function() {
for (var i = 0; i < nodeCount; i++) {
node = nodes[nodeIndices[i]];
if (node.options.mass > 0) {
- // starting with root is irrelevant, it never passes the BarnesHut condition
+ // starting with root is irrelevant, it never passes the BarnesHutSolver condition
this._getForceContribution(barnesHutTree.root.children.NW,node);
this._getForceContribution(barnesHutTree.root.children.NE,node);
this._getForceContribution(barnesHutTree.root.children.SW,node);
@@ -48,7 +48,7 @@ exports._getForceContribution = function(parentBranch,node) {
dy = parentBranch.centerOfMass.y - node.y;
distance = Math.sqrt(dx * dx + dy * dy);
- // BarnesHut condition
+ // BarnesHutSolver condition
// original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed
// calcSize = 1/s --> d * 1/s > 1/theta = passed
if (distance * parentBranch.calcSize > this.constants.physics.barnesHut.thetaInverted) {
diff --git a/lib/network/mixins/physics/HierarchialRepulsionMixin.js b/lib/network/mixins/physics/HierarchialRepulsionMixin.js
index 774b4257..5797e1e3 100644
--- a/lib/network/mixins/physics/HierarchialRepulsionMixin.js
+++ b/lib/network/mixins/physics/HierarchialRepulsionMixin.js
@@ -81,7 +81,7 @@ exports._calculateHierarchicalSpringForces = function () {
for (edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
edge = edges[edgeId];
- if (edge.connected) {
+ if (edge.connected === true) {
// only calculate forces if nodes are in the same sector
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
edgeLength = edge.physics.springLength;
diff --git a/lib/network/mixins/physics/PhysicsMixin.js b/lib/network/mixins/physics/PhysicsMixin.js
index c17cacf7..c5ffd48d 100644
--- a/lib/network/mixins/physics/PhysicsMixin.js
+++ b/lib/network/mixins/physics/PhysicsMixin.js
@@ -71,11 +71,6 @@ exports._initializeForceCalculation = function () {
this.nodes[this.nodeIndices[0]]._setForce(0, 0);
}
else {
- // if there are too many nodes on screen, we cluster without repositioning
- if (this.nodeIndices.length > this.constants.clustering.clusterThreshold && this.constants.clustering.enabled == true) {
- this.clusterToFit(this.constants.clustering.reduceToNodes, false);
- }
-
// we now start the force calculation
this._calculateForces();
}
@@ -202,12 +197,10 @@ exports._calculateSpringForces = function () {
for (edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
edge = edges[edgeId];
- if (edge.connected) {
+ if (edge.connected === true) {
// only calculate forces if nodes are in the same sector
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
edgeLength = edge.physics.springLength;
- // this implies that the edges between big clusters are longer
- edgeLength += (edge.to.clusterSize + edge.from.clusterSize - 2) * this.constants.clustering.edgeGrowth;
dx = (edge.from.x - edge.to.x);
dy = (edge.from.y - edge.to.y);
@@ -242,14 +235,14 @@ exports._calculateSpringForces = function () {
* @private
*/
exports._calculateSpringForcesWithSupport = function () {
- var edgeLength, edge, edgeId, combinedClusterSize;
+ var edgeLength, edge, edgeId;
var edges = this.edges;
// forces caused by the edges, modelled as springs
for (edgeId in edges) {
if (edges.hasOwnProperty(edgeId)) {
edge = edges[edgeId];
- if (edge.connected) {
+ if (edge.connected === true) {
// only calculate forces if nodes are in the same sector
if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
if (edge.via != null) {
@@ -259,10 +252,6 @@ exports._calculateSpringForcesWithSupport = function () {
edgeLength = edge.physics.springLength;
- combinedClusterSize = node1.clusterSize + node3.clusterSize - 2;
-
- // this implies that the edges between big clusters are longer
- edgeLength += combinedClusterSize * this.constants.clustering.edgeGrowth;
this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
}
diff --git a/lib/network/mixins/physics/RepulsionMixin.js b/lib/network/mixins/physics/RepulsionMixin.js
index 4b3ef6ac..32e4ac2d 100644
--- a/lib/network/mixins/physics/RepulsionMixin.js
+++ b/lib/network/mixins/physics/RepulsionMixin.js
@@ -31,7 +31,7 @@ exports._calculateNodeForces = function () {
dy = node2.y - node1.y;
distance = Math.sqrt(dx * dx + dy * dy);
- // same condition as BarnesHut, making sure nodes are never 100% overlapping.
+ // same condition as BarnesHutSolver, making sure nodes are never 100% overlapping.
if (distance == 0) {
distance = 0.1*Math.random();
dx = distance;
diff --git a/lib/network/modules/ClusterEngine.js b/lib/network/modules/ClusterEngine.js
new file mode 100644
index 00000000..76dd3acf
--- /dev/null
+++ b/lib/network/modules/ClusterEngine.js
@@ -0,0 +1,647 @@
+/**
+ * Created by Alex on 2/20/2015.
+ */
+
+var Node = require('../Node');
+var Edge = require('../Edge');
+var util = require('../../util');
+
+function ClusterEngine(data,options) {
+ this.nodes = data.nodes;
+ this.edges = data.edges;
+ this.nodeIndices = data.nodeIndices;
+ this.emitter = data.emitter;
+ this.clusteredNodes = {};
+}
+
+
+/**
+ *
+ * @param hubsize
+ * @param options
+ */
+ClusterEngine.prototype.clusterByConnectionCount = function(hubsize, options) {
+ if (hubsize === undefined) {
+ hubsize = this._getHubSize();
+ }
+ else if (tyepof(hubsize) == "object") {
+ options = this._checkOptions(hubsize);
+ hubsize = this._getHubSize();
+ }
+
+ var nodesToCluster = [];
+ for (var i = 0; i < this.nodeIndices.length; i++) {
+ var node = this.nodes[this.nodeIndices[i]];
+ if (node.edges.length >= hubsize) {
+ nodesToCluster.push(node.id);
+ }
+ }
+
+ for (var i = 0; i < nodesToCluster.length; i++) {
+ var node = this.nodes[nodesToCluster[i]];
+ this.clusterByConnection(node,options,{},{},true);
+ }
+ this.emitter.emit('dataChanged');
+}
+
+
+/**
+ * loop over all nodes, check if they adhere to the condition and cluster if needed.
+ * @param options
+ * @param doNotUpdateCalculationNodes
+ */
+ClusterEngine.prototype.clusterByNodeData = function(options, doNotUpdateCalculationNodes) {
+ if (options === undefined) {throw new Error("Cannot call clusterByNodeData without options.");}
+ if (options.joinCondition === undefined) {throw new Error("Cannot call clusterByNodeData without a joinCondition function in the options.");}
+
+ // check if the options object is fine, append if needed
+ options = this._checkOptions(options);
+
+ var childNodesObj = {};
+ var childEdgesObj = {}
+
+ // collect the nodes that will be in the cluster
+ for (var i = 0; i < this.nodeIndices.length; i++) {
+ var nodeId = this.nodeIndices[i];
+ var clonedOptions = this._cloneOptions(nodeId);
+ if (options.joinCondition(clonedOptions) == true) {
+ childNodesObj[nodeId] = this.nodes[nodeId];
+ }
+ }
+
+ this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
+}
+
+
+/**
+ * Cluster all nodes in the network that have only 1 edge
+ * @param options
+ * @param doNotUpdateCalculationNodes
+ */
+ClusterEngine.prototype.clusterOutliers = function(options, doNotUpdateCalculationNodes) {
+ options = this._checkOptions(options);
+
+ var clusters = []
+
+ // collect the nodes that will be in the cluster
+ for (var i = 0; i < this.nodeIndices.length; i++) {
+ var childNodesObj = {};
+ var childEdgesObj = {};
+ var nodeId = this.nodeIndices[i];
+ if (this.nodes[nodeId].edges.length == 1) {
+ var edge = this.nodes[nodeId].edges[0];
+ var childNodeId = this._getConnectedId(edge, nodeId);
+ if (childNodeId != nodeId) {
+ if (options.joinCondition === undefined) {
+ childNodesObj[nodeId] = this.nodes[nodeId];
+ childNodesObj[childNodeId] = this.nodes[childNodeId];
+ }
+ else {
+ var clonedOptions = this._cloneOptions(nodeId);
+ if (options.joinCondition(clonedOptions) == true) {
+ childNodesObj[nodeId] = this.nodes[nodeId];
+ }
+ clonedOptions = this._cloneOptions(childNodeId);
+ if (options.joinCondition(clonedOptions) == true) {
+ childNodesObj[childNodeId] = this.nodes[childNodeId];
+ }
+ }
+ clusters.push({nodes:childNodesObj, edges:childEdgesObj})
+ }
+ }
+ }
+
+ for (var i = 0; i < clusters.length; i++) {
+ this._cluster(clusters[i].nodes, clusters[i].edges, options, true)
+ }
+
+ if (doNotUpdateCalculationNodes !== true) {
+ this.emitter.emit('dataChanged');
+ }
+}
+
+/**
+ *
+ * @param nodeId
+ * @param options
+ * @param doNotUpdateCalculationNodes
+ */
+ClusterEngine.prototype.clusterByConnection = function(nodeId, options, doNotUpdateCalculationNodes) {
+ // kill conditions
+ if (nodeId === undefined) {throw new Error("No nodeId supplied to clusterByConnection!");}
+ if (this.nodes[nodeId] === undefined) {throw new Error("The nodeId given to clusterByConnection does not exist!");}
+
+ var node = this.nodes[nodeId];
+ options = this._checkOptions(options, node);
+ if (options.clusterNodeProperties.x === undefined) {options.clusterNodeProperties.x = node.x; options.clusterNodeProperties.allowedToMoveX = !node.xFixed;}
+ if (options.clusterNodeProperties.y === undefined) {options.clusterNodeProperties.y = node.y; options.clusterNodeProperties.allowedToMoveY = !node.yFixed;}
+
+ var childNodesObj = {};
+ var childEdgesObj = {}
+ var parentNodeId = node.id;
+ var parentClonedOptions = this._cloneOptions(parentNodeId);
+ childNodesObj[parentNodeId] = node;
+
+ // collect the nodes that will be in the cluster
+ for (var i = 0; i < node.edges.length; i++) {
+ var edge = node.edges[i];
+ var childNodeId = this._getConnectedId(edge, parentNodeId);
+
+ if (childNodeId !== parentNodeId) {
+ if (options.joinCondition === undefined) {
+ childEdgesObj[edge.id] = edge;
+ childNodesObj[childNodeId] = this.nodes[childNodeId];
+ }
+ else {
+ // clone the options and insert some additional parameters that could be interesting.
+ var childClonedOptions = this._cloneOptions(childNodeId);
+ if (options.joinCondition(parentClonedOptions, childClonedOptions) == true) {
+ childEdgesObj[edge.id] = edge;
+ childNodesObj[childNodeId] = this.nodes[childNodeId];
+ }
+ }
+ }
+ else {
+ childEdgesObj[edge.id] = edge;
+ }
+ }
+
+ this._cluster(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes);
+}
+
+
+/**
+ * This returns a clone of the options or properties of the edge or node to be used for construction of new edges or check functions for new nodes.
+ * @param objId
+ * @param type
+ * @returns {{}}
+ * @private
+ */
+ClusterEngine.prototype._cloneOptions = function(objId, type) {
+ var clonedOptions = {};
+ if (type === undefined || type == 'node') {
+ util.deepExtend(clonedOptions, this.nodes[objId].options, true);
+ util.deepExtend(clonedOptions, this.nodes[objId].properties, true);
+ clonedOptions.amountOfConnections = this.nodes[objId].edges.length;
+ }
+ else {
+ util.deepExtend(clonedOptions, this.edges[objId].properties, true);
+ }
+ return clonedOptions;
+}
+
+
+/**
+ * This function creates the edges that will be attached to the cluster.
+ *
+ * @param childNodesObj
+ * @param childEdgesObj
+ * @param newEdges
+ * @param options
+ * @private
+ */
+ClusterEngine.prototype._createClusterEdges = function (childNodesObj, childEdgesObj, newEdges, options) {
+ var edge, childNodeId, childNode;
+
+ var childKeys = Object.keys(childNodesObj);
+ for (var i = 0; i < childKeys.length; i++) {
+ childNodeId = childKeys[i];
+ childNode = childNodesObj[childNodeId];
+
+ // mark all edges for removal from global and construct new edges from the cluster to others
+ for (var j = 0; j < childNode.edges.length; j++) {
+ edge = childNode.edges[j];
+ childEdgesObj[edge.id] = edge;
+
+ var otherNodeId = edge.toId;
+ var otherOnTo = true;
+ if (edge.toId != childNodeId) {
+ otherNodeId = edge.toId;
+ otherOnTo = true;
+ }
+ else if (edge.fromId != childNodeId) {
+ otherNodeId = edge.fromId;
+ otherOnTo = false;
+ }
+
+ if (childNodesObj[otherNodeId] === undefined) {
+ var clonedOptions = this._cloneOptions(edge.id, 'edge');
+ util.deepExtend(clonedOptions, options.clusterEdgeProperties);
+ // avoid forcing the default color on edges that inherit color
+ if (edge.properties.color === undefined) {
+ delete clonedOptions.color;
+ }
+
+ if (otherOnTo === true) {
+ clonedOptions.from = options.clusterNodeProperties.id;
+ clonedOptions.to = otherNodeId;
+ }
+ else {
+ clonedOptions.from = otherNodeId;
+ clonedOptions.to = options.clusterNodeProperties.id;
+ }
+ clonedOptions.id = 'clusterEdge:' + util.randomUUID();
+ newEdges.push(new Edge(clonedOptions,this,this.constants))
+ }
+ }
+ }
+}
+
+
+/**
+ * This function checks the options that can be supplied to the different cluster functions
+ * for certain fields and inserts defaults if needed
+ * @param options
+ * @returns {*}
+ * @private
+ */
+ClusterEngine.prototype._checkOptions = function(options) {
+ if (options === undefined) {options = {};}
+ if (options.clusterEdgeProperties === undefined) {options.clusterEdgeProperties = {};}
+ if (options.clusterNodeProperties === undefined) {options.clusterNodeProperties = {};}
+
+ return options;
+}
+
+/**
+ *
+ * @param {Object} childNodesObj | object with node objects, id as keys, same as childNodes except it also contains a source node
+ * @param {Object} childEdgesObj | object with edge objects, id as keys
+ * @param {Array} options | object with {clusterNodeProperties, clusterEdgeProperties, processProperties}
+ * @param {Boolean} doNotUpdateCalculationNodes | when true, do not wrap up
+ * @private
+ */
+ClusterEngine.prototype._cluster = function(childNodesObj, childEdgesObj, options, doNotUpdateCalculationNodes) {
+ // kill condition: no children so cant cluster
+ if (Object.keys(childNodesObj).length == 0) {return;}
+
+ // check if we have an unique id;
+ if (options.clusterNodeProperties.id === undefined) {options.clusterNodeProperties.id = 'cluster:' + util.randomUUID();}
+ var clusterId = options.clusterNodeProperties.id;
+
+ // create the new edges that will connect to the cluster
+ var newEdges = [];
+ this._createClusterEdges(childNodesObj, childEdgesObj, newEdges, options);
+
+ // construct the clusterNodeProperties
+ var clusterNodeProperties = options.clusterNodeProperties;
+ if (options.processProperties !== undefined) {
+ // get the childNode options
+ var childNodesOptions = [];
+ for (var nodeId in childNodesObj) {
+ var clonedOptions = this._cloneOptions(nodeId);
+ childNodesOptions.push(clonedOptions);
+ }
+
+ // get clusterproperties based on childNodes
+ var childEdgesOptions = [];
+ for (var edgeId in childEdgesObj) {
+ var clonedOptions = this._cloneOptions(edgeId, 'edge');
+ childEdgesOptions.push(clonedOptions);
+ }
+
+ clusterNodeProperties = options.processProperties(clusterNodeProperties, childNodesOptions, childEdgesOptions);
+ if (!clusterNodeProperties) {
+ throw new Error("The processClusterProperties function does not return properties!");
+ }
+ }
+ if (clusterNodeProperties.label === undefined) {
+ clusterNodeProperties.label = 'cluster';
+ }
+
+
+ // give the clusterNode a postion if it does not have one.
+ var pos = undefined
+ if (clusterNodeProperties.x === undefined) {
+ pos = this._getClusterPosition(childNodesObj);
+ clusterNodeProperties.x = pos.x;
+ clusterNodeProperties.allowedToMoveX = true;
+ }
+ if (clusterNodeProperties.x === undefined) {
+ if (pos === undefined) {
+ pos = this._getClusterPosition(childNodesObj);
+ }
+ clusterNodeProperties.y = pos.y;
+ clusterNodeProperties.allowedToMoveY = true;
+ }
+
+
+ // force the ID to remain the same
+ clusterNodeProperties.id = clusterId;
+
+
+ // create the clusterNode
+ var clusterNode = new Node(clusterNodeProperties, this.images, this.groups, this.constants);
+ clusterNode.isCluster = true;
+ clusterNode.containedNodes = childNodesObj;
+ clusterNode.containedEdges = childEdgesObj;
+
+
+ // delete contained edges from global
+ for (var edgeId in childEdgesObj) {
+ if (childEdgesObj.hasOwnProperty(edgeId)) {
+ if (this.edges[edgeId] !== undefined) {
+ if (this.edges[edgeId].via !== null) {
+ var viaId = this.edges[edgeId].via.id;
+ if (viaId) {
+ this.edges[edgeId].via = null
+ delete this.sectors['support']['nodes'][viaId];
+ }
+ }
+ this.edges[edgeId].disconnect();
+ delete this.edges[edgeId];
+ }
+ }
+ }
+
+
+ // remove contained nodes from global
+ for (var nodeId in childNodesObj) {
+ if (childNodesObj.hasOwnProperty(nodeId)) {
+ this.clusteredNodes[nodeId] = {clusterId:clusterNodeProperties.id, node: this.nodes[nodeId]};
+ delete this.nodes[nodeId];
+ }
+ }
+
+
+ // finally put the cluster node into global
+ this.nodes[clusterNodeProperties.id] = clusterNode;
+
+
+ // push new edges to global
+ for (var i = 0; i < newEdges.length; i++) {
+ this.edges[newEdges[i].id] = newEdges[i];
+ this.edges[newEdges[i].id].connect();
+ }
+
+
+ // create bezier nodes for smooth curves if needed
+ this._createBezierNodes(newEdges);
+
+
+ // set ID to undefined so no duplicates arise
+ clusterNodeProperties.id = undefined;
+
+
+ // wrap up
+ if (doNotUpdateCalculationNodes !== true) {
+ this.emitter.emit('dataChanged');
+ }
+}
+
+
+/**
+ * Check if a node is a cluster.
+ * @param nodeId
+ * @returns {*}
+ */
+ClusterEngine.prototype.isCluster = function(nodeId) {
+ if (this.nodes[nodeId] !== undefined) {
+ return this.nodes[nodeId].isCluster;
+ }
+ else {
+ console.log("Node does not exist.")
+ return false;
+ }
+
+}
+
+/**
+ * get the position of the cluster node based on what's inside
+ * @param {object} childNodesObj | object with node objects, id as keys
+ * @returns {{x: number, y: number}}
+ * @private
+ */
+ClusterEngine.prototype._getClusterPosition = function(childNodesObj) {
+ var childKeys = Object.keys(childNodesObj);
+ var minX = childNodesObj[childKeys[0]].x;
+ var maxX = childNodesObj[childKeys[0]].x;
+ var minY = childNodesObj[childKeys[0]].y;
+ var maxY = childNodesObj[childKeys[0]].y;
+ var node;
+ for (var i = 0; i < childKeys.lenght; i++) {
+ node = childNodesObj[childKeys[0]];
+ minX = node.x < minX ? node.x : minX;
+ maxX = node.x > maxX ? node.x : maxX;
+ minY = node.y < minY ? node.y : minY;
+ maxY = node.y > maxY ? node.y : maxY;
+ }
+ return {x: 0.5*(minX + maxX), y: 0.5*(minY + maxY)};
+}
+
+
+/**
+ * Open a cluster by calling this function.
+ * @param {String} clusterNodeId | the ID of the cluster node
+ * @param {Boolean} doNotUpdateCalculationNodes | wrap up afterwards if not true
+ */
+ClusterEngine.prototype.openCluster = function(clusterNodeId, doNotUpdateCalculationNodes) {
+ // kill conditions
+ if (clusterNodeId === undefined) {throw new Error("No clusterNodeId supplied to openCluster.");}
+ if (this.nodes[clusterNodeId] === undefined) {throw new Error("The clusterNodeId supplied to openCluster does not exist.");}
+ if (this.nodes[clusterNodeId].containedNodes === undefined) {console.log("The node:" + clusterNodeId + " is not a cluster."); return};
+
+ var node = this.nodes[clusterNodeId];
+ var containedNodes = node.containedNodes;
+ var containedEdges = node.containedEdges;
+
+ // release nodes
+ for (var nodeId in containedNodes) {
+ if (containedNodes.hasOwnProperty(nodeId)) {
+ this.nodes[nodeId] = containedNodes[nodeId];
+ // inherit position
+ this.nodes[nodeId].x = node.x;
+ this.nodes[nodeId].y = node.y;
+
+ // inherit speed
+ this.nodes[nodeId].vx = node.vx;
+ this.nodes[nodeId].vy = node.vy;
+
+ delete this.clusteredNodes[nodeId];
+ }
+ }
+
+ // release edges
+ for (var edgeId in containedEdges) {
+ if (containedEdges.hasOwnProperty(edgeId)) {
+ this.edges[edgeId] = containedEdges[edgeId];
+ this.edges[edgeId].connect();
+ var edge = this.edges[edgeId];
+ if (edge.connected === false) {
+ if (this.clusteredNodes[edge.fromId] !== undefined) {
+ this._connectEdge(edge, edge.fromId, true);
+ }
+ if (this.clusteredNodes[edge.toId] !== undefined) {
+ this._connectEdge(edge, edge.toId, false);
+ }
+ }
+ }
+ }
+ this._createBezierNodes(containedEdges);
+
+ var edgeIds = [];
+ for (var i = 0; i < node.edges.length; i++) {
+ edgeIds.push(node.edges[i].id);
+ }
+
+ // remove edges in clusterNode
+ for (var i = 0; i < edgeIds.length; i++) {
+ var edge = this.edges[edgeIds[i]];
+ // if the edge should have been connected to a contained node
+ if (edge.fromArray.length > 0 && edge.fromId == clusterNodeId) {
+ // the node in the from array was contained in the cluster
+ if (this.nodes[edge.fromArray[0].id] !== undefined) {
+ this._connectEdge(edge, edge.fromArray[0].id, true);
+ }
+ }
+ else if (edge.toArray.length > 0 && edge.toId == clusterNodeId) {
+ // the node in the to array was contained in the cluster
+ if (this.nodes[edge.toArray[0].id] !== undefined) {
+ this._connectEdge(edge, edge.toArray[0].id, false);
+ }
+ }
+ else {
+ var edgeId = edgeIds[i];
+ var viaId = this.edges[edgeId].via.id;
+ if (viaId) {
+ this.edges[edgeId].via = null
+ delete this.sectors['support']['nodes'][viaId];
+ }
+ // this removes the edge from node.edges, which is why edgeIds is formed
+ this.edges[edgeId].disconnect();
+ delete this.edges[edgeId];
+ }
+ }
+
+ // remove clusterNode
+ delete this.nodes[clusterNodeId];
+
+ if (doNotUpdateCalculationNodes !== true) {
+ this.emitter.emit('dataChanged');
+ }
+}
+
+
+/**
+ * Recalculate navigation nodes, color edges dirty, update nodes list etc.
+ * @private
+ */
+ClusterEngine.prototype._wrapUp = function() {
+
+ this._updateNodeIndexList();
+ this._updateCalculationNodes();
+ this._markAllEdgesAsDirty();
+ if (this.initializing !== true) {
+ this.moving = true;
+ this.start();
+ }
+}
+
+
+/**
+ * Connect an edge that was previously contained from cluster A to cluster B if the node that it was originally connected to
+ * is currently residing in cluster B
+ * @param edge
+ * @param nodeId
+ * @param from
+ * @private
+ */
+ClusterEngine.prototype._connectEdge = function(edge, nodeId, from) {
+ var clusterStack = this._getClusterStack(nodeId);
+ if (from == true) {
+ edge.from = clusterStack[clusterStack.length - 1];
+ edge.fromId = clusterStack[clusterStack.length - 1].id;
+ clusterStack.pop()
+ edge.fromArray = clusterStack;
+ }
+ else {
+ edge.to = clusterStack[clusterStack.length - 1];
+ edge.toId = clusterStack[clusterStack.length - 1].id;
+ clusterStack.pop();
+ edge.toArray = clusterStack;
+ }
+ edge.connect();
+}
+
+/**
+ * Get the stack clusterId's that a certain node resides in. cluster A -> cluster B -> cluster C -> node
+ * @param nodeId
+ * @returns {Array}
+ * @private
+ */
+ClusterEngine.prototype._getClusterStack = function(nodeId) {
+ var stack = [];
+ var max = 100;
+ var counter = 0;
+
+ while (this.clusteredNodes[nodeId] !== undefined && counter < max) {
+ stack.push(this.clusteredNodes[nodeId].node);
+ nodeId = this.clusteredNodes[nodeId].clusterId;
+ counter++;
+ }
+ stack.push(this.nodes[nodeId]);
+ return stack;
+}
+
+
+/**
+ * Get the Id the node is connected to
+ * @param edge
+ * @param nodeId
+ * @returns {*}
+ * @private
+ */
+ClusterEngine.prototype._getConnectedId = function(edge, nodeId) {
+ if (edge.toId != nodeId) {
+ return edge.toId;
+ }
+ else if (edge.fromId != nodeId) {
+ return edge.fromId;
+ }
+ else {
+ return edge.fromId;
+ }
+}
+
+/**
+ * We determine how many connections denote an important hub.
+ * We take the mean + 2*std as the important hub size. (Assuming a normal distribution of data, ~2.2%)
+ *
+ * @private
+ */
+ClusterEngine.prototype._getHubSize = function() {
+ var average = 0;
+ var averageSquared = 0;
+ var hubCounter = 0;
+ var largestHub = 0;
+
+ for (var i = 0; i < this.nodeIndices.length; i++) {
+ var node = this.nodes[this.nodeIndices[i]];
+ if (node.edges.length > largestHub) {
+ largestHub = node.edges.length;
+ }
+ average += node.edges.length;
+ averageSquared += Math.pow(node.edges.length,2);
+ hubCounter += 1;
+ }
+ average = average / hubCounter;
+ averageSquared = averageSquared / hubCounter;
+
+ var variance = averageSquared - Math.pow(average,2);
+ var standardDeviation = Math.sqrt(variance);
+
+ var hubThreshold = Math.floor(average + 2*standardDeviation);
+
+ // always have at least one to cluster
+ if (hubThreshold > largestHub) {
+ hubThreshold = largestHub;
+ }
+
+ return hubThreshold;
+};
+
+
+
+
+
+module.exports = clusterEngine
\ No newline at end of file
diff --git a/lib/network/modules/PhysicsEngine.js b/lib/network/modules/PhysicsEngine.js
new file mode 100644
index 00000000..5de2daae
--- /dev/null
+++ b/lib/network/modules/PhysicsEngine.js
@@ -0,0 +1,33 @@
+/**
+ * Created by Alex on 2/23/2015.
+ */
+
+var BarnesHut = require("./compontents/BarnesHutSolver")
+var SpringSolver = require("./compontents/SpringSolver")
+var CentralGravitySolver = require("./compontents/CentralGravitySolver")
+
+function PhysicsEngine(body, options) {
+ this.body = body;
+
+ this.nodesSolver = new BarnesHut(body, options);
+ this.edgesSolver = new SpringSolver(body, options);
+ this.gravitySolver = new CentralGravitySolver(body, options);
+}
+
+PhysicsEngine.prototype.calculateField = function () {
+ this.nodesSolver.solve();
+};
+
+PhysicsEngine.prototype.calculateSprings = function () {
+ this.edgesSolver.solve();
+};
+
+PhysicsEngine.prototype.calculateCentralGravity = function () {
+ this.gravitySolver.solve();
+};
+
+PhysicsEngine.prototype.calculate = function () {
+ this.calculateCentralGravity();
+ this.calculateField();
+ this.calculateSprings();
+};
\ No newline at end of file
diff --git a/lib/network/modules/components/BarnesHutSolver.js b/lib/network/modules/components/BarnesHutSolver.js
new file mode 100644
index 00000000..0ba9237f
--- /dev/null
+++ b/lib/network/modules/components/BarnesHutSolver.js
@@ -0,0 +1,409 @@
+/**
+ * Created by Alex on 2/23/2015.
+ */
+
+function BarnesHutSolver(body, options) {
+ this.body = body;
+ this.options = options;
+}
+
+/**
+ * This function calculates the forces the nodes apply on eachother based on a gravitational model.
+ * The Barnes Hut method is used to speed up this N-body simulation.
+ *
+ * @private
+ */
+BarnesHutSolver.prototype.solve = function() {
+ if (this.options.gravitationalConstant != 0) {
+ var node;
+ var nodes = this.body.calculationNodes;
+ var nodeIndices = this.body.calculationNodeIndices;
+ var nodeCount = nodeIndices.length;
+
+ var barnesHutTree = this._formBarnesHutTree(nodes,nodeIndices);
+
+ // place the nodes one by one recursively
+ for (var i = 0; i < nodeCount; i++) {
+ node = nodes[nodeIndices[i]];
+ if (node.options.mass > 0) {
+ // starting with root is irrelevant, it never passes the BarnesHutSolver condition
+ this._getForceContribution(barnesHutTree.root.children.NW,node);
+ this._getForceContribution(barnesHutTree.root.children.NE,node);
+ this._getForceContribution(barnesHutTree.root.children.SW,node);
+ this._getForceContribution(barnesHutTree.root.children.SE,node);
+ }
+ }
+ }
+};
+
+
+/**
+ * This function traverses the barnesHutTree. It checks when it can approximate distant nodes with their center of mass.
+ * If a region contains a single node, we check if it is not itself, then we apply the force.
+ *
+ * @param parentBranch
+ * @param node
+ * @private
+ */
+BarnesHutSolver.prototype._getForceContribution = function(parentBranch,node) {
+ // we get no force contribution from an empty region
+ if (parentBranch.childrenCount > 0) {
+ var dx,dy,distance;
+
+ // get the distance from the center of mass to the node.
+ dx = parentBranch.centerOfMass.x - node.x;
+ dy = parentBranch.centerOfMass.y - node.y;
+ distance = Math.sqrt(dx * dx + dy * dy);
+
+ // BarnesHutSolver condition
+ // original condition : s/d < thetaInverted = passed === d/s > 1/theta = passed
+ // calcSize = 1/s --> d * 1/s > 1/theta = passed
+ if (distance * parentBranch.calcSize > this.options.thetaInverted) {
+ // duplicate code to reduce function calls to speed up program
+ if (distance == 0) {
+ distance = 0.1*Math.random();
+ dx = distance;
+ }
+ var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance);
+ var fx = dx * gravityForce;
+ var fy = dy * gravityForce;
+ node.fx += fx;
+ node.fy += fy;
+ }
+ else {
+ // Did not pass the condition, go into children if available
+ if (parentBranch.childrenCount == 4) {
+ this._getForceContribution(parentBranch.children.NW,node);
+ this._getForceContribution(parentBranch.children.NE,node);
+ this._getForceContribution(parentBranch.children.SW,node);
+ this._getForceContribution(parentBranch.children.SE,node);
+ }
+ else { // parentBranch must have only one node, if it was empty we wouldnt be here
+ if (parentBranch.children.data.id != node.id) { // if it is not self
+ // duplicate code to reduce function calls to speed up program
+ if (distance == 0) {
+ distance = 0.5*Math.random();
+ dx = distance;
+ }
+ var gravityForce = this.options.gravitationalConstant * parentBranch.mass * node.options.mass / (distance * distance * distance);
+ var fx = dx * gravityForce;
+ var fy = dy * gravityForce;
+ node.fx += fx;
+ node.fy += fy;
+ }
+ }
+ }
+ }
+};
+
+/**
+ * This function constructs the barnesHut tree recursively. It creates the root, splits it and starts placing the nodes.
+ *
+ * @param nodes
+ * @param nodeIndices
+ * @private
+ */
+BarnesHutSolver.prototype._formBarnesHutTree = function(nodes,nodeIndices) {
+ var node;
+ var nodeCount = nodeIndices.length;
+
+ var minX = Number.MAX_VALUE,
+ minY = Number.MAX_VALUE,
+ maxX =-Number.MAX_VALUE,
+ maxY =-Number.MAX_VALUE;
+
+ // get the range of the nodes
+ for (var i = 0; i < nodeCount; i++) {
+ var x = nodes[nodeIndices[i]].x;
+ var y = nodes[nodeIndices[i]].y;
+ if (nodes[nodeIndices[i]].options.mass > 0) {
+ if (x < minX) { minX = x; }
+ if (x > maxX) { maxX = x; }
+ if (y < minY) { minY = y; }
+ if (y > maxY) { maxY = y; }
+ }
+ }
+ // make the range a square
+ var sizeDiff = Math.abs(maxX - minX) - Math.abs(maxY - minY); // difference between X and Y
+ if (sizeDiff > 0) {minY -= 0.5 * sizeDiff; maxY += 0.5 * sizeDiff;} // xSize > ySize
+ else {minX += 0.5 * sizeDiff; maxX -= 0.5 * sizeDiff;} // xSize < ySize
+
+
+ var minimumTreeSize = 1e-5;
+ var rootSize = Math.max(minimumTreeSize,Math.abs(maxX - minX));
+ var halfRootSize = 0.5 * rootSize;
+ var centerX = 0.5 * (minX + maxX), centerY = 0.5 * (minY + maxY);
+
+ // construct the barnesHutTree
+ var barnesHutTree = {
+ root:{
+ centerOfMass: {x:0, y:0},
+ mass:0,
+ range: {
+ minX: centerX-halfRootSize,maxX:centerX+halfRootSize,
+ minY: centerY-halfRootSize,maxY:centerY+halfRootSize
+ },
+ size: rootSize,
+ calcSize: 1 / rootSize,
+ children: { data:null},
+ maxWidth: 0,
+ level: 0,
+ childrenCount: 4
+ }
+ };
+ this._splitBranch(barnesHutTree.root);
+
+ // place the nodes one by one recursively
+ for (i = 0; i < nodeCount; i++) {
+ node = nodes[nodeIndices[i]];
+ if (node.options.mass > 0) {
+ this._placeInTree(barnesHutTree.root,node);
+ }
+ }
+
+ // make global
+ return barnesHutTree
+};
+
+
+/**
+ * this updates the mass of a branch. this is increased by adding a node.
+ *
+ * @param parentBranch
+ * @param node
+ * @private
+ */
+BarnesHutSolver.prototype._updateBranchMass = function(parentBranch, node) {
+ var totalMass = parentBranch.mass + node.options.mass;
+ var totalMassInv = 1/totalMass;
+
+ parentBranch.centerOfMass.x = parentBranch.centerOfMass.x * parentBranch.mass + node.x * node.options.mass;
+ parentBranch.centerOfMass.x *= totalMassInv;
+
+ parentBranch.centerOfMass.y = parentBranch.centerOfMass.y * parentBranch.mass + node.y * node.options.mass;
+ parentBranch.centerOfMass.y *= totalMassInv;
+
+ parentBranch.mass = totalMass;
+ var biggestSize = Math.max(Math.max(node.height,node.radius),node.width);
+ parentBranch.maxWidth = (parentBranch.maxWidth < biggestSize) ? biggestSize : parentBranch.maxWidth;
+
+};
+
+
+/**
+ * determine in which branch the node will be placed.
+ *
+ * @param parentBranch
+ * @param node
+ * @param skipMassUpdate
+ * @private
+ */
+BarnesHutSolver.prototype._placeInTree = function(parentBranch,node,skipMassUpdate) {
+ if (skipMassUpdate != true || skipMassUpdate === undefined) {
+ // update the mass of the branch.
+ this._updateBranchMass(parentBranch,node);
+ }
+
+ if (parentBranch.children.NW.range.maxX > node.x) { // in NW or SW
+ if (parentBranch.children.NW.range.maxY > node.y) { // in NW
+ this._placeInRegion(parentBranch,node,"NW");
+ }
+ else { // in SW
+ this._placeInRegion(parentBranch,node,"SW");
+ }
+ }
+ else { // in NE or SE
+ if (parentBranch.children.NW.range.maxY > node.y) { // in NE
+ this._placeInRegion(parentBranch,node,"NE");
+ }
+ else { // in SE
+ this._placeInRegion(parentBranch,node,"SE");
+ }
+ }
+};
+
+
+/**
+ * actually place the node in a region (or branch)
+ *
+ * @param parentBranch
+ * @param node
+ * @param region
+ * @private
+ */
+BarnesHutSolver.prototype._placeInRegion = function(parentBranch,node,region) {
+ switch (parentBranch.children[region].childrenCount) {
+ case 0: // place node here
+ parentBranch.children[region].children.data = node;
+ parentBranch.children[region].childrenCount = 1;
+ this._updateBranchMass(parentBranch.children[region],node);
+ break;
+ case 1: // convert into children
+ // if there are two nodes exactly overlapping (on init, on opening of cluster etc.)
+ // we move one node a pixel and we do not put it in the tree.
+ if (parentBranch.children[region].children.data.x == node.x &&
+ parentBranch.children[region].children.data.y == node.y) {
+ node.x += Math.random();
+ node.y += Math.random();
+ }
+ else {
+ this._splitBranch(parentBranch.children[region]);
+ this._placeInTree(parentBranch.children[region],node);
+ }
+ break;
+ case 4: // place in branch
+ this._placeInTree(parentBranch.children[region],node);
+ break;
+ }
+};
+
+
+/**
+ * this function splits a branch into 4 sub branches. If the branch contained a node, we place it in the subbranch
+ * after the split is complete.
+ *
+ * @param parentBranch
+ * @private
+ */
+BarnesHutSolver.prototype._splitBranch = function(parentBranch) {
+ // if the branch is shaded with a node, replace the node in the new subset.
+ var containedNode = null;
+ if (parentBranch.childrenCount == 1) {
+ containedNode = parentBranch.children.data;
+ parentBranch.mass = 0; parentBranch.centerOfMass.x = 0; parentBranch.centerOfMass.y = 0;
+ }
+ parentBranch.childrenCount = 4;
+ parentBranch.children.data = null;
+ this._insertRegion(parentBranch,"NW");
+ this._insertRegion(parentBranch,"NE");
+ this._insertRegion(parentBranch,"SW");
+ this._insertRegion(parentBranch,"SE");
+
+ if (containedNode != null) {
+ this._placeInTree(parentBranch,containedNode);
+ }
+};
+
+
+/**
+ * This function subdivides the region into four new segments.
+ * Specifically, this inserts a single new segment.
+ * It fills the children section of the parentBranch
+ *
+ * @param parentBranch
+ * @param region
+ * @param parentRange
+ * @private
+ */
+BarnesHutSolver.prototype._insertRegion = function(parentBranch, region) {
+ var minX,maxX,minY,maxY;
+ var childSize = 0.5 * parentBranch.size;
+ switch (region) {
+ case "NW":
+ minX = parentBranch.range.minX;
+ maxX = parentBranch.range.minX + childSize;
+ minY = parentBranch.range.minY;
+ maxY = parentBranch.range.minY + childSize;
+ break;
+ case "NE":
+ minX = parentBranch.range.minX + childSize;
+ maxX = parentBranch.range.maxX;
+ minY = parentBranch.range.minY;
+ maxY = parentBranch.range.minY + childSize;
+ break;
+ case "SW":
+ minX = parentBranch.range.minX;
+ maxX = parentBranch.range.minX + childSize;
+ minY = parentBranch.range.minY + childSize;
+ maxY = parentBranch.range.maxY;
+ break;
+ case "SE":
+ minX = parentBranch.range.minX + childSize;
+ maxX = parentBranch.range.maxX;
+ minY = parentBranch.range.minY + childSize;
+ maxY = parentBranch.range.maxY;
+ break;
+ }
+
+
+ parentBranch.children[region] = {
+ centerOfMass:{x:0,y:0},
+ mass:0,
+ range:{minX:minX,maxX:maxX,minY:minY,maxY:maxY},
+ size: 0.5 * parentBranch.size,
+ calcSize: 2 * parentBranch.calcSize,
+ children: {data:null},
+ maxWidth: 0,
+ level: parentBranch.level+1,
+ childrenCount: 0
+ };
+};
+
+
+/**
+ * This function is for debugging purposed, it draws the tree.
+ *
+ * @param ctx
+ * @param color
+ * @private
+ */
+BarnesHutSolver.prototype._drawTree = function(ctx,color) {
+ if (this.barnesHutTree !== undefined) {
+
+ ctx.lineWidth = 1;
+
+ this._drawBranch(this.barnesHutTree.root,ctx,color);
+ }
+};
+
+
+/**
+ * This function is for debugging purposes. It draws the branches recursively.
+ *
+ * @param branch
+ * @param ctx
+ * @param color
+ * @private
+ */
+BarnesHutSolver.prototype._drawBranch = function(branch,ctx,color) {
+ if (color === undefined) {
+ color = "#FF0000";
+ }
+
+ if (branch.childrenCount == 4) {
+ this._drawBranch(branch.children.NW,ctx);
+ this._drawBranch(branch.children.NE,ctx);
+ this._drawBranch(branch.children.SE,ctx);
+ this._drawBranch(branch.children.SW,ctx);
+ }
+ ctx.strokeStyle = color;
+ ctx.beginPath();
+ ctx.moveTo(branch.range.minX,branch.range.minY);
+ ctx.lineTo(branch.range.maxX,branch.range.minY);
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.moveTo(branch.range.maxX,branch.range.minY);
+ ctx.lineTo(branch.range.maxX,branch.range.maxY);
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.moveTo(branch.range.maxX,branch.range.maxY);
+ ctx.lineTo(branch.range.minX,branch.range.maxY);
+ ctx.stroke();
+
+ ctx.beginPath();
+ ctx.moveTo(branch.range.minX,branch.range.maxY);
+ ctx.lineTo(branch.range.minX,branch.range.minY);
+ ctx.stroke();
+
+ /*
+ if (branch.mass > 0) {
+ ctx.circle(branch.centerOfMass.x, branch.centerOfMass.y, 3*branch.mass);
+ ctx.stroke();
+ }
+ */
+};
+
+
+module.exports = BarnesHutSolver;
\ No newline at end of file
diff --git a/lib/network/modules/components/CentralGravitySolver.js b/lib/network/modules/components/CentralGravitySolver.js
new file mode 100644
index 00000000..154450f8
--- /dev/null
+++ b/lib/network/modules/components/CentralGravitySolver.js
@@ -0,0 +1,32 @@
+/**
+ * Created by Alex on 2/23/2015.
+ */
+
+function CentralGravitySolver(body, options) {
+ this.body = body;
+ this.options = options;
+}
+
+
+CentralGravitySolver.prototype.solve = function () {
+ var dx, dy, distance, node, i;
+ var nodes = this.body.calculationNodes;
+ var gravity = this.options.centralGravity;
+ var gravityForce = 0;
+
+ for (i = 0; i < this.body.calculationNodeIndices.length; i++) {
+ node = nodes[this.body.calculationNodeIndices[i]];
+ node.damping = this.options.damping; // possibly add function to alter damping properties of clusters.
+
+ dx = -node.x;
+ dy = -node.y;
+ distance = Math.sqrt(dx * dx + dy * dy);
+
+ gravityForce = (distance == 0) ? 0 : (gravity / distance);
+ node.fx = dx * gravityForce;
+ node.fy = dy * gravityForce;
+ }
+};
+
+
+module.exports = CentralGravitySolver;
\ No newline at end of file
diff --git a/lib/network/modules/components/SpringSolver.js b/lib/network/modules/components/SpringSolver.js
new file mode 100644
index 00000000..c22abc68
--- /dev/null
+++ b/lib/network/modules/components/SpringSolver.js
@@ -0,0 +1,101 @@
+/**
+ * Created by Alex on 2/23/2015.
+ */
+
+function SpringSolver(body, options) {
+ this.body = body;
+ this.options = options;
+}
+
+
+
+/**
+ * this function calculates the effects of the springs in the case of unsmooth curves.
+ *
+ * @private
+ */
+SpringSolver.prototype._calculateSpringForces = function () {
+ var edgeLength, edge, edgeId;
+ var edges = this.edges;
+
+ // forces caused by the edges, modelled as springs
+ for (edgeId in edges) {
+ if (edges.hasOwnProperty(edgeId)) {
+ edge = edges[edgeId];
+ if (edge.connected === true) {
+ // only calculate forces if nodes are in the same sector
+ if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
+ edgeLength = edge.physics.springLength;
+
+ this._calculateSpringForce(edge.from, edge.to, edgeLength);
+ }
+ }
+ }
+ }
+};
+
+
+
+
+/**
+ * This function calculates the springforces on the nodes, accounting for the support nodes.
+ *
+ * @private
+ */
+SpringSolver.prototype._calculateSpringForcesWithSupport = function () {
+ var edgeLength, edge, edgeId;
+ var edges = this.edges;
+
+ // forces caused by the edges, modelled as springs
+ for (edgeId in edges) {
+ if (edges.hasOwnProperty(edgeId)) {
+ edge = edges[edgeId];
+ if (edge.connected === true) {
+ // only calculate forces if nodes are in the same sector
+ if (this.nodes.hasOwnProperty(edge.toId) && this.nodes.hasOwnProperty(edge.fromId)) {
+ if (edge.via != null) {
+ var node1 = edge.to;
+ var node2 = edge.via;
+ var node3 = edge.from;
+
+ edgeLength = edge.physics.springLength;
+
+ this._calculateSpringForce(node1, node2, 0.5 * edgeLength);
+ this._calculateSpringForce(node2, node3, 0.5 * edgeLength);
+ }
+ }
+ }
+ }
+ }
+};
+
+
+/**
+ * This is the code actually performing the calculation for the function above. It is split out to avoid repetition.
+ *
+ * @param node1
+ * @param node2
+ * @param edgeLength
+ * @private
+ */
+SpringSolver.prototype._calculateSpringForce = function (node1, node2, edgeLength) {
+ var dx, dy, fx, fy, springForce, distance;
+
+ dx = (node1.x - node2.x);
+ dy = (node1.y - node2.y);
+ distance = Math.sqrt(dx * dx + dy * dy);
+ distance = distance == 0 ? 0.01 : distance;
+
+ // the 1/distance is so the fx and fy can be calculated without sine or cosine.
+ springForce = this.options.springConstant * (edgeLength - distance) / distance;
+
+ fx = dx * springForce;
+ fy = dy * springForce;
+
+ node1.fx += fx;
+ node1.fy += fy;
+ node2.fx -= fx;
+ node2.fy -= fy;
+};
+
+module.exports = SpringSolver;
\ No newline at end of file
diff --git a/lib/timeline/Timeline.js b/lib/timeline/Timeline.js
index 6deea30a..2a61d12e 100644
--- a/lib/timeline/Timeline.js
+++ b/lib/timeline/Timeline.js
@@ -13,8 +13,8 @@ var ItemSet = require('./component/ItemSet');
/**
* Create a timeline visualization
* @param {HTMLElement} container
- * @param {vis.DataSet | Array | google.visualization.DataTable} [items]
- * @param {vis.DataSet | Array | google.visualization.DataTable} [groups]
+ * @param {vis.DataSet | vis.DataView | Array | google.visualization.DataTable} [items]
+ * @param {vis.DataSet | vis.DataView | Array | google.visualization.DataTable} [groups]
* @param {Object} [options] See Timeline.setOptions for the available options.
* @constructor
* @extends Core
@@ -25,7 +25,7 @@ function Timeline (container, items, groups, options) {
}
// if the third element is options, the forth is groups (optionally);
- if (!(Array.isArray(groups) || groups instanceof DataSet) && groups instanceof Object) {
+ if (!(Array.isArray(groups) || groups instanceof DataSet || groups instanceof DataView) && groups instanceof Object) {
var forthArgument = options;
options = groups;
groups = forthArgument;
diff --git a/lib/timeline/component/LineGraph.js b/lib/timeline/component/LineGraph.js
index d934127f..16ff6b3a 100644
--- a/lib/timeline/component/LineGraph.js
+++ b/lib/timeline/component/LineGraph.js
@@ -981,9 +981,17 @@ LineGraph.prototype._convertYcoordinates = function (datapoints, group) {
}
for (var i = 0; i < datapoints.length; i++) {
+ var labelValue;
+ //if (datapoints[i].label) {
+ // labelValue = datapoints[i].label;
+ //}
+ //else {
+ // labelValue = null;
+ //}
+ labelValue = datapoints[i].label ? datapoints[i].label : null;
xValue = toScreen(datapoints[i].x) + this.props.width;
yValue = Math.round(axis.convertValue(datapoints[i].y));
- extractedData.push({x: xValue, y: yValue});
+ extractedData.push({x: xValue, y: yValue, label:labelValue});
}
group.setZeroPosition(Math.min(svgHeight, axis.convertValue(0)));
diff --git a/lib/timeline/component/graph2d_types/bar.js b/lib/timeline/component/graph2d_types/bar.js
index 3dee4b21..1ba41365 100644
--- a/lib/timeline/component/graph2d_types/bar.js
+++ b/lib/timeline/component/graph2d_types/bar.js
@@ -58,7 +58,8 @@ Bargraph.draw = function (groupIds, processedGroupData, framework) {
combinedData.push({
x: processedGroupData[groupIds[i]][j].x,
y: processedGroupData[groupIds[i]][j].y,
- groupId: groupIds[i]
+ groupId: groupIds[i],
+ label: processedGroupData[groupIds[i]][j].label
});
barPoints += 1;
}
@@ -114,7 +115,8 @@ Bargraph.draw = function (groupIds, processedGroupData, framework) {
DOMutil.drawBar(combinedData[i].x + drawData.offset, combinedData[i].y - heightOffset, drawData.width, group.zeroPosition - combinedData[i].y, group.className + ' bar', framework.svgElements, framework.svg);
// draw points
if (group.options.drawPoints.enabled == true) {
- DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y, group, framework.svgElements, framework.svg);
+ Points.draw([combinedData[i]], group, framework, drawData.offset);
+ //DOMutil.drawPoint(combinedData[i].x + drawData.offset, combinedData[i].y, group, framework.svgElements, framework.svg);
}
}
};
diff --git a/lib/timeline/component/graph2d_types/points.js b/lib/timeline/component/graph2d_types/points.js
index 2624644d..586614b1 100644
--- a/lib/timeline/component/graph2d_types/points.js
+++ b/lib/timeline/component/graph2d_types/points.js
@@ -35,7 +35,7 @@ Points.prototype.draw = function(dataset, group, framework, offset) {
Points.draw = function (dataset, group, framework, offset) {
if (offset === undefined) {offset = 0;}
for (var i = 0; i < dataset.length; i++) {
- DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, framework.svgElements, framework.svg);
+ DOMutil.drawPoint(dataset[i].x + offset, dataset[i].y, group, framework.svgElements, framework.svg, dataset[i].label);
}
};
diff --git a/lib/util.js b/lib/util.js
index c4416276..bfc59ba2 100644
--- a/lib/util.js
+++ b/lib/util.js
@@ -225,22 +225,24 @@ exports.selectiveNotDeepExtend = function (props, a, b) {
* Deep extend an object a with the properties of object b
* @param {Object} a
* @param {Object} b
+ * @param {Boolean} protoExtend --> optional parameter. If true, the prototype values will also be extended.
+ * (ie. the options objects that inherit from others will also get the inherited options)
* @returns {Object}
*/
-exports.deepExtend = function(a, b) {
+exports.deepExtend = function(a, b, protoExtend) {
// TODO: add support for Arrays to deepExtend
if (Array.isArray(b)) {
throw new TypeError('Arrays are not supported by deepExtend');
}
for (var prop in b) {
- if (b.hasOwnProperty(prop)) {
+ if (b.hasOwnProperty(prop) || protoExtend === true) {
if (b[prop] && b[prop].constructor === Object) {
if (a[prop] === undefined) {
a[prop] = {};
}
if (a[prop].constructor === Object) {
- exports.deepExtend(a[prop], b[prop]);
+ exports.deepExtend(a[prop], b[prop], protoExtend);
}
else {
a[prop] = b[prop];