1
0
Fork 0

graph ui devel

This commit is contained in:
hkernbach 2016-06-24 23:21:39 +02:00
parent 068a43c75d
commit bedab04286
10 changed files with 792 additions and 4 deletions

View File

@ -58,6 +58,8 @@
"frontend/js/lib/typeahead.bundle.min.js",
"frontend/js/lib/numeral.min.js",
"frontend/js/lib/sigma.min.js",
"frontend/js/lib/sigma.plugins.animate.js",
"frontend/js/lib/sigma.layout.noverlap.js",
"frontend/js/lib/jsoneditor-min.js",
"frontend/js/lib/strftime-min.js",
"frontend/js/lib/d3.fisheye.min.js",

View File

@ -294,3 +294,73 @@ authRouter.get('/job', function(req, res) {
.description(dd`
This function returns the job ids of all currently running jobs.
`);
authRouter.get('/graph/:name', function(req, res) {
var _ = require("lodash");
var name = req.pathParams.name;
var gm = require("@arangodb/general-graph");
//var traversal = require("@arangodb/graph/traversal");
var graph = gm._graph(name);
var vertexName = graph._vertexCollections()[0].name();
var startVertex = db[vertexName].any();
var aqlQuery =
'FOR v, e, p IN 1..2 ANY "' + startVertex._id + '" GRAPH "' + name + '"' +
'RETURN p'
;
var cursor = AQL_EXECUTE(aqlQuery);
var nodesObj = {}, nodesArr = [], edgesObj = {}, edgesArr = [];
_.each(cursor.json, function(obj) {
_.each(obj.edges, function(edge) {
if (edge._to && edge._from) {
edgesObj[edge._from + edge._to] = {
id: edge._id,
source: edge._from,
target: edge._to
};
}
});
var label;
_.each(obj.vertices, function(node) {
if (node.label) {
label = node.label;
}
else {
label = node._id;
}
nodesObj[node._id] = {
id: node._id,
label: label,
size: Math.random(),
color: '#2ecc71',
x: Math.random(),
y: Math.random()
};
});
});
//array format for sigma.js
_.each(edgesObj, function(node) {
edgesArr.push(node);
});
_.each(nodesObj, function(node) {
nodesArr.push(node);
});
res.json({
nodes: nodesArr,
edges: edgesArr
});
})
.summary('Return vertices and edges of a graph.')
.description(dd`
This function returns vertices and edges for a specific graph.
`);

View File

@ -0,0 +1,408 @@
;(function(undefined) {
'use strict';
if (typeof sigma === 'undefined')
throw new Error('sigma is not declared');
// Initialize package:
sigma.utils.pkg('sigma.layout.noverlap');
/**
* Noverlap Layout
* ===============================
*
* Author: @apitts / Andrew Pitts
* Algorithm: @jacomyma / Mathieu Jacomy (originally contributed to Gephi and ported to sigma.js under the MIT license by @andpitts with permission)
* Acknowledgement: @sheyman / Sébastien Heymann (some inspiration has been taken from other MIT licensed layout algorithms authored by @sheyman)
* Version: 0.1
*/
var settings = {
speed: 3,
scaleNodes: 1.2,
nodeMargin: 5.0,
gridSize: 20,
permittedExpansion: 1.1,
rendererIndex: 0,
maxIterations: 500
};
var _instance = {};
/**
* Event emitter Object
* ------------------
*/
var _eventEmitter = {};
/**
* Noverlap Object
* ------------------
*/
function Noverlap() {
var self = this;
this.init = function (sigInst, options) {
options = options || {};
// Properties
this.sigInst = sigInst;
this.config = sigma.utils.extend(options, settings);
this.easing = options.easing;
this.duration = options.duration;
if (options.nodes) {
this.nodes = options.nodes;
delete options.nodes;
}
if (!sigma.plugins || typeof sigma.plugins.animate === 'undefined') {
throw new Error('sigma.plugins.animate is not declared');
}
// State
this.running = false;
};
/**
* Single layout iteration.
*/
this.atomicGo = function () {
if (!this.running || this.iterCount < 1) return false;
var nodes = this.nodes || this.sigInst.graph.nodes(),
nodesCount = nodes.length,
i,
n,
n1,
n2,
xmin = Infinity,
xmax = -Infinity,
ymin = Infinity,
ymax = -Infinity,
xwidth,
yheight,
xcenter,
ycenter,
grid,
row,
col,
minXBox,
maxXBox,
minYBox,
maxYBox,
adjacentNodes,
subRow,
subCol,
nxmin,
nxmax,
nymin,
nymax;
this.iterCount--;
this.running = false;
for (i=0; i < nodesCount; i++) {
n = nodes[i];
n.dn.dx = 0;
n.dn.dy = 0;
//Find the min and max for both x and y across all nodes
xmin = Math.min(xmin, n.dn_x - (n.dn_size*self.config.scaleNodes + self.config.nodeMargin) );
xmax = Math.max(xmax, n.dn_x + (n.dn_size*self.config.scaleNodes + self.config.nodeMargin) );
ymin = Math.min(ymin, n.dn_y - (n.dn_size*self.config.scaleNodes + self.config.nodeMargin) );
ymax = Math.max(ymax, n.dn_y + (n.dn_size*self.config.scaleNodes + self.config.nodeMargin) );
}
xwidth = xmax - xmin;
yheight = ymax - ymin;
xcenter = (xmin + xmax) / 2;
ycenter = (ymin + ymax) / 2;
xmin = xcenter - self.config.permittedExpansion*xwidth / 2;
xmax = xcenter + self.config.permittedExpansion*xwidth / 2;
ymin = ycenter - self.config.permittedExpansion*yheight / 2;
ymax = ycenter + self.config.permittedExpansion*yheight / 2;
grid = {}; //An object of objects where grid[row][col] is an array of node ids representing nodes that fall in that grid. Nodes can fall in more than one grid
for(row = 0; row < self.config.gridSize; row++) {
grid[row] = {};
for(col = 0; col < self.config.gridSize; col++) {
grid[row][col] = [];
}
}
//Place nodes in grid
for (i=0; i < nodesCount; i++) {
n = nodes[i];
nxmin = n.dn_x - (n.dn_size*self.config.scaleNodes + self.config.nodeMargin);
nxmax = n.dn_x + (n.dn_size*self.config.scaleNodes + self.config.nodeMargin);
nymin = n.dn_y - (n.dn_size*self.config.scaleNodes + self.config.nodeMargin);
nymax = n.dn_y + (n.dn_size*self.config.scaleNodes + self.config.nodeMargin);
minXBox = Math.floor(self.config.gridSize* (nxmin - xmin) / (xmax - xmin) );
maxXBox = Math.floor(self.config.gridSize* (nxmax - xmin) / (xmax - xmin) );
minYBox = Math.floor(self.config.gridSize* (nymin - ymin) / (ymax - ymin) );
maxYBox = Math.floor(self.config.gridSize* (nymax - ymin) / (ymax - ymin) );
for(col = minXBox; col <= maxXBox; col++) {
for(row = minYBox; row <= maxYBox; row++) {
grid[row][col].push(n.id);
}
}
}
adjacentNodes = {}; //An object that stores the node ids of adjacent nodes (either in same grid box or adjacent grid box) for all nodes
for(row = 0; row < self.config.gridSize; row++) {
for(col = 0; col < self.config.gridSize; col++) {
grid[row][col].forEach(function(nodeId) {
if(!adjacentNodes[nodeId]) {
adjacentNodes[nodeId] = [];
}
for(subRow = Math.max(0, row - 1); subRow <= Math.min(row + 1, self.config.gridSize - 1); subRow++) {
for(subCol = Math.max(0, col - 1); subCol <= Math.min(col + 1, self.config.gridSize - 1); subCol++) {
grid[subRow][subCol].forEach(function(subNodeId) {
if(subNodeId !== nodeId && adjacentNodes[nodeId].indexOf(subNodeId) === -1) {
adjacentNodes[nodeId].push(subNodeId);
}
});
}
}
});
}
}
//If two nodes overlap then repulse them
for (i=0; i < nodesCount; i++) {
n1 = nodes[i];
adjacentNodes[n1.id].forEach(function(nodeId) {
var n2 = self.sigInst.graph.nodes(nodeId);
var xDist = n2.dn_x - n1.dn_x;
var yDist = n2.dn_y - n1.dn_y;
var dist = Math.sqrt(xDist*xDist + yDist*yDist);
var collision = (dist < ((n1.dn_size*self.config.scaleNodes + self.config.nodeMargin) + (n2.dn_size*self.config.scaleNodes + self.config.nodeMargin)));
if(collision) {
self.running = true;
if(dist > 0) {
n2.dn.dx += xDist / dist * (1 + n1.dn_size);
n2.dn.dy += yDist / dist * (1 + n1.dn_size);
} else {
n2.dn.dx += xwidth * 0.01 * (0.5 - Math.random());
n2.dn.dy += yheight * 0.01 * (0.5 - Math.random());
}
}
});
}
for (i=0; i < nodesCount; i++) {
n = nodes[i];
if(!n.fixed) {
n.dn_x = n.dn_x + n.dn.dx * 0.1 * self.config.speed;
n.dn_y = n.dn_y + n.dn.dy * 0.1 * self.config.speed;
}
}
if(this.running && this.iterCount < 1) {
this.running = false;
}
return this.running;
};
this.go = function () {
this.iterCount = this.config.maxIterations;
while (this.running) {
this.atomicGo();
};
this.stop();
};
this.start = function() {
if (this.running) return;
var nodes = this.sigInst.graph.nodes();
var prefix = this.sigInst.renderers[self.config.rendererIndex].options.prefix;
this.running = true;
// Init nodes
for (var i = 0; i < nodes.length; i++) {
nodes[i].dn_x = nodes[i][prefix + 'x'];
nodes[i].dn_y = nodes[i][prefix + 'y'];
nodes[i].dn_size = nodes[i][prefix + 'size'];
nodes[i].dn = {
dx: 0,
dy: 0
};
}
_eventEmitter[self.sigInst.id].dispatchEvent('start');
this.go();
};
this.stop = function() {
var nodes = this.sigInst.graph.nodes();
this.running = false;
if (this.easing) {
_eventEmitter[self.sigInst.id].dispatchEvent('interpolate');
sigma.plugins.animate(
self.sigInst,
{
x: 'dn_x',
y: 'dn_y'
},
{
easing: self.easing,
onComplete: function() {
self.sigInst.refresh();
for (var i = 0; i < nodes.length; i++) {
nodes[i].dn = null;
nodes[i].dn_x = null;
nodes[i].dn_y = null;
}
_eventEmitter[self.sigInst.id].dispatchEvent('stop');
},
duration: self.duration
}
);
}
else {
// Apply changes
for (var i = 0; i < nodes.length; i++) {
nodes[i].x = nodes[i].dn_x;
nodes[i].y = nodes[i].dn_y;
}
this.sigInst.refresh();
for (var i = 0; i < nodes.length; i++) {
nodes[i].dn = null;
nodes[i].dn_x = null;
nodes[i].dn_y = null;
}
_eventEmitter[self.sigInst.id].dispatchEvent('stop');
}
};
this.kill = function() {
this.sigInst = null;
this.config = null;
this.easing = null;
};
};
/**
* Interface
* ----------
*/
/**
* Configure the layout algorithm.
* Recognized options:
* **********************
* Here is the exhaustive list of every accepted parameter in the settings
* object:
*
* {?number} speed A larger value increases the convergence speed at the cost of precision
* {?number} scaleNodes The ratio to scale nodes by - a larger ratio will lead to more space around larger nodes
* {?number} nodeMargin A fixed margin to apply around nodes regardless of size
* {?number} maxIterations The maximum number of iterations to perform before the layout completes.
* {?integer} gridSize The number of rows and columns to use when partioning nodes into a grid for efficient computation
* {?number} permittedExpansion A permitted expansion factor to the overall size of the network applied at each iteration
* {?integer} rendererIndex The index of the renderer to use for node co-ordinates. Defaults to zero.
* {?(function|string)} easing Either the name of an easing in the sigma.utils.easings package or a function. If not specified, the
* quadraticInOut easing from this package will be used instead.
* {?number} duration The duration of the animation. If not specified, the "animationsTime" setting value of the sigma instance will be used instead.
*
*
* @param {object} config The optional configuration object.
*
* @return {sigma.classes.dispatcher} Returns an event emitter.
*/
sigma.prototype.configNoverlap = function(config) {
var sigInst = this;
if (!config) throw new Error('Missing argument: "config"');
// Create instance if undefined
if (!_instance[sigInst.id]) {
_instance[sigInst.id] = new Noverlap();
_eventEmitter[sigInst.id] = {};
sigma.classes.dispatcher.extend(_eventEmitter[sigInst.id]);
// Binding on kill to clear the references
sigInst.bind('kill', function() {
_instance[sigInst.id].kill();
_instance[sigInst.id] = null;
_eventEmitter[sigInst.id] = null;
});
}
_instance[sigInst.id].init(sigInst, config);
return _eventEmitter[sigInst.id];
};
/**
* Start the layout algorithm. It will use the existing configuration if no
* new configuration is passed.
* Recognized options:
* **********************
* Here is the exhaustive list of every accepted parameter in the settings
* object
*
* {?number} speed A larger value increases the convergence speed at the cost of precision
* {?number} scaleNodes The ratio to scale nodes by - a larger ratio will lead to more space around larger nodes
* {?number} nodeMargin A fixed margin to apply around nodes regardless of size
* {?number} maxIterations The maximum number of iterations to perform before the layout completes.
* {?integer} gridSize The number of rows and columns to use when partioning nodes into a grid for efficient computation
* {?number} permittedExpansion A permitted expansion factor to the overall size of the network applied at each iteration
* {?integer} rendererIndex The index of the renderer to use for node co-ordinates. Defaults to zero.
* {?(function|string)} easing Either the name of an easing in the sigma.utils.easings package or a function. If not specified, the
* quadraticInOut easing from this package will be used instead.
* {?number} duration The duration of the animation. If not specified, the "animationsTime" setting value of the sigma instance will be used instead.
*
*
*
* @param {object} config The optional configuration object.
*
* @return {sigma.classes.dispatcher} Returns an event emitter.
*/
sigma.prototype.startNoverlap = function(config) {
var sigInst = this;
if (config) {
this.configNoverlap(sigInst, config);
}
_instance[sigInst.id].start();
return _eventEmitter[sigInst.id];
};
/**
* Returns true if the layout has started and is not completed.
*
* @return {boolean}
*/
sigma.prototype.isNoverlapRunning = function() {
var sigInst = this;
return !!_instance[sigInst.id] && _instance[sigInst.id].running;
};
}).call(this);

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,204 @@
/**
* This plugin provides a method to animate a sigma instance by interpolating
* some node properties. Check the sigma.plugins.animate function doc or the
* examples/animate.html code sample to know more.
*/
(function() {
'use strict';
if (typeof sigma === 'undefined')
throw 'sigma is not declared';
sigma.utils.pkg('sigma.plugins');
var _id = 0,
_cache = {};
// TOOLING FUNCTIONS:
// ******************
function parseColor(val) {
if (_cache[val])
return _cache[val];
var result = [0, 0, 0];
if (val.match(/^#/)) {
val = (val || '').replace(/^#/, '');
result = (val.length === 3) ?
[
parseInt(val.charAt(0) + val.charAt(0), 16),
parseInt(val.charAt(1) + val.charAt(1), 16),
parseInt(val.charAt(2) + val.charAt(2), 16)
] :
[
parseInt(val.charAt(0) + val.charAt(1), 16),
parseInt(val.charAt(2) + val.charAt(3), 16),
parseInt(val.charAt(4) + val.charAt(5), 16)
];
} else if (val.match(/^ *rgba? *\(/)) {
val = val.match(
/^ *rgba? *\( *([0-9]*) *, *([0-9]*) *, *([0-9]*) *(,.*)?\) *$/
);
result = [
+val[1],
+val[2],
+val[3]
];
}
_cache[val] = {
r: result[0],
g: result[1],
b: result[2]
};
return _cache[val];
}
function interpolateColors(c1, c2, p) {
c1 = parseColor(c1);
c2 = parseColor(c2);
var c = {
r: c1.r * (1 - p) + c2.r * p,
g: c1.g * (1 - p) + c2.g * p,
b: c1.b * (1 - p) + c2.b * p
};
return 'rgb(' + [c.r | 0, c.g | 0, c.b | 0].join(',') + ')';
}
/**
* This function will animate some specified node properties. It will
* basically call requestAnimationFrame, interpolate the values and call the
* refresh method during a specified duration.
*
* Recognized parameters:
* **********************
* Here is the exhaustive list of every accepted parameters in the settings
* object:
*
* {?array} nodes An array of node objects or node ids. If
* not specified, all nodes of the graph
* will be animated.
* {?(function|string)} easing Either the name of an easing in the
* sigma.utils.easings package or a
* function. If not specified, the
* quadraticInOut easing from this package
* will be used instead.
* {?number} duration The duration of the animation. If not
* specified, the "animationsTime" setting
* value of the sigma instance will be used
* instead.
* {?function} onComplete Eventually a function to call when the
* animation is ended.
*
* @param {sigma} s The related sigma instance.
* @param {object} animate An hash with the keys being the node properties
* to interpolate, and the values being the related
* target values.
* @param {?object} options Eventually an object with options.
*/
sigma.plugins.animate = function(s, animate, options) {
var o = options || {},
id = ++_id,
duration = o.duration || s.settings('animationsTime'),
easing = typeof o.easing === 'string' ?
sigma.utils.easings[o.easing] :
typeof o.easing === 'function' ?
o.easing :
sigma.utils.easings.quadraticInOut,
start = sigma.utils.dateNow(),
nodes,
startPositions;
if (o.nodes && o.nodes.length) {
if (typeof o.nodes[0] === 'object')
nodes = o.nodes;
else
nodes = s.graph.nodes(o.nodes); // argument is an array of IDs
}
else
nodes = s.graph.nodes();
// Store initial positions:
startPositions = nodes.reduce(function(res, node) {
var k;
res[node.id] = {};
for (k in animate)
if (k in node)
res[node.id][k] = node[k];
return res;
}, {});
s.animations = s.animations || Object.create({});
sigma.plugins.kill(s);
// Do not refresh edgequadtree during drag:
var k,
c;
for (k in s.cameras) {
c = s.cameras[k];
c.edgequadtree._enabled = false;
}
function step() {
var p = (sigma.utils.dateNow() - start) / duration;
if (p >= 1) {
nodes.forEach(function(node) {
for (var k in animate)
if (k in animate)
node[k] = node[animate[k]];
});
// Allow to refresh edgequadtree:
var k,
c;
for (k in s.cameras) {
c = s.cameras[k];
c.edgequadtree._enabled = true;
}
s.refresh();
if (typeof o.onComplete === 'function')
o.onComplete();
} else {
p = easing(p);
nodes.forEach(function(node) {
for (var k in animate)
if (k in animate) {
if (k.match(/color$/))
node[k] = interpolateColors(
startPositions[node.id][k],
node[animate[k]],
p
);
else
node[k] =
node[animate[k]] * p +
startPositions[node.id][k] * (1 - p);
}
});
s.refresh();
s.animations[id] = requestAnimationFrame(step);
}
}
step();
};
sigma.plugins.kill = function(s) {
for (var k in (s.animations || {}))
cancelAnimationFrame(s.animations[k]);
// Allow to refresh edgequadtree:
var k,
c;
for (k in s.cameras) {
c = s.cameras[k];
c.edgequadtree._enabled = true;
}
};
}).call(window);

View File

@ -41,6 +41,7 @@
"node/:name": "node",
"logs": "logs",
"helpus": "helpUs",
"graph2/:name": "graph2",
"support": "support"
},
@ -683,6 +684,20 @@
}
this.queryView.render();
},
graph2: function (name, initialized) {
this.checkUser();
if (!initialized) {
this.waitForInit(this.graph2.bind(this), name);
return;
}
if (!this.graphViewer2) {
this.graphViewer2 = new window.GraphViewer2({
name: name
});
}
this.graphViewer2.render();
},
helpUs: function (initialized) {
this.checkUser();

View File

@ -0,0 +1,3 @@
<script id="graphViewer2.ejs" type="text/template">
<div id="graph-container"></div>
</script>

View File

@ -0,0 +1,80 @@
/*jshint browser: true */
/*jshint unused: false */
/*global arangoHelper, sigma, Backbone, templateEngine, $, window*/
(function () {
"use strict";
window.GraphViewer2 = Backbone.View.extend({
el: "#content",
template: templateEngine.createTemplate("graphViewer2.ejs"),
initialize: function(options) {
this.name = options.name;
},
render: function () {
this.$el.html(this.template.render({}));
//adjust container widht + height
$('#graph-container').width($('.centralContent').width());
$('#graph-container').height($('.centralRow').height() - 150);
this.fetchGraph();
},
fetchGraph: function() {
var self = this;
$.ajax({
type: "GET",
url: arangoHelper.databaseUrl("/_admin/aardvark/graph/" + encodeURIComponent(this.name)),
contentType: "application/json",
success: function(data) {
self.renderGraph(data);
}
});
},
renderGraph: function(graph) {
var s;
if (graph.edges.left === 0) {
return;
}
console.log(graph);
s = new sigma({
graph: graph,
container: 'graph-container'
});
// Configure the noverlap layout:
var noverlapListener = s.configNoverlap({
nodeMargin: 0.1,
scaleNodes: 1.05,
gridSize: 75,
easing: 'quadraticInOut', // animation transition function
duration: 10000 // animation duration. Long here for the purposes of this example only
});
// Bind the events:
noverlapListener.bind('start stop interpolate', function(e) {
console.log(e.type);
if(e.type === 'start') {
console.time('noverlap');
}
if(e.type === 'interpolate') {
console.timeEnd('noverlap');
}
});
// Start the layout:
s.startNoverlap();
}
});
}());

View File

@ -0,0 +1,3 @@
#graph-container {
background-color: $c-white;
}

View File

@ -55,6 +55,8 @@
@import 'logs';
// Graph Viewer
@import 'graphViewer';
// Graph Viewer 2
@import 'graphViewer2';
// General Dialogs
@import 'dialogs';
// Collections