/* sigmajs.org - an open-source light-weight JavaScript graph drawing library - Version: 0.1 - Author: Alexis Jacomy - License: MIT */ |
// Define packages: |
var sigma={tools:{},classes:{},instances:{}}; |
var sigma = {}; |
(function(){if(!Array.prototype.some)Array.prototype.some=function(i,n){var g=this.length;if("function"!=typeof i)throw new TypeError;for(var j=0;j<g;j++)if(j in this&&i.call(n,this[j],j,this))return!0;return!1};if(!Array.prototype.forEach)Array.prototype.forEach=function(i,n){var g=this.length;if("function"!=typeof i)throw new TypeError;for(var j=0;j<g;j++)j in this&&i.call(n,this[j],j,this)};if(!Array.prototype.map)Array.prototype.map=function(i,n){var g=this.length;if("function"!=typeof i)throw new TypeError; |
sigma.tools = {}; |
for(var j=Array(g),m=0;m<g;m++)m in this&&(j[m]=i.call(n,this[m],m,this));return j};if(!Array.prototype.filter)Array.prototype.filter=function(i,n){var g=this.length;if("function"!=typeof i)throw new TypeError;for(var j=[],m=0;m<g;m++)if(m in this){var t=this[m];i.call(n,t,m,this)&&j.push(t)}return j};if(!Object.keys)Object.keys=function(){var i=Object.prototype.hasOwnProperty,n=!{toString:null}.propertyIsEnumerable("toString"),g="toString,toLocaleString,valueOf,hasOwnProperty,isPrototypeOf,propertyIsEnumerable,constructor".split(","), |
sigma.classes = {}; |
j=g.length;return function(m){if("object"!==typeof m&&"function"!==typeof m||null===m)throw new TypeError("Object.keys called on non-object");var t=[],p;for(p in m)i.call(m,p)&&t.push(p);if(n)for(p=0;p<j;p++)i.call(m,g[p])&&t.push(g[p]);return t}}()})();sigma.classes.Cascade=function(){this.p={};this.config=function(i,n){if("string"==typeof i&&void 0==n)return this.p[i];var g="object"==typeof i&&void 0==n?i:{};"string"==typeof i&&(g[i]=n);for(var j in g)void 0!=this.p[j]&&(this.p[j]=g[j]);return this}}; |
sigma.instances = {}; |
sigma.classes.EventDispatcher=function(){var i={},n=this;this.one=function(g,j){if(!j||!g)return n;("string"==typeof g?g.split(" "):g).forEach(function(g){i[g]||(i[g]=[]);i[g].push({h:j,one:!0})});return n};this.bind=function(g,j){if(!j||!g)return n;("string"==typeof g?g.split(" "):g).forEach(function(g){i[g]||(i[g]=[]);i[g].push({h:j,one:!1})});return n};this.unbind=function(g,j){g||(i={});var m="string"==typeof g?g.split(" "):g;j?m.forEach(function(g){i[g]&&(i[g]=i[g].filter(function(g){return g.h!= |
|
j}));i[g]&&0==i[g].length&&delete i[g]}):m.forEach(function(g){delete i[g]});return n};this.dispatch=function(g,j){i[g]&&(i[g].forEach(function(i){i.h({type:g,content:j,target:n})}),i[g]=i[g].filter(function(g){return!g.one}));return n}}; |
// Adding Array helpers, if not present yet: |
(function(){var i;function n(){function b(a){return{x:a.x,y:a.y,size:a.size,degree:a.degree,displayX:a.displayX,displayY:a.displayY,displaySize:a.displaySize,label:a.label,id:a.id,color:a.color,fixed:a.fixed,active:a.active,hidden:a.hidden,attr:a.attr}}function h(a){return{source:a.source.id,target:a.target.id,size:a.size,type:a.type,weight:a.weight,displaySize:a.displaySize,label:a.label,id:a.id,attr:a.attr,color:a.color}}function e(){d.nodes=[];d.nodesIndex={};d.edges=[];d.edgesIndex={};return d} |
(function() { |
sigma.classes.Cascade.call(this);sigma.classes.EventDispatcher.call(this);var d=this;this.p={minNodeSize:0,maxNodeSize:0,minEdgeSize:0,maxEdgeSize:0,scalingMode:"inside",nodesPowRatio:0.5,edgesPowRatio:0};this.borders={};e();this.addNode=function(a,b){if(d.nodesIndex[a])throw Error('Node "'+a+'" already exists.');var b=b||{},c={x:0,y:0,size:1,degree:0,fixed:!1,active:!1,hidden:!1,label:a.toString(),id:a.toString(),attr:{}},e;for(e in b)switch(e){case "id":break;case "x":case "y":case "size":c[e]= |
if (!Array.prototype.some) { |
+b[e];break;case "fixed":case "active":case "hidden":c[e]=!!b[e];break;case "color":case "label":c[e]=b[e];break;default:c.attr[e]=b[e]}d.nodes.push(c);d.nodesIndex[a.toString()]=c;return d};this.addEdge=function(a,b,c,e){if(d.edgesIndex[a])throw Error('Edge "'+a+'" already exists.');if(!d.nodesIndex[b])throw Error("Edge's source \""+b+'" does not exist yet.');if(!d.nodesIndex[c])throw Error("Edge's target \""+c+'" does not exist yet.');e=e||{};b={source:d.nodesIndex[b],target:d.nodesIndex[c],size:1, |
Array.prototype.some = function(fun /*, thisp*/) { |
weight:1,displaySize:0.5,label:a.toString(),id:a.toString(),attr:{}};b.source.degree++;b.target.degree++;for(var k in e)switch(k){case "id":case "source":case "target":break;case "size":b[k]=+e[k];break;case "color":b[k]=e[k].toString();break;case "type":b[k]=e[k].toString();break;case "label":b[k]=e[k];break;default:b.attr[k]=e[k]}d.edges.push(b);d.edgesIndex[a.toString()]=b;return d};this.dropNode=function(a){((a instanceof Array?a:[a])||[]).forEach(function(a){if(d.nodesIndex[a]){var c=null;d.nodes.some(function(b, |
var len = this.length; |
d){return b.id==a?(c=d,!0):!1});null!=c&&d.nodes.splice(c,1);delete d.nodesIndex[a];d.edges=d.edges.filter(function(c){return c.source.id==a||c.target.id==a?(delete d.edgesIndex[c.id],!1):!0})}else sigma.log('Node "'+a+'" does not exist.')});return d};this.dropEdge=function(a){((a instanceof Array?a:[a])||[]).forEach(function(a){if(d.edgesIndex[a]){var c=null;d.edges.some(function(b,d){return b.id==a?(c=d,!0):!1});null!=c&&d.edges.splice(c,1);delete d.edgesIndex[a]}else sigma.log('Edge "'+a+'" does not exist.')}); |
if (typeof fun != 'function') { |
return d};this.iterEdges=function(a,b){var c=b?b.map(function(a){return d.edgesIndex[a]}):d.edges,e=c.map(h);e.forEach(a);c.forEach(function(a,c){var b=e[c],l;for(l in b)switch(l){case "id":case "weight":case "displaySize":break;case "size":a[l]=+b[l];break;case "source":case "target":a[l]=d.nodesIndex[l]||a[l];break;case "color":case "label":case "type":a[l]=(b[l]||"").toString();break;default:a.attr[l]=b[l]}});return d};this.iterNodes=function(a,e){var c=e?e.map(function(a){return d.nodesIndex[a]}): |
throw new TypeError(); |
d.nodes,h=c.map(b);h.forEach(a);c.forEach(function(a,c){var b=h[c],d;for(d in b)switch(d){case "id":case "attr":case "degree":case "displayX":case "displayY":case "displaySize":break;case "x":case "y":case "size":a[d]=+b[d];break;case "fixed":case "active":case "hidden":a[d]=!!b[d];break;case "color":case "label":a[d]=b[d].toString();break;default:a.attr[d]=b[d]}});return d};this.getEdges=function(a){var b=((a instanceof Array?a:[a])||[]).map(function(a){return h(d.edgesIndex[a])});return a instanceof |
} |
Array?b:b[0]};this.getNodes=function(a){var e=((a instanceof Array?a:[a])||[]).map(function(a){return b(d.nodesIndex[a])});return a instanceof Array?e:e[0]};this.empty=e;this.rescale=function(a,b,c,e){var k=0,f=0;c&&d.nodes.forEach(function(a){f=Math.max(a.size,f)});e&&d.edges.forEach(function(a){k=Math.max(a.size,k)});var f=f||1,k=k||1,h,g,s,q;c&&d.nodes.forEach(function(a){g=Math.max(a.x,g||a.x);h=Math.min(a.x,h||a.x);q=Math.max(a.y,q||a.y);s=Math.min(a.y,s||a.y)});var z="outside"==d.p.scalingMode? |
|
Math.max(a/Math.max(g-h,1),b/Math.max(q-s,1)):Math.min(a/Math.max(g-h,1),b/Math.max(q-s,1)),i,j;!d.p.maxNodeSize&&!d.p.minNodeSize?(i=1,j=0):d.p.maxNodeSize==d.p.minNodeSize?(i=0,j=d.p.maxNodeSize):(i=(d.p.maxNodeSize-d.p.minNodeSize)/f,j=d.p.minNodeSize);var m,w;!d.p.maxEdgeSize&&!d.p.minEdgeSize?(m=1,w=0):(m=d.p.maxEdgeSize==d.p.minEdgeSize?0:(d.p.maxEdgeSize-d.p.minEdgeSize)/k,w=d.p.minEdgeSize);c&&d.nodes.forEach(function(c){c.displaySize=c.size*i+j;if(!c.fixed)c.displayX=(c.x-(g+h)/2)*z+a/2, |
var thisp = arguments[1]; |
c.displayY=(c.y-(q+s)/2)*z+b/2});e&&d.edges.forEach(function(a){a.displaySize=a.size*m+w});return d};this.translate=function(a,b,c,e,k){var f=Math.pow(c,d.p.nodesPowRatio);e&&d.nodes.forEach(function(d){if(!d.fixed)d.displayX=d.displayX*c+a,d.displayY=d.displayY*c+b;d.displaySize*=f});f=Math.pow(c,d.p.edgesPowRatio);k&&d.edges.forEach(function(a){a.displaySize*=f});return d};this.setBorders=function(){d.borders={};d.nodes.forEach(function(a){d.borders.minX=Math.min(void 0==d.borders.minX?a.displayX- |
for (var i = 0; i < len; i++) { |
a.displaySize:d.borders.minX,a.displayX-a.displaySize);d.borders.maxX=Math.max(void 0==d.borders.maxX?a.displayX+a.displaySize:d.borders.maxX,a.displayX+a.displaySize);d.borders.minY=Math.min(void 0==d.borders.minY?a.displayY-a.displaySize:d.borders.minY,a.displayY-a.displaySize);d.borders.maxY=Math.max(void 0==d.borders.maxY?a.displayY-a.displaySize:d.borders.maxY,a.displayY-a.displaySize)})};this.checkHover=function(a,b){var c,e,k,f=[],h=[];d.nodes.forEach(function(d){if(d.hidden)d.hover=!1;else{c= |
if (i in this && |
Math.abs(d.displayX-a);e=Math.abs(d.displayY-b);k=d.displaySize;var s=d.hover,q=c<k&&e<k&&Math.sqrt(c*c+e*e)<k;if(s&&!q)d.hover=!1,h.push(d.id);else if(q&&!s)d.hover=!0,f.push(d.id)}});f.length&&d.dispatch("overnodes",f);h.length&&d.dispatch("outnodes",h);return d}}function g(b,h){function e(){var a;a="<p>GLOBAL :</p>";for(var b in d.p.globalProbes)a+="<p>"+b+" : "+d.p.globalProbes[b]()+"</p>";a+="<br><p>LOCAL :</p>";for(b in d.p.localProbes)a+="<p>"+b+" : "+d.p.localProbes[b]()+"</p>";d.p.dom.innerHTML= |
fun.call(thisp, this[i], i, this)) { |
a;return d}sigma.classes.Cascade.call(this);var d=this;this.instance=b;this.monitoring=!1;this.p={fps:40,dom:h,globalProbes:{"Time (ms)":sigma.chronos.getExecutionTime,Queue:sigma.chronos.getQueuedTasksCount,Tasks:sigma.chronos.getTasksCount,FPS:sigma.chronos.getFPS},localProbes:{"Nodes count":function(){return d.instance.graph.nodes.length},"Edges count":function(){return d.instance.graph.edges.length}}};this.activate=function(){if(!d.monitoring)d.monitoring=window.setInterval(e,1E3/d.p.fps);return d}; |
return true; |
this.desactivate=function(){if(d.monitoring)window.clearInterval(d.monitoring),d.monitoring=null,d.p.dom.innerHTML="";return d}}function j(b){function h(b){if(a.p.mouseEnabled&&(e(a.mouseX,a.mouseY,a.ratio*(0<(void 0!=b.wheelDelta&&b.wheelDelta||void 0!=b.detail&&-b.detail)?a.p.zoomMultiply:1/a.p.zoomMultiply)),a.p.blockScroll))b.preventDefault?b.preventDefault():b.returnValue=!1}function e(b,c,e){if(!a.isMouseDown&&(window.clearInterval(a.interpolationID),n=void 0!=e,i=a.stageX,j=b,k=a.stageY,l= |
} |
c,f=e||a.ratio,f=Math.min(Math.max(f,a.p.minRatio),a.p.maxRatio),u=a.p.directZooming?1-(n?a.p.zoomDelta:a.p.dragDelta):0,a.ratio!=f||a.stageX!=j||a.stageY!=l))d(),a.interpolationID=window.setInterval(d,50),a.dispatch("startinterpolate")}function d(){u+=n?a.p.zoomDelta:a.p.dragDelta;u=Math.min(u,1);var b=sigma.easing.quadratic.easeout(u),c=a.ratio;a.ratio=c*(1-b)+f*b;n?(a.stageX=j+(a.stageX-j)*a.ratio/c,a.stageY=l+(a.stageY-l)*a.ratio/c):(a.stageX=i*(1-b)+j*b,a.stageY=k*(1-b)+l*b);a.dispatch("interpolate"); |
} |
if(1<=u)window.clearInterval(a.interpolationID),b=a.ratio,n?(a.ratio=f,a.stageX=j+(a.stageX-j)*a.ratio/b,a.stageY=l+(a.stageY-l)*a.ratio/b):(a.stageX=j,a.stageY=l),a.dispatch("stopinterpolate")}sigma.classes.Cascade.call(this);sigma.classes.EventDispatcher.call(this);var a=this;this.p={minRatio:1,maxRatio:32,marginRatio:1,zoomDelta:0.1,dragDelta:0.3,zoomMultiply:2,directZooming:!1,blockScroll:!0,inertia:1.1,mouseEnabled:!0};var g=0,c=0,i=0,k=0,f=1,j=0,l=0,s=0,q=0,z=0,m=0,u=0,n=!1;this.stageY=this.stageX= |
|
0;this.ratio=1;this.mouseY=this.mouseX=0;this.isMouseDown=!1;b.addEventListener("DOMMouseScroll",h,!0);b.addEventListener("mousewheel",h,!0);b.addEventListener("mousemove",function(b){a.mouseX=void 0!=b.offsetX&&b.offsetX||void 0!=b.layerX&&b.layerX||void 0!=b.clientX&&b.clientX;a.mouseY=void 0!=b.offsetY&&b.offsetY||void 0!=b.layerY&&b.layerY||void 0!=b.clientY&&b.clientY;if(a.isMouseDown){var d=a.mouseX-g+i,f=a.mouseY-c+k;if(d!=a.stageX||f!=a.stageY)q=s,m=z,s=d,z=f,a.stageX=d,a.stageY=f,a.dispatch("drag")}a.dispatch("move"); |
return false; |
b.preventDefault?b.preventDefault():b.returnValue=!1},!0);b.addEventListener("mousedown",function(b){if(a.p.mouseEnabled)a.isMouseDown=!0,a.dispatch("mousedown"),i=a.stageX,k=a.stageY,g=a.mouseX,c=a.mouseY,q=s=a.stageX,m=z=a.stageY,a.dispatch("startdrag"),b.preventDefault?b.preventDefault():b.returnValue=!1},!0);document.addEventListener("mouseup",function(b){if(a.p.mouseEnabled&&a.isMouseDown)a.isMouseDown=!1,a.dispatch("mouseup"),(i!=a.stageX||k!=a.stageY)&&e(a.stageX+a.p.inertia*(a.stageX-q),a.stageY+ |
}; |
a.p.inertia*(a.stageY-m)),b.preventDefault?b.preventDefault():b.returnValue=!1},!0);this.checkBorders=function(){return a};this.interpolate=e}function m(b,h,e,d,a,g,c){function i(a){var b=d,c="fixed"==f.p.labelSize?f.p.defaultLabelSize:f.p.labelSizeRatio*a.displaySize;b.font=(f.p.hoverFontStyle||f.p.fontStyle||"")+" "+c+"px "+(f.p.hoverFont||f.p.font||"");b.fillStyle="node"==f.p.labelHoverBGColor?a.color||f.p.defaultNodeColor:f.p.defaultHoverLabelBGColor;b.beginPath();if(f.p.labelHoverShadow)b.shadowOffsetX= |
} |
0,b.shadowOffsetY=0,b.shadowBlur=4,b.shadowColor=f.p.labelHoverShadowColor;sigma.tools.drawRoundRect(b,Math.round(a.displayX-c/2-2),Math.round(a.displayY-c/2-2),Math.round(b.measureText(a.label).width+1.5*a.displaySize+c/2+4),Math.round(c+4),Math.round(c/2+2),"left");b.closePath();b.fill();b.shadowOffsetX=0;b.shadowOffsetY=0;b.shadowBlur=0;b.beginPath();b.fillStyle="node"==f.p.nodeBorderColor?a.color||f.p.defaultNodeColor:f.p.defaultNodeBorderColor;b.arc(Math.round(a.displayX),Math.round(a.displayY), |
|
a.displaySize+f.p.borderSize,0,2*Math.PI,!0);b.closePath();b.fill();b.beginPath();b.fillStyle="node"==f.p.nodeHoverColor?a.color||f.p.defaultNodeColor:f.p.defaultNodeHoverColor;b.arc(Math.round(a.displayX),Math.round(a.displayY),a.displaySize,0,2*Math.PI,!0);b.closePath();b.fill();b.fillStyle="node"==f.p.labelHoverColor?a.color||f.p.defaultNodeColor:f.p.defaultLabelHoverColor;b.fillText(a.label,Math.round(a.displayX+1.5*a.displaySize),Math.round(a.displayY+c/2-3));return f}function k(a){if(isNaN(a.x)|| |
if (!Array.prototype.forEach) { |
isNaN(a.y))throw Error("A node's coordinate is not a number (id: "+a.id+")");return!a.hidden&&a.displayX+a.displaySize>-j/3&&a.displayX-a.displaySize<4*j/3&&a.displayY+a.displaySize>-l/3&&a.displayY-a.displaySize<4*l/3}sigma.classes.Cascade.call(this);var f=this;this.p={labelColor:"default",defaultLabelColor:"#000",labelHoverBGColor:"default",defaultHoverLabelBGColor:"#fff",labelHoverShadow:!0,labelHoverShadowColor:"#000",labelHoverColor:"default",defaultLabelHoverColor:"#000",labelActiveBGColor:"default", |
Array.prototype.forEach = function(fun /*, thisp*/) { |
defaultActiveLabelBGColor:"#fff",labelActiveShadow:!0,labelActiveShadowColor:"#000",labelActiveColor:"default",defaultLabelActiveColor:"#000",labelSize:"fixed",defaultLabelSize:12,labelSizeRatio:2,labelThreshold:6,font:"Arial",hoverFont:"",activeFont:"",fontStyle:"",hoverFontStyle:"",activeFontStyle:"",edgeColor:"source",defaultEdgeColor:"#aaa",defaultEdgeType:"line",defaultNodeColor:"#aaa",nodeHoverColor:"node",defaultNodeHoverColor:"#fff",nodeActiveColor:"node",defaultNodeActiveColor:"#fff",borderSize:0, |
var len = this.length; |
nodeBorderColor:"node",defaultNodeBorderColor:"#fff",edgesSpeed:200,nodesSpeed:200,labelsSpeed:200};var j=g,l=c;this.currentLabelIndex=this.currentNodeIndex=this.currentEdgeIndex=0;this.task_drawLabel=function(){for(var b=a.nodes.length,c=0;c++<f.p.labelsSpeed&&f.currentLabelIndex<b;)if(f.isOnScreen(a.nodes[f.currentLabelIndex])){var d=a.nodes[f.currentLabelIndex++],h=e;if(d.displaySize>=f.p.labelThreshold){var k="fixed"==f.p.labelSize?f.p.defaultLabelSize:f.p.labelSizeRatio*d.displaySize;h.font= |
if (typeof fun != 'function') { |
f.p.fontStyle+k+"px "+f.p.font;h.fillStyle="node"==f.p.labelColor?d.color||f.p.defaultNodeColor:f.p.defaultLabelColor;h.fillText(d.label,Math.round(d.displayX+1.5*d.displaySize),Math.round(d.displayY+k/2-3))}}else f.currentLabelIndex++;return f.currentLabelIndex<b};this.task_drawEdge=function(){for(var b=a.edges.length,c,d,e=0;e++<f.p.edgesSpeed&&f.currentEdgeIndex<b;)if(c=a.edges[f.currentEdgeIndex].source,d=a.edges[f.currentEdgeIndex].target,c.hidden||d.hidden||!f.isOnScreen(c)&&!f.isOnScreen(d))f.currentEdgeIndex++; |
throw new TypeError(); |
else{c=a.edges[f.currentEdgeIndex++];d=c.source.displayX;var k=c.source.displayY,g=c.target.displayX,i=c.target.displayY,j=c.color;if(!j)switch(f.p.edgeColor){case "source":j=c.source.color||f.p.defaultNodeColor;break;case "target":j=c.target.color||f.p.defaultNodeColor;break;default:j=f.p.defaultEdgeColor}var l=h;switch(c.type||f.p.defaultEdgeType){case "curve":l.strokeStyle=j;l.lineWidth=c.displaySize/3;l.beginPath();l.moveTo(d,k);l.quadraticCurveTo((d+g)/2+(i-k)/4,(k+i)/2+(d-g)/4,g,i);l.stroke(); |
} |
break;default:l.strokeStyle=j,l.lineWidth=c.displaySize/3,l.beginPath(),l.moveTo(d,k),l.lineTo(g,i),l.stroke()}}return f.currentEdgeIndex<b};this.task_drawNode=function(){for(var c=a.nodes.length,d=0;d++<f.p.nodesSpeed&&f.currentNodeIndex<c;)if(f.isOnScreen(a.nodes[f.currentNodeIndex])){var e=a.nodes[f.currentNodeIndex++],k=Math.round(10*e.displaySize)/10,h=b;h.fillStyle=e.color;h.beginPath();h.arc(e.displayX,e.displayY,k,0,2*Math.PI,!0);h.closePath();h.fill();e.hover&&i(e)}else f.currentNodeIndex++; |
|
return f.currentNodeIndex<c};this.drawActiveNode=function(a){var b=d;if(!k(a))return f;var c="fixed"==f.p.labelSize?f.p.defaultLabelSize:f.p.labelSizeRatio*a.displaySize;b.font=(f.p.activeFontStyle||f.p.fontStyle||"")+" "+c+"px "+(f.p.activeFont||f.p.font||"");b.fillStyle="node"==f.p.labelHoverBGColor?a.color||f.p.defaultNodeColor:f.p.defaultActiveLabelBGColor;b.beginPath();if(f.p.labelActiveShadow)b.shadowOffsetX=0,b.shadowOffsetY=0,b.shadowBlur=4,b.shadowColor=f.p.labelActiveShadowColor;sigma.tools.drawRoundRect(b, |
var thisp = arguments[1]; |
Math.round(a.displayX-c/2-2),Math.round(a.displayY-c/2-2),Math.round(b.measureText(a.label).width+1.5*a.displaySize+c/2+4),Math.round(c+4),Math.round(c/2+2),"left");b.closePath();b.fill();b.shadowOffsetX=0;b.shadowOffsetY=0;b.shadowBlur=0;b.beginPath();b.fillStyle="node"==f.p.nodeBorderColor?a.color||f.p.defaultNodeColor:f.p.defaultNodeBorderColor;b.arc(Math.round(a.displayX),Math.round(a.displayY),a.displaySize+f.p.borderSize,0,2*Math.PI,!0);b.closePath();b.fill();b.beginPath();b.fillStyle="node"== |
for (var i = 0; i < len; i++) { |
f.p.nodeActiveColor?a.color||f.p.defaultNodeColor:f.p.defaultNodeActiveColor;b.arc(Math.round(a.displayX),Math.round(a.displayY),a.displaySize,0,2*Math.PI,!0);b.closePath();b.fill();b.fillStyle="node"==f.p.labelActiveColor?a.color||f.p.defaultNodeColor:f.p.defaultLabelActiveColor;b.fillText(a.label,Math.round(a.displayX+1.5*a.displaySize),Math.round(a.displayY+c/2-3));return f};this.drawHoverNode=i;this.isOnScreen=k;this.resize=function(a,b){j=a;l=b;return f}}function t(b,h){function e(){sigma.chronos.removeTask("node_"+ |
if (i in this) { |
c.id,2).removeTask("edge_"+c.id,2).removeTask("label_"+c.id,2).stopTasks();return c}function d(a,b){c.domElements[a]=document.createElement(b);c.domElements[a].style.position="absolute";c.domElements[a].setAttribute("id","sigma_"+a+"_"+c.id);c.domElements[a].setAttribute("class","sigma_"+a+"_"+b);c.domElements[a].setAttribute("width",c.width+"px");c.domElements[a].setAttribute("height",c.height+"px");c.domRoot.appendChild(c.domElements[a]);return c}function a(){c.p.drawHoverNodes&&(c.graph.checkHover(c.mousecaptor.mouseX, |
fun.call(thisp, this[i], i, this); |
c.mousecaptor.mouseY),c.graph.nodes.forEach(function(a){a.hover&&!a.active&&c.plotter.drawHoverNode(a)}));return c}function A(){c.p.drawActiveNodes&&c.graph.nodes.forEach(function(a){a.active&&c.plotter.drawActiveNode(a)});return c}sigma.classes.Cascade.call(this);sigma.classes.EventDispatcher.call(this);var c=this;this.id=h.toString();this.p={auto:!0,drawNodes:2,drawEdges:1,drawLabels:2,lastNodes:2,lastEdges:0,lastLabels:2,drawHoverNodes:!0,drawActiveNodes:!0};this.domRoot=b;this.width=this.domRoot.offsetWidth; |
} |
this.height=this.domRoot.offsetHeight;this.graph=new n;this.domElements={};d("edges","canvas");d("nodes","canvas");d("labels","canvas");d("hover","canvas");d("monitor","div");d("mouse","canvas");this.plotter=new m(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);this.monitor=new g(this,this.domElements.monitor);this.mousecaptor=new j(this.domElements.mouse, |
} |
this.id);this.mousecaptor.bind("drag interpolate",function(){c.draw(c.p.auto?2:c.p.drawNodes,c.p.auto?0:c.p.drawEdges,c.p.auto?2:c.p.drawLabels,!0)}).bind("stopdrag stopinterpolate",function(){c.draw(c.p.auto?2:c.p.drawNodes,c.p.auto?1:c.p.drawEdges,c.p.auto?2:c.p.drawLabels,!0)}).bind("mousedown mouseup",function(a){var b=c.graph.nodes.filter(function(a){return!!a.hover}).map(function(a){return a.id});c.dispatch("mousedown"==a.type?"downgraph":"upgraph");b.length&&c.dispatch("mousedown"==a.type? |
}; |
"downnodes":"upnodes",b)}).bind("move",function(){c.domElements.hover.getContext("2d").clearRect(0,0,c.domElements.hover.width,c.domElements.hover.height);a();A()});sigma.chronos.bind("startgenerators",function(){sigma.chronos.getGeneratorsIDs().some(function(a){return!!a.match(RegExp("_ext_"+c.id+"$",""))})&&c.draw(c.p.auto?2:c.p.drawNodes,c.p.auto?0:c.p.drawEdges,c.p.auto?2:c.p.drawLabels)}).bind("stopgenerators",function(){c.draw()});for(var B=0;B<i.length;B++)i[B](this);this.draw=function(a,b, |
} |
d,h){if(h&&sigma.chronos.getGeneratorsIDs().some(function(a){return!!a.match(RegExp("_ext_"+c.id+"$",""))}))return c;a=void 0==a?c.p.drawNodes:a;b=void 0==b?c.p.drawEdges:b;d=void 0==d?c.p.drawLabels:d;h={nodes:a,edges:b,labels:d};c.p.lastNodes=a;c.p.lastEdges=b;c.p.lastLabels=d;e();c.graph.rescale(c.width,c.height,0<a,0<b).setBorders();c.mousecaptor.checkBorders(c.graph.borders,c.width,c.height);c.graph.translate(c.mousecaptor.stageX,c.mousecaptor.stageY,c.mousecaptor.ratio,0<a,0<b);c.dispatch("graphscaled"); |
|
for(var g in c.domElements)"canvas"==c.domElements[g].nodeName.toLowerCase()&&(void 0==h[g]||0<=h[g])&&c.domElements[g].getContext("2d").clearRect(0,0,c.domElements[g].width,c.domElements[g].height);c.plotter.currentEdgeIndex=0;c.plotter.currentNodeIndex=0;c.plotter.currentLabelIndex=0;g=null;h=!1;if(a)if(1<a)for(;c.plotter.task_drawNode(););else sigma.chronos.addTask(c.plotter.task_drawNode,"node_"+c.id,!1),h=!0,g="node_"+c.id;if(d)if(1<d)for(;c.plotter.task_drawLabel(););else g?sigma.chronos.queueTask(c.plotter.task_drawLabel, |
if (!Array.prototype.map) { |
"label_"+c.id,g):sigma.chronos.addTask(c.plotter.task_drawLabel,"label_"+c.id,!1),h=!0,g="label_"+c.id;if(b)if(1<b)for(;c.plotter.task_drawEdge(););else g?sigma.chronos.queueTask(c.plotter.task_drawEdge,"edge_"+c.id,g):sigma.chronos.addTask(c.plotter.task_drawEdge,"edge_"+c.id,!1),h=!0,g="edge_"+c.id;c.dispatch("draw");c.refresh();h&&sigma.chronos.runTasks();return c};this.resize=function(a,b){var d=c.width,e=c.height;void 0!=a&&void 0!=b?(c.width=a,c.height=b):(c.width=c.domRoot.offsetWidth,c.height= |
Array.prototype.map = function(fun /*, thisp*/) { |
c.domRoot.offsetHeight);if(d!=c.width||e!=c.height){for(var h in c.domElements)c.domElements[h].setAttribute("width",c.width+"px"),c.domElements[h].setAttribute("height",c.height+"px");c.plotter.resize(c.width,c.height);c.draw(c.p.lastNodes,c.p.lastEdges,c.p.lastLabels,!0)}return c};this.refresh=function(){c.domElements.hover.getContext("2d").clearRect(0,0,c.domElements.hover.width,c.domElements.hover.height);a();A();return c};this.drawHover=a;this.drawActive=A;this.clearSchedule=e;window.addEventListener("resize", |
var len = this.length; |
function(){c.resize()})}function p(b){var h=this;sigma.classes.EventDispatcher.call(this);this._core=b;this.kill=function(){};this.getID=function(){return b.id};this.configProperties=function(e,d){var a=b.config(e,d);return a==b?h:a};this.drawingProperties=function(e,d){var a=b.plotter.config(e,d);return a==b.plotter?h:a};this.mouseProperties=function(e,d){var a=b.mousecaptor.config(e,d);return a==b.mousecaptor?h:a};this.graphProperties=function(e,d){var a=b.graph.config(e,d);return a==b.graph?h: |
if (typeof fun != 'function') { |
a};this.getMouse=function(){return{mouseX:b.mousecaptor.mouseX,mouseY:b.mousecaptor.mouseY,down:b.mousecaptor.isMouseDown}};this.position=function(e,d,a){if(0==arguments.length)return{stageX:b.mousecaptor.stageX,stageY:b.mousecaptor.stageY,ratio:b.mousecaptor.ratio};b.mousecaptor.stageX=void 0!=e?e:b.mousecaptor.stageX;b.mousecaptor.stageY=void 0!=d?d:b.mousecaptor.stageY;b.mousecaptor.ratio=void 0!=a?a:b.mousecaptor.ratio;return h};this.goTo=function(e,d,a){b.mousecaptor.interpolate(e,d,a);return h}; |
throw new TypeError(); |
this.zoomTo=function(e,d,a){a=Math.min(Math.max(b.mousecaptor.config("minRatio"),a),b.mousecaptor.config("maxRatio"));a==b.mousecaptor.ratio?b.mousecaptor.interpolate(e-b.width/2+b.mousecaptor.stageX,d-b.height/2+b.mousecaptor.stageY):b.mousecaptor.interpolate((a*e-b.mousecaptor.ratio*b.width/2)/(a-b.mousecaptor.ratio),(a*d-b.mousecaptor.ratio*b.height/2)/(a-b.mousecaptor.ratio),a);return h};this.resize=function(e,d){b.resize(e,d);return h};this.draw=function(e,d,a,g){b.draw(e,d,a,g);return h};this.refresh= |
} |
function(){b.refresh();return h};this.addGenerator=function(e,d,a){sigma.chronos.addGenerator(e+"_ext_"+b.id,d,a);return h};this.removeGenerator=function(e){sigma.chronos.removeGenerator(e+"_ext_"+b.id);return h};this.addNode=function(e,d){b.graph.addNode(e,d);return h};this.addEdge=function(e,d,a,g){b.graph.addEdge(e,d,a,g);return h};this.dropNode=function(e){b.graph.dropNode(e);return h};this.dropEdge=function(e){b.graph.dropEdge(e);return h};this.pushGraph=function(e,d){e.nodes&&e.nodes.forEach(function(a){a.id&& |
|
(!d||!b.graph.nodesIndex[a.id])&&h.addNode(a.id,a)});e.edges&&e.edges.forEach(function(a){(validID=a.source&&a.target&&a.id)&&(!d||!b.graph.edgesIndex[a.id])&&h.addNode(a.id,a.source,a.target,a)});return h};this.emptyGraph=function(){b.graph.empty();return h};this.getNodesCount=function(){return b.graph.nodes.length};this.getEdgesCount=function(){return b.graph.edges.length};this.iterNodes=function(e,d){b.graph.iterNodes(e,d);return h};this.iterEdges=function(e,d){b.graph.iterEdges(e,d);return h}; |
var res = new Array(len); |
this.getNodes=function(e){return b.graph.getNodes(e)};this.getEdges=function(e){return b.graph.getEdges(e)};this.activateMonitoring=function(){return b.monitor.activate()};this.desactivateMonitoring=function(){return b.monitor.desactivate()};b.bind("downnodes upnodes downgraph upgraph",function(b){h.dispatch(b.type,b.content)});b.graph.bind("overnodes outnodes",function(b){h.dispatch(b.type,b.content)})}var x=0;i=void 0;i=[];sigma.init=function(b){b=new t(b,(++x).toString());sigma.instances[x]=new p(b); |
var thisp = arguments[1]; |
return sigma.instances[x]};sigma.addPlugin=function(b,h,e){p.prototype[b]=h;i.push(e)};sigma.chronos=new function(){function b(a){window.setTimeout(a,0);return f}function h(){for(f.dispatch("frameinserted");m&&r.length&&e(););!m||!r.length?a():(w=(new Date).getTime(),q++,C=u-p,t=p-C,f.dispatch("insertframe"),b(h))}function e(){y%=r.length;if(!r[y].task()){var a=r[y].taskName;v=v.filter(function(b){b.taskParent==a&&r.push({taskName:b.taskName,task:b.task});return b.taskParent!=a});f.dispatch("killed", |
for (var i = 0; i < len; i++) { |
r.splice(y--,1)[0])}y++;u=(new Date).getTime()-w;return u<=t}function d(){m=!0;q=y=0;x=w=(new Date).getTime();f.dispatch("start");f.dispatch("insertframe");b(h);return f}function a(){f.dispatch("stop");m=!1;return f}function g(a,b,c){if("function"!=typeof a)throw Error('Task "'+b+'" is not a function');r.push({taskName:b,task:a});m=!(!m&&!(c&&d()||1));return f}function c(a){return a?Object.keys(o).filter(function(a){return!!o[a].on}).length:Object.keys(o).length}function i(){Object.keys(o).length? |
if (i in this) { |
(f.dispatch("startgenerators"),f.unbind("killed",j),b(function(){for(var a in o)o[a].on=!0,g(o[a].task,a,!1)}),f.bind("killed",j).runTasks()):f.dispatch("stopgenerators");return f}function j(a){if(void 0!=o[a.content.taskName])o[a.content.taskName].del||!o[a.content.taskName].condition()?delete o[a.content.taskName]:o[a.content.taskName].on=!1,0==c(!0)&&i()}sigma.classes.EventDispatcher.call(this);var f=this,m=!1,l=80,n=0,q=0,p=1E3/l,t=p,u=0,x=0,w=0,C=0,o={},r=[],v=[],y=0;this.frequency=function(a){return void 0!= |
res[i] = fun.call(thisp, this[i], i, this); |
a?(l=Math.abs(1*a),p=1E3/l,q=0,f):l};this.runTasks=d;this.stopTasks=a;this.insertFrame=b;this.addTask=g;this.queueTask=function(a,b,c){if("function"!=typeof a)throw Error('Task "'+b+'" is not a function');if(!r.concat(v).some(function(a){return a.taskName==c}))throw Error('Parent task "'+c+'" of "'+b+'" is not attached.');v.push({taskParent:c,taskName:b,task:a});return f};this.removeTask=function(b,c){if(void 0==b)r=[],1==c?v=[]:2==c&&(r=v,v=[]),a();else{var d="string"==typeof b?b:"";r=r.filter(function(a){return("string"== |
} |
typeof b?a.taskName==b:a.task==b)?(d=a.taskName,!1):!0});0<c&&(v=v.filter(function(a){1==c&&a.taskParent==d&&r.push(a);return a.taskParent!=d}))}m=!(r.length&&(!a()||1));return f};this.addGenerator=function(a,b,d){if(void 0!=o[a])return f;o[a]={task:b,condition:d};0==c(!0)&&i();return f};this.removeGenerator=function(a){if(o[a])o[a].on=!1,o[a].del=!0;return f};this.startGenerators=i;this.getGeneratorsIDs=function(){return Object.keys(o)};this.getFPS=function(){m&&(n=Math.round(1E4*(q/((new Date).getTime()- |
} |
x)))/10);return n};this.getTasksCount=function(){return r.length};this.getQueuedTasksCount=function(){return v.length};this.getExecutionTime=function(){return w-x};return this};sigma.debugMode=0;sigma.log=function(){if(1==sigma.debugMode)for(var b in arguments)console.log(arguments[b]);else if(1<sigma.debugMode)for(b in arguments)throw Error(arguments[b]);return sigma};sigma.easing={linear:{},quadratic:{}};sigma.easing.linear.easenone=function(b){return b};sigma.easing.quadratic.easein=function(b){return b* |
|
b};sigma.easing.quadratic.easeout=function(b){return-b*(b-2)};sigma.easing.quadratic.easeinout=function(b){return 1>(b*=2)?0.5*b*b:-0.5*(--b*(b-2)-1)};sigma.tools.drawRoundRect=function(b,h,e,d,a,g,c){var g=g?g:0,i=c?c:[],i="string"==typeof i?i.split(" "):i,c=g&&(0<=i.indexOf("topleft")||0<=i.indexOf("top")||0<=i.indexOf("left")),j=g&&(0<=i.indexOf("topright")||0<=i.indexOf("top")||0<=i.indexOf("right")),f=g&&(0<=i.indexOf("bottomleft")||0<=i.indexOf("bottom")||0<=i.indexOf("left")),i=g&&(0<=i.indexOf("bottomright")|| |
return res; |
0<=i.indexOf("bottom")||0<=i.indexOf("right"));b.moveTo(h,e+g);c?b.arcTo(h,e,h+g,e,g):b.lineTo(h,e);j?(b.lineTo(h+d-g,e),b.arcTo(h+d,e,h+d,e+g,g)):b.lineTo(h+d,e);i?(b.lineTo(h+d,e+a-g),b.arcTo(h+d,e+a,h+d-g,e+a,g)):b.lineTo(h+d,e+a);f?(b.lineTo(h+g,e+a),b.arcTo(h,e+a,h,e+a-g,g)):b.lineTo(h,e+a);b.lineTo(h,e+g)};sigma.tools.getRGB=function(b,g){var b=b.toString(),e={r:0,g:0,b:0};if(3<=b.length&&"#"==b.charAt(0)){var d=b.length-1;6==d?e={r:parseInt(b.charAt(1)+b.charAt(2),16),g:parseInt(b.charAt(3)+ |
}; |
b.charAt(4),16),b:parseInt(b.charAt(5)+b.charAt(5),16)}:3==d&&(e={r:parseInt(b.charAt(1)+b.charAt(1),16),g:parseInt(b.charAt(2)+b.charAt(2),16),b:parseInt(b.charAt(3)+b.charAt(3),16)})}g&&(e=[e.r,e.g,e.b]);return e};sigma.tools.rgbToHex=function(b,g,e){return sigma.tools.toHex(b)+sigma.tools.toHex(g)+sigma.tools.toHex(e)};sigma.tools.toHex=function(b){b=parseInt(b,10);if(isNaN(b))return"00";b=Math.max(0,Math.min(b,255));return"0123456789ABCDEF".charAt((b-b%16)/16)+"0123456789ABCDEF".charAt(b%16)}; |
} |
sigma.publicPrototype=p.prototype})(); |
|
|
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.<string,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 += '<p>GLOBAL :</p>'; |
|
for (var k in self.p.globalProbes) { |
|
s += '<p>' + k + ' : ' + self.p.globalProbes[k]() + '</p>'; |
|
} |
|
|
|
s += '<br><p>LOCAL :</p>'; |
|
for (var k in self.p.localProbes) { |
|
s += '<p>' + k + ' : ' + self.p.localProbes[k]() + '</p>'; |
|
} |
|
|
|
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.<string>)} 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': |
|
case 'type': |
|
edge[k] = (copy[k] || '').toString(); |
|
break; |
|
default: |
|
edge['attr'][k] = copy[k]; |
|
} |
|
} |
|
|
|
return self; |
|
}; |
|
|
|
/** |
|
* Deletes one or several edges from the graph. |
|
* @param {(string|Array.<string>)} v A string ID, or an Array of several |
|
* IDs. |
|
* @return {Graph} Returns itself. |
|
*/ |
|
function dropEdge(v) { |
|
var a = (v instanceof Array ? v : [v]) || []; |
|
|
|
a.forEach(function(id) { |
|
if (self.edgesIndex[id]) { |
|
self.edgesIndex[id]['source']['degree']--; |
|
self.edgesIndex[id]['source']['outDegree']--; |
|
self.edgesIndex[id]['target']['degree']--; |
|
self.edgesIndex[id]['target']['inDegree']--; |
|
|
|
var index = null; |
|
self.edges.some(function(n, i) { |
|
if (n['id'] == id) { |
|
index = i; |
|
return true; |
|
} |
|
return false; |
|
}); |
|
|
|
index != null && self.edges.splice(index, 1); |
|
delete self.edgesIndex[id]; |
|
}else { |
|
sigma.log('Edge "' + id + '" does not exist.'); |
|
} |
|
}); |
|
|
|
return self; |
|
}; |
|
|
|
/** |
|
* Deletes every nodes and edges from the graph. |
|
* @return {Graph} Returns itself. |
|
*/ |
|
function empty() { |
|
self.nodes = []; |
|
self.nodesIndex = {}; |
|
self.edges = []; |
|
self.edgesIndex = {}; |
|
|
|
return self; |
|
}; |
|
|
|
/** |
|
* Computes the display x, y and size of each node, relatively to the |
|
* original values and the borders determined in the parameters, such as |
|
* each node is in the described area. |
|
* @param {number} w The area width (actually the width of the DOM |
|
* root). |
|
* @param {number} h The area height (actually the height of the |
|
* DOM root). |
|
* @param {boolean} parseNodes Indicates if the nodes have to be parsed. |
|
* @param {boolean} parseEdges Indicates if the edges have to be parsed. |
|
* @return {Graph} Returns itself. |
|
*/ |
|
function rescale(w, h, parseNodes, parseEdges) { |
|
var weightMax = 0, sizeMax = 0; |
|
|
|
parseNodes && self.nodes.forEach(function(node) { |
|
sizeMax = Math.max(node['size'], sizeMax); |
|
}); |
|
|
|
parseEdges && self.edges.forEach(function(edge) { |
|
weightMax = Math.max(edge['size'], weightMax); |
|
}); |
|
|
|
sizeMax = sizeMax || 1; |
|
weightMax = weightMax || 1; |
|
|
|
// Recenter the nodes: |
|
var xMin, xMax, yMin, yMax; |
|
parseNodes && self.nodes.forEach(function(node) { |
|
xMax = Math.max(node['x'], xMax || node['x']); |
|
xMin = Math.min(node['x'], xMin || node['x']); |
|
yMax = Math.max(node['y'], yMax || node['y']); |
|
yMin = Math.min(node['y'], yMin || node['y']); |
|
}); |
|
|
|
// First, we compute the scaling ratio, without considering the sizes |
|
// of the nodes : Each node will have its center in the canvas, but might |
|
// be partially out of it. |
|
var scale = self.p.scalingMode == 'outside' ? |
|
Math.max(w / Math.max(xMax - xMin, 1), |
|
h / Math.max(yMax - yMin, 1)) : |
|
Math.min(w / Math.max(xMax - xMin, 1), |
|
h / Math.max(yMax - yMin, 1)); |
|
|
|
// Then, we correct that scaling ratio considering a margin, which is |
|
// basically the size of the biggest node. |
|
// This has to be done as a correction since to compare the size of the |
|
// biggest node to the X and Y values, we have to first get an |
|
// approximation of the scaling ratio. |
|
var margin = (self.p.maxNodeSize || sizeMax) / scale; |
|
xMax += margin; |
|
xMin -= margin; |
|
yMax += margin; |
|
yMin -= margin; |
|
|
|
scale = self.p.scalingMode == 'outside' ? |
|
Math.max(w / Math.max(xMax - xMin, 1), |
|
h / Math.max(yMax - yMin, 1)) : |
|
Math.min(w / Math.max(xMax - xMin, 1), |
|
h / Math.max(yMax - yMin, 1)); |
|
|
|
// Size homothetic parameters: |
|
var a, b; |
|
if (!self.p.maxNodeSize && !self.p.minNodeSize) { |
|
a = 1; |
|
b = 0; |
|
}else if (self.p.maxNodeSize == self.p.minNodeSize) { |
|
a = 0; |
|
b = self.p.maxNodeSize; |
|
}else { |
|
a = (self.p.maxNodeSize - self.p.minNodeSize) / sizeMax; |
|
b = self.p.minNodeSize; |
|
} |
|
|
|
var c, d; |
|
if (!self.p.maxEdgeSize && !self.p.minEdgeSize) { |
|
c = 1; |
|
d = 0; |
|
}else if (self.p.maxEdgeSize == self.p.minEdgeSize) { |
|
c = 0; |
|
d = self.p.minEdgeSize; |
|
}else { |
|
c = (self.p.maxEdgeSize - self.p.minEdgeSize) / weightMax; |
|
d = self.p.minEdgeSize; |
|
} |
|
|
|
// Rescale the nodes: |
|
parseNodes && self.nodes.forEach(function(node) { |
|
node['displaySize'] = node['size'] * a + b; |
|
|
|
if (!node['fixed']) { |
|
node['displayX'] = (node['x'] - (xMax + xMin) / 2) * scale + w / 2; |
|
node['displayY'] = (node['y'] - (yMax + yMin) / 2) * scale + h / 2; |
|
} |
|
}); |
|
|
|
parseEdges && self.edges.forEach(function(edge) { |
|
edge['displaySize'] = edge['size'] * c + d; |
|
}); |
|
|
|
return self; |
|
}; |
|
|
|
/** |
|
* Translates the display values of the nodes and edges relatively to the |
|
* scene position and zoom ratio. |
|
* @param {number} sceneX The x position of the scene. |
|
* @param {number} sceneY The y position of the scene. |
|
* @param {number} ratio The zoom ratio of the scene. |
|
* @param {boolean} parseNodes Indicates if the nodes have to be parsed. |
|
* @param {boolean} parseEdges Indicates if the edges have to be parsed. |
|
* @return {Graph} Returns itself. |
|
*/ |
|
function translate(sceneX, sceneY, ratio, parseNodes, parseEdges) { |
|
var sizeRatio = Math.pow(ratio, self.p.nodesPowRatio); |
|
parseNodes && self.nodes.forEach(function(node) { |
|
if (!node['fixed']) { |
|
node['displayX'] = node['displayX'] * ratio + sceneX; |
|
node['displayY'] = node['displayY'] * ratio + sceneY; |
|
} |
|
|
|
node['displaySize'] = node['displaySize'] * sizeRatio; |
|
}); |
|
|
|
sizeRatio = Math.pow(ratio, self.p.edgesPowRatio); |
|
parseEdges && self.edges.forEach(function(edge) { |
|
edge['displaySize'] = edge['displaySize'] * sizeRatio; |
|
}); |
|
|
|
return self; |
|
}; |
|
|
|
/** |
|
* Determines the borders of the graph as it will be drawn. It is used to |
|
* avoid the user to drag the graph out of the canvas. |
|
*/ |
|
function setBorders() { |
|
self.borders = {}; |
|
|
|
self.nodes.forEach(function(node) { |
|
self.borders.minX = Math.min( |
|
self.borders.minX == undefined ? |
|
node['displayX'] - node['displaySize'] : |
|
self.borders.minX, |
|
node['displayX'] - node['displaySize'] |
|
); |
|
|
|
self.borders.maxX = Math.max( |
|
self.borders.maxX == undefined ? |
|
node['displayX'] + node['displaySize'] : |
|
self.borders.maxX, |
|
node['displayX'] + node['displaySize'] |
|
); |
|
|
|
self.borders.minY = Math.min( |
|
self.borders.minY == undefined ? |
|
node['displayY'] - node['displaySize'] : |
|
self.borders.minY, |
|
node['displayY'] - node['displaySize'] |
|
); |
|
|
|
self.borders.maxY = Math.max( |
|
self.borders.maxY == undefined ? |
|
node['displayY'] - node['displaySize'] : |
|
self.borders.maxY, |
|
node['displayY'] - node['displaySize'] |
|
); |
|
}); |
|
} |
|
|
|
/** |
|
* Checks which nodes are under the (mX, mY) points, representing the mouse |
|
* position. |
|
* @param {number} mX The mouse X position. |
|
* @param {number} mY The mouse Y position. |
|
* @return {Graph} Returns itself. |
|
*/ |
|
function checkHover(mX, mY) { |
|
var dX, dY, s, over = [], out = []; |
|
self.nodes.forEach(function(node) { |
|
if (node['hidden']) { |
|
node['hover'] = false; |
|
return; |
|
} |
|
|
|
dX = Math.abs(node['displayX'] - mX); |
|
dY = Math.abs(node['displayY'] - mY); |
|
s = node['displaySize']; |
|
|
|
var oldH = node['hover']; |
|
var newH = dX < s && dY < s && Math.sqrt(dX * dX + dY * dY) < s; |
|
|
|
if (oldH && !newH) { |
|
node['hover'] = false; |
|
out.push(node.id); |
|
} else if (newH && !oldH) { |
|
node['hover'] = true; |
|
over.push(node.id); |
|
} |
|
}); |
|
|
|
over.length && self.dispatch('overnodes', over); |
|
out.length && self.dispatch('outnodes', out); |
|
|
|
return self; |
|
}; |
|
|
|
/** |
|
* Applies a function to a clone of each node (or indicated nodes), and then |
|
* tries to apply the modifications made on the clones to the original nodes. |
|
* @param {function(Object)} fun The function to execute. |
|
* @param {?Array.<string>} ids An Array of node IDs (optional). |
|
* @return {Graph} Returns itself. |
|
*/ |
|
function iterNodes(fun, ids) { |
|
var a = ids ? ids.map(function(id) { |
|
return self.nodesIndex[id]; |
|
}) : self.nodes; |
|
|
|
var aCopies = a.map(cloneNode); |
|
aCopies.forEach(fun); |
|
|
|
a.forEach(function(n, i) { |
|
checkNode(n, aCopies[i]); |
|
}); |
|
|
|
return self; |
|
}; |
|
|
|
/** |
|
* Applies a function to a clone of each edge (or indicated edges), and then |
|
* tries to apply the modifications made on the clones to the original edges. |
|
* @param {function(Object)} fun The function to execute. |
|
* @param {?Array.<string>} ids An Array of edge IDs (optional). |
|
* @return {Graph} Returns itself. |
|
*/ |
|
function iterEdges(fun, ids) { |
|
var a = ids ? ids.map(function(id) { |
|
return self.edgesIndex[id]; |
|
}) : self.edges; |
|
|
|
var aCopies = a.map(cloneEdge); |
|
aCopies.forEach(fun); |
|
|
|
a.forEach(function(e, i) { |
|
checkEdge(e, aCopies[i]); |
|
}); |
|
|
|
return self; |
|
}; |
|
|
|
/** |
|
* Returns a specific node clone or an array of specified node clones. |
|
* @param {(string|Array.<string>)} ids The ID or an array of node IDs. |
|
* @return {(Object|Array.<Object>)} The clone or the array of clones. |
|
*/ |
|
function getNodes(ids) { |
|
var a = ((ids instanceof Array ? ids : [ids]) || []).map(function(id) { |
|
return cloneNode(self.nodesIndex[id]); |
|
}); |
|
|
|
return (ids instanceof Array ? a : a[0]); |
|
}; |
|
|
|
/** |
|
* Returns a specific edge clone or an array of specified edge clones. |
|
* @param {(string|Array.<string>)} ids The ID or an array of edge IDs. |
|
* @return {(Object|Array.<Object>)} The clone or the array of clones. |
|
*/ |
|
function getEdges(ids) { |
|
var a = ((ids instanceof Array ? ids : [ids]) || []).map(function(id) { |
|
return cloneEdge(self.edgesIndex[id]); |
|
}); |
|
|
|
return (ids instanceof Array ? a : a[0]); |
|
}; |
|
|
|
empty(); |
|
|
|
this.addNode = addNode; |
|
this.addEdge = addEdge; |
|
this.dropNode = dropNode; |
|
this.dropEdge = dropEdge; |
|
|
|
this.iterEdges = iterEdges; |
|
this.iterNodes = iterNodes; |
|
|
|
this.getEdges = getEdges; |
|
this.getNodes = getNodes; |
|
|
|
this.empty = empty; |
|
this.rescale = rescale; |
|
this.translate = translate; |
|
this.setBorders = setBorders; |
|
this.checkHover = checkHover; |
|
} |
|
|
|
sigma.easing = { |
|
linear: {}, |
|
quadratic: {} |
|
}; |
|
|
|
sigma.easing.linear.easenone = function(k) { |
|
return k; |
|
}; |
|
|
|
sigma.easing.quadratic.easein = function(k) { |
|
return k * k; |
|
}; |
|
|
|
sigma.easing.quadratic.easeout = function(k) { |
|
return - k * (k - 2); |
|
}; |
|
|
|
sigma.easing.quadratic.easeinout = function(k) { |
|
if ((k *= 2) < 1) return 0.5 * k * k; |
|
return - 0.5 * (--k * (k - 2) - 1); |
|
}; |
|
|
|
sigma.debugMode = 0; |
|
|
|
sigma.log = function() { |
|
if (sigma.debugMode == 1) { |
|
for (var k in arguments) { |
|
console.log(arguments[k]); |
|
} |
|
}else if (sigma.debugMode > 1) { |
|
for (var k in arguments) { |
|
throw new Error(arguments[k]); |
|
} |
|
} |
|
|
|
return sigma; |
|
}; |
|
|
|
/** |
|
* Add a function to the prototype of SigmaPublic, but with access to the |
|
* Sigma class properties. |
|
* @param {string} pluginName [description]. |
|
* @param {function} caller [description]. |
|
* @param {function(Sigma)} launcher [description]. |
|
*/ |
|
sigma.addPlugin = function(pluginName, caller, launcher) { |
|
SigmaPublic.prototype[pluginName] = caller; |
|
local.plugins.push(launcher); |
|
}; |
|
sigma.tools.drawRoundRect = function(ctx, x, y, w, h, ellipse, corners) { |
|
var e = ellipse ? ellipse : 0; |
|
var c = corners ? corners : []; |
|
c = ((typeof c) == 'string') ? c.split(' ') : c; |
|
|
|
var tl = e && (c.indexOf('topleft') >= 0 || |
|
c.indexOf('top') >= 0 || |
|
c.indexOf('left') >= 0); |
|
var tr = e && (c.indexOf('topright') >= 0 || |
|
c.indexOf('top') >= 0 || |
|
c.indexOf('right') >= 0); |
|
var bl = e && (c.indexOf('bottomleft') >= 0 || |
|
c.indexOf('bottom') >= 0 || |
|
c.indexOf('left') >= 0); |
|
var br = e && (c.indexOf('bottomright') >= 0 || |
|
c.indexOf('bottom') >= 0 || |
|
c.indexOf('right') >= 0); |
|
|
|
ctx.moveTo(x, y + e); |
|
|
|
if (tl) { |
|
ctx.arcTo(x, y, x + e, y, e); |
|
}else { |
|
ctx.lineTo(x, y); |
|
} |
|
|
|
if (tr) { |
|
ctx.lineTo(x + w - e, y); |
|
ctx.arcTo(x + w, y, x + w, y + e, e); |
|
}else { |
|
ctx.lineTo(x + w, y); |
|
} |
|
|
|
if (br) { |
|
ctx.lineTo(x + w, y + h - e); |
|
ctx.arcTo(x + w, y + h, x + w - e, y + h, e); |
|
}else { |
|
ctx.lineTo(x + w, y + h); |
|
} |
|
|
|
if (bl) { |
|
ctx.lineTo(x + e, y + h); |
|
ctx.arcTo(x, y + h, x, y + h - e, e); |
|
}else { |
|
ctx.lineTo(x, y + h); |
|
} |
|
|
|
ctx.lineTo(x, y + e); |
|
}; |
|
|
|
sigma.tools.getRGB = function(s, asArray) { |
|
s = s.toString(); |
|
var res = { |
|
'r': 0, |
|
'g': 0, |
|
'b': 0 |
|
}; |
|
|
|
if (s.length >= 3) { |
|
if (s.charAt(0) == '#') { |
|
var l = s.length - 1; |
|
if (l == 6) { |
|
res = { |
|
'r': parseInt(s.charAt(1) + s.charAt(2), 16), |
|
'g': parseInt(s.charAt(3) + s.charAt(4), 16), |
|
'b': parseInt(s.charAt(5) + s.charAt(5), 16) |
|
}; |
|
}else if (l == 3) { |
|
res = { |
|
'r': parseInt(s.charAt(1) + s.charAt(1), 16), |
|
'g': parseInt(s.charAt(2) + s.charAt(2), 16), |
|
'b': parseInt(s.charAt(3) + s.charAt(3), 16) |
|
}; |
|
} |
|
} |
|
} |
|
|
|
if (asArray) { |
|
res = [ |
|
res['r'], |
|
res['g'], |
|
res['b'] |
|
]; |
|
} |
|
|
|
return res; |
|
}; |
|
|
|
sigma.tools.rgbToHex = function(R, G, B) { |
|
return sigma.tools.toHex(R) + sigma.tools.toHex(G) + sigma.tools.toHex(B); |
|
}; |
|
|
|
sigma.tools.toHex = function(n) { |
|
n = parseInt(n, 10); |
|
|
|
if (isNaN(n)) { |
|
return '00'; |
|
} |
|
n = Math.max(0, Math.min(n, 255)); |
|
return '0123456789ABCDEF'.charAt((n - n % 16) / 16) + |
|
'0123456789ABCDEF'.charAt(n % 16); |
|
}; |
|
|
|
/** |
|
* sigma.chronos manages frames insertion to simulate asynchronous computing. |
|
* It has been designed to make possible to execute heavy computing tasks |
|
* for the browser, without freezing it. |
|
* @constructor |
|
* @extends sigma.classes.Cascade |
|
* @extends sigma.classes.EventDispatcher |
|
* @this {sigma.chronos} |
|
*/ |
|
sigma.chronos = new (function() { |
|
sigma.classes.EventDispatcher.call(this); |
|
|
|
/** |
|
* Represents "this", without the well-known scope issue. |
|
* @private |
|
* @type {sigma.chronos} |
|
*/ |
|
var self = this; |
|
|
|
/** |
|
* Indicates whether any task is actively running or not. |
|
* @private |
|
* @type {boolean} |
|
*/ |
|
var isRunning = false; |
|
|
|
/** |
|
* Indicates the FPS "goal", that will define the theoretical |
|
* frame length. |
|
* @private |
|
* @type {number} |
|
*/ |
|
var fpsReq = 80; |
|
|
|
/** |
|
* Stores the last computed FPS value (FPS is computed only when any |
|
* task is running). |
|
* @private |
|
* @type {number} |
|
*/ |
|
var lastFPS = 0; |
|
|
|
/** |
|
* The number of frames inserted since the last start. |
|
* @private |
|
* @type {number} |
|
*/ |
|
var framesCount = 0; |
|
|
|
/** |
|
* The theoretical frame time. |
|
* @private |
|
* @type {number} |
|
*/ |
|
var frameTime = 1000 / fpsReq; |
|
|
|
/** |
|
* The theoretical frame length, minus the last measured delay. |
|
* @private |
|
* @type {number} |
|
*/ |
|
var correctedFrameTime = frameTime; |
|
|
|
/** |
|
* The measured length of the last frame. |
|
* @private |
|
* @type {number} |
|
*/ |
|
var effectiveTime = 0; |
|
|
|
/** |
|
* The time passed since the last runTasks action. |
|
* @private |
|
* @type {number} |
|
*/ |
|
var currentTime = 0; |
|
|
|
/** |
|
* The time when the last frame was inserted. |
|
* @private |
|
* @type {number} |
|
*/ |
|
var startTime = 0; |
|
|
|
/** |
|
* The difference between the theoretical frame length and the |
|
* last measured frame length. |
|
* @private |
|
* @type {number} |
|
*/ |
|
var delay = 0; |
|
|
|
/** |
|
* The container of all active generators. |
|
* @private |
|
* @type {Object.<string, Object>} |
|
*/ |
|
var generators = {}; |
|
|
|
/** |
|
* The array of all the referenced and active tasks. |
|
* @private |
|
* @type {Array.<Object>} |
|
*/ |
|
var tasks = []; |
|
|
|
/** |
|
* The array of all the referenced and queued tasks. |
|
* @private |
|
* @type {Array.<Object>} |
|
*/ |
|
var queuedTasks = []; |
|
|
|
/** |
|
* The index of the next task to execute. |
|
* @private |
|
* @type {number} |
|
*/ |
|
var taskIndex = 0; |
|
|
|
|
|
/** |
|
* Inserts a frame before executing the callback. |
|
* @param {function()} callback The callback to execute after having |
|
* inserted the frame. |
|
* @return {sigma.chronos} Returns itself. |
|
*/ |
|
function insertFrame(callback) { |
|
window.setTimeout(callback, 0); |
|
return self; |
|
} |
|
|
|
/** |
|
* The local method that executes routine, and inserts frames when needed. |
|
* It dispatches a "frameinserted" event after having inserted any frame, |
|
* and an "insertframe" event before. |
|
* @private |
|
*/ |
|
function frameInserter() { |
|
self.dispatch('frameinserted'); |
|
while (isRunning && tasks.length && routine()) {} |
|
|
|
if (!isRunning || !tasks.length) { |
|
stopTasks(); |
|
} else { |
|
startTime = (new Date()).getTime(); |
|
framesCount++; |
|
delay = effectiveTime - frameTime; |
|
correctedFrameTime = frameTime - delay; |
|
|
|
self.dispatch('insertframe'); |
|
insertFrame(frameInserter); |
|
} |
|
}; |
|
|
|
/** |
|
* The local method that executes the tasks, and compares the current frame |
|
* length to the ideal frame length. |
|
* @private |
|
* @return {boolean} Returns false if the current frame should be ended, |
|
* and true else. |
|
*/ |
|
function routine() { |
|
taskIndex = taskIndex % tasks.length; |
|
|
|
if (!tasks[taskIndex].task()) { |
|
var n = tasks[taskIndex].taskName; |
|
|
|
queuedTasks = queuedTasks.filter(function(e) { |
|
(e.taskParent == n) && tasks.push({ |
|
taskName: e.taskName, |
|
task: e.task |
|
}); |
|
return e.taskParent != n; |
|
}); |
|
|
|
self.dispatch('killed', tasks.splice(taskIndex--, 1)[0]); |
|
} |
|
|
|
taskIndex++; |
|
effectiveTime = (new Date()).getTime() - startTime; |
|
return effectiveTime <= correctedFrameTime; |
|
}; |
|
|
|
/** |
|
* Starts tasks execution. |
|
* @return {sigma.chronos} Returns itself. |
|
*/ |
|
function runTasks() { |
|
isRunning = true; |
|
taskIndex = 0; |
|
framesCount = 0; |
|
|
|
startTime = (new Date()).getTime(); |
|
currentTime = startTime; |
|
|
|
self.dispatch('start'); |
|
self.dispatch('insertframe'); |
|
insertFrame(frameInserter); |
|
return self; |
|
}; |
|
|
|
/** |
|
* Stops tasks execution, and dispatch a "stop" event. |
|
* @return {sigma.chronos} Returns itself. |
|
*/ |
|
function stopTasks() { |
|
self.dispatch('stop'); |
|
isRunning = false; |
|
return self; |
|
}; |
|
|
|
/** |
|
* A task is a function that will be executed continuously while it returns |
|
* true. As soon as it return false, the task will be removed. |
|
* If several tasks are present, they will be executed in parallele. |
|
* This method will add the task to this execution process. |
|
* @param {function(): boolean} task The task to add. |
|
* @param {string} name The name of the worker, used for |
|
* managing the different tasks. |
|
* @param {boolean} autostart If true, sigma.chronos will start |
|
* automatically if it is not working |
|
* yet. |
|
* @return {sigma.chronos} Returns itself. |
|
*/ |
|
function addTask(task, name, autostart) { |
|
if (typeof task != 'function') { |
|
throw new Error('Task "' + name + '" is not a function'); |
|
} |
|
|
|
tasks.push({ |
|
taskName: name, |
|
task: task |
|
}); |
|
|
|
isRunning = !!(isRunning || (autostart && runTasks()) || true); |
|
return self; |
|
}; |
|
|
|
/** |
|
* Will add a task that will be start to be executed as soon as a task |
|
* named as the parent will be removed. |
|
* @param {function(): boolean} task The task to add. |
|
* @param {string} name The name of the worker, used for |
|
* managing the different tasks. |
|
* @param {string} parent The name of the parent task. |
|
* @return {sigma.chronos} Returns itself. |
|
*/ |
|
function queueTask(task, name, parent) { |
|
if (typeof task != 'function') { |
|
throw new Error('Task "' + name + '" is not a function'); |
|
} |
|
|
|
if (!tasks.concat(queuedTasks).some(function(e) { |
|
return e.taskName == parent; |
|
})) { |
|
throw new Error( |
|
'Parent task "' + parent + '" of "' + name + '" is not attached.' |
|
); |
|
} |
|
|
|
queuedTasks.push({ |
|
taskParent: parent, |
|
taskName: name, |
|
task: task |
|
}); |
|
|
|
return self; |
|
}; |
|
|
|
/** |
|
* Removes a task. |
|
* @param {string} v If v is undefined, then every tasks will |
|
* be removed. If not, each task named v will |
|
* be removed. |
|
* @param {number} queueStatus Determines the queued tasks behaviour. If 0, |
|
* then nothing will happen. If 1, the tasks |
|
* queued to any removed task will be triggered. |
|
* If 2, the tasks queued to any removed task |
|
* will be removed as well. |
|
* @return {sigma.chronos} Returns itself. |
|
*/ |
|
function removeTask(v, queueStatus) { |
|
if (v == undefined) { |
|
tasks = []; |
|
if (queueStatus == 1) { |
|
queuedTasks = []; |
|
}else if (queueStatus == 2) { |
|
tasks = queuedTasks; |
|
queuedTasks = []; |
|
} |
|
stopTasks(); |
|
} else { |
|
var n = (typeof v == 'string') ? v : ''; |
|
tasks = tasks.filter(function(e) { |
|
if ((typeof v == 'string') ? e.taskName == v : e.task == v) { |
|
n = e.taskName; |
|
return false; |
|
} |
|
return true; |
|
}); |
|
|
|
if (queueStatus > 0) { |
|
queuedTasks = queuedTasks.filter(function(e) { |
|
if (queueStatus == 1 && e.taskParent == n) { |
|
tasks.push(e); |
|
} |
|
return e.taskParent != n; |
|
}); |
|
} |
|
} |
|
|
|
isRunning = !!(!tasks.length || (stopTasks() && false)); |
|
return self; |
|
}; |
|
|
|
/** |
|
* A generator is a pair task/condition. The task will be executed |
|
* while it returns true. |
|
* When it returns false, the condition will be tested. If |
|
* the condition returns true, the task will be executed |
|
* again at the next process iteration. If not, the generator |
|
* is removed. |
|
* If several generators are present, they will be executed one |
|
* by one: When the first stops, the second will start, etc. When |
|
* they are all ended, then the conditions will be tested to know |
|
* which generators have to be started again. |
|
* @param {string} id The generators ID. |
|
* @param {function(): boolean} task The generator's task. |
|
* @param {function(): boolean} condition The generator's condition. |
|
* @return {sigma.chronos} Returns itself. |
|
*/ |
|
function addGenerator(id, task, condition) { |
|
if (generators[id] != undefined) { |
|
return self; |
|
} |
|
|
|
generators[id] = { |
|
task: task, |
|
condition: condition |
|
}; |
|
|
|
getGeneratorsCount(true) == 0 && startGenerators(); |
|
return self; |
|
}; |
|
|
|
/** |
|
* Removes a generator. It means that the task will continue being eecuted |
|
* until it returns false, but then the |
|
* condition will not be tested. |
|
* @param {string} id The generator's ID. |
|
* @return {sigma.chronos} Returns itself. |
|
*/ |
|
function removeGenerator(id) { |
|
if (generators[id]) { |
|
generators[id].on = false; |
|
generators[id].del = true; |
|
} |
|
return self; |
|
}; |
|
|
|
/** |
|
* Returns the number of generators. |
|
* @private |
|
* @param {boolean} running If true, returns the number of active |
|
* generators instead. |
|
* @return {sigma.chronos} Returns itself. |
|
*/ |
|
function getGeneratorsCount(running) { |
|
return running ? |
|
Object.keys(generators).filter(function(id) { |
|
return !!generators[id].on; |
|
}).length : |
|
Object.keys(generators).length; |
|
}; |
|
|
|
/** |
|
* Returns the array of the generators IDs. |
|
* @return {array.<string>} The array of IDs. |
|
*/ |
|
function getGeneratorsIDs() { |
|
return Object.keys(generators); |
|
} |
|
|
|
/** |
|
* startGenerators is the method that manages which generator |
|
* is the next to start when another one stops. It will dispatch |
|
* a "stopgenerators" event if there is no more generator to start, |
|
* and a "startgenerators" event else. |
|
* @return {sigma.chronos} Returns itself. |
|
*/ |
|
function startGenerators() { |
|
if (!Object.keys(generators).length) { |
|
self.dispatch('stopgenerators'); |
|
}else { |
|
self.dispatch('startgenerators'); |
|
|
|
self.unbind('killed', onTaskEnded); |
|
insertFrame(function() { |
|
for (var k in generators) { |
|
generators[k].on = true; |
|
addTask( |
|
generators[k].task, |
|
k, |
|
false |
|
); |
|
} |
|
}); |
|
|
|
self.bind('killed', onTaskEnded).runTasks(); |
|
} |
|
|
|
return self; |
|
}; |
|
|
|
/** |
|
* A callback triggered everytime the task of a generator stops, that will |
|
* test the related generator's condition, and see if there is still any |
|
* generator to start. |
|
* @private |
|
* @param {Object} e The sigma.chronos "killed" event. |
|
*/ |
|
function onTaskEnded(e) { |
|
if (generators[e['content'].taskName] != undefined) { |
|
if (generators[e['content'].taskName].del || |
|
!generators[e['content'].taskName].condition()) { |
|
delete generators[e['content'].taskName]; |
|
}else { |
|
generators[e['content'].taskName].on = false; |
|
} |
|
|
|
if (getGeneratorsCount(true) == 0) { |
|
startGenerators(); |
|
} |
|
} |
|
}; |
|
|
|
/** |
|
* Either set or returns the fpsReq property. This property determines |
|
* the number of frames that should be inserted per second. |
|
* @param {?number} v The frequency asked. |
|
* @return {(Chronos|number)} Returns the frequency if v is undefined, and |
|
* itself else. |
|
*/ |
|
function frequency(v) { |
|
if (v != undefined) { |
|
fpsReq = Math.abs(1 * v); |
|
frameTime = 1000 / fpsReq; |
|
framesCount = 0; |
|
return self; |
|
} else { |
|
return fpsReq; |
|
} |
|
}; |
|
|
|
/** |
|
* Returns the actual average number of frames that are inserted per |
|
* second. |
|
* @return {number} The actual average FPS. |
|
*/ |
|
function getFPS() { |
|
if (isRunning) { |
|
lastFPS = |
|
Math.round( |
|
framesCount / |
|
((new Date()).getTime() - currentTime) * |
|
10000 |
|
) / 10; |
|
} |
|
|
|
return lastFPS; |
|
}; |
|
|
|
/** |
|
* Returns the number of tasks. |
|
* @return {number} The number of tasks. |
|
*/ |
|
function getTasksCount() { |
|
return tasks.length; |
|
} |
|
|
|
/** |
|
* Returns the number of queued tasks. |
|
* @return {number} The number of queued tasks. |
|
*/ |
|
function getQueuedTasksCount() { |
|
return queuedTasks.length; |
|
} |
|
|
|
/** |
|
* Returns how long sigma.chronos has active tasks running |
|
* without interuption for, in ms. |
|
* @return {number} The time chronos is running without interuption for. |
|
*/ |
|
function getExecutionTime() { |
|
return startTime - currentTime; |
|
} |
|
|
|
this.frequency = frequency; |
|
|
|
this.runTasks = runTasks; |
|
this.stopTasks = stopTasks; |
|
this.insertFrame = insertFrame; |
|
|
|
this.addTask = addTask; |
|
this.queueTask = queueTask; |
|
this.removeTask = removeTask; |
|
|
|
this.addGenerator = addGenerator; |
|
this.removeGenerator = removeGenerator; |
|
this.startGenerators = startGenerators; |
|
this.getGeneratorsIDs = getGeneratorsIDs; |
|
|
|
this.getFPS = getFPS; |
|
this.getTasksCount = getTasksCount; |
|
this.getQueuedTasksCount = getQueuedTasksCount; |
|
this.getExecutionTime = getExecutionTime; |
|
|
|
return this; |
|
})(); |
|
|
|
sigma.publicPrototype = SigmaPublic.prototype; |
|
})(); |
|
|
|
|