| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553 |
- /* *
- *
- * Networkgraph series
- *
- * (c) 2010-2020 Paweł Fus
- *
- * License: www.highcharts.com/license
- *
- * !!!!!!! SOURCE GETS TRANSPILED BY TYPESCRIPT. EDIT TS FILE ONLY. !!!!!!!
- *
- * */
- 'use strict';
- import Chart from '../../Core/Chart/Chart.js';
- import H from '../../Core/Globals.js';
- import U from '../../Core/Utilities.js';
- var addEvent = U.addEvent, clamp = U.clamp, defined = U.defined, extend = U.extend, isFunction = U.isFunction, pick = U.pick, setAnimation = U.setAnimation;
- import './Integrations.js';
- import './QuadTree.js';
- /* eslint-disable no-invalid-this, valid-jsdoc */
- H.layouts = {
- 'reingold-fruchterman': function () {
- }
- };
- 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.enableSimulation = options.enableSimulation;
- this.attractiveForce = pick(options.attractiveForce, this.integration.attractiveForceFunction);
- this.repulsiveForce = pick(options.repulsiveForce, this.integration.repulsiveForceFunction);
- this.approximation = options.approximation;
- },
- updateSimulation: function (enable) {
- this.enableSimulation = pick(enable, this.options.enableSimulation);
- },
- start: function () {
- var layout = this, series = this.series, options = this.options;
- layout.currentStep = 0;
- layout.forces = series[0] && series[0].forces || [];
- layout.chart = series[0] && series[0].chart;
- if (layout.initialRendering) {
- layout.initPositions();
- // Render elements in initial positions:
- series.forEach(function (s) {
- s.finishedAnimating = true; // #13169
- s.render();
- });
- }
- layout.setK();
- layout.resetSimulation(options);
- if (layout.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 (layout.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);
- },
- addElementsToCollection: function (elements, collection) {
- elements.forEach(function (elem) {
- if (collection.indexOf(elem) === -1) {
- collection.push(elem);
- }
- });
- },
- removeElementFromCollection: function (element, collection) {
- var index = collection.indexOf(element);
- if (index !== -1) {
- collection.splice(index, 1);
- }
- },
- 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();
- },
- restartSimulation: function () {
- if (!this.simulation) {
- // When dragging nodes, we don't need to calculate
- // initial positions and rendering nodes:
- this.setInitialRendering(false);
- // Start new simulation:
- if (!this.enableSimulation) {
- // Run only one iteration to speed things up:
- this.setMaxIterations(1);
- }
- else {
- this.start();
- }
- if (this.chart) {
- this.chart.redraw();
- }
- // Restore defaults:
- this.setInitialRendering(true);
- }
- else {
- // Extend current simulation:
- this.resetSimulation();
- }
- },
- setMaxIterations: function (maxIterations) {
- this.maxIterations = pick(maxIterations, this.options.maxIterations);
- },
- setTemperature: function () {
- this.temperature = this.startTemperature =
- Math.sqrt(this.nodes.length);
- },
- setDiffTemperature: function () {
- this.diffTemperature = this.startTemperature /
- (this.options.maxIterations + 1);
- },
- setInitialRendering: function (enable) {
- this.initialRendering = enable;
- },
- createQuadTree: function () {
- this.quadTree = new H.QuadTree(this.box.left, this.box.top, this.box.width, this.box.height);
- this.quadTree.insertNodes(this.nodes);
- },
- initPositions: function () {
- var initialPositions = this.options.initialPositions;
- if (isFunction(initialPositions)) {
- initialPositions.call(this);
- this.nodes.forEach(function (node) {
- if (!defined(node.prevX)) {
- node.prevX = node.plotX;
- }
- if (!defined(node.prevY)) {
- node.prevY = node.plotY;
- }
- node.dispX = 0;
- node.dispY = 0;
- });
- }
- else if (initialPositions === 'circle') {
- this.setCircularPositions();
- }
- else {
- this.setRandomPositions();
- }
- },
- setCircularPositions: function () {
- var box = this.box, nodes = this.nodes, nodesLength = nodes.length + 1, angle = 2 * Math.PI / nodesLength, rootNodes = nodes.filter(function (node) {
- return node.linksTo.length === 0;
- }), sortedNodes = [], visitedNodes = {}, radius = this.options.initialPositionRadius;
- /**
- * @private
- */
- function addToNodes(node) {
- node.linksFrom.forEach(function (link) {
- if (!visitedNodes[link.toNode.id]) {
- visitedNodes[link.toNode.id] = true;
- sortedNodes.push(link.toNode);
- addToNodes(link.toNode);
- }
- });
- }
- // Start with identified root nodes an sort the nodes by their
- // hierarchy. In trees, this ensures that branches don't cross
- // eachother.
- rootNodes.forEach(function (rootNode) {
- sortedNodes.push(rootNode);
- addToNodes(rootNode);
- });
- // Cyclic tree, no root node found
- if (!sortedNodes.length) {
- sortedNodes = nodes;
- // Dangling, cyclic trees
- }
- else {
- nodes.forEach(function (node) {
- if (sortedNodes.indexOf(node) === -1) {
- sortedNodes.push(node);
- }
- });
- }
- // Initial positions are laid out along a small circle, appearing
- // as a cluster in the middle
- sortedNodes.forEach(function (node, index) {
- node.plotX = node.prevX = pick(node.plotX, box.width / 2 + radius * Math.cos(index * angle));
- node.plotY = node.prevY = pick(node.plotY, box.height / 2 + radius * Math.sin(index * angle));
- node.dispX = 0;
- node.dispY = 0;
- });
- },
- setRandomPositions: function () {
- var box = this.box, nodes = this.nodes, nodesLength = nodes.length + 1;
- /**
- * Return a repeatable, quasi-random number based on an integer
- * input. For the initial positions
- * @private
- */
- function unrandom(n) {
- var rand = n * n / Math.PI;
- rand = rand - Math.floor(rand);
- return rand;
- }
- // Initial positions:
- nodes.forEach(function (node, index) {
- node.plotX = node.prevX = pick(node.plotX, box.width * unrandom(index));
- node.plotY = node.prevY = pick(node.plotY, box.height * unrandom(nodesLength + index));
- node.dispX = 0;
- node.dispY = 0;
- });
- },
- force: function (name) {
- this.integration[name].apply(this, Array.prototype.slice.call(arguments, 1));
- },
- barycenterForces: function () {
- this.getBarycenter();
- this.force('barycenter');
- },
- getBarycenter: function () {
- var systemMass = 0, cx = 0, cy = 0;
- this.nodes.forEach(function (node) {
- cx += node.plotX * node.mass;
- cy += node.plotY * node.mass;
- systemMass += node.mass;
- });
- this.barycenter = {
- x: cx,
- y: cy,
- xFactor: cx / systemMass,
- yFactor: cy / systemMass
- };
- return this.barycenter;
- },
- barnesHutApproximation: function (node, quadNode) {
- var layout = this, distanceXY = layout.getDistXY(node, quadNode), distanceR = layout.vectorLength(distanceXY), goDeeper, force;
- if (node !== quadNode && distanceR !== 0) {
- if (quadNode.isInternal) {
- // Internal node:
- if (quadNode.boxSize / distanceR <
- layout.options.theta &&
- distanceR !== 0) {
- // Treat as an external node:
- force = layout.repulsiveForce(distanceR, layout.k);
- layout.force('repulsive', node, force * quadNode.mass, distanceXY, distanceR);
- goDeeper = false;
- }
- else {
- // Go deeper:
- goDeeper = true;
- }
- }
- else {
- // External node, direct force:
- force = layout.repulsiveForce(distanceR, layout.k);
- layout.force('repulsive', node, force * quadNode.mass, distanceXY, distanceR);
- }
- }
- return goDeeper;
- },
- repulsiveForces: function () {
- var layout = this;
- if (layout.approximation === 'barnes-hut') {
- layout.nodes.forEach(function (node) {
- layout.quadTree.visitNodeRecursive(null, function (quadNode) {
- return layout.barnesHutApproximation(node, quadNode);
- });
- });
- }
- else {
- layout.nodes.forEach(function (node) {
- layout.nodes.forEach(function (repNode) {
- var force, distanceR, distanceXY;
- if (
- // Node can not repulse itself:
- node !== repNode &&
- // Only close nodes affect each other:
- // layout.getDistR(node, repNode) < 2 * k &&
- // Not dragged:
- !node.fixedPosition) {
- distanceXY = layout.getDistXY(node, repNode);
- distanceR = layout.vectorLength(distanceXY);
- if (distanceR !== 0) {
- force = layout.repulsiveForce(distanceR, layout.k);
- layout.force('repulsive', node, force * repNode.mass, distanceXY, distanceR);
- }
- }
- });
- });
- }
- },
- attractiveForces: function () {
- var layout = this, distanceXY, distanceR, force;
- layout.links.forEach(function (link) {
- if (link.fromNode && link.toNode) {
- distanceXY = layout.getDistXY(link.fromNode, link.toNode);
- distanceR = layout.vectorLength(distanceXY);
- if (distanceR !== 0) {
- force = layout.attractiveForce(distanceR, layout.k);
- layout.force('attractive', link, force, distanceXY, distanceR);
- }
- }
- });
- },
- applyLimits: function () {
- var layout = this, nodes = layout.nodes;
- nodes.forEach(function (node) {
- if (node.fixedPosition) {
- return;
- }
- layout.integration.integrate(layout, node);
- layout.applyLimitBox(node, layout.box);
- // Reset displacement:
- node.dispX = 0;
- node.dispY = 0;
- });
- },
- /**
- * External box that nodes should fall. When hitting an edge, node
- * should stop or bounce.
- * @private
- */
- applyLimitBox: function (node, box) {
- var radius = node.radius;
- /*
- TO DO: Consider elastic collision instead of stopping.
- o' means end position when hitting plotting area edge:
- - "inelastic":
- o
- \
- ______
- | o'
- | \
- | \
- - "elastic"/"bounced":
- o
- \
- ______
- | ^
- | / \
- |o' \
- Euler sample:
- if (plotX < 0) {
- plotX = 0;
- dispX *= -1;
- }
- if (plotX > box.width) {
- plotX = box.width;
- dispX *= -1;
- }
- */
- // Limit X-coordinates:
- node.plotX = clamp(node.plotX, box.left + radius, box.width - radius);
- // Limit Y-coordinates:
- node.plotY = clamp(node.plotY, box.top + radius, box.height - radius);
- },
- /**
- * From "A comparison of simulated annealing cooling strategies" by
- * Nourani and Andresen work.
- * @private
- */
- coolDown: function (temperature, temperatureStep, currentStep) {
- // Logarithmic:
- /*
- return Math.sqrt(this.nodes.length) -
- Math.log(
- currentStep * layout.diffTemperature
- );
- */
- // Exponential:
- /*
- var alpha = 0.1;
- layout.temperature = Math.sqrt(layout.nodes.length) *
- Math.pow(alpha, layout.diffTemperature);
- */
- // Linear:
- return temperature - temperatureStep * currentStep;
- },
- isStable: function () {
- return Math.abs(this.systemTemperature -
- this.prevSystemTemperature) < 0.00001 || this.temperature <= 0;
- },
- getSystemTemperature: function () {
- return this.nodes.reduce(function (value, node) {
- return value + node.temperature;
- }, 0);
- },
- vectorLength: function (vector) {
- return Math.sqrt(vector.x * vector.x + vector.y * vector.y);
- },
- getDistR: function (nodeA, nodeB) {
- var distance = this.getDistXY(nodeA, nodeB);
- return this.vectorLength(distance);
- },
- getDistXY: function (nodeA, nodeB) {
- var xDist = nodeA.plotX - nodeB.plotX, yDist = nodeA.plotY - nodeB.plotY;
- return {
- x: xDist,
- y: yDist,
- absX: Math.abs(xDist),
- absY: Math.abs(yDist)
- };
- }
- });
- /* ************************************************************************** *
- * Multiple series support:
- * ************************************************************************** */
- // Clear previous layouts
- addEvent(Chart, 'predraw', function () {
- if (this.graphLayoutsLookup) {
- this.graphLayoutsLookup.forEach(function (layout) {
- layout.stop();
- });
- }
- });
- addEvent(Chart, 'render', function () {
- var systemsStable, afterRender = false;
- /**
- * @private
- */
- function layoutStep(layout) {
- if (layout.maxIterations-- &&
- isFinite(layout.temperature) &&
- !layout.isStable() &&
- !layout.enableSimulation) {
- // Hook similar to build-in addEvent, but instead of
- // creating whole events logic, use just a function.
- // It's faster which is important for rAF code.
- // Used e.g. in packed-bubble series for bubble radius
- // calculations
- if (layout.beforeStep) {
- layout.beforeStep();
- }
- layout.step();
- systemsStable = false;
- afterRender = true;
- }
- }
- if (this.graphLayoutsLookup) {
- setAnimation(false, this);
- // Start simulation
- this.graphLayoutsLookup.forEach(function (layout) {
- layout.start();
- });
- // Just one sync step, to run different layouts similar to
- // async mode.
- while (!systemsStable) {
- systemsStable = true;
- this.graphLayoutsLookup.forEach(layoutStep);
- }
- if (afterRender) {
- this.series.forEach(function (s) {
- if (s && s.layout) {
- s.render();
- }
- });
- }
- }
- });
- // disable simulation before print if enabled
- addEvent(Chart, 'beforePrint', function () {
- if (this.graphLayoutsLookup) {
- this.graphLayoutsLookup.forEach(function (layout) {
- layout.updateSimulation(false);
- });
- this.redraw();
- }
- });
- // re-enable simulation after print
- addEvent(Chart, 'afterPrint', function () {
- if (this.graphLayoutsLookup) {
- this.graphLayoutsLookup.forEach(function (layout) {
- // return to default simulation
- layout.updateSimulation();
- });
- }
- this.redraw();
- });
|