UNPKG

sigma-react

Version:

A JavaScript library dedicated to graph drawing.

409 lines (332 loc) 10 kB
;(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);