UNPKG

sigma-react

Version:

A JavaScript library dedicated to graph drawing.

378 lines (316 loc) 12.2 kB
;(function(undefined) { 'use strict'; if (typeof sigma === 'undefined') throw new Error('sigma is not declared'); if (typeof dagre === 'undefined' || typeof dagre.graphlib === 'undefined') console.warn('to use the dagre plugin, ' +'you have to include dagre and dagre.graphlib'); // Initialize package: sigma.utils.pkg('sigma.layouts.dagre'); /** * Sigma Dagre layout * =============================== * * Require https://github.com/cpettitt/dagre * Author: Sébastien Heymann @ Linkurious * Version: 0.1 */ // see https://github.com/cpettitt/dagre/wiki#configuring-the-layout var settings = { directed: true, // take edge direction into account multigraph: true, // allows multiple edges between the same pair of nodes compound: false, // // dagre algo options rankDir: 'TB', // Direction for rank nodes. Can be TB, BT, LR, or RL, // where T = top, B = bottom, L = left, and R = right. }; var _instance = {}; /** * Event emitter Object * ------------------ */ var _eventEmitter = {}; function getBoundaries(nodes, prefix) { var i, l, prefix = prefix || '', sizeMax = -Infinity, minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity; for (i = 0, l = nodes.length; i < l; i++) { sizeMax = Math.max(nodes[i][prefix + 'size'], sizeMax); maxX = Math.max(nodes[i][prefix + 'x'], maxX); minX = Math.min(nodes[i][prefix + 'x'], minX); maxY = Math.max(nodes[i][prefix + 'y'], maxY); minY = Math.min(nodes[i][prefix + 'y'], minY); } sizeMax = sizeMax || 1; return { sizeMax: sizeMax, minX: minX, minY: minY, maxX: maxX, maxY: maxY }; }; function scaleRange (value, baseMin, baseMax, limitMin, limitMax) { return ((limitMax - limitMin) * (value - baseMin) / (baseMax - baseMin)) + limitMin; }; function rescalePosition(point, baseBox, limitBox) { return { x: scaleRange(point.x, baseBox.minX, baseBox.maxX, limitBox.minX, limitBox.maxX), y: scaleRange(point.y, baseBox.minY, baseBox.maxY, limitBox.minY, limitBox.maxY) } }; /** * Dagre Object * ------------------ */ function dagreLayout() { if (typeof dagre === 'undefined') throw new Error('dagre is not declared'); if (typeof dagre.graphlib === 'undefined') throw new Error('dagre.graphlib is not declared'); var self = this, dg; this.init = function (sigInst, options) { options = options || {}; if (options.nodes) { this.nodes = options.nodes; delete options.nodes; } if (options.boundingBox) { this.boundingBox = options.boundingBox; delete options.boundingBox; } // Properties this.sigInst = sigInst; this.config = sigma.utils.extend(options, settings); this.easing = options.easing; this.duration = options.duration; if (this.easing && (!sigma.plugins || typeof sigma.plugins.animate === 'undefined')) { throw new Error('sigma.plugins.animate is not declared'); } // State this.running = false; }; this.start = function() { if (this.running) return; this.running = true; // Create a new directed graph dg = new dagre.graphlib.Graph({ directed: this.config.directed, multigraph: this.config.multigraph, compound: this.config.compound }); // Set an object for the graph label dg.setGraph(this.config); var nodes = this.nodes || this.sigInst.graph.nodes(); for (var i = 0; i < nodes.length; i++) { if (!nodes[i].fixed) { dg.setNode(nodes[i].id, {}); } } if (this.boundingBox === true) { this.boundingBox = getBoundaries(nodes); } var edges = this.sigInst.graph.edges(); for (var i = 0; i < edges.length; i++) { if (dg.node(edges[i].source) != null && dg.node(edges[i].target) != null) { dg.setEdge(edges[i].source, edges[i].target, { id: edges[i].id }); } }; _eventEmitter[self.sigInst.id].dispatchEvent('start'); // console.time('sigma.layouts.dagre'); dagre.layout(dg); // console.timeEnd('sigma.layouts.dagre'); var edge; dg.edges().map(function(e) { edge = self.sigInst.graph.edges(dg.edge(e).id); edge.points = dg.edge(e).points; }); this.stop(); }; this.stop = function() { if (!dg) return; var nodes = dg.nodes().map(function(nid) { return self.sigInst.graph.nodes(nid) || self.sigInst.graph.nodes(Number(nid)); }); var coord; if (this.boundingBox) { var baseBoundingBox = getBoundaries(dg.nodes().map(function(nid) { return dg.node(nid); })); } this.running = false; if (this.easing) { // Set new node coordinates for (var i = 0; i < nodes.length; i++) { if (this.boundingBox) { coord = rescalePosition(dg.node(nodes[i].id), baseBoundingBox, self.boundingBox); nodes[i].dagre_x = coord.x; nodes[i].dagre_y = coord.y; } else { nodes[i].dagre_x = dg.node(nodes[i].id).x; nodes[i].dagre_y = dg.node(nodes[i].id).y; } } _eventEmitter[self.sigInst.id].dispatchEvent('interpolate'); sigma.plugins.animate( self.sigInst, { x: 'dagre_x', y: 'dagre_y' }, { nodes: nodes, easing: self.easing, onComplete: function() { for (var i = 0; i < nodes.length; i++) { nodes[i].dagre_x = null; nodes[i].dagre_y = null; } _eventEmitter[self.sigInst.id].dispatchEvent('stop'); self.sigInst.refresh(); }, duration: self.duration } ); } else { // Apply changes var node; dg.nodes().forEach(function(nid) { node = self.sigInst.graph.nodes(nid); node.x = dg.node(nid).x; node.y = dg.node(nid).y; }); _eventEmitter[self.sigInst.id].dispatchEvent('stop'); this.sigInst.refresh(); } }; this.kill = function() { this.sigInst = null; this.config = null; this.easing = null; }; }; /** * Interface * ---------- */ /** * Configure the layout algorithm. * Recognized options: * ********************** * Here is the exhaustive list of every accepted parameters in the settings * object: * * {?array} nodes The subset of nodes to apply the layout. * {?object} boundingBox Constrain layout bounds. Value: {minX, maxX, minY, maxY} * or true (all current positions of the given nodes) * {?boolean} directed If `true`, take edge direction into * account. Default: `true`. * {?boolean} multigraph If `true`, allows multiple edges between * the same pair of nodes. Default: `true`. * {?boolean} compound If `true`, allows ompound nodes, i.e. * nodes which can be the parent of other * nodes. Default: `false`. * {?string} rankDir Direction for rank nodes. Can be TB, BT, * LR, or RL, where T = top, B = bottom, * L = left, and R = right. * {?(function|string)} easing Either the name of an easing in the * sigma.utils.easings package or a * function. If not specified, the * quadraticInOut easing from this package * will be used instead. * {?number} duration The duration of the animation. If not * specified, the "animationsTime" setting * value of the sigma instance will be used * instead. * * * @param {sigma} sigInst The related sigma instance. * @param {object} config The optional configuration object. * * @return {sigma.classes.dispatcher} Returns an event emitter. */ sigma.layouts.dagre.configure = function(sigInst, config) { if (!sigInst) throw new Error('Missing argument: "sigInst"'); if (!config) throw new Error('Missing argument: "config"'); // Create instance if undefined if (!_instance[sigInst.id]) { _instance[sigInst.id] = new dagreLayout(); _eventEmitter[sigInst.id] = {}; sigma.classes.dispatcher.extend(_eventEmitter[sigInst.id]); // Binding on kill to clear the references sigInst.bind('kill', function() { _instance[sigInst.id].kill(); _instance[sigInst.id] = null; _eventEmitter[sigInst.id] = null; }); } _instance[sigInst.id].init(sigInst, config); return _eventEmitter[sigInst.id]; }; /** * Start the layout algorithm. It will use the existing configuration if no * new configuration is passed. * Recognized options: * ********************** * Here is the exhaustive list of every accepted parameters in the settings * object: * * {?array} nodes The subset of nodes to apply the layout. * {?object} boundingBox Constrain layout bounds. Value: {minX, maxX, minY, maxY} * or true (all current positions of the given nodes). * {?boolean} directed If `true`, take edge direction into * account. Default: `true`. * {?boolean} multigraph If `true`, allows multiple edges between * the same pair of nodes. Default: `true`. * {?boolean} compound If `true`, allows ompound nodes, i.e. * nodes which can be the parent of other * nodes. Default: `false`. * {?string} rankDir Direction for rank nodes. Can be TB, BT, * LR, or RL, where T = top, B = bottom, * L = left, and R = right. * {?(function|string)} easing Either the name of an easing in the * sigma.utils.easings package or a * function. If not specified, the * quadraticInOut easing from this package * will be used instead. * {?number} duration The duration of the animation. If not * specified, the "animationsTime" setting * value of the sigma instance will be used * instead. * * * @param {sigma} sigInst The related sigma instance. * @param {?object} config The optional configuration object. * * @return {sigma.classes.dispatcher} Returns an event emitter. */ sigma.layouts.dagre.start = function(sigInst, config) { if (!sigInst) throw new Error('Missing argument: "sigInst"'); if (config) { this.configure(sigInst, config); } _instance[sigInst.id].start(); return _eventEmitter[sigInst.id]; }; /** * Returns true if the layout has started and is not completed. * * @param {sigma} sigInst The related sigma instance. * * @return {boolean} */ sigma.layouts.dagre.isRunning = function(sigInst) { if (!sigInst) throw new Error('Missing argument: "sigInst"'); return !!_instance[sigInst.id] && _instance[sigInst.id].running; }; }).call(this);