1
0
Fork 0
This commit is contained in:
hkernbach 2016-07-19 15:06:44 +02:00
parent a0ff5e9dc3
commit 604637abdd
13 changed files with 1337 additions and 87 deletions

View File

@ -61,8 +61,10 @@
"frontend/js/lib/sigma.min.js",
"frontend/js/lib/sigma.plugins.animate.js",
"frontend/js/lib/sigma.plugins.dragNodes.js",
"frontend/js/lib/sigma.layout.noverlap.js",
"frontend/js/lib/sigma.plugins.fullScreen.js",
"frontend/js/lib/sigma.plugins.filter.js",
"frontend/js/lib/sigma.plugins.lasso.js",
"frontend/js/lib/sigma.layout.noverlap.js",
"frontend/js/lib/sigma.layout.fruchtermanReingold.js",
"frontend/js/lib/sigma.exporters.svg.js",
"frontend/js/lib/sigma.canvas.edges.labels.curve.js",

View File

@ -92,7 +92,7 @@ window.ArangoDocument = Backbone.Collection.extend({
}
},
error: function (data) {
callback(true, data._id);
callback(true, data._id, data.responseJSON);
}
});
},

View File

@ -0,0 +1,504 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
// Initialize package:
sigma.utils.pkg('sigma.plugins');
// Add custom graph methods:
/**
* This methods returns an array of nodes that are adjacent to a node.
*
* @param {string} id The node id.
* @return {array} The array of adjacent nodes.
*/
if (!sigma.classes.graph.hasMethod('adjacentNodes'))
sigma.classes.graph.addMethod('adjacentNodes', function(id) {
if (typeof id !== 'string')
throw 'adjacentNodes: the node id must be a string.';
var target,
nodes = [];
for(target in this.allNeighborsIndex[id]) {
nodes.push(this.nodesIndex[target]);
}
return nodes;
});
/**
* This methods returns an array of edges that are adjacent to a node.
*
* @param {string} id The node id.
* @return {array} The array of adjacent edges.
*/
if (!sigma.classes.graph.hasMethod('adjacentEdges'))
sigma.classes.graph.addMethod('adjacentEdges', function(id) {
if (typeof id !== 'string')
throw 'adjacentEdges: the node id must be a string.';
var a = this.allNeighborsIndex[id],
eid,
target,
edges = [];
for(target in a) {
for(eid in a[target]) {
edges.push(a[target][eid]);
}
}
return edges;
});
/**
* Sigma Filter
* =============================
*
* @author Sébastien Heymann <seb@linkurio.us> (Linkurious)
* @version 0.1
*/
var _g = undefined,
_s = undefined,
_chain = [], // chain of wrapped filters
_keysIndex = Object.create(null),
Processors = {}; // available predicate processors
/**
* Library of processors
* ------------------
*/
/**
*
* @param {function} fn The predicate.
*/
Processors.nodes = function nodes(fn) {
var n = _g.nodes(),
ln = n.length,
e = _g.edges(),
le = e.length;
// hide node, or keep former value
while(ln--)
n[ln].hidden = !fn.call(_g, n[ln]) || n[ln].hidden;
while(le--)
if (_g.nodes(e[le].source).hidden || _g.nodes(e[le].target).hidden)
e[le].hidden = true;
};
/**
*
* @param {function} fn The predicate.
*/
Processors.edges = function edges(fn) {
var e = _g.edges(),
le = e.length;
// hide edge, or keep former value
while(le--)
e[le].hidden = !fn.call(_g, e[le]) || e[le].hidden;
};
/**
*
* @param {string} id The center node.
*/
Processors.neighbors = function neighbors(id) {
var n = _g.nodes(),
ln = n.length,
e = _g.edges(),
le = e.length,
neighbors = _g.adjacentNodes(id),
nn = neighbors.length,
no = {};
while(nn--)
no[neighbors[nn].id] = true;
while(ln--)
if (n[ln].id !== id && !(n[ln].id in no))
n[ln].hidden = true;
while(le--)
if (_g.nodes(e[le].source).hidden || _g.nodes(e[le].target).hidden)
e[le].hidden = true;
};
/**
* This function adds a filter to the chain of filters.
*
* @param {function} fn The filter (i.e. predicate processor).
* @param {function} p The predicate.
* @param {?string} key The key to identify the filter.
*/
function register(fn, p, key) {
if (key != undefined && typeof key !== 'string')
throw 'The filter key "'+ key.toString() +'" must be a string.';
if (key != undefined && !key.length)
throw 'The filter key must be a non-empty string.';
if (typeof fn !== 'function')
throw 'The predicate of key "'+ key +'" must be a function.';
if ('undo' === key)
throw '"undo" is a reserved key.';
if (_keysIndex[key])
throw 'The filter "' + key + '" already exists.';
if (key)
_keysIndex[key] = true;
_chain.push({
'key': key,
'processor': fn,
'predicate': p
});
};
/**
* This function removes a set of filters from the chain.
*
* @param {object} o The filter keys.
*/
function unregister (o) {
_chain = _chain.filter(function(a) {
return !(a.key in o);
});
for(var key in o)
delete _keysIndex[key];
};
/**
* Filter Object
* ------------------
* @param {sigma} s The related sigma instance.
*/
function Filter(s) {
_s = s;
_g = s.graph;
};
/**
* This method is used to filter the nodes. The method must be called with
* the predicate, which is a function that takes a node as argument and
* returns a boolean. It may take an identifier as argument to undo the
* filter later. The method wraps the predicate into an anonymous function
* that looks through each node in the graph. When executed, the anonymous
* function hides the nodes that fail a truth test (predicate). The method
* adds the anonymous function to the chain of filters. The filter is not
* executed until the apply() method is called.
*
* > var filter = new sigma.plugins.filter(s);
* > filter.nodesBy(function(n) {
* > return this.degree(n.id) > 0;
* > }, 'degreeNotNull');
*
* @param {function} fn The filter predicate.
* @param {?string} key The key to identify the filter.
* @return {sigma.plugins.filter} Returns the instance.
*/
Filter.prototype.nodesBy = function(fn, key) {
// Wrap the predicate to be applied on the graph and add it to the chain.
register(Processors.nodes, fn, key);
return this;
};
/**
* This method is used to filter the edges. The method must be called with
* the predicate, which is a function that takes a node as argument and
* returns a boolean. It may take an identifier as argument to undo the
* filter later. The method wraps the predicate into an anonymous function
* that looks through each edge in the graph. When executed, the anonymous
* function hides the edges that fail a truth test (predicate). The method
* adds the anonymous function to the chain of filters. The filter is not
* executed until the apply() method is called.
*
* > var filter = new sigma.plugins.filter(s);
* > filter.edgesBy(function(e) {
* > return e.size > 1;
* > }, 'edgeSize');
*
* @param {function} fn The filter predicate.
* @param {?string} key The key to identify the filter.
* @return {sigma.plugins.filter} Returns the instance.
*/
Filter.prototype.edgesBy = function(fn, key) {
// Wrap the predicate to be applied on the graph and add it to the chain.
register(Processors.edges, fn, key);
return this;
};
/**
* This method is used to filter the nodes which are not direct connections
* of a given node. The method must be called with the node identifier. It
* may take an identifier as argument to undo the filter later. The filter
* is not executed until the apply() method is called.
*
* > var filter = new sigma.plugins.filter(s);
* > filter.neighborsOf('n0');
*
* @param {string} id The node id.
* @param {?string} key The key to identify the filter.
* @return {sigma.plugins.filter} Returns the instance.
*/
Filter.prototype.neighborsOf = function(id, key) {
if (typeof id !== 'string')
throw 'The node id "'+ id.toString() +'" must be a string.';
if (!id.length)
throw 'The node id must be a non-empty string.';
// Wrap the predicate to be applied on the graph and add it to the chain.
register(Processors.neighbors, id, key);
return this;
};
/**
* This method is used to execute the chain of filters and to refresh the
* display.
*
* > var filter = new sigma.plugins.filter(s);
* > filter
* > .nodesBy(function(n) {
* > return this.degree(n.id) > 0;
* > }, 'degreeNotNull')
* > .apply();
*
* @return {sigma.plugins.filter} Returns the instance.
*/
Filter.prototype.apply = function() {
for (var i = 0, len = _chain.length; i < len; ++i) {
_chain[i].processor(_chain[i].predicate);
};
if (_chain[0] && 'undo' === _chain[0].key) {
_chain.shift();
}
_s.refresh();
return this;
};
/**
* This method undoes one or several filters, depending on how it is called.
*
* To undo all filters, call "undo" without argument. To undo a specific
* filter, call it with the key of the filter. To undo multiple filters, call
* it with an array of keys or multiple arguments, and it will undo each
* filter, in the same order. The undo is not executed until the apply()
* method is called. For instance:
*
* > var filter = new sigma.plugins.filter(s);
* > filter
* > .nodesBy(function(n) {
* > return this.degree(n.id) > 0;
* > }, 'degreeNotNull');
* > .edgesBy(function(e) {
* > return e.size > 1;
* > }, 'edgeSize')
* > .undo();
*
* Other examples:
* > filter.undo();
* > filter.undo('myfilter');
* > filter.undo(['myfilter1', 'myfilter2']);
* > filter.undo('myfilter1', 'myfilter2');
*
* @param {?(string|array|*string))} v Eventually one key, an array of keys.
* @return {sigma.plugins.filter} Returns the instance.
*/
Filter.prototype.undo = function(v) {
var q = Object.create(null),
la = arguments.length;
// find removable filters
if (la === 1) {
if (Object.prototype.toString.call(v) === '[object Array]')
for (var i = 0, len = v.length; i < len; i++)
q[v[i]] = true;
else // 1 filter key
q[v] = true;
} else if (la > 1) {
for (var i = 0; i < la; i++)
q[arguments[i]] = true;
}
else
this.clear();
unregister(q);
function processor() {
var n = _g.nodes(),
ln = n.length,
e = _g.edges(),
le = e.length;
while(ln--)
n[ln].hidden = false;
while(le--)
e[le].hidden = false;
};
_chain.unshift({
'key': 'undo',
'processor': processor
});
return this;
};
// fast deep copy function
function deepCopy(o) {
var copy = Object.create(null);
for (var i in o) {
if (typeof o[i] === "object" && o[i] !== null) {
copy[i] = deepCopy(o[i]);
}
else if (typeof o[i] === "function" && o[i] !== null) {
// clone function:
eval(" copy[i] = " + o[i].toString());
//copy[i] = o[i].bind(_g);
}
else
copy[i] = o[i];
}
return copy;
};
function cloneChain(chain) {
// Clone the array of filters:
var copy = chain.slice(0);
for (var i = 0, len = copy.length; i < len; i++) {
copy[i] = deepCopy(copy[i]);
if (typeof copy[i].processor === "function")
copy[i].processor = 'filter.processors.' + copy[i].processor.name;
};
return copy;
}
/**
* This method is used to empty the chain of filters.
* Prefer the undo() method to reset filters.
*
* > var filter = new sigma.plugins.filter(s);
* > filter.clear();
*
* @return {sigma.plugins.filter} Returns the instance.
*/
Filter.prototype.clear = function() {
_chain.length = 0; // clear the array
_keysIndex = Object.create(null);
return this;
};
/**
* This method clones the filter chain and return the copy.
*
* > var filter = new sigma.plugins.filter(s);
* > var chain = filter.export();
*
* @return {object} The cloned chain of filters.
*/
Filter.prototype.export = function() {
var c = cloneChain(_chain);
return c;
};
/**
* This method sets the chain of filters with the specified chain.
*
* > var filter = new sigma.plugins.filter(s);
* > var chain = [
* > {
* > key: 'my-filter',
* > predicate: function(n) {...},
* > processor: 'filter.processors.nodes'
* > }, ...
* > ];
* > filter.import(chain);
*
* @param {array} chain The chain of filters.
* @return {sigma.plugins.filter} Returns the instance.
*/
Filter.prototype.import = function(chain) {
if (chain === undefined)
throw 'Wrong arguments.';
if (Object.prototype.toString.call(chain) !== '[object Array]')
throw 'The chain" must be an array.';
var copy = cloneChain(chain);
for (var i = 0, len = copy.length; i < len; i++) {
if (copy[i].predicate === undefined || copy[i].processor === undefined)
throw 'Wrong arguments.';
if (copy[i].key != undefined && typeof copy[i].key !== 'string')
throw 'The filter key "'+ copy[i].key.toString() +'" must be a string.';
if (typeof copy[i].predicate !== 'function')
throw 'The predicate of key "'+ copy[i].key +'" must be a function.';
if (typeof copy[i].processor !== 'string')
throw 'The processor of key "'+ copy[i].key +'" must be a string.';
// Replace the processor name by the corresponding function:
switch(copy[i].processor) {
case 'filter.processors.nodes':
copy[i].processor = Processors.nodes;
break;
case 'filter.processors.edges':
copy[i].processor = Processors.edges;
break;
case 'filter.processors.neighbors':
copy[i].processor = Processors.neighbors;
break;
default:
throw 'Unknown processor ' + copy[i].processor;
}
};
_chain = copy;
return this;
};
/**
* Interface
* ------------------
*
* > var filter = new sigma.plugins.filter(s);
*/
var filter = null;
/**
* @param {sigma} s The related sigma instance.
*/
sigma.plugins.filter = function(s) {
// Create filter if undefined
if (!filter) {
filter = new Filter(s);
}
return filter;
};
}).call(this);

View File

@ -0,0 +1,337 @@
/**
* Sigma Lasso
* =============================
*
* @author Florent Schildknecht <florent.schildknecht@gmail.com> (Florent Schildknecht)
* @version 0.0.2
*/
;(function (undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
// Initialize package:
sigma.utils.pkg('sigma.plugins');
var _body = undefined,
_instances = {};
/**
* Lasso Object
* ------------------
* @param {sigma} sigmaInstance The related sigma instance.
* @param {renderer} renderer The sigma instance renderer.
* @param {sigma.classes.configurable} settings A settings class
*/
function Lasso (sigmaInstance, renderer, settings) {
// Lasso is also an event dispatcher
sigma.classes.dispatcher.extend(this);
// A quick hardcoded rule to prevent people from using this plugin with the
// WebGL renderer (which is impossible at the moment):
if (
sigma.renderers.webgl &&
renderer instanceof sigma.renderers.webgl
)
throw new Error(
'The sigma.plugins.lasso is not compatible with the WebGL renderer'
);
this.sigmaInstance = sigmaInstance;
this.renderer = renderer;
this.drawingCanvas = undefined;
this.drawingContext = undefined;
this.drewPoints = [];
this.selectedNodes = [];
this.isActive = false;
this.isDrawing = false;
_body = document.body;
// Extends default settings
this.settings = new sigma.classes.configurable({
'strokeStyle': 'black',
'lineWidth': 2,
'fillWhileDrawing': false,
'fillStyle': 'rgba(200, 200, 200, 0.25)',
'cursor': 'crosshair'
}, settings || {});
};
/**
* This method is used to destroy the lasso.
*
* > var lasso = new sigma.plugins.lasso(sigmaInstance);
* > lasso.clear();
*
* @return {sigma.plugins.lasso} Returns the instance.
*/
Lasso.prototype.clear = function () {
this.deactivate();
this.sigmaInstance = undefined;
this.renderer = undefined;
return this;
};
// Lasso.prototype.getSigmaInstance = function () {
// return this.sigmaInstance;
// }
/**
* This method is used to activate the lasso mode.
*
* > var lasso = new sigma.plugins.lasso(sigmaInstance);
* > lasso.activate();
*
* @return {sigma.plugins.lasso} Returns the instance.
*/
Lasso.prototype.activate = function () {
if (this.sigmaInstance && !this.isActive) {
this.isActive = true;
// Add a new background layout canvas to draw the path on
if (!this.renderer.domElements['lasso']) {
this.renderer.initDOM('canvas', 'lasso');
this.drawingCanvas = this.renderer.domElements['lasso'];
this.drawingCanvas.width = this.renderer.container.offsetWidth;
this.drawingCanvas.height = this.renderer.container.offsetHeight;
this.renderer.container.appendChild(this.drawingCanvas);
this.drawingContext = this.drawingCanvas.getContext('2d');
this.drawingCanvas.style.cursor = this.settings('cursor');
}
_bindAll.apply(this);
}
return this;
};
/**
* This method is used to deactivate the lasso mode.
*
* > var lasso = new sigma.plugins.lasso(sigmaInstance);
* > lasso.deactivate();
*
* @return {sigma.plugins.lasso} Returns the instance.
*/
Lasso.prototype.deactivate = function () {
if (this.sigmaInstance && this.isActive) {
this.isActive = false;
this.isDrawing = false;
_unbindAll.apply(this);
if (this.renderer.domElements['lasso']) {
this.renderer.container.removeChild(this.renderer.domElements['lasso']);
delete this.renderer.domElements['lasso'];
this.drawingCanvas.style.cursor = '';
this.drawingCanvas = undefined;
this.drawingContext = undefined;
this.drewPoints = [];
}
}
return this;
};
/**
* This method is used to bind all events of the lasso mode.
*
* > var lasso = new sigma.plugins.lasso(sigmaInstance);
* > lasso.activate();
*
* @return {sigma.plugins.lasso} Returns the instance.
*/
var _bindAll = function () {
// Mouse events
this.drawingCanvas.addEventListener('mousedown', onDrawingStart.bind(this));
_body.addEventListener('mousemove', onDrawing.bind(this));
_body.addEventListener('mouseup', onDrawingEnd.bind(this));
// Touch events
this.drawingCanvas.addEventListener('touchstart', onDrawingStart.bind(this));
_body.addEventListener('touchmove', onDrawing.bind(this));
_body.addEventListener('touchcancel', onDrawingEnd.bind(this));
_body.addEventListener('touchleave', onDrawingEnd.bind(this));
_body.addEventListener('touchend', onDrawingEnd.bind(this));
};
/**
* This method is used to unbind all events of the lasso mode.
*
* > var lasso = new sigma.plugins.lasso(sigmaInstance);
* > lasso.activate();
*
* @return {sigma.plugins.lasso} Returns the instance.
*/
var _unbindAll = function () {
// Mouse events
this.drawingCanvas.removeEventListener('mousedown', onDrawingStart.bind(this));
_body.removeEventListener('mousemove', onDrawing.bind(this));
_body.removeEventListener('mouseup', onDrawingEnd.bind(this));
// Touch events
this.drawingCanvas.removeEventListener('touchstart', onDrawingStart.bind(this));
this.drawingCanvas.removeEventListener('touchmove', onDrawing.bind(this));
_body.removeEventListener('touchcancel', onDrawingEnd.bind(this));
_body.removeEventListener('touchleave', onDrawingEnd.bind(this));
_body.removeEventListener('touchend', onDrawingEnd.bind(this));
};
/**
* This method is used to retrieve the previously selected nodes
*
* > var lasso = new sigma.plugins.lasso(sigmaInstance);
* > lasso.getSelectedNodes();
*
* @return {array} Returns an array of nodes.
*/
Lasso.prototype.getSelectedNodes = function () {
return this.selectedNodes;
};
function onDrawingStart (event) {
var drawingRectangle = this.drawingCanvas.getBoundingClientRect();
if (this.isActive) {
this.isDrawing = true;
this.drewPoints = [];
this.selectedNodes = [];
this.sigmaInstance.refresh();
this.drewPoints.push({
x: event.clientX - drawingRectangle.left,
y: event.clientY - drawingRectangle.top
});
this.drawingCanvas.style.cursor = this.settings('cursor');
event.stopPropagation();
}
}
function onDrawing (event) {
if (this.isActive && this.isDrawing) {
var x = 0,
y = 0,
drawingRectangle = this.drawingCanvas.getBoundingClientRect();
switch (event.type) {
case 'touchmove':
x = event.touches[0].clientX;
y = event.touches[0].clientY;
break;
default:
x = event.clientX;
y = event.clientY;
break;
}
this.drewPoints.push({
x: x - drawingRectangle.left,
y: y - drawingRectangle.top
});
// Drawing styles
this.drawingContext.lineWidth = this.settings('lineWidth');
this.drawingContext.strokeStyle = this.settings('strokeStyle');
this.drawingContext.fillStyle = this.settings('fillStyle');
this.drawingContext.lineJoin = 'round';
this.drawingContext.lineCap = 'round';
// Clear the canvas
this.drawingContext.clearRect(0, 0, this.drawingContext.canvas.width, this.drawingContext.canvas.height);
// Redraw the complete path for a smoother effect
// Even smoother with quadratic curves
var sourcePoint = this.drewPoints[0],
destinationPoint = this.drewPoints[1],
pointsLength = this.drewPoints.length,
getMiddlePointCoordinates = function (firstPoint, secondPoint) {
return {
x: firstPoint.x + (secondPoint.x - firstPoint.x) / 2,
y: firstPoint.y + (secondPoint.y - firstPoint.y) / 2
};
};
this.drawingContext.beginPath();
this.drawingContext.moveTo(sourcePoint.x, sourcePoint.y);
for (var i = 1; i < pointsLength; i++) {
var middlePoint = getMiddlePointCoordinates(sourcePoint, destinationPoint);
// this.drawingContext.lineTo(this.drewPoints[i].x, this.drewPoints[i].y);
this.drawingContext.quadraticCurveTo(sourcePoint.x, sourcePoint.y, middlePoint.x, middlePoint.y);
sourcePoint = this.drewPoints[i];
destinationPoint = this.drewPoints[i+1];
}
this.drawingContext.lineTo(sourcePoint.x, sourcePoint.y);
this.drawingContext.stroke();
if (this.settings('fillWhileDrawing')) {
this.drawingContext.fill();
}
event.stopPropagation();
}
}
function onDrawingEnd (event) {
if (this.isActive && this.isDrawing) {
this.isDrawing = false;
// Select the nodes inside the path
var nodes = this.renderer.nodesOnScreen,
nodesLength = nodes.length,
i = 0,
prefix = this.renderer.options.prefix || '';
// Loop on all nodes and check if they are in the path
while (nodesLength--) {
var node = nodes[nodesLength],
x = node[prefix + 'x'],
y = node[prefix + 'y'];
if (this.drawingContext.isPointInPath(x, y) && !node.hidden) {
this.selectedNodes.push(node);
}
}
// Dispatch event with selected nodes
this.dispatchEvent('selectedNodes', this.selectedNodes);
// Clear the drawing canvas
this.drawingContext.clearRect(0, 0, this.drawingCanvas.width, this.drawingCanvas.height);
this.drawingCanvas.style.cursor = this.settings('cursor');
event.stopPropagation();
}
}
/**
* @param {sigma} sigmaInstance The related sigma instance.
* @param {renderer} renderer The sigma instance renderer.
* @param {sigma.classes.configurable} settings A settings class
*
* @return {sigma.plugins.lasso} Returns the instance
*/
sigma.plugins.lasso = function (sigmaInstance, renderer, settings) {
// Create lasso if undefined
if (!_instances[sigmaInstance.id]) {
_instances[sigmaInstance.id] = new Lasso(sigmaInstance, renderer, settings);
}
// Listen for sigmaInstance kill event, and remove the lasso isntance
sigmaInstance.bind('kill', function () {
if (_instances[sigmaInstance.id] instanceof Lasso) {
_instances[sigmaInstance.id].clear();
delete _instances[sigmaInstance.id];
}
});
return _instances[sigmaInstance.id];
};
}).call(this);

View File

@ -53,6 +53,10 @@
if (callback) {
callback.apply(this, args);
}
if (this.graphViewer2) {
this.graphViewer2.graphSettingsView.hide();
}
},
checkUser: function () {

View File

@ -11,23 +11,58 @@
<div id="graphSettingsView" class="innerContent">
<div class="pure-g" style="margin-top: -15px">
<div class="pure-u-1-1 pure-u-md-1-1 pure-u-lg-1-1 pure-u-xl-1-1">
<div class="pure-g pure-table pure-table-body">
<% _.each(general, function(val, key) { %>
<% if (val.type === 'divider') { %>
<div class="pure-u-1-1 left heading"><%=val.name%></div>
<% } else { %>
<div class="<%= genClass %> left"><%=val.name%></div>
<div class="<%= genClass2 %> left">
<% if (val.type === 'select') { %>
<select id="g_<%=key%>">
<% _.each(val, function(option, optKey) { %>
<% if (option.name) { %>
<option value="<%=option.val%>"> <%=option.name%> </option>
<% } %>
<% }); %>
</select>
<% } %>
<% if (val.type === 'string') { %>
<input id="g_<%=key%>" type="text" placeholder="string"></input>
<% } %>
<% if (val.type === 'number') { %>
<input id="g_<%=key%>" type="text" id="<%=val %>" value="<%=val.value %>" placeholder=""></input>
<% } %>
<% if (val.type === 'range') { %>
<input id="g_<%=key%>" type='range' min="0" max="50" val="<%=VALUE%>"/>
<% } %>
<% if (val.type === 'color') { %>
<input id="g_<%=key%>" type='color' name='color' value="<%=VALUE%>"/>
<% } %>
</div>
<% } %>
<% }); %>
</div>
</div>
<div class="pure-u-1-1 pure-u-md-1-1 pure-u-lg-1-1 pure-u-xl-1-1">
<div class="sectionHeader pure-g">
<div class="pure-u-1-1">
<div class="title">
Graph specific
</div>
</div>
</div>
<div class="pure-g pure-table pure-table-body">
<% _.each(specific, function(val, key) { %>
<% if (val.type === 'divider') { %>
<div class="heading <%= genClass %> left"><%=val.name%></div>
<div class="<%= genClass2 %> left"></div>
<div class="pure-u-1-1 left heading"><%=val.name%></div>
<% } else { %>
<div class="<%= genClass %> left"><%=val.name%></div>
@ -53,6 +88,10 @@
<input id="g_<%=key%>" type='color' name='color' value="<%=VALUE%>"/>
<% } %>
<% if (val.type === 'range') { %>
<input id="g_<%=key%>" type='range' min="0" max="100" val="<%=VALUE%>"/>
<% } %>
<% if (val.type === 'select') { %>
<select id="g_<%=key%>">
<% _.each(val, function(option, optKey) { %>
@ -70,50 +109,8 @@
</div>
<div class="pure-u-1-1 pure-u-md-1-1 pure-u-lg-1-1 pure-u-xl-1-1">
<div class="sectionHeader pure-g">
<div class="pure-u-1-1">
<div class="title">
General
</div>
</div>
</div>
<div class="pure-g pure-table pure-table-body">
<% _.each(general, function(val, key) { %>
<% if (val.type === 'divider') { %>
<div class="heading <%= genClass %> left"><%=val.name%></div>
<div class="<%= genClass2 %> left"></div>
<% } else { %>
<div class="<%= genClass %> left"><%=val.name%></div>
<% if (val.type === 'select') { %>
<select id="g_<%=key%>">
<% _.each(val, function(option, optKey) { %>
<% if (option.name) { %>
<option value="<%=option.val%>"> <%=option.name%> </option>
<% } %>
<% }); %>
</select>
<% } %>
<% if (val.type === 'string') { %>
<input id="g_<%=key%>" type="text" placeholder="string"></input>
<% } %>
<% if (val.type === 'number') { %>
<input id="g_<%=key%>" type="text" id="<%=val %>" value="<%=val.value %>" placeholder=""></input>
<% } %>
<% } %>
<% }); %>
</div>
</div>
<button id="saveGraphSettings" style="margin-top: 20px;" class="button-success pull-right">Save</button>
<button id="saveGraphSettings" style="margin-top: 20px; margin-right: 10px;" class="button-success pull-right">Save</button>
<button id="restoreGraphSettings" style="margin-top: 20px;" class="button-success pull-right">Restore defaults</button>
</div>

View File

@ -67,7 +67,13 @@
'nodeLabel': {
type: 'string',
name: 'Label',
desc: 'Default node color. RGB or HEX value.',
desc: 'Node label. Please choose a valid and available node attribute.',
default: '_key'
},
'nodeLabelThreshold': {
type: 'range',
name: 'Node label threshold',
desc: 'The minimum size a node must have on screen to see its label displayed. This does not affect hovering behavior.',
default: '_key'
},
'nodeColor': {
@ -90,6 +96,12 @@
name: 'Label',
desc: 'Default edge label.'
},
'edgeLabelThreshold': {
type: 'range',
name: 'Edge label threshold',
desc: 'The minimum size an edge must have on screen to see its label displayed. This does not affect hovering behavior.',
default: '_key'
},
'edgeColor': {
type: 'color',
name: 'Color',
@ -112,7 +124,8 @@
curve: {
name: 'Curve',
val: 'curve'
},
}
/*
arrow: {
name: 'Arrow',
val: 'arrow'
@ -121,6 +134,7 @@
name: 'Curved Arrow',
val: 'curvedArrow'
}
*/
}
},
@ -137,13 +151,14 @@
'click #restoreGraphSettings': 'restoreGraphSettings',
'keyup #graphSettingsView input': 'checkEnterKey',
'keyup #graphSettingsView select': 'checkEnterKey',
'change input[type="range"]': 'saveGraphSettings',
'change input[type="color"]': 'checkColor',
'change select': 'saveGraphSettings',
'focus #graphSettingsView input': 'lastFocus',
'focus #graphSettingsView select': 'lastFocus'
},
lastFocus: function (e) {
console.log(e.currentTarget.id);
console.log(e.currentTarget);
this.lastFocussed = e.currentTarget.id;
},
@ -167,17 +182,25 @@
});
},
saveGraphSettings: function () {
checkColor: function () {
this.saveGraphSettings(true);
},
saveGraphSettings: function (color, nodeStart) {
var self = this;
console.log('CLICK');
var combinedName = window.App.currentDB.toJSON().name + '_' + this.name;
var config = {};
config[combinedName] = {
layout: $('#g_layout').val(),
renderer: $('#g_renderer').val(),
depth: $('#g_depth').val(),
nodeColor: $('#g_nodeColor').val(),
nodeLabelThreshold: $('#g_nodeLabelThreshold').val(),
edgeColor: $('#g_edgeColor').val(),
edgeLabelThreshold: $('#g_edgeLabelThreshold').val(),
nodeLabel: $('#g_nodeLabel').val(),
edgeLabel: $('#g_edgeLabel').val(),
edgeType: $('#g_edgeType').val(),
@ -186,9 +209,17 @@
nodeStart: $('#g_nodeStart').val()
};
if (nodeStart) {
config[combinedName].nodeStart = nodeStart;
}
var callback = function () {
if (window.App.graphViewer2) {
window.App.graphViewer2.render(self.lastFocussed);
if (color) {
window.App.graphViewer2.updateColors();
} else {
window.App.graphViewer2.render(self.lastFocussed);
}
} else {
arangoHelper.arangoNotification('Graph ' + this.name, 'Configuration saved.');
}

View File

@ -34,7 +34,8 @@
events: {
'click #downloadPNG': 'downloadSVG',
'click #reloadGraph': 'reloadGraph',
'click #settingsMenu': 'toggleSettings'
'click #settingsMenu': 'toggleSettings',
'click #noGraphToggle': 'toggleSettings'
},
cursorX: 0,
@ -62,6 +63,18 @@
}
return neighbors;
});
sigma.classes.graph.addMethod('getNodeEdges', function (nodeId) {
var edges = this.edges();
var edgesToReturn = [];
_.each(edges, function (edge) {
if (edge.source === nodeId || edge.target === nodeId) {
edgesToReturn.push(edge.id);
}
});
return edgesToReturn;
});
} catch (ignore) {}
},
@ -220,19 +233,96 @@
this.cursorY = e.y;
},
deleteNode: function () {
var self = this;
var documentKey = $('#delete-node-attr-id').text();
var collectionId = documentKey.split('/')[0];
var documentId = documentKey.split('/')[1];
if ($('#delete-node-edges-attr').val() === 'yes') {
$.ajax({
cache: false,
type: 'DELETE',
contentType: 'application/json',
url: arangoHelper.databaseUrl(
'/_api/gharial/' + encodeURIComponent(self.name) + '/vertex/' + encodeURIComponent(documentKey.split('/')[0]) + '/' + encodeURIComponent(documentKey.split('/')[1])
),
success: function (data) {
self.currentGraph.graph.dropNode(documentKey);
self.currentGraph.refresh();
},
error: function () {
arangoHelper.arangoError('Graph', 'Could not delete node.');
}
});
} else {
var callback = function (error) {
if (!error) {
self.currentGraph.graph.dropNode(documentKey);
// rerender graph
self.currentGraph.refresh();
} else {
arangoHelper.arangoError('Graph', 'Could not delete node.');
}
};
this.documentStore.deleteDocument(collectionId, documentId, callback);
}
window.modalView.hide();
},
deleteNodeModal: function (nodeId) {
var buttons = []; var tableContent = [];
tableContent.push(
window.modalView.createReadOnlyEntry('delete-node-attr-id', 'Really delete node', nodeId)
);
tableContent.push(
window.modalView.createSelectEntry(
'delete-node-edges-attr',
'Also delete edges?',
undefined,
undefined,
[
{
value: 'yes',
label: 'Yes'
},
{
value: 'no',
label: 'No'
}
]
)
);
buttons.push(
window.modalView.createDeleteButton('Delete', this.deleteNode.bind(this))
);
window.modalView.show(
'modalTable.ejs',
'Delete node',
buttons,
tableContent
);
},
addNode: function () {
var self = this;
var collectionId = $('.modal-body #new-node-collection-attr').val();
var key = $('.modal-body #new-node-key-attr').last().val();
var callback = function (error, id) {
var callback = function (error, id, msg) {
if (error) {
arangoHelper.arangoError('Error', 'Could not create node');
arangoHelper.arangoError('Could not create node', msg.errorMessage);
} else {
self.currentGraph.graph.addNode({
id: id,
label: self.graphConfig.nodeLabel,
label: self.graphConfig.nodeLabel || '',
size: self.graphConfig.nodeSize || Math.random(),
color: self.graphConfig.nodeColor || '#2ecc71',
x: self.cursorX,
@ -412,6 +502,26 @@
}
},
updateColors: function () {
var combinedName = window.App.currentDB.toJSON().name + '_' + this.name;
var self = this;
this.userConfig.fetch({
success: function (data) {
self.graphConfig = data.toJSON().graphs[combinedName];
self.currentGraph.graph.nodes().forEach(function (n) {
n.color = self.graphConfig.nodeColor;
});
self.currentGraph.graph.edges().forEach(function (e) {
e.color = self.graphConfig.edgeColor;
});
self.currentGraph.refresh();
}
});
},
// right click background context menu
createContextMenu: function (e) {
var self = this;
@ -498,7 +608,7 @@
wheel.multiSelect = true;
wheel.clickModeRotate = false;
wheel.slicePathFunction = slicePath().DonutSlice;
wheel.createWheel([icon.edit, icon.trash, icon.arrowleft2, icon.connect]);
wheel.createWheel([icon.edit, icon.trash, icon.play, icon.connect]);
wheel.navItems[0].selected = false;
wheel.navItems[0].hovered = false;
@ -513,7 +623,7 @@
// function 1: delete
wheel.navItems[1].navigateFunction = function (e) {
self.clearOldContextMenu();
self.deleteNode(nodeId);
self.deleteNodeModal(nodeId);
};
// function 2: mark as start node
@ -599,6 +709,11 @@
});
},
setStartNode: function (id) {
this.graphConfig.nodeStart = id;
this.graphSettingsView.saveGraphSettings(null, id);
},
editNode: function (id) {
var callback = function () {};
@ -627,6 +742,44 @@
return array;
},
initializeGraph: function (sigmaInstance, graph) {
var self = this;
// sigmaInstance.graph.read(graph);
sigmaInstance.refresh();
this.Sigma.plugins.Lasso = sigma.plugins.lasso;
var lasso = new this.Sigma.plugins.Lasso(sigmaInstance, sigmaInstance.renderers[0], {
'strokeStyle': 'black',
'lineWidth': 1,
'fillWhileDrawing': true,
'fillStyle': 'rgba(41, 41, 41, 0.2)',
'cursor': 'crosshair'
});
// Listen for selectedNodes event
lasso.bind('selectedNodes', function (event) {
// Do something with the selected nodes
var nodes = event.data;
console.log('nodes', nodes);
// For instance, reset all node size as their initial size
sigmaInstance.graph.nodes().forEach(function (node) {
node.color = self.graphConfig.nodeColor;
});
// Then increase the size of selected nodes...
nodes.forEach(function (node) {
node.color = 'red';
});
sigmaInstance.refresh();
});
return lasso;
},
renderGraph: function (graph, toFocus) {
var self = this;
@ -634,16 +787,22 @@
if (graph.edges.length === 0) {
var string = 'No edges found for starting point: <span style="font-weight: 400">' + self.graphSettings.startVertex._id + '</span>';
arangoHelper.arangoError('Graph', string);
$('#calculatingGraph').html(
'<div style="font-weight: 300; font-size: 10.5pt"><span style="font-weight: 400">Stopped. </span></br></br>' +
string +
'. Please <a style="color: #3498db" href="' + window.location.href +
'/settings">choose a different start node </a>or try to reload the graph. ' +
'. Please <span id="noGraphToggle" style="cursor: pointer; color: #3498db">choose a different start node </span>or try to reload the graph. ' +
'<i id="reloadGraph" class="fa fa-refresh" style="cursor: pointer"></i></div>'
);
return;
} else {
$('#content').append(
'<div style="position: absolute; right: 25px; bottom: 45px;">' +
'<span style="margin-right: 10px" class="arangoState">' + graph.nodes.length + ' nodes</span>' +
'<span class="arangoState">' + graph.edges.length + ' edges</span>' +
'</div>'
);
}
this.Sigma = sigma;
// defaults
@ -674,15 +833,15 @@
defaultEdgeHoverColor: '#000',
defaultEdgeType: 'line',
edgeHoverSizeRatio: 2,
edgeHoverExtremities: true
edgeHoverExtremities: true,
// lasso settings
autoRescale: true,
mouseEnabled: true,
touchEnabled: true,
nodesPowRatio: 1,
edgesPowRatio: 1
};
if (this.graphConfig) {
if (this.graphConfig.edgeType) {
settings.defaultEdgeType = this.graphConfig.edgeType;
}
}
// adjust display settings for big graphs
if (graph.nodes.length > 500) {
// show node label if size is 15
@ -690,6 +849,20 @@
settings.hideEdgesOnMove = true;
}
if (this.graphConfig) {
if (this.graphConfig.edgeType) {
settings.defaultEdgeType = this.graphConfig.edgeType;
}
if (this.graphConfig.nodeLabelThreshold) {
settings.labelThreshold = this.graphConfig.nodeLabelThreshold;
}
if (this.graphConfig.edgeLabelThreshold) {
settings.edgeLabelThreshold = this.graphConfig.edgeLabelThreshold;
}
}
// adjust display settings for webgl renderer
if (renderer === 'webgl') {
settings.enableEdgeHovering = false;
@ -751,6 +924,54 @@
self.clearMouseCanvas();
});
s.bind('overNode', function (e) {
$('.nodeInfoDiv').remove();
if (self.contextState.createEdge === false) {
var callback = function (error, data) {
if (!error) {
var obj = {};
var counter = 0;
var more = false;
_.each(data, function (val, key) {
if (counter < 15) {
if (typeof val === 'string') {
if (val.length > 10) {
obj[key] = val.substr(0, 10) + ' ...';
} else {
obj[key] = val;
}
}
} else {
more = true;
}
counter++;
});
var string = '<div id="nodeInfoDiv" class="nodeInfoDiv">' +
'<pre>' + JSON.stringify(obj, null, 2);
if (more) {
string = string.substr(0, string.length - 2);
string += ' \n\n ... \n\n } </pre></div>';
} else {
string += '</pre></div>';
}
$('#content').append(string);
}
};
self.documentStore.getDocument(e.data.node.id.split('/')[0], e.data.node.id.split('/')[1], callback);
}
});
s.bind('outNode', function (e) {
if (self.contextState.createEdge === false) {
$('.nodeInfoDiv').remove();
}
});
s.bind('clickNode', function (e) {
if (self.contextState.createEdge === true) {
// create the edge
@ -831,6 +1052,7 @@
window.setTimeout(function () {
s.stopForceAtlas2();
dragListener = sigma.plugins.dragNodes(s, s.renderers[0]);
console.log(dragListener);
}, duration);
} else if (algorithm === 'fruchtermann') {
// Start the Fruchterman-Reingold algorithm:
@ -839,7 +1061,6 @@
} else {
dragListener = sigma.plugins.dragNodes(s, s.renderers[0]);
}
console.log(dragListener);
// add listener to keep track of cursor position
var c = document.getElementsByClassName('sigma-mouse')[0];
@ -849,6 +1070,28 @@
if (toFocus) {
$('#' + toFocus).focus();
}
// init graph lasso
self.graphLasso = self.initializeGraph(s, graph);
self.graphLasso.activate();
self.graphLasso.deactivate();
// add lasso event
// Toggle lasso activation on Alt + l
document.addEventListener('keyup', function (event) {
switch (event.keyCode) {
case 76:
if (event.altKey) {
if (self.graphLasso.isActive) {
self.graphLasso.deactivate();
} else {
self.graphLasso.activate();
}
}
break;
}
});
// clear up info div
$('#calculatingGraph').remove();
}

View File

@ -1473,15 +1473,6 @@
);
}
}
if (data.extra.stats.scannedFull > 0) {
appendSpan(
'full collection scan', 'fa-exclamation-circle warning', 'additional'
);
} else {
appendSpan(
'no full collection scan', 'fa-check-circle positive', 'additional'
);
}
}
}

View File

@ -1,5 +1,7 @@
.graphContent {
margin-top: 3px;
#graph-container {
background-color: $c-white;
z-index: 5;
@ -40,6 +42,22 @@
}
}
.nodeInfoDiv {
left: 25px;
overflow: hidden;
position: absolute;
top: 130px;
pre {
background-color: rgba(64, 74, 83, .9);
border-radius: 2px;
color: $c-white;
max-height: 400px;
max-width: 330px;
overflow: hidden;
}
}
.nodeContextMenu {
position: fixed;
@ -67,6 +85,11 @@
border: 0;
border-radius: 0;
height: 100%;
overflow-y: scroll;
button {
margin-bottom: 125px;
}
}
#graphSettingsContent {
@ -79,13 +102,29 @@
width: 400px;
.pure-g {
font-size: 10pt;
input,
select {
color: $c-black;
}
.left {
color: $c-white;
height: 40px;
}
.pure-u-2-3 {
text-align: right;
}
.heading {
border-bottom: 1px solid $c-white;
height: 10px;
margin-bottom: 15px;
}
}
.pure-table {

View File

@ -0,0 +1,89 @@
input[type=range] {
-webkit-appearance: none;
border: 1px solid $c-white;
border-radius: 3px;
margin-top: 10px;
width: 218px;
}
input[type=range]::-webkit-slider-runnable-track {
background: $c-tab-bottom-border;
border: 0;
border-radius: 3px;
height: 5px;
width: 218px;
}
input[type=range]::-webkit-slider-thumb {
-webkit-appearance: none;
background: $c-positive;
border: 0;
border-radius: 50%;
height: 16px;
margin-top: -5px;
width: 16px;
}
input[type=range]:focus {
outline: none;
}
input[type=range]:focus::-webkit-slider-runnable-track {
background: $c-accordion-heading;
}
input[type=range]::-moz-range-track {
background: $c-e1grey;
border: 0;
border-radius: 3px;
height: 5px;
width: 218px;
}
input[type=range]::-moz-range-thumb {
background: $c-positive;
border: 0;
border-radius: 50%;
height: 16px;
width: 16px;
}
input[type=range]:-moz-focusring {
outline: 1px solid $c-white;
outline-offset: -1px;
}
input[type=range]::-ms-track {
background: transparent;
border-color: transparent;
border-width: 6px 0;
color: transparent;
height: 5px;
width: 218px;
}
input[type=range]::-ms-fill-lower {
background: $c-darker-grey;
border-radius: 10px;
}
input[type=range]::-ms-fill-upper {
background: $c-tab-bottom-border;
border-radius: 10px;
}
input[type=range]::-ms-thumb {
background: $c-positive;
border: 0;
border-radius: 50%;
height: 16px;
width: 16px;
}
input[type=range]:focus::-ms-fill-lower {
background: $c-dark-grey;
}
input[type=range]:focus::-ms-fill-upper {
background: $c-accordion-heading;
}

View File

@ -0,0 +1,8 @@
.arangoState {
background-color: $c-bluegrey-dark;
border-radius: 3px;
color: $c-white;
font-size: 10pt;
font-weight: 100;
padding: 5px 8px;
}

View File

@ -86,6 +86,11 @@
// screen shards
@import 'shards';
// input type range
@import 'range';
// state
@import 'state';
//arangoTable Template
@import 'arangoTable';
//arangoTabbar Template