// Define packages: var sigma = {}; sigma.tools = {}; sigma.classes = {}; sigma.instances = {}; // Adding Array helpers, if not present yet: (function() { if (!Array.prototype.some) { Array.prototype.some = function(fun /*, thisp*/) { var len = this.length; if (typeof fun != 'function') { throw new TypeError(); } var thisp = arguments[1]; for (var i = 0; i < len; i++) { if (i in this && fun.call(thisp, this[i], i, this)) { return true; } } return false; }; } if (!Array.prototype.forEach) { Array.prototype.forEach = function(fun /*, thisp*/) { var len = this.length; if (typeof fun != 'function') { throw new TypeError(); } var thisp = arguments[1]; for (var i = 0; i < len; i++) { if (i in this) { fun.call(thisp, this[i], i, this); } } }; } if (!Array.prototype.map) { Array.prototype.map = function(fun /*, thisp*/) { var len = this.length; if (typeof fun != 'function') { throw new TypeError(); } var res = new Array(len); var thisp = arguments[1]; for (var i = 0; i < len; i++) { if (i in this) { res[i] = fun.call(thisp, this[i], i, this); } } return res; }; } if (!Array.prototype.filter) { Array.prototype.filter = function(fun /*, thisp*/) { var len = this.length; if (typeof fun != 'function') throw new TypeError(); var res = new Array(); var thisp = arguments[1]; for (var i = 0; i < len; i++) { if (i in this) { var val = this[i]; // in case fun mutates this if (fun.call(thisp, val, i, this)) { res.push(val); } } } return res; }; } if (!Object.keys) { Object.keys = (function() { var hasOwnProperty = Object.prototype.hasOwnProperty, hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'), dontEnums = [ 'toString', 'toLocaleString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable', 'constructor' ], dontEnumsLength = dontEnums.length; return function(obj) { if (typeof obj !== 'object' && typeof obj !== 'function' || obj === null ) { throw new TypeError('Object.keys called on non-object'); } var result = []; for (var prop in obj) { if (hasOwnProperty.call(obj, prop)) result.push(prop); } if (hasDontEnumBug) { for (var i = 0; i < dontEnumsLength; i++) { if (hasOwnProperty.call(obj, dontEnums[i])) { result.push(dontEnums[i]); } } } return result; } })(); } })(); /** * A jQuery like properties management class. It works like jQuery .css() * method: You can call it with juste one string to get the corresponding * property, with a string and anything else to set the corresponding property, * or directly with an object, and then each pair string / object (or any type) * will be set in the properties. * @constructor * @this {sigma.classes.Cascade} */ sigma.classes.Cascade = function() { /** * This instance properties. * @protected * @type {Object} */ this.p = {}; /** * The method to use to set/get any property of this instance. * @param {(string|Object)} a1 If it is a string and if a2 is undefined, * then it will return the corresponding * property. * If it is a string and if a2 is set, then it * will set a2 as the property corresponding to * a1, and return this. * If it is an object, then each pair string / * object (or any other type) will be set as a * property. * @param {*?} a2 The new property corresponding to a1 if a1 is * a string. * @return {(*|sigma.classes.Cascade)} Returns itself or the corresponding * property. */ this.config = function(a1, a2) { if (typeof a1 == 'string' && a2 == undefined) { return this.p[a1]; } else { var o = (typeof a1 == 'object' && a2 == undefined) ? a1 : {}; if (typeof a1 == 'string') { o[a1] = a2; } for (var k in o) { if (this.p[k] != undefined) { this.p[k] = o[k]; } } return this; } }; }; /** * sigma.js custom event dispatcher class. * @constructor * @this {sigma.classes.EventDispatcher} */ sigma.classes.EventDispatcher = function() { /** * An object containing all the different handlers bound to one or many * events, indexed by these events. * @private * @type {Object.} */ var _h = {}; /** * Represents "this", without the well-known scope issue. * @private * @type {sigma.classes.EventDispatcher} */ var _self = this; /** * Will execute the handler the next (and only the next) time that the * indicated event (or the indicated events) will be triggered. * @param {string} events The name of the event (or the events * separated by spaces). * @param {function(Object)} handler The handler to bind. * @return {sigma.classes.EventDispatcher} Returns itself. */ function one(events, handler) { if (!handler || !events) { return _self; } var eArray = ((typeof events) == 'string') ? events.split(' ') : events; eArray.forEach(function(event) { if (!_h[event]) { _h[event] = []; } _h[event].push({ 'h': handler, 'one': true }); }); return _self; } /** * Will execute the handler everytime that the indicated event (or the * indicated events) will be triggered. * @param {string} events The name of the event (or the events * separated by spaces). * @param {function(Object)} handler The handler to bind. * @return {sigma.classes.EventDispatcher} Returns itself. */ function bind(events, handler) { if (!handler || !events) { return _self; } var eArray = ((typeof events) == 'string') ? events.split(' ') : events; eArray.forEach(function(event) { if (!_h[event]) { _h[event] = []; } _h[event].push({ 'h': handler, 'one': false }); }); return _self; } /** * Unbinds the handler from a specified event (or specified events). * @param {?string} events The name of the event (or the events * separated by spaces). If undefined, * then all handlers are unbound. * @param {?function(Object)} handler The handler to unbind. If undefined, * each handler bound to the event or the * events will be unbound. * @return {sigma.classes.EventDispatcher} Returns itself. */ function unbind(events, handler) { if (!events) { _h = {}; } var eArray = typeof events == 'string' ? events.split(' ') : events; if (handler) { eArray.forEach(function(event) { if (_h[event]) { _h[event] = _h[event].filter(function(e) { return e['h'] != handler; }); } if (_h[event] && _h[event].length == 0) { delete _h[event]; } }); }else { eArray.forEach(function(event) { delete _h[event]; }); } return _self; } /** * Executes each handler bound to the event * @param {string} type The type of the event. * @param {?Object} content The content of the event (optional). * @return {sigma.classes.EventDispatcher} Returns itself. */ function dispatch(type, content) { if (_h[type]) { _h[type].forEach(function(e) { e['h']({ 'type': type, 'content': content, 'target': _self }); }); _h[type] = _h[type].filter(function(e) { return !e['one']; }); } return _self; } /* PUBLIC INTERFACE: */ this.one = one; this.bind = bind; this.unbind = unbind; this.dispatch = dispatch; }; (function() { // Define local shortcut: var id = 0; // Define local package: var local = {}; local.plugins = []; sigma.init = function(dom) { var inst = new Sigma(dom, (++id).toString()); sigma.instances[id] = new SigmaPublic(inst); return sigma.instances[id]; }; /** * This class listen to all the different mouse events, to normalize them and * dispatch action events instead (from "startinterpolate" to "isdragging", * etc). * @constructor * @extends sigma.classes.Cascade * @extends sigma.classes.EventDispatcher * @param {element} dom The DOM element to bind the handlers on. * @this {MouseCaptor} */ function MouseCaptor(dom) { sigma.classes.Cascade.call(this); sigma.classes.EventDispatcher.call(this); /** * Represents "this", without the well-known scope issue. * @private * @type {MouseCaptor} */ var self = this; /** * The DOM element to bind the handlers on. * @type {element} */ var dom = dom; /** * The different parameters that define how this instance should work. * @see sigma.classes.Cascade * @type {Object} */ this.p = { minRatio: 1, maxRatio: 32, marginRatio: 1, zoomDelta: 0.1, dragDelta: 0.3, zoomMultiply: 2, directZooming: false, blockScroll: true, inertia: 1.1, mouseEnabled: true }; var oldMouseX = 0; var oldMouseY = 0; var startX = 0; var startY = 0; var oldStageX = 0; var oldStageY = 0; var oldRatio = 1; var targetRatio = 1; var targetStageX = 0; var targetStageY = 0; var lastStageX = 0; var lastStageX2 = 0; var lastStageY = 0; var lastStageY2 = 0; var progress = 0; var isZooming = false; this.stageX = 0; this.stageY = 0; this.ratio = 1; this.mouseX = 0; this.mouseY = 0; this.isMouseDown = false; /** * Extract the local X position from a mouse event. * @private * @param {event} e A mouse event. * @return {number} The local X value of the mouse. */ function getX(e) { return e.offsetX != undefined && e.offsetX || e.layerX != undefined && e.layerX || e.clientX != undefined && e.clientX; }; /** * Extract the local Y position from a mouse event. * @private * @param {event} e A mouse event. * @return {number} The local Y value of the mouse. */ function getY(e) { return e.offsetY != undefined && e.offsetY || e.layerY != undefined && e.layerY || e.clientY != undefined && e.clientY; }; /** * Extract the wheel delta from a mouse event. * @private * @param {event} e A mouse event. * @return {number} The wheel delta of the mouse. */ function getDelta(e) { return e.wheelDelta != undefined && e.wheelDelta || e.detail != undefined && -e.detail; }; /** * The handler listening to the 'move' mouse event. It will set the mouseX * and mouseY values as the mouse position values, prevent the default event, * and dispatch a 'move' event. * @private * @param {event} event A 'move' mouse event. */ function moveHandler(event) { oldMouseX = self.mouseX; oldMouseY = self.mouseY; self.mouseX = getX(event); self.mouseY = getY(event); self.isMouseDown && drag(event); self.dispatch('move'); if (event.preventDefault) { event.preventDefault(); } else { event.returnValue = false; } }; /** * The handler listening to the 'up' mouse event. It will set the isMouseDown * value as false, dispatch a 'mouseup' event, and trigger stopDrag(). * @private * @param {event} event A 'up' mouse event. */ function upHandler(event) { if (self.p.mouseEnabled && self.isMouseDown) { self.isMouseDown = false; self.dispatch('mouseup'); stopDrag(); if (event.preventDefault) { event.preventDefault(); } else { event.returnValue = false; } } }; /** * The handler listening to the 'down' mouse event. It will set the * isMouseDown value as true, dispatch a 'mousedown' event, and trigger * startDrag(). * @private * @param {event} event A 'down' mouse event. */ function downHandler(event) { if (self.p.mouseEnabled) { self.isMouseDown = true; oldMouseX = self.mouseX; oldMouseY = self.mouseY; self.dispatch('mousedown'); startDrag(); if (event.preventDefault) { event.preventDefault(); } else { event.returnValue = false; } } }; /** * The handler listening to the 'wheel' mouse event. It will trigger * {@link startInterpolate} with the event delta as parameter. * @private * @param {event} event A 'wheel' mouse event. */ function wheelHandler(event) { if (self.p.mouseEnabled) { startInterpolate( self.mouseX, self.mouseY, self.ratio * (getDelta(event) > 0 ? self.p.zoomMultiply : 1 / self.p.zoomMultiply) ); if (self.p['blockScroll']) { if (event.preventDefault) { event.preventDefault(); } else { event.returnValue = false; } } } }; /** * Will start computing the scene X and Y, until {@link stopDrag} is * triggered. */ function startDrag() { oldStageX = self.stageX; oldStageY = self.stageY; startX = self.mouseX; startY = self.mouseY; lastStageX = self.stageX; lastStageX2 = self.stageX; lastStageY = self.stageY; lastStageY2 = self.stageY; self.dispatch('startdrag'); }; /** * Stops computing the scene position. */ function stopDrag() { if (oldStageX != self.stageX || oldStageY != self.stageY) { startInterpolate( self.stageX + self.p.inertia * (self.stageX - lastStageX2), self.stageY + self.p.inertia * (self.stageY - lastStageY2) ); } }; /** * Computes the position of the scene, relatively to the mouse position, and * dispatches a "drag" event. */ function drag() { var newStageX = self.mouseX - startX + oldStageX; var newStageY = self.mouseY - startY + oldStageY; if (newStageX != self.stageX || newStageY != self.stageY) { lastStageX2 = lastStageX; lastStageY2 = lastStageY; lastStageX = newStageX; lastStageY = newStageY; self.stageX = newStageX; self.stageY = newStageY; self.dispatch('drag'); } }; /** * Will start computing the scene zoom ratio, until {@link stopInterpolate} is * triggered. * @param {number} x The new stage X. * @param {number} y The new stage Y. * @param {number} ratio The new zoom ratio. */ function startInterpolate(x, y, ratio) { if (self.isMouseDown) { return; } window.clearInterval(self.interpolationID); isZooming = ratio != undefined; oldStageX = self.stageX; targetStageX = x; oldStageY = self.stageY; targetStageY = y; oldRatio = self.ratio; targetRatio = ratio || self.ratio; targetRatio = Math.min( Math.max(targetRatio, self.p.minRatio), self.p.maxRatio ); progress = self.p.directZooming ? 1 - (isZooming ? self.p.zoomDelta : self.p.dragDelta) : 0; if ( self.ratio != targetRatio || self.stageX != targetStageX || self.stageY != targetStageY ) { interpolate(); self.interpolationID = window.setInterval(interpolate, 50); self.dispatch('startinterpolate'); } }; /** * Stops the move interpolation. */ function stopInterpolate() { var oldRatio = self.ratio; if (isZooming) { self.ratio = targetRatio; self.stageX = targetStageX + (self.stageX - targetStageX) * self.ratio / oldRatio; self.stageY = targetStageY + (self.stageY - targetStageY) * self.ratio / oldRatio; }else { self.stageX = targetStageX; self.stageY = targetStageY; } self.dispatch('stopinterpolate'); }; /** * Computes the interpolate ratio and the position of the scene, relatively * to the last mouse event delta received, and dispatches a "interpolate" * event. */ function interpolate() { progress += (isZooming ? self.p.zoomDelta : self.p.dragDelta); progress = Math.min(progress, 1); var k = sigma.easing.quadratic.easeout(progress); var oldRatio = self.ratio; self.ratio = oldRatio * (1 - k) + targetRatio * k; if (isZooming) { self.stageX = targetStageX + (self.stageX - targetStageX) * self.ratio / oldRatio; self.stageY = targetStageY + (self.stageY - targetStageY) * self.ratio / oldRatio; } else { self.stageX = oldStageX * (1 - k) + targetStageX * k; self.stageY = oldStageY * (1 - k) + targetStageY * k; } self.dispatch('interpolate'); if (progress >= 1) { window.clearInterval(self.interpolationID); stopInterpolate(); } }; /** * Checks that there is always a part of the graph that is displayed, to * avoid the user to drag the graph out of the stage. * @param {Object} b An object containing the borders of the graph. * @param {number} width The width of the stage. * @param {number} height The height of the stage. * @return {MouseCaptor} Returns itself. */ function checkBorders(b, width, height) { // TODO : Find the good formula /*if (!isNaN(b.minX) && !isNaN(b.maxX)) { self.stageX = Math.min( self.stageX = Math.max( self.stageX, (b.minX - width) * self.ratio + self.p.marginRatio*(b.maxX - b.minX) ), (b.maxX - width) * self.ratio + width - self.p.marginRatio*(b.maxX - b.minX) ); } if (!isNaN(b.minY) && !isNaN(b.maxY)) { self.stageY = Math.min( self.stageY = Math.max( self.stageY, (b.minY - height) * self.ratio + self.p.marginRatio*(b.maxY - b.minY) ), (b.maxY - height) * self.ratio + height - self.p.marginRatio*(b.maxY - b.minY) ); }*/ return self; }; // ADD CALLBACKS dom.addEventListener('DOMMouseScroll', wheelHandler, true); dom.addEventListener('mousewheel', wheelHandler, true); dom.addEventListener('mousemove', moveHandler, true); dom.addEventListener('mousedown', downHandler, true); document.addEventListener('mouseup', upHandler, true); this.checkBorders = checkBorders; this.interpolate = startInterpolate; } /** * A class to monitor some local / global probes directly on an instance, * inside a div DOM element. * It executes different methods (called "probes") regularly, and displays * the results on the element. * @constructor * @extends sigma.classes.Cascade * @param {Sigma} instance The instance to monitor. * @param {element} dom The div DOM element to draw write on. * @this {Monitor} */ function Monitor(instance, dom) { sigma.classes.Cascade.call(this); /** * Represents "this", without the well-known scope issue. * @private * @type {Monitor} */ var self = this; /** * {@link Sigma} instance owning this Monitor instance. * @type {Sigma} */ this.instance = instance; /** * Determines if the monitoring is activated or not. * @type {Boolean} */ this.monitoring = false; /** * The different parameters that define how this instance should work. It * also contains the different probes. * @see sigma.classes.Cascade * @type {Object} */ this.p = { fps: 40, dom: dom, globalProbes: { 'Time (ms)': sigma.chronos.getExecutionTime, 'Queue': sigma.chronos.getQueuedTasksCount, 'Tasks': sigma.chronos.getTasksCount, 'FPS': sigma.chronos.getFPS }, localProbes: { 'Nodes count': function() { return self.instance.graph.nodes.length; }, 'Edges count': function() { return self.instance.graph.edges.length; } } }; /** * Activates the monitoring: Some texts describing some values about sigma.js * or the owning {@link Sigma} instance will appear over the graph, but * beneath the mouse sensible DOM element. * @return {Monitor} Returns itself. */ function activate() { if (!self.monitoring) { self.monitoring = window.setInterval(routine, 1000 / self.p.fps); } return self; } /** * Desactivates the monitoring: Will disappear, and stop computing the * different probes. * @return {Monitor} Returns itself. */ function desactivate() { if (self.monitoring) { window.clearInterval(self.monitoring); self.monitoring = null; self.p.dom.innerHTML = ''; } return self; } /** * The private method dedicated to compute the different values to observe. * @private * @return {Monitor} Returns itself. */ function routine() { var s = ''; s += '

GLOBAL :

'; for (var k in self.p.globalProbes) { s += '

' + k + ' : ' + self.p.globalProbes[k]() + '

'; } s += '

LOCAL :

'; for (var k in self.p.localProbes) { s += '

' + k + ' : ' + self.p.localProbes[k]() + '

'; } self.p.dom.innerHTML = s; return self; } this.activate = activate; this.desactivate = desactivate; } /** * Sigma is the main class. It represents the core of any instance id sigma.js. * It is private and can be initialized only from inside sigma.js. To see its * public interface, see {@link SigmaPublic}. * It owns its own {@link Graph}, {@link MouseCaptor}, {@link Plotter} * and {@link Monitor}. * @constructor * @extends sigma.classes.Cascade * @extends sigma.classes.EventDispatcher * @param {element} root The DOM root of this instance (a div, for example). * @param {string} id The ID of this instance. * @this {Sigma} */ function Sigma(root, id) { sigma.classes.Cascade.call(this); sigma.classes.EventDispatcher.call(this); /** * Represents "this", without the well-known scope issue. * @private * @type {Sigma} */ var self = this; /** * The ID of the instance. * @type {string} */ this.id = id.toString(); /** * The different parameters that define how this instance should work. * @see sigma.classes.Cascade * @type {Object} */ this.p = { auto: true, drawNodes: 2, drawEdges: 1, drawLabels: 2, lastNodes: 2, lastEdges: 0, lastLabels: 2, drawHoverNodes: true, drawActiveNodes: true }; /** * The root DOM element of this instance, containing every other elements. * @type {element} */ this.domRoot = root; /** * The width of this instance - initially, the root's width. * @type {number} */ this.width = this.domRoot.offsetWidth; /** * The height of this instance - initially, the root's height. * @type {number} */ this.height = this.domRoot.offsetHeight; /** * The graph of this instance - initiallyempty. * @type {Graph} */ this.graph = new Graph(); /** * An object referencing every DOM elements used by this instance. * @type {Object} */ this.domElements = {}; initDOM('edges', 'canvas'); initDOM('nodes', 'canvas'); initDOM('labels', 'canvas'); initDOM('hover', 'canvas'); initDOM('monitor', 'div'); initDOM('mouse', 'canvas'); /** * The class dedicated to manage the drawing process of the graph of the * different canvas. * @type {Plotter} */ this.plotter = new Plotter( this.domElements.nodes.getContext('2d'), this.domElements.edges.getContext('2d'), this.domElements.labels.getContext('2d'), this.domElements.hover.getContext('2d'), this.graph, this.width, this.height ); /** * The class dedicated to monitor different probes about the running * processes or the data, such as the number of nodes or edges, or how * many times the graph is drawn per second. * @type {Monitor} */ this.monitor = new Monitor( this, this.domElements.monitor ); /** * The class dedicated to manage the different mouse events. * @type {MouseCaptor} */ this.mousecaptor = new MouseCaptor( this.domElements.mouse, this.id ); // Interaction listeners: this.mousecaptor.bind('drag interpolate', function(e) { self.draw( self.p.auto ? 2 : self.p.drawNodes, self.p.auto ? 0 : self.p.drawEdges, self.p.auto ? 2 : self.p.drawLabels, true ); }).bind('stopdrag stopinterpolate', function(e) { self.draw( self.p.auto ? 2 : self.p.drawNodes, self.p.auto ? 1 : self.p.drawEdges, self.p.auto ? 2 : self.p.drawLabels, true ); }).bind('mousedown mouseup', function(e) { var targeted = self.graph.nodes.filter(function(n) { return !!n['hover']; }).map(function(n) { return n.id; }); self.dispatch( e['type'] == 'mousedown' ? 'downgraph' : 'upgraph' ); if (targeted.length) { self.dispatch( e['type'] == 'mousedown' ? 'downnodes' : 'upnodes', targeted ); } }).bind('move', function() { self.domElements.hover.getContext('2d').clearRect( 0, 0, self.domElements.hover.width, self.domElements.hover.height ); drawHover(); drawActive(); }); sigma.chronos.bind('startgenerators', function() { if (sigma.chronos.getGeneratorsIDs().some(function(id) { return !!id.match(new RegExp('_ext_' + self.id + '$', '')); })) { self.draw( self.p.auto ? 2 : self.p.drawNodes, self.p.auto ? 0 : self.p.drawEdges, self.p.auto ? 2 : self.p.drawLabels ); } }).bind('stopgenerators', function() { self.draw(); }); /** * Resizes the element, and redraws the graph with the last settings. * @param {?number} w The new width (if undefined, it will use the root * width). * @param {?number} h The new height (if undefined, it will use the root * height). * @return {Sigma} Returns itself. */ function resize(w, h) { var oldW = self.width, oldH = self.height; if (w != undefined && h != undefined) { self.width = w; self.height = h; }else { self.width = self.domRoot.offsetWidth; self.height = self.domRoot.offsetHeight; } if (oldW != self.width || oldH != self.height) { for (var k in self.domElements) { self.domElements[k].setAttribute('width', self.width + 'px'); self.domElements[k].setAttribute('height', self.height + 'px'); } self.plotter.resize(self.width, self.height); self.draw( self.p.lastNodes, self.p.lastEdges, self.p.lastLabels, true ); } return self; }; /** * Kills every drawing task currently running. Basically, it stops this * instance's drawing process. * @return {Sigma} Returns itself. */ function clearSchedule() { sigma.chronos.removeTask( 'node_' + self.id, 2 ).removeTask( 'edge_' + self.id, 2 ).removeTask( 'label_' + self.id, 2 ).stopTasks(); return self; }; /** * Initialize a DOM element, that will be stores by this instance, to make * automatic these elements resizing. * @private * @param {string} id The element's ID. * @param {string} type The element's nodeName (Example : canvas, div, ...). * @return {Sigma} Returns itself. */ function initDOM(id, type) { self.domElements[id] = document.createElement(type); self.domElements[id].style.position = 'absolute'; self.domElements[id].setAttribute('id', 'sigma_' + id + '_' + self.id); self.domElements[id].setAttribute('class', 'sigma_' + id + '_' + type); self.domElements[id].setAttribute('width', self.width + 'px'); self.domElements[id].setAttribute('height', self.height + 'px'); self.domRoot.appendChild(self.domElements[id]); return self; }; /** * Starts the graph drawing process. The three first parameters indicate * how the different layers have to be drawn: * . -1: The layer is not drawn, but it is not erased. * . 0: The layer is not drawn. * . 1: The layer is drawn progressively. * . 2: The layer is drawn directly. * @param {?number} nodes Determines if and how the nodes must be drawn. * @param {?number} edges Determines if and how the edges must be drawn. * @param {?number} labels Determines if and how the labels must be drawn. * @param {?boolean} safe If true, nothing will happen if any generator * affiliated to this instance is currently running * (an iterative layout, for example). * @return {Sigma} Returns itself. */ function draw(nodes, edges, labels, safe) { if (safe && sigma.chronos.getGeneratorsIDs().some(function(id) { return !!id.match(new RegExp('_ext_' + self.id + '$', '')); })) { return self; } var n = (nodes == undefined) ? self.p.drawNodes : nodes; var e = (edges == undefined) ? self.p.drawEdges : edges; var l = (labels == undefined) ? self.p.drawLabels : labels; var params = { nodes: n, edges: e, labels: l }; self.p.lastNodes = n; self.p.lastEdges = e; self.p.lastLabels = l; // Remove tasks: clearSchedule(); // Rescale graph: self.graph.rescale( self.width, self.height, n > 0, e > 0 ).setBorders(); self.mousecaptor.checkBorders( self.graph.borders, self.width, self.height ); self.graph.translate( self.mousecaptor.stageX, self.mousecaptor.stageY, self.mousecaptor.ratio, n > 0, e > 0 ); self.dispatch( 'graphscaled' ); // Clear scene: for (var k in self.domElements) { if ( self.domElements[k].nodeName.toLowerCase() == 'canvas' && (params[k] == undefined || params[k] >= 0) ) { self.domElements[k].getContext('2d').clearRect( 0, 0, self.domElements[k].width, self.domElements[k].height ); } } self.plotter.currentEdgeIndex = 0; self.plotter.currentNodeIndex = 0; self.plotter.currentLabelIndex = 0; var previous = null; var start = false; if (n) { if (n > 1) { while (self.plotter.task_drawNode()) {} }else { sigma.chronos.addTask( self.plotter.task_drawNode, 'node_' + self.id, false ); start = true; previous = 'node_' + self.id; } } if (l) { if (l > 1) { while (self.plotter.task_drawLabel()) {} } else { if (previous) { sigma.chronos.queueTask( self.plotter.task_drawLabel, 'label_' + self.id, previous ); } else { sigma.chronos.addTask( self.plotter.task_drawLabel, 'label_' + self.id, false ); } start = true; previous = 'label_' + self.id; } } if (e) { if (e > 1) { while (self.plotter.task_drawEdge()) {} }else { if (previous) { sigma.chronos.queueTask( self.plotter.task_drawEdge, 'edge_' + self.id, previous ); }else { sigma.chronos.addTask( self.plotter.task_drawEdge, 'edge_' + self.id, false ); } start = true; previous = 'edge_' + self.id; } } self.dispatch( 'draw' ); self.refresh(); start && sigma.chronos.runTasks(); return self; }; /** * Draws the hover and active nodes labels. * @return {Sigma} Returns itself. */ function refresh() { self.domElements.hover.getContext('2d').clearRect( 0, 0, self.domElements.hover.width, self.domElements.hover.height ); drawHover(); drawActive(); return self; } /** * Draws the hover nodes labels. This method is applied directly, and does * not use the pseudo-asynchronous tasks process. * @return {Sigma} Returns itself. */ function drawHover() { if (self.p.drawHoverNodes) { self.graph.checkHover( self.mousecaptor.mouseX, self.mousecaptor.mouseY ); self.graph.nodes.forEach(function(node) { if (node.hover && !node.active) { self.plotter.drawHoverNode(node); } }); } return self; } /** * Draws the active nodes labels. This method is applied directly, and does * not use the pseudo-asynchronous tasks process. * @return {Sigma} Returns itself. */ function drawActive() { if (self.p.drawActiveNodes) { self.graph.nodes.forEach(function(node) { if (node.active) { self.plotter.drawActiveNode(node); } }); } return self; } // Apply plugins: for (var i = 0; i < local.plugins.length; i++) { local.plugins[i](this); } this.draw = draw; this.resize = resize; this.refresh = refresh; this.drawHover = drawHover; this.drawActive = drawActive; this.clearSchedule = clearSchedule; window.addEventListener('resize', function() { self.resize(); }); } /** * This class draws the graph on the different canvas DOM elements. It just * contains all the different methods to draw the graph, synchronously or * pseudo-asynchronously. * @constructor * @param {CanvasRenderingContext2D} nodesCtx Context dedicated to draw nodes. * @param {CanvasRenderingContext2D} edgesCtx Context dedicated to draw edges. * @param {CanvasRenderingContext2D} labelsCtx Context dedicated to draw * labels. * @param {CanvasRenderingContext2D} hoverCtx Context dedicated to draw hover * nodes labels. * @param {Graph} graph A reference to the graph to * draw. * @param {number} w The width of the DOM root * element. * @param {number} h The width of the DOM root * element. * @extends sigma.classes.Cascade * @this {Plotter} */ function Plotter(nodesCtx, edgesCtx, labelsCtx, hoverCtx, graph, w, h) { sigma.classes.Cascade.call(this); /** * Represents "this", without the well-known scope issue. * @private * @type {Plotter} */ var self = this; /** * The different parameters that define how this instance should work. * @see sigma.classes.Cascade * @type {Object} */ this.p = { // ------- // LABELS: // ------- // Label color: // - 'node' // - default (then defaultLabelColor // will be used instead) labelColor: 'default', defaultLabelColor: '#000', // Label hover background color: // - 'node' // - default (then defaultHoverLabelBGColor // will be used instead) labelHoverBGColor: 'default', defaultHoverLabelBGColor: '#fff', // Label hover shadow: labelHoverShadow: true, labelHoverShadowColor: '#000', // Label hover color: // - 'node' // - default (then defaultLabelHoverColor // will be used instead) labelHoverColor: 'default', defaultLabelHoverColor: '#000', // Label active background color: // - 'node' // - default (then defaultActiveLabelBGColor // will be used instead) labelActiveBGColor: 'default', defaultActiveLabelBGColor: '#fff', // Label active shadow: labelActiveShadow: true, labelActiveShadowColor: '#000', // Label active color: // - 'node' // - default (then defaultLabelActiveColor // will be used instead) labelActiveColor: 'default', defaultLabelActiveColor: '#000', // Label size: // - 'fixed' // - 'proportional' // Label size: // - 'fixed' // - 'proportional' labelSize: 'fixed', defaultLabelSize: 12, // for fixed display only labelSizeRatio: 2, // for proportional display only labelThreshold: 6, font: 'Arial', hoverFont: '', activeFont: '', fontStyle: '', hoverFontStyle: '', activeFontStyle: '', // ------ // EDGES: // ------ // Edge color: // - 'source' // - 'target' // - default (then defaultEdgeColor or edge['color'] // will be used instead) edgeColor: 'source', defaultEdgeColor: '#aaa', defaultEdgeType: 'line', // ------ // NODES: // ------ defaultNodeColor: '#aaa', // HOVER: // Node hover color: // - 'node' // - default (then defaultNodeHoverColor // will be used instead) nodeHoverColor: 'node', defaultNodeHoverColor: '#fff', // ACTIVE: // Node active color: // - 'node' // - default (then defaultNodeActiveColor // will be used instead) nodeActiveColor: 'node', defaultNodeActiveColor: '#fff', // Node border color: // - 'node' // - default (then defaultNodeBorderColor // will be used instead) borderSize: 0, nodeBorderColor: 'node', defaultNodeBorderColor: '#fff', // -------- // PROCESS: // -------- edgesSpeed: 200, nodesSpeed: 200, labelsSpeed: 200 }; /** * The canvas context dedicated to draw the nodes. * @type {CanvasRenderingContext2D} */ var nodesCtx = nodesCtx; /** * The canvas context dedicated to draw the edges. * @type {CanvasRenderingContext2D} */ var edgesCtx = edgesCtx; /** * The canvas context dedicated to draw the labels. * @type {CanvasRenderingContext2D} */ var labelsCtx = labelsCtx; /** * The canvas context dedicated to draw the hover nodes. * @type {CanvasRenderingContext2D} */ var hoverCtx = hoverCtx; /** * A reference to the graph to draw. * @type {Graph} */ var graph = graph; /** * The width of the stage to draw on. * @type {number} */ var width = w; /** * The height of the stage to draw on. * @type {number} */ var height = h; /** * The index of the next edge to draw. * @type {number} */ this.currentEdgeIndex = 0; /** * The index of the next node to draw. * @type {number} */ this.currentNodeIndex = 0; /** * The index of the next label to draw. * @type {number} */ this.currentLabelIndex = 0; /** * An atomic function to drawn the N next edges, with N as edgesSpeed. * The counter is {@link this.currentEdgeIndex}. * This function has been designed to work with {@link sigma.chronos}, that * will insert frames at the middle of the calls, to make the edges drawing * process fluid for the user. * @see sigma.chronos * @return {boolean} Returns true if all the edges are drawn and false else. */ function task_drawEdge() { var c = graph.edges.length; var s, t, i = 0; while (i++< self.p.edgesSpeed && self.currentEdgeIndex < c) { e = graph.edges[self.currentEdgeIndex]; s = e['source']; t = e['target']; if (e['hidden'] || s['hidden'] || t['hidden'] || (!self.isOnScreen(s) && !self.isOnScreen(t))) { self.currentEdgeIndex++; }else { drawEdge(graph.edges[self.currentEdgeIndex++]); } } return self.currentEdgeIndex < c; }; /** * An atomic function to drawn the N next nodes, with N as nodesSpeed. * The counter is {@link this.currentEdgeIndex}. * This function has been designed to work with {@link sigma.chronos}, that * will insert frames at the middle of the calls, to make the nodes drawing * process fluid for the user. * @see sigma.chronos * @return {boolean} Returns true if all the nodes are drawn and false else. */ function task_drawNode() { var c = graph.nodes.length; var i = 0; while (i++< self.p.nodesSpeed && self.currentNodeIndex < c) { if (!self.isOnScreen(graph.nodes[self.currentNodeIndex])) { self.currentNodeIndex++; }else { drawNode(graph.nodes[self.currentNodeIndex++]); } } return self.currentNodeIndex < c; }; /** * An atomic function to drawn the N next labels, with N as labelsSpeed. * The counter is {@link this.currentEdgeIndex}. * This function has been designed to work with {@link sigma.chronos}, that * will insert frames at the middle of the calls, to make the labels drawing * process fluid for the user. * @see sigma.chronos * @return {boolean} Returns true if all the labels are drawn and false else. */ function task_drawLabel() { var c = graph.nodes.length; var i = 0; while (i++< self.p.labelsSpeed && self.currentLabelIndex < c) { if (!self.isOnScreen(graph.nodes[self.currentLabelIndex])) { self.currentLabelIndex++; }else { drawLabel(graph.nodes[self.currentLabelIndex++]); } } return self.currentLabelIndex < c; }; /** * Draws one node to the corresponding canvas. * @param {Object} node The node to draw. * @return {Plotter} Returns itself. */ function drawNode(node) { var size = Math.round(node['displaySize'] * 10) / 10; var ctx = nodesCtx; ctx.fillStyle = node['color']; ctx.beginPath(); ctx.arc(node['displayX'], node['displayY'], size, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); node['hover'] && drawHoverNode(node); return self; }; /** * Draws one edge to the corresponding canvas. * @param {Object} edge The edge to draw. * @return {Plotter} Returns itself. */ function drawEdge(edge) { var x1 = edge['source']['displayX']; var y1 = edge['source']['displayY']; var x2 = edge['target']['displayX']; var y2 = edge['target']['displayY']; var color = edge['color']; if (!color) { switch (self.p.edgeColor) { case 'source': color = edge['source']['color'] || self.p.defaultNodeColor; break; case 'target': color = edge['target']['color'] || self.p.defaultNodeColor; break; default: color = self.p.defaultEdgeColor; break; } } var ctx = edgesCtx; switch (edge['type'] || self.p.defaultEdgeType) { case 'curve': ctx.strokeStyle = color; ctx.lineWidth = edge['displaySize'] / 3; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.quadraticCurveTo((x1 + x2) / 2 + (y2 - y1) / 4, (y1 + y2) / 2 + (x1 - x2) / 4, x2, y2); ctx.stroke(); break; case 'line': default: ctx.strokeStyle = color; ctx.lineWidth = edge['displaySize'] / 3; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); break; } return self; }; /** * Draws one label to the corresponding canvas. * @param {Object} node The label to draw. * @return {Plotter} Returns itself. */ function drawLabel(node) { var ctx = labelsCtx; if (node['displaySize'] >= self.p.labelThreshold || node['forceLabel']) { var fontSize = self.p.labelSize == 'fixed' ? self.p.defaultLabelSize : self.p.labelSizeRatio * node['displaySize']; ctx.font = self.p.fontStyle + fontSize + 'px ' + self.p.font; ctx.fillStyle = self.p.labelColor == 'node' ? (node['color'] || self.p.defaultNodeColor) : self.p.defaultLabelColor; ctx.fillText( node['label'], Math.round(node['displayX'] + node['displaySize'] * 1.5), Math.round(node['displayY'] + fontSize / 2 - 3) ); } return self; }; /** * Draws one hover node to the corresponding canvas. * @param {Object} node The hover node to draw. * @return {Plotter} Returns itself. */ function drawHoverNode(node) { var ctx = hoverCtx; var fontSize = self.p.labelSize == 'fixed' ? self.p.defaultLabelSize : self.p.labelSizeRatio * node['displaySize']; ctx.font = (self.p.hoverFontStyle || self.p.fontStyle || '') + ' ' + fontSize + 'px ' + (self.p.hoverFont || self.p.font || ''); ctx.fillStyle = self.p.labelHoverBGColor == 'node' ? (node['color'] || self.p.defaultNodeColor) : self.p.defaultHoverLabelBGColor; // Label background: ctx.beginPath(); if (self.p.labelHoverShadow) { ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; ctx.shadowBlur = 4; ctx.shadowColor = self.p.labelHoverShadowColor; } sigma.tools.drawRoundRect( ctx, Math.round(node['displayX'] - fontSize / 2 - 2), Math.round(node['displayY'] - fontSize / 2 - 2), Math.round(ctx.measureText(node['label']).width + node['displaySize'] * 1.5 + fontSize / 2 + 4), Math.round(fontSize + 4), Math.round(fontSize / 2 + 2), 'left' ); ctx.closePath(); ctx.fill(); ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; ctx.shadowBlur = 0; // Node border: ctx.beginPath(); ctx.fillStyle = self.p.nodeBorderColor == 'node' ? (node['color'] || self.p.defaultNodeColor) : self.p.defaultNodeBorderColor; ctx.arc(Math.round(node['displayX']), Math.round(node['displayY']), node['displaySize'] + self.p.borderSize, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); // Node: ctx.beginPath(); ctx.fillStyle = self.p.nodeHoverColor == 'node' ? (node['color'] || self.p.defaultNodeColor) : self.p.defaultNodeHoverColor; ctx.arc(Math.round(node['displayX']), Math.round(node['displayY']), node['displaySize'], 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); // Label: ctx.fillStyle = self.p.labelHoverColor == 'node' ? (node['color'] || self.p.defaultNodeColor) : self.p.defaultLabelHoverColor; ctx.fillText( node['label'], Math.round(node['displayX'] + node['displaySize'] * 1.5), Math.round(node['displayY'] + fontSize / 2 - 3) ); return self; }; /** * Draws one active node to the corresponding canvas. * @param {Object} node The active node to draw. * @return {Plotter} Returns itself. */ function drawActiveNode(node) { var ctx = hoverCtx; if (!isOnScreen(node)) { return self; } var fontSize = self.p.labelSize == 'fixed' ? self.p.defaultLabelSize : self.p.labelSizeRatio * node['displaySize']; ctx.font = (self.p.activeFontStyle || self.p.fontStyle || '') + ' ' + fontSize + 'px ' + (self.p.activeFont || self.p.font || ''); ctx.fillStyle = self.p.labelHoverBGColor == 'node' ? (node['color'] || self.p.defaultNodeColor) : self.p.defaultActiveLabelBGColor; // Label background: ctx.beginPath(); if (self.p.labelActiveShadow) { ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; ctx.shadowBlur = 4; ctx.shadowColor = self.p.labelActiveShadowColor; } sigma.tools.drawRoundRect( ctx, Math.round(node['displayX'] - fontSize / 2 - 2), Math.round(node['displayY'] - fontSize / 2 - 2), Math.round(ctx.measureText(node['label']).width + node['displaySize'] * 1.5 + fontSize / 2 + 4), Math.round(fontSize + 4), Math.round(fontSize / 2 + 2), 'left' ); ctx.closePath(); ctx.fill(); ctx.shadowOffsetX = 0; ctx.shadowOffsetY = 0; ctx.shadowBlur = 0; // Node border: ctx.beginPath(); ctx.fillStyle = self.p.nodeBorderColor == 'node' ? (node['color'] || self.p.defaultNodeColor) : self.p.defaultNodeBorderColor; ctx.arc(Math.round(node['displayX']), Math.round(node['displayY']), node['displaySize'] + self.p.borderSize, 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); // Node: ctx.beginPath(); ctx.fillStyle = self.p.nodeActiveColor == 'node' ? (node['color'] || self.p.defaultNodeColor) : self.p.defaultNodeActiveColor; ctx.arc(Math.round(node['displayX']), Math.round(node['displayY']), node['displaySize'], 0, Math.PI * 2, true); ctx.closePath(); ctx.fill(); // Label: ctx.fillStyle = self.p.labelActiveColor == 'node' ? (node['color'] || self.p.defaultNodeColor) : self.p.defaultLabelActiveColor; ctx.fillText( node['label'], Math.round(node['displayX'] + node['displaySize'] * 1.5), Math.round(node['displayY'] + fontSize / 2 - 3) ); return self; }; /** * Determines if a node is on the screen or not. The limits here are * bigger than the actual screen, to avoid seeing labels disappear during * the graph manipulation. * @param {Object} node The node to check if it is on or out the screen. * @return {boolean} Returns false if the node is hidden or not on the screen * or true else. */ function isOnScreen(node) { if (isNaN(node['x']) || isNaN(node['y'])) { throw (new Error('A node\'s coordinate is not a ' + 'number (id: ' + node['id'] + ')') ); } return !node['hidden'] && (node['displayX'] + node['displaySize'] > -width / 3) && (node['displayX'] - node['displaySize'] < width * 4 / 3) && (node['displayY'] + node['displaySize'] > -height / 3) && (node['displayY'] - node['displaySize'] < height * 4 / 3); }; /** * Resizes this instance. * @param {number} w The new width. * @param {number} h The new height. * @return {Plotter} Returns itself. */ function resize(w, h) { width = w; height = h; return self; } this.task_drawLabel = task_drawLabel; this.task_drawEdge = task_drawEdge; this.task_drawNode = task_drawNode; this.drawActiveNode = drawActiveNode; this.drawHoverNode = drawHoverNode; this.isOnScreen = isOnScreen; this.resize = resize; } function SigmaPublic(sigmaInstance) { var s = sigmaInstance; var self = this; sigma.classes.EventDispatcher.call(this); this._core = sigmaInstance; this.kill = function() { // TODO }; this.getID = function() { return s.id; }; // Config: this.configProperties = function(a1, a2) { var res = s.config(a1, a2); return res == s ? self : res; }; this.drawingProperties = function(a1, a2) { var res = s.plotter.config(a1, a2); return res == s.plotter ? self : res; }; this.mouseProperties = function(a1, a2) { var res = s.mousecaptor.config(a1, a2); return res == s.mousecaptor ? self : res; }; this.graphProperties = function(a1, a2) { var res = s.graph.config(a1, a2); return res == s.graph ? self : res; }; this.getMouse = function() { return { mouseX: s.mousecaptor.mouseX, mouseY: s.mousecaptor.mouseY, down: s.mousecaptor.isMouseDown }; }; // Actions: this.position = function(stageX, stageY, ratio) { if (arguments.length == 0) { return { stageX: s.mousecaptor.stageX, stageY: s.mousecaptor.stageY, ratio: s.mousecaptor.ratio }; }else { s.mousecaptor.stageX = stageX != undefined ? stageX : s.mousecaptor.stageX; s.mousecaptor.stageY = stageY != undefined ? stageY : s.mousecaptor.stageY; s.mousecaptor.ratio = ratio != undefined ? ratio : s.mousecaptor.ratio; return self; } }; this.goTo = function(stageX, stageY, ratio) { s.mousecaptor.interpolate(stageX, stageY, ratio); return self; }; this.zoomTo = function(x, y, ratio) { ratio = Math.min( Math.max(s.mousecaptor.config('minRatio'), ratio), s.mousecaptor.config('maxRatio') ); if (ratio == s.mousecaptor.ratio) { s.mousecaptor.interpolate( x - s.width / 2 + s.mousecaptor.stageX, y - s.height / 2 + s.mousecaptor.stageY ); }else { s.mousecaptor.interpolate( (ratio * x - s.mousecaptor.ratio * s.width/2) / (ratio - s.mousecaptor.ratio), (ratio * y - s.mousecaptor.ratio * s.height/2) / (ratio - s.mousecaptor.ratio), ratio ); } return self; }; this.resize = function(w, h) { s.resize(w, h); return self; }; this.draw = function(nodes, edges, labels, safe) { s.draw(nodes, edges, labels, safe); return self; }; this.refresh = function() { s.refresh(); return self; }; // Tasks methods: this.addGenerator = function(id, task, condition) { sigma.chronos.addGenerator(id + '_ext_' + s.id, task, condition); return self; }; this.removeGenerator = function(id) { sigma.chronos.removeGenerator(id + '_ext_' + s.id); return self; }; // Graph methods: this.addNode = function(id, params) { s.graph.addNode(id, params); return self; }; this.addEdge = function(id, source, target, params) { s.graph.addEdge(id, source, target, params); return self; } this.dropNode = function(v) { s.graph.dropNode(v); return self; }; this.dropEdge = function(v) { s.graph.dropEdge(v); return self; }; this.pushGraph = function(object, safe) { object.nodes && object.nodes.forEach(function(node) { node['id'] && (!safe || !s.graph.nodesIndex[node['id']]) && self.addNode(node['id'], node); }); var isEdgeValid; object.edges && object.edges.forEach(function(edge) { validID = edge['source'] && edge['target'] && edge['id']; validID && (!safe || !s.graph.edgesIndex[edge['id']]) && self.addNode( edge['id'], edge['source'], edge['target'], edge ); }); return self; }; this.emptyGraph = function() { s.graph.empty(); return self; }; this.getNodesCount = function() { return s.graph.nodes.length; }; this.getEdgesCount = function() { return s.graph.edges.length; }; this.iterNodes = function(fun, ids) { s.graph.iterNodes(fun, ids); return self; }; this.iterEdges = function(fun, ids) { s.graph.iterEdges(fun, ids); return self; }; this.getNodes = function(ids) { return s.graph.getNodes(ids); }; this.getEdges = function(ids) { return s.graph.getEdges(ids); }; // Monitoring this.activateMonitoring = function() { return s.monitor.activate(); }; this.desactivateMonitoring = function() { return s.monitor.desactivate(); }; // Events s.bind('downnodes upnodes downgraph upgraph', function(e) { self.dispatch(e.type, e.content); }); s.graph.bind('overnodes outnodes', function(e) { self.dispatch(e.type, e.content); }); } /** * The graph data model used in sigma.js. * @constructor * @extends sigma.classes.Cascade * @extends sigma.classes.EventDispatcher * @this {Graph} */ function Graph() { sigma.classes.Cascade.call(this); sigma.classes.EventDispatcher.call(this); /** * Represents "this", without the well-known scope issue. * @private * @type {Graph} */ var self = this; /** * The different parameters that determine how the nodes and edges should be * translated and rescaled. * @type {Object} */ this.p = { minNodeSize: 0, maxNodeSize: 0, minEdgeSize: 0, maxEdgeSize: 0, // Scaling mode: // - 'inside' (default) // - 'outside' scalingMode: 'inside', nodesPowRatio: 0.5, edgesPowRatio: 0 }; /** * Contains the borders of the graph. These are useful to avoid the user to * drag the graph out of the canvas. * @type {Object} */ this.borders = {}; /** * Inserts a node in the graph. * @param {string} id The node's ID. * @param {object} params An object containing the different parameters * of the node. * @return {Graph} Returns itself. */ function addNode(id, params) { if (self.nodesIndex[id]) { throw new Error('Node "' + id + '" already exists.'); } params = params || {}; var n = { // Numbers : 'x': 0, 'y': 0, 'size': 1, 'degree': 0, 'inDegree': 0, 'outDegree': 0, // Flags : 'fixed': false, 'active': false, 'hidden': false, 'forceLabel': false, // Strings : 'label': id.toString(), 'id': id.toString(), // Custom attributes : 'attr': {} }; for (var k in params) { switch (k) { case 'id': break; case 'x': case 'y': case 'size': n[k] = +params[k]; break; case 'fixed': case 'active': case 'hidden': case 'forceLabel': n[k] = !!params[k]; break; case 'color': case 'label': n[k] = params[k]; break; default: n['attr'][k] = params[k]; } } self.nodes.push(n); self.nodesIndex[id.toString()] = n; return self; }; /** * Generates the clone of a node, to make it easier to be exported. * @private * @param {Object} node The node to clone. * @return {Object} The clone of the node. */ function cloneNode(node) { return { 'x': node['x'], 'y': node['y'], 'size': node['size'], 'degree': node['degree'], 'inDegree': node['inDegree'], 'outDegree': node['outDegree'], 'displayX': node['displayX'], 'displayY': node['displayY'], 'displaySize': node['displaySize'], 'label': node['label'], 'id': node['id'], 'color': node['color'], 'fixed': node['fixed'], 'active': node['active'], 'hidden': node['hidden'], 'forceLabel': node['forceLabel'], 'attr': node['attr'] }; }; /** * Checks the clone of a node, and inserts its values when possible. For * example, it is possible to modify the size or the color of a node, but it * is not possible to modify its display values or its id. * @private * @param {Object} node The original node. * @param {Object} copy The clone. * @return {Graph} Returns itself. */ function checkNode(node, copy) { for (var k in copy) { switch (k) { case 'id': case 'attr': case 'degree': case 'inDegree': case 'outDegree': case 'displayX': case 'displayY': case 'displaySize': break; case 'x': case 'y': case 'size': node[k] = +copy[k]; break; case 'fixed': case 'active': case 'hidden': case 'forceLabel': node[k] = !!copy[k]; break; case 'color': case 'label': node[k] = (copy[k] || '').toString(); break; default: node['attr'][k] = copy[k]; } } return self; }; /** * Deletes one or several nodes from the graph, and the related edges. * @param {(string|Array.)} v A string ID, or an Array of several * IDs. * @return {Graph} Returns itself. */ function dropNode(v) { var a = (v instanceof Array ? v : [v]) || []; a.forEach(function(id) { if (self.nodesIndex[id]) { var index = null; self.nodes.some(function(n, i) { if (n['id'] == id) { index = i; return true; } return false; }); index != null && self.nodes.splice(index, 1); delete self.nodesIndex[id]; var edgesToRemove = []; self.edges = self.edges.filter(function(e) { if (e['source']['id'] == id) { delete self.edgesIndex[e['id']]; e['target']['degree']--; e['target']['inDegree']--; return false; }else if (e['target']['id'] == id) { delete self.edgesIndex[e['id']]; e['source']['degree']--; e['source']['outDegree']--; return false; } return true; }); }else { sigma.log('Node "' + id + '" does not exist.'); } }); return self; }; /** * Inserts an edge in the graph. * @param {string} id The edge ID. * @param {string} source The ID of the edge source. * @param {string} target The ID of the edge target. * @param {object} params An object containing the different parameters * of the edge. * @return {Graph} Returns itself. */ function addEdge(id, source, target, params) { if (self.edgesIndex[id]) { throw new Error('Edge "' + id + '" already exists.'); } if (!self.nodesIndex[source]) { var s = 'Edge\'s source "' + source + '" does not exist yet.'; throw new Error(s); } if (!self.nodesIndex[target]) { var s = 'Edge\'s target "' + target + '" does not exist yet.'; throw new Error(s); } params = params || {}; var e = { 'source': self.nodesIndex[source], 'target': self.nodesIndex[target], 'size': 1, 'weight': 1, 'displaySize': 0.5, 'label': id.toString(), 'id': id.toString(), 'hidden': false, 'attr': {} }; e['source']['degree']++; e['source']['outDegree']++; e['target']['degree']++; e['target']['inDegree']++; for (var k in params) { switch (k) { case 'id': case 'source': case 'target': break; case 'hidden': e[k] = !!params[k]; break; case 'size': case 'weight': e[k] = +params[k]; break; case 'color': e[k] = params[k].toString(); break; case 'type': e[k] = params[k].toString(); break; case 'label': e[k] = params[k]; break; default: e['attr'][k] = params[k]; } } self.edges.push(e); self.edgesIndex[id.toString()] = e; return self; }; /** * Generates the clone of a edge, to make it easier to be exported. * @private * @param {Object} edge The edge to clone. * @return {Object} The clone of the edge. */ function cloneEdge(edge) { return { 'source': edge['source']['id'], 'target': edge['target']['id'], 'size': edge['size'], 'type': edge['type'], 'weight': edge['weight'], 'displaySize': edge['displaySize'], 'label': edge['label'], 'hidden': edge['hidden'], 'id': edge['id'], 'attr': edge['attr'], 'color': edge['color'] }; }; /** * Checks the clone of an edge, and inserts its values when possible. For * example, it is possible to modify the label or the type of an edge, but it * is not possible to modify its display values or its id. * @private * @param {Object} edge The original edge. * @param {Object} copy The clone. * @return {Graph} Returns itself. */ function checkEdge(edge, copy) { for (var k in copy) { switch (k) { case 'id': case 'displaySize': break; case 'weight': case 'size': edge[k] = +copy[k]; break; case 'source': case 'target': edge[k] = self.nodesIndex[k] || edge[k]; break; case 'hidden': edge[k] = !!copy[k]; break; case 'color': case 'label':