highcharts
Version:
JavaScript charting framework
1,265 lines (1,166 loc) • 127 kB
JavaScript
/**
* @license Highcharts JS v7.1.2 (2019-06-04)
*
* Force directed graph module
*
* (c) 2010-2019 Torstein Honsi
*
* License: www.highcharts.com/license
*/
'use strict';
(function (factory) {
if (typeof module === 'object' && module.exports) {
factory['default'] = factory;
module.exports = factory;
} else if (typeof define === 'function' && define.amd) {
define('highcharts/modules/networkgraph', ['highcharts'], function (Highcharts) {
factory(Highcharts);
factory.Highcharts = Highcharts;
return factory;
});
} else {
factory(typeof Highcharts !== 'undefined' ? Highcharts : undefined);
}
}(function (Highcharts) {
var _modules = Highcharts ? Highcharts._modules : {};
function _registerModule(obj, path, args, fn) {
if (!obj.hasOwnProperty(path)) {
obj[path] = fn.apply(null, args);
}
}
_registerModule(_modules, 'mixins/nodes.js', [_modules['parts/Globals.js']], function (H) {
var pick = H.pick,
defined = H.defined,
Point = H.Point;
H.NodesMixin = {
// Create a single node that holds information on incoming and outgoing
// links.
createNode: function (id) {
function findById(nodes, id) {
return H.find(nodes, function (node) {
return node.id === id;
});
}
var node = findById(this.nodes, id),
PointClass = this.pointClass,
options;
if (!node) {
options = this.options.nodes && findById(this.options.nodes, id);
node = (new PointClass()).init(
this,
H.extend({
className: 'highcharts-node',
isNode: true,
id: id,
y: 1 // Pass isNull test
}, options)
);
node.linksTo = [];
node.linksFrom = [];
node.formatPrefix = 'node';
node.name = node.name || node.options.id; // for use in formats
// Mass is used in networkgraph:
node.mass = pick(
// Node:
node.options.mass,
node.options.marker && node.options.marker.radius,
// Series:
this.options.marker && this.options.marker.radius,
// Default:
4
);
// Return the largest sum of either the incoming or outgoing links.
node.getSum = function () {
var sumTo = 0,
sumFrom = 0;
node.linksTo.forEach(function (link) {
sumTo += link.weight;
});
node.linksFrom.forEach(function (link) {
sumFrom += link.weight;
});
return Math.max(sumTo, sumFrom);
};
// Get the offset in weight values of a point/link.
node.offset = function (point, coll) {
var offset = 0;
for (var i = 0; i < node[coll].length; i++) {
if (node[coll][i] === point) {
return offset;
}
offset += node[coll][i].weight;
}
};
// Return true if the node has a shape, otherwise all links are
// outgoing.
node.hasShape = function () {
var outgoing = 0;
node.linksTo.forEach(function (link) {
if (link.outgoing) {
outgoing++;
}
});
return !node.linksTo.length || outgoing !== node.linksTo.length;
};
this.nodes.push(node);
}
return node;
},
// Extend generatePoints by adding the nodes, which are Point objects
// but pushed to the this.nodes array.
generatePoints: function () {
var nodeLookup = {},
chart = this.chart;
H.Series.prototype.generatePoints.call(this);
if (!this.nodes) {
this.nodes = []; // List of Point-like node items
}
this.colorCounter = 0;
// Reset links from previous run
this.nodes.forEach(function (node) {
node.linksFrom.length = 0;
node.linksTo.length = 0;
node.level = undefined;
});
// Create the node list and set up links
this.points.forEach(function (point) {
if (defined(point.from)) {
if (!nodeLookup[point.from]) {
nodeLookup[point.from] = this.createNode(point.from);
}
nodeLookup[point.from].linksFrom.push(point);
point.fromNode = nodeLookup[point.from];
// Point color defaults to the fromNode's color
if (chart.styledMode) {
point.colorIndex = pick(
point.options.colorIndex,
nodeLookup[point.from].colorIndex
);
} else {
point.color =
point.options.color || nodeLookup[point.from].color;
}
}
if (defined(point.to)) {
if (!nodeLookup[point.to]) {
nodeLookup[point.to] = this.createNode(point.to);
}
nodeLookup[point.to].linksTo.push(point);
point.toNode = nodeLookup[point.to];
}
point.name = point.name || point.id; // for use in formats
}, this);
// Store lookup table for later use
this.nodeLookup = nodeLookup;
},
// Destroy all nodes on setting new data
setData: function () {
if (this.nodes) {
this.nodes.forEach(function (node) {
node.destroy();
});
this.nodes.length = 0;
}
H.Series.prototype.setData.apply(this, arguments);
},
// Destroy alll nodes and links
destroy: function () {
// Nodes must also be destroyed (#8682, #9300)
this.data = [].concat(this.points || [], this.nodes);
return H.Series.prototype.destroy.apply(this, arguments);
},
// When hovering node, highlight all connected links. When hovering a link,
// highlight all connected nodes.
setNodeState: function () {
var args = arguments,
others = this.isNode ? this.linksTo.concat(this.linksFrom) :
[this.fromNode, this.toNode];
others.forEach(function (linkOrNode) {
if (linkOrNode.series) {
Point.prototype.setState.apply(linkOrNode, args);
if (!linkOrNode.isNode) {
if (linkOrNode.fromNode.graphic) {
Point.prototype.setState.apply(
linkOrNode.fromNode,
args
);
}
if (linkOrNode.toNode.graphic) {
Point.prototype.setState.apply(
linkOrNode.toNode,
args
);
}
}
}
});
Point.prototype.setState.apply(this, args);
}
};
});
_registerModule(_modules, 'modules/networkgraph/integrations.js', [_modules['parts/Globals.js']], function (H) {
/* *
* Networkgraph series
*
* (c) 2010-2019 Paweł Fus
*
* License: www.highcharts.com/license
*/
H.networkgraphIntegrations = {
verlet: {
/**
* Attractive force funtion. Can be replaced by API's
* `layoutAlgorithm.attractiveForce`
*
* @private
*
* @param {number} d current distance between two nodes
* @param {number} k expected distance between two nodes
*
* @return {number} force
*/
attractiveForceFunction: function (d, k) {
// Used in API:
return (k - d) / d;
},
/**
* Repulsive force funtion. Can be replaced by API's
* `layoutAlgorithm.repulsiveForce`
*
* @private
*
* @param {number} d current distance between two nodes
* @param {number} k expected distance between two nodes
*
* @return {number} force
*/
repulsiveForceFunction: function (d, k) {
// Used in API:
return (k - d) / d * (k > d ? 1 : 0); // Force only for close nodes
},
/**
* Barycenter force. Calculate and applys barycenter forces on the
* nodes. Making them closer to the center of their barycenter point.
*
* In Verlet integration, force is applied on a node immidatelly to it's
* `plotX` and `plotY` position.
*
* @private
*
* @return {void}
*/
barycenter: function () {
var gravitationalConstant = this.options.gravitationalConstant,
xFactor = this.barycenter.xFactor,
yFactor = this.barycenter.yFactor;
// To consider:
xFactor = (xFactor - (this.box.left + this.box.width) / 2) *
gravitationalConstant;
yFactor = (yFactor - (this.box.top + this.box.height) / 2) *
gravitationalConstant;
this.nodes.forEach(function (node) {
if (!node.fixedPosition) {
node.plotX -= xFactor / node.mass / node.degree;
node.plotY -= yFactor / node.mass / node.degree;
}
});
},
/**
* Repulsive force.
*
* In Verlet integration, force is applied on a node immidatelly to it's
* `plotX` and `plotY` position.
*
* @private
*
* @param {Highcharts.Point} node node that should be translated by
* force.
* @param {number} force force calcualated in `repulsiveForceFunction`
* @param {object} distance Distance between two nodes e.g. `{x, y}`
*
* @return {void}
*/
repulsive: function (node, force, distanceXY) {
var factor = force * this.diffTemperature / node.mass / node.degree;
if (!node.fixedPosition) {
node.plotX += distanceXY.x * factor;
node.plotY += distanceXY.y * factor;
}
},
/**
* Attractive force.
*
* In Verlet integration, force is applied on a node immidatelly to it's
* `plotX` and `plotY` position.
*
* @private
*
* @param {Highcharts.Point} link link that connects two nodes
* @param {number} force force calcualated in `repulsiveForceFunction`
* @param {object} distance Distance between two nodes e.g. `{x, y}`
*
* @return {void}
*/
attractive: function (link, force, distanceXY) {
var massFactor = link.getMass(),
translatedX = -distanceXY.x * force * this.diffTemperature,
translatedY = -distanceXY.y * force * this.diffTemperature;
if (!link.fromNode.fixedPosition) {
link.fromNode.plotX -= translatedX * massFactor.fromNode /
link.fromNode.degree;
link.fromNode.plotY -= translatedY * massFactor.fromNode /
link.fromNode.degree;
}
if (!link.toNode.fixedPosition) {
link.toNode.plotX += translatedX * massFactor.toNode /
link.toNode.degree;
link.toNode.plotY += translatedY * massFactor.toNode /
link.toNode.degree;
}
},
/**
* Integration method.
*
* In Verlet integration, forces are applied on node immidatelly to it's
* `plotX` and `plotY` position.
*
* Verlet without velocity:
*
* x(n+1) = 2 * x(n) - x(n-1) + A(T) * deltaT ^ 2
*
* where:
* - x(n+1) - new position
* - x(n) - current position
* - x(n-1) - previous position
*
* Assuming A(t) = 0 (no acceleration) and (deltaT = 1) we get:
*
* x(n+1) = x(n) + (x(n) - x(n-1))
*
* where:
* - (x(n) - x(n-1)) - position change
*
* TO DO:
* Consider Verlet with velocity to support additional
* forces. Or even Time-Corrected Verlet by Jonathan
* "lonesock" Dummer
*
* @private
*
* @param {object} layout layout object
* @param {Highcharts.Point} node node that should be translated
*
* @return {void}
*/
integrate: function (layout, node) {
var friction = -layout.options.friction,
maxSpeed = layout.options.maxSpeed,
prevX = node.prevX,
prevY = node.prevY,
// Apply friciton:
diffX = (node.plotX + node.dispX - prevX) * friction,
diffY = (node.plotY + node.dispY - prevY) * friction,
abs = Math.abs,
signX = abs(diffX) / (diffX || 1), // need to deal with 0
signY = abs(diffY) / (diffY || 1);
// Apply max speed:
diffX = signX * Math.min(maxSpeed, Math.abs(diffX));
diffY = signY * Math.min(maxSpeed, Math.abs(diffY));
// Store for the next iteration:
node.prevX = node.plotX + node.dispX;
node.prevY = node.plotY + node.dispY;
// Update positions:
node.plotX += diffX;
node.plotY += diffY;
node.temperature = layout.vectorLength({
x: diffX,
y: diffY
});
},
/**
* Estiamte the best possible distance between two nodes, making graph
* readable.
*
* @private
*
* @param {object} layout layout object
*
* @return {number}
*/
getK: function (layout) {
return Math.pow(
layout.box.width * layout.box.height / layout.nodes.length,
0.5
);
}
},
euler: {
/**
* Attractive force funtion. Can be replaced by API's
* `layoutAlgorithm.attractiveForce`
*
* Other forces that can be used:
*
* basic, not recommended:
* `function (d, k) { return d / k }`
*
* @private
*
* @param {number} d current distance between two nodes
* @param {number} k expected distance between two nodes
*
* @return {number} force
*/
attractiveForceFunction: function (d, k) {
return d * d / k;
},
/**
* Repulsive force funtion. Can be replaced by API's
* `layoutAlgorithm.repulsiveForce`.
*
* Other forces that can be used:
*
* basic, not recommended:
* `function (d, k) { return k / d }`
*
* standard:
* `function (d, k) { return k * k / d }`
*
* grid-variant:
* `function (d, k) { return k * k / d * (2 * k - d > 0 ? 1 : 0) }`
*
* @private
*
* @param {number} d current distance between two nodes
* @param {number} k expected distance between two nodes
*
* @return {number} force
*/
repulsiveForceFunction: function (d, k) {
return k * k / d;
},
/**
* Barycenter force. Calculate and applys barycenter forces on the
* nodes. Making them closer to the center of their barycenter point.
*
* In Euler integration, force is stored in a node, not changing it's
* position. Later, in `integrate()` forces are applied on nodes.
*
* @private
*
* @return {void}
*/
barycenter: function () {
var gravitationalConstant = this.options.gravitationalConstant,
xFactor = this.barycenter.xFactor,
yFactor = this.barycenter.yFactor;
this.nodes.forEach(function (node) {
if (!node.fixedPosition) {
var degree = node.getDegree(),
phi = degree * (1 + degree / 2);
node.dispX += (xFactor - node.plotX) *
gravitationalConstant * phi / node.degree;
node.dispY += (yFactor - node.plotY) *
gravitationalConstant * phi / node.degree;
}
});
},
/**
* Repulsive force.
*
* @private
*
* @param {Highcharts.Point} node
* Node that should be translated by force.
* @param {number} force
* Force calcualated in `repulsiveForceFunction`
* @param {object} distance
* Distance between two nodes e.g. `{x, y}`
*
* @return {void}
*/
repulsive: function (node, force, distanceXY, distanceR) {
node.dispX += (distanceXY.x / distanceR) * force / node.degree;
node.dispY += (distanceXY.y / distanceR) * force / node.degree;
},
/**
* Attractive force.
*
* In Euler integration, force is stored in a node, not changing it's
* position. Later, in `integrate()` forces are applied on nodes.
*
* @private
*
* @param {Highcharts.Point} link link that connects two nodes
* @param {number} force force calcualated in `repulsiveForceFunction`
* @param {object} distance Distance between two nodes e.g. `{x, y}`
*
* @return {void}
*/
attractive: function (link, force, distanceXY, distanceR) {
var massFactor = link.getMass(),
translatedX = (distanceXY.x / distanceR) * force,
translatedY = (distanceXY.y / distanceR) * force;
if (!link.fromNode.fixedPosition) {
link.fromNode.dispX -= translatedX * massFactor.fromNode /
link.fromNode.degree;
link.fromNode.dispY -= translatedY * massFactor.fromNode /
link.fromNode.degree;
}
if (!link.toNode.fixedPosition) {
link.toNode.dispX += translatedX * massFactor.toNode /
link.toNode.degree;
link.toNode.dispY += translatedY * massFactor.toNode /
link.toNode.degree;
}
},
/**
* Integration method.
*
* In Euler integration, force were stored in a node, not changing it's
* position. Now, in the integrator method, we apply changes.
*
* Euler:
*
* Basic form:
* `x(n+1) = x(n) + v(n)`
*
* With Rengoild-Fruchterman we get:
* <pre>
* x(n+1) = x(n) +
* v(n) / length(v(n)) *
* min(v(n), temperature(n))
* </pre>
* where:
* <pre>
* x(n+1) - next position
* x(n) - current position
* v(n) - velocity (comes from net force)
* temperature(n) - current temperature
* </pre>
*
* Known issues:
* Oscillations when force vector has the same magnitude but opposite
* direction in the next step. Potentially solved by decreasing force by
* `v * (1 / node.degree)`
*
* Note:
* Actually `min(v(n), temperature(n))` replaces simulated annealing.
*
* @private
*
* @param {object} layout layout object
* @param {Highcharts.Point} node node that should be translated
*
* @return {void}
*/
integrate: function (layout, node) {
var distanceR;
node.dispX += node.dispX * layout.options.friction;
node.dispY += node.dispY * layout.options.friction;
distanceR = node.temperature = layout.vectorLength({
x: node.dispX,
y: node.dispY
});
if (distanceR !== 0) {
node.plotX += node.dispX / distanceR *
Math.min(Math.abs(node.dispX), layout.temperature);
node.plotY += node.dispY / distanceR *
Math.min(Math.abs(node.dispY), layout.temperature);
}
},
/**
* Estiamte the best possible distance between two nodes, making graph
* readable.
*
* @private
*
* @param {object} layout layout object
*
* @return {number}
*/
getK: function (layout) {
return Math.pow(
layout.box.width * layout.box.height / layout.nodes.length,
0.3
);
}
}
};
});
_registerModule(_modules, 'modules/networkgraph/QuadTree.js', [_modules['parts/Globals.js']], function (H) {
/* *
* Networkgraph series
*
* (c) 2010-2019 Paweł Fus
*
* License: www.highcharts.com/license
*/
/**
* The QuadTree node class. Used in Networkgraph chart as a base for Barnes-Hut
* approximation.
*
* @private
* @class
* @name Highcharts.QuadTreeNode
*
* @param {Highcharts.RectangleObject} Available space for the node
*/
var QuadTreeNode = H.QuadTreeNode = function (box) {
/**
* Read only. The available space for node.
*
* @name Highcharts.QuadTreeNode#box
* @type {Highcharts.RectangleObject}
*/
this.box = box;
/**
* Read only. The minium of width and height values.
*
* @name Highcharts.QuadTreeNode#boxSize
* @type {number}
*/
this.boxSize = Math.min(box.width, box.height);
/**
* Read only. Array of subnodes. Empty if QuadTreeNode has just one Point.
* When added another Point to this QuadTreeNode, array is filled with four
* subnodes.
*
* @name Highcharts.QuadTreeNode#nodes
* @type {Array<Highcharts.QuadTreeNode>}
*/
this.nodes = [];
/**
* Read only. Flag to determine if QuadTreeNode is internal (and has
* subnodes with mass and central position) or external (bound to Point).
*
* @name Highcharts.QuadTreeNode#isInternal
* @type {boolean}
*/
this.isInternal = false;
/**
* Read only. If QuadTreeNode is an external node, Point is stored in
* `this.body`.
*
* @name Highcharts.QuadTreeNode#body
* @type {boolean|Highcharts.Point}
*/
this.body = false;
/**
* Read only. Internal nodes when created are empty to reserve the space. If
* Point is added to this QuadTreeNode, QuadTreeNode is no longer empty.
*
* @name Highcharts.QuadTreeNode#isEmpty
* @type {boolean}
*/
this.isEmpty = true;
};
H.extend(
QuadTreeNode.prototype,
/** @lends Highcharts.QuadTreeNode.prototype */
{
/**
* Insert recursively point(node) into the QuadTree. If the given
* quadrant is already occupied, divide it into smaller quadrants.
*
* @param {Highcharts.Point} point point/node to be inserted
* @param {number} depth max depth of the QuadTree
*/
insert: function (point, depth) {
if (this.isInternal) {
// Internal node:
this.nodes[this.getBoxPosition(point)].insert(point, depth - 1);
} else {
this.isEmpty = false;
if (!this.body) {
// First body in a quadrant:
this.isInternal = false;
this.body = point;
} else {
if (depth) {
// Every other body in a quadrant:
this.isInternal = true;
this.divideBox();
// Reinsert main body only once:
if (this.body !== true) {
this.nodes[this.getBoxPosition(this.body)]
.insert(this.body, depth - 1);
this.body = true;
}
// Add second body:
this.nodes[this.getBoxPosition(point)]
.insert(point, depth - 1);
} else {
this.nodes.push(point);
}
}
}
},
/**
* Each quad node requires it's mass and center position. That mass and
* position is used to imitate real node in the layout by approximation.
*/
updateMassAndCenter: function () {
var mass = 0,
plotX = 0,
plotY = 0;
if (this.isInternal) {
// Calcualte weightened mass of the quad node:
this.nodes.forEach(function (pointMass) {
if (!pointMass.isEmpty) {
mass += pointMass.mass;
plotX += pointMass.plotX * pointMass.mass;
plotY += pointMass.plotY * pointMass.mass;
}
});
plotX /= mass;
plotY /= mass;
} else if (this.body) {
// Just one node, use coordinates directly:
mass = this.body.mass;
plotX = this.body.plotX;
plotY = this.body.plotY;
}
// Store details:
this.mass = mass;
this.plotX = plotX;
this.plotY = plotY;
},
/**
* When inserting another node into the box, that already hove one node,
* divide the available space into another four quadrants.
*
* Indexes of quadrants are:
*
* <pre>
* ------------- -------------
* | | | | |
* | | | 0 | 1 |
* | | divide() | | |
* | 1 | -----------> -------------
* | | | | |
* | | | 3 | 2 |
* | | | | |
* ------------- -------------
* </pre>
*/
divideBox: function () {
var halfWidth = this.box.width / 2,
halfHeight = this.box.height / 2;
// Top left
this.nodes[0] = new QuadTreeNode({
left: this.box.left,
top: this.box.top,
width: halfWidth,
height: halfHeight
});
// Top right
this.nodes[1] = new QuadTreeNode({
left: this.box.left + halfWidth,
top: this.box.top,
width: halfWidth,
height: halfHeight
});
// Bottom right
this.nodes[2] = new QuadTreeNode({
left: this.box.left + halfWidth,
top: this.box.top + halfHeight,
width: halfWidth,
height: halfHeight
});
// Bottom left
this.nodes[3] = new QuadTreeNode({
left: this.box.left,
top: this.box.top + halfHeight,
width: halfWidth,
height: halfHeight
});
},
/**
* Determine which of the quadrants should be used when placing node in
* the QuadTree. Returned index is always in range `<0, 3>`.
*
* @param {Highcharts.Point} node
* @return {number}
*/
getBoxPosition: function (node) {
var left = node.plotX < this.box.left + this.box.width / 2,
top = node.plotY < this.box.top + this.box.height / 2,
index;
if (left) {
if (top) {
// Top left
index = 0;
} else {
// Bottom left
index = 3;
}
} else {
if (top) {
// Top right
index = 1;
} else {
// Bottom right
index = 2;
}
}
return index;
}
}
);
/**
* The QuadTree class. Used in Networkgraph chart as a base for Barnes-Hut
* approximation.
*
* @private
* @class
* @name Highcharts.QuadTree
*
* @param {number} x left position of the plotting area
* @param {number} y top position of the plotting area
* @param {number} width width of the plotting area
* @param {number} height height of the plotting area
*/
var QuadTree = H.QuadTree = function (x, y, width, height) {
// Boundary rectangle:
this.box = {
left: x,
top: y,
width: width,
height: height
};
this.maxDepth = 25;
this.root = new QuadTreeNode(this.box, '0');
this.root.isInternal = true;
this.root.isRoot = true;
this.root.divideBox();
};
H.extend(
QuadTree.prototype,
/** @lends Highcharts.QuadTree.prototype */
{
/**
* Insert nodes into the QuadTree
*
* @param {Array<Highcharts.Point>} points
*/
insertNodes: function (nodes) {
nodes.forEach(function (node) {
this.root.insert(node, this.maxDepth);
}, this);
},
/**
* Depfth first treversal (DFS). Using `before` and `after` callbacks,
* we can get two results: preorder and postorder traversals, reminder:
*
* <pre>
* (a)
* / \
* (b) (c)
* / \
* (d) (e)
* </pre>
*
* DFS (preorder): `a -> b -> d -> e -> c`
*
* DFS (postorder): `d -> e -> b -> c -> a`
*
* @param {Highcharts.QuadTreeNode} node
* @param {function} beforeCallback function to be called before
* visiting children nodes
* @param {function} afterCallback function to be called after
* visiting children nodes
*/
visitNodeRecursive: function (
node,
beforeCallback,
afterCallback,
chart,
clear
) {
var goFurther;
if (!node) {
node = this.root;
}
if (node === this.root && beforeCallback) {
goFurther = beforeCallback(node);
}
if (goFurther === false) {
return;
}
node.nodes.forEach(
function (qtNode) {
if (chart) {
// this.renderBox(qtNode, chart, clear);
}
if (qtNode.isInternal) {
if (beforeCallback) {
goFurther = beforeCallback(qtNode);
}
if (goFurther === false) {
return;
}
this.visitNodeRecursive(
qtNode,
beforeCallback,
afterCallback,
chart,
clear
);
} else if (qtNode.body) {
if (beforeCallback) {
beforeCallback(qtNode.body);
}
}
if (afterCallback) {
afterCallback(qtNode);
}
},
this
);
if (node === this.root && afterCallback) {
afterCallback(node);
}
},
/**
* Calculate mass of the each QuadNode in the tree.
*/
calculateMassAndCenter: function () {
this.visitNodeRecursive(null, null, function (node) {
node.updateMassAndCenter();
});
},
render: function (chart, clear) {
this.visitNodeRecursive(this.root, null, null, chart, clear);
},
clear: function (chart) {
this.render(chart, true);
},
renderBox: function (qtNode, chart, clear) {
if (!qtNode.graphic && !clear) {
qtNode.graphic = chart.renderer
.rect(
qtNode.box.left + chart.plotLeft,
qtNode.box.top + chart.plotTop,
qtNode.box.width,
qtNode.box.height
)
.attr({
stroke: 'rgba(100, 100, 100, 0.5)',
'stroke-width': 2
})
.add();
if (!isNaN(qtNode.plotX)) {
qtNode.graphic2 = chart.renderer
.circle(
qtNode.plotX,
qtNode.plotY,
qtNode.mass / 10
)
.attr({
fill: 'red',
translateY: chart.plotTop,
translateX: chart.plotLeft
})
.add();
}
} else if (clear) {
if (qtNode.graphic) {
qtNode.graphic = qtNode.graphic.destroy();
}
if (qtNode.graphic2) {
qtNode.graphic2 = qtNode.graphic2.destroy();
}
if (qtNode.label) {
qtNode.label = qtNode.label.destroy();
}
}
}
}
);
});
_registerModule(_modules, 'modules/networkgraph/layouts.js', [_modules['parts/Globals.js']], function (H) {
/* *
* Networkgraph series
*
* (c) 2010-2019 Paweł Fus
*
* License: www.highcharts.com/license
*/
var pick = H.pick,
defined = H.defined,
addEvent = H.addEvent,
Chart = H.Chart;
H.layouts = {
'reingold-fruchterman': function () {
}
};
H.extend(
/**
* Reingold-Fruchterman algorithm from
* "Graph Drawing by Force-directed Placement" paper.
* @private
*/
H.layouts['reingold-fruchterman'].prototype,
{
init: function (options) {
this.options = options;
this.nodes = [];
this.links = [];
this.series = [];
this.box = {
x: 0,
y: 0,
width: 0,
height: 0
};
this.setInitialRendering(true);
this.integration = H.networkgraphIntegrations[options.integration];
this.attractiveForce = pick(
options.attractiveForce,
this.integration.attractiveForceFunction
);
this.repulsiveForce = pick(
options.repulsiveForce,
this.integration.repulsiveForceFunction
);
this.approximation = options.approximation;
},
start: function () {
var layout = this,
series = this.series,
options = this.options;
layout.currentStep = 0;
layout.forces = series[0] && series[0].forces || [];
if (layout.initialRendering) {
layout.initPositions();
// Render elements in initial positions:
series.forEach(function (s) {
s.render();
});
}
layout.setK();
layout.resetSimulation(options);
if (options.enableSimulation) {
layout.step();
}
},
step: function () {
var layout = this,
series = this.series,
options = this.options;
// Algorithm:
layout.currentStep++;
if (layout.approximation === 'barnes-hut') {
layout.createQuadTree();
layout.quadTree.calculateMassAndCenter();
}
layout.forces.forEach(function (forceName) {
layout[forceName + 'Forces'](layout.temperature);
});
// Limit to the plotting area and cool down:
layout.applyLimits(layout.temperature);
// Cool down the system:
layout.temperature = layout.coolDown(
layout.startTemperature,
layout.diffTemperature,
layout.currentStep
);
layout.prevSystemTemperature = layout.systemTemperature;
layout.systemTemperature = layout.getSystemTemperature();
if (options.enableSimulation) {
series.forEach(function (s) {
// Chart could be destroyed during the simulation
if (s.chart) {
s.render();
}
});
if (
layout.maxIterations-- &&
isFinite(layout.temperature) &&
!layout.isStable()
) {
if (layout.simulation) {
H.win.cancelAnimationFrame(layout.simulation);
}
layout.simulation = H.win.requestAnimationFrame(
function () {
layout.step();
}
);
} else {
layout.simulation = false;
}
}
},
stop: function () {
if (this.simulation) {
H.win.cancelAnimationFrame(this.simulation);
}
},
setArea: function (x, y, w, h) {
this.box = {
left: x,
top: y,
width: w,
height: h
};
},
setK: function () {
// Optimal distance between nodes,
// available space around the node:
this.k = this.options.linkLength || this.integration.getK(this);
},
addNodes: function (nodes) {
nodes.forEach(function (node) {
if (this.nodes.indexOf(node) === -1) {
this.nodes.push(node);
}
}, this);
},
removeNode: function (node) {
var index = this.nodes.indexOf(node);
if (index !== -1) {
this.nodes.splice(index, 1);
}
},
removeLink: function (link) {
var index = this.links.indexOf(link);
if (index !== -1) {
this.links.splice(index, 1);
}
},
addLinks: function (links) {
links.forEach(function (link) {
if (this.links.indexOf(link) === -1) {
this.links.push(link);
}
}, this);
},
addSeries: function (series) {
if (this.series.indexOf(series) === -1) {
this.series.push(series);
}
},
clear: function () {
this.nodes.length = 0;
this.links.length = 0;
this.series.length = 0;
this.resetSimulation();
},
resetSimulation: function () {
this.forcedStop = false;
this.systemTemperature = 0;
this.setMaxIterations();
this.setTemperature();
this.setDiffTemperature();
},
setMaxIterations: function (maxIterations) {
this.maxIterations = pick(
maxIterations,
this.options.maxIterations
);
},
setTemperature: function () {
this.temperature = this.startTemperature =
Math.sqrt(this.nodes.length);