;(function(undefined) {
|
|
'use strict';
|
|
|
|
if (typeof sigma === 'undefined')
|
|
throw 'sigma is not declared';
|
|
|
|
if (typeof conrad === 'undefined')
|
|
throw 'conrad is not declared';
|
|
|
|
// Initialize packages:
|
|
sigma.utils.pkg('sigma.renderers');
|
|
|
|
/**
|
|
* This function is the constructor of the svg sigma's renderer.
|
|
*
|
|
* @param {sigma.classes.graph} graph The graph to render.
|
|
* @param {sigma.classes.camera} camera The camera.
|
|
* @param {configurable} settings The sigma instance settings
|
|
* function.
|
|
* @param {object} object The options object.
|
|
* @return {sigma.renderers.svg} The renderer instance.
|
|
*/
|
|
sigma.renderers.svg = function(graph, camera, settings, options) {
|
|
if (typeof options !== 'object')
|
|
throw 'sigma.renderers.svg: Wrong arguments.';
|
|
|
|
if (!(options.container instanceof HTMLElement))
|
|
throw 'Container not found.';
|
|
|
|
var i,
|
|
l,
|
|
a,
|
|
fn,
|
|
self = this;
|
|
|
|
sigma.classes.dispatcher.extend(this);
|
|
|
|
// Initialize main attributes:
|
|
this.graph = graph;
|
|
this.camera = camera;
|
|
this.domElements = {
|
|
graph: null,
|
|
groups: {},
|
|
nodes: {},
|
|
edges: {},
|
|
labels: {},
|
|
hovers: {}
|
|
};
|
|
this.measurementCanvas = null;
|
|
this.options = options;
|
|
this.container = this.options.container;
|
|
this.settings = (
|
|
typeof options.settings === 'object' &&
|
|
options.settings
|
|
) ?
|
|
settings.embedObjects(options.settings) :
|
|
settings;
|
|
|
|
// Is the renderer meant to be freestyle?
|
|
this.settings('freeStyle', !!this.options.freeStyle);
|
|
|
|
// SVG xmlns
|
|
this.settings('xmlns', 'http://www.w3.org/2000/svg');
|
|
|
|
// Indexes:
|
|
this.nodesOnScreen = [];
|
|
this.edgesOnScreen = [];
|
|
|
|
// Find the prefix:
|
|
this.options.prefix = 'renderer' + sigma.utils.id() + ':';
|
|
|
|
// Initialize the DOM elements
|
|
this.initDOM('svg');
|
|
|
|
// Initialize captors:
|
|
this.captors = [];
|
|
a = this.options.captors || [sigma.captors.mouse, sigma.captors.touch];
|
|
for (i = 0, l = a.length; i < l; i++) {
|
|
fn = typeof a[i] === 'function' ? a[i] : sigma.captors[a[i]];
|
|
this.captors.push(
|
|
new fn(
|
|
this.domElements.graph,
|
|
this.camera,
|
|
this.settings
|
|
)
|
|
);
|
|
}
|
|
|
|
// Bind resize:
|
|
window.addEventListener('resize', function() {
|
|
self.resize();
|
|
});
|
|
|
|
// Deal with sigma events:
|
|
// TODO: keep an option to override the DOM events?
|
|
sigma.misc.bindDOMEvents.call(this, this.domElements.graph);
|
|
this.bindHovers(this.options.prefix);
|
|
|
|
// Resize
|
|
this.resize(false);
|
|
};
|
|
|
|
/**
|
|
* This method renders the graph on the svg scene.
|
|
*
|
|
* @param {?object} options Eventually an object of options.
|
|
* @return {sigma.renderers.svg} Returns the instance itself.
|
|
*/
|
|
sigma.renderers.svg.prototype.render = function(options) {
|
|
options = options || {};
|
|
|
|
var a,
|
|
i,
|
|
k,
|
|
e,
|
|
l,
|
|
o,
|
|
source,
|
|
target,
|
|
start,
|
|
edges,
|
|
renderers,
|
|
subrenderers,
|
|
index = {},
|
|
graph = this.graph,
|
|
nodes = this.graph.nodes,
|
|
prefix = this.options.prefix || '',
|
|
drawEdges = this.settings(options, 'drawEdges'),
|
|
drawNodes = this.settings(options, 'drawNodes'),
|
|
drawLabels = this.settings(options, 'drawLabels'),
|
|
embedSettings = this.settings.embedObjects(options, {
|
|
prefix: this.options.prefix,
|
|
forceLabels: this.options.forceLabels
|
|
});
|
|
|
|
// Check the 'hideEdgesOnMove' setting:
|
|
if (this.settings(options, 'hideEdgesOnMove'))
|
|
if (this.camera.isAnimated || this.camera.isMoving)
|
|
drawEdges = false;
|
|
|
|
// Apply the camera's view:
|
|
this.camera.applyView(
|
|
undefined,
|
|
this.options.prefix,
|
|
{
|
|
width: this.width,
|
|
height: this.height
|
|
}
|
|
);
|
|
|
|
// Hiding everything
|
|
// TODO: find a more sensible way to perform this operation
|
|
this.hideDOMElements(this.domElements.nodes);
|
|
this.hideDOMElements(this.domElements.edges);
|
|
this.hideDOMElements(this.domElements.labels);
|
|
|
|
// Find which nodes are on screen
|
|
this.edgesOnScreen = [];
|
|
this.nodesOnScreen = this.camera.quadtree.area(
|
|
this.camera.getRectangle(this.width, this.height)
|
|
);
|
|
|
|
// Node index
|
|
for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++)
|
|
index[a[i].id] = a[i];
|
|
|
|
// Find which edges are on screen
|
|
for (a = graph.edges(), i = 0, l = a.length; i < l; i++) {
|
|
o = a[i];
|
|
if (
|
|
(index[o.source] || index[o.target]) &&
|
|
(!o.hidden && !nodes(o.source).hidden && !nodes(o.target).hidden)
|
|
)
|
|
this.edgesOnScreen.push(o);
|
|
}
|
|
|
|
// Display nodes
|
|
//---------------
|
|
renderers = sigma.svg.nodes;
|
|
subrenderers = sigma.svg.labels;
|
|
|
|
//-- First we create the nodes which are not already created
|
|
if (drawNodes)
|
|
for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++) {
|
|
if (!a[i].hidden && !this.domElements.nodes[a[i].id]) {
|
|
|
|
// Node
|
|
e = (renderers[a[i].type] || renderers.def).create(
|
|
a[i],
|
|
embedSettings
|
|
);
|
|
|
|
this.domElements.nodes[a[i].id] = e;
|
|
this.domElements.groups.nodes.appendChild(e);
|
|
|
|
// Label
|
|
e = (subrenderers[a[i].type] || subrenderers.def).create(
|
|
a[i],
|
|
embedSettings
|
|
);
|
|
|
|
this.domElements.labels[a[i].id] = e;
|
|
this.domElements.groups.labels.appendChild(e);
|
|
}
|
|
}
|
|
|
|
//-- Second we update the nodes
|
|
if (drawNodes)
|
|
for (a = this.nodesOnScreen, i = 0, l = a.length; i < l; i++) {
|
|
|
|
if (a[i].hidden)
|
|
continue;
|
|
|
|
// Node
|
|
(renderers[a[i].type] || renderers.def).update(
|
|
a[i],
|
|
this.domElements.nodes[a[i].id],
|
|
embedSettings
|
|
);
|
|
|
|
// Label
|
|
(subrenderers[a[i].type] || subrenderers.def).update(
|
|
a[i],
|
|
this.domElements.labels[a[i].id],
|
|
embedSettings
|
|
);
|
|
}
|
|
|
|
// Display edges
|
|
//---------------
|
|
renderers = sigma.svg.edges;
|
|
|
|
//-- First we create the edges which are not already created
|
|
if (drawEdges)
|
|
for (a = this.edgesOnScreen, i = 0, l = a.length; i < l; i++) {
|
|
if (!this.domElements.edges[a[i].id]) {
|
|
source = nodes(a[i].source);
|
|
target = nodes(a[i].target);
|
|
|
|
e = (renderers[a[i].type] || renderers.def).create(
|
|
a[i],
|
|
source,
|
|
target,
|
|
embedSettings
|
|
);
|
|
|
|
this.domElements.edges[a[i].id] = e;
|
|
this.domElements.groups.edges.appendChild(e);
|
|
}
|
|
}
|
|
|
|
//-- Second we update the edges
|
|
if (drawEdges)
|
|
for (a = this.edgesOnScreen, i = 0, l = a.length; i < l; i++) {
|
|
source = nodes(a[i].source);
|
|
target = nodes(a[i].target);
|
|
|
|
(renderers[a[i].type] || renderers.def).update(
|
|
a[i],
|
|
this.domElements.edges[a[i].id],
|
|
source,
|
|
target,
|
|
embedSettings
|
|
);
|
|
}
|
|
|
|
this.dispatchEvent('render');
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* This method creates a DOM element of the specified type, switches its
|
|
* position to "absolute", references it to the domElements attribute, and
|
|
* finally appends it to the container.
|
|
*
|
|
* @param {string} tag The label tag.
|
|
* @param {string} id The id of the element (to store it in "domElements").
|
|
*/
|
|
sigma.renderers.svg.prototype.initDOM = function(tag) {
|
|
var dom = document.createElementNS(this.settings('xmlns'), tag),
|
|
c = this.settings('classPrefix'),
|
|
g,
|
|
l,
|
|
i;
|
|
|
|
dom.style.position = 'absolute';
|
|
dom.setAttribute('class', c + '-svg');
|
|
|
|
// Setting SVG namespace
|
|
dom.setAttribute('xmlns', this.settings('xmlns'));
|
|
dom.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
|
|
dom.setAttribute('version', '1.1');
|
|
|
|
// Creating the measurement canvas
|
|
var canvas = document.createElement('canvas');
|
|
canvas.setAttribute('class', c + '-measurement-canvas');
|
|
|
|
// Appending elements
|
|
this.domElements.graph = this.container.appendChild(dom);
|
|
|
|
// Creating groups
|
|
var groups = ['edges', 'nodes', 'labels', 'hovers'];
|
|
for (i = 0, l = groups.length; i < l; i++) {
|
|
g = document.createElementNS(this.settings('xmlns'), 'g');
|
|
|
|
g.setAttributeNS(null, 'id', c + '-group-' + groups[i]);
|
|
g.setAttributeNS(null, 'class', c + '-group');
|
|
|
|
this.domElements.groups[groups[i]] =
|
|
this.domElements.graph.appendChild(g);
|
|
}
|
|
|
|
// Appending measurement canvas
|
|
this.container.appendChild(canvas);
|
|
this.measurementCanvas = canvas.getContext('2d');
|
|
};
|
|
|
|
/**
|
|
* This method hides a batch of SVG DOM elements.
|
|
*
|
|
* @param {array} elements An array of elements to hide.
|
|
* @param {object} renderer The renderer to use.
|
|
* @return {sigma.renderers.svg} Returns the instance itself.
|
|
*/
|
|
sigma.renderers.svg.prototype.hideDOMElements = function(elements) {
|
|
var o,
|
|
i;
|
|
|
|
for (i in elements) {
|
|
o = elements[i];
|
|
sigma.svg.utils.hide(o);
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
/**
|
|
* This method binds the hover events to the renderer.
|
|
*
|
|
* @param {string} prefix The renderer prefix.
|
|
*/
|
|
// TODO: add option about whether to display hovers or not
|
|
sigma.renderers.svg.prototype.bindHovers = function(prefix) {
|
|
var renderers = sigma.svg.hovers,
|
|
self = this,
|
|
hoveredNode;
|
|
|
|
function overNode(e) {
|
|
var node = e.data.node,
|
|
embedSettings = self.settings.embedObjects({
|
|
prefix: prefix
|
|
});
|
|
|
|
if (!embedSettings('enableHovering'))
|
|
return;
|
|
|
|
var hover = (renderers[node.type] || renderers.def).create(
|
|
node,
|
|
self.domElements.nodes[node.id],
|
|
self.measurementCanvas,
|
|
embedSettings
|
|
);
|
|
|
|
self.domElements.hovers[node.id] = hover;
|
|
|
|
// Inserting the hover in the dom
|
|
self.domElements.groups.hovers.appendChild(hover);
|
|
hoveredNode = node;
|
|
}
|
|
|
|
function outNode(e) {
|
|
var node = e.data.node,
|
|
embedSettings = self.settings.embedObjects({
|
|
prefix: prefix
|
|
});
|
|
|
|
if (!embedSettings('enableHovering'))
|
|
return;
|
|
|
|
// Deleting element
|
|
self.domElements.groups.hovers.removeChild(
|
|
self.domElements.hovers[node.id]
|
|
);
|
|
hoveredNode = null;
|
|
delete self.domElements.hovers[node.id];
|
|
|
|
// Reinstate
|
|
self.domElements.groups.nodes.appendChild(
|
|
self.domElements.nodes[node.id]
|
|
);
|
|
}
|
|
|
|
// OPTIMIZE: perform a real update rather than a deletion
|
|
function update() {
|
|
if (!hoveredNode)
|
|
return;
|
|
|
|
var embedSettings = self.settings.embedObjects({
|
|
prefix: prefix
|
|
});
|
|
|
|
// Deleting element before update
|
|
self.domElements.groups.hovers.removeChild(
|
|
self.domElements.hovers[hoveredNode.id]
|
|
);
|
|
delete self.domElements.hovers[hoveredNode.id];
|
|
|
|
var hover = (renderers[hoveredNode.type] || renderers.def).create(
|
|
hoveredNode,
|
|
self.domElements.nodes[hoveredNode.id],
|
|
self.measurementCanvas,
|
|
embedSettings
|
|
);
|
|
|
|
self.domElements.hovers[hoveredNode.id] = hover;
|
|
|
|
// Inserting the hover in the dom
|
|
self.domElements.groups.hovers.appendChild(hover);
|
|
}
|
|
|
|
// Binding events
|
|
this.bind('overNode', overNode);
|
|
this.bind('outNode', outNode);
|
|
|
|
// Update on render
|
|
this.bind('render', update);
|
|
};
|
|
|
|
/**
|
|
* This method resizes each DOM elements in the container and stores the new
|
|
* dimensions. Then, it renders the graph.
|
|
*
|
|
* @param {?number} width The new width of the container.
|
|
* @param {?number} height The new height of the container.
|
|
* @return {sigma.renderers.svg} Returns the instance itself.
|
|
*/
|
|
sigma.renderers.svg.prototype.resize = function(w, h) {
|
|
var oldWidth = this.width,
|
|
oldHeight = this.height,
|
|
pixelRatio = 1;
|
|
|
|
if (w !== undefined && h !== undefined) {
|
|
this.width = w;
|
|
this.height = h;
|
|
} else {
|
|
this.width = this.container.offsetWidth;
|
|
this.height = this.container.offsetHeight;
|
|
|
|
w = this.width;
|
|
h = this.height;
|
|
}
|
|
|
|
if (oldWidth !== this.width || oldHeight !== this.height) {
|
|
this.domElements.graph.style.width = w + 'px';
|
|
this.domElements.graph.style.height = h + 'px';
|
|
|
|
if (this.domElements.graph.tagName.toLowerCase() === 'svg') {
|
|
this.domElements.graph.setAttribute('width', (w * pixelRatio));
|
|
this.domElements.graph.setAttribute('height', (h * pixelRatio));
|
|
}
|
|
}
|
|
|
|
return this;
|
|
};
|
|
|
|
|
|
/**
|
|
* The labels, nodes and edges renderers are stored in the three following
|
|
* objects. When an element is drawn, its type will be checked and if a
|
|
* renderer with the same name exists, it will be used. If not found, the
|
|
* default renderer will be used instead.
|
|
*
|
|
* They are stored in different files, in the "./svg" folder.
|
|
*/
|
|
sigma.utils.pkg('sigma.svg.nodes');
|
|
sigma.utils.pkg('sigma.svg.edges');
|
|
sigma.utils.pkg('sigma.svg.labels');
|
|
}).call(this);
|