Skip to content

Instantly share code, notes, and snippets.

@jessicamcinchak
Forked from bnchdrff/README.md
Created January 30, 2014 00:03
Show Gist options
  • Save jessicamcinchak/8699908 to your computer and use it in GitHub Desktop.
Save jessicamcinchak/8699908 to your computer and use it in GitHub Desktop.

Detroit Ed Ind Cplx Visualizationizerator

Description

By pulling in data from these sheets, I used sigma.js to build a graph of relationships.

How to use

Zoom in using mouse wheel

Todo

Everything

<!DOCTYPE html>
<html>
<head>
<title>DETROIT EDUCATIONAL INDUSTRIAL COMPLEX... the sigma.js edition!</title>
<style>
#sigma-example {
position: absolute;
width: 100%;
height: 100%;
top: 0px;
left: 0px;
}
</style>
<script src="rsvp.js"></script>
<script src="sigma.concat.js"></script>
<script src="sigma.forceatlas2.js"></script>
</head>
<body>
<div id="sigma-example"></div>
<script type="text/javascript">
var create_cb_promise = function(cb_name) {
var promise = new RSVP.Promise(function(resolve, reject) {
window[cb_name] = function(data) {
resolve(data);
};
});
return promise;
};
var promises = {
nodes: create_cb_promise('cb_nodes'),
edges: create_cb_promise('cb_edges')
};
// after spreadsheets are loaded, create the graph
RSVP.hash(promises).then(function(sheets) {
var sigRoot = document.getElementById('sigma-example');
// for ez debug, attach sigInst to global scope!
window.sigInst = sigma.init(sigRoot).drawingProperties({
defaultLabelColor: '#333',
defaultLabelSize: 10,
defaultLabelBGColor: '#ddd',
defaultLabelHoverColor: '#000',
labelThreshold: 6,
defaultEdgeType: 'curve',
edgeLabels: true
}).graphProperties({
minNodeSize: 0.1,
maxNodeSize: 5,
minEdgeSize: 5,
maxEdgeSize: 5,
sideMargin: 50
}).mouseProperties({
maxRatio: 10
});
sheets.nodes.feed.entry.forEach(function(val, idx) {
sigInst.addNode(val.title.$t, {
label: val.content.$t,
//cluster: val.content.$t.match(/tagtype: (\w*)/)[1], // organization,district,person,foundation,etc
color: 'blue',
x: Math.random()*10,
y: Math.random()*10,
size: 0.1,
});
});
sheets.edges.feed.entry.forEach(function(val, idx) {
var origin = val.title.$t;
var destination = val.content.$t.match(/target: ([0-9]*)/)[1];
var label = origin+'->'+destination+': '+val.content.$t.match(/label: (.*)/)[1];
sigInst.addEdge(label, origin, destination);
});
sigInst
.draw()
.startForceAtlas2();
// stupid stupid stupid
window.setTimeout(function() {
sigInst.stopForceAtlas2();
}, 10000);
});
</script>
<!-- nodes -->
<script src="https://spreadsheets.google.com/feeds/list/0Aty9maMoYSDFdFd6aVpFNFZucDk5R3NkajJ6a3JvOXc/1/public/basic/?alt=json-in-script&callback=cb_nodes"></script>
<!-- edges -->
<script src="https://spreadsheets.google.com/feeds/list/0Aty9maMoYSDFdFd6aVpFNFZucDk5R3NkajJ6a3JvOXc/2/public/basic/?alt=json-in-script&callback=cb_edges"></script>
</body>
</html>
(function(global) {
var define, requireModule, require, requirejs;
(function() {
var registry = {}, seen = {};
define = function(name, deps, callback) {
registry[name] = { deps: deps, callback: callback };
};
requirejs = require = requireModule = function(name) {
requirejs._eak_seen = registry;
if (seen[name]) { return seen[name]; }
seen[name] = {};
if (!registry[name]) {
throw new Error("Could not find module " + name);
}
var mod = registry[name],
deps = mod.deps,
callback = mod.callback,
reified = [],
exports;
for (var i=0, l=deps.length; i<l; i++) {
if (deps[i] === 'exports') {
reified.push(exports = {});
} else {
reified.push(requireModule(resolve(deps[i])));
}
}
var value = callback.apply(this, reified);
return seen[name] = exports || value;
function resolve(child) {
if (child.charAt(0) !== '.') { return child; }
var parts = child.split("/");
var parentBase = name.split("/").slice(0, -1);
for (var i=0, l=parts.length; i<l; i++) {
var part = parts[i];
if (part === '..') { parentBase.pop(); }
else if (part === '.') { continue; }
else { parentBase.push(part); }
}
return parentBase.join("/");
}
};
})();
define("rsvp/all",
["./promise","exports"],
function(__dependency1__, __exports__) {
"use strict";
var Promise = __dependency1__["default"];
__exports__["default"] = function all(array, label) {
return Promise.all(array, label);
};
});
define("rsvp/all_settled",
["./promise","./utils","exports"],
function(__dependency1__, __dependency2__, __exports__) {
"use strict";
var Promise = __dependency1__["default"];
var isArray = __dependency2__.isArray;
var isNonThenable = __dependency2__.isNonThenable;
/**
`RSVP.allSettled` is similar to `RSVP.all`, but instead of implementing
a fail-fast method, it waits until all the promises have returned and
shows you all the results. This is useful if you want to handle multiple
promises' failure states together as a set.
Returns a promise that is fulfilled when all the given promises have been
settled. The return promise is fulfilled with an array of the states of
the promises passed into the `promises` array argument.
Each state object will either indicate fulfillment or rejection, and
provide the corresponding value or reason. The states will take one of
the following formats:
```javascript
{ state: 'fulfilled', value: value }
or
{ state: 'rejected', reason: reason }
```
Example:
```javascript
var promise1 = RSVP.Promise.resolve(1);
var promise2 = RSVP.Promise.reject(new Error('2'));
var promise3 = RSVP.Promise.reject(new Error('3'));
var promises = [ promise1, promise2, promise3 ];
RSVP.allSettled(promises).then(function(array){
// array == [
// { state: 'fulfilled', value: 1 },
// { state: 'rejected', reason: Error },
// { state: 'rejected', reason: Error }
// ]
// Note that for the second item, reason.message will be "2", and for the
// third item, reason.message will be "3".
}, function(error) {
// Not run. (This block would only be called if allSettled had failed,
// for instance if passed an incorrect argument type.)
});
```
@method @allSettled
@for RSVP
@param {Array} promises;
@param {String} label - optional string that describes the promise.
Useful for tooling.
@return {Promise} promise that is fulfilled with an array of the settled
states of the constituent promises.
*/
__exports__["default"] = function allSettled(entries, label) {
return new Promise(function(resolve, reject) {
if (!isArray(entries)) {
throw new TypeError('You must pass an array to allSettled.');
}
var remaining = entries.length;
var entry;
if (remaining === 0) {
resolve([]);
return;
}
var results = new Array(remaining);
function fulfilledResolver(index) {
return function(value) {
resolveAll(index, fulfilled(value));
};
}
function rejectedResolver(index) {
return function(reason) {
resolveAll(index, rejected(reason));
};
}
function resolveAll(index, value) {
results[index] = value;
if (--remaining === 0) {
resolve(results);
}
}
for (var index = 0; index < entries.length; index++) {
entry = entries[index];
if (isNonThenable(entry)) {
resolveAll(index, fulfilled(entry));
} else {
Promise.cast(entry).then(fulfilledResolver(index), rejectedResolver(index));
}
}
}, label);
};
function fulfilled(value) {
return { state: 'fulfilled', value: value };
}
function rejected(reason) {
return { state: 'rejected', reason: reason };
}
});
define("rsvp/asap",
["exports"],
function(__exports__) {
"use strict";
__exports__["default"] = function asap(callback, arg) {
var length = queue.push([callback, arg]);
if (length === 1) {
// If length is 1, that means that we need to schedule an async flush.
// If additional callbacks are queued before the queue is flushed, they
// will be processed by this flush that we are scheduling.
scheduleFlush();
}
};
var browserGlobal = (typeof window !== 'undefined') ? window : {};
var BrowserMutationObserver = browserGlobal.MutationObserver || browserGlobal.WebKitMutationObserver;
// node
function useNextTick() {
return function() {
process.nextTick(flush);
};
}
function useMutationObserver() {
var iterations = 0;
var observer = new BrowserMutationObserver(flush);
var node = document.createTextNode('');
observer.observe(node, { characterData: true });
return function() {
node.data = (iterations = ++iterations % 2);
};
}
function useSetTimeout() {
return function() {
setTimeout(flush, 1);
};
}
var queue = [];
function flush() {
for (var i = 0; i < queue.length; i++) {
var tuple = queue[i];
var callback = tuple[0], arg = tuple[1];
callback(arg);
}
queue = [];
}
var scheduleFlush;
// Decide what async method to use to triggering processing of queued callbacks:
if (typeof process !== 'undefined' && {}.toString.call(process) === '[object process]') {
scheduleFlush = useNextTick();
} else if (BrowserMutationObserver) {
scheduleFlush = useMutationObserver();
} else {
scheduleFlush = useSetTimeout();
}
});
define("rsvp/config",
["./events","exports"],
function(__dependency1__, __exports__) {
"use strict";
var EventTarget = __dependency1__["default"];
var config = {
instrument: false
};
EventTarget.mixin(config);
function configure(name, value) {
if (name === 'onerror') {
// handle for legacy users that expect the actual
// error to be passed to their function added via
// `RSVP.configure('onerror', someFunctionHere);`
config.on('error', value);
return;
}
if (arguments.length === 2) {
config[name] = value;
} else {
return config[name];
}
}
__exports__.config = config;
__exports__.configure = configure;
});
define("rsvp/defer",
["./promise","exports"],
function(__dependency1__, __exports__) {
"use strict";
var Promise = __dependency1__["default"];
/**
`RSVP.defer` returns an object similar to jQuery's `$.Deferred` objects.
`RSVP.defer` should be used when porting over code reliant on `$.Deferred`'s
interface. New code should use the `RSVP.Promise` constructor instead.
The object returned from `RSVP.defer` is a plain object with three properties:
* promise - an `RSVP.Promise`.
* reject - a function that causes the `promise` property on this object to
become rejected
* resolve - a function that causes the `promise` property on this object to
become fulfilled.
Example:
```javascript
var deferred = RSVP.defer();
deferred.resolve("Success!");
defered.promise.then(function(value){
// value here is "Success!"
});
```
@method defer
@for RSVP
@param {String} label optional string for labeling the promise.
Useful for tooling.
@return {Object}
*/
__exports__["default"] = function defer(label) {
var deferred = { };
deferred.promise = new Promise(function(resolve, reject) {
deferred.resolve = resolve;
deferred.reject = reject;
}, label);
return deferred;
};
});
define("rsvp/events",
["exports"],
function(__exports__) {
"use strict";
var indexOf = function(callbacks, callback) {
for (var i=0, l=callbacks.length; i<l; i++) {
if (callbacks[i] === callback) { return i; }
}
return -1;
};
var callbacksFor = function(object) {
var callbacks = object._promiseCallbacks;
if (!callbacks) {
callbacks = object._promiseCallbacks = {};
}
return callbacks;
};
/**
//@module RSVP
//@class EventTarget
*/
__exports__["default"] = {
/**
`RSVP.EventTarget.mixin` extends an object with EventTarget methods. For
Example:
```javascript
var object = {};
RSVP.EventTarget.mixin(object);
object.on("finished", function(event) {
// handle event
});
object.trigger("finished", { detail: value });
```
`EventTarget.mixin` also works with prototypes:
```javascript
var Person = function() {};
RSVP.EventTarget.mixin(Person.prototype);
var yehuda = new Person();
var tom = new Person();
yehuda.on("poke", function(event) {
console.log("Yehuda says OW");
});
tom.on("poke", function(event) {
console.log("Tom says OW");
});
yehuda.trigger("poke");
tom.trigger("poke");
```
@method mixin
@param {Object} object object to extend with EventTarget methods
@private
*/
mixin: function(object) {
object.on = this.on;
object.off = this.off;
object.trigger = this.trigger;
object._promiseCallbacks = undefined;
return object;
},
/**
Registers a callback to be executed when `eventName` is triggered
```javascript
object.on('event', function(eventInfo){
// handle the event
});
object.trigger('event');
```
@method on
@param {String} eventName name of the event to listen for
@param {Function} callback function to be called when the event is triggered.
@private
*/
on: function(eventName, callback) {
var allCallbacks = callbacksFor(this), callbacks;
callbacks = allCallbacks[eventName];
if (!callbacks) {
callbacks = allCallbacks[eventName] = [];
}
if (indexOf(callbacks, callback) === -1) {
callbacks.push(callback);
}
},
/**
You can use `off` to stop firing a particular callback for an event:
```javascript
function doStuff() { // do stuff! }
object.on('stuff', doStuff);
object.trigger('stuff'); // doStuff will be called
// Unregister ONLY the doStuff callback
object.off('stuff', doStuff);
object.trigger('stuff'); // doStuff will NOT be called
```
If you don't pass a `callback` argument to `off`, ALL callbacks for the
event will not be executed when the event fires. For example:
```javascript
var callback1 = function(){};
var callback2 = function(){};
object.on('stuff', callback1);
object.on('stuff', callback2);
object.trigger('stuff'); // callback1 and callback2 will be executed.
object.off('stuff');
object.trigger('stuff'); // callback1 and callback2 will not be executed!
```
@method off
@param {String} eventName event to stop listening to
@param {Function} callback optional argument. If given, only the function
given will be removed from the event's callback queue. If no `callback`
argument is given, all callbacks will be removed from the event's callback
queue.
@private
*/
off: function(eventName, callback) {
var allCallbacks = callbacksFor(this), callbacks, index;
if (!callback) {
allCallbacks[eventName] = [];
return;
}
callbacks = allCallbacks[eventName];
index = indexOf(callbacks, callback);
if (index !== -1) { callbacks.splice(index, 1); }
},
/**
Use `trigger` to fire custom events. For example:
```javascript
object.on('foo', function(){
console.log('foo event happened!');
});
object.trigger('foo');
// 'foo event happened!' logged to the console
```
You can also pass a value as a second argument to `trigger` that will be
passed as an argument to all event listeners for the event:
```javascript
object.on('foo', function(value){
console.log(value.name);
});
object.trigger('foo', { name: 'bar' });
// 'bar' logged to the console
```
@method trigger
@param {String} eventName name of the event to be triggered
@param {Any} options optional value to be passed to any event handlers for
the given `eventName`
@private
*/
trigger: function(eventName, options) {
var allCallbacks = callbacksFor(this),
callbacks, callbackTuple, callback, binding;
if (callbacks = allCallbacks[eventName]) {
// Don't cache the callbacks.length since it may grow
for (var i=0; i<callbacks.length; i++) {
callback = callbacks[i];
callback(options);
}
}
}
};
});
define("rsvp/filter",
["./all","./map","./utils","exports"],
function(__dependency1__, __dependency2__, __dependency3__, __exports__) {
"use strict";
var all = __dependency1__["default"];
var map = __dependency2__["default"];
var isFunction = __dependency3__.isFunction;
var isArray = __dependency3__.isArray;
/**
`RSVP.filter` is similar to JavaScript's native `filter` method, except that it
waits for all promises to become fulfilled before running the `filterFn` on
each item in given to `promises`. `RSVP.filterFn` returns a promise that will
become fulfilled with the result of running `filterFn` on the values the
promises become fulfilled with.
For example:
```javascript
var promise1 = RSVP.resolve(1);
var promise2 = RSVP.resolve(2);
var promise3 = RSVP.resolve(3);
var filterFn = function(item){
return item > 1;
};
RSVP.filter(promises, filterFn).then(function(result){
// result is [ 2, 3 ]
});
```
If any of the `promises` given to `RSVP.filter` are rejected, the first promise
that is rejected will be given as an argument to the returned promises's
rejection handler. For example:
```javascript
var promise1 = RSVP.resolve(1);
var promise2 = RSVP.reject(new Error("2"));
var promise3 = RSVP.reject(new Error("3"));
var promises = [ promise1, promise2, promise3 ];
var filterFn = function(item){
return item > 1;
};
RSVP.filter(promises, filterFn).then(function(array){
// Code here never runs because there are rejected promises!
}, function(reason) {
// reason.message === "2"
});
```
`RSVP.filter` will also wait for any promises returned from `filterFn`.
For instance, you may want to fetch a list of users then return a subset
of those users based on some asynchronous operation:
```javascript
var alice = { name: 'alice' };
var bob = { name: 'bob' };
var users = [ alice, bob ];
var promises = users.map(function(user){
return RSVP.resolve(user);
});
var filterFn = function(user){
// Here, Alice has permissions to create a blog post, but Bob does not.
return getPrivilegesForUser(user).then(function(privs){
return privs.can_create_blog_post === true;
});
};
RSVP.filter(promises, filterFn).then(function(users){
// true, because the server told us only Alice can create a blog post.
users.length === 1;
// false, because Alice is the only user present in `users`
users[0] === bob;
});
```
@method filter
@for RSVP
@param {Array} promises
@param {Function} filterFn - function to be called on each resolved value to
filter the final results.
@param {String} label optional string describing the promise. Useful for
tooling.
@return {Promise}
*/
function filter(promises, filterFn, label) {
if (!isArray(promises)) {
throw new TypeError('You must pass an array to filter.');
}
if (!isFunction(filterFn)){
throw new TypeError("You must pass a function to filter's second argument.");
}
return all(promises, label).then(function(values){
return map(promises, filterFn, label).then(function(filterResults){
var i,
valuesLen = values.length,
filtered = [];
for (i = 0; i < valuesLen; i++){
if(filterResults[i]) filtered.push(values[i]);
}
return filtered;
});
});
}
__exports__["default"] = filter;
});
define("rsvp/hash",
["./promise","./utils","exports"],
function(__dependency1__, __dependency2__, __exports__) {
"use strict";
var Promise = __dependency1__["default"];
var isNonThenable = __dependency2__.isNonThenable;
var keysOf = __dependency2__.keysOf;
/**
`RSVP.hash` is similar to `RSVP.all`, but takes an object instead of an array
for its `promises` argument.
Returns a promise that is fulfilled when all the given promises have been
fulfilled, or rejected if any of them become rejected. The returned promise
is fulfilled with a hash that has the same key names as the `promises` object
argument. If any of the values in the object are not promises, they will
simply be copied over to the fulfilled object.
Example:
```javascript
var promises = {
myPromise: RSVP.resolve(1),
yourPromise: RSVP.resolve(2),
theirPromise: RSVP.resolve(3),
notAPromise: 4
};
RSVP.hash(promises).then(function(hash){
// hash here is an object that looks like:
// {
// myPromise: 1,
// yourPromise: 2,
// theirPromise: 3,
// notAPromise: 4
// }
});
````
If any of the `promises` given to `RSVP.hash` are rejected, the first promise
that is rejected will be given as as the first argument, or as the reason to
the rejection handler. For example:
```javascript
var promises = {
myPromise: RSVP.resolve(1),
rejectedPromise: RSVP.reject(new Error("rejectedPromise")),
anotherRejectedPromise: RSVP.reject(new Error("anotherRejectedPromise")),
};
RSVP.hash(promises).then(function(hash){
// Code here never runs because there are rejected promises!
}, function(reason) {
// reason.message === "rejectedPromise"
});
```
An important note: `RSVP.hash` is intended for plain JavaScript objects that
are just a set of keys and values. `RSVP.hash` will NOT preserve prototype
chains.
Example:
```javascript
function MyConstructor(){
this.example = RSVP.resolve("Example");
}
MyConstructor.prototype = {
protoProperty: RSVP.resolve("Proto Property")
};
var myObject = new MyConstructor();
RSVP.hash(myObject).then(function(hash){
// protoProperty will not be present, instead you will just have an
// object that looks like:
// {
// example: "Example"
// }
//
// hash.hasOwnProperty('protoProperty'); // false
// 'undefined' === typeof hash.protoProperty
});
```
@method hash
@for RSVP
@param {Object} promises
@param {String} label - optional string that describes the promise.
Useful for tooling.
@return {Promise} promise that is fulfilled when all properties of `promises`
have been fulfilled, or rejected if any of them become rejected.
*/
__exports__["default"] = function hash(object, label) {
return new Promise(function(resolve, reject){
var results = {};
var keys = keysOf(object);
var remaining = keys.length;
var entry, property;
if (remaining === 0) {
resolve(results);
return;
}
function fulfilledTo(property) {
return function(value) {
results[property] = value;
if (--remaining === 0) {
resolve(results);
}
};
}
function onRejection(reason) {
remaining = 0;
reject(reason);
}
for (var i = 0; i < keys.length; i++) {
property = keys[i];
entry = object[property];
if (isNonThenable(entry)) {
results[property] = entry;
if (--remaining === 0) {
resolve(results);
}
} else {
Promise.cast(entry).then(fulfilledTo(property), onRejection);
}
}
});
};
});
define("rsvp/instrument",
["./config","./utils","exports"],
function(__dependency1__, __dependency2__, __exports__) {
"use strict";
var config = __dependency1__.config;
var now = __dependency2__.now;
__exports__["default"] = function instrument(eventName, promise, child) {
// instrumentation should not disrupt normal usage.
try {
config.trigger(eventName, {
guid: promise._guidKey + promise._id,
eventName: eventName,
detail: promise._detail,
childGuid: child && promise._guidKey + child._id,
label: promise._label,
timeStamp: now(),
stack: new Error(promise._label).stack
});
} catch(error) {
setTimeout(function(){
throw error;
}, 0);
}
};
});
define("rsvp/map",
["./promise","./all","./utils","exports"],
function(__dependency1__, __dependency2__, __dependency3__, __exports__) {
"use strict";
var Promise = __dependency1__["default"];
var all = __dependency2__["default"];
var isArray = __dependency3__.isArray;
var isFunction = __dependency3__.isFunction;
/**
`RSVP.map` is similar to JavaScript's native `map` method, except that it
waits for all promises to become fulfilled before running the `mapFn` on
each item in given to `promises`. `RSVP.map` returns a promise that will
become fulfilled with the result of running `mapFn` on the values the promises
become fulfilled with.
For example:
```javascript
var promise1 = RSVP.resolve(1);
var promise2 = RSVP.resolve(2);
var promise3 = RSVP.resolve(3);
var promises = [ promise1, promise2, promise3 ];
var mapFn = function(item){
return item + 1;
};
RSVP.map(promises, mapFn).then(function(result){
// result is [ 2, 3, 4 ]
});
```
If any of the `promises` given to `RSVP.map` are rejected, the first promise
that is rejected will be given as an argument to the returned promises's
rejection handler. For example:
```javascript
var promise1 = RSVP.resolve(1);
var promise2 = RSVP.reject(new Error("2"));
var promise3 = RSVP.reject(new Error("3"));
var promises = [ promise1, promise2, promise3 ];
var mapFn = function(item){
return item + 1;
};
RSVP.map(promises, mapFn).then(function(array){
// Code here never runs because there are rejected promises!
}, function(reason) {
// reason.message === "2"
});
```
`RSVP.map` will also wait if a promise is returned from `mapFn`. For example,
say you want to get all comments from a set of blog posts, but you need
the blog posts first becuase they contain a url to those comments.
```javscript
var mapFn = function(blogPost){
// getComments does some ajax and returns an RSVP.Promise that is fulfilled
// with some comments data
return getComments(blogPost.comments_url);
};
// getBlogPosts does some ajax and returns an RSVP.Promise that is fulfilled
// with some blog post data
RSVP.map(getBlogPosts(), mapFn).then(function(comments){
// comments is the result of asking the server for the comments
// of all blog posts returned from getBlogPosts()
});
```
@method map
@for RSVP
@param {Array} promises
@param {Function} mapFn function to be called on each fulfilled promise.
@param {String} label optional string for labeling the promise.
Useful for tooling.
@return {Promise} promise that is fulfilled with the result of calling
`mapFn` on each fulfilled promise or value when they become fulfilled.
The promise will be rejected if any of the given `promises` become rejected.
*/
__exports__["default"] = function map(promises, mapFn, label) {
if (!isArray(promises)) {
throw new TypeError('You must pass an array to map.');
}
if (!isFunction(mapFn)){
throw new TypeError("You must pass a function to map's second argument.");
}
return all(promises, label).then(function(results){
var resultLen = results.length,
mappedResults = [],
i;
for (i = 0; i < resultLen; i++){
mappedResults.push(mapFn(results[i]));
}
return all(mappedResults, label);
});
};
});
define("rsvp/node",
["./promise","exports"],
function(__dependency1__, __exports__) {
"use strict";
var Promise = __dependency1__["default"];
var slice = Array.prototype.slice;
function makeNodeCallbackFor(resolve, reject) {
return function (error, value) {
if (error) {
reject(error);
} else if (arguments.length > 2) {
resolve(slice.call(arguments, 1));
} else {
resolve(value);
}
};
}
/**
`RSVP.denodeify` takes a "node-style" function and returns a function that
will return an `RSVP.Promise`. You can use `denodeify` in Node.js or the
browser when you'd prefer to use promises over using callbacks. For example,
`denodeify` transforms the following:
```javascript
var fs = require('fs');
fs.readFile('myfile.txt', function(err, data){
if (err) return handleError(err);
handleData(data);
});
```
into:
```javascript
var fs = require('fs');
var readFile = RSVP.denodeify(fs.readFile);
readFile('myfile.txt').then(handleData, handleError);
```
Using `denodeify` makes it easier to compose asynchronous operations instead
of using callbacks. For example, instead of:
```javascript
var fs = require('fs');
var log = require('some-async-logger');
fs.readFile('myfile.txt', function(err, data){
if (err) return handleError(err);
fs.writeFile('myfile2.txt', data, function(err){
if (err) throw err;
log('success', function(err) {
if (err) throw err;
});
});
});
```
You can chain the operations together using `then` from the returned promise:
```javascript
var fs = require('fs');
var denodeify = RSVP.denodeify;
var readFile = denodeify(fs.readFile);
var writeFile = denodeify(fs.writeFile);
var log = denodeify(require('some-async-logger'));
readFile('myfile.txt').then(function(data){
return writeFile('myfile2.txt', data);
}).then(function(){
return log('SUCCESS');
}).then(function(){
// success handler
}, function(reason){
// rejection handler
});
```
@method denodeify
@for RSVP
@param {Function} nodeFunc a "node-style" function that takes a callback as
its last argument. The callback expects an error to be passed as its first
argument (if an error occurred, otherwise null), and the value from the
operation as its second argument ("function(err, value){ }").
@param {Any} binding optional argument for binding the "this" value when
calling the `nodeFunc` function.
@return {Function} a function that wraps `nodeFunc` to return an
`RSVP.Promise`
*/
__exports__["default"] = function denodeify(nodeFunc, binding) {
return function() {
var nodeArgs = slice.call(arguments), resolve, reject;
var thisArg = this || binding;
return new Promise(function(resolve, reject) {
Promise.all(nodeArgs).then(function(nodeArgs) {
try {
nodeArgs.push(makeNodeCallbackFor(resolve, reject));
nodeFunc.apply(thisArg, nodeArgs);
} catch(e) {
reject(e);
}
});
});
};
};
});
define("rsvp/promise",
["./config","./events","./instrument","./utils","./promise/cast","./promise/all","./promise/race","./promise/resolve","./promise/reject","exports"],
function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __exports__) {
"use strict";
var config = __dependency1__.config;
var EventTarget = __dependency2__["default"];
var instrument = __dependency3__["default"];
var objectOrFunction = __dependency4__.objectOrFunction;
var isFunction = __dependency4__.isFunction;
var now = __dependency4__.now;
var cast = __dependency5__["default"];
var all = __dependency6__["default"];
var race = __dependency7__["default"];
var Resolve = __dependency8__["default"];
var Reject = __dependency9__["default"];
var guidKey = 'rsvp_' + now() + '-';
var counter = 0;
function noop() {}
__exports__["default"] = Promise;
/**
Promise objects represent the eventual result of an asynchronous operation. The
primary way of interacting with a promise is through its `then` method, which
registers callbacks to receive either a promise’s eventual value or the reason
why the promise cannot be fulfilled.
Terminology
-----------
- `promise` is an object or function with a `then` method whose behavior conforms to this specification.
- `thenable` is an object or function that defines a `then` method.
- `value` is any legal JavaScript value (including undefined, a thenable, or a promise).
- `exception` is a value that is thrown using the throw statement.
- `reason` is a value that indicates why a promise was rejected.
- `settled` the final resting state of a promise, fulfilled or rejected.
A promise can be in one of three states: pending, fulfilled, or rejected.
Basic Usage:
------------
```js
var promise = new Promise(function(resolve, reject) {
// on success
resolve(value);
// on failure
reject(reason);
});
promise.then(function(value) {
// on fulfillment
}, function(reason) {
// on rejection
});
```
Advanced Usage:
---------------
Promises shine when abstracting away asynchronous interactions such as
`XMLHttpRequest`s.
```js
function getJSON(url) {
return new Promise(function(resolve, reject){
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.onreadystatechange = handler;
xhr.responseType = 'json';
xhr.setRequestHeader('Accept', 'application/json');
xhr.send();
function handler() {
if (this.readyState === this.DONE) {
if (this.status === 200) {
resolve(this.response);
} else {
reject(new Error("getJSON: `" + url + "` failed with status: [" + this.status + "]");
}
}
};
});
}
getJSON('/posts.json').then(function(json) {
// on fulfillment
}, function(reason) {
// on rejection
});
```
Unlike callbacks, promises are great composable primitives.
```js
Promise.all([
getJSON('/posts'),
getJSON('/comments')
]).then(function(values){
values[0] // => postsJSON
values[1] // => commentsJSON
return values;
});
```
@class Promise
@param {function}
@param {String} label optional string for labeling the promise.
Useful for tooling.
@constructor
*/
function Promise(resolver, label) {
if (!isFunction(resolver)) {
throw new TypeError('You must pass a resolver function as the first argument to the promise constructor');
}
if (!(this instanceof Promise)) {
throw new TypeError("Failed to construct 'Promise': Please use the 'new' operator, this object constructor cannot be called as a function.");
}
this._id = counter++;
this._label = label;
this._subscribers = [];
if (config.instrument) {
instrument('created', this);
}
if (noop !== resolver) {
invokeResolver(resolver, this);
}
}
function invokeResolver(resolver, promise) {
function resolvePromise(value) {
resolve(promise, value);
}
function rejectPromise(reason) {
reject(promise, reason);
}
try {
resolver(resolvePromise, rejectPromise);
} catch(e) {
rejectPromise(e);
}
}
Promise.cast = cast;
Promise.all = all;
Promise.race = race;
Promise.resolve = Resolve;
Promise.reject = Reject;
var PENDING = void 0;
var SEALED = 0;
var FULFILLED = 1;
var REJECTED = 2;
function subscribe(parent, child, onFulfillment, onRejection) {
var subscribers = parent._subscribers;
var length = subscribers.length;
subscribers[length] = child;
subscribers[length + FULFILLED] = onFulfillment;
subscribers[length + REJECTED] = onRejection;
}
function publish(promise, settled) {
var child, callback, subscribers = promise._subscribers, detail = promise._detail;
if (config.instrument) {
instrument(settled === FULFILLED ? 'fulfilled' : 'rejected', promise);
}
for (var i = 0; i < subscribers.length; i += 3) {
child = subscribers[i];
callback = subscribers[i + settled];
invokeCallback(settled, child, callback, detail);
}
promise._subscribers = null;
}
Promise.prototype = {
/**
@property constructor
*/
constructor: Promise,
_id: undefined,
_guidKey: guidKey,
_label: undefined,
_state: undefined,
_detail: undefined,
_subscribers: undefined,
_onerror: function (reason) {
config.trigger('error', reason);
},
/**
A promise represents the eventual result of an asynchronous operation. The
primary way of interacting with a promise is through its `then` method, which
registers callbacks to receive either a promise's eventual value or the reason
why the promise cannot be fulfilled.
```js
findUser().then(function(user){
// user is available
}, function(reason){
// user is unavailable, and you are given the reason why
});
```
Chaining
--------
The return value of `then` is itself a promise. This second, "downstream"
promise is resolved with the return value of the first promise's fulfillment
or rejection handler, or rejected if the handler throws an exception.
```js
findUser().then(function (user) {
return user.name;
}, function (reason) {
return "default name";
}).then(function (userName) {
// If `findUser` fulfilled, `userName` will be the user's name, otherwise it
// will be `"default name"`
});
findUser().then(function (user) {
throw "Found user, but still unhappy";
}, function (reason) {
throw "`findUser` rejected and we're unhappy";
}).then(function (value) {
// never reached
}, function (reason) {
// if `findUser` fulfilled, `reason` will be "Found user, but still unhappy".
// If `findUser` rejected, `reason` will be "`findUser` rejected and we're unhappy".
});
```
If the downstream promise does not specify a rejection handler, rejection reasons will be propagated further downstream.
```js
findUser().then(function (user) {
throw new PedagogicalException("Upstream error");
}).then(function (value) {
// never reached
}).then(function (value) {
// never reached
}, function (reason) {
// The `PedgagocialException` is propagated all the way down to here
});
```
Assimilation
------------
Sometimes the value you want to propagate to a downstream promise can only be
retrieved asynchronously. This can be achieved by returning a promise in the
fulfillment or rejection handler. The downstream promise will then be pending
until the returned promise is settled. This is called *assimilation*.
```js
findUser().then(function (user) {
return findCommentsByAuthor(user);
}).then(function (comments) {
// The user's comments are now available
});
```
If the assimliated promise rejects, then the downstream promise will also reject.
```js
findUser().then(function (user) {
return findCommentsByAuthor(user);
}).then(function (comments) {
// If `findCommentsByAuthor` fulfills, we'll have the value here
}, function (reason) {
// If `findCommentsByAuthor` rejects, we'll have the reason here
});
```
Simple Example
--------------
Synchronous Example
```javascript
var result;
try {
result = findResult();
// success
} catch(reason) {
// failure
}
```
Errback Example
```js
findResult(function(result, err){
if (err) {
// failure
} else {
// success
}
});
```
Promise Example;
```javacsript
findResult().then(function(result){
}, function(reason){
});
```
Advanced Example
--------------
Synchronous Example
```javascript
var author, books;
try {
author = findAuthor();
books = findBooksByAuthor(author);
// success
} catch(reason) {
// failure
}
```
Errback Example
```js
function foundBooks(books) {
}
function failure(reason) {
}
findAuthor(function(author, err){
if (err) {
failure(err);
// failure
} else {
try {
findBoooksByAuthor(author, function(books, err) {
if (err) {
failure(err);
} else {
try {
foundBooks(books);
} catch(reason) {
failure(reason);
}
}
});
} catch(error) {
failure(err);
}
// success
}
});
```
Promise Example;
```javacsript
findAuthor().
then(findBooksByAuthor).
then(function(books){
// found books
}).catch(function(reason){
// something went wrong;
});
```
@method then
@param {Function} onFulfillment
@param {Function} onRejection
@param {String} label optional string for labeling the promise.
Useful for tooling.
@return {Promise}
*/
then: function(onFulfillment, onRejection, label) {
var promise = this;
this._onerror = null;
var thenPromise = new this.constructor(noop, label);
if (this._state) {
var callbacks = arguments;
config.async(function invokePromiseCallback() {
invokeCallback(promise._state, thenPromise, callbacks[promise._state - 1], promise._detail);
});
} else {
subscribe(this, thenPromise, onFulfillment, onRejection);
}
if (config.instrument) {
instrument('chained', promise, thenPromise);
}
return thenPromise;
},
/**
`catch` is simply sugar for `then(null, onRejection)` which makes it the same
as the catch block, of a try/catch statement.
```js
function findAuthor(){
throw new Error("couldn't find that author");
}
// synchronous
try {
findAuthor();
} catch(reason) {
}
// async with promises
findAuthor().catch(function(reason){
// something went wrong;
});
```
@method catch
@param {Function} onRejection
@param {String} label optional string for labeling the promise.
Useful for tooling.
@return {Promise}
*/
'catch': function(onRejection, label) {
return this.then(null, onRejection, label);
},
/**
`finally` will be invoked regardless of the promise's fate just as native
try/catch/finally behaves
```js
findAuthor() {
if (Math.random() > 0.5) {
throw new Error();
}
return new Author();
}
try {
return findAuthor(); // succeed or fail
} catch(error) {
return findOtherAuther();
} finally {
// always runs
// doesn't effect the return value
}
findAuthor().finally(function(){
// author was either found, or not
});
```
@method finally
@param {Function} callback
@param {String} label optional string for labeling the promise.
Useful for tooling.
@return {Promise}
*/
'finally': function(callback, label) {
var constructor = this.constructor;
return this.then(function(value) {
return constructor.cast(callback()).then(function(){
return value;
});
}, function(reason) {
return constructor.cast(callback()).then(function(){
throw reason;
});
}, label);
}
};
function invokeCallback(settled, promise, callback, detail) {
var hasCallback = isFunction(callback),
value, error, succeeded, failed;
if (hasCallback) {
try {
value = callback(detail);
succeeded = true;
} catch(e) {
failed = true;
error = e;
}
} else {
value = detail;
succeeded = true;
}
if (handleThenable(promise, value)) {
return;
} else if (hasCallback && succeeded) {
resolve(promise, value);
} else if (failed) {
reject(promise, error);
} else if (settled === FULFILLED) {
resolve(promise, value);
} else if (settled === REJECTED) {
reject(promise, value);
}
}
function handleThenable(promise, value) {
var then = null,
resolved;
try {
if (promise === value) {
throw new TypeError("A promises callback cannot return that same promise.");
}
if (objectOrFunction(value)) {
then = value.then;
if (isFunction(then)) {
then.call(value, function(val) {
if (resolved) { return true; }
resolved = true;
if (value !== val) {
resolve(promise, val);
} else {
fulfill(promise, val);
}
}, function(val) {
if (resolved) { return true; }
resolved = true;
reject(promise, val);
}, 'derived from: ' + (promise._label || ' unknown promise'));
return true;
}
}
} catch (error) {
if (resolved) { return true; }
reject(promise, error);
return true;
}
return false;
}
function resolve(promise, value) {
if (promise === value) {
fulfill(promise, value);
} else if (!handleThenable(promise, value)) {
fulfill(promise, value);
}
}
function fulfill(promise, value) {
if (promise._state !== PENDING) { return; }
promise._state = SEALED;
promise._detail = value;
config.async(publishFulfillment, promise);
}
function reject(promise, reason) {
if (promise._state !== PENDING) { return; }
promise._state = SEALED;
promise._detail = reason;
config.async(publishRejection, promise);
}
function publishFulfillment(promise) {
publish(promise, promise._state = FULFILLED);
}
function publishRejection(promise) {
if (promise._onerror) {
promise._onerror(promise._detail);
}
publish(promise, promise._state = REJECTED);
}
});
define("rsvp/promise/all",
["../utils","exports"],
function(__dependency1__, __exports__) {
"use strict";
var isArray = __dependency1__.isArray;
var isNonThenable = __dependency1__.isNonThenable;
/**
`RSVP.Promise.all` returns a new promise which is fulfilled with an array of
fulfillment values for the passed promises, or rejects with the reason of the
first passed promise that rejects. It casts all elements of the passed iterable
to promises as it runs this algorithm.
Example:
```javascript
var promise1 = RSVP.resolve(1);
var promise2 = RSVP.resolve(2);
var promise3 = RSVP.resolve(3);
var promises = [ promise1, promise2, promise3 ];
RSVP.Promise.all(promises).then(function(array){
// The array here would be [ 1, 2, 3 ];
});
```
If any of the `promises` given to `RSVP.all` are rejected, the first promise
that is rejected will be given as an argument to the returned promises's
rejection handler. For example:
Example:
```javascript
var promise1 = RSVP.resolve(1);
var promise2 = RSVP.reject(new Error("2"));
var promise3 = RSVP.reject(new Error("3"));
var promises = [ promise1, promise2, promise3 ];
RSVP.Promise.all(promises).then(function(array){
// Code here never runs because there are rejected promises!
}, function(error) {
// error.message === "2"
});
```
@method all
@for RSVP.Promise
@param {Array} promises
@param {String} label optional string for labeling the promise.
Useful for tooling.
@return {Promise} promise that is fulfilled when all `promises` have been
fulfilled, or rejected if any of them become rejected.
*/
__exports__["default"] = function all(entries, label) {
/*jshint validthis:true */
var Constructor = this;
return new Constructor(function(resolve, reject) {
if (!isArray(entries)) {
throw new TypeError('You must pass an array to all.');
}
var remaining = entries.length;
var results = new Array(remaining);
var entry, pending = true;
if (remaining === 0) {
resolve(results);
return;
}
function fulfillmentAt(index) {
return function(value) {
results[index] = value;
if (--remaining === 0) {
resolve(results);
}
};
}
function onRejection(reason) {
remaining = 0;
reject(reason);
}
for (var index = 0; index < entries.length; index++) {
entry = entries[index];
if (isNonThenable(entry)) {
results[index] = entry;
if (--remaining === 0) {
resolve(results);
}
} else {
Constructor.cast(entry).then(fulfillmentAt(index), onRejection);
}
}
}, label);
};
});
define("rsvp/promise/cast",
["exports"],
function(__exports__) {
"use strict";
/**
`RSVP.Promise.cast` cast coerces its argument to a promise, or returns the
argument if it is already a promise which shares a constructor with the caster;
Example:
```javascript
var promise = RSVP.Promise.resolve(1);
var casted = RSVP.Promise.cast(promise);
console.log(promise === casted); // true
```
In the case of a promise whose constructor does not match, it is assimilated.
The resulting promise will fulfill or reject based on the outcome of the
promise being casted.
In the case of a non-promise, a promise which will fulfill with that value is
returned.
Example:
```javascript
var value = 1; // could be a number, boolean, string, undefined...
var casted = RSVP.Promise.cast(value);
console.log(value === casted); // false
console.log(casted instanceof RSVP.Promise) // true
casted.then(function(val) {
val === value // => true
});
```
`RSVP.Promise.cast` is similar to `RSVP.Promise.resolve`, but `RSVP.Promise.cast` differs in the
following ways:
* `RSVP.Promise.cast` serves as a memory-efficient way of getting a promise, when you
have something that could either be a promise or a value. RSVP.resolve
will have the same effect but will create a new promise wrapper if the
argument is a promise.
* `RSVP.Promise.cast` is a way of casting incoming thenables or promise subclasses to
promises of the exact class specified, so that the resulting object's `then` is
ensured to have the behavior of the constructor you are calling cast on (i.e., RSVP.Promise).
@method cast
@for RSVP.Promise
@param {Object} object to be casted
@param {String} label optional string for labeling the promise.
Useful for tooling.
@return {Promise} promise
*/
__exports__["default"] = function cast(object, label) {
/*jshint validthis:true */
var Constructor = this;
if (object && typeof object === 'object' && object.constructor === Constructor) {
return object;
}
return new Constructor(function(resolve) {
resolve(object);
}, label);
};
});
define("rsvp/promise/race",
["../utils","exports"],
function(__dependency1__, __exports__) {
"use strict";
/* global toString */
var isArray = __dependency1__.isArray;
var isFunction = __dependency1__.isFunction;
var isNonThenable = __dependency1__.isNonThenable;
/**
`RSVP.Promise.race` returns a new promise which is settled in the same way as the
first passed promise to settle.
Example:
```javascript
var promise1 = new RSVP.Promise(function(resolve, reject){
setTimeout(function(){
resolve("promise 1");
}, 200);
});
var promise2 = new RSVP.Promise(function(resolve, reject){
setTimeout(function(){
resolve("promise 2");
}, 100);
});
RSVP.Promise.race([promise1, promise2]).then(function(result){
// result === "promise 2" because it was resolved before promise1
// was resolved.
});
```
`RSVP.Promise.race` is deterministic in that only the state of the first
completed promise matters. For example, even if other promises given to the
`promises` array argument are resolved, but the first completed promise has
become rejected before the other promises became fulfilled, the returned
promise will become rejected:
```javascript
var promise1 = new RSVP.Promise(function(resolve, reject){
setTimeout(function(){
resolve("promise 1");
}, 200);
});
var promise2 = new RSVP.Promise(function(resolve, reject){
setTimeout(function(){
reject(new Error("promise 2"));
}, 100);
});
RSVP.Promise.race([promise1, promise2]).then(function(result){
// Code here never runs because there are rejected promises!
}, function(reason){
// reason.message === "promise2" because promise 2 became rejected before
// promise 1 became fulfilled
});
```
@method race
@for RSVP.Promise
@param {Array} promises array of promises to observe
@param {String} label optional string for describing the promise returned.
Useful for tooling.
@return {Promise} a promise which settles in the same way as the first passed
promise to settle.
*/
__exports__["default"] = function race(entries, label) {
/*jshint validthis:true */
var Constructor = this, entry;
return new Constructor(function(resolve, reject) {
if (!isArray(entries)) {
throw new TypeError('You must pass an array to race.');
}
var pending = true;
function onFulfillment(value) { if (pending) { pending = false; resolve(value); } }
function onRejection(reason) { if (pending) { pending = false; reject(reason); } }
for (var i = 0; i < entries.length; i++) {
entry = entries[i];
if (isNonThenable(entry)) {
pending = false;
resolve(entry);
return;
} else {
Constructor.cast(entry).then(onFulfillment, onRejection);
}
}
}, label);
};
});
define("rsvp/promise/reject",
["exports"],
function(__exports__) {
"use strict";
/**
`RSVP.Promise.reject` returns a promise rejected with the passed `reason`.
It is essentially shorthand for the following:
```javascript
var promise = new RSVP.Promise(function(resolve, reject){
reject(new Error('WHOOPS'));
});
promise.then(function(value){
// Code here doesn't run because the promise is rejected!
}, function(reason){
// reason.message === 'WHOOPS'
});
```
Instead of writing the above, your code now simply becomes the following:
```javascript
var promise = RSVP.Promise.reject(new Error('WHOOPS'));
promise.then(function(value){
// Code here doesn't run because the promise is rejected!
}, function(reason){
// reason.message === 'WHOOPS'
});
```
@method reject
@for RSVP.Promise
@param {Any} reason value that the returned promise will be rejected with.
@param {String} label optional string for identifying the returned promise.
Useful for tooling.
@return {Promise} a promise rejected with the given `reason`.
*/
__exports__["default"] = function reject(reason, label) {
/*jshint validthis:true */
var Constructor = this;
return new Constructor(function (resolve, reject) {
reject(reason);
}, label);
};
});
define("rsvp/promise/resolve",
["exports"],
function(__exports__) {
"use strict";
/**
`RSVP.Promise.resolve` returns a promise that will become fulfilled with the passed
`value`. It is essentially shorthand for the following:
```javascript
var promise = new RSVP.Promise(function(resolve, reject){
resolve(1);
});
promise.then(function(value){
// value === 1
});
```
Instead of writing the above, your code now simply becomes the following:
```javascript
var promise = RSVP.Promise.resolve(1);
promise.then(function(value){
// value === 1
});
```
@method resolve
@for RSVP.Promise
@param {Any} value value that the returned promise will be resolved with
@param {String} label optional string for identifying the returned promise.
Useful for tooling.
@return {Promise} a promise that will become fulfilled with the given
`value`
*/
__exports__["default"] = function resolve(value, label) {
/*jshint validthis:true */
var Constructor = this;
return new Constructor(function(resolve, reject) {
resolve(value);
}, label);
};
});
define("rsvp/race",
["./promise","exports"],
function(__dependency1__, __exports__) {
"use strict";
var Promise = __dependency1__["default"];
__exports__["default"] = function race(array, label) {
return Promise.race(array, label);
};
});
define("rsvp/reject",
["./promise","exports"],
function(__dependency1__, __exports__) {
"use strict";
var Promise = __dependency1__["default"];
__exports__["default"] = function reject(reason, label) {
return Promise.reject(reason, label);
};
});
define("rsvp/resolve",
["./promise","exports"],
function(__dependency1__, __exports__) {
"use strict";
var Promise = __dependency1__["default"];
__exports__["default"] = function resolve(value, label) {
return Promise.resolve(value, label);
};
});
define("rsvp/rethrow",
["exports"],
function(__exports__) {
"use strict";
/**
`RSVP.rethrow` will rethrow an error on the next turn of the JavaScript event
loop in order to aid debugging.
Promises A+ specifies that any exceptions that occur with a promise must be
caught by the promises implementation and bubbled to the last handler. For
this reason, it is recommended that you always specify a second rejection
handler function to `then`. However, `RSVP.rethrow` will throw the exception
outside of the promise, so it bubbles up to your console if in the browser,
or domain/cause uncaught exception in Node. `rethrow` will throw the error
again so the error can be handled by the promise.
```javascript
function throws(){
throw new Error('Whoops!');
}
var promise = new RSVP.Promise(function(resolve, reject){
throws();
});
promise.catch(RSVP.rethrow).then(function(){
// Code here doesn't run because the promise became rejected due to an
// error!
}, function (err){
// handle the error here
});
```
The 'Whoops' error will be thrown on the next turn of the event loop
and you can watch for it in your console. You can also handle it using a
rejection handler given to `.then` or `.catch` on the returned promise.
@method rethrow
@for RSVP
@param {Error} reason reason the promise became rejected.
@throws Error
*/
__exports__["default"] = function rethrow(reason) {
setTimeout(function() {
throw reason;
});
throw reason;
};
});
define("rsvp/utils",
["exports"],
function(__exports__) {
"use strict";
function objectOrFunction(x) {
return typeof x === "function" || (typeof x === "object" && x !== null);
}
__exports__.objectOrFunction = objectOrFunction;function isFunction(x) {
return typeof x === "function";
}
__exports__.isFunction = isFunction;function isNonThenable(x) {
return !objectOrFunction(x);
}
__exports__.isNonThenable = isNonThenable;function isArray(x) {
return Object.prototype.toString.call(x) === "[object Array]";
}
__exports__.isArray = isArray;// Date.now is not available in browsers < IE9
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/now#Compatibility
var now = Date.now || function() { return new Date().getTime(); };
__exports__.now = now;
var keysOf = Object.keys || function(object) {
var result = [];
for (var prop in object) {
result.push(prop);
}
return result;
};
__exports__.keysOf = keysOf;
});
define("rsvp",
["./rsvp/promise","./rsvp/events","./rsvp/node","./rsvp/all","./rsvp/all_settled","./rsvp/race","./rsvp/hash","./rsvp/rethrow","./rsvp/defer","./rsvp/config","./rsvp/map","./rsvp/resolve","./rsvp/reject","./rsvp/asap","./rsvp/filter","exports"],
function(__dependency1__, __dependency2__, __dependency3__, __dependency4__, __dependency5__, __dependency6__, __dependency7__, __dependency8__, __dependency9__, __dependency10__, __dependency11__, __dependency12__, __dependency13__, __dependency14__, __dependency15__, __exports__) {
"use strict";
var Promise = __dependency1__["default"];
var EventTarget = __dependency2__["default"];
var denodeify = __dependency3__["default"];
var all = __dependency4__["default"];
var allSettled = __dependency5__["default"];
var race = __dependency6__["default"];
var hash = __dependency7__["default"];
var rethrow = __dependency8__["default"];
var defer = __dependency9__["default"];
var config = __dependency10__.config;
var configure = __dependency10__.configure;
var map = __dependency11__["default"];
var resolve = __dependency12__["default"];
var reject = __dependency13__["default"];
var asap = __dependency14__["default"];
var filter = __dependency15__["default"];
config.async = asap; // default async is asap;
function async(callback, arg) {
config.async(callback, arg);
}
function on() {
config.on.apply(config, arguments);
}
function off() {
config.off.apply(config, arguments);
}
// Set up instrumentation through `window.__PROMISE_INTRUMENTATION__`
if (typeof window !== 'undefined' && typeof window.__PROMISE_INSTRUMENTATION__ === 'object') {
var callbacks = window.__PROMISE_INSTRUMENTATION__;
configure('instrument', true);
for (var eventName in callbacks) {
if (callbacks.hasOwnProperty(eventName)) {
on(eventName, callbacks[eventName]);
}
}
}
__exports__.Promise = Promise;
__exports__.EventTarget = EventTarget;
__exports__.all = all;
__exports__.allSettled = allSettled;
__exports__.race = race;
__exports__.hash = hash;
__exports__.rethrow = rethrow;
__exports__.defer = defer;
__exports__.denodeify = denodeify;
__exports__.configure = configure;
__exports__.on = on;
__exports__.off = off;
__exports__.resolve = resolve;
__exports__.reject = reject;
__exports__.async = async;
__exports__.map = map;
__exports__.filter = filter;
});
global.RSVP = requireModule('rsvp');
}(window));
<?xml version="1.0" encoding="UTF-8"?>
<gexf xmlns="http://www.gexf.net/1.2draft" version="1.2" xmlns:viz="http://www.gexf.net/1.2draft/viz" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.gexf.net/1.2draft http://www.gexf.net/1.2draft/gexf.xsd">
<meta lastmodifieddate="2014-01-22">
<creator>Gephi 0.8.1</creator>
<description></description>
</meta>
<graph defaultedgetype="directed" mode="static">
<attributes class="node" mode="static">
<attribute id="Id " title="Id " type="string"></attribute>
<attribute id="Label " title="Label " type="string"></attribute>
<attribute id="Tag_Type" title="Tag_Type" type="string"></attribute>
<attribute id="Tag_Title" title="Tag_Title" type="string"></attribute>
<attribute id="Tag_Salary" title="Tag_Salary" type="string"></attribute>
</attributes>
<attributes class="edge" mode="static">
<attribute id="Tags" title="Tags" type="string"></attribute>
</attributes>
<nodes>
<node id="1">
<attvalues>
<attvalue for="Id " value="1"></attvalue>
<attvalue for="Label " value="Education Trust"></attvalue>
<attvalue for="Tag_Type" value="Organization"></attvalue>
<attvalue for="Tag_Title" value=""></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="199.98834" y="464.33978" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="2">
<attvalues>
<attvalue for="Id " value="2"></attvalue>
<attvalue for="Label " value="Amber Arellano"></attvalue>
<attvalue for="Tag_Type" value="Person"></attvalue>
<attvalue for="Tag_Title" value="Executive Director"></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="158.87555" y="65.031494" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="3">
<attvalues>
<attvalue for="Id " value="3"></attvalue>
<attvalue for="Label " value="Bill and Melinda Gates Foundation"></attvalue>
<attvalue for="Tag_Type" value="Foundation"></attvalue>
<attvalue for="Tag_Title" value=""></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="480.2505" y="29.168762" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="4">
<attvalues>
<attvalue for="Id " value="4"></attvalue>
<attvalue for="Label " value="Kresge Foundation"></attvalue>
<attvalue for="Tag_Type" value="Foundation"></attvalue>
<attvalue for="Tag_Title" value=""></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="363.2213" y="503.75934" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="5">
<attvalues>
<attvalue for="Id " value="5"></attvalue>
<attvalue for="Label " value="Walton Family Foundation"></attvalue>
<attvalue for="Tag_Type" value="Foundation"></attvalue>
<attvalue for="Tag_Title" value=""></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="-336.96863" y="-177.06299" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="6">
<attvalues>
<attvalue for="Id " value="6"></attvalue>
<attvalue for="Label " value="Ford Foundation"></attvalue>
<attvalue for="Tag_Type" value="Foundation"></attvalue>
<attvalue for="Tag_Title" value=""></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="-274.31992" y="-57.44461" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="7">
<attvalues>
<attvalue for="Id " value="7"></attvalue>
<attvalue for="Label " value="Skillman Foundation"></attvalue>
<attvalue for="Tag_Type" value="Foundation"></attvalue>
<attvalue for="Tag_Title" value=""></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="423.4713" y="-434.81006" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="8">
<attvalues>
<attvalue for="Id " value="8"></attvalue>
<attvalue for="Label " value="Metlife"></attvalue>
<attvalue for="Tag_Type" value="Organization"></attvalue>
<attvalue for="Tag_Title" value=""></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="95.53662" y="-258.02087" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="9">
<attvalues>
<attvalue for="Id " value="9"></attvalue>
<attvalue for="Label " value="W.K. Kellogg Foundation"></attvalue>
<attvalue for="Tag_Type" value="Foundation"></attvalue>
<attvalue for="Tag_Title" value=""></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="-266.5833" y="-226.53937" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="10">
<attvalues>
<attvalue for="Id " value="10"></attvalue>
<attvalue for="Label " value="Broad Foundation"></attvalue>
<attvalue for="Tag_Type" value="Foundation"></attvalue>
<attvalue for="Tag_Title" value=""></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="-42.05014" y="-337.87976" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="11">
<attvalues>
<attvalue for="Id " value="11"></attvalue>
<attvalue for="Label " value="Teach for America - Detroit"></attvalue>
<attvalue for="Tag_Type" value="Organization"></attvalue>
<attvalue for="Tag_Title" value=""></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="-221.044" y="-65.97397" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="12">
<attvalues>
<attvalue for="Id " value="12"></attvalue>
<attvalue for="Label " value="Michigan Education Excellence Foundation (MEEF)"></attvalue>
<attvalue for="Tag_Type" value="Foundation"></attvalue>
<attvalue for="Tag_Title" value=""></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="-64.40994" y="-336.94763" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="13">
<attvalues>
<attvalue for="Id " value="13"></attvalue>
<attvalue for="Label " value="John Covington"></attvalue>
<attvalue for="Tag_Type" value="Person"></attvalue>
<attvalue for="Tag_Title" value="EAA Chancellor"></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="-392.03616" y="144.3164" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="14">
<attvalues>
<attvalue for="Id " value="14"></attvalue>
<attvalue for="Label " value="Robert Bobb"></attvalue>
<attvalue for="Tag_Type" value="Person"></attvalue>
<attvalue for="Tag_Title" value="Former DPS EM"></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="248.02142" y="97.18347" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="15">
<attvalues>
<attvalue for="Id " value="15"></attvalue>
<attvalue for="Label " value="Education Achievement Authority (EAA)"></attvalue>
<attvalue for="Tag_Type" value="District"></attvalue>
<attvalue for="Tag_Title" value=""></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="181.14551" y="84.30939" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="16">
<attvalues>
<attvalue for="Id " value="16"></attvalue>
<attvalue for="Label " value="Mike Duggan"></attvalue>
<attvalue for="Tag_Type" value="Person"></attvalue>
<attvalue for="Tag_Title" value="Mayor"></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="-85.86075" y="141.55542" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="17">
<attvalues>
<attvalue for="Id " value="17"></attvalue>
<attvalue for="Label " value="Mark Murray"></attvalue>
<attvalue for="Tag_Type" value="Person"></attvalue>
<attvalue for="Tag_Title" value="U-PREP Connect"></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="-112.23007" y="382.24652" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="18">
<attvalues>
<attvalue for="Id " value="18"></attvalue>
<attvalue for="Label " value="Shirley Stancato"></attvalue>
<attvalue for="Tag_Type" value="Person"></attvalue>
<attvalue for="Tag_Title" value="New Detroit, Inc"></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="-176.20966" y="-395.7215" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="19">
<attvalues>
<attvalue for="Id " value="19"></attvalue>
<attvalue for="Label " value="Roy Roberts"></attvalue>
<attvalue for="Tag_Type" value="Person"></attvalue>
<attvalue for="Tag_Title" value="Former DPS EM"></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="131.224" y="54.2677" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="20">
<attvalues>
<attvalue for="Id " value="20"></attvalue>
<attvalue for="Label " value="Carol Goss"></attvalue>
<attvalue for="Tag_Type" value="Person"></attvalue>
<attvalue for="Tag_Title" value="Skillman"></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="-198.22314" y="431.66962" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="21">
<attvalues>
<attvalue for="Id " value="21"></attvalue>
<attvalue for="Label " value="Sharlonda Buckman"></attvalue>
<attvalue for="Tag_Type" value="Person"></attvalue>
<attvalue for="Tag_Title" value="Detroit Parent Network"></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="289.71643" y="-281.93195" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="22">
<attvalues>
<attvalue for="Id " value="22"></attvalue>
<attvalue for="Label " value="Detroit Parent Network"></attvalue>
<attvalue for="Tag_Type" value="Organization"></attvalue>
<attvalue for="Tag_Title" value=""></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="440.7627" y="341.29535" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="23">
<attvalues>
<attvalue for="Id " value="23"></attvalue>
<attvalue for="Label " value="Detroit Public Schools"></attvalue>
<attvalue for="Tag_Type" value="District"></attvalue>
<attvalue for="Tag_Title" value=""></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="213.90533" y="-292.56143" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="24">
<attvalues>
<attvalue for="Id " value="24"></attvalue>
<attvalue for="Label " value="GM Foundation"></attvalue>
<attvalue for="Tag_Type" value="Foundation"></attvalue>
<attvalue for="Tag_Title" value=""></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="-210.82748" y="11.004547" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="25">
<attvalues>
<attvalue for="Id " value="25"></attvalue>
<attvalue for="Label " value="Michigan Education Choice Center (MECC)"></attvalue>
<attvalue for="Tag_Type" value="Organization"></attvalue>
<attvalue for="Tag_Title" value=""></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="435.91797" y="47.55951" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="26">
<attvalues>
<attvalue for="Id " value="26"></attvalue>
<attvalue for="Label " value="U-Prep Connect"></attvalue>
<attvalue for="Tag_Type" value="Organization"></attvalue>
<attvalue for="Tag_Title" value=""></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="-54.005463" y="165.16718" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="27">
<attvalues>
<attvalue for="Id " value="27"></attvalue>
<attvalue for="Label " value="New Detroit, Inc"></attvalue>
<attvalue for="Tag_Type" value="Organization"></attvalue>
<attvalue for="Tag_Title" value=""></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="433.17706" y="490.1742" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
<node id="28">
<attvalues>
<attvalue for="Id " value="28"></attvalue>
<attvalue for="Label " value="City of Detroit"></attvalue>
<attvalue for="Tag_Type" value="Municipality"></attvalue>
<attvalue for="Tag_Title" value=""></attvalue>
<attvalue for="Tag_Salary" value=""></attvalue>
</attvalues>
<viz:size value="10.0"></viz:size>
<viz:position x="236.46692" y="-283.281" z="0.0"></viz:position>
<viz:color r="153" g="153" b="153"></viz:color>
</node>
</nodes>
<edges>
<edge source="1" target="2" label="Executive Director">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="3" target="1" label="Granted $41M+ since 2009">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="3" target="15" label="Granted $300K in 2013">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="4" target="1" label="Granted $1M+ since 2011">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="5" target="1" label="Granted $950K+ since 2010">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="6" target="1" label="Granted $100K in 2011">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="7" target="1" label="Granted $700K+ since 2011">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="7" target="15" label="Granted $500K+ since 2011">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="7" target="20" label="Employee">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="8" target="1" label="Granted $250K in 2011">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="9" target="1" label="Granted $1.9M+ since 2010">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="9" target="15" label="Granted $5M+ since 2012">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="10" target="11" label="Granted $1M+ in 2010">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="10" target="12" label="Granted $10M+ in 2013">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="12" target="15" label="Granted $9.5M in 2013">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="13" target="10" label="Graduated Broad Urban Schools Superintendent Academy">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="14" target="10" label="Fellow of 2005 Broad Urban Schools Superintendent Academy">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="15" target="11" label="Hired 123 teachers in 2012-2013">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="15" target="13" label="Chancellor">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="15" target="16" label="Board Member">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="15" target="17" label="Board Member">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="15" target="18" label="Board Member">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="15" target="19" label="Board Member">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="15" target="20" label="Board Member">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="15" target="21" label="Board Member (resigned)">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="22" target="21" label="Employee">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="23" target="14" label="Former EM">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="23" target="19" label="Former EM">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="24" target="15" label="Granted $500K in 2013">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="25" target="15" label="Granted $312K+ in 2013">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="26" target="17" label="Employee">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="27" target="18" label="Employee">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
<edge source="28" target="16" label="Mayor">
<attvalues>
<attvalue for="Tags" value=""></attvalue>
</attvalues>
</edge>
</edges>
</graph>
</gexf>
// Define packages:
var sigma = {};
sigma.tools = {};
sigma.classes = {};
sigma.instances = {};
// Adding Array helpers, if not present yet:
(function() {
if (!Array.prototype.some) {
Array.prototype.some = function(fun /*, thisp*/) {
var len = this.length;
if (typeof fun != 'function') {
throw new TypeError();
}
var thisp = arguments[1];
for (var i = 0; i < len; i++) {
if (i in this &&
fun.call(thisp, this[i], i, this)) {
return true;
}
}
return false;
};
}
if (!Array.prototype.forEach) {
Array.prototype.forEach = function(fun /*, thisp*/) {
var len = this.length;
if (typeof fun != 'function') {
throw new TypeError();
}
var thisp = arguments[1];
for (var i = 0; i < len; i++) {
if (i in this) {
fun.call(thisp, this[i], i, this);
}
}
};
}
if (!Array.prototype.map) {
Array.prototype.map = function(fun /*, thisp*/) {
var len = this.length;
if (typeof fun != 'function') {
throw new TypeError();
}
var res = new Array(len);
var thisp = arguments[1];
for (var i = 0; i < len; i++) {
if (i in this) {
res[i] = fun.call(thisp, this[i], i, this);
}
}
return res;
};
}
if (!Array.prototype.filter) {
Array.prototype.filter = function(fun /*, thisp*/) {
var len = this.length;
if (typeof fun != 'function')
throw new TypeError();
var res = new Array();
var thisp = arguments[1];
for (var i = 0; i < len; i++) {
if (i in this) {
var val = this[i]; // in case fun mutates this
if (fun.call(thisp, val, i, this)) {
res.push(val);
}
}
}
return res;
};
}
if (!Object.keys) {
Object.keys = (function() {
var hasOwnProperty = Object.prototype.hasOwnProperty,
hasDontEnumBug = !({toString: null}).propertyIsEnumerable('toString'),
dontEnums = [
'toString',
'toLocaleString',
'valueOf',
'hasOwnProperty',
'isPrototypeOf',
'propertyIsEnumerable',
'constructor'
],
dontEnumsLength = dontEnums.length;
return function(obj) {
if (typeof obj !== 'object' &&
typeof obj !== 'function' ||
obj === null
) {
throw new TypeError('Object.keys called on non-object');
}
var result = [];
for (var prop in obj) {
if (hasOwnProperty.call(obj, prop)) result.push(prop);
}
if (hasDontEnumBug) {
for (var i = 0; i < dontEnumsLength; i++) {
if (hasOwnProperty.call(obj, dontEnums[i])) {
result.push(dontEnums[i]);
}
}
}
return result;
}
})();
}
})();
/**
* 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;
};
/**
* 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;
}
};
};
(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];
};
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.addEdge(
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);
});
}
/**
* 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;
}
/**
* 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,
sideMargin: 0,
// Increasing / decreasing the following parameter will respectively make
// arrows bigger / smaller relatively to their edges.
arrowRatio: 3
};
/**
* 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]) || [];
var nodesIdsToRemove = {};
// Create hash to make lookups faster
a.forEach(function(id) {
if (self.nodesIndex[id]) {
nodesIdsToRemove[id] = true;
} else {
sigma.log('Node "' + id + '" does not exist.');
}
});
var indexesToRemove = [];
self.nodes.forEach(function(n, i) {
if (n['id'] in nodesIdsToRemove) {
// Add to front, so we have a reverse-sorted list
indexesToRemove.unshift(i);
// No edges means we are done
if (n['degree'] == 0) {
delete nodesIdsToRemove[n['id']];
}
}
});
indexesToRemove.forEach(function(index) {
self.nodes.splice(index, 1);
});
self.edges = self.edges.filter(function(e) {
if (e['source']['id'] in nodesIdsToRemove) {
delete self.edgesIndex[e['id']];
e['target']['degree']--;
e['target']['inDegree']--;
return false;
}else if (e['target']['id'] in nodesIdsToRemove) {
delete self.edgesIndex[e['id']];
e['source']['degree']--;
e['source']['outDegree']--;
return false;
}
return true;
});
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':
case 'arrow':
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'],
'arrow': edge['arrow'],
'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 'arrow':
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 + self.p.sideMargin;
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;
});
parseEdges && self.edges.forEach(function(edge) {
edge['displaySize'] =
edge['displaySize'] *
Math.pow(ratio, self.p.edgesPowRatio);
edge['arrowDisplaySize'] =
edge['displaySize'] *
self.p.arrowRatio *
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 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',
defaultEdgeArrow: 'none',
edgeLabels: false,
// ------
// 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) {
// Using array for coordinates so we can easily modify/return from
// applyArrow():
var sourceCoordinates = [
edge['source']['displayX'],
edge['source']['displayY']
],
targetCoordinates = [
edge['target']['displayX'],
edge['target']['displayY']
],
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;
var controlPointX =
(sourceCoordinates[0] + targetCoordinates[0]) / 2 +
(targetCoordinates[1] - sourceCoordinates[1]) / 4,
controlPointY =
(sourceCoordinates[1] + targetCoordinates[1]) / 2 +
(sourceCoordinates[0] - targetCoordinates[0]) / 4;
// Assignment is redundant here but makes it clear that this call
// mutates sourceCoordinates:
if (isArrowDrawRequired('source', edge['arrow'])) {
sourceCoordinates = applyArrow(
ctx,
sourceCoordinates,
edge['source']['displaySize'],
controlPointX,
controlPointY,
edge['arrowDisplaySize']
);
}
// Assignment is redundant here but makes it clear that this call
// mutates targetCoordinates:
if (isArrowDrawRequired('target', edge['arrow'])) {
targetCoordinates = applyArrow(
ctx,
targetCoordinates,
edge['target']['displaySize'],
controlPointX,
controlPointY,
edge['arrowDisplaySize']
);
}
ctx.lineWidth = edge['displaySize'] / 3;
ctx.beginPath();
ctx.moveTo(sourceCoordinates[0], sourceCoordinates[1]);
ctx.quadraticCurveTo(controlPointX,
controlPointY,
targetCoordinates[0],
targetCoordinates[1]);
ctx.stroke();
break;
case 'line':
default:
ctx.strokeStyle = color;
// Assignment is redundant here but makes it clear that this call
// mutates sourceCoordinates:
if (isArrowDrawRequired('source', edge['arrow'])) {
sourceCoordinates = applyArrow(ctx,
sourceCoordinates,
edge['source']['displaySize'],
targetCoordinates[0],
targetCoordinates[1],
edge['arrowDisplaySize']
);
}
// Assignment is redundant here but makes it clear that this call
// mutates targetCoordinates:
if (isArrowDrawRequired('target', edge['arrow'])) {
targetCoordinates = applyArrow(ctx,
targetCoordinates,
edge['target']['displaySize'],
sourceCoordinates[0],
sourceCoordinates[1],
edge['arrowDisplaySize']
);
}
ctx.lineWidth = edge['displaySize'] / 3;
ctx.beginPath();
ctx.moveTo(sourceCoordinates[0], sourceCoordinates[1]);
ctx.lineTo(targetCoordinates[0], targetCoordinates[1]);
ctx.stroke();
break;
}
if(self.p.edgeLabels && edge['label']){
var p1 = {};
p1.x = sourceCoordinates[0];
p1.y = sourceCoordinates[1];
var p2 = {};
p2.x = targetCoordinates[0];
p2.y = targetCoordinates[1];
drawEdgeLabel(ctx,edge['label'],p1,p2, color);
}
return self;
};
function drawEdgeLabel(ctx, text, p1, p2, color) {
var alignment = 'center';
var padding = 10;
var dx = p2.x - p1.x;
var dy = p2.y - p1.y;
var len = Math.sqrt(dx * dx + dy * dy);
var avail = len - 2 * padding;
// Keep text upright
var angle = Math.atan2(dy, dx);
if (angle < -Math.PI / 2 || angle > Math.PI / 2) {
var p = p1;
p1 = p2;
p2 = p;
dx *= -1;
dy *= -1;
angle -= Math.PI;
}
var p = p1;
var pad = 1 / 2;
ctx.save();
ctx.textAlign = alignment;
ctx.translate(p.x + dx * pad, p.y + dy * pad);
ctx.rotate(angle);
var fontSize = self.p.defaultLabelSize;
ctx.font = self.p.fontStyle + fontSize + 'px ' + self.p.font;
ctx.fillStyle = color;
ctx.fillText(text, 0, -5);
ctx.restore();
};
/**
* 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;
};
/**
* Helper function that tells us if a 'source'/'target' arrow setting
* applies, based on active edge/plotter settings.
* @param {string} arrowheadLocation 'source' or 'target'.
* @param {string} edgeArrowSetting arrow setting ('none', 'source',
* 'target', 'both') on the edge itself,
* if any.
* @return {boolean} Returns true if draw is required for the passed-in
* arrowheadLocation.
*/
function isArrowDrawRequired(arrowheadLocation, edgeArrowSetting) {
return (
edgeArrowSetting == arrowheadLocation ||
edgeArrowSetting == 'both' ||
(
!edgeArrowSetting &&
(
self.p.defaultEdgeArrow == arrowheadLocation ||
self.p.defaultEdgeArrow == 'both'
)
)
);
};
/**
* Helper function that draws an arrowhead at a node's border based on the
* node's center coordinates and size, as well as a set of "control"
* coordinates. Future implementations could add a nodeShape parm to this
* function.
* For efficiency, nodeCoordinates parm is mutated according to calculated
* arrowhead tip coordinates.
* @param {CanvasRenderingContext2D} ctx The context within which to draw
* arrows.
* @param {Array} nodeCoordinates [x,y] coordinate of center of node.
* THIS FUNCTION ADJUSTS THE COORDINATES IN THIS ARRAY TO MATCH THE
* INTERSECTION BETWEEN THE NODE BORDER AND THE LINE FROM THE CENTER OF
* THE NODE TO THE CONTROL COORDINATES.
* Parm mutation here favors performance but may be considered bad form
* in the general case.
* @param {number} nodeSize The size of the node. For circle
* nodes (the only supported node
* shape as of 2013-05-14), size is
* radius.
* @param {number} ctrlX x-coordinate of the control point.
* @param {number} ctrlY y-coordinate of the control point.
* @param {number} size length of arrowhead.
* @return {Array} Returns the mutated (arrowhead) coordinates.
*/
function applyArrow(ctx, nodeCoordinates, nodeSize, ctrlX, ctrlY, size) {
// Calculate and re-assign edge connection coordinates (at node border
// instead of node center):
var xDiff = nodeCoordinates[0] - ctrlX;
var yDiff = nodeCoordinates[1] - ctrlY;
var ratio = nodeSize / Math.sqrt(xDiff * xDiff + yDiff * yDiff);
nodeCoordinates[0] = nodeCoordinates[0] - xDiff * ratio;
nodeCoordinates[1] = nodeCoordinates[1] - yDiff * ratio;
// Draw arrowhead:
ctx.lineWidth = 0;
ctx.fillStyle = ctx.strokeStyle;
sigma.tools.drawArrowhead(
ctx,
nodeCoordinates[0],
nodeCoordinates[1],
size,
sigma.tools.getIncidenceAngle(
ctrlX,
ctrlY,
nodeCoordinates[0],
nodeCoordinates[1]
)
);
return nodeCoordinates;
};
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;
}
/**
* 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.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;
};
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.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.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);
};
/**
* Draws a filled arrowhead shape to the corresponding canvas.
* @param {CanvasRenderingContext2D} ctx The context within which to draw the
* arrowhead.
* @param {number} x0 The x-coordinate of the tip of the
* arrowhead.
* @param {number} y0 The y-coordinate of the tip of the
* arrowhead.
* @param {number} size The length of the arrowhead.
* @param {number} rotationAngle The angle of rotation of the
* arrowhead, in degrees.
* @return {undefined}
*/
sigma.tools.drawArrowhead = function(ctx, x0, y0, size, rotationAngle) {
// Angle between one side of arrowhead and shaft:
var ARROW_SHARPNESS = 22;
ctx.beginPath();
ctx.moveTo(x0, y0);
// (Math.PI / 180) === 0.017453292519943295
var coef = 0.017453292519943295;
var x1 = x0 + Math.cos(coef * (ARROW_SHARPNESS + rotationAngle)) * size;
var y1 = y0 + Math.sin(coef * (ARROW_SHARPNESS + rotationAngle)) * size;
var x2 = x0 + Math.cos(coef * (rotationAngle - ARROW_SHARPNESS)) * size;
var y2 = y0 + Math.sin(coef * (rotationAngle - ARROW_SHARPNESS)) * size;
ctx.lineTo(x1, y1);
ctx.quadraticCurveTo((x0 + x1 + x2) / 3, (y0 + y1 + y2) / 3, x2, y2);
ctx.lineTo(x0, y0);
ctx.fill();
};
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);
};
/**
* Provides the angle of incidence of the end point of a line or quadratic
* curve, in degrees.
* @param {number} x1 The x-coordinate of the start point of the line or
* control point of the quadratic curve.
* @param {number} y1 The y-coordinate of the start point of the line or
* control point of the quadratic curve.
* @param {number} x2 The x-coordinate of the line or quadratic curve end
* point.
* @param {number} y2 The y-coordinate of the line or quadratic curve end
* point.
* @return {number} Returns the angle of incidence of the end point of the
* line or quadratic curve cooresponding to the coordinate
* parms, in degrees.
*/
sigma.tools.getIncidenceAngle = function(x1, y1, x2, y2) {
return (
(x1 <= x2 ? 180 : 0) +
Math.atan(((y2 - y1) / (x2 - x1))) * 180 / Math.PI
);
};
sigma.publicPrototype = SigmaPublic.prototype;
})();
// Mathieu Jacomy @ Sciences Po Médialab & WebAtlas
// (requires sigma.js to be loaded)
sigma.forceatlas2 = sigma.forceatlas2 || {};
sigma.forceatlas2.ForceAtlas2 = function(graph) {
sigma.classes.Cascade.call(this);
var self = this;
this.graph = graph;
this.p = {
linLogMode: false,
outboundAttractionDistribution: false,
adjustSizes: false,
edgeWeightInfluence: 0,
scalingRatio: 1,
strongGravityMode: false,
gravity: 1,
jitterTolerance: 1,
barnesHutOptimize: false,
barnesHutTheta: 1.2,
speed: 1,
outboundAttCompensation: 1,
totalSwinging: 0,
totalEffectiveTraction: 0,
complexIntervals: 500,
simpleIntervals: 1000
};
// The state tracked from one atomic "go" to another
this.state = {step: 0, index: 0};
this.rootRegion;
// Runtime (the ForceAtlas2 itself)
this.init = function() {
self.state = {step: 0, index: 0};
self.graph.nodes.forEach(function(n) {
n.fa2 = {
mass: 1 + n.degree,
old_dx: 0,
old_dy: 0,
dx: 0,
dy: 0
};
});
return self;
}
this.go = function() {
while (self.atomicGo()) {}
}
this.atomicGo = function() {
var graph = self.graph;
var nodes = graph.nodes;
var edges = graph.edges;
var cInt = self.p.complexIntervals;
var sInt = self.p.simpleIntervals;
switch (self.state.step) {
case 0: // Pass init
// Initialise layout data
nodes.forEach(function(n) {
if(n.fa2) {
n.fa2.mass = 1 + n.degree;
n.fa2.old_dx = n.fa2.dx;
n.fa2.old_dy = n.fa2.dx;
n.fa2.dx = 0;
n.fa2.dy = 0;
} else {
n.fa2 = {
mass: 1 + n.degree,
old_dx: 0,
old_dy: 0,
dx: 0,
dy: 0
};
}
});
// If Barnes Hut active, initialize root region
if (self.p.barnesHutOptimize) {
self.rootRegion = new sigma.forceatlas2.Region(nodes, 0);
self.rootRegion.buildSubRegions();
}
// If outboundAttractionDistribution active, compensate.
if (self.p.outboundAttractionDistribution) {
self.p.outboundAttCompensation = 0;
nodes.forEach(function(n) {
self.p.outboundAttCompensation += n.fa2.mass;
});
self.p.outboundAttCompensation /= nodes.length;
}
self.state.step = 1;
self.state.index = 0;
return true;
break;
case 1: // Repulsion
var Repulsion = self.ForceFactory.buildRepulsion(
self.p.adjustSizes,
self.p.scalingRatio
);
if (self.p.barnesHutOptimize) {
var rootRegion = self.rootRegion;
// Pass to the scope of forEach
var barnesHutTheta = self.p.barnesHutTheta;
var i = self.state.index;
while (i < nodes.length && i < self.state.index + cInt) {
var n = nodes[i++];
if(n.fa2)
rootRegion.applyForce(n, Repulsion, barnesHutTheta);
}
if (i == nodes.length) {
self.state.step = 2;
self.state.index = 0;
} else {
self.state.index = i;
}
} else {
var i1 = self.state.index;
while (i1 < nodes.length && i1 < self.state.index + cInt) {
var n1 = nodes[i1++];
if(n1.fa2)
nodes.forEach(function(n2, i2) {
if (i2 < i1 && n2.fa2) {
Repulsion.apply_nn(n1, n2);
}
});
}
if (i1 == nodes.length) {
self.state.step = 2;
self.state.index = 0;
} else {
self.state.index = i1;
}
}
return true;
break;
case 2: // Gravity
var Gravity = (self.p.strongGravityMode) ?
(self.ForceFactory.getStrongGravity(
self.p.scalingRatio
)) :
(self.ForceFactory.buildRepulsion(
self.p.adjustSizes,
self.p.scalingRatio
));
// Pass gravity and scalingRatio to the scope of the function
var gravity = self.p.gravity,
scalingRatio = self.p.scalingRatio;
var i = self.state.index;
while (i < nodes.length && i < self.state.index + sInt) {
var n = nodes[i++];
if (n.fa2)
Gravity.apply_g(n, gravity / scalingRatio);
}
if (i == nodes.length) {
self.state.step = 3;
self.state.index = 0;
} else {
self.state.index = i;
}
return true;
break;
case 3: // Attraction
var Attraction = self.ForceFactory.buildAttraction(
self.p.linLogMode,
self.p.outboundAttractionDistribution,
self.p.adjustSizes,
1 * ((self.p.outboundAttractionDistribution) ?
(self.p.outboundAttCompensation) :
(1))
);
var i = self.state.index;
if (self.p.edgeWeightInfluence == 0) {
while (i < edges.length && i < self.state.index + cInt) {
var e = edges[i++];
Attraction.apply_nn(e.source, e.target, 1);
}
} else if (self.p.edgeWeightInfluence == 1) {
while (i < edges.length && i < self.state.index + cInt) {
var e = edges[i++];
Attraction.apply_nn(e.source, e.target, e.weight || 1);
}
} else {
while (i < edges.length && i < self.state.index + cInt) {
var e = edges[i++];
Attraction.apply_nn(
e.source, e.target,
Math.pow(e.weight || 1, self.p.edgeWeightInfluence)
);
}
}
if (i == edges.length) {
self.state.step = 4;
self.state.index = 0;
} else {
self.state.index = i;
}
return true;
break;
case 4: // Auto adjust speed
var totalSwinging = 0; // How much irregular movement
var totalEffectiveTraction = 0; // Hom much useful movement
nodes.forEach(function(n) {
var fixed = n.fixed || false;
if (!fixed && n.fa2) {
var swinging = Math.sqrt(Math.pow(n.fa2.old_dx - n.fa2.dx, 2) +
Math.pow(n.fa2.old_dy - n.fa2.dy, 2));
// If the node has a burst change of direction,
// then it's not converging.
totalSwinging += n.fa2.mass * swinging;
totalEffectiveTraction += n.fa2.mass *
0.5 *
Math.sqrt(
Math.pow(n.fa2.old_dx + n.fa2.dx, 2) +
Math.pow(n.fa2.old_dy + n.fa2.dy, 2)
);
}
});
self.p.totalSwinging = totalSwinging;
self.p.totalEffectiveTraction = totalEffectiveTraction;
// We want that swingingMovement < tolerance * convergenceMovement
var targetSpeed = Math.pow(self.p.jitterTolerance, 2) *
self.p.totalEffectiveTraction /
self.p.totalSwinging;
// But the speed shoudn't rise too much too quickly,
// since it would make the convergence drop dramatically.
var maxRise = 0.5; // Max rise: 50%
self.p.speed = self.p.speed +
Math.min(
targetSpeed - self.p.speed,
maxRise * self.p.speed
);
// Save old coordinates
nodes.forEach(function(n) {
n.old_x = +n.x;
n.old_y = +n.y;
});
self.state.step = 5;
return true;
break;
case 5: // Apply forces
var i = self.state.index;
if (self.p.adjustSizes) {
var speed = self.p.speed;
// If nodes overlap prevention is active,
// it's not possible to trust the swinging mesure.
while (i < nodes.length && i < self.state.index + sInt) {
var n = nodes[i++];
var fixed = n.fixed || false;
if (!fixed && n.fa2) {
// Adaptive auto-speed: the speed of each node is lowered
// when the node swings.
var swinging = Math.sqrt(
(n.fa2.old_dx - n.fa2.dx) *
(n.fa2.old_dx - n.fa2.dx) +
(n.fa2.old_dy - n.fa2.dy) *
(n.fa2.old_dy - n.fa2.dy)
);
var factor = 0.1 * speed / (1 + speed * Math.sqrt(swinging));
var df = Math.sqrt(Math.pow(n.fa2.dx, 2) +
Math.pow(n.fa2.dy, 2));
factor = Math.min(factor * df, 10) / df;
n.x += n.fa2.dx * factor;
n.y += n.fa2.dy * factor;
}
}
} else {
var speed = self.p.speed;
while (i < nodes.length && i < self.state.index + sInt) {
var n = nodes[i++];
var fixed = n.fixed || false;
if (!fixed && n.fa2) {
// Adaptive auto-speed: the speed of each node is lowered
// when the node swings.
var swinging = Math.sqrt(
(n.fa2.old_dx - n.fa2.dx) *
(n.fa2.old_dx - n.fa2.dx) +
(n.fa2.old_dy - n.fa2.dy) *
(n.fa2.old_dy - n.fa2.dy)
);
var factor = speed / (1 + speed * Math.sqrt(swinging));
n.x += n.fa2.dx * factor;
n.y += n.fa2.dy * factor;
}
}
}
if (i == nodes.length) {
self.state.step = 0;
self.state.index = 0;
return false;
} else {
self.state.index = i;
return true;
}
break;
default:
throw new Error('ForceAtlas2 - atomic state error');
break;
}
}
this.end = function() {
this.graph.nodes.forEach(function(n) {
n.fa2 = null;
});
}
// Auto Settings
this.setAutoSettings = function() {
var graph = this.graph;
// Tuning
if (graph.nodes.length >= 100) {
this.p.scalingRatio = 2.0;
} else {
this.p.scalingRatio = 10.0;
}
this.p.strongGravityMode = false;
this.p.gravity = 1;
// Behavior
this.p.outboundAttractionDistribution = false;
this.p.linLogMode = false;
this.p.adjustSizes = false;
this.p.edgeWeightInfluence = 1;
// Performance
if (graph.nodes.length >= 50000) {
this.p.jitterTolerance = 10;
} else if (graph.nodes.length >= 5000) {
this.p.jitterTolerance = 1;
} else {
this.p.jitterTolerance = 0.1;
}
if (graph.nodes.length >= 1000) {
this.p.barnesHutOptimize = true;
} else {
this.p.barnesHutOptimize = false;
}
this.p.barnesHutTheta = 1.2;
return this;
}
// All the different forces
this.ForceFactory = {
buildRepulsion: function(adjustBySize, coefficient) {
if (adjustBySize) {
return new this.linRepulsion_antiCollision(coefficient);
} else {
return new this.linRepulsion(coefficient);
}
},
getStrongGravity: function(coefficient) {
return new this.strongGravity(coefficient);
},
buildAttraction: function(logAttr, distributedAttr, adjustBySize, c) {
if (adjustBySize) {
if (logAttr) {
if (distributedAttr) {
return new this.logAttraction_degreeDistributed_antiCollision(c);
} else {
return new this.logAttraction_antiCollision(c);
}
} else {
if (distributedAttr) {
return new this.linAttraction_degreeDistributed_antiCollision(c);
} else {
return new this.linAttraction_antiCollision(c);
}
}
} else {
if (logAttr) {
if (distributedAttr) {
return new this.logAttraction_degreeDistributed(c);
} else {
return new this.logAttraction(c);
}
} else {
if (distributedAttr) {
return new this.linAttraction_massDistributed(c);
} else {
return new this.linAttraction(c);
}
}
}
},
// Repulsion force: Linear
linRepulsion: function(c) {
this.coefficient = c;
this.apply_nn = function(n1, n2) {
if(n1.fa2 && n2.fa2)
{
// Get the distance
var xDist = n1.x - n2.x;
var yDist = n1.y - n2.y;
var distance = Math.sqrt(xDist * xDist + yDist * yDist);
if (distance > 0) {
// NB: factor = force / distance
var factor = this.coefficient *
n1.fa2.mass *
n2.fa2.mass /
Math.pow(distance, 2);
n1.fa2.dx += xDist * factor;
n1.fa2.dy += yDist * factor;
n2.fa2.dx -= xDist * factor;
n2.fa2.dy -= yDist * factor;
}
}
}
this.apply_nr = function(n, r) {
// Get the distance
var xDist = n.x - r.config('massCenterX');
var yDist = n.y - r.config('massCenterY');
var distance = Math.sqrt(xDist * xDist + yDist * yDist);
if (distance > 0) {
// NB: factor = force / distance
var factor = this.coefficient *
n.fa2.mass *
r.config('mass') /
Math.pow(distance, 2);
n.fa2.dx += xDist * factor;
n.fa2.dy += yDist * factor;
}
}
this.apply_g = function(n, g) {
// Get the distance
var xDist = n.x;
var yDist = n.y;
var distance = Math.sqrt(xDist * xDist + yDist * yDist);
if (distance > 0) {
// NB: factor = force / distance
var factor = this.coefficient * n.fa2.mass * g / distance;
n.fa2.dx -= xDist * factor;
n.fa2.dy -= yDist * factor;
}
}
},
linRepulsion_antiCollision: function(c) {
this.coefficient = c;
this.apply_nn = function(n1, n2) {
if(n1.fa2 && n2.fa2)
{
// Get the distance
var xDist = n1.x - n2.x;
var yDist = n1.y - n2.y;
var distance = Math.sqrt(xDist * xDist + yDist * yDist) -
n1.size -
n2.size;
if (distance > 0) {
// NB: factor = force / distance
var factor = this.coefficient *
n1.fa2.mass *
n2.fa2.mass /
Math.pow(distance, 2);
n1.fa2.dx += xDist * factor;
n1.fa2.dy += yDist * factor;
n2.fa2.dx -= xDist * factor;
n2.fa2.dy -= yDist * factor;
} else if (distance < 0) {
var factor = 100 * this.coefficient * n1.fa2.mass * n2.fa2.mass;
n1.fa2.dx += xDist * factor;
n1.fa2.dy += yDist * factor;
n2.fa2.dx -= xDist * factor;
n2.fa2.dy -= yDist * factor;
}
}
}
this.apply_nr = function(n, r) {
// Get the distance
var xDist = n.fa2.x() - r.getMassCenterX();
var yDist = n.fa2.y() - r.getMassCenterY();
var distance = Math.sqrt(xDist * xDist + yDist * yDist);
if (distance > 0) {
// NB: factor = force / distance
var factor = this.coefficient *
n.fa2.mass *
r.getMass() /
Math.pow(distance, 2);
n.fa2.dx += xDist * factor;
n.fa2.dy += yDist * factor;
} else if (distance < 0) {
var factor = -this.coefficient * n.fa2.mass * r.getMass() / distance;
n.fa2.dx += xDist * factor;
n.fa2.dy += yDist * factor;
}
}
this.apply_g = function(n, g) {
// Get the distance
var xDist = n.x;
var yDist = n.y;
var distance = Math.sqrt(xDist * xDist + yDist * yDist);
if (distance > 0) {
// NB: factor = force / distance
var factor = this.coefficient * n.fa2.mass * g / distance;
n.fa2.dx -= xDist * factor;
n.fa2.dy -= yDist * factor;
}
}
},
// Repulsion force: Strong Gravity
// (as a Repulsion Force because it is easier)
strongGravity: function(c) {
this.coefficient = c;
this.apply_nn = function(n1, n2) {
// Not Relevant
}
this.apply_nr = function(n, r) {
// Not Relevant
}
this.apply_g = function(n, g) {
// Get the distance
var xDist = n.x;
var yDist = n.y;
var distance = Math.sqrt(xDist * xDist + yDist * yDist);
if (distance > 0) {
// NB: factor = force / distance
var factor = this.coefficient * n.fa2.mass * g;
n.fa2.dx -= xDist * factor;
n.fa2.dy -= yDist * factor;
}
}
},
// Attraction force: Linear
linAttraction: function(c) {
this.coefficient = c;
this.apply_nn = function(n1, n2, e) {
if(n1.fa2 && n2.fa2)
{
// Get the distance
var xDist = n1.x - n2.x;
var yDist = n1.y - n2.y;
// NB: factor = force / distance
var factor = -this.coefficient * e;
n1.fa2.dx += xDist * factor;
n1.fa2.dy += yDist * factor;
n2.fa2.dx -= xDist * factor;
n2.fa2.dy -= yDist * factor;
}
}
},
// Attraction force: Linear, distributed by mass (typically, degree)
linAttraction_massDistributed: function(c) {
this.coefficient = c;
this.apply_nn = function(n1, n2, e) {
if(n1.fa2 && n2.fa2)
{
// Get the distance
var xDist = n1.x - n2.x;
var yDist = n1.y - n2.y;
// NB: factor = force / distance
var factor = -this.coefficient * e / n1.fa2.mass;
n1.fa2.dx += xDist * factor;
n1.fa2.dy += yDist * factor;
n2.fa2.dx -= xDist * factor;
n2.fa2.dy -= yDist * factor;
}
}
},
// Attraction force: Logarithmic
logAttraction: function(c) {
this.coefficient = c;
this.apply_nn = function(n1, n2, e) {
if(n1.fa2 && n2.fa2)
{
// Get the distance
var xDist = n1.x - n2.x;
var yDist = n1.y - n2.y;
var distance = Math.sqrt(xDist * xDist + yDist * yDist);
if (distance > 0) {
// NB: factor = force / distance
var factor = -this.coefficient *
e *
Math.log(1 + distance) /
distance;
n1.fa2.dx += xDist * factor;
n1.fa2.dy += yDist * factor;
n2.fa2.dx -= xDist * factor;
n2.fa2.dy -= yDist * factor;
}
}
}
},
// Attraction force: Linear, distributed by Degree
logAttraction_degreeDistributed: function(c) {
this.coefficient = c;
this.apply_nn = function(n1, n2, e) {
if(n1.fa2 && n2.fa2)
{
// Get the distance
var xDist = n1.x - n2.x;
var yDist = n1.y - n2.y;
var distance = Math.sqrt(xDist * xDist + yDist * yDist);
if (distance > 0) {
// NB: factor = force / distance
var factor = -this.coefficient *
e *
Math.log(1 + distance) /
distance /
n1.fa2.mass;
n1.fa2.dx += xDist * factor;
n1.fa2.dy += yDist * factor;
n2.fa2.dx -= xDist * factor;
n2.fa2.dy -= yDist * factor;
}
}
}
},
// Attraction force: Linear, with Anti-Collision
linAttraction_antiCollision: function(c) {
this.coefficient = c;
this.apply_nn = function(n1, n2, e) {
if(n1.fa2 && n2.fa2)
{
// Get the distance
var xDist = n1.x - n2.x;
var yDist = n1.y - n2.y;
var distance = Math.sqrt(xDist * xDist + yDist * yDist);
if (distance > 0) {
// NB: factor = force / distance
var factor = -this.coefficient * e;
n1.fa2.dx += xDist * factor;
n1.fa2.dy += yDist * factor;
n2.fa2.dx -= xDist * factor;
n2.fa2.dy -= yDist * factor;
}
}
}
},
// Attraction force: Linear, distributed by Degree, with Anti-Collision
linAttraction_degreeDistributed_antiCollision: function(c) {
this.coefficient = c;
this.apply_nn = function(n1, n2, e) {
if(n1.fa2 && n2.fa2)
{
// Get the distance
var xDist = n1.x - n2.x;
var yDist = n1.y - n2.y;
var distance = Math.sqrt(xDist * xDist + yDist * yDist);
if (distance > 0) {
// NB: factor = force / distance
var factor = -this.coefficient * e / n1.fa2.mass;
n1.fa2.dx += xDist * factor;
n1.fa2.dy += yDist * factor;
n2.fa2.dx -= xDist * factor;
n2.fa2.dy -= yDist * factor;
}
}
}
},
// Attraction force: Logarithmic, with Anti-Collision
logAttraction_antiCollision: function(c) {
this.coefficient = c;
this.apply_nn = function(n1, n2, e) {
if(n1.fa2 && n2.fa2)
{
// Get the distance
var xDist = n1.x - n2.x;
var yDist = n1.y - n2.y;
var distance = Math.sqrt(xDist * xDist + yDist * yDist);
if (distance > 0) {
// NB: factor = force / distance
var factor = -this.coefficient *
e *
Math.log(1 + distance) /
distance;
n1.fa2.dx += xDist * factor;
n1.fa2.dy += yDist * factor;
n2.fa2.dx -= xDist * factor;
n2.fa2.dy -= yDist * factor;
}
}
}
},
// Attraction force: Linear, distributed by Degree, with Anti-Collision
logAttraction_degreeDistributed_antiCollision: function(c) {
this.coefficient = c;
this.apply_nn = function(n1, n2, e) {
if(n1.fa2 && n2.fa2)
{
// Get the distance
var xDist = n1.x - n2.x;
var yDist = n1.y - n2.y;
var distance = Math.sqrt(xDist * xDist + yDist * yDist);
if (distance > 0) {
// NB: factor = force / distance
var factor = -this.coefficient *
e *
Math.log(1 + distance) /
distance /
n1.fa2.mass;
n1.fa2.dx += xDist * factor;
n1.fa2.dy += yDist * factor;
n2.fa2.dx -= xDist * factor;
n2.fa2.dy -= yDist * factor;
}
}
}
}
};
};
// The Region class, as used by the Barnes Hut optimization
sigma.forceatlas2.Region = function(nodes, depth) {
sigma.classes.Cascade.call(this);
this.depthLimit = 20;
this.size = 0;
this.nodes = nodes;
this.subregions = [];
this.depth = depth;
this.p = {
mass: 0,
massCenterX: 0,
massCenterY: 0
};
this.updateMassAndGeometry();
}
sigma.forceatlas2.Region.prototype.updateMassAndGeometry = function() {
if (this.nodes.length > 1) {
// Compute Mass
var mass = 0;
var massSumX = 0;
var massSumY = 0;
this.nodes.forEach(function(n) {
mass += n.fa2.mass;
massSumX += n.x * n.fa2.mass;
massSumY += n.y * n.fa2.mass;
});
var massCenterX = massSumX / mass;
massCenterY = massSumY / mass;
// Compute size
var size;
this.nodes.forEach(function(n) {
var distance = Math.sqrt(
(n.x - massCenterX) *
(n.x - massCenterX) +
(n.y - massCenterY) *
(n.y - massCenterY)
);
size = Math.max(size || (2 * distance), 2 * distance);
});
this.p.mass = mass;
this.p.massCenterX = massCenterX;
this.p.massCenterY = massCenterY;
this.size = size;
}
};
sigma.forceatlas2.Region.prototype.buildSubRegions = function() {
if (this.nodes.length > 1) {
var leftNodes = [];
var rightNodes = [];
var subregions = [];
var massCenterX = this.p.massCenterX;
var massCenterY = this.p.massCenterY;
var nextDepth = this.depth + 1;
var self = this;
this.nodes.forEach(function(n) {
var nodesColumn = (n.x < massCenterX) ? (leftNodes) : (rightNodes);
nodesColumn.push(n);
});
var tl = [], bl = [], br = [], tr = [];
leftNodes.forEach(function(n) {
var nodesLine = (n.y < massCenterY) ? (tl) : (bl);
nodesLine.push(n);
});
rightNodes.forEach(function(n) {
var nodesLine = (n.y < massCenterY) ? (tr) : (br);
nodesLine.push(n);
});
[tl, bl, br, tr].filter(function(a) {
return a.length;
}).forEach(function(a) {
if (nextDepth <= self.depthLimit && a.length < self.nodes.length) {
var subregion = new sigma.forceatlas2.Region(a, nextDepth);
subregions.push(subregion);
} else {
a.forEach(function(n) {
var oneNodeList = [n];
var subregion = new sigma.forceatlas2.Region(oneNodeList, nextDepth);
subregions.push(subregion);
});
}
});
this.subregions = subregions;
subregions.forEach(function(subregion) {
subregion.buildSubRegions();
});
}
};
sigma.forceatlas2.Region.prototype.applyForce = function(n, Force, theta) {
if (this.nodes.length < 2) {
var regionNode = this.nodes[0];
Force.apply_nn(n, regionNode);
} else {
var distance = Math.sqrt(
(n.x - this.p.massCenterX) *
(n.x - this.p.massCenterX) +
(n.y - this.p.massCenterY) *
(n.y - this.p.massCenterY)
);
if (distance * theta > this.size) {
Force.apply_nr(n, this);
} else {
this.subregions.forEach(function(subregion) {
subregion.applyForce(n, Force, theta);
});
}
}
};
sigma.publicPrototype.startForceAtlas2 = function() {
//if(!this.forceatlas2) {
this.forceatlas2 = new sigma.forceatlas2.ForceAtlas2(this._core.graph);
this.forceatlas2.setAutoSettings();
this.forceatlas2.init();
//}
this.addGenerator('forceatlas2', this.forceatlas2.atomicGo, function(){
return true;
});
};
sigma.publicPrototype.stopForceAtlas2 = function() {
this.removeGenerator('forceatlas2');
};
/* sigmajs.org - an open-source light-weight JavaScript graph drawing library - Version: 0.1 - Author: Alexis Jacomy - License: MIT */
var sigma={tools:{},classes:{},instances:{}};
(function(){Array.prototype.some||(Array.prototype.some=function(k,n){var g=this.length;if("function"!=typeof k)throw new TypeError;for(var m=0;m<g;m++)if(m in this&&k.call(n,this[m],m,this))return!0;return!1});Array.prototype.forEach||(Array.prototype.forEach=function(k,n){var g=this.length;if("function"!=typeof k)throw new TypeError;for(var m=0;m<g;m++)m in this&&k.call(n,this[m],m,this)});Array.prototype.map||(Array.prototype.map=function(k,n){var g=this.length;if("function"!=typeof k)throw new TypeError;
for(var m=Array(g),q=0;q<g;q++)q in this&&(m[q]=k.call(n,this[q],q,this));return m});Array.prototype.filter||(Array.prototype.filter=function(k,n){var g=this.length;if("function"!=typeof k)throw new TypeError;for(var m=[],q=0;q<g;q++)if(q in this){var u=this[q];k.call(n,u,q,this)&&m.push(u)}return m});Object.keys||(Object.keys=function(){var k=Object.prototype.hasOwnProperty,n=!{toString:null}.propertyIsEnumerable("toString"),g="toString toLocaleString valueOf hasOwnProperty isPrototypeOf propertyIsEnumerable constructor".split(" "),
m=g.length;return function(q){if("object"!==typeof q&&"function"!==typeof q||null===q)throw new TypeError("Object.keys called on non-object");var u=[],t;for(t in q)k.call(q,t)&&u.push(t);if(n)for(t=0;t<m;t++)k.call(q,g[t])&&u.push(g[t]);return u}}())})();
sigma.classes.EventDispatcher=function(){var k={},n=this;this.one=function(g,m){if(!m||!g)return n;("string"==typeof g?g.split(" "):g).forEach(function(g){k[g]||(k[g]=[]);k[g].push({h:m,one:!0})});return n};this.bind=function(g,m){if(!m||!g)return n;("string"==typeof g?g.split(" "):g).forEach(function(g){k[g]||(k[g]=[]);k[g].push({h:m,one:!1})});return n};this.unbind=function(g,m){g||(k={});var q="string"==typeof g?g.split(" "):g;m?q.forEach(function(g){k[g]&&(k[g]=k[g].filter(function(g){return g.h!=
m}));k[g]&&0==k[g].length&&delete k[g]}):q.forEach(function(g){delete k[g]});return n};this.dispatch=function(g,m){k[g]&&(k[g].forEach(function(k){k.h({type:g,content:m,target:n})}),k[g]=k[g].filter(function(g){return!g.one}));return n}};sigma.classes.Cascade=function(){this.p={};this.config=function(k,n){if("string"==typeof k&&void 0==n)return this.p[k];var g="object"==typeof k&&void 0==n?k:{};"string"==typeof k&&(g[k]=n);for(var m in g)void 0!=this.p[m]&&(this.p[m]=g[m]);return this}};
(function(){function k(d,p){function f(){sigma.chronos.removeTask("node_"+c.id,2).removeTask("edge_"+c.id,2).removeTask("label_"+c.id,2).stopTasks();return c}function b(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,c.mousecaptor.mouseY),c.graph.nodes.forEach(function(a){a.hover&&!a.active&&c.plotter.drawHoverNode(a)}));return c}function l(){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=p.toString();this.p={auto:!0,drawNodes:2,drawEdges:1,drawLabels:2,lastNodes:2,lastEdges:0,lastLabels:2,
drawHoverNodes:!0,drawActiveNodes:!0};this.domRoot=d;this.width=this.domRoot.offsetWidth;this.height=this.domRoot.offsetHeight;this.graph=new u;this.domElements={};b("edges","canvas");b("nodes","canvas");b("labels","canvas");b("hover","canvas");b("monitor","div");b("mouse","canvas");this.plotter=new q(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 m(this,this.domElements.monitor);this.mousecaptor=new g(this.domElements.mouse,this.id);this.mousecaptor.bind("drag interpolate",function(a){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(a){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();l()});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 s=0;s<C.plugins.length;s++)C.plugins[s](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;f();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 l in c.domElements)"canvas"==c.domElements[l].nodeName.toLowerCase()&&(void 0==h[l]||0<=h[l])&&c.domElements[l].getContext("2d").clearRect(0,0,c.domElements[l].width,c.domElements[l].height);c.plotter.currentEdgeIndex=0;c.plotter.currentNodeIndex=0;c.plotter.currentLabelIndex=0;l=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,l="node_"+
c.id;if(d)if(1<d)for(;c.plotter.task_drawLabel(););else l?sigma.chronos.queueTask(c.plotter.task_drawLabel,"label_"+c.id,l):sigma.chronos.addTask(c.plotter.task_drawLabel,"label_"+c.id,!1),h=!0,l="label_"+c.id;if(b)if(1<b)for(;c.plotter.task_drawEdge(););else l?sigma.chronos.queueTask(c.plotter.task_drawEdge,"edge_"+c.id,l):sigma.chronos.addTask(c.plotter.task_drawEdge,"edge_"+c.id,!1),h=!0,l="edge_"+c.id;c.dispatch("draw");c.refresh();h&&sigma.chronos.runTasks();return c};this.resize=function(a,
b){var d=c.width,h=c.height;void 0!=a&&void 0!=b?(c.width=a,c.height=b):(c.width=c.domRoot.offsetWidth,c.height=c.domRoot.offsetHeight);if(d!=c.width||h!=c.height){for(var l in c.domElements)c.domElements[l].setAttribute("width",c.width+"px"),c.domElements[l].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();l();return c};this.drawHover=a;this.drawActive=l;this.clearSchedule=f;window.addEventListener("resize",function(){c.resize()})}function n(d){var p=this;sigma.classes.EventDispatcher.call(this);this._core=d;this.kill=function(){};this.getID=function(){return d.id};this.configProperties=function(f,b){var a=d.config(f,b);return a==d?p:a};this.drawingProperties=function(f,b){var a=d.plotter.config(f,b);return a==d.plotter?p:a};this.mouseProperties=function(f,b){var a=d.mousecaptor.config(f,b);return a==
d.mousecaptor?p:a};this.graphProperties=function(f,b){var a=d.graph.config(f,b);return a==d.graph?p:a};this.getMouse=function(){return{mouseX:d.mousecaptor.mouseX,mouseY:d.mousecaptor.mouseY,down:d.mousecaptor.isMouseDown}};this.position=function(f,b,a){if(0==arguments.length)return{stageX:d.mousecaptor.stageX,stageY:d.mousecaptor.stageY,ratio:d.mousecaptor.ratio};d.mousecaptor.stageX=void 0!=f?f:d.mousecaptor.stageX;d.mousecaptor.stageY=void 0!=b?b:d.mousecaptor.stageY;d.mousecaptor.ratio=void 0!=
a?a:d.mousecaptor.ratio;return p};this.goTo=function(f,b,a){d.mousecaptor.interpolate(f,b,a);return p};this.zoomTo=function(f,b,a){a=Math.min(Math.max(d.mousecaptor.config("minRatio"),a),d.mousecaptor.config("maxRatio"));a==d.mousecaptor.ratio?d.mousecaptor.interpolate(f-d.width/2+d.mousecaptor.stageX,b-d.height/2+d.mousecaptor.stageY):d.mousecaptor.interpolate((a*f-d.mousecaptor.ratio*d.width/2)/(a-d.mousecaptor.ratio),(a*b-d.mousecaptor.ratio*d.height/2)/(a-d.mousecaptor.ratio),a);return p};this.resize=
function(f,b){d.resize(f,b);return p};this.draw=function(f,b,a,l){d.draw(f,b,a,l);return p};this.refresh=function(){d.refresh();return p};this.addGenerator=function(f,b,a){sigma.chronos.addGenerator(f+"_ext_"+d.id,b,a);return p};this.removeGenerator=function(f){sigma.chronos.removeGenerator(f+"_ext_"+d.id);return p};this.addNode=function(f,b){d.graph.addNode(f,b);return p};this.addEdge=function(f,b,a,l){d.graph.addEdge(f,b,a,l);return p};this.dropNode=function(f){d.graph.dropNode(f);return p};this.dropEdge=
function(f){d.graph.dropEdge(f);return p};this.pushGraph=function(f,b){f.nodes&&f.nodes.forEach(function(a){!a.id||b&&d.graph.nodesIndex[a.id]||p.addNode(a.id,a)});f.edges&&f.edges.forEach(function(a){validID=a.source&&a.target&&a.id;!validID||b&&d.graph.edgesIndex[a.id]||p.addEdge(a.id,a.source,a.target,a)});return p};this.emptyGraph=function(){d.graph.empty();return p};this.getNodesCount=function(){return d.graph.nodes.length};this.getEdgesCount=function(){return d.graph.edges.length};this.iterNodes=
function(f,b){d.graph.iterNodes(f,b);return p};this.iterEdges=function(f,b){d.graph.iterEdges(f,b);return p};this.getNodes=function(f){return d.graph.getNodes(f)};this.getEdges=function(f){return d.graph.getEdges(f)};this.activateMonitoring=function(){return d.monitor.activate()};this.desactivateMonitoring=function(){return d.monitor.desactivate()};d.bind("downnodes upnodes downgraph upgraph",function(d){p.dispatch(d.type,d.content)});d.graph.bind("overnodes outnodes",function(d){p.dispatch(d.type,
d.content)})}function g(d){function p(b){a.p.mouseEnabled&&(f(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 f(c,d,l){a.isMouseDown||(window.clearInterval(a.interpolationID),n=void 0!=l,s=a.stageX,y=c,g=a.stageY,h=d,r=l||a.ratio,r=Math.min(Math.max(r,a.p.minRatio),a.p.maxRatio),w=a.p.directZooming?1-(n?a.p.zoomDelta:a.p.dragDelta):
0,a.ratio==r&&a.stageX==y&&a.stageY==h)||(b(),a.interpolationID=window.setInterval(b,50),a.dispatch("startinterpolate"))}function b(){w+=n?a.p.zoomDelta:a.p.dragDelta;w=Math.min(w,1);var b=sigma.easing.quadratic.easeout(w),c=a.ratio;a.ratio=c*(1-b)+r*b;n?(a.stageX=y+(a.stageX-y)*a.ratio/c,a.stageY=h+(a.stageY-h)*a.ratio/c):(a.stageX=s*(1-b)+y*b,a.stageY=g*(1-b)+h*b);a.dispatch("interpolate");1<=w&&(window.clearInterval(a.interpolationID),b=a.ratio,n?(a.ratio=r,a.stageX=y+(a.stageX-y)*a.ratio/b,a.stageY=
h+(a.stageY-h)*a.ratio/b):(a.stageX=y,a.stageY=h),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 l=0,c=0,s=0,g=0,r=1,y=0,h=0,k=0,m=0,B=0,F=0,w=0,n=!1;this.stageY=this.stageX=0;this.ratio=1;this.mouseY=this.mouseX=0;this.isMouseDown=!1;d.addEventListener("DOMMouseScroll",p,!0);d.addEventListener("mousewheel",
p,!0);d.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-l+s,h=a.mouseY-c+g;if(d!=a.stageX||h!=a.stageY)m=k,F=B,k=d,B=h,a.stageX=d,a.stageY=h,a.dispatch("drag")}a.dispatch("move");b.preventDefault?b.preventDefault():b.returnValue=!1},!0);d.addEventListener("mousedown",function(b){a.p.mouseEnabled&&
(a.isMouseDown=!0,a.dispatch("mousedown"),s=a.stageX,g=a.stageY,l=a.mouseX,c=a.mouseY,m=k=a.stageX,F=B=a.stageY,a.dispatch("startdrag"),b.preventDefault?b.preventDefault():b.returnValue=!1)},!0);document.addEventListener("mouseup",function(b){a.p.mouseEnabled&&a.isMouseDown&&(a.isMouseDown=!1,a.dispatch("mouseup"),s==a.stageX&&g==a.stageY||f(a.stageX+a.p.inertia*(a.stageX-m),a.stageY+a.p.inertia*(a.stageY-F)),b.preventDefault?b.preventDefault():b.returnValue=!1)},!0);this.checkBorders=function(b,
c,d){return a};this.interpolate=f}function m(d,p){function f(){var a;a="<p>GLOBAL :</p>";for(var d in b.p.globalProbes)a+="<p>"+d+" : "+b.p.globalProbes[d]()+"</p>";a+="<br><p>LOCAL :</p>";for(d in b.p.localProbes)a+="<p>"+d+" : "+b.p.localProbes[d]()+"</p>";b.p.dom.innerHTML=a;return b}sigma.classes.Cascade.call(this);var b=this;this.instance=d;this.monitoring=!1;this.p={fps:40,dom:p,globalProbes:{"Time (ms)":sigma.chronos.getExecutionTime,Queue:sigma.chronos.getQueuedTasksCount,Tasks:sigma.chronos.getTasksCount,
FPS:sigma.chronos.getFPS},localProbes:{"Nodes count":function(){return b.instance.graph.nodes.length},"Edges count":function(){return b.instance.graph.edges.length}}};this.activate=function(){b.monitoring||(b.monitoring=window.setInterval(f,1E3/b.p.fps));return b};this.desactivate=function(){b.monitoring&&(window.clearInterval(b.monitoring),b.monitoring=null,b.p.dom.innerHTML="");return b}}function q(d,p,f,b,a,l,c){function s(a){var c=b,d="fixed"==h.p.labelSize?h.p.defaultLabelSize:h.p.labelSizeRatio*
a.displaySize;c.font=(h.p.hoverFontStyle||h.p.fontStyle||"")+" "+d+"px "+(h.p.hoverFont||h.p.font||"");c.fillStyle="node"==h.p.labelHoverBGColor?a.color||h.p.defaultNodeColor:h.p.defaultHoverLabelBGColor;c.beginPath();h.p.labelHoverShadow&&(c.shadowOffsetX=0,c.shadowOffsetY=0,c.shadowBlur=4,c.shadowColor=h.p.labelHoverShadowColor);sigma.tools.drawRoundRect(c,Math.round(a.displayX-d/2-2),Math.round(a.displayY-d/2-2),Math.round(c.measureText(a.label).width+1.5*a.displaySize+d/2+4),Math.round(d+4),Math.round(d/
2+2),"left");c.closePath();c.fill();c.shadowOffsetX=0;c.shadowOffsetY=0;c.shadowBlur=0;c.beginPath();c.fillStyle="node"==h.p.nodeBorderColor?a.color||h.p.defaultNodeColor:h.p.defaultNodeBorderColor;c.arc(Math.round(a.displayX),Math.round(a.displayY),a.displaySize+h.p.borderSize,0,2*Math.PI,!0);c.closePath();c.fill();c.beginPath();c.fillStyle="node"==h.p.nodeHoverColor?a.color||h.p.defaultNodeColor:h.p.defaultNodeHoverColor;c.arc(Math.round(a.displayX),Math.round(a.displayY),a.displaySize,0,2*Math.PI,
!0);c.closePath();c.fill();c.fillStyle="node"==h.p.labelHoverColor?a.color||h.p.defaultNodeColor:h.p.defaultLabelHoverColor;c.fillText(a.label,Math.round(a.displayX+1.5*a.displaySize),Math.round(a.displayY+d/2-3));return h}function g(a){if(isNaN(a.x)||isNaN(a.y))throw Error("A node's coordinate is not a number (id: "+a.id+")");return!a.hidden&&a.displayX+a.displaySize>-k/3&&a.displayX-a.displaySize<4*k/3&&a.displayY+a.displaySize>-m/3&&a.displayY-a.displaySize<4*m/3}function r(a,b){return b==a||"both"==
b||!b&&(h.p.defaultEdgeArrow==a||"both"==h.p.defaultEdgeArrow)}function y(a,b,c,d,h,l){var f=b[0]-d,s=b[1]-h;c/=Math.sqrt(f*f+s*s);b[0]-=f*c;b[1]-=s*c;a.lineWidth=0;a.fillStyle=a.strokeStyle;sigma.tools.drawArrowhead(a,b[0],b[1],l,sigma.tools.getIncidenceAngle(d,h,b[0],b[1]));return b}sigma.classes.Cascade.call(this);var h=this;this.p={labelColor:"default",defaultLabelColor:"#000",labelHoverBGColor:"default",defaultHoverLabelBGColor:"#fff",labelHoverShadow:!0,labelHoverShadowColor:"#000",labelHoverColor:"default",
defaultLabelHoverColor:"#000",labelActiveBGColor:"default",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",defaultEdgeArrow:"none",defaultNodeColor:"#aaa",nodeHoverColor:"node",defaultNodeHoverColor:"#fff",
nodeActiveColor:"node",defaultNodeActiveColor:"#fff",borderSize:0,nodeBorderColor:"node",defaultNodeBorderColor:"#fff",edgesSpeed:200,nodesSpeed:200,labelsSpeed:200};var k=l,m=c;this.currentLabelIndex=this.currentNodeIndex=this.currentEdgeIndex=0;this.task_drawLabel=function(){for(var b=a.nodes.length,c=0;c++<h.p.labelsSpeed&&h.currentLabelIndex<b;)if(h.isOnScreen(a.nodes[h.currentLabelIndex])){var d=a.nodes[h.currentLabelIndex++],l=f;if(d.displaySize>=h.p.labelThreshold||d.forceLabel){var s="fixed"==
h.p.labelSize?h.p.defaultLabelSize:h.p.labelSizeRatio*d.displaySize;l.font=h.p.fontStyle+s+"px "+h.p.font;l.fillStyle="node"==h.p.labelColor?d.color||h.p.defaultNodeColor:h.p.defaultLabelColor;l.fillText(d.label,Math.round(d.displayX+1.5*d.displaySize),Math.round(d.displayY+s/2-3))}}else h.currentLabelIndex++;return h.currentLabelIndex<b};this.task_drawEdge=function(){for(var b=a.edges.length,c,d,l=0;l++<h.p.edgesSpeed&&h.currentEdgeIndex<b;)if(e=a.edges[h.currentEdgeIndex],c=e.source,d=e.target,
e.hidden||c.hidden||d.hidden||!h.isOnScreen(c)&&!h.isOnScreen(d))h.currentEdgeIndex++;else{c=a.edges[h.currentEdgeIndex++];d=[c.source.displayX,c.source.displayY];var f=[c.target.displayX,c.target.displayY],s=c.color;if(!s)switch(h.p.edgeColor){case "source":s=c.source.color||h.p.defaultNodeColor;break;case "target":s=c.target.color||h.p.defaultNodeColor;break;default:s=h.p.defaultEdgeColor}var g=p;switch(c.type||h.p.defaultEdgeType){case "curve":g.strokeStyle=s;var s=(d[0]+f[0])/2+(f[1]-d[1])/4,
k=(d[1]+f[1])/2+(d[0]-f[0])/4;r("source",c.arrow)&&(d=y(g,d,c.source.displaySize,s,k,c.arrowDisplaySize));r("target",c.arrow)&&(f=y(g,f,c.target.displaySize,s,k,c.arrowDisplaySize));g.lineWidth=c.displaySize/3;g.beginPath();g.moveTo(d[0],d[1]);g.quadraticCurveTo(s,k,f[0],f[1]);g.stroke();break;default:g.strokeStyle=s,r("source",c.arrow)&&(d=y(g,d,c.source.displaySize,f[0],f[1],c.arrowDisplaySize)),r("target",c.arrow)&&(f=y(g,f,c.target.displaySize,d[0],d[1],c.arrowDisplaySize)),g.lineWidth=c.displaySize/
3,g.beginPath(),g.moveTo(d[0],d[1]),g.lineTo(f[0],f[1]),g.stroke()}}return h.currentEdgeIndex<b};this.task_drawNode=function(){for(var b=a.nodes.length,c=0;c++<h.p.nodesSpeed&&h.currentNodeIndex<b;)if(h.isOnScreen(a.nodes[h.currentNodeIndex])){var l=a.nodes[h.currentNodeIndex++],f=Math.round(10*l.displaySize)/10,g=d;g.fillStyle=l.color;g.beginPath();g.arc(l.displayX,l.displayY,f,0,2*Math.PI,!0);g.closePath();g.fill();l.hover&&s(l)}else h.currentNodeIndex++;return h.currentNodeIndex<b};this.drawActiveNode=
function(a){var c=b;if(!g(a))return h;var d="fixed"==h.p.labelSize?h.p.defaultLabelSize:h.p.labelSizeRatio*a.displaySize;c.font=(h.p.activeFontStyle||h.p.fontStyle||"")+" "+d+"px "+(h.p.activeFont||h.p.font||"");c.fillStyle="node"==h.p.labelHoverBGColor?a.color||h.p.defaultNodeColor:h.p.defaultActiveLabelBGColor;c.beginPath();h.p.labelActiveShadow&&(c.shadowOffsetX=0,c.shadowOffsetY=0,c.shadowBlur=4,c.shadowColor=h.p.labelActiveShadowColor);sigma.tools.drawRoundRect(c,Math.round(a.displayX-d/2-2),
Math.round(a.displayY-d/2-2),Math.round(c.measureText(a.label).width+1.5*a.displaySize+d/2+4),Math.round(d+4),Math.round(d/2+2),"left");c.closePath();c.fill();c.shadowOffsetX=0;c.shadowOffsetY=0;c.shadowBlur=0;c.beginPath();c.fillStyle="node"==h.p.nodeBorderColor?a.color||h.p.defaultNodeColor:h.p.defaultNodeBorderColor;c.arc(Math.round(a.displayX),Math.round(a.displayY),a.displaySize+h.p.borderSize,0,2*Math.PI,!0);c.closePath();c.fill();c.beginPath();c.fillStyle="node"==h.p.nodeActiveColor?a.color||
h.p.defaultNodeColor:h.p.defaultNodeActiveColor;c.arc(Math.round(a.displayX),Math.round(a.displayY),a.displaySize,0,2*Math.PI,!0);c.closePath();c.fill();c.fillStyle="node"==h.p.labelActiveColor?a.color||h.p.defaultNodeColor:h.p.defaultLabelActiveColor;c.fillText(a.label,Math.round(a.displayX+1.5*a.displaySize),Math.round(a.displayY+d/2-3));return h};this.drawHoverNode=s;this.isOnScreen=g;this.resize=function(a,c){k=a;m=c;return h}}function u(){function d(a){return{x:a.x,y:a.y,size:a.size,degree:a.degree,
inDegree:a.inDegree,outDegree:a.outDegree,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,forceLabel:a.forceLabel,attr:a.attr}}function g(a){return{source:a.source.id,target:a.target.id,size:a.size,type:a.type,arrow:a.arrow,weight:a.weight,displaySize:a.displaySize,label:a.label,hidden:a.hidden,id:a.id,attr:a.attr,color:a.color}}function f(){b.nodes=[];b.nodesIndex={};b.edges=[];b.edgesIndex={};return b}
sigma.classes.Cascade.call(this);sigma.classes.EventDispatcher.call(this);var b=this;this.p={minNodeSize:0,maxNodeSize:0,minEdgeSize:0,maxEdgeSize:0,scalingMode:"inside",nodesPowRatio:0.5,edgesPowRatio:0,sideMargin:0,arrowRatio:3};this.borders={};f();this.addNode=function(a,d){if(b.nodesIndex[a])throw Error('Node "'+a+'" already exists.');d=d||{};var c={x:0,y:0,size:1,degree:0,inDegree:0,outDegree:0,fixed:!1,active:!1,hidden:!1,forceLabel:!1,label:a.toString(),id:a.toString(),attr:{}},f;for(f in d)switch(f){case "id":break;
case "x":case "y":case "size":c[f]=+d[f];break;case "fixed":case "active":case "hidden":case "forceLabel":c[f]=!!d[f];break;case "color":case "label":c[f]=d[f];break;default:c.attr[f]=d[f]}b.nodes.push(c);b.nodesIndex[a.toString()]=c;return b};this.addEdge=function(a,d,c,f){if(b.edgesIndex[a])throw Error('Edge "'+a+'" already exists.');if(!b.nodesIndex[d])throw Error("Edge's source \""+d+'" does not exist yet.');if(!b.nodesIndex[c])throw Error("Edge's target \""+c+'" does not exist yet.');f=f||{};
d={source:b.nodesIndex[d],target:b.nodesIndex[c],size:1,weight:1,displaySize:0.5,label:a.toString(),id:a.toString(),hidden:!1,attr:{}};d.source.degree++;d.source.outDegree++;d.target.degree++;d.target.inDegree++;for(var g in f)switch(g){case "id":case "source":case "target":break;case "hidden":d[g]=!!f[g];break;case "size":case "weight":d[g]=+f[g];break;case "color":case "arrow":case "type":d[g]=f[g].toString();break;case "label":d[g]=f[g];break;default:d.attr[g]=f[g]}b.edges.push(d);b.edgesIndex[a.toString()]=
d;return b};this.dropNode=function(a){var d={};((a instanceof Array?a:[a])||[]).forEach(function(a){b.nodesIndex[a]?d[a]=!0:sigma.log('Node "'+a+'" does not exist.')});var c=[];b.nodes.forEach(function(a,b){a.id in d&&(c.unshift(b),0==a.degree&&delete d[a.id])});c.forEach(function(a){b.nodes.splice(a,1)});b.edges=b.edges.filter(function(a){return a.source.id in d?(delete b.edgesIndex[a.id],a.target.degree--,a.target.inDegree--,!1):a.target.id in d?(delete b.edgesIndex[a.id],a.source.degree--,a.source.outDegree--,
!1):!0});return b};this.dropEdge=function(a){((a instanceof Array?a:[a])||[]).forEach(function(a){if(b.edgesIndex[a]){b.edgesIndex[a].source.degree--;b.edgesIndex[a].source.outDegree--;b.edgesIndex[a].target.degree--;b.edgesIndex[a].target.inDegree--;var c=null;b.edges.some(function(b,d){return b.id==a?(c=d,!0):!1});null!=c&&b.edges.splice(c,1);delete b.edgesIndex[a]}else sigma.log('Edge "'+a+'" does not exist.')});return b};this.iterEdges=function(a,d){var c=d?d.map(function(a){return b.edgesIndex[a]}):
b.edges,f=c.map(g);f.forEach(a);c.forEach(function(a,c){var d=f[c],h;for(h in d)switch(h){case "id":case "displaySize":break;case "weight":case "size":a[h]=+d[h];break;case "source":case "target":a[h]=b.nodesIndex[h]||a[h];break;case "hidden":a[h]=!!d[h];break;case "color":case "label":case "arrow":case "type":a[h]=(d[h]||"").toString();break;default:a.attr[h]=d[h]}});return b};this.iterNodes=function(a,f){var c=f?f.map(function(a){return b.nodesIndex[a]}):b.nodes,g=c.map(d);g.forEach(a);c.forEach(function(a,
c){var d=g[c],b;for(b in d)switch(b){case "id":case "attr":case "degree":case "inDegree":case "outDegree":case "displayX":case "displayY":case "displaySize":break;case "x":case "y":case "size":a[b]=+d[b];break;case "fixed":case "active":case "hidden":case "forceLabel":a[b]=!!d[b];break;case "color":case "label":a[b]=(d[b]||"").toString();break;default:a.attr[b]=d[b]}});return b};this.getEdges=function(a){var d=((a instanceof Array?a:[a])||[]).map(function(a){return g(b.edgesIndex[a])});return a instanceof
Array?d:d[0]};this.getNodes=function(a){var f=((a instanceof Array?a:[a])||[]).map(function(a){return d(b.nodesIndex[a])});return a instanceof Array?f:f[0]};this.empty=f;this.rescale=function(a,d,c,f){var g=0,p=0;c&&b.nodes.forEach(function(a){p=Math.max(a.size,p)});f&&b.edges.forEach(function(a){g=Math.max(a.size,g)});var p=p||1,g=g||1,k,h,m,n;c&&b.nodes.forEach(function(a){h=Math.max(a.x,h||a.x);k=Math.min(a.x,k||a.x);n=Math.max(a.y,n||a.y);m=Math.min(a.y,m||a.y)});var q="outside"==b.p.scalingMode?
Math.max(a/Math.max(h-k,1),d/Math.max(n-m,1)):Math.min(a/Math.max(h-k,1),d/Math.max(n-m,1)),t=(b.p.maxNodeSize||p)/q+b.p.sideMargin;h+=t;k-=t;n+=t;m-=t;var q="outside"==b.p.scalingMode?Math.max(a/Math.max(h-k,1),d/Math.max(n-m,1)):Math.min(a/Math.max(h-k,1),d/Math.max(n-m,1)),w,u;b.p.maxNodeSize||b.p.minNodeSize?b.p.maxNodeSize==b.p.minNodeSize?(w=0,u=b.p.maxNodeSize):(w=(b.p.maxNodeSize-b.p.minNodeSize)/p,u=b.p.minNodeSize):(w=1,u=0);var A,E;b.p.maxEdgeSize||b.p.minEdgeSize?(A=b.p.maxEdgeSize==b.p.minEdgeSize?
0:(b.p.maxEdgeSize-b.p.minEdgeSize)/g,E=b.p.minEdgeSize):(A=1,E=0);c&&b.nodes.forEach(function(c){c.displaySize=c.size*w+u;c.fixed||(c.displayX=(c.x-(h+k)/2)*q+a/2,c.displayY=(c.y-(n+m)/2)*q+d/2)});f&&b.edges.forEach(function(a){a.displaySize=a.size*A+E});return b};this.translate=function(a,d,c,f,g){var p=Math.pow(c,b.p.nodesPowRatio);f&&b.nodes.forEach(function(b){b.fixed||(b.displayX=b.displayX*c+a,b.displayY=b.displayY*c+d);b.displaySize*=p});g&&b.edges.forEach(function(a){a.displaySize*=Math.pow(c,
b.p.edgesPowRatio);a.arrowDisplaySize=a.displaySize*b.p.arrowRatio*p});return b};this.setBorders=function(){b.borders={};b.nodes.forEach(function(a){b.borders.minX=Math.min(void 0==b.borders.minX?a.displayX-a.displaySize:b.borders.minX,a.displayX-a.displaySize);b.borders.maxX=Math.max(void 0==b.borders.maxX?a.displayX+a.displaySize:b.borders.maxX,a.displayX+a.displaySize);b.borders.minY=Math.min(void 0==b.borders.minY?a.displayY-a.displaySize:b.borders.minY,a.displayY-a.displaySize);b.borders.maxY=
Math.max(void 0==b.borders.maxY?a.displayY-a.displaySize:b.borders.maxY,a.displayY-a.displaySize)})};this.checkHover=function(a,d){var c,f,g,p=[],k=[];b.nodes.forEach(function(b){if(b.hidden)b.hover=!1;else{c=Math.abs(b.displayX-a);f=Math.abs(b.displayY-d);g=b.displaySize;var m=b.hover,n=c<g&&f<g&&Math.sqrt(c*c+f*f)<g;m&&!n?(b.hover=!1,k.push(b.id)):n&&!m&&(b.hover=!0,p.push(b.id))}});p.length&&b.dispatch("overnodes",p);k.length&&b.dispatch("outnodes",k);return b}}var t=0,C={plugins:[]};sigma.init=
function(d){d=new k(d,(++t).toString());sigma.instances[t]=new n(d);return sigma.instances[t]};sigma.debugMode=0;sigma.log=function(){if(1==sigma.debugMode)for(var d in arguments)console.log(arguments[d]);else if(1<sigma.debugMode)for(d in arguments)throw Error(arguments[d]);return sigma};sigma.tools.drawRoundRect=function(d,g,f,b,a,l,c){l=l?l:0;var k=c?c:[],k="string"==typeof k?k.split(" "):k;c=l&&(0<=k.indexOf("topleft")||0<=k.indexOf("top")||0<=k.indexOf("left"));var m=l&&(0<=k.indexOf("topright")||
0<=k.indexOf("top")||0<=k.indexOf("right")),n=l&&(0<=k.indexOf("bottomleft")||0<=k.indexOf("bottom")||0<=k.indexOf("left")),k=l&&(0<=k.indexOf("bottomright")||0<=k.indexOf("bottom")||0<=k.indexOf("right"));d.moveTo(g,f+l);c?d.arcTo(g,f,g+l,f,l):d.lineTo(g,f);m?(d.lineTo(g+b-l,f),d.arcTo(g+b,f,g+b,f+l,l)):d.lineTo(g+b,f);k?(d.lineTo(g+b,f+a-l),d.arcTo(g+b,f+a,g+b-l,f+a,l)):d.lineTo(g+b,f+a);n?(d.lineTo(g+l,f+a),d.arcTo(g,f+a,g,f+a-l,l)):d.lineTo(g,f+a);d.lineTo(g,f+l)};sigma.tools.drawArrowhead=function(d,
g,f,b,a){d.beginPath();d.moveTo(g,f);var k=g+Math.cos(0.017453292519943295*(22+a))*b,c=f+Math.sin(0.017453292519943295*(22+a))*b,m=g+Math.cos(0.017453292519943295*(a-22))*b;b=f+Math.sin(0.017453292519943295*(a-22))*b;d.lineTo(k,c);d.quadraticCurveTo((g+k+m)/3,(f+c+b)/3,m,b);d.lineTo(g,f);d.fill()};sigma.tools.getRGB=function(d,g){d=d.toString();var f={r:0,g:0,b:0};if(3<=d.length&&"#"==d.charAt(0)){var b=d.length-1;6==b?f={r:parseInt(d.charAt(1)+d.charAt(2),16),g:parseInt(d.charAt(3)+d.charAt(4),16),
b:parseInt(d.charAt(5)+d.charAt(5),16)}:3==b&&(f={r:parseInt(d.charAt(1)+d.charAt(1),16),g:parseInt(d.charAt(2)+d.charAt(2),16),b:parseInt(d.charAt(3)+d.charAt(3),16)})}g&&(f=[f.r,f.g,f.b]);return f};sigma.tools.rgbToHex=function(d,g,f){return sigma.tools.toHex(d)+sigma.tools.toHex(g)+sigma.tools.toHex(f)};sigma.tools.toHex=function(d){d=parseInt(d,10);if(isNaN(d))return"00";d=Math.max(0,Math.min(d,255));return"0123456789ABCDEF".charAt((d-d%16)/16)+"0123456789ABCDEF".charAt(d%16)};sigma.tools.getIncidenceAngle=
function(d,g,f,b){return(d<=f?180:0)+180*Math.atan((b-g)/(f-d))/Math.PI};sigma.chronos=new function(){function d(a){window.setTimeout(a,0);return r}function g(){for(r.dispatch("frameinserted");q&&x.length&&f(););q&&x.length?(A=(new Date).getTime(),u++,E=w-B,F=B-E,r.dispatch("insertframe"),d(g)):a()}function f(){D%=x.length;if(!x[D].task()){var a=x[D].taskName;z=z.filter(function(b){b.taskParent==a&&x.push({taskName:b.taskName,task:b.task});return b.taskParent!=a});r.dispatch("killed",x.splice(D--,
1)[0])}D++;w=(new Date).getTime()-A;return w<=F}function b(){q=!0;u=D=0;C=A=(new Date).getTime();r.dispatch("start");r.dispatch("insertframe");d(g);return r}function a(){r.dispatch("stop");q=!1;return r}function k(a,c,d){if("function"!=typeof a)throw Error('Task "'+c+'" is not a function');x.push({taskName:c,task:a});q=!!(q||d&&b()||1);return r}function c(a){return a?Object.keys(v).filter(function(a){return!!v[a].on}).length:Object.keys(v).length}function m(){Object.keys(v).length?(r.dispatch("startgenerators"),
r.unbind("killed",n),d(function(){for(var a in v)v[a].on=!0,k(v[a].task,a,!1)}),r.bind("killed",n).runTasks()):r.dispatch("stopgenerators");return r}function n(a){void 0!=v[a.content.taskName]&&(v[a.content.taskName].del||!v[a.content.taskName].condition()?delete v[a.content.taskName]:v[a.content.taskName].on=!1,0==c(!0)&&m())}sigma.classes.EventDispatcher.call(this);var r=this,q=!1,h=80,t=0,u=0,B=1E3/h,F=B,w=0,C=0,A=0,E=0,v={},x=[],z=[],D=0;this.frequency=function(a){return void 0!=a?(h=Math.abs(1*
a),B=1E3/h,u=0,r):h};this.runTasks=b;this.stopTasks=a;this.insertFrame=d;this.addTask=k;this.queueTask=function(a,b,c){if("function"!=typeof a)throw Error('Task "'+b+'" is not a function');if(!x.concat(z).some(function(a){return a.taskName==c}))throw Error('Parent task "'+c+'" of "'+b+'" is not attached.');z.push({taskParent:c,taskName:b,task:a});return r};this.removeTask=function(b,c){if(void 0==b)x=[],1==c?z=[]:2==c&&(x=z,z=[]),a();else{var d="string"==typeof b?b:"";x=x.filter(function(a){return("string"==
typeof b?a.taskName==b:a.task==b)?(d=a.taskName,!1):!0});0<c&&(z=z.filter(function(a){1==c&&a.taskParent==d&&x.push(a);return a.taskParent!=d}))}q=!!(!x.length||a()&&0);return r};this.addGenerator=function(a,b,d){if(void 0!=v[a])return r;v[a]={task:b,condition:d};0==c(!0)&&m();return r};this.removeGenerator=function(a){v[a]&&(v[a].on=!1,v[a].del=!0);return r};this.startGenerators=m;this.getGeneratorsIDs=function(){return Object.keys(v)};this.getFPS=function(){q&&(t=Math.round(1E4*(u/((new Date).getTime()-
C)))/10);return t};this.getTasksCount=function(){return x.length};this.getQueuedTasksCount=function(){return z.length};this.getExecutionTime=function(){return A-C};return this};sigma.addPlugin=function(d,g,f){n.prototype[d]=g;C.plugins.push(f)};sigma.easing={linear:{},quadratic:{}};sigma.easing.linear.easenone=function(d){return d};sigma.easing.quadratic.easein=function(d){return d*d};sigma.easing.quadratic.easeout=function(d){return-d*(d-2)};sigma.easing.quadratic.easeinout=function(d){return 1>
(d*=2)?0.5*d*d:-0.5*(--d*(d-2)-1)};sigma.publicPrototype=n.prototype})();
// Mathieu Jacomy @ Sciences Po Médialab & WebAtlas
// (requires sigma.js to be loaded)
sigma.publicPrototype.parseGexf = function(gexfPath) {
// Load XML file:
var gexfhttp, gexf;
var sigmaInstance = this;
gexfhttp = window.XMLHttpRequest ?
new XMLHttpRequest() :
new ActiveXObject('Microsoft.XMLHTTP');
gexfhttp.overrideMimeType('text/xml');
gexfhttp.open('GET', gexfPath, false);
gexfhttp.send();
gexf = gexfhttp.responseXML;
var viz='http://www.gexf.net/1.2draft/viz'; // Vis namespace
var i, j, k;
// Parse Attributes
// This is confusing, so I'll comment heavily
var nodesAttributes = []; // The list of attributes of the nodes of the graph that we build in json
var edgesAttributes = []; // The list of attributes of the edges of the graph that we build in json
var attributesNodes = gexf.getElementsByTagName('attributes'); // In the gexf (that is an xml), the list of xml nodes 'attributes' (note the plural 's')
for(i = 0; i<attributesNodes.length; i++){
var attributesNode = attributesNodes[i]; // attributesNode is each xml node 'attributes' (plural)
if(attributesNode.getAttribute('class') == 'node'){
var attributeNodes = attributesNode.getElementsByTagName('attribute'); // The list of xml nodes 'attribute' (no 's')
for(j = 0; j<attributeNodes.length; j++){
var attributeNode = attributeNodes[j]; // Each xml node 'attribute'
var id = attributeNode.getAttribute('id'),
title = attributeNode.getAttribute('title'),
type = attributeNode.getAttribute('type');
var attribute = {id:id, title:title, type:type};
nodesAttributes.push(attribute);
}
} else if(attributesNode.getAttribute('class') == 'edge'){
var attributeNodes = attributesNode.getElementsByTagName('attribute'); // The list of xml nodes 'attribute' (no 's')
for(j = 0; j<attributeNodes.length; j++){
var attributeNode = attributeNodes[j]; // Each xml node 'attribute'
var id = attributeNode.getAttribute('id'),
title = attributeNode.getAttribute('title'),
type = attributeNode.getAttribute('type');
var attribute = {id:id, title:title, type:type};
edgesAttributes.push(attribute);
}
}
}
var nodes = []; // The nodes of the graph
var nodesNodes = gexf.getElementsByTagName('nodes') // The list of xml nodes 'nodes' (plural)
for(i=0; i<nodesNodes.length; i++){
var nodesNode = nodesNodes[i]; // Each xml node 'nodes' (plural)
var nodeNodes = nodesNode.getElementsByTagName('node'); // The list of xml nodes 'node' (no 's')
for(j=0; j<nodeNodes.length; j++){
var nodeNode = nodeNodes[j]; // Each xml node 'node' (no 's')
window.NODE = nodeNode;
var id = nodeNode.getAttribute('id');
var label = nodeNode.getAttribute('label') || id;
//viz
var size = 1;
var x = 100 - 200*Math.random();
var y = 100 - 200*Math.random();
var color;
var poorBrowserXmlNsSupport = (nodeNode.getElementsByTagNameNS == null);
var sizeNodes = nodeNode.getElementsByTagName('size');
sizeNodes = sizeNodes.length || poorBrowserXmlNsSupport ?
sizeNodes :
nodeNode.getElementsByTagNameNS('*','size');
if(sizeNodes.length>0){
sizeNode = sizeNodes[0];
size = parseFloat(sizeNode.getAttribute('value'));
}
var positionNodes = nodeNode.getElementsByTagName('position');
positionNodes = positionNodes.length || poorBrowserXmlNsSupport ?
positionNodes :
nodeNode.getElementsByTagNameNS('*','position');
if(positionNodes.length>0){
var positionNode = positionNodes[0];
x = parseFloat(positionNode.getAttribute('x'));
y = parseFloat(positionNode.getAttribute('y'));
}
var colorNodes = nodeNode.getElementsByTagName('color');
colorNodes = colorNodes.length || poorBrowserXmlNsSupport ?
colorNodes :
nodeNode.getElementsByTagNameNS('*','color');
if(colorNodes.length>0){
colorNode = colorNodes[0];
color = '#'+sigma.tools.rgbToHex(parseFloat(colorNode.getAttribute('r')),
parseFloat(colorNode.getAttribute('g')),
parseFloat(colorNode.getAttribute('b')));
}
// Create Node
var node = {label:label, size:size, x:x, y:y, attributes:[], color:color}; // The graph node
// Attribute values
var attvalueNodes = nodeNode.getElementsByTagName('attvalue');
for(k=0; k<attvalueNodes.length; k++){
var attvalueNode = attvalueNodes[k];
var attr = attvalueNode.getAttribute('for');
var val = attvalueNode.getAttribute('value');
node.attributes.push({attr:attr, val:val});
}
sigmaInstance.addNode(id,node);
}
}
var edges = [];
var edgesNodes = gexf.getElementsByTagName('edges');
for(i=0; i<edgesNodes.length; i++){
var edgesNode = edgesNodes[i];
var edgeNodes = edgesNode.getElementsByTagName('edge');
for(j=0; j<edgeNodes.length; j++){
var edgeNode = edgeNodes[j];
var id = edgeNode.getAttribute('id');
var source = edgeNode.getAttribute('source');
var target = edgeNode.getAttribute('target');
var label = edgeNode.getAttribute('label');
var edge = {
id: id,
sourceID: source,
targetID: target,
label: label,
attributes: []
};
var weight = edgeNode.getAttribute('weight');
if(weight!=undefined){
edge['weight'] = weight;
edge['size'] = weight;
}
var attvalueNodes = edgeNode.getElementsByTagName('attvalue');
for(k=0; k<attvalueNodes.length; k++){
var attvalueNode = attvalueNodes[k];
var attr = attvalueNode.getAttribute('for');
var val = attvalueNode.getAttribute('value');
edge.attributes.push({attr:attr, val:val});
}
sigmaInstance.addEdge(id,source,target,edge);
}
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment