/**
* @module "biojs-dag-viewer"
* @version 0.0.1
* @author Alex Kalderimis
* @license LGPL
*/
(function (module) {
/**
* @lends Biojs.DAGViewer
**/
module.DAGViewer = DAGViewer;
// A plain alpha-numeric string with underscores and hyphens.
var ID_RE = /^[a-z0-9_-]+$/i;
/**
* DAGViewer - Convenient Display of Directed Acyclic Graphs
* ============================================================
*
* A component for displaying directed acyclic graphs (DAGs)
* in a visually clear manner.
*
* @constructor
* @class
*
* @requires Dagify
* @requires Foundation 5
* @requires jQuery 1.9.1
* @requires jQuery-UI 1.10.3
*
* @dependency
* @dependency
* @dependency
* @dependency
* @dependency
* @dependency
* @dependency
* @dependency
*
* @option {string|element|jQuery} target
* A reference to the element into which the component should be rendered.
* @option {String[]} [nodeLabels=["name", "value", "label", "class"]]
* The properties of each node object that should be read for a label to display, in order of priority.
* @option {String[]} [edgeLabels=["name", "value", "label", "class"]]
* The properties of each edge object that should be read for a label to display, in order of priority.
* @option {Number[]} [rankScale=[1,1]]
* The opacity gradient to apply to each node in the graph, from leaf to highest level. As opacity levels,
* each value must be within the range 0 <= x <= 1.
* @option {function} [isClosable=function (node) { return true; }]
* A handler to determine if a node is closable.
* @option {String} [termTemplate="<%- name %> (<%- identifier %>)"]
* A string to render a node in the filter section of the controls.
* @option {function} [termKey=function (t) { return t.identifier }]
* Return the integration key for nodes.
* @option {function} [getEnds]
* Return the source and target of an edge. The default implementation is to
* call the current nodeKey function on the source and target properties (also
* configurable) of the edge.
* @option {function} [edgeKey]
* A function that returns a unique identifier for each edge.
* @option {function} [nodeKey]
* A function that returns a unique identifier for each node.
* @option {function} [getNodeClass]
* A function that returns a string to be used as a class name for each node.
*/
function DAGViewer (options) {
var element, w;
if (!options) {
throw new Error("No options provided");
}
element = resolveElement(options.target);
if (!element) {
throw new Error(options.target + " not found");
}
this._cbs = {};
this._globalListeners = [];
this._widget = w = new DAGWidget(options);
w.setElement(element);
w.render();
// Proxy all events.
w.on('all', this.fireEvent.bind(this));
}
DAGViewer.prototype = {
constructor: DAGViewer
,eventTypes: [
/**
* Called when a node in the Graph has been clicked on.
* @event
* @name Biojs.InterMineTable#click:node
* @param {function} callback The function to call.
* @eventData {String} id The id of the node.
* @eventData {Backbone.Model} node The model representing the node.
*
* @example
* table.addListener('click:node', function (id, node) {
* alert(node.get('name'));
* });
*/
'click:node',
/**
* Called when an edge in the Graph has been clicked on.
* @event
* @name Biojs.InterMineTable#click:edge
* @param {function} callback The function to call.
* @eventData {Graph} graph The current graph.
* @eventData {String} edgeId The id of the current edge.
*
* @example
* table.addListener('click:edge', function (graph, edge) {
* var src = graph.node(edge.get('source'));
* var tgt = graph.node(edge.get('target'));
* alert(src.get('name') + ' are ' + tgt.get('name'));
* });
*/
'click:edge'
]
/**
* Set the graph model that this viewer presents.
*
* This method sets this graph as the current graph, and triggers
* the viewer to render the data. The graph should be an object
* with "nodes" and "edges" properties, which must be arrays of
* objects. The precise interface of the node and edge objects
* is flexible - see the constructor options above.
*
* @param {Object} graph The graph to view.
*
* @example
* var graph = {
* nodes: [
* {id: 1, name: "Animals"},
* {id: 2, name: "Fish"},
* {id: 3, name: "Reptiles"},
* {id: 4, name: "Dinosaurs"},
* {id: 5, name: "Mammals"},
* {id: 6, name: "Birds"}
* ],
* edges: [
* {source: 2, target: 1},
* {source: 3, target: 2},
* {source: 4, target: 3},
* {source: 5, target: 3},
* {source: 6, target: 4}
* ]
* };
* viewer.setGraph(graph);
*
*/
,setGraph: function (graph) {
this._widget.setGraph(graph);
}
/**
* Add a event handler for the named event.
*
* @param {string} eventName
* The name of the event to handle.
* @param {function} callback
* The handler to call when events of this name are received.
*
* @example
* viewer.addListener('event:happened', function(why, when) {
* alert("An event happened at " + when + " because " + why);
* });
*/
,addListener: function (eventName, callback) {
if (!(eventName && callback)) {
throw new Error("Both eventName and callback are required arguments");
}
var cbs = this._cbs;
var cbsForThisEvent = (cbs[eventName] || (cbs[eventName] = []));
cbsForThisEvent.push(callback);
}
/**
* Add a global event listener.
*
* @param {function} callback
* The handler to call for all events.
*
* @example
* viewer.addGlobalListener(function (evt) {
* alert("Caught event: " + evt);
* }
*/
,addGlobalListener: function (callback) {
if (!(callback)) {
throw new Error("callback is a required argument");
}
this._globalListeners.push(callback);
}
/**
* Remove this component from the DOM and release all its resources.
*
* This method should always be called if the component is to be disposed of,
* or else event handler callbacks will likely result in memory leaks.
*
* @example
* viewer.destroy();
*
*/
,destroy: function () {
this._cbs = {};
this._globalListeners = [];
this._widget.remove();
this._widget = null;
this.options = null;
}
/**
* Fire an event with the given name and event data.
*
* @private
* @param {string} eventName
* The name of the event to trigger.
* @param {Object...} args
* The event arguments to send to handlers.
*
* @example
* viewer.fireEvent('event:happened', 'because reasons', new Date());
*
*/
,fireEvent: function (eventName) {
if (!eventName) {
throw new Error("Not enough arguments - at least one is required.");
}
var args = [].slice.call(arguments, 1)
, cbs = this._cbs
, cbsForThisEvent = (cbs[eventName] || (cbs[eventName] = [])).slice()
, globalListeners = this._globalListeners.slice()
, cb;
while (cb = globalListeners.shift()) {
cb.apply(null, arguments);
}
while (cb = cbsForThisEvent.shift()) {
cb.apply(null, args);
}
}
};
/**
* Resolve a target parameter to an element.
* @private
*/
function resolveElement (target) {
if (typeof target === 'string') {
if (ID_RE.test(target)) {
return document.getElementById(target);
} else {
return download.querySelector(target);
}
} else if (target && target.appendChild) {
return target; // It is an element
} else if (target && target.append && target.length) {
// A jqueryish thing.
if (target.length > 1) {
throw new Error(target + " refers to more than one element");
}
return target[0];
}
throw new Error("Could not interpret '" + target + "' as an element");
}
})(window.Biojs || (window.Biojs = {}), window.jQuery || window.$);