UNPKG

react-sigma-conglei

Version:

Lightweight but powerful library for drawing network graphs built on top of dunnock/react-sigma

1,604 lines (1,317 loc) 58.5 kB
var Sigma = /******/ (function(modules) { // webpackBootstrap /******/ // The module cache /******/ var installedModules = {}; /******/ /******/ // The require function /******/ function __webpack_require__(moduleId) { /******/ /******/ // Check if module is in cache /******/ if(installedModules[moduleId]) { /******/ return installedModules[moduleId].exports; /******/ } /******/ // Create a new module (and put it into the cache) /******/ var module = installedModules[moduleId] = { /******/ i: moduleId, /******/ l: false, /******/ exports: {} /******/ }; /******/ /******/ // Execute the module function /******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); /******/ /******/ // Flag the module as loaded /******/ module.l = true; /******/ /******/ // Return the exports of the module /******/ return module.exports; /******/ } /******/ /******/ /******/ // expose the modules object (__webpack_modules__) /******/ __webpack_require__.m = modules; /******/ /******/ // expose the module cache /******/ __webpack_require__.c = installedModules; /******/ /******/ // identity function for calling harmony imports with the correct context /******/ __webpack_require__.i = function(value) { return value; }; /******/ /******/ // define getter function for harmony exports /******/ __webpack_require__.d = function(exports, name, getter) { /******/ if(!__webpack_require__.o(exports, name)) { /******/ Object.defineProperty(exports, name, { /******/ configurable: false, /******/ enumerable: true, /******/ get: getter /******/ }); /******/ } /******/ }; /******/ /******/ // getDefaultExport function for compatibility with non-harmony modules /******/ __webpack_require__.n = function(module) { /******/ var getter = module && module.__esModule ? /******/ function getDefault() { return module['default']; } : /******/ function getModuleExports() { return module; }; /******/ __webpack_require__.d(getter, 'a', getter); /******/ return getter; /******/ }; /******/ /******/ // Object.prototype.hasOwnProperty.call /******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; /******/ /******/ // __webpack_public_path__ /******/ __webpack_require__.p = ""; /******/ /******/ // Load entry module and return exports /******/ return __webpack_require__(__webpack_require__.s = 135); /******/ }) /************************************************************************/ /******/ ({ /***/ 135: /***/ (function(module, exports, __webpack_require__) { __webpack_require__(17); module.exports = __webpack_require__(18); /***/ }), /***/ 17: /***/ (function(module, exports) { /*** IMPORTS FROM imports-loader ***/ (function() { ;(function(undefined) { 'use strict'; if (typeof sigma === 'undefined') throw new Error('sigma is not declared'); // Initialize package: sigma.utils.pkg('sigma.layouts'); /** * Sigma ForceLink Supervisor * =============================== * * Author: Guillaume Plique (Yomguithereal) * Extensions author: Sébastien Heymann @ Linkurious * Version: 0.1 */ var _root = this; /** * Feature detection * ------------------ */ var webWorkers = 'Worker' in _root; /** * Event emitter Object * ------------------ */ var eventEmitter = {}; sigma.classes.dispatcher.extend(eventEmitter); /** * Supervisor Object * ------------------ */ function Supervisor(sigInst, options) { // Window URL Polyfill _root.URL = _root.URL || _root.webkitURL; options = options || {}; // Properties this.sigInst = sigInst; this.graph = this.sigInst.graph; this.ppn = 10; this.ppe = 3; this.config = {}; this.worker = null; this.shouldUseWorker = null; this.workerUrl = null; this.runOnBackground = null; this.easing = null; this.randomize = null; this.configure(options); // State this.started = false; this.running = false; this.initWorker(); } Supervisor.prototype.makeBlob = function(workerFn) { var blob; try { blob = new Blob([workerFn], {type: 'application/javascript'}); } catch (e) { _root.BlobBuilder = _root.BlobBuilder || _root.WebKitBlobBuilder || _root.MozBlobBuilder; blob = new BlobBuilder(); blob.append(workerFn); blob = blob.getBlob(); } return blob; }; Supervisor.prototype.graphToByteArrays = function() { var nodes = this.graph.nodes(), edges = this.graph.edges(), nbytes = nodes.length * this.ppn, ebytes = edges.length * this.ppe, nIndex = {}, i, j, l; // Allocating Byte arrays with correct nb of bytes this.nodesByteArray = new Float32Array(nbytes); this.edgesByteArray = new Float32Array(ebytes); // Iterate through nodes for (i = j = 0, l = nodes.length; i < l; i++) { // Populating index nIndex[nodes[i].id] = j; // Populating byte array this.nodesByteArray[j] = this.randomize(nodes[i].x); this.nodesByteArray[j + 1] = this.randomize(nodes[i].y); this.nodesByteArray[j + 2] = 0; this.nodesByteArray[j + 3] = 0; this.nodesByteArray[j + 4] = 0; this.nodesByteArray[j + 5] = 0; this.nodesByteArray[j + 6] = 1 + this.graph.degree(nodes[i].id); this.nodesByteArray[j + 7] = 1; this.nodesByteArray[j + 8] = nodes[i].size; this.nodesByteArray[j + 9] = nodes[i].fixed || 0; j += this.ppn; } // Iterate through edges for (i = j = 0, l = edges.length; i < l; i++) { this.edgesByteArray[j] = nIndex[edges[i].source]; this.edgesByteArray[j + 1] = nIndex[edges[i].target]; this.edgesByteArray[j + 2] = edges[i].weight || 0; j += this.ppe; } }; // TODO: make a better send function Supervisor.prototype.applyLayoutChanges = function(prefixed) { var nodes = this.graph.nodes(), j = 0, realIndex; // Moving nodes for (var i = 0, l = this.nodesByteArray.length; i < l; i += this.ppn) { if (!nodes[j].fixed) { if (prefixed) { nodes[j].fa2_x = this.nodesByteArray[i]; nodes[j].fa2_y = this.nodesByteArray[i + 1]; } else { nodes[j].x = this.nodesByteArray[i]; nodes[j].y = this.nodesByteArray[i + 1]; } } j++; } }; Supervisor.prototype.sendByteArrayToWorker = function(action) { var content = { action: action || 'loop', nodes: this.nodesByteArray.buffer }; var buffers = [this.nodesByteArray.buffer]; if (action === 'start') { content.config = this.config || {}; content.edges = this.edgesByteArray.buffer; buffers.push(this.edgesByteArray.buffer); } if (this.shouldUseWorker) this.worker.postMessage(content, buffers); else _root.postMessage(content, '*'); }; Supervisor.prototype.start = function() { if (this.running) return; this.running = true; if (!this.started) { // Sending init message to worker this.sendByteArrayToWorker('start'); this.started = true; eventEmitter.dispatchEvent('start'); } else { this.sendByteArrayToWorker(); } }; Supervisor.prototype.stop = function() { if (!this.running) return; this.running = false; eventEmitter.dispatchEvent('stop'); }; Supervisor.prototype.initWorker = function() { var _this = this, workerFn = sigma.layouts.getForceLinkWorker(); // Web worker or classic DOM events? if (this.shouldUseWorker) { if (!this.workerUrl) { var blob = this.makeBlob(workerFn); this.worker = new Worker(URL.createObjectURL(blob)); } else { this.worker = new Worker(this.workerUrl); } // Post Message Polyfill this.worker.postMessage = this.worker.webkitPostMessage || this.worker.postMessage; } else { // TODO: do we crush? eval(workerFn); } // Worker message receiver this.msgName = (this.worker) ? 'message' : 'newCoords'; this.listener = function(e) { // Retrieving data _this.nodesByteArray = new Float32Array(e.data.nodes); // If ForceLink is running, we act accordingly if (_this.running) { // Applying layout _this.applyLayoutChanges(_this.runOnBackground); // Send data back to worker and loop _this.sendByteArrayToWorker(); // Rendering graph if (!_this.runOnBackground) _this.sigInst.refresh({skipIndexation: true}); } // Stop ForceLink if it has converged if (e.data.converged) { _this.running = false; } if (!_this.running) { _this.killWorker(); if (_this.runOnBackground && _this.easing) { _this.applyLayoutChanges(true); eventEmitter.dispatchEvent('interpolate'); // reset fa_x/y in case a pinned node was previously layed out _this.graph.nodes() .filter(function(node) { return node.fixed; }) .forEach(function(node) { node.fa2_x = node.x; node.fa2_y = node.y; }); sigma.plugins.animate( _this.sigInst, { x: 'fa2_x', y: 'fa2_y' }, { easing: _this.easing, onComplete: function() { _this.sigInst.refresh(); eventEmitter.dispatchEvent('stop'); } } ); } else { _this.applyLayoutChanges(false); _this.sigInst.refresh(); eventEmitter.dispatchEvent('stop'); } } }; (this.worker || document).addEventListener(this.msgName, this.listener); // Filling byteArrays this.graphToByteArrays(); // Binding on kill to properly terminate layout when parent is killed _this.sigInst.bind('kill', function() { sigma.layouts.killForceLink(); }); }; Supervisor.prototype.killWorker = function() { if (this.worker) { this.worker.terminate(); } else { _root.postMessage({action: 'kill'}, '*'); document.removeEventListener(this.msgName, this.listener); } }; Supervisor.prototype.configure = function(config) { // Setting configuration this.config = config; this.shouldUseWorker = config.worker === false ? false : true && webWorkers; this.workerUrl = config.workerUrl; this.runOnBackground = (config.background) ? true : false; this.easing = config.easing; switch (config.randomize) { case 'globally': this.randomize = function(x) { return Math.random() * (config.randomizeFactor || 1) }; break; case 'locally': this.randomize = function(x) { return x + (Math.random() * (config.randomizeFactor || 1)) }; break; default: this.randomize = function(x) { return x }; } if (!this.started) return; var data = {action: 'config', config: this.config}; if (this.shouldUseWorker) this.worker.postMessage(data); else _root.postMessage(data, '*'); }; /** * Interface * ---------- */ var supervisor = null; sigma.layouts.startForceLink = function(sigInst, config) { // Create supervisor if undefined if (!supervisor) { supervisor = new Supervisor(sigInst, config); } else if (!supervisor.running) { supervisor.killWorker(); supervisor.initWorker(); supervisor.started = false; } // Configuration provided? if (config) supervisor.configure(config); // Start algorithm supervisor.start(); return eventEmitter; }; sigma.layouts.stopForceLink = function() { if (!supervisor) return; // Stop algorithm supervisor.stop(); return supervisor; }; sigma.layouts.killForceLink = function() { if (!supervisor) return; // Stop Algorithm supervisor.stop(); // Kill Worker supervisor.killWorker(); // Kill supervisor supervisor = null; eventEmitter = {}; sigma.classes.dispatcher.extend(eventEmitter); }; sigma.layouts.configForceLink = function(sigInst, config) { if (!supervisor) { supervisor = new Supervisor(sigInst, config); } else if (!supervisor.running) { supervisor.killWorker(); supervisor.initWorker(); supervisor.started = false; } supervisor.configure(config); return eventEmitter; }; sigma.layouts.isForceLinkRunning = function() { return !!supervisor && supervisor.running; }; }).call(this); }.call(window)); /***/ }), /***/ 18: /***/ (function(module, exports) { /*** IMPORTS FROM imports-loader ***/ (function() { ;(function(undefined) { 'use strict'; if (typeof sigma === 'undefined') throw new Error('sigma is not declared'); // Initialize package: sigma.utils.pkg('sigma.layouts'); /** * Sigma ForceLink Webworker * ============================== * * Author: Guillaume Plique (Yomguithereal) * Algorithm author: Mathieu Jacomy @ Sciences Po Medialab & WebAtlas * Extensions author: Sébastien Heymann @ Linkurious * Version: 1.0.0 */ var _root = this, inWebWorker = !('document' in _root); /** * Worker Function Wrapper * ------------------------ * * The worker has to be wrapped into a single stringified function * to be passed afterwards as a BLOB object to the supervisor. */ var Worker = function(undefined) { 'use strict'; /** * Worker settings and properties */ var W = { // Properties ppn: 10, ppe: 3, ppr: 9, maxForce: 10, iterations: 0, converged: false, // Possible to change through config settings: { // force atlas 2: linLogMode: false, outboundAttractionDistribution: false, adjustSizes: false, edgeWeightInfluence: 0, scalingRatio: 1, strongGravityMode: false, gravity: 1, slowDown: 1, barnesHutOptimize: false, barnesHutTheta: 0.5, startingIterations: 1, iterationsPerRender: 1, // stopping condition: maxIterations: 1000, avgDistanceThreshold: 0.01, autoStop: false, // node siblings: alignNodeSiblings: false, nodeSiblingsScale: 1, nodeSiblingsAngleMin: 0, minNodeDistance: 0 } }; var NodeMatrix, EdgeMatrix, RegionMatrix; /** * Helpers */ function extend() { var i, k, res = {}, l = arguments.length; for (i = l - 1; i >= 0; i--) for (k in arguments[i]) res[k] = arguments[i][k]; return res; } function __emptyObject(obj) { var k; for (k in obj) if (!('hasOwnProperty' in obj) || obj.hasOwnProperty(k)) delete obj[k]; return obj; } /** * Return the euclidian distance between two points of a plane * with an orthonormal basis. * * @param {number} x0 The X coordinate of the first point. * @param {number} y0 The Y coordinate of the first point. * @param {number} x1 The X coordinate of the second point. * @param {number} y1 The Y coordinate of the second point. * @return {number} The euclidian distance. */ function getDistance(x0, y0, x1, y1) { return Math.sqrt((x1 - x0) * (x1 - x0) + (y1 - y0) * (y1 - y0)); } /** * Scale a value from the range [baseMin, baseMax] to the range * [limitMin, limitMax]. * * @param {number} value The value to rescale. * @param {number} baseMin The min value of the range of origin. * @param {number} baseMax The max value of the range of origin. * @param {number} limitMin The min value of the range of destination. * @param {number} limitMax The max value of the range of destination. * @return {number} The scaled value. */ function scaleRange(value, baseMin, baseMax, limitMin, limitMax) { return ((limitMax - limitMin) * (value - baseMin) / (baseMax - baseMin)) + limitMin; } /** * Get the angle of the vector (in radian). * * @param {object} v The 2d vector with x,y coordinates. * @return {number} The angle of the vector (in radian). */ function getVectorAngle(v) { return Math.acos( v.x / Math.sqrt(v.x * v.x + v.y * v.y) ); } /** * Get the normal vector of the line segment, i.e. the vector * orthogonal to the line. * http://stackoverflow.com/a/1243614/ * * @param {number} aX The x coorinates of the start point. * @param {number} aY The y coorinates of the start point. * @param {number} bX The x coorinates of the end point. * @param {number} bY The y coorinates of the end point. * @return {object} The 2d vector with (xi,yi), (xi_prime,yi_prime) coordinates. */ function getNormalVector(aX, aY, bX, bY) { return { xi: -(bY - aY), yi: bX - aX, xi_prime: bY - aY, yi_prime: -(bX - aX) }; } /** * Get the normalized vector. * * @param {object} v The 2d vector with (xi,yi), (xi_prime,yi_prime) coordinates. * @param {number} length The vector length. * @return {object} The normalized vector */ function getNormalizedVector(v, length) { return { x: (v.xi_prime - v.xi) / length, y: (v.yi_prime - v.yi) / length }; } /** * Get the a point the line segment [A,B] at a specified distance percentage * from the start point. * * @param {number} aX The x coorinates of the start point. * @param {number} aY The y coorinates of the start point. * @param {number} bX The x coorinates of the end point. * @param {number} bY The y coorinates of the end point. * @param {number} t The distance percentage from the start point. * @return {object} The (x,y) coordinates of the point. */ function getPointOnLineSegment(aX, aY, bX, bY, t) { return { x: aX + (bX - aX) * t, y: aY + (bY - aY) * t }; } /** * Matrices properties accessors */ var nodeProperties = { x: 0, y: 1, dx: 2, dy: 3, old_dx: 4, old_dy: 5, mass: 6, convergence: 7, size: 8, fixed: 9 }; var edgeProperties = { source: 0, target: 1, weight: 2 }; var regionProperties = { node: 0, centerX: 1, centerY: 2, size: 3, nextSibling: 4, firstChild: 5, mass: 6, massCenterX: 7, massCenterY: 8 }; function np(i, p) { // DEBUG: safeguards if ((i % W.ppn) !== 0) throw new Error('Invalid argument in np: "i" is not correct (' + i + ').'); if (i !== parseInt(i)) throw new TypeError('Invalid argument in np: "i" is not an integer.'); if (p in nodeProperties) return i + nodeProperties[p]; else throw new Error('ForceLink.Worker - ' + 'Inexistant node property given (' + p + ').'); } function ep(i, p) { // DEBUG: safeguards if ((i % W.ppe) !== 0) throw new Error('Invalid argument in ep: "i" is not correct (' + i + ').'); if (i !== parseInt(i)) throw new TypeError('Invalid argument in ep: "i" is not an integer.'); if (p in edgeProperties) return i + edgeProperties[p]; else throw new Error('ForceLink.Worker - ' + 'Inexistant edge property given (' + p + ').'); } function rp(i, p) { // DEBUG: safeguards if ((i % W.ppr) !== 0) throw new Error('Invalid argument in rp: "i" is not correct (' + i + ').'); if (i !== parseInt(i)) throw new TypeError('Invalid argument in rp: "i" is not an integer.'); if (p in regionProperties) return i + regionProperties[p]; else throw new Error('ForceLink.Worker - ' + 'Inexistant region property given (' + p + ').'); } /** * Algorithm initialization */ function init(nodes, edges, config) { config = config || {}; var i, l; // Matrices NodeMatrix = nodes; EdgeMatrix = edges; // Length W.nodesLength = NodeMatrix.length; W.edgesLength = EdgeMatrix.length; // Merging configuration configure(config); } function configure(o) { W.settings = extend(o, W.settings); } /** * Algorithm pass */ // MATH: get distances stuff and power 2 issues function pass() { var a, i, j, l, r, n, n1, n2, e, w, g, k, m; var outboundAttCompensation, coefficient, xDist, yDist, oldxDist, oldyDist, ewc, mass, distance, size, factor, minNodeDistance = W.settings.minNodeDistance; // 1) Initializing layout data //----------------------------- // Resetting positions & computing max values for (n = 0; n < W.nodesLength; n += W.ppn) { NodeMatrix[np(n, 'old_dx')] = NodeMatrix[np(n, 'dx')]; NodeMatrix[np(n, 'old_dy')] = NodeMatrix[np(n, 'dy')]; NodeMatrix[np(n, 'dx')] = 0; NodeMatrix[np(n, 'dy')] = 0; } // If outbound attraction distribution, compensate if (W.settings.outboundAttractionDistribution) { outboundAttCompensation = 0; for (n = 0; n < W.nodesLength; n += W.ppn) { outboundAttCompensation += NodeMatrix[np(n, 'mass')]; } outboundAttCompensation /= W.nodesLength; } // 1.bis) Barnes-Hut computation //------------------------------ if (W.settings.barnesHutOptimize) { var minX = Infinity, maxX = -Infinity, minY = Infinity, maxY = -Infinity, q, q0, q1, q2, q3; // Setting up // RegionMatrix = new Float32Array(W.nodesLength / W.ppn * 4 * W.ppr); RegionMatrix = []; // Computing min and max values for (n = 0; n < W.nodesLength; n += W.ppn) { minX = Math.min(minX, NodeMatrix[np(n, 'x')]); maxX = Math.max(maxX, NodeMatrix[np(n, 'x')]); minY = Math.min(minY, NodeMatrix[np(n, 'y')]); maxY = Math.max(maxY, NodeMatrix[np(n, 'y')]); } // Build the Barnes Hut root region RegionMatrix[rp(0, 'node')] = -1; RegionMatrix[rp(0, 'centerX')] = (minX + maxX) / 2; RegionMatrix[rp(0, 'centerY')] = (minY + maxY) / 2; RegionMatrix[rp(0, 'size')] = Math.max(maxX - minX, maxY - minY); RegionMatrix[rp(0, 'nextSibling')] = -1; RegionMatrix[rp(0, 'firstChild')] = -1; RegionMatrix[rp(0, 'mass')] = 0; RegionMatrix[rp(0, 'massCenterX')] = 0; RegionMatrix[rp(0, 'massCenterY')] = 0; // Add each node in the tree l = 1; for (n = 0; n < W.nodesLength; n += W.ppn) { // Current region, starting with root r = 0; while (true) { // Are there sub-regions? // We look at first child index if (RegionMatrix[rp(r, 'firstChild')] >= 0) { // There are sub-regions // We just iterate to find a "leave" of the tree // that is an empty region or a region with a single node // (see next case) // Find the quadrant of n if (NodeMatrix[np(n, 'x')] < RegionMatrix[rp(r, 'centerX')]) { if (NodeMatrix[np(n, 'y')] < RegionMatrix[rp(r, 'centerY')]) { // Top Left quarter q = RegionMatrix[rp(r, 'firstChild')]; } else { // Bottom Left quarter q = RegionMatrix[rp(r, 'firstChild')] + W.ppr; } } else { if (NodeMatrix[np(n, 'y')] < RegionMatrix[rp(r, 'centerY')]) { // Top Right quarter q = RegionMatrix[rp(r, 'firstChild')] + W.ppr * 2; } else { // Bottom Right quarter q = RegionMatrix[rp(r, 'firstChild')] + W.ppr * 3; } } // Update center of mass and mass (we only do it for non-leave regions) RegionMatrix[rp(r, 'massCenterX')] = (RegionMatrix[rp(r, 'massCenterX')] * RegionMatrix[rp(r, 'mass')] + NodeMatrix[np(n, 'x')] * NodeMatrix[np(n, 'mass')]) / (RegionMatrix[rp(r, 'mass')] + NodeMatrix[np(n, 'mass')]); RegionMatrix[rp(r, 'massCenterY')] = (RegionMatrix[rp(r, 'massCenterY')] * RegionMatrix[rp(r, 'mass')] + NodeMatrix[np(n, 'y')] * NodeMatrix[np(n, 'mass')]) / (RegionMatrix[rp(r, 'mass')] + NodeMatrix[np(n, 'mass')]); RegionMatrix[rp(r, 'mass')] += NodeMatrix[np(n, 'mass')]; // Iterate on the right quadrant r = q; continue; } else { // There are no sub-regions: we are in a "leave" // Is there a node in this leave? if (RegionMatrix[rp(r, 'node')] < 0) { // There is no node in region: // we record node n and go on RegionMatrix[rp(r, 'node')] = n; break; } else { // There is a node in this region // We will need to create sub-regions, stick the two // nodes (the old one r[0] and the new one n) in two // subregions. If they fall in the same quadrant, // we will iterate. // Create sub-regions RegionMatrix[rp(r, 'firstChild')] = l * W.ppr; w = RegionMatrix[rp(r, 'size')] / 2; // new size (half) // NOTE: we use screen coordinates // from Top Left to Bottom Right // Top Left sub-region g = RegionMatrix[rp(r, 'firstChild')]; RegionMatrix[rp(g, 'node')] = -1; RegionMatrix[rp(g, 'centerX')] = RegionMatrix[rp(r, 'centerX')] - w; RegionMatrix[rp(g, 'centerY')] = RegionMatrix[rp(r, 'centerY')] - w; RegionMatrix[rp(g, 'size')] = w; RegionMatrix[rp(g, 'nextSibling')] = g + W.ppr; RegionMatrix[rp(g, 'firstChild')] = -1; RegionMatrix[rp(g, 'mass')] = 0; RegionMatrix[rp(g, 'massCenterX')] = 0; RegionMatrix[rp(g, 'massCenterY')] = 0; // Bottom Left sub-region g += W.ppr; RegionMatrix[rp(g, 'node')] = -1; RegionMatrix[rp(g, 'centerX')] = RegionMatrix[rp(r, 'centerX')] - w; RegionMatrix[rp(g, 'centerY')] = RegionMatrix[rp(r, 'centerY')] + w; RegionMatrix[rp(g, 'size')] = w; RegionMatrix[rp(g, 'nextSibling')] = g + W.ppr; RegionMatrix[rp(g, 'firstChild')] = -1; RegionMatrix[rp(g, 'mass')] = 0; RegionMatrix[rp(g, 'massCenterX')] = 0; RegionMatrix[rp(g, 'massCenterY')] = 0; // Top Right sub-region g += W.ppr; RegionMatrix[rp(g, 'node')] = -1; RegionMatrix[rp(g, 'centerX')] = RegionMatrix[rp(r, 'centerX')] + w; RegionMatrix[rp(g, 'centerY')] = RegionMatrix[rp(r, 'centerY')] - w; RegionMatrix[rp(g, 'size')] = w; RegionMatrix[rp(g, 'nextSibling')] = g + W.ppr; RegionMatrix[rp(g, 'firstChild')] = -1; RegionMatrix[rp(g, 'mass')] = 0; RegionMatrix[rp(g, 'massCenterX')] = 0; RegionMatrix[rp(g, 'massCenterY')] = 0; // Bottom Right sub-region g += W.ppr; RegionMatrix[rp(g, 'node')] = -1; RegionMatrix[rp(g, 'centerX')] = RegionMatrix[rp(r, 'centerX')] + w; RegionMatrix[rp(g, 'centerY')] = RegionMatrix[rp(r, 'centerY')] + w; RegionMatrix[rp(g, 'size')] = w; RegionMatrix[rp(g, 'nextSibling')] = RegionMatrix[rp(r, 'nextSibling')]; RegionMatrix[rp(g, 'firstChild')] = -1; RegionMatrix[rp(g, 'mass')] = 0; RegionMatrix[rp(g, 'massCenterX')] = 0; RegionMatrix[rp(g, 'massCenterY')] = 0; l += 4; // Now the goal is to find two different sub-regions // for the two nodes: the one previously recorded (r[0]) // and the one we want to add (n) // Find the quadrant of the old node if (NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'x')] < RegionMatrix[rp(r, 'centerX')]) { if (NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'y')] < RegionMatrix[rp(r, 'centerY')]) { // Top Left quarter q = RegionMatrix[rp(r, 'firstChild')]; } else { // Bottom Left quarter q = RegionMatrix[rp(r, 'firstChild')] + W.ppr; } } else { if (NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'y')] < RegionMatrix[rp(r, 'centerY')]) { // Top Right quarter q = RegionMatrix[rp(r, 'firstChild')] + W.ppr * 2; } else { // Bottom Right quarter q = RegionMatrix[rp(r, 'firstChild')] + W.ppr * 3; } } // We remove r[0] from the region r, add its mass to r and record it in q RegionMatrix[rp(r, 'mass')] = NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'mass')]; RegionMatrix[rp(r, 'massCenterX')] = NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'x')]; RegionMatrix[rp(r, 'massCenterY')] = NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'y')]; RegionMatrix[rp(q, 'node')] = RegionMatrix[rp(r, 'node')]; RegionMatrix[rp(r, 'node')] = -1; // Find the quadrant of n if (NodeMatrix[np(n, 'x')] < RegionMatrix[rp(r, 'centerX')]) { if (NodeMatrix[np(n, 'y')] < RegionMatrix[rp(r, 'centerY')]) { // Top Left quarter q2 = RegionMatrix[rp(r, 'firstChild')]; } else { // Bottom Left quarter q2 = RegionMatrix[rp(r, 'firstChild')] + W.ppr; } } else { if(NodeMatrix[np(n, 'y')] < RegionMatrix[rp(r, 'centerY')]) { // Top Right quarter q2 = RegionMatrix[rp(r, 'firstChild')] + W.ppr * 2; } else { // Bottom Right quarter q2 = RegionMatrix[rp(r, 'firstChild')] + W.ppr * 3; } } if (q === q2) { // If both nodes are in the same quadrant, // we have to try it again on this quadrant r = q; continue; } // If both quadrants are different, we record n // in its quadrant RegionMatrix[rp(q2, 'node')] = n; break; } } } } } // 2) Repulsion //-------------- // NOTES: adjustSizes = antiCollision & scalingRatio = coefficient if (W.settings.barnesHutOptimize) { coefficient = W.settings.scalingRatio; // Applying repulsion through regions for (n = 0; n < W.nodesLength; n += W.ppn) { // Computing leaf quad nodes iteration r = 0; // Starting with root region while (true) { if (RegionMatrix[rp(r, 'firstChild')] >= 0) { // The region has sub-regions // We run the Barnes Hut test to see if we are at the right distance distance = Math.sqrt( (NodeMatrix[np(n, 'x')] - RegionMatrix[rp(r, 'massCenterX')]) * (NodeMatrix[np(n, 'x')] - RegionMatrix[rp(r, 'massCenterX')]) + (NodeMatrix[np(n, 'y')] - RegionMatrix[rp(r, 'massCenterY')]) * (NodeMatrix[np(n, 'y')] - RegionMatrix[rp(r, 'massCenterY')]) ); if (2 * RegionMatrix[rp(r, 'size')] / distance < W.settings.barnesHutTheta) { // We treat the region as a single body, and we repulse xDist = NodeMatrix[np(n, 'x')] - RegionMatrix[rp(r, 'massCenterX')]; yDist = NodeMatrix[np(n, 'y')] - RegionMatrix[rp(r, 'massCenterY')]; if (W.settings.adjustSizes) { //-- Linear Anti-collision Repulsion if (distance > 0) { factor = coefficient * NodeMatrix[np(n, 'mass')] * RegionMatrix[rp(r, 'mass')] / distance / distance; NodeMatrix[np(n, 'dx')] += xDist * factor; NodeMatrix[np(n, 'dy')] += yDist * factor; } else if (distance < 0) { factor = -coefficient * NodeMatrix[np(n, 'mass')] * RegionMatrix[rp(r, 'mass')] / distance; NodeMatrix[np(n, 'dx')] += xDist * factor; NodeMatrix[np(n, 'dy')] += yDist * factor; } } else { //-- Linear Repulsion if (distance > 0) { factor = coefficient * NodeMatrix[np(n, 'mass')] * RegionMatrix[rp(r, 'mass')] / distance / distance; NodeMatrix[np(n, 'dx')] += xDist * factor; NodeMatrix[np(n, 'dy')] += yDist * factor; } } // When this is done, we iterate. We have to look at the next sibling. if (RegionMatrix[rp(r, 'nextSibling')] < 0) break; // No next sibling: we have finished the tree r = RegionMatrix[rp(r, 'nextSibling')]; continue; } else { // The region is too close and we have to look at sub-regions r = RegionMatrix[rp(r, 'firstChild')]; continue; } } else { // The region has no sub-region // If there is a node r[0] and it is not n, then repulse if (RegionMatrix[rp(r, 'node')] >= 0 && RegionMatrix[rp(r, 'node')] !== n) { xDist = NodeMatrix[np(n, 'x')] - NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'x')]; yDist = NodeMatrix[np(n, 'y')] - NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'y')]; distance = Math.sqrt(xDist * xDist + yDist * yDist); if (W.settings.adjustSizes) { //-- Linear Anti-collision Repulsion if (distance > 0) { factor = coefficient * NodeMatrix[np(n, 'mass')] * NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'mass')] / distance / distance; NodeMatrix[np(n, 'dx')] += xDist * factor; NodeMatrix[np(n, 'dy')] += yDist * factor; } else if (distance < 0) { factor = -coefficient * NodeMatrix[np(n, 'mass')] * NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'mass')] / distance; NodeMatrix[np(n, 'dx')] += xDist * factor; NodeMatrix[np(n, 'dy')] += yDist * factor; } } else { //-- Linear Repulsion if (distance > 0) { factor = coefficient * NodeMatrix[np(n, 'mass')] * NodeMatrix[np(RegionMatrix[rp(r, 'node')], 'mass')] / distance / distance; NodeMatrix[np(n, 'dx')] += xDist * factor; NodeMatrix[np(n, 'dy')] += yDist * factor; } } } // When this is done, we iterate. We have to look at the next sibling. if (RegionMatrix[rp(r, 'nextSibling')] < 0) break; // No next sibling: we have finished the tree r = RegionMatrix[rp(r, 'nextSibling')]; continue; } } } } else { coefficient = W.settings.scalingRatio; // Square iteration for (n1 = 0; n1 < W.nodesLength; n1 += W.ppn) { for (n2 = 0; n2 < n1; n2 += W.ppn) { // Common to both methods xDist = NodeMatrix[np(n1, 'x')] - NodeMatrix[np(n2, 'x')]; yDist = NodeMatrix[np(n1, 'y')] - NodeMatrix[np(n2, 'y')]; if (W.settings.adjustSizes) { //-- Anticollision Linear Repulsion distance = Math.sqrt(xDist * xDist + yDist * yDist) - NodeMatrix[np(n1, 'size')] - NodeMatrix[np(n2, 'size')]; if (distance > 0) { factor = coefficient * NodeMatrix[np(n1, 'mass')] * NodeMatrix[np(n2, 'mass')] / distance / distance; // Updating nodes' dx and dy NodeMatrix[np(n1, 'dx')] += xDist * factor; NodeMatrix[np(n1, 'dy')] += yDist * factor; NodeMatrix[np(n2, 'dx')] += xDist * factor; NodeMatrix[np(n2, 'dy')] += yDist * factor; } else if (distance < 0) { factor = 100 * coefficient * NodeMatrix[np(n1, 'mass')] * NodeMatrix[np(n2, 'mass')]; // Updating nodes' dx and dy NodeMatrix[np(n1, 'dx')] += xDist * factor; NodeMatrix[np(n1, 'dy')] += yDist * factor; NodeMatrix[np(n2, 'dx')] -= xDist * factor; NodeMatrix[np(n2, 'dy')] -= yDist * factor; } } else { //-- Linear Repulsion distance = Math.sqrt(xDist * xDist + yDist * yDist); if (distance > 0) { factor = coefficient * NodeMatrix[np(n1, 'mass')] * NodeMatrix[np(n2, 'mass')] / distance / distance; // Updating nodes' dx and dy NodeMatrix[np(n1, 'dx')] += xDist * factor; NodeMatrix[np(n1, 'dy')] += yDist * factor; NodeMatrix[np(n2, 'dx')] -= xDist * factor; NodeMatrix[np(n2, 'dy')] -= yDist * factor; } } } } } // 3) Gravity //------------ g = W.settings.gravity / W.settings.scalingRatio; coefficient = W.settings.scalingRatio; for (n = 0; n < W.nodesLength; n += W.ppn) { factor = 0; // Common to both methods xDist = NodeMatrix[np(n, 'x')]; yDist = NodeMatrix[np(n, 'y')]; distance = Math.sqrt(xDist * xDist + yDist * yDist); if (W.settings.strongGravityMode) { //-- Strong gravity if (distance > 0) factor = coefficient * NodeMatrix[np(n, 'mass')] * g; } else { //-- Linear Anti-collision Repulsion n if (distance > 0) factor = coefficient * NodeMatrix[np(n, 'mass')] * g / distance; } // Updating node's dx and dy NodeMatrix[np(n, 'dx')] -= xDist * factor; NodeMatrix[np(n, 'dy')] -= yDist * factor; } // 4) Attraction //--------------- coefficient = 1 * (W.settings.outboundAttractionDistribution ? outboundAttCompensation : 1); // TODO: simplify distance // TODO: coefficient is always used as -c --> optimize? for (e = 0; e < W.edgesLength; e += W.ppe) { n1 = EdgeMatrix[ep(e, 'source')]; n2 = EdgeMatrix[ep(e, 'target')]; w = EdgeMatrix[ep(e, 'weight')]; // Edge weight influence ewc = Math.pow(w, W.settings.edgeWeightInfluence); // Common measures xDist = NodeMatrix[np(n1, 'x')] - NodeMatrix[np(n2, 'x')]; yDist = NodeMatrix[np(n1, 'y')] - NodeMatrix[np(n2, 'y')]; // Applying attraction to nodes if (W.settings.adjustSizes) { distance = Math.sqrt( (xDist * xDist + yDist * yDist) - NodeMatrix[np(n1, 'size')] - NodeMatrix[np(n2, 'size')] ) - minNodeDistance; if (W.settings.linLogMode) { if (W.settings.outboundAttractionDistribution) { //-- LinLog Degree Distributed Anti-collision Attraction if (distance > 0) { factor = -coefficient * ewc * Math.log(1 + distance) / distance / NodeMatrix[np(n1, 'mass')]; } } else { //-- LinLog Anti-collision Attraction if (distance > 0) { factor = -coefficient * ewc * Math.log(1 + distance) / distance; } } } else { if (W.settings.outboundAttractionDistribution) { //-- Linear Degree Distributed Anti-collision Attraction if (distance > 0) { factor = -coefficient * ewc / NodeMatrix[np(n1, 'mass')]; } } else { //-- Linear Anti-collision Attraction if (distance > 0) { factor = -coefficient * ewc; } } } } else { distance = Math.sqrt(xDist * xDist + yDist * yDist) - minNodeDistance; if (W.settings.linLogMode) { if (W.settings.outboundAttractionDistribution) { //-- LinLog Degree Distributed Attraction if (distance > 0) { factor = -coefficient * ewc * Math.log(1 + distance) / distance / NodeMatrix[np(n1, 'mass')]; } } else { //-- LinLog Attraction if (distance > 0) factor = -coefficient * ewc * Math.log(1 + distance) / distance; } } else { if (W.settings.outboundAttractionDistribution) { //-- Linear Attraction Mass Distributed // NOTE: Distance is set to 1 to override next condition distance = 1; factor = -coefficient * ewc / NodeMatrix[np(n1, 'mass')]; } else { //-- Linear Attraction // NOTE: Distance is set to 1 to override next condition distance = 1; factor = -coefficient * ewc; } } } // Updating nodes' dx and dy // TODO: if condition or factor = 1? if (distance > 0) { // Updating nodes' dx and dy NodeMatrix[np(n1, 'dx')] += xDist * factor; NodeMatrix[np(n1, 'dy')] += yDist * factor; NodeMatrix[np(n2, 'dx')] -= xDist * factor; NodeMatrix[np(n2, 'dy')] -= yDist * factor; } } // 5) Apply Forces //----------------- var force, swinging, traction, nodespeed, alldistance = 0; // MATH: sqrt and square distances if (W.settings.adjustSizes) { for (n = 0; n < W.nodesLength; n += W.ppn) { if (!NodeMatrix[np(n, 'fixed')]) { force = Math.sqrt( NodeMatrix[np(n, 'dx')] * NodeMatrix[np(n, 'dx')] + NodeMatrix[np(n, 'dy')] * NodeMatrix[np(n, 'dy')] ); if (force > W.maxForce) { NodeMatrix[np(n, 'dx')] = NodeMatrix[np(n, 'dx')] * W.maxForce / force; NodeMatrix[np(n, 'dy')] = NodeMatrix[np(n, 'dy')] * W.maxForce / force; } swinging = NodeMatrix[np(n, 'mass')] * Math.sqrt( (NodeMatrix[np(n, 'old_dx')] - NodeMatrix[np(n, 'dx')]) * (NodeMatrix[np(n, 'old_dx')] - NodeMatrix[np(n, 'dx')]) + (NodeMatrix[np(n, 'old_dy')] - NodeMatrix[np(n, 'dy')]) * (NodeMatrix[np(n, 'old_dy')] - NodeMatrix[np(n, 'dy')]) ); traction = Math.sqrt( (NodeMatrix[np(n, 'old_dx')] + NodeMatrix[np(n, 'dx')]) * (NodeMatrix[np(n, 'old_dx')] + NodeMatrix[np(n, 'dx')]) + (NodeMatrix[np(n, 'old_dy')] + NodeMatrix[np(n, 'dy')]) * (NodeMatrix[np(n, 'old_dy')] + NodeMatrix[np(n, 'dy')]) ) / 2; nodespeed = 0.1 * Math.log(1 + traction) / (1 + Math.sqrt(swinging)); oldxDist = NodeMatrix[np(n, 'x')]; oldyDist = NodeMatrix[np(n, 'y')]; // Updating node's positon NodeMatrix[np(n, 'x')] = NodeMatrix[np(n, 'x')] + NodeMatrix[np(n, 'dx')] * (nodespeed / W.settings.slowDown); NodeMatrix[np(n, 'y')] = NodeMatrix[np(n, 'y')] + NodeMatrix[np(n, 'dy')] * (nodespeed / W.settings.slowDown); xDist = NodeMatrix[np(n, 'x')]; yDist = NodeMatrix[np(n, 'y')]; distance = Math.sqrt( (xDist - oldxDist) * (xDist - oldxDist) + (yDist - oldyDist) * (yDist - oldyDist) ); alldistance += distance; } } } else { for (n = 0; n < W.nodesLength; n += W.ppn) { if (!NodeMatrix[np(n, 'fixed')]) { swinging = NodeMatrix[np(n, 'mass')] * Math.sqrt( (NodeMatrix[np(n, 'old_dx')] - NodeMatrix[np(n, 'dx')]) * (NodeMatrix[np(n, 'old_dx')] - NodeMatrix[np(n, 'dx')]) + (NodeMatrix[np(n, 'old_dy')] - NodeMatrix[np(n, 'dy')]) * (NodeMatrix[np(n, 'old_dy')] - NodeMatrix[np(n, 'dy')]) ); traction = Math.sqrt( (NodeMatrix[np(n, 'old_dx')] + NodeMatrix[np(n, 'dx')]) * (NodeMatrix[np(n, 'old_dx')] + NodeMatrix[np(n, 'dx')]) + (NodeMatrix[np(n, 'old_dy')] + NodeMatrix[np(n, 'dy')]) * (NodeMatrix[np(n, 'old_dy')] + NodeMatrix[np(n, 'dy')]) ) / 2; nodespeed = NodeMatrix[np(n, 'convergence')] * Math.log(1 + traction) / (1 + Math.sqrt(swinging)); // Updating node convergence NodeMatrix[np(n, 'convergence')] = Math.min(1, Math.sqrt( nodespeed * (NodeMatrix[np(n, 'dx')] * NodeMatrix[np(n, 'dx')] + NodeMatrix[np(n, 'dy')] * NodeMatrix[np(n, 'dy')]) / (1 + Math.sqrt(swinging)) )); oldxDist = NodeMatrix[np(n, 'x')]; oldyDist = NodeMatrix[np(n, 'y')]; // Updating node's positon NodeMatrix[np(n, 'x')] = NodeMatrix[np(n, 'x')] + NodeMatrix[np(n, 'dx')] * (nodespeed / W.settings.slowDown); NodeMatrix[np(n, 'y')] = NodeMatrix[np(n, 'y')] + NodeMatrix[np(n, 'dy')] * (nodespeed / W.settings.slowDown); xDist = NodeMatrix[np(n, 'x')]; yDist = NodeMatrix[np(n, 'y')]; distance = Math.sqrt( (xDist - oldxDist) * (xDist - oldxDist) + (yDist - oldyDist) * (yDist - oldyDist) ); alldistance += distance; } } } // Counting one more iteration W.iterations++; // Auto stop. // The greater the ratio nb nodes / nb edges, // the greater the number of iterations needed to converge. if (W.settings.autoStop) { W.converged = ( W.iterations > W.settings.maxIterations || alldistance / W.nodesLength < W.settings.avgDistanceThreshold ); // align nodes that are linked to the same two nodes only: if (W.converged && W.settings.alignNodeSiblings) { // console.time("alignment"); var neighbors = {}, // index of neighbors parallelNodes = {}, // set of parallel nodes indexed by same <source;target> setKey, // tmp keysN; // tmp // build index of neighbors: for (e = 0; e < W.edgesLength; e += W.ppe) { n1 = EdgeMatrix[ep(e, 'source')]; n2 = EdgeMatrix[ep(e, 'target')]; if (n1 === n2) continue; neighbors[n1] = neighbors[n1] || {}; neighbors[n2] = neighbors[n2] || {}; neighbors[n1][n2] = true; neighbors[n2][n1] = true; } // group triplets by same <source, target> (resp. target, source): Object.keys(neighbors).forEach(function(n) { n = ~~n; // string to int