By pulling in data from these sheets, I used sigma.js to build a graph of relationships.
Zoom in using mouse wheel.
Show only kilgore by appending "#kilgore" to end of URL.
Everything
By pulling in data from these sheets, I used sigma.js to build a graph of relationships.
Zoom in using mouse wheel.
Show only kilgore by appending "#kilgore" to end of URL.
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') | |
}; | |
function limit_to_kilgore(str) { | |
if (window.location.hash !== '#kilgore') { | |
return true; | |
} else { | |
return (str.indexOf('kilgoreuniverse: TRUE') > -1); | |
} | |
} | |
// 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) { | |
if (limit_to_kilgore(val.content.$t)) { | |
sigInst.addNode(val.title.$t, { | |
label: val.content.$t, | |
color: 'blue', | |
x: Math.random()*10, | |
y: Math.random()*10, | |
size: 0.1, | |
cluster: val.content.$t.match(/tagtype: (\w*)/)[1] | |
}); | |
} | |
}); | |
sheets.edges.feed.entry.forEach(function(val, idx) { | |
if (limit_to_kilgore(val.content.$t)) { | |
try { | |
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); | |
} catch (e) { | |
console.error('wtf:'); | |
console.error(e); | |
} | |
} | |
}); | |
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); | |
} | |
} | |
}; |