diff --git a/js/apps/system/_admin/aardvark/APP/GruntFile.js b/js/apps/system/_admin/aardvark/APP/GruntFile.js index 72e23b1109..bddd6d6638 100644 --- a/js/apps/system/_admin/aardvark/APP/GruntFile.js +++ b/js/apps/system/_admin/aardvark/APP/GruntFile.js @@ -65,6 +65,9 @@ "frontend/js/lib/sigma.plugins.fullScreen.js", "frontend/js/lib/sigma.layout.fruchtermanReingold.js", "frontend/js/lib/sigma.exporters.svg.js", + "frontend/js/lib/sigma.canvas.edges.labels.curve.js", + "frontend/js/lib/sigma.canvas.edges.labels.curvedArrow.js", + "frontend/js/lib/sigma.canvas.edges.labels.def.js", "frontend/js/lib/worker.js", "frontend/js/lib/supervisor.js", // END SIGMA LIBRARIES diff --git a/js/apps/system/_admin/aardvark/APP/aardvark.js b/js/apps/system/_admin/aardvark/APP/aardvark.js index a0b73bb46a..ae5da384d0 100644 --- a/js/apps/system/_admin/aardvark/APP/aardvark.js +++ b/js/apps/system/_admin/aardvark/APP/aardvark.js @@ -292,10 +292,33 @@ authRouter.get('/graph/:name', function (req, res) { var graph = gm._graph(name); var vertexName = graph._vertexCollections()[0].name(); - var startVertex = db[vertexName].any(); + + var startVertex; + var config; + + try { + config = req.queryParams; + } catch (e) { + res.throw('bad request', e.message, {cause: e}); + } + + if (config.nodeStart) { + try { + startVertex = db._document(config.nodeStart); + } catch (e) { + res.throw('bad request', e.message, {cause: e}); + } + + console.log(startVertex); + if (!startVertex) { + startVertex = db[vertexName].any(); + } + } else { + startVertex = db[vertexName].any(); + } var aqlQuery = - 'FOR v, e, p IN 1..3 ANY "' + startVertex._id + '" GRAPH "' + name + '"' + + 'FOR v, e, p IN 1..' + (config.depth || '2') + ' ANY "' + startVertex._id + '" GRAPH "' + name + '"' + 'RETURN p' ; @@ -307,29 +330,53 @@ authRouter.get('/graph/:name', function (req, res) { var edgesArr = []; _.each(cursor.json, function (obj) { + var edgeLabel; + _.each(obj.edges, function (edge) { if (edge._to && edge._from) { + if (config.edgeLabel) { + // configure edge labels + edgeLabel = edge[config.edgeLabel]; + + if (edgeLabel) { + edgeLabel = edgeLabel.toString(); + } + + if (!edgeLabel) { + edgeLabel = 'attribute ' + config.edgeLabel + ' not found'; + } + } + edgesObj[edge._from + edge._to] = { id: edge._id, source: edge._from, - color: '#cccccc', + label: edgeLabel, + color: config.edgeColor || '#cccccc', target: edge._to }; } }); - var label; + + var nodeLabel; + var nodeSize; + _.each(obj.vertices, function (node) { - if (node.label) { - label = node.label; - } else { - label = node._id; + if (config.nodeLabel) { + nodeLabel = node[config.nodeLabel]; + } + if (!nodeLabel) { + nodeLabel = node._id; + } + + if (config.nodeSize) { + nodeSize = node[config.nodeSize]; } nodesObj[node._id] = { id: node._id, - label: label, - size: Math.random(), - color: '#2ecc71', + label: nodeLabel, + size: nodeSize || Math.random(), + color: config.nodeColor || '#2ecc71', x: Math.random(), y: Math.random() }; diff --git a/js/apps/system/_admin/aardvark/APP/frontend/js/lib/sigma.canvas.edges.labels.curve.js b/js/apps/system/_admin/aardvark/APP/frontend/js/lib/sigma.canvas.edges.labels.curve.js new file mode 100644 index 0000000000..69f906ffd2 --- /dev/null +++ b/js/apps/system/_admin/aardvark/APP/frontend/js/lib/sigma.canvas.edges.labels.curve.js @@ -0,0 +1,112 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize packages: + sigma.utils.pkg('sigma.canvas.edges.labels'); + + /** + * This label renderer will just display the label on the curve of the edge. + * The label is rendered at half distance of the edge extremities, and is + * always oriented from left to right on the top side of the curve. + * + * @param {object} edge The edge object. + * @param {object} source node The edge source node. + * @param {object} target node The edge target node. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.edges.labels.curve = + function(edge, source, target, context, settings) { + if (typeof edge.label !== 'string') + return; + + var prefix = settings('prefix') || '', + size = edge[prefix + 'size'] || 1; + + if (size < settings('edgeLabelThreshold')) + return; + + var fontSize, + sSize = source[prefix + 'size'], + sX = source[prefix + 'x'], + sY = source[prefix + 'y'], + tX = target[prefix + 'x'], + tY = target[prefix + 'y'], + dX = tX - sX, + dY = tY - sY, + sign = (sX < tX) ? 1 : -1, + cp = {}, + c, + angle, + t = 0.5; //length of the curve + + if (source.id === target.id) { + cp = sigma.utils.getSelfLoopControlPoints(sX, sY, sSize); + c = sigma.utils.getPointOnBezierCurve( + t, sX, sY, tX, tY, cp.x1, cp.y1, cp.x2, cp.y2 + ); + angle = Math.atan2(1, 1); // 45° + } else { + cp = sigma.utils.getQuadraticControlPoint(sX, sY, tX, tY); + c = sigma.utils.getPointOnQuadraticCurve(t, sX, sY, tX, tY, cp.x, cp.y); + angle = Math.atan2(dY * sign, dX * sign); + } + + // The font size is sublineraly proportional to the edge size, in order to + // avoid very large labels on screen. + // This is achieved by f(x) = x * x^(-1/ a), where 'x' is the size and 'a' + // is the edgeLabelSizePowRatio. Notice that f(1) = 1. + // The final form is: + // f'(x) = b * x * x^(-1 / a), thus f'(1) = b. Application: + // fontSize = defaultEdgeLabelSize if edgeLabelSizePowRatio = 1 + fontSize = (settings('edgeLabelSize') === 'fixed') ? + settings('defaultEdgeLabelSize') : + settings('defaultEdgeLabelSize') * + size * + Math.pow(size, -1 / settings('edgeLabelSizePowRatio')); + + context.save(); + + + if (edge.active) { + context.font = [ + settings('activeFontStyle'), + fontSize + 'px', + settings('activeFont') || settings('font') + ].join(' '); + + context.fillStyle = + settings('edgeActiveColor') === 'edge' ? + (edge.active_color || settings('defaultEdgeActiveColor')) : + settings('defaultEdgeLabelActiveColor'); + } + else { + context.font = [ + settings('fontStyle'), + fontSize + 'px', + settings('font') + ].join(' '); + + context.fillStyle = + (settings('edgeLabelColor') === 'edge') ? + (edge.color || settings('defaultEdgeColor')) : + settings('defaultEdgeLabelColor'); + } + + context.textAlign = 'center'; + context.textBaseline = 'alphabetic'; + + context.translate(c.x, c.y); + context.rotate(angle); + context.fillText( + edge.label, + 0, + (-size / 2) - 3 + ); + + context.restore(); + }; +}).call(this); diff --git a/js/apps/system/_admin/aardvark/APP/frontend/js/lib/sigma.canvas.edges.labels.curvedArrow.js b/js/apps/system/_admin/aardvark/APP/frontend/js/lib/sigma.canvas.edges.labels.curvedArrow.js new file mode 100644 index 0000000000..e32012c9fa --- /dev/null +++ b/js/apps/system/_admin/aardvark/APP/frontend/js/lib/sigma.canvas.edges.labels.curvedArrow.js @@ -0,0 +1,25 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize packages: + sigma.utils.pkg('sigma.canvas.edges.labels'); + + /** + * This label renderer will just display the label on the curve of the edge. + * The label is rendered at half distance of the edge extremities, and is + * always oriented from left to right on the top side of the curve. + * + * @param {object} edge The edge object. + * @param {object} source node The edge source node. + * @param {object} target node The edge target node. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.edges.labels.curvedArrow = + function(edge, source, target, context, settings) { + sigma.canvas.edges.labels.curve(edge, source, target, context, settings); + }; +}).call(this); diff --git a/js/apps/system/_admin/aardvark/APP/frontend/js/lib/sigma.canvas.edges.labels.def.js b/js/apps/system/_admin/aardvark/APP/frontend/js/lib/sigma.canvas.edges.labels.def.js new file mode 100644 index 0000000000..51f4bf96c1 --- /dev/null +++ b/js/apps/system/_admin/aardvark/APP/frontend/js/lib/sigma.canvas.edges.labels.def.js @@ -0,0 +1,96 @@ +;(function(undefined) { + 'use strict'; + + if (typeof sigma === 'undefined') + throw 'sigma is not declared'; + + // Initialize packages: + sigma.utils.pkg('sigma.canvas.edges.labels'); + + /** + * This label renderer will just display the label on the line of the edge. + * The label is rendered at half distance of the edge extremities, and is + * always oriented from left to right on the top side of the line. + * + * @param {object} edge The edge object. + * @param {object} source node The edge source node. + * @param {object} target node The edge target node. + * @param {CanvasRenderingContext2D} context The canvas context. + * @param {configurable} settings The settings function. + */ + sigma.canvas.edges.labels.def = + function(edge, source, target, context, settings) { + if (typeof edge.label !== 'string' || source == target) + return; + + var prefix = settings('prefix') || '', + size = edge[prefix + 'size'] || 1; + + if (size < settings('edgeLabelThreshold')) + return; + + if (0 === settings('edgeLabelSizePowRatio')) + throw '"edgeLabelSizePowRatio" must not be 0.'; + + var fontSize, + x = (source[prefix + 'x'] + target[prefix + 'x']) / 2, + y = (source[prefix + 'y'] + target[prefix + 'y']) / 2, + dX = target[prefix + 'x'] - source[prefix + 'x'], + dY = target[prefix + 'y'] - source[prefix + 'y'], + sign = (source[prefix + 'x'] < target[prefix + 'x']) ? 1 : -1, + angle = Math.atan2(dY * sign, dX * sign); + + // The font size is sublineraly proportional to the edge size, in order to + // avoid very large labels on screen. + // This is achieved by f(x) = x * x^(-1/ a), where 'x' is the size and 'a' + // is the edgeLabelSizePowRatio. Notice that f(1) = 1. + // The final form is: + // f'(x) = b * x * x^(-1 / a), thus f'(1) = b. Application: + // fontSize = defaultEdgeLabelSize if edgeLabelSizePowRatio = 1 + fontSize = (settings('edgeLabelSize') === 'fixed') ? + settings('defaultEdgeLabelSize') : + settings('defaultEdgeLabelSize') * + size * + Math.pow(size, -1 / settings('edgeLabelSizePowRatio')); + + context.save(); + + if (edge.active) { + context.font = [ + settings('activeFontStyle'), + fontSize + 'px', + settings('activeFont') || settings('font') + ].join(' '); + + context.fillStyle = + settings('edgeActiveColor') === 'edge' ? + (edge.active_color || settings('defaultEdgeActiveColor')) : + settings('defaultEdgeLabelActiveColor'); + } + else { + context.font = [ + settings('fontStyle'), + fontSize + 'px', + settings('font') + ].join(' '); + + context.fillStyle = + (settings('edgeLabelColor') === 'edge') ? + (edge.color || settings('defaultEdgeColor')) : + settings('defaultEdgeLabelColor'); + } + + context.textAlign = 'center'; + context.textBaseline = 'alphabetic'; + + context.translate(x, y); + context.rotate(angle); + context.fillText( + edge.label, + 0, + (-size / 2) - 3 + ); + + context.restore(); + }; +}).call(this); diff --git a/js/apps/system/_admin/aardvark/APP/frontend/js/models/userConfig.js b/js/apps/system/_admin/aardvark/APP/frontend/js/models/userConfig.js index f69bebba45..eba3139f69 100644 --- a/js/apps/system/_admin/aardvark/APP/frontend/js/models/userConfig.js +++ b/js/apps/system/_admin/aardvark/APP/frontend/js/models/userConfig.js @@ -42,7 +42,7 @@ window.UserConfig = Backbone.Model.extend({ } }, error: function () { - arangoHelper.arangoNotification('User configuration', 'Could not update user configuration for key: ' + keyName); + arangoHelper.arangoError('User configuration', 'Could not update user configuration for key: ' + keyName); } }); }, @@ -61,7 +61,7 @@ window.UserConfig = Backbone.Model.extend({ callback(keyValue); }, error: function () { - arangoHelper.arangoNotification('User configuration', 'Could not fetch user configuration for key: ' + keyName); + arangoHelper.arangoError('User configuration', 'Could not fetch user configuration for key: ' + keyName); } }); } diff --git a/js/apps/system/_admin/aardvark/APP/frontend/js/routers/router.js b/js/apps/system/_admin/aardvark/APP/frontend/js/routers/router.js index fb00c199e7..76d20359c3 100644 --- a/js/apps/system/_admin/aardvark/APP/frontend/js/routers/router.js +++ b/js/apps/system/_admin/aardvark/APP/frontend/js/routers/router.js @@ -671,6 +671,9 @@ this.waitForInit(this.graph2.bind(this), name); return; } + if (this.graphViewer2) { + this.graphViewer2.remove(); + } this.graphViewer2 = new window.GraphViewer2({ name: name, userConfig: this.userConfig @@ -684,6 +687,9 @@ this.waitForInit(this.graph2settings.bind(this), name); return; } + if (this.graphSettingsView) { + this.graphSettingsView.remove(); + } this.graphSettingsView = new window.GraphSettingsView({ name: name, userConfig: this.userConfig diff --git a/js/apps/system/_admin/aardvark/APP/frontend/js/templates/graphSettingsView.ejs b/js/apps/system/_admin/aardvark/APP/frontend/js/templates/graphSettingsView.ejs index 6b45d1a181..e0a124629d 100644 --- a/js/apps/system/_admin/aardvark/APP/frontend/js/templates/graphSettingsView.ejs +++ b/js/apps/system/_admin/aardvark/APP/frontend/js/templates/graphSettingsView.ejs @@ -7,7 +7,7 @@ <% return name.charAt(0).toUpperCase() + string.slice(1);%> <% }; %> -
+
@@ -21,47 +21,51 @@
-
-
-
Name
-
Property
-
-
-
<% _.each(specific, function(val, key) { %> -
<%=val.name%>
-
+ + <% if (val.type === 'divider') { %> +
<%=val.name%>
+
+ <% } else { %> + +
<%=val.name%>
+
- <% var VALUE; %> - <% if (val.value) { %> - <% VALUE = val.value %> - <% } else { %> - <% VALUE = val.default %> - <% } %> + <% var VALUE; %> + <% if (val.value) { %> + <% VALUE = val.value %> + <% } else { %> + <% VALUE = val.default %> + <% } %> - <% if (val.type === 'string') { %> - - <% } %> + <% if (val.type === 'string') { %> + + <% } %> - <% if (val.type === 'color') { %> - - <% } %> + <% if (val.type === 'number') { %> + + <% } %> - <% if (val.type === 'select') { %> -
- -
- <% } %> + <% if (val.type === 'color') { %> + + <% } %> -
+ <% if (val.type === 'select') { %> +
+ +
+ <% } %> + +
+ <% } %> <% }); %>
@@ -77,16 +81,13 @@
-
-
-
Name
-
Property
-
-
-
<% _.each(general, function(val, key) { %> + <% if (val.type === 'divider') { %> +
<%=val.name%>
+
+ <% } else { %>
<%=val.name%>
<% if (val.type === 'select') { %> @@ -101,9 +102,14 @@
<% } %> - <% if (val.type === 'numeric') { %> + <% if (val.type === 'string') { %> + + <% } %> + + <% if (val.type === 'number') { %> <% } %> + <% } %> <% }); %> diff --git a/js/apps/system/_admin/aardvark/APP/frontend/js/views/graphSettingsView.js b/js/apps/system/_admin/aardvark/APP/frontend/js/views/graphSettingsView.js index 76b49cad23..2eb07212fd 100644 --- a/js/apps/system/_admin/aardvark/APP/frontend/js/views/graphSettingsView.js +++ b/js/apps/system/_admin/aardvark/APP/frontend/js/views/graphSettingsView.js @@ -7,7 +7,23 @@ window.GraphSettingsView = Backbone.View.extend({ el: '#content', + remove: function () { + this.$el.empty().off(); /* off to unbind the events */ + this.stopListening(); + return this; + }, + general: { + 'graph': { + type: 'divider', + name: 'Graph' + }, + 'nodeStart': { + type: 'string', + name: 'Starting node', + desc: 'A valid node id. If empty, a random node will be chosen.', + value: 2 + }, 'layout': { type: 'select', name: 'Layout algorithm', @@ -37,58 +53,73 @@ } }, 'depth': { - type: 'numeric', + type: 'number', name: 'Search depth', value: 2 } }, specific: { + 'nodes': { + type: 'divider', + name: 'Nodes' + }, 'nodeLabel': { type: 'string', - name: 'Node label', + name: 'Label', desc: 'Default node color. RGB or HEX value.', default: '_key' }, 'nodeColor': { type: 'color', - name: 'Node color', + name: 'Color', desc: 'Default node color. RGB or HEX value.', default: '#2ecc71' }, 'nodeSize': { type: 'string', - name: 'Node size', - desc: 'Default node size. Numeric value > 0.', - value: undefined + name: 'Sizing attribute', + desc: 'Default node size. Numeric value > 0.' + }, + 'edges': { + type: 'divider', + name: 'Edges' }, 'edgeLabel': { type: 'string', - name: 'Edge label', - desc: 'Default edge label.', - value: undefined + name: 'Label', + desc: 'Default edge label.' }, 'edgeColor': { type: 'color', - name: 'Edge color', + name: 'Color', desc: 'Default edge color. RGB or HEX value.', default: '#cccccc' }, 'edgeSize': { - type: 'string', - name: 'Edge thickness', - desc: 'Default edge thickness. Numeric value > 0.', - value: undefined + type: 'number', + name: 'Sizing', + desc: 'Default edge thickness. Numeric value > 0.' }, 'edgeType': { type: 'select', - name: 'Edge type', + name: 'Type', desc: 'The type of the edge', - canvas: { - name: 'Straight' + line: { + name: 'Line', + val: 'line' }, - webgl: { - name: 'Curved' + curve: { + name: 'Curve', + val: 'curve' + }, + arrow: { + name: 'Arrow', + val: 'arrow' + }, + curvedArrow: { + name: 'Curved Arrow', + val: 'curvedArrow' } } }, @@ -112,7 +143,6 @@ this.userConfig.fetch({ success: function (data) { self.graphConfig = data.toJSON().graphs[combinedName]; - if (render) { self.continueRender(); } @@ -127,7 +157,15 @@ config[combinedName] = { layout: $('#g_layout').val(), renderer: $('#g_renderer').val(), - depth: $('#g_depth').val() + depth: $('#g_depth').val(), + nodeColor: $('#g_nodeColor').val(), + edgeColor: $('#g_edgeColor').val(), + nodeLabel: $('#g_nodeLabel').val(), + edgeLabel: $('#g_edgeLabel').val(), + edgeType: $('#g_edgeType').val(), + nodeSize: $('#g_nodeSize').val(), + edgeSize: $('#g_edgeSize').val(), + nodeStart: $('#g_nodeStart').val() }; var callback = function () { diff --git a/js/apps/system/_admin/aardvark/APP/frontend/js/views/graphViewer2.js b/js/apps/system/_admin/aardvark/APP/frontend/js/views/graphViewer2.js index 1a2a64378b..6774f5f091 100644 --- a/js/apps/system/_admin/aardvark/APP/frontend/js/views/graphViewer2.js +++ b/js/apps/system/_admin/aardvark/APP/frontend/js/views/graphViewer2.js @@ -1,12 +1,18 @@ /* jshint browser: true */ /* jshint unused: false */ -/* global arangoHelper, slicePath, icon, wheelnav, document, sigma, Backbone, templateEngine, $, window*/ +/* global arangoHelper, _, slicePath, icon, wheelnav, document, sigma, Backbone, templateEngine, $, window*/ (function () { 'use strict'; window.GraphViewer2 = Backbone.View.extend({ el: '#content', + remove: function () { + this.$el.empty().off(); /* off to unbind the events */ + this.stopListening(); + return this; + }, + template: templateEngine.createTemplate('graphViewer2.ejs'), initialize: function (options) { @@ -64,30 +70,104 @@ $('#content').append( '
' + '' + - 'Calculating layout. Please wait ...
' + 'Fetching graph data. Please wait ... ' ); - var fetchGraph = function () { + var continueFetchGraph = function () { + var ajaxData = {}; + if (this.graphConfig) { + ajaxData = _.clone(this.graphConfig); + + // remove not needed params + delete ajaxData.layout; + delete ajaxData.edgeType; + delete ajaxData.renderer; + } + + this.setupSigma(); + $.ajax({ type: 'GET', url: arangoHelper.databaseUrl('/_admin/aardvark/graph/' + encodeURIComponent(this.name)), contentType: 'application/json', + data: ajaxData, success: function (data) { + $('#calcText').html('Calculating layout. Please wait ... '); arangoHelper.buildGraphSubNav(self.name, 'Content'); self.renderGraph(data); }, - error: function () { + error: function (e) { + console.log(e); + try { + arangoHelper.arangoError('Graph', e.responseJSON.exception); + } catch (ignore) {} + $('#calculatingGraph').html('Failed to fetch graph information.'); } }); }.bind(this); - // TODO LOAD GRAPH SETTINGS - this.getGraphSettings(fetchGraph); + // load graph configuration + this.getGraphSettings(continueFetchGraph); }, - clearOldContextMenu: function () { + setupSigma: function () { + if (this.graphConfig) { + if (this.graphConfig.edgeLabel) { + // Initialize package: + sigma.utils.pkg('sigma.settings'); + + var settings = { + defaultEdgeLabelColor: '#000', + defaultEdgeLabelActiveColor: '#000', + defaultEdgeLabelSize: 10, + edgeLabelSize: 'fixed', + edgeLabelSizePowRatio: 1, + edgeLabelThreshold: 1 + }; + + // Export the previously designed settings: + sigma.settings = sigma.utils.extend(sigma.settings || {}, settings); + + // Override default settings: + sigma.settings.drawEdgeLabels = true; + } + } + }, + + contextState: { + createEdge: false, + _from: false, + _to: false, + fromX: false, + fromY: false + }, + + clearOldContextMenu: function (states) { + var self = this; + + // clear dom $('#nodeContextMenu').remove(); + var string = '
'; + $('#graph-container').append(string); + + // clear state + if (states) { + _.each(this.contextState, function (val, key) { + self.contextState[key] = false; + }); + } + + // clear info div + }, + + createContextMenu: function (e) { + var x = e.data.captor.clientX; + var y = e.data.captor.clientX; + console.log('Context menu'); + console.log(x); + console.log(y); + this.clearOldContextMenu(); }, createNodeContextMenu: function (nodeId, e) { @@ -97,8 +177,6 @@ var y = e.data.node['renderer1:y']; this.clearOldContextMenu(); - var string = '
'; - $('#graph-container').append(string); var generateMenu = function (e, nodeId) { var hotaru = ['#364C4A', '#497C7F', '#92C5C0', '#858168', '#CCBCA5']; @@ -110,35 +188,49 @@ wheel.wheelRadius = 50; wheel.clockwise = false; wheel.colors = hotaru; + wheel.multiSelect = true; wheel.clickModeRotate = false; wheel.slicePathFunction = slicePath().DonutSlice; - wheel.createWheel([icon.edit, icon.trash, icon.smallgear, icon.smallgear]); + wheel.createWheel([icon.edit, icon.trash, icon.arrowleft2, icon.connect]); + wheel.navItems[0].selected = false; + wheel.navItems[0].hovered = false; // add menu events // function 0: edit - wheel.navItems[0].navigateFunction = function () { + wheel.navItems[0].navigateFunction = function (e) { self.clearOldContextMenu(); self.editNode(nodeId); }; - // function 1: - wheel.navItems[1].navigateFunction = function () { + // function 1: delete + wheel.navItems[1].navigateFunction = function (e) { self.clearOldContextMenu(); - self.editNode(nodeId); + self.deleteNode(nodeId); }; - // function 2: - wheel.navItems[2].navigateFunction = function () { + // function 2: mark as start node + wheel.navItems[2].navigateFunction = function (e) { self.clearOldContextMenu(); - self.editNode(nodeId); + self.setStartNode(nodeId); }; - // function 3: delete - wheel.navItems[3].navigateFunction = function () { + // function 3: create edge + wheel.navItems[3].navigateFunction = function (e) { + self.contextState.createEdge = true; + self.contextState._from = nodeId; + self.contextState.fromX = x; + self.contextState.fromY = y; + + var c = document.getElementsByClassName('sigma-mouse')[0]; + c.addEventListener('mousemove', self.drawLine.bind(this), false); + self.clearOldContextMenu(); - self.editNode(nodeId); }; + + // deselect active default entry + wheel.navItems[0].selected = false; + wheel.navItems[0].hovered = false; }; $('#nodeContextMenu').css('left', x + 115); @@ -149,6 +241,31 @@ generateMenu(e, nodeId); }, + clearMouseCanvas: function () { + var c = document.getElementsByClassName('sigma-mouse')[0]; + var ctx = c.getContext('2d'); + ctx.clearRect(0, 0, $(c).width(), $(c).height()); + }, + + drawLine: function (e) { + var context = window.App.graphViewer2.contextState; + + if (context.createEdge) { + var fromX = context.fromX; + var fromY = context.fromY; + var toX = e.offsetX; + var toY = e.offsetY; + + var c = document.getElementsByClassName('sigma-mouse')[0]; + var ctx = c.getContext('2d'); + ctx.clearRect(0, 0, $(c).width(), $(c).height()); + ctx.beginPath(); + ctx.moveTo(fromX, fromY); + ctx.lineTo(toX, toY); + ctx.stroke(); + } + }, + getGraphSettings: function (callback) { var self = this; var combinedName = window.App.currentDB.toJSON().name + '_' + this.name; @@ -158,7 +275,7 @@ self.graphConfig = data.toJSON().graphs[combinedName]; if (callback) { - callback(); + callback(self.graphConfig); } } }); @@ -184,8 +301,6 @@ var renderer = 'canvas'; if (this.graphConfig) { - console.log(this.graphConfig); - if (this.graphConfig.layout) { algorithm = this.graphConfig.layout; } @@ -201,20 +316,28 @@ var settings = { doubleClickEnabled: false, - minEdgeSize: 0.5, + minNodeSize: 3.5, + minEdgeSize: 1, maxEdgeSize: 4, enableEdgeHovering: true, - // edgeHoverColor: 'edge', - // defaultEdgeHoverColor: '#000', - // defaultEdgeType: 'curve', - edgeHoverSizeRatio: 1, + edgeHoverColor: '#000', + defaultEdgeHoverColor: '#000', + defaultEdgeType: 'line', + edgeHoverSizeRatio: 2, edgeHoverExtremities: true }; + 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 20 - settings.labelThreshold = 20; + // show node label if size is 15 + settings.labelThreshold = 15; + settings.hideEdgesOnMove = true; } // adjust display settings for webgl renderer @@ -271,7 +394,33 @@ e.originalColor = e.color; }); - if (renderer !== 'webgl') { + // for canvas renderer allow graph editing + if (renderer === 'canvas') { + s.bind('rightClickStage', function (e) { + self.createContextMenu(e); + self.clearMouseCanvas(); + }); + + s.bind('clickNode', function (e) { + if (self.contextState.createEdge === true) { + // create the edge + self.contextState._to = e.data.node.id; + + self.currentGraph.graph.addEdge({ + source: self.contextState._from, + target: self.contextState._to, + id: Math.random(), + color: self.graphConfig.edgeColor + }); + + // rerender graph + self.currentGraph.refresh(); + + // then clear states + self.clearOldContextMenu(true); + } + }); + s.bind('rightClickNode', function (e) { var nodeId = e.data.node.id; self.createNodeContextMenu(nodeId, e); @@ -314,7 +463,8 @@ }); s.bind('clickStage', function () { - self.clearOldContextMenu(); + self.clearOldContextMenu(true); + self.clearMouseCanvas(); }); } @@ -327,11 +477,18 @@ } else if (algorithm === 'force') { s.startForceAtlas2({worker: true, barnesHutOptimize: false}); + var duration = 3000; + + if (graph.nodes.length > 2500) { + duration = 5000; + } else if (graph.nodes.length < 50) { + duration = 500; + } + window.setTimeout(function () { s.stopForceAtlas2(); dragListener = sigma.plugins.dragNodes(s, s.renderers[0]); - console.log('stopped force'); - }, 3000); + }, duration); } else if (algorithm === 'fruchtermann') { // Start the Fruchterman-Reingold algorithm: sigma.layouts.fruchtermanReingold.start(s); diff --git a/js/apps/system/_admin/aardvark/APP/frontend/scss/_graphViewer2.scss b/js/apps/system/_admin/aardvark/APP/frontend/scss/_graphViewer2.scss index c4a7760ff1..2ad6ad4920 100644 --- a/js/apps/system/_admin/aardvark/APP/frontend/scss/_graphViewer2.scss +++ b/js/apps/system/_admin/aardvark/APP/frontend/scss/_graphViewer2.scss @@ -45,8 +45,19 @@ position: fixed; svg { - path { - font-size: 2pt !important; + #wheelnav-nodeContextMenu-title-0 { + transform: translate(24px, 14px) scale(.7) !important; + } + + #wheelnav-nodeContextMenu-title-0, + #wheelnav-nodeContextMenu-title-1, + #wheelnav-nodeContextMenu-title-2, + #wheelnav-nodeContextMenu-title-3 { + fill: $c-white; + + &:hover { + fill: $c-positive; + } } } } diff --git a/js/apps/system/_admin/aardvark/APP/frontend/scss/_pure.scss b/js/apps/system/_admin/aardvark/APP/frontend/scss/_pure.scss index 45493dd71f..6796dcc0e7 100644 --- a/js/apps/system/_admin/aardvark/APP/frontend/scss/_pure.scss +++ b/js/apps/system/_admin/aardvark/APP/frontend/scss/_pure.scss @@ -9,6 +9,13 @@ .dataTables_empty { padding-left: 10px; } + + .heading { + font-weight: 600; + height: 40px; + padding-bottom: 10px; + padding-top: 10px; + } } .pure-table {